4. Arduino框架下I2C编程:Wire库详解

好,咱们进入实战环节。这一章我带你彻底搞懂Arduino框架下的Wire库。说白了,Wire库就是ESP32跟I2C设备对话的翻译官。你调用它,它帮你把数据发到SDA/SCL线上。

我个人习惯把Wire库的API分成三组:初始化、主机发送、主机接收。下面一个一个拆开讲。

4.1 begin() 与从机地址配置

先看最基础的 Wire.begin()。这个函数干两件事:初始化I2C外设,把ESP32设成主机模式。

#include <Wire.h>

void setup() {
  Wire.begin();           // 默认引脚:SDA=21, SCL=22
  // Wire.begin(16, 17);  // 你也可以指定其他引脚
}

嗯,这里要注意。ESP32的I2C引脚是可以任意映射的。我在项目中遇到过客户把SDA和SCL接反了,结果调了两天没反应。你想想看,硬件连线错了,软件再怎么调也没用。

如果你想让ESP32作为从机,那就传一个地址进去:

Wire.begin(0x42);  // ESP32作为从机,地址0x42

不过咱们课程主要讲主机模式,从机模式后面章节会单独说。

我的习惯:每次写I2C代码前,先用 Wire.begin() 不加参数。如果后续要改引脚,再传参。这样代码可读性更好。

4.2 beginTransmission() 与 endTransmission()

这是主机发送数据的核心组合。beginTransmission() 告诉Wire库:我要开始跟某个从机说话了。参数就是从机地址。

Wire.beginTransmission(0x3C);  // 开始跟地址0x3C的设备通信
Wire.write(0x00);              // 发送寄存器地址
Wire.write(0x42);              // 发送数据
Wire.endTransmission();        // 结束传输

endTransmission() 会返回一个状态码,告诉你这次传输是否成功。这个返回值很多人忽略,但我建议你一定要检查。

返回值 含义
0 成功
1 数据过长,超出缓冲区
2 从机地址发送后收到NACK
3 数据发送后收到NACK
4 其他错误
我曾经踩过的坑:有一次写传感器驱动,一直返回2。查了半天,发现从机地址写错了。0x3C和0x78就差一位,但完全不通。所以地址一定要跟数据手册再三核对。

4.3 requestFrom() 从机读取数据

读数据跟写数据不太一样。你需要先告诉从机你要读哪个寄存器,然后再发起读取请求。

// 第一步:写寄存器地址
Wire.beginTransmission(0x3C);
Wire.write(0x00);              // 要读取的寄存器地址
Wire.endTransmission(false);   // false表示不发送停止条件

// 第二步:读取数据
Wire.requestFrom(0x3C, 1);     // 从0x3C读取1个字节
if (Wire.available()) {
  byte data = Wire.read();
}

这里有个细节:endTransmission(false) 中的 false 参数。它告诉总线不要发送停止条件,这样紧接着的 requestFrom() 可以发送重复起始条件。很多传感器要求这种操作顺序。

我刚开始学的时候,总忘记加这个 false,结果读出来的数据全是0xFF。后来才明白,停止条件之后总线就释放了,再读就得重新寻址。

4.4 从机地址配置详解

从机地址是I2C通信的门牌号。地址错了,数据就到不了。地址对了,一切好说。

I2C地址通常是7位,范围0x08到0x77。0x00到0x07是保留地址,0x78到0x7F也是保留的。你想想看,如果选了保留地址,总线上的设备会怎么反应?

常见的地址配置方式有两种:

  • 固定地址:芯片出厂就定死了,比如某些温度传感器固定0x48
  • 可配置地址:通过引脚电平选择,比如AD0引脚接GND是0x3C,接VCC是0x3D

地址检查三步法:

  1. 看数据手册的"Slave Address"章节
  2. 注意7位地址和8位地址的区别(左移一位)
  3. 用I2C扫描程序验证

说到扫描程序,我每次接新传感器都会先跑一遍。下面这个代码我用了好几年:

#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.begin();
  
  Serial.println("开始扫描I2C设备...");
  for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    byte error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("找到设备,地址: 0x");
      Serial.println(addr, HEX);
    }
  }
  Serial.println("扫描完成");
}

void loop() {
  // 空循环
}

这个程序会遍历所有可能的I2C地址。如果某个地址有设备应答,就打印出来。我在项目中用这个程序排查过很多次硬件问题——有时候是虚焊,有时候是地址配置错了,一扫描就现原形。

4.5 知识体系总览

下面这张图把Wire库的核心API和它们之间的关系画出来了。你看一眼就能明白整个调用流程。

Wire库核心API调用流程 Wire库核心API调用流程 Wire.begin() 选择操作类型 主机发送 (写) beginTransmission(addr) write(data) endTransmission() 主机接收 (读) requestFrom(addr, len) available() 检查 read() 读取数据 返回状态码

从这张图能看出来,Wire库的API其实就两条主线。左边是写操作,右边是读操作。中间是初始化。你只要记住这个流程,写任何I2C驱动都不会乱。

4.6 常见问题与避坑指南

  • 地址左移问题:有些数据手册给的是8位地址(比如0x78),但Wire库要7位(0x3C)。记得右移一位。
  • 上拉电阻:ESP32内部有弱上拉,但长线或高速通信时建议外接4.7kΩ上拉电阻。我遇到过不加电阻,通信距离超过10cm就出错的情况。
  • 时钟延展:有些慢速传感器会拉低SCL,ESP32默认支持时钟延展。但如果遇到通信超时,可以试试降低I2C时钟频率。
调试小技巧:用逻辑分析仪抓SDA和SCL波形,一眼就能看出是地址错了、数据错了还是时序不对。比用万用表量电压靠谱多了。

好了,这一章的内容就这些。Wire库的API不多,但每个参数都有讲究。你写代码的时候多想想:地址对不对?停止条件要不要发?返回值检查了没有?养成这些习惯,I2C通信基本不会出大问题。