В комнате у меня жили три прибора с тремя пультами: люстра на реле, ночник на светодиоде и зуммер-таймер, отгоняющий от стола. Три пульта, три схемы, три батарейки — и вечная путаница, какой из них куда смотрит. Пульт-центр собрался за один вечер: одна плата, один приёмник, один пульт, а на экране меню, из которого видно, чем управляешь прямо сейчас. Через месяц бардак на полке исчез, а щелчок реле стал звучать как ответ: «принято, исполняю».
Проект — точка сборки раздела, и здесь сходятся навыки по одному: таблица кодов из читалки, switch и приручённый автоповтор из урока про управление светом, короткий loop и приём без потерь из урока про прерывания. Плюс экран: LCD 16x2 на шине I2C показывает режим и статус, и меню перестаёт быть угадайкой по монитору порта. Статус на экране выручает и в отладке: видно, что скетч услышал команду, даже когда Serial закрыт.
План такой: сначала архитектура словаря команд, потом полный скетч и симулятор, в котором всё крутится — реле щёлкает, светодиод зажигается, зуммер отрабатывает длительность. Если вы шли по разделу с макеткой в руках, каждый приём здесь узнается: и закольцовка режимов, и millis-планировщик, и гигиена окна приёма — я специально не ввожу ни одного нового приёма, только собираю. После урока останется перенести скетч на плату и заменить виртуальные нагрузки настоящими: распиновка сохранена один в один.
Архитектура: навигация и исполнение на двух этажах
Команды делятся на два этажа. Первый — навигация: CH- и CH+ листают режим — свет, ночник, сирена, а OK исполняет действие активного режима. Второй этаж — исполнение внутри режима: в сирене VOL+ и VOL- меняют длительность сигнала. Такой двухэтажный словарь экономит кнопки: шести-семи команд хватает на десяток функций. У пульта из набора всё необходимое есть: 70 и 69 — каналы, 67 — OK, 21 и 7 — громкость, вся таблица снята ещё в первой читалке. Появится четвёртый режим — добавить один case и одну строку на экран, остальное не трогается: в этом весь смысл этажей.
Экран обновляем только при изменении: флаг needLcd поднимается в обработчике команд и опускается после отрисовки. Полный кадр 16x2 — это 32 байта плюс служебные, при 100 кГц шины I2C выходит около трёх-четырёх миллисекунд; печать на каждом проходе loop означала бы тысячу перерисовок в секунду — шина жила бы только экраном, а глаз видел бы мерцание. С флагом экран перерисовывается два-три раза за нажатие и спит между ними. Эти три-четыре миллисекунды стоит помнить, когда в проекте появится своя шина с датчиками: отрисовка по флагу оставляет шине воздух.
Зуммер взят активный, на digitalWrite: HIGH пищит, LOW молчит. Выбор не случайный — в прошлом уроке мы разобрали, что пассивный зуммер на tone() на Uno садится на таймер приёмника, а здесь пульт и сирена обязаны работать одновременно. Нагрузка посерьёзнее — реле: модуль с оптопарой берёт с пина пару миллиампер и сам коммутирует свои 220 вольт на люстру. Светодиод ночника — прямой пин через резистор 220 Ом; яркость тут не нужна, ночнику хватит включённого и погашенного, а плавная регулировка при желании добавится позже на ШИМ-пине.
#include <IRremote.hpp>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); const int IR_PIN = 2; // приёмник VS1838Bconst int PIN_RELAY = 7; // реле люстрыconst int PIN_LED = 12; // ночникconst int PIN_BUZ = 8; // активный зуммер int mode = 0; // 0 свет, 1 ночник, 2 сиренаbool relayOn = false;bool ledOn = false;int durSec = 2; // длительность сирены: 2-6 сunsigned long buzOffMs = 0;bool needLcd = true; void setup() { Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); pinMode(PIN_RELAY, OUTPUT); pinMode(PIN_LED, OUTPUT); pinMode(PIN_BUZ, OUTPUT); lcd.init(); lcd.backlight();} void drawLcd() { lcd.clear(); lcd.setCursor(0, 0); if (mode == 0) { lcd.print("REZHIM: SVET"); } if (mode == 1) { lcd.print("REZHIM: NOCHNIK"); } if (mode == 2) { lcd.print("REZHIM: SIRENA"); } lcd.setCursor(0, 1); if (mode == 0) { if (relayOn) { lcd.print("RELE VKL"); } else { lcd.print("RELE VYKL"); } } if (mode == 1) { if (ledOn) { lcd.print("LED GORIT"); } else { lcd.print("LED POGASHEN"); } } if (mode == 2) { lcd.print("S="); lcd.print(durSec); lcd.print("s"); }} void loop() { if (IrReceiver.decode()) { int cmd = IrReceiver.decodedIRData.command; if (cmd == 70) { // CH-: следующий режим mode = mode + 1; if (mode > 2) { mode = 0; } needLcd = true; } if (cmd == 69) { // CH+: предыдущий mode = mode - 1; if (mode < 0) { mode = 2; } needLcd = true; } if (cmd == 67) { // OK: действие режима if (mode == 0) { relayOn = !relayOn; } if (mode == 1) { ledOn = !ledOn; } if (mode == 2) { digitalWrite(PIN_BUZ, HIGH); buzOffMs = millis() + durSec * 1000L; } needLcd = true; } if (cmd == 21 && mode == 2) { // VOL+: дольше durSec = constrain(durSec + 2, 2, 6); needLcd = true; } if (cmd == 7 && mode == 2) { // VOL-: короче durSec = constrain(durSec - 2, 2, 6); needLcd = true; } IrReceiver.resume(); // печать — после resume: окно занятости не растягиваем Serial.print("cmd="); Serial.println(cmd); } digitalWrite(PIN_RELAY, relayOn); digitalWrite(PIN_LED, ledOn); if (buzOffMs > 0 && millis() >= buzOffMs) { digitalWrite(PIN_BUZ, LOW); buzOffMs = 0; } if (needLcd) { drawLcd(); needLcd = false; }}Разбор начинается с гигиены приёма: IrReceiver.resume() стоит сразу после обработки команд, а печать cmd= — уже после resume(), ровно по правилам из урока про прерывания: окно занятости длится микросекунды, и буфер приёмника не переполняется даже при быстром щёлканьи. Печать в Serial оставлена одна строка на кадр — этого хватает для отладки, а подробности живут на экране, куда выглядывать дешевле.
Переключение режимов идёт с закольцовкой: mode растёт до 2 и возвращается к нулю, вниз от нуля — к двойке. Длительность сирены держит метка buzOffMs, а loop лишь сравнивает её с millis() — ни одного delay во всём скетче, ни в меню, ни в сигнале. Это тот самый планировщик, который мы строили, когда уходили от delay, только теперь на нём стоит весь проект целиком. Единственный тонкий момент — умножение durSec * 1000: без суффикса L на плате это умножение на int, и 6 секунд ещё влезают, но привычка ставить суффикс у больших чисел спасает от внезапного переполнения в похожих скетчах.
Автоповтор здесь сознательно не используется: все команды переключатели, и повторный кадр им не страшен. Но если захотите плавно гасить ночник — вспомните флаг IRDATA_FLAGS_IS_REPEAT из урока про управление: регулировки исполняют каждый кадр подряд, переключатели — только первый. Тогда пульт сдержит и второе обещание, данное люстре: держишь VOL+ — свет ползёт, отпустил — остановился. И практика от стенки: пульт от люстры после падения стал слать по два кадра за нажатие, и реле перещёлкивалось дважды — лечится меткой последней команды: тот же код раньше 300 миллисекунд игнорируем, millis() уже стоит на месте.
Перенос на плату: от макетки к стене
На реальной плате раскладка сохранена один в один: приёмник на пин 2, реле на 7, светодиод ночника на 12 через резистор 220 Ом, зуммер на 8, экран — на стандартные A4 и A5, где на Uno живут SDA и SCL. Экран с I2C-адресом 0x27 — самый ходовой; если сканер шины увидел 0x3F, это тот же экран с другим преобразователем — меняется одна константа в конструкторе. Единственная нежность — питание: катушка реле в модуле тянет 70-90 миллиампер, экран ещё 20-30, и от USB-порта с длинным тонким проводом этому уже тесно. Я вешаю центр на зарядку от телефона на 1 ампер — и забываю про питание на месяцы.
Люстра подключается к клеммам реле: фаза через размыкающий контакт, ноль идёт мимо платы напрямую. Сборку проверяю всегда на холодную: жмёшь OK — модуль щёлкает и мигает своим светодиодом, опасная нагрузка ещё не подключена. И железное правило из урока про реле: макетка — территория 5 вольт, всё, что выше, — на клеммной колодке, в корпусе и без ковыряния под напряжением.
Тупик первого центра — просадка питания: плата жила от порта ноутбука, и на щелчке реле вместе со вспышкой светодиода экран тускнел, а плата перезагружалась — меню сбрасывалось на свет прямо посреди вечера. Лечится конденсатором 470-1000 мкФ по шине 5 вольт рядом с модулем реле или нормальным блоком питания. После этой правки центр прожил зиму без единой перезагрузки — и научил меня проверять питание раньше кода.
| Кнопка | Код | Действие |
|---|---|---|
| CH- | 70 | следующий режим: свет, ночник, сирена |
| CH+ | 69 | предыдущий режим |
| OK | 67 | исполнить: реле перещёлкнуть, светодиод перещёлкнуть, тест сирены |
| VOL+ / VOL- | 21 / 7 | в режиме сирены: длительность 2-6 с |
| Остальные кнопки | - | игнорируются: состояние не трогается |
- Двухэтажный словарь: CH-, CH+ и OK — навигация, VOL+ и VOL- — исполнение внутри режима; семи кнопок хватает на десяток функций
- LCD перерисовывается по флагу needLcd: полный кадр 16x2 — три-четыре мс шины I2C, печать на каждом проходе loop не нужна
- Активный зуммер на digitalWrite живёт рядом с приёмником; пассивный на tone() на Uno сел бы на таймер приёмника
- Ни одного delay: длительность сирены держит метка buzOffMs и сравнение с millis()
- resume() в конце decode-блока и одна строка печати на кадр — приём не глохнет даже при быстром щёлканьи
- 1. Почему для сирены взят активный зуммер на digitalWrite, а не tone()?
- 2. Зачем флаг needLcd вместо печати на каждом проходе loop?
- 3. Кнопка VOL+ нажата в режиме свет. Что произойдёт?
- 4. Нажатие CH- в режиме 2 — куда попадём?
- 5. Где должен стоять IrReceiver.resume() в этом скетче?
Пульт-центр — точка сборки раздела: приёмник понимает пульт, меню живёт на экране, нагрузки щёлкают по команде, и ни один delay не стоит между командой и исполнением. Дальше два пути. Железный: заменить виртуальные нагрузки настоящими и повесить центр на стену — распиновка уже выбрана, а просадку питания вы теперь ловите на раз. Для стенки добавьте режим «всё выключить» — одна команда, одна ветка, десять минут работы. Программный: научить плату говорить самой — передатчик из урока про RAW и sendRaw умеет повторять кадры чужих пультов, и тогда центр начнёт не только слушать комнату, но и отвечать ей её же языком. Мой центр дорастил до шлюза: кнопка пульта теперь добирается до браузера — но это уже история для другого раздела.
Частые вопросы по теме урока
- Почему для сирены взят активный зуммер на digitalWrite, а не tone()?
- На AVR таймер приёмника IRremote совпадает с таймером tone(). Активный зуммер ест просто уровень с пина, и пульт с сиреной работают одновременно. На Uno tone() занимает таймер приёмника ИК
- Зачем флаг needLcd вместо печати на каждом проходе loop?
- Полный кадр 16x2 — около трёх-четырёх мс при 100 кГц. Тысяча перерисовок в секунду заняла бы всю шину и мерцала; перерисовка по флагу — два-три раза за нажатие. Чтобы не забивать шину I2C и не мерцать экраном
- Кнопка VOL+ нажата в режиме свет. Что произойдёт?
- В скетче VOL+ привязан условием cmd == 21 && mode == 2. В остальных режимах команда игнорируется — двухэтажный словарь исполняет кнопки по контексту. Ничего: действие VOL+ определено только в режиме сирены
- Нажатие CH- в режиме 2 — куда попадём?
- mode растёт до 2 и возвращается к нулю, вниз от нуля — к двойке. Закольцовка в обе стороны снимает вопрос, в какую сторону листать обратно. В режим 0: листание закольцовано
- Где должен стоять IrReceiver.resume() в этом скетче?
- Окно decode-resume — момент, когда буфер глух. Чем короче окно, тем надёжнее приём: вся логика, печать одной строкой — и сразу resume(). В самом конце decode-блока, после всей логики и печати