STM32 串口接收数据包(自定义帧头帧尾)

发布于:2025-09-10 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、基本概述

本实验基于 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();  // 回显接收到的数据包(将接收的内容原样发送回去)
        }
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到