ELECTRONICS

Библиотека Wire и шина I2C

Обновлено

Скачивать не нужно. Wire входит в пакет платы: для UNO, Nano и Mega это пакет Arduino AVR Boards, он ставится вместе со средой. В менеджере библиотек её нет — и искать там не надо.

Шина I2C: два провода, десятки устройств с разными адресами. Сканер адресов, чтение регистров, связь двух плат.

Wire — стандартная библиотека Arduino для шины I2C, по которой к плате подключают дисплеи, часы, гироскопы и датчики давления. Файл Wire.h отдельно не скачивают: он уже лежит в пакете платы. Файла i2c.h в Arduino нет — шиной I2C занимается именно Wire.

Не путайте с 1-Wire: несмотря на похожее имя, это другая шина с одним проводом данных, для датчиков DS18B20. Для неё нужна OneWire.

Напрямую с Wire пишут реже, чем через неё: библиотеки дисплеев и датчиков используют её внутри. Но без неё не обойтись в трёх случаях: найти адрес неизвестного модуля сканером, прочитать регистры чипа, для которого нет библиотеки, и связать две платы между собой.

Как скачать и установить

Ничего ставить не нужно: достаточно строки подключения в начале скетча. Если среда пишет Wire.h: No such file or directory, проверьте, какая плата выбрана в меню «Инструменты»: у некоторых пакетов плат этой библиотеки нет или она называется иначе.

Строка подключения:

#include <Wire.h>

Создание объекта — обычно до setup():

Wire.begin();             // в setup(); объект Wire уже создан библиотекой
Wire.setClock(400000);    // необязательно: 400 кГц вместо 100 кГц, если все устройства умеют

Шина I2C и выводы на платах

I2C — два провода на все устройства сразу: данные SDA и такт SCL, плюс общая земля. Каждое устройство слушает свой адрес — семибитное число, обычно его пишут шестнадцатеричным: 0x27 у переходника дисплея 1602, 0x68 у часов DS3231 и гироскопа MPU6050, 0x3C у OLED. Плата-ведущий обращается к ним по очереди.

Линиям нужна подтяжка к питанию — резисторы 4,7 кОм. На модулях она почти всегда уже стоит: на схеме ниже это два резистора с маркировкой 472 на плате часов. Своих ставить не надо, пока модулей на шине два-три и провода короткие.

Схема подключения часов DS3231 к Arduino UNO по шине I2C: SDA к A4, SCL к A5, VCC к 5 В, GND к GND
Схема с пояснением к каждому проводу — на странице модуля «Часы DS3231».
Arduino UNO
SDA: A4SCL: A5

Те же линии выведены отдельными гнёздами SDA и SCL рядом с AREF.

Arduino Nano
SDA: A4SCL: A5

Как у UNO.

Arduino Pro Mini
SDA: A4SCL: A5

На массовом клоне — отверстия внутри платы, над A2 и A3.

Arduino Mega
SDA: 20SCL: 21

Не A4 и A5, как у UNO. Дублируются гнёздами SDA и SCL у AREF.

Leonardo, Micro
SDA: 2SCL: 3

Выводы 2 и 3 заняты шиной — прерывания на них с I2C не совместить.

ESP8266, Wemos D1 mini
SDA: D2 (GPIO4)SCL: D1 (GPIO5)

Можно назначить другие: Wire.begin(sda, scl).

ESP32
SDA: GPIO21SCL: GPIO22

Любые другие — Wire.begin(sda, scl).

ESP32-S3, ESP32-C3
SDA: GPIO8SCL: GPIO9

Так по умолчанию в ядре. На C3 SuperMini на GPIO8 встроенный светодиод, а GPIO9 — кнопка BOOT, поэтому шину удобнее перенести: Wire.begin(sda, scl).

Встроенная подтяжка — только на коротких проводах

Wire.begin() на AVR включает подтяжку внутри микроконтроллера, но она слабая, десятки килоом. С модулем, на котором резисторов нет, шина работает на проводах в пару сантиметров и сбоит на макетке. Тогда ставят свои 4,7 кОм от SDA и SCL к питанию.

Модуль на 3,3 В и плата на 5 В

Если у модуля свой стабилизатор и подтяжка к 3,3 В, с UNO он обычно работает напрямую. Голый чип на 3,3 В без стабилизатора подключают через преобразователь уровней: встроенная подтяжка UNO тянет линии к 5 В.

Провода — десятки сантиметров, не метры

I2C придумана для связи внутри одной платы. На длинных проводах растёт ёмкость, фронты заваливаются, и обмен сбоит. Помогают частота 100 кГц, подтяжка посильнее — 2,2 кОм — и витая пара; для метров есть удлинители шины.

