Два провода для всех
I2C (Inter-Integrated Circuit, она же TWI) — шина, где десятки устройств висят на двух проводах: SDA (данные) и SCL (тактирование). Каждое устройство имеет адрес (0x00–0x7F), ведущий (мастер, Arduino) опрашивает их по очереди.
На Uno пины жёсткие: A4 = SDA, A5 = SCL. Подтяжки к 5 В резисторами ~4.7 кОм обязательны — но почти все модули уже имеют их на плате.
Arduino A4 (SDA) ── SDA датчика 1 ── SDA датчика 2 ── ...
Arduino A5 (SCL) ── SCL датчика 1 ── SCL датчика 2 ── ...
5V ── VCC всех модулей
GND ── GND всех
Библиотека Wire
#include <Wire.h>
void setup() {
Wire.begin(); // Arduino — мастер
Serial.begin(9600);
}
void loop() {
Wire.beginTransmission(0x27); // адрес устройства
Wire.write(0x00); // регистр/байт
Wire.write(0x42);
Wire.endTransmission();
Wire.requestFrom(0x27, 2); // просим 2 байта
while (Wire.available()) {
byte b = Wire.read();
Serial.println(b, HEX);
}
delay(1000);
}
Базовый цикл: beginTransmission → write → endTransmission для отправки; requestFrom + read для чтения.
Сканер адресов — первый скетч
Не знаете адрес модуля — найдите его сканером:
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("I2C scan:");
for (byte i = 8; i < 120; ++i) {
Wire.beginTransmission(i);
if (Wire.endTransmission() == 0) {
Serial.print(" 0x"); Serial.println(i, HEX);
}
}
Serial.println("готово");
}
void loop() {}
Типичные адреса: LCD1602 — 0x27 или 0x3F, OLED SSD1306 — 0x3C, MPU6050 — 0x68, BME280 — 0x76/0x77, PCF8574 — 0x20–0x27.
Работа с конкретным датчиком: MPU6050
#include <Wire.h>
const int MPU = 0x68;
void setup() {
Wire.begin();
Wire.beginTransmission(MPU);
Wire.write(0x6B); // регистр PWR_MGMT_1
Wire.write(0); // выход из сна
Wire.endTransmission();
}
void loop() {
Wire.beginTransmission(MPU);
Wire.write(0x3B); // первый регистр акселерометра
Wire.endTransmission(false); // повторный старт
Wire.requestFrom(MPU, 6);
int16_t ax = Wire.read() << 8 | Wire.read();
int16_t ay = Wire.read() << 8 | Wire.read();
int16_t az = Wire.read() << 8 | Wire.read();
// ax, ay, az — ускорения
}
endTransmission(false) — restart без отпускания шины: важно для атомарности чтения регистров.
Несколько устройств на шине
Красота I2C: датчики просто вешаются параллельно. Одна шина — экран + акселерометр + датчик давления:
LiquidCrystal_I2C lcd(0x27, 16, 2); // дисплей
// MPU6050 на 0x68, BME280 на 0x76 — все на одних A4/A5
void loop() {
float t = readBME(); // по адресу 0x76
int16_t ax = readMPU(); // по адресу 0x68
lcd.setCursor(0, 0); // дисплей на 0x27
lcd.print(t);
}
Конфликт адресов
Два одинаковых датчика (например, два MPU6050 на 0x68) на одной шине конфликтуют — оба отвечают одновременно. Решения:
- Пин-выбор адреса: у многих модулей есть AD0/AD1 — подтягиваете к VCC/GND и меняете младший бит адреса (MPU6050: 0x68/0x69).
- I2C-мультиплексор TCA9548A — переключает до 8 каналов шины программно.
- Вторая шина — на ESP32 два аппаратных I2C; на Uno — программная реализация.
Подтяжки и длина проводов
Без подтяжек SDA/SCL к VCC шина не работает. Большинство модулей имеют встроенные — но их сумма при многих устройствах может стать слишком сильной. Итоговое сопротивление должно быть ~1–4.7 кОм; если модулей много — считайте параллельное сопротивление.
Длина шины — десятки сантиметров для надёжности. Метры — только со снижением скорости и усилением подтяжек.
Типичные проблемы
- Ничего не находится: перепутаны SDA/SCL (A4/A5!), нет питания модуля, нет общего GND.
- Мусор на дисплее/данных: нет подтяжек, длинные провода, помехи — укоротите шину, проверьте резисторы.
- Два устройства мешают: конфликт адресов — пин AD или мультиплексор.
- Чтение возвращает FF: не сделали
endTransmission(false)перед requestFrom — шина отпущена, датчик «забыл» регистр. - На ESP32 не те пины: у ESP32 SDA/SCL можно назначать
Wire.begin(21, 22).
Итог
I2C — стандарт датчиков: SDA/SCL на A4/A5, Wire-библиотека, сканер для адресов, несколько устройств на двух проводах. Три вещи: подтяжки обязательны, endTransmission(false) для чтения регистров, адреса не должны совпадать. С этим на одной плате соседствуют дисплей, акселерометр, барометр и ещё десяток датчиков.
Полный пример: BME280 на I2C
#include <Wire.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme;
void setup() {
Serial.begin(9600);
if (!bme.begin(0x76)) { // адрес 0x76 или 0x77
Serial.println("BME280 не найден");
while (true);
}
}
void loop() {
Serial.printf("T=%.1f P=%.1f h=%.1f%%\n",
bme.readTemperature(),
bme.readPressure() / 100.0f,
bme.readHumidity());
delay(2000);
}
Чтение регистров напрямую
Без библиотеки для конкретного датчика — работа через Wire:
uint8_t readReg(uint8_t dev, uint8_t reg) {
Wire.beginTransmission(dev);
Wire.write(reg);
Wire.endTransmission(false); // повторный старт — шина не отпускается
Wire.requestFrom(dev, (uint8_t)1);
return Wire.read();
}
void writeReg(uint8_t dev, uint8_t reg, uint8_t val) {
Wire.beginTransmission(dev);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
I2C-сканер: незаменимая утилита
Сохраните скетч-сканер в отдельный файл — запускаете при каждой проблеме с адресом. Показывает все отвечающие устройства — моментально видно, жив ли модуль и на каком адресе.
Мультиплексор TCA9548A
Восемь каналов I2C на одну плату — подключаете конфликтующие датчики на разные каналы и переключаете:
void tcaSelect(uint8_t ch) {
Wire.beginTransmission(0x70);
Wire.write(1 << ch);
Wire.endTransmission();
}
// датчики на каналах 0 и 1
tcaSelect(0); readSensor();
tcaSelect(1); readSensor();
Уровни и кабели
- Длинные провода — капают скорость и надёжность; до 30 см стабильно, дальше — уменьшайте скорость (
Wire.setClock(100000)вместо 400000). - Датчики на 3.3 В и модули на 5 В на одной шине — level shifter или общий 3.3 В.
- Убедитесь, что подтяжки есть — на большинстве модулей уже на плате, на голых чипах ставьте 4.7к.
Частые ошибки
- Перепутаны A4/A5 — шина не отвечает.
endTransmission(true)вместоfalseпередrequestFrom— датчик «забывает» регистр.- Два устройства на одном адресе — отвечают одновременно, мусор.
- Нет общего GND — устройства не видят друг друга.
- Питание датчика не то — 3.3V-датчик от 5V умирает.