一、基本概述
本实验基于 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:等待帧头 0xFE
if (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:等待帧尾 0xFF
if (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. 使能USART1
USART_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; // 抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; // 响应优先级3
NVIC_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,波特率115200
USART2_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(); // 回显接收到的数据包(将接收的内容原样发送回去)
}
}
}