☰
SPI与I2C协议详解:从原理到时序调试
2026/10/23 1:21:57 网站建设 项目流程

在很多嵌入式项目里,最让人头疼的不是代码逻辑,而是你明明配置好了寄存器,数据却读不出来,或者波形乱到看不懂。我从一个实际需求出发:当时手里有一块加速度传感器,SPI接口,我一个字节发过去,读回来的全是0xFF。后来换了I2C版本的传感器,又出现SDA被拉死的现象。折腾了一周,才把“片内总线”和“片间通信”这两层概念真正理清楚。今天这篇就想把SPI和I2C这两个协议从头到尾讲透,包括它们的设计思路、时序细节、寄存器配置,以及我实测踩过的坑。不管你是刚接触单片机的学生,还是做驱动开发的工程师,只要需要让芯片和外部设备对话,这篇文章都能帮上忙。


1. 从片内到片间的第一道门:为什么需要SPI和I2C

1.1 片内总线与片间接口的分工

一个嵌入式系统里,CPU核心不是孤岛。它要访问内存、控制外设、读写寄存器。这些动作发生在芯片内部,走的是片内总线。比如ARM核里常见的高级高性能总线(AHB)和高级外设总线(APB),它们的特点是线多、并行、速度快、时序严格。片内总线可以做到128位数据同时传输,频率动辄几百MHz,但代价是物理走线非常密集,功耗也高。

然而,芯片外部面对的是传感器、Flash、触摸屏控制器这类独立器件。它们和主控芯片之间隔着PCB走线,不可能把几十根并行线都拉出来。所以业界定义了几种串行通信协议,用少数几根信号线把数据一位一位传过去。SPI和I2C就是其中最经典的两种。它们承担的角色是“片间通信”,也就是芯片与芯片之间的数据交换。

这两个协议的设计哲学完全不同:SPI是“高速搬运工”,四个引脚,速度可以跑到几十Mbps,数据流方向明确,但设备一多就要浪费片选引脚;I2C是“省线管家”,只靠两根线就能挂上百个设备,速度不高,但协议本身自带寻址和应答机制,非常优雅。理解了这个分工,你再看项目选型就不会拍脑袋。

1.2 选型第一课:你的外设到底需要什么

我见过不少人在刚开始选通信接口时只盯着“哪个快”,结果为了一个温湿度传感器上了SPI,把一堆引脚全占用了,后面接屏幕都没地方用。其实判断用哪个协议,抓住三点就够了:

选型维度SPII2C
引脚数量至少4根(MOSI、MISO、SCLK、CS),每多一个设备多一根CS只需要2根(SDA、SCL),所有设备共用
传输速率通常10Mbps~100Mbps,甚至更高标准模式100kbps,快速模式400kbps,Fast Mode Plus可达1Mbps甚至5Mbps
设备数量通过CS片选,设备多时引脚爆炸通过地址区分设备,一个总线最多挂数十个,但受电容限制
硬件复杂度全双工,主从简单,没地址概念开漏+上拉,协议复杂,有地址、ACK、仲裁
适用场景高吞吐:Flash、ADC、屏幕、无线模块低功耗、低速、多设备:传感器、EEPROM、RTC、电源管理

你可能会说,I2C速率那么低,为什么还那么流行?因为大部分传感器对数据率的要求真的不高。一颗温度传感器每秒发一次数据,1kbps都嫌快。I2C省下的引脚和PCB面积,在可穿戴设备和小型模块里太香了。反过来,如果你要驱动一颗2Mbps的ADC连续采样,或者给Flash烧固件,I2C就是给自己找麻烦,这时候SPI才是正解。

注意:选协议从来不是“哪个先进就选哪个”,而是把数据量、引脚预算、功耗、PCB约束全部列出来,再做权衡。

2. SPI:四条线打天下的高速同步串行总线

2.1 SPI的四条线:MOSI、MISO、SCLK、CS

SPI的全称是Serial Peripheral Interface,串行外设接口。它在物理上由四根信号线组成。

SCLK是串行时钟,由主机发出,决定了什么时候采样数据。MOSI是Master Out Slave In,主机输出从机输入,数据从主机流向从机。MISO是Master In Slave Out,从机输出主机输入,数据从从机流向主机。CS是片选信号,也是主机控制,通常低电平有效。当CS拉低时,该从机被选中,参与通信;CS拉高时,从机忽略SCLK和MOSI,保持高阻态。

