跳转至

通信协议(UART / SPI / I2C)

嵌入式系统中,MCU 很少独立工作,几乎总是需要与传感器、模块、PC 或其他 MCU 通信。UART、SPI、I2C 是三种最常用的串行通信协议,掌握它们就能与绝大多数外设模块对话。


零、通信基础概念

在学习具体协议之前,先理解几个关键概念:

概念 含义 举例
串行 vs 并行 数据逐位发送 vs 多位同时发送 UART 是串行;内存总线是并行
同步 vs 异步 有/无独立时钟线 SPI/I2C 是同步(有 CLK);UART 是异步
全双工 vs 半双工 能否同时收发 SPI 全双工;I2C 半双工
主从模式 谁发起通信 SPI/I2C 有主机从机之分

三种协议对比

特性 UART SPI I2C
信号线数量 2(TX、RX) 4+(SCLK、MOSI、MISO、CS) 2(SCL、SDA)
同步方式 异步 同步 同步
双工方式 全双工 全双工 半双工
速度 低(~几 Mbps) 高(~几十 Mbps) 中(100k/400k/3.4Mbps)
设备数量 点对点(一对一) 一主多从(每从需一根 CS) 一主多从(地址区分)
典型应用 串口调试、GPS、蓝牙模块 Flash、LCD 屏、SD 卡 传感器(MPU6050)、EEPROM

一、UART 串口通信

什么是 UART?

UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是最简单、最常用的通信接口。STM32 中称为 USART(多了一个 S = Synchronous,支持同步模式,但通常用异步模式)。

串口调试

串口是嵌入式开发中最重要的调试工具之一。通过 USB 转 TTL 模块将 STM32 的串口连接到 PC,就可以用串口助手收发数据,打印调试信息。

数据帧格式

UART 的每一帧数据格式如下:

空闲(高电平)
      │  起始位  │     数据位      │校验位│ 停止位 │
      ↓         ↓                 ↓      ↓       ↓
 ─────┐    ┌─┬─┬─┬─┬─┬─┬─┬─┐   ┌─┐   ┌───┐
 高    │    │D0│D1│D2│D3│D4│D5│D6│D7│   │ P│   │ 1 │  ─── 高
      └────┘  └─┴─┴─┴─┴─┴─┴─┘   └─┘   └───┘
      起始位=0    LSB ────→ MSB   可选    1或2位
字段 说明
起始位 1 位低电平,通知接收方数据来了
数据位 8 位(或 9 位),LSB 先发
校验位 可选:无校验 / 奇校验 / 偶校验
停止位 1 位或 2 位高电平,标志一帧结束

波特率

波特率 = 每秒传输的 bit 数(bps)。收发双方必须约定相同的波特率。

常用波特率:9600、115200、460800

异步通信的关键

UART 没有时钟线,收发双方各自产生时钟。如果波特率不匹配,数据就会乱码。实际中允许的误差通常 < 2%。

CubeMX 配置串口

  1. Connectivity → USART1
    • Mode: Asynchronous
    • Baud Rate: 115200
    • Word Length: 8 Bits
    • Parity: None
    • Stop Bits: 1
  2. PA9(TX)和 PA10(RX)会自动配置
  3. 如需中断接收:在 NVIC Settings 中勾选 USART1 global interrupt
  4. 生成代码

代码示例:串口发送数据

CubeMX 自动生成 MX_USART1_UART_Init()UART_HandleTypeDef huart1

/* main.c */
#include <stdio.h>
#include <string.h>

/* USER CODE BEGIN 2 */
char msg[] = "Hello, STM32!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
/* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
while (1)
{
    char buf[] = "Running...\r\n";
    HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), HAL_MAX_DELAY);
    HAL_Delay(1000);
    /* USER CODE END WHILE */
}

printf 重定向到串口

main.c 中重定向 printf(需要在 Keil 中勾选 Use MicroLIB):

/* USER CODE BEGIN 0 */
#include <stdio.h>

int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}
/* USER CODE END 0 */

串口中断接收

/* main.c */

uint8_t rx_data;  // 单字节接收缓冲区

/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, &rx_data, 1);  // 启动中断接收(1字节)
/* USER CODE END 2 */

/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        // 回显:收到什么就发回什么
        HAL_UART_Transmit(&huart1, &rx_data, 1, HAL_MAX_DELAY);

        // 重新启动接收(HAL 中断接收是单次的)
        HAL_UART_Receive_IT(&huart1, &rx_data, 1);
    }
}
/* USER CODE END 4 */

HAL 中断接收是单次触发

HAL_UART_Receive_IT() 完成指定字节数的接收后就停止了。必须在回调函数中重新调用才能继续接收。如果忘记重新调用,就只能收到一次数据。


二、SPI 通信

什么是 SPI?

SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的通信协议。

信号线

