ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

IIC通信详解:上拉电阻、时序、STM32实操与异常排查

IIC通信详解:上拉电阻、时序、STM32实操与异常排查

1. IIC到底在传什么:先从物理连接和寻址说起

1.1 两根线怎么撑起一堆设备

刚接触嵌入式的时候,很多人第一次看到IIC原理图都会愣一下:整个总线就两根线,一根SDA(数据线)、一根SCL(时钟线),结果板上挂了四五个芯片,各自还能收发数据,这怎么做到的?

IIC全称Inter-Integrated Circuit,是飞利浦(现在的NXP)在几十年前搞出来的芯片间通信方案。它的核心思想特别朴素:把时钟线和数据线做成公共总线,所有设备都挂在这两根线上,谁要说话,先报自己的“门牌号”。这种设计决定了它的两个天然优势——接线少、扩展容易。你要挂8个传感器,还是这两根线,地址不同就行。

但“公共总线”这四个字背后有个硬件层面的关键设计:所有设备的SDA和SCL引脚必须是开漏输出。开漏的意思是,设备只能把线拉低,不能主动拉高。那谁来把线拉高?答案就是每个IIC电路板上都少不了的上拉电阻。两根线上各接一个电阻到电源,所有设备都不拉低的时候,线就被电阻拉成高电平;某个设备把线拉低,线就变成低电平。这样多个设备同时操作也不会短路,这是IIC能“一总线挂多设备”的根本原因。

IIC总线上的角色分两种:主设备和从设备。主设备负责产生时钟、发起通信,从设备只能被动响应。绝大多数场景下,单片机是唯一的主设备,传感器、存储器、IO扩展芯片都是从设备。总线上也可以挂多个主设备,但先别急着去玩多主通信,能把单主模式跑稳已经是很多项目的及格线了。

1.2 设备地址和寄存器地址别搞混

IIC寻址靠的是设备地址。标准模式是7位地址,加上读写位凑成一个字节。比如你写代码时经常看到0xA0这种地址,它实际上是把7位设备地址0x50左移了一位,最低位填0表示写、填1表示读。很多新手在这里第一次犯晕:EEPROM的地址明明是0x50,为什么代码里写0xA0?就是因为这移位的一位。

7位寻址理论上能挂128个设备,但实际上有个限制:地址0x00是广播地址,所有从设备都要响应;0x01到0x07保留给特殊用途。真正能自由分配的地址大概是从0x08到0x77。实际项目中,我见过的板子挂十几个IIC设备已经是极限了,因为还要考虑总线电容和地址冲突的问题。

寄存器地址和设备地址是另一对容易混淆的概念。设备地址定位的是“跟哪个芯片说话”,寄存器地址定位的是“这个芯片内部的哪个存储位置”。比如一个温湿度传感器,设备地址是0x38,内部有温度寄存器、湿度寄存器、配置寄存器,你读写数据时,要先发设备地址,再发寄存器地址,然后才是数据。这个流程在后面读写时序那一节会详细拆解。

1.3 IIC能跑多快:速率等级和实际选择

IIC规范里定义了几档速率:标准模式100kHz、快速模式400kHz、快速模式Plus 1MHz、高速模式3.4MHz。最早期的IIC只有100kHz,现在绝大多数传感器和存储芯片都支持400kHz,部分新器件支持1MHz。

实际项目里选速率,不是能跑多快就跑多快。IIC总线速率受两个因素制约:上拉电阻和总线电容。这两者的关系直接影响信号上升时间,后面专门有一节讲怎么算。我的经验是,板上设备少、走线短的时候直接上400kHz;设备多、走线长、传感器线缆外接的时候,老老实实降回100kHz。一次通信多花几十微秒,对绝大多数应用毫无影响,但稳定性是实打实的。

还有一个很多教程没提的点:IIC的速率是指SCL时钟频率,不是数据吞吐量。8个数据位加1个ACK位,传一个字节实际需要9个时钟。所以100kHz的IIC理论吞吐也就11KB/s左右,传大块数据并不合适。如果要从SD卡读照片、从Flash读几千字节固件,SPI或者SDIO才是对口的方案。IIC适合的是小数据量的控制、寄存器和传感器读取。

