Датчики дождя и концевики ворот стояли на крыше, индикатор и реле — в комнате. Пять сигнальных проводов через этаж я тянуть отказался на этапе подсчёта: витая пара даёт мне четыре линии и запас. Оставшиеся две — SDA и SCL — это готовая шина I2C между двумя платами, и всё свелось к тому, чтобы платы договорились о ролях. Первая пара скетчей молчала вежливо и полчаса: на обе платы я залил мастер, и шина собралась из двух командиров без единого солдата.
Разбор двух плат уже лежит в соседних уроках: полный обмен Uno с Uno — в уроке про обмен двух Arduino и уроке про мастер-слейв, анатомия обработчиков — в уроке про onReceive и onRequest, тонкости ответа — в уроке про onRequest. Здесь мой угол — минимальный договор: один байт команды, два байта ответа, фиксированный порядок байтов и три провода вместо пяти.
Договор: 0x08, два байта, старший вперёд
Роли делятся жёстко. Мастер вызывает Wire.begin() без аргументов: он единственный заводит каждый разговор. Слейв вызывает Wire.begin(8): восьмёрка — его адрес на шине, я беру 0x08 для первого слейва из предсказуемости — адреса ниже 0x08 зарезервированы, и 0x08 самый чистый из свободных. У слейва два обработчика: onReceive срабатывает, когда мастер прислал данные, onRequest — когда мастер запросил ответ; оба библиотека вызывает сама, между вашими проходами loop.
Договор о данных — та часть, на которой я обжигался дважды. Значение больше 255 (у меня — литры бочки, умноженные на десять) едут двумя байтами, и кто из них старший — решаете вы, потому что Wire про ваши структуры не знает. Я фиксирую старший вперёд и в мастере собираю обратно тем же порядком: (старший << 8) | младший. Смена любой стороны договора — это правка обеих плат в один день, иначе получите 4210 вместо 114 и будете искать брак в датчиках. Последняя проверка — requestFrom возвращает число принятых байтов: ноль значит «слейв молчит», и это отдельная ветка диагностики, а не ноль в данных.
// ===== ПЛАТА 1: МАСТЕР (в комнате) =====#include <Wire.h>#define SLAVE 0x08 void setup() { Wire.begin(); Serial.begin(9600);} void loop() { Wire.beginTransmission(SLAVE); Wire.write(0x01); // команда 01: "дай состояние" Wire.endTransmission(); delay(50); // слейву время на подготовку byte n = Wire.requestFrom(SLAVE, 2); // просим два байта if (n == 2) { int v = (Wire.read() << 8) | Wire.read(); // старший вперёд Serial.print("Sostoyanie: "); Serial.println(v); } else { Serial.println("Sleyv molchit!"); // n = 0 - проверьте провода } delay(1000);} // ===== ПЛАТА 2: СЛЕЙВ (на крыше) =====#include <Wire.h>#define ADDR 0x08 volatile int sostoyanie = 4210; // литры бочки * 10 void gotCommand(int howMany) { // мастер прислал данные while (Wire.available()) Wire.read(); // команды пока вычитываем} void giveData() { // мастер просит ответ Wire.write(highByte(sostoyanie)); // старший Wire.write(lowByte(sostoyanie)); // младший} void setup() { Wire.begin(ADDR); // 8 - адрес этого слейва Wire.onReceive(gotCommand); Wire.onRequest(giveData);} void loop() { // здесь датчики крыши обновляют sostoyanie}Разбор мастеровой стороны. Цикл двухходовый: сначала beginTransmission/write/endTransmission кладёт команду в адрес слейва, пауза 50 мс даёт слейву пересчитать состояние, затем requestFrom забирает два байта. Проверка n == 2 — единственная защита от тишины: оборванный провод, чужой адрес, незалитый скетч — всё это выглядит как ноль принятых байтов, и лучше увидеть «Sleyv molchit», чем ноль в показаниях.
Слейвовая сторона проще, чем кажется. gotCommand получает число присланных байтов и обязана буфер вычистить — иначе следующий запрос начнётся с чужих остатков. giveData пишется только в момент запроса: Wire.write здесь не отправляет ничего сразу, байты лежат до стоп-условия. И третья ножка, про которую забывают чаще всего: между платами обязана быть общая земля — SDA и SCL без GND не работают, потому что «земле» обеих плат не о чем договориться. Три провода: SDA, SCL, GND — и подтяжки уже стоят на платах Uno.
| Событие | Инициатор | У слейва | У мастера |
|---|---|---|---|
| Команда (запись) | beginTransmission → write → endTransmission | onReceive(gotCommand) | шлёт байты |
| Ответ (чтение) | requestFrom(0x08, 2) | onRequest(giveData) | читает Wire.read() |
| Слейв молчит | requestFrom | — | n == 0: проверять провод и адрес |
- 1. Чем на слейве отличается Wire.begin(8) от Wire.begin()?
- 2. requestFrom вернул 0. Что это значит?
- Роли жёсткие: мастер командует, слейв отвечает в двух событиях onReceive/onRequest
- Слейв: Wire.begin(8) + обработчики; мастер: Wire.begin() без адреса
- Значения больше 255 едут двумя байтами; порядок (старший вперёд) фиксируете сами
- requestFrom возвращает число байтов: ноль — слейв молчит, это ветка диагностики
- Общая земля обязательна: SDA и SCL без GND между платами не работают
Частые вопросы по теме урока
- Чем на слейве отличается Wire.begin(8) от Wire.begin()?
- Адрес в begin переводит плату в слейв-режим: она перестаёт инициировать передачи и отвечает на свой адрес обработчиками. begin() без аргумента — режим мастера. begin(8) объявляет плату слейвом с адресом 8; begin() без адреса — плату мастером
- requestFrom вернул 0. Что это значит?
- Возврат requestFrom — число реально принятых байтов. Ноль — тишина на адресе: обрыв, не тот адрес, незалитый слейв-скетч или нет общей земли. Ни один байт не принят: адрес не отвечает, проверьте проводку и адрес