Files
vd_960/vd960DBN/docs/devlog.md
T
wangfq 3c9a6f900b docs(MQTT): DLD960_IoT_MQTT协议 V1.12 — 4G 通道适配修订为方案 C(Air780 协议转换)
方案 B → 方案 C(用户 2026-08-31 决策):
- Air780 解析 0x7F 帧转换为标准 JSON 命令(loop_data/event_report/initialize/heartbeat),平台零改动
- V1.11 的 frame_report/frame_cmd 降级为可选兜底
- 4G 事件面 = 仅线圈事件(car_enter/car_leave/loop_cut/loop_restore),不含 DBN 内部网络事件
- event_report 复刻 DBN iot_evt_* 逻辑(Lua: 沿检测+ACK+5s×3+16深队列+跨重连同 msg_id),可行性已评估无硬障碍
- 命令响应: Air780 单命令状态机 + 超时回 code=5(与 g_lup_cmd 同模式)
- 链路层: DBN UART2↔UART1 纯转发(魔数分流),转发不丢帧是沿检测前提

同步: README 索引 V1.12 + devlog 置顶条目
2026-08-31 15:24:31 +08:00

105 KiB
Raw Blame History

vd960DBN 开发日志

MCU: CH32V208 (RISC-V, WCH) | 通信: BLE + ETH (WCHNET) + UART2→Loop MCU | TCP JSON 端口: 5960

项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接

2026-08-31 — MQTT 协议 V1.124G 通道适配修订为方案 C(Air780 协议转换)

协议先行修订(同日 V1.11 方案 B 未落地即修订,固件均未实现)。

修订动因

V1.11 曾定方案 Bvd960DBN 纯转发 + frame_report/frame_cmd hex 透传)。讨论后改为方案 CAir780 解析 0x7F 帧并转换为标准 JSON 命令loop_data / event_report / initialize / heartbeat),平台零改动、与有线通道一致。

四个决策点(用户 2026-08-31

# 决策 结果
1 event_report Air780 以 Lua 复刻 DBN iot_evt_* 逻辑(沿检测 + ACK + 5s×3 + 16 深队列 + 跨重连同 msg_id),可行性已评估:全部逻辑块可映射 Lua,无硬障碍
2 事件面 4G 仅线圈事件car_enter/car_leave/loop_cut/loop_restore),不含 DBN 内部网络事件(协议 §6.4 明确)
3 命令响应 Air780 单命令状态机 + 超时回 code=5(与 DBN g_lup_cmd 同模式),协议 §6.5
4 frame_* 去留 保留为可选兜底(Air780 未实现转换命令/未识别帧透传),协议 §6.8

协议变更(V1.11 → V1.12

  • §6 重写:方案 C 架构(Air780 协议转换)/ 命令面 = 标准 JSON / 转换职责表(0x7F↔JSON)/ 事件上报 / 命令响应链路 / link / 平台要求
  • §3 命令详表:frame_cmd/frame_report 标注改为"可选兜底"
  • 修订记录 V1.12

vd960DBN 侧开发计划(固件未实现,不变)

  • UART1(PB6/PB7) 通道 + UART2↔UART1 双向透传(⚠ 转发不丢帧是 Air780 沿检测的前提)
  • BLE 设置服务器/topic 参数时同步下发 Air780 配置
  • 通道切换策略(待讨论;暂定默认 4G

同步

  • README.md 文档索引 V1.11 → V1.12
  • 协议文档 §6 重写 + 修订记录 V1.12(V1.11 保留作历史)

2026-08-31 — MQTT 协议 V1.11:4G 通道适配(方案 B:原始帧透传 + hex 封装)协议先行

本次为协议文档先行Air8781P 4G 兜底通道设计稿并入主协议;vd960DBN 固件未实现,列入开发计划)。

背景

vd960DBN 有线网络失效时需要 4G 兜底上报。接入方案定为 方案 Bvd960DBN 纯转发——Loop 传感数据(0x7F 帧)经 UART1 原样透传 Air780(Air8781P/Air780EPM, LuatOS vd960Air 工程),由 Air780 走 MQTT;4G 下行地感指令经 Air780 透传回 Loop。

协议变更(DLD960_IoT_MQTT协议 V1.10 → V1.11)

内容
新增命令 frame_report(dev→srv, 上行 0x7F 帧 hex 封装, 每条上行携带 link)+ frame_cmd(srv→dev, 下行 0x7F/0x8F 帧 hex)
4G 通道命令面 不使用有线标准 JSON 业务命令(loop_data/event_report);initialize 由 Air780 发(JSON + link, 用于上线识别 + report_config 时钟校准)
不适用命令 ssc_net_/iot_net_/iot_topic_*(4G 通道下发回 code=4)
link 对象 imei/iccid/imsi/msisdn/csq/net(4G 特有字段, 流量卡管理/信号监控)
链路层 数据面 0x7F 帧字节流透传(魔数分流在 DBN 侧: 0x7F→UART2 / 0x8F→本地);0x7D 帧仅 DBN↔Air780 配置同步/握手
平台侧 双通道区分解析 + 新增《DLD960Loop_串口通信协议》解析依赖

vd960DBN 侧开发计划(固件未实现)

  • UART1(PB6/PB7)通道: 0x7F 帧魔数分流
  • UART2↔UART1 双向透传(复用 manage_dbn_ble_transparent 模式)
  • BLE 设置服务器/topic 参数时同步下发 Air780 配置
  • 通道切换策略(待讨论: 动态自动 vs 人工;暂定默认 4G)

同步

  • README.md 文档索引 MQTT V1.10 → V1.11
  • 协议文档 §3 命令详表 + §6 4G 通道适配章节 + 修订记录 V1.11
  • CHANGELOG 历史条目保留不动

2026-08-31 — BLE 协议文档补全 V1.03:配置命令全表 + 未实现如实标注

本次为文档补录(固件代码未变,不升固件版本):对照 dbn_ble_srv.cGBK+CRLF)逐命令梳理,DLD960_BLE协议.md 由 V1.02(仅脱机日志/快照 6 命令)补齐为完整 BLE 协议。

背景

DLD960_BLE协议.md 此前只覆盖 0x250x2AOFFLOG/SNAP 6 命令),既有的 BLE 配置命令(0x090x24 等)没有文档——小程序/APP 对接无从下手。本次从代码反向梳理全部命令。

文档补全内容

命令码 名称 实现状态(代码核实)
0x09/0x10/0x11/0x12 序列码/设备信息/SSC 网络读写 manage_dbn_ble_default
0x13/0x14/0x15/0x16 IoT 网络/Topic 读写 unpack_packs(字符串数组 0x00 分隔)
0x1C/0x1D 验证/修改密码 unpack_packs0x1D 不在 switch,分包收尾处理)
0x1E/0x92 出厂初始化 回 [0] + factory_dev_info()
0x1F 设备复位 无响应直接 NVIC_SystemReset
0x22 子功能码 2B LE,无响应
0x23/0x24 车检器参数读写 ⚠ 空壳(0x23 仅回 [0] 不处理参数;0x24 空响应)
0x31 UART 波特率读写 (⚠ 写响应多 1B 重复低字节,文档已标注)
0x8A Loop 灵敏度列表 ⚠ 空响应
0xC5 传感上报使能 设置生效无响应;⚠ report_sens_acs 无定义 → 0xC0 上报未实现
0x17/0x18/0x20/0x21/0x87/0x88/0x89 交通参数/通知/Loop 采样等 ⚠ 头文件有定义,switch 无 case(未实现)
0x7F/0x9F 透传通道 UART2 原样转发 / OTA 透传

关键发现

  • 0x1D MODIFY_PASS 实际已实现:不在 manage_dbn_ble_default 的 switch,而在 unpack_packs 分包收尾分支(头文件有定义、switch 无 case ≠ 未实现,须看 unpack_packs
  • 0xC0 BLE 主动上报已废弃report_sens_acs() 仅 dbn_ble_srv.h 声明,全工程无定义无调用;0xC5 使能后无上报动作(上报走 MQTT/TCP)
  • 0x31 写响应固件 bugtmp_ble_buf[i++] = g_storage_uart_baud; 后紧跟 tmp_ble_buf[i++] = (uint8_t)g_storage_uart_baud; 重复写低字节,响应 10B 而非 9B——文档按语义描述 + 标注实际多 1B,待板级联调时确认是否修固件
  • 请求分包(unpack_packs)与响应分包(set_response_to_notify)格式一致:header 高 4 位=包数、低 4 位=序号(从 1 起)

同步

  • README.md 文档索引 BLE 协议 V1.02 → V1.03(描述更新为完整命令表)
  • CHANGELOG 历史发布条目(V1.02 不变)保留不动
  • 待板级:0x31 写响应长度、0xC5 使能后无上报是否符合预期

2026-08-21 — MQTT 协议 V1.10 代码落地:网络上报携带地感版本(loop_ver/loop_hw_ver

固件版本:归入 vd960DBN V1.02.05 发布2026-08-21 升版,cmcng.h 三段式一致:FIRMWARE_VER="1.02.05" + MAIN=1/SUB=2/SUBSUB=5)。V1.02.05 涵盖 8-20 联网稳定性修复系列(SocketSend 0x11 / OTA hex 空格 / union 合并 / 刷写状态语义)+ 本次 V1.10 协议实现。

背景

协议 V1.10DLD960_IoT_MQTT协议.mdcommit 3be9e50)三处新增,本次为固件实现:

  1. dev_info_query 响应 data 补 loop_ver / loop_hw_ver(地感 Loop 版本,0x4A 缓存值)
  2. initialize 上报同样携带(尽力,可为空)
  3. 新增 §4.25 loop_version_query 命令:实时查询 Loop 版本,配合远程 OTA 升级前后核对

改动

位置 内容
loop_uart_proto.h/c 新增 LoopVerCache 结构 + 全局 g_lup_ver_cache0x4A 查询结果缓存);lup_cache_from_info() 解析结果写缓存、lup_refresh_version_cache() 后台刷新(仅命令通道空闲时发 0x4A)
iot_mqtt_srv.c dev_info_query data 补 loop_ver / loop_hw_ver(缓存格式化字符串,查询未完成/失败为空串)
iot_mqtt_srv.c initialize data 补 loop_ver / loop_hw_ver(尽力携带,可为空)
iot_mqtt_srv.c 新命令 loop_version_query 0x4A 异步发起 + 暂存 msg_id / deadline(300ms)iot_verq_poll()g_lup_cmd.state == RESPONSE_READY 时解析回包(code=0 + loop_ver/loop_hw_ver/version_str)并同步刷新缓存;TIMEOUT/超时回 code=5loop_ver 保持缓存值)
iot_mqtt_srv.c 后台刷新 上电 iot_mqtt_init() 置 dirty + OTA doneiot_ota_report_result ok=1)置 dirty;poll 在命令通道空闲时自动发 0x4A 刷新缓存

关键设计

  • 版本来源 = 0x4A 实时查询lup_parse_version),不是 OTA 元数据 version——那是"目标版本"(审计用),0x4A 才是 Loop 当前真实固件版本
  • 缓存值语义dev_info_query/initialize 带缓存值(上电自动查一次 + OTA done 后刷新);MQTT 命令处理是同步回包,不能干等 0x4A 异步响应,故查询未完成/失败回空串;平台要最新版本走 loop_version_query 实时查询
  • 异步回包:与 TCP json_check_pending 同模式——暂存 msg_id(回包 topic 固定 dld960/{sn}/dev,无需存 topic),等 g_lup_cmd.state 状态机推进
  • 单命令通道互斥g_lup_cmd 全局唯一,TCP/MQTT/后台刷新共用;后发覆盖、先发者超时(低频命令,理论无冲突)

验证

  • gcc 隔离单测 tests/test_iot_loop_ver.c 44 断言全过
    • 版本帧解析:正常帧(Status/Hard=1.0.0/Soft=1.2.3+ 错误路径(len<10 / bad magic / bad cmd / LEN<7
    • 缓存:lup_cache_from_info 写入、iot_loop_ver_str/iot_loop_hw_ver_str 格式化(含无效缓存空串、NULL 防护)
    • verq_poll 命令成功回包:code=0 + 三字段 + msg_id 回显 + topic 正确 + 缓存同步刷新 + 通道释放
    • 超时回包:code=5 + no response + 缓存值保留
    • 后台刷新:dirty=1+IDLE 发起查询(不设 pending)、通道忙不消费、响应消费更新缓存、残留超时清理
  • check_c_balance.py 三个改动文件语法平衡 OK
  • 待板上验证0x4A 在 MQTT 工作期间与 loop_data/事件上报共用 UART2 总线的时序(查询是低频命令,理论无冲突);OTA done 后缓存刷新(Loop 复位时间窗内查询可能超时,可接受——平台可用 loop_version_query 主动核对)

2026-08-20 — 联网 SocketSend 0x11 死循环修复:发送退避 + SocketCreat 失败中止

现象(板级)

收 report_config 后 loop_data606B)发送失败:SocketSend FAIL ret=0x11 sent=0/606 → 立即重试 10 次无效 → 3 次强制断连 → 重连 SocketCreat 返回 0x1D(ISCONN) + Connect 0x17(CLSD) → TCP connect timeout 死循环,永久失联。

根因

  1. WCHNET_NUM_TCP_SEG = WCHNET_NUM_TCP×2 = 2(RAM 瘦身改小):TCP 发送段缓冲仅 2 个,重传队列占用即无缓冲 → 606B 发送 WCHNET_ERR_MEM(0x11)
  2. iot_mqtt_send 失败处理过激:0x11 是瞬时(SEG 被占),却立即重试 10 次(无退避,WCHNET 没时间释放)+ 3 次就强制断连
  3. 重连缺陷WCHNET_CreateTcpMqttSocket 里 SocketCreat 失败(0x1D ISCONN,旧 socket 未清干净)后仍继续 ConnectmStopIfError 只打印不中止)→ 无效 socket 等超时 → 死循环;且强制重连时 SocketId_TCP 未置 0xFFSocketCreat 沿用旧 id

修复

位置 改动
iot_mqtt_srv.c iot_mqtt_send 0x11 时 200ms 退避重试(等 WCHNET 释放 SEG,最多 10 次×200ms≈2s),不立即断连;非 MEM 错误立即重试
iot_mqtt_srv.c 强制重连 SocketId_TCP = 0xFF(防 SocketCreat 沿用旧 id
net_srv.c WCHNET_CreateTcpMqttSocket SocketCreat 失败 → 打印 + SocketId_TCP=0xFF + return 不 Connect,由上层指数退避重试

验证

  • 语法 0 错误
  • 待板级确认:重发场景(瞬时 0x11)应自动恢复不断连;真断连重连失败应退避而非死循环

2026-08-20 — OTA 下载失败修复:hex 提取不兼容 json.dumps 空格(单片 CRC 全错)

现象(板级)

DBNMQTTool 下载第一片即失败:ota_data code=1 crc/gap errorreceived=0,重试 4 次全败。

根因

工具 json.dumps 默认带空格"data": "hex..."),设备端手工 strstr(json, "\"data\":\"")无空格格式 → 匹配不到 → hexbuf 空 → ota_hex_decode 失败 → code=1。其他命令走 simple_parse_json(容错空格)不受影响,只有 OTA 的 hex 提取踩坑。