信号线 全称 方向 功能
SCLK Serial Clock 主→从 时钟信号,由主机产生
MOSI Master Out Slave In 主→从 主机发送数据
MISO Master In Slave Out 从→主 从机返回数据
CS/NSS Chip Select 主→从 片选,低电平有效,选中从机
主机 (STM32)              从机 (Flash/传感器)
┌────────────┐           ┌────────────┐
│    SCLK  ──┼──────────→┼── SCLK     │
│    MOSI  ──┼──────────→┼── MOSI     │
│    MISO  ←─┼───────────┼── MISO     │
│    CS    ──┼──────────→┼── CS       │
└────────────┘           └────────────┘

SPI 四种工作模式(CPOL & CPHA)

SPI 有两个可配置参数:

  • CPOL(Clock Polarity):空闲时时钟电平。0 = 低电平,1 = 高电平
  • CPHA(Clock Phase):数据采样时机。0 = 第一个边沿,1 = 第二个边沿
模式 CPOL CPHA 空闲时 CLK 采样边沿
Mode 0 0 0 上升沿采样
Mode 1 0 1 下降沿采样
Mode 2 1 0 下降沿采样
Mode 3 1 1 上升沿采样

最常用的模式

Mode 0Mode 3 最常用。大多数 SPI 器件(如 W25Q Flash、OLED 屏)默认使用 Mode 0 或 Mode 3。查阅从机数据手册即可确定。

SPI 数据传输过程

SPI 使用移位寄存器实现数据交换——主机和从机各有一个 8 位移位寄存器,在每个时钟周期交换 1 位数据:

主机移位寄存器          从机移位寄存器
[D7|D6|D5|D4|D3|D2|D1|D0] ←→ [D7|D6|D5|D4|D3|D2|D1|D0]
         ↓ MOSI →                  
         ← MISO ↓                  

8 个时钟周期后,主机和从机各自收到了对方的 1 字节数据。SPI 的发送和接收是同时进行的

CubeMX 配置 SPI

  1. Connectivity → SPI1
    • Mode: Full-Duplex Master
    • Prescaler: 8(72MHz / 8 = 9MHz)
    • CPOL: Low
    • CPHA: 1 Edge(即 Mode 0)
    • Data Size: 8 Bits
    • First Bit: MSB
    • NSS Signal: Disable(CS 用软件 GPIO 控制)
  2. PA5(SCK)、PA6(MISO)、PA7(MOSI)自动配置
  3. PA4 手动设置为 GPIO_Output(用作 CS 片选)
  4. 生成代码

SPI 代码示例

/* main.c */

// CS 控制宏
#define CS_LOW()   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define CS_HIGH()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

// SPI 收发一个字节(全双工,发送和接收同时进行)
uint8_t SPI_TransferByte(uint8_t txData)
{
    uint8_t rxData;
    HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, HAL_MAX_DELAY);
    return rxData;
}

// 使用示例:读取 Flash(W25Qxx)的 JEDEC ID
void Flash_ReadID(void)
{
    CS_LOW();                              // 选中从机

    SPI_TransferByte(0x9F);                // 发送读 ID 命令
    uint8_t mfr = SPI_TransferByte(0xFF);  // 读制造商 ID
    uint8_t id1 = SPI_TransferByte(0xFF);  // 读设备 ID 高字节
    uint8_t id2 = SPI_TransferByte(0xFF);  // 读设备 ID 低字节

    CS_HIGH();                             // 释放从机
}

HAL SPI 的几种传输函数

函数 用途
HAL_SPI_Transmit() 只发送
HAL_SPI_Receive() 只接收
HAL_SPI_TransmitReceive() 全双工收发
HAL_SPI_Transmit_IT() 中断方式发送
HAL_SPI_Transmit_DMA() DMA 方式发送

三、I2C 通信

什么是 I2C?

I2C(Inter-Integrated Circuit,集成电路互联)是一种只需 2 根线 就能连接多个设备的同步半双工通信协议。

信号线 功能
SCL 时钟线,主机产生
SDA 数据线,双向传输

为什么只需 2 根线?

I2C 使用开漏输出 + 外部上拉电阻,所有设备共享 SDA 和 SCL 总线。通过地址区分不同从机(每个从机有唯一的 7 位地址)。

I2C 通信时序

一次完整的 I2C 通信包含以下步骤:

         起始位            地址 + 读写位     应答   数据字节     应答    停止位
SCL: ────┐  ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐       ┌────
         └──┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘       │
SDA: ──┐     A6  A5  A4  A3  A2  A1  A0  R/W ACK D7  D6 ... D0  ACK    ┌──┘
       └─────────────────────────────────────────────────────────────────┘
时序 条件 含义
起始位(S) SCL 高电平时,SDA 下降沿 通信开始
停止位(P) SCL 高电平时,SDA 上升沿 通信结束
应答(ACK) 接收方拉低 SDA 收到数据,继续
非应答(NACK) 接收方保持 SDA 高 通信结束或出错

I2C 地址

每个 I2C 从机有一个 7 位地址(也有 10 位地址模式,较少用)。

发送地址时的第 8 位(最低位)表示读写方向:

  • 地址 + 0 = 写操作(主机向从机发数据)
  • 地址 + 1 = 读操作(主机从从机读数据)

MPU6050 的地址

