☰
SPI与I2C,两种片间通信哲学,选型与调试实战指南
2026/10/25 1:02:11 网站建设 项目流程

这个系列写到第4.2节,终于要从芯片内部走出来,聊聊两颗芯片之间的对话了。前面那些篇幅,我们一直在处理“片内”——总线矩阵、存储器映射、外设寄存器,这些东西跑在同一个内核下,复杂但有序。但一个嵌入式系统从来不是单核自嗨,主控旁边永远躺着Flash、传感器、ADC、显示控制器,要让这些来自不同厂商、连电平标准都不太一样的芯片协同工作,就得靠SPI和I2C这两个老牌协议。

很多刚接触嵌入式的同学,会把SPI和I2C当成两个“长得不一样”的协议来背:一个四根线、一个两根线,一个速度快、一个速度慢,然后就没有然后了。真正把它俩放在同一个工程里调过几回之后,你会发现它们背后其实是两种完全不同的“片间对话哲学”。这一节就把这两种哲学拆开揉碎,讲清楚“为什么要这么设计”“选型时怎么权衡”“调不出来时怎么查”。

1. 为什么突然要聊“片间”这件事

1.1 片内总线和片间总线的分界线

先说清楚一个容易混淆的边界。MCU内部有总线矩阵、AHB/APB这类片内总线,CPU通过它们去访问Flash、SRAM和外设寄存器。这些通信全程发生在“芯片内部”,不经过引脚,速度快、并行度高、时序由芯片自己保证。但嵌入式系统要干活,光访问自己家内存是不够的,主控必须能和“别人家的芯片”说话——外部存储、传感器、ADC、IO扩展器,全在片外。

为什么不能直接把片内的AHB/APB总线拉出去接外围设备?因为片内总线是为高带宽、并行、低延迟设计的,信号线动辄几十根到上百根,时序要求极严格,一旦走过PCB走线、经过接插件、面对不同厂商芯片的输入特性,信号立刻劣化。所以几乎每一颗SoC/MCU都会在片内总线末端挂一组“通用串行外设接口”,把并行数据转换成低速串行信号送到引脚上。SPI和I2C就是这组串行接口里最常见的两个代表。

打个不严谨但好懂的比方:片内总线像一家公司内部的OA系统,所有部门都在同一张高速局域网里协作,怎么吵都行。片间通信则像两家不同公司之间的商务邮件——报文必须标准、线要少、双方都要容忍对方的办事习惯和节奏。SPI和I2C就是这套“跨公司邮件格式”的两个版本。

1.2 SPI和I2C在整个嵌入式体系的定位

理解了边界,再看这两兄弟的分工就清楚得多。SPI适合“数据量大、连续搬运、双向往返密集”的场景,典型代表是Flash存储器读写、ADC采样数据回传、显示缓冲区推送。I2C适合“报文短、多设备挂总线、慢速状态类访问”的场景,典型代表是传感器配置寄存器、温度湿度周期性读取、外部EEPROM的小块读写。

这两者的市场生态也完全不同。因为I2C只需要SCL和SDA两根线,几乎所有传感器厂商都优先把芯片封装成I2C设备,一块主板上挂七八个传感器是常态。SPI则跟“块存取”关系更近,SD卡、NOR Flash、显示控制器基本以SPI起步,需要快速搬运大块数据时,SPI几乎是默认答案。所以不存在谁替代谁,更多是两种“使用习惯”的互补。做板级选型时,你只需要问自己三个问题:数据量多大?多少时间传一次?总线上还要挂多少设备?答案一出来,选哪个基本就定了。

2. SPI:最“直接”的片间对话

2.1 四条线为什么能全双工(以及什么时候用得上DMA)

SPI四根线:SCK(时钟线)、MOSI(主出从入)、MISO(主入从出)、SS/CS(片选线)。主设备负责产生SCK时钟,MOSI和MISO在时钟边沿配合,主机发送一个字节的同时,也能从MISO上读回一个字节,读写同时进行,这就是“全双工”。

这里有个新手几乎必踩的认知误区:以为SPI是“先发完一包,再收紧一包”。其实一次SPI传输,主机总是在“发一个字节的同时收一个字节”。如果你想读从机的一个寄存器,必须先发一个“地址/指令字节”,这个字节发送期间,MISO上回来的是垃圾数据;等到下一字节,主机再往MOSI发一个“任意字节”(通常写0x00或0xFF),从机才把真正的寄存器数据放到MISO上。理解了“同时性”,就理解为什么寄存器读取普遍是“先发地址、再读数据、中间还要夹一个dummy字节”的流程。