修复

  • ota_srv.c/h 新增 ota_json_extract_hex():扫描 "data" 键后跳过任意空格/冒号/引号,兼容 "data":"...""data": "..." 两种格式
  • iot_mqtt_srv.c ota_data 分支改用该函数(删除 strstr 手工提取 + 残留 hp 变量)
  • 单测新增 test_json_extract_hex:无空格/带空格(事故场景)/512 hex 满片/找不到,4 场景全过

教训

嵌入式 JSON 协议字段提取统一用 simple_parse_json 或兼容空格的扫描,不要 strstr 精确匹配——上位机 JSON 序列化器(json.dumps/不同语言库)的空格行为不可控。


2026-08-20 — 联网复位事故修复:OTA 命令缓冲合并 union(RAM 90% 栈余量不足)

现象(板级)

联网后 SUBACK → 平台下发 MQTT PUBLISHlen=197,即 report_config 时钟同步)→ HardFault → NVIC_SystemResetRST_REASON: 0x10000000 = SFT 软件复位位,非 IWDG 0x20000000CH32V20x 默认 HardFault_Handler 即软件复位)→ 死循环复位。

根因

MRS 编译 RAM 90.02%44248/48KB)逼近历史 .bss 挤栈红线。本次 OTA 实现 6 个命令分支各声明独立 static char resp[256~400] + static char hexbuf[513](合计 ~2.5KB BSS)→ 栈余量被挤 → iot_handle_publish 深调用链(MQTT 解析 → simple_json → snprintfreport_config 分支本身还有局部 data[512]栈溢出 → HardFault。复现稳定(每次收 report_config 必崩)。

修复

改动
iot_mqtt_srv.c 6 个 resp + hexbuf 合并为函数级 static union _ota_io { resp[512]; hexbuf[513]; }(互斥复用) ~2KB BSS
ota_srv.c _chunk_buf[256] + _fs_frame[254] 合并 union(下载/校验与刷写帧互斥) ~260B
ota_srv.c 删无用 _fs_block_crc 4B

教训RAM 90% 环境下"局部栈改 static"是伪优化——BSS 增加 = 栈余量减少,深调用链照样溢出。正确方向是合并复用缓冲减少总占用union),不是搬家。

验证

  • 语法 0 错误(基线 interrupt 假阳性除外);gcc 隔离单测 10/10 PASS
  • 待板级确认:拉最新代码重编,RAM 应回 ~88%,联网收 report_config 不再复位

2026-08-20 — Loop MCU 远程 OTA 固件实现(MQTT V1.08ROADMAP P1.4 ①)

背景

协议设计稿 V1.01 拍板 + 并入 MQTT 协议 V1.08 + DBNMQTTool OTA 页签/协议单测先行。本次固件实现先存后刷MQTT 分片下载 → W25Qxx OTA 暂存区(0x010000 起 512KB)→ 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCUAT32F421)。

变更

文件 变更
ota_srv.c/h(新增) CRC32ISO-HDLC 逐位,零查表 RAM)、OtaMeta 元数据(双备份 0x010000/0x010200,节流 4KB flush)、会话状态机(begin/data/end/abort/flash)、本地刷写状态机(非阻塞 tick 驱动:A5→pre_ok→A6→addr_ok→A7×N 停等 ACK,1s 超时 ×3)、安全窗口(有车拒绝,force 跳过)、失败 event_report ota_error
iot_mqtt_srv.c 命令分发加 6 个 ota_* 分支;会话静默(downloading/flashing 暂停 event_report 发送 + offlog/快照落盘,队列积压结束后补发);IOT_EVT_OTA_ERROR 事件类型 + iot_evt_report_ota_error + iot_any_car
usart_biz.c uart_srv OTA 分支 ACK 分发:g_ota_flash_active 时交给 ota_flash_feed_ack(本地刷写),否则透传 BLE(现状)
peripheral_main.c ota_init() 初始化 + ota_poll() 挂主循环(uart_srv 后,先消费 ACK 再推进)
offlog.c/h 事件类型 OTA_START 0x60 / OTA_RESULT 0x61 + 包装函数(升级开始/结果审计留痕)

关键参数

下载单片 256B + 单片 CRC32hex 512 字符 < RECV_BUF 1024
镜像上限 96KBSlot 100KB - 4KB 边界余量)
刷写块 248B0x9F LEN 为 uint8 → DATA ≤254Bbootloader 2KB 扇区累积写)
分区 元数据 4KB + Slot A/B 各 100KB0x011000/0x02A000+ 预留 308KB
断点续传 元数据 received 节流 4KB flush,掉电丢 ≤4KB(平台重发幂等,协议已定义)
刷写窗口 64KB ≈ 265 块 × 20ms ≈ 68s;非阻塞调度保 MQTT 保活/IWDG

单测(gcc 隔离,tests/test_ota_srv.cmock NOR/UART2/时钟)

9 断言组全过:CRC32 标准向量 0xCBF43926 / hex 解码 / 元数据双备份恢复 / 会话流 20 片→ready / 断点续传(16 片 flush 后精确续传)/ 幂等·乱序·单片 CRC / 刷写全流程(21 块 A7 逐块 ACK)/ 有车拒绝 + force 跳过 / pre_ok 超时重试失败告警。

抓到 2 个真 bug(单测价值实证)

  1. A7 块序号未递减_fs_sub 每块 ACK 后不减 → bootloader 永远等不到末包(sub==1)→ 刷写卡死。修复:ACK 成功路径 _fs_sub--
  2. 重发时 retry 清零START/ADDR 分支 _fs_retry=0 每次重发清零 → 超时重试永不失败。修复:重发保持计数(首发由 ota_cmd_flash 置 0ACK 成功由 feed_ack 清 0)。

待板上验证

  • UART2 TX 阻塞轮询(248B ≈ 13ms)与主循环时序、擦除 45ms×N 分散阻塞对 MQTT 保活影响
  • Loop 实际 bin 大小(验证 96KB 余量)、bootloader 真实 ACK 时序
  • 会话静默后 event_report 补发行为(队列积压恢复)

2026-08-19 — 固件版本 V1.02.04(SPI 存储适配两项修复)

内容

说明
SPI Flash 识别去厂商代码 W25Qxx 宏去 0XEF 前缀、SPI_Flash_ReadJEDEC_ID() 删厂商校验、storage_init 低字节匹配(f1d9ac4
恢复 factory 配置写入 解除 8-13 止血,空片首启写 magic+默认参数,配置读写恢复正常(8e58d5f + 881a774
版本号 cmcng.h 三段式一致:FIRMWARE_VER="1.02.04" + MAIN=1/SUB=2/SUBSUB=4

板级验证:W25Q128 配置读写正常(用户 2026-08-19 确认)。

文档同步:README 当前发布/版本表、CHANGELOG V1.02.04 条目、产品手册/技术规格书配套版本(文档保持 V1.02,同日内补充不升版)。


2026-08-19 — 恢复 factory 配置写入(换新空片后"老是进入出厂初始化配置")

背景

换 W25Q128 新片后每次上电日志 CFG: magic mismatch - MEMORY DEFAULTS (factory SPI write disabled),用户保存的配置不生效。

根因

  1. 8-13 止血禁用了 factory 写入(当时 SPI 写触发硬件复位死循环,临时方案:不写 Flash 用内存默认)——但该根因已在 8-17 闭环(.bss 挤占 RAM → 栈溢出 + printf 重入,非 SPI 硬件问题),offlog/snapshot 自 8-18 起持续 SPI 写入稳定,止血早该解除
  2. 新空片(W25Q128)参数区全 0xFFmagicPRODUCT_MODEL 字符串)不匹配 → 每次上电走内存默认
  3. write_net_config/set_ble_safe_pass 保存配置只改字段不写 magic → 即使保存过,下次上电 magic 仍 0xFF → 配置"永久丢失"

变更(cfig_flash.cUTF-8+CRLF

load_cfg_from_flash() mismatch 分支:删除"不写 Flash"止血,恢复调用 factory_dev_info()(写 magic + 默认参数 + 网络配置到参数区 0x00),打印改 factory defaults written。首次上电自动初始化参数区,后续上电正常加载 + 保存配置生效。

验证

  • check_c_balance.py OKgcc -fsyntax-only 无新错误(仅原有 unused warning
  • SPI 读写实现对 W25Q128 兼容确认:4KB 扇区擦除 + 256B 页编程分页(storage.c 标准实现),指令集与 W25Q32 相同

板级验证(用户 2026-08-19 确认)

  • 读写正常:新片(W25Q128)配置读写恢复正常,不再每次上电进出厂初始化
  • 新芯片规格书无需再核对(华邦 W25Q128,与 W25Q32 指令/结构兼容)

2026-08-19 — SPI Flash 识别去掉厂商代码判断(兼容多厂家同容量型号)

背景

SPI_Flash_ReadID()0x90 指令)返回 0xEF15(华邦:厂商 0xEF + 设备 ID 0x15),SPI_Flash_ReadJEDEC_ID()(0x9F 指令)返回 3 字节(0xEF 0x40 0x16)。原代码在 JEDEC 读取处硬编码校验 id[0]==0xEF && id[1]==0x40,且 W25Qxx 宏含 0XEF 厂商前缀——换其他厂家同容量型号(如 0x1A 开头)时识别不一致。

变更(storage.cGBK+CRLF 保持原编码)

位置 改动
W25Q80~W25Q128 0XEF13~0XEF170X13~0X17(去掉厂商前缀,只留设备 ID
SPI_Flash_ReadJEDEC_ID() 删除 if (id[0] != 0xEF || id[1] != 0x40) return 0;,直接 return id[2];(JEDEC 容量码跨厂商标准化:0x16=32Mbit 等)
storage_init() switch(Flash_Model)switch(Flash_Model & 0xFF)(只匹配设备 ID 低字节)

验证

  • check_c_balance.py OKgcc -fsyntax-only 无新错误(仅原有 unused warning
  • 逻辑说明:真正决定分区容量的是 offlog/snapshot 用 SPI_Flash_ReadJEDEC_ID() 容量码(0x16/0x17/0x18/0x19)匹配 OFFLOG_CHIP_*——这部分本来就不含厂商判断,随本次一并生效

遗留

  • /* Winbond SPIFalsh ID */ 历史注释未改(宏已不限于 Winbond,后续可顺手更新)
  • 待板级:换非华邦同容量芯片实测识别

2026-08-19 — UART2 RX DMA 下 BLE→Loop OTA 失效修复:0x9F 帧透传回 BLE

背景

DMA 改造(fed4335)后通过 BLE 给地感(Loop MCU)OTA 升级失效。代码排查 + DLD960LoopBootloader 源码确认根因。

根因

  1. lup_feed_byte() IDLE 态只认 0x7FLoop bootloader 回的 0x9F 响应帧(pre_ok/addr_ok/data ACK)第一个字节就被丢弃
  2. OTA 是停等协议0xA7 (TRAN_PKG_WITH_BACK) 每收一块必须回 back_9F_data() ACK,工具等不到 ACK 不发下一块 → DBN 吞掉 ACK = 升级必然卡死
  3. uart_srv OTA 分支"忽略 Loop MCU 数据"——即使解析成功也不透传回 BLE
  4. DMA 版相对中断版丢失"收帧中 tick 归零";溢出保护 256B 阈值在 TX 阻塞窗口可能误杀 ACK

0x9F 帧格式(DLD960LoopBootloader 源码确认)

方向 帧格式 说明
下行 9F SubL SubH LEN CMD DATA(LEN-1) CHECK CHECK = SUM(SubL..DATA)无 XORLEN 含 CMD
上行 9F SubL SubH 02 CMD Status CHECK 例:pre_ok = 9F 01 00 02 A5 00 A8
CMD A5 READY / A6 ADDR_START / A7 TRAN_WITH_BACK(回ACK) / A8 TRAN_WITHOUT_BACK(不回) 总帧长 = LEN+5 与 0x7F 相同

变更

文件 变更
loop_uart_proto.h LUP_FrameState 追加 OTA 专用状态(OTA_LEN/OTA_CMD/OTA_CHECK);声明 lup_feed_byte_ota()
loop_uart_proto.c 实现 lup_feed_byte_ota():0x9F 帧状态机(SUM 校验,复用 g_lup_parser 缓冲);lup_feed_byte 加 default 防御性 reset
usart_biz.c uart2_dma_poll:OTA 模式切换时 reset 解析器 + 按 g_flag_counter_ota.flag 选 0x9F/0x7F 解析器 + OTA 模式溢出阈值放宽到整缓冲(0x9F ACK 仅 7B,停等协议同刻至多 1 帧)+ 收帧中 tick 归零(对齐中断版);uart_srv OTA 分支:0x9F 帧 set_response_tran_to_notify 透传回 BLE + InitPkgUart 清 flag

验证

  • gcc 隔离单测 tests/test_lup_ota_parser.c8 断言全过bootloader 真实帧向量 pre_ok/addr_ok/addr_err/data ACK、错误校验拒绝并复位、0x7F 帧隔离、两帧粘包独立解析)
  • gcc -fsyntax-onlyloop_uart_proto.c/usart_biz.c 无新错误(仅 RISC-V interrupt attribute 假阳性 + 原有 unused warning

版本与板级验证

  • 固件版本 V1.02.032026-08-19 用户更新 cmcng.h 三段式:FIRMWARE_VER="1.02.03" + MAIN=1/SUB=2/SUBSUB=3,字符串与数字宏一致)
  • 板级 BLE OTA 全流程实测通过(用户 2026-08-19):OTA 修复生效
  • 本修复 + 8-18 快照流/hex 上报/MQTT 命令补齐 一并归入 V1.02.03 发布(README/CHANGELOG 已同步)

遗留

  • g_flag_counter_ota.flag 无退出机制:升级完成后 DBN 仍卡 OTA 透传态,只能断电重启恢复 0x7F 通信——用户 2026-08-19 拍板维持现状,作为已知约束记录在 CHANGELOG

2026-08-18 — log_query 改为 hex 原始字节上报(协议 V1.03/V1.07 修订)

背景

板级实测:MQTT 快照流 log_query(stream=snapshot) fetched=2MQTTSerialize_publish failed——JSON 化快照记录(4ch×12 字段 ≈ 810B/条)超 IOT_MQTT_SEND_BUF_LEN=800B,事件流 4 条 ~600B 可发,快照 1 条都发不出。

方案(用户拍板):对齐 BLE 通道——原始字节 hex 上报

  • 事件记录 OfflogEvt 32B → 64 hex 字符;快照记录 SnapRec 64B → 128 hex 字符flash 存储字节原样小端)
  • 上位机按《DLD960 BLE 协议》§6.4/§7 字段表解析,与 BLE 通道同语义、解析逻辑复用
  • 体积实测:2 条快照 hex 响应 406B < 800B——无需扩容缓冲、无需砍 count

变更

文件 变更
offlog.c/h 新增 offlog_evt_to_hex()32B→64 hex
snapshot.c/h 新增 snap_rec_to_hex()64B→128 hex);删 SNAP_MAX_QUERY_JSON 宏,恢复 SNAP_MAX_QUERY_RECORDS=2 全通道统一
tcp_json_srv.c log_query event/snapshot 分支改 {"seq":N,"hex":"..."} 上报
iot_mqtt_srv.c 同上;IOT_MQTT_SEND_BUF_LEN 保持 800(hex 方案体积可控,此前 1024 变更撤销)
协议文档 TCP JSON V1.03 / MQTT V1.07 §4.17records 改 hex + 解析表引用 BLE 字段表;快照 count 恢复 2

验证

  • gcc 单测 tests/test_snap_to_hex.c9 断言全过(128 hex、全小写、字段字节序、缓冲截断安全)
  • 工具 hex 解析器:boot(POR/SFT)/coil/evt_retry payload + 快照 4ch 解析全过;2 条快照 hex 响应 406B
  • offscreen UI:事件流/快照流 hex 展示 + 翻页正常
  • 保留 offlog_evt_to_json/snap_rec_to_json(旧方案,未来可能用于调试导出)

遗留

  • 待板级:MRS 编译 + 真机 MQTT/TCP log_query(stream=snapshot) 2 条拉取

2026-08-18 — MQTT 命令集补齐:ssc_net_query / iot_net_query / iot_topic_query

背景

DBNMQTTool 实测发现 MQTT 通道 ssc_net_query / iot_net_query 返回 code=4 unsupported command。根因:MQTT 协议文档命令表声明 14 条 srv→dev 命令,固件 manage_mqtt_recv_message 只实现 6 条(dev_info_query / report_config / pwd_verify / event_report ACK / log_*),注释即"目前仅实现基本响应框架"。查询类命令缺口本次补齐。

变更

命令 数据源 说明
ssc_net_query local_net_cfg + net_center_info dev_ip/subnet_mask/route_ip/lssc_ip/dns/port 组包,与 TCP JSON §4.5 一致
iot_net_query iot_net_info host/port/client_id/username/password,与 TCP JSON §4.7 一致
iot_topic_query g_iot_topic client_id_enable/topic_pub/topic_sub,与 TCP JSON §4.9 一致

三条均为只读全局变量组包,低风险;插入点:log_clear 分支后、unsupported else 前。

仍未实现(协议声明但固件缺,工具已禁用对应按钮)

ssc_net_set / iot_net_set / iot_topic_set / pwd_set / factory_reset / device_reset / dev_serial_set / loop_param_query / loop_param_set——设置类涉及写 Flash/设备重启,loop_param 需 UART 透传异步机制,留待后续(工具侧已禁用按钮 + tooltip 引导走 TCP/BLE)。

验证

  • check_c_balance OK;全局变量可见性确认(net_srv.h extern
  • DBNMQTTool offscreen6 个未实现按钮禁用 + tooltip,查询类可用
  • 待板级:MRS 编译 + 真机 MQTT ssc_net_query / iot_net_query / iot_topic_query

2026-08-18 — TCP/MQTT 脱机日志快照流支持(协议 V1.03/V1.07 落地)

背景

协议文档先行(TCP JSON V1.03 + IoT MQTT V1.072026-08-18):log_stat / log_query / log_clear 通过 data.stream 区分 event / snapshot 流(BLE 0x28/0x29/0x2A 的网络通道对称语义)。固件侧此前只实现事件流(offlog_*),快照流缺失——本次补齐。

变更

文件 变更
snapshot.c/h 新增 snap_rec_to_json()SnapRec 64B 原始结构 → JSONchannels 对齐 0xC0 传感单元,variation 3B 符号扩展,misc_type 全枚举 time/cut_count/flow_count/relay_count
tcp_json_srv.c handle_log_stat/log_query/log_clearstream 解析 + snapshot 分支(snap_* API);事件流 count=0 按上限处理(与 BLE 对齐)
iot_mqtt_srv.c log_stat/log_query/log_clearstream 解析 + snapshot 分支;同上

关键决策

  • 复用 log_ 命令 + stream 字段*(协议 V1.00 即预留"快照流预留"),不新增命令码;与 BLE 侧独立命令(SNAP_*)不同,JSON 协议命令面保持 18 条不变
  • 快照 QUERY count≤2SNAP_MAX_QUERY_RECORDS=264B×2 记录),事件流仍 ≤4
  • 快照 CLEAR 阻塞 ~45ms(逻辑清除+当前写扇区),事件流 ~2.8s 不变
  • capacity/count JSON 类型 uint32W25Q256 事件流 130944 超 16bit,原文档 uint16 标注修正)
  • snap_rec_to_json 放 snapshot.c 公共模块,TCP/MQTT 两路共用(避免重复序列化代码)

