假设你是一家快递公司的调度员。客户要寄一个文件,你可以派一个骑电动车的小哥直接送到对方手里——点对点、随叫随到,这就是 UART;如果客户是 VIP,要求高速准确,你需要给他开通一条专属车队通道,配上独立的通知线和时钟线,这就是 SPI;而如果整个园区有几十个设备要交换数据,最省钱的办法是开通一辆共享巴士,所有人共用两条主干道,上车时报一声门牌号,这就是 I2C。STM32 作为嵌入式开发中最常用的主控芯片,把这三套通信系统做成了标准外设。你不需要亲手铺每一根线,但必须知道该派哪辆车、怎么发车、遇上拥堵怎么处理。本文就用这个物流调度比喻,带你彻底搞懂三大串行通信协议的原理、对比和 STM32 实战配置。

岗位关联

掌握 UART / SPI / I2C,等于拿到嵌入式通信的驾照——无论是传感器采集、屏显驱动还是多设备组网,你都能在 STM32 上从容选型、正确配置、快速定位问题。

目录
  1. 可视化演示
  2. UART通信基础
  3. SPI总线机制
  4. I2C协议详解
  5. 三大协议对比
  6. HAL库配置实战
  7. 典型应用场景
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

\u5c55\u793aUART/SPI/I2C\u5f15\u811a\u8fde\u63a5\u62d3\u6251\u3001DMA\u6570\u636e\u4f20\u8f93\u6d41\u7a0b(CPU\u91ca\u653e+IRQ\u5b8c\u6210)\u3001\u534f\u8bae\u6027\u80fd\u5bf9\u6bd4(\u901f\u5ea6/\u5f15\u811a/CPU\u5360\u7528/\u4f20\u8f93\u8ddd\u79bb)\uff0c\u5207\u6362\u6a21\u5f0f\u67e5\u770b\u3002

本节配有交互动效(进入视口后自动加载,约 3KB)。

UART/SPI/I2C\u5f15\u811a\u8fde\u63a5\u62d3\u6251 UART: TX/RX\u4e24\u7ebf,\u5f02\u6b65\u901a\u4fe1,\u652f\u6301\u8fdc\u8ddd\u79bb SPI: MOSI/MISO/SCK/CS\u56db\u7ebf,\u540c\u6b65\u5168\u53cc\u5de5 UART(2\u7ebf) < I2C(2\u7ebf) < SPI(4\u7ebf)

UART通信基础

快递公司最基层的业务,是让一个骑电动车的小哥把一个文件袋从 A 点送到 B 点。他不进中转站、不与别人拼车,一次只服务一个客户。这种“一人一单、点对点、你情我愿”的派送方式,就是 UART 的缩影——在嵌入式世界里,它是最朴素也最通用的通信方式。

UART 全称是 Universal Asynchronous Receiver/Transmitter,通用异步收发器。它只需要两根信号线:发送端 TX 和接收端 RX,交叉相连,再加一条公共地 GND 形成电流回路。由于双方没有共享的时钟线,通讯全靠“约定好蹬车节奏”——这就是波特率(baud rate)。

每次发送一个字节,不是把 8 个 bit 直接扔到线上,而是封装成标准信封:先是起始位(低电平)提醒对方“注意,我要开口了”;接着 8 位数据;可选的校验位用于初略检错;最后停止位(高电平)表示“这封信到此结束”。STM32 的 USART 硬件会自动完成这套封包与拆包流程,开发者只需关心数据本身。

回到物流中心:选 UART,就像派一辆电动车去跑一对一专线。接线少、代码简单、可靠性高,但一台车只能服务一个客户。调试串口、GPS 模块这类“点对点”设备,UART 永远是最省心的选择。

Baud=fPCLKUSARTDIV×16\displaystyle \text{Baud} = \frac{f_{PCLK}}{\text{USARTDIV} \times 16}
波特率就是快递小哥蹬车的频率:频率越高,每秒送的比特越多,但距离越短、受干扰风险越大。
  1. 外设时钟 f_{PCLK}STM32 挂在 APB 总线上的时钟源,相当于快递公司提供的总运力。
    fPCLKf_{PCLK}
  2. 分频系数 USARTDIV可配置的分频值,决定把总运力切分成多少份。
    USARTDIV\text{USARTDIV}
  3. ×16 倍过采样每一位信号用 16 个采样点确认,防止噪声干扰,相当于送文件时反复核对门牌号。
    ×16\times 16

