I2C这东西,刚入行的时候觉得它简单得不行——两根线,一根时钟一根数据,挂一堆从设备,地址一喊谁应答谁说话,能有多难?结果真到了调试现场,波形抓出来一看,上升沿软塌塌像条抛物线,多主系统里两个主机同时开口把总线锁死,EEPROM写一半丢数据,逻辑分析仪上START条件后面跟着一串莫名其妙的NACK。这时候才明白,I2C的坑全藏在物理层和仲裁机制这些"看不见"的地方。这篇内容就是把这一个礼拜踩过的坑、翻过的手册、抓过的波形整理出来,从开漏输出为什么必须配上拉电阻讲起,一路拆到多主仲裁的位级竞争,再到RTL实现里那些容易翻车的时序细节。不管你是刚接触I2C的嵌入式新手,还是写过几版I2C控制器但总在边界条件上翻车的RTL工程师,应该都能从里面找到点有用的东西。
1. 开漏输出与上拉电阻:I2C物理层的根基
1.1 为什么I2C死活不用推挽输出
先把这个最基础的问题说清楚。推挽输出(Push-Pull)的结构是一对互补的MOS管,上管导通时输出拉高到VDD,下管导通时输出拉低到GND,驱动能力强,上升沿下降沿都干脆利落。SPI用的就是推挽,所以SPI能跑到几十兆甚至上百兆。但I2C偏偏选了开漏(Open-Drain),输出级只有下管,上管根本不存在,想输出高电平?对不起,只能靠外部上拉电阻把线拉上去。
这个选择不是设计者偷懒,而是被"线与"(Wired-AND)逻辑逼出来的。I2C总线上可以挂多个设备,每个设备的SDA和SCL引脚都是开漏结构。只要有一个设备把线拉低,整条线就是低电平;只有当所有设备都释放总线(输出高阻态),上拉电阻才能把线拉到高电平。这就实现了硬件级别的"线与"——不需要任何额外的逻辑门,两根线就能完成多设备的冲突检测。
如果用推挽输出会怎样?假设设备A想输出高电平,上管导通把线拉到VDD;设备B想输出低电平,下管导通把线拉到GND。两个管子同时导通,VDD到GND之间形成低阻抗通路,瞬间大电流灌过去,轻则烧管子,重则整条总线上的设备一起冒烟。所以开漏不是可选项,是I2C多设备共享总线的必要条件。
注意:有些MCU的I2C引脚可以配置成推挽模式,如果你只挂一个从设备且确定不会有总线冲突,推挽确实能跑更高的速率。但一旦总线上有第二个设备,或者从设备需要拉低SCL做时钟拉伸(Clock Stretching),推挽就会出大问题。我个人的建议是:只要用I2C协议,就老老实实配成开漏。
1.2 上拉电阻的取值:不是随便找个4.7k就行
上拉电阻选多大,是I2C物理层设计里最容易被忽视、又最容易出问题的环节。很多人看别人原理图上写4.7k,自己也跟着写4.7k,结果换了个板子、换了个速率、换了个供电电压,通信就时好时坏。
上拉电阻的取值受两个方向的约束。往小了说,电阻太小会导致低电平灌电流过大。I2C规范规定,标准模式(100kHz)和快速模式(400kHz)下,器件拉低SDA/SCL时,引脚上的低电平电压VOL不能超过0.4V。假设VOL=0.4V,VDD=3.3V,那么灌电流IOL=(3.3-0.4)/Rp。如果Rp=1k,灌电流就是2.9mA;如果Rp=470Ω,灌电流就飙到6.2mA。大部分I2C器件的IOL额定值在3mA左右,超过这个值,输出级可能进入非线性区,VOL会进一步升高,甚至损坏器件。
往大了说,电阻太大会导致上升沿太慢。I2C总线的上升时间tr受RC时间常数限制,tr≈0.847×Rp×Cb,其中Cb是总线电容。I2C规范对上升时间有明确要求:标准模式最大1000ns,快速模式最大300ns,快速模式+最大120ns。假设总线电容Cb=200pF(这是很常见的值,PCB走线、引脚电容、连接器加起来轻松到200pF),快速模式下tr≤300ns,代入公式:Rp≤300ns/(0.847×200pF)≈1.77kΩ。也就是说,在400kHz速率、200pF总线电容的条件下,上拉电阻不能超过1.77kΩ。
但实际选型还要留余量。我一般会按以下步骤来算:
- 确认总线电容Cb。用LCR表实测最准,没有的话按经验估算:每米PCB走线约1pF,每个器件引脚约10pF,每个连接器约5pF。
- 根据目标速率查规范里的最大上升时间tr_max。
- 计算Rp_max = tr_max / (0.847 × Cb)。
- 根据器件的IOL和VOL_max计算Rp_min = (VDD - VOL_max) / IOL。
- 在Rp_min和Rp_max之间选一个标准值,通常取中间偏小一点,给上升沿留余量。
举个例子:VDD=3.3V,Cb=150pF,目标400kHz快速模式,tr_max=300ns。Rp_max=300ns/(0.847×150pF)≈2.36kΩ。假设器件IOL=3mA,VOL_max=0.4V,Rp_min=(3.3-0.4)/3mA≈967Ω。那么Rp可以在1k到2.2k之间选,我一般选1.5k或1.8k。
| 参数 | 标准模式(100kHz) | 快速模式(400kHz) | 快速模式+(1MHz) |
|---|---|---|---|
| 最大上升时间tr | 1000ns | 300ns | 120ns |
| 最大总线电容Cb | 400pF | 400pF | 550pF |
| 典型上拉电阻范围 | 4.7k-10k | 1.5k-4.7k | 1k-2k |
1.3 上拉电阻小了不通信:一个反直觉的故障现象
热词里有个"i2c上拉电阻小了不通信",这个现象确实存在,而且很多人第一次遇到会懵——电阻小了不是上升沿更快吗,怎么会不通信?
原因通常有两个。第一个是灌电流超限。前面说了,电阻太小,器件拉低时灌电流超过IOL额定值,VOL升高。如果VOL升高到超过接收端的VIL_max(输入低电平最大阈值,通常是0.3×VDD=0.99V),接收端就认为这根线还是高电平,根本识别不到低电平,通信自然失败。我实测过一颗老型号的EEPROM,IOL只有2mA,上拉电阻用1k时VOL直接飙到0.8V,逻辑分析仪上看低电平只有0.8V,虽然勉强能识别,但噪声容限已经很小了,稍微有点干扰就误判。
第二个原因是多个设备同时拉低时的电流叠加。如果总线上挂了8个设备,每个设备都试图把线拉低,虽然开漏结构下它们共享同一个上拉电阻,但每个设备的下管都会分走一部分电流。如果上拉电阻太小,总灌电流可能超过单个器件的承受能力,导致某个器件先损坏,然后整条总线瘫痪。
实操心得:调试I2C通信失败时,先别急着怀疑协议或代码,拿示波器看低电平电压。如果低电平明显高于0.4V,先把上拉电阻换大一点试试。我遇到过好几次都是电阻太小导致的,换成2.2k立刻就好了。
2. 多主仲裁:两根线上的位级战争
2.1 仲裁到底在争什么
多主系统里,总线上挂了不止一个主机。两个主机可能同时想发起传输,这时候就需要仲裁机制来决定谁先说话。I2C的仲裁是"无损"的——输掉仲裁的主机不会丢失数据,只是自动退出,等总线空闲后再重试。赢家甚至不知道发生过仲裁,因为它的数据完全没受影响。
仲裁的核心机制是:每个主机在发送每一位数据的同时,也在监听SDA线上的实际电平。如果自己发的是高电平,但读回来的是低电平,说明有另一个主机在拉低SDA,自己输了,立刻退出。如果自己发的是低电平,读回来也是低电平,那可能是自己拉的,也可能是别人拉的,但至少不冲突,继续发。
这里的关键是:仲裁只发生在SDA线上,SCL线不参与仲裁。因为所有主机都用同一个时钟频率(或者至少是兼容的频率),SCL上的时钟信号是同步的。但SDA上的数据可能不同,冲突就在SDA上解决。
2.2 仲裁的位级过程拆解
假设主机A和主机B同时发起传输,它们的SCL是同步的(实际上是通过线与实现的,谁拉低SCL,SCL就是低)。在每一个SCL高电平期间,SDA上的数据必须稳定。
第一个可能冲突的位是地址位。假设主机A要访问地址0x50,主机B要访问地址0x51。地址0x50的二进制是1010000,0x51是1010001。前6位完全相同,第7位(最低位)A是0,B是1。当发送到第7位时,A拉低SDA(发0),B释放SDA(发1)。由于线与逻辑,SDA实际是低电平。B读回SDA发现是低电平,但自己发的是高电平,知道自己输了,立刻停止发送,释放总线。A继续传输,完全不受影响。
如果两个主机要访问同一个地址呢?那就要看数据位了。假设它们都要写同一个EEPROM,但写的数据不同。数据位上的冲突同样按位仲裁,谁先发出低电平谁赢。如果两个主机发的数据完全一样,那仲裁不会发生冲突,两个主机都会以为自己是赢家——但这时候从设备会收到两份相同的数据,实际上只执行一次,因为数据是一样的,不会造成问题。
注意:仲裁过程中,输掉的主机必须立即释放SDA和SCL,但释放的时机有讲究。如果输在SDA上,SCL可能还在被赢家控制,输家不能去拉低SCL,否则会干扰赢家的时钟。所以输家要等到当前字节传输结束,检测到STOP条件或者总线空闲后,才能重新发起传输。
2.3 仲裁丢失后的重试策略
仲裁丢失不是错误,是正常的总线竞争。但重试策略设计不好,会导致两个主机反复碰撞,总线利用率极低。
最简单的策略是:仲裁丢失后立即重试。但如果两个主机的重试时机完全一样,它们会再次同时发起传输,再次碰撞。所以需要引入随机退避。我一般会在固件里加一个随机延迟,延迟时间在0到几个字节传输时间之间随机取值。这样两个主机下次发起传输的时机就错开了。
更优雅的做法是让主机在仲裁丢失后,先监听总线一段时间,确认总线空闲后再发起。但监听时间也要随机化,否则还是可能碰撞。
在RTL实现里,仲裁丢失的处理需要特别小心。状态机要从"发送"状态跳转到"监听"状态,同时清空发送缓冲区,准备重试。如果状态机设计得不好,可能会在仲裁丢失后卡在某个中间状态,导致总线死锁。
2.4 时钟同步与时钟拉伸的交互
多主系统里,SCL的时钟同步是通过线与逻辑实现的。每个主机在SCL高电平期间开始计时,如果自己的高电平时间到了,但SCL还是低(被别的设备拉低),就继续等。只有当所有主机都释放SCL,SCL才真正变高。这就实现了时钟同步——慢的主机会把快的主机拖慢。
时钟拉伸(Clock Stretching)是另一种机制,从设备可以拉低SCL来暂停传输,等自己准备好数据再释放。在多主系统里,时钟拉伸和仲裁可能同时发生。如果从设备在仲裁期间拉低SCL,主机们会以为SCL被其他主机控制,继续等待。这通常不会造成问题,但如果从设备拉伸时间过长,主机可能会超时退出。
实操心得:调试多主系统时,逻辑分析仪一定要开"协议解码"功能,把SDA和SCL的波形解码成地址、数据、ACK/NACK。这样仲裁过程一目了然。我用的Saleae Logic Pro 16,解码I2C很稳,还能同时抓4路I2C总线,对比分析很方便。
3. I2C时序的魔鬼细节:从START到STOP
3.1 START和STOP条件的精确时序
START条件(S):SCL为高电平时,SDA从高变低。STOP条件(P):SCL为高电平时,SDA从低变高。这两个条件的时序要求非常严格,因为它们是I2C协议里唯一允许SDA在SCL高电平期间变化的情况。
具体参数:START条件的建立时间tSU;STA,标准模式最小4.7μs,快速模式最小0.6μs。START条件的保持时间tHD;STA,标准模式最小4.0μs,快速模式最小0.6μs。STOP条件的建立时间tSU;STO,标准模式最小4.0μs,快速模式最小0.6μs。
这些时间看起来很长,但在高速传输时很容易被忽略。比如在400kHz快速模式下,一个SCL周期是2.5μs,tSU;STA=0.6μs占了将近四分之一周期。如果RTL里的状态机没有精确控制SDA的变化时机,很容易违反这个时序。
3.2 数据位的建立和保持时间
数据位在SCL低电平期间变化,在SCL高电平期间必须稳定。具体参数:数据建立时间tSU;DAT,标准模式最小250ns,快速模式最小100ns。数据保持时间tHD;DAT,标准模式最小0ns(是的,0ns,因为SCL下降沿之后数据可以立即变化),快速模式最小0ns。
这里有个容易踩的坑:tHD;DAT=0ns意味着SCL下降沿之后,SDA可以立即变化。但如果SDA变化太快,可能会在SCL下降沿附近产生毛刺,被从设备误判。我一般会在RTL里加一点延迟,让SDA在SCL下降沿之后至少保持几十纳秒再变化。
3.3 ACK/NACK的采样时机
ACK/NACK位是接收方在SCL高电平期间拉低SDA(ACK)或释放SDA(NACK)。发送方在SCL高电平期间采样SDA,判断是否收到ACK。
关键参数:ACK建立时间tSU;DAT,和普通数据位一样。ACK保持时间tHD;DAT,也是0ns。但ACK的采样时机很关键——发送方必须在SCL高电平的中间位置采样,太早可能还没稳定,太晚可能已经变化。
在RTL实现里,我一般会在SCL高电平的中间点(比如高电平持续时间的50%处)采样SDA。这样即使有轻微的时序偏差,也能采到正确的值。
3.4 总线空闲与超时检测
总线空闲的定义是:SDA和SCL都保持高电平超过一定时间(通常是tBUF,总线空闲时间,标准模式4.7μs,快速模式1.3μs)。如果总线空闲时间不够,主机不应该发起新的传输。
超时检测是防止总线死锁的重要机制。如果SCL被某个设备一直拉低(比如从设备死机了),主机等待超过一定时间后应该复位总线。复位方法是:主机发送9个SCL脉冲,然后发送STOP条件。这9个脉冲可以让卡住的从设备完成当前字节的传输,释放SDA,然后STOP条件复位整个总线状态。
实操心得:我在RTL里实现I2C控制器时,一定会加一个超时计数器。如果SCL低电平持续超过10ms(这个值可以配置),就触发总线复位。这个机制救过我好几次——有一次一个从设备固件有bug,在传输过程中死机了,SCL一直被拉低,整个系统卡死。加了超时复位后,系统能自动恢复。
4. RTL实现I2C控制器的关键设计决策
4.1 时钟分频与SCL生成
I2C控制器的时钟分频模块负责从系统时钟生成SCL。假设系统时钟是50MHz,目标SCL是400kHz,分频系数就是50MHz/(400kHz×2)=62.5。因为SCL的高电平和低电平各占半个周期,所以分频系数要除以2。
分频系数不是整数时,需要做小数分频或者用相位累加器。我一般用相位累加器:每个系统时钟周期,累加器加上一个步进值,累加器溢出时翻转SCL。步进值=目标频率×2^N/系统频率,N是累加器位宽。这样可以得到非常精确的SCL频率,而且频率切换很方便。
但要注意:SCL的占空比不一定是50%。I2C规范只要求高电平和低电平的最小时间,不要求占空比。我一般设成50%,这样时序余量最大。
4.2 移位寄存器的设计
I2C的数据传输是MSB先出。发送时,移位寄存器在SCL低电平期间左移,最高位输出到SDA。接收时,在SCL高电平期间采样SDA,存入移位寄存器的最低位。
移位寄存器的位宽通常是8位(一个字节)。但地址帧可能是7位地址+1位读写位,也可能是10位地址。10位地址需要两个字节传输,第一个字节是11110XX+读写位,第二个字节是地址的低8位。所以移位寄存器要能处理不同长度的帧。
我一般用一个8位的移位寄存器,外加一个位计数器。位计数器从7递减到0,表示当前发送的是第几位。当位计数器减到0时,一个字节传输完成,进入ACK/NACK阶段。
4.3 状态机的设计
I2C控制器的状态机是核心。我一般设计成以下几个状态:
- IDLE:总线空闲,等待发起传输
- START:发送START条件
- SEND_ADDR:发送地址字节
- CHECK_ACK:检查从设备的ACK
- SEND_DATA:发送数据字节
- RECV_DATA:接收数据字节
- SEND_ACK:发送ACK/NACK
- STOP:发送STOP条件
- WAIT:等待总线空闲
每个状态之间的跳转条件要精确控制。比如从SEND_ADDR到CHECK_ACK,必须在第8个SCL下降沿之后跳转。从CHECK_ACK到SEND_DATA或STOP,取决于是否收到ACK。
仲裁丢失的处理:在SEND_ADDR和SEND_DATA状态,每个SCL高电平期间都要比较SDA的输出值和输入值。如果不一致,跳转到ARBITRATION_LOST状态,释放总线,等待重试。
4.4 时钟拉伸的处理
从设备拉低SCL时,主机必须等待。在RTL里,这通过检测SCL输入来实现。当主机释放SCL(输出高阻态)后,如果SCL输入还是低,说明从设备在拉伸时钟,主机进入WAIT状态,直到SCL变高才继续。
这里有个细节:主机释放SCL后,不能立即检测SCL输入,要等几个纳秒让上拉电阻把线拉高。如果立即检测,可能会因为线还没拉高而误判为时钟拉伸。我一般会加一个小的延迟(比如2-3个系统时钟周期),然后再检测。
4.5 跨时钟域的处理
如果I2C控制器的系统时钟和I2C总线的时钟不同步(通常都是不同的),就需要处理跨时钟域问题。SDA和SCL是异步信号,直接采样可能会有亚稳态。
我一般用两级同步器来同步SDA和SCL输入。第一级寄存器的输出可能不稳定,第二级寄存器的输出就稳定了。但两级同步器会引入两个时钟周期的延迟,在高速I2C下(比如1MHz),两个50MHz时钟周期是40ns,占SCL周期的4%,可以接受。
对于SCL的边沿检测,我一般用同步后的SCL信号做边沿检测。上升沿检测:当前周期为高,上一周期为低。下降沿检测:当前周期为低,上一周期为高。
实操心得:跨时钟域处理是I2C控制器RTL实现里最容易出bug的地方。我建议在仿真时加入随机延迟,模拟亚稳态,看看状态机是否能正确处理。另外,综合后的时序报告一定要看,确保同步器的建立时间和保持时间满足要求。
5. 逻辑分析仪抓I2C波形的实战技巧
5.1 采样率的选择
逻辑分析仪的采样率至少要是I2C速率的10倍。400kHz的I2C,采样率至少4MHz。但实际用的时候,我一般会设到20MHz以上,这样能看清上升沿的细节,也方便解码。
如果采样率太低,可能会漏掉窄脉冲,导致解码错误。比如START条件,SDA从高变低的时间可能只有几十纳秒,采样率不够就抓不到。
5.2 触发的设置
抓I2C波形,触发条件很关键。我一般用START条件触发:SCL为高时,SDA从高变低。这样每次传输都能抓到完整的帧。
如果要抓特定的地址或数据,可以用协议解码后的触发。Saleae Logic软件支持在解码后的数据上触发,比如"地址等于0x50时触发"。这样能精准抓到目标设备的传输。
5.3 解码的注意事项
逻辑分析仪的解码器需要正确配置:SDA和SCL的通道要对应,时钟频率要设置正确,地址位数(7位或10位)要选对。
解码结果里,ACK/NACK的显示很关键。如果解码器显示NACK,说明从设备没有应答,可能是地址不对、从设备没上电、或者从设备忙。如果显示ACK但数据不对,可能是时序问题或者从设备内部寄存器地址不对。
5.4 常见波形异常的分析
上升沿太慢:波形像抛物线,上升时间超过规范。原因通常是上拉电阻太大或总线电容太大。解决方法是减小上拉电阻或减少总线上的设备。
低电平太高:低电平电压超过0.4V。原因通常是上拉电阻太小,灌电流超限。解决方法是增大上拉电阻。
SCL被拉低不释放:SCL一直低,总线死锁。原因可能是从设备死机或时钟拉伸超时。解决方法是发送9个SCL脉冲复位总线。
SDA和SCL串扰:SDA变化时SCL跟着抖动。原因通常是PCB走线太近,或者上拉电阻太小导致电流突变。解决方法是增加走线间距,或者调整上拉电阻。
| 波形异常 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 上升沿太慢 | 上拉电阻太大/总线电容太大 | 测上升时间,算RC常数 | 减小上拉电阻/减少设备 |
| 低电平太高 | 上拉电阻太小/灌电流超限 | 测低电平电压 | 增大上拉电阻 |
| SCL不释放 | 从设备死机/时钟拉伸超时 | 看SCL是否一直低 | 发送9个SCL脉冲复位 |
| SDA/SCL串扰 | 走线太近/电流突变 | 看SDA变化时SCL是否抖动 | 增加走线间距/调整电阻 |
6. I2C与相关协议的边界:什么时候该换方案
6.1 I2C vs SPI:速率与引脚数的权衡
SPI用4根线(CS、SCK、MOSI、MISO),全双工,推挽输出,速率可以到几十MHz。I2C用2根线,半双工,开漏输出,速率通常到400kHz或1MHz。如果应用需要高速传输(比如LCD屏、高速ADC),SPI更合适。如果应用需要挂很多设备但引脚有限(比如传感器阵列),I2C更合适。
我一般这样选:如果总线上设备超过3个,且速率要求不高(<1MHz),用I2C。如果设备少但速率要求高(>10MHz),用SPI。如果设备多且速率要求高,可以考虑I3C(I2C的升级版,速率到12.5MHz,还兼容I2C)。
6.2 I2C vs UART:同步与异步的区别
UART是异步通信,没有时钟线,靠波特率匹配来同步。I2C是同步通信,有时钟线,靠SCL来同步。UART通常用于点对点通信(两个设备之间),I2C用于总线通信(多个设备共享)。
UART的优点是简单,不需要时钟线,适合远距离通信(加上RS-485收发器可以到千米级)。I2C的优点是总线结构,适合板内多设备通信。
6.3 I2C vs CAN:物理层容错的差异
CAN总线和I2C都是总线结构,都支持多主。但CAN的物理层是差分信号,抗干扰能力强,适合汽车、工业等恶劣环境。I2C是单端信号,抗干扰能力弱,适合板内短距离通信。
CAN的仲裁机制和I2C类似,也是基于标识符的位级仲裁。但CAN的标识符有11位或29位,仲裁优先级由标识符数值决定(数值越小优先级越高)。I2C的仲裁优先级由地址决定,地址越小优先级越高。
热词里有个"can通信物理层容错测试-故障排查需要增加终端电阻吗",这个问题和I2C无关,但可以对比一下:CAN需要终端电阻(通常是120Ω)来匹配阻抗,减少反射。I2C不需要终端电阻,但需要上拉电阻。两者的物理层设计思路完全不同。
6.4 I2C vs PMBus:协议层的扩展
PMBus是基于I2C的电源管理协议,物理层和I2C完全一样,但协议层增加了电源管理相关的命令和数据结构。PMBus的速率通常是100kHz或400kHz,和I2C一样。
如果你在用I2C控制电源芯片,很可能用的就是PMBus协议。PMBus的地址分配、命令格式、数据格式都有明确规定,不能随便改。调试PMBus时,逻辑分析仪的解码器要选PMBus,而不是I2C,否则解码结果可能不对。
7. I2C扩展与进阶应用
7.1 I2C多路复用器:解决地址冲突
总线上挂多个相同地址的设备时,I2C多路复用器(比如TCA9548A)可以把总线分成多路,每路挂一个设备。主机通过多路复用器的地址选择哪一路导通,然后和该路上的设备通信。
TCA9548A的地址是0x70-0x77(通过A0-A2引脚配置),内部有8个通道。主机先写TCA9548A的控制寄存器,选择通道,然后正常和从设备通信。切换通道时,要先发送STOP条件,再重新START。
注意:TCA9548A的通道切换有建立时间,切换后要等一段时间才能通信。我一般等1ms,确保通道稳定。
7.2 I2C缓冲器:延长总线距离
I2C总线的电容限制是400pF,超过这个值通信就不稳定。如果总线要走很长(比如背板上的多个板卡),就需要I2C缓冲器(比如PCA9515)来分段驱动。
缓冲器把总线分成两段,每段独立驱动,电容不累加。但缓冲器会引入延迟,而且缓冲器两侧的电压可能不同(比如一侧3.3V,一侧5V),需要电平转换。
7.3 I2C GPIO扩展:用两根线控制多个IO
I2C GPIO扩展芯片(比如PCA9555)可以用两根线控制16个GPIO。主机通过I2C读写PCA9555的寄存器,控制每个GPIO的方向和电平。
PCA9555的地址是0x20-0x27(通过A0-A2配置),内部有输入寄存器、输出寄存器、极性反转寄存器、配置寄存器。配置寄存器设置每个引脚是输入还是输出,输出寄存器设置输出电平,输入寄存器读取输入电平。
7.4 I2C在RTL中的仿真验证
写完I2C控制器的RTL后,必须做仿真验证。我一般用以下测试用例:
- 单字节写:START + 地址 + 数据 + STOP
- 单字节读:START + 地址 + 读命令 + 数据 + NACK + STOP
- 多字节写:START + 地址 + 数据1 + 数据2 + ... + STOP
- 多字节读:START + 地址 + 读命令 + 数据1 + ACK + 数据2 + NACK + STOP
- 仲裁丢失:两个主机同时发起传输,验证输家是否正确退出
- 时钟拉伸:从设备拉低SCL,验证主机是否正确等待
- 超时复位:从设备一直拉低SCL,验证主机是否触发复位
仿真时要用真实的I2C从设备模型,或者用Verilog写一个简单的从设备模型。从设备模型要能响应ACK/NACK,能拉低SCL做时钟拉伸,能模拟仲裁。
实操心得:仿真时一定要加时序检查。我一般在testbench里加assertion,检查START/STOP条件的建立时间和保持时间,检查数据位的建立时间和保持时间。如果违反时序,assertion会报错,比看波形快多了。
8. 从波形到代码:一个完整的I2C EEPROM读写实例
8.1 EEPROM的I2C时序特点
以AT24C02为例,这是最常用的I2C EEPROM,容量2Kbit(256字节),地址0x50-0x57(通过A0-A2配置)。写操作:START + 地址(写)+ 字地址 + 数据 + STOP。读操作:START + 地址(写)+ 字地址 + START + 地址(读)+ 数据 + NACK + STOP。
注意读操作里的"重复START"(Repeated START):在发送字地址后,不发送STOP,而是再发送一个START条件,然后发送读地址。这是I2C协议里很常见的操作,用于在不释放总线的情况下切换读写方向。
8.2 Verilog实现的关键代码片段
以下是I2C控制器发送START条件的Verilog代码片段:
// 状态机:发送START条件 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sda_out <= 1'b1; scl_out <= 1'b1; state <= IDLE; end else begin case (state) IDLE: begin if (start_tx) begin state <= START; sda_out <= 1'b1; scl_out <= 1'b1; end end START: begin // SCL为高时,SDA从高变低 scl_out <= 1'b1; sda_out <= 1'b0; state <= SEND_ADDR; end // ... 其他状态 endcase end end这段代码看起来简单,但有几个细节要注意:sda_out和scl_out是开漏输出,1表示高阻态(释放总线),0表示拉低。实际输出到引脚时,需要加上拉电阻。另外,START条件的时序要精确控制,sda_out从1变0的时机必须在scl_out为1期间。
8.3 读写EEPROM的完整流程
写EEPROM的完整流程:
- 主机发送START条件
- 主机发送地址字节(0x50<<1 | 0 = 0xA0),等待ACK
- 主机发送字地址(0x00-0xFF),等待ACK
- 主机发送数据字节,等待ACK
- 主机发送STOP条件
- 等待EEPROM内部写周期完成(通常5ms)
读EEPROM的完整流程:
- 主机发送START条件
- 主机发送地址字节(0xA0),等待ACK
- 主机发送字地址,等待ACK
- 主机发送重复START条件
- 主机发送地址字节(0xA1),等待ACK
- 主机接收数据字节,发送NACK
- 主机发送STOP条件
注意:EEPROM的写周期是5ms,在这期间EEPROM不响应任何I2C命令。如果在这期间发起新的传输,EEPROM会返回NACK。所以写操作后要等5ms再发起下一次传输。我一般用轮询的方式:写完后不断发起读操作,直到收到ACK,说明EEPROM准备好了。
8.4 调试EEPROM读写的常见问题
问题1:写完后立即读,读到旧数据。原因是没有等EEPROM写周期完成。解决方法是加5ms延迟,或者用轮询方式等待ACK。
问题2:读操作返回NACK。原因可能是地址不对、EEPROM没上电、或者EEPROM忙。解决方法是检查地址配置、检查电源、等待写周期完成。
问题3:数据位错误。原因可能是时序问题、上拉电阻不合适、或者总线电容太大。解决方法是检查波形、调整上拉电阻、减少总线上的设备。
问题4:多字节读写时地址不连续。原因可能是EEPROM的页写模式。AT24C02的页大小是8字节,跨页写会回卷到页首。解决方法是分页写,每页不超过8字节。
9. 那些年我踩过的I2C坑
9.1 上拉电阻的"经验值"陷阱
刚入行时,我看别人的原理图上写4.7k,自己也用4.7k。结果有一次做了一块板子,I2C通信时好时坏,有时候能读到数据,有时候全是0xFF。折腾了两天,最后拿示波器看波形,发现上升沿慢得离谱,从0V到3.3V花了将近2μs。算了一下,总线电容大概300pF,4.7k的上拉电阻,RC时间常数是1.41μs,上升时间约1.2μs,超过了快速模式的300ns限制。换成1.8k后,上升时间降到460ns,通信立刻稳定了。
这个坑的教训是:上拉电阻不能照抄,要根据总线电容和速率算。我后来养成了习惯,每块新板子都要测一下I2C波形的上升时间,确保满足规范。
9.2 多主仲裁的"幽灵冲突"
有一次做多主系统,两个MCU共享一条I2C总线。大部分时间通信正常,但偶尔会出现总线死锁,两个MCU都卡住。抓波形看了很久,发现是两个MCU同时发起传输,仲裁过程中一个MCU输了,但它没有正确释放SDA,导致总线电平异常。
查了RTL代码,发现仲裁丢失后的状态机跳转有问题:输家检测到仲裁丢失后,跳转到了WAIT状态,但在WAIT状态里没有释放SDA,SDA一直被拉低。修复方法是:仲裁丢失后立即释放SDA和SCL,然后跳转到WAIT状态。
这个坑的教训是:仲裁丢失的处理要精确到每一个信号。输家不仅要停止发送数据,还要释放所有输出,让赢家完全控制总线。
9.3 时钟拉伸的"超时误判"
有一次调试一个传感器,I2C通信偶尔失败。抓波形发现,传感器在传输过程中会拉低SCL做时钟拉伸,拉伸时间大概200μs。但我的I2C控制器超时设置是100μs,超过100μs就触发总线复位。结果传感器还在拉伸,控制器就复位了总线,通信失败。
修复方法是把超时时间改到1ms,给传感器足够的拉伸时间。但超时时间也不能太长,否则从设备真死机了,控制器要等很久才能复位。我一般设成10ms,兼顾两者。
这个坑的教训是:超时时间要根据从设备的时钟拉伸能力来设。查从设备手册,看最大时钟拉伸时间是多少,超时时间设成它的2-3倍。
9.4 逻辑分析仪的"解码陷阱"
有一次用逻辑分析仪抓I2C波形,解码结果全是乱码。检查了通道配置、时钟频率、地址位数,都没问题。后来发现是采样率设得太低,只有1MHz,而I2C速率是400kHz,采样率只有速率的2.5倍,漏掉了很多边沿。把采样率提到20MHz后,解码正常了。
这个坑的教训是:逻辑分析仪的采样率至少要是I2C速率的10倍,最好20倍以上。另外,解码器的配置要和实际协议匹配,7位地址和10位地址不能搞混。
9.5 EEPROM的"页写回卷"
有一次写EEPROM,从地址0x06开始写8个字节,结果写到0x08时数据回卷到了0x00,把前面的数据覆盖了。查了手册才知道,AT24C02的页大小是8字节,页边界是0x00、0x08、0x10...从0x06开始写8个字节,会写到0x06、0x07、0x00、0x01...回卷到页首。
这个坑的教训是:写EEPROM时要按页对齐,每页不超过页大小。如果数据跨页,要分多次写,每次写完等5ms。
10. 从I2C到I3C:下一代总线的演进
I2C用了四十多年,虽然稳定可靠,但速率和功能已经跟不上现代系统的需求。I3C(Improved Inter-Integrated Circuit)是I2C的升级版,保留了I2C的物理层(两根线、开漏输出、上拉电阻),但协议层做了大量改进。
I3C的速率可以到12.5MHz(SDR模式)甚至25MHz(HDR模式),比I2C快得多。I3C还支持动态地址分配(不需要固定的从设备地址)、带内中断(In-Band Interrupt,从设备可以直接向主机发中断)、热加入(Hot-Join,设备可以在总线运行时加入)。
但I3C的物理层还是开漏+上拉电阻,所以I2C的物理层设计经验(上拉电阻计算、总线电容限制、上升时间控制)在I3C里依然适用。如果你现在在搞I2C,将来要过渡到I3C,物理层的知识不会浪费。
实操心得:I3C的推挽模式(Push-Pull)在高速传输时使用,但START条件和仲裁还是用开漏模式。所以I3C的物理层设计比I2C复杂,需要支持两种输出模式。如果你在选型,注意看MCU是否支持I3C的推挽模式。
11. 写在最后:I2C的"简单"与"不简单"
I2C协议本身确实简单,两根线、几个状态、一套时序,半天就能看懂。但要把I2C用稳、用好,需要理解的远不止协议本身。开漏输出的物理层约束、上拉电阻的计算、多主仲裁的位级竞争、时钟拉伸的交互、RTL实现里的跨时钟域处理、逻辑分析仪的波形分析——每一个环节都有坑,每一个坑都可能让你调试好几天。
我个人的体会是:I2C的调试,70%的问题在物理层,20%在时序,10%在协议。所以遇到通信失败,先看波形,先测电平,先算上升时间。把物理层搞定了,协议层的问题往往迎刃而解。
最后分享一个小技巧:如果你手头没有逻辑分析仪,可以用MCU的GPIO模拟I2C,然后在关键位置翻转一个调试引脚,用示波器看调试引脚的波形,间接判断I2C的状态。虽然不如逻辑分析仪直观,但应急够用了。