声明
文中内容为观看 BiliBili 视频【STM32入门教程-2023版 细致讲解 中文字幕】后学习并扩展总结。
本文章为个人学习使用,版面观感若有不适请谅解,文中知识仅代表个人观点,若出现错误,欢迎各位批评指正。
一、数据模式及数据包
1.1 数据模式
在数字通信系统中,发送方与接收方通过编码 - 译码 的双向映射实现字符与二进制数据的转换,这一过程构成了信息可靠传输的核心基础。以字符 'A'为例,发送方将其编码为十六进制值 0x41(对应二进制 01000001),并以原始比特流形式传输至接收方;接收方则通过译码操作将 0x41还原为字符 'A',完成信息的完整闭环传递。
在此过程中,数据的显示模式直接影响对传输内容的解读维度:十六进制模式(HEX 模式)/ 二进制模式 以原始数据形式呈现,直观展示通信链路中传输的比特序列或其十六进制等价形式,可精准定位传输过程中的比特错误与数据完整性;文本模式(字符模式)则以编码后的字符形式呈现,更贴近人类可读的信息语义,便于理解传输内容的实际含义 。二者分别从物理层数据传输 与应用层语义 表达两个层面,共同构成了数字通信系统中数据观测与分析的完整视角,为协议调试、故障排查及性能优化提供了互补的技术手段。

1.2 HEX 数据包 及 数据包接收
-
固定包长 HEX 数据包格式
固定包长 HEX 数据包采用帧头 - 数据 - 帧尾 的定长封装结构,以特定字节作为帧边界标识:帧头固定为
0xFF,帧尾固定为0xFE,中间承载 4 字节定长有效载荷。该格式通过帧头0xFF与帧尾0xFE的字节序列实现帧同步与边界划分,有效载荷长度严格固定为 4 字节,无需额外长度字段即可完成帧解析,适用于对传输时延确定性要求较高的场景。典型数据包序列为0xFF 0x01 0x02 0x03 0x04 0xFE,其中0xFF为帧起始标识,0x01~0x04为有效数据,0xFE为帧结束标识。 -
可变包长 HEX 数据包格式
可变包长 HEX 数据包同样采用帧头 - 数据 - 帧尾 的封装范式,帧头
0xFF与帧尾0xFE的边界标识规则与固定包长格式保持一致,但有效载荷长度可动态变化。该格式通过帧头0xFF标记帧起始,帧尾0xFE标记帧结束,有效载荷长度由应用层逻辑或协议约定动态调整,无需在帧结构中显式携带长度字段,仅依赖帧尾0xFE完成帧边界判定。典型数据包序列为0xFF 0x01 0x02 0x03 0x04 0xFE(4 字节载荷)或0xFF 0x05 0x06 0x07 0xFE(3 字节载荷),适用于数据长度可变的异步通信场景。 -
HEX 数据包接收状态机流程
HEX 数据包接收采用 有限状态机(FSM) 实现帧同步与解析,核心状态定义如下:
状态 S=0(等待包头): 系统初始状态,持续监听接收字节流,仅当检测到帧头字节
0xFF时,状态迁移至 S=1(接收数据);若收到其他字节,则保持 S=0 状态。状态 S=1(接收数据): 进入数据接收阶段,累计接收有效载荷字节。对于固定包长格式,需累计接收 4 字节有效数据;对于可变包长格式,持续接收直至触发帧尾检测。若未收满约定长度的有效数据或收到非帧尾字节,则保持 S=1 状态;当收满约定长度数据时,状态迁移至 S=2(等待包尾)。
状态 S=2(等待包尾): 等待帧尾字节
0xFE的到来,若检测到0xFE,则判定当前帧接收完成,状态重置为 S=0(等待包头),并向上层提交完整数据包;若收到其他字节,则保持 S=2 状态,直至检测到0xFE或超时丢弃当前帧。该状态机通过字节级别的状态跃迁,实现了对固定 / 可变包长 HEX 数据包的可靠同步与解析,有效避免了帧黏连与帧丢失问题,保障了串行通信的帧完整性。