验证

  • gcc 隔离单测 tests/test_snap_to_json.c34 断言全过(高频/低频/方向判别/初始频率、负 variation -10 符号扩展、misc_type 全枚举、loop_ok/has_car 位解析、空记录 coil_count=0 边界)
  • check_c_balance.py 5 文件 OKtcp_json_srv.c / iot_mqtt_srv.c / snapshot.c / snapshot.h ×2
  • simple_parse_json 字符串提取语义确认:去引号取内容,strcmp(stream,"snapshot") 成立
  • 待板级验证:MRS 真编译 + 真机 TCP/MQTT log_query stream=snapshot

注意

  • 固件版本未升(仍 1.02.01),随下次发版统一
  • 协议文档已先行(V1.03/V1.07),固件本次同步跟上(README/规格书/产品手册索引已同步)

2026-08-17 — 频繁"复位"根因闭环:.bss 挤占 RAM → 栈溢出 → PC 跑飞

背景

08-14 恢复快照区功能(8873b33)后,现场频繁"复位"100~180ms 一轮完整启动日志)。

诊断过程(Hex 日志 + 二分实验)

观察 推断
RST_REASON 首次 0x08000000(POR),之后恒 0x00000002(无复位标志位) 非硬件复位(POR/IWDG/SFT/PIN 都会置位),是 PC 跑飞跳回 0
无 FAULT_DIAG 输出 非 HardFault
snap ok 后 71B 乱码 + 无 ASCII 打印 跑飞点在 offlog_boot/load_cfg/output_cfg 区间
实验A(跳过 offlog_boot, 0c717e4):仍崩 offlog_boot 非触发点
实验D(跳过 load_cfg/output_cfg, 452ff68):完全正常启动 触发点 = load_cfg/output_cfg
BOOT_CNT@0x2000f84c END:0x2000f834 .bss≈46KB → 栈仅 ~1.9KB(危险)

根因

.bss 挤占 RAM → 栈仅 1.9KB → load_cfg/output_cfg 的 printf 栈峰值触顶 → 溢出覆盖 .noinitg_boot_count 恒 1、g_fault_diag 消失)+ 返回地址 → PC 跑飞 → 循环。 08-13"稳定 6 分钟"实为实验状态(snap_init/offlog_boot 均 #if 0),非完整固件; 快照区延后无效,因为问题不是快照 SPI,是栈空间被 .bss 挤占。

修复链(4 个 commit

commit 内容
452ff68 实验DBOOT_CNT 加 END/SP 打印 + 跳过 load_cfg/output_cfg(定位触发点)
26f2326 止血:load_cfg 去调试打印、output_cfg 暂关(设备稳定启动)
421334e RAM 瘦身一轮:ETH 1520→768、payload/MQTT buf 1400/1024→800(栈 1.9→4.7KB
89ba602 闭环:恢复全部调试打印 + load_cfg 字符串 0 终止保险

.map 分析(用户提供编译产物)

  • BLE 栈(libwchble.a)固定占用 RAM 低 16KB0x20000000~0x20004000,链接脚本外)——WCH CH32V20x 工程约定,不可裁剪
  • 用户 48KB.data 1.6K + .bss 41.7K + .noinit 28B → 栈 4.7KB
  • .bss 大块:RECE_BUF_LEN 系列 ~10KB、Mem_Heap 7.2KB、MACRx/TxBuf 3.8KB、eth DMA 5.9KB、SPI_FLASH_BUF 4KB、ARP 1.2KB、iot buf 2KB

二轮瘦身(cb63c58)— 栈 4.7KB → ~6.3KB + 中断栈峰值 -1.8KB

文件 项目 改动 收益
net_config.h RECE_BUF_LEN MSS×2(1400)→1024MSS=700 帧+头≈740B 够) 5 数组省 ~2.6KB
net_config.h WCHNET_NUM_ARP_TABLE 50→16 ~0.8KB
tcp_json_srv.c tmp_buf[RECE_BUF_LEN]/frame[800] 局部→static(中断上下文栈数组) 栈峰值 -1.8KB

板级验证(用户实测)

  • 一轮瘦身+打印恢复版:无复位MQTT 连 159.75.137.141:1883、BLE 广播正常、dev_serial=DC045A49718F 配置加载生效
  • 二轮瘦身版(RECE_BUF_LEN 1024 + ARP 16 + static):测试无异常重启
  • 待回归:TCP JSON 命令交互(RECE_BUF_LEN 减小后接收缓冲 1024 ≥ MSS 帧 740B,理论无影响)
  • 待回归:快照区 3s 后初始化(INIT: snap ok)、断网快照落盘、BLE 0x28/0x29/0x2A

UART2 RX DMA 改造(2026-08-17 晚,方案A 实施)

遗留项"UART2 偶发丢帧"1-3 分钟一次 checksum fail)落地修复:

  • 根因:PRINT 临界区(关中断 ~7.4ms)期间 USART2 RXNE 中断被屏蔽 → 丢字节 → 粘帧
  • 方案:DMA1_Ch6 循环模式硬件收字节(不依赖 CPU 中断),主循环轮询消费
  • usart_biz.c 改动:
    • 新增 uart2_dma_init():Ch6 循环模式 + 512B 环形缓冲(aligned(4)+ 关 RXNE 中断
    • 新增 uart2_dma_poll():主循环每轮读 DMA_GetCurrDataCounter 算新字节, 批量喂 lup_feed_byte(状态机移入主循环,无中断竞争);溢出保护(未消费>256B 重置+丢帧计数)
    • uart_init() 末尾调 uart2_dma_init()USART2_IRQHandler 清空(RXNE 已关, 保留 IDLE 注释备用)
  • 资源:DMA1 全空闲(WCHNET 用独立 ETH DMA、BLE 栈不用 DMA1)→ Ch6 独占无冲突
  • 注意:.bss +512B(缓冲),栈 6.3KB → 5.8KB 仍充裕

调试代码结案清理(2026-08-17 晚)

频繁"复位"问题闭环后,移除临时诊断代码:

  • peripheral.c performPeriodicTask():删除 _dbg_cnt 500ms 调试心跳打印(BLE 状态已由上层链路验证)
  • peripheral_main.c:删除 g_boot_count.noinit 计数器)+ BOOT_CNT/END/SP 诊断打印块
  • 保留fault_diagFAULT_MARKER + .noinit 现场)——诊断基础设施,成本低,未来疑难杂症复用; RST_REASON 一行打印(offlog_boot 同源,区分复位源)
  • 注意:peripheral.c 为 GBK+CRLF,新增注释必须 ASCII(中文会被 GBK 编码搞乱)

固件版本 1.0 → 1.12026-08-17

cmcng.hFIRMWARE_VER "1.0"→"1.1"MAIN=1 SUB=1,顺带修正字符串与数字不一致——旧 "1.0" vs MAIN=1 SUB=1)。 本次版本内容:栈溢出修复(频繁复位闭环)+ RAM 瘦身两轮 + UART2 RX DMA + 调试代码结案清理。 ⚠ 发布时需同步:固件 tag v1.1.0 + README 版本表 + 技术规格书(若含版本号)。

遗留

  • 快照区延后 3s 保留(非根因,但作为启动早期 SPI 减压无害)
  • (可选)g_report_cfg 1400B 结构体审查、WCHNET Mem_Heap 7.2KB 库配置——收益有限暂不动
  • UART2 DMA 板级验证:Loop MCU 数据正常解析(无 checksum fail)、 粘帧/半帧/长阻塞(SPI 擦除时)不丢帧

2026-08-14 — 快照区功能恢复启用 + UART2 波特率澄清

背景

printf 重入复位问题已确诊修复(2026-08-13,SPI 写无罪)。实验遗留的隔离代码收尾:快照区恢复启用、模拟写密度的 sim_snap_spi 实验桩移除。

变更

文件 变更
peripheral_main.c snap_init() 恢复(去 #if 0);offlog_boot() 恢复(上电复位原因记入事件日志);删除 sim_snap_spi 实验函数 + 主循环调用
loop_uart_proto.h 注释 UART2 波特率 115200 → 192000
loop_uart_proto.c 注释 UART2(19200) → UART2(192000)
snapshot.c/h 无需改动(2026-08-12 已实现:环形 64B SnapRec、懒擦、掉电恢复、BLE 0x28/0x29/0x2A

UART2 波特率澄清

  • UART2Loop MCU 口)实际波特率 = 192000usart_biz.c 硬编码;协议文档写 115200 已过时,以代码为准
  • g_storage_uart_baud = 19200peripheral_main.c)是独立配置项BLE 可读可改 + flash 存储),不驱动 UART2 波特率——BLE 小程序看到 19200 不代表 UART2 实际速率
  • 后续规划:UART2 RX 改 DMA/双缓冲,解决偶发丢帧(1-3 分钟一次 checksum fail

验证

  • tests/test_snapshot.c 9 用例 ALL PASSgcc 隔离单测)
  • check_c_balance.py 3 文件 OK
  • 板级待办:上电 INIT: snap ok、BLE 0x28/0x29/0x2A 查询、断网期间快照落盘

遗留

  • UART2 RX DMA 方案落地(防丢帧)
  • 板级验证快照 BLE 查询命令

2026-08-13 — 复位/卡死全链路诊断闭环:printf 重入 + 回调卡死 + UART2 丢字节

现场症状(多轮)

阶段 现象 关键数据
① 快照区落地后 SPI 写触发复位 + 串口乱码 RST=0x00000000
② factory 写死循环 参数区 magic 不匹配 → 写→复位循环 RST=0x08000000 → 0x00000000
③ 加 HardFault 诊断后 LUP 帧后复位 RST=0x10000000 (SFT)
④ PRINT 临界区修复后 乱码消失,但 LUP 帧后卡死 无复位(IWDG 未兜底)
⑤ 注释 enter PRINT 后 不卡死,偶发 checksum fail 刷屏 坏帧 7F 03 39 EF... 多帧拼接
⑥ 关高频打印+清 flag 后 系统稳定6+ 分钟无复位) SIM 心跳持续

根因链(三层,全部是软件问题)

1. printf 重入: BLE 协议栈回调(peripheral.c, BB 中断上下文)与主循环都有 PRINT
   → newlib printf 非中断安全交叉调用 → 输出乱码 + 堆/状态破坏 → HardFault
2. json_sensor_callback enter PRINT: printf 处理 %d %d %d 死循环
   (LUP Rx %s 打印成功, enter %d 打印卡死) → 主循环永久卡死