2. IIC上拉电阻到底取多大:一张表搞定从1k到4.7k的取舍

2.1 开路输出结构决定了非接上拉不可

前面提到了开漏输出,这里把原理说透。IIC设备的SDA/SCL引脚内部结构,本质上是一个MOS管开关,一端接地,一端接到引脚。当设备要输出低电平时,MOS管导通,把引脚拉低到地;当设备要输出高电平时,MOS管截止,引脚变成高阻态,什么都不驱动。

问题来了:如果引脚保持高阻态,线上电平是谁决定的?如果没有外部上拉电阻,这根线上会悬空,电平不确定,逻辑全是乱的。所以必须在外部接一个电阻到电源。这个电阻负责在没有任何设备拉低的时候,把线拉到确定的高电平。这就是“上拉电阻”存在的意义。

推挽输出为什么不行?推挽结构上面一个管子拉高、下面一个管子拉低,驱动能力强。但两个设备接在同一条线上,一个输出高、一个输出低,就等于一个把电源短路到地,直接冒烟。IIC不是用软件来解决这种冲突的,是从电路结构上就杜绝了这种可能。

2.2 上拉电阻的计算方法:从上升时间反推

上拉电阻的取值,本质上是和总线电容在玩一个RC充放电游戏。当设备释放总线时,上拉电阻对总线电容充电,电压从低电平爬升到高电平。这个爬升过程有多快,决定了信号能不能在规定的时序内稳定下来。

充放电的时间常数是τ = R_p × C_bus,从低电平到高电平的上升时间大约为t_r ≈ 0.8473 × R_p × C_bus。IIC规范对上升时间有明确要求:标准模式100kHz要求上升时间不超过1000ns,快速模式400kHz要求不超过300ns。

举个实打实的例子:假设你的板子总线上挂了4个设备,走线总长10cm左右,总线电容C_bus大概在100pF附近。要跑400kHz,要求:

t_r < 300ns 0.8473 × R_p × 100pF < 300ns R_p < 3.54kΩ

所以上拉电阻必须小于3.5kΩ才能保证400kHz的上升时间达标。这是上限,不是越小越好。

下限怎么定?上拉电阻的下限由输出低电平时的灌电流决定。设备拉低时,电流从电源流过电阻进到MOS管里。IIC规范要求VOL不超过0.4V,设备在VOL=0.4V时的灌电流能力通常按3mA算(快速模式的标准)。以3.3V系统为例:

R_p > (VCC - VOL) / IOL = (3.3 - 0.4) / 0.003 = 966Ω

所以1kΩ基本是下限。如果VCC是5V,下限就变成(5 - 0.4) / 0.003 = 1.53kΩ,这时候选1k就会让VOL超标,逻辑低电平可能被识别成中间电平。

2.3 典型取值和选型建议

综合上限和下限,IIC上拉电阻的典型取值范围是1kΩ到4.7kΩ。我自己的习惯是:3.3V系统、走线短、设备少(2-3个)、跑400kHz时选2.2kΩ;5V系统选3.3kΩ或4.7kΩ;如果总线需要引出排线连接外部设备,或者板子上设备超过5个、总线电容偏大,就用2.2k甚至1k,同时考虑把速率降到100kHz。

系统电压总线电容目标速率推荐上拉电阻
3.3V50pF以下400kHz2.2kΩ
3.3V100pF400kHz2.2kΩ~3.3kΩ
3.3V200pF100kHz3.3kΩ~4.7kΩ
5V100pF400kHz3.3kΩ
5V200pF100kHz4.7kΩ

电阻选大了,上升沿变缓,波形出现圆角,速率稍微提一点就可能误码;选小了,低电平抬高,功耗增加,在恶劣情况下低电平不达标。如果对取值拿不准,优先选2.2kΩ,这是最通用的值。但具体项目里还是建议用示波器实测一下SDA的上升沿,看有没有超过规范。

2.4 总线电容的估算:为什么设备多了要降速

总线电容来自每个芯片引脚的寄生电容加上PCB走线的电容。单个IIC芯片引脚电容大约5-10pF,PCB走线每厘米约0.5-1pF。算下来,6个设备的板子总线电容轻松超过100pF。