1.3 文本数据包 及 数据包接收
-
固定包长文本数据包格式
固定包长文本数据包采用帧头 - 数据 - 帧尾 的定长封装范式,以字符
@作为帧起始标识(帧头),以\r\n(回车换行符)作为帧结束标识(帧尾),中间承载 3 字节固定长度的有效文本载荷。该格式通过@与\r\n的字符序列实现帧同步与边界划分,有效载荷长度严格限定为 3 字节,无需额外长度字段即可完成帧解析,适用于对传输时延确定性、数据完整性要求较高的文本通信场景。典型数据包序列为@ABC\r\n,其中@为帧起始标识,A/B/C为有效文本数据,\r\n为帧结束标识。 -
可变包长文本数据包格式
可变包长文本数据包同样遵循帧头 - 数据 - 帧尾 的封装结构,帧头
@与帧尾\r\n的边界标识规则与固定包长格式保持一致,但有效载荷长度可动态变化。该格式以@标记帧起始,以\r\n标记帧结束,有效载荷长度由应用层业务逻辑动态调整,无需在帧结构中显式携带长度字段,仅依赖\r\n序列完成帧边界判定。典型数据包序列为@ABC\r\n(3 字节载荷)或@DE\r\n(2 字节载荷),适用于数据长度可变的异步文本通信场景,如串口调试、传感器文本数据上报等。 -
文本数据包接收状态机流程
文本数据包接收采用 有限状态机(FSM) 实现帧同步与解析,核心状态定义如下::
状态 S=0(等待包头): 系统初始状态,持续监听接收字符流,仅当检测到帧头字符
@时,状态迁移至 S=1(接收数据等待包尾);若收到其他字符,则保持 S=0 状态。状态 S=1(接收数据等待包尾): 进入数据接收阶段,累计接收有效文本载荷。对于固定包长格式,需累计接收 3 字节有效数据;对于可变包长格式,持续接收直至检测到
\r字符。若收到其他字符,则保持 S=1 状态;当检测到\r字符时,状态迁移至 S=2(等待包尾)。状态 S=2(等待包尾): 等待帧尾第二个字符
\n的到来,若检测到\n,则判定当前帧接收完成,状态重置 为 S=0(等待包头),并向上层提交完整文本数据包;若收到其他字符,则保持 S=2 状态,直至检测到\n或超时丢弃当前帧。该状态机通过字符级别的状态跃迁,实现了对固定 / 可变包长文本数据包的可靠同步与解析,有效避免了帧黏连与帧丢失问题,保障了串行文本通信的帧完整性。

二、串口收发 HEX 及 文本 数据包
2.1 串口收发 HEX 数据包的实现
-
首先,按下图接线方式,搭建面包板电路连接 OLED 显示屏,并将 USB 转串口的 TXD 和 RXD 分别与 PA2 和 PA3 连接,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

-
直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末。
#include "stm32f10x.h" // Device header
#include "Serial_HEX.h"
#include "OLED.h"
#include "Key.h"uint8_t KeyNum;
int main(void)
{
OLED_Init();
Key_Init();
Serial_Init();OLED_ShowString(1, 1, "TxPacket"); OLED_ShowString(3, 1, "RxPacket"); Serial_TxPacket[0] = 0x01; Serial_TxPacket[1] = 0x02; Serial_TxPacket[2] = 0x03; Serial_TxPacket[3] = 0x04; while (1) { KeyNum = Key_GetNum(); if (KeyNum == 1){ Serial_TxPacket[0] ++; Serial_TxPacket[1] ++; Serial_TxPacket[2] ++; Serial_TxPacket[3] ++; Serial_SendPacket(); OLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2); OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2); OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2); OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2); } if (Serial_GetRxFlag() == 1){ OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2); OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2); OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2); OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2); } }}
-
打开串口助手(串口助手软件 请在【江协科技】视频下方下载) ,按照预设的数据包格式,在串口助手的发送区域编辑数据 "FF 11 22 33 44 FE" 并点击发送后,即可在 RxPacket 区域下方查看到所发送的数据包内容。