3. UART2 丢字节: LUP Rx 打印 190B @256000 = 7.4ms 关中断 (PRINT 临界区)
   → UART2(192000) RX 溢出丢字节 → 帧解析错位 → 粘帧 → checksum fail
   + g_pkg_uart_2.flag 清理缺失 → 坏帧反复处理 → fail 刷屏

重要认知修正

  • SPI 写完全无罪:模拟实验跑 17000+ 条写入(含 45ms 擦除 + 100ms 头写),0 硬件复位。推翻此前"SPI 写触发 VDD 跌落复位"判断
  • CH32V208GBU6 QFN28 无 NRST 引脚(数据手册第 1400 行 QFN28 列 = "-")→ "PA7/NRST 共线"假设作废
  • 5 分钟"神秘复位"真相tcp_jsonTCP_JSON_IDLE_TIMEOUT_MS=3000005min idle → restart)。主循环卡死时表现为 SFT 复位,恢复后只重启 TCP 服务
  • IWDG 卡死未兜底iot_enable=0 时 iot_mqtt_poll 不跑 → 无人喂狗且未初始化 → 已改为 main 无条件 init + 主循环无条件 kick(待确认 LSI 频率偏差导致的超时漂移)

修复清单

文件 修复
SRC/Debug/debug.h PRINT 宏加临界区:保存/恢复 MSTATUS(防 printf 重入,中断里调用也能正确恢复)
loop_uart_proto.c LUP Rx 逐字节打印→缓冲一次性输出→#if 0 关闭(7.4ms 关中断丢字节根因)
tcp_json_srv.c json_sensor_callback enter PRINT #if 0printf %d 卡死规避);not authed 打印 #if 0;修 \\n 双反斜杠
usart_biz.c uart_srv 所有分支消费后统一 InitPkgUart0xC0 + 非0xC0无BLE 原来不清 flag);重复逐字节打印 #if 0
ch32v20x_it.c HardFault_Handler 纯 RAM 写 mcause/mepc/mtval(官方 __get_MCAUSE/MEPC/MTVAL+ fault_diag 无条件打印 marker
Link.ld + fault_diag.h .noinit 段(复位不清零)跨复位留痕:HardFault 现场 + 执行轨迹 marker
peripheral_main.c + iot_mqtt_srv.h IWDG 无条件初始化 + 主循环无条件喂狗
cfig_flash.c factory 写止血:magic 不匹配时内存默认值(原 SPI 写→复位死循环)

遗留待办

  • UART2 偶发丢帧(1-3 分钟一次 checksum fail):RX 改 DMA/双缓冲(波特率已确认 192000,非 19200
  • json_sensor_callback enter PRINT 的 printf %d 卡死根因(newlib lock / UART1 TC 轮询)——已注释规避,恢复前需查明
  • IWDG 卡死时未在 4s 复位:LSI 频率偏差 or 配置问题
  • g_lup_sensor_cb 单回调竞争:iot_evt_sensor_cb 与 json_sensor_callback 后注册覆盖先注册,事件沿检测可能被跳过(需多回调或合并)
  • 恢复快照区功能(2026-08-14):snap_init 已恢复、sim_snap_spi 已移除,待板级验证 BLE 0x28/0x29/0x2A 查询

2026-08-12 — 复位原因位定义错位修正 + POR 复位定位(电源方向)

诊断进展(现场 RST_REASON 打印)

新固件启动首行输出 RST_REASON: 0x08000000

对照 WCH 官方库 ch32v20x.hRCC_RSTSCKR 定义)发现 offlog.h 复位原因位定义整体错位

实际位 含义 offlog 原定义 状态
bit31 LPWR bit29
bit30 WWDG bit30
bit29 IWDG bit31
bit28 SFT 软件复位 bit24
bit27 POR 上电/掉电复位 bit25
bit26 PIN NRST bit26
bit24 RMVF 清标志 main 写 bit27

两个结论

  1. 0x08000000 = PORRSTF(上电/掉电复位) → 设备被电源跌落复位,不是看门狗、不是软件死锁。方向:SPI 擦除电流脉冲(W25Q32 ~20mA 级)或供电余量不足 → 3.3V 跌落触发 PDR。待现场量测 3.3V 在 SPI 擦除瞬间的跌落
  2. RMVF 清除写错位bit27 实为 PORRSTF 只读位)→ 复位标志从未清除、历史累积 → 修正为 bit24。

修正

  • offlog.h:复位原因位定义对齐 WCH 库(IWDG=bit29 / SFT=bit28 / POR=bit27 / LPWR=bit31),新增 OFFLOG_RST_RMVF=bit24
  • peripheral_main.cRMVF 清除 |= OFFLOG_RST_RMVF
  • 协议文档 boot 事件位解析同步更新

下一步(板上)

修正后重新上电,RST_REASON 将只显示最近一次复位原因:

  • 若仍显示 PORRSTFbit27)→ 电源问题实锤:查 3.3V 供电余量、W25Q32 VCC 电容、SPI 擦除瞬间跌落
  • 若显示 IWDGRSTF(bit29)→ 主循环死锁,继续查软件
  • 若显示 PINRSTFbit26)→ NRST 干扰/复位电路

2026-08-12 — 设备不断重启修复:init/clear 同步全量擦除阻塞超 IWDG

现场症状

烧录快照固件后 CH32V208 不断重启,启动打印(CH32V20x_BLE_LIB / SystemCoreClock / W25Q32 OK!)后出现乱码:>>8>潈8>0>8...

根因链

# 环节 问题
1 snap_init() 全新区 首次初始化同步擦全部 751 个数据扇区 ≈ 34sSPI 擦除 45ms/扇区)
2 IWDG 看门狗 4siot_mqtt_srv.c:978 34s 主循环阻塞 → 看门狗超时 → 复位
3 复位后快照区仍无 "SNAP" magic 头在擦完数据扇区后才写 → 下次启动又全新区 → 再擦 34s → 再复位 = 死循环
4 offlog_init() 首擦 5.7s / offlog_clear() 2.8s 同类隐患(事件区全新或清空时)
5 snap_clear() 擦 751 扇区 34s IWDG 启用后执行 BLE SNAP_CLEAR 命令 → 必复位
6 usart_biz.c:195 非 0x7F 帧 %s 打印 Loop MCU 数据被当字符串 → 串口乱码刷屏

附带修正认知:lup_process_frame 实际由主循环 uart_srv() 调用(usart_biz.c:166),USART2 ISR 只逐字节喂 lup_feed_byte()。快照 enqueue/flush 全程主循环上下文——此前 snapshot.h 注释误标"ISR 上下文"。

修复:懒擦(init/clear 只擦当前写扇区,45ms

  • snap_init / offlog_init 全新区:只擦数据扇区 1(写指针起点),其余扇区由环形写切扇区逻辑自动擦write_raw 已有 has_data 判定)
  • snap_clear / offlog_clear逻辑清除(count=0 → 旧数据立即不可读)+ 只擦写指针起点扇区;其余扇区写覆盖时逐个擦
  • usart_biz.c:非 0x7F 帧 %s 打印改 hex %02X),杜绝二进制当字符串

启动阻塞 34s → 45msSNAP_CLEAR 阻塞 34s → 45ms。环形覆盖语义完全不变。

测试

test_snapshot 9 例全过(新增 test_lazy_erase:预埋扇区 2 旧数据,init 后保持,写满 64 条切扇区时正确覆盖);test_offlog 8 例、test_ble_offlog 7 例回归全过。

待现场确认

  • 乱码另一可能源:Loop MCU ↔ CH32 UART2 波特率/接线192000 应为标准 0x7F 帧;若 Loop 发送端配置不同,帧头识别失败 → 非 0x7F 路径)。hex 打印修复后可通过 Rcv_len:xx,dat: 7F 00 ... 判断 Loop 帧是否正常到达。

2026-08-12 — 传感快照区落地(snapshot.cBLE 新增 0x28/0x29/0x2A

背景

ROADMAP P1.2 分区规划(参数区→OTA→事件日志区→传感快照区)中,事件区已落地(offlog),快照区此前为预留。本次实现快照流:0xC0 传感帧按上报节奏落盘,断网期间波形照常记录,可离线回放。

关键设计决策

  1. 64B 定长记录 SnapRec:头部 16Bmagic=0xA6/len/flags/seq/ts_ms/boot_seq+ 4×12B 线圈数据(与 0xC0 线上格式逐字节一致,可直接对照抓包)→ W25Q32 下 48064 条。
  2. 线程安全(踩坑预警)iot_sensor_ingest()USART2 ISR 上下文loop_uart_proto.h:209 明确标注)被调用!SPI 擦除 ~45ms 绝不能在中断里做 → 中断只打包+入 RAM 暂存(8 深,满丢新),主循环 iot_mqtt_publish_sensor() 每轮 snap_flush() 落盘。单生产者(中断)单消费者(主循环)count 计数环形,volatile 字节天然原子。
  3. 分区表同一事实来源:快照区 = 总容量 − 固定区 576KB − 事件区(g_offlog_part.area_size),杜绝两模块对事件区大小理解不一致。
  4. 审计:快照清除写事件流 log_clearpayload[0]=2=快照流)。
  5. BLE 指令对齐 OFFLOG 模式SNAP_STAT(0x28)/SNAP_QUERY(0x29)/SNAP_CLEAR(0x2A)QUERY 上限 2 条(64×2+2=130B ≤ 单包缓冲,ODR 教训防御)。

测试

test_snapshot.c 8 例全过(中断安全/打包格式/环形回绕/掉电恢复/扇区切换/暂存满丢新/清空审计/分区表);test_offlog 8 例、test_ble_offlog 7 例回归全过。

⚠ 测试踩坑:不要在同文件 include offlog.c + snapshot.c —— 两个 .c 的 static 变量(如 _wr_off)在同一翻译单元符号冲突,快照 clear 审计会污染事件写指针。测试改为 mock g_offlog_part + offlog_evt

待板上验证

  • 快照区 0x110000 起、W25Q32 48064 条 capacity 打印
  • 中断风暴下暂存满丢新行为(观察点:g_snap_drop
  • 掉电恢复跨扇区扫描

2026-08-12 — BLE 分包粒度动态化修复(Too large noti 丢包)

背景

CMD_DBN_OFFLOG_QUERY 拉取 4 条日志时响应 dat=130B,set_response_buf写死的 MAX_BLE_DAT_RESPONSE_LEN=96 分包:第一包 = 帧头4 + dat96 + ckb2 = 102B。而小程序把 MTU 协商到 96 后,ATT 通知 payload 上限 = 96-3 = 93BperipheralChar4Notify 判定 102 > 93"Too large noti" 直接 return,第一包永久丢失,小程序重组永远不完整(只收到第二包 34B)。

现场日志证据:

BLE: offlog_query start_seq=1 req=4 fetched=4
Too large noti, len:102, peripheralMTU:96

根因链

# 环节 问题
1 BLE_BUFF_MAX_LEN=100 (config.h) MAX_BLE_DAT_RESPONSE_LEN = 100-4 = 96 分包块大小写死
2 分包粒度不随协商 MTU 变化 MTU 协商到 96 后,每包 96B dat 必然超 93B 上限
3 BLE_Notify_Buf.buf[100] set_response_to_notify 写 102B → 越界 2BUB,可能踩坏相邻 g_flag_notify_temp/g_buf_ble_response
4 peripheralChar4Notify 超限 return 丢包无重传,小程序重组永久不完整

2026-08-10 日志中"响应超 96B 自动分包(既有机制)"的假设不成立:分包机制存在但粒度没跟随协商 MTU

修复:ble_notify_chunk_max() 动态分包

/* 整包 = 帧头4(magic/header/len/cmd) + dat + ckb2
   须满足 整包 <= peripheralMTU - 3 (ATT opcode+handle)
   且 整包 <= MAX_BLE_Notify_Buf_LEN (本地缓冲防越界)
   MTU=23 -> 14, MTU=96 -> 87, MTU>=103 -> 94 */
static uint16_t ble_notify_chunk_max(void)
{
    uint16_t _mtu = peripheral_get_mtu();
    if (_mtu < 23) _mtu = ATT_MTU_SIZE;   /* 未协商兜底 */
    uint16_t _limit = _mtu - 9;            /* 整包上限-帧头4-ckb2 */
    if (_limit > (MAX_BLE_Notify_Buf_LEN - 6))
        _limit = MAX_BLE_Notify_Buf_LEN - 6;
    return _limit;
}

改动文件(GBK+CRLF,全部 Python 二进制替换):

文件 改动
peripheral.c 新增 peripheral_get_mtu() getterperipheralMTU 是 static
dbn_ble_srv.c 新增 ble_notify_chunk_max()set_response_buf / set_response_to_notify / set_response_iot_net / set_response_iot_topic 四处 MAX_BLE_DAT_RESPONSE_LEN 全部改用动态 chunk(组包包数与实际切包必须同一粒度,否则 pkg_amount 错乱)

验证

  • 隔离 C 测试(真实函数体 + mock peripheral_get_mtu):MTU=23/96/185/517 四组,每组验证 chunk 上限、单包 ≤ MTU-3、单包 ≤ 100B 缓冲、帧结构一致、seq 递增、重组 130B 完整且逐字节一致,全部 PASS
  • MTU=96 时:2 包(93B + 49B)✓;MTU=185 时:2 包(100B + 42B)✓;MTU=23 未协商时降级 10 包(每包 20Bpkg_amount=10 ≤ header 高4位上限15
  • 顺带消除 g_notify_buftemp.buf[100] 越界 2B 隐患
  • 待板级验证:MRS 真编译 + 真机小程序 QUERY 拉 4 条日志(本地无 RISC-V 工具链)

小程序端(可选优化,非必须)

  • Android 可在连接后 wx.setBLEMTU({mtu: 185}),分包粒度升到 94B/包,减少包数;iOS 系统自动协商,固件已自适应,不调也能正常拉取

2026-08-12 — 续:第二分包丢失 — performPeriodicTask 主动拉包

现场复测

小程序发 QUERY 后,第一包 93B 正常收到(value: ArrayBuffer(93)header 0x21 = amount=2/seq=1,含 2 条完整记录 + 第 3 条 21B 残片),第二包 49B 永远收不到。设备侧无 Too large noti(MTU 检查已过),两包间隔 50ms。

根因:分包续传依赖"下一次收包事件"

poll_dbn_ble 尾部 set_response_to_notify 虽在主循环每轮执行,但分包第二包的填充/发送链路脆弱:

  1. 第一包由 performPeriodicTask50ms TMOS)发出并 clear_ble_notify_buf
  2. 第二包的填充依赖主循环下一轮 poll_dbn_ble 执行 set_response_to_notify —— 与 TMOS 周期事件交错,任何主循环阻塞(WCHNET/uart/网络轮询)都会让时序错位
  3. peripheralChar4Notify 发送失败(simpleProfile_Notify != SUCCESS)时静默丢弃,无任何日志

修复

static void performPeriodicTask(void)
{
    if(g_flag_notify_temp){
        g_flag_notify_temp = 0;
        peripheralChar4Notify(g_notify_buftemp.buf, g_notify_buftemp.len);
        clear_ble_notify_buf(&g_notify_buftemp);
    }
    /* 分包续传: notify 空闲且响应队列还有分片时, 50ms 周期主动拉下一包,
       不依赖 poll_dbn_ble 收包事件 */
    if(g_notify_buftemp.flag == 0)
    {
        g_flag_notify_temp = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
    }
}
  • peripheral.cperformPeriodicTask 发完当前包后主动 set_response_to_notify 拉下一分片;peripheralChar4Notify 增加发送结果打印(BLE notify OK/FAIL, len, MTU),定位发送失败不再静默
  • 时序:cycle1 填充 pkt1 → cycle2 发 pkt1 + 填充 pkt2 → cycle3 发 pkt2两包间隔 50ms,与收包事件解耦

验证

  • 隔离 C 测试:无任何收包事件、仅靠 performPeriodicTask 驱动,MTU=96 → 3 周期发出 93B+49B 两包,重组 130B 逐字节一致;MTU=185 → 100B+42B,全 PASS
  • 板上烧录后设备日志应出现:BLE notify OK, len:93BLE notify OK, len:49;若出现 FAIL 则问题在 GATT 层(连接/通知使能)
  • 待真机确认:若固件打印两包都 OK 但小程序仍只收 1 包 → 检查小程序 onBLECharacteristicValueChange 订阅连续性/连接参数(连接间隔大 + 从机延迟会拉长第二包到达时间)

2026-08-12 — 事件日志区动态分区(ROADMAP 落地:JEDEC ID 选档)

背景

ROADMAP P1.2 更新:分区顺序调整为 参数区 → OTA 镜像暂存区 → 事件日志区 → 传感快照区;事件日志区容量随存储芯片动态(W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB),上电读 JEDEC ID 选档。移除 W25Q80 支持。

代码改动

文件 改动
offlog.h 新增 OfflogPart 运行时分区表 + g_offlog_partOFFLOG_AREA_BASE 改 0x090000(参数64KB+OTA512KB);OFFLOG_AREA_SIZE/DATA_SECTOR_CNT/MAX_RECORDS/DATA_SIZE 宏改为展开运行时值(主体零改动);新增 OFFLOG_CHIP_W25Q32/64/128/256 JEDEC ID 宏
offlog.c 定义 g_offlog_part(默认 W25Q32 512KB/16256条);新增 offlog_part_detect()(读 JEDEC ID → 填表,未知按 W25Q32 兜底);offlog_init 开头调用
storage.c 新增 SPI_Flash_ReadJEDEC_ID()0x9F 读 3 字节,校验 EF 40,返回 device id
storage.h 声明

注:offlog.h/c 是 UTF-82026-08-04 新建),storage.c 是 GBK——改造按各自编码精确替换,未破坏。

