Добавить в избранное

Шина I2C на Arduino: несколько датчиков на двух проводах

Чтение ≈ 4 мин

I2C (TWI) на Arduino: шина на двух проводах SDA/SCL, адресация устройств, подтяжки, библиотека Wire, сканер адресов, подключение нескольких датчиков и конфликты адресов.

Два провода для всех

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 умирает.
Поделиться
Понравился материал?

Комментарии

Добавить комментарий