2.2 串口收发文本数据包的实现
-
首先,按下图接线方式,搭建面包板电路连接 OLED 显示屏,并将 USB 转串口的 TXD 和 RXD 分别与 PA2 和 PA3 连接,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

-
直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末。
注:文末 LED 代码中保留了void LED_Turn(void);功能的实现,感兴趣的可以尝试将 LED 灯状态转换添加进来,并通过设定 LED 初始状态以及 FLAG 标记位返回当前 LED 灯的状态信息(即:当前 LED 是开还是关)。#include "stm32f10x.h" // Device header
#include "Serial_TEXT.h"
#include "OLED.h"
#include "LED.h"
#include <string.h>int main(void)
{
OLED_Init();
LED_Init();
Serial_Init();OLED_ShowString(1, 1, "TxPacket"); OLED_ShowString(3, 1, "RxPacket"); while (1) { if (Serial_RxFlag == 1){ OLED_ShowString(4, 1, " "); OLED_ShowString(4, 1, Serial_RxPacket); if (strcmp(Serial_RxPacket, "LED_ON") == 0){ LED_ON(); Serial_SendString("LED_ON_OK\r\n"); OLED_ShowString(2, 1, " "); OLED_ShowString(2, 1, "LED_ON_OK"); } else if (strcmp(Serial_RxPacket, "LED_OFF") == 0){ LED_OFF(); Serial_SendString("LED_OFF_OK\r\n"); OLED_ShowString(2, 1, " "); OLED_ShowString(2, 1, "LED_OFF_OK"); } else { Serial_SendString("ERROR_COMMAND\r\n"); OLED_ShowString(2, 1, " "); OLED_ShowString(2, 1, "ERROR_COMMAND"); } Serial_RxFlag = 0; } }}
-
打开串口助手(串口助手软件 请在【江协科技】视频下方下载) ,按照预设的数据包格式,在串口助手的发送区域编辑数据 "@LED_ON + 换行" 并点击发送后,即可观察到 LED 灯亮。


注:文末LED.h中保留了void LED_Turn(void);功能的实现,可尝试完善 LED_TURN 部分内容(输入 @LED_TURN 转换当前 LED 状态,输入 @GET_STATE 获取当前 LED 开关状态。)。