总线上所有设备的电容是并联关系,设备越多电容越大,上升时间越慢。所以一个常见规律是:板子上IIC设备数量越多,上拉电阻越小,但电阻过小又会违反低电平规范,这时唯一的出路是降低通信速率。我见过一个项目,一块板上挂了8个IIC设备,上拉电阻用1k才勉强跑到100kHz。后来改成两颗1k并联也就是500Ω,结果低电平升到了0.6V,从机直接不认了。最后还是老老实实降速到100kHz解决。

如果总线电容实在太大(超过400pF),光靠调电阻就不太现实了,这时候可以用IIC总线缓冲器,比如PCA9600这种芯片,它能隔离两侧电容,重新驱动总线信号。不过绝大多数开发板到不了这个程度,先把电阻和速率调好就够了。

3. IIC时序拆解:看懂START、ACK、读写的完整节奏

3.1 起始条件和停止条件:SCL高电平期间的SDA边沿

IIC通信的帧结构,所有动作都是从**起始条件(START)**开始的。起始条件是:SCL保持高电平时,SDA从高电平跳变到低电平。这个边沿是主设备的一种“宣告”,告诉总线上所有设备:我要开始通信了,大家准备监听地址。

**停止条件(STOP)**恰好相反:SCL保持高电平时,SDA从低电平跳变到高电平。所有从设备听到停止条件后,结束本次通信,释放总线。

为什么非要在SCL高电平时检测边沿?因为在正常数据传输阶段,SDA的电平变化只允许发生在SCL低电平期间,SCL高电平期间SDA必须保持稳定。所以“SCL高时SDA跳变”就成了一个不会跟数据混淆的特殊标志。这个设计相当巧妙,相当于在时域上给控制和数据划了两条完全独立的轨道。

发送起始条件和停止条件时,SCL的时序也要注意细节:先确保SCL为高,然后SDA跳变,然后SCL拉低。软件模拟IIC时,很多人最后一个字节发完就结束,少了停止条件,从机状态机卡死在半路,下一次通信就出问题。停止条件不是可有可无的,它是一个必须发的“句号”。

3.2 字节传输和应答信号:第9个时钟不是传数据

地址广播之后,就是正式的数据传输阶段。IIC规定:每个字节必须是8位,从最高位(MSB)开始发送,发完8位后,主设备释放SDA,然后产生第9个时钟脉冲,这个脉冲是给**应答信号(ACK)**用的。

应答的规则是:接收方在第9个时钟的低电平期间把SDA拉低,表示“我收到了”。如果是主设备发数据、从设备接收,那么从设备在每收到一个字节后拉低SDA应答;如果是主设备读取数据,那么主设备在收到每个字节后拉低SDA应答,但在收到最后一个字节前,主设备要发送一个非应答信号(NACK,即保持SDA为高),然后发送停止条件。

NACK不是错误,它往往是一种流程控制信号。比如读取EEPROM时,主机读满自己期望的字节数后发一个NACK,告诉从机“我不要了,结束吧”。从机看到NACK就会停止发送。如果主机在应该发ACK的时候发了NACK,或者反过来,从机的状态机会错乱,表现就是读出来的数据少一位或者多一位。

设备地址发出去之后,从机也会应答。如果总线上没有这个地址的设备,或者设备忙没准备好,第9个时钟SDA不会被拉低,表现为NACK。所以调试IIC时,第一步永远是检查地址发送后有没有ACK。没有ACK,后面的数据收发全是白搭——先查地址对不对、上拉电阻有没有接、SDA有没有被拉死。

3.3 一个完整的读写序列:以EEPROM为例

拿最常见的AT24C02(2Kbit串行EEPROM)来拆一个完整的写操作和读操作。

写单个字节到指定地址,主机发的东西依次是:

  1. START条件
  2. 设备地址字节:0xA0(7位地址0x50左移一位,最低位为0表示写)
  3. 寄存器地址:比如0x00,指定往哪个地址写
  4. 要写的数据:比如0x55
  5. STOP条件

