ESP32 I2C: настройка пинов, несколько шин и периферийных устройств (Arduino IDE)

ESP32 имеет два интерфейса шины I2C, которые могут работать как мастер (master) или как подчиненное устройство (slave). В этом руководстве мы рассмотрим протокол связи I2C с ESP32 в Arduino IDE: как выбрать пины I2C, подключить несколько устройств I2C к одной шине и как использовать два интерфейса шины I2C.

ESP32 I2C коммуникация: настройка пинов, несколько шин и периферийных устройств в Arduino IDE

В этом руководстве мы рассмотрим следующие темы:

Мы будем программировать ESP32 с помощью Arduino IDE, поэтому перед началом работы с этим руководством у вас должно быть установлено дополнение ESP32 в Arduino IDE. Следуйте следующему руководству, чтобы установить ESP32 в Arduino IDE, если вы этого еще не сделали.

Введение в протокол I2C для ESP32

I2C означает Inter Integrated Circuit (произносится «ай-квадрат-си»), и это синхронный протокол связи с несколькими мастерами и несколькими подчиненными устройствами. Вы можете подключить:

  • несколько подчиненных устройств к одному мастеру: например, ваш ESP32 считывает данные с датчика BME280 по I2C и выводит показания на I2C OLED дисплей.

  • несколько мастеров, управляющих одним подчиненным устройством: например, две платы ESP32, записывающие данные на один и тот же I2C OLED дисплей.

Мы часто используем этот протокол с ESP32 для связи с внешними устройствами, такими как датчики и дисплеи. В таких случаях ESP32 является мастером (master), а внешние устройства – подчиненными (slave).

Протокол связи I2C с платой ESP32 в Arduino IDE, несколько устройств

У нас есть несколько руководств по работе ESP32 с устройствами I2C:

Интерфейсы шины I2C ESP32

ESP32 поддерживает связь I2C через два интерфейса шины I2C, которые могут работать как мастер или подчиненное устройство, в зависимости от конфигурации пользователя. Согласно техническому описанию ESP32, интерфейсы I2C ESP32 поддерживают:

  • Стандартный режим (100 Кбит/с)

  • Быстрый режим (400 Кбит/с)

  • До 5 МГц, однако ограничен силой подтяжки SDA

  • 7-битный/10-битный режим адресации

  • Двойной режим адресации. Пользователи могут программировать командные регистры для управления интерфейсами I2C, что обеспечивает большую гибкость

Подключение устройств I2C к ESP32

Протокол связи I2C использует два провода для обмена информацией. Один используется для тактового сигнала (SCL), а другой – для отправки и приема данных (SDA).

Примечание: на многих платах расширения линия SDA также может быть обозначена как SDI, а линия SCL – как SCK.

Датчик BME280 с пинами I2C SDA (SDI) и SCL (SCK)

Линии SDA и SCL имеют активный низкий уровень, поэтому они должны быть подтянуты к питанию через резисторы. Типичные значения – 4,7 кОм для устройств на 5В и 2,4 кОм для устройств на 3,3В.

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

Подключение устройства I2C к ESP32 обычно так же просто, как подключение GND к GND, SDA к SDA, SCL к SCL и положительного напряжения питания к периферийному устройству, обычно 3,3В (но это зависит от используемого модуля).

Устройство I2C

ESP32

SDA

SDA (по умолчанию GPIO 21)

SCL

SCL (по умолчанию GPIO 22)

GND

GND

VCC

обычно 3,3В или 5В

При использовании ESP32 с Arduino IDE пины I2C по умолчанию – GPIO 22 (SCL) и GPIO 21 (SDA), но вы можете настроить свой код на использование любых других пинов.

Рекомендуемое чтение: Справочное руководство по GPIO ESP32

Сканирование I2C адреса с ESP32

При связи I2C каждое подчиненное устройство на шине имеет свой собственный адрес – шестнадцатеричное число, которое позволяет ESP32 общаться с каждым устройством.

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

Вы можете использовать следующий скетч для определения I2C адресов ваших устройств.

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com
*********/

#include <Wire.h>

void setup() {
  Wire.begin();
  Serial.begin(115200);
  Serial.println("\nI2C Scanner");
}

