I2C、SPI、UART、I2S四种通信协议选型指南:从原理到实战
1. 四种通信协议到底该怎么选从踩坑现场说起刚入行那会儿我接手一个传感器采集板的小项目主控和一颗六轴传感器之间要传数据同时还要驱动一块小尺寸的段码屏另外留一个口子给上位机调试。当时脑子里第一反应就是“随便挑一个呗能通就行”结果第一版板子打回来屏幕闪烁、传感器数据偶尔丢包、调试口还跟下载口打架。后来老老实实把 I2C、SPI、UART、I2S 这四种最常见的板级通信方式从头捋了一遍才发现每一种协议背后都对应着一类非常明确的适用场景选错了不是“能不能通”的问题而是“稳不稳、快不快、省不省”的问题。这篇内容就是把我这些年在这四种协议上踩过的坑、做过的取舍、以及实际调试时的一些经验整理出来。不管你是刚接触单片机、正在画第一块板子的新手还是已经做过几个项目、但对协议底层细节还有点模糊的开发者都能从里面找到可以直接参考的东西。核心关键词就四个I2C、SPI、UART、I2S我会围绕它们的电气特性、时序逻辑、典型应用和实际选型展开尽量说人话把那些手册里写得绕来绕去的地方用生活化的方式讲清楚。先说结论性的东西方便你快速建立印象UART 是点对点的异步串行口最简单也最通用I2C 是两根线的总线适合挂一堆低速器件SPI 是四根线的高速同步口适合刷屏、读 Flash、接高速传感器I2S 则是专门为音频数据流设计的同步串行协议跟前面三个不是一个赛道。下面逐个拆开讲把每个协议的“为什么这么设计”和“实际怎么用”都过一遍。2. UART最老牌也最容易被低估的点对点通信2.1 UART 的本质没有时钟线的“约定式”通信UART 全称通用异步收发器关键词就在“异步”两个字上。它没有单独的时钟线发送方和接收方靠事先约定好的波特率来对齐每一位数据。你可以把它想象成两个人约好每隔一秒说一个字谁也没拿秒表全靠各自的节奏感。节奏对上了通信就正常节奏偏了数据就乱套。一个完整的 UART 数据帧由起始位、数据位、校验位、停止位组成。起始位是一个低电平告诉对方“我要开始发了”接着是 5 到 9 位数据位通常用 8 位校验位可选用来做简单的奇偶校验最后是停止位拉高表示这一帧结束。空闲时线路保持高电平这也是为什么 UART 的波形在逻辑分析仪上看是一串串“低-数据-高”的脉冲。实际配置时最常见的组合是115200-8-N-1意思是波特率 115200、8 位数据、无校验、1 位停止位。这个组合在绝大多数调试场景下都能直接用STM32 的标准库和 HAL 库默认例程也基本是这个配置。2.2 波特率误差为什么会导致乱码很多人遇到过“串口助手收到一堆乱码”的情况第一反应是线接错了其实更常见的原因是波特率误差。UART 接收方通常在每一位的中间时刻采样如果双方波特率偏差超过一定程度采样点就会漂移到位的边缘甚至隔壁位数据自然就错了。一般来说波特率误差控制在2% 以内比较安全超过 3% 就很容易出问题。以 STM32F103 为例它的 USART 波特率由外设时钟分频得到公式是波特率 fCK / (16 × USARTDIV)其中 USARTDIV 是一个带小数部分的定点数整数部分写在 BRR 寄存器的高 12 位小数部分写在低 4 位。比如系统时钟 72MHz、要得到 115200 波特率USARTDIV 72000000 / (16 × 115200) ≈ 39.0625整数部分 39小数部分 0.0625 × 16 1所以 BRR 写 0x271。算下来实际波特率是 72000000 / (16 × 39.0625) 115200误差为 0。但如果换成 9600 波特率USARTDIV 468.75小数部分 0.75 × 16 12BRR 写 0x1D4C实际波特率 72000000 / (16 × 468.75) 9600同样无误差。这就是为什么常用波特率都是经过精心挑选的能整除时钟的优先选。2.3 UART 的阻塞、中断和 DMA 三种收发方式UART 的收发方式直接决定了 CPU 的占用率这也是实际项目里最需要权衡的地方。阻塞方式最简单调用发送函数后 CPU 一直等到数据发完才返回。适合发几个字节的调试信息但如果你要连续发几百字节CPU 就被占死了其他任务全得等着。中断方式是每发完一个字节触发一次中断在中断里填下一个字节。CPU 占用率降下来了但中断频率高的时候比如波特率 921600中断开销本身就很可观。DMA 方式是把一整块数据交给 DMA 控制器由它自动搬运到 USART 数据寄存器搬完再触发一次中断通知 CPU。这是大数据量收发的首选。我在 STM32F103 上用标准库做过一个 UART DMA 接收的项目配置流程大致是先初始化 DMA 通道设置外设地址为 USART 数据寄存器、内存地址为接收缓冲区、传输方向为外设到内存、循环模式打开然后在 USART 初始化里使能 DMA 接收请求最后在 DMA 传输完成中断里处理数据。这里有个坑DMA 循环模式下你无法直接知道当前收到了多少字节需要配合空闲中断IDLE来判断一帧数据是否结束。空闲中断的用法是使能 USART 的 IDLE 中断当总线空闲一个字节时间后触发此时读取 DMA 剩余传输数量就能算出实际接收长度。注意UART 的 TX 和 RX 要交叉连接即 A 的 TX 接 B 的 RXA 的 RX 接 B 的 TXGND 必须共地。只接 TX 和 RX 不接 GND通信会时好时坏这是新手最常犯的错误之一。2.4 UART 转 USB调试口的标准做法现在大部分开发板上的调试口都是通过一颗 USB 转 UART 芯片实现的常见的有 FT231X、CH340、CP2102 等。FT231X 是 FTDI 家的芯片驱动成熟在 Windows、Linux、macOS 上都能免驱或简单装驱使用稳定性口碑很好。它的典型接法是USB 端接电脑UART 端接 MCU 的 TX、RX、GND有些还带 RTS、CTS 流控引脚但一般调试用不上。用 FT231X 的时候有个细节它的 VCCIO 引脚决定了 UART 侧的电平接 3.3V 就是 3.3V 电平接 5V 就是 5V 电平必须和 MCU 的电平匹配。我曾经把 5V 的 FT231X 直接怼到 3.3V 的 MCU 上结果通信偶尔出错查了半天才发现是电平不匹配导致接收端判据临界。3. I2C两根线挂一堆器件的总线艺术3.1 I2C 的物理层为什么必须用开漏加外部上拉I2C 只有两根线SCL时钟和 SDA数据。这两根线都是开漏输出结构也就是说器件只能把线拉低不能主动拉高。线要变高靠的是外部的上拉电阻。这个设计乍看很别扭但它是 I2C 能挂多个器件的关键。想象一条绳子每个器件都只能往下拽不能往上推。只要有一个器件拽着绳子就是低的所有器件都松手绳子才被弹簧拉高。这样多个器件同时接在同一根线上谁也不会因为“我想输出高、你想输出低”而短路。这就是线与逻辑也是 I2C 支持多主多从的物理基础。上拉电阻的取值是个经典问题。阻值太小上升沿快但功耗大器件拉低时灌电流也大阻值太大上升沿慢高速通信时波形还没到高电平就被拉低了通信直接失败。经验公式是Rp(max) tr / (0.8473 × Cb)其中 tr 是允许的最大上升时间标准模式 1000ns快速模式 300nsCb 是总线电容包括走线、引脚、器件电容一般按 100pF 到 400pF 估算。以快速模式 300ns、总线电容 200pF 为例Rp(max) ≈ 300ns / (0.8473 × 200pF) ≈ 1.77kΩ。所以快速模式下上拉电阻通常选 1.5k 到 2.2k标准模式 100kHz 下选 4.7k 到 10k 都行。我遇到过“I2C 上拉电阻小了不通信”的情况当时用的是 1k 电阻总线电容又比较大结果器件拉低时灌电流超过规格SDA 被拉不到足够低的电平接收方识别不出低电平。换成 2.2k 后立刻正常。所以上拉电阻不是越小越好要结合总线电容和器件灌电流能力综合选。3.2 I2C 时序起始、地址、应答、数据、停止I2C 的一帧完整传输包括起始条件、从机地址、读写位、应答位、数据字节、应答位、停止条件。起始条件是 SCL 高电平时 SDA 从高变低停止条件是 SCL 高电平时 SDA 从低变高。这两个条件很特殊因为正常数据位变化时 SDA 只能在 SCL 低电平期间改变SCL 高电平期间 SDA 必须稳定。所以起始和停止是唯一两个在 SCL 高电平期间改变 SDA 的动作接收方靠这个特征识别帧边界。地址通常是 7 位加上一位读写位组成一个字节。比如某 EEPROM 的地址是 0x50写操作就是 0xA0读操作就是 0xA1。发送完地址后从机如果存在并且空闲会在第 9 个时钟周期把 SDA 拉低这就是应答位ACK如果从机没响应SDA 保持高就是非应答NACK。主机读到 NACK 就知道从机没接或者地址错了。数据字节是 8 位高位先发每发完一个字节从机回一个 ACK。整个传输过程中SCL 完全由主机控制多主竞争时另说从机只是被动响应。3.3 I2C 读写 EEPROM 的典型流程以 24C02 这类 EEPROM 为例写一个字节的流程是主机发起始条件发从机地址 写位0xA0等 ACK发要写入的内存地址比如 0x00等 ACK发要写入的数据字节等 ACK发停止条件读一个字节稍微绕一点需要先“写地址”再“读数据”主机发起始条件发从机地址 写位0xA0等 ACK发要读取的内存地址等 ACK再发一次起始条件这叫重复起始发从机地址 读位0xA1等 ACK读取一个字节然后主机发 NACK 表示“我读够了”发停止条件这个“先写地址再读”的流程是 I2C 器件的通用套路因为 I2C 没有单独的地址线器件内部的寄存器地址必须通过数据线传进去。3.4 I2C 扩展与 PMBus 的关系I2C 的扩展能力很强最直接的就是IO 扩展芯片比如 PCF8574一颗芯片用 I2C 两根线就能扩展出 8 个 GPIO地址通过 A0、A1、A2 引脚配置同一条总线上最多挂 8 片扩展出 64 个 IO。还有I2C 多路复用器比如 TCA9548A可以把一条 I2C 总线分成 8 条独立子总线解决地址冲突问题。PMBus 是在 I2C 基础上发展出来的电源管理协议物理层完全兼容 I2C区别在于它定义了一套标准的命令集用来读电压、电流、温度、配置电源输出等。你可以理解为 PMBus 是 I2C 的“应用层方言”专门用于电源管理场景。调试 PMBus 的时候逻辑分析仪抓到的波形和 I2C 一模一样只是数据内容有特定含义。4. SPI四根线跑出高速同步通信4.1 SPI 的四根线各自干什么SPI 全称串行外设接口标准配置是四根线SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。跟 I2C 最大的区别是SPI 有独立的时钟线数据在时钟边沿采样所以速度可以做得非常高几十 MHz 甚至上百 MHz 都很常见。SPI 是主从架构主机产生时钟和片选从机被动响应。片选信号 CS 拉低表示选中该从机拉高表示释放。每个从机需要一根独立的 CS 线所以 SPI 挂 N 个从机就需要 N 根片选线这是它相比 I2C 在引脚数量上的劣势。4.2 SPI 的四种模式CPOL 和 CPHA 的组合SPI 有四种工作模式由CPOL时钟极性和 CPHA时钟相位两个参数决定模式CPOLCPHA空闲时钟采样边沿Mode 000低上升沿Mode 101低下降沿Mode 210高下降沿Mode 311高上升沿Mode 0 和 Mode 3 最常用。选哪个模式取决于从机器件手册主机和从机必须配置成同一个模式否则数据会错位。我调试 MT6701 磁编码器的时候手册上写的是 Mode 3一开始我按默认的 Mode 0 配读出来的角度值全是乱的改成 Mode 3 后立刻正常。所以拿到一颗 SPI 器件第一件事就是翻手册确认它支持哪个模式。4.3 硬件片选与软件片选的取舍SPI 的片选有两种做法硬件片选和软件片选。硬件片选是用 MCU 的 SPI 外设自动控制 CS 引脚发送数据前自动拉低发完自动拉高。优点是时序精准不占 CPU缺点是 CS 引脚必须映射到 SPI 外设的 NSS 功能上引脚选择不灵活。软件片选是用普通 GPIO 手动控制 CS发送前拉低发完拉高。优点是引脚随便选灵活缺点是需要 CPU 干预高速通信时可能因为 GPIO 操作延迟导致 CS 时序不够紧。实际项目里如果 SPI 速率不高比如 1MHz 以下软件片选完全够用如果速率上到 10MHz 以上建议用硬件片选或者至少把 GPIO 操作优化到最短。我在 STM32F103 上做过一个 SPI 刷屏的项目屏幕刷新率要求高一开始用软件片选发现每次传输之间 CS 拉高的时间太长导致帧率上不去后来改成硬件片选帧率直接翻了一倍。4.4 STM32F103 的 SPI 通过 DMA 读取芯片数据STM32F103 的 SPI 外设支持 DMA 请求配置流程如下初始化 SPI 外设配置模式、速率、数据宽度等初始化 DMA 通道外设地址设为 SPI 数据寄存器内存地址设为接收缓冲区方向为外设到内存使能 SPI 的 DMA 接收请求拉低 CS启动 DMA 传输等待 DMA 传输完成拉高 CS用 CubeMX 配置的话步骤更简单在 SPI 配置页使能 DMA 接收在 DMA 配置页确认通道和优先级生成代码后在发送函数前后手动控制 CS 即可。注意SPI 的 DMA 传输完成中断里不要做耗时操作否则会影响下一次传输的启动。我一般只在中断里置一个标志位主循环里再处理数据。4.5 SPI Flash 和 SPI 加载固件的场景SPI Flash 是最典型的 SPI 应用之一比如 W25Q 系列。它的读写命令有固定格式发送命令字节、发送地址、然后读或写数据。读数据用 0x03 命令写数据前要先发写使能 0x06然后发页编程 0x02写完还要等忙状态位清零。有些方案里主控芯片本身没有内置 Flash需要从外部 SPI Flash 加载固件运行比如 RTL9071CP-VB 这类芯片。这种场景下SPI 的读取速度和可靠性直接决定了启动时间。调试这类方案时逻辑分析仪是必备工具抓一下上电后的 SPI 波形看看命令序列对不对、数据有没有丢比盲猜快得多。5. I2S专为音频数据流设计的同步串行协议5.1 I2S 和 SPI 的本质区别I2S 看起来跟 SPI 很像都是同步串行都有时钟线但它的设计目标完全不同。SPI 是通用数据传输数据内容无所谓I2S 是专门传音频采样数据的对时序精度和声道分离有严格要求。I2S 至少有三根线SCK位时钟、WS声道选择也叫 LRCLK、SD数据。SCK 的频率 采样率 × 位宽 × 声道数。比如 44.1kHz 采样率、16 位位宽、双声道SCK 44100 × 16 × 2 1.4112MHz。WS 的频率等于采样率低电平表示左声道高电平表示右声道。5.2 I2S 的数据对齐方式I2S 的数据对齐有几种标准I2S 标准、左对齐、右对齐。I2S 标准是 WS 变化后的第二个 SCK 边沿开始传数据左对齐是 WS 变化后立刻开始右对齐是数据靠右对齐到 WS 周期末尾。不同音频芯片支持的对齐方式不同配置的时候要跟 codec 手册对齐。我在调试一颗音频 DAC 的时候发现左右声道反了查了半天发现是 WS 的极性配反了。I2S 的 WS 极性可以在寄存器里配置有的 codec 默认低电平左声道有的默认高电平左声道不看手册很容易搞错。5.3 I2S 的典型应用场景I2S 主要用在音频领域接音频 codec、接数字麦克风、接蓝牙音频模块等。跟 UART 和 I2C 不同I2S 不传控制信息只传音频采样数据控制信息通常走 I2C 或 SPI。比如一个典型的音频方案MCU 通过 I2C 配置 codec 的寄存器采样率、增益、通路通过 I2S 传输音频数据。这种“I2C 控制 I2S 数据”的组合非常常见。6. 四种协议横向对比与选型实战6.1 关键参数对比表特性UARTI2CSPII2S线数2TX/RX2SCL/SDA4SCLK/MOSI/MISO/CS3SCK/WS/SD时钟异步同步同步同步速率通常 ≤ 1Mbps100k/400k/1M/3.4M可达数十 Mbps取决于音频采样率拓扑点对点多主多从一主多从点对点为主寻址无7 位地址片选无典型场景调试口、模块通信传感器、EEPROM、PMBusFlash、屏幕、高速传感器音频 codec、数字麦克风6.2 选型决策树拿到一个通信需求可以按下面的顺序判断是不是音频数据流是就选 I2S不是继续。是不是点对点、速率要求不高、想省线是就选 UART。是不是要挂多个低速器件、想省引脚是就选 I2C。是不是要高速传输、数据量大是就选 SPI。这个决策树覆盖了大部分场景但实际项目里经常有交叉。比如一个项目既要接传感器又要接屏幕传感器用 I2C、屏幕用 SPI这是完全正常的不需要强行统一。6.3 常见问题速查表问题现象可能原因排查方向UART 乱码波特率不匹配、电平不匹配核对双方波特率、测 TX/RX 波形I2C 无应答地址错、上拉电阻不合适、器件没供电逻辑分析仪抓起始和地址字节SPI 数据错位模式不匹配、CS 时序问题核对 CPOL/CPHA、检查 CS 拉低时机I2S 声道反WS 极性配置错查 codec 手册确认 WS 极性SPI 通信不生效片选没拉低、时钟没输出先测 SCLK 有无波形再测 CS7. 调试工具与实操心得逻辑分析仪是调这四种协议最实用的工具没有之一。抓 I2C 的时候把 SCL 和 SDA 接上设置好触发条件比如起始条件就能看到完整的地址、数据、ACK 序列。抓 SPI 的时候把 SCLK、MOSI、MISO、CS 四根线都接上设置 SCLK 为采样时钟就能直接解码出数据字节。抓 UART 的时候设置好波特率直接看解码后的字符。抓 I2S 的时候把 SCK、WS、SD 接上设置 WS 为声道指示就能看到左右声道的数据。我个人的习惯是新板子第一次调试通信先不写代码先用逻辑分析仪抓一遍波形确认硬件连接和电平没问题再开始写驱动。这样能省掉大量“到底是硬件问题还是软件问题”的纠结时间。还有一个经验I2C 总线上挂的器件越多总线电容越大上拉电阻就要越小但也不能太小。我一般先用 4.7k 试如果波形上升沿太慢就换 2.2k如果器件灌电流不够就换回 4.7k 并降低速率。SPI 则相反速率越高走线越要短必要时加串联匹配电阻抑制反射。最后说一个关于 STM32 半双工 SPI 的坑。有些器件只支持半双工比如某些单线 SPI 的传感器。STM32 的 SPI 外设支持 BIDIMODE 配置把 MOSI 和 MISO 合并成一根线用。配置的时候要把 BIDIMODE 置 1BIDIOE 控制方向发送时置 1接收时置 0。这个切换时机很关键切早了数据没发完切晚了接收窗口过了我在这上面调了整整一个下午才搞明白。实际做项目的时候这四种协议经常是混着用的。一块板子上 UART 接调试口、I2C 接传感器、SPI 接 Flash、I2S 接音频 codec各司其职。关键不是记住每个协议的每一个寄存器位而是理解它们各自的设计初衷和适用边界这样拿到新器件的时候看一眼手册就能判断该用哪个协议、该怎么配。