注意从机电脑的EEPROM在收到一个字节后会触发内部写周期,需要时间把数据真正固化到存储单元里(AT24C02大约要5ms)。在这段时间内从机会保持NACK,称为“写周期”。很多程序控制逻辑复杂,查了半天查不出来,其实只要在写完后延时5ms以上再操作就能解决。

读操作稍微复杂一点。随机读的模式是:

  1. START条件
  2. 设备地址0xA0(写方向)
  3. 寄存器地址0x00(你要读的地址)
  4. 再次发送START条件(这叫做重复起始条件REPEATED START)
  5. 设备地址0xA1(最低位为1表示读)
  6. 此时从机开始发送数据,主机每收到一个字节发ACK
  7. 主机收到需要的字节数后,最后一个字节回NACK
  8. STOP条件

重复起始条件的作用是:在不释放总线的情况下,把通信方向从写切换成读。很多软件模拟IIC的代码里,重复起始处理和起始处理完全一样,这会有隐患——重复起始时SCL本来就是低电平,如果还走“SCL高时SDA拉低”的逻辑,SCL还没拉高,SDA就变了,从机可能识别不了。正确的做法是:先让SDA和SCL同时为低,然后SCL保持低,SDA拉高,再SCL拉高,SDA再拉低,这样才形成一个完整的重复起始边沿。

3.4 时钟拉伸和其他需要知道的特殊机制

时钟拉伸(Clock Stretching)是IIC协议里一个重要的从机保护机制。某些从设备(比如MLX90614红外测温传感器)处理数据需要时间,处理不过来的时候会把SCL拉低不放。因为开漏结构下从机有能力拉低SCL,主机此时必须等待——士别三日当刮目相待,协议要求主机在释放SCL后检查SCL是否为高,如果被从机拉着不放,就继续等。

软件模拟IIC时,最容易忽略这个机制。很多人的软件IIC发送时钟是纯延时驱动的,SCL拉高后延时一下就发数据,从机来不及处理数据就把SCL拉低,这时主机还在按自己的节奏走,时序就乱了。正确做法是:SCL拉高后,轮询读取SCL引脚电平,直到它确实变为高电平才继续。这个做法能兼容几乎所有带时钟拉伸的从设备,多写几行代码换来的是广泛的兼容性。

4. 用STM32实操IIC:软件模拟代码+硬件外设+IO扩展芯片

4.1 为什么我建议先把软件模拟跑通

很多人问我,STM32不是有硬件IIC外设吗,为什么还要用软件模拟?这里有个背景:STM32早期的硬件IIC确实存在一些设计上的小毛病,状态机复杂、事件标志容易弄混,加上HAL库对IIC的封装处理也比较绕,很多教程干脆建议直接用软件模拟。即便现在芯片和库都改进了不少,我还是建议学习阶段先手写一遍软件IIC——因为只有自己把SCL和SDA的每一拍都控制过,才能真正理解时序里那些微妙的细节。

软件模拟IIC的好处是逻辑透明,出问题能直接看到是卡在哪个等待上。缺点是占CPU、时序精度取决于主频和延时函数的一致性。在STM32上跑400kHz,软件模拟是能扛住的;但你要是用Arduino的digitalWrite一个引脚一个引脚翻转,速度就会很惨。我一般用寄存器直接操作IO口翻转,延时用空循环或者定时器微秒级延时,实测在72MHz的STM32F103上软件模拟400kHz没有问题。

4.2 软件模拟IIC的核心代码

我用STM32标准库风格写一套最精简的软件IIC主站代码,方便直接移植。

先定义IO口操作宏(以PB6为SCL、PB7为SDA为例):

#define SCL_H GPIOB->BSRR = GPIO_Pin_6 #define SCL_L GPIOB->BRR = GPIO_Pin_6 #define SDA_H GPIOB->BSRR = GPIO_Pin_7 #define SDA_L GPIOB->BRR = GPIO_Pin_7 #define SDA_READ (GPIOB->IDR & GPIO_Pin_7)

要注意的是,SDA引脚要配置成开漏输出,这样在读的时候可以直接切换成输入模式读电平,不需要在输出和输入之间切换:开漏输出在高电平时引脚处于高阻态,外部上拉会把电平拉高,此时读IDR寄存器也能读到正确电平。

