Коридорный ночник с кнопкой я собрал за вечер, а переделывал неделю. Свет должен был набирать яркость плавно, секундой-полторы, чтобы не слепило в темноте. Первый вариант выглядел невинно: цикл for от нуля до 255, внутри analogWrite(9, y) и delay(6) — полторы секунды подъёма, на столе красиво. А потом посреди подъёма я нажал кнопку выключить — и кнопка молчала полторы секунды: loop был заперт внутри for, до digitalRead очередь не дошла. Свет догорел до максимума, отработал спад, и только потом скетч заметил нажатие. Классический delay-грабель, только растянутый на 255 кусочков. Полторы секунды — это 255 шагов по 6 миллисекунд: для пальца на кнопке вечность, для человека в коридоре — полдороги до двери.
Плохая новость шире одной кнопки: пока цикл с паузами крутится, скетч не делает вообще ничего. Датчик движения не опрашивается, счёт секунд стоит, ответ в Serial не уходит — всё, что жил по правилам кооперативного цикла, замерло на полторы секунды. Датчик движения в этой же сборке ждал опроса каждые сто миллисекунд — и ждал полторы, каждый раз. Блокирующий фейдер — это один длинный delay, замаскированный под плавность: глаз видит плавный свет, а скетч видит мёртвое время.
Лечение — вывернуть фейдер наизнанку: не «сейчас проедем всю яркость за раз», а «сделаем один маленький шаг, если подошло время, и вернёмся в loop». Цель и текущая яркость, шаг по millis-гейту, кнопка меняет цель мгновенно — весь словарь урока. Всё это уже знакомо вам по millis вместо delay и планировщику задач, здесь только применяем механику к яркости — и она впервые едет без единого блокирующего вызова.
Яркость как задача планировщика
Механика — три переменных и один таймер. tek — текущая яркость 0-255, cel — цель, lastShag — отметка времени последнего шага. Каждый оборот loop() проверяет гейт: прошло 6 миллисекунд — делаем один шаг к цели: tek меньше cel — плюс единица, tek больше — минус единица, и analogWrite. Кнопка с антидребезгом из урока про дребезг в момент фронта меняет цель с нуля на 255 или обратно — мгновенно, одним присваиванием. А плавность едет сама: 255 шагов по 6 мс — это 1,53 секунды на полный ход, по 12 мс — 3,06 секунды. Хотите темп резче — увеличивайте шаг до двух-трёх единиц за раз, время хода делится на шаг ровно. Гейт на 6 мс — это 166 шагов в секунду, а loop между гейтами крутится свободно: кнопка, датчики и печать получают сотни оборотов между двумя шагами яркости.
Проверить, что loop жив, проще всего второй задачей в том же цикле: маяк на отдельном пине мигает по своему гейту, печать tek= и cel= идёт раз в 300-400 миллисекунд — и во время подъёма яркости секунды продолжают тикать. В for-версии счётчик стоял намертво. Это и есть главный тест фейдера: умирайте в нём с закрытыми глазами — пусть всё остальное дышит. Только следите за объёмом печати: строка в двадцать символов на 9600 бод стоит порту около двадцати миллисекунд, и печать на каждый шаг задавила бы монитор и съела те самые полторы секунды заново.
Про пин — сразу жёстко: analogWrite работает только на пинах с тильдой, 3, 5, 6, 9, 10 и 11; базу duty cycle 0-255 мы разбирали в уроке про ШИМ. На остальных пинах вызов молча вырождается в digitalWrite, и фейдер превращается в щелчок: до половины хода темно, потом резко горит. Ещё одна привычка: плавность фейдера живёт в темпе шагов, а не в герцах таймера — 490 герц пина 9 с запасом выше порога различения глаза, задирайте гейт, а не частоту ШИМ.
// Фейдер на ШИМ без delay: кнопка D2 меняет цель, яркость доезжает шагами.// Лампа на ШИМ-пине 9 (тильда), маяк на 12 — вторая задача того же loop.const int LAMPA = 9;const int KNOP = 2;const int MAYAK = 12; int tek = 0; // текущая яркость 0-255int cel = 255; // цель: при старте свет набираетсяunsigned long lastShag = 0;unsigned long lastKnop = 0;unsigned long lastMayak = 0;int byloKnop = HIGH;int mayak = 0; void setup() { pinMode(LAMPA, OUTPUT); pinMode(MAYAK, OUTPUT); pinMode(KNOP, INPUT_PULLUP); Serial.begin(9600);} void loop() { // кнопка: фронт + антидребезг — меняет только цель int kn = digitalRead(KNOP); if (kn == LOW && byloKnop == HIGH && millis() - lastKnop > 50) { byloKnop = LOW; lastKnop = millis(); if (cel == 0) { cel = 255; } else { cel = 0; } Serial.print("cel="); Serial.println(cel); } if (kn == HIGH) { byloKnop = HIGH; } // фейдер: один шаг раз в 12 мс — 255 шагов, около 3 с на полный ход if (millis() - lastShag >= 12) { lastShag = millis(); if (tek < cel) { tek = tek + 1; } else if (tek > cel) { tek = tek - 1; } analogWrite(LAMPA, tek); } // вторая задача: маяк мигает своим гейтом и не зависит от яркости if (millis() - lastMayak >= 700) { lastMayak = millis(); if (mayak == 1) { mayak = 0; } else { mayak = 1; } digitalWrite(MAYAK, mayak); }}| Темп шага | Полный ход 0-255 | Как ощущается | Где применяю |
|---|---|---|---|
| 1 единица раз в 4 мс | около 1,0 с | быстрый клик, но не щелчок | коридор, лестница |
| 1 единица раз в 6 мс | около 1,5 с | комфортный ночник | спальня, прикроватная |
| 1 единица раз в 12 мс | около 3,1 с | театральная пауза | детская на ночь |
| 3 единицы раз в 6 мс | около 0,5 с | ступеньки заметны глазу | не применяю: экономия мизерная |
Симулятор: лампа, кнопка и секунды на одном millis
В макетке лампа на пине 12, кнопка на 2; про ШИМ на любом пине в симуляторе — оговорка из note выше, на плате переносите лампу на 9. Кнопка живёт на D2 с INPUT_PULLUP, нажатие читается как LOW — те же правила, что в уроке про кнопку и digitalRead, одна привычка на все уроки. Кнопка работает по фронту с антидребезгом 30 мс: одно нажатие — одна смена цели 0 и 255. Рядом с фейдером крутится вторая задача — счётчик секунд sek: он печатается в мониторе и продолжает расти во время всего подъёма и спада. Это ваша проверка на прочность: в блокирующей for-версии sek замирал ровно на время хода. Печать tek= и cel= идёт раз в 400 мс — строки короткие, монитор не захлёбывается.
Подключите плоттер — урок про график из Serial — и посмотрите на tek=: пила со срезанными верхушками. Верхушки — это паузы на цели, плоские, потому что шагов больше нет, а analogWrite держит последнее значение. По крутизне склонов видно темп: поднимите период шага — склоны разлягутся. Если пила дрожит на склонах, гейт считает шаги неровно — ищите блокирующий вызов, вернувшийся в loop.
Концы хода: ease и нелинейный глаз
Линейный фейдер закрывает процентов восемьдесят случаев, но у глаза своя кривая: восприятие яркости нелинейное, и последние ступени хода кажутся резче, чем идут по числам. В базовом уроке про ШИМ мы это уже лечили квадратом duty для диммера на потенциометре — тут второй рычаг, ease: размер шага зависит от расстояния до цели. Далеко — шаг 3-4 единицы, ближе чем на 40 — шаг 1. Полное время почти то же, а концы хода расплываются мягко. Мой критерий, когда оно надо: если свет виден прямо с дивана и ход длиннее полутора секунд — делаю ease, иначе линейный; усложнять фейдер ради фейдера я перестал после второй переделки ночника. Числами: линейный фейдер делает 255 шагов; ease с шагом 4 вдали от цели и шагом 1 ближе чем на 40 — около сотни вызовов, а концы хода ползут, а не скачут. И потолок: ночнику не нужен duty выше 180 — глаз не отличит, а лампа живёт дольше.
И мостик дальше: та же самая механика цели и шагов едет на нагрузке любого размера — стоит поменять светодиод на 12-вольтовую ленту через ключ. Что встанет между пином и лентой и почему там нужен именно логический MOSFET — следующий урок про PWM-диммер на MOSFET.
- 1. Почему кнопка молчала в первой версии ночника?
- 2. Полный ход 0-255 при шаге 1 единица раз в 6 мс — это сколько?
- 3. analogWrite(12, 200) на настоящей Uno — что реально на пине?
- 4. Как кнопка меняет яркость в фейдере без delay?
- 5. sek= в мониторе не останавливается во время подъёма яркости. Что это доказывает?
- Блокирующий фейдер — delay, размазанный на 255 кусочков: for с паузами запирает кнопку, датчики и Serial на весь ход
- Фейдер без блокировок — три переменных и один гейт: cel цель, tek текущая, шаг раз в N мс; 255 шагов по 6 мс = 1,5 с на ход
- Кнопка меняет только цель — мгновенно и без потерь; плавность едет сама, нажатие на середине хода разворачивает яркость
- analogWrite живёт только на пинах с тильдой 3, 5, 6, 9, 10, 11: на других он молча становится digitalWrite, и фейдер щёлкает на середине хода
- Концы хода смягчает ease — шаг больше вдали от цели и шаг 1 у цели; нелинейность глаза лечится и квадратом duty из урока про ШИМ
Частые вопросы по теме урока
- Почему кнопка молчала в первой версии ночника?
- for с 255 шагами по delay(6) — это полторы секунды сплошного мёртвого времени: до digitalRead очередь не доходит. Блокирующий фейдер — тот же delay, размазанный по шагам. loop был заперт внутри for с delay и не доходил до digitalRead
- Полный ход 0-255 при шаге 1 единица раз в 6 мс — это сколько?
- 255 шагов по 6 мс = 1530 мс, около полутора секунд. Время хода равно числу шагов на период гейта, делённому на размер шага, — частота ШИМ тут ни при чём. около 1,5 секунды
- analogWrite(12, 200) на настоящей Uno — что реально на пине?
- Пины ШИМ на Uno — 3, 5, 6, 9, 10, 11. На остальных analogWrite молча становится digitalWrite: duty до 127 даёт LOW, с 128 — HIGH. Фейдер на пине 12 превращается в щелчок на середине хода. вырождается в digitalWrite(HIGH): у пина 12 нет тильды
- Как кнопка меняет яркость в фейдере без delay?
- Смена цели — мгновенная, дешёвая и неблокирующая; плавность едет сама, по шагам в millis-гейте. Поэтому нажатия не теряются никогда, даже посреди хода. меняет цель cel одним присваиванием, а яркость доезжает шагами по гейту
- sek= в мониторе не останавливается во время подъёма яркости. Что это доказывает?
- Счётчик секунд крутится по своему millis-гейту в том же loop. Пока шаги яркости не съедают время целиком, все задачи живут: в for-версии с delay sek замирал на весь ход. что loop не блокирован: фейдер — обычная задача среди других