SPI是同步协议,意味着收发两边共用同一个时钟。每一个时钟周期,MOSI和MISO上同时更新一位数据。这就是全双工:主机发数据的同时,从机也在发数据。所以SPI的收发其实是一对对称操作。你发送一个0x55,同时也会收到一个字节,这个字节可能是从机在发状态寄存器,也可能是无意义的填充数据。意识到这一点,对调试SPI非常重要,后面我会再讲。

2.2 时钟极性CPOL和相位CPHA:新手最容易栽的坑

SPI虽然没有像I2C那么复杂的协议帧,但它的可配置项反而让很多人一头雾水。每个SPI设备手册里都会给一个“SPI Mode”,比如Mode 0、Mode 1、Mode 2、Mode 3。这四个模式由两个参数确定,它们就是CPOL和CPHA。

CPOL决定空闲时SCLK是高电平还是低电平。CPOL=0,空闲时SCLK拉低;CPOL=1,空闲时SCLK拉高。

CPHA决定数据在哪个边沿被采样。CPHA=0,在第一个跳变沿(前沿)采样;CPHA=1,在第二个跳变沿(后沿)采样。

组合起来就是:

模式CPOLCPHA空闲时钟电平采样边沿
Mode 000低上升沿
Mode 101低下降沿
Mode 210高下降沿
Mode 311高上升沿

绝大多数SPI Flash、SD卡、传感器都支持Mode 0,所以默认配置一般没问题。但有些器件偏偏只支持Mode 3,比如某些LCD控制器。你不小心把主控配成Mode 0,波形看起来也跳动了,但数据就是错的。我在调试一块OLED屏时遇到过:屏幕初始化能成功,但显示花屏。后来用逻辑分析仪抓取发现,主机在SCLK上升沿采样,而屏是在下降沿送数据。把SPI模式改成Mode 3后,花屏立刻消失。

怎么确定器件到底用哪个模式?结论写在数据手册里。绝大多数情况下,数据手册的时序图会标明“SCLK空闲为高”还是“数据在什么边沿采样”。如果手册写得模糊,你还可以用示波器或逻辑分析仪抓一次标准读写操作,对照SCLK和MOSI的跳变关系来反推。

配置SPI时,还有一个与模式无关但容易忽略的细节:数据位顺序。大部分器件默认MSB优先,但有的EEPROM要求LSB优先。这个顺序错了,读回来的字节就是镜像,比如0x01变成0x80。寄存器里通常有LFS/DF位来切换,务必下手前看清楚。

2.3 SPI常见形态:标准四线、双线、Quad与外围电接口

标准SPI是四线制:MOSI(或叫DI、SDI),MISO(或叫DO、SDO),SCLK,CS。但当存储设备需要更高吞吐时,四线就不够了。很多SPI Flash支持Dual SPI和Quad SPI。

Dual SPI把标准四线中的MOSI和MISO改造成双向IO。写指令时沿用标准四线,一旦进入数据传输阶段,两根IO线同时发送或接收数据,吞吐翻倍。Quad SPI更进一步,把四根IO线都用于数据,一个时钟周期可以传4位。这种模式在串行Flash里很常见,比如某些Flash读操作会先用标准模式发送0xEB命令,再切换到Quad模式。

这些非标准形态虽然好用,但它只能在特定外设和配套主控支持时生效。如果你用软件模拟SPI,Dual/Quad几乎无法高效实现,因为它们要求在单个时钟周期内同时读写多条IO线。所以在选择主控时,如果项目里有大容量Flash,尽量选硬件支持QSPI的型号。

还有一类是SPI的一些变种,比如DSPI(带延时线的SPI)、DESS。这里不展开,但你可以记住:SPI协议的“灵活”是一把双刃剑,没有统一标准,所以每次拿到新器件,都要回归数据手册逐引脚核对。

2.4 实操:用逻辑分析仪抓SPI并读取一次Flash操作

工具方面,我强烈建议准备一台便宜的逻辑分析仪,最好至少8通道、100MHz采样率,能解码SPI/I2C/CAN等协议。当然示波器也可以,但示波器做协议解码不够直观,逻辑分析仪自带解码器,能直接帮你看懂波形。

