1. 这个系列为什么突然讲起了片间通信先交代一下背景。这个“从沙子到车辙”系列的上一期我们聊的是芯片内部怎么把数据从CPU核心里搬出来——总线、DMA、存储控制器那套东西。到了第4.2篇视角要从“片内”跨到“片间”了也就是芯片和芯片之间怎么说话。这个话题的入口正好就是SPI和I2C这两个名字几乎每个嵌入式工程师都挂在嘴边、但未必真正吃透的协议。很多人对SPI和I2C的印象停留在“会用就行”传感器接上去读个寄存器出来数据能用就算了。但实际做项目你会发现同样一颗传感器挂在SPI总线上和挂在I2C总线上行为完全不一样同样是I2C100K和400K的时序要求不一样多主机和单主机的处理更是两码事。这篇我不打算只罗列协议特性而是想把两条协议从“为什么会出现”讲到“实际调板时怎么避坑”补齐那些手册里不会写、但调试时一定会撞上的细节。适合谁看正在学嵌入式的学生、刚接触驱动开发的初级工程师以及那些天天写逻辑但没深究过信号时序的“应用层玩家”。不会涉及太难的高频信号理论但读完你应该能说清楚为什么SPI没有应答机制也能活得很好为什么I2C用两根线就能挂几十个设备以及你的下一个项目到底该选哪个。2. 整体思路从片内总线的尾巴说起2.1 片内通信的“家规”为什么不能直接搬出去上一期提到过片内的数据传递靠的是总线矩阵、AHB/APB这类的并行总线。特点很鲜明线多几十根起、速度快、时序极短而且芯片内部的走线很短信号同步性好。这套机制如果直接拿来做芯片间通信会遇到一个现实的物理问题引脚不够。你去看看一颗MCU的封装通用型号通常在48到144个引脚之间去掉电源地、复位、晶振、调试口能做通用IO的撑死也就一半。如果每接一个外部芯片都要拉30根并行线那接一个外部ADC你的GPIO就快没了更别提还要同时挂Flash、屏幕、传感器。所以片间通信的第一诉求不是极致速度而是“用尽量少的线完成控制的活”。这就解释了为什么SPI和I2C都是串行协议——一根数据线代替八根并行线代价是速度下降但对绝大多数外设传感器、EEPROM、ADC、DAC、RTC来说几十K到几十M的速率已经够用。另一个关键差异是同步方式片内总线有全局时钟所有模块共享同一个节拍片间则必须由主机主动产生时钟从机跟着节拍干活于是就有了“主从模式”这个最基本的结构。2.2 两条路线的设计哲学差异SPI和I2C是在同一个历史时期为了解决同样的问题串行外设互连而出现的但设计思路完全相反这在电气层面就直接分道扬镳。SPI走的是“简单粗暴”路线至少四根线SCLK提供时钟MOSI和MISO各管一个方向的数据CS片选负责点名。它不管从机地址不管总线冲突各设备自主死各自占各自的CS。实现简单性能上限高代价是引脚多、无错误反馈、一主多从时CS线数量跟着设备数涨。I2C走的是“精打细算”路线只有两根线SDA送数据、SCL送时钟。所有设备并挂在同一条总线上靠7位地址来区分。为了做到真正的“总线”形态它发明了开漏加外部上拉的电气结构还设计了ACK应答、仲裁、时钟拉伸这一整套协议层机制。线省了但软件和硬件的复杂度上来了速度还被上拉电阻和总线电容卡住。这两条路线没有谁绝对优于谁只有是否适合场景。我做方案选型的时候基本就看三件事通讯距离、从机数量和速度要求。器件间距离不超过10厘米、从机数量在4个以下、单次传输几百字节以内两边都能干差别不大但一旦从机超过5个或者速率要求超过10Mbps我基本会倒向SPI因为I2C在高速下对PCB走线、上拉阻值的容错率太低了。3. SPI核心细节四根线背后没那么简单3.1 主从拓扑和数据交换机制先建立一个基本认知SPI是同步、全双工、主从架构的串行通信协议。同步的意思是发送和接收由同一个时钟驱动主机发SCLK从机在时钟的边沿采样和移位数据。全双工是说MOSI和MISO独立主机发数据的同时也能收数据收发在同一个时钟周期内完成。把SPI想象成两个移位寄存器首尾相连的环形结构主机的移位寄存器移位到MOSI时从机的移位寄存器同时通过MISO把数据送回主机。所以SPI的每一次传输交换字节数一定是相等的——你想从从机读回1个字节主机也必须要“发”出去1个字节哪怕这个字节是废的0x00。初学者最容易犯的错就是只发不收、只收不发导致读出来的数据永远错位就是没理解这条“交换”逻辑。CS片选线的意义是低电平有效选中谁谁就参与传输。之所以叫“片选”而不是“使能”是因为它同时承担了总线占用声明和从机内部逻辑复位的职责。高强度项目里CS的建立时间CS拉低到第一个时钟边沿的时间和保持时间最后一个时钟边沿到CS拉高的时间都有具体指标搞错了从机可能直接收不到数据。3.2 CPOL和CPHA四个Mode的区分不是玄学SPI的时序核心是时钟极性CPOL和时钟相位CPHA。CPOL决定空闲时SCLK是高还是低CPHA决定数据在哪个边沿被采样。这两个比特组合出Mode 0到Mode 3绝大多数的从机设备默认支持的是Mode 0CPOL0, CPHA0也就是空闲时钟为低、第一个边沿上升沿采样数据。我建议你写驱动前先翻数据手册看时序图而不是直接问“这个设备用哪个Mode”。因为不同厂商画的时序图风格千差万别有些把MOSI和MISO画得含混不清有些甚至不标注CPOL/CPHA只给出“Data shifted in on falling edge”之类的文字。对照“数据在哪个沿变化、在哪个沿采样”这两个信息去核对比死记模式编号可靠得多。调试时遇到数据错位、乱码第一个要排查的往往是匹配主机和从机的Mode。我踩过一次很典型的坑某国产温度传感器手册里推荐Mode 3但它的示例代码按Mode 0初始化结果读出来的温度每隔几个样本就会跳一次大数看起来像硬件问题实际上就是采沿采在了数据变化的瞬间属性和时序竞争导致的偶发毛刺。3.3 速率上限和时钟极性带来的隐患SPI的速率上限不像I2C那样由协议规定而是取决于从机能跑多快、PCB走线又多长。常规板级通信10M到几十M都很正常但速度上去之后会开始碰到信号完整性问题走线长了会出现振铃和过冲SCLK和数据之间的时序偏差会导致采样失败。这时候可以适当降速、缩短走线、加串阻或者用示波器查看实际信号质量再做调整。另外有一个容易被忽略的点SPI是全双工同步传输主机和从机如果都工作在极速状态从机的处理跟不上会在MISO半路变数据导致前半段有效、后半段无效。很多外设手册会给一个“maximum SCLK frequency”参数那个是电气上限软件上还要给从机留出“数据准备好”的时间比如读ADC转换结果之前先等转换完成标志。这类等待直接体现在业务层状态机里而不是SPI控制器本身。4. I2C核心细节两根线省出来的麻烦4.1 开漏结构、上拉电阻与“线与”逻辑I2C最值得玩味的硬件设计是开漏输出加外部上拉。每个器件只是把SDA和SCL拉低释放后靠上拉电阻把电平拉回高这就是所谓的“线与”逻辑只要有一个器件拉低总线就是低电平。这套结构带来的第一个直接好处是支持多主机仲裁和从机时钟拉伸。第二个隐性好处是电平转换容易——只要上拉电阻接到目标电压的电源总线就会是那个电压电平不同电压域的设备也能共存于一条总线上。比如3.3V主控和5V传感器之间只在I2C信号线上用开漏结构加上拉到5V就能安全通信不需要额外的电平转换芯片。上拉电阻的取值不是随便选的。电阻太小总线低电平拉不低、静态功耗大严重时直接拉坏器件电阻太大上升沿变缓高速模式根本无法建立有效时序。工程经验上100K模式配10K上拉400K模式配2.2K到4.7K前提是整个总线的等效电容不超过400pF。总线上的设备数量、走线长度、每个引脚的寄生电容都会加总设备挂多了就得“电阻换小”或者“降速跑”。你可以用RC充电时间去估算上升沿大约是0.7倍的上拉电阻乘以总线电容保守做法是让上升沿时间不超过位周期的四分之一。4.2 7位地址、写死地址和寻址冲突I2C有两种地址长度7位和10位。绝大多数普通传感器和EEPROM用的是7位地址10位地址主要用于地址资源不够的场合。7位意味着理论上一条总线上最多可以挂128个设备但实际因为A0/A1/A2引脚通常只引出3个引脚一般同型号设备最多只能挂8个再多就地址冲突了。器件手册里常写的地址是一个8位的“写地址”或“读地址”比如0xA0是写、0xA1是读其实是7位地址0x50左移一位加读写位得到的。写驱动时千万要看清你的代码里填的地址到底是完整的8位地址还是7位地址。很多开发库按7位处理你填0xA0进去多出的方向位会被当成地址的一部分分分钟总线NACK。我自己就在某存储芯片上因为这个问题折腾过半个晚上后来换用示波器抓波形才明白是自己地址填翻车了。挂多个相同型号器件时A0/A1/A2的接法决定了可用地址范围。有的芯片支持地址内部分配有的则焊死固定采购和PCB设计时就要先把编址方案定下来避免软件里改地址、硬件上又要飞线。4.3 ACK/NACK、时钟拉伸和多主机仲裁I2C的应答机制是强制性的每个字节传输之后接收方必须在第9个时钟位上拉低SDA来回应ACK如果接收方主动拉高SDA也就是NACK就表示“我不想收了”或者“我收出错了”。主机读到NACK基本可以判定从机地址错、器件不存在或者从机忙到拒绝响应。时钟拉伸是I2C里比较特殊但实际非常常见的机制从机发现自己处理不过来可以把SCL拉低强制主机暂停。比如某些寄存器需要内部EEPROM刷新时间刷新期间访问就会时钟拉伸主机等它释放时钟再继续。如果主机侧用的是不带时钟拉伸感知的软件模拟I2C很容易在高速下超时所以写软件I2C时一定要把等待SCL释放的逻辑写上。多主机仲裁解决的是两个主机同时在总线上发言的问题。逻辑是“低电平优先”谁先把SDA拉低谁就赢得总线。另一台主机在发送数据时会不断监测SDA发现总线电平和自己预期不符就退出竞争。这套机制在很多实时系统里有实际意义比如一个MCU按需做主、一个平时只做从需要切换时直接做仲裁不扯皮。但要注意的是多主机I2C的软件状态机复杂度比单主机高得多如果一个项目没有必要别给自己找活干老老实实单主机。5. 实操对比SPI和I2C的选型与现场调参5.1 一张表看懂关键差异维度SPII2C线数MOSIMISOSCLK每设备至少1个CSSDASCL地址机制CS片选无地址概念7位/10位地址挂在同一条总线上寻址双工性全双工半双工SDA双向分时复用应答多数无不支持硬件ACK部分从机有状态位每字节ACK/NACK硬件强制速度板级可到数10M以上上限由器件和布线决定标准/快速/高速模式常见100K/400K/1M错误恢复靠主机超时和片选复位NACK、超时、总线复位序列9个时钟电平兼容电平域不同需要转换芯片开漏上拉即可跨电平设备规模从机多CS多引脚爆炸地址不冲突就能挂很多线还是2根5.2 什么时候选SPI高吞吐是硬指标SPI适合对吞吐有明确要求的场景。最典型的是驱动一个带帧缓冲的彩色LCD屏或者一颗高速ADC一次要传的数据是几十到几百字节而且每个字节都是有效数据I2C每发送一帧还要先传地址、寄存器地址、再传数据有效载荷比例明显偏低。另一个选SPI的理由是你的从机数量少但要求“独占通道”。比如板载一颗带中断输出的传感器又有一颗Nor Flash如果都挂I2C传感器频繁中断时可能要和Flash的数据传输抢总线时序尤其Flash搞页编程的时候动不动时钟拉伸几十毫秒传感器侧的数据刷新就会卡顿。改成SPI之后每颗器件各用各的CSDMA传输也完全独立互不干扰实时性更好。SPI也适合做主控和无“智能”外设之间的桥梁比如和FPGA做板级数据交换。FPGA侧往往把SPI接口做成简单的FIFO读写逻辑数据吞吐量可以做得很大甚至有人用SPI跑几十Mbps的连续流I2C在这种场景下基本没有竞争力。5.3 什么时候选I2C省引脚才是王道I2C的杀手锏是省引脚和总线复用。我做过一个项目主控除了常规的MCU外设还要挂六个I2C器件三个温湿度传感器、两个EEPROM、一个RTC。如果全用SPICS线就要六根加上时钟和数据线引脚直接爆炸而I2C只需要两根线最多再加两个中断响应引脚。I2C的“线少”还带来一个工程红利PCB布线好走双面敷铜的板子也能在不破坏电源平面完整性的前提下把两条信号线走得干净。另外I2C天然支持总线枚举调试时可以写一段扫描代码把总线上所有响应地址列出来核对硬件接线是否和你预期一致比SPI一根根CS去试快得多。如果你做的是多板互联系统I2C的地址机制同样有用。比如子板上放一个微控制器做I2C从机主控通过I2C去读写它的寄存器两条线就把“控制通道”打通了。这种情况下I2C的ACK机制其实帮了大忙主机能直接确认究竟有没有子板在线上SPI则做不到——你只能看CS拉低后从机有没有响应而大部分SPI从机不会主动报错。5.4 速度路线的取舍一个真实例子举个我调过的具体例子某数据采集板主控和一块高速ADC之间ADC输出8位并行数据我选了SPI读如果ADC支持串行模式的话。ADC本身是1M采样率每样本12位SPI时钟我配到32MHz一次读一个样本只要不到0.5微秒DMA搬运完全不用CPU介入主控可以一边采集一边跑浮点算法。同一个板子上的RTC和EEPROM我则全部走I2C。因为它们一天最多被读写几次用SPI纯属浪费引脚而且I2C的ACK和重传机制在低频小数据包场景下非常可靠不会因为传输速率过高产生边缘毛刺。这种“一板双协议”其实最能说明问题SPI和I2C不是替代关系而是互补关系。选型的本质是算清楚你的数据速率需求、设备数量、引脚预算、信号完整性风险四者权衡之后再落方案。6. 实操体验波形怎么看、代码怎么写6.1 用一个最小例程打通SPI读写我以最常见的“主机读一颗传感器寄存器”为例讲一下SPI主机侧的操作流程。这里先假设传感器挂在SPI总线上CS接普通GPIO控制器用原生SPI外设。第一步初始化SPI外设时钟分频要算目标速率用“系统时钟 / 分频系数”得到不能直接写目标频率Mode根据传感器手册选数据大小一般是8位少数12位或16位的ADC要对应改。第二步初始化和CS相关的GPIO默认输出高电平不选中设备。第三步是每个读写事务的标准动作拉低CS发命令字节发寄存器地址字节然后连续发N个0x00字节同时在MISO上收数据收完拉高CS。提示如果从机要求命令和地址之间、地址和数据之间需要延时一定要在状态机里显式写us级延时别指望SPI控制器自动处理。这是新手容易漏的。关键点是SPI的DMA传输可以整段安排用DMA把发送数组和接收数组配对好一个传输完成后产生中断CPU只在最后做一次数据解析。吞吐率差距非常大如果用中断逐字收发每字节都会被中断代码拖慢高时钟就白配了。6.2 软件模拟I2C和硬件I2C的坑I2C这个协议在MCU上实现有两种方式硬件外设或软件模拟GPIO翻转延时。硬件I2C占用CPU少但每个厂家的控制器寄存器风格不同遇到无应答自动重试、中断标志位不清、FIFO溢出这类问题排查起来非常费劲且各平台差异大。软件I2C的稳定性和可调试性反而更好尤其遇到地址有歧义、总线上有多个不同电压的设备逐一翻转波形看数据心里非常有底。软件I2C要正确实现核心是状态机的严谨性。标准的写时序是起始条件SCL高时SDA拉低→发送7位地址写位→等待ACK→发送寄存器地址→等待ACK→发送数据每个字节后等待ACK→停止条件SCL高时SDA拉高。读时序稍微复杂一点读到最后一个字节后主机要发NACK再发停止条件告诉从机别再发了。很多人软件模拟I2C翻车问题大多出在“SCL和SDA操作顺序”上。每次数据变化必须发生在SCL低电平期间SCL从低变高之后总线电平就必须稳定下来。如果操作顺序反了会在SCL上升沿前后出现数据跳变直接把I2C时序破坏。写代码时每一位的处理建议顺序是SDA先设好输出值再延迟SCL拉高再延迟SCL拉低再延迟这样保证采样窗口内数据稳定。6.3 示波器看波形的几个判断点调通信时示波器是最好的朋友。建议至少看这几个点先看波形整体——SCL是否连续SDA电平是否在SCL低电平期间才跳变。随便卡一处“在SCL高时SDA变了”就说明代码顺序或者时钟相位有严重问题。再看地址阶段的ACK位第9个时钟后SDA有没有被拉低。如果没有要么从机地址错要么从机压根不在线上。然后看停止条件主机结束传输时有没有正确地“在SCL高时SDA拉高”很多从机依赖于这个条件提交内部操作如果停止条件有问题寄存器写入会莫名其妙地不生效。用逻辑分析仪比示波器更高效可以直接解码出I2C的地址和操作序列哪一步错了一目了然。调SPI时同理逻辑分析仪能看到CS波形和时钟脉冲个数快速判断是不是多传或少传了一个字节。6.4 讲讲DMA和中断的分工无论是SPI还是I2C一次传大量数据时都建议用DMA这个在上一期聊过总线矩阵时有提过。SPI的DMA通常最简单外面两个缓冲区一个配置发送、一个配置接收就可以I2C的DMA则要注意很多硬件I2C控制器在每字节ACK/NACK处理上和DMA之间有配合关系用DMA时直接关掉字节中断只在传输完成时开一个完成中断能省掉很多重复的ISR调度开销。中断回调里能做的事要尽量少只做标志位设置、数据搬运把解析放主循环。如果中断里做浮点运算或调用延迟函数系统整体响应会变得不可预期尤其是多设备共享总线时慢中断会显著拖平均传输间隔。7. 常见问题与排查技巧实录7.1 一抖一个坑我实测中遇到的问题速查表现象可能原因排查方法I2C扫描不到设备地址填错、线接错、上拉遗漏用逻辑分析仪确认起始条件、地址字节、ACK位I2C通讯正常但数据错乱上拉电阻太大、总线电容超限、SCL上升沿太缓示波器检查上升沿时间换小电阻或降速I2C偶发死锁、总线上SDA低住不动从机异常后一直拉低总线被锁死连续发9个时钟脉冲让从机释放总线查电源是否稳定SPI读回来的数据整体偏移1位主机和从机的CPOL/CPHA不一致用示波器核对数据和时钟的采样沿直接改ModeSPI数据前几位对、后面错主从机数据位宽不匹配确认从机是8位还是16位传输别默认全8位SPI CS拉低后从机没反应上电顺序或从机复位时间未满足看从机数据手册的CS建立时间增加延时高速传输偶发错字节走线长、串扰、电源噪声降速确认现象再改善布线、加滤波电容7.2 总线死锁的“9脉冲恢复法”和软件处理I2C总线被从机拉死这个问题处理过生产环境的人应该都熟。SPI这种主从强控制的协议天然不会遇到因为CS直接重置了从机状态机的所有状态I2C则不同只要总线上出现残次时序有些从机就会把SDA拉死表现为总线SCL正常但SDA一直是低、且你发任何东西都不ACK。常规对策是标准的总线恢复序列把SCL当作普通GPIO主动翻转9个时钟脉冲同时监测SDA是否在某个脉冲后释放。这个操作能够把残次状态机推回到空闲状态。但要注意恢复之前必须先确认没有其它设备正在占用总线否则会把别人的正常事务打断。生产代码里建议把总线恢复逻辑做成独立任务检测到锁死时自动触发避免设备死等。软件层的处理同样重要每次I2C事务之前和之后都要保证总线上没有“半途而废”的传输尤其不要在中断里突发读写。中断里做I2C一旦被更高优先级的中断打断SCL和SDA操作序列就会错位代码写完发现只有低速模式能跑高速模式随机挂基本就是这个原因。7.3 换了个从机型号同样的代码却不好使不同厂商的从机在同一个协议框架下的行为差异很大这个坑比想象中常见。换器件后首先要重新核对地址位宽、寄存器映射、命令格式和从机支持的时钟频率特别是I2C的不少新器件开始支持“高速模式”或者“超快速模式”你要确认主机能力是否匹配而SPI则是Mode和位宽的差异。其次是命令的“组合形式”不同。有的EEPROM写一个字节需要“写指令地址数据”三条时序完成另一家的逻辑可能是“地址数据”就行。如果你的驱动是从之前某个项目复制过来的建议别省看数据手册那几分钟。我就遇到过换上某国产存储芯片后读ID一直失败折腾了一个下午最后发现这个芯片要求CS拉低后先发一个特殊的“读取ID使能”命令而原来那家根本不需要。7.4 电平不匹配看似能通实则随时可能炸现在不少设备支持宽电源电压但还远不是全部。I2C靠开漏上拉的电气结构能够实现不同电压域共存但仍需确认引脚是否“真正支持开漏”。有些MCU的IO在做开漏输出时如果没有设置成“开漏模式”实际输出的是推挽结构总线拉低速度极快但释放时只能由外部上拉慢慢抬逻辑上能用、信号上却丑长线时偶发误码。SPI的情况更苛刻协议本身没有任何电平协商机制主机3.3V直接接从机5V从机的内部钳位二极管可能被灌电流反复冲击短期不出问题时间久了器件性能退化或直接击穿。跨电压做SPI必须上电平转换芯片或者选带电平转换功能的IO扩展器千万不能靠“电阻分压”偷懒因为MOSI、MISO、SCLK、CS四个信号方向各不相同单靠分压很容易把后沿搞缓。8. 最后说点个人习惯和小技巧我调板多年有个习惯每一个用到SPI或I2C模块的项目原理图阶段就把CS或I2C地址的分配规划写在设计文档里排查问题时直接对着文档核对而不是临时翻数据手册。别小看这一步多人协作时它避免了一堆无谓的“地址冲突”“CS错位”争执。另一个小技巧是调试时先跑最低速。SPI先跑1MHz、I2C先跑100K验证逻辑无误之后再把时钟拉上去这样能有效隔离“逻辑错误”和“信号质量错误”两件事。如果你一上来就高速出错了整个问题空间就大了很难分清到底代码错、时序错还是布线错。还有一个非常实用的习惯把所有I2C设备的地址扫描做成一个开机自检函数每次上电自动跑一遍把扫描到的设备地址、响应时间打印出来。这既能在产线测试时快速发现焊错、贴错的板子也能在后期维护时判断是不是有器件老化到不响应。SPI侧则可以在自检里做“回环测试”——把MISO和MOSI短接发一串测试数据读回来比对有出入就说明链路有问题。这两个协议的本质区别我总结成一句话SPI追求速度和简单适合一对一、高速、大批量的数据搬运I2C追求布线和寻址效率适合一对多、低速、控制型的小包交互。把这句话刻在脑子里做选型判断时就不会纠结太久了。
阅读完成 · 觉得有帮助?