04-UART串口通信详解.md 19 KB

STM32 UART 串口通信

1. 通信协议基础

串行通信 vs 并行通信

  • 并行通信:多个数据线同时传输多位数据(如 8 位或 16 位),速度快但占用引脚多,适合短距离
  • 串行通信:一个数据线逐位传输,速度相对慢但占用引脚少,适合长距离

串口属于串行通信,用一根 TX 发送、一根 RX 接收。

同步 vs 异步

  • 同步通信:发送方和接收方共用同一个时钟线,双方在时钟的节拍下传输数据,如 SPI、I2C
  • 异步通信:没有单独的时钟线,发送方和接收方各自约定好通信速率(波特率)来采样数据,如 UART

异步通信的难点在于:接收方不知道数据何时到来,必须靠起始位来同步。

UART 数据帧结构

UART 数据帧与时序参数 上图:UART 串口帧结构及不同波特率/校验方式下的时序对比(来源:STM32入门教程 PPT 第112-113页)

空闲:  高电平
起始位: 1 位低电平
数据位: 5~8 位 (通常 8 位)
校验位: 可选 (奇校验/偶校验)
停止位: 1/1.5/2 位高电平

UART 发送帧格式 上图:UART 发送数据帧格式(来源:STM32入门教程 PPT 第117页)

以最常用的 8N1(8 数据位、无校验、1 停止位)为例,一帧共 10 位:

起始位(0) + D0 + D1 + D2 + D3 + D4 + D5 + D6 + D7 + 停止位(1)

发送时低位在前(LSB first),先发 D0,最后发 D7。

波特率

波特率(Baud Rate)定义每秒传输的符号数。在 UART 中,一个符号 = 1 位,所以比特率 = 波特率。

常用波特率:9600、115200。

波特率决定了每一位的时长:T = 1 / 波特率。例如 9600 时,每位约 104 μs。

TX 与 RX 交叉连接

两个设备通信时,必须TX 接 RX,RX 接 TX

设备 A (TX) ────────── (RX) 设备 B
设备 A (RX) ────────── (TX) 设备 B

如果 TX 接 TX(RX 接 RX),双方都在发送,无法收到对方的信号。


2. STM32 USART 外设

USART vs UART

  • UART:Universal Asynchronous Receiver/Transmitter,通用异步收发器
  • USART:Universal Synchronous/Asynchronous Receiver/Transmitter,通用同步/异步收发器

STM32 的 USART 支持同步模式(需要额外时钟线),但最常用的还是异步模式(即 UART 模式)。

USART 模块结构框图

USART 模块框图 上图:STM32 USART 模块框图(来源:STM32入门教程 PPT 第115页)

USART 模块内部包含:

  • 发送数据寄存器 (TDR):CPU 写入要发送的数据
  • 发送移位寄存器:将 TDR 中的数据逐位通过 TX 引脚发出
  • 接收移位寄存器:从 RX 引脚逐位接收数据
  • 接收数据寄存器 (RDR):CPU 读取接收到的数据
  • 控制寄存器 (CR1/CR2/CR3):配置模式、中断、使能等
  • 状态寄存器 (SR):查询 TXE、TC、RXNE 等标志位

相关的 GPIO 引脚

以 USART1 为例,默认引脚为 PA9 (TX) 和 PA10 (RX):

USART 模块 TX 引脚 RX 引脚
USART1 PA9 PA10
USART2 PA2 PA3
USART3 PB10 PB11

TX 引脚配置为复用推挽输出(AF_PP),RX 引脚配置为上拉输入(IPU)或浮空输入。


3. 数据发送

CubeMX 配置

  1. 选择 USART1,Mode 设置为 Asynchronous
  2. 参数设置:
    • Baud Rate: 9600
    • Word Length: 8 Bit
    • Parity: None
    • Stop Bits: 1
  3. PA9 自动配置为 USART1_TX,PA10 自动配置为 USART1_RX
  4. 生成代码

标准外设库:USART_SendData 与轮询发送

发送一个字节的核心操作:

USART_SendData(USART1, Byte);           // 写入数据寄存器
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);  // 等待发送完成

