STM32 串口通信:串口的接收和发送详解

1. 前言

串口通信是 STM32 开发中最常用的通信方式之一。
在实际项目中,串口经常用于程序调试、数据打印、上位机通信、蓝牙模块、WiFi 模块、GPS 模块以及各种串口外设模块。

本文主要基于 STM32 标准外设库,讲解 USART 串口的发送和接收过程。
示例芯片以 STM32F103 系列 为例,使用 USART1,对应引脚为:

功能 引脚
USART1_TX PA9
USART1_RX PA10

本文重点讲解以下内容:

  • 串口通信的基本原理;
  • USART1 的初始化配置;
  • 串口发送数据的实现;
  • 串口接收数据的实现;
  • 串口中断接收的实现;
  • 常见标志位和常见问题。

2. 串口通信的基本原理

STM32 中常用的串口外设是 USART

USART 全称是:

Universal Synchronous / Asynchronous Receiver Transmitter

也就是通用同步 / 异步收发器。

在普通单片机项目中,我们大多数时候使用的是 USART 的 异步通信模式,也就是平时说的 UART 串口通信。

串口通信最少需要三根线:

引脚 作用
TX 发送数据
RX 接收数据
GND 共地

连接时要注意交叉连接:

STM32_TX  --->  外部设备_RX
STM32_RX  <---  外部设备_TX
STM32_GND --->  外部设备_GND

也就是说,发送端要接对方的接收端,接收端要接对方的发送端。
在这里插入图片描述


3. 串口数据帧格式

串口发送数据时,并不是一次性发送一个完整字节,而是按照固定的数据帧格式一位一位发送。

常见的数据帧格式如下:

起始位 + 数据位 + 校验位 + 停止位

最常见的配置是:

115200 8N1

含义如下:

配置 含义
115200 波特率
8 8 位数据位
N 无校验位
1 1 位停止位

例如发送字符 A,它的 ASCII 码是 0x41
USART 外设会根据设置好的波特率,把这个字节拆成一位一位的数据从 TX 引脚发出。


4. USART1 初始化流程

使用 STM32 标准库配置 USART1,一般需要完成以下步骤:

  1. 开启 GPIOA 和 USART1 外设时钟;
  2. 配置 PA9 为复用推挽输出;
  3. 配置 PA10 为浮空输入;
  4. 配置 USART1 的波特率、数据位、停止位、校验位;
  5. 使能 USART1;
  6. 如果使用中断接收,还需要配置 NVIC 和 USART 接收中断。

5. USART1 初始化代码

下面给出 USART1 的完整初始化函数。

#include "stm32f10x.h"

void USART1_Init(uint32_t baudrate)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    /* 1. 开启 GPIOA 和 USART1 时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

    /* 2. 配置 PA9 为 USART1_TX:复用推挽输出 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 3. 配置 PA10 为 USART1_RX:浮空输入 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 4. 配置 USART1 参数 */
    USART_InitStructure.USART_BaudRate = baudrate;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;

    USART_Init(USART1, &USART_InitStructure);

    /* 5. 使能 USART1 */
    USART_Cmd(USART1, ENABLE);
}

这个函数完成了 USART1 的基础配置。
如果只需要轮询发送和轮询接收,到这里就已经可以使用了。


6. 串口发送一个字节

标准库中,串口发送数据使用:

USART_SendData(USART1, data);

但是发送前后需要配合标志位判断。

常用发送标志位有两个:

标志位 含义
USART_FLAG_TXE 发送数据寄存器为空
USART_FLAG_TC 发送完成

其中:

  • TXE 表示数据寄存器空了,可以写入下一个字节;
  • TC 表示最后一位数据已经真正从 TX 引脚发送完成。

发送一个字节的函数如下:

void USART1_SendByte(uint8_t data)
{
    USART_SendData(USART1, data);

    /* 等待发送数据寄存器为空 */
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}

