玩转 STM32 单片机,肯定离不开串口。串口使用一个称为串行通信协议的协议来管理数据传输,该协议在数据传输期间控制数据流,包括数据位数、波特率、校验位和停止位等。由于串口简单易用,在各种产品交互中都有广泛应用。
但在使用串口通讯的时候,我们并不知道对方会发送多少个数据,也不知道数据什么时候发送完,简单来讲就是:如何确保收到一帧完整的数据?
串口发送的数据有长有短,如果没有接收完整,肯定会影响后续业务的处理。为了接收不定长数据,常见的处理方法有:
1. 固定格式
比如双方约定,一帧的数据以 AA BB 开头,以 BB AA 结尾,这样在从机接收数据的时候,一旦收到 AA BB 字符,就知道对方要发来一个数据包了,然后就把后面发来的数据保存起来,直到接收到 BB AA 为止。
这种方法简单高效,但缺点就是需要每个字符都进行判断,浪费 CPU 资源,增加功耗。
2. 接收中断+超时判断
串口接收到一个数据时,就会触发接收中断。但如何判断数据已经发送完了呢?
通常来讲,两帧数据之间,会有个时间间隔。因此,我们可以使用一个计时器,如果在一个固定的时间点里没接收到新的字符,则认为一帧数据接收完成了。
3. 空闲中断
串口在空闲时,也就是说串口在一段时间里没有接收到新数据,则会触发空闲中断。细心的同学应该发现了,空闲中断实际上跟上面的超时判断是一样样的,只不过空闲中断是硬件自带,但超时判断需要我们自己实现。
所以,一旦接收到空闲中断,可以认为接收到一帧完整的数据。
但是,空闲中断并不是所有的 MCU 都具备,一般高端一点的 MCU 才有,低端一些的 MCU 并没有空闲中断。
1. 源码下载及前置阅读
本文首发 良许嵌入式网 :https://www.lxlinux.net/e/ ,欢迎关注!
本文所涉及的源码及安装包如下(由于平台限制,请点击以下链接阅读原文下载):
https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-rxne-time-out.html
如果你是个零基础的小白,连 STM32 都没见过,我也给你准备了一个保姆级教程,手把手教你搭建好 STM32 开发环境,并教你如何下载程序,简直业界良心!
https://www.lxlinux.net/e/stm32/stm32-quick-start-for-beginner.html
如果你连代码都不知道怎么烧录到 STM32 的,可以参考下文,提供了 5 种代码烧录方式:
https://www.lxlinux.net/e/stm32/five-ways-to-flash-program-to-stm32.html
如果你想自己搭一个属于自己的工程模板,可以参考下面这篇文章:
https://www.lxlinux.net/e/stm32/create-stm32-hal-project-template.html
在本文中,我们详细来介绍如何使用接收中断+超时判断完成不定长数据的接收,对于空闲中断的接收,请查看下文:
https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-idle-dma.html
2. 什么是接收中断?
前文已经提到,当接收到一字节数据时,会触发接收中断,对应串口状态寄存器第 5 位被置 1 ,如下图示。
当我们将 DR 寄存器的值读取之后,该位又被自动清零。
3. 硬件准备
STM32 核心板
本文使用正点原子 M48Z 核心板,小巧好用,某宝 20 元出头。
USB 转 TTL
这种设备主要作用是用来调试或下载程序。价格也很便宜,普遍 5~8 元。
ST-Link
ST-Link 是一种用于 STM32 微控制器的调试和编程工具,它可以通过 SWD 或 JTAG 接口与开发板进行通信。一般也很便宜,七八元左右。
4. 编程实战
在本实验中,我们将串口 1 作为 log 输出端口,串口 2 作为本次实验的接收端口。
因此我们需要提前创建 uart2 模块,包含 uart2.c 及 uart2.h 两个文件,并加载进工程模板。
4.1 串口初始化
串口的初始化大家应该不陌生,主要步骤为:
定义串口句柄 uart2_handle ,并调用 HAL_UART_Init 进行初始化;
初始化串口底层函数,调用 HAL_UART_MspInit 函数。
第一步在 uart2.c 文件里进行:
UART_HandleTypeDef uart2_handle;
void uart2_init(uint32_t baudrate)
{
uart2_handle.Instance = UART2_INTERFACE; /* UART2 */
uart2_handle.Init.BaudRate = baudrate; /* 波特率 */
uart2_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 数据位 */
uart2_handle.Init.StopBits = UART_STOPBITS_1; /* 停止位 */
uart2_handle.Init.Parity = UART_PARITY_NONE; /* 校验位 */
uart2_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */
uart2_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
uart2_handle.Init.OverSampling = UART_OVERSAMPLING_16; /* 过采样 */
HAL_UART_Init(&uart2_handle); /* 使能UART2 */
}
第二步在 usart.c 文件里进行,其实也可以在 uart2.c 文件里做,但我懒~
在最下面一行代码,我们使用 __HAL_UART_ENABLE_IT() 使能接收中断。
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef gpio_init_struct;
if (huart->Instance == USART_UX) /* 如果是串口1,进行串口1 MSP初始化 */
{
....
// 节略串口1相关代码
....
}
else if (huart->Instance == UART2_INTERFACE) /* 如果是UART2 */
{
UART2_TX_GPIO_CLK_ENABLE(); /* 使能UART2 TX引脚时钟 */
UART2_RX_GPIO_CLK_ENABLE(); /* 使能UART2 RX引脚时钟 */
UART2_CLK_ENABLE(); /* 使能UART2时钟 */
gpio_init_struct.Pin = UART2_TX_GPIO_PIN; /* UART2 TX引脚 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(UART2_TX_GPIO_PORT, &gpio_init_struct); /* 初始化UART2 TX引脚 */
gpio_init_struct.Pin = UART2_RX_GPIO_PIN; /* UART2 RX引脚 */
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(UART2_RX_GPIO_PORT, &gpio_init_struct); /* 初始化UART2 RX引脚 */
HAL_NVIC_SetPriority(UART2_IRQn, 0, 0); /* 抢占优先级0,子优先级0 */
HAL_NVIC_EnableIRQ(UART2_IRQn); /* 使能UART2中断通道 */
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); /* 使能UART2接收中断 */
}
}
4.2 判断接收中断
在串口 2 接收中断里,我们先使用 __HAL_UART_GET_FLAG() 函数判断 RXNE 这一位有没有被置 1 ,如果被置 1 ,则代表接收到字符,调用 HAL_UART_Receive() 函数接收字符,并保存于临时变量 receive_data 中。
之后,再调用 HAL_UART_Transmit() 函数将接收到的字符打印出来。
void UART2_IRQHandler(void)
{
uint8_t receive_data = 0;
if(__HAL_UART_GET_FLAG(&uart2_handle,UART_FLAG_RXNE) != RESET)
{
HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000); //串口2接收1位数据
HAL_UART_Transmit(&uart2_handle, &receive_data, 1, 1000); //将接收的数据打印出来
}
}
现在我们通过接收中断就可以实现了自发自收,编译后烧进板子,效果如下:
但现在我们只实现了字符的接收,并不知道一帧的数据什么时候接收完。
在下面的操作里,我们就通过超时的方法,进一步判断数据是否完成传输。
4.3 数据接收完成判断
如何判断一帧的数据接收完成了?
在本文中,我们使用超时的方法进行判断,这种方法虽然会耗费 CPU 资源,但因为比较简单,所以使用也很广泛。在下一篇文章里,我们将使用空闲中断+DMA 的方法,更高效进行帧数据接收完成判断。
超时判断的思路如下:
将接收到的字符保存在接收缓冲区里,并定义一个变量 uart2_cnt 计算总共收到了多少个字符;
假如一帧的数据接收完成了,那么 uart2_cnt 变量的值应该维持不变。
第一个步骤比较好实现,还是在串口 2 接收中断里,做一些小小的改动:
uint16_t uart2_cnt = 0, uart2_cntPre = 0;
void UART2_IRQHandler(void)
{
uint8_t receive_data = 0;
if(__HAL_UART_GET_FLAG(&uart2_handle, UART_FLAG_RXNE) != RESET){ //获取接收RXNE标志位是否被置位
if(uart2_cnt >= sizeof(uart2_rx_buf)) //如果接收的字符数大于接收缓冲区大小,
uart2_cnt = 0; //则将接收计数器清零
HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000); //接收一个字符
uart2_rx_buf[uart2_cnt++] = receive_data; //将接收到的字符保存在接收缓冲区
}
}
关键是第二步,我们如何判断 uart2_cnt 什么时候维持不变(也就是一帧的数据接收完成了)?也很简单,我们就定时去查看一下这个变量的值,看看是否跟上一次一样,如果一样的话就说明数据接收完成了。
因此我们需要再借助一个新的变量 uart2_cntPre ,记录上一次接收到的数据的长度(上面的代码已经定义好了)。
uint8_t uart2_wait_receive(void)
{
if(uart2_cnt == 0) //如果接收计数为0,则说明没有处于接收数据中,所以直接跳出,结束函数
return UART_ERROR;
if(uart2_cnt == uart2_cntPre) { //如果上一次的值和这次相同,则说明接收完毕
uart2_cnt = 0; //清0接收计数
return UART_EOK; //返回接收完成标志
}
uart2_cntPre = uart2_cnt; //置为相同
return UART_ERROR; //返回接收未完成标志
}
然后我们在 main 函数里的 while 死循环定期(例如10ms)调用 uart2_wait_receive 函数,如果返回值为 UART_EOK 则代表帧数据接收完成,我们就可以将数据打印出来。
while(1)
{
if(uart2_wait_receive() == UART_EOK) { //判断串口2是否数据接收完成
printf('recv: %srn', uart2_rx_buf); //打印收到的数据
uart2_rx_clear(); //清空接收缓冲区
}
delay_ms(10); //每隔10毫秒判断一次
}
当然,接收到的数据使用完成之后,我们就应该清空接收缓冲区,并将计数器置 0 ,方便下一次接收,所以我们调用了 uart2_rx_clear() 函数,其代码实现为:
上一篇:《嵌入式-STM32开发指南》第二部分 基础篇 - 第3章 按键(HAL库)
下一篇:《嵌入式-STM32开发指南》第二部分 基础篇 - 第7章DMA(HAL库)
推荐阅读最新更新时间:2026-03-25 12:27
- 用于 7VIN 至 16VIN、1.5V 和 1.2V 输出的 LTM4628EV DC/DC 模块稳压器的典型应用电路
- 使用 Analog Devices 的 LTC3728LIGN 的参考设计
- DER-406 - 适用于 A19 灯的 5.76 W 高 PF 非隔离降压-升压型 TRIAC 调光 LED 驱动器
- ADR5045B 5V 输出精密微功率并联模式电压基准的典型应用
- LT3970EDDB-3.42 2.5V 降压转换器的典型应用
- MC78M08BDTG 8V 电流调节器的典型应用
- LT1021DCN8-5 精密电压基准的典型应用
- DER-282 - 100W, 扁平(11 mm), LLC DC-DC转换器
- REF193 低压差开尔文连接电压基准的典型应用电路
- LT3088EM 线性稳压器用于添加软启动的典型应用



stm32驱动屏IC rm68042
STM32 PMSM FOC 简介
基于STM32G031的测试测量训练平台完成虚拟仪器实现
现代雷达系统的信号设计
BFR340T






京公网安备 11010802033920号