TXE 标志位:Transmit data register Empty。当 TXE = 1 时,表示 TDR 已空,可以写入下一个数据。

注意:TXE 只表示数据已从 TDR 转移到移位寄存器,如果还需要等待所有数据实际发送完成(包括停止位发出),应查询 TC 标志位(Transmission Complete):

while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

寄存器:USART驱动(尚硅谷风格)

void USART_Init(void) {
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // PA9: TX - 复用推挽输出 CNF-10, MODE-11
    GPIOA->CRH |= GPIO_CRH_MODE9;
    GPIOA->CRH |= GPIO_CRH_CNF9_1;  GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
    // PA10: RX - 浮空输入 CNF-01, MODE-00
    GPIOA->CRH &= ~GPIO_CRH_MODE10;
    GPIOA->CRH &= ~GPIO_CRH_CNF10_1;  GPIOA->CRH |= GPIO_CRH_CNF10_0;

    USART1->BRR = 0x271;  // 72MHz/62500=115200
    USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
    USART1->CR1 &= ~(USART_CR1_M | USART_CR1_PCE);  // 8N1
    USART1->CR2 &= ~USART_CR2_STOP;  // 1 stop bit
}

void USART_SendChar(uint8_t ch) {
    while ((USART1->SR & USART_SR_TXE) == 0) {}
    USART1->DR = ch;
}

uint8_t USART_ReceiveChar(void) {
    while ((USART1->SR & USART_SR_RXNE) == 0) {
        if (USART1->SR & USART_SR_IDLE) return 0;
    }
    return USART1->DR;
}
uint8_t buffer[256];
uint16_t size = 0;
uint8_t isOver = 0;

void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        buffer[size++] = USART1->DR;
    } else if (USART1->SR & USART_SR_IDLE) {
        USART1->DR;  // 读DR清除IDLE标志
        isOver = 1;
    }
}
int fputc(int ch, FILE *f) {
    USART_SendChar((uint8_t)ch);
    return ch;
}

Serial_Init 串口初始化(发送模式)

标准库:

void Serial_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1, &USART_InitStructure);

    USART_Cmd(USART1, ENABLE);
}

HAL库:

UART_HandleTypeDef huart1;

void Serial_Init(void)
{
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    huart1.Instance = USART1;
    huart1.Init.BaudRate = 9600;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart1);
}

发送字节、数组、字符串

// 发送单字节
void Serial_SendByte(uint8_t Byte) {
    USART_SendData(USART1, Byte);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}

// 发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length) {
    for (uint16_t i = 0; i < Length; i++)
        Serial_SendByte(Array[i]);
}

// 发送字符串
void Serial_SendString(char *String) {
    for (uint8_t i = 0; String[i] != '\0'; i++)
        Serial_SendByte(String[i]);
}

HAL库 发送等效:

// 发送单字节(阻塞超时模式)
void Serial_SendByte(uint8_t Byte)
{
    HAL_UART_Transmit(&huart1, &Byte, 1, HAL_MAX_DELAY);
}

// 发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
    HAL_UART_Transmit(&huart1, Array, Length, HAL_MAX_DELAY);
}

// 发送字符串
void Serial_SendString(char *String)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)String, strlen(String), HAL_MAX_DELAY);
}

发送数字

数字需要转换为 ASCII 码再发送。例如数字 123 要分别发送 '1'、'2'、'3' 三个字符。

// 10 的 n 次方(内部函数)
uint32_t Serial_Pow(uint32_t X, uint32_t Y) {
    uint32_t Result = 1;
    while (Y--) Result *= X;
    return Result;
}

// 发送数字(按十进制,每位转 ASCII)
void Serial_SendNumber(uint32_t Number, uint8_t Length) {
    for (uint8_t i = 0; i < Length; i++)
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}

printf 重定向

方法一:重定向 fputc(需勾选 Use MicroLIB)

int fputc(int ch, FILE *f) {
    Serial_SendByte(ch);
    return ch;
}

之后可直接使用 printf()

方法二:封装自己的 Printf 函数

#include <stdio.h>
#include <stdarg.h>