void loop() {
  byte error, address;
  int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
  for(address = 1; address < 127; address++ ) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
      nDevices++;
    }
    else if (error==4) {
      Serial.print("Unknow error at address 0x");
      if (address<16) {
        Serial.print("0");
      }
      Serial.println(address,HEX);
    }
  }
  if (nDevices == 0) {
    Serial.println("No I2C devices found\n");
  }
  else {
    Serial.println("done\n");
  }
  delay(5000);
}

Смотреть исходный код

Вы получите примерно такой результат в мониторе последовательного порта. Этот конкретный пример для I2C LCD дисплея.

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

Использование других пинов I2C с ESP32 (изменение пинов I2C по умолчанию)

С ESP32 вы можете назначить почти любому пину возможности I2C, вам просто нужно указать это в коде.

При использовании ESP32 с Arduino IDE, используйте библиотеку Wire.h для связи с устройствами по I2C. С этой библиотекой вы инициализируете I2C следующим образом:

Wire.begin(I2C_SDA, I2C_SCL);

Таким образом, вам просто нужно установить желаемые GPIO для SDA и SCL в переменных I2C_SDA и I2C_SCL.

Однако, если вы используете библиотеки для связи с этими датчиками, это может не сработать, и выбор других пинов может быть немного сложным. Это происходит потому, что эти библиотеки могут перезаписать ваши пины, если вы не передадите свой собственный экземпляр Wire при инициализации библиотеки.

В таких случаях вам нужно внимательнее изучить файлы .cpp библиотеки и посмотреть, как передать свои собственные параметры TwoWire.

Например, если вы внимательно посмотрите на библиотеку Adafruit BME280, вы обнаружите, что можете передать свой собственный TwoWire в метод begin().

Библиотека Adafruit_BME280 с использованием других пинов I2C

Итак, пример скетча для чтения данных с BME280 с использованием других пинов, например GPIO 33 в качестве SDA и GPIO 32 в качестве SCL, выглядит следующим образом.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-i2c-communication-arduino-ide/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*/

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define I2C_SDA 33
#define I2C_SCL 32

#define SEALEVELPRESSURE_HPA (1013.25)

TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

unsigned long delayTime;

void setup() {
  Serial.begin(115200);
  Serial.println(F("BME280 test"));
  I2CBME.begin(I2C_SDA, I2C_SCL, 100000);

  bool status;

  // default settings
  // (you can also pass in a Wire library object like &Wire2)
  status = bme.begin(0x76, &I2CBME);
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  Serial.println("-- Default Test --");
  delayTime = 1000;

  Serial.println();
}

void loop() {
  printValues();
  delay(delayTime);
}

void printValues() {
  Serial.print("Temperature = ");
  Serial.print(bme.readTemperature());
  Serial.println(" *C");

  // Convert temperature to Fahrenheit
  /*Serial.print("Temperature = ");
  Serial.print(1.8 * bme.readTemperature() + 32);
  Serial.println(" *F");*/

  Serial.print("Pressure = ");
  Serial.print(bme.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.print("Approx. Altitude = ");
  Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
  Serial.println(" m");

  Serial.print("Humidity = ");
  Serial.print(bme.readHumidity());
  Serial.println(" %");

  Serial.println();
}

Смотреть исходный код

ESP32 с BME280 на других пинах I2C, схема подключения

Нажмите на изображение для увеличения

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

Сначала определите новые пины I2C в переменных I2C_SDA и I2C_SCL. В данном случае мы используем GPIO 33 и GPIO 32.

#define I2C_SDA 33
#define I2C_SCL 32

Создайте новый экземпляр TwoWire. В данном случае он называется I2CBME. Это просто создает шину I2C.

TwoWire I2CBME = TwoWire(0);

В setup() инициализируйте связь I2C с пинами, которые вы определили ранее. Третий параметр – это тактовая частота.

I2CBME.begin(I2C_SDA, I2C_SCL, 400000);

Наконец, инициализируйте объект BME280 с адресом вашего датчика и объектом TwoWire.

status = bme.begin(0x76, &I2CBME);

После этого вы можете использовать обычные методы вашего объекта bme для запроса температуры, влажности и давления.

Примечание: если библиотека, которую вы используете, содержит оператор wire.begin() в своем файле, вам может потребоваться закомментировать эту строку, чтобы вы могли использовать свои собственные пины.


ESP32 с несколькими устройствами I2C

Как мы упоминали ранее, каждое устройство I2C имеет свой собственный адрес, поэтому можно иметь несколько устройств I2C на одной шине.

Когда у нас есть несколько устройств с разными адресами, их настройка тривиальна:

  • подключите оба периферийных устройства к линиям SCL и SDA ESP32;

  • в коде обращайтесь к каждому периферийному устройству по его адресу;

Посмотрите на следующий пример, который получает показания датчика BME280 (по I2C) и отображает результаты на I2C OLED дисплее.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-i2c-communication-arduino-ide/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

Adafruit_BME280 bme;

void setup() {
  Serial.begin(115200);

  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }

  bool status = bme.begin(0x76);
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  delay(2000);
  display.clearDisplay();
  display.setTextColor(WHITE);
}