假设我们要通过SPI读取一颗SPI Flash的状态寄存器。标准操作是先CS拉低,然后发送命令0x05,再读一个字节。用逻辑分析仪抓取时,你会看到5个信号:CS、SCLK、MOSI、MISO,可能还有GND参考线。协议解码器会自动标出命令字节和数据字节。

但实际调试中,我最常遇到的是“抓到了波形但解析长度不对”的问题。原因通常是:读取操作中主机要在发送命令之后继续输出SCLK,用来给从机一个回传数据的时钟窗口。这个SCLK不能停。如果代码里你写的是“发送完成就释放时钟”,从机根本没法把数据送到MISO上。正确做法是:命令发送完后,再发送N个时钟(N是你要读的字节数乘以8),同时把发送数据寄存器写0x00或0xFF,从而产生空时钟。

具体到代码模拟,伪代码是这样:

cs_low(); spi_write_byte(0x05); // 命令 for (int i = 0; i < 1; i++) { data[i] = spi_read_write(0x00); // 发送dummy时钟,接收MISO } cs_high();

如果你用的是硬件SPI外设,一般不会遇到这个问题,因为硬件会自动处理时钟连续输出。但在GPIO模拟时,新手特别容易漏掉dummy时钟。这个坑我栽过不止一次,写出来希望你少走弯路。

3. I2C:两根线解决多设备通信的慢速王

3.1 开漏+上拉:为什么I2C需要上拉电阻

I2C和SPI最大的硬件区别是:I2C的SDA和SCL都是开漏(Open-Drain)结构的,不能主动输出高电平,只能把线拉低或释放。当设备释放引脚时,引脚处于高阻态,此时高电平依靠外部上拉电阻提供。

这个设计特别巧妙。开漏结构让多个设备可以把同一条线拉低,实现“线与”逻辑——只要任何一个设备输出低电平,总线就是低电平;只有所有设备都释放,总线才恢复高电平。正因为如此,I2C天然支持多主机和多从机,同时避免了设备和设备之间直接推挽输出造成的冲突。如果两个设备的IO口都配置成推挽输出,一个想发高,一个想发低,就会形成大短路电流,烧毁引脚也不是没可能。

上拉电阻的取值直接影响I2C的时序。常见误区是随便选个4.7kΩ完事。电阻太小,灌电流大,低电平可能拉不到0.4V以下;电阻太大,上升沿时间太长,协议时序就崩了。一般计算方法是看总线电容。总线电容包括器件引脚电容、PCB走线电容、过孔电容。上升时间约等于0.8473乘以上拉电阻和总线电容的乘积。Fast Mode要求上升时间不超过300ns,标准模式不超过1000ns。工程上常用经验值:1.8V系统用1kΩ~2kΩ,3.3V系统用2.2kΩ~4.7kΩ,5V系统用4.7kΩ~10kΩ。总线设备多、走线长时选小一点,追求低功耗时选大一点。

提示:如果你发现I2C波形上升边很“圆滑”,SCL已经高不上去,多半是上拉电阻太大或总线电容超标。把上拉电阻换小一档试一下,很多疑难杂症就消失了。

3.2 地址、ACK和读写位:I2C协议的基本帧

I2C协议比SPI“聪明”的地方在于,它有一套完整的数据链路层协议。每次通信以起始条件开始:SCL保持高电平期间,SDA由高跳变到低。接着发送第一个字节的7位从机地址和1位读写标志。最低位是0表示主机要写从机,是1表示主机要从从机读。从机地址正确的话,从机会在第9个时钟周期把SDA拉低,表示应答(ACK)。

之后,主机发送数据字节。每发送一个字节,从机都要回一个ACK。如果是主机读取,那么从机往SDA上发送数据,主机收到后要回应ACK继续读下一字节,或者回应NACK(SDA保持高)来停止读取。最后以停止条件结束:SCL高电平时,SDA由低跳变到高。

这一堆规则看着复杂,但它们在硬件I2C外设里都有对应寄存器。只要进入I2C模式,起始/停止条件由硬件自动生成,你只需要填充地址和数据寄存器。用GPIO软件模拟时,你则需要按位精确控制延时和跳变,稍有不慎就会卡死在某个等待ACK的状态。

务必留意ACK行为。当主机读取时要记得主机也需要在最后一个字节发NACK,否则从机不知道主机不想再要数据了。有些新手在写驱动读传感器时,每次都发ACK,导致从机不断发送,数据错位,寄存器读出来的全是垃圾。这在代码里通常是一个“接收最后一字节前配置不发送ACK”的位。

