SPI与I2C总线协议详解:从片内到片间通信的实战指南
发布时间:2026/10/23 8:20:51 锦皓数字建站

1. 从片内总线到片间总线的思维跃迁1.1 为什么片内通信方案不能直接搬到板级做过单片机或者嵌入式开发的朋友大概率都经历过这样一个阶段在芯片内部各个外设模块通过内部总线跟CPU核打交道寄存器读写、DMA搬运、中断响应一切都井井有条。可一旦要把两颗独立的芯片连起来让它们交换数据事情就完全不一样了。片内总线是芯片设计厂商在硅片层面就定好的规则时序、电平、协议栈都是固定的你作为开发者只需要读写寄存器就行。但片间通信你得自己拉线、自己定协议、自己处理电平匹配和时序协调自由度大了坑也多了。这个系列叫“从沙子到车辙”前面几篇聊的是从原材料到芯片内部结构的事情这一篇要讲的是芯片怎么跟外面的世界说话。标题里的“从片内到片间”说的就是这个跨越——从芯片内部的总线通信过渡到芯片与芯片之间的板级通信。SPI和I2C就是在这个场景下最常用的两种协议。它们不像USB、以太网那么复杂但在传感器、存储器、显示屏、EEPROM这些外设的连接上几乎是绕不开的选择。我个人的经验是很多初学者在学完GPIO点灯和串口打印之后下一步就会碰到SPI或I2C的传感器模块。这时候如果只是照着例程抄一遍能跑通但一旦换个芯片或者换个传感器就不知道从哪儿改起了。根本原因在于没有理解这两种总线的设计哲学和时序本质。这篇文章就是要把这两种协议从底层逻辑到实操细节全部拆开讲清楚让你以后拿到任何一款SPI或I2C设备都能自己看懂时序图、自己算参数、自己排查问题。1.2 两种协议的核心定位与适用边界SPI和I2C经常被放在一起比较但它们的设计目标其实完全不同。SPI的全称是Serial Peripheral Interface翻译过来就是串行外设接口它的核心思路是“快”——用四根线SCLK、MOSI、MISO、CS实现全双工高速通信时钟频率可以跑到几十兆甚至上百兆赫兹。它不关心总线上挂了多少设备每个从设备都需要一根独立的片选线所以引脚数量会随着设备增多而线性增长。I2C的全称是Inter-Integrated Circuit读作“I-squared-C”或者“I-two-C”都行它的核心思路是“省”——只用两根线SCL和SDA就能挂载最多127个设备通过地址来区分不同的从机。代价是速度相对较慢标准模式100kHz快速模式400kHz高速模式也就3.4MHz。而且它是半双工的同一时刻只能单向传输数据。选择哪种协议本质上是在速度、引脚数量、复杂度和成本之间做权衡。举个例子如果你要接一个高速ADC采样芯片数据率要求几兆字节每秒那SPI是唯一选择。如果你要接五六个低速传感器每个传感器每秒就传几个字节那I2C用两根线搞定所有设备PCB布线会清爽很多。我在实际项目中见过不少设计同一块板子上SPI和I2C并存各司其职这是很正常的做法。注意SPI没有严格的协议标准不同厂商的时序模式可能不同而I2C是有明确定义的规范文档的。这意味着SPI的兼容性问题通常比I2C更多换一颗芯片可能就要重新调时序。2. SPI协议深度拆解与时序模式详解2.1 四线制的基本工作原理SPI的物理层非常直观一根时钟线SCLK由主机产生一根数据线MOSI负责主机发送从机接收一根数据线MISO负责从机发送主机接收外加每个从机一根片选线CS。通信时主机拉低对应从机的CS然后开始在SCLK上输出时钟脉冲数据在时钟的边沿上被采样或移出。由于MOSI和MISO是独立的所以SPI天然支持全双工——主机在发送数据的同时也在接收数据。这里有一个容易被忽略的细节SPI的数据移出和采样是同时进行的。主机内部的移位寄存器和从机内部的移位寄存器在时钟驱动下形成一个环形结构主机移出一位到MOSI上同时从MISO上移入一位。所以即使你只想读从机的数据也必须发送一些数据通常是0xFF或者0x00来产生时钟脉冲从机才会把数据吐出来。这个机制在写驱动的时候一定要记住很多新手会疑惑“为什么读数据还要先写一个字节”。片选信号CS的作用不仅仅是选中设备它还承担着帧同步的功能。CS拉低表示一次传输的开始CS拉高表示传输结束。有些从机芯片要求CS在整帧数据传输期间保持低电平中间不能有跳变否则内部状态机会复位。我在调试一款Flash存储芯片时就遇到过这个问题因为用了硬件SPI的自动CS控制结果在多字节连续读取时CS中间被拉高了导致数据错乱。后来改成软件手动控制CS才解决。2.2 四种时序模式的本质区别SPI最让人头疼的地方就是时序模式。CPOL和CPHA两个参数组合出四种模式很多人在选模式的时候就是一个个试试到能通就行。但如果你理解了这两个参数的本质就能直接从从机芯片的数据手册里读出该用哪种模式。CPOL是Clock Polarity的缩写意思是时钟极性。CPOL0表示时钟空闲时为低电平CPOL1表示时钟空闲时为高电平。CPHA是Clock Phase的缩写意思是时钟相位。CPHA0表示数据在第一个时钟边沿被采样CPHA1表示数据在第二个时钟边沿被采样。所谓“第一个边沿”和“第二个边沿”是相对于时钟从空闲状态跳变出去的那个边沿而言的。具体来说当CPOL0时时钟空闲为低第一个边沿是上升沿第二个边沿是下降沿。当CPOL1时时钟空闲为高第一个边沿是下降沿第二个边沿是上升沿。CPHA0时数据在第一个边沿采样在第二个边沿移出。CPHA1时数据在第一个边沿移出在第二个边沿采样。模式CPOLCPHA空闲电平采样边沿移出边沿Mode 000低上升沿下降沿Mode 101低下降沿上升沿Mode 210高下降沿上升沿Mode 311高上升沿下降沿实际工作中Mode 0和Mode 3是最常见的。大部分传感器和存储器都支持Mode 0有些支持Mode 3。Mode 1和Mode 2相对少见但也不是没有。我的建议是拿到一颗新芯片先翻数据手册找时序图看SCLK空闲时是高还是低看数据是在哪个边沿稳定的然后对照上表直接确定模式不要盲目试。2.3 时钟频率与数据建立保持时间SPI的时钟频率理论上可以很高但实际能跑多快取决于好几个因素主机的SPI控制器能力、从机芯片的最高时钟频率、PCB走线的信号完整性、以及数据建立时间和保持时间是否满足要求。从机数据手册里通常会给出一个最高SCLK频率比如20MHz但这不意味着你一定能跑到20MHz。数据建立时间Setup Time是指数据在采样边沿到来之前必须保持稳定的最短时间保持时间Hold Time是指数据在采样边沿之后必须继续保持稳定的最短时间。如果时钟太快从机还没来得及把数据准备好采样边沿就到了读到的就是错误数据。这时候示波器或者逻辑分析仪就派上用场了你可以直接观察SCLK和数据线的波形看数据跳变是否发生在采样边沿附近。我在一个项目里用SPI驱动一块TFT显示屏从机标称支持50MHz但实际跑到30MHz以上就出现花屏。用逻辑分析仪抓波形发现MISO线上的数据跳变几乎和SCLK的上升沿重合建立时间严重不足。后来把时钟降到24MHz问题消失。这个案例说明数据手册的标称值是在理想条件下测得的实际PCB上的寄生电容、走线长度、驱动能力都会影响最终能跑的最高频率。实操心得如果你不确定SPI能跑多快从低速开始比如1MHz确认通信正常后再逐步提高每次翻倍直到出现误码为止然后回退到上一个稳定值。这个方法比直接看数据手册更靠谱。3. I2C协议精讲与地址机制3.1 两根线如何实现多设备通信I2C只用SCL和SDA两根线却能挂载上百个设备靠的是一套精巧的地址寻址机制。每次通信都由主机发起主机先在SDA上发送一个起始条件Start Condition然后发送7位从机地址加1位读写标志位。总线上所有从机都会收到这个地址但只有地址匹配的从机才会响应其他的保持沉默。被选中的从机在第9个时钟周期拉低SDA作为应答ACK表示“我在我准备好了”。起始条件和停止条件是I2C协议里非常关键的两个特殊信号。起始条件定义为SCL为高电平时SDA从高电平跳变到低电平。停止条件定义为SCL为高电平时SDA从低电平跳变到高电平。注意这两个条件都要求SCL处于高电平状态这是为了跟普通的数据位传输区分开——普通数据位传输时SDA的跳变必须发生在SCL为低电平期间。SDA和SCL都是开漏输出这意味着它们只能主动拉低不能主动拉高。高电平是靠外部的上拉电阻拉上去的。这个设计的好处是天然支持总线仲裁和多主机模式而且不同电压域的设备可以共存只要上拉电阻接到合适的电压上就行。上拉电阻的阻值选择是有讲究的阻值太小功耗大而且可能超出设备的灌电流能力阻值太大上升沿变缓高速通信时波形会变形。常见的取值在2.2kΩ到10kΩ之间具体要看总线电容和通信速度。3.2 7位地址与10位地址的取舍I2C标准最初定义了7位地址空间去掉保留地址后实际可用的地址有112个左右。后来因为设备越来越多地址不够用了又扩展出了10位地址模式。10位地址的通信流程稍微复杂一些主机先发送一个特殊的保留地址11110xx其中xx是10位地址的最高两位然后再发送剩下的8位地址。这样总共能支持1024个地址。不过在实际项目中10位地址的设备并不常见。大部分传感器、EEPROM、IO扩展芯片用的都是7位地址。而且很多芯片的地址不是完全固定的会留出几个引脚让你通过拉高或拉低来配置地址的低几位。比如一款常见的EEPROM它的7位地址是1010xxx其中低三位由A2、A1、A0三个引脚决定这样同一款芯片最多可以挂8片在一条总线上而不冲突。这里有一个新手经常踩的坑数据手册上给出的地址可能是8位格式包含读写位也可能是7位格式。比如手册写“设备地址为0xA0”这通常是8位格式实际7位地址是0x50。在写驱动的时候你需要把7位地址左移一位再或上读写位才能得到最终发送到总线上的字节。我见过不少人直接把0xA0当作7位地址用结果怎么都通信不上。3.3 时钟拉伸与总线仲裁I2C有两个比较高级的特性时钟拉伸Clock Stretching和总线仲裁Arbitration。时钟拉伸是指从机在需要更多时间处理数据时可以主动把SCL拉低强制主机等待。这个机制对于慢速从机非常有用比如某些传感器在转换完成后才能提供数据它就可以在转换期间拉低SCL主机检测到SCL没有被释放就不会继续发时钟。总线仲裁发生在多主机场景下。当两个主机同时发起通信时它们会同时发送起始条件和地址。每个主机在发送每一位的同时也在监听SDA线的状态如果自己发送的是高电平但检测到SDA被拉低了说明另一个主机发送了低电平优先级更高自己就退出竞争转为从机模式。这个过程不会丢失数据因为赢得仲裁的主机的数据一直在总线上保持不变。不过说实话在大多数嵌入式项目里总线仲裁基本用不到因为通常只有一个主机比如一颗MCU。时钟拉伸倒是偶尔会遇到有些从机芯片在特定操作时会拉低SCL如果你的主机不支持时钟拉伸通信就会卡死。所以在选型的时候要确认主机的I2C控制器是否支持时钟拉伸功能。4. 从零搭建SPI与I2C通信的实操过程4.1 硬件连接与上拉电阻计算先讲I2C的硬件连接。假设你要把一颗MCU和一颗温度传感器用I2C连起来你需要把MCU的SCL接到传感器的SCLSDA接到SDA然后在SCL和SDA上各接一个上拉电阻到电源正极。电源电压要跟两颗芯片的IO电平匹配如果不匹配就需要电平转换电路。上拉电阻的阻值怎么算公式是R (Vdd - Vol) / Iol其中Vol是设备输出低电平时的最大电压Iol是设备能灌入的最大电流。以3.3V系统为例假设Vol最大为0.4VIol为3mA那么R (3.3 - 0.4) / 0.003 ≈ 967Ω。这是理论最小值实际取值要考虑总线电容带来的上升时间。上升时间t 0.847 × R × C其中C是总线总电容包括PCB走线电容和引脚电容。I2C标准规定标准模式下上升时间不能超过1000ns快速模式下不能超过300ns。假设总线电容为100pF快速模式要求上升时间小于300ns那么R 300ns / (0.847 × 100pF) ≈ 3.5kΩ。综合来看2.2kΩ到4.7kΩ是比较安全的范围。如果总线上设备多、走线长电容大就要用更小的阻值如果追求低功耗可以用大一点的阻值但速度要降下来。SPI的硬件连接更简单不需要上拉电阻除非从机的MISO是开漏输出这种情况很少见。把主机的SCLK、MOSI、MISO分别接到从机的对应引脚CS接到从机的片选引脚。如果总线上有多个从机每个从机需要一根独立的CS线SCLK、MOSI、MISO可以共用。注意MISO线在多从机场景下需要处理——当某个从机的CS为高时它的MISO应该处于高阻态否则会跟其他从机的输出冲突。大部分SPI从机都支持这个特性但设计时还是要确认一下。4.2 寄存器配置与初始化代码以一款常见的ARM Cortex-M系列MCU为例配置SPI控制器的步骤大致如下。首先使能SPI外设的时钟配置GPIO复用功能把SCLK、MOSI、MISO、CS引脚设置为SPI功能。然后配置SPI控制寄存器设置时钟极性CPOL和相位CPHA设置数据位宽通常8位设置时钟分频系数设置主从模式为主机使能SPI。// SPI初始化示例伪代码具体寄存器名因芯片而异 SPI_CR1 0; SPI_CR1 | (0 0); // CPHA 0 SPI_CR1 | (0 1); // CPOL 0即Mode 0 SPI_CR1 | (1 2); // 主机模式 SPI_CR1 | (0 3); // 波特率预分频具体值看时钟树 SPI_CR1 | (1 6); // 使能SPI SPI_CR1 | (1 8); // 内部片选管理如果硬件支持I2C的初始化类似但多了一些参数时钟频率100kHz或400kHz、自身地址多主机模式下用、应答使能、时钟拉伸使能等。配置完成后发送起始条件发送地址检查ACK然后读写数据最后发送停止条件。// I2C写一个字节的流程伪代码 I2C_CR2 | (1 8); // 产生起始条件 while(!(I2C_SR1 0x01)); // 等待起始条件发送完成 I2C_DR (dev_addr 1) | 0; // 发送7位地址写标志 while(!(I2C_SR1 0x02)); // 等待地址发送完成 I2C_DR reg_addr; // 发送寄存器地址 while(!(I2C_SR1 0x80)); // 等待数据发送完成 I2C_DR data; // 发送数据 while(!(I2C_SR1 0x80)); // 等待数据发送完成 I2C_CR2 | (1 9); // 产生停止条件上面这段代码是寄存器级别的操作实际开发中通常会使用厂商提供的HAL库或者LL库代码会更简洁。但理解底层流程对于排查问题非常有帮助。比如当通信失败时你可以通过检查状态寄存器的标志位来判断卡在哪一步是起始条件没发出去还是地址没被应答还是数据发送超时。4.3 用逻辑分析仪验证通信波形代码写完了下载进去发现读回来的数据全是0xFF或者0x00这时候怎么办我的第一建议是上逻辑分析仪。逻辑分析仪能同时抓取SCL和SDA或者SCLK、MOSI、MISO、CS的波形然后自动解码出协议内容。你一眼就能看出是地址发错了、ACK没收到、还是数据位错位了。以I2C为例正常的波形应该是这样的起始条件后7位地址加1位读写位然后从机拉低SDA一个时钟周期作为ACK接着是数据字节和ACK最后是停止条件。如果地址后面没有ACK说明从机没有响应可能原因包括地址不对、从机没供电、上拉电阻没接、SDA或SCL接反了、从机芯片损坏。如果ACK正常但数据不对可能是寄存器地址发错了或者读写标志位搞反了。SPI的波形分析类似重点看CS是否在整帧期间保持低电平数据是否在正确的时钟边沿稳定MISO上的数据是否跟从机手册描述的一致。我习惯把逻辑分析仪的采样率设到时钟频率的10倍以上这样波形细节不会丢失。比如SPI时钟是10MHz采样率至少100MHz才能看清楚每个边沿附近的数据变化。注意逻辑分析仪的探头也有电容接到高速信号上可能会影响波形。如果发现接上探头后通信反而出问题了可以试试用短地线弹簧代替长地线减少环路面积。5. 常见问题排查与避坑经验实录5.1 SPI通信失败的典型原因SPI通信不上按概率从高到低排我遇到的情况大概是这样的时序模式选错排第一片选信号处理不当排第二时钟频率过高排第三数据位宽或字节序搞错排第四。时序模式选错是最常见的。很多从机芯片同时支持Mode 0和Mode 3但有些只支持其中一种。如果你用Mode 0驱动一个只支持Mode 3的芯片数据就会错位。判断方法很简单看数据手册的时序图如果数据在时钟的第一个边沿就被采样那就是CPHA0如果在第二个边沿被采样那就是CPHA1。再结合时钟空闲电平确定CPOL。片选信号的问题也很常见。有些开发者用硬件自动控制CS但硬件CS的时序可能不满足从机要求。比如从机要求CS拉低后至少等待100ns才能开始发时钟但硬件CS和时钟是同时发出的。这时候就需要改成软件手动控制CS在拉低CS后加一个短延时再启动SPI传输。时钟频率过高的问题通常表现为低速能通高速不通或者短距离能通长距离不通。解决办法就是降频或者改善PCB走线缩短走线、增加地平面、减小负载电容。我在一个项目里用排线连接SPI显示屏排线长度20厘米时钟超过10MHz就花屏降到5MHz就稳定了。后来把排线改成短的FPC排线20MHz也没问题。5.2 I2C总线死锁与恢复方法I2C有一个经典问题总线死锁。现象是SCL被从机拉低不放或者SDA被拉低不放主机无法发起新的通信。造成死锁的原因通常是通信过程中主机复位了但从机还在等待时钟脉冲它拉着SDA不放主机又以为总线空闲发起起始条件时发现SDA是低的产生总线错误。解决I2C死锁的标准方法是在SCL上手动发送9个时钟脉冲让从机把剩余的位发完然后发送停止条件。具体操作是把SCL配置为GPIO输出手动翻转9次每次高电平持续至少4微秒然后发送一个停止条件SCL高时SDA从低变高。如果从机是正常工作的这9个脉冲会让它释放SDA线。// I2C总线恢复示例伪代码 GPIO_SetOutput(SCL_PIN); GPIO_SetOutput(SDA_PIN); GPIO_SetHigh(SDA_PIN); for(int i 0; i 9; i) { GPIO_SetLow(SCL_PIN); delay_us(5); GPIO_SetHigh(SCL_PIN); delay_us(5); } // 发送停止条件 GPIO_SetLow(SDA_PIN); delay_us(5); GPIO_SetHigh(SCL_PIN); delay_us(5); GPIO_SetHigh(SDA_PIN); delay_us(5); // 重新初始化I2C外设 I2C_Init();预防死锁的方法包括在I2C通信中加入超时机制一旦超时就执行恢复流程在主机复位时也复位I2C从机如果从机有复位引脚的话避免在通信过程中频繁复位主机。5.3 多设备共存时的地址冲突与信号完整性当一条I2C总线上挂了很多设备时地址冲突和信号完整性是两个需要重点关注的问题。地址冲突的排查方法是逐个设备单独通信确认每个设备的地址都能正常应答然后再全部接上看是否还有冲突。如果两个设备地址相同可以考虑用I2C多路复用器比如TCA9548A这类芯片把总线分成多路每路挂一个冲突的设备。信号完整性问题在设备多、走线长的时候尤其明显。总线电容增大导致上升沿变缓可能使数据在采样点还没达到有效高电平。用示波器观察SDA和SCL的上升沿如果上升时间超过规范值就要减小上拉电阻。但减小上拉电阻会增加功耗所以要在速度和功耗之间找平衡。另一个办法是降低通信速度给上升沿更多时间。SPI在多设备场景下的主要问题是MISO线的竞争。如果某个从机的MISO没有正确进入高阻态它会跟其他从机的输出打架导致数据错误甚至芯片损坏。排查方法是只接一个从机确认通信正常然后接上第二个从机如果通信异常检查第一个从机的MISO在CS为高时是否真的高阻。有些芯片的MISO是推挽输出不支持高阻态这种芯片就不能和其他SPI设备共用MISO线只能单独用一根MISO或者加一个多路复用器。问题现象可能原因排查方法解决方案SPI读回全0xFFMISO未连接或从机未响应检查MISO连线用逻辑分析仪看波形确认从机供电和CS信号SPI数据错位时序模式错误对照数据手册时序图修改CPOL/CPHA配置I2C无ACK地址错误或从机未就绪用逻辑分析仪解码地址确认7位/8位地址格式I2C总线死锁主机复位导致从机状态机卡死测量SDA/SCL电平发送9个时钟脉冲恢复高速通信不稳定信号完整性差示波器看上升沿和建立时间降频或减小上拉电阻多设备地址冲突两个设备地址相同逐个单独测试使用I2C多路复用器5.4 软件模拟SPI/I2C的适用场景有时候硬件SPI或I2C外设不够用或者引脚分配受限就需要用GPIO软件模拟。软件模拟的优点是灵活任何引脚都能用时序可以精确控制缺点是速度慢占用CPU资源。软件模拟SPI在低速场景下完全够用比如驱动一个OLED屏幕或者读一个低速传感器。软件模拟I2C稍微复杂一些因为要处理起始条件、停止条件、ACK检测和时钟拉伸。写软件模拟I2C的时候最关键的是延时控制。SCL的高电平和低电平时间要满足从机的要求通常标准模式下高电平至少4微秒低电平至少4.7微秒。如果从机支持时钟拉伸还要在拉高SCL后检测SCL是否真的变高了如果没变高说明从机在拉伸时钟要等待。软件模拟的代码虽然看起来简单但要在各种边界条件下都稳定工作还是需要仔细调试的。我个人建议如果硬件外设够用优先用硬件如果硬件不够用或者引脚受限再考虑软件模拟。软件模拟的代码最好封装成独立的模块方便在不同项目之间移植。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。