Методы

Только то, что нужно в обычном скетче. Сигнатуры сверены по заголовкам: ядро AVR 1.8.8.

Запуск

void begin()

Включить шину ведущим устройством — так работают почти все скетчи. Вызывают в setup() один раз, до любого обмена. На AVR заодно включает встроенную подтяжку на SDA и SCL.

Wire.begin();

void begin(uint8_t address)

Включить плату ведомым устройством со своим адресом — когда она сама отвечает другой плате.

address
адрес 8–119: младшие и старшие адреса зарезервированы стандартом.
Wire.begin(8);

void end()

Выключить шину и отпустить выводы. Нужно редко — например, чтобы перед сном платы снять подтяжку и не тратить на неё ток.

Wire.end();

void setClock(uint32_t clock)

Частота шины. Звать после begin(). Быстрее — только если все устройства на шине её поддерживают, а провода короткие.

clock
100000 — по умолчанию, 400000 — быстрый режим; большинство дисплеев и датчиков его умеют.
Wire.setClock(400000);

Передача

void beginTransmission(uint8_t address)

Начать посылку устройству. Ничего не отправляет — только готовит буфер; байты кладёт write(), уходят они в endTransmission().

address
семибитный адрес устройства, 0x08–0x77. Если в даташите 0x4E или 0xD0 — это адрес со сдвигом, делите на два.
Wire.beginTransmission(0x68);

size_t write(uint8_t data)

Положить байт в буфер посылки. Буфер — 32 байта: всё сверх молча отбрасывается.

data
байт: номер регистра, значение, команда.

Возвращает: 1 — байт в буфере, 0 — буфер полон.

Wire.write(0x6B);   // номер регистра
Wire.write(0);      // значение

size_t write(const uint8_t* data, size_t quantity)

Положить в буфер сразу несколько байт — массив или переменную целиком.

data
откуда: имя массива или (uint8_t*)&переменная.
quantity
сколько байт; вместе с уже лежащими — не больше 32.

Возвращает: сколько байт поместилось.

Wire.write((uint8_t*)&n, sizeof(n));

uint8_t endTransmission()

Отправить буфер и отпустить шину. По коду возврата видно, что пошло не так, — на нём построен сканер.

Возвращает: 0 — доставлено; 2 — по адресу никто не ответил; 3 — устройство не приняло байт данных; 4 — другая ошибка шины; 5 — таймаут, если включён setWireTimeout().

byte err = Wire.endTransmission();

uint8_t endTransmission(uint8_t sendStop)

То же, но можно не отпускать шину — повторный старт. Так читают регистр: записать его номер и сразу, не отдавая шину, запросить ответ.

sendStop
true — отпустить шину, как без аргумента; false — удержать, следом обязательно requestFrom().

Возвращает: те же коды 0–5.

Wire.endTransmission(false);

Чтение

uint8_t requestFrom(uint8_t address, uint8_t quantity)

Запросить байты у устройства. Ждёт, пока они придут, и складывает в буфер приёма — дальше их забирает read().

address
адрес устройства.
quantity
сколько байт. Больше 32 не бывает: буфер приёма 32 байта, лишнее обрезается.

Возвращает: сколько байт пришло; 0 — устройство не ответило.

if (Wire.requestFrom(0x68, 7) == 7) { /* читаем */ }

uint8_t requestFrom(uint8_t address, uint8_t quantity, uint8_t sendStop)

То же с выбором, отпускать ли шину после чтения. Нужно, когда за чтением сразу идёт следующая посылка тому же устройству.

sendStop
true — по умолчанию, отпустить шину; false — удержать.

Возвращает: сколько байт пришло.

Wire.requestFrom(0x68, 2, true);

uint8_t requestFrom(uint8_t address, uint8_t quantity, uint32_t iaddress, uint8_t isize, uint8_t sendStop)

Чтение регистра одним вызовом: библиотека сама отправит номер регистра и сделает повторный старт. Короче, чем связка write + endTransmission(false).

iaddress
номер регистра.
isize
сколько байт в номере регистра: 1 у датчиков, 2 у больших EEPROM вроде 24C256.
sendStop
true — отпустить шину после чтения.

Возвращает: сколько байт пришло.

Wire.requestFrom(0x68, 1, 0x75, 1, true);   // WHO_AM_I

int available()

Сколько принятых байт ещё не прочитано.

Возвращает: 0–32.

while (Wire.available()) buf[i++] = Wire.read();

int read()

Забрать следующий принятый байт.

Возвращает: байт 0–255; -1 — читать нечего.

uint8_t sec = Wire.read();

