1. 从片内总线到片间总线的思维切换
很多刚接触嵌入式或者芯片设计的同行,在啃完片内总线那一套东西之后,都会有一种"我已经懂了通信"的错觉。片内总线比如常见的那些,走的是并行、同步、短距离的路子,时钟一给,数据一排,一拍一拍往前推,逻辑清晰得很。可一旦把视角从芯片内部挪到芯片与芯片之间,也就是从片内走到片间,整个游戏规则就变了。你面对的不再是同一块硅片上几毫米甚至几微米的走线,而是几厘米、几十厘米的PCB走线,甚至是排线、连接器、双绞线。距离一拉长,信号完整性、时钟同步、线数成本、抗干扰能力,全都成了必须重新算账的问题。
这个系列叫"从沙子到车辙",前面几篇聊的是从硅片到系统的那条链路,这一篇聚焦在片间通信最经典的两条腿:SPI和I2C。为什么是这两条?因为它们几乎是每个嵌入式工程师绕不开的起点。你打开任何一块开发板,传感器、存储器、显示屏、电源管理芯片,十有八九挂在这两条总线上。它们不像以太网、USB那么复杂,但恰恰因为简单,很多人只停留在"能通就行"的层面,真到了时序出问题、多设备冲突、长线传输翻车的时候,就抓瞎了。
我写这篇的目的很直接:把SPI和I2C从"会用"拉到"懂为什么这么设计"的层次。不管你是刚画完第一块板子的新手,还是已经调过几十个传感器驱动的老手,我都希望你能从片间通信的物理约束、协议取舍、时序细节和实战踩坑这几个角度,重新审视这两条总线。文章会涉及具体的时序参数计算、上拉电阻选型、多从机拓扑、常见故障排查链路,以及一些只有实际调过板子才会知道的细节。你不需要有很深的模拟电路背景,但需要有一点数字通信的常识,剩下的我会用生活化的类比和实际案例把它讲透。
2. SPI:用线数换速度的直球打法
2.1 SPI的四线制到底解决了什么问题
SPI的全称是串行外设接口,名字里就带着"外设"两个字,说明它从诞生之初就是冲着片间通信去的。它的核心思路非常直白:既然并行总线线太多、距离一长就同步不了,那就把数据拆成一位一位,用一根线慢慢传。但"慢"是相对的,SPI通过独立的时钟线和全双工的数据线,把串行通信的速度拉到了几十兆赫兹甚至上百兆赫兹的量级,这在片间通信里已经相当可观了。
标准SPI是四根线:SCLK、MOSI、MISO、SS。SCLK是时钟,由主机产生,所有数据都踩着这个节拍走;MOSI是主机输出从机输入,MISO是主机输入从机输出,这两根线让SPI天然支持全双工,发和收可以同时进行;SS是片选,低电平有效,主机拉低哪根SS,就代表要跟哪个从机说话。这个设计的精妙之处在于,它把"跟谁通信"和"怎么通信"彻底分开了。时钟和数据格式是全局统一的,片选是每个从机独立的。你想加一个从机,就多拉一根SS线,协议本身不用动。
这里有个很多人一开始会忽略的点:SPI没有设备地址的概念。I2C靠地址来区分设备,SPI靠片选线。这意味着SPI的从机数量直接受限于主机有多少个可用的GPIO。你挂八个从机,就得八根SS线。这在引脚紧张的系统里是个硬约束。我见过不少项目,前期图省事全用SPI,结果从机一多,主控的引脚不够用了,最后不得不加IO扩展芯片或者改用I2C。所以选型的时候,从机数量是个必须提前算清楚的账。
2.2 时钟极性和相位:SPI最容易翻车的地方
SPI的时序里有两个参数,CPOL和CPHA,合起来叫SPI模式,一共四种组合。CPOL决定时钟空闲时是高电平还是低电平,CPHA决定数据是在时钟的第一个边沿采样还是第二个边沿采样。这两个参数如果配错,现象通常是:数据能读到,但全是乱的,或者偶尔对偶尔错。很多新手调SPI,第一反应是怀疑线没接好,其实十有八九是模式没对上。
我用一个生活化的类比来解释。假设你和另一个人隔着窗户打手势传消息,CPOL就是你们约定"不说话的时候手是举着还是放下",CPHA就是约定"是抬手的时候看对方还是放手的时候看对方"。如果两个人约定不一致,你抬手的时候对方在看,你放手的时候对方也在看,那读到的信息就完全错位了。SPI的四种模式就是这四种约定组合,主机和从机必须用同一种。
实际选型的时候,从机手册里一定会写明它支持哪种模式,常见的是模式0和模式3。主机这边通常可以配置成任意模式,所以原则是"从机说了算"。我个人的习惯是,拿到一个新传感器,先翻手册找SPI模式,配好之后再动别的。如果手册写得含糊,就用逻辑分析仪抓一下波形,看时钟空闲电平和数据跳变沿,一眼就能判断出来。这个排查动作花不了两分钟,但能省掉后面几个小时的瞎猜。
2.3 上拉、串联电阻和走线:SPI的物理层细节
SPI的物理层看起来简单,推挽输出,不需要上拉电阻,直接驱动就行。但真到了高速或者长走线的时候,问题就来了。推挽输出的驱动能力强,边沿陡,这本来是好事,但边沿太陡会带来过冲和振铃,尤其是在走线阻抗不匹配的时候。我遇到过一块板子,SPI时钟跑到20MHz以上就开始丢数据,示波器一打,时钟线上全是振铃,峰峰值都快翻倍了。解决办法是在时钟线和数据线上串一个小电阻,通常22欧姆到33欧姆,把边沿稍微缓一缓,振铃就压下去了。
串联电阻的位置也有讲究,要尽量靠近信号源,也就是主机端。因为电阻的作用是匹配源端阻抗,放在源端才能有效吸收反射。如果放在接收端,效果会打折扣。这个细节在很多参考设计里不会写,但实际调板子的时候非常管用。
另外,SPI的走线尽量等长,尤其是时钟线和数据线之间。虽然SPI不像DDR那样对时序要求苛刻到皮秒级,但在高速下,时钟和数据之间的偏斜如果太大,采样窗口就会被压缩。我一般建议时钟线走内层,两边包地,数据线跟时钟线保持差不多的长度,误差控制在几毫米以内。如果板子空间实在紧张,至少保证时钟线不要绕远路,数据线可以稍微迁就一下。
还有一个容易被忽略的点:MISO线在从机未被选中时的状态。有些从机的MISO是推挽输出,片选无效时会变成高阻;但有些从机的MISO是开漏输出,需要外部上拉。如果多个从机共享MISO线,而其中某个从机的MISO在未选中时不是高阻,就会跟其他从机打架,导致总线电平异常。这种情况的典型现象是:单独读某个从机没问题,一旦多个从机同时挂着,就随机出错。解决办法是查手册确认MISO的输出类型,必要时加缓冲器或者用片选信号控制MISO的使能。
3. I2C:两根线撑起的多设备江湖
3.1 I2C的开漏结构和上拉电阻的账怎么算
I2C只有两根线,SDA和SCL,都是开漏输出,必须外接上拉电阻。开漏的好处是天然支持线与逻辑,任何设备都可以把线拉低,但不会强行拉高,这样就避免了多个设备同时驱动时的短路冲突。这个设计是I2C能挂这么多设备还相安无事的根本原因。
上拉电阻的选型是I2C实战里最常被问的问题。阻值太大,上升沿太慢,高速通信时波形还没到高电平就被拉低了,数据就错了;阻值太小,功耗大,而且开漏输出的下拉能力有限,低电平可能拉不到足够低。这个账要算清楚,得看三个参数:总线电容、上升时间要求和输出低电平电流。
总线电容来自PCB走线、连接器、每个设备的引脚电容,经验值是每根线大概10pF到20pF,设备多了或者走线长了会更大。上升时间要求跟通信速率有关,标准模式100kHz、快速模式400kHz、快速模式+1MHz,要求各不相同。粗略的估算公式是上升时间约等于0.847乘以上拉电阻乘以总线电容。假设总线电容200pF,想要上升时间在300ns以内,上拉电阻大概在1.8kΩ左右。实际选型时,我一般从4.7kΩ起步,如果波形上升沿太缓就往下调,如果功耗或者低电平有问题就往上调。
这里有个实际经验:很多模块自带上拉电阻,比如常见的传感器模块,板上已经焊了4.7kΩ或者10kΩ。如果你把好几个这样的模块挂在一起,上拉电阻就并联了,等效阻值变小,可能导致低电平拉不下去。我见过一个案例,挂了四个模块,每个都有4.7kΩ上拉,并联后等效1.175kΩ,结果通信时好时坏,最后把其中三个模块的上拉电阻拆掉才稳定。所以多设备系统里,上拉电阻最好只在主机端保留一组,从机端能拆就拆。
3.2 地址冲突:I2C多设备系统的头号难题
I2C用7位地址,理论上可以挂128个设备,但实际可用的地址空间要扣掉保留地址,而且很多传感器的地址是固定的,或者只有一两个可选。这就导致一个很尴尬的局面:你买了两个同型号的传感器,想挂在同一条I2C总线上,结果地址一模一样,冲突了。
解决地址冲突有几种常见办法。第一种是选带地址选择引脚的型号,比如有些传感器提供一个ADDR引脚,接高电平是一个地址,接低电平是另一个地址,这样两个同型号设备就能共存。第二种是用I2C多路复用器,比如常见的8通道复用器,主机先选通道,再跟通道上的设备通信,相当于把一条总线扩展成八条。第三种是软件模拟I2C,用两个GPIO分别控制两条独立的总线,适合从机数量不多的情况。
我个人的建议是,在项目选型阶段就把地址问题考虑进去。如果预判到可能会用多个同型号传感器,优先选带地址选择引脚的。如果已经买了固定地址的,那就提前规划好多路复用方案,别等到画板子的时候才发现两个设备地址撞了,那时候改硬件就麻烦了。
3.3 时钟拉伸和总线死锁:I2C的暗坑
I2C有一个SPI没有的机制:时钟拉伸。从机如果处理不过来,可以把SCL线拉低,强制主机等待。这个机制本意是好的,让慢速从机也能跟上快速主机。但实际用起来,时钟拉伸经常变成麻烦的来源。有些从机的时钟拉伸逻辑写得不好,拉低之后忘了释放,总线就死锁了,SCL一直低,主机怎么发时钟都没用。
处理时钟拉伸导致的死锁,常规做法是主机在检测到SCL长时间为低时,主动发送若干个时钟脉冲,试图把从机的状态机推出去。如果还不行,就只能复位从机或者重新初始化总线。我在实际项目里遇到过一种情况:某个传感器在电源上电过程中会短暂拉低SCL,如果主机初始化太快,正好撞上这个窗口,总线就卡住了。解决办法是在初始化I2C之前加一段延时,等从机电源稳定之后再开始通信。这个延时通常几十毫秒就够了,但很多标准驱动里没有,需要自己加。
另一个暗坑是总线电容过大导致的边沿变缓。I2C标准对总线电容有上限要求,通常是400pF。超过这个值,上升沿会慢到影响采样。如果你挂了很多设备,或者走线很长,就要考虑用I2C缓冲器或者中继器,把总线分成几段,每段电容控制在限值以内。这个在实验室里短距离调试时不容易发现,一到现场长线部署就暴露出来。
4. SPI和I2C的选型逻辑:没有最好,只有最合适
4.1 速度、引脚数和拓扑结构的三角权衡
选SPI还是I2C,本质上是在速度、引脚数和拓扑灵活性之间做权衡。SPI速度快,全双工,但每个从机要一根片选线,引脚开销大;I2C速度相对慢,半双工,但两根线就能挂一堆设备,引脚开销小。这个对比看起来简单,但实际项目里的决策往往更微妙。
我整理了一个对比表格,把关键维度列出来,方便你在选型时快速对照:
| 维度 | SPI | I2C |
|---|---|---|
| 线数 | 4根起,每从机加1根SS | 2根,与从机数量无关 |
| 最高速率 | 几十MHz到上百MHz | 标准100kHz,快速400kHz,快速+1MHz |
| 双工 | 全双工 | 半双工 |
| 设备寻址 | 片选线 | 7位地址 |
| 上拉电阻 | 不需要 | 需要 |
| 时钟拉伸 | 不支持 | 支持 |
| 多主机 | 一般不支持 | 支持 |
| 典型场景 | 高速ADC、显示屏、Flash | 传感器、EEPROM、电源管理 |
从表里能看出来,SPI适合"少而快"的场景,I2C适合"多而慢"的场景。但实际选型还要考虑软件复杂度。SPI的驱动通常比I2C简单,因为没有地址仲裁和时钟拉伸这些机制,时序也更好预测。I2C的驱动要处理地址、ACK/NACK、时钟拉伸、总线仲裁,代码量更大,出问题的概率也更高。所以如果从机数量不多,速度要求又高,SPI往往是更省心的选择。
4.2 混合拓扑:一条总线上同时跑SPI和I2C
实际系统里,SPI和I2C经常是共存的。主控通常有多个SPI和多个I2C控制器,你可以把高速设备挂SPI,低速设备挂I2C,各取所长。但要注意的是,SPI和I2C的引脚不能混用,它们是独立的控制器,物理层也不一样。画板子的时候,要把这两组线分开走,避免相互干扰。
我见过一种做法,用GPIO模拟I2C,把原本的硬件I2C引脚省出来给SPI用。这种做法在从机不多、速度要求不高的时候可行,但软件模拟I2C对时序的控制不如硬件精确,尤其是在高负载或者中断频繁的系统里,容易出现时序偏差。如果非要用软件模拟,建议把I2C的速率降下来,比如跑到100kHz甚至更低,给软件留出足够的余量。
还有一种情况是,主控的硬件I2C控制器不够用,但SPI控制器有富余。这时候可以用SPI转I2C的桥接芯片,把SPI接口扩展成I2C总线。这种方案在需要挂很多I2C设备但主控I2C资源紧张的时候很实用。桥接芯片会帮你处理I2C的时序和仲裁,主机这边只需要按SPI的方式读写寄存器就行。
5. 调试实战:从波形到代码的完整排查链路
5.1 逻辑分析仪抓波形的正确姿势
调SPI和I2C,逻辑分析仪是最趁手的工具。但很多人抓波形的方式不对,导致抓到的数据没法用。我总结几个关键点。第一,采样率要足够高,至少是被测信号频率的5到10倍。比如SPI时钟20MHz,采样率最好在100MHz以上,否则边沿细节看不清。第二,触发条件要设对。调SPI的时候,用片选信号的下降沿触发,这样能抓到完整的传输帧;调I2C的时候,用起始条件触发,也就是SDA在SCL高电平时的下降沿。第三,通道分配要合理,SPI至少接SCLK、MOSI、MISO、SS四路,I2C接SCL和SDA两路,如果有条件,把电源和地也接上,方便对照。
抓完波形之后,逻辑分析仪软件通常自带协议解码功能,能把电平直接翻译成字节。这个功能很省事,但不要完全依赖它。我遇到过解码结果和实际不符的情况,原因是解码器的参数设错了,比如SPI的CPOL/CPHA设反了,或者I2C的地址位宽设错了。所以解码结果只能作为参考,关键数据还是要自己对着波形数一遍,确认无误。
5.2 常见故障的现象、根因和修复对照
调SPI和I2C遇到的问题,现象就那么几种,但根因可能五花八门。我整理了一个排查对照表,把常见现象和可能的根因列出来,方便你按图索骥:
| 现象 | 可能根因 | 排查动作 |
|---|---|---|
| 完全读不到数据 | 片选没拉低、时钟没输出、电源没上 | 查SS电平、查SCLK波形、量从机供电 |
| 数据偶尔错 | 模式配错、时序余量不足、干扰 | 核对CPOL/CPHA、降速测试、查振铃 |
| 多个从机时随机出错 | MISO冲突、地址冲突、上拉过强 | 查MISO输出类型、查地址、拆上拉 |
| I2C总线死锁 | 时钟拉伸未释放、从机复位中 | 发时钟脉冲、加初始化延时 |
| 高速下丢数据 | 走线太长、无匹配电阻、电容过大 | 加串联电阻、缩短走线、加缓冲器 |
这个表不是万能的,但能覆盖八成以上的常见问题。实际排查的时候,我的习惯是先用示波器看电源和地,确认供电干净;再用逻辑分析仪看波形,确认时序正确;最后才怀疑代码。这个顺序很重要,因为硬件问题用软件是修不好的,先排除硬件能省很多时间。
5.3 从寄存器操作到驱动框架:代码层面的注意事项
SPI和I2C的代码,从最底层的寄存器操作到上层的驱动框架,每一层都有坑。寄存器层面,要注意时钟使能和引脚复用的配置顺序。很多主控要求先使能外设时钟,再配置引脚复用,顺序反了外设不工作。这个在手册里通常有写,但容易被忽略。
驱动框架层面,Linux下的SPI和I2C子系统已经封装得很完善了,但设备树的配置是关键。SPI设备要在设备树里指定片选号、最大频率、模式;I2C设备要指定地址、时钟频率。这些参数如果和硬件对不上,驱动加载了也通不了。我见过一个案例,设备树里SPI最大频率写了50MHz,但实际从机只能跑10MHz,结果通信时好时坏,最后把频率降下来才稳定。所以设备树里的参数一定要跟硬件手册核对,不能想当然。
还有一个经验:在驱动里加超时和重试机制。SPI和I2C的通信失败是常态,尤其是在电磁环境复杂的现场。如果每次失败都直接返回错误,上层应用会频繁报错。合理的做法是失败后重试两到三次,再失败才上报。重试之间加一点延时,给总线恢复的时间。这个机制在实验室里可能用不上,但到了现场就是稳定性的保障。
6. 长线传输和隔离:当片间通信走出PCB
6.1 差分传输和隔离器:长距离下的必要升级
SPI和I2C的设计初衷是板内通信,走线长度通常在几十厘米以内。一旦超过这个距离,比如要通过排线连到另一个机箱,或者要在电机、继电器旁边走线,单端信号就扛不住了。这时候要么改用差分传输,要么加隔离器。
差分传输的思路是把单端信号转成一对互补信号,接收端取差值。干扰在两根线上是共模的,取差值的时候就被抵消了。常见的做法是用差分收发器,把SPI或I2C转成差分信号,传过去之后再转回来。这样传输距离可以拉到几米甚至几十米,抗干扰能力也强很多。
隔离器则是另一种思路,用光耦或者数字隔离器把两边的电气连接断开,只传信号不传电。这在需要电气隔离的场合是必须的,比如高压侧和低压侧之间的通信。隔离器要注意的是传输延迟和速率匹配,低速光耦的延迟可能到微秒级,会限制通信速率。数字隔离器速度快很多,但成本也高。选型的时候要根据实际速率要求来定。
6.2 地环路和共模干扰:长线传输的隐形杀手
长线传输还有一个容易被忽略的问题:地环路。当两个设备分别接地,而两个接地点之间存在电位差时,这个电位差会在信号线上形成共模干扰,严重的时候直接淹没信号。解决地环路的方法有几种:一是用隔离器断开地连接;二是用差分传输,让接收端只取差值;三是用单点接地,避免形成环路。
共模干扰的另一个来源是附近的强电设备。电机启停、继电器动作、变频器工作,都会在空间里产生强烈的电磁场,耦合到信号线上。对付这种干扰,除了差分和隔离,还可以在信号线上加共模扼流圈,或者用屏蔽线并把屏蔽层单端接地。这些措施在实验室里可能看不出效果,但到了现场就是能不能稳定运行的分水岭。
我个人的经验是,长线传输的项目,前期设计就要把隔离和差分考虑进去,不要等到现场出问题了再补救。补救的成本往往比前期设计高得多,而且现场环境复杂,改起来很麻烦。如果预算允许,优先选带隔离的收发器或者隔离器,把问题扼杀在源头。
7. 一些只有调过板子才知道的细节
调SPI和I2C这些年,积累了一些手册上不会写、但实际很管用的经验。第一个是关于片选信号的。SPI的片选在两次传输之间应该拉高,让从机有时间处理数据。有些从机在片选一直拉低的情况下会进入连续传输模式,但有些从机不行,片选不拉高它就不更新输出。我遇到过一种Flash芯片,片选不拉高的话,读出来的数据永远是第一页的。所以养成习惯,每次传输结束都把片选拉高,哪怕下一个传输紧接着要来。
第二个是关于I2C的ACK。I2C每传一个字节,接收方要回一个ACK位。如果主机发了地址之后没收到ACK,说明从机没响应,可能是地址错了、从机没上电、或者从机正忙。这时候不要急着继续发数据,先停下来查原因。我见过不少代码,地址没ACK还继续往下发,结果后面全是乱的,排查起来反而更费劲。
第三个是关于电源和地的。SPI和I2C的通信问题,有相当一部分根源在电源上。从机供电不稳、地线太细、去耦电容不够,都会导致通信时好时坏。我一般会在每个从机的电源引脚旁边放一个0.1微法的去耦电容,位置尽量靠近引脚。如果从机功耗大,还要加一个更大的电容,比如10微法。地线要尽量粗,最好用完整的地平面,不要用细线串联接地。
第四个是关于上电顺序的。有些系统里,主控和从机的上电时间不一样。如果主控先上电,从机还没准备好,主机就开始发数据,从机可能进入异常状态。解决办法是在主机初始化之后加一段延时,等从机电源稳定再开始通信。这个延时通常几十到几百毫秒,具体看从机的上电时间。如果从机手册里有上电时序要求,一定要遵守。
最后说一个心态上的经验。SPI和I2C看起来简单,但真要把它们调稳,需要耐心和细致。遇到问题不要慌,按"电源、波形、时序、代码"的顺序一步步排查,大部分问题都能定位。我调过最久的一个I2C问题,花了整整两天,最后发现是一个从机的上拉电阻在高温下阻值漂移,导致上升沿变缓。这种问题在常温下测不出来,只有到了现场高温环境才暴露。所以如果条件允许,做一下高低温测试,能提前发现很多隐患。