串口 HEX 与文本数据包收发
课程录音:2026-09-09《串口数据包的发送接收》
对应源码:
9-3 串口收发HEX数据包、9-4 串口收发文本数据包前置内容:21_USART发送与接收代码流程.md
整理日期:2026-09-09
1. 本节要解决的问题
USART 硬件一次只告诉程序"收到一个字节"。实际业务却经常需要一次传递多个相关数据,例如陀螺仪的 X、Y、Z 三轴值,或者一条 LED_ON 指令。
如果数据流只是不断重复 XYZXYZXYZ...,接收方从任意位置接入时,不知道当前字节属于 X、Y 还是 Z。数据包协议的作用就是给连续字节流增加边界,让接收方能够:
- 找到一包从哪里开始;
- 确定哪些字节属于有效载荷;
- 判断一包在哪里结束;
- 把完整的一包交给主程序处理。
#mermaid-svg-udNbWyfR2sj4AdsK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-udNbWyfR2sj4AdsK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-udNbWyfR2sj4AdsK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-udNbWyfR2sj4AdsK .error-icon{fill:#552222;}#mermaid-svg-udNbWyfR2sj4AdsK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-udNbWyfR2sj4AdsK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-udNbWyfR2sj4AdsK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-udNbWyfR2sj4AdsK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-udNbWyfR2sj4AdsK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-udNbWyfR2sj4AdsK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-udNbWyfR2sj4AdsK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-udNbWyfR2sj4AdsK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-udNbWyfR2sj4AdsK .marker.cross{stroke:#333333;}#mermaid-svg-udNbWyfR2sj4AdsK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-udNbWyfR2sj4AdsK p{margin:0;}#mermaid-svg-udNbWyfR2sj4AdsK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-udNbWyfR2sj4AdsK .cluster-label text{fill:#333;}#mermaid-svg-udNbWyfR2sj4AdsK .cluster-label span{color:#333;}#mermaid-svg-udNbWyfR2sj4AdsK .cluster-label span p{background-color:transparent;}#mermaid-svg-udNbWyfR2sj4AdsK .label text,#mermaid-svg-udNbWyfR2sj4AdsK span{fill:#333;color:#333;}#mermaid-svg-udNbWyfR2sj4AdsK .node rect,#mermaid-svg-udNbWyfR2sj4AdsK .node circle,#mermaid-svg-udNbWyfR2sj4AdsK .node ellipse,#mermaid-svg-udNbWyfR2sj4AdsK .node polygon,#mermaid-svg-udNbWyfR2sj4AdsK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-udNbWyfR2sj4AdsK .rough-node .label text,#mermaid-svg-udNbWyfR2sj4AdsK .node .label text,#mermaid-svg-udNbWyfR2sj4AdsK .image-shape .label,#mermaid-svg-udNbWyfR2sj4AdsK .icon-shape .label{text-anchor:middle;}#mermaid-svg-udNbWyfR2sj4AdsK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-udNbWyfR2sj4AdsK .rough-node .label,#mermaid-svg-udNbWyfR2sj4AdsK .node .label,#mermaid-svg-udNbWyfR2sj4AdsK .image-shape .label,#mermaid-svg-udNbWyfR2sj4AdsK .icon-shape .label{text-align:center;}#mermaid-svg-udNbWyfR2sj4AdsK .node.clickable{cursor:pointer;}#mermaid-svg-udNbWyfR2sj4AdsK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-udNbWyfR2sj4AdsK .arrowheadPath{fill:#333333;}#mermaid-svg-udNbWyfR2sj4AdsK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-udNbWyfR2sj4AdsK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-udNbWyfR2sj4AdsK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-udNbWyfR2sj4AdsK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-udNbWyfR2sj4AdsK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-udNbWyfR2sj4AdsK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-udNbWyfR2sj4AdsK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-udNbWyfR2sj4AdsK .cluster text{fill:#333;}#mermaid-svg-udNbWyfR2sj4AdsK .cluster span{color:#333;}#mermaid-svg-udNbWyfR2sj4AdsK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-udNbWyfR2sj4AdsK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-udNbWyfR2sj4AdsK rect.text{fill:none;stroke-width:0;}#mermaid-svg-udNbWyfR2sj4AdsK .icon-shape,#mermaid-svg-udNbWyfR2sj4AdsK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-udNbWyfR2sj4AdsK .icon-shape p,#mermaid-svg-udNbWyfR2sj4AdsK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-udNbWyfR2sj4AdsK .icon-shape .label rect,#mermaid-svg-udNbWyfR2sj4AdsK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-udNbWyfR2sj4AdsK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-udNbWyfR2sj4AdsK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-udNbWyfR2sj4AdsK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} USART逐字节接收
寻找包头
按协议收集载荷
验证长度和包尾
置完整包标志
主循环解析并执行业务
2. 老师的讲解路线
| 录音时间 | 课堂内容 |
|---|---|
| 00:08~02:27 | 演示 HEX 包收发、文本指令控制 LED |
| 02:33~07:38 | 为什么需要数据包,固定/可变包长及包头包尾冲突 |
| 07:38~09:24 | HEX 包与文本包的特点和应用场景 |
| 09:24~13:36 | 发送为何简单、接收为何需要状态机 |
| 13:36~24:28 | 编写 9-3 固定长度 HEX 数据包工程 |
| 24:38~30:39 | 编写 9-4 文本指令包工程并处理缓存竞争 |
3. 资料定位
| 问题 | 资料位置 |
|---|---|
| HEX 包格式 | 教师 PPT 第 123 页 |
| 文本包格式 | 教师 PPT 第 124 页 |
| HEX 包接收状态机 | 教师 PPT 第 125 页 |
| 文本包接收状态机 | 教师 PPT 第 126 页 |
| USART 接收流程、RXNE、ORE | 《STM32F10xxx参考手册(中文)》PDF 第 521~524 页 |
| USART 中断 | 参考手册 PDF 第 538 页 |
USART_SR 状态位 |
参考手册 PDF 第 540~541 页 |
USART_DR 数据寄存器 |
参考手册 PDF 第 542 页 |
CR1.RXNEIE/RE/TE/UE |
参考手册 PDF 第 542~543 页 |
| USART1 的 PA9/PA10 引脚 | 《STM32F103x8B数据手册(中文)》PDF 第 17~20 页 |
包头、包尾、长度、校验等属于应用层协议,并不是 STM32 USART 寄存器自动提供的功能。手册解释的是"字节如何收发";源码中的状态机负责"如何把字节重新组成包"。
4. 两种数据包格式
4.1 HEX 数据包
本实验定义固定 6 字节的数据包:
text
0xFF Data0 Data1 Data2 Data3 0xFE
包头 4字节载荷 包尾