容量对照(事件日志区)

芯片 JEDEC 事件区 数据扇区 max_records capacity LE
W25Q32(默认) 0x16 512KB 127 16256 80 3F 00 00
W25Q64 0x17 1MB 255 32640 80 7F 00 00
W25Q128 0x18 2MB 511 65408 80 FF 00 00
W25Q256 0x19 4MB 1023 130944 80 FF 01 00

容量对照(传感快照区)

快照区 = 总容量 − 固定区 576KB(参数 64KB + OTA 512KB)− 事件区,起始 = 0x090000 + 事件区大小(snap_part_detect() 依赖 g_offlog_part.area_size 随动);数据扇区去 1 头扇区;每条 64B(32B 事件的 2 倍),每扇区 64 条。

芯片 JEDEC 快照区起始 快照区大小 数据扇区 max_records capacity LE
W25Q32(默认) 0x16 0x110000 3008KB 751 48064 00 BB 00 00
W25Q64 0x17 0x190000 6592KB 1647 105408 C0 9B 01 00
W25Q128 0x18 0x290000 13760KB 3439 220096 C0 5B 03 00
W25Q256 0x19 0x490000 28096KB 7023 449472 C0 DB 06 00

兼容性

  • 旧固件事件区在 0x010000256KB),新固件在 0x090000——旧日志数据不会误读(新地址头扇区无 magic → 视为全新区自动重建),属预期行为
  • offlog_initstorage_init 之后调用(peripheral_main.c:305-306),SPI 已就绪

文档同步

  • DLD960_BLE协议.md / DLD960_IoT_MQTT协议.md / DLD960_TCP_JSON协议.mdcapacity 字段 8064 → 动态(含四芯片对照)
  • DLD960_BLE日志查询交互.mdSTAT 示例 capacity 更新
  • ROADMAP.mdP1.2 分区规划(前序提交)

验证

  • tests/test_offlog.cmock JEDEC=0x168 例全过(环形回绕自动适配 16256)
  • tests/test_ble_offlog.cmock g_offlog_partcapacity 断言 162567 例全过
  • 地址范围模拟:四芯片事件区 0x090000 起均不超芯片容量

2026-08-12 — 续2:第二分包丢失 — 根因确认(响应缓冲越界踩踏 g_notify_buftemp

破案过程(三份现场日志 + .map 对照)

证据 结论
CLEAR busy resp: amt=2 seq=2 off=130 ret=00008122 ret 对 .map = set_response_to_notify 内部(0x7FEC+0x136)→ 第二包填充成功(正常清空),队列清空不是问题
BLE ntf flag 1->0 (len=0 temp=0)BLE send 第二包(49B)从通知缓冲物理消失,非发送路径
CLR_NTF: buf=20007bdc flag=1 len=93 ret=0000d604 第一包发送后正常清空(ret=Peripheral_ProcessEvent 内联 performPeriodicTask
.map RAM 布局 g_buf_ble_response=0x20007B58 +0x6B(107B)g_notify_buftemp=0x20007BDC107B = 7 + dat[100] = MAX_BLE_DAT_BUF_LEN 编译时是 100(旧宏)

根因链(ODR 违规)

用户 MRS 工程 dbn_ble_srv.h 宏未同步: MAX_BLE_TMP_BUF_LEN/MAX_BLE_DAT_BUF_LEN=100 (repo 为 132)
→ Buf_DBN_BLE 实际 107B (dat[100]), 链接器把 g_notify_buftemp 排在 0x20007BDC
→ QUERY 响应组包 130B: tmp_ble_buf[100..129] 越界 + set_response_buf 写 dat[0..129] 越界 30B
→ 越界写踩中 g_notify_buftemp.flag/len (物理地址重叠)
→ 第二分包填充后 flag/len 被清零 → 下一 TMOS 周期不发送 → 永久丢失

同一时间戳 BLE resp flag 1->0 (pull=0) 是越界后的连锁反应notify flag 被清 0 → poll_dbn_ble 走 idle 分支 → g_flag_notify_temp=0 → 发送周期被跳过。

修复(防御性根治,兼容新旧宏)

  1. QUERY 组包动态限制记录数_max_rec = (MAX_BLE_TMP_BUF_LEN - 2) / sizeof(OfflogEvt) → 宏=100 时最多 3 条(98B),宏=132 时最多 4 条(130B),永不越界
  2. set_response_buf 截断防御dat_len > MAX_BLE_DAT_BUF_LEN 时截断
  3. 验证:旧宏 100 下 3 条 = 98B → 分包 [93, 17] 全合规;新宏 132 下 4 条 = 130B → [93, 49] 全合规

⚠ 必做:确认 MRS 工程头文件

.map 铁证:用户编译的固件里 MAX_BLE_DAT_BUF_LEN=100(旧宏),repo 已是 132。 即使加防御,也请确认 MRS 工程 APP/include/dbn_ble_srv.hMAX_BLE_TMP_BUF_LEN/MAX_BLE_DAT_BUF_LEN 均为 132(与 repo 一致),否则结构体大小不一致(ODR 违规)的隐患仍在。


2026-08-10 — BLE 读取脱机日志接口 (OFFLOG_STAT/QUERY/CLEAR)

背景

offlog 日志已落地 MQTT V1.06 / TCP JSON V1.02 分发(2026-08-05),但 BLE 侧(小程序/APP 现场离线取证)无读取通道。现场常见场景:设备无网/断网,需蓝牙直连拉日志判断"重复上线"是平台问题还是设备复位。

方案:3 条 BLE 命令,复用 offlog 底层 API

命令码 名称 语义(对齐 MQTT log_*
0x25 CMD_DBN_OFFLOG_STAT 日志统计:status + boot_seq(2) + count(4) + capacity(4) + seq_first(4) + seq_last(4),全 LE19B
0x26 CMD_DBN_OFFLOG_QUERY 分页拉取:请求 start_seq(LE32) + count(1);响应 status(1) + count(1) + N×32B OfflogEvt 原始结构N≤4(对齐 OFFLOG_MAX_QUERY_RECORDS
0x27 CMD_DBN_OFFLOG_CLEAR 清空(审计留痕),阻塞 ~2.8s,主循环上下文可接受
  • QUERY 定位复用 MQTT 同款公式 idx = start_seq - seq_firstofflog_read_idx(),不新增 offlog API
  • 记录直接传 32B 二进制 OfflogEvtBLE 是二进制协议,无需 JSON 化;文档给出结构偏移 + 事件类型表)
  • 响应超 96B 自动分包(set_response_buf 既有机制):QUERY 最大 130B → 2 包

缓冲扩容(RAM +64B

旧值 新值 原因
MAX_BLE_TMP_BUF_LEN BLE_BUFF_MAX_LEN(100) 132 QUERY 组包 1+4×32=129B
Buf_DBN_BLE.dat MAX_BLE_BUF_LEN(100) MAX_BLE_DAT_BUF_LEN(132) set_response_buf 拷贝 129B

新增 MAX_BLE_DAT_BUF_LEN=132clear_buf_dbn_ble_all 的 memset 同步改用新宏。tmp_ble_buf/Buf_DBN_BLE 仅 dbn_ble_srv.c/h 内使用,影响面局部。

关键决策与坑

  1. GBK+CRLF 文件二进制编辑dbn_ble_srv.c/h 是 GBK 编码(file 报 ISO-8859),patch 工具会静默转码成 UTF-8 致中文注释乱码。全部用 Python rb/wb 精确替换,每处 count==1 校验;改后 file 复核仍 ISO-8859。新增注释用 ASCII 规避编码问题。
  2. case 内变量名冲突manage_dbn_ble_default 顶部已有 uint8_t i = 0, k = 0case 内再声明 i 会 redefinition。全部改用 _i/_j
  3. QUERY 短帧防御len < 11magic+header+len+cmd+5data+2ckb)返回 status=0x02,防 pkg[4..8] 越界读。
  4. LOG_CLEAR 阻塞告警:BLE 连接期间 2.8s 擦除阻塞,若 MQTT 在线会导致 WCHNET 短时失服务(60s keepalive 可吸收),文档已标注。
  5. 编码验证check_c_balance.py 自身 docstring 有转义 bug\\x 在普通字符串触发 unicodeescape),已改 raw string 修复。

验证

  • 新增 tests/test_ble_offlog.c隔离测试框架嵌入 dbn_ble_srv.c 提取的 3 case 真实文本extract_offlog_cases.py 提取,生成物 gitignore),mock offlog_* + set_response_buf7 例全过:
    • STAT 正常(19B 字段逐字节验证)/ disabled
    • QUERY 正常(4×32B 记录 + seq/type/payload 偏移验证)/ 越界空 / 短帧 / disabled
    • CLEAR 正常
  • offlog 回归 8 例 ALL PASS(既有 7 例 + export_json 未破坏)
  • 新增 docs/DLD960_BLE协议.md V1.00(帧格式 + 分包 + 3 命令 + OfflogEvt 32B 结构 + 事件类型表)
  • README 协议文档表新增 BLE 协议行
  • 本地无 RISC-V 工具链,MRS 真编译待板上联调(语法级 check_c_balance 通过)

2026-08-05 — offlog 协议导出命令分发落地 (MQTT V1.06 + TCP JSON V1.02)

背景

协议文档先行(MQTT V1.06 / TCP JSON V1.02commit 08893d5)新增 log_stat / log_query / log_clear 三命令规范,固件命令分发未接。本次补齐 P1.3:offlog 底层 API 已就绪(count/read_idx/clear),差协议层接线。

offlog 导出 API(两侧共用,放 offlog 模块避免两协议层重复)

API 作用
offlog_enabled() 日志功能是否启用(Flash 初始化成功)
offlog_seq_last() 最新一条记录全局序号(_wr_seq - 10=空)
offlog_type_str(type) 事件类型数字 → 协议字符串名(10 类 + unknown
offlog_evt_to_json() 单条记录 → JSON 对象(data 按事件类型组装,无参数事件 data=null)
OFFLOG_MAX_QUERY_RECORDS 分页上限 4(MQTT ≤500B 发布限制)

payload 大端解析(offlog_payload_u32)与写入侧 offlog_boot/offlog_coil 完全一致;coil sub 映射 1=car_enter 2=car_leave 3=loop_cut 4=loop_restore。

命令分发

  • MQTTiot_handle_publish 三分支):log_statenabled/boot_seq/count/capacity/seq_first/seq_last)、log_query(按全局序号分页,idx = start_seq - seq_first 映射 offlog_read_idx,越界返回空 records,响应用 static resp[IOT_MQTT_SEND_BUF_LEN] 防大栈)、log_clear
  • TCP JSON(三 handler + cmd 表 3 条目,全部需鉴权):同上,data_json[TCP_JSON_DATA_BUF_LEN] 组装后 json_send_ok

关键决策与坑

  1. log_clear 阻塞 ~2.8s63 扇区擦除 × ~45ms):SPI 擦除只能在主循环上下文(中断写 SPI 会炸栈),三命令 handler 均在主循环 poll 链上调用,可接受;MQTT 侧 60s keepalive、TCP 有状态重传均能吸收。注释已标注。
  2. seq_first = seq_last - count + 1(count=0 时置 0):环形覆盖后 count < capacity 是正确语义,分页定位按全局序号不按时间(未同步段时间不可靠)。
  3. CRLF 二进制编辑:四个 C 源文件 UTF-8+CRLF,用 Python rb/wb 精确替换,每处校验 count==1;gcc 全文件括号/引号平衡检查通过。

验证

  • test_offlog.c 新增测试8 test_export_jsonenabled/seq_last/type_str/evt_to_jsonboot rst 大端、coil sub/ch/value、evt_retry msg_id+retry、无参数 data:null、seq_first 公式)
  • gcc 隔离单测 8 例全过(含既有 7 例回归)
  • CRLF/UTF-8 编码零漂移(file 复核)

2026-07-15 — 🔴 loop_data 陈旧快照: 事件与缓存不同源 (帧竞争)

现象 (现场测试: 长车过双线圈)

车离开线圈1后, event_report(car_leave ch1 val=97) 正确上报并 ACK; 但随后 loop_data 连续 34 秒 100+ 条 ch1/ch2 iscar:true, 直到 msg_id 141 才恢复正常。

根因: 两条帧消费路径的数据源分裂

0xC0 帧有两条竞争消费路径 (谁先看到 g_pkg_uart_2.flag 谁消费):

  • uart_srv → lup_process_frame → lup 回调: 只喂事件检测 iot_evt_feed, 不更新 _cached_sr
  • iot_mqtt_publish_sensor Step1 直读: 事件 + 缓存都更新