void loop() {
  display.clearDisplay();
  // display temperature
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("Temperature: ");
  display.setTextSize(2);
  display.setCursor(0,10);
  display.print(String(bme.readTemperature()));
  display.print(" ");
  display.setTextSize(1);
  display.cp437(true);
  display.write(167);
  display.setTextSize(2);
  display.print("C");

  // display humidity
  display.setTextSize(1);
  display.setCursor(0, 35);
  display.print("Humidity: ");
  display.setTextSize(2);
  display.setCursor(0, 45);
  display.print(String(bme.readHumidity()));
  display.print(" %");

  display.display();

  delay(1000);
}

Смотреть исходный код

ESP32 с BME280 и OLED дисплеем, схема подключения

Нажмите на изображение для увеличения

Поскольку OLED и BME280 имеют разные адреса, мы можем использовать одни и те же линии SDA и SCL без каких-либо проблем. Адрес OLED дисплея – 0x3C, а адрес BME280 – 0x76.

if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
  Serial.println(F("SSD1306 allocation failed"));
  for(;;);
}

bool status = bme.begin(0x76);
if (!status) {
  Serial.println("Could not find a valid BME280 sensor, check wiring!");
  while (1);
}
Отображение показаний BME280 на I2C OLED дисплее -- два периферийных устройства I2C на одной шине

Рекомендуемое чтение: ESP32 OLED дисплей с Arduino IDE

Но что если у вас есть несколько периферийных устройств с одинаковым адресом? Например, несколько OLED дисплеев или несколько датчиков BME280? Есть несколько решений.

  • изменить I2C адрес устройства;

  • использовать I2C мультиплексор.

Многие платы расширения имеют возможность изменить I2C адрес в зависимости от их схемотехники. Например, посмотрите на следующий OLED дисплей.

Выбор другого I2C адреса на OLED дисплее SSD1306

Размещая резистор на одной или другой стороне, вы можете выбрать разные I2C адреса. Это также работает с другими компонентами.

Однако в предыдущем примере это позволяет иметь только два I2C дисплея на одной шине: один с адресом 0x3C и другой с адресом 0x3D.

Кроме того, иногда изменить I2C адрес не так просто. Поэтому, чтобы иметь несколько устройств с одинаковым адресом на одной шине I2C, вы можете использовать I2C мультиплексор, такой как TCA9548A, который позволяет общаться с 8 устройствами с одинаковым адресом.

I2C мультиплексор TCA9548A

У нас есть подробное руководство, объясняющее, как использовать I2C мультиплексор для подключения нескольких устройств с одинаковым адресом к ESP32: Руководство по TCA9548A I2C мультиплексору: ESP32, ESP8266, Arduino.


ESP32 с двумя интерфейсами шины I2C

Для использования двух интерфейсов шины I2C ESP32 вам нужно создать два экземпляра TwoWire.

TwoWire I2Cone = TwoWire(0);
TwoWire I2Ctwo = TwoWire(1)

Затем инициализируйте связь I2C на желаемых пинах с заданной частотой.

