很多第一次接触嵌入式的人看到 i2c、i2s、spi、uart 这四个缩写,第一反应都是“这不都是串行通信吗,干吗还要分这么细”。其实它们之间的关系有点像物流体系里的几种运输方式:挂号信、快递、公交车、专用货道,虽然都在“运东西”,但适用场景完全不同。这篇文章我想从实际项目选型的角度,把这四条总线从头到尾捋一遍,顺便把我这些年踩过的坑也一并交代清楚。
1. 先从一张“关系图”看起:UART、SPI、I2C、I2S 到底在争什么
1.1 串行通信家族里的四条主干道
串行通信的核心思想,就是一根或几根线上一位一位地传数据。跟并行总线相比,它省引脚、省PCB面积、抗干扰也好处理,所以板级通信里基本被串行总线统治。但“串行”只是个统称,i2c、i2s、spi、uart 四条总线解决的是完全不同的痛点。
UART 是异步的,就像两个人发微信,不需要一根额外的时钟线来告诉对方“我现在发的是第几位”。双方只要约定好波特率,比如说 115200,就能在数据线上自己找节奏。SPI 是同步的,主机同时给从机发时钟,相当于一个人拿着扩音器讲话,另一个人在旁边打节拍,收数据的人跟着节拍听就行。I2C 也是同步,但它把数据线和时钟线都做成了开漏结构,两根线上可以挂一大堆设备,靠地址来点名,像公交车线路,每个站点都有编号,司机报站谁要下车就停一下。I2S 更特殊,它是为了音频流设计的同步总线,它不关心你发的是寄存器还是传感器数据,只关心左右声道的比特流有没有按节拍送出去。
刚开始接触这些协议时,最忌讳的就是上来就背时序图。书上的时序图主要用来写驱动、调逻辑分析仪时对照,实际上绝大多数项目里,你先要搞清楚自己需要的到底是“发一句话”“读一个寄存器”“搬一块数据流”还是“传一段采样流”,这决定了总线怎么选。
1.2 别急着背时序图,先问自己三个问题
我在帮别人选型时,一般只问三个问题:多少距离?多少吞吐?挂几个设备?
距离方面,UART 做板内通信或者短距离板间接线都非常顺手,搭配 RS485 还能拉到几百米。SPI、I2C、I2S 基本就是板级通信,超过 10cm 就开始有麻烦,尤其 I2C 线一长,上升沿变缓,波形就糊了。吞吐方面,SPI 可以跑几十甚至上百 MHz,适合搬固件、读 ADC 连续采样;I2C 标准模式 100kbps,快速模式 400kbps,高速模式也有 1Mbps,但稳定性和 PCB 设计难度也跟着上去;UART 最常见的还是 115200bps,调试足够,搬大数据量就痛苦。I2S 的速率由音频采样率决定,48kHz 16bit 立体声的位时钟就是 1.536MHz,一看就知道它不能用来干别的。
设备数量方面,UART 天生点对点,SPI 靠片选扩展,I2C 靠地址扩展,I2S 基本是一对一。挂多个传感器第一个想到 I2C,追求速度想到 SPI,这基本不会错。想清楚这三点,后面看时序图才有意义。
2. 逐个拆解:四条总线的时序和选型要点
2.1 UART:异步、全双工、点对点,永远留一条调试口
UART 是我个人认为最适合入门的基础协议。它只需要 TX、RX 两根线交叉连接,外加一根共地。8250/16550 这个行业标准寄存器模型相当经典,哪怕现在的新平台,底层串口控制器也基本沿用了这一思路。对固件工程师来说,把 printf 重定向到 UART,几乎是每个板子最先做的一件事。
UART 波形很好认:空闲时 TX 线保持高电平,发送一个字符时先拉一个起始位低电平,然后按比特顺序发数据位,最后是校验位和停止位。最常见的配置是 8N1,也就是 8 个数据位、无校验、1 个停止位。逻辑分析仪接上 RX/TX,解码器选 115200,波形里能看到一长串 0/1 组合自动变成 ASCII 字符。如果解码出来是乱码,第一个查波特率,第二个查地线,第三个查 USB 转串口芯片是不是驱动装错了。
这里必须提一个老坑:FT232R、FT231X 这两种 USB 转 UART 芯片我都遇到过 Windows 突然不识别、设备管理器报错误代码 12 的情况。代码 12 不是芯片坏了,多半是 USB 资源分配冲突或者旧驱动残留。解决方法是先把旧驱动干净卸载,再装官方驱动;如果还不行,换个 USB 口、关掉 USB 节能模式。FT231X 是新一些的封装,有些劣质线材用的芯片型号被反复擦写,枚举 ID 和标准不一致,也会出现没驱动的情况。
至于 STM32F103 标准库下用 UART DMA 中断收发,我最初也走了不少弯路。不定长数据接收最舒服的方案是:DMA 接收 + 空闲中断。开启 DMA 的不定长接收模式,把数据一直往缓冲区里灌,收到一帧停下时,串口空闲中断会触发,这时候去看 DMA 剩余计数,就能算出这次收到了多少个字节。发送侧用 DMA 完成中断,发送完成后再关闭 DMA,避免下一次发数据时上一个传输还没结束。这套组合在 Modbus、GPS 解析这些场景里非常稳定。
2.2 SPI:速度优先,片选决定“谁是老大”
SPI 是同步全双工,主机把时钟拉起来,MISO/MOSI 同时进出数据。它没有应答机制,主机只管发,从机有没有收到全靠从机自己的时序是否靠谱。做项目时如果想省事,SPI 是最不容易出低级错误的:只要把模式对上、片选搞对,基本一次通。
要理解 SPI,首先理解片选。硬件片选是外设自动控制 CS 引脚,适合单从机;软件片选是普通 GPIO 手动拉低再拉高,适合多从机或者需要延长 CS 有效时间的情况。曾经有人问我“CS 最小能做到多少 us”,这个真不能凭感觉。每颗从机手册里都写了 CS 建立时间、保持时间,比如有些 ADC 要求 CS 拉低后至少 50ns 才能开始采样,有些 Flash 要求 CS 拉高后至少 10ns 才能进行下一次命令。我自己调 FPGA 驱动 SPI ADC 时,就直接按手册数值再加 20% 余量来设计状态机,宁可慢一点也不能把采样时序算到临界值。
SPI 的模式选择也很玄学。模式 0 到模式 3 由 CPOL 和 CPHA 决定,很多从机默认 Mode 0,但像某些磁编码器、特定 ADC 就要求 Mode 1。我当初调 MT6701 SPI 读角度时,数据手册上没把模式写清楚,抓波形发现 MOSI 发出的命令没错,MISO 返回全是 0xFF,最后把模式从 0 改成 1 立刻恢复。调试时不要背模式表,直接用逻辑分析仪看 SCLK 空闲电平是低还是高,数据是在上升沿还是下降沿被采样,然后对应设置即可。
STM32Cubemx 配置 SPI DMA 时,最大的坑是 DMA 内存地址对齐。SPI 接收 DMA 的缓冲区如果是 u8 数组,容易遇到地址非自然对齐的问题,导致接收数据错位。我建议缓冲区至少用 uint32_t 类型数组,或者把 DMA 的 memory burst 设置成单次传输。另一个坑是“只开接收 DMA 不开发送 DMA”,SPI 是移位寄存器结构,主机若只收不发,时钟谁来产生?很多人只配置了接收通道,结果 MISO 永远没有数据,一定要同时打开发送 DMA,发送缓冲区里填 0xFF 即可。
如果你不想动固件,想用电脑直接读 SPI 设备,Python 调用 USB 模拟 SPI 接口是个好办法。FT232H 这类芯片配合 pyftdi 库,可以在 PC 上直接模拟 SPI 主机,用来读传感器寄存器、读 Flash ID 都是秒级完成。我调试 FPGA SPI ADC 时就用这套方式,先电脑发读命令,再看返回波形,确认整条链路没问题后再写 FPGA 代码,能把排错时间压缩一大截。
2.3 I2C:两根线上演“点名与应答”
I2C 是这些协议里最“讲规矩”的一个。SDA 和 SCL 都是开漏结构,必须外接上拉电阻,工作时靠下拉拉低、靠上拉释放。它有一整套起始、停止、应答、地址机制,读设备寄存器时先发设备地址和寄存器地址,然后重新发起始位,再读数据。
I2C 时序里最重要的是起始条件和停止条件。SCL 高电平时 SDA 从高到低是起始,SCL 高电平时 SDA 从低到高是停止。数据在 SCL 高电平期间必须保持稳定,否则会被误判成起始或停止。用逻辑分析仪抓出来,Start、8bit Address+R/W、ACK、Data、Stop 非常清楚。如果看到 ACK 变成 NACK,通常是地址不对、器件没上电、或者上拉电阻阻值过大导致信号根本没拉起来。
我在写 I2C 读写 EEPROM 的 Verilog 代码时,核心思路是状态机加计数器:IDLE 到 START、发送地址、等到 ACK、发送寄存器地址、再来一次 START、读数据,最后发 NACK 加 STOP。SCL 频率要由主时钟分频出来,比如 50MHz 主频跑 100k 的 I2C,一个时钟周期就是 500 个主时钟计数。如果主机复位期间 SCL 停在低电平,从机可能把状态机锁死,这时候主机快速翻转 9 个 SCL 时钟就能把总线恢复,这是最常用的 I2C 总线解死方法。
I2C 从机是不能主动发数据的,只能主机来读。很多人问“I2C 从机怎么主动更新主机寄存器”,换句话说是从机数据变了,主机怎么知道?标准做法是从机拉一个 GPIO 中断脚告诉主机“快来读”,主机在中断服务程序里发起读请求。如果不想加中断线,就只能主机定时轮询寄存器,用“自由数据模式”直接以块读方式把整片数据搬回来,这样也能缓解实时性问题,但代价是浪费一部分总线带宽。
Linux 下 I2C 最常见的场景是读取 PHY 寄存器。有些板子的 PHY 没有接 MDIO 引脚,只留了 I2C 接口,就不能再按 MDIO 的流程去访问。解决办法是在设备树里注册一个 i2c client,用 i2c-dev 直接访问 PHY 的寄存器地址,读出来再去对照 PHY 手册解析状态位。本质上就是把原本走 MDIO 的寄存器映射,搬到了 I2C 协议上。
调试 I2C 时还要注意地址冲突。I2C 扩展器、多路复用器(比如 PCA9548)在系统里很常用,如果两块传感器硬件地址完全一样,就得用复用器先选通道再接主路。另外,触摸屏 GT911 这类 I2C 设备,上电时序和中断脚复位时序比协议本身更关键——我遇到过 GT911 I2C 通信失败的案例,逻辑分析仪上看波形完全正常,就是没 ACK,最后发现是复位引脚拉低时间不够,芯片没起来。
Windows 下 I2C HID 设备报错代码 12 也有类似逻辑:不是 I2C 协议本身的问题,而是 ACPI 资源分配冲突,导致系统没有足够的中断资源给 I2C HID 控制器。解决方向是进 BIOS 检查中断路由、更新固件,而不是去重装触摸屏驱动。
顺便说一句,PMBus 和 I2C 的区别也常被搞混。PMBus 物理层基本沿用了 I2C 的时钟和数据机制,但它在协议层上定义了电源管理的命令集,比如输出电压、电流、温度告警寄存器。所以用 PMBus 时不能直接套普通 I2C 寄存器的读法,要和电源芯片手册对齐命令格式。
2.4 I2S:为音频而生的同步总线
I2S 和前面三条总线的定位完全不同。前面三种都在传“命令”和“寄存器数据”,I2S 只关心“音频流”。它有 BCLK 位时钟、LRCLK 左右声道时钟、SD 数据线,有些芯片还要 MCLK 主时钟。LRCLK 低电平通常是左声道,高电平右声道(也有芯片是反的),每个 BCLK 周期传一个 bit,数据在 BCLK 边沿更新。
I2S 的频率计算很简单:BCLK = 采样率 x 位深 x 声道数。比如 48kHz 采样、16bit 位深、立体声,BCLK 就是 48k x 16 x 2 = 1.536MHz。LRCLK 就等于采样率 48kHz。逻辑分析仪看 I2S 波形,第一步先确认 BCLK 频率对不对,再确认 LRCLK 是不是音频采样率,最后看数据线上有没有实际脉冲。如果数据线长时间恒高或恒低,多半是代码里没有正确填充音频数据。
ESP32-C3 的 I2S 输出我实际调过,它只有一个 I2S 外设,用来接 MAX98357 这类功放或者 DAC 芯片。ESP-IDF 新的驱动里配置 I2S 标准模式、设置 slot 位数和 GPIO 后,主要注意时钟树配置。ESP32-C3 内部有些音频时钟默认不给 I2S 供时钟,如果初始化顺序不对,I2S 的 BCLK 根本不会翻转。遇到没声音,先用逻辑分析仪量 BCLK 和 LRCLK,再回来查代码。
I2S 最容易犯的错是把 LRCLK 和 BCLK 接反,或者左右声道极性接反,结果声音变成从右边跑到左边,音调还可能不对。这类问题用逻辑分析仪一抓就现原形,不要盲调。
3. 一张表看完:I2C/I2S/SPI/UART 全参数对比
3.1 核心参数横向对比表
做了这么多年板级通信,我经常会把这张表贴在电脑前:
| 协议 | 同步/异步 | 全双工/半双工 | 常规引脚 | 拓扑 | 常见速率 | 特点 |
|---|---|---|---|---|---|---|
| UART | 异步 | 全双工 | TX、RX、GND | 点对点为主 | 115200bps,也能跑到数 Mbps | 无时钟线,必须对齐波特率,适合调试和板间通信 |
| SPI | 同步 | 全双工 | SCLK、MOSI、MISO、CS | 1 主多从,靠片选区分 | 几 MHz 到几十 MHz | 速度快,无应答,片选时序关键 |
| I2C | 同步 | 半双工 | SDA、SCL | 多主多从,靠地址区分 | 100k/400k/1M | 两根线挂一堆设备,有应答机制 |
| I2S | 同步 | 连续流 | BCLK、LRCLK、SD、MCLK | 点对点 | 由采样率与位深决定 | 专用音频流,不承载命令交互 |
这张表是选型的起点,不是终点。实际项目里,UART、SPI、I2C、I2S 四者往往共存。比如一块音频板,功率放大器用 I2S 传音频流,音频编解码器的寄存器配置用 I2C 控制,MCU 和主控之间用 UART 打日志,Flash 用 SPI 存录音数据。它们不是互斥关系,而是分工关系。
3.2 从需求反推总线选型
如果距离超过一米,第一个排除 SPI 和 I2C,先考虑 UART(加 RS232/RS485)或者 CAN。板内通信如果要从机数量多、数据量不大,选 I2C 最省引脚。数据吞吐高、需要连续传几十 KB 甚至几 MB 的场景,直接上 SPI,DMA 配合起来很爽。音频波形必须走 I2S 或者类似 PDM/TDM 总线,没有第二种选择。
我还遇到过有人试图用 UART 去传音频数据的,勉强能传,但同步和抗抖动很差,声音会忽快忽慢。也有人试图用 SPI 去带一大批传感器,结果片选引脚占用太多,还不如 I2C 加地址方便。选型时先定数据形态,再定速率,最后看引脚预算,这几步走完基本不会错。
3.3 混合存储方案给我的启发
看过 RK3588S 混合存储方案的人都知道:SPI NOR Flash 放引导程序,PCIe NVMe SSD 放系统和数据,这种组合已经成为很多板卡的标准做法。SPI NOR 容量小、便宜、启动时序简单,NVMe 容量大、带宽高、适合大文件读写。启动流程大致是 ROM 引导 SPI NOR 里的 u-boot,u-boot 初始化 PCIe 后加载 NVMe 里的系统镜像。虽然中间会遇到初始化时序、PCIe 链路训练等问题,但整体思路很有代表性:不同总线干不同的事,别指望一种总线包打天下。
4. 实操实录:波形、DMA、驱动和那些一言难尽的坑
4.1 逻辑分析仪是我的第二个眼睛:UART/SPI/I2C/I2S 波形的抓法
我强烈建议做嵌入式的人人手一台逻辑分析仪,采样率至少 100MHz,通道数 8 个以上。调试 UART 时,直接接 RX、TX、GND,解码器选对应波特率,立刻能看到文本;调试 SPI 时,接 SCLK、MOSI、MISO、CS,特别注意 CS 的有效区间是否覆盖完整帧;调试 I2C 时,接 SDA、SCL,解码器会自动解析地址和 ACK,非常直观;调试 I2S 时,接 BCLK、LRCLK、SD,配置好声道和位宽,就能看到左右声道数据是否正确。
新手容易忽略的是逻辑分析仪的采样频率。采样率至少要抓到信号频率的四倍以上,否则边沿判断不准,解码会出错。I2C 400k 模式下采样率 24MHz 足够了,SPI 20MHz 时钟最好用 100MHz 采样率。抓 I2S 的时候要看清楚 LRCLK 是低有效还是高有效,有些芯片极性配置不一样。
4.2 我遇到过的 10 个高频问题,以及排查顺序
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| I2C SDA 卡在低电平 | 从机把总线拉死 | 用主机翻转 9 个 SCL 解除死锁,再查上拉电阻 |
| SPI 读回全是 0xFF | 从机没响应,或模式不对 | 先量 CS 有没有拉低,再查 CPOL/CPHA |
| UART 乱码 | 波特率不一致、地线没接 | 统一波特率,重新检查 TX/RX 连线 |
| I2C 设备找不到资源 | 代码 12 资源冲突 | 查 ACPI 中断路由,更新 BIOS |
| I2S 没声音 | 时钟没产生、极性错误 | 先用逻辑分析仪量 BCLK/LRCLK |
| SPI DMA 丢字节 | 内存对齐问题 | 缓冲区用 uint32_t 对齐 |
| GT911 触摸没反应 | 复位时序不对 | 先查 INT/RST 引脚时序,再查 I2C 地址 |
| I2C NACK 一直出现 | 设备没上电或地址错 | 检查地址位和器件供电 |
| SPI CS 时间不够 | 从机时序要求严 | 软件片选手动控制 CS 提前拉低 |
| FT232R/FT231X 驱动异常 | 驱动残留 | 官方驱动干净重装 |
排查顺序也很重要,永远先看供电,再看波形,最后猜代码。供电都错了,后面全白费。
4.3 最后再分享一点我自己的使用体会
我做了这么多年固件,最常用的组合还是“UART 打日志 + SPI 搬数据 + I2C 挂传感器 + I2S 传音频”。每次新板子点亮,第一步就是先把 UART 调通,能看到日志板子就算活了。然后逐个调 I2C 和 SPI 外设,最后才碰 I2S。别一上来就四路总线一起开,会把问题绞在一起。时序这种东西,看着手册是一回事,用逻辑分析仪实测是另一回事。很多看起来像“玄学”的问题,最后都会落到一句很朴素的结论:供电不够、引脚配置错、或者没看示波器。把这四条总线背后的数据形态想清楚,项目就成功一半了。