主循环里 uart_srv 先于 publish_sensor 执行, Step1 只能吃到"帧恰好在两调用之间完成"的窄窗口 — 实测命中率 ~1/56 ≈ 2%。结果:

  1. 事件路径 (回调) 每帧必达 → car_leave 正确
  2. _cached_sr 冻结在车压线圈时的旧帧 (freq=61518/diff=517/relay_count, msg 28~140 逐字节一致)
  3. 陈旧数据自激: 冻结的 diff=517 > 阈值 → fast_mode 锁死 → 以 300ms 最高频率刷屏错误快照
  4. 34s 后 Step1 偶然赢一次 → 缓存刷新 → car_edge(陈旧true→新false) 立即档发出 msg 141 恢复

证据: 同期真实 LUP 帧 eval 字节 car_state=0、variation=±个位数、misc_type 0→3 轮转, 与冻结快照完全对不上。

修复

  • 新增 iot_sensor_ingest(): 事件沿检测 + 刷新 _cached_sr, 单一数据源; lup 回调与 Step1 直读两路全部汇入 → 帧竞争从此无关紧要
  • Step1 直读路径补 lup_verify_checksum (此前只有 uart_srv 路径过校验, 直读裸解析)

教训 (通用)

同一物理量的多个消费者必须同源。事件说"车走了"、快照说"车还在", 这种自相矛盾一定是数据源分裂 — 排查时先问: 这两个字段是从同一帧解析的吗?

板上验证

长车复测同场景: car_leave 事件后下一条 loop_data 的 iscar 必须已翻 false (两者同帧同源); 车走后 diff 回落 → 300ms 快速档应在数秒内退回空闲档, 不再刷屏。


2026-07-15 — loop_data 上报调度升级三档: car_state 沿立即上报

背景

原双速率 (空闲 interval / 变化态 300ms) 下, 进/出车沿最坏也要等 300ms 门控。车辆进出是最关键时刻 (道闸联动/计数), 王工要求: car_state 翻转沿立即上报, 不等间隔

实现 (iot_mqtt_publish_sensor Step2/3)

三档调度:

档位 触发 间隔
立即档 任一通道 car_state 翻转沿 0 (直通门控)
快速档 任一通道 |variation| > 阈值(10) 300ms
空闲档 平稳 配置 interval (默认 60s, 最小 1s)
  • car_edge 优先级最高, 命中即定档; variation 记录但不 break (后续通道可能有沿)
  • 门控: if (!car_edge && 未到间隔) return; — 沿直通, 其余照旧
  • 快照仍在门控通过后刷新 → 同一个沿只触发一次立即发布, 下一轮回归常规档

防洪泛分析

立即档天然有界: Loop MCU 事件帧最快 150ms 一帧, car_state 是 Loop 侧防抖后的状态 (进入确认 3 次 + 离开检测), 不存在毛刺沿; 且发布后快照即同步, 同沿不重复。最坏情况 = 帧率上限 150ms/次, 可接受。

验证

tests/test_report_cadence.c 8 组全过: 空闲60s到点 / 进车沿距上次50ms立即发 / 同沿不重复 / 快速档仍受300ms门控 / 出车沿20ms立即 / 回落空闲 / 双通道同帧沿单包覆盖。


2026-07-15 — 设备时钟同步 (方案B): 平台经 report_config 下发 Unix ts

背景

原上行 ts = mstick()/1000 (上电秒数), 非 Unix 时间戳 (现场事故报告实锤)。设备无 RTC/SNTP。王工定方案B: initialize 上线 → 平台经 report_config 下发真 Unix ts → 设备校准

实现

  • net_srv.c 新增时钟模块: dev_time_sync(unix_ts) (合法性门槛 ≥1600000000 挡掉上电秒数/0) + dev_time_now() (已校准→真 Unix; 未校准→退回上电秒数)。基准用 base_unix + (mstick()-base_tick)/1000, 无符号相减天然处理 49.7 天回绕。
  • manage_mqtt_recv_message 解析 msg_id 后, 从任意下行命令信封 ts 校准 (report_config 为主同步点)。
  • 全部上行 tsmstick()/1000dev_time_now(): net_srv.c 14 处命令响应 + initialize; iot_mqtt_srv.c loop_data + event_report(首发时刻, 重发不刷新) + 遗留响应。heartbeat 的 uptime 字段保留 mstick()/1000 (那本就是运行时长, 非时间戳)。

关键点

  • 校准前 (含首个 initialize) ts=上电秒数, 平台按量级识别未校准。
  • 设备重启无掉电保持 → 每次 initialize 后平台都须重下发 report_config 带 ts。
  • 门槛 1600000000 (2020-09) 挡掉上电秒数(几百)/0/异常, 防污染基准。

验证

tests/test_dev_time_sync.c 6 组全过: 未同步退回上电秒数 / 非法ts被拒 / 校准瞬间对齐 / 校准后随时钟递增 / 门槛边界 / mstick 49.7天回绕无符号相减正确。

⚠️ 平台侧须配合: 收到 initialize 后, 下发 report_config 时信封 ts 填当前 Unix 时间 (协议 V1.05 §2.3)。


2026-07-15 — 🔴 现场事故: MQTT protocol error 重连风暴 (根因/修复)

现象

设备 DC045A49718F (DLD960GA) 上电后每 ~1s 被 mosquitto 以 disconnected due to protocol error 踢下线, 15 分钟 80+ 次重连。平台抓包: 设备发出 520B 垃圾包 = 前 512B 全 0x00 + 尾部 8B ASCII "report_c"。

根因 (静态分析 + 报文长度量化坐实, 与平台"环形缓冲/event_report队列"推测不同)

mqtt_publishmqttBuf 只有 512B, 但 loop_data(4通道)≈604B → 溢出。

链路:

  1. loop_data 4 通道 JSON = 576B payload / 604B 整包 MQTT > MAX_MQTTBUF_LEN=512
  2. MQTTSerialize_publish 检测缓冲不足 → 返回 MQTTPACKET_BUFFER_TOO_SHORT(-2), 且一字节不写 buf(仍是 clear_mqtt_buf 的全零)
  3. mqtt_publishlenuint32_t → -2 变 4294967294
  4. WCHNET_SocketSend(mqttBuf, &len) 把清零的 512B + 越界相邻全局 temp_guide("report_config"→"report_c") 当一包发出 = 520B 垃圾
  5. broker 见非法报文类型 0x00 → RST → TCP Timeout(0x40) → 全量重连 → 风暴

⚠️ 与 event_report 无关: 事故日志中设备从未发过 event_report; mqtt_publish 是扁平缓冲非环形。此为先前就存在的隐患, 4 通道 loop_data 必触发。report_config 响应仅 216B 装得下, 故看着正常。

修复 (net_srv.c)

  1. MAX_MQTTBUF_LEN 512 → 1024: 容纳 604B loop_data + 余量 (TCP 自动分段, MQTT 不关心段边界)
  2. mqtt_publishlenint + 守卫: if (len <= 0) return; 序列化失败绝不发残缓冲 —— 这是根本防线, 即便未来任何 payload 溢出也不再吐垃圾包
  3. keepalive MQTT_KEEPALIVE_INTERVAL 9 → 60 (报告建议; 9s 过激易误断)

协议违规修复 (event_report, 报告实锤)

  • 断线重连后重发用了新 msg_id → 平台 (sn,msg_id) 去重失效重复入库。修复 iot_evt_process 重连沿: 有未决包时保持原 msg_id/原 ts 立即重发 (V1.04 §5.3-2), 不再作废换号。

待决 (需老大拍板)

  • ts 字段是上电秒数 (mstick()/1000) 而非 Unix 时间戳: MQTT 模式设备无 RTC/SNTP 时间源。选项: (a) 平台以服务端收包时间为准忽略设备 ts (最省, 推荐); (b) 平台下发时间同步命令; (c) 设备加 SNTP。暂未改, 待定方案。

验证

  • tests/test_mqtt_publish_overflow.c: 复现旧版 512 缓冲发 520B 垃圾包(首字节0x00) + 验证守卫拦截 + 1024 缓冲 641B 正常 + report_config 回归。全过。
  • tests/test_event_report.c T8 改为断言重连保持同 msg_id/ts。8 组全过。

⚠️ 板上验证: 编译查 .map 确认 RAM 余量(mqttBuf +512B); 烧录后观察不再有 TCP Timeout 风暴, loop_data 正常周期上报, 压线圈看 event_report 断线重连去重。


2026-07-15 — event_report 实现: 平台必答 + 设备重发 (协议 V1.04)

实现 (iot_mqtt_srv.c 事件模块 + net_srv.c ACK 路由)

组件 说明
沿检测 iot_evt_feed 每帧比对 car_state/loop_state 快照。双路汇聚: Step1 消费路径直接喂 + lup_set_sensor_callback(iot_evt_sensor_cb) 覆盖 uart_srv 消费路径(两路都存在帧竞争, 单独任一路都会漏帧漏沿)
事件队列 16 深环形, 溢出丢最旧; 事件仅在 ACK(code=0) 后出队
发送状态机 iot_evt_process 每轮主循环调用(挂在 iot_mqtt_publish_sensor 内, 置于 READY/enable 检查之前)。5s 超时重发, 同 msg_id/原始 ts, 最多 3 次; 耗尽→挂起, 新事件或重连沿解除并以新 msg_id 合并重报
ACK 入口 iot_evt_handle_ack net_srv.c manage_mqtt_recv_message 收到 cmd=event_report 的回显帧时调用, 必须 return 不回 unsupported(否则与平台互打乒乓)
msg_id 事件独立 uint32 计数器(g_iot_msg_id 是 uint8 且与 MQTT packet id 混用, 255 回绕会破坏平台去重窗口)

关键决策

  1. 帧消费提前到 READY/enable 之前: 断网/未使能期间事件照样检测入队, 重连后补报。loop_data 周期上报仍受 READY+enable 门控。
  2. event_report 不受 report_config.enable 门控(王工 2026-07-15 拍板): 事件为关键不可再生数据, 上电即检测上报, 与 loop_data 周期上报的开关解耦。report_config.enable 只管 loop_data。
  3. 线圈断开期间屏蔽 car 沿: 断开时 car_state 不可信, 仅前后两帧 loop 均正常才判进出车; loop_restore 的 value = DBN 本地计时(mstick 差)/50。
  4. car_leave 的 value 直接取离开帧 misc(时间量) = 通过时间(50ms 单位)。
  5. 首帧只建快照: 上电线圈上已有车不算进入沿。

已知边界

  • 队列溢出且恰有未决包时: 作废未决包重发新 msg_id → 平台 (sn,msg_id) 去重失效, 可能重复入库一包。触发条件: broker 不应答的 20s 窗口内涌入 >16 条事件, 现场概率极低, 平台可按事件内容+ts 二次去重兜底。
  • BSS 增量 ~700B(队列128B + 发送缓冲 512B + 状态), 编译后查 .map 确认 RAM 余量

验证

gcc 隔离单测 (/tmp/test_event_report.c) 8 组全过: 首帧快照/进出车沿/5s×3 重发同 id 同 ts/挂起与新事件解除/错 msg_id 及 code≠0 不出队/断开恢复时长+断开期屏蔽 car 沿/单包 6 条上限(实测 317B<500B)/重连沿补报。

⚠️ 平台端注意: 收到 event_report 先落库后应答, 按 (dev_serial,msg_id) 10 分钟窗口去重, 重复包直接答 code=0。


2026-07-15 — MQTT 快速上报增加 car_state 翻转沿触发

背景

原 fast_mode 仅由 |variation| >= 10 触发。灵敏度设高档时小车信号弱,variation 可能不过阈值,但 Loop MCU 已判定 car_state 翻转——进/出车事件仍按空闲间隔(≥1s)慢发,后台看到的过车时刻误差大。故增加:任一通道 car_state 翻转沿也触发 300ms 快速上报

实现要点(两个坑,首版都踩了)

  1. if (fast_mode = 0) 单等号 → 赋值恒假,沿检测死代码。修正为直接判断(循环内走到该行 fast_mode 必为 0,无需再判)。
  2. 快照刷新必须在间隔门控之后。若在门控前无条件刷新 _last_car_state,沿被门控吞掉(如距上次发布 <300ms)时快照已同步,下一轮检测不到翻转 → 事件退化为空闲间隔上报。修正:return(未到间隔)时不刷快照,沿保持"待发"持续顶住 fast_mode,直到真正发布才同步。
/* Step 2 循环内: variation 阈值 与 car_state 沿, 任一命中即 fast */
if (av >= IOT_MQTT_VARIATION_THRESHOLD) { fast_mode = 1; break; }
if (_last_car_state[i] != coils[i].car_state) { fast_mode = 1; break; }

/* Step 3 门控通过后才刷新快照 */
for (i = 0; i < coil_count; i++) _last_car_state[i] = coils[i].car_state;

验证

本地 gcc 隔离单测(buggy vs fixed 对照):进车沿后 buggy 延迟 950ms(退化空闲间隔),fixed 250ms(≤300ms 快速档);出车沿、负向 variation 回归均通过。

⚠️ 上电首帧若已有车(_last_car_state 初值 0 vs car_state=1),会触发一次 fast_mode——属良性,首帧本就该尽快发。


2026-07-14 — Loop 上报 variation 解析升级 2B→3B 有符号 (协议 V1.05)

背景

Loop MCU 上报的 SENS_MULTI_LOOP_DYNAMIC (0xC0/0x0C) 中变化量 variation 由 2B 无符号扩展为 3B 有符号补码(详见 Loop 侧 devlog 与协议 V1.05)。DBN 作为接收/转发端,解析逻辑须同步升级,否则每通道单元 12B 会被按旧的 11B 步长错位解析,4 通道数据全错

改动 (4 处)

1. loop_uart_proto.h — 结构体字段类型

// uint16_t variation;  ->  int32_t variation;  // 3B有符号, = Origin-CAPVD

2. loop_uart_proto.c — 解析函数 lup_parse_sensor_report

  • 每通道单元步长 i*11i*12
  • 通道数计算 data_len/11data_len/12
  • 频率仍 3B 无符号;变化量 3B 解码 + bit23 符号扩展