void Serial_Printf(char *format, ...) {
    char String[100];
    va_list arg;
    va_start(arg, format);
    vsprintf(String, format, arg);
    va_end(arg);
    Serial_SendString(String);
}

这样可以在多处使用而不受单一 printf 限制。

示例:串口发送

main.c 综合示例:

int main(void) {
    OLED_Init();
    Serial_Init();

    // 1. 发送单字节 0x41 (即字符 'A')
    Serial_SendByte(0x41);

    // 2. 发送数组
    uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
    Serial_SendArray(MyArray, 4);

    // 3. 发送字符串
    Serial_SendString("\r\nNum1=");

    // 4. 发送数字
    Serial_SendNumber(111, 3);

    // 5. 方法一:printf 重定向
    printf("\r\nNum2=%d", 222);

    // 6. 方法二:sprintf + 串口发送
    char String[100];
    sprintf(String, "\r\nNum3=%d", 333);
    Serial_SendString(String);

    // 7. 方法三:自定义 Printf
    Serial_Printf("\r\nNum4=%d", 444);

    while (1);
}

4. 数据接收

接收方式

UART 接收有两种方式:

  1. 轮询接收:CPU 不断查询 RXNE 标志位,效率低
  2. 中断接收:收到数据后触发中断,在中断服务函数中处理,效率高

推荐使用中断接收。

RXNE 中断

RXNE:Read data register Not Empty。当 RXNE = 1 时,表示 RDR 中已收到一个字节,需要及时读取,否则下一个字节到来时会覆盖。

配置中断接收

Serial_Init 中增加:

// 1. 开启串口接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

// 2. 配置 NVIC 分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 3. 配置 NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);

完整代码(包含 GPIO、USART 和中断配置):

标准库:

uint8_t Serial_RxData;
uint8_t Serial_RxFlag;

void Serial_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1, &USART_InitStructure);

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);

    USART_Cmd(USART1, ENABLE);
}

HAL库:

UART_HandleTypeDef huart1;
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;

void Serial_Init(void)
{
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    huart1.Instance = USART1;
    huart1.Init.BaudRate = 9600;
    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);

    __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);

    HAL_NVIC_SetPriority(USART1_IRQn, 1, 1);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
}

中断服务函数

uint8_t Serial_RxData;    // 接收的数据
uint8_t Serial_RxFlag;    // 接收标志位

void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
        Serial_RxData = USART_ReceiveData(USART1);
        Serial_RxFlag = 1;
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

// 获取标志位(读取后自动清零)
uint8_t Serial_GetRxFlag(void) {
    if (Serial_RxFlag == 1) {
        Serial_RxFlag = 0;
        return 1;
    }
    return 0;
}

// 获取数据
uint8_t Serial_GetRxData(void) {
    return Serial_RxData;
}

HAL库 中断接收等效:

uint8_t rx_buffer;                         // 接收缓冲区

// 启动中断接收(在 main 中调用一次即可)
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);

// 中断回调函数(HAL 自动调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        Serial_RxData = rx_buffer;
        Serial_RxFlag = 1;
        HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);  // 重新启动接收
    }
}

回传实验

int main(void) {
    OLED_Init();
    OLED_ShowString(1, 1, "RxData:");
    Serial_Init();

    while (1) {
        if (Serial_GetRxFlag() == 1) {
            uint8_t RxData = Serial_GetRxData();
            Serial_SendByte(RxData);                    // 回传
            OLED_ShowHexNum(1, 8, RxData, 2);           // OLED 显示
        }
    }
}

5. 中断进阶:串口中断接收实验

可变间隔闪烁

利用串口接收命令,控制 LED 闪烁速度:

uint16_t BlinkInterval = 500;   // 默认 500ms

void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
        uint8_t cmd = USART_ReceiveData(USART1);
        
        // 解析命令:控制 LED 闪烁间隔
        if (cmd >= '0' && cmd <= '9')
            BlinkInterval = (cmd - '0') * 100 + 100;   // 100ms ~ 1000ms
        
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

命令解析思路

通过串口发送特定格式的命令(如 LED1_ONLED1_OFF),在中断中用状态机解析,可实现更复杂的控制。


