☰
SPI与I2C协议详解:片间串行通信的取舍与调试实战
2026/10/23 3:43:46 网站建设 项目流程

芯片内部的互联和芯片之间的互联,是两个世界。片内信号走的是几百微米的金属线,片间信号走的是几厘米的 PCB 铜线;片内你可以随时拉一组 32 位并行总线,片间你往往只剩几个引脚可用。SPI 和 I2C 就是专为“片间”这种场景设计的两个经典串行协议,也是这个系列从处理器内部走向真实外设的第一步。如果你跟我一样正在自己折腾 CPU 核、FPGA 原型或者单片机外设,这篇把两个协议的脾气、取舍和调试经验讲清楚,能省下不少弯路。

其实“从片内到片间”这个跨度,难点从来不在协议本身的复杂度——SPI 只有四根线,I2C 只有两根线,它们的吞吐连片内总线的零头都比不上。真正的难点在于思维切换:片内你可以假设全局时钟、无限制数据位宽、微米级走线;片外你要面对引脚成本、信号完整性、电平域差异、多设备抢占。下面我不打算照着数据手册念,尽量把自己调通这两个协议的过程、踩过的坑、以及一些“当年没人告诉我”的细节摆出来。

1. 为什么芯片之间不能直接拉一组并行总线

1.1 片内总线的“特权”从哪来

如果你跟着这个系列看到了这里,应该已经熟悉片上总线的玩法:32 位甚至 64 位数据线并排铺开,配上一堆地址线和控制线,一个周期就能搬一个完整的数据字。片内总线敢这么干,是因为它在硅片内部有天然红利。一根线在 die 上就是一长条金属,拉多远都不涉及“引出引脚”,线间距可以做到微米级,多布几十根线完全不心疼。时钟更是如此,同一片 die 里走线长度被工艺严格约束,全局时钟树可以把偏移压在几十皮秒以内,所有寄存器都能在一个统一的节奏里锁存数据。

这也是为什么 CPU 内部的寄存器堆、ALU、Cache 接口全是宽并行的——带宽是片内设计的第一追求,引脚数量和走线面积相对不值钱。你极少会看到有人为了让 CPU 核少用几根线,而去把内部数据通路改成串行,那纯粹是没事找事。

1.2 出了 die 之后,世界立刻变糙

可一旦信号穿过封装的引脚来到 PCB 上,之前那些“不值钱”的东西全变得昂贵起来。引脚是物理资产,一颗芯片无论封装多豪华,引脚数都是有限且成本敏感的。你不可能为了外接一个外设就占掉 40 个引脚,那这颗芯片的其他功能基本就废了。我见过有人用一颗一百多个引脚的主控去接 8 位并口屏,结果整版看起来像蜘蛛网,其余传感器全挤在剩下几个 GPIO 上,回头自己调试都想骂人。

比引脚成本更要命的是信号完整性。PCB 走线不再是微米级短金属条,而是几厘米以上的铜线,线宽线距都在毫米级,过孔、插排、连接器处处都是阻抗不连续点。把 8 根并行数据线同时翻转,地弹、串扰、偏斜一起上,传输距离稍微一长,数据窗口就被糟蹋得没法看。我早年用并行方式接过一个外部存储器,20MHz 一下稳定,一超就随机丢数据,后来发现是两根相邻线间的串扰在特定数据组合下把电平整个带翻。这类问题在串行协议里也会遇到,但串行数据线数量少、速率可控,处理起来便宜得多。

1.3 串行化的代价与收益

所以片间通信的共识就是:牺牲带宽,换取引脚数和信号完整性。实现手法也很朴素——并行转串行、串行转并行,本质上是两个移位寄存器。数据位宽从 32 降成 1,引脚数从几十降到几条,代价是传同样一帧数据要多花 N 倍的时间。但对大多数外设来说这个代价完全可接受:读个温度传感器的结果是 16 位,放着几 Mbps 不用,一秒钟照样能刷几百次,完全没有瓶颈。

SPI 和 I2C 就是在“低速但够用”的甜点区里长出来的两个经典方案。一个走四根线、全双工、协议薄如纸;一个走两根线、半双工、协议精致到带仲裁和时钟拉伸。它们的性格差异,恰好对应着两类不同的工程偏好。下面我把它们拆开讲。

2. SPI:四根线,一个主人,全双工的搬运工

2.1 结构简单到了什么程度

SPI 的结构骨架就四根线:SCK(串行时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选,低有效)。主机负责产生所有时钟,从机不需要任何内部时钟,也不需要知道“现在轮到谁说话”,一切动作都由 SCK 的边沿驱动。