起始条件函数:

void IIC_Start(void) { SDA_H; SCL_H; delay_us(5); // 建立时间 SDA_L; // SCL高期间SDA拉低,产生START delay_us(5); SCL_L; // 拉低SCL准备传数据 }

停止条件函数:

void IIC_Stop(void) { SDA_L; SCL_H; delay_us(5); SDA_H; // SCL高期间SDA拉高,产生STOP delay_us(5); }

写一个字节并检查ACK:

uint8_t IIC_WriteByte(uint8_t data) { uint8_t i, ack; for (i = 0; i < 8; i++) { if (data & 0x80) SDA_H; else SDA_L; data <<= 1; delay_us(2); SCL_H; // 等待从机时钟拉伸 while (SCL_READ == 0); delay_us(2); SCL_L; } // 第9个时钟,读ACK SDA_H; // 释放SDA delay_us(2); SCL_H; while (SCL_READ == 0); ack = SDA_READ; // 为0表示从机应答 delay_us(2); SCL_L; return ack; }

读一个字节,形参为0表示发送ACK,为1表示发送NACK:

uint8_t IIC_ReadByte(uint8_t ack_flag) { uint8_t i, data = 0; SDA_H; // 释放SDA for (i = 0; i < 8; i++) { data <<= 1; SCL_H; while (SCL_READ == 0); delay_us(2); if (SDA_READ) data |= 0x01; SCL_L; delay_us(2); } if (ack_flag) { SDA_H; // 发送NACK } else { SDA_L; // 发送ACK } SCL_H; while (SCL_READ == 0); delay_us(2); SCL_L; SDA_H; return data; }

这段代码我把“等待SCL被拉高”的逻辑加进去了,这样就能兼容需要时钟拉伸的从设备。另外注意每次操作之间加的2微秒延时,这是为了保证满足IIC的建立时间和保持时间,具体数值只要不低于数据手册的最小要求就行,不用卡得很死。

写一次EEPROM的完整函数就变成:

void AT24C02_WriteByte(uint8_t addr, uint8_t data) { IIC_Start(); IIC_WriteByte(0xA0); // 设备地址,写方向 IIC_WriteByte(addr); // 写入的寄存器地址 IIC_WriteByte(data); // 数据 IIC_Stop(); delay_ms(10); // 等待写周期结束 }

读EEPROM时,注意先发一个伪写地址(写方向),再发重复起始和读方向地址:

uint8_t AT24C02_ReadByte(uint8_t addr) { uint8_t data; IIC_Start(); IIC_WriteByte(0xA0); // 写方向 IIC_WriteByte(addr); // 要读的地址 IIC_Start(); // 重复起始 IIC_WriteByte(0xA1); // 读方向 data = IIC_ReadByte(1); // 最后一个字节,返回NACK IIC_Stop(); return data; }

这套代码在STM32F1/F4上实测可以直接跑,逻辑简单,也方便调试。真要在工程里用,可以把这些函数封装成独立文件,读写接口对上就行。

4.3 STM32硬件IIC外设的配置要点

硬件IIC外设的优势是省CPU、响应快、能处理多主仲裁和DMA传输。但用之前要确认自己用的库版本跟芯片匹配。如果坚持用HAL库的硬件IIC,有几个配置点必须注意:

第一是GPIO配置,SCL和SDA都要配置成开漏输出、速度要够(GPIO_Speed_HIGH),同时使能内部上拉或者外部接好上拉电阻。第二是IIC的时钟配置,要根据系统时钟算好I2C时钟分频,让SCL稳定在设定的频率。第三是使能I2C外设的应答功能,默认就是ENABLE,但要确认在读取最后一个字节之前要关闭应答再产生停止条件,否则主机会多收一个字节。

HAL库使用I2C的阻塞式传输HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive是最简单的。但有个小坑:HAL库I2C的接收函数,你传入的缓冲长度如果是1,它会在收完这1个字节后自动发NACK并产生STOP;但如果你传入的缓冲长度大于1,它会在除最后一个字节外的所有字节都自动回ACK。这一点不搞清楚,读EEPROM很容易数据错位。

4.4 IO扩展芯片实操:PCF8574挂4个就能扩展16路

热搜词里有“iic io扩展芯片”,这块确实是IIC在项目里的高频应用。最典型的器件是PCF8574:一个8位IO扩展芯片,IIC地址是0x20加上三个引脚A0/A1/A2的电平设置,也就是0x20到0x27,一片能扩展8个IO,一个IIC总线上最多挂8片,扩展出64个IO。

PCF8574的用法极其简单,就一个寄存器。写数据:主设备发设备地址+一个字节数据,这个字节的每一位对应P0到P7的输出电平。读数据:主设备发设备地址+读方向,从设备把当前引脚电平发出来。

下面是一个控制4片PCF8574的示例,把总线上的8个地址全部扫描一遍:

uint8_t pcf_addr[] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}; for (int i = 0; i < 8; i++) { if (IIC_WriteByte(pcf_addr[i]) == 0) // 返回0表示有ACK { printf("PCF8574 found at 0x%02X\n", pcf_addr[i]); IIC_Stop(); } }

扫描总线的思路是非常通用的调试手段,遇到任何IIC设备,先写一个扫描程序把所有地址试一遍,能确认设备到底在哪个地址上、是否正常应答。这里要注意:扫描时向每个地址发一个字节,通常用写方向发0x00,如果返回ACK就说明这个地址有设备响应。

PCF8574输出的时候,初始状态要提前想好。它上电后所有引脚是高电平,而不是低电平。很多控制继电器的电路默认高电平是“断开”,那没问题;但如果你接的是灌电流负载,上电瞬间所有IO都是高,会把负载全部误触发。项目上遇到过控制LED灯条,上电就全亮,后来在初始化函数里先写一个所有引脚置低的值,才解决。

5. IIC通信异常的快速定位:波形台阶、死锁、ACK丢失排查

5.1 波形出现台阶:先别急着怪代码

“IIC波形出现台阶”是很多人在示波器上看到的异常现象,我最早遇到时以为是逻辑分析仪采样率不够或者探头接触不良,后来才发现是实实在在的信号问题。

波形台阶的核心原因是多个设备同时驱动总线或者总线电容分布不均匀。具体场景有两种。一种是总线上某个设备在SCL低电平期间释放SDA的同时,另一个设备还没释放完,两边一先一后放开,SDA的电压曲线就会出现两段爬升,形成台阶。另一种是总线电容过大且分布不均,信号上升沿被不同位置的大电容“拖”出阶梯状,尤其是连接线较长的时候。

识别台阶后,先确认是不是上升沿的RC效果:在示波器上量一下台阶发生的位置,如果每个通信波形里台阶都出现在同一个位置,多半是跟某个从设备的行为有关;如果台阶出现在长距离走线的末端,则是电容不均导致的。前者重点查从设备的上电时序和复位是否干净,后者优先减小上拉电阻、缩短线缆或降低速率。

还有一种台阶是SDA电平在SCL高电平期间不稳定,这就不只是“台阶”了,而是数据冲突。IIC规范要求SCL高期间SDA稳定,如果这个期间SDA还在跳变,从机可能把它误识别成START/STOP条件,整个通信就崩了。出现这种情况,多半是软件模拟IIC时SDA翻转和SCL翻转的先后顺序没按协议来。

5.2 SDA一直为低:总线死锁的恢复方法

现象是:通信一次之后,SDA一直被拉低,发什么命令都没反应,总线像被锁死了。

死锁的典型成因是主设备在通信中途异常复位(看门狗、断电、程序卡死),从设备还维持着对SDA的低电平输出。此时主设备认为自己刚上电,一切都正常,但SDA已经被从机拽住,起始条件根本发不出来,因为SDA无法从高跳低。

恢复办法有两个。第一个是“9个时钟法”:让主机连续产生9个SCL时钟脉冲,并且在这期间保持SDA为高。开漏结构下,第9个时钟后从机状态机会被复位,释放SDA。注意9个时钟期间SDA必须保持高,拉低会变成普通数据,起不到复位效果。第二个办法是给从机断电复位,或者直接发一个START信号——如果软件上能检测到SDA异常为低,先拉低SCL做几个空转时钟。

这个问题的根源是协议机制里没有“看门狗”的概念,双方都不知道对方什么时候会突然中断。工程上的经验是:主设备代码里加IIC超时检测,任何等待SCL拉高的循环都要有超时退出机制,不能死等。同时,通信的最后一帧一定要发STOP,不要在数据发一半时断电或者复位。

5.3 ACK丢失和通信飘忽不定:常见问题速查

现象可能原因优先级排查方向
寻址后无ACK设备地址错误用总线扫描程序逐一尝试地址
寻址后无ACK上拉电阻没接或阻值过大示波器看SDA上升沿是否过于平缓
寻址后无ACK从设备未上电或复位异常量从设备的VCC和RESET引脚电平
读写中途丢失ACK速率过快,不满足从设备时序降到100kHz试一下,能稳定说明就是时序问题
通信时好时坏电源纹波或地线电位漂移共地是否可靠,VCC是否稳定
通信时好时坏SCL干扰,线缆过长示波器看SCL高电平期间有没有毛刺
数据错位读操作少了NACK确认最后一个字节发的是NACK
数据错位主机发了多余时钟查软件模拟IIC的循环次数是否多跑一拍

通信飘忽不定是最让人头疼的。我调试过一个项目,传感器数据十次里有两三次是错的,程序逻辑反复检查都没毛病。后来用示波器看波形,发现SCL高电平时SDA上有微弱的毛刺,排查到是传感器线缆和电源线走得太近,串扰过来的。把线缆分开、加磁环之后问题消失。这种问题在纯逻辑层面永远找不到,所以IIC调试一定要养成“先看波形、再改代码”的习惯。

5.4 用逻辑分析仪快速定位IIC异常

调试IIC,示波器不是每次都方便,特别是出差、现场调试的时候,一个USB逻辑分析仪比示波器轻便得多,采样率也不用太高——测100kHz的IIC,采样率4MHz足够。

逻辑分析仪抓IIC波形的要点是:触发条件设成SDA下降沿(起始条件),采样深度要能覆盖完整的一帧。抓到之后,先看起始条件是否合法,然后看地址字节对不对,再看ACK位是否存在,最后数一下时钟个数——整个帧的SCL脉冲数是不是“字节数乘以9”再加起始停止的边沿。多数时候看一眼波形就能定位问题。

如果是软件模拟IIC,还可以在代码里加上调试断言:在关键步骤翻转一个空闲GPIO,用示波器观察这个GPIO和IIC波形的对应关系。比如在发送起始条件前、发送停止条件后各翻转一次引脚,就能确认软件流程到底走到哪一步。这个方法看起来土,但在定位“卡死在哪里”的时候效率比在代码里插打印高得多。

6. 我在实际项目里的一些习惯和忠告

做嵌入式这些年,IIC相关的调试经验攒了不少,最后分享几条个人觉得特别管用的做法。

第一,所有IIC通信代码必须带超时。不管是用硬件外设还是软件模拟,等待ACK或者等待SCL拉高的循环,一定要设一个超时上限。IIC总线上什么幺蛾子都可能出现,没有超时的代码一旦总线卡死,整个系统就跟着完蛋。这个超时不说精确到微秒,随便一个几百上千次的计数都行。

第二,新板子第一次上电,先别写业务代码,先把总线扫描跑起来。这能验证设备地址、上拉电阻、硬件焊接是否正常,也能确认多个设备的地址是否有冲突。扫描代码几十行,能省后面一整天的调试时间。

第三,调试阶段先跑100kHz,稳定了再往上提。很多项目功能验证阶段就跑400kHz,出了问题就开始怀疑时序、怀疑代码,结果降速一试全部正常。速率这个东西能做多快,得在保证最恶劣供电、最长线缆的前提下才算数。

第四,软件模拟IIC和硬件IIC各有用武之地。硬件IIC适合大数据量传输和主中断驱动的场景,软件模拟IIC适合时序要精确控制、设备行为不规范的场景。两个都掌握,根据情况选,才是工程上该有的态度。我自己的项目里,凡是只需要温度、湿度这类小数据传感器的,优先用软件模拟,出问题好查;凡是Flash、触摸屏这类大数据量的才开硬件外设。

返回列表