数据传输改造小记:从等推送,到自己出门打饭

做嵌入式久了,多少都有点"队列情结"。数据嘛,天经地义应该从串口那头流过来,进队列,后面慢慢消费。我一开始也是这么想的,直到这套东西在 MH1000 上开始作妖。

以前的队列是怎么干活的

老路子很标准:MCU 主动推 CMC 帧,串口收进来塞进队列,数据中心的解析任务用 xQueueReceive 一条条掏出来解,解完往 field_store 里一放,业务层再算,最后通知 UI。

纸面上特别顺。可老话讲得好,理论上是解耦,实际上是把两个人拴一块了。

首先,MCU 想推就推。它心情好一口气推一堆,队列满了怎么办?要么丢帧,要么背压,丢在哪个环节、哪一帧没的,这种问题查起来是真的痛苦。其次,不管值变没变它都推,车明明停着没动,车速还是一遍遍报同一串数字,我这边就得一遍遍解同一串数字,CPU 白白烧掉。再加上队列水位又没什么好看的监控,出了事基本靠猜。

最让人头大的是 ROT 那个旋钮和按键。这类事件型字段,设备端是"锁存"的:你读完得写个 0 把它清掉,不然一次操作只报一回。走队列的时候,读和清之间隔着消费延迟,偶尔就出点幺蛾子,按钮时灵时不灵。

轮询这事,说白了就是自己动手

后来我一拍大腿:数据本来就躺在车控那边的 DB 里,我为什么非得等 MCU 施舍我帧?自己读不就完了。

于是有了 dc_niu_poller。逻辑非常朴素:起一个 FreeRTOS 任务(优先级 tskIDLE_PRIORITY + 3,栈 8K),照着一张表,周期性地调 niu_data_read_data / niu_data_read_value 把变量读回来。

读回来之后不无脑上报,先跟本地 cache 来一次 memcmp,也就是"照镜子":跟上次长得一样,闭嘴;不一样,才生成一条样本往下走。这样真正流下去的,只有变化过的字段。

节奏分两档:快组 25ms,伺候旋钮按键这种急脾气;慢组 100ms,伺候车速、电量、胎压这些慢悠悠的。慢组字段扫出来一百多个,我要是手抄进表里,估计得抄到怀疑人生,所以干脆让脚本去扫代码里实际用到的字段,自动生成 dc_niu_poller_table.inc

数据进了数据中心,后面其实一点没变。还是老一套 field_store → business_mapper → state_store,还是那套版本化变更和订阅通知。我只是把变化样本合成一条 RAN_WRITE_MULT 帧,让它走原来解析串口帧的那条路。对上层来说,车还是那个车,数据还是那个数据,SubscribeGetDatasOnDataChange 一个都没动。

打个不太恰当的比方:以前像等外卖,平台派什么吃什么;现在像自己端着盘子去食堂窗口转一圈,只打想吃的。主动权在自己手里的感觉,真好。

几个差点把我坑哭的细节

  1. 事件字段读后清零。 ROT/WINK 那几个键,读到非零值要用写接口马上写回 0,不然后面再按就不灵。第一次发现按钮"只灵一次"的时候,我盯着日志怀疑了半天人生。

  2. 提交失败不能丢值。 轮询任务把样本交给服务,偶尔服务没就绪会返回个忙。这时候不能算了,得打个标记,下一拍全量重采一遍。宁可重发,不能漏。

  3. 开机的面子问题。 刚上电还没采到数据,UI 上是一片 "--",用户会以为车坏了。所以留了启动 reconcile 字段,开机先把该采的采一遍撑脸面。

  4. 串口流不是一刀切。 关掉的只是串口上的车辆字段帧,FOTA、命令应答、变长数据这些还照走老路。手一抖全关了,FOTA 就得来找你聊天。

  5. 没车可玩怎么办。 加了几条 msh 命令:mcu_poll_test / mcu_poll_write / mcu_poll_read,不接真实 MCU 也能往表里灌值,救我于水火。

改完之后,以及一点感想

最直观的变化是 CPU 没那么烫了,更重要的是查问题清爽了:读没读到、差分有没有、提交成没成,每一环都能打日志。

当然轮询也不是银弹。轮太慢,UI 会迟滞;轮太快,总线扛不住。25/100 这个组合是试出来的,不是拍脑袋。

这次改造让我想通一件事:技术选型没有高下,只有合不合适。队列是好东西,可它把"什么时候有数据"的决定权交给了别人;轮询土是土了点,但它把主动权拿回自己手里。做嵌入式久了就懂,比起"设计得多漂亮","半夜不会被叫起来"才是硬道理。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