
很多第一次接触嵌入式的人看到 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 AddressR/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、CS1 主多从靠片选区分几 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 的场景直接上 SPIDMA 配合起来很爽。音频波形必须走 I2S 或者类似 PDM/TDM 总线没有第二种选择。我还遇到过有人试图用 UART 去传音频数据的勉强能传但同步和抗抖动很差声音会忽快忽慢。也有人试图用 SPI 去带一大批传感器结果片选引脚占用太多还不如 I2C 加地址方便。选型时先定数据形态再定速率最后看引脚预算这几步走完基本不会错。3.3 混合存储方案给我的启发看过 RK3588S 混合存储方案的人都知道SPI NOR Flash 放引导程序PCIe NVMe SSD 放系统和数据这种组合已经成为很多板卡的标准做法。SPI NOR 容量小、便宜、启动时序简单NVMe 容量大、带宽高、适合大文件读写。启动流程大致是 ROM 引导 SPI NOR 里的 u-bootu-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/CPHAUART 乱码波特率不一致、地线没接统一波特率重新检查 TX/RX 连线I2C 设备找不到资源代码 12 资源冲突查 ACPI 中断路由更新 BIOSI2S 没声音时钟没产生、极性错误先用逻辑分析仪量 BCLK/LRCLKSPI DMA 丢字节内存对齐问题缓冲区用 uint32_t 对齐GT911 触摸没反应复位时序不对先查 INT/RST 引脚时序再查 I2C 地址I2C NACK 一直出现设备没上电或地址错检查地址位和器件供电SPI CS 时间不够从机时序要求严软件片选手动控制 CS 提前拉低FT232R/FT231X 驱动异常驱动残留官方驱动干净重装排查顺序也很重要永远先看供电再看波形最后猜代码。供电都错了后面全白费。4.3 最后再分享一点我自己的使用体会我做了这么多年固件最常用的组合还是“UART 打日志 SPI 搬数据 I2C 挂传感器 I2S 传音频”。每次新板子点亮第一步就是先把 UART 调通能看到日志板子就算活了。然后逐个调 I2C 和 SPI 外设最后才碰 I2S。别一上来就四路总线一起开会把问题绞在一起。时序这种东西看着手册是一回事用逻辑分析仪实测是另一回事。很多看起来像“玄学”的问题最后都会落到一句很朴素的结论供电不够、引脚配置错、或者没看示波器。把这四条总线背后的数据形态想清楚项目就成功一半了。