Пульт от музыкального центра год лежал без дела: сам центр уехал на дачу, а пульт остался, и батарейка в нём не садилась. Превратить такой брошенный пульт в панель выключателей — самое выгодное вложение вечера во всём разделе: приёмник уже подключён, коды кнопок уже собраны читалкой, и всё, что остаётся, — научиться превращать байт команды в щелчок реле и вспышку светодиода. Розеточные радиопульты за эту работу просят деньги и ещё просят эфир, а тут подоконник уже всё умеет. Мой рекорд — три нагрузки на одном пульте за один вечер: люстра на реле, ночник на светодиоде и вентилятор, до которого потом добралась отдельная тройка уроков про скорости.
Схема урока честная: четыре кнопки — четыре действия. OK щёлкает реле-модулем, на клеммы которого потом встанут настоящие светильники; CH+ и CH- зажигают и гасят светодиод ночника; EQ гасит всё разом — кнопка «выходя, гасите свет». Коды взяты у пульта из наборов, но принцип не меняется: у вашего пульта числа свои, из карты кодов, которую вы сняли в уроке-сканере.
Новая часть здесь одна — словарь команд. Он живёт во флеше через PROGMEM и читается функцией pgm_read_byte, а диспетчер действий собран на switch. Переключать нагрузку пультом мы уже умеем из урока про управление светом, итоговый пульт-центр раздела складывал эти приёмы в одно устройство; здесь добираем недостающий этаж — как держать таблицу команд, не съедая оперативную память, и почему это правило становится обязательным, как только словарь растёт и обрастает журналами.
Словарь команд во флеше: зачем PROGMEM, когда есть RAM
У Uno два этажа памяти: 32 килобайта флеша под скетч и 2 килобайта SRAM под переменные. Таблица из четырёх команд весит восемь байт и в оперативной памяти помещается шутя. Но словари растут: у полноформатного пульта двадцать кнопок, а если хранить не только код, но и адрес пульта-источника, номер нагрузки и признак «переключатель или кнопка», счёт идёт на сотни байт. Рядом живут буферы самой IRremote, очереди Serial, ваши переменные состояния — и вот на двух килобайтах начинается охота за каждым байтом, с симптомами, о которых ниже.
Правило простое: всё, что только читается и никогда не меняется, — во флеш. Ключевое слово PROGMEM переселяет массив в программную память, и он не копируется в SRAM при старте. Механика и ловушки разобраны в уроке про таблицы во флеше — там объяснено, почему после переезда обычная запись table[i] читает мусор: таблица теперь лежит на другой шине, и доставать её надо функцией pgm_read_byte, скормив ей адрес ячейки.
Цена переезда — пара лишних тактов на чтение, и одна таблица, которую опрашивают раз в нажатие, секундомера не заметит. Выигрыш — целые сотни байт SRAM под скетч. Я перевёл таблицы кодов на PROGMEM после того, как журнал команд на тридцать записей уложил ночник: декодер приёмника перестал ловить каждый второй кадр, а монитор молчал — классическая тихая смерть от нехватки RAM, когда всё выглядит рабочим, но половина нажатий уходит в никуда.
#include <IRremote.hpp> const int IR_PIN = 2; // приёмник: совет из первого урока разделаconst int RELAY_PIN = 7; // реле люстрыconst int LED_PIN = 12; // ночник // номера действий — это то, что лежит во второй колонке словаряconst byte ACT_RELAY_TOGGLE = 1;const byte ACT_LED_ON = 2;const byte ACT_LED_OFF = 3;const byte ACT_ALL_OFF = 4; // словарь: код кнопки -> действие. PROGMEM держит его во флешеconst byte KOD_ACTION[][2] PROGMEM = { { 67, ACT_RELAY_TOGGLE }, // OK { 69, ACT_LED_ON }, // CH+ { 70, ACT_LED_OFF }, // CH- { 9, ACT_ALL_OFF }, // EQ};const byte KOD_COUNT = 4; void setup() { Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); pinMode(RELAY_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT);} void loop() { if (IrReceiver.decode()) { byte cmd = IrReceiver.decodedIRData.command; for (byte i = 0; i < KOD_COUNT; i++) { byte kod = pgm_read_byte(&KOD_ACTION[i][0]); if (kod == cmd) { byte act = pgm_read_byte(&KOD_ACTION[i][1]); switch (act) { case ACT_RELAY_TOGGLE: digitalWrite(RELAY_PIN, !digitalRead(RELAY_PIN)); break; case ACT_LED_ON: digitalWrite(LED_PIN, HIGH); break; case ACT_LED_OFF: digitalWrite(LED_PIN, LOW); break; case ACT_ALL_OFF: digitalWrite(RELAY_PIN, LOW); digitalWrite(LED_PIN, LOW); break; } break; } } IrReceiver.resume(); }}В скетче три детали, на которых я спотыкался сам. Запись &KOD_ACTION[i][0] — это адрес ячейки во флеше, именно его ждёт pgm_read_byte; вторая колонка живёт по соседству, в [i][1]. Переменная cmd объявлена как byte — команда NEC влезает в один байт, и больший тип тут только пестрит. И break в каждом case: провал в следующий блок означал бы, что EQ заодно потушит ночник и щёлкнет реле ещё раз — тихий дефект без единого сообщения компилятора, о котором мы уже предупреждали в уроке про команды и повтор.
Диспетчер получился двухэтажным: таблица переводит код кнопки в номер действия, switch переводит номер в электричество. Такой расклад удобен при росте — добавить нагрузку значит дописать строку в таблицу и один case, а перечитывать весь каскад if не придётся. На реальной плате перед клеммами перечитайте урок про реле-модуль и 220 вольт: макетка — территория пяти вольт, всё выше — на клеммной колодке, в корпусе и без ковыряния под напряжением.
Номера действий я заворачиваю в именованные константы сразу, над setup(): ACT_RELAY_TOGGLE вместо голой единицы. Компилятор подставит те же байты, а через полгода скетч читается как расписание, а не как шифровка. Вторая привычка — комментарий с названием кнопки в каждой строке словаря: коды из карты сами по себе ни о чём не говорят, и строка { 67, ACT_RELAY_TOGGLE }, // OK стоит ровно столько же, сколько без неё.
Порядок запуска: журнал, потом нагрузки
Мой порядок ввода нагрузок в строй не меняется годами. Первый шаг — читалка: приёмник на месте, жму кнопки, сверяю команды с картой кодов. Второй — светодиод вместо нагрузки: он отрабатывает команды без щелчков, и логика видна глазами. Третий — реле-модуль: его вход берёт с пина считанные миллиамперы через оптопару, катушка питается от самого модуля, а не от пина. И только четвёртым шагом к клеммам идёт настоящая лампа — после того как каждая кнопка отработала на холодной схеме хотя бы десяток раз подряд.
Питанию уделите минуту сейчас, чтобы не лечить призраков потом. Катушка реле в модуле тянет 70–90 миллиампер в момент щелчка, и при питании от тонкого USB-кабеля этот скачок просаживает шину: плата перезапускается, приёмник оглушает, монитор печатает мусор. Конденсатор 470–1000 мкФ по шине 5 вольт рядом с модулем лечит полностью; расплата за игнор — плавающие перезагрузки ровно в момент срабатывания, самые загадочные из всех, что мне попадались. Диагноз ставится за минуту: если плата перезагружается строго на щелчке реле и никогда иначе — это питание, а не код.
| Кнопка | Код (пример) | Действие | Нагрузка |
|---|---|---|---|
| OK | 67 | переключить реле | люстра через реле-модуль на пине 7 |
| CH+ | 69 | включить ночник | светодиод на пине 12 |
| CH- | 70 | погасить ночник | тот же светодиод |
| EQ | 9 | всё выключить | реле и светодиод разом |
Когда словарь заработал, расширяйте его в две стороны. Вглубь: действие может запускать сценарий, а не просто нагрузку — десять минут света по таймеру на millis, механика из урока про millis вместо delay. Вширь: два пульта с разными адресами, как в уроке про несколько пультов, и у каждого своя половина словаря. Оба расширения не трогают диспетчер — двухэтажный каркас «таблица плюс switch» я переношу между проектами почти без правок.
Проверьте границы словаря и на чужих кнопках: пульт, которого нет в таблице, должен молчать, а не щёлкать. Пробный кадр с чужого пульта проходит по всем двадцати сравнениям, не находит совпадения и уходит через IrReceiver.resume() без единого действия — так работает фильтр, и он бесплатный. Если хочется, чтобы панель понимала два пульта, читайте адрес до поиска по таблице и ведите словари параллельно: механика разделения разобрана в уроке про адреса, ссылка выше.
Следующий урок — про чужие пульты: как снять коды с устройства, пульт от которого потерялся, и где копирование упирается в границу протоколов. А этот скетч уйдёт с вами дальше по разделу: словарь во флеше и switch по действию — два приёма, которые переживут любые кнопки и любую смену пульта.
- 1. Зачем словарь команд держат в PROGMEM, если он и в RAM занимает всего пару десятков байт?
- 2. Как правильно прочитать ячейку PROGMEM-таблицы?
- 3. При щелчке реле плата перезагружается. Первое действие?
- 4. Что случится, если в switch забыть break?
- 5. Почему вход реле-модуля безопасно вешать прямо на пин Arduino?
- Диспетчер двухэтажный: таблица переводит код кнопки в действие, switch переводит действие в электричество
- PROGMEM держит константный словарь во флеше (32 КБ против 2 КБ RAM); чтение — только pgm_read_byte с адресом &таблица[i]
- Каждый case закрывайте break: провал в соседний блок — тихий дефект без сообщений компилятора
- Порядок ввода нагрузки: читалка, светодиод, реле, и только потом настоящие 220 вольт — на холодной схеме
- Катушка реле ест 70–90 мА в момент щелчка: конденсатор 470–1000 мкФ по шине 5 В спасает от перезагрузок
Частые вопросы по теме урока
- Зачем словарь команд держат в PROGMEM, если он и в RAM занимает всего пару десятков байт?
- Флеша 32 КБ, SRAM 2 КБ; буферы IRremote и ваши переменные уже съедают кусок. Константы во флеше освобождают RAM — на растущих словарях это вопрос работоспособности, а не аккуратности. RAM всего 2 КБ, и константная таблица не должна её занимать вместе с буферами библиотек
- Как правильно прочитать ячейку PROGMEM-таблицы?
- После PROGMEM таблица живёт на шине флеша; прямая индексация читает мусор из чужого адресного пространства. Доставка — только pgm_read_byte с адресом через амперсанд. pgm_read_byte(&table[i][0]) — функцией с адресом ячейки
- При щелчке реле плата перезагружается. Первое действие?
- Катушка реле тянет 70–90 мА в момент щелчка и просаживает питание: плата перезапускается, приёмник оглушает. Конденсатор по 5 вольт гасит скачок полностью. Конденсатор 470–1000 мкФ по шине 5 В рядом с модулем реле
- Что случится, если в switch забыть break?
- Case без break — провал: EQ потушит ночник и щёлкнет реле заодно. Дефект тихий, без сообщений компилятора, и потому самый противный. Выполнение провалится в следующий case и исполнит лишние действия
- Почему вход реле-модуля безопасно вешать прямо на пин Arduino?
- У модуля входная цепь развязана оптопарой: пин отдаёт пару миллиампер на светодиод оптопары, а катушку модуль тянет от своего питания. Вход через оптопару: с пина берутся считанные миллиамперы