这个函数的作用是发送一个字节。
TXE 置位后,说明可以继续发送下一个字节。


7. 串口发送字符串

在实际调试中,我们更常用的是发送字符串。

void USART1_SendString(char *str)
{
    while (*str != '\0')
    {
        USART1_SendByte((uint8_t)(*str));
        str++;
    }
}

使用示例:

USART1_SendString("Hello STM32\r\n");

串口助手中会显示:

Hello STM32

其中 \r\n 表示回车换行,方便串口助手换行显示。


8. 串口轮询接收一个字节

串口接收数据时,需要判断接收数据寄存器是否非空。

常用接收标志位是:

标志位 含义
USART_FLAG_RXNE 接收数据寄存器非空

RXNE 置位时,说明 USART 已经接收到一个字节,可以读取。

轮询接收一个字节的函数如下:

uint8_t USART1_ReceiveByte(void)
{
    uint8_t data;

    /* 等待接收数据寄存器非空 */
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);

    data = USART_ReceiveData(USART1);

    return data;
}

这个函数会一直等待,直到串口收到一个字节。

使用示例:

uint8_t ch;

ch = USART1_ReceiveByte();
USART1_SendByte(ch);

这段代码可以实现最简单的串口回显:

电脑发送什么,STM32 返回什么

9. 轮询接收的缺点

轮询接收的代码简单,但它有一个明显缺点:

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

程序会一直卡在这里等待数据。
如果串口一直没有收到数据,主程序就无法继续向下执行。

所以,轮询接收适合简单测试,不适合复杂项目。

在实际项目中,如果系统还需要做按键扫描、传感器采集、OLED 刷新、电机控制,就更推荐使用 串口中断接收


10. USART1 中断接收配置

使用串口中断接收,需要在初始化时增加两部分配置:

  1. 配置 NVIC;
  2. 使能 USART 接收中断。

完整初始化代码如下:

void USART1_IT_Init(uint32_t baudrate)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    /* 1. 开启 GPIOA、USART1 和 AFIO 时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

    /* 2. 配置 PA9 为 USART1_TX */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 3. 配置 PA10 为 USART1_RX */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 4. 配置 USART1 参数 */
    USART_InitStructure.USART_BaudRate = baudrate;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;

    USART_Init(USART1, &USART_InitStructure);

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

    /* 6. 使能 USART1 接收中断 */
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    /* 7. 使能 USART1 */
    USART_Cmd(USART1, ENABLE);
}

这里开启的是 USART_IT_RXNE 中断。
它表示:当串口接收数据寄存器非空时,产生中断。


11. USART1 中断服务函数

中断服务函数名称必须和启动文件中的中断函数名一致。
USART1 的中断服务函数一般写成:

void USART1_IRQHandler(void)
{
    uint8_t data;

    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        data = USART_ReceiveData(USART1);

        USART1_SendByte(data);

        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

这段代码的作用是:

  1. 判断是否发生了 RXNE 接收中断;
  2. 读取接收到的数据;
  3. 将收到的数据重新发送出去;
  4. 清除中断标志位。

这样就实现了一个基于中断的串口回显功能。


12. 关于 RXNE 中断清除的说明

在 STM32 中,RXNE 标志通常可以通过读取 USART_DR 数据寄存器来清除。
USART_ReceiveData(USART1) 本质上就是读取数据寄存器。

所以在很多代码中,会看到这样的写法:

data = USART_ReceiveData(USART1);

读取之后,RXNE 标志就会被清除。

不过在标准库写法中,也常见手动调用:

USART_ClearITPendingBit(USART1, USART_IT_RXNE);

对于初学者来说,保留这句更直观。
但是需要理解,真正关键的是及时读取接收数据寄存器。


13. 完整示例代码

下面给出一个完整的 USART1 中断接收回显示例。

#include "stm32f10x.h"

void USART1_SendByte(uint8_t data);
void USART1_SendString(char *str);
void USART1_IT_Init(uint32_t baudrate);

int main(void)
{
    USART1_IT_Init(115200);

    USART1_SendString("USART1 Init OK\r\n");

    while (1)
    {
        /*
         * 主循环可以执行其他任务。
         * 串口接收由中断完成,不会阻塞主程序。
         */
    }
}

void USART1_IT_Init(uint32_t baudrate)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

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

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

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

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

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    USART_Cmd(USART1, ENABLE);
}

