Будильник на реле, который я повесил под потолок прихожей, неделю изображал плохой приёмник. Пульт направлен, кнопки щёлкаются, а реле щёлкнет раз через три. Я спускался, подключал ноутбук, смотрел в Serial — всё декодировалось идеально, — закрывал крышку, поднимал будильник на место, и проблема возвращалась. Третьего дня я понял очевидное: между «принял кадр» и «исполнил команду» лежит пропасть, а я смотрю в неё через ноутбук на стремянке. Плата увидела кадр, напечатала cmd=21 в монитор, который никто не читал, — и тишина: 21 в её таблице не было, потому что я вписал коды с другого пульта.
Лечение — экран прямо на приборе. Крошечный LCD 16x2, подключённый по I2C, показывает каждую принятую кнопку её именем, а в отладочном режиме — код и счётчик кадров. Стали видны вещи, которые Serial на столе не показывал никогда: пустые кадры от лампы, чужие команды от пульта соседа, повторы, которых я не нажимал. Таблицу кодов, если у вас её ещё нет, снимите читалкой из урока про приём кодов — без неё именам неоткуда взяться.
В этом уроке соберём связку «приёмник плюс экран»: карта кодов переводит байт в имя, режим отладки сыплет техническую правду, а экран обновляется только когда что-то сменилось. Ничего из этого не требует новых библиотек: LiquidCrystal_I2C из урока про первый текст и IRremote, который вы уже ставили.
Раскладка железа выходит на редкость дешёвой. Экран берёт четыре провода: 5 вольт, земля, A4 (SDA) и A5 (SCL); приёмнику нужен свой цифровой пин, у меня 11-й, плюс те же питание и земля. По току связка смешная: подсветка LCD съедает примерно 20–25 мА, приёмник в покое — доли миллиампера, и только в момент приёма подрастает к полутора. У меня эта пара месяцами живёт на плате, питающейся от зарядки от телефона, и ни разу не намекнула на нехватку питания.
Карта кодов: от байта к имени
Карта — два параллельных массива: codes[] хранит команды, names[] — имена тех же ячеек. Функция nameOf() перебирает codes и возвращает имя, а если байт незнакомый — знак вопроса. Перебор из восьми сравнений занимает микросекунды и не стоит ни байта динамической памяти сверх самих строк; когда список вырастет за пару десятков, заменю его на switch, но до этого момента простота перевешивает. Отдельно скажу про знак вопроса: он не украшение, а диагноз. Кнопка, которая напечаталась вопросом, — это либо чужой пульт, либо код из необновлённой таблицы, и видно это по экрану мгновенно.
Печать кода я дублирую в hex — привычка из урока про bin, hex и dec: таблицы из даташитов и чужих статей живут в шестнадцатеричной системе, и сверять 0x0D на порядок быстрее, чем 13. В отладочной строке оба мира умещаются рядом, а глаз выбирает сам.
Имена на экране живут латиницей, и это не мой каприз: знакогенератор контроллера HD44780 хранит латиницу и японскую азбуку, а кириллицы в ней нет — её рисуют своими символами через createChar или берут экран с русской ROM. Есть и второй предел: ширина строки. На «Knopka: » уходит восемь из шестнадцати столбцов, на имя остаётся восемь: VOL+ и HASH влезают легко, а придумывать длинные названия вроде POWER пунктов не стоит — хвост не уместится, даже с затиранием пробелами. Моё правило: имя пункта — четыре-шесть символов, всё остальное — роскошь.
#include <Wire.h>#include <LiquidCrystal_I2C.h>#include <IRremote.hpp> LiquidCrystal_I2C lcd(0x27, 16, 2);const int IR_PIN = 11; String names[8] = {"OK", "STAR", "HASH", "EQ", "VOL+", "VOL-", "CH-", "CH+"};int codes[8] = {67, 68, 66, 9, 21, 7, 70, 69}; bool debugMode = false;int frames = 0; // сколько кадров принято всегоint lastCmd = -1; // что сейчас показано на экране String nameOf(int cmd) { for (int i = 0; i < 8; i++) { if (codes[i] == cmd) return names[i]; } return "?";} void setup() { Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print("Zhdem knopku");} void loop() { if (IrReceiver.decode()) { int cmd = IrReceiver.decodedIRData.command; frames++; if (cmd == 66) { // HASH: переключить режим отладки debugMode = !debugMode; lcd.clear(); lcd.setCursor(0, 0); if (debugMode) { lcd.print("Debug: on"); } else { lcd.print("Zhdem knopku"); } lastCmd = -1; // заставим перерисовать имя в следующий раз } else { if (cmd != lastCmd) { // печатаем только то, что сменилось lastCmd = cmd; lcd.setCursor(0, 0); lcd.print("Knopka: "); lcd.print(nameOf(cmd)); lcd.print(" "); // хвост затирает длинные имена } if (debugMode) { lcd.setCursor(0, 1); lcd.print("cmd="); lcd.print(cmd); lcd.print(" n="); lcd.print(frames); lcd.print(" "); } } Serial.print("cmd="); Serial.println(cmd); IrReceiver.resume(); }}Главный приём скетча — сравнение cmd != lastCmd перед печатью. Без него экран перерисовывал бы одно и то же по девять раз в секунду при удержании, а с ним шина I2C дышит ровно: движение — перерисовали, тишина — молчим. Вторую строку я обновляю всегда, когда включена отладка: счётчик кадров меняется с каждым кадром, и его печатать дёшево. Хвосты из пробелов после имени и счётчика затирают то, что было напечатано раньше, — техника из урока про меню и курсор, там про мерцание и затирание рассказано со всеми подробностями.
Кнопка HASH (решётка, код 66) у меня назначена переключателем отладки, и она сознательно не попадает в список отображаемых: служебная кнопка не должна пачкать эхо. Заметьте и lastCmd = -1 после переключения: без него после выключения отладки на экране осталось бы старое имя, а скетч был бы уверен, что печатать нечего.
Про цену этого эконома. Строка в шестнадцать символов по шине на 100 кГц передаётся за единицы миллисекунд — байт данных занимает около 90 микросекунд плюс служебные посылки, поэтому печать в каждом проходе loop() издержки размазывает незаметно, но стабильно. Сравнение двух int занимает доли микросекунды. А сам счётчик — int, на Uno он шестнадцатибитный и переполнится после 32767 кадров: при девяти кадрах в секунду непрерывного удержания это час безостановочного жатия одной кнопки. Для отладочного счётчика запас за глаза; если счётчик живёт в приборе годами, будьте готовы переключиться на unsigned long.
| Режим экрана | Строка 0 | Строка 1 |
|---|---|---|
| Ожидание после включения | Zhdem knopku | пусто |
| Эхо кнопки | Knopka: VOL+ | пусто, пока отладка выключена |
| Отладка (кнопка HASH) | Knopka: VOL+ | cmd=21 n=132: код и счётчик кадров |
| Незнакомый код | Knopka: ? | в отладке покажет байт, которого нет в карте |
Отладка на расстоянии вытянутой руки
Счётчик n= в отладочной строке — неожиданно честный детектив. Нажали VOL+ один раз, а n= подрос на пять? Значит, вы держали кнопку дольше, чем кажется, и дошли повторы — анатомия decodedIRData объясняет, откуда они берутся и как их отличить по флагу IS_REPEAT. n= растёт без кнопок вообще — светите лампой в приёмник или ловите чужой пульт: кадры-призраки видны на экране раньше, чем прибор начнёт от них дёргаться. Именно так я узнал, что моя прихожая по вечерам заливается ИК-шумом от диммера.
Отладочный режим я больше не выключаю в финальных прошивках: он не мешает (второй строкой), стоит ноль процессорного времени между кадрами и экономит стремянку с ноутбуком. Моё правило: если прибор общается с человеком через пульт, у него должна быть кнопка правды, которая покажет, что именно он услышал.
Тот же приём переносится на любой экранчик: если вместо LCD у вас OLED на SSD1306, карту кодов и имя кнопки выдают те же nameOf() и сравнение по смене, меняется только API печати — clearDisplay, setCursor, print и обязательный display.display() в конце. В симуляторе поддерживаются оба, так что тренироваться можно на любом. Разница одна: OLED не боится частой перерисовки, но бережная печать по смене и там не помешает.
- Карта кодов — два параллельных массива codes[] и names[] плюс nameOf(): незнакомый байт честно печатается знаком вопроса
- Обновляйте экран только при смене команды: cmd != lastCmd избавляет и от мерцания, и от лишнего трафика на шине
- Хвост из пробелов после печати затирает прежние имена — без него более длинное название оставляет на экране мусорные буквы
- Отладочный режим показывает cmd= и счётчик кадров n=: повторы, чужие пульты и помехи от ламп видны сразу
- Приёмник и LCD не делят ничего, кроме платы: приёмник на обычном пине, экран на I2C с адресом 0x27 или 0x3F
- 1. Приёмник висит на пине 11, LCD — на A4 и A5. Будут ли они мешать друг другу?
- 2. Пульт шлёт команду 8, которой нет в карте кодов. Что покажет nameOf()?
- 3. Зачем в скетче сравнение cmd != lastCmd перед печатью имени?
- 4. Вы нажали VOL+ один раз, а счётчик n= в отладке подрос на пять. Что произошло?
- 5. Экран остаётся пустым, приёмник заведомо жив. С чего начать?
Теперь у пультового прибора есть и голос, и уши: меню из предыдущего урока листается по экрану, а экран-эхо из этого — показывает, что приёмник слышит. Склейте оба скетча, добавьте свою таблицу кодов, и получится законченный прибор, которым управляют с дивана и который отчитывается сам за себя. Дальше по разделу можно пойти в передачу: в уроке про другие протоколы упоминался IrSender и sendRaw — плата уже не только слушает пульты, но и сама становится пультом.
Частые вопросы по теме урока
- Приёмник висит на пине 11, LCD — на A4 и A5. Будут ли они мешать друг другу?
- VS1838B выдаёт готовые импульсы на обычный цифровой пин, шина I2C ему не нужна. Экран и приёмник делят только питание и землю. Нет: приёмнику шина I2C не нужна, он общается обычным цифровым пином
- Пульт шлёт команду 8, которой нет в карте кодов. Что покажет nameOf()?
- Перебор не нашёл совпадения, и nameOf вернула знак вопроса. По экрану это видно мгновенно: чужой пульт или таблица кодов не обновлена. Знак вопроса — честный признак незнакомого байта
- Зачем в скетче сравнение cmd != lastCmd перед печатью имени?
- Удержание шлёт до девяти кадров в секунду с одной и той же командой. Печать по изменению оставляет шину свободной, а глаз — без стробоскопа. Чтобы экран не перерисовывал одно и то же при повторах и не мерцал
- Вы нажали VOL+ один раз, а счётчик n= в отладке подрос на пять. Что произошло?
- Удержание — это кадры-повторы каждые 108–110 мс с той же командой. По флагу IS_REPEAT их отличают от новых нажатий, а по счётчику — просто замечают. Кнопку держали дольше, чем кажется, и дошли повторы NEC
- Экран остаётся пустым, приёмник заведомо жив. С чего начать?
- У переходников на PCF8574 адрес 0x27, у версии A — 0x3F, внешне модули одинаковы. Сканер шины покажет фактический адрес за две минуты. С проверки адреса: попробовать 0x3F вместо 0x27 или пройтись сканером шины I2C