SPI总线机制

现在把场景切换到 VIP 调度台:调度员(主机)掌握一条专属车队,车队里有四条固定线路——时钟线 SCK 负责给所有人打拍子,数据线 MOSI/MISO 负责双向传文件,每条专属线路 CS 则像“呼叫铃”,调度员按下哪条,哪个客户(从机)就准备接货。这套“四线制、一呼一应”的制度,就是 SPI。

SPI 全称 Serial Peripheral Interface,串行外设接口。它的核心是主从架构:主机决定何时通信、速率多快,从机完全被动响应。每根 CS 线单独控制一个从机,低电平表示选中,高电平表示空闲,有效避免了“该哪个客户收货”的歧义。

SPI 是同步通信,SCK 每跳变一次,双方就按预定沿采样一个 bit。这里的“预定沿”由 CPOL(时钟极性)和 CPHA(时钟相位)两个参数决定:CPOL 决定空闲时时钟是高还是低,CPHA 决定数据在上升沿还是下降沿采样。四个组合对应四种工作模式,像四套不同路况的行车规则。

回到调度台:SPI 专线专用、全双工、速度极快,特别适合显示屏、Flash 存储这类“大数据量、主从明确”的场合。但每加一个客户就要多铺一条 CS 线,引脚消耗也随之上升。

fSCK=fPCLKBRR+1\displaystyle f_{SCK} = \frac{f_{PCLK}}{\text{BRR} + 1}
SCK 频率是车队的速度上限,BRR 是刹车系数,系数越小跑得越快,但路面太差时容易“翻车”(误码)。
  1. 外设时钟 f_{PCLK}SPI 外设的时钟输入,相当于车队发动机的总马力。
    fPCLKf_{PCLK}
  2. 波特率分频 BRR一个 0~255 的分频值,相当于把发动机动力通过变速箱降到轮速。
    BRR\text{BRR}
  3. +1 修正硬件保证分频基数至少为 1,避免除零冲突。
    +1+1

I2C协议详解

如果园区里有几十个住户,你不可能为每家都开一条 VIP 专线。更聪明的做法是开通一辆共享巴士:只有两条主干道 SDA(数据线)和 SCL(时钟线),所有住户共用。司机(主机)发车前先广播一个门牌号(设备地址),听到自己门牌号的住户才应答(ACK)上车。这套“两根线、报站上车”的制度,就是 I2C。

I2C 全称 Inter-Integrated Circuit,集成电路间总线。它靠地址寻址机制区分设备:7 位地址最多可挂 128 个设备,加上 10 位扩展模式则更多。通信前主机先在 SDA 上产生起始条件(SCL 为高时 SDA 产生下降沿),然后发送地址字节,末尾带一位读写标志;目标从机拉低 SDA 给出 ACK 应答,表示“收到,我准备好了”。

每个数据字节传输后,接收方都必须回应一位 ACK。如果从机太忙或地址不存在,它会不拉低 SDA,主机收到 NACK 后就知道“这趟白跑了”,可以终止或重试。多主机环境下,I2C 还自带时钟同步与仲裁机制,好比两辆巴士同时按喇叭,总线会自动判定谁能先走。

回到小区门口:I2C 用最少的线换来了最大的挂载量,虽然速度追不上 SPI,但对温湿度传感器、RTC 时钟这类低速外设来说,“省线、多挂载、带应答”才是真正的刚需。

fSCL=fPCLK2×(CCR+1)\displaystyle f_{SCL} = \frac{f_{PCLK}}{2 \times (\text{CCR} + 1)}
I2C 的车速由 CCR 控制,数值越小跑得越快,但所有乘客(设备)都必须跟得上这个节奏。
  1. 外设时钟 f_{PCLK}I2C 外设的时钟源,相当于巴士发动机的功率。
    fPCLKf_{PCLK}
  2. 2 倍分频I2C 需要生成 50% 占空比的时钟,先对输入时钟进行二分频。
    ×2\times 2
  3. 时钟控制寄存器 CCR决定 SCL 高电平持续多久,相当于路况允许的车速档位。
    CCR+1\text{CCR} + 1

