在本教程中,我们将学习使用 ESP32 I2C 通信 通道。我们将学习如何使用 ESP32 除了默认 I2C 引脚之外的不同引脚进行 I2C 通信,将不同的 I2C 设备连接到同一总线,以及如何使用两个 I2C 总线接口。此外,我们将 ESP32 与通过 I2C 协议通信的不同传感器和设备连接,并对我们的板进行编程以运行 I2C 扫描仪。该扫描仪将确定与 ESP32 板连接的 I2C 设备的数量。
ESP32 有两个 I2C 总线接口,用于 I2C 通信。用户可以将 ESP32 板配置为 I2C 主设备或 I2C 从设备。
目录
先决条件
我们将使用 Arduino IDE 对 ESP32 开发板进行编程。因此,您应该拥有最新版本的 Arduino IDE。此外,您还需要安装ESP32环境,安装教程可以看如下:
I2C协议介绍
I2C 也称为内部集成电路或 IIC 或 I square C。它是一种用于短距离数据传输应用的 2 线串行通信协议。它是一种异步半双工串行通信协议。此外,它是一种多主总线协议,只需要两条线即可串行传输数据,即SCL和SDA。
- SDA(双向数据线)
- SCL(双向时钟线)
它是嵌入式项目中非常流行的通信协议,用于连接 基于I2C 的传感器、数字显示器和通信模块。想要相互通信的设备通过 I2C 总线进行连接。I2C总线支持多个从设备和多个主设备。
数据沿着导线(SDA 线)逐位串行传输。与SPI一样 ,I2C 是并发的,位的输出通过主设备和从设备之间共享的时钟信号与位的测试同步。
许多传感器使用这种串行通信协议将数据传输到微控制器,或者通过该协议,不同的从属电路能够与主电路进行通信。仅适用于短距离数据传输。
I2C 引脚
I2C over SPI 的显着特点是它仅使用两条线来进行通信。一根线是 SCL(串行时钟线),用于同步设备之间的数据传输,另一根线是 SDA(串行数据),用于承载要传输的实际数据。这些线是开漏线,这意味着如果设备处于低电平有效,则需要将这些线连接到上拉电阻。每个与总线连接的从设备都有一个唯一的8位地址。
注:SDA 线也称为 SDI,SCL 线也称为 SCK。
使用两根线在特定设备之间进行通信是通过每个设备都有自己唯一的设备ID或地址并使用该地址来完成的;master可以选择任何特定的设备进行通信。
例如,我们可以将多个从站连接到一个主站。ESP32 配置为主设备,多个从设备连接在同一总线上。
我们还可以连接多个主设备来控制同一个从设备。例如,两个 ESP32 板配置为主设备,与同一个从设备连接。
I2C总线
I2C总线由多个设备组成,例如从设备和主设备。连接到 I2C 总线的每个设备都可以处于主模式或从模式。但只有主设备才能发起数据传输过程。通常,同一 I2C 总线上有一主一从或多个从设备通过上拉电阻连接。每个从地址都有一个 7 位唯一地址。
从属设备
每个从设备都有一个唯一的地址,用于识别总线上的设备。换句话说,从机地址帮助主设备向
总线上的特定从机设备发送信息。
主设备
主设备可以发送和获取信息。从设备会对主设备发送的任何内容做出反应。在总线上发送信息时,一次只能有一个设备发送信息。
简而言之,我们只需要两根线来传输数据或与不同数量的设备进行通信。I2C 允许我们同时连接多个设备。但是,您不能使用此协议进行长距离数据传输。
有关 I2C 通信的更多信息,您可以阅读这篇文章:
ESP32 I2C 引脚
如前所述,ESP32 有 2 个 I2C 控制器,可用于处理 I2C 总线上的通信。我们可以配置为主站或从站。现在让我们看看 ESP32 的 Arduino IDE 库中分配给 I2C 控制器的默认 GPIO 引脚。
ESP32 中 SDA 的默认 I2C 引脚为 GPIO21,SCL 的默认 I2C 引脚为 GPIO22。如果我们想更改 GPIO 引脚,我们必须在代码中设置它们。下图显示了 ESP32 的引脚排列,其中显示了默认的 I2C 引脚。
ESP32 I2C 接口具有以下特点:
- 标准模式(100 Kbit/s)
- 快速模式(400 Kbit/s)
- 高达 5 MHz,但受 SDA 上拉强度限制
- 7位/10位寻址模式
- 双寻址模式
在 ESP32 中使用多个从 I2C 设备(具有不同地址的 I2C 设备)
在本节中,我们将首先使用默认 I2C 引脚将三个不同的 I2C 设备与 ESP32 板连接。然后我们将对我们的板进行编程以运行 I2C 扫描仪。
所需组件
- ESP32开发板
- SSD1306 OLED显示屏
- BME280传感器
- MPU6050传感器
- 面包板
- 连接线
首先让我们简单介绍一下本指南中将使用的三个 I2C 设备: SSD1306 OLED、 BME280 和 MPU6050。
SSD1306 0.96英寸OLED显示屏
尽管市场上有多种类型的 OLED 显示屏,但我们将使用 SSD1306 0.96 英寸 OLED 显示屏。所有不同类型的 OLED 显示器的主要组件是 SSD1306 控制器,它使用 I2C 或 SPI 协议与微控制器进行通信。OLED 显示器的尺寸、颜色和形状各不相同,但主要以类似的方式进行编程。
让我们看一下本文中将使用的 OLED 显示屏。它被称为 SSD 1306 0.96 英寸 OLED 显示屏,具有 128×64 像素,仅通过 I2C 协议与 Pi Pico 板进行通信。它价格便宜并且在市场上很容易买到。
BME280
BME280 传感器用于测量有关环境温度、气压和相对湿度的读数。它主要用于低功耗是关键的网络和移动应用程序。该传感器使用 I2C 或 SPI 与微控制器进行数据通信。尽管市场上有多种不同版本的 BME280,但我们将研究的版本使用 I2C 通信协议。
ESP32 通过 I2C 协议与 BME280 传感器通信,提供温度、气压和相对湿度读数。
MPU6050传感器模块
MPU6050 传感器模块是包含集成电路 MPU6050 IC 的 MEMS(微机电系统)模块。该芯片在单个 IC 封装内包含三轴陀螺仪、三轴加速度计和数字运动控制处理器。最重要的是,它还包含一个集成温度传感器。
MPU6050 在 I2C 总线上提供输出数据。因此,我们可以使用MPU6050的I2C总线接口将3轴加速度计和3轴陀螺仪值传输到Raspberry Pi Pico。换句话说,我们可以使用任何具有 I2C 端口的微控制器来读取传感器的输出数据。MPU6050 I2C 接口中的每个参数值都分配有特定的专用地址。我们可以使用这些地址从传感器获取特定值,例如加速度、陀螺仪和温度。
使用该传感器的 I2C 接口的优点之一是我们可以将多个 MPU5060 模块与单个微控制器连接。
ESP32 与 OLED、BME280 和 MPU6050 的接口
让我们看看如何将 ESP32 与 OLED、BME280 模块和 MPU6050 模块连接在一起。我们将使用一条公共 I2C 线来连接所有设备。ESP32 将充当主设备,BME280 传感器、MPU6050 传感器和 OLED 将充当从设备。
我们使用的四个设备之间的连接如下表所示。
ESP32 | SSD1306 OLED显示屏 | BME280 | 微处理器6050 |
3.3V | 电压控制电路 | 电压控制电路 | 电压控制电路 |
GPIO21(I2C SDA) | SDA | SDA | SDA |
GPIO22(I2C SCL) | SCL | SCL | SCL |
接地 | 接地 | 接地 | 接地 |
我们使用了上表中指定的相同连接。
ESP32 I2C 扫描仪 Arduino 程序
每个 I2C 设备都有一个与其关联的地址。ESP32 使用该地址通过 I2C 协议与从机进行通信。
现在复制此代码并将其上传到您的开发板以及已连接的所有 I2C 设备。
此代码将扫描与 ESP32 连接的任何 I2C 设备,并指定串行终端中具有该地址的设备数量。
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(115200);
Serial.println("nI2C Scanner");
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("Unknown error at address 0x");
if (address<16) {
Serial.print("0");
}
Serial.println(address,HEX);
}
}
if (nDevices == 0) {
Serial.println("No I2C devices foundn");
}
else {
Serial.println("donen");
}
delay(5000);
Serial.print("i2c devices found:");
Serial.println(nDevices);
}
void loop() {
}
I2C 扫描器识别出 3 个连接到 I2C 接口的设备。OLED显示屏的I2C地址是0x3C,MPU6050是0x68,BME280是0x76。
由于这三个 I2C 设备都有不同的地址,因此可以共享相同的 I2C 总线。
使用具有 ESP32 相同地址的多个 I2C 设备
在上一节中,我们能够在 ESP32 的相同 I2C 引脚上连接三个不同的 I2C 设备。这是因为 ESP32 能够通过其唯一的地址来识别它们。但是,当我们想要使用 ESP32 连接多个设备进行 I2C 通信,但它们具有相同的地址时,会发生什么情况呢?例如,与 ESP32 连接的两个 BME280 传感器或与 ESP32 连接的两个 OLED。
要在 ESP32 板上使用相同的 I2C 设备,我们必须更改设备的 I2C 地址或使用 I2C 多路复用器。然而,更改设备的地址并不是那么简单,并且它只允许在同一 I2C 总线上使用有限的设备。因此我们可以使用多路复用器,例如。TCA9548A 允许最多 8 个具有相同地址的设备连接到同一 I2C 总线。
在 Arduino IDE 中更改 ESP32 默认 I2C 引脚
正如我们之前提到的,ESP32 中 SDA 的默认 I2C 引脚是 GPIO21,SCL 的默认 I2C 引脚是 GPIO22。如果我们想更改默认的 I2C 引脚,我们必须在代码中设置它们。ESP32 的大部分 GPIO 引脚都可以设置为 I2C 引脚。
Wire.h 库在 Arduino IDE 中用于与 I2C 设备通信。它使用 Wire 实例上的 begin() 方法来初始化协议。如果我们未在此函数内指定任何参数,则使用默认的 I2C SDA 和 SCL 引脚。
Wire.begin();
但是,如果我们想要更改默认的 I2C 引脚,那么我们将它们指定为函数内的参数。Wire.begin() 接受两个参数。第一个是 I2C SDA 引脚,第二个是我们要配置的 I2C SCL 引脚
Wire.begin(I2C_SDA, I2C_SCL);
通过库进行交流
有时,库会参与建立主站和从站之间的通信。例如,要与 BME280 传感器通信,我们需要 Adafruit BME280 库。在这种情况下,上述方法将不起作用,因为涉及到库来建立传感器和 ESP32 板之间的通信。因此,如果要为此类传感器使用不同的I2C引脚,那么我们应该首先参考库的.cpp文件,仔细分析TwoWire参数。如果库文件包含wire.begin(),那么您必须先将其注释掉才能设置您自己的I2C引脚。
让我们使用带有 ESP32 的 BME280 传感器来演示这一点。打开 Adafruit_BME.cpp 并转到 begin() 方法的定义。在这里您可以看到我们可以选择将我们自己的 TwoWire 传递给此方法。
使用 BME280 更改 I2C 引脚
在此示例中,我们使用 GPIO4 作为 SDA,使用 GPIO5 作为 SCL,以通过 ESP32 获取 BME280 传感器读数。
Arduino 程序
下面的 Arduino 程序使用 GPIO4 作为 SDA,使用 GPIO5 作为 SCL 来获取 BME280 传感器读数。
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define I2C_SDA 4
#define I2C_SCL 5
#define SEALEVELPRESSURE_HPA (1013.25)
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
I2CBME.begin(I2C_SDA, I2C_SCL, 100000ul);
bool status;
status = bme.begin(0x76, &I2CBME);
if (!status) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
delay(1000);
Serial.println();
}
void loop() {
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.readPressure() / 100.0F);
Serial.println(" hPa");
Serial.print("Altitude = ");
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.print("Humidity = ");
Serial.print(bme.readHumidity());
Serial.println(" %");
Serial.println();
delay(1000);
}
代码如何运作?
要使用不同的 I2C 引脚,我们首先在各自的变量中定义要为 SDA 和 SCL 设置的 I2C 引脚。在此示例中,我们使用 GPIO4 作为 SDA,使用 GPIO5 作为 SCL。
#define I2C_SDA 4
#define I2C_SCL 5
接下来,我们将为 I2C 总线创建一个名为 I2CBME 的 TwoWire 实例。
TwoWire I2CBME = TwoWire(0);
在 setup() 函数中,我们将使用 begin() 方法中的 TwoWire 实例来初始化 I2C 通信。它接受三个参数。第一个参数是我们之前定义的SDA引脚。第二个参数是我们之前定义的SCL引脚。第三个参数是时钟频率。
I2CBME.begin(I2C_SDA, I2C_SCL, 100000ul);
接下来,我们将使用 begin() 方法初始化 BME 对象,并将 BME280 传感器的地址指定为第一个参数,将 TwoWire 实例的地址指定为第二个参数。
status = bme.begin(0x76, &I2CBME);
为了获取 BME280 传感器读数,我们将在 bme 对象上使用相应的方法来获取温度、压力和湿度读数。
Serial.print("Temperature = ");
Serial.print(bme.readTemperature());
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.readPressure() / 100.0F);
Serial.println(" hPa");
Serial.print("Altitude = ");
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.print("Humidity = ");
Serial.print(bme.readHumidity());
Serial.println(" %");
使用 ESP32 的两个 I2C 总线接口
在之前的所有示例中,我们仅使用 ESP32 板的一个 I2C 接口。然而,ESP32 提供了两个 I2C 总线接口,让我们看看如何使用它们的一些示例。有两种方法可以实现此目的。
Arduino 程序示例 1
在该方法中,我们将使用 ESP32 的两个 I2C 接口将两个 BME280 传感器与 ESP32 连接。第一个 BME280 传感器将与 SDA GPIO4 和 SCL GPIO5 连接,第二个 BME280 传感器将与 SDA GPIO33 和 SCL GPIO32 连接。
代码如何工作?
为两个 BME280 传感器定义 I2C SDA 和 SCL 引脚。在本例中,我们将 GPIO4 用于 SDA,将 GPIO5 用于第一个传感器的 SCL。同样,我们将 GPIO33 用于 SDA,将 GPIO32 用于第二个传感器的 SCL。
#define SDA_1 4
#define SCL_1 5
#define SDA_2 33
#define SCL_2 32
接下来,我们需要为两个 I2C 总线接口创建两个 TwoWire 实例,而不是单个 TwoWire 实例。
TwoWire I2C_1 = TwoWire(0);
TwoWire I2C_2 = TwoWire(1)
为第一个传感器创建两个 Adafruit_BME280 对象,名为“bme1”,为第二个传感器创建“bme2”。稍后将使用这些来访问传感器数据。
Adafruit_BME280 bme1;
Adafruit_BME280 bme2;
接下来,在 setup() 函数内,我们将以定义的 I2C 引脚和频率初始化两个接口的 I2C 通信。
void setup() {
I2C_1.begin(SDA_1, SCL_1, frequency1);
I2C_2.begin(SDA_2, SCL_2, frequency2);
}
此外,我们还将根据设备地址和I2C总线初始化bme1和bme2对象。
bool status1 = bme1.begin(0x76, &I2C_1);
bool status2 = bme2.begin(0x76, &I2C_2);
为了获取传感器数据,我们将在 bme1 和 bme2 对象上使用相关方法。
//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");
//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");
Arduino 程序示例 2
在第二种方法中,我们将使用预定义的 Wire() 和 Wire1() 对象来使用两条 I2C 总线,其中一条将使用默认的 I2C 引脚,另一条将使用用户定义的引脚。我们将使用微控制器的两个 I2C 接口将两个 BME280 传感器与 ESP32 连接。第一个 BME280 传感器将与 ESP32 的默认 I2C 引脚连接。而第二个 BME280 传感器将与 SDA GPIO33 和 SCL GPIO32 连接。
#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);
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() {
//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(" ");
//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);
}
而 Wire1.begin(SDA_2, SCL_2) 会在第二个 I2C 总线上在其内部指定为参数的引脚处初始化 I2C 通信。这里我们已经定义了SDA_2和SCL_2引脚。
Wire.begin();
Wire1.begin(SDA_2, SCL_2);
ESP32 I2C 主从通信示例
在本节中,我们将看到一个在两个 ESP32 板之间执行 I2C 通信的示例。我们将配置一个 EPS32 作为主设备,另一个 ESP32 作为从设备。
我们将从 ESP32 I2C 主设备发送命令到从设备来控制其板载 LED。主设备将延迟 1 秒传输“0”和“1”。ESP32 从机将接收它并打开和关闭其板载 LED。
ESP32 I2C 主从连接图
使用下表中给出的 I2C 通信引脚在两个 ESP32 板之间建立连接。
ESP32主控 | ESP32 从机 | |
---|---|---|
SDA | GPIO21 | GPIO21 |
SCL | GPIO22 | GPIO22 |
接地 | 接地 |
安装ESP32 I2C从库
电线。h 库仅在主模式下配置 ESP32。要将ESP32用作I2C从设备,我们需要安装 ESP32I2CSlave 库。
打开 Arduino IDE 并单击 Sketch > Library > Manage Libraries。
当您单击管理库选项时,您将看到此窗口。在此窗口中,在搜索栏中输入“ ESP32I2CSlave ”,然后按 Enter 键。
选择突出显示的库并单击安装。
ESP32 I2C 主控代码
// Demonstrates use of the Wire and WirePacker libraries.
// Writes data to an ESP32 I2C/TWI slave device that
// uses ESP32 I2C Slave library.
// Refer to the "slave_receiver" example for use with this
#include <Arduino.h>
#include <Wire.h>
#include <WirePacker.h>
#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_SLAVE_ADDR 0x04
void setup()
{
Serial.begin(115200); // start serial for output
Wire.begin(SDA_PIN, SCL_PIN); // join i2c bus
}
void loop()
{
static unsigned long lastWireTransmit = 0;
static byte x = 0;
// send data to WireSlave device every 1000 ms
if (millis() - lastWireTransmit > 1000) {
// first create a WirePacker that will assemble a packet
WirePacker packer;
// then add data the same way as you would with Wire
packer.write("x is ");
packer.write(x);
// after adding all data you want to send, close the packet
packer.end();
// now transmit the packed data
Wire.beginTransmission(I2C_SLAVE_ADDR);
while (packer.available()) { // write every packet byte
Wire.write(packer.read());
}
Wire.endTransmission(); // stop transmitting
lastWireTransmit = millis();
if(x==0)
x = 1;
else
x = 0;
}
}
ESP32 I2C 从机代码
// Demonstrates use of the WireSlave library for ESP32.
// Receives data as an I2C/TWI slave device; data must
// be packed using WirePacker.
// Refer to the "master_writer" example for use with this
#include <Arduino.h>
#include <Wire.h>
#include <WireSlave.h>
#define SDA_PIN 21
#define SCL_PIN 22
#define I2C_SLAVE_ADDR 0x04
#define LED 2
void receiveEvent(int howMany);
void setup()
{
Serial.begin(115200);
pinMode(LED, OUTPUT);
bool success = WireSlave.begin(SDA_PIN, SCL_PIN, I2C_SLAVE_ADDR);
if (!success) {
Serial.println("I2C slave init failed");
while(1) delay(100);
}
WireSlave.onReceive(receiveEvent);
}
void loop()
{
// the slave response time is directly related to how often
// this update() method is called, so avoid using long delays
// inside loop(), and be careful with time-consuming tasks
WireSlave.update();
// let I2C and other ESP32 peripherals interrupts work
delay(1);
}
// function that executes whenever a complete and valid packet
// is received from master
// this function is registered as an event, see setup()
void receiveEvent(int howMany)
{
while (1 < WireSlave.available()) // loop through all but the last byte
{
char c = WireSlave.read(); // receive byte as a character
Serial.print(c); // print the character
}
int x = WireSlave.read(); // receive byte as an integer
Serial.println(x); // print the integer
if(x == 1)
{
Serial.println("Setting LED active HIGH ");
digitalWrite(LED, HIGH);
}
else if(x == 0)
{
Serial.println("Setting LED active LOW ");
digitalWrite(LED, LOW);
}
Serial.println("");
}
现在将上述程序上传到主从 ESP32 板。
将 Arduino 程序上传到两个 ESP32 板后,打开从设备的串行监视器,您将在串行监视器上看到以下消息:
您还会看到 ESP32 从机的板载 LED 会延迟一秒亮起和熄灭。
不错
太棒了!找到了我需要的答案。
很详细哦, 不错