int32_t v = c[5] | ((int32_t)c[6] << 8) | ((int32_t)c[7] << 16);
if (v & 0x800000) v |= (int32_t)0xFF000000;   // 符号扩展, 漏了负值会变~1600万大正数
cs->variation = v;
  • 杂项偏移 c[7..10]c[8..11](整体右移 1B

3. iot_mqtt_srv.c — fast_mode 阈值判断(隐藏坑)

// variation 变有符号后, 车进入(正)/反向漂移(负) 都应触发加速上报
int32_t av = (v >= 0) ? v : -v;
if (av >= IOT_MQTT_VARIATION_THRESHOLD) fast_mode = 1;   // 原 variation>=10 会漏掉负向

4. JSON 输出(iot_mqtt_srv.c / tcp_json_srv.c

  • "diff":%d / "variation":%d 格式符不变——CH32V208 上 int32_t == int%d 天然正确输出负号,无需改动。

缓冲核查

整帧 52B→56BDBN 侧 g_pkg_uart_2.pkg[BUFF_STACK_SIZE=512]LUP_MAX_PKG_LEN=70 均远大于 56B,接收无截断;转发 MSS=576 不分片。

⚠️ 必须与 Loop 固件同版本发布(步长死绑定)。


2026-06-26 — TCP JSON 协议框架搭建

1. TCP JSON Server 基础实现

  • WCHNET TCP listen socket 创建(端口 5960PROTO_TYPE_TCP
  • 参考 WCH 官方 EVT/EXAM/ETH/TCPServer 例程
  • g_net_state.flag 状态机: ETH_LibInit→1, WCHNET_CreateUdpSocket→2, WCHNET_CreateTcpSocket→3
  • SSC 禁用时 (NET_SSC_ENABLE=0) 手动设 flag=2 跳过 UDP 初始化

2. 鉴权 + 15 条命令

  • pwd_verify 鉴权,3次错误 → 60s 锁定
  • 命令表驱动分发: dev_info_query, ssc_net_set/query, iot_net_set/query, iot_topic_set/query, pwd_set, factory_reset, device_reset, loop_param_set/query, loop_version_query, loop_factory_init, loop_sens_read/write
  • Deferred 响应模式: Loop MCU 命令异步 → TcpJsonPending 挂起 → json_check_pending() 轮询

3. simple_json 解析器修复

  • 6 个 bug 修复: NULL 解引用崩溃(plain values)、缺少 null terminator、buffer unsafe clear、数组支持缺失
  • 字符串值提取时不带引号(调用方无需手动 strip quotes

4. WCHNET TCP 踩坑

问题 修复
listen socket 与数据 socket 混用 → 收不到数据 CONNECT 发到 N, RECV/DISCONNECT 发到 N+1
WCHNET_SocketSend 在 listen socket 静默失败 改用 g_json_socket_listen + 1
接收缓冲区与帧缓冲区重叠 → 数据损坏 分离 WCHNET 内部 buf + 帧累加 buf
#if NET_SSC_ENABLE 误包共享函数 移出 mStopIfError/GetMacAddr/get_ipstr_to_array

2026-06-30 — Loop MCU 串口协议 (0x7F) + 传感器上报

1. USART2 Loop MCU 通信

  • 波特率 192000(文档写 115200,实际硬件 192000
  • 0x7F 协议帧解析器: lup_feed_byte() 逐字节状态机 + LEN-based 帧边界
  • 校验字节站位: total_len = 5 + LENCMD 已在 LEN 中,勿重复计)
  • 命令: lup_cmd_send() 发送 → lup_cmd_check_timeout() 轮询超时

2. 0xC0 传感器数据上报

  • Loop MCU 主动推送 0xC0 帧 → lup_process_frame() 校验 → 回调 json_sensor_callback
  • TCP JSON 输出格式: {"sens_type":"multi_coil","coils":[...]}
  • g_report_active 开关控制上报启停
  • 0xC0 帧通过回调直接驱动网络上报,不经 uart_srv 阻塞

3. 0xC0 帧时间量字段

  • misc_type=0passtime_ms(通过时间 / 车间距,根据 car_state 区分)
  • misc_type=1cut_amount(线圈断开次数)
  • misc_type=2flow_amount(车流量)
  • misc_type=3relay_count(继电器动作次数)

4. 协议文档

  • docs/vd960_loop_protocol_v1.0x.md — 0x7F 帧格式、校验算法、命令参考
  • 波特率修正: 115200 → 192000

2026-07-01 — passtime_ms5 字段改名

vd960Loop 侧时间戳从 5ms 改 50ms 后,DBN 侧同步:

  • passtime_ms5passtime_ms(字段名去 5 后缀)
  • loop_uart_proto.h/c 结构体 + 解析 + tcp_json_srv.c JSON 字段同步

2026-07-02~03 — TCP Server 超时自动重启机制

1. 三项超时触发条件

条件 时间 处理
无任何连接 5min do_restart → 计数重启
无数据交互 5min do_restart → 计数重启
Auth 密码错 3 次 立即 tcp_json_restart() 直接重启

2. 重启计数保护

  • 最多连续重启 3 次,超过进入 10 分钟冷却期
  • CONNECT 成功时 restart_count 清零(正常连接重置计数,不算异常)
  • Auth timeout 不消耗计数器(正常运维行为)

3. WCHNET 限制 - listen socket 不可关闭重建

根因: WCHNET_SocketClose(listen) 后端口 5960 仍标记"已占用"SocketCreat 返回 ERR_ISCONN(0x1D)

最终方案: tcp_json_restart() 只关数据 socket(N+1) 断开客户端,listen socket 保持不动,仅重置应用层状态。WCHNET 自动处理下一个 CONNECT。

4. Auth timeout 死循环修复

原 Auth timeout 手工 WCHNET_SocketClose 后未设 g_json_socket_listen = 0xFF,下次 poll 条件仍满足 → 无限打印。改用统一的 do_restart 路径后修复。

5. Auth 3次失败 → 直接重启

去掉 60s Auth timeout 倒计时。改为连接后 3 次鉴权失败(密码错/格式错)即 tcp_json_restart()。连接后无交互由 5min 空闲检查覆盖。


2026-07-06 — MQTT IoT 基础打通

背景

在 CH32V208RISC-V, 栈仅 2KB)上实现 MQTT IoT 协议栈,对标 DBN101GA 参考项目。WCHNET TCP MSS=576, Publish 须 ≤500B。

1. MQTT 重构对标 DBN101GA

  • iot_mqtt_srv.c 对标 DBN101GA 参考实现重写
  • MQTT socket 复用 SocketId_TCP(而非独立 socket
  • iot_connect_brokerSourPort 参数
  • 出厂默认 topic 使用真实设备序列号

2. 栈溢出系列修复

问题 修复
MQTT buffer 在栈上 → 溢出 改为 static 全局分配
iot_mqtt_publish_sensor: data_json[2KB] + payload[2KB] = 4KB 远超 2KB 栈 减小缓冲区 + static 分配
缓冲区 2048 → 1024 单次交互不超 1KB

3. MQTT CONNECT 延迟发送

根因: MQTT_connect() 在中断上下文调用 WCHNET_SocketSend → 崩溃。

修复: CONNECT 包延到 poll 轮询内发送,避免中断内调用网络 API。

4. MQTTPacket 初始化修复

MQTTPacket_connectData_initializer 是复合字面量,不能用于赋值。改用临时变量中转。

5. IoT socket 重复创建 + broker IP 解析修复

  • IoT socket 重复创建 → 连接失败,改为复用已有 socket
  • broker IP 字符串解析修正

2026-07-07 — MQTT 协议 V1.01 + 命令分发

1. MQTT 主题压缩:多主题 → 双主题

协议从 V1.00 的多 topicdld960/{sn}/loop_data, /event_report, /heartbeat, /cmd, /cfg…)压缩为 V1.01 双主题:

方向 V1.00 V1.01
设备→平台 5+ topics dld960/{sn}/dev
平台→设备 5+ topics dld960/{sn}/srv

2. MQTT 命令分发

manage_mqtt_recv_message() 实现 V1.01 协议命令分发,支持 cmd / Method 双格式:pwd_verify, dev_info_query, loop_param_set/query, loop_version_query, loop_factory_init, device_reset, factory_reset 等。

3. report_config 完整 7 参数

MQTT/TCP report_config 支持完整 7 参数配置:period, loop_data, event_report, heartbeat, passtime, cut_amount, flow_amount。与 TCP JSON 协议统一。

4. MQTT 网络配置

ssc_net_set / iot_net_set / iot_topic_set 三条命令实现,支持通过 MQTT 远程配置设备网络参数和主题。


2026-07-08 — MQTT 稳定性修复系列

1. g_iot_socket 未初始化 → 硬故障重启

g_iot_socket 声明为全局变量但未初始化,值为 0xFFWCHNET_SocketSend(0xFF) 直接触发硬件故障重启。

修复: 初始化为 INVALID_SOCKET

2. loop_data JSON 超 MSS 导致 SocketSend 溢出

WCHNET TCP MSS=576,原 loop_data JSON 四通道全字段 ~800B → WCHNET_SocketSend 溢出崩溃重启。

修复: 精简 JSON 字段至 ~400B(单包达标),再分批发送2通道/包 × 2包)。

3. iot_mqtt_send() hex dump 循环 → 栈溢出

调试用的 hex dump 循环(65次 PRINT)在仅 2KB 栈的 CH32V208 上直接栈溢出重启。

修复: 移除 hex dump 循环。

4. MQTT 主动上报无数据 + PINGREQ 缺失

poll_mqtt() 未正确触发 MQTT_Yield()MQTT_Live(),导致主动上报无数据、PINGREQ 缺失断连。

修复: 改用 net_srv.c 统一的 mqtt_publish() + 修正 poll_mqtt() 调度逻辑。

5. 传感数据 topic 修正

topic 修正为 V1.01 双主题协议 dld960/{sn}/dev(此前遗留旧 topic 格式)。

6. 恢复 loop_data 完整字段

精简 JSON 后缺失部分字段(freq, passtime_ms 等),恢复完整字段并在分批框架内发送。


2026-07-10 — 双主题发布统一 + initialize 对齐 + packet_id 断连修复

1. 双主题发布统一 (dld960/{sn}/dev)

此前 heartbeat 和传感器上报仍使用旧 topic 路径(dev/statusg_iot_topic.topic_pub),与 V1.01 双主题模型不一致。

修复: 所有 MQTT 发布统一为 dld960/{sn}/dev

  • iot_mqtt_srv.c heartbeat: iot_make_topic(..., "dev", "status", ...)snprintf(..., "dld960/%s/dev", ...)
  • iot_mqtt_srv.c iot_mqtt_publish_sensor: g_iot_topic.topic_pubdld960/{sn}/dev
  • net_srv.c dev_initialize_pub: g_iot_topic.topic_pubdld960/{sn}/dev

2. initialize 消息格式对齐 V1.03

dev_initialize_pub() 原先缺少顶层 model/hard_ver/soft_ver 字段,且在 extra_info 中有已废弃的 version 字段。

修复: 对齐协议文档 §5.1

  • 补充 model(PRODUCT_MODEL)、hard_ver(HARDWARE_VER)、soft_ver(FIRMWARE_VER) 到 data 顶层
  • 移除 extra_info.version
  • 缓冲区扩容 256→512 字节

3. mqtt_publish() packet_id=0 导致 broker 断连 🔴

现象: 设备收到查询指令后正确发布响应 JSON,10ms 后 broker 主动断开 TCP 连接(SockInt stat=0x10)。

根因: net_srv.cmqtt_publish() 调用 MQTTSerialize_publish()packet_id 硬编码为 0。当 req_qos=1(命令响应使用 QoS 1)时,MQTT 规范要求 packet_id ≠ 00 属于协议违规,broker 检测后直接踢掉连接。

修复: 新增 static uint16_t s_mqtt_pkt_id 计数器,QoS>0 时自增作为 packet_idQoS=0 保持 0。

static uint16_t s_mqtt_pkt_id = 0;
uint16_t pkt_id = (req_qos > 0) ? ++s_mqtt_pkt_id : 0;

4. DBNMQTTool 同步更新

  • 协议版本注释 V1.01→V1.03
  • 协议 Topic 树 + 模拟页增加 initialize 支持
  • device_manager.mark_online() 扩展 model/hard_ver/soft_ver 参数,设备列表正确显示型号
  • 增加 dld960/+/dev/# 通配订阅兼容旧固件多级 topic


2026-07-23 — loop_data JSON 构建优化: 消灭 data_json 中间缓冲 + coil_count 硬限

背景 / 问题

现场 DC045A49718F 出现 MQTT 上报异常: 平台收到 _raw 消息, hex 解码后发现 loop_data JSON 中间嵌入了完整的 MQTT PUBLISH 二进制帧 (event_report 的封包)。

根因: iot_mqtt_publish_sensor()static char data_json[1024]static char payload[1400] 合计 2424B BSS, 叠加同文件其他大缓冲 (~9KB total), 在 CH32V208 48KB RAM 中可能导致链接器将 data_json/payloadmqttBuf[1024] 分配到重叠地址。

污染路径:

  1. iot_evt_process()mqtt_publish()MQTTSerialize_publish(mqttBuf,...) 写入 MQTT 二进制到 mqttBuf
  2. 因 BSS 重叠, data_json 也被写入相同内容
  3. snprintf(data_json, ...) 覆写前 ~427B 为通道 JSON, 尾部残留 MQTT 二进制
  4. snprintf(payload, ..., "%s", data_json)%s 将残留二进制也拷进 payload
  5. mqtt_publish(topic, payload, 0) → 发出污染后的 payload

修复

改动 说明

2026-07-23 — MQTT 栈重构 + 硬件看门狗

问题背景

vd960DBN 现场出现三类 MQTT 上报异常:

  1. _raw 异常: loop_data JSON 中间嵌入 MQTT PUBLISH 二进制帧
  2. 频繁 initialize: 每 4 秒重连, 旧 MQTT 栈与 IoT 栈抢 socket
  3. SocketSend hard fault: MQTT CONNECT 发送后设备重启

根因链

问题 根因 修复 commit
_raw data_json[1024]mqttBuf[1024] BSS 重叠 → 消灭 data_json e11a80c
_raw 仍出现 mqtt_publish() 用全局 mqttBuf, event_report 二进制污染 loop_data 086dbc6
→ 改用 iot_mqtt_publish(), buf[1024] 与 mqttBuf 物理隔离
频繁 initialize WCHNET_HandleSockInt 调旧 mqtt_connect() + IoT 栈也发 CONNECT → 双 CONNECT 3d017cc
→ socket 事件全委托给 iot_mqtt_handle_sock_int, 旧栈切断
SocketSend 重启 iot_mqtt_send()uint16_t slenuint32_t* → 栈破坏 hard fault a577537
SocketSend 还重启 WCHNET_ModifyRecvBuf(_iot_wchnet_buf) 覆盖原生 SocketRecvBuf → DMA 非法 9521a5e
tmp[1152] 栈变量在中断上下文撑爆 2KB 栈 → 改 static
SocketSend 再重启 ⑦ WCHNET SocketSend 在中断外调用 hard fault → 改回中断内发 CONNECT 32f0edb
_raw 再次出现 WCHNET_SocketSend 部分发送 520/607B → 残留拼到下一帧 9e83063
iot_mqtt_send 改为 while 循环重试
命令处理缺失 report_config/event_report ACK 无人处理 → code=4 67976ec
msg_id 跳号 ⑩ 心跳 ++g_iot_msg_id + iot_mqtt_publish 双递增 a9c36e4

三层保活

┌─ MQTT 层: PINGREQ/PINGRESP (60s) → broker 不回 → TCP 超时
├─ TCP 层:  KeepAlive → WCHNET SINT_STAT_DISCONNECT/TIMEOUT
└─ 硬件层: IWDG (LSI≈40kHz, Prescaler=256, Reload=625 → ~4s)
             主循环 iot_mqtt_poll() 每轮喂狗

heartbeat 是单向设备上报, 平台不回复; MQTT PINGREQ/PINGRESP 才是双向保活机制。

最终架构

WCHNET_HandleSockInt (net_srv.c)
  └─ iot_enable? → SocketRecvBuf(原生) + KeepLive + iot_mqtt_handle_sock_int()
      ├─ CONNECT        → iot_mqtt_send_connect() [中断内] → MQTT_CONNECTING
      ├─ RECV           → _iot_recv_buf → iot_process_recv()
      │    ├─ CONNACK   → iot_mqtt_send_subscribe() → SUBACK → READY
      │    ├─ PUBLISH   → iot_handle_publish(report_config/event_report ACK/...)
      │    └─ PINGRESP  → (MQTT 库自动处理)
      └─ DISCONNECT     → g_iot_state=DISCONNECTED → 重连退避

Main_Circulation:
  iot_mqtt_publish_sensor()   ← 0xC0→loop_data + event_report
  iot_mqtt_poll()             ← IWDG喂狗 + 状态机 + PINGREQ(60s)

发送缓冲隔离 (v3)

event_report → mqtt_publish()       → mqttBuf[1024]      (net_srv.o BSS)
loop_data    → iot_mqtt_publish()   → buf[1024]           (iot_mqtt_srv.o BSS)
heartbeat    → iot_mqtt_publish()   → buf[1024]
initialize   → iot_mqtt_publish()   → buf[1024]

msg_id 统一

所有发布点用 g_iot_msg_id + 1 预读, iot_mqtt_publish 内部 ++g_iot_msg_id (MQTT pktid for QoS>0, 对QoS=0 也递增以保持一致)。


2026-07-23 — 保活精简 + SocketSend 故障恢复

保活机制梳理

heartbeat JSON (60s 单向设备上报) 与 MQTT PINGREQ/PINGRESP 功能重叠:

  • heartbeat 是设备→平台单向, 平台不回复, 不能做存活检测
  • MQTT PINGREQ/PINGRESP 才是双向保活, broker 不回 → TCP 超时

决策: 删除 heartbeat JSON, MQTT PINGREQ 独立保活; loop_data 按 g_report_cfg.interval 上报即可。

三层保活链路

层级 机制 超时 触发
MQTT PINGREQ/PINGRESP (60s) broker无响应→TCP超时 SINT_STAT_TIM_OUT
TCP KeepAlive WCHNET 检测 SINT_STAT_DISCONNECT
硬件 IWDG (4s, LSI) 主循环卡死 硬件复位

SocketSend 故障快速恢复

问题: WCHNET_SocketSend 突然返回 ret=0x11 sent=0, TCP 超时要等 ~2 分钟才触发 SINT_STAT_TIM_OUT, 期间所有 loop_data/event_report 全丢。

修复 v1 — 连续失败检测 (7c0de6c):

  • _iot_send_fail_cnt 追踪连续失败次数
  • 3 次连续失败 → 主动 WCHNET_SocketClose + DISCONNECTED → 重连
  • 任一次成功清零, 新连接也清零

修复 v2 — 重连时重建 socket (98d9b99):

  • iot_connect_broker() 原只设 g_iot_socket = SocketId_TCP 但 close 后 socket 已销毁 → TCP connect 一直超时
  • 改为调 WCHNET_CreateTcpMqttSocket() 真正 create + connect
  • 断连后加 3s 延迟再重连, 给 WCHNET 清理时间

BSS 变动

变量 改动 ΔBSS
data_json[1024] 删除 (v1) -1024B
iot_send_heartbeat::payload[512] 删除 (函数移除) -512B
_iot_send_fail_cnt 新增 +4B
净省 -1532B

---| | 消灭 data_json[1024] | 直接在 payload[1400] 构建完整 JSON, 省 1024B BSS | | 消除 %s 拷贝 | 不再从中间缓冲格式化到 payload, 杜绝尾部二进制残留路径 | | coil_count 硬限 | if (coil_n > 4) coil_n = 4 — 防 0xC0 坏帧致 snprintf 循环溢出 | | JSON 构建顺序调整 | 先写包装头 {"msg_id":...,"data":{"channels":[, 再追加通道, 最后 ]}} |

BSS 节省: 1024 bytes 协议兼容: JSON 输出格式不变 ({"msg_id":N,"cmd":"loop_data","ts":T,"data":{"channels":[...]}})

2026-08-04 — 脱机事件日志系统 (W25Q32 环形) — 解决"重复上线"取证

背景

设备挂平台测试出现重复上线(现场未断电)。平台每次收到 initialize 都视为上线,但无本地日志无法区分两种可能:

可能 特征 诊断
设备真复位 有 BOOT 事件 + boot_seq 递增 设备侧问题(电源/看门狗/软件)
MQTT 断连重连 无 BOOT 事件,只有 IOT_* 事件,boot_seq 不变 网络问题(链路/ broker

注:iot_handle_suback() 每次 MQTT 连上 READY 都会发 iot_send_initialize()——断连重连就会触发平台"重复上线",这是最大嫌疑点。

方案 (P1.2 落地: 事件流, 快照流后置)

分区 (W25Q32 4MB):参数区 64KB(0x000000) | 事件日志区 256KB(0x010000) | OTA 区 512KB(0x050000) | 快照区 ~3.25MB(0x0D0000, 预留)

环形实现:头扇区(扇区0, 32B 元数据) + 63 数据扇区 × 128 条 × 32B = 8064 条;顺序写、满扇擦下一扇区(天然磨损均衡);头在扇区切换时刷新,上电从头部写位置向后扫描恢复(掉电不丢)

记录格式 (32B 定长)magic type len flags | seq(4) ts_ms(4) unix_ts(4) boot_seq(2) rsvd(2) | payload[12]

事件类型 (V1)

type 事件 说明
0x01 BOOT 复位原因寄存器 RCC_RSTSCKR 全量入日志 (IWDG/POR/SFT 区分)
0x10/0x11 IOT_CONNECT / IOT_READY READY 即发 initialize——重复上线直接证据
0x12 IOT_DISCONN 细分原因: 1=断开 2=超时 3=CONNACK拒绝 4=连接超时
0x13 IOT_RECONN 退避时长 ms
0x30/0x31 EVT_RETRY / EVT_GIVEUP event_report ACK 超时重发/耗尽 (网络质量)
0x40 COIL 进/出/断/恢复 (sub+ch+value)
0x50 TIME_ANCHOR 时钟同步锚点 (boot_seq↔unix 回算绝对时间)
0x70 LOG_CLEAR 清日志审计

插桩点(全部主循环上下文,socket 中断内不写 SPI):

文件 位置 事件
peripheral_main.c main() storage_init 后 offlog_init + 复位原因读+清标志
iot_mqtt_srv.c iot_mqtt_poll() 状态沿检测 CONNECT/READY/DISCONN
iot_mqtt_srv.c 重连退避 / TCP超时 / CONNACK拒绝 RECONN / DISCONN(4) / DISCONN(3)
iot_mqtt_srv.c iot_evt_process EVT_RETRY / EVT_GIVEUP
iot_mqtt_srv.c iot_evt_enqueue COIL
net_srv.c dev_time_sync 成功 TIME_ANCHOR

三个关键坑 (单测抓出来的)

  1. 中断上下文不能写 SPIiot_mqtt_handle_sock_int 是 WCHNET 中断,SPI 擦除 ~45ms 阻塞会炸;MQTT 事件改在 iot_mqtt_poll() 状态沿检测(毫秒级滞后,可接受)
  2. sizeof(OfflogEvt)=36 而非 32payload[14] 后结构体对齐补 2B padding → 每扇区实际 113 条,8064 条撑爆 63 扇区提前回绕、写指针错乱。修复:字段重排(uint8×4→uint32×3→uint16×2→payload[12]+ 编译期断言 typedef char size_must_be_32[...]
  3. 扇区级覆盖粒度 vs 记录级 count:回绕擦整扇区会丢 128 条,但 count 仍封顶 8064 → read_idx 反推逻辑首错位。修复:擦扇区前检查目标扇区是否有数据(读首条 magic),有则 count -= min(count,128)

单测 (gcc 隔离, tests/test_offlog.c)

用例 覆盖
test_fresh_init 全新初始化 + BOOT 事件字段
test_ring_wrap 8069 条环形回绕: count=7941, 逻辑首 seq=129
test_power_loss_recovery 掉电重启: boot_seq 递增, seq 跨 boot 连续
test_sector_switch 128 条扇区切换 + 头 wr_off/wr_sector
test_clear 清空 + LOG_CLEAR 审计 (seq 不重置)
test_power_loss_mid_sector 跨扇区掉电恢复

待办

  • P1.3 导出: log_query/log_stat/log_clear 命令接入 MQTT/TCP/BLE
  • 快照流 (0xC0 帧原样落盘) 后置
  • 复位原因寄存器布局待板上验证 (RCC_RSTSCKR 按 STM32F1 兼容写)
  • MRS 工程编译确认 offlog.c 被自动收集

2026-08-04 — tcp_json_srv 缓冲宏化: data_json[2048] → TCP_JSON_DATA_BUF_LEN(1024)

三处 char data_json[2048]sensor_report 推送×2、loop_param_query 响应×1)改用头文件宏 TCP_JSON_DATA_BUF_LEN(默认 1024)。

容量核验format_sensor_json 4 通道最大 ~660B、format_loop_param_json ~920B,均 < 1024 且 format 函数有 snprintf 溢出保护(written >= remaining 返回 -1)。最终发送帧受 TCP_JSON_MAX_FRAME=800 截断,data_json 只做中间组装,1024 足够。

附带收益:栈上缓冲 2048→1024,每处省 1KB 栈(CH32V208 栈紧张,中断路径 832/1322 在回调/主循环调用)。

2026-08-04 — offlog: TIME_ANCHOR 严格使用平台下发 unix_ts

offlog_time_anchor() 原为丢弃传参、内部重取 dev_time_now()(毫秒级误差)。改为 offlog_evt_ts() 直接写入平台下发值,锚点事件与平台 ts 严格一致。新增单测 test_time_anchor_exact(传 1800000000 验证写入值),7 例 ALL PASS。

修订记录

版本 时间 说明
V4.8 2026-08-21 MQTT 协议 V1.10 固件实现: dev_info_query/initialize 补 loop_ver/loop_hw_ver + 新命令 loop_version_query 异步回包 + 版本缓存(上电/OTA done 刷新), 单测 44 例
V4.7 2026-08-19 固件版本 1.02.03→1.02.04 (SPI 存储适配: 去厂商识别 + 恢复 factory 写入, 板级验证通过)
V4.6 2026-08-19 恢复 factory 配置写入: 解除 8-13 止血, 空片首启写 magic+默认参数 (换 W25Q128 后配置永久丢失修复)
V4.5 2026-08-19 SPI Flash 识别去厂商代码: W25Qxx 宏去 0XEF 前缀, ReadJEDEC_ID 删厂商校验, storage_init 低字节匹配 (兼容多厂家)
V4.4 2026-08-19 UART2 DMA 下 BLE→Loop OTA 修复: lup_feed_byte_ota 0x9F 帧解析+透传回 BLE (停等 ACK), DMA poll OTA 模式切换/溢出阈值/tick 归零, 单测 8 例
V4.3 2026-08-18 log_query 改 hex 原始字节上报 (协议 V1.03/V1.07 修订): offlog_evt_to_hex/snap_rec_to_hex, 2 条快照 406B<800B, 快照 count 恢复 2
V4.2 2026-08-18 MQTT 命令集补齐: ssc_net_query/iot_net_query/iot_topic_query 只读组包 (协议 V1.02 对齐), 工具禁用未实现按钮
V4.1 2026-08-18 TCP/MQTT 脱机日志快照流: log_stat/log_query/log_clear 支持 stream=snapshot (协议 V1.03/V1.07), snap_rec_to_json 序列化, 事件流 count=0 按上限
V4.0 2026-08-10 BLE 脱机日志接口: OFFLOG_STAT/QUERY/CLEAR (0x25/0x26/0x27), 32B OfflogEvt 二进制直传, 缓冲扩容 132B, 隔离测试 7 例 + 协议文档 V1.00
V3.9 2026-08-05 offlog 协议导出命令分发: log_stat/log_query/log_clear 落地 MQTT V1.06 + TCP JSON V1.02 (seq_first/seq_last 计算、idx 映射、OFFLOG_MAX_QUERY_RECORDS=4、log_clear 阻塞~2.8s)
V3.8 2026-08-04 offlog: TIME_ANCHOR 严格用平台下发 unix_ts (offlog_evt_ts), 单测 7 例
V3.7 2026-08-04 tcp_json_srv 缓冲宏化: data_json[2048]→TCP_JSON_DATA_BUF_LEN(1024), 省栈1KB×3
V3.6 2026-08-04 脱机事件日志系统: W25Q32 256KB 环形(8064条) + BOOT/网络/事件/线圈/时钟锚点 8 类事件 + 掉电恢复 + gcc 单测 6 例
V3.5 2026-07-23 保活精简(去heartbeat+IWDG) + SocketSend故障恢复(3次失败重连+重建socket)
V3.4 2026-07-23 MQTT 栈重构: 发送缓冲隔离/命令处理/msg_id统一/看门狗, 共10项修复
V3.3 2026-07-23 loop_data: 消灭 data_json[1024] BSS 缓冲 + coil_count 硬限, 防 mqttBuf 重叠致 _raw 异常
V3.2 2026-07-10 双主题发布统一 + initialize 数据格式对齐 V1.03 + mqtt_publish packet_id=0 断连修复
V3.1 2026-07-09 V1.03: 订阅后发 initialize 上线消息; iot_mqtt_srv 订阅改双主题
V3.0 2026-07-08 MQTT 稳定性修复: socket初始化/buffer溢出/分批发送/hex dump/PINGREQ
V2.9 2026-07-07 MQTT V1.01 双主题协议 + 命令分发 + report_config 7参数
V2.8 2026-07-07 MQTT 网络配置: ssc_net_set / iot_net_set / iot_topic_set
V2.7 2026-07-06 MQTT IoT 基础打通: 对标DBN101GA + 栈溢出修复 + CONNECT延迟发送
V2.6 2026-07-06 IoT 模式配置 bug 修复
V2.5 2026-07-03 tcp_json_restart 只关数据socket, listen保持不动 (WCHNET限制)
V2.4 2026-07-03 去掉 Auth 60s timeout, 改为3次失败即重启
V2.3 2026-07-03 Auth timeout 不计入重启限额, 修复冷却期阻断连接
V2.2 2026-07-03 Auth timeout 死循环打印修复 → goto do_restart
V2.1 2026-07-03 TCP Server 自动重启机制 (3条件 + 3次限额 + 10min冷却)
V2.0 2026-07-01 passtime_ms5 → passtime_ms 字段改名
V1.5 2026-06-30 0xC0 传感器上报 + 时间量字段完善 + 协议文档
V1.4 2026-06-30 USART2 Loop MCU 0x7F 协议实现
V1.3 2026-06-30 simple_json 6 bug 修复
V1.2 2026-06-30 WCHNET buffer 分离 + SocketSend listen→data 修正
V1.1 2026-06-30 NET_SSC_ENABLE 隔离 + #if guard 共享函数修复
V1.0 2026-06-26 TCP JSON 协议框架 (鉴权 + 15条命令)