说它是“一个主人”的全双工搬运工,是因为同一时刻两个方向都在传数据:主机往 MOSI 上推一位,同时从 MISO 上采一位。这也是为什么 SPI 经常配合移位缓存器做数据交换——主机的移位寄存器往从机的移位寄存器里转,从机的旧数据反向转回来,一次时钟边沿完成一个 bit 的双向搬运。

CS 是 SPI 里最容易被低估的一根线。它承担了地址选择的工作:主机把某个从机的 CS 拉低,就相当于宣布“我现在只跟你说话”,其他从机全部靠边站。CS 必须在整个传输帧期间保持低电平,这也是 SPI 和 I2C 在多设备拓扑上最大的不同——SPI 用引脚换隔离,I2C 用地址换走线。

2.2 四种模式的判定方法,别靠背表

SPI 协议薄,薄到它甚至不规定“数据到底在哪个边沿有效”。这个锅甩给了 CPOL 和 CPHA 两个位:CPOL 决定空闲时 SCK 的电平,CPHA 决定数据采样发生在第一个边沿还是第二个边沿。两个位组合出 0 到 3 号四种模式。

别去背“模式 0 是上升沿采样、模式 1 是下降沿采样”这类口诀,背完必忘。我在调试时直接手推:先看 SCK 空闲电平是高是低,再找从空闲电平出发遇到的第一个边沿,CPHA=0 就在第一个边沿采样,CPHA=1 就推迟到第二个边沿。比如最常见的模式 0,SCK 空闲为低,第一个边沿是上升沿,所以上升沿采样、下降沿跳变。

实际项目里,大多数 Flash、SD 卡、温湿度传感器默认都是模式 0 或模式 3,但千万不要拿“默认”当“一定”。我有一次接一颗 ADC,数据手册画得含含糊糊,我按模式 0 初始化,读回来的数值永远是一坨固定花屏。后来用逻辑分析仪把 MISO 波形拉出来才发现,器件实际工作在模式 3——CPOL 和 CPHA 正好和我的配置差半个相位。从那以后我的原则变成:先按模式 0 跑通链路,再用逻辑分析仪看真实波形反推器件的采样沿,凡是手册没写清楚的,一律以波形为准。

2.3 没有应答机制,意味着什么

SPI 没有 ACK。主机发完一个字节,从协议层面根本不知道从机是否成功接收、是否执行完毕。这个特性对简单传感器无所谓,但对接 Flash、SD 卡这类有内部状态机的存储设备就是个隐患。常规处理办法是:写入操作完成后,拉高再拉低 CS,然后发“读状态寄存器”命令,轮询忙位直到清空。这个流程完全靠驱动软件约定,SPI 协议本身不提供任何强制保证。

没有应答带来另一个后果:从机想“慢一点”也没有协议层面的手段。它只能悄悄把 MISO 拉低示意忙,但这只是约定,不是规范,主机完全可以无视它继续吐时钟。所以 SPI 器件数据手册里那个“最大 SCK 频率”是你唯一需要尊重的速度边界。一旦超频,从机自己都不知道自己在说什么胡话,你会得到一帧逻辑完美但数据垃圾的输出。

2.4 用 GPIO 手搓一个 SPI 主机

在连硬件 SPI 控制器都没有的环境下(比如正在用自研 CPU 核接外设),用普通 GPIO 模拟 SPI 是最快的验证手段,也最能帮助理解协议本质:

// 模式0:CPOL=0, CPHA=0, MSB first void spi_write_byte(uint8_t byte) { for (int i = 7; i >= 0; i--) { GPIO_MOSI = (byte >> i) & 1; // 先放数据 GPIO_SCK = 1; // 上升沿,从机采样 GPIO_SCK = 0; // 下降沿,从机移出下一位 } } uint8_t spi_read_byte(void) { uint8_t val = 0; for (int i = 7; i >= 0; i--) { GPIO_SCK = 1; // 上升沿,主机从 MISO 采样 val = (val << 1) | GPIO_MISO; GPIO_SCK = 0; } return val; }

软件模拟看起来简单,实际写的时候有一个高频错误:数据必须在 SCK 拉高之前放到 MOSI 上,而不是同时。把“置数据”和“拉时钟”两句的顺序写反,波形上会出现一个极窄的毛刺,从机有时采得对有时采得错,此bug极难排查。另一个常见问题是 CS 的粒度——一次完整的多字节传输(比如读 16 位 ADC 结果),CS 必须在整帧期间保持低,不能每个字节都拉一遍。我早期用现成的 SPI 库时被它“每字节翻转 CS”的默认行为坑过,读回来的 16 位数据永远只有低字节是对的,查了半天才发现是片选被当成了字节级片选。

