Панель для мастерской я задумал простую: две строки на LCD — часы и статус, журнал последних событий в энергонезависимой памяти, кнопки для листания. Первая версия жила на delay(1000): раз в секунду скетч брался по очереди за каждое устройство — читал время, перерисовывал экран целиком, проверял журнал. Пока он писал в EEPROM, кнопки не опрашивались: нажатие между проходами пропадало. Экран мигал от полной перерисовки каждую секунду. Шина на глаз работала усердно, а пользы было мало. Вторая версия началась не с кода, а со сканера: сначала карта адресов, потом ритмы, и только затем скетч.
Это финальный урок раздела, и соберём мы в нём всё сразу: сканирование, регистры, подтяжки, таймауты и планировщик. Четыре жильца на двух проводах — экран через PCF8574, часы, память и кнопки — будут жить по разным расписаниям и не мешать друг другу.
Шаг первый: карта адресов
Любая большая шина начинается со сканера из урока про поиск устройств — он печатает карту жильцов за минуту. Моя карта выглядит так: 0x27 — LCD1602 через переходник PCF8574, подключение разбирали в уроке про первый текст на I2C-экране; 0x68 — часы DS3231, их навыки мы раскладывали в уроке про время на DS3231; 0x57 — EEPROM 24C32. Хитрость последнего адреса: память живёт на том же модуле, что и часы, — на типовой платке часов два чипа, и сканер честно показывает оба. Кнопочная матрица 4 на 4 поедет на прямых пинах D2-D9: ноль шинного трафика, сканирование в loop; если пины дороги, второй PCF8574 сядет на 0x20, и карта пополнится пятым жильцом.
Схема складывается в одну гребёнку: SDA к SDA, SCL к SCL, общий минус — и тут же первый подводный камень. На каждом готовом модуле стоят свои бортовые подтяжки 4,7 кОм, и четыре модуля параллельно дают около 1,2 кОм на линию: ток через подтяжку при прижатой шине подбирается к четырём миллиамперам, фронты становятся резкими до звона на длинных проводах. Моё правило: до трёх модулей живём как есть, дальше отпаиваю подтяжки у всех, кроме одного, — пара минут паяльником против месяцев странных сбоев на границе кадров.
Памятной части стоит посвятить пару минут до кода. 24C32 — тридцать два килобита, четыре килобайта, и адресное слово у неё двухбайтное: старший байт, младший байт, и только потом данные. Привычка от маленьких 24C02 с одним адресным байтом тут дорого стоит: без старшего байта запись уедет в первые 256 байтов, что бы вы ни задумали. Журнал я держу кольцом: по четыре байта на метку, шестнадцать записей на первые шестьдесят четыре байта — курсор дошёл до конца, пошёл с нуля. Для панели в мастерской хватает с запасом: события редкие, а переполнение кольца затирает самое старое, что и требуется журналу.
| Жилец | Адрес | Ритм обращений | Цена одного визита |
|---|---|---|---|
| LCD1602 через PCF8574 | 0x27 | только при изменении строки | 2-8 байт при перерисовке |
| Часы DS3231 | 0x68 | раз в секунду | около 2 мс, 7-8 байт чтения |
| EEPROM 24C32 | 0x57 | по событию: запись метки | 3-4 байта записи |
| Кнопки 4x4 на пинах | без адреса, D2-D9 | сканирование в loop | ноль кадров на шине |
Планировщик на millis: у каждого свой ритм
Секрет спокойной шины — разные периоды. Часы читаются раз в секунду по millis-таймеру; журнал пишется только по событию — нажатию кнопки; экран обновляется только при изменении строки: новую строку сравниваем со старой и трогаем шину, если они различаются. Каркас многозадачного цикла без единого delay собирается по рецепту урока про планировщик задач. Прикинем занятость: чтение времени — около двух миллисекунд раз в секунду, это пятая часть процента; перерисовка экрана — единицы кадров в минуту; EEPROM — по пальцам событий. Шина простаивает девяносто девять с половиной процентов времени, и в этот запас помещается ещё десяток жильцов.
Кнопкам свой ритм тоже нужен, но шинный не нужен вовсе: матрица сканируется в каждом проходе loop, раз в десять-двадцать миллисекунд — быстрее палец не двигается. Отсюда приятное следствие: нажатия не пропадают, потому что между ними нет слепых окон на полсекунды, как было в delay-версии. Единственный кандидат на шину из этого списка — только сами события.
Второй секрет — честная проверка здоровья. Раз в десять секунд скетч пингует 0x57 и 0x68 пустым пакетом: ноль — все дома, двойка — кто-то отвалился, и печать вердикта в Serial спасает от часовых поисков отвисшего шлейфа. Страховка от глухого зависания — таймаут из урока про зависание шины: на большой шине я считаю его обязательным, как подтяжки.
Формат записи в журнал стоит придумать до скетча, а не по ходу. Мой: четыре байта — клавиша, час, минута и младший байт счётчика записей; по ним отвечает на вопрос «что нажимали после обеда» без единого разбора на бумажке. Четыре байта при кольце из шестнадцати записей занимают шестьдесят четыре байта из четырёх тысяч — даже если писать по десять событий в день, память исчерпается через год, и это при том, что кольцо затирает старое само. Развёл бы я страницы и блоки по 32 байта ради экономии ресурса? При паре записей в день — нет: износ ячейки EEPROM исчисляется сотнями тысяч циклов, и журнал в мастерской не доживёт до половины.
#include <Wire.h>#include <LiquidCrystal_I2C.h>#include <RTClib.h> // LCD 0x27 + часы 0x68 + EEPROM 24C32 0x57 + клавиатура на D2-D9 LiquidCrystal_I2C lcd(0x27, 16, 2);RTC_DS3231 chasy; const byte ADR_EE = 0x57;const int STROKI[4] = {2, 3, 4, 5}; // строки матрицыconst int STOLBY[4] = {6, 7, 8, 9}; // столбцы матрицыconst char KLAVISHI[16] = {'1','2','3','A','4','5','6','B', '7','8','9','C','*','0','#','D'}; unsigned long tChasy = 0; // таймер чтения времениunsigned long tPing = 0; // таймер пинга здоровьяint derzhim = -1; // зажатая клавиша, -1 — нетint sledAdres = 0; // курсор кольцевого журнала (по 4 байта)unsigned long zapisej = 0;String strokaVremya = "";String strokaSobytie = ""; int opros() { // индекс клавиши или -1 for (int r = 0; r < 4; r++) { digitalWrite(STROKI[r], HIGH); for (int c = 0; c < 4; c++) { if (digitalRead(STOLBY[c]) == LOW) { digitalWrite(STROKI[r], LOW); return r * 4 + c; } } digitalWrite(STROKI[r], LOW); } return -1;} bool eeZapis(byte adrh, byte adrl, byte znach) { Wire.beginTransmission(ADR_EE); Wire.write(adrh); Wire.write(adrl); // у 24C32 адресное слово ДВУХбайтное Wire.write(znach); if (Wire.endTransmission() != 0) { // 3 = чип «варит» прошлую ячейку delay(5); // пауза и один повтор Wire.beginTransmission(ADR_EE); Wire.write(adrh); Wire.write(adrl); Wire.write(znach); Wire.endTransmission(); } return true;} void sobytie(char k) { // нажатие -> запись в журнал DateTime t = chasy.now(); eeZapis(sledAdres / 256, sledAdres % 256, k); sledAdres += 4; if (sledAdres >= 64) sledAdres = 0; // кольцо на 16 записей zapisej++; strokaSobytie = "posl: "; strokaSobytie += k; Serial.print("zap="); Serial.print(zapisej); Serial.print(" ch="); Serial.println(t.hour());} void ekranObnovit() { // трогаем шину только при изменении static String bylo1 = "", bylo2 = ""; if (strokaVremya != bylo1) { lcd.setCursor(0, 0); lcd.print(strokaVremya + " "); // пробелы затирают хвост прошлой строки bylo1 = strokaVremya; } if (strokaSobytie != bylo2) { lcd.setCursor(0, 1); lcd.print(strokaSobytie + " "); bylo2 = strokaSobytie; }} void setup() { Wire.begin(); lcd.init(); lcd.backlight(); chasy.begin(); for (int r = 0; r < 4; r++) pinMode(STROKI[r], OUTPUT); for (int c = 0; c < 4; c++) pinMode(STOLBY[c], INPUT_PULLUP); Serial.begin(9600); Serial.println("Panel: 0x27 0x68 0x57 + klaviatura"); strokaVremya = "--:--:--"; strokaSobytie = "gotova";} void loop() { int i = opros(); // кнопки — каждый проход if (i >= 0 && i != derzhim) { derzhim = i; sobytie(KLAVISHI[i]); } if (i < 0) derzhim = -1; if (millis() - tChasy >= 1000) { // часы — раз в секунду tChasy = millis(); DateTime t = chasy.now(); strokaVremya = String(t.hour()) + ":" + String(t.minute()) + ":" + String(t.second()); } if (millis() - tPing >= 10000) { // здоровье шины — раз в 10 с tPing = millis(); Wire.beginTransmission(ADR_EE); if (Wire.endTransmission() == 0) Serial.println("ping EE: 0"); else Serial.println("ping EE: propal"); } ekranObnovit(); // экран — только при изменении}Про честность в симуляторе: LCD и часы показаны их виджетами — это движковые классы, их трафик панель I2C не рисует. EEPROM-часть — журналом: каждый визит на 0x57 виден в панели как START, адресное слово, байт, STOP, а Serial ведёт счёт записей. Заглушка ACK-ает всё и отдаёт по 42 в байте, поэтому чтение при старте вернёт 42 42, а код endTransmission всегда ноль — на живой плате там будут и двойка от отвалившегося модуля, и тройка от занятой памяти. Клавиатура — честная: строки на D2-D5, столбцы на D6-D9, INPUT_PULLUP и сканирование в loop, ноль кадров на шине.
Кольцевой журнал я не выдумал для этого урока — он переехал целиком из урока про журнал событий в EEPROM, где обсуждался износ и размер записей. Пересчёт прост: при паре событий в день даже шестнадцать меток крутятся полторы недели, а четырёх килобайт настоящей 24C32 хватит на годы. И заметьте ритм экрана: верхняя строка перерисовывается раз в секунду вместе с часами, нижняя — только после нажатия. Ни одного кадра на шине между этими моментами — вот что показывает пустая панель лучше любых слов. Попробуйте на макетке три вещи подряд: нажать цифру и найти в панели кадры на 0x57, зажать клавишу надолго и убедиться, что запись одна — за фронтом следит защита от удержания, и просто смотреть на пустую панель между событиями: тишина на шине тут не поломка, а показатель.
Эксперименты и куда дальше
Первое: переведите кнопки на второй PCF8574 по адресу 0x20 — карта адресов вырастет до пяти жильцов, а его ножку INT можно повесить на пин 3 и ловить нажатия прерыванием из предыдущего урока. Второе: выньте на живой плате питание часов — пинг раз в десять секунд напечатает пропажу, и скетч продолжит работать без них: отказ одного жильца не кладёт шину целиком. Третье: добавьте четвёртого кандидата — датчик температуры внутри DS3231 читается из регистров 0x11 и 0x12 раз в минуту, дешевле не бывает. Четвёртое: поднимите частоту шины до 400 кГц и убедитесь, что PCF8574 начинает рисовать кракозябры, — живое напоминание, что потолок задаёт самый медленный жилец. Пятое: ведите счёт кадров шины за минуту и сравните delay-версию панели с планировщиком — разницу в десятки раз приятнее увидеть в цифрах, чем услышать от меня.
Куда дальше: если проект разрастётся датчиками погоды, тот же каркас планировщика у меня перерос в метеостанцию на одной шине — там к жильцам добавляются датчики с автопроверкой подписи. И последнее: журнал в EEPROM стоит ограничить кольцом не по одному байту, а по записи целиком — иначе половина метки окажется от старой.
- Большая шина начинается со сканера: карта адресов 0x27, 0x68, 0x57 печатается за минуту и снимает половину вопросов
- У каждого жильца свой ритм: часы раз в секунду, EEPROM по событию, экран при изменении строки — шина занята доли процента
- Кнопки можно не сажать на шину: матрица на D2-D9 сканируется в loop без единого кадра
- 24C32 требует двухбайтного адресного слова и паузы 5 мс после записи — код 3 это штатная занятость
- Пинг раз в десять секунд и таймаут превращают зависание из чёрного ящика в строку в журнале
- 1. Первое действие с новой большой шиной?
- 2. По какому адресу живёт EEPROM 24C32 на типовом модуле часов DS3231?
- 3. Как часто итоговый скетч читает время у DS3231?
- 4. Сразу после записи 24C32 отвечает кодом 3 на новую посылку. Это...
- 5. Что делает скетч, когда ничего не меняется и таймеры молчат?
Частые вопросы по теме урока
- Первое действие с новой большой шиной?
- Сканер печатает карту жильцов за минуту: без неё вы угадываете адреса, а не проектируете. прогнать сканер и составить карту адресов
- По какому адресу живёт EEPROM 24C32 на типовом модуле часов DS3231?
- На платке часов два чипа: DS3231 на 0x68 и память AT24C32 на 0x57 — сканер показывает оба. 0x57
- Как часто итоговый скетч читает время у DS3231?
- Часам достаточно секунды: один визит в 1000 мс занимает около двух миллисекунд — пятая часть процента шины. раз в секунду по millis-таймеру
- Сразу после записи 24C32 отвечает кодом 3 на новую посылку. Это...
- Во время внутренней записи чип ACK-ает адрес, но NACK-ает данные. Пять миллисекунд — и обмен идёт. штатная занятость: пауза 5 мс и повтор посылки
- Что делает скетч, когда ничего не меняется и таймеры молчат?
- Планировщик ничего не делает, пока не подошёл срок или не случилось событие, — в этом и смысл разных периодов. ждёт millis-таймеров и событий: шина пустует