int peek()

Посмотреть следующий байт, не забирая его.

Возвращает: байт 0–255; -1 — читать нечего.

if (Wire.peek() == -1) Serial.println("Читать нечего");

Ведомое устройство

void onReceive(void (*handler)(int))

Назначить функцию, которую библиотека вызовет, когда ведущий пришлёт данные. Функция работает в прерывании: коротко, без delay() и Serial.print().

handler
функция void имя(int count), где count — сколько байт пришло.
Wire.onReceive(receiveEvent);

void onRequest(void (*handler)(void))

Назначить функцию, которая отвечает, когда ведущий просит данные. Внутри неё — write() с ответом, тоже без задержек.

handler
функция void имя() без аргументов.
Wire.onRequest(requestEvent);

Таймаут

void setWireTimeout(uint32_t timeout = 25000, bool reset_with_timeout = false)

Не ждать шину вечно. По умолчанию таймаут выключен: если датчик отключили посреди обмена или он завис, прижав линию, скетч встаёт навсегда.

timeout
микросекунды; 25000 — 25 мс, предел по стандарту SMBus. 0 — выключить.
reset_with_timeout
true — после таймаута перезапустить шину самой библиотеке. Обычно ставят true.
Wire.setWireTimeout(25000, true);

bool getWireTimeoutFlag()

Проверить, был ли таймаут с момента последнего сброса флага.

Возвращает: true — был.

if (Wire.getWireTimeoutFlag()) Serial.println("Шина зависала");

void clearWireTimeoutFlag()

Сбросить флаг таймаута, чтобы заметить следующий.

Wire.clearWireTimeoutFlag();

Примеры

Сканер I2C: найти адрес модуля

#include <Wire.h>

// сканер I2C: печатает адреса всех устройств на шине
void setup() {
  Wire.begin();
  Serial.begin(9600);
  Serial.println("Ищу устройства...");
  byte found = 0;
  for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {   // 0 — кто-то ответил
      Serial.print("Найден адрес 0x");
      if (addr < 16) Serial.print('0');
      Serial.println(addr, HEX);
      found++;
    }
  }
  Serial.print("Готово, устройств: ");
  Serial.println(found);
}

void loop() {}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Первое, что запускают с любым новым I2C-модулем. Если сканер ничего не нашёл — дело в проводах, питании или подтяжке, а не в библиотеке дисплея.

Монитор порта:

Ищу устройства...\nНайден адрес 0x27\nНайден адрес 0x68\nГотово, устройств: 2

Чтение регистра с повторным стартом

#include <Wire.h>

const uint8_t MPU = 0x68;   // MPU6050; с AD0 на питании — 0x69

// прочитать один регистр: записать его номер, повторный старт, прочитать байт
uint8_t readRegister(uint8_t addr, uint8_t reg) {
  Wire.beginTransmission(addr);
  Wire.write(reg);                  // какой регистр нужен
  Wire.endTransmission(false);      // false — без стопа: шину не отпускаем
  Wire.requestFrom(addr, (uint8_t)1);
  return Wire.available() ? Wire.read() : 0xFF;
}

void setup() {
  Serial.begin(9600);
  Wire.begin();
  uint8_t id = readRegister(MPU, 0x75);   // регистр WHO_AM_I
  Serial.print("WHO_AM_I = 0x");
  Serial.println(id, HEX);
}

void loop() {}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Главный приём работы с датчиком без библиотеки: записать номер регистра, не отпуская шину, и прочитать ответ. У MPU6050 в регистре 0x75 лежит 0x68. Ответ 0x70 значит, что на плате MPU6500 под видом 6050, 0x71 — MPU9250, 0xFF — устройство не ответило.

Монитор порта:

WHO_AM_I = 0x68

Время из DS3231 без библиотеки

#include <Wire.h>

const uint8_t RTC = 0x68;   // DS3231

// часы хранят числа в двоично-десятичном виде: 0x59 — это 59
uint8_t bcd2dec(uint8_t v) { return (v >> 4) * 10 + (v & 0x0F); }

void print2(uint8_t v, char after) {
  if (v < 10) Serial.print('0');
  Serial.print(v);
  if (after) Serial.print(after);
}

void setup() {
  Serial.begin(9600);
  Wire.begin();
}

