LED Simple — мигающий светодиод
Самый простой компонент репозитория: один светодиод, мигающий с фиксированным интервалом. Точка входа для тех, кто впервые видит проект — до датчиков, шин I2C/UART и классов.
💡 LED Simple — мигающий светодиод на ESP32
Самый простой компонент репозитория: один светодиод, который мигает с фиксированным интервалом. Точка входа для тех, кто впервые видит проект — до датчиков, шин I2C/UART и классов.
📋 Документация проекта
| Файл | Что внутри | Кому |
|---|---|---|
| GUIDE.md | Учебное пособие: модульность (.h/.cpp), const vs constexpr, разбор кода построчно, самопроверка | Изучаешь проект впервые |
| WIRING.md | Схема подключения светодиода и резистора | Собираешь железо |
Описание
Программа настраивает GPIO2 на выход и переключает его с интервалом 500 мс, зажигая
и гася подключённый через резистор светодиод.
Программная архитектура
В отличие от остальных компонентов репозитория (см. COMPONENT_STANDARD.md),
этот модуль намеренно не использует класс-драйвер: это осознанное отступление от
стандарта, а не недосмотр. LED_simple — вводный, “до-ООП” шаг учебной программы,
чья цель — показать модульность через обычные .h/.cpp-файлы со свободными функциями,
прежде чем в Components/2. LED_middle появится
класс-драйвер (LedController), неблокирующая логика на millis() и вотчдог isAlive().
| Файлы | Что внутри |
|---|---|
include/led.h, src/led.cpp | Свободные функции setupLED()/toggleLED(), константа LED_PIN. Блокирующее мигание через delay() — сознательно простой вариант, разбор компромисса в GUIDE.md, раздел 4. |
src/main.cpp | Точка входа: setup() инициализирует пин, loop() вызывает мигание. |
Работа через CLI
cd "Components/3. LED_simple"
pio run -t upload && pio device monitorОкружение
esp32c6в текущей версии ядра Arduino не собирается — ошибка компиляции внутри самого фреймворка (конфликт макросаSerial/USBSerialвHardwareSerial.h), не связана с кодом этого компонента (см.COMPONENT_STANDARD.md, раздел “Известные репозиторий-специфичные нюансы”). Используйesp32devилиesp32s3.
Связанные материалы
- Резистор — почему светодиоду нужен резистор, как читать номинал по полоскам и проверить мощность.
- LED_middle — следующий шаг: неблокирующее
мигание на
millis()и первый класс-драйвер.
WIRING.md — схема подключения
Пин
Светодиод подключён к GPIO2 — см. LED_PIN в include/led.h.
Резистор
Светодиод подключается через токоограничительный резистор 220 Ом, иначе через
него потечёт слишком большой ток и он перегорит. Сам расчёт номинала (закон Ома,
R = (Vcc − Vled) / I) не повторяем здесь — он один и тот же для всех LED-компонентов
репозитория и подробно разобран в
Components/2. LED_middle/THEORY.md, раздел 1.
Принципиальная схема
220 Ом
GPIO2 ───────/\/\/\──────┬──────► Анод светодиода
│
▼
Катод
│
GNDАнод (длинная ножка светодиода) — через резистор к GPIO2. Катод (короткая ножка,
обычно со скошенным краем на корпусе) — к GND.
Почему именно GPIO2, а не любой другой пин
На классическом ESP32 (esp32dev) GPIO2 — так называемый strapping-пин: его
уровень при включении платы влияет на режим загрузки. На практике внешний светодиод
на нём почти никогда не мешает старту (он не тянет пин в неверное состояние достаточно
сильно), поэтому именно этот пин исторически используется на большинстве плат ESP32
DevKit как пин встроенного светодиода, и выбран для всех вводных LED-примеров этого
репозитория (см. таблицу strapping/USB-пинов по всем трём платам в
Components/2. LED_middle/WIRING.md).
Если на реальном занятии сборка на esp32dev не стартует при подключённом на GPIO2
светодиоде — переключите LED_PIN в include/led.h на любой пин
из строки esp32dev в этой таблице, кроме перечисленных как избегаемые.
Плата
esp32c6в текущей версии ядра Arduino не собирается вообще (баг фреймворка, не связан со схемой) — см. предупреждение в README.md.
📘 Учебное пособие: мигающий светодиод на ESP32
Это самый простой компонент репозитория — стартовая точка перед тем, как переходить
к датчикам и классам. Пособие проведёт тебя от “зачем вообще разбивать код на файлы”
до “могу объяснить каждую строчку led.cpp”.
Чему ты научишься
- Зачем код делят на заголовочный файл (
.h) и файл реализации (.cpp). - Разницу между
constиconstexprи почему для номера пина нужен именноconstexpr. - Как одна и та же переменная (
LED_PIN) используется в трёх разных файлах проекта. - Что такое блокирующий код и почему
delay()— это упрощение, у которого есть цена.
1. Зачем делить код на файлы
Всю программу можно было бы написать в одном файле main.cpp. Для мигания одним
светодиодом это работает. Но в реальных прошивках счёт строк идёт на тысячи, и один
файл становится нечитаемым.
Поэтому код разбит на два файла с разными ролями:
include/led.h— что умеет делать модуль (объявления: имя пина, имена функций). Другие файлы подключают этот файл, чтобы узнать, что доступно.src/led.cpp— как это делается (реализация функций).src/main.cpp— точка входа: вызывает функции изled.h, не зная, как именно они устроены внутри.
Это даёт две практические выгоды: компилятор может собирать led.cpp отдельно от
main.cpp (что ускоряет пересборку больших проектов), а другой человек может
пользоваться setupLED()/toggleLED(), вообще не открывая led.cpp.
#ifndef LED_H
#define LED_H
...
#endif // LED_H
Строки 1, 2 и 27 в led.h — include guard, защита от
повторного включения. Если led.h случайно попадёт в один файл дважды (например,
через цепочку из других #include), компилятор не станет читать его содержимое
второй раз — это защищает от ошибки “переопределение”.
2. const против constexpr
Номер пина задан так:
constexpr uint8_t LED_PIN = 2;Можно было бы написать const uint8_t LED_PIN = 2; — код скомпилируется и будет
работать точно так же. Разница — в том, что происходит при компиляции:
constв C++ гарантирует только “нельзя изменить после инициализации”. Компилятор вправе (а для глобальныхconstв разных единицах трансляции — почти всегда обязан) зарезервировать под неё реальную ячейку памяти.constexpr— более сильное требование: “значение известно уже на этапе компиляции”. Компилятор не резервирует память под переменную, а подставляет число2напрямую в машинный код везде, где встречаетсяLED_PIN. Для микроконтроллера с килобайтами RAM это не абстрактная экономия — лишняя переменная на каждую константу в проекте из тысяч строк складывается в заметный расход памяти.
Практическое правило: если значение константы известно заранее и не меняется —
используй constexpr, а не const.
3. Как устроено мигание
3.1 Инициализация — setupLED()
void setupLED() {
pinMode(LED_PIN, OUTPUT);
}По умолчанию все пины ESP32 находятся в режиме входа (ожидают сигнал извне).
pinMode(LED_PIN, OUTPUT) переключает GPIO2 в режим выхода — теперь пин может сам
подавать напряжение. Эта функция вызывается один раз из setup() в
src/main.cpp:5 — настройка не должна повторяться на каждой
итерации loop().
3.2 Мигание — toggleLED()
void toggleLED(uint32_t delayMs) {
digitalWrite(LED_PIN, HIGH); // Подаём напряжение — светодиод загорается
delay(delayMs);
digitalWrite(LED_PIN, LOW); // Снимаем напряжение — светодиод гаснет
delay(delayMs);
}digitalWrite(LED_PIN, HIGH) подаёт на пин напряжение (3.3 В) — через резистор и
светодиод начинает течь ток, диод загорается (схема — WIRING.md).
delay(delayMs) останавливает выполнение на delayMs миллисекунд. LOW снимает
напряжение — диод гаснет. Один вызов toggleLED(500) — это полный цикл “моргнуть
один раз” длиной в секунду (500 мс горит + 500 мс не горит).
Параметр delayMs — uint32_t (беззнаковое 32-битное число), а не int, потому что
время в миллисекундах не бывает отрицательным, а int (обычно 16 или 32 бита в
зависимости от платформы) может не хватить для очень больших пауз.
3.3 Главный цикл — main.cpp
void setup() {
setupLED();
}
void loop() {
toggleLED(500);
}setup() вызывается один раз при включении платы. loop() — это тело бесконечного
цикла, который фреймворк Arduino запускает после setup(); каждый вызов toggleLED(500)
здесь и есть одно “моргание”.
4. Блокирующий код — и его цена
delay() не просто “ждёт” — на время ожидания весь процессор простаивает и не
может делать ничего другого: ни читать датчики, ни отвечать по Wi-Fi/Bluetooth, ни
проверять кнопку. Пока в проекте один светодиод и больше ничего не происходит, это
не проблема. Но как только нужно, например, одновременно мигать светодиодом и следить
за показаниями датчика — блокирующий delay() начинает мешать: пока он “спит”,
датчик может пропустить событие.
Решение — неблокирующая логика на счётчике millis() вместо delay(): вместо “спать
500 мс” код каждую итерацию loop() спрашивает “прошло ли уже 500 мс с последнего
переключения” и, если да, переключает пин, не останавливая исполнение. Этот паттерн,
класс-драйвер для группы светодиодов и эффект “бегущего огня” разобраны в следующем
компоненте — Components/2. LED_middle.
5. Проверь себя
Постарайся ответить, не подглядывая в код, потом сверься.
Что изменится в скомпилированной прошивке, если заменить
constexpr uint8_t LED_PINнаconst uint8_t LED_PIN?Ответ
Код продолжит работать так же, но дляLED_PINможет быть зарезервирована реальная ячейка памяти вместо подстановки числа2прямо в машинный код — то есть чуть больше расхода RAM без какой-либо пользы, так как значение и так известно заранее.Если вызвать
toggleLED(0), светодиод погаснет или будет мигать бесконечно быстро?Ответ
delay(0)не блокирует выполнение —digitalWriteв HIGH и LOW отработают практически мгновенно один за другим. Диод физически не успеет заметно моргнуть глазом и будет казаться постоянно тускло горящим (либо погашенным) — это не деление на ноль и не отказ, просто вырожденный, но валидный случай.Почему
setupLED()вызывается изsetup(), а не изloop()?Ответ
pinMode()— это одноразовая настройка режима пина. Если вызывать её изloop(), она будет повторяться на каждой итерации без всякой пользы и просто тратить время процессора.Почему в этом компоненте нет неблокирующего мигания на
millis(), как в2. LED_middle?Ответ
Это осознанное упрощение:3. LED_simple— вводный шаг, который показывает модульность (.h/.cpp) и базовый GPIO без класса и безmillis(). Неблокирующая логика на классе-драйвере — следующий шаг, разобранный в2. LED_middle.
Куда дальше
Разобрался с базовым миганием? Следующий шаг — неблокирующее мигание, группы
светодиодов и класс-драйвер: Components/2. LED_middle.
Методический план урока: первая программа — мигающий светодиод
Паспорт урока
| Параметр | Значение |
|---|---|
| Место в курсе | Components, урок 1 — точка входа, до датчиков и классов |
| Длительность | 45 мин |
| Возраст | 12–16, материал двухуровневый |
| Оборудование | ESP32 DevKit, светодиод, резистор 220 Ом, макетная плата, 2 провода, USB-кабель — на каждую пару |
| Предварительные знания | Не требуются |
| Предмет | Информатика (структура программы), физика (ток, напряжение) |
Образовательные цели
По итогам урока ученик умеет:
| № | Цель | Блум | Миллер | Чем доказывается |
|---|---|---|---|---|
| 1.1 | Собрать схему со светодиодом и резистором, соблюдая полярность | применять | делает | Светодиод горит на его плате |
| 1.2 | Объяснить, зачем нужен резистор | понимать | знает как | Устный ответ до сборки |
| 1.3 | Изменить поведение программы, поменяв значение в коде | применять | делает | Своя частота мигания |
| 1.4 | Объяснить, почему setup() выполняется один раз, а loop() — бесконечно | понимать | знает как | Ответ на контрольный вопрос |
| 1.5 | Найти в коде место, отвечающее за конкретное поведение | анализировать | показывает как | Находит 500 в main.cpp без подсказки |
Чего на этом уроке делать НЕ надо. Не объяснять
constexprпротивconst, не разбирать неблокирующий код иmillis(), не вводить классы. Всё это есть в GUIDE.md и в следующем компоненте — и всё это утопит первый урок. Задача урока одна: ребёнок написал код, и в реальном мире что-то произошло. Это переживание и есть содержание занятия.
Что должно быть готово заранее
- PlatformIO установлен и проверен на всех машинах: собрать проект до урока, чтобы первая загрузка библиотек не съела занятие
- Проект открывается и прошивается — проверить лично хотя бы на одной плате
- Светодиоды рассортированы: длинная ножка (анод) видна
- Резисторы 220 Ом — с запасом, они теряются
- Один собранный рабочий стенд как образец — для сверки, а не для копирования
- Запасной USB-кабель: часть кабелей только заряжают, данные не передают
Про кабели. Это самая частая причина «у меня не работает» на первом уроке. Если плата не определяется — сначала меняем кабель, потом ищем проблему в коде.
Сценарий занятия (45 минут)
| Время | Блок | Что делает педагог | Что делают ученики |
|---|---|---|---|
| 0–5 | Завязка | Показывает готовый мигающий стенд, задаёт вопрос про «кто им командует» | Строят догадки |
| 5–15 | Сборка схемы | Обходит, проверяет полярность | Собирают схему по WIRING.md |
| 15–22 | Первая прошивка | Показывает путь «код → плата» | Прошивают, получают мигание |
| 22–30 | Разбор кода | Ведёт разбор трёх файлов | Отвечают, находят строки |
| 30–40 | Практика | Раздаёт задания по уровням | Меняют код, наблюдают результат |
| 40–45 | Итог и ДЗ | Фиксирует вывод | Записывают в журнал |
Ключевой методический принцип — «сначала работает, потом понятно». Сборка и прошивка идут до объяснения кода. Ребёнок должен сначала увидеть результат своими руками и только потом узнать, почему он получился. Обратный порядок превращает урок в лекцию с иллюстрацией.
Блок 1. Завязка (0–5 мин)
Поставить на стол собранный стенд с мигающим светодиодом. Не объяснять ничего.
«Эта лампочка мигает ровно раз в секунду. Кто ей это приказал? И главное — где записано, что именно раз в секунду, а не два и не десять?»
Дать высказаться. Ответы будут разные: «внутри микросхемы», «так сделано на заводе», «там программа». Довести до мысли: где-то есть число, и его можно изменить. Это и будет целью урока — найти это число и стать тем, кто командует.
Что обычно происходит
| Реакция | Что за этим стоит | Что делать |
|---|---|---|
| «Это же просто лампочка» | Не видят разницы с фонариком | Выключить и включить питание: «а кто её зажёг снова?» |
| «Там внутри чип, он так умеет» | Модель «устройство = готовая функция» | «А если я захочу мигать в два раза быстрее — покупать новый чип?» |
| «Надо программу написать» | Уже есть опыт | «Верно. Сегодня напишешь. Какое слово в ней будет главным?» |
Блок 2. Сборка схемы (5–15 мин)
Работа в парах. Схема — в WIRING.md, она на экране или распечатана.
Порядок, который экономит время:
- Плата не подключена к USB — собираем обесточенными
- Резистор: от
GPIO2к свободной строке макетки - Светодиод: длинная ножка (анод) — в ту же строку, короткая — в строку к
GND - Провод от строки с короткой ножкой к
GNDна плате - Проверка соседней парой — только потом USB
Разговор про резистор (перед сборкой, 2 минуты)
Не давать формулу. Задать вопрос:
«Светодиод рассчитан примерно на 2 вольта. Плата даёт 3,3. Что произойдёт с лишним вольтом, если поставить диод напрямую?»
Довести до образа: резистор — как узкая труба, которая не даёт воде хлынуть всей разом. Без него светодиод вспыхнет один раз и погибнет.
Расчёт номинала на этом уроке не выводим. Закон Ома и формула
R = (Vcc − Vled) / Iразобраны в THEORY.md компонента LED_middle — это материал следующего занятия. Сегодня достаточно понимания «зачем», без «сколько».
Типичные ошибки сборки
| Симптом | Причина | Как ловить |
|---|---|---|
| Не горит совсем | Светодиод наоборот | Перевернуть — самая частая ошибка |
| Не горит совсем | Ножки в одной строке макетки | Показать, как строки соединены внутри |
| Горит тускло и постоянно | Провод не в GPIO2, а в 3V3 | Проверить, куда идёт резистор |
| Вспыхнул и погас | Забыли резистор | Заменить светодиод, разобрать причину вслух |
Блок 3. Первая прошивка (15–22 мин)
Показать на проекторе один раз, медленно: открыть проект → кнопка загрузки →
дождаться SUCCESS → светодиод мигает.
Дальше — сами. Педагог обходит и помогает, но не трогает чужую клавиатуру: подсказывает словами, нажимает ученик.
Момент, ради которого всё: у каждой пары мигает свой светодиод. Дать 30 секунд просто посмотреть — это и есть результат урока.
Если не прошивается
По порядку, не перескакивая:
- Кабель передаёт данные? (заменить на заведомо рабочий)
- Плата видна как порт? (в списке устройств)
- Правильная плата выбрана в
platformio.ini? - Держать
BOOTпри старте загрузки — на части плат обязательно
Плата
esp32c6в текущей версии ядра Arduino не собирается — это баг фреймворка, а не ошибка ученика (см. README.md). На уроке использоватьesp32dev.
Блок 4. Разбор кода (22–30 мин)
Три файла на экране. Разбор идёт вопросами, не рассказом.
4.1. Базовый уровень (12–14 лет)
src/main.cpp — что происходит и когда:
«Здесь два блока. Один называется
setup, другойloop. Как думаете, чем отличается “настройка” от “цикла”?»
Довести до правила и записать на доске:
setup()— один раз при включении. Как надеть кроссовки перед пробежкой.loop()— бесконечно, пока есть питание. Как сами шаги.
Где спрятано «раз в секунду»:
Попросить найти число 500 в main.cpp. Не показывать — пусть найдут. Затем:
«Почему 500, если мигает раз в секунду?»
Открыть src/led.cpp и прочитать вместе: включить → подождать →
выключить → подождать. Два ожидания по 500 = 1000 мс. Это первый разбор
исходного кода в курсе, и он окупается.
4.2. Углублённый уровень (14–16 лет)
Для тех, кто уже понял базовое — три дополнительных вопроса:
Зачем код разбит на три файла?
Показать: include/led.h — объявления («что умеет»),
src/led.cpp — реализация («как именно»),
src/main.cpp — использование. Аналогия: оглавление книги, сама
книга, читатель.
«Зачем так сложно ради двух функций?» — Пока незачем. Но когда файлов станет двадцать, найти нужное можно будет за секунды. Этому учатся на маленьком проекте, а не на большом.
Что делает digitalWrite(LED_PIN, HIGH) физически?
Подаёт на ножку 3,3 В. Не «зажигает диод» — именно подаёт напряжение. Диод горит как следствие. Разница важна: к этой ножке можно подключить что угодно.
Что плохого в delay()?
«Пока плата ждёт эти 500 мс — что ещё она может делать?»
Ответ: ничего. Процессор простаивает. Пока в проекте один светодиод — не беда;
когда появится датчик, который нельзя пропустить, — проблема. Решение
(millis() вместо delay()) — в следующем компоненте.
Блок 5. Практика по уровням (30–40 мин)
Каждый берёт свой уровень. Задания — на карточке или на доске.
Уровень A (базовый)
- Сделать мигание в два раза быстрее. (500 → 250)
- Сделать в два раза медленнее. (500 → 1000)
- Сделать так, чтобы светодиод горел долго, а гас на мгновение.
Подсказка: одно значение задаёт оба интервала — потребуется заглянуть в
led.cpp.
Уровень B (средний)
- Добавить второй светодиод на другой пин и мигать обоими одновременно.
- Сделать «маячок»: две короткие вспышки, затем длинная пауза. Проверка: ритм узнаётся со стороны.
- Ответить письменно: что будет при
toggleLED(0)? Проверить на плате. Ответ разобран в GUIDE.md, раздел 5.
Уровень C (продвинутый)
- Сделать два светодиода мигающими в противофазе (один горит — другой нет).
- Вынести номер пина в
led.hтак, чтобы можно было управлять двумя диодами, не дублируя функции. Это подводит к идее параметра — и к классу-драйверу следующего урока. - Измерить, сколько времени
loop()тратит на один цикл. Сравнить с ожидаемым. Инструмент:millis()до и после, вывод в Serial.
Ответы на вопросы, которые прозвучат
«А можно мигать по-разному без задержки вообще?» Можно, и это следующий урок. Сейчас задержка — самый простой способ, и у него есть цена, которую мы обсудили.
«Почему именно GPIO2?» На большинстве плат DevKit к нему уже припаян встроенный светодиод, поэтому пример работает даже без сборки схемы. Подробности про выбор пина — в WIRING.md.
«Я поменял число, а ничего не изменилось» Код нужно заново загрузить в плату. Правка в редакторе сама на плату не попадает — это важное открытие, стоит проговорить для всех.
«А можно сделать, чтобы мигало от хлопка?» Да, и это цель курса. Нужен датчик — до него дойдём, когда научимся управлять выходом. Сегодня мы научились именно этому.
«Светодиод нагрелся, это нормально?» Слегка тёплый резистор — нормально. Горячий светодиод — нет: проверить номинал резистора, возможно стоит меньший или его нет вовсе.
Итог урока: что записать в журнал
Три строки, которые ученик формулирует сам (педагог помогает вопросами):
setup()выполняется один раз,loop()— бесконечно.- Светодиод подключается через резистор и соблюдая полярность.
- Поведение железа задаётся числом в коде — его можно изменить.
Проверка понимания (экспресс, 2 минуты)
| Вопрос | Ожидаемый ответ |
|---|---|
| Куда вписать число, чтобы мигало реже? | toggleLED(...) в main.cpp |
Что будет, если убрать setupLED() из setup()? | Пин не настроен как выход — мигания не будет |
| Зачем резистор? | Ограничить ток, иначе светодиод сгорит |
Почему pinMode() не в loop()? | Настройка нужна один раз; в цикле — пустая трата времени |
Типичные затруднения
| Затруднение | Причина | Что делать |
|---|---|---|
| «Не понимаю, где начало программы» | Нет опыта с setup/loop | Аналогия: сборы перед выходом и сами шаги |
Меняют код в led.cpp, а ждут эффекта от main.cpp | Не видят связи файлов | Пройти путь вызова пальцем по экрану |
| Боятся сломать плату | Нормальный страх новичка | Сказать прямо: испортить прошивкой нельзя, только неверной схемой |
| Быстро закончили и скучают | Разный темп | Уровень C, затем помощь соседней паре |
Домашнее задание
Обязательная часть (все уровни): записать своими словами, что делает каждая
из трёх строк в loop() и setup(). Своими — не копируя комментарии из кода.
Уровень A: придумать и записать ритм мигания (например, как у маяка), подобрать числа, проверить на следующем занятии.
Уровень B: ответить на вопросы 1–3 из GUIDE.md, раздел «Проверь себя», сверить с ответами, выписать то, что оказалось неожиданным.
Уровень C: прочитать раздел 4 GUIDE.md
про цену delay() и письменно предложить, как мигать двумя диодами с разной
частотой одновременно. Решение разбирается на следующем уроке.
Связь с курсом
| Что дальше | Где |
|---|---|
Неблокирующее мигание, millis(), класс-драйвер | LED_middle |
| Адресные светодиоды, эффекты | LED_adress |
| Звук вместо света — тот же GPIO, другая нагрузка | ActiveBuzzer |
Этот урок — фундамент: всё дальнейшее в курсе (датчики, радио, сеть) стоит на понимании «программа управляет ножкой, ножка управляет железом».
Чек-лист педагога перед уроком
- Проект собран и прошит на одной плате лично
- Запасной USB-кабель с проверенной передачей данных
- Светодиоды и резисторы разложены по парам
- Собран образцовый стенд для завязки
- На доске заготовлено место для правила
setup/loop - Карточки с заданиями уровней A / B / C распечатаны
- Известно, на каких машинах PlatformIO ставится дольше всего