三、演示代码关联的头文件与源文件说明
-
OLED 相关头文件请从 STM32 学习 ------ 个人学习笔记4(OLED 显示屏及调试工具) 文末查看,此处不重复展示。
-
Delay 相关头文件请从 STM32 学习 ------ 个人学习笔记3-1(GPIO 输出) 文末查看,此处不重复展示。
-
Key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);}
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0);
Delay_ms(20);
KeyNum = 1;
}return KeyNum;}
-
Key.h
#ifndef __KEY_H
#define __KEY_Hvoid Key_Init(void);
uint8_t Key_GetNum(void);#endif
-
LED.c
#include "stm32f10x.h" // Device header
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_6);}
void LED_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_6);
}void LED_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_6);
}void LED_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_6) == 0)
{
GPIO_SetBits(GPIOA, GPIO_Pin_6);
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_6);
}
} -
LED.h
#ifndef __LED_H
#define __LED_Hvoid LED_Init(void);
void LED_ON(void);
void LED_OFF(void);
void LED_Turn(void);#endif
-
Serial_HEX.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;void Serial_Init(void){
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 9600; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_Init(USART2, &USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); USART_Cmd(USART2, ENABLE);}
void Serial_SendByte(uint8_t Byte){
USART_SendData(USART2, Byte);
while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
}void Serial_SendArray(uint8_t *Array, uint16_t Length){
uint16_t i;
for(i=0; i<Length; i++){
Serial_SendByte(Array[i]);
}
}void Serial_SendString(char *String){
uint8_t i;
for(i=0; String[i] != '\0'; i++){
Serial_SendByte(String[i]);
}
}uint32_t Serial_Pow(uint32_t X, uint32_t Y){
uint32_t Result = 1;
while(Y--){
Result *=X;
}
return Result;
}void Serial_SendNumber(uint32_t Number, uint8_t Length){
uint8_t i;
for(i=0; i<Length; i++){
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + 0x30);
}
}int fputc(int ch, FILE *f){
Serial_SendByte(ch);
return ch;
}void Serial_printf(char *format, ...){
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}void Serial_SendPacket(void){
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}uint8_t Serial_GetRxFlag(void){
if(Serial_RxFlag==1){
Serial_RxFlag = 0;
return 1;
}
return 0;
}void USART2_IRQHandler(void){
static uint8_t RxState = 0;
static uint8_t pRxState = 0;
if(USART_GetITStatus(USART2, USART_IT_RXNE) == SET){
uint8_t RxData = USART_ReceiveData(USART2);if (RxState == 0){ if (RxData == 0xFF){ RxState =1; pRxState = 0; } }else if (RxState == 1){ Serial_RxPacket[pRxState] = RxData; pRxState ++; if (pRxState >= 4){ RxState = 2; } }else if (RxState == 2){ if (RxData == 0xFE){ RxState =0; Serial_RxFlag = 1; } } USART_ClearITPendingBit(USART2, USART_IT_RXNE); }}
-
Serial_HEX.h
#ifndef __SERIAL_HEX_H
#define __SERIAL_HEX_H#include <stdio.h>
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
int fputc(int ch, FILE *f);
void Serial_printf(char *format, ...);void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);
#endif
-
Serial_TEXT.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>char Serial_RxPacket[100];
uint8_t Serial_RxFlag;void Serial_Init(void){
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 9600; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_Init(USART2, &USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); USART_Cmd(USART2, ENABLE);}
void Serial_SendByte(uint8_t Byte){
USART_SendData(USART2, Byte);
while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
}void Serial_SendArray(uint8_t *Array, uint16_t Length){
uint16_t i;
for(i=0; i<Length; i++){
Serial_SendByte(Array[i]);
}
}void Serial_SendString(char *String){
uint8_t i;
for(i=0; String[i] != '\0'; i++){
Serial_SendByte(String[i]);
}
}uint32_t Serial_Pow(uint32_t X, uint32_t Y){
uint32_t Result = 1;
while(Y--){
Result *=X;
}
return Result;
}void Serial_SendNumber(uint32_t Number, uint8_t Length){
uint8_t i;
for(i=0; i<Length; i++){
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + 0x30);
}
}int fputc(int ch, FILE *f){
Serial_SendByte(ch);
return ch;
}void Serial_printf(char *format, ...){
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}void USART2_IRQHandler(void){
static uint8_t RxState = 0;
static uint8_t pRxState = 0;
if(USART_GetITStatus(USART2, USART_IT_RXNE) == SET){
uint8_t RxData = USART_ReceiveData(USART2);if (RxState == 0){ if (RxData == '@' && Serial_RxFlag == 0){ RxState =1; pRxState = 0; } }else if (RxState == 1){ if (RxData == '\r'){ RxState = 2; } else { Serial_RxPacket[pRxState] = RxData; pRxState ++; } }else if (RxState == 2){ if (RxData == '\n'){ RxState =0; Serial_RxPacket[pRxState] = '\0'; Serial_RxFlag = 1; } } USART_ClearITPendingBit(USART2, USART_IT_RXNE); }}
-
Serial_TEXT.h
#ifndef __SERIAL_TEXT_H
#define __SERIAL_TEXT_H#include <stdio.h>
extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
int fputc(int ch, FILE *f);
void Serial_printf(char *format, ...);#endif
文中部分知识参考:B 站 ------ 江协科技;百度百科