之前调一块8通道ADC的时候,我把SPI时钟一抬到10MHz以上,读回来就全是0xFF。第一反应是去翻主控的寄存器,折腾了一圈模式设置,结果问题出在板卡的电源和地线上——芯片内部信号是一回事,信号走出封装、经过PCB走线,完全是另一回事。这块板子让我彻底理解了“从沙子到车辙”这个系列的意思:芯片内部那些晶体管是沙子,引脚外面的PCB走线才是车辙,片间通信的边界从来不是主控的寄存器,而是封装、接插件、线缆和总线拓扑。
这个系列第4.2节,专门聊片间通信里最常用的两种同步串行总线:SPI和I2C。它们把芯片内部高速并行世界和外部低速物理世界衔接起来,几乎所有传感器、存储器、ADC、显示屏、编码器都靠这两条总线连到主控。这篇东西我不打算按数据手册的章节给你复述,而是从“为什么片内和片间不一样”这个根上讲起,再把时钟极性、片选策略、开漏上拉、电平转换、DMA、软件模拟这些事掰开揉碎,最后聊聊调试SPI/I2C时的排查链路。适合正在学习STM32/GD32等MCU的开发者,也适合想把接口通信原理补全的嵌入式工程师。
1. 片内信号一旦走出封装,一切都变了
很多初学者在看SPI和I2C的时候,会默认把它们类比成芯片内部的寄存器读写:反正都是按地址访问、按位传数据。这个类比在概念上没错,但在工程上是陷阱。
1.1 并行总线在片内能跑,在片外跑不动
芯片内部的总线动辄几十上百根线并排走,每根线的长度可能只有零点几毫米,信号在晶圆内部传播时受到的干扰很小,而且几根线之间的延迟差(skew)可以做到纳秒以内。所以片内并行总线可以跑很高的频率,数据手册里动不动就是几百MHz甚至GHz。
但一旦信号要出片,情况就变了。封装引脚有寄生电容,PCB走线有阻抗,接插件有接触电阻,线缆还有串扰。几十根并行线同时翻转的时候,地弹效应和串扰会把信号完整性砸得稀烂。更致命的是,并行线的skew会被放大——每一根线的走线长度不一样,信号到达时间就不一样,时序余量根本hold不住。我们要在片外传输数据,第一件事就是降维:用串行替代并行,用同步时钟替代异步时钟。
1.2 为什么片间主流是同步串行,而不是异步串行
串行通信又分同步和异步。UART是异步的典型,数据线上没有时钟,接收端需要预先约定波特率,再从数据跳变里恢复时钟。异步的好处是节省时钟引脚,但坏处是双方必须精确对准波特率,稍微有点偏差就会出乱码。
SPI和I2C选择了另一条路:把时钟线单独拉出来。SPI有SCLK,I2C有SCL,接收端不用去“猜”时序,看着时钟沿采样就行。这让时钟恢复逻辑消失了,硬件实现大幅简化。所以你会发现,在绝大多数MCU的片间通信场景里,同步串行是默认答案。你让MCU的UART跑1Mbps以上通常要小心,但SPI跑个几十MHz轻轻松松——前提是PCB布线和供电扛得住。
1.3 多设备连接的两种思路:一个总线的两种哲学
片间通信经常不止两个设备,这就涉及“多个从设备怎么区分”的问题。SPI和I2C给出了两个完全不同的方案,这也是理解它们所有差别的钥匙:
- SPI像“点对点管道”。主设备每个从设备拉一条片选线CS,选中谁就和谁对话。管道是专线的,主从之间双向数据线是独立分离的,MOSI和MISO各走各的,速度可以做得很高。缺点也明显:设备多了,片选线就多,PCB散热器一样密密麻麻。
- I2C像“共享总线上的广播”。所有从设备挂在同一条SDA数据线、同一条SCL时钟线上面,靠7位地址寻址。想加设备就并联上去,几乎不占引脚。代价是总线电容变大、速度上限变低、仲裁和时钟同步机制复杂。
理解了这两种哲学,后面所有的工程细节就都有了落脚点。
2. SPI:一条随时可全双工的管道——配置、片选与DMA
SPI的硬件结构其实非常简单,核心就是移位寄存器。主机往移位寄存器里推一个字节,同时接收从机移出来的一个字节,这就是全双工的本质。你发送0xAA,就一定有机会收到某个0x00或0x55,具体是啥取决于从机的移位寄存器里当时放了什么。这个双向同时进行的特性,让SPI在高速数据采集里特别吃香:边发边收,一个时钟周期办两件事。
2.1 CPOL/CPHA:四象限时钟配置为什么搞混会出0xFF
SPI的配置里最容易把人绕晕的就是时钟极性CPOL和时钟相位CPHA,合起来四档模式。CPOL决定空闲时SCLK是高还是低,CPHA决定数据是在第一个沿采样还是第二个沿采样。
- CPOL=0,空闲低,第一个沿是上升沿,这是模式0;
- CPOL=1,空闲高,第一个沿是下降沿,这是模式3。
经验上,绝大多数外设默认支持模式0(CPOL=0、CPHA=0),但总有些芯片不走寻常路。比如某些ADC和存储器喜欢模式3,配置反了的结果就是数据全部错位——读回来的数据要么是重复字节,要么是排列诡异,要么是全0xFF。调试的时候第一步就该先确认模式,用示波器看SCLK空闲电平、数数据在哪个沿变化,而不是上来就怀疑芯片坏了。
2.2 硬件片选与软件片选,真实项目里怎么取舍
片选CS是SPI从设备的“开关”。硬件片选一般指MCU的NSS硬件自动控制:发起传输前自动拉低CS,传输完自动拉高,CPU不用管。听起来很美,但在多从机场景里有个坑:硬件NSS只有一根,要么接在唯一从机上,要么配合“菊花链”,自由度很低。而且有些MCU的硬件NSS还跟主模式绑定,配置不好会触发模式错误。
所以我更推荐在实际项目里优先用软件片选——就是把任意一个GPIO复用成CS,在SPI传输前手动拉低,传输完拉高。别小看这一步,软件片选的好处是你完全控制时序窗口,还能做“多主机互斥”的软件锁。坏处就是CS拉低和SPI时钟启动之间有极短的延迟,对于那些要求CS低后必须等待若干周期的器件(比如需要时间唤醒的Flash),你得在拉低CS之后、发数据之前加个几百纳秒的延时。
多从机场景下,软件片选几乎是唯一解。你只要GPIO够,想挂几个从机就挂几个。GPIO不足的时候,还可以用译码器扩展,例如用3个GPIO配合74HC138译码器管理8个CS,成本极低。
2.3 软件SPI和硬件SPI的区别,不只是快和慢
“软件SPI”经常在社区里被讨论,很多人问为什么放着硬件外设不用,非要拿GPIO翻转。答案有三个常见场景:
- 引脚复用不满足。MCU的硬件SPI引脚往往固定,如果你的板子把那些引脚都占了,又不想改版,GPIO模拟是救急手段。
- 需要非常规时序。比如某个从机要求时钟空闲电平特殊、或者要求数据在特定边沿更早建立,硬件SPI的基本配置不够用,软SPI就能捏成任意形状。
- 低成本芯片没有硬件SPI。比如8脚单片机,复用IO模拟一下,读个传感器足够。
软件SPI的速度上限取决于CPU主频和GPIO翻转速度,通常跑到2-4MHz已经不错,和硬件SPI动辄几十MHz没法比。但它有个硬件SPI比不上的优点:时序完全可控、可调试。比如你想让某个从机在SCK高电平期间换数据,软SPI随手就能改,硬件SPI可能得换模式。
2.4 SPI DMA与存储器类应用,速度怎么吃满
SPI本身快,但CPU一个一个字节地往数据寄存器里塞,速度会被软件循环卡住。于是就有了SPI+DMA的组合:DMA把数据从内存搬到SPI发送寄存器,同时把接收寄存器里的数据搬到内存,CPU全程不参与字节搬运,等数据块搬运完再被DMA中断叫醒。
以GD32F303为例,配置SPI+DMA时,我的建议是先配DMA、再使能SPI,避免SPI已经开始跑但DMA还没准备好的间歇。关键参数包括:DMA的外设地址指向SPI的数据寄存器,内存地址指向缓冲区,方向分别是内存到外设、外设到内存。SPI数据寄存器每次写入或读出,DMA就数一次计数,等计数归零触发传输完成中断。
SPI DMA最常见的应用就是TF卡。TF卡支持SPI模式,但注意SD卡在SPI模式下初始化时序和普通SPI Flash完全不同:上电后要先给74个以上的时钟周期,才能进入SPI模式。如果你把TF卡SPI电路照抄成普通Flash,大概率初始化失败。实际接线上,TF卡在SPI模式下只需要CS、CLK、MOSI、MISO四根信号线,加上电源和地,但卡的CD(卡检测)引脚和WP(写保护)引脚别乱接,很多电路都建议串电阻或直接拉高。
还有一类比较野的操作:不拆SPI Flash芯片直接在线刷BIOS。这个思路本质是让外部编程器或主控在SPI总线上对接焊在主板上的Flash芯片,通过总线的“片选+时钟+数据”直接读写。听起来方便,但前提是板子完全掉电、其他挂在同一SPI总线上的设备都处于高阻状态,否则总线冲突分分钟烧芯片。我个人的建议是,能用烧录夹夹住芯片引出引脚,或者干脆拆芯片用编程器,不要为了省事在线读写,除非你清楚每个设备的引脚电气状态。
2.5 一个具体的GD32F303 SPI配置示例
配置SPI的时候,最关键的就是别漏掉任何一个参数。以GD32F303的固件库为例,SPI初始化大概长这个样子:
spi_parameter_struct spi_init_para; spi_init_para.device_mode = SPI_MASTER; // 主模式 spi_init_para.trans_mode = SPI_TRANSMODE_FULLDUPLEX; // 全双工 spi_init_para.frame_size = SPI_FRAMESIZE_8BIT; // 8位帧 spi_init_para.clock_polarity = SPI_CK_PL_LOW_PH_1ST; // CPOL=0 spi_init_para.clock_phase = SPI_CK_PL_LOW_PH_1ST; // CPHA=0,模式0 spi_init_para.nss = SPI_NSS_SOFT; // 软件片选 spi_init_para.prescale = SPI_PSC_8; // 分频,1/8主时钟 spi_init_para.endian = SPI_ENDIAN_MSB; // 高位先行 spi_init(SPI0, &spi_init_para); spi_enable(SPI0);配置完成后,每次传输前拉低CS GPIO,然后往SPI_DATA寄存器写字节,同时读上一字节数据。硬件全双工模式下,读到的值总是伴随发送的那个字节一起回来,所以在做纯接收时,通常会发一个哑字节0x00,从机收到哑字节的同时把有效数据移位出来。这个“哑字节”的细节,恰好是很多调试卡壳的地方:只是读一个传感器,却忽略了自己发出的字节会影响从机的时序响应。
3. I2C:开漏总线上的人多势众——地址、上拉与电平
I2C跟SPI是完全不同的气质。SPI像专车,点对点直达,贵但快;I2C像公交车,所有人都挤在线路上,便宜灵活,但跑不快。很多写驱动的人对I2C有心理阴影,尤其是在硬件I2C上吃过卡死的亏。这一章我们从根上讲清楚I2C为什么设计成这个样子。
3.1 推挽和开漏:I2C为什么必须“只能拉低,不能拉高”
IO口的输出模式无非两种:推挽和开漏。推挽输出,既能主动输出高电平,也能主动输出低电平,输出电阻很小,驱动能力强,但缺点是多个设备同时推挽输出同一个引脚,一个想拉高一个想拉低,就直接短路烧毁。
I2C选的是开漏模式:设备只能主动拉低,没有主动拉高的能力,要靠外部上拉电阻把SDA和SCL拉到高电平。“低电平由设备保证,高电平由电阻保证”,这个设计带来了三个关键好处:
- 多个设备的开漏输出可以直接并联,谁想拉低谁拉低,不会打架,天然实现“线与”逻辑。
- 双向通信不需要额外的方向选择信号。SDA既可以被主机拉低,也可以被从机拉低,一条线上两个方向自然切换。
- 电平兼容性好。因为高电平是外部电阻决定的,低电平的判决阈值又有相对宽的范围,3.3V和5V器件理论上可以混接,只需要选好上拉电压。
在MCU的GPIO配置里,I2C引脚必须配置成开漏模式,千万别图省事配成推挽,否则多设备场景容易出现异常电流。
3.2 上拉电阻怎么选:从计算到实测的经验值
上拉电阻阻值是I2C硬件设计最容易被忽略的环节。阻值太小,拉低时电流太大,超过器件灌电流能力,低电平电压被抬起来,逻辑就会出错;阻值太大,RC充电时间太长,上升沿太缓,数据传输率上不去。
工程上粗算的办法:低电平的判决阈值一般是0.3倍VDD,你只要保证拉低后的电压低于这个值就行。常见器件灌电流能力3mA,假设用3.3V供电,上拉电阻下限Rmin=(3.3-0.4)/3mA≈1kΩ。上限以总线电容和上升时间为约束,标准模式100kbps允许最大上升时间1μs,快速模式400kbps允许300ns,Rmax=tr/(0.8473×Cbus)。
实测经验里,100kHz总线上用4.7kΩ到10kΩ都能跑,400kHz总线上建议2.2kΩ到4.7kΩ。如果总线很短(板内互联),直接用4.7kΩ最省心。如果线上挂的从机多了,每多一个设备就多几分电容,上拉就要适当调小。我曾经在一块板子上挂6个I2C设备,用10kΩ上拉,400kHz模式直接乱码,换4.7kΩ后立刻稳定,这个坑很多人会踩。
3.3 I2C电平转换到底需不需要,怎么转
3.3V主控接5V从机,或者1.8V主控接3.3V传感器,这是I2C最常见的电平混合场景。I2C的特殊之处在于它本身没有方向信号,电平转换器必须“双向自适应”。
有人问不转换行不行。答案取决于从机在引脚上的最高承受电压。很多5V器件标称SDA/SCL引脚的VIH=VDD,但实际上引脚在输入高电平时的钳位二极管会漏电,长期超压可能损伤芯片。我建议是查数据手册的绝对最大额定值,如果VIH允许3.6V以上,3.3V直接接5V系统大多数情况下可用,但不放心就加转换。
I2C双向电平转换最常用的方案其实不是专用电平转换芯片,而是单个N沟道MOS管搭成的自适应电路:两个上拉电阻分别接两个电压域,MOS管的源极接低电压侧,漏极接高电压侧。低电压侧拉低时,通过MOS管把高电压侧也拉低;高电压侧拉低时,电流从MOS管的体二极管往低压侧灌,使低压侧也拉低。这个方案天然适配开漏总线,几乎是I2C电平转换的标准答案。市面上像TXS0102这类芯片内部也是类似的MOS管结构,只是把电阻集成进去了,更省事。
3.4 软件模拟I2C:起始、停止、ACK在哪里
软件模拟I2C是理解协议最好的方式,也是硬件I2C卡死时最可靠的逃生通道。核心原则只有一句话:数据线SDA只能在SCL为低电平的时候变化,而起始和停止信号在SCL为高电平的时候翻转SDA。
- 起始条件:SCL高,SDA从高跳低。
- 停止条件:SCL高,SDA从低跳高。
- 数据位传输:SCL低时设置SDA电平,然后SCL拉高保持,此时从机采样SDA,再SCL拉低准备下一个位。
ACK的检测也很关键。发送完地址字节后,第9个时钟沿来看SDA:如果从机拉低SDA,说明从机收到了地址;如果SDA还是高,说明地址上没有从机应答,或者从机没上电。这就是I2C地址扫描的基本原理:从0x00到0x7F发地址看ACK,能收到ACK的地址就存在设备。
软件模拟时务必要注意延时。SCL高电平期间,SDA电平必须稳定,如果SDA在SCL采样点附近还在跳变,数据就废了。我的经验是SCL翻转后加一个足够的上拉稳定延时,换算成指令周期,在STM32/GD32上通常需要几百ns到几μs不等,具体看总线电容。
3.5 Verilog实现I2C读写EEPROM,状态机怎么摆
FPGA侧用Verilog写I2C主控制器,是很多数字逻辑初学者想挑战的项目。硬件工程师写I2C状态机,最头疼的地方仍然是SCL和SDA的时序约束。我建议的状态机划分大概是:
localparam IDLE=0, START=1, ADDR_W=2, ACK1=3, REG_ADDR=4, ACK2=5, RESTART=6, ADDR_R=7, ACK3=8, DATA_R=9, NACK=10, STOP=11;写EEPROM(比如AT24C32)的流程是:起始条件→发送器件地址(7位地址+写位0)→等待ACK→发送寄存器地址→等待ACK→停止。读流程则在发完寄存器地址后不直接停止,而是再次发起起始条件,发送器件地址(7位地址+读位1),然后读取数据,最后一个字节回NACK表示读完了,再发停止条件。
最关键的时刻就是:状态机的状态转换必须发生在SCL低电平期间,这样才能保证SDA在SCL高电平期间稳定。很多初学者的Verilog代码跑仿真时看着波形正常,上板就乱,原因就是状态跳转边沿和SCL边沿重叠了。我的建议是状态机在SCL低电平的“中点”跳转,用分频后的时钟在SCL低区间内留出足够建立时间。
3.6 I2C扩展:地址冲突和总线负载怎么处理
I2C设备地址一般是7位,理论上能挂127个设备,但实际上同一型号的设备地址是固定的,你把两片相同的EEPROM并联在总线上,两个从机都会回应同一个地址,冲突就来了。
解决方案有几个层次:
- 看芯片有没有地址引脚。很多EEPROM和传感器都有A0/A1/A2引脚,拉高拉低可以改地址,这是最便宜的办法。
- 用I2C多路复用器/开关,比如PCA9548A,把总线扩展成8路,每路下挂一组设备。它本身也是一个I2C从设备,选中某一通道后,该通道的设备就“接上线”了。
- 某些情况下还可以用GPIO模拟额外端口,但那样就失去了I2C多设备的意义。
总线负载的问题也值得提一句。I2C规范说400kHz下最大总线电容400pF,每根线上只能挂有限个设备。设备一多,总线电容变大,上升沿变慢,速率不降就出错。如果你手上有一大堆传感器和EEPROM要挂,最好的办法不是全并联,而是用I2C复用芯片把负载分开,或者干脆换SPI总线。
3.7 I2C外设的实战例子:OLED驱动和角度编码器
I2C最常见的应用就是驱动OLED屏。CH32V307上跑I2C OLED(SSD1306)的例程,网上很多。核心问题是初始化序列那几十个命令,必须一个字节一个字节地按数据手册发,漏一条或者顺序错了,屏就黑着。很多人调I2C OLED不亮,第一反应是硬件I2C坏了,其实多半是初始化命令没写完,或者发送地址时把0x78写成了0x3C。0x3C是7位地址,0x78是8位地址(带读写位),两者就差一个左移,搞混的比比皆是。
I2C编码器也是近两年很火的应用,像AS5600磁编码器就通过I2C输出12位角度。它内部寄存器可以配置,读取角度就两个字节,但要注意芯片有个BCW方向的递增模式,默认顺时针还是逆时针,得实测确认,不要直接采信理论值。
4. 实测排错:总线不动时的检查链路
SPI和I2C的问题,很多不是写在普通文档里的。这一章我把这几年代码调试和硬件调试里最常踩的坑串起来,给出排查顺序。
4.1 SPI接口测试最简单且有效的回环法
SPI调试第一步,永远先做回环测试。把MOSI和MISO短接,发送数据,看接收数据是不是和发送数据完全一致。如果回环都不过,问题一定在MCU的SPI配置或引脚复用;回环过了还不对,才去怀疑从机和外部电路。
回环测试的具体做法:配置SPI主机模式,软件片选拉低(回环时片选可以先忽略),然后循环发送0x55、0xAA、0x00、0xFF这类特征字节。0x55的二进制是01010101,0xAA是10101010,如果时钟极性极性错位,收到的值会和发送值有规律的位错,用这两个字节可以非常直观地判断是采样沿错还是位序错。
我遇到过一种典型坑:SPI配置正确,回环也通,但接上AD7606读出来的数据全是0xFF。用示波器一看,SCLK有脉冲,但MISO一直高,再查从机供应电压,发现VDRIVE引脚没接上拉,从机输出缓冲根本没供电。这个案例说明回环测试通过了,也要一步步查从机侧电源、片选、复位脚,别迷信主控配置。
4.2 I2C总线卡死和恢复:从“扫地址”到“九脉冲大法”
I2C调试的第一步也是基础检查:先把总线上所有设备扫一遍。软件I2C写个循环,从地址0x01到0x7F依次尝试发送地址字节,能收到ACK的地址打印出来。这个操作能快速确认从机是否上电、地址是否如预期。很多所谓的“I2C通信失败”,扫一下地址就豁然开朗——原来设备地址是0x68,不是你以为的0x30。
I2C最恶心的问题还不是通信失败,而是总线卡死。现象是SDA或者SCL一直停留在低电平,后续任何设备都无法发起通信。恢复方法第一步:可以在SCL上手动发送9个时钟脉冲。这个操作的原理是,某些从机如果在传输中途遇到异常,会一直握着SDA不放,给它9个时钟信号,它就有机会完成当前传输,释放SDA。第二步才是考虑掉电重启。
卡死的深层次原因往往不是从机抽风,而是主机侧在传输中提前退出了——发送中途软件复位、看门狗复位、或者异常分支直接跳过停止条件。解决思路是:任何I2C函数内部,在发起起始条件前,先检查总线上是否已有主机在占用,如果SDA和SCL状态不符合预期,就别硬发。
4.3 一个复现排查链路:GD32读AD7606只有0xFF
把整个排查思路串起来,我举一个实际复现的例子。主控是GD32F303,外设是安富莱家的AD7606模块,SPI读取采样值。
第一步,回环测试。MOSI和MISO短接,发送0x55收0x55,说明SPI主控本身没问题。
第二步,接线检查。确认AD7606的CS接到主控软件片选GPIO,SCLK接SPI_SCK,DOUTA接MISO,VDRIVE引脚接3.3V。
第三步,上示波器。触发条件设为CS下降沿,观察CS拉低后是否有SCLK脉冲、MISO上是否有数据跳变。实测发现CS有、SCLK有,MISO一直处于高电平。
第四步,查数据手册。AD7606的串行接口在CS拉低后,需要几个周期的等待时间才开始输出数据,数据是在SCLK的下降沿移出、主机在上升沿采样。确认GPIO配置无误后,又检查了时钟极性的实际波形,发现MCU在模式0下,空闲SCLK为低,第一个沿是上升沿,而AD7606对SCLK相位的要求很苛刻,必须严格保证CS拉低后SCLK保持低电平若干周期才能发起第一个时钟。最终在CS拉低后加了一个短延时,数据立刻正常了。
这类问题的本质不是哪个寄存器配错了,而是从设备时序和主机时序之间有几个状态周期的错位。很多外设要求CS低后等待、CS低后第一个沿必须是某个边沿,这些“软约束”最容易造成疑难问题。
4.4 示波器的探测技巧与时序分析判断法
调试串行总线,示波器或逻辑分析仪是必需品,但怎么测也有讲究。SPI测试时,头等大事是确认参考地。探头的地线夹如果太长地飞线,会引入额外感性噪声,影响看信号质量。建议用短地弹簧探头,靠近测量点接地。
抓SPI数据时,触发电平要设在CS的中间档位,用CS下降沿做触发源,这样能看到完整的通信帧。抓I2C时,可以用SDA下降沿触发,因为起始条件就是SDA下降沿,但要注意如果是SCL卡死场景,用SCL下降沿触发出不了波形,反而能帮你判断是SCL卡在高还是低。
时序分析时,第一看SCLK的周期,第二看数据是否在采样沿处稳定,第三看片选和ACK之间有没有异常延迟。遇到时钟频率超过预期的情况,先想到是不是分频配置错了,再想到是不是硬件上拉不足导致边沿变缓。
5. 从选型到落地:SPI/I2C怎么选怎么用
同一个外设,有些芯片提供SPI接口,有些提供I2C接口,有些两种都支持。做项目时你应该在硬件设计阶段就决定走哪条总线,而不是等画完板子再临时换。
5.1 什么时候选SPI,什么时候选I2C
我给自己固定了一套选型判断流程,分享出来给你参考:
| 场景 | 推荐总线 | 理由 |
|---|---|---|
| 高速数据采集(ADC、音频、图像传感器) | SPI | 时钟可以到几十MHz,全双工,数据吞吐量大 |
| 存储器件(Flash、SD卡) | SPI | 协议简单,速度和DMA配合好,I2C的EEPROM只适合小容量 |
| 多个低速传感器(温湿度、气压、姿态) | I2C | 两根线挂一堆设备,连线和PCB面积都省 |
| 引脚紧张的MCU | I2C | 只用2个引脚,地址寻址代替多路片选 |
| 对功耗要求极高(电池设备) | I2C | 开漏上拉,空闲时电流极小,总线电容小也可以低速运行 |
| 板间/机箱内长线互连 | 都不推荐 | 总线电容受限,更建议差分接口或隔离方案,真要选也是SPI降速或用I2C加缓冲器 |
5.2 低速场景里I2C比SPI好用,高速场景里I2C被SPI碾压
“速度”在这两种总线里的权重不一样。你要读一个温湿度传感器,每秒一次,数据只有两字节,SPI和I2C都绰绰有余,这时候明显选I2C更合适,因为挂多个传感器不占引脚。但你要采集音频或连续采样ADC,每秒需要几十万字节,I2C标准模式的100kbps连塞牙缝都不够,就算快速模式400kbps也捉襟见肘。而且I2C是半双工,读数据时主机不能同时发数据,每个字节都有额外ACK位,协议开销大约1/9,有效吞吐进一步打折。
搞清楚“外设本身的速率需求”和“总线能够提供的有效数据率”,选型就不会错。多数I2C从机本身内部有转换时间,比如某气压传感器测量一次要10ms,总线速率高到天上也没用,瓶颈在传感器内部。这种情况下采用400kHz I2C就够了,把注意力放在低功耗和连线简单上。
5.3 我在实际项目中固化下来的一些设计习惯
写驱动调总线这么多年,我有几个个人习惯,未必是标准答案,但确实帮我省了很多事:
第一,GPIO和复用功能的初始化一定统一放在一个文件里。SPI的引脚复用、I2C的开漏配置、软件片选的GPIO初始化,别分散在一堆初始化函数里。调试时找配置更快。
第二,所有SPI/I2C外设的操作,尽量做成统一封装的函数。比如SPI的读寄存器,封装成read_reg(uint8_t reg),I2C的读也封装成read_reg(uint8_t dev_addr, uint8_t reg_addr)。上层传感器逻辑不区分协议,后续换传感器或换总线,改动集中在底层。
第三,硬件上SPI的MOSI和MISO之间预留一个0欧电阻焊盘,I2C的SDA和SCL上预留上拉电阻焊盘,PCB调试时灵活断开或调整。这些“调试焊盘”的成本几乎为零,但能避免飞线改板的痛苦。
第四,I2C总线上的上拉电阻,我一般会在原理图上标注“上拉电阻位预留,取值调试确定”,而不是一开始就写死一个值。带着示波器实测,确定最优阻值后再回填到BOM里。
第五,真正做产品时,I2C总线上每个设备都要能在不干扰总线的情况下独立复位。至少给每个I2C从机的电源留一个MOS管开关,某设备把总线拉死时,能单独断电重启它,不用整体掉电。
回到“从沙子到车辙”的视角,SPI和I2C其实是芯片内部世界和外部世界之间的两座桥。一座叫专线,把数据用最快的管道运过去;一座叫公交,低引脚、多设备、灵活方便。理解了它们的物理本质和设计动机,你就能在原理图阶段决定怎么接线,在代码阶段知道该配什么参数,在调试阶段知道从哪下手。下一次你的SPI读出0xFF、I2C总线卡死,别慌,按链路查一遍:看电源、看波形、扫地址、调极性,问题多半就在那几条线之间。