老师 PPT 第 123 页:固定包长和可变包长的 HEX 数据包。
课程工程选择"固定 4 字节载荷 + 包头 + 包尾"。由于接收载荷时只计数、不判断内容,所以载荷本身出现 0xFF 或 0xFE 也不会误判。
4.2 文本数据包
本实验格式为:
text
@载荷\r\n
例如串口工具实际发送:
text
@LED_ON\r\n
状态机只把 LED_ON 保存到接收数组,不保存 @、\r 和 \n,最后由程序追加 \0,使它成为 C 字符串。

老师 PPT 第 124 页:以 @ 为包头、CRLF 为包尾的文本数据包。
4.3 如何选择
| 比较项 | HEX 数据包 | 文本数据包 |
|---|---|---|
| 载荷 | 原始二进制字节 | 经过字符编码的文本 |
| 传输效率 | 高,例如数值 100 只需一个字节 | 较低,100 需要字符 '1' '0' '0' 三字节 |
| 人工可读性 | 较差 | 很好,可直接在串口助手输入 |
| 解析 | 固定字段时简单 | 还需把字符串转换成数值或命令 |
| 典型场景 | 传感器、实时数据、设备间通信 | AT 指令、调试命令、人机交互 |
5. 包格式设计时必须考虑什么
5.1 固定包长与可变包长
- 固定包长:接到包头后按已知数量收载荷,再检查包尾;字段定位稳定,适合二进制协议。
- 可变包长:不断接收,直到发现包尾;灵活,但必须限制最大长度。
5.2 包头/包尾与载荷冲突
常用处理方法:
- 限制载荷取值,使其不会出现定界符;
- 使用固定长度,接收载荷阶段不解释定界符;
- 使用多字节包头、包尾,降低自然重复概率;
- 正式协议使用转义/字节填充,例如载荷遇到特殊字节时编码;
- 增加长度字段,让接收端按长度收取;
- 增加校验和或 CRC,判断一包是否可靠。
课程协议用于讲清状态机,只有包头、载荷、包尾,没有地址、命令码、长度和校验。实际项目应按可靠性要求扩展。
6. 为什么发送简单、接收复杂
发送方掌握完整数据,可以主动按顺序调用:
text
发包头 → 发载荷 → 发包尾
接收方每次 RXNE 中断只拿到一个字节,而且每次中断彼此独立。要知道上一次已经收到哪里,就必须保存跨中断的状态,这就是状态机。
#mermaid-svg-MDPo10oMFZRtFhla{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MDPo10oMFZRtFhla .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MDPo10oMFZRtFhla .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MDPo10oMFZRtFhla .error-icon{fill:#552222;}#mermaid-svg-MDPo10oMFZRtFhla .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MDPo10oMFZRtFhla .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MDPo10oMFZRtFhla .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MDPo10oMFZRtFhla .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MDPo10oMFZRtFhla .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MDPo10oMFZRtFhla .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MDPo10oMFZRtFhla .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MDPo10oMFZRtFhla .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MDPo10oMFZRtFhla .marker.cross{stroke:#333333;}#mermaid-svg-MDPo10oMFZRtFhla svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MDPo10oMFZRtFhla p{margin:0;}#mermaid-svg-MDPo10oMFZRtFhla .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MDPo10oMFZRtFhla .cluster-label text{fill:#333;}#mermaid-svg-MDPo10oMFZRtFhla .cluster-label span{color:#333;}#mermaid-svg-MDPo10oMFZRtFhla .cluster-label span p{background-color:transparent;}#mermaid-svg-MDPo10oMFZRtFhla .label text,#mermaid-svg-MDPo10oMFZRtFhla span{fill:#333;color:#333;}#mermaid-svg-MDPo10oMFZRtFhla .node rect,#mermaid-svg-MDPo10oMFZRtFhla .node circle,#mermaid-svg-MDPo10oMFZRtFhla .node ellipse,#mermaid-svg-MDPo10oMFZRtFhla .node polygon,#mermaid-svg-MDPo10oMFZRtFhla .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MDPo10oMFZRtFhla .rough-node .label text,#mermaid-svg-MDPo10oMFZRtFhla .node .label text,#mermaid-svg-MDPo10oMFZRtFhla .image-shape .label,#mermaid-svg-MDPo10oMFZRtFhla .icon-shape .label{text-anchor:middle;}#mermaid-svg-MDPo10oMFZRtFhla .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MDPo10oMFZRtFhla .rough-node .label,#mermaid-svg-MDPo10oMFZRtFhla .node .label,#mermaid-svg-MDPo10oMFZRtFhla .image-shape .label,#mermaid-svg-MDPo10oMFZRtFhla .icon-shape .label{text-align:center;}#mermaid-svg-MDPo10oMFZRtFhla .node.clickable{cursor:pointer;}#mermaid-svg-MDPo10oMFZRtFhla .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MDPo10oMFZRtFhla .arrowheadPath{fill:#333333;}#mermaid-svg-MDPo10oMFZRtFhla .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MDPo10oMFZRtFhla .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MDPo10oMFZRtFhla .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MDPo10oMFZRtFhla .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MDPo10oMFZRtFhla .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MDPo10oMFZRtFhla .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MDPo10oMFZRtFhla .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MDPo10oMFZRtFhla .cluster text{fill:#333;}#mermaid-svg-MDPo10oMFZRtFhla .cluster span{color:#333;}#mermaid-svg-MDPo10oMFZRtFhla div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MDPo10oMFZRtFhla .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MDPo10oMFZRtFhla rect.text{fill:none;stroke-width:0;}#mermaid-svg-MDPo10oMFZRtFhla .icon-shape,#mermaid-svg-MDPo10oMFZRtFhla .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MDPo10oMFZRtFhla .icon-shape p,#mermaid-svg-MDPo10oMFZRtFhla .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MDPo10oMFZRtFhla .icon-shape .label rect,#mermaid-svg-MDPo10oMFZRtFhla .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MDPo10oMFZRtFhla .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MDPo10oMFZRtFhla .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MDPo10oMFZRtFhla :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 收到当前字节
读取上次保留的状态
根据当前状态解释该字节
保存载荷或丢弃
更新状态和数组下标
退出中断,等待下一字节
7. HEX 数据包发送
7.1 缓冲区
c
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;
数组只存 4 字节载荷,不存固定的包头和包尾。主程序写 Serial_TxPacket,驱动函数负责自动封装:
c
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
USART1 Serial_SendByte Serial_SendPacket 主程序 USART1 Serial_SendByte Serial_SendPacket 主程序 #mermaid-svg-Nz2Fp24Na8Be27HD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Nz2Fp24Na8Be27HD .error-icon{fill:#552222;}#mermaid-svg-Nz2Fp24Na8Be27HD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Nz2Fp24Na8Be27HD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Nz2Fp24Na8Be27HD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Nz2Fp24Na8Be27HD .marker.cross{stroke:#333333;}#mermaid-svg-Nz2Fp24Na8Be27HD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Nz2Fp24Na8Be27HD p{margin:0;}#mermaid-svg-Nz2Fp24Na8Be27HD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Nz2Fp24Na8Be27HD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Nz2Fp24Na8Be27HD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Nz2Fp24Na8Be27HD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Nz2Fp24Na8Be27HD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Nz2Fp24Na8Be27HD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Nz2Fp24Na8Be27HD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Nz2Fp24Na8Be27HD .sequenceNumber{fill:white;}#mermaid-svg-Nz2Fp24Na8Be27HD #sequencenumber{fill:#333;}#mermaid-svg-Nz2Fp24Na8Be27HD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Nz2Fp24Na8Be27HD .messageText{fill:#333;stroke:none;}#mermaid-svg-Nz2Fp24Na8Be27HD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Nz2Fp24Na8Be27HD .labelText,#mermaid-svg-Nz2Fp24Na8Be27HD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Nz2Fp24Na8Be27HD .loopText,#mermaid-svg-Nz2Fp24Na8Be27HD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Nz2Fp24Na8Be27HD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Nz2Fp24Na8Be27HD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Nz2Fp24Na8Be27HD .noteText,#mermaid-svg-Nz2Fp24Na8Be27HD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Nz2Fp24Na8Be27HD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Nz2Fp24Na8Be27HD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Nz2Fp24Na8Be27HD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Nz2Fp24Na8Be27HD .actorPopupMenu{position:absolute;}#mermaid-svg-Nz2Fp24Na8Be27HD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Nz2Fp24Na8Be27HD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Nz2Fp24Na8Be27HD .actor-man circle,#mermaid-svg-Nz2Fp24Na8Be27HD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Nz2Fp24Na8Be27HD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 填写4字节 TxPacket 后调用 发送 0xFF 依次发送4字节载荷 发送 0xFE 写 DR,等待 TXE
8. HEX 数据包接收状态机