void loop() {
  Wire.beginTransmission(RTC);
  Wire.write(0x00);                           // читать с регистра секунд
  Wire.endTransmission(false);
  if (Wire.requestFrom(RTC, (uint8_t)7) == 7) {
    uint8_t s  = bcd2dec(Wire.read());
    uint8_t m  = bcd2dec(Wire.read());
    uint8_t h  = bcd2dec(Wire.read() & 0x3F);   // 24-часовой режим
    Wire.read();                                // день недели пропускаем
    uint8_t d  = bcd2dec(Wire.read());
    uint8_t mo = bcd2dec(Wire.read() & 0x1F);   // старший бит — признак века
    uint8_t y  = bcd2dec(Wire.read());
    print2(h, ':'); print2(m, ':'); print2(s, ' ');
    print2(d, '.'); print2(mo, '.'); print2(y, 0);
    Serial.println();
  } else {
    Serial.println("DS3231 не отвечает");
  }
  delay(1000);
}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Семь регистров подряд одним запросом: секунды, минуты, часы, день недели, число, месяц, год. Часы хранят их в двоично-десятичном виде, поэтому каждое число переводится bcd2dec(). У DS3231 и MPU6050 один адрес, 0x68: на одной шине их разводят перемычкой AD0 на гироскопе — тогда он переезжает на 0x69.

Монитор порта:

14:05:07 29.09.26\n14:05:08 29.09.26\n14:05:09 29.09.26

Две Arduino по I2C: ведущая (master) и ведомая (slave)

#include <Wire.h>

const bool MASTER = true;        // на второй плате — false
const uint8_t SLAVE_ADDR = 8;    // адрес ведомой платы, любой из 8–119

volatile int16_t lastValue = 0;  // ведомая: последнее принятое число

// ведомая: ведущая прислала данные
void receiveEvent(int count) {
  if (count >= 2) {
    int16_t v = Wire.read();
    v |= Wire.read() << 8;
    lastValue = v;
  }
  while (Wire.available()) Wire.read();   // лишнее выбрасываем
}

// ведомая: ведущая просит ответ
void requestEvent() {
  int16_t reply = lastValue * 2;
  Wire.write((uint8_t*)&reply, sizeof(reply));
}

void setup() {
  Serial.begin(9600);
  if (MASTER) {
    Wire.begin();
  } else {
    Wire.begin(SLAVE_ADDR);
    Wire.onReceive(receiveEvent);
    Wire.onRequest(requestEvent);
  }
}

void loop() {
  if (!MASTER) return;              // ведомая работает в обработчиках
  static int16_t n = 0;
  n++;
  Wire.beginTransmission(SLAVE_ADDR);
  Wire.write((uint8_t*)&n, sizeof(n));
  byte err = Wire.endTransmission();
  if (err) {
    Serial.print("Ошибка ");
    Serial.println(err);            // 2 — ведомая не ответила
  } else if (Wire.requestFrom(SLAVE_ADDR, (uint8_t)2) == 2) {
    int16_t r = Wire.read();
    r |= Wire.read() << 8;
    Serial.print(n); Serial.print(" → "); Serial.println(r);
  }
  delay(1000);
}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Один скетч на обе платы — меняется только константа MASTER. Соединяют A4 с A4, A5 с A5 и землю с землёй. Ведущая шлёт число, ведомая запоминает его и в ответ на запрос отдаёт удвоенным. Обработчики ведомой работают в прерывании, поэтому в них нет Serial.print().

Монитор порта:

1 → 2\n2 → 4\n3 → 6\nОшибка 2        ← ведомую выключили

На чём попадаются

Дисплей молчит, сканер ничего не находит

Перепутаны SDA и SCL, нет общей земли или модуль без подтяжки на длинных проводах. Сначала сканер: если он не видит устройство, библиотека дисплея не поможет.

Плата зависает при отключении датчика

Wire по умолчанию ждёт шину бесконечно. Wire.setWireTimeout(25000, true) в setup() — и вместо зависания endTransmission() вернёт 5.

Запросил 64 байта — пришло 32

Буфер Wire на AVR — 32 байта и на приём, и на передачу. requestFrom() молча обрезает запрос, write() сверх 32 байт возвращает 0. Длинные данные — EEPROM, картинки для дисплея — читают и пишут кусками.

После Wire.begin() сломался A4 или A5

На UNO и Nano эти выводы принадлежат шине. pinMode() и digitalWrite() на них ломают обмен, а analogRead() показывает не датчик, а уровень шины. Аналоговые датчики — на A0–A3.

Два устройства с одним адресом

На одной шине так нельзя. У многих модулей адрес меняется перемычкой — у MPU6050 это AD0, у переходника 1602 — площадки A0–A2. Иначе — вторая шина на других выводах (ESP32) или мультиплексор TCA9548A.

Адрес в даташите 0x4E, а сканер показывает 0x27

Даташиты иногда пишут адрес вместе с битом чтения-записи. Wire работает с семибитным: делите на два.

Где используется на сайте

Что дальше