三种协议概览

UART I2C SPI
线数 2 (TX/RX) 2 (SCL/SDA) 4 (SCK/MOSI/MISO/CS)
通信方式 异步,全双工 同步,半双工 同步,全双工
速度 最高 ~10Mbps 标准 100K/400K/1M 最高 ~50Mbps
拓扑 一对一 多设备总线(7位地址128节点) 一主多从(CS 片选)
硬件复杂度 中(需上拉电阻) 高(多路 CS)
STM32 外设 USART/UART I2C1/I2C2 SPI1/SPI2

一、UART

原理

1
2
3
4
5
6
7
8
9
10
11
12
13
起始位 + 数据位(5-9) + 校验位(可选) + 停止位(0.5/1/1.5/2)

空闲: ──────────────────────────────
│ S │ D0 D1 D2 D3 D4 D5 D6 D7 │ P │ STP │
└┐ └──────────────────────────┘ └┐ └── 空闲
└─ 0 ─┘ └─ 1 ─┘ └─ 1 ─┘

S=起始位(0), D=数据(LSB先), P=校验(可选), STP=停止位(1)

例:发送 0x41 (01000001b),8N1 格式
0 10000010 1
↑ ↑ ↑
起 LSB→MSB 停

异步机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UART 没有时钟线。接收方如何知道什么时候采样?

① 双方约定同样的波特率(如 115200bps)
② 起始位的下降沿触发接收时钟
③ 接收方在每位中点采样(16 倍过采样)

发送方 TX ─── 115200bps ───→ RX 接收方

┌──1 bit @115200 = 8.68μs──┐
│ │
TX: │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 0 │ 1 │
└────┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

RX 在 ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
每个位中点采样(由内部 16× 时钟定位)

STM32 HAL 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
UART_HandleTypeDef huart1;

void MX_USART1_UART_Init(void) {
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
}

三种收发方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 1. 阻塞(简单,浪费时间) */
uint8_t rx;
HAL_UART_Transmit(&huart1, "hello\r\n", 7, 1000); // 发,超时1s
HAL_UART_Receive(&huart1, &rx, 1, 1000); // 收,阻塞等待

/* 2. 中断(推荐,不占 CPU) */
HAL_UART_Transmit_IT(&huart1, tx_buf, len); // 发送中断
HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 每收1字节进中断
// 回调:void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

/* 3. DMA(大批量数据,CPU 完全不参与) */
HAL_UART_Transmit_DMA(&huart1, tx_buf, 1024);
HAL_UART_Receive_DMA(&huart1, rx_buf, 1024);
// 回调:void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

常用场景

场景 配置 说明
串口调试 115200-8N1 printf 重定向
GPS 模块 9600-8N1 标准 NMEA 协议
HC-05 蓝牙 9600-8N1 AT 命令 + 透传
RS485 +485 芯片 + 方向控制 差分信号,抗干扰
Modbus 9600-8E1 工业协议,偶校验

二、I2C

原理

1
2
3
4
5
6
7
8
9
10
11
12
       SCL  ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐
┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─

SDA ────┐ ┌─┐ ┌─┐
└─────┘ └─────┘ └──
START D7..D0 ACK STOP

帧格式:
START │ 地址(7b)+R/W │ ACK │ 数据(8b) │ ACK │ ... │ STOP

所有从设备共挂 SCL/SDA,主机通过地址选择通信对象。
SCL 始终由主机产生,SDA 双向开漏(需外部上拉电阻 4.7KΩ)。

起始/停止条件

1
2
3
4
5
6
7
8
9
10
11
12
SCL 高电平时 SDA 的变化定义起始和停止:

SDA ──┐ ┌──
└─ START │ ┌─ STOP
SCL ──────┐ ┌────┼───┼────
│ │ │ │
① ② ③ ④

① SDA 下降 + SCL 高 → START
② SDA 数据变化只能在 SCL 低时
③ SDA 上升 + SCL 高 → STOP
④ 总线释放(SDA 被上拉电阻拉高)

STM32 HAL 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
I2C_HandleTypeDef hi2c1;

void MX_I2C1_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000; // 400KHz 快速模式
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0; // 主机模式地址忽略
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}

读写操作

1
2
3
4
5
6
7
8
9
10
11
/* 写寄存器:设备地址(7位<<1) + 寄存器地址 + 数据 */
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, reg_addr,
I2C_MEMADD_SIZE_8BIT, &data, 1, 100);

/* 读寄存器:同上,方向改为读 */
HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, reg_addr,
I2C_MEMADD_SIZE_8BIT, buf, len, 200);

/* 简单读写(无寄存器地址的设备) */
HAL_I2C_Master_Transmit(&hi2c1, dev_addr << 1, tx_buf, len, 100);
HAL_I2C_Master_Receive(&hi2c1, dev_addr << 1, rx_buf, len, 200);

常见设备地址

设备 7位地址 8位地址(<<1) 说明
MPU6050 0x68 0xD0 AD0=GND
SSD1306 OLED 0x3C 0x78 I2C 显示
AT24C02 EEPROM 0x50 0xA0 存储芯片
DS3231 RTC 0x68 0xD0 实时时钟
BME280 0x76 0xEC 温湿压传感器