3. I2C:两根线上的谈判桌

3.1 开漏结构和上拉电阻:为什么没有第三根线

I2C 和 SPI 最大的气质差异,在于它只有 SCL 和 SDA 两根线,而且这两根线都是开漏结构。任何设备都只能把线拉低,不能主动拉高,线上的高电平全靠外接上拉电阻提供。听起来像缺陷,但这正是多主设备共存的基石:当两个设备同时驱动一条线时,大家都拉低,线就是低;一个想拉低、另一个想拉高,拉高的一方其实只是在“放手”,不会像普通推挽输出那样互相怼烧 IO。

上拉电阻的取值绝对不是“随便焊一个”。阻值太大,总线电容充放电慢,上升沿变得圆头圆脑,高速模式下直接超时;阻值太小,灌电流超限,设备可能拉不动或发热。工程经验值:标准模式 100kbit/s 常用 4.7kΩ,快速模式 400kbit/s 常用 2.2kΩ 或更低,最终数值还要结合总线上的设备数和走线电容。我实测过一条 20cm 排线挂了 8 个传感器,用 10kΩ 上拉,400k 下的信号上升沿完全是正弦波,通信时好时坏;换成 1kΩ 之后波形锐利得像刀切,问题立刻消失。

3.2 从 START 到 STOP:一个有格式的对话

I2C 的帧格式比 SPI 丰满得多。所有通信从主设备发出 START 条件开始:SCL 为高时,SDA 由高变低。结束时发 STOP 条件:SCL 为高时,SDA 由低变高。这两个边沿条件在波形上非常明显,也是逻辑分析仪解码 I2C 的锚点。

中间的数据按字节组织,每字节 8 位,高位在前,第 9 个时钟是 ACK 位。接收方在第 9 个时钟正常应答,就把 SDA 拉低一个周期;不应答(NACK)就保持高电平,通常意味着地址没匹配、设备忙或者主机通知从机“读够了”。地址方面,第一字节的高 7 位是从机地址,最低位是读写标志。7 位地址能挂的设备有限,规范又定义了 10 位扩展,用特殊的 11110 前缀标记。实践中绝大多数从机都是 7 位地址,而且很多芯片把地址的低几位做成硬件引脚,靠外部接高低电平区分同一型号的多颗设备,这个设计在工程上非常实用。

3.3 时钟拉伸:从机说“等等”的最朴素方式

I2C 给从机留了一个正式的“慢下来”机制:时钟拉伸。从机发现自己忙不过来,可以在某个时钟周期把 SCL 拉低不放。主机有义务在发送下一拍之前检测 SCL,发现被拉低了就必须暂停,直到从机松开 SCL 为止。

这个机制在软件模拟 I2C 主机时特别容易写错。很多人把 SCL 当成纯输出,发完 9 个时钟就完事,从不回读 SCL 的电平。遇到支持时钟拉伸的从机,数据就开始错位——从机拖慢了一拍,你这边却没等它。正确做法是:在发出每个高电平阶段时把 SCL 释放为输入,或者至少回读一次引脚电平,确认它没被从机拉低,再继续下一个时钟。我在接一款触摸控制器时被这个问题折磨过一整天,它只在特定操作序列下偶尔拉伸一下时钟,不处理就会随机丢字节,一旦处理,通信立刻稳定。

3.4 软件模拟 I2C 的代码骨架

// 模拟 I2C 主机,7 位地址,标准 100k void i2c_start(void) { SDA_H(); SCL_H(); SDA_L(); // SCL 高时 SDA 拉低 = START SCL_L(); } void i2c_stop(void) { SDA_L(); SCL_H(); SDA_H(); // SCL 高时 SDA 拉高 = STOP } int i2c_write_byte(uint8_t byte) { for (int i = 7; i >= 0; i--) { if (byte & (1 << i)) SDA_H(); else SDA_L(); SCL_H(); // 从机在高电平期间采样 SCL_L(); } SDA_IN(); // 释放 SDA,准备读 ACK SCL_H(); int ack = SDA_READ(); // 0 表示从机应答 SCL_L(); SDA_OUT(); return ack ? -1 : 0; }

这段代码里最容易忽略的就是SDA_IN()那一步。不释放 SDA,你自己的输出会一直占着线,读 ACK 时读到的永远是自己的低电平,你会误以为设备没应答。我用示波器卡在这个问题上时,一度怀疑器件刚上电就烧了,最后才发现是方向没切。读数据方向则反过来,主机释放 SDA,每一个 SCL 高电平期间采样从机吐出来的位,读完最后一字节前发 NACK 表示“够了”,再发 STOP。

