做嵌入式的朋友应该都跟 I2C 打过交道。你有没有遇到过这种场景:传感器死活读不到,逻辑分析仪挂上去一看,ACK 应答位没了;OLED 屏幕点不亮,示波器波形明明在跳,驱动代码看半天也没问题;EEPROM 连续写几个字节,结果数据错位,后来才发现是跨页了。I2C 协议看起来就两条线、一个上拉电阻,总共没几个时序参数,但真要在项目里用得稳,把底层的物理机制、时序演变和常见坑都理清楚,能省掉大把调试时间。
这篇文章就围绕 I2C 基础协议本身,从开漏结构讲起,把起始、停止、数据位、应答位逐段拆开,再给一套可以直接抄的软件模拟驱动,最后整理我在实际项目里踩过的、以及周围同事朋友遇到的典型问题。内容面向刚接触 I2C 的 MCU 开发者也面向被 I2C 折磨过一段时间的嵌入式工程师,不讲虚的,都是能直接落到板子上的经验。
1. 从一条上拉电阻说起——I2C 的物理层设计逻辑
1.1 两线制、半双工、多主机,I2C 到底解决了什么问题
I2C(Inter-Integrated Circuit)是飞利浦在 1982 年左右提出的串行总线协议,最核心的物理形态是两条线:SCL 串行时钟线和 SDA 串行数据线。它属于半双工通信,同一时刻只能有一个方向的数据在 SDA 上流动。和 SPI 相比,I2C 牺牲了全双工能力,换来的是引脚少、接线少、支持多主机挂载,这也是它能在小尺寸传感器、小容量存储、板级调试接口里长期占据地位的原因。
I2C 的多主机能力是很多人初学时忽略的点。所谓多主机,指的是总线上可以挂多个能发起通信的控制器,它们共享同一条 SCL 和 SDA。这里有个容易被误解的地方:多主机并不等于"大家一起说",而是通过硬件仲裁机制,保证同一时刻只有一个主机真正占据总线。仲裁的硬件基础,就是接下来要重点讲的开漏结构。
1.2 为什么是开漏输出加外部上拉,而不是推挽输出
很多第一次接触 I2C 的同学都会问:为什么 SDA 和 SCL 不直接用推挽输出?MCU 的 GPIO 默认推挽模式不是更方便吗?答案很简单:推挽输出在总线上并联多个设备时,会出现短路风险。
推挽结构里,引脚要么由内部 MOSFET 主动拉高到 VDD,要么主动拉低到 GND。如果设备 A 想输出高电平、设备 B 在同一时刻想输出低电平,两个引脚的输出级就直接对怼了,轻则电平错误,重则损坏芯片。I2C 的设计思路是让所有设备的 SDA 和 SCL 引脚都做成开漏或开源结构,设备只负责把总线拉低,不负责主动拉高,拉高的工作交给外部上拉电阻。这样任意时刻任何设备输出低电平,总线就是低电平;只有所有设备都释放总线,总线才被上拉电阻恢复到高电平。这个特性在电路上叫"线与",它同时解决了两个问题:每个设备不会因为别家的电平状态而遭殃,多主机仲裁也有了硬件基础。
关于引脚模式,这里有一个实用的经验:用 GPIO 模拟 I2C 时,最省事的方式是把 SCL 和 SDA 都配置为开漏输出加外部上拉。开漏输出模式下,往输出数据寄存器写 1,引脚实际上处于高阻态,此时读取输入寄存器,能读到外部电平。这意味着同一个引脚既能当输出,不需要频繁切换方向,就能直接读取总线状态。很多芯片的 I2C 硬件外设内部也是这个原理。
1.3 上拉电阻的取值逻辑和快速选型
上拉电阻的取值直接影响 I2C 的上升沿时间和信号完整性。取值太大,RC 充电时间常数大,SCL/SDA 的上升沿变缓,超过协议时序要求后就会通信出错;取值太小,设备拉低总线时灌入的电流太大,低电平可能压不上来,功耗也会增加。
严格的取值计算要参考 I2C 规范:总线电容 Cb 由走线长度、过孔、引脚寄生电容叠加而成,一般在几十到几百 pF 范围。最小上拉电阻由低电平输出阈值决定,最大上拉电阻由要求的上升时间决定。比如标准模式 100 kbit/s,要求上升时间不超过 1 微秒;快速模式 400 kbit/s,上升时间要求不超过 300 纳秒。
实际工程中我不太会每次都用公式算一遍,常用的是这样一组经验值:
| 工作模式 | 速率 | 上拉电阻经验值 | 备注 |
|---|---|---|---|
| 标准模式 Standard-mode | 100 kbit/s | 4.7 kΩ | 适合大多数短距离板内通信 |
| 快速模式 Fast-mode | 400 kbit/s | 2.2 kΩ | 总线电容偏大时降到 1.5 kΩ |
| 快速模式+ Fast-mode Plus | 1 Mbit/s | 1 kΩ | 需要留意灌电流是否超限 |
| 高速模式 High-speed | 3.4 Mbit/s | 需配合电流源上拉 | 民用项目很少用到 |
如果你的 I2C 走线超过 10 cm、挂了多个设备、或者屏和传感器并联了好几颗,建议按上表的下一档选择,比如 400 kbit/s 时直接用 1.5 kΩ。还有一个从示波器总结出来的经验:如果 SDA 上升沿像滑梯一样坡度明显,或者通讯速率一提上去就出错,第一步永远是减小上拉电阻,而不是改代码。
2. 把时序图变成肌肉记忆——起始、停止、数据与应答
2.1 起始条件和停止条件的判定
I2C 的时序核心,是区分 SCL 高电平和低电平两个相位下 SDA 的行为差异。数据位的传输规则是:SCL 高电平期间,SDA 必须保持稳定;SDA 的电平跳变只能发生在 SCL 低电平期间。这条规则必须刻进脑子里,几乎所有 I2C 信号线都围绕它展开。
起始条件的判断是:SCL 为高电平期间,SDA 发生下降沿,即 SDA 从高跳到低。停止条件正好相反:SCL 为高电平期间,SDA 发生上升沿,即 SDA 从低跳到高。注意这里的关键点在于,跳变发生在 SCL 高电平时段,这一点和数据位的"高电平期间保持稳定"形成了鲜明对比,也是协议能区分起始/停止和普通数据位的根本原因。
我实际调试时的一个习惯是:在逻辑分析仪上抓起始条件,先看 SDA 下降沿是否发生在 SCL 高电平期间。很多软件模拟 I2C 的 bug 都出在这——比如先拉低 SCL 再拉低 SDA,生成的就不是标准起始条件,部分严格要求时序的外设会不认。
2.2 数据位的建立时间与保持时间
数据位的传输,是每个 SCL 时钟周期对应一个 bit。在 SCL 低电平期间,主机把 SDA 设置到目标电平;随后 SCL 拉高,从机在 SCL 高电平期间采样 SDA;采样完成后再把 SCL 拉低,准备下一位。所以 SDA 的跳变要有一个提前量,这个提前量就是建立时间 tSU;DAT。同时,SCL 上升沿到来之后,SDA 还要再稳定一段时间,这叫保持时间 tHD;DAT。
协议规定的时序参数在不同速率下不一样,常见的几个关键值大概是:
| 参数 | 100 kbit/s | 400 kbit/s | 1 Mbit/s |
|---|---|---|---|
| SCL 低电平时间 tLOW | 4.7 μs | 1.3 μs | 0.5 μs |
| SCL 高电平时间 tHIGH | 4.0 μs | 0.6 μs | 0.26 μs |
| 建立时间 tSU;DAT | 250 ns | 100 ns | 50 ns |
| 保持时间 tHD;DAT | 0 | 0 | 0 |
这些参数对写软件模拟 I2C 的意义在于:延时不必卡得那么精细,宁多勿少。100 kbit/s 时一个时钟周期要 10 μs,只要你的延时能让 SCL 高低电平期间都大于上面表格的值,从机不会挑你毛病。我见到最多的失败案例是本来跑得好好的,把 I2C 速率提高到 400 kbit/s,但延时参数没同步缩短,结果协议分析仪看到的实际速率只有 100 kbit/s 出头,不满足从机手册要求的时序范围,设备时好时坏。
2.3 ACK 机制:第九个时钟周期到底在干什么
一个字节的传输是 8 个数据位加 1 个应答位,共 9 个 SCL 时钟周期。前 8 个周期传输数据,第 9 个周期用于应答。写操作时,第 9 个周期从机如果正常工作并且接收成功,会把 SDA 拉低,这就是 ACK;如果从机没有拉低,总线保持高电平,就是 NACK。
读操作时,应答角色发生了变化。主机在接收完一个字节后,如果是 ACK,表示告诉从机"再给我发下一个字节";如果主机在第 9 个周期释放 SDA,不拉低,表示 NACK,意思是"我够了,别发了,准备结束"。这个细节很关键:连续读 EEPROM 或传感器的多个字节时,最后一个字节必须发 NACK,然后发停止条件,否则从机不会停止发送,总线会被搞乱。我有一次读 BMP280 校准参数,就是忘了在最后一个字节前切换为 NACK,结果后续读取的数值始终不稳定。
2.4 完整字节帧和重复起始条件的判定思路
把上面的元素拼起来,一次完整的 I2C 写操作是:起始条件,然后发送地址字节。地址字节由 7 位从机地址加 1 位读写标志组成,读写标志为 0 表示写、为 1 表示读。例如一个 7 位地址 0x3C 的设备,发出的第一个字节就是 0x3C << 1 = 0x78。之后从机拉低 ACK,主机继续发寄存器地址或数据,每发一个字节都要等 ACK,最后以停止条件收尾。
读操作里有一个非常实用的技巧,叫重复起始条件(Repeated START)。需要改变数据传输方向时,比如先写寄存器地址再读数据,很多主机不会发停止条件,而是在读完地址并收到 ACK 之后,直接在 SCL 高电平期间拉出一个 SDA 下降沿,形成一个新起始条件,然后重新发送设备地址,这次读写标志位改为 1。这样做的意义是保持总线占用,防止其他主机中途插进来。EEPROM 的随机读、绝大多数传感器读寄存器,都用到了这个重复起始条件。你如果看到一个"START + 写地址 + 寄存器地址 + 再出现一个 START + 读地址"的抓包波形,不要以为是从机搞了什么操作,这就是标准流程。
3. 手写一个软件模拟 I2C 驱动——附完整代码
3.1 为什么还要软件模拟,硬件 I2C 外设不香吗
很多 MCU 都带硬件 I2C 外设,那为什么还要手写模拟?以我自己的经历来说,原因主要有三个。第一,某些芯片的硬件 I2C 实现存在历史 bug,比如 STM32F1 系列早期版本的 I2C 外设就出过不少问题,很多人最后直接放弃硬件外设,改用 GPIO 模拟。第二,I2C 引脚有时会被复用掉,你又不想改版,就得用 GPIO 在别的引脚上模拟。第三,软件模拟对时序的把控更直观,你想调速率、想加延时、想临时抓状态,都方便。
软件模拟 I2C 的本质,就是按时序要求用 GPIO 翻转 SCL、设置或读取 SDA。它不占用专用外设,只要一个普通定时器或者延时函数就能工作。下面的代码是基于 STM32 HAL 库风格的示例,但思路可以移植到任何平台。
3.2 GPIO 初始化和开漏模式的小技巧
先看初始化。重点是把两个引脚配成开漏输出,不加内部上拉,因为外部已经有上拉电阻了:
#define I2C_SCL_PIN GPIO_PIN_6 #define I2C_SDA_PIN GPIO_PIN_7 #define I2C_GPIO_PORT GPIOB #define I2C_SCL_H() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET) #define I2C_SCL_L() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET) #define I2C_SDA_H() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET) #define I2C_SDA_L() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET) #define I2C_SDA_READ() HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) void i2c_gpio_init(void) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin = I2C_SCL_PIN | I2C_SDA_PIN; gpio.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出 gpio.Pull = GPIO_NOPULL; // 外部已有上拉,内部就不重复了 gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(I2C_GPIO_PORT, &gpio); I2C_SCL_H(); I2C_SDA_H(); }开漏输出模式下,写 1 后引脚悬空,读取输入寄存器能直接读到外部电平,所以 SDA 不需要来回切换方向。这在很多芯片上都是可行的,少数 MCU 的开漏模式读不到实际电平,那就老老实实把方向寄存器切一下,但大部分主流 ARM 内核芯片没问题。初始化完成后,两条线都要处于高电平状态,这是 I2C 总线空闲的电气特征。
3.3 起始、停止、发送字节、读取字节的标准实现
延时函数我用了一个简单的循环,STM32 主频 72 MHz 时每次调用大约在微秒级别,具体值可以根据你的 MCU 主频调整:
static void i2c_delay(void) { volatile uint8_t i = 10; while (i--) { __NOP(); } }起始条件:SCL 为高电平时把 SDA 拉低,然后 SCL 拉低,总线进入传输状态:
void i2c_start(void) { I2C_SDA_H(); I2C_SCL_H(); i2c_delay(); I2C_SDA_L(); // SCL 高电平期间 SDA 下降沿,构成起始条件 i2c_delay(); I2C_SCL_L(); i2c_delay(); }停止条件:同样在 SCL 高电平期间,把 SDA 拉到高电平:
void i2c_stop(void) { I2C_SDA_L(); I2C_SCL_H(); // 先把 SCL 拉高 i2c_delay(); I2C_SDA_H(); // SCL 高电平期间 SDA 上升沿,构成停止条件 i2c_delay(); }发送字节并等待应答。每一位的规则都是:先在 SCL 低电平期间设置 SDA,再拉高 SCL 让从机采样,然后拉低 SCL 准备下一位。8 位发完后,释放 SDA 并产生第 9 个时钟,通过读取 SDA 判断从机是否应答:
uint8_t i2c_write_byte(uint8_t data) { for (uint8_t i = 0; i < 8; i++) { if (data & 0x80) { I2C_SDA_H(); } else { I2C_SDA_L(); } data <<= 1; I2C_SCL_H(); i2c_delay(); I2C_SCL_L(); i2c_delay(); } I2C_SDA_H(); // 释放 SDA,让从机控制 I2C_SCL_H(); i2c_delay(); uint8_t ack = I2C_SDA_READ() ? 0 : 1; // 从机拉低则 ACK I2C_SCL_L(); i2c_delay(); return ack; }读取一个字节。读取时主机的 SDA 处于释放状态,由从机驱动。每位在 SCL 高电平期间读取一次,读满 8 位后,主机需要发送 ACK 或者 NACK,决定是否继续读下一个字节:
uint8_t i2c_read_byte(uint8_t send_nack) { uint8_t data = 0; I2C_SDA_H(); // 释放 SDA,切换为输入状态 for (uint8_t i = 0; i < 8; i++) { I2C_SCL_H(); i2c_delay(); data = (data << 1); if (I2C_SDA_READ()) { data |= 0x01; } I2C_SCL_L(); i2c_delay(); } if (send_nack) { I2C_SDA_H(); // NACK:主机保持高电平 } else { I2C_SDA_L(); // ACK:主机拉低 SDA } I2C_SCL_H(); i2c_delay(); I2C_SCL_L(); I2C_SDA_H(); // 释放总线 return data; }这段代码有几个细节值得注意。发送字节后的 ACK 检测,要实现"先释放 SDA、再拉高 SCL"这个顺序,不能先拉高 SCL 再释放 SDA,否则 SDA 的高电平还没建立,从机拉低时采样就乱掉了。读取字节时,主机在第 9 个时钟之前要先把 SDA 拉低或拉高,这同样要在 SCL 拉高之前完成。读最后一个字节时传参数 send_nack=1,也就是主机不拉低 SDA,告诉从机"不要继续了"。
3.4 用这套驱动读写 EEPROM 和 OLED 的完整调用序列
现在把驱动用起来。以 AT24C02 EEPROM 为例,它的器件地址前 4 位固定为 1010,后 3 位由 A2/A1/A0 引脚决定,最低位是读写标志。如果三个引脚都接地,写地址就是 0xA0,读地址是 0xA1。单字节写的调用序列是:
i2c_start(); i2c_write_byte(0xA0); // 器件写地址 i2c_write_byte(0x00); // 目标字节地址 i2c_write_byte(0x5A); // 要写入的数据 i2c_stop();这里有一个新手最容易踩的坑:EEPROM 在收到停止条件之后,内部就开始执行写操作,这个过程需要几毫秒,AT24C02 典型是 5 ms。在写周期内,EEPROM 不会响应任何总线操作,你发地址字节它也不会 ACK。所以连续写入时,每条写命令之后必须加至少 5 ms 延时,或者循环等待 ACK。我见过有人在循环里连续写了 20 个字节,中间没延时,结果前面的数据全丢。
读 EEPROM 某地址的序列,则用到重复起始条件:
i2c_start(); i2c_write_byte(0xA0); // 先写:告诉 EEPROM 要访问哪个字节 i2c_write_byte(0x00); // 字节地址 i2c_start(); // 重复起始,不产生停止条件 i2c_write_byte(0xA1); // 切换到读方向 uint8_t val = i2c_read_byte(1); // 读一个字节,发 NACK i2c_stop();如果连续读多个字节,最后一个字节用 NACK,之前每个字节都用 ACK:也就是 i2c_read_byte(0),i2c_read_byte(0),最后 i2c_read_byte(1)。
OLED 屏这边,以 SSD1306 控制器为例,它的 I2C 地址常见是 0x3C(7 位),所以发送的地址字节为 0x78。发送数据时有一个控制字节的概念:0x00 表示后续字节是命令,0x40 表示后续字节是显存数据。常见的初始化命令都是这样发的:
i2c_start(); i2c_write_byte(0x78); // SSD1306 写地址 i2c_write_byte(0x00); // 表示接下来的字节是命令 i2c_write_byte(0xAF); // 打开屏幕命令 i2c_stop();更新显存时,把控制字节换成 0x40:
i2c_start(); i2c_write_byte(0x78); i2c_write_byte(0x40); // 显存数据 for (uint16_t i = 0; i < 1024; i++) { i2c_write_byte(frame_buffer[i]); } i2c_stop();如果你用的是 0.96 寸、0.91 寸这类 OLED,大概率是 SSD1306 或者 SH1106。两者 I2C 命令很像,但 SH1106 内部的列地址范围是 132 列,屏只显示 128 列,所以初始化时必须设置列起始地址偏移 2。如果你发现 OLED 显示的内容整体向左右偏移了一两个像素,多半是控制器是 SH1106,而你没有处理这个偏移。
4. 实战排查手册——那些让 I2C 翻车的经典现场
4.1 SDA 一直被拉低,总线锁死怎么办
I2C 总线有一个让人很头疼的故障:上电后 SDA 始终保持低电平,你发什么都没反应,波形图上只有一条平平的低电平直线。这个现象在业内叫总线锁死,产生原因通常是从机在等待后续时钟,而主机只发了几个位就停了,或者停止条件没有正确发出。
遇到这种情况,我常用的救援办法是连续给 SCL 打 9 个时钟脉冲,让卡在中间状态的从机把余下的位走完,然后发一个停止条件。具体操作可以用一个 GPIO 手动翻转 SCL 9 次,每次间隔几十微秒,SDA 保持高电平,最后把 SCL 和 SDA 都拉高。多数情况下,总线会被释放。如果这招没用,那就只能把设备逐个断电查,或者强制把 SCL/SDA 手动接高电平复位,再不行就断总电。归根结底,预防手段是在每次通信结束时确保发送了正确的停止条件,尤其是用软件模拟时,别在循环里 break 忘了发停止。
4.2 全是 NACK,设备不理你,先从地址和电源查
总线上完全没有 ACK,抓到的波形是主机发了地址字节之后,从机一个字都不回,SDA 一直保持高电平,这种情况十有八九是下面几个原因之一。第一,地址写错了。很多传感器的地址是可配置的,I2C 地址引脚的电平决定最后一位,比如 SSD1306 的 SA0 接高电平时地址是 0x3D,接低电平时是 0x3C,你发的却是另一个,那它当然不应答。第二,电源问题,从机的 VCC 没供上,或者供电电压不够,芯片压根没工作。第三,复位引脚一直被拉在复位状态,尤其是 OLED 屏的 RES 引脚悬空,屏不会正常启动。第四,上拉电阻缺失,SDA 始终低电平或波形畸形,设备没法正确识别起始条件。
排查这类问题有一个非常高效的手段:写一个 I2C 总线扫描程序,把从机地址 0x03 到 0x77 挨个发一遍写地址,看哪些地址有 ACK 回应。一次扫描能直接确认设备实际地址,也能确认总线物理层有没有问题。这个程序我几乎每个项目都留着,换一块新屏或者新传感器,第一件事就是扫描。
4.3 电平转换问题:3.3V 和 5V 设备混接,能不能直接连
I2C 设备的工作电压不同,常见的有 5V 的 EEPROM、3.3V 的传感器、1.8V 的加速度计等等。能不能直接把不同电平的设备挂到同一条总线上?这取决于低电平阈值和引脚的耐压能力。3.3V 的 MCU 直接连 5V 上拉到 5V 的总线,MCU 引脚耐压不够就会烧;5V 的设备连 3.3V 上拉的总线,可能阈值不匹配,设备误判电平。
最稳妥的方案是加双向电平转换电路。分立方案是一个 N-MOS 加两个上拉电阻,典型电路里 MOSFET 的源极接低压侧、漏极接高压侧,两个方向都能传。更省事的是直接买集成芯片,比如 PCA9306 或者 TXS0102,它们专门干这个活。我实测下来,PCA9306 在 400 kbit/s 下很稳,注意它本身也需要两侧各自的上拉电阻,不是插上就完事。
4.4 OLED 驱动芯片和 I2C 兼容性,以及 Proteus 仿真的特殊性
现在市面上 0.96 寸、0.91 寸 OLED 驱动芯片主要是 SSD1306 和 SH1106。两个芯片的指令集大部分兼容,但显存结构和列地址处理不同,这就是 0.9 寸 OLED 对 I2C 兼容问题的来源。SSD1306 是 128×64 显存,128 列,列地址 0x00 到 0x7F;SH1106 是 132 列显存,实际显示的 128 列在中间或靠边,所以需要设置显示起始列。你从网上拷贝的驱动如果默认针对 SSD1306,换到 SH1106 的屏上,画面大概率会偏移甚至撕裂。解决办法是根据屏背面丝印或者数据手册确认控制器型号,然后调整初始化时列地址偏移为 2。
Proteus 仿真里跑 OLED 12864 也要留意类似问题。仿真模型无法 100% 还原所有硬件时序行为,OLED 模型的 I2C 时序容差一般比实物严。很多人发现仿真里屏不亮,或者只亮一小块,常见原因一是 I2C 速率调太高,二是上拉电阻没接。我在仿真里习惯把 I2C 速率降到 50 kbit/s 到 100 kbit/s,上拉电阻用 10 kΩ,仿真稳定后再回到实物上验证。仿真通过不代表实物没问题,但至少能帮你把地址、初始化序列、存取流程这些逻辑层面的错误先排掉。
4.5 EEPROM 写周期和数据跨页,这两个最隐蔽
EEPROM 的写周期前面提过,AT24C 系列典型 5 ms,数据手册通常会写最大 5 ms 或 10 ms。只要你在写周期内继续发命令,从机就不会 ACK。可靠的做法是每次 write 之后轮询 ACK,一直等到设备重新响应为止,这样比固定延时更高效也更稳妥。
跨页问题更隐蔽。EEPROM 页写允许一次连续写入多个字节,但地址到达页边界后会回卷,比如 AT24C02 页大小是 8 字节,你从地址 0x05 开始写 4 个字节到 0x09,你以为写到了页 0 的末尾加页 1 的开头,实际上后面两个字节被回卷写到地址 0x00 和 0x01。解决办法是写之前判断剩余空间是否够本次写入长度,不够就分成两次或多次写,每次不超过页大小,并且从页边界对齐开始算。
4.6 波形边沿缓、速率上不去,先怀疑总线上电容和上拉阻值
总线上的电容来自走线、过孔、芯片引脚和输入保护电路,设备越多,总线电容越大。当 SDA 上升沿明显变缓,并且通信速率拉高后就出乱码,这基本就是 RC 时间常数太大。我自己量过一块挂了 3 个传感器加 1 个 OLED 的板子,走线绕了大半圈,总线电容大约在 300 pF 量级,用 10 kΩ 上拉,上升沿肉眼可见地慢。把上拉电阻换成 2.2 kΩ,问题立刻缓解。
如果换小电阻还不行,就考虑降低通信速率。I2C 不是越快越好,很多传感器在 100 kbit/s 下工作几十年都好好的。速率、上拉、电容三者是联动关系,调板子时这三个因素一起考虑,别只盯着代码使劲。
最后分享两个我的习惯
每次调 I2C 设备,我第一件事是写个扫描函数把总线上所有地址扫一遍,确认设备确实在线、地址确实正确。这个习惯帮我避免了很多自以为是。另一个习惯是手边常备一个便宜的逻辑分析仪,不用很高的采样率,几百 kbit/s 的 I2C 对逻辑分析仪的要求并不高,抓到波形后对照时序一帧一帧看,问题基本一眼就能定位。在代码里,我也习惯把 I2C 驱动封装成独立的模块,底层是 GPIO 模拟,上层只暴露读写函数,这样换平台、换引脚,只改底层那几行,上层逻辑不用动。I2C 这个协议本身不难,难的是经验,把总线上每个设备都当成有脾气的个体,耐心看波形,问题总能解决。