void setup() {
  I2Cone.begin(SDA_1, SCL_1, freq1);
  I2Ctwo.begin(SDA_2, SCL_2, freq2);
}

Затем вы можете использовать методы из библиотеки Wire.h для взаимодействия с интерфейсами шины I2C.

Более простая альтернатива – использование предопределенных объектов Wire() и Wire1(). Wire().begin() создает связь I2C на первой шине I2C, используя пины и частоту по умолчанию. Для Wire1.begin() вы должны указать желаемые пины SDA и SCL, а также частоту.

setup(){
  Wire.begin(); //uses default SDA and SCL and 100000HZ freq
  Wire1.begin(SDA_2, SCL_2, freq);
}

Этот метод позволяет использовать две шины I2C, одна из которых использует параметры по умолчанию.

Чтобы лучше понять, как это работает, рассмотрим простой пример, который считывает температуру, влажность и давление с двух датчиков BME280.

Несколько датчиков BME280 с ESP32 -- использование двух интерфейсов шины I2C ESP32

Каждый датчик подключен к отдельной шине I2C.

  • Шина I2C 1: использует GPIO 27 (SDA) и GPIO 26 (SCL);

  • Шина I2C 2: использует GPIO 33 (SDA) и GPIO 32 (SCL);

ESP32 с несколькими BME280 (несколько шин I2C), схема подключения

Нажмите на изображение для увеличения

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-i2c-communication-arduino-ide/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*/

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define SDA_1 27
#define SCL_1 26

#define SDA_2 33
#define SCL_2 32

TwoWire I2Cone = TwoWire(0);
TwoWire I2Ctwo = TwoWire(1);

Adafruit_BME280 bme1;
Adafruit_BME280 bme2;

void setup() {
  Serial.begin(115200);
  Serial.println(F("BME280 test"));

  I2Cone.begin(SDA_1, SCL_1, 100000);
  I2Ctwo.begin(SDA_2, SCL_2, 100000);

  bool status1 = bme1.begin(0x76, &I2Cone);
  if (!status1) {
    Serial.println("Could not find a valid BME280_1 sensor, check wiring!");
    while (1);
  }

  bool status2 = bme2.begin(0x76, &I2Ctwo);
  if (!status2) {
    Serial.println("Could not find a valid BME280_2 sensor, check wiring!");
    while (1);
  }

  Serial.println();
}

