嵌入式开发绕不开的一个话题就是总线选型。你打开任何一块开发板的原理图,几乎都能看到 I2C、SPI、UART 这三种接口的身影,做音频的还会碰到 I2S。很多人初学的时候是"哪个能跑通就用哪个",等到项目做大了、板子画密了、出问题了,才发现当初选型时偷的懒全变成了后期调试的债。这篇内容就是把我这些年在这四种总线上踩过的坑、做过的取舍、以及一些不太会在官方手册里写出来的经验,系统地梳理一遍。不管你是刚接触单片机的新手,还是正在做 RK3588、ESP32-C3 这类平台方案的老手,应该都能从中找到对自己有用的部分。
1. 四种总线的本质差异:先搞清楚它们各自在解决什么问题
1.1 从"谁说了算"看总线拓扑
I2C、SPI、UART、I2S 这四者最根本的区别,其实在于时钟信号的归属和设备角色的划分方式。
UART 是典型的异步通信,收发双方没有共享时钟线,靠事先约定好的波特率来对齐每一位数据。这意味着两端的时钟精度必须足够接近,否则累积误差会在帧尾导致采样错位。UART 的拓扑是点对点的,一个 TX 对一个 RX,天生只支持两个设备互通。
I2C 和 SPI 都是同步通信,有一根明确的时钟线(SCL / SCK)由主机驱动。区别在于 I2C 是半双工、多主多从的架构,两根线(SDA、SCL)上可以挂一堆设备,每个从机靠唯一的地址区分;SPI 则是全双工、单主多从,四根线(MOSI、MISO、SCK、CS),每个从机独占一根片选线。
I2S 可以理解为"SPI 的音频特化版"。它也是同步串行,但它的时钟体系更复杂——除了位时钟 BCK,还有帧时钟 LRCK(也叫 WS,字选择),专门用来区分左右声道。I2S 不关心地址,也不关心片选,它关心的是音频数据的连续性和时序精度。
一句话总结:UART 靠约定,I2C 靠地址,SPI 靠片选,I2S 靠帧同步。
1.2 速率、距离与线数的三角权衡
选型时最常被拿出来比较的三个维度是速率、传输距离和引脚开销。我把它们整理成一张表,方便你快速对照:
| 总线 | 典型速率 | 可靠传输距离 | 引脚数(单设备) | 拓扑 |
|---|---|---|---|---|
| UART | 9.6k~921.6k bps,高速可到数 Mbps | 板内几米,RS-485 可到千米级 | 2(TX/RX) | 点对点 |
| I2C | 100k/400k/1M/3.4M Hz | 板内几十厘米,长线需缓冲 | 2(SDA/SCL) | 多主多从 |
| SPI | 几 MHz 到上百 MHz | 板内十几厘米,高速时更短 | 4(含 CS) | 单主多从 |
| I2S | BCK 通常 1~12 MHz | 板内,越短越好 | 3~4 | 点对点为主 |
这张表里有个容易被忽略的点:SPI 的速率上限和走线长度是强相关的。你在 1MHz 下能跑通的 20cm 排线,到了 50MHz 可能连第一个字节都读不对。原因在于高速下走线的寄生电容和反射会严重破坏信号完整性,SCK 边沿一糊,从机采样就全乱了。所以做高速 SPI 时,走线尽量短、尽量等长、尽量远离干扰源,这不是玄学,是实打实的信号完整性要求。
1.3 为什么不能"一种总线打天下"
经常有人问:既然 SPI 最快,为什么不全用 SPI?答案在于引脚成本和设备数量的矛盾。
假设你要挂 8 个传感器。用 I2C,两根线搞定,地址不冲突就行。用 SPI,每个设备一根 CS,加上共享的 SCK/MOSI/MISO,总共需要 3+8=11 根线。如果你的 MCU 引脚本来就紧张,这个差距是致命的。反过来,如果这 8 个设备里有 ADC 需要高速采样,I2C 的 400kHz 就完全不够看了,这时候 SPI 的高速率优势又无可替代。
UART 的价值则在于简单和通用。它不需要地址、不需要片选、不需要复杂的时序协商,两根线接上就能通。调试串口、GPS 模块、一些老式传感器,用 UART 是最省心的。而且 UART 经过电平转换后可以走很长的线,工业现场的 RS-485 就是基于 UART 的差分版本。
I2S 则是音频场景的刚需。你用 SPI 去传音频数据理论上也行,但 SPI 没有帧同步信号,左右声道的区分、采样率的对齐都得靠软件额外处理,实时性和音质都没法保证。I2S 把这些硬件化了,DMA 一挂,CPU 几乎不用管。
2. I2C 的深水区:地址、上拉与那些让人抓狂的锁死
2.1 地址冲突与 7 位/10 位寻址
I2C 最让人头疼的问题之一就是地址冲突。标准 7 位地址空间理论上能挂 112 个设备(去掉保留地址),但实际上市面上大量传感器的地址是固定的,比如很多 EEPROM 是 0x50,很多温湿度传感器是 0x48。你挂两个同型号的芯片,地址就撞了。
解决办法通常有三个:一是选带地址配置引脚的型号,通过拉高拉低 ADDR 引脚改变地址;二是用 I2C 多路复用器(比如 TCA9548A),把总线分成 8 路,每路挂一个冲突设备;三是用软件模拟 I2C,把设备分到不同的 GPIO 组上。
关于 7 位和 10 位寻址,这里有个细节值得说清楚。7 位地址在传输时,实际发送的是 8 位——高 7 位是地址,最低位是读写标志位(0 写 1 读)。所以你在代码里写0x50,实际线上看到的是0xA0(写)或0xA1(读)。很多新手对着逻辑分析仪抓出来的波形一脸懵,就是因为没搞清楚这个移位关系。10 位寻址则是用两个字节传地址,第一个字节的高 5 位是固定的11110,用来和 7 位寻址区分。
2.2 上拉电阻的计算不是拍脑袋
I2C 是开漏输出,SDA 和 SCL 都必须接上拉电阻才能输出高电平。这个电阻选多大,是有计算依据的,不是随便找个 4.7k 就完事。
上拉电阻的上限由上升时间决定。I2C 规范要求标准模式(100kHz)上升时间不超过 1000ns,快速模式(400kHz)不超过 300ns。上升时间约等于0.847 × R × C(C 是总线总电容,包括走线、引脚和器件电容)。假设总线电容 200pF,快速模式下:
R ≤ 300ns / (0.847 × 200pF) ≈ 1.77kΩ下限则由灌电流能力决定。I2C 器件拉低时能承受的最大电流一般是 3mA,所以:
R ≥ (VDD - VOL) / 3mA3.3V 系统下,VOL 取 0.4V,则R ≥ 2.9V / 3mA ≈ 967Ω。
所以 3.3V、400kHz、200pF 的场景下,上拉电阻的合理区间大约是 1k~1.7k。实际选 2.2k 或 4.7k 也能跑,但波形上升沿会变缓,长走线或高容性负载下就容易出错。我见过太多"I2C 时好时坏"的案例,最后查出来都是上拉电阻偏大加上走线太长。
经验:如果总线上挂了多个设备,电容会累加,上拉电阻要相应减小。但也不能太小,否则功耗上去了,而且有些器件的灌电流能力有限,会拉不低。
2.3 总线锁死的成因与恢复
I2C 最经典的故障是总线锁死:从机在发送数据时被复位或干扰,导致它一直把 SDA 拉低,主机无法发起新的传输。这时候你重启主机都没用,因为从机还在占着线。
根本原因是 I2C 的状态机在传输中途被打断,从机停在某个中间状态。恢复的标准做法是手动发送 9 个时钟脉冲:把 SCL 配置成普通 GPIO,手动翻转 9 次,让从机把剩余的位发完,然后发一个 STOP 条件,总线就释放了。
// 伪代码:I2C 总线恢复 void i2c_bus_recover(void) { gpio_set_output(SCL); gpio_set_output(SDA); gpio_write(SDA, 1); for (int i = 0; i < 9; i++) { gpio_write(SCL, 0); delay_us(5); gpio_write(SCL, 1); delay_us(5); } // 发送 STOP 条件:SCL 高时 SDA 由低变高 gpio_write(SDA, 0); delay_us(5); gpio_write(SCL, 1); delay_us(5); gpio_write(SDA, 1); delay_us(5); // 恢复 I2C 外设配置 i2c_reinit(); }这个恢复函数建议直接放进你的驱动初始化流程里,每次上电先跑一遍,能省掉大量"偶发不通信"的排查时间。另外,GT911 这类触摸芯片在 I2C 通信失败时特别容易锁死,复位时序没做对的话,上电就挂。
3. SPI 的实战细节:模式、片选与高速走线
3.1 CPOL 和 CPHA 组合出的四种模式
SPI 有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)组合而成:
| 模式 | CPOL | CPHA | 采样边沿 | 空闲电平 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 第一个边沿(上升) | 低 |
| Mode 1 | 0 | 1 | 第二个边沿(下降) | 低 |
| Mode 2 | 1 | 0 | 第一个边沿(下降) | 高 |
| Mode 3 | 1 | 1 | 第二个边沿(上升) | 高 |
新手最容易犯的错是模式配错。现象通常是读出来的数据全是 0xFF 或者 0x00,或者数据整体移位。因为采样边沿不对,采到的全是无效位。排查方法很简单:查从机手册确认它支持哪种模式,然后主机配成一样的。如果手册没写清楚,就四种模式挨个试,能读出正确 ID 的那个就是对的。
这里有个细节:模式 0 和模式 3 最常用,因为它们在空闲时 SCK 都是低电平,很多器件默认就支持这两个。模式 1 和模式 2 相对少见。
3.2 硬件片选与软件片选的取舍
SPI 的片选(CS)有两种实现方式。硬件片选是 MCU 的 SPI 外设自动控制 CS 引脚,传输开始拉低、结束拉高,时序精准。软件片选则是用普通 GPIO 手动控制,传输前后自己拉低拉高。
硬件片选的好处是时序精确、CPU 负担小,尤其在 DMA 传输时,硬件 CS 能保证整个传输过程中 CS 稳定。软件片选的好处是灵活,你可以用任意 GPIO,而且可以在一次 CS 有效期内穿插多个操作(比如先发命令再读数据,中间不释放 CS)。
但软件片选有个坑:CS 的建立时间和保持时间。有些器件要求 CS 拉低后要等一段时间才能开始 SCK,CS 拉高前也要等最后一个时钟沿稳定。如果你用软件片选但没加延时,高速下就可能出错。我一般会在 CS 拉低后加 1~2 个微秒的延时,拉高前也加一点,虽然牺牲了一点点速度,但稳定性提升明显。
关于"CS 最小能做到多少纳秒"这个问题,其实取决于从机器件的要求和你的走线质量。理论上 CS 的有效宽度只要覆盖整个传输过程即可,但实际中要考虑从机的建立/保持时间要求,以及信号完整性。高速场景下,CS 走线最好和 SCK 等长,避免时序偏差。
3.3 高速 SPI 的走线与阻抗问题
当 SPI 速率上到几十 MHz,它就不再是"随便连根线就能通"的低速信号了。这时候要把它当传输线来看待。
几个关键点:走线尽量短,最好控制在几厘米以内;SCK 和 MOSI/MISO 尽量等长,减少时序偏差;远离高频干扰源(比如开关电源、晶振);必要时在源端串一个 22~33Ω 的电阻做阻抗匹配,抑制反射。
我做过一个 RK3588 的项目,SPI 接一块高速 ADC,初期用 10MHz 跑没问题,提到 50MHz 就疯狂出错。后来用示波器一看,SCK 的上升沿有明显的振铃,过冲都快到 4V 了。串了 27Ω 电阻、缩短走线之后,50MHz 稳定运行。所以高速 SPI 出问题,先看波形,别急着改代码。
4. UART 与 I2S:一个求稳,一个求准
4.1 UART 的波特率误差与帧格式
UART 没有时钟线,全靠波特率对齐。接收方通常在每位数据的中间采样,如果双方波特率有偏差,采样点会逐渐偏移,偏差累积到半个位宽时就采错了。
一般要求波特率误差控制在2% 以内,实际工程中最好做到 1% 以内。误差来源有两个:一是晶振本身的精度,二是分频系数取整带来的误差。比如你用 8MHz 晶振想产生 115200 波特率,分频系数是8000000 / 115200 ≈ 69.44,取整成 69 后实际波特率是8000000 / 69 ≈ 115942,误差约 0.64%,可以接受。但如果晶振是 7.3728MHz 这种专门为串口设计的频率,分频就是整数,误差为零。
帧格式方面,标准是 1 位起始位、8 位数据位、无校验、1 位停止位(俗称 8N1)。起始位是下降沿,接收方检测到这个下降沿就开始采样。这里有个细节:接收方通常会过采样,比如 16 倍波特率采样,在起始位中间确认有效后再对齐到数据位中间,这样抗干扰能力更强。
STM32 用 DMA 做 UART 收发时,有个经典坑是DMA 接收不知道数据什么时候结束。因为 UART 是异步的,DMA 只知道搬数据,不知道帧边界。解决办法是用空闲中断(IDLE):一帧数据接收完后总线会空闲,触发 IDLE 中断,在中断里处理这一帧。这个组合(DMA + IDLE)是 STM32 串口接收的标准姿势。
4.2 I2S 的三线制与音频时序
I2S 的核心是三根线:BCK(位时钟)、LRCK(帧时钟/字选择)、SD(数据)。BCK 的频率等于采样率 × 位宽 × 声道数。比如 48kHz、16 位、立体声,BCK = 48000 × 16 × 2 = 1.536MHz。
LRCK 的频率等于采样率,它用高低电平区分左右声道。通常 LRCK 低电平是左声道,高电平是右声道(具体看器件定义)。LRCK 的跳变发生在一个位时钟之前,也就是说数据在 LRCK 变化后的第二个 BCK 边沿才开始有效,这个"提前一位"的规则是 I2S 标准的一部分,很多新手在这里栽跟头。
用 ESP32-C3 做 I2S 输出时,要注意它的 I2S 外设支持多种模式,包括标准 I2S、左对齐、右对齐等。配置的时候要确认你的 DAC 或功放支持哪种格式,格式不匹配的话,声音会失真或者左右声道颠倒。另外,ESP32-C3 的 I2S 引脚是可以矩阵映射的,不是固定的,这点比很多 MCU 灵活。
用逻辑分析仪抓 I2S 波形时,重点看三个东西:BCK 是否连续稳定、LRCK 的占空比是否接近 50%、SD 上的数据在 BCK 的哪个边沿变化。如果 LRCK 占空比严重偏离 50%,说明配置有问题;如果数据在错误的边沿变化,说明采样边沿配反了。
5. 选型决策:拿到一个需求该怎么选
5.1 按数据速率和实时性筛选
选型的第一步是看数据速率需求。我一般这样划分:
- 低速、少量数据(传感器配置、EEPROM 读写、寄存器操作):优先 I2C,省引脚。
- 中高速、大数据量(ADC 采样、显示屏、Flash):优先 SPI。
- 点对点、简单可靠(调试、GPS、模块通信):优先 UART。
- 音频流:只能用 I2S。
这里有个容易忽略的点:实时性。I2C 是半双工,一次传输要经历起始、地址、应答、数据、应答、停止等多个阶段,开销大。如果你需要频繁读取一个小数据,I2C 的实际吞吐可能远低于标称速率。SPI 全双工,没有地址和应答开销,连续传输效率高得多。
5.2 按引脚预算和设备数量权衡
引脚紧张的时候,I2C 的优势就体现出来了。两根线挂一堆设备,这是 SPI 做不到的。但要注意 I2C 的总线电容限制:规范要求总线电容不超过 400pF,每个器件的引脚电容大概 10pF 左右,走线也有电容。挂太多设备或者走线太长,电容超标,波形就烂了。
SPI 挂多设备时,片选线的数量是线性增长的。如果设备超过 4~5 个,引脚开销就很可观了。这时候可以考虑用译码器(比如 74HC138)来扩展片选,用 3 根 GPIO 控制 8 个片选,能省不少引脚。
5.3 一张决策流程图(文字版)
拿到需求后,我通常这样走:
- 是音频数据吗?是 → I2S。
- 需要挂多个设备且速率要求不高吗?是 → I2C。
- 需要高速传输或全双工吗?是 → SPI。
- 只是点对点简单通信吗?是 → UART。
- 以上都不满足?考虑组合使用,或者用 CAN、SDIO 等其他总线。
实际项目中,一块板子上同时用这几种总线是常态。比如主控用 I2C 接传感器、SPI 接 Flash、UART 接调试口、I2S 接音频编解码器,各司其职,互不干扰。
6. 调试工具与常见故障速查
6.1 逻辑分析仪的正确用法
调试这四种总线,逻辑分析仪是必备工具。但很多人买了分析仪却不会用,抓一堆波形看不懂。几个要点:
采样率要足够高。根据奈奎斯特定理,采样率至少是信号最高频率的 2 倍,实际建议 5~10 倍。抓 50MHz 的 SPI,采样率至少要 250MHz 以上,否则波形全是锯齿,根本看不出边沿。
触发条件要设对。抓 I2C 可以设 SDA 下降沿触发(起始条件),抓 SPI 可以设 CS 下降沿触发,抓 UART 可以设 RX 下降沿触发。设好触发,才能抓到你想看的那一帧。
协议解码要用起来。现在的逻辑分析仪软件基本都支持 I2C、SPI、UART、I2S 的协议解码,把波形直接翻译成地址、数据、应答,比你自己数位快得多。但要注意解码器的参数要设对,比如 I2C 的地址位数、SPI 的模式、UART 的波特率,设错了解码结果就是错的。
6.2 常见故障对照表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| I2C 无应答 | 地址错、上拉缺失、器件未上电 | 查地址、量上拉电压、查供电 |
| I2C 时好时坏 | 上拉偏大、走线长、电容超标 | 减小上拉、缩短走线、加缓冲 |
| I2C 总线锁死 | 从机状态机卡死 | 发 9 个时钟脉冲恢复 |
| SPI 读全 0/全 FF | 模式错、CS 未拉低、MISO 未接 | 查模式、查 CS、查接线 |
| SPI 高速出错 | 走线长、无匹配、干扰 | 缩短走线、串匹配电阻、看波形 |
| UART 乱码 | 波特率不匹配、晶振误差大 | 核对波特率、换晶振 |
| UART 丢数据 | 无流控、中断优先级低 | 加 DMA+IDLE、提优先级 |
| I2S 无声/失真 | 格式不匹配、LRCK 反相 | 查格式、查 LRCK 极性 |
这张表里的每一条,我基本都在实际项目中遇到过。尤其是 I2C 的"时好时坏"和 SPI 的"高速出错",这两类问题最耗时间,因为它们在实验室常温下可能完全正常,一到现场或者一上高温就出问题。所以做产品的时候,一定要在极限条件下测试总线通信,别只在舒适的实验环境里验证。
6.3 一些不太会写在手册里的经验
最后分享几个零散但很实用的点:
I2C 的自由数据模式。有些器件支持一种非标准的 I2C 变体,不遵循标准的起始-地址-数据-停止格式,而是用自定义的时序。遇到这种器件,标准 I2C 外设可能搞不定,得用 GPIO 模拟。PMBus 就是基于 I2C 的一个例子,它有自己的命令集和时序要求,和标准 I2C 有区别。
SPI 的菊花链。多个 SPI 从机可以串成菊花链,数据从主机发出,依次穿过每个从机。这种方式只需要一根 CS,能省引脚,但要求从机支持菊花链模式,而且数据要按顺序组织。ADC 阵列常用这种方式。
UART 转其他接口。FT232R、FT231X 这类 USB 转 UART 芯片,驱动装好后在系统里就是一个虚拟串口。用 Python 的 pyserial 库可以直接操作,做自动化测试很方便。但要注意,这类芯片的驱动在不同系统上安装方式不同,Linux 下一般内核自带,Windows 下要装厂商驱动。
I2C 从机主动更新主机寄存器。标准 I2C 是主机发起、从机响应,但从机不能主动发起传输。如果从机有事件要通知主机(比如传感器检测到阈值),只能靠主机轮询,或者用一根额外的中断线。有些器件支持 SMBus Alert 机制,算是 I2C 的一个扩展。
FPGA 接 SPI ADC。FPGA 没有固定的 SPI 外设,通常用 Verilog 写状态机实现。好处是时序完全可控,可以做到很高的速率;坏处是什么都要自己写,包括时钟分频、边沿检测、数据对齐。写的时候一定要注意跨时钟域处理,ADC 的采样时钟和 FPGA 的系统时钟往往不同源,直接采样会有亚稳态风险。
Linux 下 I2C 与 MDIO 的复用。有些网络 PHY 芯片的 MDIO 接口可以复用成 I2C,但需要配置正确的寄存器。RK3588 这类平台在设备树里配置的时候,要注意 pinmux 的设置,别把功能搞混了。
总线这东西,说到底就是"在约束下找最优解"。没有哪种总线是万能的,关键是搞清楚你的需求边界在哪里,然后选那个最不别扭的方案。我个人的习惯是:能用 I2C 就用 I2C,引脚实在不够或者速率实在上不去再换 SPI,UART 留给调试和简单外设,I2S 只在音频场景出现。这个优先级不是绝对的,但大多数项目里都挺好使。