5.5 DMA 模式收发

在大量数据收发场景下,使用 DMA 可以释放 CPU 资源。HAL 库提供了 DMA 版本的收发函数,配合 USART 的硬件 DMA 请求使用。

CubeMX 配置

  1. USART1 的 DMA Settings 标签页中添加:
    • USART1_TX — DMA1 Channel4 (或相应通道),Direction Memory to Peripheral
    • USART1_RX — DMA1 Channel5 (或相应通道),Direction Peripheral to Memory
  2. DMA 参数:Mode Normal(或 Circular 用于连续接收), Increment Address 勾选

HAL 库 DMA 发送

// 发送数组(DMA 方式,非阻塞)
HAL_UART_Transmit_DMA(&huart1, TxBuffer, Size);

// 查询发送是否完成
while (HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);

HAL 库 DMA 接收

// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, RxBuffer, BufferSize);

// 停止 DMA 接收
HAL_UART_DMAStop(&huart1);

轮询接收(HAL 库)

// 阻塞式接收一个字节(超时等待)
uint8_t Serial_ReceiveByte(void)
{
    uint8_t data;
    HAL_UART_Receive(&huart1, &data, 1, HAL_MAX_DELAY);
    return data;
}

标准库与 HAL 库 API 对照

功能 标准库 HAL 库
发送字节 USART_SendData() HAL_UART_Transmit()
接收字节 USART_ReceiveData() HAL_UART_Receive()
中断发送 USART_ITConfig(USART_IT_TXE) HAL_UART_Transmit_IT()
中断接收 USART_ITConfig(USART_IT_RXNE) HAL_UART_Receive_IT()
DMA 发送 HAL_UART_Transmit_DMA()
DMA 接收 HAL_UART_Receive_DMA()
查询 TXE USART_GetFlagStatus(USART_FLAG_TXE) __HAL_UART_GET_FLAG(UART_FLAG_TXE)
查询 RXNE USART_GetFlagStatus(USART_FLAG_RXNE) __HAL_UART_GET_FLAG(UART_FLAG_RXNE)
使能 USART USART_Cmd(ENABLE) HAL_UART_Init() 自动使能
中断使能 USART_ITConfig() __HAL_UART_ENABLE_IT()

6. 常见问题与注意事项

波特率不一致

发送方和接收方的波特率必须一致,否则收到乱码。常见错误:一方是 9600,另一方是 115200。

TX 接 TX

双设备通信时 TX 必须接 RX,如果 TX 接 TX,双方都在发送、无法接收。可尝试用 USB 转 TTL 模块在电脑上测试。

MicroLIB 与半主机模式

使用 printf 重定向必须:

  1. 勾选工程选项 Target → Use MicroLIB
  2. 重写 fputc 函数

如果未勾选 MicroLIB,会进入半主机模式导致程序卡死。

接收丢数据

中断接收时,中断服务函数应尽快读取 RDR,否则下一个字节会覆盖当前数据。如果数据处理耗时较长,可采用环形缓冲区(Ring Buffer)

串口电压电平

STM32 的串口是 3.3V TTL 电平。如果需要与 RS232(±12V)设备通信,需加 MAX3232 电平转换芯片。与 USB 通信则需 CH340 / CP2102 等 USB 转 TTL 芯片。


7. 参考代码索引

  • 9-1 串口发送\Hardware\Serial.c — 标准外设库串口发送(字节/数组/字符串/数字/printf)
  • 9-1 串口发送\User\main.c — 发送示例主程序
  • 9-2 串口发送+接收\Hardware\Serial.c — 增加中断接收
  • 9-2 串口发送+接收\User\main.c — 接收回传示例
  • 26-[STM32 HAL库][中断]串口中断接收实验 — 中断进阶:可变间隔闪烁

本笔记综合整理自三套 STM32 教程:

  1. [STM32 HAL库] 串口基础 / 发送实验 / 接收实验 / 中断实验
  2. 铁头山羊 UART 系列(3.1~3.7)
  3. 江协科技 USART 系列(9-1~9-6)