void USART1_SendByte(uint8_t data)
{
    USART_SendData(USART1, data);

    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}

void USART1_SendString(char *str)
{
    while (*str != '\0')
    {
        USART1_SendByte((uint8_t)(*str));
        str++;
    }
}

void USART1_IRQHandler(void)
{
    uint8_t data;

    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        data = USART_ReceiveData(USART1);

        USART1_SendByte(data);

        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
}

14. 串口发送和接收的技术总结

14.1 发送过程

串口发送的核心流程如下:

用户数据
  ↓
USART_SendData()
  ↓
写入 USART_DR 数据寄存器
  ↓
USART 外设移位发送
  ↓
TX 引脚逐位输出

发送时常用 TXETC 两个标志位:

标志位 技术含义
TXE 发送数据寄存器为空,可以写入下一个数据
TC 当前数据已经完整发送结束

普通字符串发送一般判断 TXE 即可。
如果是半双工通信、RS485 方向切换等场景,则更需要关注 TC 标志。


14.2 接收过程

串口接收的核心流程如下:

RX 引脚接收电平变化
  ↓
USART 外设按波特率采样
  ↓
接收完成一个字节
  ↓
数据进入 USART_DR
  ↓
RXNE 标志置位
  ↓
CPU 读取 USART_DR

接收时最常用的标志位是 RXNE

标志位 技术含义
RXNE 接收数据寄存器非空,说明已经收到一个字节

只要 RXNE 置位,就说明可以读取数据。


15. 常见问题分析

15.1 串口没有输出

常见原因:

  • 没有开启 GPIOA 时钟;
  • 没有开启 USART1 时钟;
  • PA9 没有配置为复用推挽输出;
  • 串口助手选择了错误的 COM 口;
  • TX 和 RX 连接错误;
  • GND 没有共地。

15.2 串口接收乱码

常见原因:

  • STM32 和串口助手波特率不一致;
  • 数据位、停止位、校验位不一致;
  • 系统时钟配置错误;
  • 串口助手编码显示方式不正确;
  • 使用了错误的晶振频率。

15.3 中断不进入

常见原因:

  • 没有配置 NVIC;
  • 没有使能 USART_IT_RXNE
  • 中断函数名写错;
  • 启动文件中中断名称和代码中的函数名不一致;
  • 没有开启 USART 外设;
  • RX 引脚配置错误。

15.4 数据量大时丢数据

常见原因:

  • 中断处理函数中执行了太多耗时操作;
  • 波特率较高,CPU 来不及处理;
  • 没有及时读取 USART_DR
  • 出现了 ORE 溢出错误;
  • 接收缓冲区设计不合理。

如果项目中串口数据量较大,建议使用:

USART + DMA + 空闲中断

这种方式更适合接收不定长数据和高频数据。


16. 小结

STM32 串口通信的关键是理解发送和接收背后的流程。

发送时,程序通过 USART_SendData() 将数据写入 USART 数据寄存器,然后由 USART 外设通过 TX 引脚逐位发送。

接收时,USART 外设从 RX 引脚采样数据,当收到一个完整字节后,RXNE 标志置位,CPU 读取 USART_DR 得到数据。

对于初学者来说,可以按照下面的顺序学习:

串口 GPIO 配置
↓
USART 参数配置
↓
轮询发送
↓
轮询接收
↓
中断接收
↓
DMA 接收

只要掌握了 USART 的初始化、TXETCRXNE 这些关键点,STM32 串口通信的大部分问题都可以定位和解决。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