Финальный проект раздела собирает всё, что мы разобрали: на двух проводах A4 и A5 едут экран 16×2, часы DS3231, датчик давления BMP280 и гироскоп MPU6050 — четыре устройства, ноль дополнительных пинов — сигнальных линий всего две. Звучит грозно, пока не вспомнишь, что каждый разговор на шине выглядит одинаково: адрес, регистр, байты. Я собирал такую станцию в зиму, когда раскладывал по комнате тепло от батареи и искал, где сквозит, — а потом она прижилась на балконе как метеопост и заодно стала стендом: каждый новый I2C-модуль сначала обкатывается на ней.
План урока: сначала расчёт шины — адреса, подтяжки и конфликт, который всплывёт обязательно, — затем архитектура скетча с диагностикой при старте, полный скетч для живой платы и прогон в симуляторе, где настоящими будут шина, часы и экран, а датчики сыграют заглушки. Проект сознательно скучный по железу: вся ценность — в дисциплине запуска, которая переносится на десять устройств так же, как на четыре. По бюджету времени честно: сборка на макетке — час, первый запуск — вечер, потому что половина этого вечера уйдёт не на код, а на перепутанный провод и пересчёт подтяжек — и это нормально.
План шины: адреса, подтяжки и один конфликт
Начинаю с адресной книги — карты шины. Экран на PCF8574 живёт на 0x27 или 0x3F, если партия с PCF8574A. Часы DS3231 — 0x68. BMP280 — 0x76 при SDO на земле. MPU6050 — 0x68... стоп: часы и гироскоп просятся на один адрес, это тот самый классический конфликт из урока про разведение конфликтов. Решение на один провод: ножку ADO гироскопа притянуть к питанию — и модуль переезжает на 0x69. Ни кода, ни мультиплексора, один провод к рельсу 5 В — и в карте шины снова порядок. Адреса после сборки я всегда перепроверяю сканером: паспорт — паспорт, а модуль из другой партии имеет право удивить; запись в карту шины делается только по факту отклика.
Второй расчёт — подтяжки; на четырёх модулях он уже не теория. На моих платах резисторы 4,7 кОм распаяны на борту у экрана, часов и BMP280, а MPU6050 в этой партии пришёл без подтяжек. Считаем параллельное соединение: три резистора по 4,7 кОм дают около 1,57 кОм — а это уже на нижней границе для 5 В: ток прижима при нуле на линии выходит 5 В / 1,57 кОм ≈ 3,2 мА при допустимых спецификацией трёх. Я выпаял подтяжку из переходника экрана — сумма встала на спокойные 2,35 кОм. Мораль: перед четвёртым модулем посчитайте, сколько резисторов уже висит на шине; физика подробно разобрана в уроке про номиналы подтяжек.
| Устройство | Адрес | Что делает на шине | Как часто |
|---|---|---|---|
| LCD1602 на PCF8574 | 0x27 | пишем время и показания | раз в секунду |
| Часы DS3231 | 0x68 | читаем время и термометр чипа | раз в секунду |
| Датчик BMP280 | 0x76 | читаем сырой код температуры | раз в пять секунд |
| Гироскоп MPU6050 | 0x69 (ADO к питанию) | читаем температуру кристалла | раз в пять секунд |
Архитектура скетча — три слоя, и порядок именно такой. Слой диагностики в setup(): пинг каждого адреса через beginTransmission и endTransmission, печать, кто ответил; пока единицы не сошлись, основной цикл не запускаю — искать глюк опроса на мёртвом модуле значит потерять вечер. Слой журнала: обёртки из урока про сниффер I2C печатают кадры с меткой millis; на отладке они включены, в постоянной работе приглушены, потому что строка в тридцать символов на 9600 бод — это 30 мс тишины в loop. Слой данных: планировщик на millis из урока про millis вместо delay опрашивает часы раз в секунду, а датчики — раз в пять, чтобы шина не молотила вхолостую. Опрос раз в пять секунд — не скупость: показания меняются медленно, а каждый лишний кадр — время и энергия.
Отдельная мелочь, которая экономит часы: адреса и регистры вынесены в константы вверху скетча. Пять строк const byte — и переезд на модуль другой партии (экран с 0x27 на 0x3F, датчик с 0x76 на 0x77) становится правкой одной строки, а не поиском по всему файлу. Внутри loop() адресов в явном виде нет — по-моему, главный признак того, что проект вырос из копипасты и готов к жизни. Время на экране и в Serial живёт по правилам урока про DS3231: батарейка, lostPower(), adjust один раз в setup.
#include <Wire.h>#include <LiquidCrystal_I2C.h>#include <RTClib.h> LiquidCrystal_I2C lcd(0x27, 16, 2);RTC_DS3231 rtc; const byte ADDR_BMP = 0x76; // SDO датчика на землеconst byte ADDR_MPU = 0x69; // ADO гироскопа притянута к питанию void i2cWrite(byte addr, byte reg, byte val) { Wire.beginTransmission(addr); Wire.write(reg); Wire.write(val); Wire.endTransmission();} long i2cReadN(byte addr, byte reg, byte n) { Wire.beginTransmission(addr); Wire.write(reg); Wire.endTransmission(); Wire.requestFrom(addr, n); long raw = 0; while (Wire.available()) { raw = (raw << 8) | Wire.read(); } return raw;} bool ping(byte addr) { Wire.beginTransmission(addr); return Wire.endTransmission() == 0;} void setup() { Serial.begin(9600); Wire.begin(); // диагностика шины до всякой работы bool bmpOk = ping(ADDR_BMP); bool mpuOk = ping(ADDR_MPU); bool rtcOk = rtc.begin(); Serial.print("bmp="); Serial.print(bmpOk); Serial.print(" mpu="); Serial.print(mpuOk); Serial.print(" rtc="); Serial.println(rtcOk); lcd.init(); lcd.backlight(); if (rtc.lostPower()) { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } i2cWrite(ADDR_BMP, 0xF4, 0x27); // BMP280: разовое измерение lcd.setCursor(0, 0); lcd.print("Meteo start");} void loop() { DateTime now = rtc.now(); // датчики читаем один раз за проход long tbmp = i2cReadN(ADDR_BMP, 0xFA, 3); // сырой код температуры BMP280 long tmpu = i2cReadN(ADDR_MPU, 0x41, 2); // сырой код температуры MPU6050 float trtc = rtc.getTemperature(); // термометр внутри DS3231 lcd.clear(); lcd.setCursor(0, 0); lcd.print(now.hour()); lcd.print(":"); lcd.print(now.minute()); lcd.print(" T="); lcd.print(trtc, 1); lcd.setCursor(0, 1); lcd.print("B="); lcd.print(tbmp); lcd.print(" M="); lcd.print(tmpu); Serial.print("t_rtc="); Serial.print(trtc, 1); Serial.print(" bmp_raw="); Serial.print(tbmp); Serial.print(" mpu_raw="); Serial.print(tmpu); Serial.print(" h="); Serial.print(now.hour()); Serial.print(" m="); Serial.println(now.minute()); delay(1000);}Разбор по узлам. Диагностика в setup() печатает по единице на каждый живой адрес: bmp=1 mpu=1 rtc=1 — три единицы, и можно работать; ноль подскажет, куда смотреть, ещё до основного цикла. Датчики читаются один раз за проход в переменные — я по неопытности читал их дважды: раз для экрана, второй раз для Serial, и половина трафика шины уходила в никуда. Сырые коды в этом скетче печатаются как есть: пересчёт в градусы и паскали — через калибровочные константы из урока про BMP280, а здесь мы прокладываем каркас.
Частота опроса — тоже дисциплина. Термометр DS3231 обновляет свои регистры раз в секунду, а BMP280 в разовом режиме при однократной точности делает измерение за считанные миллисекунды — около 6 мс по даташиту, — поэтому опрашивать чаще раза в секунду бессмысленно: цифра на экране не изменится, а шина будет крутиться вхолостую. При опросе раз в пять секунд занятость шины выходит около сотой доли процента: кадр меньше миллисекунды каждые пять секунд. Экран обновляется целиком через clear(): моргание при раз в секунду терпимо, а если хочется плавнее — перерисовывайте только изменившиеся позиции, как в уроке про первый текст на LCD.
Прогон в симуляторе
В симуляторе станция собирается тем же скетчем без include-строк: LiquidCrystal_I2C и RTC_DS3231 движок понимает, Wire-трафик идёт в панель I2C. Датчики — честная заглушка: на каждый запрошенный байт слейв отвечает 42 (0x2A), поэтому сырые коды фиксированные: три байта BMP280 дают 2763306, два байта MPU6050 — 10794. На живой плате вместо них — настоящие показания, и каркас скетча не меняется ни на строку. Часы тикают по-настоящему, на экране — время и термометр DS3231, а панель I2C показывает каждый кадр вашего скетча: пинги BMP280 и MPU6050 в setup, потом ежесекундные двухходовки к датчикам. Обратите внимание на честность движка: чтение времени через rtc.now() не рисуется в панели — объект часов встроен в симулятор и шину не занимает; в панели только ваш Wire-трафик из скетча.
Куда двигать проект дальше. Первое — показания по расписанию: перевести датчики с опроса раз в пять секунд на раз в минуту и завести дневник на EEPROM — счётчик включений и записей разобран в уроке про счётчик на EEPROM. Второе — второй экран: OLED на 0x3C встаёт на ту же шину без конфликтов, и крупные цифры давления видны с другого конца комнаты. Третье — батарейное питание: датчики в спящем режиме, будим командой только на время опроса; при 100 кГц кадр опроса занимает меньше миллисекунды, и шина остаётся свободна на 99 процентов времени даже с десятком устройств.
- Четыре устройства на двух проводах: 0x27 экран, 0x68 часы, 0x76 BMP280, 0x69 MPU6050 — конфликт 0x68 решается проводом ADO к питанию
- Перед сборкой посчитайте подтяжки: три бортовых резистора 4,7 кОм дают 1,57 кОм — на нижней границе для 5 В, лишние выпаяйте
- Скетч строится слоями: пинг-диагностика в setup(), журнал с метками millis на отладке, опрос по millis-таймеру в рабочем режиме
- Датчики читаются один раз за проход в переменные: двойное чтение ради экрана и Serial — трата шины впустую
- В симуляторе настоящие шина, часы и экран; датчики отвечают заглушкой 42 — каркас скетча на живой плате не меняется
- 1. Часы DS3231 и MPU6050 оба живут на 0x68. Как развести их без мультиплексора?
- 2. Три подтяжки по 4,7 кОм на одной шине. Чему равна суммарная?
- 3. Почему датчики в скетче читаются в переменные один раз за проход?
- 4. Что напечатает пинг-диагностика, если BMP280 не отвечает?
- 5. Что в симуляторе станции работает как настоящий слейв?
Частые вопросы по теме урока
- Часы DS3231 и MPU6050 оба живут на 0x68. Как развести их без мультиплексора?
- Ножка ADO переключает младший бит адреса MPU6050: при притяжке к питанию чип занимает 0x69 и соседи перестают мешать друг другу. притянуть ADO гироскопа к питанию — он переедет на 0x69
- Три подтяжки по 4,7 кОм на одной шине. Чему равна суммарная?
- Параллельное соединение: 4700 / 3 ≈ 1567 Ом — уже на нижней границе для 5 В, лишние резисторы стоит выпаять. около 1,57 кОм
- Почему датчики в скетче читаются в переменные один раз за проход?
- Каждое чтение — отдельная двухходовка с адресом и регистром; читая дважды ради экрана и Serial, вы тратите шину впустую. второе чтение занимает шину впустую
- Что напечатает пинг-диагностика, если BMP280 не отвечает?
- ping() возвращает результат endTransmission: ноль — устройство ответило, единица — нет; на экран печати идёт bmp=0. bmp=0
- Что в симуляторе станции работает как настоящий слейв?
- Часы и их термометр симулятор эмулирует честно; датчики на шине заменяет заглушка, отвечающая 42 на каждый байт. DS3231: время и термометр