void loop() {
  // Read from bme1
  Serial.print("Temperature from BME1= ");
  Serial.print(bme1.readTemperature());
  Serial.println(" *C");

  Serial.print("Humidity from BME1 = ");
  Serial.print(bme1.readHumidity());
  Serial.println(" %");

  Serial.print("Pressure from BME1 = ");
  Serial.print(bme1.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.println("--------------------");

  // Read from bme2
  Serial.print("Temperature from BME2 = ");
  Serial.print(bme2.readTemperature());
  Serial.println(" *C");

  Serial.print("Humidity from BME2 = ");
  Serial.print(bme2.readHumidity());
  Serial.println(" %");

  Serial.print("Pressure from BME2 = ");
  Serial.print(bme2.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.println("--------------------");

  delay(5000);
}

Смотреть исходный код

Давайте рассмотрим ключевые части кода для использования двух интерфейсов шины I2C.

Определите пины SDA и SCL, которые вы хотите использовать:

#define SDA_1 27
#define SCL_1 26

#define SDA_2 33
#define SCL_2 32

Создайте два объекта TwoWire (два интерфейса шины I2C):

TwoWire I2Cone = TwoWire(0);
TwoWire I2Ctwo = TwoWire(1);

Создайте два экземпляра библиотеки Adafruit_BME280 для взаимодействия с вашими датчиками: bme1 и bme2.

Adafruit_BME280 bme1;
Adafruit_BME280 bme2;

Инициализируйте связь I2C на определенных пинах и частоте:

I2Cone.begin(SDA_1, SCL_1, 100000);
I2Ctwo.begin(SDA_2, SCL_2, 100000);

Затем вы должны инициализировать объекты bme1 и bme2 с правильным адресом и шиной I2C. bme1 использует I2Cone:

bool status = bme1.begin(0x76, &I2Cone);

А bme2 использует I2Ctwo:

bool status1 = bme2.begin(0x76, &I2Ctwo);

Теперь вы можете использовать методы библиотеки Adafruit_BME280 для объектов bme1 и bme2 для чтения температуры, влажности и давления.

Другая альтернатива

Для простоты вы можете использовать предопределенные объекты Wire() и Wire1():

  • Wire(): создает связь I2C на пинах по умолчанию GPIO 21 (SDA) и GPIO 22 (SCL)

  • Wire1(SDA_2, SCL_2, freq): создает связь I2C на определенных пинах SDA_2 и SCL_2 с заданной частотой.

ESP32 с несколькими BME280, схема подключения

Нажмите на изображение для увеличения

Вот тот же пример, но с использованием этого метода. Теперь один из ваших датчиков использует пины по умолчанию, а другой – GPIO 32 и GPIO 33.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-i2c-communication-arduino-ide/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*/

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define SDA_2 33
#define SCL_2 32

Adafruit_BME280 bme1;
Adafruit_BME280 bme2;

void setup() {
  Serial.begin(115200);
  Serial.println(F("BME280 test"));

  Wire.begin();
  Wire1.begin(SDA_2, SCL_2);

  bool status1 = bme1.begin(0x76);
  if (!status1) {
    Serial.println("Could not find a valid BME280_1 sensor, check wiring!");
    while (1);
  }

  bool status2 = bme2.begin(0x76, &Wire1);
  if (!status2) {
    Serial.println("Could not find a valid BME280_2 sensor, check wiring!");
    while (1);
  }

  Serial.println();
}

void loop() {
  // Read from bme1
  Serial.print("Temperature from BME1= ");
  Serial.print(bme1.readTemperature());
  Serial.println(" *C");

  Serial.print("Humidity from BME1 = ");
  Serial.print(bme1.readHumidity());
  Serial.println(" %");

  Serial.print("Pressure from BME1 = ");
  Serial.print(bme1.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.println("--------------------");

  // Read from bme2
  Serial.print("Temperature from BME2 = ");
  Serial.print(bme2.readTemperature());
  Serial.println(" *C");

  Serial.print("Humidity from BME2 = ");
  Serial.print(bme2.readHumidity());
  Serial.println(" %");

  Serial.print("Pressure from BME2 = ");
  Serial.print(bme2.readPressure() / 100.0F);
  Serial.println(" hPa");

  Serial.println("--------------------");
  delay(5000);
}

Смотреть исходный код

Вы должны получить показания обоих датчиков в мониторе последовательного порта.

Вывод нескольких датчиков BME280 ESP32 в мониторе последовательного порта

ESP32 I2C Master и Slave (I2C связь между двумя ESP32)

Вы можете обмениваться данными между двумя платами ESP32, используя протокол связи I2C. Одна плата должна быть настроена как подчиненное устройство (slave), а другая – как мастер (master).

Плата-подчиненное устройство содержит данные, которые мастер может запросить.

Вы должны соединить обе платы ESP32, используя соответствующие пины I2C. Не забудьте соединить пины GND вместе.

Подключение двух плат ESP32 через I2C

На предыдущем рисунке показана схема подключения для двух плат ESP32 DOIT V1. Пины I2C по умолчанию – GPIO 21 (SDA) и GPIO 22 (SCL). Если вы используете ESP32-C3, ESP32-S3 или другую модель, пины I2C по умолчанию могут отличаться. Пожалуйста, проверьте распиновку используемой платы. Вы также можете использовать пользовательские пины I2C.

Для полного руководства по обмену данными между двумя платами ESP32 с использованием протокола связи I2C перейдите по ссылке ниже:

Заключение

В этом руководстве вы узнали больше о протоколе связи I2C с ESP32. Мы надеемся, что информация в этой статье была для вас полезной.

Чтобы узнать больше о GPIO ESP32, прочитайте наше справочное руководство: Справочник по распиновке ESP32: какие GPIO пины следует использовать?

Узнайте больше об ESP32 с нашими ресурсами:

Спасибо за чтение.


Источник: ESP32 I2C Communication: Set Pins, Multiple Bus Interfaces and Peripherals (Arduino IDE)