说实话,第一次看到“从沙子到车辙”这个系列名的时候,我以为是讲硬件从零开始DIY的,结果读了几篇才发现,这是讲嵌入式底层通信的,从“沙子”里的硅片讲到“车辙”一样清晰的信号路径,脑洞确实大。这一篇聊的是“从片内到片间”,也就是芯片和芯片之间、模块和模块之间怎么把数据说清楚。而说到这个,绕不开的两大主角就是SPI和I2C。
做嵌入式这么多年,和这两种总线打的交道实在太多了。很多刚入门的朋友一上来就背协议时序图,背完就忘,遇到波形不对还是不会调。这篇我打算换个角度,用做事踩坑的经验来讲清楚SPI和I2C到底是什么,它们各自的性格、脾气,以及哪种场景选谁更合适。顺便把搜索热词里大家最关心的几个问题——比如I2C开漏模式、SPI硬件片选和软件片选怎么选、软件SPI和硬件SPI的差距、SPI接DMA要怎么配——一并梳理清楚。
1. 内容整体设计与思路拆解
1.1 两种总线,其实是两种世界观
先聊个最基础但很多人没深想的问题:为什么有了UART,还要搞SPI和I2C?
UART是异步的,双方各用各的时钟,靠起始位和停止位把数据框起来。它的好处是简单,两根线就能跑,但缺点是速度上不去,也不适合一拖多。而SPI和I2C解决的核心问题都是“同步通信”,就是收和发共用同一个时钟节拍,时序精确可控。这两兄弟在大学教材里经常挨着讲,但实际上是两种完全不同的设计思路。
SPI的全称是Serial Peripheral Interface,串行外设接口。它是一个主子架构的总线,主设备提供时钟SCLK,数据在MOSI和MISO两根线上各走各的,全双工,想多挂设备就靠片选信号CS一个个点名。它的风格是“我给你时钟,你就得立刻回话”。
I2C的全称是Inter-Integrated Circuit,集成电路间总线。它只有两根线,一根时钟SCL,一根数据SDA,半双工。它带设备地址,总线上挂一堆设备,靠地址区分谁应答。它的风格是“我叫你,你应一声,我才继续说”。
用生活化的类比来说,SPI像是上课点名,老师挨个儿被点到谁谁就回答问题,没点到的不要插嘴,效率高但人多的时候老师点名点到手软。I2C更像对讲机群聊,大家一起听一个频道,但每条消息带个呼号,只有呼号对得上的那台才回应,线少但规矩多。
这个本质差异决定了后续的选型。SPI适合量大、要求速度、主从关系明确的场景,比如Flash存储、SD卡、ADC采样、屏幕刷新。I2C适合数量多、速率要求不高的传感器、RTC、EEPROM这类设备。
1.2 选型思路:速度之外还要看什么
很多人选总线只看速度,SPI几十MHz,I2C通常几百kHz到几MHz,那是不是无脑上SPI就行了?不是这样的。我见过有人用I2C读一个加速度计,每秒采样几百次,完全跑得动,但他偏要换SPI,结果改动了一大堆驱动代码,把CS引脚接错,折腾了两天才好。反过来,也有朋友在SPI上挂了一个温湿度传感器,非要按I2C的时序去读卡的,最后连设备地址都找不到。
选型的时候,速度只是一个维度。还要看引脚数量够不够。SPI最少四根线,I2C只要两根。在引脚紧张的板子上,I2C的复用优势就很明显。还要看设备数量和寻址方式。I2C天然支持一总线多设备,SPI每多挂一个设备就要多占一个片选引脚。还有实时性的要求,SPI的时序更确定,适合硬实时场景。
我把这两种总线的关键属性整理成一个对照表,大家在选型的时候可以对着看:
| 特性 | SPI | I2C |
|---|---|---|
| 线数 | 4根(SCLK、MOSI、MISO、CS) | 2根(SCL、SDA) |
| 通信模式 | 全双工 | 半双工 |
| 速率范围 | 通常数十MHz | 通常数百kHz到数MHz |
| 设备寻址 | 靠硬件片选CS | 靠7位或10位软件地址 |
| 拓扑结构 | 主从点对点为主 | 多主多从总线型 |
| 电气特性 | 推挽输出 | 开漏输出加上拉 |
| 适合场景 | 高速数据、存储、采样 | 低速传感器、RTC、EEPROM、配置芯片 |
拿做项目来说,我常挂在嘴边的一句话是:SPI干的是累活,I2C干的是杂活。累活要的是力气,杂活要的是人脉。理解了这个比喻,选型思路就清晰了一大半。
2. 核心细节解析与实操要点
2.1 SPI的四种模式,你真的会配吗
很多朋友在SPI上踩的第一个坑就是模式配错。SPI有四种模式,由CPOL(时钟极性)和CPHA(时钟相位)共同决定。CPOL决定空闲时时钟是高还是低,CPHA决定数据是在第一个跳变沿还是第二个跳变沿采样。
具体来说:
- 模式0:CPOL=0,CPHA=0,空闲时钟低,第一个沿采样
- 模式1:CPOL=0,CPHA=1,空闲时钟低,第二个沿采样
- 模式2:CPOL=1,CPHA=0,空闲时钟高,第一个沿采样
- 模式3:CPOL=1,CPHA=1,空闲时钟高,第二个沿采样
绝大多数Flash芯片、SD卡、显示屏都支持模式0和模式3,你在网上搜别人的代码,很多默认配的也是模式0。但这不是想当然的,我遇到过一款传感器,手册里写得明明白白是模式1,有人没看手册直接用模式0去读,出来的数据全是歪的,波形看着好像有数据,但整理出来基本是乱码。
配模式这件事,唯一的正确做法就是翻芯片手册,找到SPI章节的时序图,看清楚CPOL和CPHA,然后和你代码里的配置对应上。没有第二种捷径。如果条件允许,先用示波器或者逻辑分析仪抓一下主设备发出的时钟和数据波形,对照手册觉得没问题再往深处调试。
2.2 I2C的开漏模式与上拉电阻
搜索热词里有一句“I2C的推挽模式和开漏模式”,这个值得好好展开。I2C的SDA和SCL必须是开漏输出,这是协议层面规定的,不是硬件设计时随随便便定的。开漏的意思是,芯片内部不主动输出高电平,只能把引脚拉低或者释放。释放之后,引脚的电平由外部上拉电阻决定。
这个设计精妙在哪?在于它天然支持“线与”逻辑。任何一台设备想拉低总线,都能把总线拉低,多个设备同时驱动也不会出现短路冲突。这在多主设备场景下特别重要,因为总线仲裁就是靠“看谁先把总线拉低”来判定的。
但开漏也带来一个非常实际的问题:速度上不去。因为释放引脚之后,高电平要靠上拉电阻慢慢把寄生电容充满,电阻越大,上升沿越缓,速率越高越容易出错。所以上拉电阻的取值是个权衡,常见的取值范围在1k到10k欧姆之间。
具体怎么选?我之前做过一个项目,I2C总线上挂了三个从设备,总线长度大概10厘米,我用的是4.7k的上拉,跑400kHz标准模式,很稳定。后来加了一块距离稍远的扩展板,总线长了之后,4.7k就偶尔出现ACK丢失,换成了2.2k才好。一般来说:
- 低速模式,100kHz,总线短,可以用10k或4.7k,省电
- 标准模式,400kHz,推荐4.7k
- 高速模式,1MHz以上,总线短,用2.2k或1k更稳
还有一件事容易被忽略,就是上拉电阻接的电源电压要和器件电平匹配。比如MCU是3.3V,但I2C从设备是5V的,这种场景电平转换不是简单串联电阻能搞定的。最可靠的办法是用电平转换芯片,或者用支持混合电压的开漏结构自己搭离散MOSFET转换。网上热词里“I2C需要电平转换吗”问的就是这个,我说句经验之谈:如果两边电平不一致,接口处标称又没写“电平兼容”,那大概率需要做转换。
2.3 硬件片选与软件片选之争
SPI的片选看起来最简单,就一根线拉低选通、拉高释放。但这里藏着一个老生常谈的问题:硬件片选(硬件NSS自动控制)和软件片选(GPIO手动控制)到底用哪个?
不少MCU的SPI外设自带NSS引脚,启用硬件片选后,外设会自动管理片选的时序。听起来很智能,但实际用起来反而容易出事。因为硬件片选在一些MCU上的行为和预期不一样,尤其在连续传输多字节数据时,NSS可能在每个字节之间跳变一次,导致从设备以为传输结束了,直接把数据丢掉。我调过的某个STM32型号就有这种表现,当年排查了好几个小时,最后把CS改成软件控制立刻就好了。
软件片选的好处是控制权完全在你手里。你可以在一次事务开始之前把CS拉低,传输结束后手动拉高,中间不管发多少个字节都不会中断。对多数从设备来说,这是最稳的。那什么情况用硬件片选?有的MCU结合DMA做数据流传输时,硬件NSS的翻转和DMA搬运配合得非常好,能省CPU干预。但现在很多工程师的做法是:默认软件片选,除非你对这颗MCU的硬件NSS行为非常了解,并且验证过它不会中途乱跳。
还有个小坑是片选引脚的电平极性。绝大多数器件是低有效,但有些屏幕模块、射频芯片是高有效片选。代码里如果默认拉低选通,遇到高有效器件就永远选不中。解决办法还是看手册,看清CS的active level,然后代码里配置好。
3. 实操过程与核心环节实现
3.1 用硬件SPI驱动一款Flash存储芯片
纸上谈兵没意思,来点实际的。我之前做过一个数据记录器的小板子,MCU要把采集的数据存进一颗SPI接口的NOR Flash。当时用的具体型号不说了,反正时序是标准SPI模式0。整个配置流程可以分成几步,第一步是把SPI外设和GPIO初始化好,第二步是CS引脚拉高,Flash默认不选中,第三步是写使能,第四步是发写指令和地址,第五步是把数据发出去,最后轮询状态寄存器等忙标志清掉。
初始化SPI的时候,时钟速率我当时设的是系统主频的四分之一分频。选四分之一不是拍脑袋的,因为Flash手册里写的最大时钟是某一档,我的分频之后约等于那个值的一半,留了余量。如果分频太狠,速度上不去,写入很慢;如果分频太激进,时序余量不足,遇到批次不太一样的芯片就容易偶发错误。我曾经图快把SPI时钟调到和手册极限值差不多的位置,板子单独跑测试没问题,但装上整机之后因为电源纹波变大,偶尔写出来的数据校验不过,把时钟降回来之后就再也没出过事。所以SPI时钟设置的第一个原则是别贴着极限跑。
代码层面的核心逻辑大致是:
flash_cs_low(); spi_write_byte(CMD_READ); // 读指令 spi_write_byte(addr >> 16); // 地址高字节 spi_write_byte(addr >> 8); // 地址中字节 spi_write_byte(addr & 0xFF); // 地址低字节 for (i = 0; i < len; i++) { buf[i] = spi_read_write_byte(0x00); } flash_cs_high();这段代码有一个容易被忽略的点:读数据时主设备必须不停地给从设备提供一个时钟才能让数据出来,而这个“喂”时钟的动作在SPI里就是发送任意字节。我这里发的0x00,有些驱动里写成0xFF也没问题,只要保证时钟在跑就行。有的同学以为读数据不用发东西,结果时序出来对不上,就是这个原理没理解透。
这个案例里最重要的是理解CS和指令的配合。在一个片选周期内,你可以连续发“读指令+地址+若干数据”作为一个完整的事务。同理写操作也要把“写使能指令”和“页编程指令”分开两个片选周期完成。很多新手把指令全部串在一起,没有单独做写使能,结果写操作永远不生效,查半天找不到原因。
3.2 软件SPI和硬件SPI到底差在哪
搜索热词里有“软件SPI和硬件SPI的区别”,这个问题问得特别好。我说的软件SPI,就是不用外设,直接用GPIO翻转时钟和数据线来模拟SPI时序。硬件SPI则是MCU内部集成的SPI外设自动完成移位和电平翻转。
软件SPI和硬件SPI的最核心区别是速度。硬件SPI的时钟可以跑到几十MHz,软件SPI我实测在普通8位MCU上能稳定跑的模拟速率通常也就是几百kHz到一两MHz。因为每一位数据都要靠CPU去翻转引脚,再怎么优化也甩不开这个瓶颈。但软件SPI的价值在于引脚任意、模式随便改、调试起来直观。
我自己的经验是:如果在做原型验证或者临时替代方案,软件SPI完全可以顶上。比如某个模块的硬件SPI引脚被别的功能占用了,我就用两个普通GPIO模拟SPI,跑个1MHz,给一颗温湿度传感器做采样完全够用。但如果做产品量产,或者数据量很大,那还是老老实实硬件SPI加DMA,CPU的负担差远了。
软件SPI实现的时候要注意时钟极性和相位的模拟,尤其要保证数据的建立时间和保持时间。我写模拟SPI时习惯先在时钟边沿之前把数据线拉好,然后再翻转时钟。这个顺序不能反,反了会让从设备采样到不稳定数据。
3.3 I2C读写EEPROM的完整流程
I2C总线最经典的练手项目就是读写EEPROM。搜索热词里有一条“I2C读写EEPROM代码 Verilog”,看来硬件描述语言的场景也有人在搞,我先说通用流程,再补充一点针对性的建议。
I2C写EEPROM的标准流程是:发出起始条件,然后发设备地址加写位,等待从设备ACK,接着发寄存器地址,再等ACK,然后发送要写的数据,等待ACK,最后发停止条件。读的流程稍有不同,要把读写位从写切到读,发送一个重复起始条件,然后再读字节,读完要回NACK表示“我没有更多数据要读了”,最后停止。
实际调I2C时最常见的三个问题,我在这里集中说一下。
第一个问题是ACK检测。很多I2C控制器在发送完地址字节后会自动检查从机的ACK位,但如果用GPIO模拟I2C,就需要手动检测SDA在第九个时钟里是否为低。漏掉这一环节,可能导致你在写一个不存在的地址时,代码还继续往下发,白白浪费时间。
第二个问题是重复起始条件。有些简化版驱动读EEPROM时,发完地址后直接用停止条件再重新启动一次,这在很多情况下也能跑通,但严格按I2C规范来说应该用重复起始条件。少数器件对这两个操作的容忍度不一样,为了保险,按规范来实现不会错。
第三个问题是上拉电阻。前面说过开漏模式,如果上拉电阻太大,总线的上升沿变慢,在高速模式下就会读到不该读到的电平变化。EEPROM本身速度通常不快,100kHz和400kHz比较多,但总线电容一大,照样会出问题。
如果是Verilog方向想实现I2C控制器,思路其实也很简单,核心是一个状态机。状态机里有起始、地址发送、ACK检测、数据处理、停止这几个状态。难点反而在时钟分频和SDA方向控制上,因为双向口在FPGA里要用inout类型,要控制好什么时候当输入、什么时候当输出。网上很多开源例程的方向控制写得混乱,建议自己动手画个时序图,把方向和SCL的时序对起来,就容易理解了。
I2C上传数据的速率我并不特别推荐所有人先去追求高性能,我见过很多项目跑400kHz跑得很欢,但系统其他中断一多,I2C时序被拉长,偶尔就废了。先稳后快,先把100kHz调通,再去追求高速,这条路最踏实。
3.4 SPI与DMA:解放CPU的正确姿势
热词里“SPI DMA”反复出现,这确实是SPI使用里的一个高级用法。SPI每次发一个字节,如果数据量上千,CPU就一直在循环里转悠,别的活儿全被耽误了。DMA就是把这个搬运的工作接管过去,CPU只需要告诉DMA“从哪搬、搬到哪、搬多少”,然后就去忙别的,搬完DMA会拉一个中断告诉CPU完事了。
我在GD32F303上做过一个SPI读取ADC数据流的系统,8通道同步采样的ADC,数据量很大。直接CPU轮询方式读SPI,中断响应稍微慢一点就会丢数据。后来改成SPI加DMA,读取由DMA自动完成,数据搬到一个环形缓冲区里,主程序只有缓冲区半满时才会被叫去处理数据。同样的CPU频率,处理能力一下子宽裕很多。
SPI DMA的配置有一个特别要注意的点:接收DMA和发送DMA的触发源要匹配好。SPI是全双工的,接收DMA的触发条件是RX缓冲区非空,发送DMA的触发条件是TX缓冲区空。有些MCU里这两个DMA通道是可以独立配置的,你可能会遇到“只收数据不需要发数据”的操作,但为了产生接收时钟,SPI外设还是要被动地发送一些字节去推动时钟,这时候接收DMA单独工作就无法持续触发,必须辅以一个发送DMA或者手动发送逻辑。
打个比方,SPI读数据这件事就像坐手摇船,你想要对岸的货物,可你的船必须有人摇船桨才能前进。接收端收得再快,没有发送端在摇桨,数据也不会自己从对岸飞过来。实际调的时候,可以先测发送侧,用逻辑分析仪确认时钟和数据正常,再开接收DMA,这样问题容易定位。
DMA中断里有不少小坑,最常见的两个:一个是传输完成中断触发条件要比对寄存器版本,另一点是循环模式和正常模式的差异。循环模式下DMA会在传输结束后自动重新装载地址,适合连续采样;正常模式则止步。如果你的代码里用了循环模式,但初始化时DMA中断标志没有及时清理,再次启动时可能触发一次假中断,造成缓冲区错位。这些都是真实踩过的坑,写在这里提醒大家。
3.5 关于TF卡SPI和AD7606、编码器这些实际应用
盘点这些热词,“TF卡SPI电路”也是很多人的痛点。TF卡跑SPI模式是比较经典的玩法。TF卡默认应该是SD模式,切到SPI模式需要在初始化时先给卡发至少74个时钟脉冲,然后发CMD0进入SPI模式,再发CMD8、ACMD41这些初始化指令。硬件电路上最常见的坑是TF卡的DAT1、DAT2、CD引脚在SPI模式下要接上拉,否则卡可能识别不稳定。
“安富莱AD7606”这个热词说明有人在用AD7606做高速同步采样。AD7606支持SPI接口,也支持并行接口,如果用SPI模式,要特别注意它的BUSY引脚和FRSTDATA引脚的时序关系。我调试时最喜欢的做法是用逻辑分析仪同时抓CS、SCLK、BUSY三根线,确认每次转换完成后数据输出的帧格式。AD7606可以配置成过采样,过采样倍数越高,输出数据的有效位数越好,但输出速率会降低,两者要权衡。
“I2C编码器”这个热词也挺有意思。旋转编码器通常是AB相脉冲直接接GPIO中断,但也有带I2C接口的编码器,内部带寄存器,可以读到绝对位置。这种I2C编码器对时序要求不严格,但通信频率一般不高,挂总线上注意地址别冲突就行。
“SPI引擎”这个词如果我没理解错,应该是指SPI接口的加密引擎或者硬件加速模块。这类模块一般在网络安全芯片和存储加密上用得比较多,实现原理大多是内置AES等算法的硬件电路,通过SPI接口与主控交互。这种场景对SPI时序要求非常苛刻,因为加密数据一旦因为时序问题中断,整个会话就废了,所以这类应用我强烈建议用硬件片选和DMA配合。
4. 常见问题与排查技巧实录
4.1 有波形但读不出正确数据,问题出在哪
我在调试SPI和I2C时,最怕的不是完全没波形,而是波形看着正常但数据不对。这种情况最容易把人绕晕,因为你会怀疑是软件逻辑不对,又怀疑是硬件连接不对,两边反复查。
对于SPI,第一件事就是确认时钟极性和相位。拿出逻辑分析仪,抓一次主设备发指令的波形,对照手册的时序图,一格一格看空闲电平、采样沿。数据错误绝大多数都是模式不对,尤其是用别人现成代码的时候,更容易被默认的模式带偏。
确认模式没问题之后,再检查片选时序。CS拉低之后是不是马上就发数据了?中间有没有留一点点时间让从设备准备好?有的从设备对CS建立时间有最低要求,如果主设备在CS拉低的同一瞬间就发第一个时钟,某些慢速设备会响应不过来。
对于I2C,同样用抓波形的办法,重点看起始条件、地址字节和ACK位。我经常遇到一种情况:地址不对,总线上的设备不ACK,波形里SDA在第九个时钟一直是高,代码却还硬撑着继续读。这种情况要回头查设备地址到底是多少。7位地址和8位地址经常有人搞混,很多器件手册写上的是8位含读写位的,代码里写的却是7位的,结果差一位对不上。
4.2 总线挂死和时钟拉伸,I2C专属的麻烦
I2C有一个让很多人头疼的现象:总线挂死,SDA一直为低,时钟还在跳或者干脆也不动了。这通常是某个从设备把SDA钳在低电平,等它释放却一直不释放。最简单的解救办法是把SCL多翻转几次,有些从设备在时钟翻转几次后会重新同步释放总线。如果这招不行,就只能把总线的电源断一下,或者把主设备复位一下。
另外还有一个概念叫时钟拉伸,就是某些从设备处理不过来的时候,会把SCL拉低,让主设备等一等。这在很多传感器上很常见。如果你的控制器不支持时钟拉伸,可能会因为等不到回应而超时报错。我建议在选用I2C器件时,留意一下它是否有时钟拉伸需求,特别是做高速传输的时候。
4.3 上拉电阻反复调就是不行,换思路
上拉电阻的问题,前面已经说了不少,但实际调试中还有一种情况:阻值怎么调都调不好。这时要冷静怀疑是不是I2C总线上挂的某个设备本身有问题,比如某个引脚的漏电流特别大,把总线电平拖垮了。我就遇到过一颗器件,本身有内部上拉,和外部上拉并联之后总阻值太小,低电平都拉不下去,导致整个总线工作异常。排查这种问题只能一个一个挂载设备试,把可疑的器件先摘下来,看总线状态有没有恢复。
另外,多从设备的总线,每个从设备的输入引脚都带有电容,数量一多总线等效电容就变大,即便你换更小的上拉电阻,上升沿还是不够陡峭。这时候要考虑降速,或者减少总线上挂的设备数。I2C总线扩展器就是干这个的,热词里也有“I2C扩展”,本质是把一条总线扩成多条,或者把I2C信号转成别的接口再接多个设备。扩展之后要注意延时会变大,速率不宜调高。
4.4 常见问题速查表
下面这张表是这几类问题的高频排查要点,我在项目里基本每次都参照这个顺序来排除,分享出来给大家当个参考。
| 现象 | 可能原因 | 排查顺序 |
|---|---|---|
| 读到0xFF或全0 | SPI模式不对、CS未选通、从设备未上电 | 1. 查模式 2. 查CS电平 3. 查供电 |
| 数据随机跳变 | 时钟过快、线路过长、干扰 | 1. 降速 2. 查波形 3. 缩短走线 |
| I2C无ACK | 地址错误、总线电平不对、上拉失效 | 1. 查地址 2. 量SDA电平 3. 查上拉 |
| I2C总线挂死 | 从设备锁死、主从不同步 | 1. 翻转SCL 2. 断电复位 3. 查时钟拉伸 |
| 偶尔通信失败 | 片选时序不达标、中断打断 | 1. 加延时 2. 关中断 3. 加DMA |
| DMA数据错位 | 标志位未清、循环模式配置错误 | 1. 查中断初始化 2. 查缓冲区指针 3. 单步调试 |
看着这张表,再回头看问题,其实很多难调的bug都不是玄学,而是时序关系没有完全对上。理解每一种现象背后的协议细节,再配合示波器或逻辑分析仪去验证,问题往往很快就能锁定。
5. 实践中的选型心得与学习路径建议
5.1 我的选型决策习惯
做了这么多项目,我慢慢养成了一个选型决策习惯。第一步永远是列接口需求,把要接的器件数量、数据量、速率要求、引脚余量全部列出来。第二步才是看速度参数,但不会只盯着上限,而是看典型值和工作电压下的可靠值。第三步是画原理图时就把上拉、下拉和去耦电容的位置预留好,就算最后没用上,也比板子回来之后再飞线强。
之前有个模拟项目我印象很深:要接三颗I2C传感器加一颗I2C EEPROM,MCU引脚刚好够用,但I2C总线上四颗器件,如果全部直接挂上去,总线电容变大,可能不稳。我就干脆加了一个I2C总线扩展器,把三颗传感器分到另一条总线上,EEPROM和主控留在原始总线。这样每条总线都是轻载,运行非常稳,而且扩展器本身也增加了系统的灵活性。
另一个项目中需要接一颗SPI接口的SD卡,数据量比较大。我选了硬件SPI加DMA方案,并且用硬件片选。为什么这时候用硬件片选?因为我在那款MCU上验证过硬件NSS在大批量DMA传输时行为正常,不会中途跳变,同时软件片选在DMA连续传输中可能需要专门配合,开发量反而大。选型没有绝对的对错,只有对自己硬件外设特性的把握程度。
5.2 软件模拟和硬件外设的配合策略
很多项目初期调试阶段,硬件外设的引脚可能被调试器占用或者芯片封装引脚有限。这种情况下我的策略是先用软件模拟验证逻辑,确认器件能正常通信,然后再切到硬件外设,把速率拉起来。这个策略有一个明显好处:软件的时序完全可控,出问题容易定位;硬件外设一旦配好,性能又远超软件模拟。
切换到硬件外设时,记得把软件模拟的延时逻辑全部去掉,因为硬件外设自动管理时钟和数据,不需要你在代码里手动翻转。如果切过去之后数据不对,不要怀疑硬件外设坏了,先检查外设的配置参数,尤其是模式、位数、分频、片选极性,大概率是配置里某一位和软件模拟时有差异。
软件模拟SPI和I2C还有一层价值:当你需要把一个老设备接到新MCU上,而新MCU的外设寄存器细节你还不太熟时,可以先拿软件模拟把器件驱动起来,等验证完器件没问题,再去啃外设驱动,压力会小很多。
5.3 面向新手的学习路径建议
新手学SPI和I2C,我不建议一上来就啃协议手册或者看一堆例程。我建议按这个顺序来。
第一步,用逻辑分析仪抓现成设备通信的波形。随便找一个开发板上的加速度计或EEPROM例程,抓SCL、SDA或者SCLK、MOSI、MISO的波形,对照时序图看明白每个字节是怎么被发出去的。
第二步,动手用GPIO模拟一遍最简单的协议。不需要接真实设备,先把起始条件、停止条件、字节发送的波形模拟出来,可以用逻辑分析仪验证,确认自己完全理解协议时序。这一步做完,你会对协议有真正的“手感”,而不是只会抄代码。
第三步,再切换到硬件外设。用同一颗器件,配置好硬件外设,读数据,对比软件模拟的结果,确认两边行为一致。此时你已经理解时序本质了,硬件外设的寄存器配置对你来说只是换了一种实现方式。
第四步,进阶到DMA和中断的结合,把CPU从通信里解放出来,这时候你才算真正用一个产品的思路在做这件事。
5.4 几个容易忽略的细节彩蛋
最后讲几个容易被忽略的细节,都是我实际用过的经验。
第一个是SPI在高速模式下,MOSI和MISO之间的串扰问题。如果板子上这两根线走在一起而且靠得很近,高速翻转的时候可能会串扰,轻则丢位,重则整个数据帧乱套。解决方法是拉开间距,或者在关键信号之间加地线隔离。我做过一次板子,SPI Clock 20MHz,MISO数据偶发错误,查了半天的软件,最后发现是布线平行的锅。
第二个细节是I2C的地址冲突。有些器件支持多个地址引脚,比如A0、A1、A2,设计板子的时候要预留跳线或电阻配置,避免多颗同型号器件地址一样导致冲突。我见过有人偷懒把三颗一样的传感器A0、A1、A2全部接地,结果三颗地址完全一样,总线上永远只能识别到一颗,剩下两颗直接失联。
第三个细节是中断优先级。SPI和I2C通信过程被高优先级中断打断,可能导致时序被拉伸,尤其在严格的主从同步场景下会出问题。如果系统里有硬实时中断,比如电机控制、PWM斩波,要给通信中断设置合理的优先级,或者干脆用DMA把这些通信从CPU中断里摘出去。这点在之前那个AD7606采样系统里体现得最明显,DMA介入之后,无论中断多频繁,数据流都稳稳的。
第四个细节是关于热词里的“不拆SPI Flash芯片可以刷BIOS吗”。这其实是个很有意思的工具场景,很多主板上的SPI Flash可以通过专用的编程器夹具,用SPI协议直接在线刷写,不需要拆芯片。原理就是编程器通过夹具夹住Flash芯片的引脚,用SPI时序去读写内部数据。对于不想拆焊的人来说,这个场景本身就是SPI协议价值的又一个体现。不过操作的时候务必断开Flash芯片和主板其他部分的电气连接,至少要把主板的电源断开,不然编程器和主板上的电路同时驱动同一根信号线,轻则烧引脚,重则损坏器件。
这些零零散散的细节,单独拿出来看好像都很小,但每一个都有可能在项目关键时刻把人卡住几个小时甚至几天。通信总线的调试就是这样,基础原理全懂,细节才见真功夫。
做嵌入式的路上,大家都是从每一次波形乱掉、每一次ACK丢失、每一次SPI数据错位里爬出来的。SPI和I2C只是这个大千世界里最常见的两个接口,但把这两个接口吃透了,其他总线的学习路径也会清晰很多。希望这篇经验分享能帮你少走几步弯路,把片内到片间的这趟旅程走得顺畅一些。