制氧机触控屏主控板(HC32L136K8TA + DGUS 串口屏)
做这块板子的初衷是练一件我一直不太顺手的事:在资源最紧的一档里把通信和状态组织清楚。
它没有 RTOS、没有外部存储、没有屏幕显存,所有显示都交给一块串口屏,主控只往屏的变量地址里写数;
传感器侧则通过一路 9600 的低功耗串口,周期性收一帧 12 字节的数据。
功能最后都跑通了:屏能显示、能按键、能定时、能倒计时、能遥控。
但我在上面留下了一条至今没有追到根因的 delay1ms(1),也留下了一块没有 CRC、
没有备份、地址硬编码的参数区——这两件事都写在这篇记录里。
一、这块板要做什么
它的角色是制氧设备上的人机界面主控板:屏幕负责显示和触摸,主控负责 “把界面要显示的数算出来、把用户按下的键翻译成动作”。整块板子对外只有两路串口:
- 触控屏那一路:115200、8N1。用的是串口屏方案——屏自己带显存和字库, 主控只按变量地址(VP)读写数值、切换页面、开关控件。
- 传感器板那一路:9600,走的是低功耗串口(LPUART)。 传感器板周期性上报一帧 12 字节的数据:氧气浓度、湿度、温度、气压。
主控在这两条链路之间做的业务,比通信本身多得多:氧浓度的上下限设定与校准、 三组时段定时开关、倒计时、红外遥控(NEC 编码)、关机与密码管理、风机与清洁键一类的开关量输出、 以及把气压换算成绝对海拔、再把氧浓度换算成“等效海拔”和氧分压。
主控是 HC32L136K8TA,Cortex-M0+ 内核,64 KB Flash、8 KB SRAM;
系统时钟用内部高速 RC 经 PLL 倍频到 48 MHz,不依赖外部晶振;
没有 RTOS,main() 里就是 while(1){ user_main(); },
编译器优化等级开的是 -O0。这块板的资源清单如下:
| 器件 / 外设 | 接口 | 用途 |
|---|---|---|
| DGUS 串口屏 | UART,115200 8N1 | 触控界面:读写 VP 变量地址、切页、模拟触摸 |
| 传感器板 | LPUART,9600 | 接收氧浓度 / 湿度 / 温度 / 气压的 12 字节帧 |
| GT20L16S1Y 点阵字库芯片 | 软件 SPI | 汉字与 ASCII 点阵取模 |
| 74HC595 | 软件移位 | 输出扩展 |
| 红外接收头 | TIM3 + 高级定时器输入捕获 | NEC 红外遥控解码(按脉宽判断 0/1) |
| 蜂鸣器 / 按键 | GPIO | 按键音与报警、本地按键 |
| 内置 Flash | — | 参数掉电保存(按项编号硬编码,本文不列地址) |
| 内置 ADC | — | 顺序扫描采集 |
二、资源极度受限的后果:8 KB SRAM 下怎么写代码
8 KB SRAM 是什么概念?先把通信这一块的账算出来,因为这是最容易低估的部分。 这块板子上常驻的缓冲区一共是这几项,每一项的大小都在本地定死:
| 用途 | 谁占的 | 量级 |
|---|---|---|
| 传感器帧接收区 | 传感器那一路的整帧接收缓冲 | 按一帧 12 字节、留足余量来定 |
| 屏帧接收区 | 触控屏那一路的整帧接收缓冲 | 与传感器那一路同一量级 |
| 屏帧发送区 | 组帧用的发送缓冲 | 按“一帧最多带多少个字”反推,量级最小 |
| 环形队列 1 | 对应一路串口的收发队列 | 与另一路同量级 |
| 环形队列 2 | 对应另一路串口的收发队列 | 同上 |
| 环形队列 3 | 第三组队列 | 比前两组略大 |
| 小计 | 不含结构体头、栈与其余全局量 | 按各项加总约占几个 KB 里的一大块 |
把上面这些按各自的长度加总,结果已经占掉了 8 KB 里的很大一块比例, 而栈、全局状态、字库与按键/红外/ADC 的缓冲都还没算。剩下的余量只够“处处省”:
省法一:不上 RTOS,用三段状态机加软延时排班
没有任务栈、没有堆、没有调度器的数据结构,所有逻辑在一个大循环里排队。
需要“等一会儿”的地方不用 delay_ms,而是三套独立的非阻塞软延时
(no_blocking_delay_1/2/3):每个实例自己维护一个“首次调用”标记和上次的时间戳,
时间到返回 1、没到返回 0,调用方直接跳过就行。这样“等 500 ms”不会把整个循环堵死。
关于这种写法的展开,我在
《不用 RTOS 的固件怎么写:时间片轮询 + 消息队列》
里写过更系统的整理。
省法二:把状态塞进一个 u32
工程里有一个叫 User_BIT 的 32 位位域集合,塞了二十多个业务标志:
指示灯、传感器数据有效、两段式主状态、6 位的 send_type(合成命令类型)、
通信状态、2 位的接收状态、倒计时启动、三组时段、红外开关、4 位的环境状态、
睡眠、自动睡眠控制等等,全在一个 4 字节的变量里。
在 8 KB 的机器上,这不是炫技,是实打实的省。
省法三:参数直接落内置 Flash,地址硬编码
没有外部 EEPROM,所有参数项全部存在内置 Flash 里, 每一项的地址以宏的形式硬编码在头文件里。 地址的排布有一个明显的规律:每项占两个字节,各项之间按一个固定的步进递增, 低字节像是值、高字节像是一个序号。具体从哪个地址起、步进是多少,本文不列出。
这种写法很省事,但它没有 CRC,也没有备份。结构一旦变动, 老设备里已经存好的参数位置就对不上了;而且单区写入遇到掉电或位翻转,没有任何机制能发现。 这一块我在 《嵌入式参数存储设计:Flash 分区、双区备份与 CRC 校验》 里做了一次完整的返工设计,这里只记下问题。
三、与串口屏的通信:DGUS 帧格式与自加 CRC
串口屏的指令集本身很简洁:一个固定帧头,一个长度字节,一个命令字节,
两个字节的变量地址,然后是数据。屏厂公开的开发指南里,标准帧是这样的:
5A A5 <LEN> <CMD> <ADDR_H> <ADDR_L> <DATA...>,
其中 CMD = 0x82 表示写、0x83 表示读,
LEN 是它自己之后、数据为止的字节数。
这个工程在标准帧的基础上做了一处扩展:在数据后面追加两个字节的 Modbus CRC16(小端), 校验范围从命令字节开始(也就是发送数组里命令字节那一位)一直到数据末尾。 源码注释里把这个约定写得很清楚,还留了一个完整的实例: 一帧写命令里,长度字节之后正好是“命令 + 两个地址字节 + 四个数据字节”, 而四个数据字节恰好是单精度浮点的长度。我没有把那个具体实例的字节抄进来—— 它带着实际使用的变量地址与写入值,属于屏工程里的分配,本文只讲结构。
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2 字节 | 固定 5A A5 |
| LEN | 1 字节 | 它之后、CRC 之前的字节数 = 1(命令) + 2(地址) + 2 × 字数 |
| CMD | 1 字节 | 0x82 写 / 0x83 读 |
| ADDR | 2 字节 | VP 变量地址,高字节在前 |
| DATA | 2 × n 字节 | 每个参数占 2 字节(16 位寄存器模型) |
| CRC | 2 字节 | Modbus CRC16,低字节在前;从 CMD 算到数据末尾 |
读命令(0x83)的结构类似:帧头 + LEN + 命令 + 2 字节地址 + 要读的字数。
比如查询当前页面号就是读 1 个字,查询主界面状态就是一次读 3 个字。
组帧这一段我按个人理解重写成最小片段,并把实际的变量地址换成了占位符:
/* 组一帧“往 VP 写 n 个字”的屏指令(含自加 CRC16),示意片段 */
static unsigned char dgus_build_write(unsigned short vp, const unsigned char *data, unsigned char cnt)
{
unsigned char i, n = 0;
buf[n++] = 0x5Au; buf[n++] = 0xA5u; /* 帧头,屏厂公开协议 */
buf[n++] = (unsigned char)(1u + 2u + 2u * cnt); /* LEN */
buf[n++] = 0x82u; /* 0x82 写 / 0x83 读 */
buf[n++] = (unsigned char)(vp >> 8); buf[n++] = (unsigned char)(vp & 0xFFu);
for (i = 0; i < (unsigned char)(2u * cnt); i++) buf[n++] = data[i];
/* 这里追加 2 字节 Modbus CRC16:从命令字节算到数据末尾,低字节先发 */
return n; /* 返回整帧长度,交给串口发送 */
}用这个结构对一下前面的实例:长度字节正好等于“1 个命令 + 2 个地址 + 2 × 字数”, 整帧在 CRC 之前的部分加起来和注释里的实例完全一致。 发送缓冲只有很小的几十字节,也就是说一帧最多带十几个字——这就是为什么工程里 把参数打包成一条写命令,而不是一个参数发一帧: 数据采集那一条命令一次把氧浓度、温度、湿度、气压、等效海拔、绝对海拔、氧分压 全部送进同一个起始地址起的一段连续地址,每一项占两个字节。
两个特殊的变量地址
屏厂在变量空间里另外留了两个用途固定的地址,用起来非常方便 (具体地址值由屏工程分配,本文不列出):
- 页面切换:往它写一个短帧
5A 01 00 <页面号>,屏就切到指定界面。 - 模拟触摸:往它写一个短帧,可以让屏“以为”自己被触摸了一次, 这是主控替屏补一次触摸事件的唯一办法。
这个工程用到的 VP 地址全部是按屏工程分配的变量地址, 地址值本身随屏工程走,本文不列出。但它们按用途可以分成几类, 这才是看这块板子时真正有用的一张表:
| 用途类别 | 具体的量 | 读写方向 |
|---|---|---|
| 屏厂保留地址 | 页面切换、模拟触摸 | 主控写,屏自己执行 |
| 系统信息类 | 系统时间、当前页面号、当前亮度 | 主控读 |
| 开关与模式类 | 自动睡眠开关、自动睡眠时间、唤醒、清洁键关闭、风机 | 主控读写 |
| 设定值类 | 氧浓度设定、海拔设定、时段设定、倒计时设定 | 主控读写 |
| 状态回读类 | 主界面状态 / 手自动切换、环境状态、密码查询 | 主控读 |
| 数据展示类 | 数据采集那一条命令写入的连续一段(氧浓度、温度、湿度、气压、两种海拔、氧分压) | 主控写 |
| 整体刷新 | 让屏把一屏内容整体重刷 | 主控写 |
界面同样用代号表示,切页时直接写页面号。工程里一共二十多个界面 (主界面、各类设置界面、中文键盘、密码界面与几类错误提示界面等), 页面号的分配由屏工程决定,本文不逐个列出。 值得记下来的是这一类设计的共同点:主控和屏之间只交换编号与数值,不传字符串, 显示什么文字完全由屏侧的工程文件决定。
顺便说一句我在这件事上的判断:给屏的协议自加 CRC 是划算的。 串口屏通常不校验主控发来的数据,而主控到屏这一段线往往是整机上最长的一段, 一旦误码,用户看到的就是“界面显示了奇怪的数”。 加上 CRC 之后,至少能区分“数据错了”和“逻辑错了”这两类完全不同的故障。 同时也要记住:这是自定义扩展,屏侧不会替你校验, 接收回来的帧必须由主控自己校验,不能指望屏帮你挡。
同一批项目里还有一块自己扫描 LED 单元板的控制卡:它的点阵必须由主控在 SRAM 里组织, 所以要先算清楚内存预算(我写在 《点阵显示的内存预算:一次把 SRAM 算清楚》 里)。 这块板子把显示整体外包给了屏,等于省掉了那一整笔账—— 代价是屏侧状态和主控状态变成了两份,后面第七节那条“自动睡眠”的坑就是这么来的。
四、与传感器板的通信:双重校验的 12 字节帧
传感器那一路是 9600 的低功耗串口,帧长固定 12 字节,多字节量统一用大端:
| 字节位置 | 含义 | 说明 |
|---|---|---|
Byte[0..2] | 帧头 / 命令 | 解析时没有显式校验 |
Byte[3..4] | 氧气浓度 | 大端 16 位 |
Byte[5..6] | 湿度 | 大端 16 位 |
Byte[7..8] | 温度 | 大端 16 位 |
Byte[9..10] | 气压 | 大端 16 位 |
Byte[11] | LRC 校验 | 前 11 字节的校验字节 |
校验有两条,两条都过才算有效帧:
LRC(recvbuf_sensor, 11) == recvbuf_sensor[11]:按前 11 字节算校验字节并与第 12 字节比较;sum(recvbuf_sensor[0..11]) & 0xFF == 0:全帧 12 字节累加后取低字节,必须为 0。
任意一条不过,就 memset 清空接收区、把整帧丢掉。
这里我有一个保留意见:如果 LRC 就是最常见的“累加取补”定义,
那么第 1 条成立时,全帧累加必然为 0,第 2 条只是第 1 条的推论,
并不会带来额外的检错能力。真想加“第二重保险”,应该换一个不同的维度,
比如换成 CRC16、或者校验“帧头与长度是否自洽”。
多写一条判断的成本几乎为零,所以我理解这种写法,但不建议把它当成“两重校验”来宣传——
真正薄弱的地方其实是那 3 个字节的帧头:解析时没有校验,长度又固定,
只能靠超时和校验字节兜住。
超时与帧完整性
两条链路的超时阈值不一样,这个差异是有意的:
- 传感器通道:累计
recvtimeout_sensor >= 5000(5 s)没有新字节就清空接收区。 传感器板是周期性上报,正常间隔远小于 5 s;超时说明线掉了或那一侧停了, 清掉半帧,避免“半帧 + 新帧”被拼成一帧。 - 触控屏通道:累计
recvtimeout_control >= 1000(1 s)清空。 屏这一路是问答式的,1 s 足够。 - 屏帧完整性:用
recvlenth_control >= (3 + recvbuf_control[2])判断—— 帧里的第 3 个字节就是 LEN,而整帧长度 = 2 字节帧头 + 1 字节 LEN + LEN,所以这个式子是自洽的。
这三条放在一起看,能看出一个共同点:这一层的设计目标不是“解析得快”,而是“绝不把半帧当整帧用”。 在只有一个 256 字节接收区、没有 DMA 环形缓冲的机器上,这个取舍是对的。
五、气压 → 海拔 → 氧分压的换算链
传感器给出的三个量(氧浓度、气压、温湿度)本身没什么可看的, 用户真正关心的是“我现在相当于处在一个什么样的空气环境里”。 所以主控要把它们换算成三个更有意义的数:绝对海拔、等效海拔、氧分压。 这条换算链我没有贴代码,因为它带着标定与拟合系数; 但它的输入、公式来源和遇到的问题都可以讲清楚:
| 要算的量 | 输入量 | 公式来源 | 说明 |
|---|---|---|---|
| 绝对海拔 | 气压 | 公开的气压高度公式 | 用标准大气压做基准、按指数关系反推高度;气压要先把单位统一到 kPa 再代入 |
| 氧分压 | 气压、氧浓度 | 分压的定义式 | 氧分压 = 气压 × 氧浓度,只差一个单位换算;系数里藏的其实是浓度与气压各自的单位 |
| 等效海拔 | 氧浓度 | 两段线性近似(非公开公式) | 把氧浓度翻译成“相当于在多少米海拔呼吸空气”;我用了两段线性近似,因为原公式在低浓度段反算出来的值不单调 |
| 几个量的下限 | 上面全部结果 | 工程里的钳位约定 | 负值一律夹到 0(温度、湿度、海拔同理),等效海拔另有一个显示上限 |
按这个顺序走一遍就是完整流程:第一步统一单位 → 第二步按公开公式算绝对海拔 → 第三步按分压定义算氧分压 → 第四步用两段线性近似算等效海拔 → 第五步统一做钳位。 每一步都能单独验证,这也是我后来把这条链拆开重写的原因。
单位是我按算式反推出来的
原来的实现里单位没有一处注释说明,但算式本身把单位定死了, 只能反推:气压那一路必然是先按标准大气压的基准换算过的, 氧浓度那一路必然是“以 0.1% 为单位”的整数表示, 否则算出来的数不会落在海平面那个熟悉的量级上。 这里我不列出具体数值,只保留这条经验: 当单位藏在算式里时,唯一的验证办法就是把已知的物理量代进去,看它会不会落在常识范围内。
这类“单位藏在算式里”的写法是我最想改的一处:那些除以 10、除以 1000 的换算 散落在各个表达式里,没有一处统一定义。单位错一次,后面所有结论都会错, 而且错得很隐蔽——界面上的数值看起来仍然“像个合理的数”。
等效海拔:一个我没查到解释的跳变
“等效海拔”的思路我很喜欢:把氧浓度翻译成“相当于在多少米海拔呼吸空气”, 比单纯显示一个百分比直观得多。但我把两段公式在分界点上各代了一次, 发现它们接不上:
- 分界点偏低的那一侧落在第二段,算出来的等效海拔明显偏小;
- 分界点偏高的一侧落在第一段,算出来的等效海拔反而更大。
两侧差了很大一截,而且方向是反的——浓度高一点,算出来的等效海拔反而更大, 这不符合物理直觉。我没有在源码里找到对这个跳变的解释, 也没有确认屏侧是否只把它当显示值用。 我的判断是:这两段公式来自两份不同的拟合资料,分界点更像是在“区分传感器读数是否可信”, 而不是物理量本身的拐点。 如果让我改,我会换成一张小的查表,并且在表里加一条单调性检查 (把分界点前后各代一次,确认不会来回跳), 至少保证显示出来的数不会在某个点附近突然反转。
其余两个细节
氧浓度校准:Flash 里存一个校正值,用一个特定的取值作为“不校正”的基准, 只有它偏离这个基准时才参与运算。 但这种把“基准值”和“校正量”混在同一个数里、还带一个偏移量的写法很容易看错, 我自己在读的时候也停顿了一下。我的建议是用明确的千分比系数,并在定义处写明单位。
环境状态判定:把温湿度组合映射成几档文案——清爽舒适、微凉干燥、 微凉湿润、微凉、湿润、干燥、炎热、炎热干燥、炎热湿润,用一个很短的位域存在状态字里, 界面拿到编号直接显示对应文案。这种“枚举换文案”的做法在 8 KB RAM 的机器上很划算: 主控不需要存任何字符串。
六、通信状态机与合成数据类型
整块板子的业务逻辑挂在一个三段式状态机上:
DATA_SYNTHESIS(0) → COMMUNICATION(1) → DATA_PROCESSING(2),
由一个 State_management() 轮流推进;通信阶段内部再分成
SEND_STATE(0) 和 RECEIVE_STATE(1)。接收结果用 2 位表示:
0 = 未接收完成、1 = 接收完成、3 = 超时。
关键是合成数据类型:一个 6 位的 send_type,
一共定义了 34 种业务命令。合成阶段先决定“这一轮要干哪件事”,
通信阶段只负责把这一条发出去、等回来,处理阶段再按类型码决定怎么解释回来的数据。
下表是我挑出来的一部分:
| 编号 | 命令 | 做什么 |
|---|---|---|
| 0 | QUERY_PAGE | 查询当前页面号 |
| 1 | DATA_COLLECTION | 把传感器数据打包写给屏 |
| 2 | QUERY_MAIN_INTERFACE | 查询主界面状态 |
| 3 | MANUAL_OFF | 手动关机 |
| 4 | SWITCH_PAGE | 切换页面 |
| 11 | START_COUNTDOWN | 启动倒计时 |
| 12 | COUNTDOWN_SYNCHRONIZATION | 倒计时同步 |
| 13 | COUNTDOWN_ON | 倒计时开关 |
| 19 | QUERY_SYSTEM_TIME | 查询系统时间 |
| 29 | SIMULATION_TOUCH | 模拟触摸 |
| 32 | HAND_SWITCH | 手自动切换 |
| 33 | CONTROL_FAN | 风机控制 |
我的理解是:在没有 RTOS、队列深度也很浅的情况下,
这是把“很多业务都想说话”排成一队的最省内存的办法——
不需要每个业务各占一套收发缓冲,也不需要回调注册表,
一个 send_type 加一个状态机就够了。
代价也很清楚:同一时刻只能有一条命令在飞。
数据采集(要周期性刷新)、倒计时同步、页面查询、按键响应全都要排队,
优先级靠“轮转 + 状态位”协商。所以在这类架构里,“实时性”实际上等于“轮转周期”,
而不是任何调度参数。我的下一个版本会把它换成带优先级的队列——
队列本身其实已经有了(两个 512 字节的环形队列加一个 768 字节的,
配套 mQueueSend / mQueueReceive),只是没有优先级。
位域打包也是同一套思路的延伸:一个 User_BIT(32 位)里放了二十多个标志,
send_type 也在其中。好处是省,坏处是调试时“这个位现在是什么”必须靠查表,
而且位宽一旦调整就会波及所有引用点。我现在的习惯是在定义旁边贴一份“位图注释”,
把每一位的名字和宽度画出来。
七、问题与解决
这一节是我写这篇记录的主要原因。下面 6 条都是我在这个工程里真实遇到并处理过的, 按“现象 → 原因 → 办法”整理:
| 现象 | 原因 | 办法 |
|---|---|---|
手动输入倒计时后重新上电,倒计时不可用;状态机本该从 START_COUNTDOWN 走到 COUNTDOWN_ON,却始终走到查询页面,程序也没死机 |
当时没有定位到根因;合理推测是状态机轮询太快,屏的应答还没回来就进入下一轮判断,接收标志还是上一轮的残留值,把状态机带偏了 | 在状态机循环末尾加 delay1ms(1) 限制轮询速率。加上之后恢复正常(详见下面单独一段) |
| 自动睡眠时间改完、重新上电后不生效 | 屏侧的自动睡眠逻辑要收到一次触摸事件才会被激活,重新上电后没有触摸,主控写入的值就没有“生效的时机” | 开机时往模拟触摸那个特殊地址主动下发一次模拟触摸帧,替屏补上那次事件 |
| 字库芯片上电后第一次取模的数据是错的 | 初始化完成后的首次读取时序不满足,第一帧数据不可信 | 初始化后先做一次丢弃性的“模拟读取”,把第一次结果丢掉再用 |
| 取模地址计算担心中间结果溢出 | 字库驱动从更小的平台移植而来,中间乘法结果可能超出当时的位宽 | 把地址计算拆成三步做,避免中间结果溢出 |
| 传感器帧偶尔解析出离谱的数值 | 只有单字节校验、帧头又没有校验,干扰下容易混帧或拼帧 | 加“全帧字节累加为 0”的第二条校验;两路都设接收超时(传感器 5 s、屏 1 s),超时清空接收区 |
| 8 KB RAM 下任何阻塞都会让屏和传感器同时停摆 | 没有 RTOS、没有任务栈,所有逻辑共用一个主循环 | 三套非阻塞软延时替代 delay_ms;收发走自研环形队列;业务拆成三段状态机轮转 |
那条“神奇的 1 ms 延时”
第一行值得单独说。当时的现象是:手动输入倒计时之后重新上电,倒计时功能就用不了。
状态机本该在启动倒计时之后跳到“倒计时开启”,但它始终跳到查询页面——
程序没有死机,其他功能也都正常,只有这一条路径永远走不到。
我对着这段代码看了很久,改了下一次要跳转的状态也没用,最后在状态机循环的末尾加了一句
delay1ms(1),一切恢复正常。
/* 状态机循环末尾的"神奇延时":加不加这一行,行为完全不同 */
delay1ms(1); /* 当时只知道加上就好,并不知道它修好了什么 */
合理的推测是:这个循环跑得太快,屏的应答还没回来,下一轮状态判断就已经开始了, 而接收标志里还留着上一轮的残留值,于是状态机被这个残留值带去了另一条分支。 1 ms 让轮询慢下来,应答就有机会先到。
需要坦白的是:这条延时至今没有被我追到根因。当时它“一加就好”, 我就去赶别的功能了。写这篇记录时我又把这段代码看了一遍—— 从源码上我看不出 1 ms 这个数值是怎么来的:它不是屏的应答时间,也不是 9600 或 115200 波特率算出来的任何一个量,更像是试出来的。这正是我对这个工程最不满意的地方: 治好了症状,没有定位病因。
如果现在重来一次,我会把这类问题拆成三步来定位,而不是先加延时:
- 把时序假设写出来。 这个状态每次轮询之间,期望外部发生了什么? 如果期望是“屏已经应答”,那就必须有一个明确的等待条件和超时, 而不是靠“循环慢一点”来假装它已经发生。
- 把残留状态清掉。 每次发起新请求之前,把接收标志、接收长度、缓冲都复位; 应答回来之后,先判断“这条应答属于哪一次请求”(比如回读命令码或页面号做比对), 再决定是否采信。这一条如果能做到,就算循环再快也不会被带偏。
-
最后才考虑节流。 如果确实需要限制轮询速率,也应该写成显式的周期约束
(例如“这个状态最快 1 ms 轮询一次”,并说明依据是屏的最短应答时间),
而不是一句含义不明的
delay1ms(1)。
顺带说第二条(自动睡眠):它表面上是个“设置不生效”的 bug,本质上是 串口屏方案里的职责边界问题。 屏侧固件和主控固件是两份代码、两套状态,凡是屏侧会自己维护的东西(睡眠、亮度、页面), 主控“写一次值”并不足以让它生效。我在主控里补一次模拟触摸,等于是替屏补状态—— 这不算错,但它是一种耦合:以后换屏或者升级屏固件时,这段代码就是隐患。 我现在会在文档里明确写清“哪些状态归屏管、哪些归主控管、交界处谁负责触发”, 而不是把这个知识留在我的记忆里。
八、局限与还没做完的部分
这个工程的功能都实现了,但下面这些地方我很清楚迟早要返工,写出来也算给自己留个清单:
- 参数区没有保护。 所有参数项都以固定步进硬编码在内置 Flash 里, 每项两个字节(低字节是值、高字节像序号),没有 CRC、没有双区备份、没有版本号。 结构一变,老设备里的数据位置就对不上。返工方案我在 参数存储设计那篇笔记里写过: 双区 + CRC + 版本号。
- 错误只能用“跳界面”表达。 密码错误跳 36、海拔错误跳 45、 时段错误跳 46、氧浓度错误跳 48——都指向具体界面,但没有任何数值化的错误码。 现场没法通过串口问出“到底哪一条不通”,只能站在屏前面看。 我想加的是“最近 N 条错误记录(错误码 + 时间戳)”,并且可以从串口读出来。
- VP 地址表没有单一来源。 全部靠宏定义, 还出现了同一个地址值被两个不同含义的宏共用的情况。改界面时很容易看错, 而且不会编译报错。想做的是一份表同时生成宏和文档。
-
命令没有优先级。
send_type靠轮转, 周期性的数据采集和用户按键排在同一个队列里。队列已经有了, 下一步是给它加优先级,让按键这一类交互先走。 - 换算链要重做。 两段线性换成查表 + 单调性检查; 单位换算从表达式里抽出来,集中定义一处并写明单位。
- 那条 1 ms 延时还欠一个根因。 这是我最想补的一件事: 把接收标志的复位与应答匹配补上,然后再把延时删掉,看它是否还正常。 如果不能删,那就说明我对这个状态机的时序理解仍然是错的。
最后说明一下这篇记录的边界:工程里还有若干驱动我只确认了“存在、行数、接口”, 没有确认它在这台设备上是否真的被接入使用,所以这篇里只写我能从工程里读出来的东西, 没有依据的部分就不写了。
参考资料与说明
- DWIN DGUS 串口屏公开开发指南中关于变量地址(VP)、0x82 / 0x83 指令、页面切换与模拟触摸的说明;本文涉及的屏型号与指令集仅用于说明技术方案。
- HC32L136 系列 MCU 公开用户手册中关于 LPUART、串口、定时器输入捕获、内置 Flash 与 ADC 的说明。
- 点阵字库芯片(如 GT20L16S1Y)公开数据手册中关于取模与地址计算的说明。
- Modbus 串行链路标准中关于 CRC16 的定义(本文只用到 CRC16 校验字节的追加方式)。
- 气压高度换算的公开公式(国际气压高度公式,指数约 1/5.255)的公开说明。
- 文中所有帧格式与结构均来自个人学习项目中读到的工程源码与注释, 但具体的变量地址、参数地址与换算系数已在本文中略去; 示例代码为按个人理解重写的最小片段,不代表任何产品或交付代码, 也未包含任何单位、客户或项目信息。
- 出于对个人项目实现细节的保护,本文只公开方法与思路; 涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。
- 文中芯片、屏与工具型号仅用于说明技术方案,与相关厂商无隶属或授权关系。