老师 PPT 第 125 页:S=0 等包头、S=1 收 4 字节、S=2 等包尾。
8.1 状态定义
RxState |
含义 | 当前字节的处理 |
|---|---|---|
| 0 | 等待包头 | 只有收到 0xFF 才进入状态 1 |
| 1 | 接收载荷 | 无条件存入数组;收满 4 字节后进入状态 2 |
| 2 | 等待包尾 | 收到 0xFE 后完成一包并回到状态 0 |
状态和数组下标定义为 static:
c
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
普通局部变量每次进入中断都重新创建,不能记住上次进度;静态局部变量在函数退出后仍保留值,同时作用域仍限制在中断函数内。
8.2 代码流程
#mermaid-svg-D1hWvx8myjUXb1Js{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D1hWvx8myjUXb1Js .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D1hWvx8myjUXb1Js .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D1hWvx8myjUXb1Js .error-icon{fill:#552222;}#mermaid-svg-D1hWvx8myjUXb1Js .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D1hWvx8myjUXb1Js .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D1hWvx8myjUXb1Js .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D1hWvx8myjUXb1Js .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D1hWvx8myjUXb1Js .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D1hWvx8myjUXb1Js .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D1hWvx8myjUXb1Js .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D1hWvx8myjUXb1Js .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D1hWvx8myjUXb1Js .marker.cross{stroke:#333333;}#mermaid-svg-D1hWvx8myjUXb1Js svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D1hWvx8myjUXb1Js p{margin:0;}#mermaid-svg-D1hWvx8myjUXb1Js defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-D1hWvx8myjUXb1Js g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-D1hWvx8myjUXb1Js g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-D1hWvx8myjUXb1Js g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-D1hWvx8myjUXb1Js g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-D1hWvx8myjUXb1Js g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-D1hWvx8myjUXb1Js .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-D1hWvx8myjUXb1Js .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-D1hWvx8myjUXb1Js .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-D1hWvx8myjUXb1Js .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-D1hWvx8myjUXb1Js .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-D1hWvx8myjUXb1Js .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-D1hWvx8myjUXb1Js .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-D1hWvx8myjUXb1Js .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D1hWvx8myjUXb1Js .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D1hWvx8myjUXb1Js .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D1hWvx8myjUXb1Js .edgeLabel .label text{fill:#333;}#mermaid-svg-D1hWvx8myjUXb1Js .label div .edgeLabel{color:#333;}#mermaid-svg-D1hWvx8myjUXb1Js .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-D1hWvx8myjUXb1Js .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-D1hWvx8myjUXb1Js .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-D1hWvx8myjUXb1Js .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-D1hWvx8myjUXb1Js .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-D1hWvx8myjUXb1Js .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D1hWvx8myjUXb1Js .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D1hWvx8myjUXb1Js #statediagram-barbEnd{fill:#333333;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D1hWvx8myjUXb1Js .cluster-label,#mermaid-svg-D1hWvx8myjUXb1Js .nodeLabel{color:#131300;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-D1hWvx8myjUXb1Js .note-edge{stroke-dasharray:5;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-note text{fill:black;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram-note .nodeLabel{color:black;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagram .edgeLabel{color:red;}#mermaid-svg-D1hWvx8myjUXb1Js #dependencyStart,#mermaid-svg-D1hWvx8myjUXb1Js #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-D1hWvx8myjUXb1Js .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D1hWvx8myjUXb1Js :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RxData == 0xFF / 下标清零
其他字节 / 丢弃
未收满4字节 / 保存并加1
已收满4字节
RxData == 0xFE / RxFlag置1
其他字节 / 继续等待
State 0 等待包头
State 1 接收4字节载荷
State 2 等待包尾
源码使用 if ... else if ... else if,是为了保证一次中断只执行一个状态。若写成三个独立 if,状态 0 中刚把 RxState 改为 1,下面状态 1 的 if 可能在同一次中断中立刻成立,导致一个字节被重复解释。
9. HEX 工程主循环
c
if (KeyNum == 1)
{
Serial_TxPacket[0]++;
Serial_TxPacket[1]++;
Serial_TxPacket[2]++;
Serial_TxPacket[3]++;
Serial_SendPacket();
}
if (Serial_GetRxFlag() == 1)
{
// 在 OLED 显示 Serial_RxPacket[0..3]
}
Serial_GetRxFlag() 采用"读取即清零":完整包到达后 ISR 置 1;主循环取到 1 时清为 0。这表示主循环已经获知新包,但不保证处理期间数组不会被下一包改写。
10. 文本数据包接收状态机