3.3 速率与多主机:从Standard-mode到Fast-mode Plus

I2C的速率等级分为标准模式100kbps、快速模式400kbps、快速模式+1Mbps,还有高速模式3.4Mbps。低速是I2C最被吐槽的点,但在很多场合这根本不是问题。高温传感器每秒才更新几Hz,控制寄存器偶尔写一次,100kbps绰绰有余。反而是因为速率低,信号完整性要求低,你能用飞线连接传感器,调试起来非常方便。

多主机是I2C的进阶话题。多个主机挂在同一总线上时,它们有可能同时发起通信。I2C通过仲裁来解决:当一个主机在发送高位时发现总线上已被另一个主机拉低,它会自动停止发送,把总线让给对方。仲裁发生在每一位上,从地址开始。因为所有主机都遵循“先显性后隐性”的原则,优先级高的低地址主机最终获胜。硬件I2C外设通常会自动处理仲裁,但软件模拟就需要不断读取引脚电平来检测冲突,复杂度高很多。

我在项目中很少用多主机,但如果你做的是电池管理模组,主控和充电管理芯片之间彼此都要读对方数据,就可能需要多主机设计。这时候强烈建议用硬件I2C,别自己模拟。

3.4 实操:用I2C读取温度传感器并解析数据

以一个常见的I2C温度传感器为例,读取温度值的标准流程是:初始化I2C外设,配置引脚为推挽加开漏模式?不对,I2C引脚本身要支持开漏功能,一般MCU的I2C引脚配置为复用开漏输出,并外接上拉电阻。

然后发送起始条件,发送从机地址+写位,等待ACK,然后发送寄存器地址(假设温度寄存器是0x00),再发一个重复起始条件,发送从机地址+读位,读取两个字节,主机对第一字节回ACK,对第二字节回NACK,最后发送停止条件。

合并后的寄存器值通常是一个16位有符号数。具体位宽和符号表示要看手册。常见格式是高位有效符号位,低若干位为小数位。比如一个12位精度,左对齐到16位的传感器,原始值右移4位才是真正的12位温度代码,再乘以分辨率(如0.0625°C)就是温度值。如果不做符号扩展,负温度会显示成几百度的正数。

这种细节,软件文档一般不替你做,必须拿着数据手册的寄存器映射表对照着写。我每次拿到新传感器,第一件事就是把“Data Format”页截图打印出来,再写解析代码,能省很多返工时间。

4. 片间通信的地基:波形、上下拉与电平

4.1 电平标准与电压域

SPI和I2C都是数字电平通信,但遇到混压系统就麻烦了。很多传感器是3.3V或1.8V供电,而你的主控可能是5V,如果不做电平转换,轻则通讯异常,重则烧毁芯片。

I2C因为是开漏结构,转换相对好做。你只需要把两端的上拉电阻分别接到各自电源上,开漏结构天然实现电平转换。前提是两边IO口都支持开漏,且耐压足够。如果从机是1.8V,主机是3.3V,SDA和SCL上拉分别接各自电源,开漏器件各自拉低,高电平由对侧电源决定,自然完成了电平适配。

SPI的问题更大:SCLK、MOSI、CS这些信号是推挽输出,无法靠开漏自动转换。常规方案是使用双向电平转换芯片或者分立MOS开关管。但在低速率、线短、方向固定的场景下,有时可以串电阻分压,不过我不推荐。老老实实买电平转换模块,或者选同时支持3.3V和5V的主控。

4.2 布线长度与速率的关系

协议调试到最后,你会发现问题往往出在物理层。SPI在几十厘米长的杜邦线上能跑到1Mbps,但你要跑50Mbps就必须缩短走线、控制阻抗。I2C虽然速度慢,但因为是开漏,上升沿完全靠电阻充电,线越长、电容越大,上升沿越慢。所以总线长度和挂载设备数量有直接关系。总线电容超过规格上限(一般400pF)后,必须降速或增加上拉驱动能力。

用逻辑分析仪抓波形时,除了看时序,还要关注边沿是否“肥”。一个标准的方波,上升沿应该陡峭。如果你看到上升沿像斜坡一样缓慢爬升,第一反应就是电容过大或上拉电阻过大。尤其注意飞线I2C时,把SDA和SCL分别绞合在地线上,减小环路,会明显改善波形。

