
I2C和SPI这两个协议我前前后后用了快十年了。从最早调一个陀螺仪传感器到后来在产线上帮客户排查一批主板通信不稳的问题再到现在带新人写驱动几乎每天都在跟这两兄弟打交道。可以说搞嵌入式如果能把I2C和SPI吃透板级通信这块基本就稳了一大半。这篇就把我这些年积累的、踩过的坑、总结出的经验一次性讲清楚。先说说为什么这两个协议这么重要。在嵌入式系统里芯片和芯片之间、主板和传感器之间要交换数据方式无非就那么几种UART、I2C、SPI、CAN加上现在越来越常见的USB和以太网。其中I2C和SPI是板级通信也就是同一块PCB上芯片之间通信的绝对主力。你随便拆一个开发板、一个物联网模组、一个电机驱动板上面至少有七八颗芯片是通过I2C或SPI挂在主控上的。它们管的事包括但不限于读传感器数据、配置音频解码芯片的寄存器、读写Flash、控制屏幕、扩展IO口。这两个协议看起来简单实际用起来门道很多。I2C的外围电路就几个电阻但选型不对直接通信失败SPI速率上限很高但模式配错、片选选错数据全是乱的。很多初学者在Arduino或者STM32上能跑通Demo一放到自己设计的板子上就翻车往往就是没搞懂协议背后的电气原理和时序细节。这篇文章我打算分几块来讲先把I2C的完整工作原理、上拉电阻选型、时序细节掰开揉碎再把SPI的四种模式、硬件片选和软件片选、DMA传输这些核心点讲透最后结合实际调试经验给出一份常见问题排查清单。不管你是准备入门的新手还是被I2C波形折磨过的老手这篇文章应该都能给你一些新的启发。1. I2C通信协议核心拆解I2CInter-Integrated Circuit是Philips现在的NXP在1982年发明的初衷就是让电视、音响这类消费电子设备内部能用更少的线连接各个芯片。它的设计极简却非常有生命力两条线一条时钟SCL一条数据SDA就能挂上百个设备。这也是它到今天仍在使用的根本原因。1.1 为什么是开漏输出加上拉电阻而不是推挽输出这个问题我面试过很多人十个有八个答不完整。I2C为什么不能像SPI那样直接推挽输出关键在于“多主机仲裁”和“设备热插拔”这两个需求。先看多主机仲裁。I2C允许总线上挂多个主机两个主机同时发起通信时怎么决定谁先谁后靠的是线与逻辑。开漏输出意味着设备只能把电平拉低不能主动拉高拉高的工作由外部上拉电阻完成。这样一来如果两个主机的输出电平冲突只要有一个拉低总线就是低电平另一个通过回读SDA电平就能感知到冲突主动退出这个过程就是仲裁。如果换成推挽输出两个主机一个输出高一个输出低直接就是短路瞬间烧毁引脚。再看设备热插拔。开漏结构下每个设备的SDA/SCL引脚其实都是一颗MOS管的漏极设备不工作时只要不上电对总线来说就是高阻态不影响其他设备通信。这对现在很多可插拔的传感器模块特别重要。所以I2C外围那两个上拉电阻不是随便放的它是协议能工作的物理基础。没有上拉电阻总线永远是低电平通信完全瘫痪上拉电阻选得不对不是波形爬不上去就是功耗超标。1.2 上拉电阻的选型计算与100k速率的特殊场景上拉电阻的数值怎么定我一般直接按下面这个思路算。电阻不能太大因为总线存在寄生电容RC充电时间常数决定上升沿时间。I2C规范里标准模式100kHz要求上升沿不超过1000ns快速模式400kHz要求不超过300ns。你这个板子上总线的寄生电容粗略估法是一根走线大概1pF/cm加上每个芯片引脚还有几pF一条总线上挂四五个设备总电容差不多50pF。用公式t_rise≈0.8473×R×C反推100kHz模式下R取10kΩ上升沿差不多是424ns符合要求但400kHz模式下就必须降到3kΩ以下了。电阻也不能太小因为设备拉低总线时电流要流过这个电阻。2V到5V的系统里阻值太低灌入引脚的电流可能超出芯片的灌电流能力IOL一般是3mA到20mA还会额外消耗功耗。我的习惯是3.3V系统、100kHz标准模式选4.7kΩ到10kΩ都能跑跑400kHz选2.2kΩ到4.7kΩ如果总线很长、挂载设备很多超过8个就选1kΩ到2.2kΩ。这里特别说一下热搜词里那个“100k I2C信号规格”。我猜这是指低速场景比如EEPROM等一些器件在配置阶段只支持100kHz甚至更低。这种情况把主控的I2C时钟配置成100kHz以下同时上拉电阻可以适当选大一点比如10kΩ甚至20kΩ以降低功耗。但要注意不是所有从机都支持低速某些新的传感器芯片要求最低400kHz才能完成初始化序列如果用了太大的上拉电阻导致边沿过缓从机可能识别不到起始信号。所以最稳妥的做法是先用数据手册确认器件的最高和最低通信速率再做配置。1.3 时序是如何保证的起始、停止、数据、ACKI2C的时序可以拆成四个基础动作理解了这四个动作基本就能看懂示波器上的任何I2C波形。起始条件STARTSCL为高电平时SDA从高跳变到低。这个动作宣告总线开始通信。停止条件STOPSCL为高电平时SDA从低跳变到高。这个动作宣告总线释放。数据传输SDA上的数据必须在SCL高电平期间保持稳定只在SCL低电平期间才能变化。也就是说数据是在SCL上升沿附近被从机采样锁存的。这句话是整个I2C时序的核心能对照着波形图和这句话把数据读出来的话I2C时序就不再是一个黑盒子。ACK应答每传输完一个字节8位第9个时钟周期是应答位。主机释放SDA从机如果正常接收就把SDA拉低表示“我收到了”如果从机忙不过来或者地址不匹配就保持高电平产生NACK。实操中我建议初学者做一件事拿逻辑分析仪抓一段真实的I2C通信波形对照数据手册手动解析一遍地址字节和数据字节。不要只是跑通代码就完事手动解析一次之后你对I2C时序的理解会产生质的飞跃。工具用廉价的Saleae逻辑分析仪或者金沙滩的逻辑分析仪都行配合PulseView软件免费好用。1.4 寻址机制7位地址与10位地址I2C总线上挂多个设备时靠地址区分彼此。最常见的7位地址模式地址范围是0x08到0x770x00是广播地址0x01到0x07保留0x78以上留作10位地址扩展。发送地址时主机发送的是7位地址左移一位后的结果最低位是读写标志0表示写1表示读。所以你在代码里经常看到0x68、0xD0这种写法前者是7位地址比如MPU6050后者是把7位地址加上读写位后的8位形式。这个细节经常让新手困惑我见过有人把器件地址搞反连调一周都读不到数据。7位地址只能挂127个设备实际器件一般通过地址引脚A0、A1、A2来设置低几位比如常见的EEPROM AT24Cxx用A0、A1、A2引脚组合出8个不同地址一条总线上最多挂8颗同样的芯片。10位地址模式用得少一般只在需要大量设备的工业场景才用日常开发碰到的概率很小。1.5 I2C的扩展应用和常见误区总线上需要挂的设备太多或者从机地址冲突时怎么办两种方案比较常用一是I2C多路复用器典型芯片是TCA9548A它可以把一条I2C总线扩展成8路每路都可以挂完整的设备链。注意复用器的切换命令要在通信间隙发送不要在一个连续读操作中间切换通道否则从机状态会错乱。二是I2C电平转换器典型芯片是PCA9306或TXS0102用来连接不同电压域的I2C总线比如主控是3.3V传感器是5V。这里有个坑很多电平转换器是双向的但必须正确接参考电压引脚如果接反了通信表现为“偶尔成功、随机失败”。之前有个客户I2C总线挂了一个5V的气压传感器他们用PCA9306接3.3V主控结果经常出现读到的气压值跳变排查半天发现是OE引脚悬空导致输出高阻态改成上拉固定使能后问题立刻消失。还有一个常见误区是I2C是否需要配置内部上拉。很多MCU的GPIO内部自带上拉电阻典型值30kΩ到50kΩ但I2C规范一般推荐外部上拉。如果你在板子上已经放了外部上拉电阻内部上拉可开可不开影响不大但如果你懒得放外部电阻想依赖内部上拉要注意内部上拉阻值太大400kHz快速模式下上升沿可能不达标而且一条总线上多个设备都开启内部上拉等效电阻还会并联变小反而影响一致性。我的建议是硬件设计阶段就规划好外部上拉电阻软件里不使能内部上拉减少变量。2. SPI通信协议核心拆解SPISerial Peripheral Interface是Motorola在1980年代推出的设计目标很直接高速度、全双工、简单到几乎没有协议。它用四根线——SCLK时钟、MOSI主出从入、MISO主入从出、CS片选——实现了点对点的快速通信。速率轻松跑到几十兆赫兹是I2C的几十倍所以高速传感器、Flash存储、屏幕驱动这些场景基本都是SPI的天下。2.1 SPI四种模式CPOL和CPHASPI没有像I2C那样复杂的起始停止条件和ACK机制它的核心是时钟极性和时钟相位。CPOLClock Polarity决定空闲时SCLK是高电平还是低电平CPHAClock Phase决定数据是在时钟的哪个边沿被采样。两者组合出四种模式对应SPI Mode 0到Mode 3。我在实际项目中总结出来的规律Mode 0CPOL0, CPHA0是默认最常用的大多数Flash、SD卡、LCD屏都支持Mode 1和Mode 2比较容易踩坑一般是某些特定芯片要求Mode 3和Mode 0一样常见很多射频芯片喜欢用。关键是查器件手册看到“CPOL1, CPHA1”就是Mode 3不要自己猜。这里有个常见bug主从配置不一致时从机读到的数据完全乱掉但电平看起来是好的。我在用示波器排查时遇到过几次最后发现就是CPOL/CPHA错了。排查方法很简单用示波器抓SCLK空闲电平和数据变化时刻对照数据手册确定应该用哪种模式。2.2 硬件片选与软件片选的取舍片选CS的作用是告诉从机“该你上场了”。实现方式有两种硬件片选和软件片选。硬件片选就是MCU的SPI外设自带NSS引脚启动传输时硬件自动拉低片选。好处是时序精确从机响应快尤其适合高速通信坏处是灵活性差比如有的MCU只支持固定引脚做NSS不支持任意引脚映射或者当总线上挂多个设备时硬件片选管理起来麻烦。软件片选就是用普通GPIO手动控制片选。这是我最常用的方式。好处是极大灵活任何GPIO都可以做片选总线上挂多少设备都不怕坏处是时序上可能引入延迟因为GPIO翻转速度不如硬件外设。这个延迟一般在几百纳秒级别对几MHz的SPI通信完全足够但对十几MHz以上的高速Flash读写需要留意时序余量。还有一个容易忽略的点片选电平在两次传输之间是否需要恢复为高电平。有些从机比如某些Flash芯片要求片选在高电平后保持一段时间才能进行下一次操作如果两次通信之间片选拉高时间太短芯片状态机可能无法复位。我一般会在软件里保证两次CS激活之间至少间隔几个微秒尤其是同一颗芯片连续读写的场景。具体时长以数据手册为准不同芯片差异很大。2.3 SPI的传输结构上升沿/下降沿采样和MSB/LSB顺序SPI的数据是按位串行传输的。主控在时钟边沿把MOSI上的数据发出从机在下一个边沿采样同时从机在MISO上把数据返回。整个过程是全双工的主机每发出8位数据的同时也能收到从机返回的8位数据。这里有个初学者很容易想不通的问题为什么读Flash的时候主控明明只是在发命令MISO上的数据却是对的因为SPI是全双工主机发出的数据就是时钟发生器从机就是在这个时钟边沿去采样主机发来的命令同时把自身的数据放到MISO上。所以每次SPI读操作本质上是一个写命令读数据的同时发生的过程。MSB最高位在前还是LSB最低位在前也是常见坑。绝大多数芯片默认MSB first但有些传感器特别是国产的某些芯片默认LSB first。配置错的话读出来的数据永远是镜像的比如0x01变成0x80。我在一次调试温湿度传感器时就遇到这个问题折腾了半小时才发现是字节序反了。2.4 通过DMA方式读取芯片数据以STM32F103为例SPI高速传输时CPU逐个字节搬运数据会浪费大量算力所以主流做法是配合DMADirect Memory Access使用。下面我用STM32F103 CubeMX配置DMA读取一颗SPI Flash为例讲一下核心步骤。第一步在CubeMX里开启SPI外设配置为主模式设置时钟极性、相位、波特率启用DMA请求。SPI的DMA请求有两个方向RX请求和TX请求。读数据时RX方向必须开启DMA并且需要在DMA设置里选择“Normal模式”而不是“Circular模式”。很多初学者在这里用错了模式导致DMA不收手内存被写穿。第二步生成代码后在初始化函数里调用HAL_SPI_Receive_DMA(hspi1, buffer, length)启动接收。但注意HAL库的SPI DMA模式默认会在传输完成后关闭SPI而且DMA接收过程中CPU不能直接操作buffer要在回调函数HAL_SPI_RxHalfCpltCallback或HAL_SPI_RxCpltCallback中处理数据。第三步读Flash数据前主机需要先发送读命令0x03和24位地址这个可以用HAL_SPI_Transmit发送然后再调用DMA接收。但是坑在这里如果不想把发送和接收分成两步可以用HAL_SPI_TransmitReceive_DMA同时发送命令和接收数据。此时要用一个数组前几个字节是命令地址后面的字节是无效发送一般填0xFF接收到的数组前几个字节是无效数据从第4个字节开始才是真正读到的Flash内容。我在实际项目中更多是直接操作寄存器或者用LL库来写因为HAL库的DMA回调机制在某些场景下比如高频率中断会有性能瓶颈。但无论用HAL还是LL核心逻辑是一样的SPI的DMA传输本质上是“时钟产生器”主机必须在每个位周期都提供时钟从机才有机会输出数据。这也是为什么读操作也要发送全0xFF而不是什么也不发。2.5 STM32半双工SPI什么时候能用什么时候别用STM32的SPI外设支持半双工模式也就是只使用一根数据线通过配置方向位选择发送还是接收。这种模式在引脚紧张时可以省一根线但代价是通信变成半双工收发不能同时进行。我的建议是协议本身就支持半双工的芯片比如部分LCD驱动、部分传感器可以用如果从机是全双工协议一定不要用半双工模式去驱动因为从机会同时往MISO上发送数据但主机根本没有MISO引脚连接数据没法接收。还有在半双工模式下由于只需要一根数据线MOSI和MISO引脚实际上是复用的如果从机还在等待主机发数据主机又切到接收时序上会产生尴尬的中间态。实战中除非引脚确实极度紧张否则我不推荐半双工模式。2.6 SPI Flash加载固件的场景边界与风险控制热搜词里有个“rtl9071cp-vb 使用spi加载固件”。这指的是从SPI NOR Flash启动的常见设计——主控上电后通过SPI接口从外接Flash读取固件并搬移到内存里执行。这个机制在路由器、交换机、网卡等设备上非常常见。这个场景和普通SPI通信最大的区别是启动阶段主控自身软件还没跑起来SPI控制器的初始化可能依赖BootROM代码完成所以硬件上对SPI Flash连接的稳定性、上电时序要求很高。常见的坑包括Flash芯片的WP写保护引脚没有正确上拉导致固件写入失败HOLD引脚悬空导致传输时数据被卡死SPI时钟线走线过长高速启动时信号反射严重。针对这些我一般会在硬件设计阶段就做三层检查上电时序是否满足Flash芯片要求、SPI信号线阻抗是否匹配串联33Ω电阻、WP和HOLD引脚是否通过10kΩ电阻上拉到VCC。另外如果要在应用运行中通过SPI升级固件必须注意Flash的擦写时间典型的是几十毫秒到几百毫秒这段时间内主控不能断电。否则Flash数据会写到一半丢数据轻则升级失败重则把Bootloader也冲掉板子变砖。所以设计固件升级逻辑时一定要在升级前写入一个“升级标志”升级完成后清除重启时检测到这个标志就等Flash操作完成后才继续启动流程。3. I2C与SPI的对比与选型决策很多刚入行的朋友经常纠结“同一个外设芯片既有I2C版本又有SPI版本到底选哪个”。这个问题没有标准答案但可以从几个维度来判断。对比维度I2CSPI信号线数量2根SCLSDA4根SCLKMOSIMISOCS每加一个设备多一根CS通信速率标准100kHz快速400kHz高速3.4MHz常用1MHz~几十MHz可到百MHz硬件复杂度需要上拉电阻但设备连接简单无需上拉实际上拉/下拉用于空闲电平稳定但片选管理复杂多设备支持通过地址区分一条总线可挂多个设备通过独立的CS区分每个设备需要一根CS线全双工半双工全双工应答机制有ACK/NACK无应答主机需自行判断数据有效性典型应用传感器读取、EEPROM、RTC、PMIC配置寄存器Flash、SD卡、LCD屏、高速ADC/DAC、射频收发器选择逻辑我的经验是低速传感器、寄存器配置、需要省引脚、多设备共享总线优先I2C高速数据传输、大容量存储、全双工要求优先SPI。如果两者都能满足那就看团队熟悉度和现有代码库。比如你手头已经有一堆封装好的I2C传感器驱动那新传感器也选I2C开发效率高很多。还有一个小知识点PMBus和I2C的关系。PMBus实际上是在I2C物理层之上定义了一套完整的电源管理命令集专门用于电源模块DC-DC、VRM等的配置和监控。它复用了I2C的物理电气特性和时序但地址分配、命令格式、状态检测都是独立的规范。所以调试PMBus时你用的工具和I2C一样但协议解析要按照PMBus手册来。我之前调过一个数字电源模块用I2C命令直接读寄存器没问题但要用PMBus的标准命令比如READ_VIN才能拿到正确的电压值因为寄存器映射和命令解析都是PMBus标准的。4. 实战排障I2C和SPI通信问题排查清单与技巧4.1 排查工具的准备没有工具的调试就是盲人摸象。I2C/SPI调试我建议至少准备逻辑分析仪8通道以上采样率越高越好20MHz以上就够用这是性价比最高的工具能直观看到时序。示波器双通道以上100MHz带宽以上主要用于看模拟波形质量比如上升沿是否过缓、振铃是否严重。万用表检查电压、电阻、短路/断路。实际调试时我习惯先用逻辑分析仪抓时序判断通信是否建立再用示波器看信号质量。不要一上来就拿着示波器去量波形因为是触发的问题量半天可能什么都抓不到。4.2 常见问题速查表现象可能原因排查方法I2C设备完全不响应SDA一直为高上拉电阻缺失/阻值过大设备地址错误从机未上电量SDA/SCL电压确认空闲电平为高用I2C扫描程序枚举设备地址I2C通信不稳定时好时坏上拉电阻过小导致灌电流总线过长导致信号反射挂载设备过多用示波器看波形边沿降低通信速率降到10kHz试一下改用更合适的电阻值I2C读写EEPROM数据错乱时钟速率超过器件规格应答时序不对电压域不匹配对照数据手册检查时序示波器抓第9个时钟周期的ACK位SPI设备初始化正常但读回数据全0xFF片选没有正确拉低MISO引脚配置错误从机时钟模式不匹配检查CS逻辑示波器抓CS和MISO波形确认SPI模式SPI通信正常但DMA读数据错位DMA缓冲区没有对齐请求长度配置错误字节顺序错误检查DMA缓冲区地址在回调中检查接收长度确认MSB/LSB设置SPI Flash擦写后读回数据不符写保护引脚未处理Flash忙状态未等待检查WP引脚电平在编程后等待状态寄存器里的忙标志清除4.3 避坑经验I2C上拉电阻小了不通信SPI通信不生效上拉电阻小了不通信这个现象听起来反直觉但我还真遇到过几次。原因在于阻值太小会导致总线拉低时的灌电流过大超过从机引脚的IOL额定值从机引脚驱动能力不足无法将SDA拉到可靠的低电平。等效看就是低电平无法达到逻辑阈值。有个典型案例一块板子上I2C上拉用了330Ω3.3V系统总线挂了一颗EEPROM和一颗RTCEEPROM写操作总是失败。量波形发现SDA低电平是0.8V左右超过芯片要求的VIL最大值0.1×VDD≈0.33V。换成2.2kΩ后低电平降到0.2V以下问题解决。所以I2C上拉不是越小越好要在上升沿和低电平之间找平衡。SPI通信不生效最常见的因素有两个一是片选逻辑反了有人把低有效CS写成了高有效二是MISO/MOSI接反或者SCLK接错。画板子时如果SPI信号线走了排针或者连接器特别容易把MOSI和MISO弄混。排查办法万用表测通断把主控和从机的引脚关系对一遍示波器抓CS拉低期间的MISO波形如果一直为低或一直为高大概率是MISO接错了。4.4 逻辑分析仪看时序的实操技巧用逻辑分析仪抓I2C时采样率至少设为通信速率10倍以上100kHz的I2C建议用2MHz采样率SPI的话20MHz以上比较稳妥。触发条件可以设置为起始条件I2C或片选下降沿SPI这样抓到的数据正好是通信开始的部分。拿到波形后先看空闲电平I2C空闲时SDA/SCL应该都是高SPI空闲时SCLK电平由CPOL决定CS必须是高。然后看时序I2C里找START条件、地址字节、ACK位确认从机是否应答SPI里数一下SCLK边沿和数据变化时刻确认MSB顺序是否正确。对照数据手册一步一步分析很快就能定位是主机、从机还是接线的问题。5. 从协议到系统选择背后的工程思维聊了这么多I2C和SPI的具体技术点最后想说的是协议选择本质上是一个系统工程问题不单纯是一个“哪个更快”的选择题。做产品方案设计时我一般会先列一张需求清单需要挂多少个外设每个外设的数据量多大、刷新率多少引脚资源是否充足系统的工作电压域是统一的还是要做电平转换这些约束条件清楚了才能确定用I2C还是SPI或者是I2CSPI混合。举个例子我之前设计过一块环境监测主板上面挂了一颗温湿度传感器、一颗气压传感器、一颗光照传感器、一颗PM2.5激光粉尘传感器、一颗256Mbit Flash、一块LCD屏。方案是三个慢速传感器全部走I2C共用一个总线上拉电阻4.7kΩFlash和LCD走SPI共用一个SCLK/MOSI/MISO但CS各自独立软件控制。这样引脚总数最低传感器读取方便高速数据不受干扰。这个方案跑了一整年稳得很。还有一点是标准化思维。I2C和SPI本身就是标准协议芯片之间兼容性很好但你如果在其上定义私有协议最好遵循一些约定比如命令头统一为固定字节加校验CRC8或CRC16从机状态上报要有明确的忙/空闲标志。做工业级产品时这些约定能极大减少后续联调的工作量。6. 实操心得调试I2C/SPI的几条铁律最后再分享几条我这些年在项目里总结出来的调试经验希望能帮你少走弯路。第一代码没问题时先查硬件。程序无法通信时先用万用表量电压I2C的SDA/SCL空闲电平是否正常、从机的VCC是否到位、GND是否共地。很多“通信诡异”的问题最后都是供电或共地问题。特别是两块板子用长线连接时不共地会导致通信完全瘫痪这个坑我踩过三次。第二遇到时序问题先把速率降到最低。I2C降到10kHzSPI降到100kHz看能不能恢复正常。能恢复说明是信号完整性或器件速度极限的问题不能恢复说明是配置错、接线错、或器件本身坏了。这个二分法排查从低速开始能省很多时间。第三把I2C和SPI调试助手练得滚瓜烂熟。无论是逻辑分析仪还是示波器都要熟练掌握触发、解码、滤波这些功能。很多人买了逻辑分析仪但只会点“Start”遇到问题抓不到关键波形等于白买。第四也是最重要的一条从机手册永远是你的第一参考。每个器件的地址、指令集、时序要求都可能存在细微差别网上不少文章是基于某个特定芯片写的不一定都适用于你的芯片。花十分钟认真读一遍手册比在论坛上逛两小时有效得多。I2C和SPI这两个协议说到底是嵌入式工程师的必修基本功。它们的原理简单但要把它们用得稳、用得灵活还是需要在真实项目里反复打磨。希望这篇总结能帮你在调试的路上少踩几个坑。如果后面有机会我再把UART、CAN、USB这些通信协议也整理出来凑成一套完整的板级通信协议笔记那时候再跟各位深入聊。