全双工带来的实际工程价值是DMA。以某8通道同步采样ADC为例,每次转换结束后要把8组16位结果读回主控,如果用查询方式,每个字节都等SPI时钟,CPU占用率极高。把SPI配上连续模式、DMA同时搬运发送缓冲和接收缓冲后,发送缓冲全部填0xFF(读操作时的激励字节),接收缓冲直接覆盖采样结果数组,CPU几乎不用管。实测在某个主频240MHz的MCU上,SPI配置为50MHz时钟,连续读256字节数据,从触发到DMA完成中断大概几十微秒的量级,而用查询方式可能在三百微秒以上还带着大量CPU空转。

不过SPI DMA有个高频坑:DMA传输完成中断里顺手把SPI关闭或者清空FIFO,可能导致最后一个字节被截断。正确做法是,在DMA传输完成中断里先确认SPI总线空闲(等待发送缓冲空、忙标志清除),再对结果数组做收尾,千万别急着复位SPI外设。

2.2 模式、极性和相位:新手的第一个坎

SPI有四种工作模式,由CPOL和CPHA两个参数组合而成。CPOL决定SCK空闲电平:为0时空闲低,为1时空闲高。CPHA决定数据采样边沿:为0时在第一个边沿采样,为1时在第二个边沿采样。两两组合成模式0、1、2、3。

大多数从机芯片支持模式0或模式3,少数偏向模式1或模式2。如果模式配置不对,现象非常典型:数据整体错一位、读到0x00或0xFF满天飞、或者第一个字节偶尔丢失。我的习惯是,拿到新从机后先翻数据手册里的时序图,看它明确标注“data sampled on rising edge”还是“falling edge”,再推CPOL/CPHA。不建议盲试模式,盲试对简单外设还行,遇到复杂协议栈的从机,浪费时间不说,还可能因为时序错乱把从机状态机带跑。

这里还有个容易忽略的细节:某些从机对SCK空闲电平敏感。比如从机只能在SCK空闲为高时工作,你配成了模式0(空闲低),MCU一上电后SCK保持低,从机可能把还没开始的传输当成异常。这种问题代码层面几乎排查不出来,只能靠示波器确认空闲电平后回头改配置。

2.3 片选:硬件片选与软件片选的取舍

片选线CS的本质,是让从机知道“这一轮传输属于我”。多数MCU都支持硬件片选,外设会在每次传输开始自动拉低CS、结束自动拉高。硬件片选的好处是时序精准、无需CPU干预,坏处是“太死板”。很多MCU的硬件片选默认是单次传输即释放,如果从机要求“CS保持低电平期间连续传多个字节/跨多个帧”(比如Flash的多字节页编程命令),就必须额外配置才能让CS在整个操作期间保持低。搞不好还会出现“帧间出现CS抖动的毛刺”,直接让从机断帧。

软件片选则是拿一个普通GPIO引脚手动控制CS:传输前置低,传完置高。好处是灵活到极致——想保持低多久就保持多久,想跨多少个SPI帧都由你决定。代价是必须保证“拉低CS”和“拉高CS”两步之间不被RTOS的高优先级任务强行打断,否则CS低电平时间被无限拉长,同样会引发从机超时。

还有一个容易被踩到的问题:当总线上挂了多个从机时,如果某个从机的MISO采用了推挽输出,而它没有被片选使能,理论上应该处于高阻态。但有些芯片在非使能状态下MISO仍然被内部下拉或上拉逻辑影响,一旦另一片被选中并使劲驱动MISO,两者就可能打架,导致读回数据偶发出现0xFF。我在同时挂两片SPI Flash的板子上踩过这个坑,最后把未选中从机的输出使能显式关掉,问题才消失。所以,SPI多从机共用一个MISO时,提前算一下“哪颗芯片会输出、哪颗不会输出”很重要。

3. I2C:一根线卖百万颗芯片的秘诀

3.1 开漏和推挽:别小看线的驱动方式

I2C只有两根线:SCL(时钟)、SDA(数据),但有一个强制前提:两线都必须工作在开漏模式,并靠外部上拉电阻把电平拉高。开漏输出只有“输出低”和“悬空/高阻”两个状态,加上上拉电阻,就实现了“谁都能拉低、谁都能释放”的总线共享。几颗芯片挂在同一对线上,只要任意一个器件把线拉低,其他器件看到的就是低电平,这天然实现了“线与”逻辑。

