STM32 串口通信:串口的接收和发送详解
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,一般需要完成以下步骤:
- 开启 GPIOA 和 USART1 外设时钟;
- 配置 PA9 为复用推挽输出;
- 配置 PA10 为浮空输入;
- 配置 USART1 的波特率、数据位、停止位、校验位;
- 使能 USART1;
- 如果使用中断接收,还需要配置 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 中断接收配置
使用串口中断接收,需要在初始化时增加两部分配置:
- 配置 NVIC;
- 使能 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);
}
}
这段代码的作用是:
- 判断是否发生了 RXNE 接收中断;
- 读取接收到的数据;
- 将收到的数据重新发送出去;
- 清除中断标志位。
这样就实现了一个基于中断的串口回显功能。
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 引脚逐位输出
发送时常用 TXE 和 TC 两个标志位:
| 标志位 | 技术含义 |
|---|---|
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 的初始化、TXE、TC、RXNE 这些关键点,STM32 串口通信的大部分问题都可以定位和解决。
更多推荐
所有评论(0)