5. 调试实录:SPI与I2C常见问题排查技巧

5.1 常见问题速查表

我用表格把项目里真实遇到过的典型问题整理出来,不管你是新手还是老手,都可以当速查手册。

现象可能原因排查方法
SPI读数全为0xFF从机未选中、CS时序不对、供电异常先量CS是否拉低,再量SCLK和电源
SPI读数全为0x00MOSI和MISO接反对照原理图检查信号连接
SPI数据错位CPOL/CPHA配置错误或字节序不对判断器件属于Mode 0~3,检查MSB/LSB
I2C执行卡死在ACK等待SCL状态不对,设备未工作,地址错误用逻辑分析仪抓起始条件,确认设备地址
I2C SDA一直低有设备把总线拉死逐个断开设备,检查从机状态机
I2C SCL没有上升沿上拉电阻虚焊,或漏接了上拉量SCL高电平是否接近电源电压
设备偶尔无响应总线电容太大、上拉太弱、电源不稳降低速率、减小上拉电阻、加稳压电容
SPI能读但CRC错时序余量不足降时钟频率或优化走线

5.2 一个真实案例:I2C总线卡死的过程与恢复

有一次我在调试一个电池管理芯片,刚上电时读寄存器还正常,但连续读了几十次后,软件就卡在等待SCL释放的循环里。用逻辑分析仪一抓,发现SDA被拉低后一直没释放,SCL倒是正常的。我首先怀疑是主控软件死锁:假设主控发送起始条件后,从机没有应答,而主控代码又等待ACK超时,但部分硬件I2C在超时前不会主动复位总线,于是SDA保持低电平。问题出在我读取时没有正确配置“不发送ACK”导致从机永远在发数据;也有可能是上电过程中总线状态错乱。

当时排查步骤:先断开外部传感器,让它自己悬空,再检查SDA引脚电平。确认SDA还低,说明不是从机拽住的,而是主控自己拉低了。反复复位不能解决。最后解决办法是把I2C外设软件复位,并重新初始化引脚。在那之后,我每次初始化I2C前都会将SCL和SDA拉高至少10us,确保总线处于空闲态。这个操作在硬件初始化顺序里并不显眼,但它能避免很多“死锁”问题。后来我把所有软件模拟I2C的代码都加了类似的总线复位逻辑:连续切换SCL九次以上,让可能处于困境的从机恢复正常。

5.3 一个真实案例:SPI读取始终全FF

另一个案例是访问一颗SPI接口的ADC,读回数据全是0xFF。我当时第一反应是CS没拉低,于是用万用表量CS引脚,发现它在传输过程中确实变了,但变化幅度很小,只有0.7V左右。继续查,原来CS引脚复用成了普通GPIO,但GPIO模式配成了模拟输入,导致输出驱动力不足。改成推挽输出后,CS拉到0V,数据立刻正常。

这个案例想说明什么?SPI看起来简单,但引脚复用配置不对,带来的现象和走线干扰几乎一样。如果你的SPI数据全是FF,别急着怀疑代码,先回头检查引脚功能配置。特别是不少厂商的库函数会自动把IO口配置好,但如果你禁止了复用,硬件外设无法访问引脚,自然失败。

调试过程中,我习惯把主控的频率尽量调低来做基础验证。比如SPI时钟从20Mbps降到1Mbps,I2C从400k降到100k。如果低速正常而高速失败,大概率是时序余量或信号完整性问题。如果高速低速都失败,那就是配置、连接或电气问题。

6. 最后再分享一个实用的经验

我个人的体会是,学SPI和I2C最好的方式不是背协议,而是拿一个逻辑分析仪去抓真实的波形。刚开始你可能觉得那些跳变很乱,但只要你把SCLK或SCL当作时间轴,一个周期一个周期地对照协议时序图,很快就能建立直观感觉。以后即便遇到没见过的芯片,看着时序图也能够写出驱动。

如果让我给你一个工作习惯上的建议,我会说:不要直接复制别人的驱动代码。先从数据手册里把通信协议帧画出来,再写自己的读写函数,至少把起始条件、地址、数据、应答每一个环节都用代码注释标明。这样排查问题时,你能一个字节一个字节地对照波形,而不是对着一个“调不通”的驱动瞎猜。SPI和I2C用好了,你基本能接这个地球上绝大多数传感器和存储芯片,这是嵌入式工程师性价比最高的一笔投资。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询