这跟SPI的推挽输出形成了鲜明对比。推挽输出会主动驱动高和低,两个器件同时驱动会发生冲突烧毁或电平打架,所以SPI总线很难像I2C那样随随便便挂几十个设备。I2C能做到多主仲裁、多设备挂载、时钟拉伸,根子就在这个开漏机制上。

实操上要特别注意的是:很多MCU的GPIO虽然内部有弱上拉或支持开漏模式,但外部板子上未必焊了合适的上拉电阻。有的芯片内部上拉阻值太大,跟总线电容一叠加,边沿变得很缓,高速模式下直接跑不起来。现象就是扫描设备失败、数据错乱、偶尔读到一半卡住。我的排查习惯是:先确认板子上SDA和SCL的上拉电阻不只是“存在”,还要看阻值是否匹配——标准模式常用4.7kΩ,快速模式常用1kΩ~2.2kΩ,总线越长、设备越多,上拉要越强。

3.2 时序、时钟拉伸和寻址(7位/10位)

I2C的协议细节集中在几个基本时序单元上:起始条件(SCL为高时SDA由高变低)、停止条件(SCL为高时SDA由低变高)、数据位(SCL为低期间SDA允许变化,SCL为高期间SDA必须稳定)。这一套看起来枯燥,但在排查“总线卡死”时,缺少任何一个条件都会让从机状态机乱掉。比如早期调试常见的“发送停止条件不完整,从机不释放总线”,信号稍微差一点,示波器上可能看不出来,但总线上就是下一个起始条件起不来。

时钟拉伸是I2C特有的机制,从机觉得自己没准备好,可以一直把SCL拉低,强制主机等待。这在EEPROM写周期、模数转换内部完成阶段尤其常见。如果主机的I2C控制器没配置超时机制,从机又一直不释放时钟,整条总线就会彻底卡死,波及总线上所有设备。很多老工程师会教一招:遇到I2C疑似卡死时,手动给SCL连续发9个时钟脉冲,再发一个停止条件,让从机状态机复位。这个方法非常实用,但只治标,要治本还是得在初始化时打开总线超时中断、通信前检查总线是否空闲。

寻址方面,7位地址最多挂一百多个设备,10位地址用于超多设备场景,工程里用到7位寻址就够。最容易搞错的点是“地址左移”:数据手册写一个7位地址,比如0x50,实际通过总线发送的字节是0xA0——因为最低位要被读写标志占用(0x50左移一位再补0表示写,补1表示读)。这个“左移一位”,是新人在I2C上踩得最深的一个坑。

3.3 电平转换:什么时候需要,什么时候不需要

I2C电平转换是个非常实际的话题。很多传感器模块是3.3V,主控可能是5V或者1.8V。幸运的是,正因为I2C是开漏结构,电平转换天生就简单:把上拉电阻接到目标电压域,就能实现电平匹配。不需要像UART、SPI那样在两个电平域之间插转换芯片(串联电阻方案也是常用的凑合手段,但可靠性一般)。

常见的做法有两种:一种是MOS管双向电平转换电路,原理是任意一侧拉低都会把另一侧也拉低,高电平由上拉电阻完成;另一种是专用的I2C总线缓冲器/电平转换芯片,内部做双向缓冲和电平检测。选哪种,看总线速度和电容。如果总线上只有两三个设备、时钟不超过400kbps,MOS管方案完全够用;如果总线长、设备多、要跑1MHz甚至3.4MHz高速模式,就必须上专用芯片,否则MOS管的阈值会把低电平“吃掉”,从机根本没收到有效的低电平。

我踩过的一个坑是:传感器模块板载上拉接到了1.8V,我这边主板又接了一颗上拉电阻到3.3V,结果两个电压域同时对上拉“抢电压”,SDA和SCL的电平被抬到一个不上不下的位置,主从两端的识别都变得极其不稳定,波形在示波器上看就是锯齿。最后把所有上拉电阻统一到同一个电压域,问题立刻消失。所以接I2C总线前,先看清楚模块上自带上拉的电源轨是哪一路,然后再决定要不要额外加上拉。

4. 实操对比:ADC、EEPROM、OLED、编码器该选谁

4.1 典型外设的接口选型

实际项目里选择SPI还是I2C,不是凭喜好,而是看需求结构。

