3 串口解析

串口解析知识层

这个主题解决什么问题

串口解析解决的是“上位机发来一串字符后,单片机如何判断一帧已经收完,并从字符串里提取参数或识别查询命令”的问题。

在蓝桥杯单片机题里,串口通常不是单纯回显,而是用来完成:

  • 设置参数,例如 H1:23.5F:100

  • 查询参数,例如 H1,?

  • 设置时间,例如 T:235950

  • 回复执行结果,例如 OK 或当前参数值

所以串口解析的核心不是 printf,而是把接收缓冲区里的字符转换成题目需要的变量更新和查询响应。

核心机制

串口解析可以拆成四个环节:

  • 中断接收:每收到 1 个字节,就放入 uart_rec_string

  • 帧结束判断:超过一段时间没有新数据,就认为这一帧收完

  • 内容解析:用关键字定位参数或查询命令

  • 收尾复位:清空缓冲区,重置索引和状态

原始笔记中的帧结束策略是:

 if (uart_index == 0) return;
 if (uart_tick < 10) return;

这里的稳定知识是:

  • uart_index == 0 表示当前没有收到任何数据,不需要解析

  • uart_tick < 10 表示距离上一个接收字节还不够久,先继续等

  • uart_tick >= 10 表示 10ms 内没有新字节,可以把当前缓冲区当作完整一帧

这种做法适合比赛里的简单命令帧。它不依赖固定帧头帧尾,写起来快,但前提是串口发送端不会故意制造非常复杂的粘包、拆包场景。

get_num 的知识点

get_num 的作用是:在接收缓冲区中寻找某个字段名,如果找到了,就读取字段名后面的数字。

原始笔记中的核心代码是:

 float get_num(const char* string)
 {
     char* p1 = NULL;
     float temp = 0;
 ​
     p1 = strstr(uart_rec_string, string);
     if (p1 == NULL) return -99;
 ​
     sscanf(p1 + strlen(string), "%f", &temp);
     return temp;
 }

它背后的关系是:

  • strstr(uart_rec_string, string) 用来找关键字,例如 "H1:"

  • 找不到关键字时返回 -99,表示本帧没有这个字段

  • p1 + strlen(string) 会跳过关键字,让 sscanf 从数字部分开始读

  • "%f" 可以读取整数或小数形式,再交给上层决定是否放大、取整或转换

例如缓冲区是:

 H1:23.5

调用:

 temp = get_num("H1:");

strstr 会定位到 H1:p1 + strlen("H1:") 指向 23.5,最终 temp 得到 23.5

参数设置和查询命令的区别

串口命令大致分成两类。

第一类是设置参数,用冒号后跟数字:

 H1:23.5
 H2:40.0
 F:100
 S:20
 T:235950

这类命令适合用 get_num("字段名:") 解析。解析成功后,更新变量并把 state 设为 0,最后回复 OK

第二类是查询参数,用类似 H1,? 的字符串:

 H1,?
 H2,?
 F,?
 S,?

这类命令不需要取数字,只需要用 strstr 判断命令是否存在,然后把 state 设置成对应查询编号,最后在 switch 中输出对应值。

稳定分工是:

  • 设置类命令:字段定位 + 数字读取 + 更新变量 + 回复 OK

  • 查询类命令:字符串匹配 + 选择输出项 + 回复当前值

state 的意义

原始笔记里的 state 不是系统状态机,而是“本帧串口处理结果”的临时分类。

常见含义是:

  • state = 0:本帧是有效设置命令,回复 OK

  • state = 1:查询 H1

  • state = 2:查询 H2

  • state = 3:查询 F

  • state = 4:查询 S

  • state = 88:默认无效状态,不回复有效内容

处理结束后要把它恢复为默认值:

 state = 88;

否则下一帧如果解析失败,可能沿用上一帧的 state,导致误回复。

数量级和数据类型

串口读到的数字不等于最终变量的存储格式,设置参数时必须处理数量级。

原始笔记中:

 H1 = temp * 10;
 H2 = temp * 10;
 F = temp;
 S = temp;

说明 H1/H2 可能要保存为 10 倍值,而 F/S 直接保存为整数。稳定知识是:串口输入可以是小数,但内部变量要按题目显示、比较、存储的数量级统一。

设置时间时还有一个数据类型风险:

 ucrtc[0] = (unsigned long int)temp / 10000 % 100;
 ucrtc[1] = (unsigned long int)temp / 100 % 100;
 ucrtc[2] = (unsigned long int)temp % 100;

235950 大于 unsigned int 常见的 65535 范围,所以拆 HHMMSS 这类六位数时不能直接依赖 16 位 unsigned int。应先转成 unsigned long int 再做除法和取模。

清缓冲和复位

一帧处理完后必须收尾:

 memset(uart_rec_string, 0, uart_index);
 uart_index = 0;
 state = 88;

这三个动作职责不同:

  • memset 清掉旧字符,避免下一帧 strstr 匹配到残留内容

  • uart_index = 0 告诉接收逻辑从缓冲区开头重新存

  • state = 88 清掉本帧结果,避免下一帧误用

如果只清 uart_index 不清字符串,在某些情况下旧内容仍可能被 strstr 搜到。比赛里最稳的做法是每帧处理完都清缓冲和复位状态。

依赖的前置知识

要稳定使用串口解析,需要先理解:

  • 串口中断如何把 SBUF 存入接收缓冲区

  • uart_index 表示当前缓冲区已接收长度

  • uart_tick 由 1ms 节拍累加,用来判断接收间隔

  • strstr 用来找子串,strlen 用来跳过字段名

  • sscanf 可以从字符串中按格式读取数字

  • printf 最终会通过串口发送回复

常见错误

  • 忘记判断 uart_tick < 10,导致一帧还没收完就提前解析

  • 缓冲区太小,命令稍长就越界或截断

  • get_num 找不到字段时没有特殊返回值,导致误把默认数字写入参数

  • -99 当错误标记时,又允许合法参数等于 -99

  • 只清 uart_index,不清接收缓冲区,导致旧命令残留影响下一帧

  • 处理完不重置 state,下一帧无效命令沿用上一帧回复

  • H1/H2 等带小数参数忘记放大存储,显示或比较时数量级混乱

  • T:235950 这类六位时间时使用 16 位整数,导致溢出

  • 设置命令和查询命令的匹配条件写得过宽,导致一帧里多个条件同时命中

这份知识层的边界

这份笔记只说明蓝桥杯场景下的轻量串口命令解析:如何判断帧结束、如何从字符串取数、如何区分设置和查询、如何复位缓冲。

暂时不展开:

  • 严格协议帧头帧尾和校验和

  • 粘包、拆包、环形缓冲区的完整工程级处理

  • 多命令同时存在时的优先级设计

  • 完整可抄写的执行层代码骨架

  • 比赛题中什么时候应该选择串口解析、什么时候只保留回显

这些内容后续应继续放到:

  • 03-决策层

  • 04-执行层

  • 05-练习层

超短复习卡

  • 串口解析 = 接收缓冲 + 10ms 空闲判帧 + 字符串匹配 + 清缓冲复位

  • get_num("H1:"):先 strstr 找字段,再 sscanf 读字段后的数字

  • 设置命令用 字段名:数字,查询命令用 字段名,?

  • state 只表示本帧处理结果,处理完必须恢复默认值

  • 小数参数要统一内部数量级,六位时间拆分要防 16 位溢出

  • 每帧结束后清 uart_rec_string、清 uart_index、重置 state