4. 片内与片间的本质差异:为什么不能直接沿用片上总线

4.1 时钟分发模式的改变

片内总线可以依赖全局时钟树,靠一根到位的时钟把所有寄存器的锁存动作对齐。但到了片间,你不可能给所有芯片都发一根绝对同步的全局时钟。SPI 确实这么干了——把 SCK 当作那个全局时钟,但代价是这根线必须短、负载必须小、速率必须收敛。I2C 则走了另一条路:时钟由主设备按需产生,而且允许从机通过时钟拉伸来反向干预,整个总线的时序更像一场有协商的异步会议,而不是你熟悉的同步状态机。

这也是为什么很多从数字逻辑入手的人第一次看 I2C 波形会觉得别扭:你不能只盯着时钟沿看信号,而要盯住 SDA 和 SCL 之间的边沿关系——什么时候允许变、什么时候必须稳、什么时候算条件满足。你要从“时钟沿驱动一切”切换到“边沿条件驱动状态机”。

4.2 电平与驱动能力的重新思考

片内信号的电压由工艺决定,1.2V、1.8V 随便用,反正都在同一颗 die 上。一出片,情况立刻变成:主控 3.3V、外设 5V、传感器可能 1.8V,电平域不统一是常态。SPI 和 I2C 应对这个问题的能力差异非常大。I2C 因为是开漏结构,天然可以用上拉电阻接不同电平——总线低压侧上拉到 3.3V、高压侧上拉到 5V,只要两边的灌电流都能把线拉低就行。这也是工业现场大量 5V 传感器能直接挂在 3.3V 主控的 I2C 总线上的原因。SPI 是推挽输出,电平转换更容易出错,要么加转换芯片,要么仔细核对双方输入输出的 VIH/VIL 门槛。

我的实际操作建议是:先拿万用表量清楚两边的逻辑高、低门槛,再决定要不要加转换器。很多标称“3.3V 兼容 5V”的芯片只是输入耐压 5V,输出摆幅仍按 3.3V 算,看起来能跑,但噪声容限已经踩线。我在一块板上就吃过亏,SPI Flash 的 MISO 回灌到 5V 主控时电平刚好在门槛附近,温度一高就随机读错,最后加了一级缓冲器才根治。

4.3 从“我们”到“你们”:多设备拓扑的复杂度转移

片内总线面对的从设备是固定的,地址空间由硬件设计决定,不存在“别人家的设备”。片间则完全不同,同一根 I2C 总线上可以挂着不同厂商、不同批次、不同类型的芯片,它们各自认领自己的地址。好处是走线极少,坏处是软件里要做冲突排查——两个设备地址撞车是 I2C 系统里最常见的上电即死因。我就遇到过两块同型号传感器默认地址相同,必须用它们的地址引脚拨到不同地址才能共存。

SPI 解决多设备的方式是“片选矩阵”和“菊花链”。每个从机一根独立 CS,主机想和谁说话就拉谁的 CS,设备一多引脚就多,但总线独占、互不干扰。菊花链则是把 MISO/MOSI 首尾相接,适合移位寄存器这类天然级联的器件。如果你在给 FPGA 原型或自研 CPU 加外设,我的倾向很明确:需要持续吞吐的用 SPI,只需要低频读写配置寄存器的用 I2C。这是“异构系统设计”最朴素也最实用的准则。

5. 选型不是二选一:写在硅片之外的经验

5.1 一张表看懂两种协议的适用场景

维度SPII2C
引脚数4 + N×CS(N 为从机数)2
速度典型几十 MHz,可到百 MHz100k / 400k / 1M / 3.4M bit/s
双工模式全双工半双工
地址机制无,靠 CS 选择7 位或 10 位地址
应答机制无每字节 ACK/NACK
多主机困难支持,有仲裁机制
从机主动反馈仅靠约定时钟拉伸、NACK
典型器件Flash、SD 卡、ADC、TFT 屏传感器、EEPROM、RTC、PMIC

看这张表就能自行推断出各自的忠实用户群:需要持续吞吐的(刷屏、读固件、存日志)选 SPI;只需要低频读写配置项的(温湿度、RTC、电池管理)选 I2C。还有些器件天生就是 I2C 命,比如 EEPROM 这种一字节一字节慢慢抠的存储,SPI 的带宽优势根本用不上,I2C 的 ACK 反而让每个字节的写入校验变得简单可靠。

5.2 总线速率不是越高越好,关键是时序余量