常见问题

1
2
3
4
5
6
7
8
9
10
1. SCL/SDA 一直被拉低
→ 某个从设备把总线锁住了。断电重开,或给 SCL 发 9 个时钟脉冲释放总线。

2. HAL_I2C_Mem_Read 返回 HAL_ERROR
→ 设备地址不对(注意是 7 位还是 8 位)
→ 上拉电阻太大(>10KΩ),换 4.7KΩ 或 2.2KΩ

3. 偶尔通信失败
→ 上拉电阻接触不良
→ 线太长(>30cm),降低频率到 100KHz

三、SPI

原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        SCK ─────┐  ┌───┐  ┌───
└──┘ └──┘ └── (主机产生)
MOSI ────┬───┬───┬───┬─── (Master Out Slave In)
MISO ────┬───┬───┬───┬─── (Master In Slave Out)
CS ─────┘ └─ (片选,低有效)

全双工:MOSI 和 MISO 同时传输,每个时钟沿交换一位。

┌── CPOL=0 ──┐ ┌── CPOL=1 ──┐
SCK: ───┐ ┌─── ───┐ ┌───
└─────────┘ └─────────┘

CPHA=0: 第1个边沿采样 CPHA=1: 第2个边沿采样

共 4 种模式 (CPOL/CPHA),主从双方必须一致。
模式 0 (CPOL=0,CPHA=0) 最常用。

STM32 HAL 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SPI_HandleTypeDef hspi1;

void MX_SPI1_Init(void) {
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式
hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 双线全双工
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制 CS
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 72M/16=4.5M
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位在前
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
HAL_SPI_Init(&hspi1);
}

读写操作

1
2
3
4
5
6
7
8
9
10
11
12
/* 收发同时(全双工) */
uint8_t tx[2] = {0x80, 0x00}; // 寄存器地址 + 空字节
uint8_t rx[2];
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // CS 拉低
HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 100);
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // CS 拉高

/* 仅发送 */
HAL_SPI_Transmit(&hspi1, tx_buf, len, 100);

/* DMA 高速传输 */
HAL_SPI_TransmitReceive_DMA(&hspi1, tx, rx, 1024);

四、三种协议怎么选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──────────────────────────────────────────────────┐
│ 选型流程 │
├──────────────────────────────────────────────────┤
│ 需要多个设备通信? │
│ ├─ 是 → 设备 > 2? │
│ │ ├─ 是 → 每个设备需要独立 CS → 选 SPI │
│ │ └─ 否 → 选 I2C(两根线可挂 128 设备) │
│ └─ 否 → 选 UART(最简单) │
│ │
│ 速度要求? │
│ ├─ > 1Mbps → 选 SPI │
│ ├─ 100K~1M → I2C 快速模式 / UART 高波特率 │
│ └─ < 100K → 都可以 │
│ │
│ 线数限制? │
│ ├─ 2 线 → UART / I2C │
│ └─ 不限 → SPI │
│ │
│ 距离 > 1m? │
│ ├─ 是 → RS232/RS485(UART 电平转换) │
│ └─ 否 → 都可以 │
└──────────────────────────────────────────────────┘

五、实战对比(读取一个传感器)

步骤 UART(GPS 模块) I2C(MPU6050) SPI(BME280)
初始化 HAL_UART_Init() HAL_I2C_Init() HAL_SPI_Init()
读取 HAL_UART_Receive_IT() 逐字节解析 HAL_I2C_Mem_Read() 读寄存器 HAL_SPI_TransmitReceive() 全双工
解析 查 NMEA 协议,找 $GPRMC 高/低字节拼 `(H<<8) L`
调试 逻辑分析仪 + 串口助手 逻辑分析仪看 SCL/SDA 逻辑分析仪 4 线

六、CubeMX 配置速查

外设 CubeMX 要点
USART1 Mode: Asynchronous, PA9=TX, PA10=RX
USART2 PA2=TX, PA3=RX(常用蓝牙接这里)
I2C1 PB6=SCL, PB7=SDA, Speed: Fast Mode
I2C2 PB10=SCL, PB11=SDA
SPI1 PA5=SCK, PA7=MOSI, PA6=MISO
SPI2 PB13=SCK, PB15=MOSI, PB14=MISO

七、调试技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 逻辑分析仪是最重要的调试工具
→ Saleae Logic 8 (淘宝 ~30 元克隆版)
→ 直接看波形,比代码调试快 10 倍

2. UART 调试:先用 USB-TTL 接电脑验证 TX/RX
→ 确保波特率、数据位、停止位一致
→ 短接 TX/RX 自发自收验证硬件

3. I2C 调试:
→ 用逻辑分析仪看 START/STOP/ACK
→ i2c_scanner 代码扫描所有地址,确认设备在线

4. SPI 调试:
→ CS 线最容易忘记拉低/拉高
→ 检查 CPOL/CPHA 是否与数据手册一致