C语言基础回炉第五天:UART 帧解析与校验
前言
前一天我完成了环形缓冲区,解决的是“UART 接收来的字节先放在哪里”的问题。今天继续往后走,开始处理“这些字节怎样组成一条有效消息”。
这次练习文件是 03_uart_frame_parser.c,目标是从一段二进制字节流中找到第一条合法 UART 数据帧。它不是字符串处理,而是协议解析:要识别帧头、读取长度、提取命令和 payload,还要用 checksum 判断这条帧是否可靠。
今天的完成情况
本次完整运行 Day5 相关练习,关键输出如下:
03_uart_frame_parser passed
Day5 UART 帧解析练习通过
这说明今天的 UART 帧解析练习已经通过当前测试。
一、这次协议长什么样
本练习约定的帧格式是:
0xAA 0x55 LEN CMD PAYLOAD... CHECKSUM
每个字段的含义如下:
0xAA 0x55:帧头,用来从连续字节流中定位一条消息的开始。LEN:表示CMD + PAYLOAD的总长度。CMD:命令字,表示这条消息要做什么。PAYLOAD:命令携带的数据,长度为LEN - 1。CHECKSUM:从LEN到 payload 结束的低 8 位累加和。
例如测试中的合法帧:
uint8_t valid[] = {
0xAA, 0x55, 0x03, 0x10, 0x01, 0x02, 0x16
};
这里 LEN = 0x03,表示后面有 3 个参与业务的数据字节:CMD=0x10,payload 为 0x01 0x02。checksum 的计算是:
0x03 + 0x10 + 0x01 + 0x02 = 0x16
所以最后一个字节 0x16 是正确校验和。
二、解析函数的核心流程
今天的核心函数是:
static int ParseFirstFrame(const uint8_t *stream,
uint16_t stream_len,
UartFrame *frame,
uint16_t *used_len)
它的返回值设计比较清晰:
1:找到一条合法帧。0:当前没有合法帧,或者数据还不完整。-1:参数无效。
解析过程可以拆成 6 步:
- 检查
stream、frame、used_len是否为空。 - 从字节流中查找帧头
0xAA 0x55。 - 找到帧头后读取
LEN。 - 判断长度是否合法,不能小于 1,也不能超过最大 payload 限制。
- 判断当前字节流里是否已经包含完整帧。
- 计算 checksum,校验通过后填充
UartFrame。
其中最容易写错的是下标关系。以帧头位置 i 为起点:
i + 0: 0xAA
i + 1: 0x55
i + 2: LEN
i + 3: CMD
i + 4: PAYLOAD 起始位置
i + 3 + LEN: CHECKSUM
完整帧长度是:
2 字节帧头 + 1 字节 LEN + LEN 字节内容 + 1 字节 CHECKSUM
= LEN + 4
因此代码里判断完整帧时使用:
if (i + len + 4u > stream_len) {
break;
}
三、今天踩到的错误
一开始测试失败在第一条合法帧:
FAIL ...03_uart_frame_parser.c:130 expression:
ParseFirstFrame(valid, sizeof(valid), &frame, &used_len) == 1
根因是 checksum 的位置算错了。原先把校验和位置理解成:
stream[i + 2 + len]
但这个位置实际上还在 CMD + PAYLOAD 区间内。正确的 checksum 位置应该跳过帧头、LEN 和 LEN 个内容字节:
stream[i + 3 + len]
同时,used_len 也应该返回从字节流开头到这条完整帧结束的总消耗长度:
*used_len = i + len + 4u;
这个错误很典型:协议解析里差一个字节,下游看到的就不是“差一点”,而是整条帧都解析失败。
四、测试覆盖了哪些场景
当前测试不是只测了一条正常数据,而是覆盖了几个重要边界:
- 正常合法帧可以解析出
cmd和 payload。 - 前面存在噪声字节时,解析器可以跳过噪声找到后面的合法帧。
- 半包数据不会被误认为合法帧。
- checksum 错误的数据会被拒绝。
- 空指针参数会返回
-1。
其中 noise_then_valid 这个用例很贴近真实串口场景。UART 是连续字节流,接收缓冲区里不一定刚好从帧头开始,可能前面残留了无效字节。解析器不能假设第一个字节就是 0xAA,必须有同步能力。
五、和嵌入式开发的关系
在 STM32 项目中,UART 接收通常会和中断、DMA、环形缓冲区或者 FreeRTOS 队列结合使用。接收层负责把字节安全地收下来,协议层负责判断这些字节能不能组成一条有效消息。
一个更合理的分层可以是:
UART ISR / DMA
-> 环形缓冲区或队列
-> 协议解析器
-> 命令处理逻辑
这样做的好处是 ISR 不需要执行复杂业务逻辑。查找帧头、计算 checksum、复制 payload 这些操作都可以放在主循环或任务中完成,避免中断执行时间过长。
这也让我更清楚地理解了昨天和今天的衔接:
- 环形缓冲区解决“字节流暂存”。
- 帧解析器解决“从字节流中识别消息”。
- 后续命令处理解决“消息对应什么业务动作”。
六、今天必须能口述的问题
这部分是我今天真正需要能说出来的内容。代码通过测试只是第一步,如果不能解释下面几个问题,说明对协议解析还没有真正消化。
1. 一帧通常由哪些字段组成?
一帧通常由帧头、长度、命令字、数据区和校验字段组成。帧头负责同步位置,长度字段说明后面有多少有效内容,命令字说明消息类型,payload 保存业务数据,checksum 或 CRC 用来判断这一帧在传输或接收过程中有没有明显错误。
用今天的协议表示就是:
0xAA 0x55 LEN CMD PAYLOAD... CHECKSUM
真实项目里的字段可能更多,比如设备地址、序号、版本号、CRC16 等,但核心思想一样:先解决“从哪里开始”,再解决“有多长”,最后解决“是否可靠”。
2. LEN 表示什么,为什么容易算错?
今天这个练习里,LEN 表示 CMD + PAYLOAD 的长度,不包含帧头本身,也不包含 LEN 字段自己,更不包含最后的 CHECKSUM。
也就是说:
payload_len = LEN - 1
full_frame_len = LEN + 4
它容易算错,是因为不同协议对 LEN 的定义不一样。有的协议让 LEN 只表示 payload 长度,有的表示从命令字到校验前的长度,有的甚至包含校验字段。如果不先把定义写清楚,后面的 checksum 位置、payload 长度和完整帧长度都会跟着错。
今天的 bug 本质上就是下标边界没有按这个定义统一起来。
3. 半包为什么不能直接当错包丢弃?
半包不是错误数据,而是“数据还没收完”。UART 是连续字节流,程序某一次读取缓冲区时,可能只拿到了帧头和一部分内容,剩下的字节还在路上。
例如:
AA 55 03 10
这只能说明当前还缺 payload 和 checksum,不能说明这条帧是坏的。如果直接丢弃,后续真正到达的字节就失去了前面的上下文,原本完整的一帧会被人为拆坏。
所以半包应该返回“需要更多数据”或“当前没有完整帧”,并把已有数据保留在缓冲区中,等待下一次接收后继续解析。
4. checksum 错误后如何继续寻找下一帧?
checksum 错误说明当前这个候选帧不可信,但不能简单清空整个缓冲区。正确做法是从当前帧头的下一个字节继续扫描,重新寻找新的 0xAA 0x55。
原因是坏帧后面可能紧跟着一条好帧,如果一次性把后面的数据全部丢掉,就会误删有效数据。
实际解析策略可以概括为:
找到帧头 -> 长度足够 -> checksum 错
继续从当前帧头后一位扫描,而不是清空全部数据
这也是协议解析器“重新同步”的能力。串口通信不保证每次读取都刚好对齐到一帧的开头,所以解析器必须能从错误中恢复。
5. 噪声字节为什么必须跳过?
噪声字节是帧头之前的无效数据,可能来自上电瞬间、电气干扰、上一次残留数据,或者程序开始读取时刚好切在一帧中间。
如果解析器要求第一个字节必须是 0xAA,那只要缓冲区前面多一个无效字节,后面的合法帧也会解析失败。今天的 noise_then_valid 测试就是这个场景:
00 33 AA 55 02 20 7E A0
前面的 00 33 不是有效帧的一部分,但后面的 AA 55 ... 是合法帧。解析器要跳过噪声,并在成功解析后让 used_len 包含这些被跳过的字节,这样调用者才能从缓冲区里移除已经处理过的部分。
6. UART 粘包与 TCP 粘包有什么共同点?
共同点是:它们都提供连续字节流,而不是天然的一条条业务消息。
发送端可能分多次发送一帧,接收端一次只读到半帧;发送端也可能连续发送多帧,接收端一次读到多帧拼在一起。这就是“半包”和“粘包”问题。
解决思路也相同:应用层协议必须自己定义消息边界。常见做法包括:
- 固定长度。
- 帧头 + 长度。
- 分隔符。
- 长度 + 校验。
今天的 UART 练习使用的是“帧头 + 长度 + checksum”。TCP 场景里也经常用类似结构,只是底层传输介质从串口换成了网络连接。
7. 为什么二进制协议不能使用 strlen 判断长度?
strlen 只能用于以 \0 结尾的 C 字符串,它遇到字节 0x00 就会认为字符串结束。但二进制协议里的 payload 可以包含任意字节,包括 0x00。
例如 payload 是:
01 00 02
这在二进制协议里是 3 个有效字节,但如果按字符串处理,strlen 会在 00 处提前停止,得到错误长度。
所以二进制协议必须使用显式长度字段,比如 stream_len、LEN、payload_len,不能依赖字符串结束符。
七、今天的收获
今天主要掌握了 UART 帧解析中的几个关键点:
- 二进制协议不能按字符串思路处理,必须严格按字节和长度字段解析。
LEN的定义要先讲清楚,否则 checksum、payload 和完整帧长度都会算错。- 帧头可以帮助解析器从噪声中重新同步。
- 半包和错包不能强行解析,要返回“当前没有合法帧”。
- 协议解析里最容易出错的是下标和边界,测试必须覆盖这些场景。
总结
Day5 完成了 UART 二进制帧解析练习,实现了从字节流中查找帧头、校验长度、提取命令和 payload、验证 checksum 的基本流程。今天的 UART 帧解析练习已经通过验证。
下一步可以继续围绕 UART 做更贴近真实工程的训练,例如把环形缓冲区和帧解析器串起来,模拟连续接收多帧数据,或者增加更多异常场景,比如 payload 中包含帧头、checksum 溢出、坏帧后紧跟好帧等。
更多推荐


所有评论(0)