老师 PPT 第 126 页:等待 @、接收不定长字符并等待 \r、再等待 \n。
10.1 状态定义
| 状态 | 任务 | 转移条件 |
|---|---|---|
| 0 | 等待 @ |
收到 @ 且上一包已处理完,进入 1 |
| 1 | 接收文本 | 普通字符写入数组;收到 \r 进入 2 |
| 2 | 等待 \n |
收到 \n 后补 \0、置标志并回到 0 |
#mermaid-svg-F9YVpK0mYjhWaosa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F9YVpK0mYjhWaosa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F9YVpK0mYjhWaosa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F9YVpK0mYjhWaosa .error-icon{fill:#552222;}#mermaid-svg-F9YVpK0mYjhWaosa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F9YVpK0mYjhWaosa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F9YVpK0mYjhWaosa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F9YVpK0mYjhWaosa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F9YVpK0mYjhWaosa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F9YVpK0mYjhWaosa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F9YVpK0mYjhWaosa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F9YVpK0mYjhWaosa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F9YVpK0mYjhWaosa .marker.cross{stroke:#333333;}#mermaid-svg-F9YVpK0mYjhWaosa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F9YVpK0mYjhWaosa p{margin:0;}#mermaid-svg-F9YVpK0mYjhWaosa defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-F9YVpK0mYjhWaosa g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-F9YVpK0mYjhWaosa g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-F9YVpK0mYjhWaosa g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-F9YVpK0mYjhWaosa g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-F9YVpK0mYjhWaosa g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-F9YVpK0mYjhWaosa .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-F9YVpK0mYjhWaosa .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-F9YVpK0mYjhWaosa .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-F9YVpK0mYjhWaosa .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-F9YVpK0mYjhWaosa .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-F9YVpK0mYjhWaosa .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-F9YVpK0mYjhWaosa .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-F9YVpK0mYjhWaosa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F9YVpK0mYjhWaosa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F9YVpK0mYjhWaosa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F9YVpK0mYjhWaosa .edgeLabel .label text{fill:#333;}#mermaid-svg-F9YVpK0mYjhWaosa .label div .edgeLabel{color:#333;}#mermaid-svg-F9YVpK0mYjhWaosa .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-F9YVpK0mYjhWaosa .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-F9YVpK0mYjhWaosa .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-F9YVpK0mYjhWaosa .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-F9YVpK0mYjhWaosa .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-F9YVpK0mYjhWaosa .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F9YVpK0mYjhWaosa .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F9YVpK0mYjhWaosa #statediagram-barbEnd{fill:#333333;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F9YVpK0mYjhWaosa .cluster-label,#mermaid-svg-F9YVpK0mYjhWaosa .nodeLabel{color:#131300;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-F9YVpK0mYjhWaosa .note-edge{stroke-dasharray:5;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-note text{fill:black;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram-note .nodeLabel{color:black;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagram .edgeLabel{color:red;}#mermaid-svg-F9YVpK0mYjhWaosa #dependencyStart,#mermaid-svg-F9YVpK0mYjhWaosa #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-F9YVpK0mYjhWaosa .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F9YVpK0mYjhWaosa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 收到 @ 且 RxFlag==0 / 下标清零
其他字节
普通字符 / 保存并加1
收到 CR
收到 LF / 补零并置 RxFlag
State 0 等待 @
State 1 接收载荷
State 2 等待 LF
10.2 为什么要补 \0
Serial_RxPacket 是字符数组。strcmp、OLED_ShowString 等 C 字符串函数不知道有效内容有多长,只会一直读到 \0。所以收到 \r\n 后必须执行:
c
Serial_RxPacket[pRxPacket] = '\0';
这不会通过串口发出去,它只是 RAM 中的字符串终止标记。
11. 文本命令怎样驱动 LED
主循环只有在 Serial_RxFlag == 1 时解析完整字符串:
c
if (strcmp(Serial_RxPacket, "LED_ON") == 0)
{
LED1_ON();
Serial_SendString("LED_ON_OK\r\n");
}
else if (strcmp(Serial_RxPacket, "LED_OFF") == 0)
{
LED1_OFF();
Serial_SendString("LED_OFF_OK\r\n");
}
else
{
Serial_SendString("ERROR_COMMAND\r\n");
}
Serial_RxFlag = 0;
strcmp(a, b) 返回 0 才表示两个字符串完全相等。整体链路为:
#mermaid-svg-G9ARJZ1O1EqhQ7vq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .error-icon{fill:#552222;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .marker.cross{stroke:#333333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq p{margin:0;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .cluster-label text{fill:#333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .cluster-label span{color:#333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .cluster-label span p{background-color:transparent;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .label text,#mermaid-svg-G9ARJZ1O1EqhQ7vq span{fill:#333;color:#333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .node rect,#mermaid-svg-G9ARJZ1O1EqhQ7vq .node circle,#mermaid-svg-G9ARJZ1O1EqhQ7vq .node ellipse,#mermaid-svg-G9ARJZ1O1EqhQ7vq .node polygon,#mermaid-svg-G9ARJZ1O1EqhQ7vq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .rough-node .label text,#mermaid-svg-G9ARJZ1O1EqhQ7vq .node .label text,#mermaid-svg-G9ARJZ1O1EqhQ7vq .image-shape .label,#mermaid-svg-G9ARJZ1O1EqhQ7vq .icon-shape .label{text-anchor:middle;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .rough-node .label,#mermaid-svg-G9ARJZ1O1EqhQ7vq .node .label,#mermaid-svg-G9ARJZ1O1EqhQ7vq .image-shape .label,#mermaid-svg-G9ARJZ1O1EqhQ7vq .icon-shape .label{text-align:center;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .node.clickable{cursor:pointer;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .arrowheadPath{fill:#333333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G9ARJZ1O1EqhQ7vq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G9ARJZ1O1EqhQ7vq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G9ARJZ1O1EqhQ7vq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .cluster text{fill:#333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .cluster span{color:#333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G9ARJZ1O1EqhQ7vq rect.text{fill:none;stroke-width:0;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .icon-shape,#mermaid-svg-G9ARJZ1O1EqhQ7vq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .icon-shape p,#mermaid-svg-G9ARJZ1O1EqhQ7vq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .icon-shape .label rect,#mermaid-svg-G9ARJZ1O1EqhQ7vq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G9ARJZ1O1EqhQ7vq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G9ARJZ1O1EqhQ7vq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G9ARJZ1O1EqhQ7vq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 电脑发送 @LED_ON CR LF
USART RXNE逐字节中断
状态机去掉包头包尾
数组得到 LED_ON 加字符串终止符
RxFlag置1
主循环 strcmp 匹配命令
执行 LED1_ON
回传 LED_ON_OK CR LF
RxFlag 在业务处理完成后才清零;状态 0 又要求 RxFlag==0 才接受新包头。这样 ISR 不会在主循环读取数组时写入下一包,但处理期间到来的数据包会被丢弃。这是一种"互斥但不排队"的简单策略。
12. 本节关键函数与硬件关系
| 函数/语句 | 所属层 | 作用及其前后关联 |
|---|---|---|
USART_GetITStatus(USART1, USART_IT_RXNE) |
标准外设库 | 检查 SR.RXNE 和对应中断使能位,确认收到字节事件 |
USART_ReceiveData(USART1) |
标准外设库 | 读取 DR 的接收数据;读 DR 会清 RXNE |
USART_ClearITPendingBit(...RXNE) |
标准外设库 | 课程代码再次清标志;读取 DR 后通常不需要 |
Serial_SendByte |
课程驱动层 | 写 DR 并等待 TXE,是所有发送封装的最底层 |
Serial_SendArray |
课程驱动层 | 循环发送定长二进制数组 |
Serial_SendPacket |
协议层 | 为 4 字节载荷添加 0xFF/0xFE |
Serial_GetRxFlag |
模块接口 | 通知主程序完整 HEX 包已到,并采用读取后清零策略 |
strcmp |
C 标准库 | 比较已补 \0 的文本载荷与预设指令 |
Serial_SendString |
课程驱动层 | 发送文本响应,字符串自身以 \0 结束,线路包尾为 \r\n |
13. 手册视角:硬件到底做到了哪一步
#mermaid-svg-NvaoUldBxnC5cWBK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NvaoUldBxnC5cWBK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NvaoUldBxnC5cWBK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NvaoUldBxnC5cWBK .error-icon{fill:#552222;}#mermaid-svg-NvaoUldBxnC5cWBK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NvaoUldBxnC5cWBK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NvaoUldBxnC5cWBK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NvaoUldBxnC5cWBK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NvaoUldBxnC5cWBK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NvaoUldBxnC5cWBK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NvaoUldBxnC5cWBK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NvaoUldBxnC5cWBK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NvaoUldBxnC5cWBK .marker.cross{stroke:#333333;}#mermaid-svg-NvaoUldBxnC5cWBK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NvaoUldBxnC5cWBK p{margin:0;}#mermaid-svg-NvaoUldBxnC5cWBK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NvaoUldBxnC5cWBK .cluster-label text{fill:#333;}#mermaid-svg-NvaoUldBxnC5cWBK .cluster-label span{color:#333;}#mermaid-svg-NvaoUldBxnC5cWBK .cluster-label span p{background-color:transparent;}#mermaid-svg-NvaoUldBxnC5cWBK .label text,#mermaid-svg-NvaoUldBxnC5cWBK span{fill:#333;color:#333;}#mermaid-svg-NvaoUldBxnC5cWBK .node rect,#mermaid-svg-NvaoUldBxnC5cWBK .node circle,#mermaid-svg-NvaoUldBxnC5cWBK .node ellipse,#mermaid-svg-NvaoUldBxnC5cWBK .node polygon,#mermaid-svg-NvaoUldBxnC5cWBK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NvaoUldBxnC5cWBK .rough-node .label text,#mermaid-svg-NvaoUldBxnC5cWBK .node .label text,#mermaid-svg-NvaoUldBxnC5cWBK .image-shape .label,#mermaid-svg-NvaoUldBxnC5cWBK .icon-shape .label{text-anchor:middle;}#mermaid-svg-NvaoUldBxnC5cWBK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NvaoUldBxnC5cWBK .rough-node .label,#mermaid-svg-NvaoUldBxnC5cWBK .node .label,#mermaid-svg-NvaoUldBxnC5cWBK .image-shape .label,#mermaid-svg-NvaoUldBxnC5cWBK .icon-shape .label{text-align:center;}#mermaid-svg-NvaoUldBxnC5cWBK .node.clickable{cursor:pointer;}#mermaid-svg-NvaoUldBxnC5cWBK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NvaoUldBxnC5cWBK .arrowheadPath{fill:#333333;}#mermaid-svg-NvaoUldBxnC5cWBK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NvaoUldBxnC5cWBK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NvaoUldBxnC5cWBK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NvaoUldBxnC5cWBK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NvaoUldBxnC5cWBK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NvaoUldBxnC5cWBK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NvaoUldBxnC5cWBK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NvaoUldBxnC5cWBK .cluster text{fill:#333;}#mermaid-svg-NvaoUldBxnC5cWBK .cluster span{color:#333;}#mermaid-svg-NvaoUldBxnC5cWBK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NvaoUldBxnC5cWBK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NvaoUldBxnC5cWBK rect.text{fill:none;stroke-width:0;}#mermaid-svg-NvaoUldBxnC5cWBK .icon-shape,#mermaid-svg-NvaoUldBxnC5cWBK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NvaoUldBxnC5cWBK .icon-shape p,#mermaid-svg-NvaoUldBxnC5cWBK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NvaoUldBxnC5cWBK .icon-shape .label rect,#mermaid-svg-NvaoUldBxnC5cWBK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NvaoUldBxnC5cWBK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NvaoUldBxnC5cWBK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NvaoUldBxnC5cWBK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} PA10检测串行电平
USART完成起始位检测和位采样
完整字节进入 USART_DR
USART_SR.RXNE置1
RXNEIE和NVIC触发中断
软件读取 DR
软件状态机解释包结构
参考手册能保证并说明 A~F;G 完全由我们的协议和 C 代码决定。若 CPU 未及时读 DR,而下一字节已经完成接收,SR.ORE 会置位,表示溢出错误,可能丢数据。数据包状态机不能修复已经丢失的字节,因此高数据率场景要使用更快的中断处理、环形缓冲区或 DMA。
14. 源码中必须认识的风险
14.1 文本数组没有边界检查
源码定义:
c
char Serial_RxPacket[100];
但状态 1 每收到普通字符就让 pRxPacket++,没有检查是否达到数组末尾。超过容量会写坏其他内存。安全逻辑至少应确保只写入 sizeof(Serial_RxPacket)-1 个字符,为末尾 \0 留一个位置;超长包应丢弃并复位状态。
14.2 异常包尾会让状态机卡住
- HEX 状态 2 若收到的不是
0xFE,源码一直等待0xFE; - 文本状态 2 若
\r后不是\n,源码一直等待\n。
教学代码容易理解,但正式项目通常会在错误时复位、重新把当前字节当作潜在包头,或增加超时机制,以便快速重新同步。
14.3 接收数组可能被同时读写
9-3 中 ISR 写 Serial_RxPacket,主循环读同一数组。若下一包来得太快,主循环可能读到两包混合内容。可选方案:
- 处理完才允许接收下一包,忙时丢包;
- ISR 写接收缓冲,完整后复制/交换到处理缓冲,即"双缓冲";
- 使用环形缓冲区保存原始字节,在主循环解析;
- 使用消息队列保存多包。
14.4 共享变量建议使用 volatile
Serial_RxFlag 在 ISR 和主循环之间共享,正式代码建议声明为 volatile uint8_t,防止编译器假定它不会被异步修改。volatile 只保证实际访问内存,不解决竞争、原子性或排队问题。
14.5 没有校验字段
包头包尾只能帮助划分边界,不能证明载荷正确。受干扰导致某个数据位翻转时,现有协议仍可能把错误载荷当成有效包。可靠通信应加入校验和或 CRC,并明确错误包的丢弃与重传策略。
15. 怎样仿制一个自己的数据包
先写协议表,再写代码,不要边写状态机边决定格式。
| 字段 | 示例 | 问题 |
|---|---|---|
| 包头 | 0xAA 0x55 |
如何快速重新同步? |
| 命令 | 1 字节 | 这一包要执行什么? |
| 长度 | 1 字节 | 载荷到底有多少字节? |
| 载荷 | N 字节 | 字段顺序、单位、字节序是什么? |
| 校验 | CRC16 | 校验覆盖哪些字段? |
然后按以下流程实现:
#mermaid-svg-sEoU21lSWe9VuFth{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sEoU21lSWe9VuFth .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sEoU21lSWe9VuFth .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sEoU21lSWe9VuFth .error-icon{fill:#552222;}#mermaid-svg-sEoU21lSWe9VuFth .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sEoU21lSWe9VuFth .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sEoU21lSWe9VuFth .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sEoU21lSWe9VuFth .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sEoU21lSWe9VuFth .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sEoU21lSWe9VuFth .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sEoU21lSWe9VuFth .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sEoU21lSWe9VuFth .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sEoU21lSWe9VuFth .marker.cross{stroke:#333333;}#mermaid-svg-sEoU21lSWe9VuFth svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sEoU21lSWe9VuFth p{margin:0;}#mermaid-svg-sEoU21lSWe9VuFth .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sEoU21lSWe9VuFth .cluster-label text{fill:#333;}#mermaid-svg-sEoU21lSWe9VuFth .cluster-label span{color:#333;}#mermaid-svg-sEoU21lSWe9VuFth .cluster-label span p{background-color:transparent;}#mermaid-svg-sEoU21lSWe9VuFth .label text,#mermaid-svg-sEoU21lSWe9VuFth span{fill:#333;color:#333;}#mermaid-svg-sEoU21lSWe9VuFth .node rect,#mermaid-svg-sEoU21lSWe9VuFth .node circle,#mermaid-svg-sEoU21lSWe9VuFth .node ellipse,#mermaid-svg-sEoU21lSWe9VuFth .node polygon,#mermaid-svg-sEoU21lSWe9VuFth .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sEoU21lSWe9VuFth .rough-node .label text,#mermaid-svg-sEoU21lSWe9VuFth .node .label text,#mermaid-svg-sEoU21lSWe9VuFth .image-shape .label,#mermaid-svg-sEoU21lSWe9VuFth .icon-shape .label{text-anchor:middle;}#mermaid-svg-sEoU21lSWe9VuFth .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sEoU21lSWe9VuFth .rough-node .label,#mermaid-svg-sEoU21lSWe9VuFth .node .label,#mermaid-svg-sEoU21lSWe9VuFth .image-shape .label,#mermaid-svg-sEoU21lSWe9VuFth .icon-shape .label{text-align:center;}#mermaid-svg-sEoU21lSWe9VuFth .node.clickable{cursor:pointer;}#mermaid-svg-sEoU21lSWe9VuFth .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sEoU21lSWe9VuFth .arrowheadPath{fill:#333333;}#mermaid-svg-sEoU21lSWe9VuFth .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sEoU21lSWe9VuFth .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sEoU21lSWe9VuFth .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sEoU21lSWe9VuFth .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sEoU21lSWe9VuFth .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sEoU21lSWe9VuFth .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sEoU21lSWe9VuFth .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sEoU21lSWe9VuFth .cluster text{fill:#333;}#mermaid-svg-sEoU21lSWe9VuFth .cluster span{color:#333;}#mermaid-svg-sEoU21lSWe9VuFth div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sEoU21lSWe9VuFth .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sEoU21lSWe9VuFth rect.text{fill:none;stroke-width:0;}#mermaid-svg-sEoU21lSWe9VuFth .icon-shape,#mermaid-svg-sEoU21lSWe9VuFth .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sEoU21lSWe9VuFth .icon-shape p,#mermaid-svg-sEoU21lSWe9VuFth .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sEoU21lSWe9VuFth .icon-shape .label rect,#mermaid-svg-sEoU21lSWe9VuFth .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sEoU21lSWe9VuFth .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sEoU21lSWe9VuFth .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sEoU21lSWe9VuFth :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写出协议字段表
为每个阶段定义状态
画状态转移条件
确定最大载荷和超时
编写发送封装
编写逐字节接收状态机
加入长度、边界和校验
测试正常包
测试错包头、错包尾、断包、粘包、超长包
16. 调试步骤
HEX 工程
- 串口助手发送和接收均切换到 HEX 模式;
- 发送
FF 11 22 33 44 FE; - OLED 应显示
11 22 33 44; - 再测试载荷含定界符:
FF FE FF 88 FE FE; - 按键触发 STM32 发送,检查电脑是否收到严格 6 字节。
文本工程
- 串口助手切换到文本模式,并启用发送换行;
- 发送
@LED_ON加 CRLF,LED 应点亮并返回LED_ON_OK; - 发送
@LED_OFF加 CRLF,LED 应熄灭; - 发送未知命令,检查
ERROR_COMMAND; - 测试缺少
@、缺少 LF、空载荷、超长载荷和连续快速发送。
17. 复习自测
- 为什么仅仅连续发送 XYZ 会出现字段错位?
- 固定包长时,为什么载荷中的
0xFF/0xFE不一定造成误判? static RxState为什么不能换成普通局部变量?- 为什么三个状态分支要用
else if? \r\n和\0分别属于线路协议还是内存字符串规则?RXNE代表一个字节到达,完整包标志由谁产生?RxFlag==0才接收新包解决了什么,又牺牲了什么?- 如何防止 100 字节文本缓冲区越界?
- 包头包尾能否检测载荷被干扰?为什么还需要 CRC?
- 高速连续数据为什么更适合环形缓冲区或 DMA?
18. 一句话总结
USART 硬件负责可靠地收发"一个字节",数据包格式负责规定"哪些字节属于一条消息",状态机负责在多次 RXNE 中断之间记住进度并恢复完整消息。