STM32 串口接收数据包(自定义帧头帧尾)
一、基本概述
本实验基于 STM32C8T6 单片机 开发,串口作为嵌入式系统中基础且核心的外设,广泛应用于设备间数据通信。本文重点讲解 自定义帧头帧尾的串口数据包接收与发送逻辑,核心是理解 “如何通过帧结构区分有效数据、避免数据混淆”,并掌握中断驱动的串口数据处理思路。
核心设计思路
通过自定义数据包格式(帧头 + 定长数据 + 帧尾),解决串口通信中 “数据边界模糊” 的问题:
- 帧头(Header):固定为
0xFE
,用于标记数据包的开始。 - 数据段(Data):定长 4 字节,存储实际业务数据(可根据需求调整长度)。
- 帧尾(Tail):固定为
0xFF
,用于标记数据包的结束。
二、关键变量定义与逻辑解析
2.1 核心变量
// 接收相关变量(usart.c中定义,需通过usart.h extern导出供其他文件使用)
uint8_t rxd_buf[4]; // 接收缓冲区,存储4字节定长数据段
uint8_t rxd_flag = 0; // 接收完成标志:0=未完成,1=完成(用于主函数判断是否处理数据)
uint8_t rxd_index = 0; // 接收索引:记录当前接收数据在rxd_buf中的位置// 发送相关变量(示例用,可根据需求修改)
uint8_t txd_buf[4] = {1,2,3,4}; // 默认发送数据包(仅示例)
2.2 核心逻辑:串口中断服务函数
串口接收采用 中断驱动方式(仅当有数据接收时触发中断,降低 CPU 占用),通过 状态机(switch-case) 解析数据包,流程如下:
状态机设计
状态(recv_state) | 功能描述 |
---|---|
0(等待帧头) | 检测是否接收到帧头 0xFE ,若收到则切换到状态 1,同时重置接收索引;否则保持状态 0。 |
1(接收数据段) | 将接收到的字节依次存入 rxd_buf ,每存 1 字节rxd_index 自增 1;当接收满 4 字节(rxd_index>=4 ),切换到状态 2。 |
2(等待帧尾) | 检测是否接收到帧尾 0xFF ,若收到则置位 rxd_flag=1 (标记接收完成),同时重置状态为 0;否则丢弃该包(不置位标志)。 |
中断服务函数代码
/*** @brief USART1中断服务程序(核心:数据包解析)* @param 无* @retval 无*/
void USART1_IRQHandler(void)
{u8 recv_dat; // 临时变量,存储单次接收到的字节static uint8_t recv_state = 0; // 静态状态变量,默认从状态0开始(中断退出后不丢失值)// 1. 判断是否为“接收数据寄存器非空”中断(有新数据接收)if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {// 2. 读取接收到的1字节数据(从USART1的DR寄存器读取)recv_dat = USART_ReceiveData(USART1); // 3. 状态机解析数据包switch (recv_state){ case 0: // 状态0:等待帧头 0xFEif (recv_dat == 0XFE) // 检测到帧头{recv_state = 1; // 切换到“接收数据段”状态rxd_index = 0; // 重置接收索引(从buf[0]开始存数据)}else // 未检测到帧头,保持状态0{recv_state = 0;}break;case 1: // 状态1:接收4字节数据段rxd_buf[rxd_index] = recv_dat; // 存入缓冲区rxd_index++; // 索引自增,指向下一个存储位置if (rxd_index >= 4) // 判断是否接收满4字节数据{recv_state = 2; // 切换到“等待帧尾”状态}break;case 2: // 状态2:等待帧尾 0xFFif (recv_dat == 0XFF) // 检测到帧尾{rxd_flag = 1; // 置位接收完成标志(主函数可检测该标志处理数据)recv_state = 0; // 重置状态为0,准备接收下一包数据}break;}// 4. 清除中断标志位(必须操作,否则会重复触发中断)USART_ClearITPendingBit(USART1, USART_IT_RXNE); }
}
三、串口工具测试验证
使用 XCOM V2.6 串口助手 发送自定义格式数据包,验证接收与回显功能,测试配置与结果如下:
3.1 串口配置
配置项 | 参数值 |
---|---|
串口端口 | COM3(根据实际硬件选择) |
波特率 | 115200 |
数据位 | 8 位 |
停止位 | 1 位 |
校验位 | None(无校验) |
发送格式 | 16 进制发送 |
3.2 测试数据与结果
- 发送数据包(16 进制):
FE 00 00 00 01 FF
(帧头 + 4 字节数据 + 帧尾) - 接收回显结果(串口助手显示):
FE 00 00 00 01 FF
(STM32 接收完成后回显相同数据包)
串口助手日志示例
plaintext
[2023-12-06 22:29:55,476] TX: FE 00 00 00 01 FF 0D 0A // 上位机发送
[2023-12-06 22:29:55.689] RX: FE 00 00 00 01 FF // STM32回显
[2023-12-06 22:29:56.790] TX: FE 00 00 00 01 FF 0D 0A // 再次发送
[2023-12-06 22:29:57.672] RX: FE 00 00 00 01 FF // 再次回显
四、完整程序代码
4.1 usart.c(串口驱动与数据包处理)
c
运行
#include "usart.h"
#include "led.h" // 全局变量定义(接收相关)
uint8_t rxd_buf[4]; // 接收缓冲区(4字节定长)
uint8_t rxd_flag = 0; // 接收完成标志
uint8_t rxd_index = 0; // 接收索引
uint8_t txd_buf[4] = {1,2,3,4}; // 默认发送缓冲区(示例)/*** @brief USART1初始化函数* @param bound:波特率(如115200、9600等)* @retval 无*/
void USART1_Init(u32 bound)
{// 1. 定义初始化结构体GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;// 2. 使能时钟(GPIOA和USART1都挂载在APB2总线上)RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // 3. 配置GPIO(TX: PA9,复用推挽输出;RX: PA10,浮空输入)// 配置TX引脚(PA9)GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出(串口发送需要复用功能)GPIO_Init(GPIOA, &GPIO_InitStructure);// 配置RX引脚(PA10)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 = bound; // 波特率USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据位USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1位停止位USART_InitStructure.USART_Parity = USART_Parity_No; // 无奇偶校验USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 同时使能接收和发送USART_Init(USART1, &USART_InitStructure); // 初始化USART1// 5. 使能USART1USART_Cmd(USART1, ENABLE); USART_ClearFlag(USART1, USART_FLAG_TC); // 清除发送完成标志(避免初始状态异常)// 6. 配置串口接收中断(使能“接收数据寄存器非空”中断)USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);// 7. 配置NVIC(中断优先级)NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // 串口1中断通道NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; // 抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 响应优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能该中断通道NVIC_Init(&NVIC_InitStructure); // 初始化NVIC
}/*** @brief 重定义printf函数(支持通过USART1打印调试信息)* @param ch:要打印的字符,FILE*:标准库文件指针(无需手动传参)* @retval 打印的字符(符合printf函数返回值要求)*/
int fputc(int ch, FILE *p)
{USART_SendData(USART1, (u8)ch); // 发送1字节数据while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成return ch;
}/*** @brief 发送1字节数据* @param byte:要发送的字节* @retval 无*/
void send_byte(uint8_t byte)
{USART_SendData(USART1, byte);while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成
}/*** @brief 发送字符串(以'\0'为结束符)* @param str:指向字符串的指针* @retval 无*/
void send_string(uint8_t *str)
{while (*str != '\0') // 遍历字符串,直到遇到结束符{send_byte(*str++); // 发送当前字符,指针自增指向下一个字符}
}/*** @brief 发送指定长度的字节数组* @param buf:指向数组的指针,len:要发送的字节数* @retval 无*/
void send_buf(uint8_t *buf, uint16_t len)
{uint16_t i;for (i = 0; i < len; i++){send_byte(buf[i]); // 逐个发送数组元素}
}/*** @brief 发送自定义格式数据包(帧头+数据段+帧尾)* @param 无(使用rxd_buf作为数据段,可根据需求修改)* @retval 无*/
void send_pack(void)
{send_byte(0xFE); // 发送帧头send_buf(rxd_buf, 4); // 发送4字节数据段(接收缓冲区的数据)send_byte(0xFF); // 发送帧尾
}/*** @brief USART1中断服务程序(核心:数据包解析)* @param 无* @retval 无*/
void USART1_IRQHandler(void)
{u8 recv_dat;static uint8_t recv_state = 0;if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {recv_dat = USART_ReceiveData(USART1); switch (recv_state){ case 0: if (recv_dat == 0XFE){recv_state = 1;rxd_index = 0;}else{recv_state = 0;}break;case 1: rxd_buf[rxd_index] = recv_dat;rxd_index++;if (rxd_index >= 4){recv_state = 2;}break;case 2: if (recv_dat == 0XFF){rxd_flag = 1;recv_state = 0;}break;}USART_ClearITPendingBit(USART1, USART_IT_RXNE); }
}
4.2 usart.h(头文件,声明函数与全局变量)
c
运行
#ifndef _usart_H
#define _usart_H #include "system.h"
#include "stdio.h" // 声明全局变量(供其他文件使用)
extern uint8_t rxd_flag; // 接收完成标志// 声明函数(供其他文件调用)
void USART1_Init(u32 bound);
void send_byte(uint8_t byte);
void send_string(uint8_t *str);
void send_buf(uint8_t *buf, uint16_t len);
void send_pack(void);#endif
4.3 main.c(主函数,业务逻辑处理)
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "pwm.h"
#include "usart.h"
#include "key.h"
#include "oled.h"int main(void)
{// 1. 初始化外设SysTick_Init(72); // 初始化SysTick定时器(72MHz时钟,用于延时)NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 中断优先级分组:2组(2位抢占+2位响应)USART1_Init(115200); // 初始化USART1,波特率115200USART2_Init(115200); // 初始化USART2(按需使用,本示例未用到)OLED_Init(); // 初始化OLED显示屏(按需使用)KEY_Init(); // 初始化按键(按需使用)LED_Init(); // 初始化LED(按需使用)// 2. 发送初始化提示信息send_string("hello stm32\r\n"); // 发送字符串(\r\n为换行符,使串口助手显示换行)// 3. 主循环(处理接收完成的数据包)while (1){// 检测到数据包接收完成(rxd_flag=1)if (rxd_flag == 1){rxd_flag = 0; // 重置标志位(避免重复处理)send_pack(); // 回显接收到的数据包(将接收的内容原样发送回去)}}
}