数据吞吐量大的接口,SPI优先。8通道同步采样ADC、SD卡读写、LCD显示缓冲区刷新,这类场景动辄每秒几MB到几十MB,I2C在400kbps下极限也就每秒几十KB,就算跑1MHz,协议本身的地址、停止条件、ACK位还会吃掉一部分带宽,根本喂不饱。反过来,传感器配置寄存器、温度湿度周期性读取、姿态数据低频上报,这类都是几十字节量级,I2C两根线能挂十几个传感器,布线成本极低,效率反而更高。

可靠性上,SPI倾向于“主机完全掌控”,从机没有机会把总线挂死;I2C则在多设备、多主场景下更灵活,但引入的时钟拉伸、总线仲裁都可能成为新的不稳定点。跑量产设备时,我会给I2C总线加上总线错误中断和定时器复位机制,SPI则重点做CRC或校验和。

一块典型的板子可能是这样:主控通过SPI接NOR Flash和高速ADC,通过I2C接温度传感器、气压计、电池管理芯片和外置IO扩展。两条总线各司其职,谁也不耽误谁。所以“选SPI还是I2C”在真正的板级系统里并不是单选题,而是混合使用。

4.2 SPI和I2C的速率与可靠性的实测差异

用同一块板子实测过(某国产Cortex-M内核MCU,主频120MHz),数据很能说明问题:

  • SPI设为18MHz时钟,读256字节:查询方式大约300~400µs,DMA方式大约150µs左右;
  • I2C设为400kbps,读连续256字节:不考虑从机额外延迟,单是协议开销,理论就需要256×9×2.5µs≈5.7ms,再加ACK等待和停止条件,实际更容易落在6~10ms。

同样是读256字节,SPI比I2C快一个数量级还不止。这不是I2C“差”,而是两者的设计目标不同——I2C为省引脚、多设备、小报文而生,速度上限天然低;SPI为搬运大块数据而生,代价是引脚多、单主结构、时序敏感。可靠性方面,SPI更容易出时序不匹配的问题,但一旦稳定后极少因总线上其他设备干扰而出错;I2C更容易出总线挂死类问题,但可用的排查手段也多。

5. 常见问题与排查技巧

5.1 SPI挂在第一个字节,I2C死在NACK

SPI挂在第一个字节,常见原因就那么几条:片选没拉低、SPI模式不对、MISO/MOSI接反、从机供电刚上电还没稳定。其中“接反”最隐蔽,因为接反后有时读回全是0,有时读回的是正确值左移一位,很多人会反复调时序而不去查接线。我的排查顺序永远是先查接线和电平,再用示波器或逻辑分析仪看SCK和片选波形,最后才怀疑外设寄存器配置。

I2C死在NACK,常见原因:地址没左移或读写位错误、从机没上电、EEPROM正在页写入周期、总线上拉电阻缺失。排查NACK最好的工具是逻辑分析仪,专抓起始条件、地址字节、ACK位和停止条件这四个关键信息,一眼就能看出是“从机就不认识这个地址”还是“从机正忙没空应答”。切忌重复重试,先定位根因。

5.2 软件模拟和硬件外设的坑

用GPIO模拟SPI/I2C的优势是任意引脚都能用、时序完全可控,缺点是实时性难以保证。我在一个带RTOS的项目里,软件模拟I2C发送地址字节和数据字节之间被高优先级任务打断,硬生生把一个字节拆成了两段,从机状态机直接乱掉。后来换成硬件I2C外设加中断,问题才算根治。

硬件外设也有它的坑:引脚复用功能没开、外设时钟没打开、内部弱上拉没有启用,这些“既然寄存器配置了对但引脚就是没信号”的情况,往往要卡半天。所以配置硬件I2C/SPI时,我的流程固定是:先查引脚复用表,再开外设时钟,再配置GPIO模式和上下拉,最后才初始化外设本身。