三大协议对比

物流公司同时拥有三种交通工具:电动车、VIP 车队和共享巴士。调度员最头疼的问题永远是——这单货该派哪辆车?在嵌入式开发中,这个决策对应 UART、SPI、I2C 的三选一。

三种协议的差异首先在速率:I2C 标准模式 100 kbit/s,快速模式 400 kbit/s,高速模式 3.4 Mbit/s;UART 常见 9600~921600 baud,最高可达数 Mbps;SPI 则通常跑几十 Mbps 甚至更高,因为它的同步机制不受异步容差限制。其次是引脚:UART 最少 2 根数据线,SPI 固定 4 根信号线起步,I2C 无论挂多少设备都只用 2 根总线。最后是拓扑:UART 只支持点对点,SPI 是一主多从,I2C 支持多主多从。

调度员的经验是:能省线就选 I2C,要速度就选 SPI,最简单直接的点对点就选 UART。很多系统甚至三者并用——I2C 采集传感器,SPI 驱动屏幕,UART 连接外部模组,各司其职。

协议选择={UART点对点、异步、中低速SPI主从、同步、高速I2C多设备、两线、低速\text{协议选择} = \begin{cases} \text{UART} & \text{点对点、异步、中低速} \\ \text{SPI} & \text{主从、同步、高速} \\ \text{I2C} & \text{多设备、两线、低速} \end{cases}
没有最好的协议,只有最合适的组合。选型第一步先看拓扑,第二步看速度,第三步看引脚预算。
  1. 第一行 UART适合两个设备间简单的双向通信,比如调试串口、GPS 模块。
    点对点、异步、中低速\text{点对点、异步、中低速}
  2. 第二行 SPI适合主从明确、吞吐量大的场景,比如 TFT 屏、Flash 存储。
    主从、同步、高速\text{主从、同步、高速}
  3. 第三行 I2C适合一总线上挂多个低速外设,比如传感器阵列、RTC。
    多设备、两线、低速\text{多设备、两线、低速}

HAL库配置实战

物流中心的调度不能靠嘴喊,必须有一套标准化操作手册。STM32 的 HAL 库正是这套手册——它把底层寄存器封装成结构体和函数,开发者只需要“填表格、按按钮”就能发车。

以 UART 为例,初始化分三步:先填 UART_HandleTypeDef 结构体(波特率、字长、校验位、停止位);再配置对应 GPIO 为复用功能模式;最后调用 HAL_UART_Init() 让外设生效。SPI 和 I2C 的套路完全一致,只是结构体字段换成了时钟极性、地址模式、应答开关等。HAL 库还提供了三种收发模式:轮询模式适合短暂传输,中断模式适合不定长接收,DMA 模式适合大批量搬运。

回到操作手册:轮询是“调度员盯着电话等”,中断是“电话响了再跑一趟”,DMA 则是“安排一个自动分拣机器人,根本不占用调度员时间”。实际项目中,高波特率的 UART、高速 SPI 屏显几乎都离不开 DMA。

CPU 占用率=T中断处理T帧间隔×100%\displaystyle \text{CPU 占用率} = \frac{T_{\text{中断处理}}}{T_{\text{帧间隔}}} \times 100\%
传输任务占据 CPU 的时间比例越低,主控就有越多的精力跑算法、做控制,这就是 DMA 的核心价值。
  1. 中断处理时间 T_{\text{中断处理}}CPU 进入中断服务函数执行搬运指令所需的时间,相当于调度员亲自跑一趟货仓。
    T中断处理T_{\text{中断处理}}
  2. 帧间隔 T_{\text{帧间隔}}两次数据帧之间的总时间,相当于快递班车的发车间隔。
    T帧间隔T_{\text{帧间隔}}
  3. × 100%将占用比例换算成百分比,便于直观评估系统余量。
    ×100%\times 100\%

典型应用场景

现在来验收整个物流园。聪明的调度员不会只认一种车,而是根据货物类型派单:一辆电动车、一支 VIP 车队、一辆共享巴士,三套系统协同运转。

传感器数据读取是 I2C 的主场。一颗 STM32 可以用两根线挂载 SHT30 温湿度、BMP280 气压、ADS1115 模拟量采集等多个传感器,每个设备有独立地址,主机轮询读取即可,布线成本极低。屏幕显示驱动则更依赖 SPI,例如 2.4 寸 TFT 屏需要每秒刷新 30 帧 RGB 数据,I2C 的 400 kbit/s 根本喂不饱,而 SPI 配合 DMA 可以轻松把像素数据倒进显存。多设备组网则要看现场需求:调试串口用 UART 连接 WiFi 模组,机箱内多板互联用 I2C,板间高速交换用 SPI,各取所长。

回到园区调度台:I2C 适合“少线多挂载”的感知层,SPI 适合“高速大流量”的表现层,UART 适合“简单直接”的通信层。理解每种协议的先天性格,你就能在 STM32 上搭出一套高效稳妥的通信网络。

f数据=FPS×W×H×bpp\displaystyle f_{\text{数据}} = \text{FPS} \times W \times H \times \text{bpp}
屏幕刷新率 FPS、分辨率 W×H、每像素位数 bpp 三者相乘,就是每秒需要搬运的数据量。这个数超过总线带宽时,画面就会卡顿撕裂。
  1. 刷新率 FPS每秒显示多少帧画面,相当于快递车队每秒发几班车。
    FPS\text{FPS}
  2. 分辨率 W \times H画面总像素数,相当于每班车要装的包裹数量。
    W×HW \times H
  3. 每像素位数 bpp每个像素用多少 bit 表示颜色,相当于每个包裹的重量。
    bpp\text{bpp}

AI 应用场景

边缘 AI 传感器网关

阶段:数据采集与预处理

解决的问题:传统多传感器采用模拟信号或并口采集,接线复杂、抗干扰差。AI 模型需要统一格式的数字数据流,用 I2C 总线挂载多颗数字传感器,配合 DMA 定时读取,可以稳定地把数据送入归一化与推理流水线。

具体案例:智慧大棚部署 20 个节点,每个节点用 STM32 通过 I2C 读取 SHT30 温湿度和 BMP280 气压,再通过 UART 把 JSON 打包发给边缘 AI 盒子(如 RK3568)做病虫害预测。I2C 省线、UART 长距可靠,两根线协同完成感知上传。

高速 SPI 屏显 AI 边缘盒子

阶段:推理结果可视化

解决的问题:AI 门禁机识别到人脸后,需要立即在 LCD 上绘制框和文字。I2C 速度不足会导致画面撕裂,SPI 配合 DMA 则能以几十 Mbps 刷新率把结果实时投屏。

具体案例:AI 门禁机在 STM32 上跑轻量级人脸检测,检测框坐标通过 SPI 总线写入 2.4 寸 TFT 屏的显存,CPU 全程只负责画框,像素搬运完全交给 DMA,帧率稳定在 30 FPS 以上。

多协议融合的传感器中枢

阶段:数据融合与时间戳对齐

解决的问题:无人机、机器人等设备常同时存在 IMU(SPI)、GPS(UART)、气象站(I2C)等异构传感器。STM32 作为协议转换中枢,把不同速率、不同格式的数据统一编码后交给上层 AI 算法。

具体案例:无人机飞控中,IMU 以 1 kHz 频率通过 SPI 输出姿态数据,GPS 以 10 Hz 通过 UART 输出定位,外部气象站通过 I2C 每 100 ms 更新一次。STM32 利用定时器为每组数据打时间戳,再通过 DMA 缓冲发送给算法端做融合解算。

Python 演示

python29 行
class SerialFrame:    def __init__(self, data: int, baud: int = 9600):        self.data = data & 0xFF        self.baud = baud     def uart_bits(self):        parity = 0        bits = [0]  # start bit low        for i in range(8):            bit = (self.data >> (7 - i)) & 1            bits.append(bit)            parity ^= bit        bits.append(parity)  # even parity bit        bits.append(1)       # stop bit high        return bits     def spi_bits(self):        return [(self.data >> (7 - i)) & 1 for i in range(8)]     def i2c_packet(self, addr):        addr_bits = [(addr >> i) & 1 for i in range(6, -1, -1)]        return [0] + addr_bits + [0, 0]  # start + addr + write + ACK  if __name__ == "__main__":    frame = SerialFrame(0xA5, baud=115200)    print("UART bits:", frame.uart_bits())    print("SPI  bits:", frame.spi_bits())    print("I2C  packet:", frame.i2c_packet(0x50))
目的:用纯 Python 模拟三大协议在物理层的位序列生成,帮助理解 UART 帧封装、SPI 原始数据位和 I2C 地址报站机制。
  1. self.data = data & 0xFF把输入数据截断为 8 bit,确保后面位运算不越界。
  2. bits = [0]UART 帧的第一位是低电平起始位,等于快递小哥按响车铃。
  3. bit = (self.data >> (7 - i)) & 1从最高位到最低位逐位取出数据,像拆包裹一样依次排列。
  4. parity ^= bit计算偶校验位,让整帧 1 的个数保持偶数。
  5. bits.append(parity)把校验位塞进帧中间,相当于给信封贴上防拆封条。
  6. bits.append(1)停止位必须为高电平,告知接收方这封信封结束。
  7. [(self.data >> (7 - i)) & 1 for i in range(8)]SPI 没有起止位,只有裸的 8 位数据,直接按位输出。
  8. addr_bits = [(addr >> i) & 1 for i in range(6, -1, -1)]I2C 先广播 7 位地址,从最高位开始报门牌号。
  9. [0] + addr_bits + [0, 0]起始位 + 7 位地址 + 读写标志 + ACK 位,完整还原 I2C 的“报站上车”流程。

常见误区

误区我把两块板子的 TX 和 RX 交叉一连,数据就应该能跑通,为什么收的全是乱码?

正解UART 是异步通信,收发双方没有共享时钟,必须约定相同的波特率。此外还要共地(GND),否则电平基准不同,信号会漂移。就像两个快递小哥约定在 14 点碰头,但一个看的是北京时间,一个看的是东京时间,永远碰不上。

为何常见:教程里大多只演示两根数据线,初学者容易忽略 GND 和波特率这两个隐藏前提。

误区I2C 设备地址像网络 MAC 一样全球唯一,写程序时随便填一个就能找到设备。

正解I2C 的 7 位地址由芯片引脚电平或寄存器配置决定,通常是硬编码的。例如 MPU6050 的地址是 0x68 或 0x69,取决于 AD0 引脚电平。地址错了,从机不会响应 ACK,总线直接产生 NACK。

为何常见:很多开发者把 I2C 的“地址”类比成网络 IP,没意识到它更接近楼栋号——开发商建楼时就固定了。

误区SPI 支持几十 Mbps,那我直接把分频系数设成最小,让通信跑到最快,性能一定最好。

正解SCK 频率受从机最高工作频率、PCB 走线长度、信号完整性和供电噪声共同限制。速度拉满后,上升沿变缓、串扰变大,反而出现随机错位。正确做法是查阅从机 datasheet 的 max SCK,预留 1.5~2 倍余量。

为何常见:人们习惯用“跑满分”来证明系统能力强,忽略了通信本质是“收发双方都能稳定采样”。

误区DMA 传输不占 CPU,开个 `HAL_UART_Receive_DMA` 就完事,数据自动进内存,不需要写回调。

正解DMA 只是搬运工,搬完仍然要通过中断通知 CPU“货到了”。如果不开 DMA 传输完成中断、不实现 HAL_UART_RxCpltCallback,数据永远躺在缓冲区里没人处理。半传输中断、错误中断同样需要处理。

为何常见:“不占 CPU”让人误以为完全零交互,实际上 DMA 只是把“搬运”这项工作外包了,验收签收环节依然需要 CPU 参与。

面试题

Q1. UART 帧格式中,哪一位用于标识一帧的开始?

单选题 · easy

  • A.起始位低电平← 正确答案
  • B.停止位高电平
  • C.校验位偶校验
  • D.数据位最高位

答案:A。UART 空闲时总线为高电平,发送方先拉低一个 bit 周期作为起始位,接收方检测到这个下降沿后开始采样后续 8 位数据。停止位表示结束,校验位用于检错,都不是起始标志。

Q2. SPI 设置 CPOL=1 表示什么?

单选题 · mid

  • A.时钟空闲为高电平← 正确答案
  • B.时钟空闲为低电平
  • C.数据在上升沿采样
  • D.数据在下降沿采样

答案:A。CPOL 决定 SCK 空闲时的电平,CPOL=1 即空闲高电平,CPOL=0 即空闲低电平;CPHA 才决定采样沿。四者组合出 SPI 的四种工作模式,选错会导致工程师最常见的数据错位。

Q3. I2C 标准模式(Standard Mode)的最高速率是多少?

单选题 · hard

  • A.100 kbit/s← 正确答案
  • B.400 kbit/s
  • C.1 Mbit/s
  • D.3.4 Mbit/s

答案:A。I2C 有标准模式 100 kbit/s、快速模式 400 kbit/s、高速模式 3.4 Mbit/s 和超快模式 5 Mbit/s。题目问的是标准模式,只有 100 kbit/s。选项 B 是快速模式,D 是高速模式,C 是 I2C 没有的档位。

Q4. UART 异步通信中,收发双方必须一致的是什么?

单选题 · mid

  • A.波特率参数← 正确答案
  • B.引脚电平
  • C.中断优先级
  • D.从机地址

答案:A。UART 没有时钟线,完全靠波特率对齐每个 bit 的宽度。波特率不一致时,采样点会逐渐偏移,最终产生乱码。UART 也不存在从机地址概念,中断优先级是软件层面的配置。

Q5. 下列关于 SPI 特性的描述,正确的有哪些?

多选题 · mid

  • A.支持一主多从← 正确答案
  • B.全双工通信← 正确答案
  • C.需要应答信号
  • D.每个从机有独立CS← 正确答案

答案:ABD。SPI 通过独立 CS 线选择从机,天然支持一主多从;MOSI 和 MISO 同时工作,是全双工。但 SPI 没有 ACK 机制,发送完一个字节后只能靠软件约定来确认,这正是它和 I2C 的显著区别。

Q6. I2C 总线上的起始条件是什么?

单选题 · easy

  • A.SCL 高时 SDA 下拉← 正确答案
  • B.SCL 低时 SDA 上拉
  • C.同时拉高两根线
  • D.同时拉低两根线

答案:A。I2C 规定 SCL 为高电平期间,SDA 产生一个下降沿即为起始条件。这个组合是硬件同步的唯一“发车信号”,所有从机都要监听它来准备接收地址。

Q7. STM32 HAL 中,串口接收使用 DMA 的主要优点是?

单选题 · mid

  • A.节省 CPU 占用← 正确答案
  • B.支持多主机仲裁
  • C.提高波特率上限
  • D.自动产生停止位

答案:A。DMA 外设在内存和外设间自动搬运数据,不打断 CPU 执行主流程,因此大幅降低 CPU 负载。它改变不了波特率上限,也与多主机仲裁、停止位生成无关。

Q8. 某 STM32 的 UART 外设时钟为 16 MHz,USARTDIV 设为 26,理论波特率约为多少?

单选题 · extreme

  • A.38462← 正确答案
  • B.62500
  • C.25806
  • D.9600

答案:A。波特率 = 16 MHz / (26 × 16) = 16,000,000 / 416 ≈ 38461.5。USARTDIV 是 16 倍过采样的分频系数,选项 B 是忽略过采样直接除 16 MHz 的结果,C 是误用其他分频公式。

Q9. I2C 通信过程中,哪些情况会导致主机需要做错误处理?

多选题 · hard

  • A.无应答 NACK← 正确答案
  • B.仲裁丢失← 正确答案
  • C.地址冲突← 正确答案
  • D.DMA 超时

答案:ABC。目标从机不存在或繁忙时主机收到 NACK;多主机同时发起通信时仲裁失败的设备需退避;总线上多个设备地址相同时会发生冲突。DMA 超时是 STM32 传输层的软件问题,不是 I2C 协议本身的错误类型。

Q10. 关于三大协议的引脚描述,正确的有哪些?

多选题 · easy

  • A.UART 使用TX/RX数据线← 正确答案
  • B.SPI需要四根信号线← 正确答案
  • C.I2C只需SDA/SCL总线← 正确答案
  • D.三种协议都支持多主机

答案:ABC。UART 用 TX 和 RX 两根数据线点对点连接;SPI 至少四根信号线(SCK、MOSI、MISO、CS);I2C 只用 SDA 和 SCL 两根总线。UART 不支持多主机,SPI 多主机能力有限,只有 I2C 原生支持多主多从,因此 D 错误。