MPU6050 的 7 位地址是 0x68(AD0 接地时)或 0x69(AD0 接高)。

  • 写操作发送:0x68 << 1 | 0 = 0xD0
  • 读操作发送:0x68 << 1 | 1 = 0xD1

CubeMX 配置 I2C

  1. Connectivity → I2C1
    • I2C Speed Mode: Standard Mode(100kHz)或 Fast Mode(400kHz)
  2. PB6(SCL)和 PB7(SDA)自动配置
  3. 生成代码

HAL 硬件 I2C vs 软件 I2C

STM32F1 的硬件 I2C 曾经有一些已知 bug(官方勘误表中列出),但 HAL 库已经做了很多兼容处理。对于大多数应用,直接使用 HAL 硬件 I2C 即可,简单且稳定。如果遇到极端情况也可以用软件模拟。

HAL I2C 代码示例:读写 MPU6050

/* main.c */

#define MPU6050_ADDR  (0x68 << 1)  // HAL 需要 8 位地址(左移 1 位)

// 向 MPU6050 寄存器写一个字节
HAL_StatusTypeDef MPU6050_WriteReg(uint8_t reg, uint8_t data)
{
    return HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, reg,
                             I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);
}

// 从 MPU6050 寄存器读一个字节
uint8_t MPU6050_ReadReg(uint8_t reg)
{
    uint8_t data;
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, reg,
                     I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);
    return data;
}

// 连续读取多个字节(如读取 6 字节加速度数据)
void MPU6050_ReadAccel(int16_t *ax, int16_t *ay, int16_t *az)
{
    uint8_t buf[6];
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x3B,
                     I2C_MEMADD_SIZE_8BIT, buf, 6, HAL_MAX_DELAY);

    *ax = (int16_t)(buf[0] << 8 | buf[1]);
    *ay = (int16_t)(buf[2] << 8 | buf[3]);
    *az = (int16_t)(buf[4] << 8 | buf[5]);
}

// 初始化 MPU6050
void MPU6050_Init(void)
{
    MPU6050_WriteReg(0x6B, 0x00);  // 解除休眠
    MPU6050_WriteReg(0x1C, 0x00);  // 加速度量程 ±2g
    MPU6050_WriteReg(0x1B, 0x00);  // 陀螺仪量程 ±250°/s
}

HAL I2C 函数说明

函数 说明
HAL_I2C_Mem_Write() 向从机指定寄存器写数据(最常用)
HAL_I2C_Mem_Read() 从从机指定寄存器读数据(最常用)
HAL_I2C_Master_Transmit() 向从机发送数据(不指定寄存器)
HAL_I2C_Master_Receive() 从从机接收数据(不指定寄存器)
HAL_I2C_IsDeviceReady() 检测从机是否在线

I2C 设备扫描

调试时可以用这段代码扫描总线上所有设备:

/* USER CODE BEGIN 2 */
printf("Scanning I2C bus...\r\n");
for (uint8_t addr = 1; addr < 128; addr++)
{
    if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 3, 10) == HAL_OK)
    {
        printf("Found device at 0x%02X\r\n", addr);
    }
}
printf("Scan done.\r\n");
/* USER CODE END 2 */

四、三种协议选型指南

场景 推荐协议 原因
PC 调试打印 UART 简单直接,串口助手即可查看
高速读写 Flash/SD 卡 SPI 速度快,全双工
连接多个传感器(IMU、温湿度) I2C 只需 2 根线,节省引脚
显示屏(OLED、TFT) SPI(首选)或 I2C SPI 刷屏速度快
两个 MCU 之间通信 UART 或 SPI 根据速度需求选择
引脚极度紧张 I2C 占用引脚最少

五、常见问题

UART 接收到乱码?

  1. 检查波特率:双方必须完全一致
  2. 检查时钟配置:CubeMX 时钟树配置不对会导致实际波特率偏差
  3. 检查接线:TX 接 RX,RX 接 TX(交叉连接)
  4. 检查电平:STM32 是 3.3V TTL 电平,不能直接接 RS232(±12V)

SPI 读到的全是 0xFF 或 0x00?

  1. 检查接线:MOSI/MISO 是否接反
  2. 检查 CS 引脚:是否正确拉低(HAL 不自动管理软件 CS)
  3. 检查 SPI 模式:CPOL 和 CPHA 是否和从机匹配
  4. 检查速度:Prescaler 是否让速率超过从机支持的最大频率

I2C 设备扫描不到?

  1. 检查上拉电阻:I2C 总线必须有上拉电阻(通常 4.7kΩ)
  2. 检查地址:HAL 使用 8 位地址(7 位地址左移 1 位),确认是否正确
  3. 检查接线:SDA 和 SCL 是否接反
  4. 检查供电:从机是否正常上电

HAL_UART_Transmit 和 HAL_UART_Transmit_IT 的区别?

  • HAL_UART_Transmit():阻塞发送,函数返回时数据已发送完毕
  • HAL_UART_Transmit_IT():非阻塞发送,数据在后台通过中断发送,完成后触发 HAL_UART_TxCpltCallback()
  • HAL_UART_Transmit_DMA():DMA 方式发送,CPU 几乎不参与