5.3 几个值得记住的经验细节

  • 读EEPROM时,写完地址后一定要先发停止条件再发起读,否则一部分型号的EEPROM不会正常切换到读状态。
  • 用SPI访问SD/TF卡模块时,卡初始化阶段尽量压低SCK频率,等卡完成初始化后再切高速模式。初始化都读不出来,别急着怀疑SPI模式,先降频试试。
  • 对某些从机,CS拉低后就等着SCK来,如果CS拉低后主机迟迟不发时钟,从机可能直接进入异常状态。遇到“CS拉低后从机死锁”时,试着在CS拉低到第一个SCK边沿之间加几十纳秒到几微秒的延时,往往能解决。
  • 同一I2C总线上若挂了多颗地址完全相同的芯片(且地址引脚不可改),它们一定会冲突。要么换不同地址,要么用I2C多路复用器(常用的一种是8通道的I2C开关,TCA开头的某型号),要么引出多路I2C物理接口。
  • 不同厂商基于Cortex-M内核的MCU,SPI外设初始化参数差异极大,尤其“硬件片选”和“软件片选”的默认行为、NSS引脚要不要复用、FIFO阈值怎么设,都不能把一家芯片厂商的代码直接复制到另一家。这早就不是“配置一个SPI模式0就通用”的时代了。
  • 有些工程师会用“SPI回环测试”来判断接口好坏:把MOSI和MISO短接,发什么收什么。这个方法用来验证硬件通路确实好用,但只能证明“通路是好的”,证明不了“从机协议正常”。真正的接口测试还是要拿一块已知正常的从机,配合校验和或已知模式数据来比对。

这些经验看起来零碎,但每一个都对应着真实项目里耽误过的时间。留成清单的好处是,再遇到类似现象时不至于重新踩一遍。

6. 调试装备与实战抓波形(新手最容易忽略的一环)

6.1 逻辑分析仪优先于示波器

调试SPI和I2C,我强烈建议先把逻辑分析仪准备好,而不是一上来就抱着示波器。逻辑分析仪便宜、通道多、触发灵活,一次能同时抓SCK、MOSI、MISO、CS四路信号,或者I2C的SCL+SDA组合。它的采样率不用太高,25MHz以上基本够用(SPI跑到50MHz时有点吃力,但绝大多数场景在18MHz以内)。关键是,逻辑分析仪能长时间保存波形,方便逐字节解码和回头翻看异常点。

示波器当然也有用,看信号质量、边沿陡峭程度、毛刺都必须靠它。但排查协议逻辑问题时,逻辑分析仪追求的是“能不能正确解码出字节”,对新手友好得多。

6.2 抓SPI波形时重点看哪些信号

抓SPI波形先看四样东西:CS低电平区间是否覆盖了整个传输过程、SCK时钟个数是否跟预期字节数匹配、MOSI上发出的地址/指令是否与代码一致、MISO上返回的数据是否在正确的采样边沿对齐。

特别要注意CS“提前释放”的问题。CS本该在最后一个字节的最后一个SCK边沿之后才拉高,如果配置了硬件片选但帧长和字节数没配对,CS可能在中间就弹回去了。这种波形不仔细看很容易被当成“偶尔读取失败”。还有,如果MISO上的数据看起来像是错位了一位,往往不是从机问题,而是主机把CPHA配错了,导致在错误的边沿采样。

6.3 抓I2C波形时重点看哪些信号

抓I2C波形,重点看起始条件是否干净、地址字节的8个bit是否符合预期、ACK位是否被从机拉低、停止条件是否完整。常见异常是:起始条件之后地址字节只发出了7个bit就没了——这是地址左移没做对,或者是写入的寄存器地址长度填错了。

另一个隐蔽问题:总线空闲时,SDA和SCL必须都是高。如果发现空闲时SDA被拉低,说明某个从机的状态机卡在半途,或者上拉电阻没接到正确的电源轨。处理办法就是前面说的,先发9个SCL脉冲加停止条件让总线复位,然后排查具体是哪颗设备在拉低SDA。

调试的时候养成一个习惯:复位后的第一件事,永远先抓一下总线空闲电平。这一步能排除一大半“莫名其妙”的问题。

做嵌入式做到后面,你会发现SPI和I2C不是需要“背熟”的协议,而是需要“信任”的工具。我个人的体会是,尽早买一台逻辑分析仪,比什么资料都顶用。每次调不通,就抓波形,对着时序图比对,你很快就能形成一种直觉——看到波形就知道是模式配错了,还是上拉不够,还是CS提前收了。另一个经验是,板级选型时想清楚“每一帧数据有多大、多少时间传一次、总线上还挂了多少设备”,这两个协议就真的不会用错。引脚永远是板上最贵的资源,该省的地方(像I2C多设备共线)坚决省,该阔的地方(像SPI高速搬运)也绝不因为想省两跟线把整块板子的性能拖垮。这一节写到这儿差不多正好,下一节就可以沿着这些总线继续往外走,看看真正连接两个世界的信号,是怎么变成系统里的车辙印的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询