1. 从"片内"到"片间":为什么总线是绕不开的那道坎
搞嵌入式的人迟早会撞上一堵墙:芯片内部那点事你摸得差不多了,寄存器、中断、DMA、时钟树,翻来覆去就那些花样。可一旦你的系统里出现了第二颗芯片——不管是传感器、存储器、显示屏还是另一颗MCU——你就必须面对一个全新的问题:这两颗芯片之间怎么说话?
这个问题听起来简单,实际上它是整个嵌入式系统设计里最容易翻车的地方之一。片内通信是"一家人关起门来说话",地址总线、数据总线都是芯片设计者替你铺好的高速公路,你只管用就行。片间通信就不一样了,那是"两个独立王国之间的外交",电气特性、时序协议、拓扑结构、错误处理,每一样都得你自己操心。
这个系列叫"从沙子到车辙",前面几篇聊的是芯片内部的事——从晶体管怎么造出来,到寄存器怎么控制外设。到了这一篇,视角要从"片内"抬到"片间"。而片间通信里最基础、最常用、也最值得掰开揉碎讲的两条总线,就是SPI和I2C。
为什么是这两个?因为它们几乎覆盖了中低速片间通信的绝大多数场景。你打开任何一块嵌入式开发板,上面大概率同时跑着这两条总线:SPI挂着Flash或者屏幕,I2C挂着EEPROM或者各种传感器。它们一个快而糙,一个慢而稳,一个像专线电话,一个像会议室广播。理解它们的设计哲学,比记住几个时序图重要得多。
这篇文章适合谁看?如果你已经会点灯、会配串口,但一遇到"我要接一个传感器,该选SPI还是I2C"就犯迷糊,那这篇就是写给你的。如果你已经用过这两条总线但总是遇到通信失败、数据错乱、总线锁死的问题,那这篇也能帮你把那些"玄学问题"背后的物理和逻辑原因理清楚。我会从电气层、协议层、软件层三个维度拆开讲,中间穿插大量实际调试中踩过的坑和总结出来的经验。
先说一个反直觉的结论:SPI和I2C的很多"坑",根源不在协议本身,而在于你没搞清楚它们的电气模型。很多人把总线当成理想的逻辑连线,觉得只要代码写对了就该通。但真实世界里,导线有电阻电容、引脚有驱动能力、电平有阈值范围,这些"不理想"才是问题的真正来源。所以下面我会先讲电气,再讲协议,最后讲软件,这个顺序不能反。
2. SPI:一根时钟线打天下的"专线电话"
2.1 SPI的四根线到底在干什么
SPI的全称是Serial Peripheral Interface,串行外设接口。它的设计思路极其简单粗暴:既然要同步传输,那就专门拿一根线来传时钟,收发双方都跟着这个时钟节拍走,谁也不用猜对方的速度。这个思路带来的直接好处是SPI可以跑得很快——几十兆赫兹是家常便饭,上百兆的也有。
标准SPI是四根线:
- SCLK(Serial Clock):时钟线,由主机产生,所有数据都踩着它的节拍走。
- MOSI(Master Out Slave In):主机发、从机收的数据线。
- MISO(Master In Slave Out):从机发、主机收的数据线。
- CS/SS(Chip Select / Slave Select):片选线,主机用它来"点名"要跟哪个从机说话。
这里有个关键点很多人一开始不理解:SPI是全双工的。也就是说,主机在MOSI上发一个bit的同时,从机也在MISO上发一个bit,这两个动作由同一个时钟沿驱动,同时发生。所以SPI本质上是一个移位寄存器环——主机的移位寄存器和从机的移位寄存器首尾相连,时钟每跳一下,双方各移出一位、各移入一位,八个时钟之后,两个寄存器的内容就完整交换了。
这个模型解释了一个新手常犯的困惑:"我只想读数据,为什么还要发数据?"因为在SPI的物理模型里,读和写是同一个动作的两面。你发出去的可能是无意义的0xFF(占位),但正是这些时钟脉冲把从机的数据"推"了出来。理解这一点,你才能理解为什么SPI的读操作代码里总是要先发一个dummy byte。
2.2 片选:SPI最容易被低估的一根线
四根线里,CS是最不起眼但最容易出问题的。SPI没有像I2C那样的地址机制,它靠的是物理片选——每个从机一根独立的CS线,主机拉低哪根就代表跟哪个从机通信。
这带来两个后果。第一,从机数量受限于主机的GPIO数量。你要挂8个SPI从机,主机就得有8个片选引脚,这在引脚紧张的场合很要命。第二,CS的时序极其关键。很多SPI从机要求CS在传输开始前拉低、传输完全结束后才能拉高,中间不能有毛刺。如果你用软件GPIO控制CS,在拉低和发第一个时钟之间如果被中断打断,某些敏感的从机可能就把这次通信当成无效帧丢掉了。
我踩过的一个经典坑:用软件控制CS,代码逻辑是先拉低CS、再调用SPI发送函数。结果SPI发送函数内部有初始化开销,导致CS拉低之后过了好几微秒才来第一个时钟。大部分从机能忍,但有一颗特定的ADC芯片忍不了,它的数据手册里明确写了CS拉低后必须在多少纳秒内开始时钟。后来改成硬件自动控制CS(很多MCU的SPI外设支持自动片选),问题立刻消失。
提示:如果你的MCU的SPI外设支持硬件片选(NSS输出),优先用它。软件控制CS在低速场景下能用,但一旦速度上去或者系统中断频繁,就容易出问题。
2.3 时钟极性和相位:SPI最劝退新手的四个组合
SPI有CPOL(Clock Polarity)和CPHA(Clock Phase)两个参数,组合出四种模式,通常记作Mode 0到Mode 3。这是新手最容易晕的地方,但理解了本质其实很简单。
CPOL决定时钟空闲时是高还是低。CPOL=0表示空闲低电平,CPOL=1表示空闲高电平。
CPHA决定数据在哪个时钟沿采样。CPHA=0表示在第一个时钟沿(即从空闲态跳变的那一下)采样,CPHA=1表示在第二个时钟沿采样。
组合起来:
| 模式 | CPOL | CPHA | 空闲电平 | 采样沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 上升沿 |
| Mode 1 | 0 | 1 | 低 | 下降沿 |
| Mode 2 | 1 | 0 | 高 | 下降沿 |
| Mode 3 | 1 | 1 | 高 | 上升沿 |
实际工作中,Mode 0和Mode 3加起来占了九成以上的场景。大部分Flash和传感器用Mode 0,部分器件用Mode 3。Mode 1和Mode 2相对少见。
这里的关键经验是:主从双方的CPOL和CPHA必须完全一致,否则数据必然错位。而且这个错误往往不是"完全读不到",而是"读到的数据看起来像但又不对"——比如你读到的值总是实际值的两倍或者移位了,那八成就是模式不匹配。调试时如果遇到这种"似是而非"的数据,第一件事就是去核对双方的SPI模式。
2.4 SPI的电气现实:为什么高速时波形会变丑
前面说的都是逻辑层的事,现在说电气层。SPI跑低速(比如1MHz以下)时,你几乎不用管电气,接上线就能通。但速度一上去,问题就来了。
SPI的时钟线是方波,方波在频域上包含大量高频谐波。当这些高频成分遇到导线的寄生电容和电感时,波形就会发生畸变:上升沿变缓、出现振铃、过冲、下冲。如果畸变严重到采样时刻电平还没稳定,从机就会采到错误的值。
影响波形质量的因素主要有几个:
- 线长:线越长,寄生电容电感越大,波形越差。SPI在板内短距离(几厘米)跑几十兆没问题,但如果你用排线拉出去二三十厘米,高速下基本必挂。
- 上拉/下拉电阻:SPI是推挽输出,一般不需要上拉。但如果你在MISO上加了上拉电阻,而主机的MISO又是推挽驱动,就会形成"两个源在打架",白白增加功耗和干扰。
- 串联电阻:在高速SPI的信号线上串一个几十欧姆的电阻,可以抑制振铃,这是很实用的一个技巧。很多参考设计里SCLK和MOSI上都有这个电阻,不是随便放的。
- 地线回流:高速信号的回流路径很关键。如果你用排线把SPI引出去,一定要保证信号线旁边有地线,最好是一根信号配一根地,否则回流路径绕远,辐射和串扰都会加剧。
我实测过一个案例:同一块板子,SPI时钟从10MHz提到20MHz,读Flash开始偶发校验错误。示波器一看,SCLK的上升沿有明显振铃,峰值超过了电源电压。在SCLK上串了33欧姆电阻之后,振铃基本消失,20MHz稳定运行。这个电阻的作用是跟导线的寄生电容形成一个RC低通,把过冲的能量吸收掉。
注意:串联电阻的阻值不能太大,否则会拖慢上升沿,反而限制了最高速度。一般从22到100欧姆之间试,用示波器看波形调到最干净为止。
3. I2C:两根线挂一串设备的"会议室广播"
3.1 I2C的开漏结构和上拉电阻
I2C只有两根线:SCL(时钟)和SDA(数据)。但它能挂的设备数量远超SPI,原因在于它有一套完全不同的电气结构——开漏输出加外部上拉。
开漏的意思是,器件的引脚只能把线拉低,不能主动拉高。要输出高电平,靠的是外部上拉电阻把线"拽"上去。这个设计看起来多此一举,实际上非常巧妙:它允许多个器件同时挂在一根线上而不会短路。因为任何器件都只能拉低,如果两个器件一个想拉低一个想拉高,结果是线被拉低,不会出现"一个输出高一个输出低直接对撞"的短路情况。
上拉电阻的选型是I2C设计里最需要动脑子的地方。阻值太大,上升沿太慢,高速通信时电平还没升到位就开始采样,直接出错;阻值太小,器件拉低时的灌电流太大,可能超过器件的驱动能力,而且功耗也上去了。
计算上拉电阻有个经验公式。I2C的上升时间由RC决定,R是上拉电阻,C是总线电容(包括导线、引脚、器件输入电容)。标准模式(100kHz)要求上升时间小于1000ns,快速模式(400kHz)要求小于300ns。假设总线电容是100pF,快速模式下:
R_max = t_r / (0.8473 × C) ≈ 300ns / (0.8473 × 100pF) ≈ 3.5kΩ
所以快速模式下上拉电阻不能超过3.5kΩ左右。同时还要考虑灌电流:标准规定器件拉低时电压要低于0.4V,假设电源3.3V,上拉电阻R,灌电流I = (3.3 - 0.4) / R。如果器件能灌3mA,那R最小约1kΩ。
综合下来,3.3V系统、400kHz、总线电容100pF左右的场景,上拉电阻取2.2kΩ到4.7kΩ之间比较稳妥。总线电容越大,上拉电阻要越小,但功耗也越大,这是个权衡。
我见过最常见的I2C问题就是上拉电阻没配对。有人从某个模块上抄了个10kΩ的上拉,结果那个模块总线电容小能跑400kHz,他自己板子上挂了五六个器件电容大,10kΩ根本拉不起来,通信时好时坏。换成2.2kΩ立刻稳定。
3.2 地址机制:7位地址和那个容易搞混的读写位
I2C靠地址来区分设备,不需要片选线,这是它能"两根线挂一串"的关键。标准I2C用7位地址,理论上可以挂128个设备(实际因为保留地址,可用的大概112个)。
这里有个新手极易搞混的点:7位地址和8位"设备地址字节"不是一回事。很多数据手册给的地址是8位的,比如"0xA0",这其实是7位地址0x50左移一位、最低位补0(写操作)得到的。而有些手册直接给7位地址0x50。你在写代码时,如果驱动库要求填7位地址,你填了0xA0,那就错了,实际访问的是0x50这个地址的读操作。
判断方法很简单:7位地址的范围是0x00到0x7F,如果手册给的地址大于0x7F,那它一定是8位格式,右移一位才是7位地址。比如0xA0右移一位是0x50,0xAE右移一位是0x57。
读写位是地址字节的最低位。主机发完7位地址后,紧跟一位R/W位,0表示接下来要写,1表示接下来要读。所以完整的地址字节是:(7位地址 << 1) | R/W。
3.3 起始、停止、应答:I2C的握手礼仪
I2C的协议层有一套严格的"礼仪",理解这套礼仪是调试I2C的基础。
起始条件(START):SCL为高时,SDA从高变低。这个特殊的跳变告诉所有设备"注意,要开始通信了"。
停止条件(STOP):SCL为高时,SDA从低变高。表示通信结束。
数据位:SCL为低时SDA可以变化,SCL为高时SDA必须稳定,接收方在SCL高电平期间采样。这个规则保证了数据在时钟高电平期间是有效的。
应答(ACK/NACK):每发完8个bit(一个字节),接收方要在第9个时钟周期把SDA拉低,表示"收到了"(ACK)。如果接收方不拉低(SDA保持高),就是NACK,表示"没收到"或者"不要再发了"。
这套机制里,ACK是调试时最有用的信号。当你发完地址后如果收到NACK,说明这个地址上没有设备应答——要么地址错了,要么设备没上电,要么线没接好。如果发数据时收到NACK,可能是设备忙或者写保护。用逻辑分析仪抓一次完整的I2C波形,看哪个字节后面跟的是NACK,问题范围立刻缩小一大半。
3.4 时钟拉伸:从机也能"喊暂停"
I2C有一个SPI没有的机制:时钟拉伸(Clock Stretching)。因为SCL也是开漏结构,从机如果处理不过来,可以在需要的时候把SCL拉低,主机发现SCL没按预期升上去,就知道从机在"喊暂停",于是等待,直到从机释放SCL。
这个机制很人性化,但也是坑的来源。有些主机的硬件I2C外设不支持时钟拉伸,或者支持得不好,遇到会拉伸的从机就卡死。更麻烦的是,如果从机因为某种原因一直拉着SCL不放,整个总线就锁死了,所有设备都没法通信。
处理总线锁死的常见办法是:主机在检测到SCL长时间为低时,尝试手动发送9个时钟脉冲(用GPIO模拟),把从机的状态机"冲"出来。这个技巧在调试EEPROM时特别有用,因为EEPROM在写周期内会拉伸时钟,如果主机在写周期中途复位,EEPROM可能停在某个中间状态拉着总线不放。
4. SPI和I2C到底怎么选:一张决策表背后的权衡
4.1 速度、引脚、距离、多设备的四维对比
选SPI还是I2C,本质上是在几个维度上做权衡。我把关键维度整理成表:
| 维度 | SPI | I2C |
|---|---|---|
| 速度 | 高,可达几十MHz | 中低,标准100kHz,快速400kHz,高速3.4MHz |
| 引脚数 | 每增加一个从机多一根CS | 固定两根,与从机数量无关 |
| 多设备 | 片选多,引脚压力大 | 地址寻址,天然支持多设备 |
| 距离 | 适合板内短距离 | 抗干扰稍好,可稍长,但也不宜过长 |
| 全双工 | 支持 | 半双工 |
| 协议开销 | 低,几乎无额外字节 | 有起始、地址、ACK等开销 |
| 错误检测 | 无(需上层加校验) | 有ACK机制 |
| 时钟拉伸 | 不支持 | 支持 |
| 典型用途 | Flash、屏幕、高速ADC | EEPROM、传感器、RTC |
从这张表能看出两者的定位差异。SPI是"性能优先":要快、要简单、要全双工,那就SPI,代价是引脚多、没地址机制。I2C是"引脚优先":设备多、引脚紧张、速度要求不高,那就I2C,代价是速度慢、协议开销大。
4.2 那些"选错了"的真实场景
理论说完了,说几个实际选型时容易犯的错。
场景一:用I2C接高速ADC。有人为了省引脚,把一颗采样率要求挺高的ADC挂在I2C上。结果发现采样率上不去,因为I2C 400kHz下,读一次转换结果要发地址、等ACK、读两个字节、再等ACK,光协议开销就占了一大半时间。这种场景应该用SPI,SPI可以连续读,没有地址和ACK的开销,速度能高一个数量级。
场景二:用SPI接一堆慢速传感器。反过来,有人用SPI挂了七八个传感器,每个都要一根CS,MCU的GPIO不够用了,还得加IO扩展芯片,反而更复杂。这种场景如果传感器速度要求不高,用I2C更合适,两根线全搞定。
场景三:长距离传输。有人想把I2C拉出去一两米接个外部模块。I2C虽然比SPI抗干扰稍好,但一两米的距离,加上线缆电容,400kHz基本跑不动,得降速到100kHz甚至更低,而且上拉电阻要重新算。这种场景其实应该考虑差分总线或者加缓冲器,而不是硬拉I2C。
4.3 混合使用:一个系统里两条总线并存
实际项目里,SPI和I2C经常同时存在,各司其职。一个典型的嵌入式系统可能是这样:SPI接外部Flash存固件和日志(要快),SPI接显示屏(要带宽),I2C接温度传感器、加速度计、EEPROM(要省引脚)。
这种混合架构下,要注意的是总线隔离和电源域。如果SPI上的Flash和I2C上的传感器供电电压不同(比如一个3.3V一个1.8V),电平转换就是必须的。I2C的电平转换尤其要注意,因为开漏结构加上拉,转换电路和推挽信号不一样,不能简单用电阻分压。
提示:I2C电平转换推荐用专用的电平转换芯片或者MOSFET方案,不要用简单的电阻分压。电阻分压会改变上拉等效阻值,影响上升时间,而且双向通信时会有方向问题。
5. 调试实战:从波形里读出真相
5.1 逻辑分析仪:调试总线的第一件武器
调SPI和I2C,没有逻辑分析仪基本等于盲人摸象。示波器能看电气质量,但看协议内容还是逻辑分析仪方便。现在几十块钱的USB逻辑分析仪配合开源软件,就能解码SPI和I2C,把每一个字节、每一个ACK都标出来。
用逻辑分析仪的正确姿势是:先看有没有波形,再看波形对不对,最后看数据对不对。如果连波形都没有,那是硬件问题(线没接、引脚没配、电源没上)。如果有波形但解码失败,那是协议参数问题(SPI模式不对、I2C速率不对)。如果能解码但数据不对,那是软件逻辑问题(地址错、寄存器错、字节序错)。
我调试时习惯把逻辑分析仪的采样率设得比总线频率高至少10倍。比如400kHz的I2C,采样率至少4MHz,最好10MHz以上,这样波形细节才看得清。采样率太低会把细节漏掉,反而误导判断。
5.2 一个I2C读不到数据的完整排查链路
说一个我实际遇到的案例,把排查思路完整走一遍。
现象:一颗I2C温度传感器,代码写好了,读出来永远是0xFFFF或者0x00,偶尔能读到正确值但概率很低。
第一步,确认硬件。用万用表量SCL和SDA的静态电平,发现都是高电平,说明上拉正常,没有短路。量电源,3.3V正常。这一步排除了供电和短路问题。
第二步,上逻辑分析仪。抓一次读操作,发现主机发了起始条件、发了地址字节,但地址字节后面跟的是NACK。这说明从机没有应答。
第三步,核对地址。查数据手册,传感器7位地址是0x48。代码里填的是0x48,看起来没错。但仔细看驱动库的说明,发现这个库要求填8位地址,也就是0x90(0x48<<1)。填错格式导致实际访问的地址不对,从机自然不应答。改成0x90后,地址字节后面出现了ACK。
第四步,继续抓波形。地址ACK了,但读数据时又出问题。发现主机在读第二个字节时,从机把SDA拉低了,但主机没有正确处理——原来这颗传感器在连续读时,第一个字节是温度高8位,第二个字节是低8位,主机读第二个字节后应该发NACK表示"读完了",但代码里发的是ACK,导致从机以为主机还要读,继续输出,时序就乱了。修正NACK逻辑后,数据正常。
这个案例里,问题一层套一层,如果不用逻辑分析仪看波形,光靠猜代码,很难定位。逻辑分析仪的价值就在于把"看不见的通信"变成"看得见的波形",让每一步都有据可查。
5.3 SPI数据错位的三种典型原因
SPI调试中,"数据错位"是最常见的症状,表现为读到的值总是实际值的移位版本,或者高低字节颠倒。三种典型原因:
原因一:SPI模式不匹配。前面说过,CPOL/CPHA不一致会导致采样点错位。症状是数据整体移位或者完全乱码。解决办法是核对双方数据手册的模式设置。
原因二:字节序问题。SPI本身不规定字节序,但很多多字节器件(比如16位ADC)有固定的输出顺序,先高字节还是先低字节取决于器件。如果你按错误的顺序拼接,读出来的值就是高低字节颠倒的。这个看数据手册就能确认。
原因三:CS时序问题。如果CS在传输中途被意外拉高(比如被中断打断),从机会认为一帧结束,下次通信时从机的移位寄存器状态就不对了。症状是第一次读对,后面越读越乱。解决办法是用硬件CS或者关中断保护CS操作。
5.4 总线锁死的应急恢复
I2C总线锁死是现场调试的噩梦。现象是SCL或SDA被某个设备一直拉低,所有通信都停了。常见原因是主机在从机传输中途复位,从机停在某个状态出不来。
应急恢复的标准操作是:把SCL配置成GPIO输出,手动发送9个时钟脉冲,然后发一个停止条件。9个脉冲足以让任何从机的状态机走完当前字节,停止条件让它复位到空闲态。
代码逻辑大概是这样:
// 假设SCL和SDA都能切换为GPIO void i2c_bus_recovery(void) { // 配置SCL和SDA为开漏输出 gpio_set_open_drain(SCL_PIN); gpio_set_open_drain(SDA_PIN); // 确保SDA为高(释放) gpio_set_high(SDA_PIN); // 发送9个时钟脉冲 for (int i = 0; i < 9; i++) { gpio_set_low(SCL_PIN); delay_us(5); gpio_set_high(SCL_PIN); delay_us(5); } // 发送停止条件:SCL高时SDA从低变高 gpio_set_low(SDA_PIN); delay_us(5); gpio_set_high(SCL_PIN); delay_us(5); gpio_set_high(SDA_PIN); delay_us(5); // 恢复I2C外设功能 i2c_reinit(); }这个恢复函数应该放在I2C初始化的最前面,每次上电先执行一次,能解决大部分因为上次异常复位导致的总线锁死。
6. 软件层的那些"想当然"与"实际上"
6.1 硬件外设 vs GPIO模拟:什么时候该自己写时序
大部分MCU都有硬件SPI和硬件I2C外设,用起来省心。但有些场景下,你不得不或者最好用GPIO模拟(也就是常说的软件模拟、bit-banging)。
必须用GPIO模拟的场景:MCU的硬件外设数量不够(比如你要四路SPI但MCU只有两路);硬件外设不支持某些特殊时序(比如某些器件的SPI时序有古怪的延迟要求);引脚被固定死了,硬件外设的引脚没法用。
GPIO模拟的代价:速度慢(受GPIO翻转速度和中断延迟限制),CPU占用高(每个bit都要软件干预),时序精度差(容易被中断打断)。
我的经验是:能用硬件外设就用硬件外设,GPIO模拟是最后的手段。硬件外设不仅快,而且时序精确,不占CPU。只有在硬件外设实在满足不了需求时才考虑模拟。而且模拟的时候,关键时序段要关中断,否则一个中断进来就可能把时序打乱。
6.2 中断和DMA:让总线传输不占CPU
SPI和I2C都支持中断和DMA方式。轮询方式最简单,但CPU要一直等着,效率低。中断方式在传输完成时通知CPU,CPU可以去干别的。DMA方式则由DMA控制器直接搬运数据,CPU完全不参与,适合大批量数据传输。
用DMA跑SPI读Flash是很经典的用法。配置好DMA源地址(SPI数据寄存器)和目的地址(内存缓冲区),启动传输,然后CPU就可以去处理其他任务,等DMA传输完成中断来了再处理数据。这样读几KB的数据,CPU几乎不占用。
但DMA也有坑。DMA传输期间,缓冲区不能被其他代码修改,否则数据就乱了。而且DMA传输完成中断里不能做太耗时的操作,否则会影响下一次传输。还有,DMA和Cache的配合在带Cache的MCU上要特别注意,可能需要手动维护Cache一致性。
6.3 超时机制:别让一次通信失败拖垮整个系统
这是我最想强调的一点。任何总线操作都必须有超时机制。我见过太多代码,I2C读操作就是死等,等ACK、等数据,如果从机没响应,程序就卡死在那里,看门狗都救不回来(如果看门狗喂狗在别的地方)。
正确的做法是:每次等待(等ACK、等标志位、等数据)都设一个超时计数,超过就返回错误,让上层决定怎么处理——重试、报错、还是降级运行。
// I2C等待ACK的超时示例 int i2c_wait_ack_timeout(int timeout_ms) { int count = 0; int max_count = timeout_ms * 1000 / 10; // 假设每次循环10us while (i2c_check_ack() != OK) { if (++count > max_count) { return TIMEOUT_ERROR; } delay_us(10); } return OK; }超时时间设多少?取决于总线速度和从机响应时间。I2C 100kHz下,一个字节加ACK大概90us,超时设10ms已经很宽松了。SPI更快,超时可以设得更短。关键是要有,具体值可以调。
6.4 重试策略:不是所有失败都值得重试
有了超时之后,下一步是重试。但重试不是无脑循环,要分情况。
值得重试的:总线仲裁丢失(多主机场景)、从机暂时忙(比如EEPROM写周期内)、偶发的电气干扰导致的单次错误。这些重试一两次通常就好了。
不值得重试的:地址错误(重试一万次也没用)、从机没上电(硬件问题)、总线物理损坏。这些重试只是浪费时间。
我的做法是:重试2到3次,每次之间加一点延迟(给从机恢复的时间),如果还失败就上报错误。同时记录错误次数,如果某个设备频繁出错,那说明有更深层的问题(接触不良、电源不稳、干扰),需要从硬件上解决,而不是靠软件重试掩盖。
7. 从总线选择看系统设计的分层思维
聊了这么多SPI和I2C的细节,最后我想把视角再拉高一点。片间总线的选择,本质上反映的是系统设计里的分层思维。
芯片内部,设计者用最快的总线(比如AXI、AHB)连接CPU和内存,用中等速度的总线连接外设,用低速总线连接那些对速度不敏感的模块。这是一个速度分层的金字塔。到了片间,同样的逻辑在重演:SPI负责高速短距离,I2C负责中低速多设备,再往外可能还有CAN、RS485、以太网负责更长距离和更复杂的环境。
理解这个分层,你就能理解为什么没有"万能总线"。每条总线都是在速度、引脚、距离、成本、复杂度这几个维度上取了不同的平衡点。选型的时候,先问自己这几个维度里哪个是瓶颈,答案自然就出来了。
我在实际项目里养成了一个习惯:在画原理图之前,先画一张"通信拓扑图",把系统里所有需要通信的芯片画成节点,标注每个节点的数据量、实时性要求、距离、供电。画完之后,哪条线该用什么总线,一目了然。这个习惯帮我避免了很多"选完才发现不合适"的返工。
还有一点体会是:总线的可靠性,八成取决于硬件设计,两成取决于软件。上拉电阻选对了、线走好了、地平面完整了,软件随便写写都能通。反过来,硬件有隐患,软件再怎么加校验、加重试,也是治标不治本。所以遇到通信问题,先怀疑硬件,再怀疑软件,这个顺序能帮你少走很多弯路。
最后分享一个我常用的调试小技巧:在总线的关键信号上预留测试点。画PCB的时候,在SCLK、SDA、MOSI、MISO、CS这些线上留出裸露的焊盘或者测试环,调试时直接夹探头,不用去戳芯片引脚。这个小小的预留,在调试阶段能省下大量时间。很多问题不是想出来的,是看出来的——而要看,就得有地方下探头。