不少人理所当然地认为“能开 400k 就别开 100k”,但 I2C 总线不只是主从两个节点,还有上拉电阻、走线寄生电容、多个设备的内部电容一起参与时序。速度翻倍,SCL 高电平时间窗口减半,从机的响应裕量也被压缩。

我调过的板子里有一块很典型:SPI Flash 跑 40MHz 读 ID 正常,读大数据流时偶发 CRC 错。示波器一看,MISO 边沿在 40MHz 下出现明显振铃,数据建立时间不够,把 SCK 降到 33MHz 问题消失。这说明数据手册的“最大频率”是器件能力上限,不是链路保证值,每一段走线、每一个过孔、每一颗寄生电容都在蚕食时序裕量。如果你自己写控制器,我建议从 1MHz 起步,跑稳后用逻辑分析仪看建立保持时间,再逐步升频,一次只改一个变量。

5.3 上拉电阻、电气隔离和大电流外设的实际处理

几个非常实用的提醒:

  • I2C 上拉电阻不是“装上就行”。高速模式建议 1k~2.2kΩ,但要复核灌电流:总线上所有设备在低电平时吸收的电流总和不能超过每颗芯片的规格。线越长、设备越多,需要的上拉越强,但强到一定程度,低电平就可能压不到规范要求的 0.4V 以下。
  • 两块板之间走排线连接 SPI/I2C,千万不要忘记接地线。信号的地返回路径就是那根地线,缺了它回路面积变大,电磁干扰和串扰直接爆表。我见过太多人只接信号线不接地,结果所有高速通信都是满屏毛刺。
  • 总线负载电容超过 400pF 时,规范上建议加总线缓冲器。你测不了电容也关系不大,当上拉电阻压到 1k 还是看到上升沿圆头圆脑时,就该改拓扑或加缓冲了。

6. 调通它们的实战路线:从回环到读 ID

6.1 第一阶段:逻辑分析仪是你的第二双眼睛

调 SPI/I2C 的第一课不是写代码,而是学看波形。便宜的多通道逻辑分析仪足够把所有信号线接上,抓一帧波形,对照数据手册的时序图逐边沿核对。我第一次调 I2C 时代码逻辑自查了好几遍都“没问题”,波形一抓出来就露馅:SDA 在 SCL 为低时就开始变化,等 SCL 拉高再拉低 SDA 做 START,起始条件根本没有形成,自然一个字节都发不出去。SPI 那边也踩过类似的顺序坑——CS 必须在 SCK 之前至少半个周期拉低,我却在 SCK 拉高之后才拉 CS,从机压根没进选中状态。这类问题只靠读代码很难发现,波形却一目了然。

6.2 第二阶段:先读 ID,再读数据

面对任何陌生器件,我的调试顺序固定不变:第一步永远是读它的 ID 或厂商寄存器。因为 ID 是出厂写死的固定值,一旦读出来和手册一致,说明通信链路、字节序、模式配置全部正确,接下来才放心去读业务数据。如果 ID 都读不对,就不要在业务数据上浪费时间,先回头查时序。

还有个细节:初版驱动一律固定 8 位字节、MSB first,等链路彻底调通,再优化成硬件控制器或 DMA。一上来就开满 FIFO 和中断,出问题的时候你连是哪一层在骗你都不知道。

6.3 第三阶段:写一个自检回环程序

手里有 FPGA 开发板或自制 CPU 的话,可以把 SPI 的 MOSI 和 MISO 在外部直接短接,发一串已知数据(0xAA、0x55、0xFF、0x00 交替),看能否原样收回,这是验证控制器本身最直接的手段。我在自己搭的 CPU 核上先跑了这个回环,再接入真实器件读 ID,基本能做到一次调通。I2C 同样可以在板级回环测试,把 SDA 回接到一个 GPIO 模拟从机,或者挂一颗支持回环测试模式的编解码芯片,验证 SCL/SDA 两路的时序和电平转换。

硬件调试最磨人的不是信号问题,而是你根本不知道信号有问题。先把链路分层验证,每一层用最小用例确认无误,再去信任上一层,整个过程会清晰很多。


回想一下,从片内走到片间,收获的不只是两个协议本身,而是一套更谨慎的硬件思维:有限的引脚、有限的速度、有限的时序窗口,每个选择都要拿别的东西去换。SPI 是拿引脚换简单,I2C 是拿协议复杂度换走线。如果你也在自己动手搭 CPU 或 FPGA 原型,我真心建议先把软件模拟版的 SPI/I2C 彻底调通,再考虑硬件控制器——这个过程里养成的“看波形、查数据手册、分步验证”的习惯,比协议本身值钱得多,后面碰到 PCIe、USB 这些更复杂的串行总线时,你也不会慌。

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

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

立即咨询