diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h index 999abdc..387805a 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/iot_mqtt_srv.h @@ -52,6 +52,8 @@ void iot_mqtt_poll(void); // 主循环轮询 (状态机 + 心跳 + void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理 void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04) +void iot_evt_report_ota_error(uint32_t err); // OTA 刷写失败告警入队 (V1.08, 走 event_report 闭环) +uint8_t iot_any_car(void); // 任一通道有车 (ota_flash 安全窗口检查) void iot_watchdog_init(void); // 初始化硬件 IWDG (无条件, 主循环卡死兜底) void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用) diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h index 288aca7..d266bed 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h @@ -116,6 +116,8 @@ enum { OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */ OFFLOG_EVT_COIL = 0x40, /* 线圈事件; payload[0]=sub(1=进 2=出 3=断开 4=恢复), payload[1]=ch, payload[2..5]=value(50ms) */ OFFLOG_EVT_TIME_ANCHOR = 0x50, /* 时钟同步锚点; unix_ts=同步值 */ + OFFLOG_EVT_OTA_START = 0x60, /* 固件升级开始 (V1.08); payload[0]=slot, payload[1]=target, payload[2..5]=size BE */ + OFFLOG_EVT_OTA_RESULT = 0x61, /* 固件升级结果 (V1.08); payload[0..3]=result code BE (0=成功, 非0=OTA_ERR_*) */ OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */ }; @@ -150,6 +152,8 @@ void offlog_evt_retry(uint32_t msg_id, uint8_t retry); void offlog_evt_giveup(uint32_t msg_id); void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value); void offlog_time_anchor(uint32_t unix_ts); +void offlog_ota_start(uint8_t slot, uint32_t size); /* 固件升级开始 (V1.08, 会话静默前写入) */ +void offlog_ota_result(uint32_t code); /* 固件升级结果 (V1.08, 恢复后补记) */ uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */ uint16_t offlog_count(void); /* 有效记录数 */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/ota_srv.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/ota_srv.h new file mode 100644 index 0000000..3fb70c7 --- /dev/null +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/ota_srv.h @@ -0,0 +1,144 @@ +/** + ****************************************************************************** + * @file ota_srv.h + * @author wangfq + * @version V1.0 + * @date 2026-08-20 + * @brief DLD960 Loop MCU 远程 OTA 服务 (MQTT 协议 V1.08, ROADMAP P1.4 ①) + * + * 先存后刷: MQTT 分片下载 → W25Qxx 暂存区 (0x010000 起 512KB) → + * 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU + * + * 协议依据: docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5 + * 设计稿: docs/DLD960_MQTT_OTA协议.md V1.01 + ****************************************************************************** + */ +#ifndef __OTA_SRV_H__ +#define __OTA_SRV_H__ + +#include + +/*=========================================================================== + * 分区常量 (与 offlog.h: OFFLOG_PARAM_SIZE=64KB + OFFLOG_OTA_SIZE=512KB) + * OTA 区 = 0x010000 ~ 0x090000 (512KB) + * 布局: 元数据扇区(4KB) + Slot A(100KB) + Slot B(100KB) + 预留(308KB) + *===========================================================================*/ +#define OTA_AREA_BASE 0x10000UL /* 0x010000 (参数区 64KB 后) */ +#define OTA_AREA_SIZE 0x80000UL /* 512KB */ + +#define OTA_META_BASE (OTA_AREA_BASE + 0x0000UL) /* 元数据主备份 */ +#define OTA_META_BACKUP (OTA_AREA_BASE + 0x0200UL) /* 元数据备份 (同扇区) */ +#define OTA_META_SECTOR (OTA_AREA_BASE / 4096UL) /* 元数据所在扇区号 */ + +#define OTA_SLOT_A_BASE (OTA_AREA_BASE + 0x1000UL) /* 0x011000 */ +#define OTA_SLOT_B_BASE (OTA_AREA_BASE + 0x1A000UL) /* 0x02A000 */ +#define OTA_SLOT_SIZE (100UL * 1024UL) /* 100KB */ +#define OTA_MAX_IMAGE (96UL * 1024UL) /* 镜像上限 96KB (留 4KB 余量) */ + +/* 下载单片 (协议常量, 256B 页对齐) */ +#define OTA_CHUNK_SIZE 256 + +/* 刷写块 (0x9F LEN 为 uint8 → DATA ≤ 254B; 取 248B) */ +#define OTA_FLASH_BLOCK 248 + +/*=========================================================================== + * 会话状态 + *===========================================================================*/ +enum { + OTA_STATE_IDLE = 0, /* 空闲 */ + OTA_STATE_DOWNLOADING, /* 下载中 */ + OTA_STATE_READY, /* 校验通过, 可刷写 */ + OTA_STATE_FLASHING, /* 刷写中 */ + OTA_STATE_FLASH_FAILED, /* 刷写失败 (保留镜像可重试) */ + OTA_STATE_ABORTED, /* 已中止 */ +}; + +/* 刷写子状态 (非阻塞 tick 驱动) */ +enum { + OTA_FLASH_IDLE = 0, + OTA_FLASH_START, /* 发 A5 启动帧 */ + OTA_FLASH_WAIT_READY, /* 等 pre_ok */ + OTA_FLASH_ADDR, /* 发 A6 地址帧 */ + OTA_FLASH_WAIT_ADDR, /* 等 addr_ok */ + OTA_FLASH_DATA, /* 发 A7 数据块 */ + OTA_FLASH_WAIT_ACK, /* 等 data ACK */ +}; + +/* OTA 错误码 (event_report type=ota_error 的 value, 协议 §5.3) */ +#define OTA_ERR_READY_TIMEOUT 0x1001 /* 启动帧无响应 (pre_ok 超时) */ +#define OTA_ERR_ADDR_FAIL 0x1002 /* 地址帧错误 (addr_err / 超时) */ +#define OTA_ERR_DATA_ACK_LIMIT 0x1003 /* 数据块 ACK 超限 (重试×3 耗尽) */ +#define OTA_ERR_CRC_FAIL 0x1004 /* 全镜像校验失败 */ +#define OTA_ERR_FORCE_FAIL 0x1005 /* 安全窗口拒绝后强制失败 */ + +/*=========================================================================== + * 元数据结构 (56B, 写 64B 槽) + *===========================================================================*/ +typedef struct { + uint32_t magic; /* OTA_MAGIC 'DLD9' */ + uint32_t state; /* OTA_STATE_* */ + uint8_t target; /* 0=loop (1=dbn 预留) */ + uint8_t slot; /* 0=A, 1=B */ + uint16_t rsv; + uint32_t size; /* 镜像字节数 */ + uint32_t crc32; /* 全镜像 CRC32 */ + uint32_t received; /* 已下载字节数 (片对齐, 断点续传依据) */ + uint32_t last_result; /* 上次刷写结果: 0=无/成功, 非0=错误码 */ + uint32_t begin_ts; /* 会话开始 (同步后 Unix 秒, 0=未同步) */ + char version[16]; /* 目标固件版本 (审计) */ + uint32_t flash_cnt; /* 刷写尝试次数 */ + uint32_t rsv2; +} OtaMeta; /* 56B */ + +#define OTA_MAGIC 0x39444C44UL /* 'DLD9' */ + +/*=========================================================================== + * API + *===========================================================================*/ + +/* 初始化: 读元数据 (双备份校验), 刷写状态机复位 */ +void ota_init(void); + +/* 主循环轮询: 刷写状态机 (非阻塞 tick 驱动, 绝不阻塞主循环) */ +void ota_poll(void); + +/* OTA 会话期间静默标志: downloading/flashing 时置位 + → event_report 暂停发送 (队列积压) + offlog/快照落盘暂停 */ +uint8_t ota_silent_active(void); + +/* 当前会话元数据 (只读, MQTT 响应组装用) */ +const OtaMeta *ota_meta(void); + +/* 状态字符串 (JSON state 字段) */ +const char *ota_state_str(uint8_t state); + +/*=========================================================================== + * MQTT 命令入口 (iot_mqtt_srv.c 调用, 主循环上下文) + * 返回 0=成功, 非0=错误码 (1=参数 3=忙 5=内部; data.err_code 用 OTA_ERR_*) + *===========================================================================*/ +int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force); +int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex); +int ota_cmd_end(uint32_t crc32); +int ota_cmd_abort(void); +int ota_cmd_flash(uint8_t slot, uint8_t force); + +/* 分片落盘用: hex → bin (n 字节), 成功 0 */ +int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n); +/* CRC-32/ISO-HDLC (逐位实现, 零查表 RAM) */ +uint32_t ota_crc32(const uint8_t *data, uint32_t len); + +/*=========================================================================== + * 本地刷写与 UART2 联动 (usart_biz.c 使用) + *===========================================================================*/ +/* 1 = 本地刷写进行中: uart_srv OTA 分支把 0x9F ACK 交给 ota_flash_feed_ack, + 而非透传回 BLE (extern 定义于 ota_srv.c) */ +extern uint8_t g_ota_flash_active; + +/* usart_srv 收到 bootloader 0x9F ACK 帧时调用 (主循环上下文) */ +void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len); + +/* 刷写进度 (已送字节 / 总量, ota_report 用) */ +uint32_t ota_flash_sent(void); +uint32_t ota_flash_total(void); + +#endif /* __OTA_SRV_H__ */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index 3265c3e..84b9d86 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -26,6 +26,7 @@ #include "offlog.h" #include "snapshot.h" #include "fault_diag.h" +#include "ota_srv.h" #include "ch32v20x_iwdg.h" #include #include @@ -73,7 +74,8 @@ static uint32_t _last_publish_ms; // 上次上报时刻 (ms) #define IOT_EVT_ACK_TIMEOUT_MS 5000 // 确认超时 (协议 V1.04) #define IOT_EVT_MAX_RETRY 3 // 最大重发次数 (含首发共4次) -enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE }; +enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE, + IOT_EVT_OTA_ERROR }; /* V1.08: OTA 刷写失败告警 (value=OTA_ERR_*) */ typedef struct { uint8_t type; // IOT_EVT_xxx @@ -112,8 +114,10 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) { IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH]; e->type = type; e->ch = ch; e->value = value; _evt_count++; - /* 事件日志: 线圈事件 (主循环上下文) */ - offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */ + /* 事件日志: 线圈事件 (主循环上下文); OTA 会话期间暂停落盘 (V1.08) */ + if (!ota_silent_active()) { + offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */ + } } _evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报 PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count); @@ -170,7 +174,9 @@ static void iot_sensor_ingest(const LUP_SensorReport *sr) { iot_evt_feed(sr); // 事件沿检测 memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照 _cached_sr_valid = 1; - snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */ + if (!ota_silent_active()) { + snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘; OTA 会话暂停 (V1.08) */ + } FAULT_MARKER(MK_INGEST_OUT); } @@ -187,12 +193,18 @@ static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) { /* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */ static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) { - static const char *evt_names[] = {"car_enter", "car_leave", "loop_cut", "loop_restore"}; + static const char *evt_names[] = {"car_enter", "car_leave", "loop_cut", "loop_restore", "ota_error"}; static char payload[512]; // static: 避免 2KB 栈溢出 int w, rem = sizeof(payload); char *p = payload; uint8_t i; + /* OTA 会话期间静默 (V1.08): 暂停发送, 事件保留队列, 结束后补发 */ + if (ota_silent_active()) { + PRINT("EVT: silent (OTA), skip publish\n"); + return; + } + w = snprintf(p, rem, "{\"msg_id\":%lu,\"cmd\":\"event_report\",\"ts\":%lu," "\"data\":{\"events\":[", id, ts); if (w < 0 || w >= rem) return; @@ -201,7 +213,7 @@ static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) { for (i = 0; i < n; i++) { const IotEvent *e = &_evt_queue[(_evt_head + i) % IOT_EVT_QUEUE_DEPTH]; w = snprintf(p, rem, "%s{\"type\":\"%s\",\"ch\":%d,\"value\":%lu}", - (i > 0) ? "," : "", evt_names[e->type & 0x03], e->ch, e->value); + (i > 0) ? "," : "", evt_names[(e->type < 5) ? e->type : 0], e->ch, e->value); if (w < 0 || w >= rem) return; p += w; rem -= w; } @@ -230,6 +242,25 @@ void iot_evt_handle_ack(uint32_t msg_id, int code) { _evt_retry = 0; } +/* OTA 刷写失败告警入队 (V1.08): 走 event_report ACK 闭环, 平台必答 + (ota_srv.c ota_flash_fail 调用; OTA_ERR_* 码见 ota_srv.h) */ +void iot_evt_report_ota_error(uint32_t err) +{ + iot_evt_enqueue(IOT_EVT_OTA_ERROR, 0, err); + PRINT("EVT: ota_error enqueue err=0x%lX\n", (unsigned long)err); +} + +/* 任一通道有车 (ota_flash 安全窗口检查用, 基于最新 0xC0 缓存) */ +uint8_t iot_any_car(void) +{ + uint8_t i; + if (!_cached_sr_valid) return 0; + for (i = 0; i < _cached_sr.coil_count && i < LUP_COIL_COUNT; i++) { + if (_cached_sr.coils[i].car_state) return 1; + } + return 0; +} + /* 事件发送状态机: 主循环每轮调用 (置于 READY 检查之前, 以感知重连沿) */ static void iot_evt_process(void) { static uint8_t _was_ready = 0; @@ -677,6 +708,182 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_ msg_id, dev_time_now()); iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); + } else if (strcmp(cmd_str, "ota_begin") == 0) { + /* V1.08: 开启 OTA 会话 / 断点续传定位 (Loop MCU 远程 OTA, 先存后刷) */ + char resp[400]; + uint32_t size = 0, crc32 = 0; + char version[16] = {0}; + uint8_t force = 0; + int rc; + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"size\"", tmp); + if (strlen(tmp) > 0) size = (uint32_t)strtoul(tmp, NULL, 10); + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"crc32\"", tmp); + if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10); + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"version\"", tmp); + strncpy(version, tmp, sizeof(version) - 1); + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"force\"", tmp); + if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0); + rc = ota_cmd_begin(size, crc32, version, force); + if (rc == 0) { + const OtaMeta *m = ota_meta(); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":0,\"msg\":\"success\"," + "\"data\":{\"target\":\"loop\",\"slot\":\"%s\",\"offset\":%lu,\"received\":%lu," + "\"size\":%lu,\"crc32\":%lu,\"state\":\"%s\"}}", + msg_id, dev_time_now(), + (m->slot == 0) ? "a" : "b", + (unsigned long)m->received, (unsigned long)m->received, + (unsigned long)m->size, (unsigned long)m->crc32, + ota_state_str((uint8_t)m->state)); + } else { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\"," + "\"data\":{\"target\":\"loop\",\"err_code\":%d}}", + msg_id, dev_time_now(), (rc == 3) ? 2 : 4); + } + iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); + PRINT("IOT: ota_begin size=%lu rc=%d\n", (unsigned long)size, rc); + + } else if (strcmp(cmd_str, "ota_data") == 0) { + /* V1.08: 分片下发 (256B/片, 单片 CRC32; 重复片幂等, 乱序拒收指示续传) */ + char resp[400]; + char hexbuf[512 + 1]; + const char *hp; + uint32_t offset = 0, crc32 = 0; + int rc; + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"offset\"", tmp); + if (strlen(tmp) > 0) offset = (uint32_t)strtoul(tmp, NULL, 10); + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"crc32\"", tmp); + if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10); + /* hex 值提取: data 对象内字段是 "data":"...", 顶层 data 对象是 "data":{ — 前者唯一 */ + hp = strstr(json, "\"data\":\""); + memset(hexbuf, 0, sizeof(hexbuf)); + if (hp) { + hp += 8; /* 跳过 "data":" */ + strncpy(hexbuf, hp, sizeof(hexbuf) - 1); + { char *q = strchr(hexbuf, '"'); if (q) *q = '\0'; } + } + rc = ota_cmd_data(offset, crc32, hexbuf); + if (rc == 0) { + const OtaMeta *m = ota_meta(); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":0,\"msg\":\"success\"," + "\"data\":{\"offset\":%lu,\"received\":%lu}}", + msg_id, dev_time_now(), (unsigned long)offset, (unsigned long)m->received); + } else if (rc == 1) { + const OtaMeta *m = ota_meta(); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":1,\"msg\":\"crc/gap error\"," + "\"data\":{\"err_code\":1,\"offset\":%lu,\"received\":%lu}}", + msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received); + } else { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\"," + "\"data\":{\"err_code\":2}}", + msg_id, dev_time_now()); + } + iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); + PRINT("IOT: ota_data offset=%lu rc=%d\n", (unsigned long)offset, rc); + + } else if (strcmp(cmd_str, "ota_end") == 0) { + /* V1.08: 结束下载, 全镜像 CRC32 复核 */ + char resp[300]; + uint32_t crc32 = 0; + int rc; + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"crc32\"", tmp); + if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10); + rc = ota_cmd_end(crc32); + if (rc == 0) { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":0,\"msg\":\"success\"," + "\"data\":{\"crc_ok\":true}}", + msg_id, dev_time_now()); + } else if (rc == 5) { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":5,\"msg\":\"crc mismatch\"," + "\"data\":{\"err_code\":5,\"crc_ok\":false}}", + msg_id, dev_time_now()); + } else if (rc == 3) { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\"," + "\"data\":{\"err_code\":2}}", + msg_id, dev_time_now()); + } else { + const OtaMeta *m = ota_meta(); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":1,\"msg\":\"incomplete\"," + "\"data\":{\"offset\":%lu,\"received\":%lu}}", + msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received); + } + iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); + PRINT("IOT: ota_end rc=%d\n", rc); + + } else if (strcmp(cmd_str, "ota_abort") == 0) { + /* V1.08: 中止会话, 释放暂存 */ + char resp[256]; + int rc = ota_cmd_abort(); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_abort\",\"ts\":%lu,\"code\":%d,\"msg\":\"%s\"}", + msg_id, dev_time_now(), (rc == 0) ? 0 : 1, (rc == 0) ? "success" : "internal error"); + iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); + PRINT("IOT: ota_abort rc=%d\n", rc); + + } else if (strcmp(cmd_str, "ota_flash") == 0) { + /* V1.08: 触发本地 ISP 刷写 (同步安全检查 → 异步执行) */ + char resp[300]; + char slot_buf[8] = {0}; + uint8_t slot = 0, force = 0; + int rc; + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"slot\"", tmp); + strncpy(slot_buf, tmp, sizeof(slot_buf) - 1); + if (strcmp(slot_buf, "b") == 0) slot = 1; + memset(tmp, 0, sizeof(tmp)); + simple_parse_json(json, "\"force\"", tmp); + if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0); + rc = ota_cmd_flash(slot, force); + if (rc == 0) { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":0,\"msg\":\"success, flashing async\"}", + msg_id, dev_time_now()); + } else if (rc == 3) { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\"," + "\"data\":{\"err_code\":3}}", + msg_id, dev_time_now()); + } else { + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\"}", + msg_id, dev_time_now()); + } + iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); + PRINT("IOT: ota_flash slot=%d force=%d rc=%d\n", slot, force, rc); + + } else if (strcmp(cmd_str, "ota_status") == 0) { + /* V1.08: 查询 OTA 状态 (含刷写进度) */ + char resp[400]; + const OtaMeta *m = ota_meta(); + snprintf(resp, sizeof(resp), + "{\"msg_id\":%lu,\"cmd\":\"ota_status\",\"ts\":%lu,\"code\":0,\"msg\":\"success\"," + "\"data\":{\"target\":\"loop\",\"state\":\"%s\",\"slot\":\"%s\",\"size\":%lu," + "\"received\":%lu,\"crc32\":%lu,\"version\":\"%s\"," + "\"progress\":{\"sent\":%lu,\"total\":%lu},\"last_result\":%lu,\"last_error\":%lu}}", + msg_id, dev_time_now(), + ota_state_str((uint8_t)m->state), (m->slot == 0) ? "a" : "b", + (unsigned long)m->size, (unsigned long)m->received, (unsigned long)m->crc32, + m->version, + (unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(), + (unsigned long)m->last_result, (unsigned long)m->last_result); + iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); + PRINT("IOT: ota_status state=%s\n", ota_state_str((uint8_t)m->state)); + } else if (strcmp(cmd_str, "ssc_net_query") == 0) { /* SSC 网络配置查询 (只读全局, 协议 V1.02) */ char resp[400]; diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c index dd509f8..48cd2c0 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c @@ -342,6 +342,28 @@ void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value) offlog_evt(OFFLOG_EVT_COIL, p, 6); } +void offlog_ota_start(uint8_t slot, uint32_t size) +{ + uint8_t p[6]; + p[0] = slot; + p[1] = 0; /* target: 0=loop */ + p[2] = (uint8_t)(size >> 24); + p[3] = (uint8_t)(size >> 16); + p[4] = (uint8_t)(size >> 8); + p[5] = (uint8_t)size; + offlog_evt(OFFLOG_EVT_OTA_START, p, 6); +} + +void offlog_ota_result(uint32_t code) +{ + uint8_t p[4]; + p[0] = (uint8_t)(code >> 24); + p[1] = (uint8_t)(code >> 16); + p[2] = (uint8_t)(code >> 8); + p[3] = (uint8_t)code; + offlog_evt(OFFLOG_EVT_OTA_RESULT, p, 4); +} + void offlog_time_anchor(uint32_t unix_ts) { offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */ @@ -398,6 +420,8 @@ const char *offlog_type_str(uint8_t type) case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup"; case OFFLOG_EVT_COIL: return "coil"; case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor"; + case OFFLOG_EVT_OTA_START: return "ota_start"; + case OFFLOG_EVT_OTA_RESULT: return "ota_result"; case OFFLOG_EVT_LOG_CLEAR: return "log_clear"; default: return "unknown"; } diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c new file mode 100644 index 0000000..e01bfed --- /dev/null +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/ota_srv.c @@ -0,0 +1,586 @@ +/** + ****************************************************************************** + * @file ota_srv.c + * @author wangfq + * @version V1.0 + * @date 2026-08-20 + * @brief DLD960 Loop MCU 远程 OTA 服务 (MQTT 协议 V1.08, ROADMAP P1.4 ①) + * + * 先存后刷: + * 阶段1 下载: MQTT 分片 (256B/片, 单片+全镜像 CRC32) → W25Qxx 暂存区 + * 阶段2 校验: ota_end 全镜像 CRC32 复核 → state=ready + * 阶段3 刷写: ota_flash → 本地 0x9F ISP 透传 (A5/A6/A7 停等协议), + * 非阻塞 tick 驱动挂 ota_poll(), 刷写窗口内 MQTT 保活不受影响 + * + * 会话静默: downloading/flashing 期间 event_report 暂停发送 (队列积压, + * 结束后补发) + offlog/快照落盘暂停 ("升级开始"日志暂停前写, + * "升级结果"恢复后补记) + * + * 协议依据: docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5 + * 设计稿: docs/DLD960_MQTT_OTA协议.md V1.01 + ****************************************************************************** + */ +#include "CONFIG.h" +#include "cmcng.h" +#include "ota_srv.h" +#include "offlog.h" +#include "storage.h" +#include "loop_uart_proto.h" +#include + +/*=========================================================================== + * 本地状态 + *===========================================================================*/ +static OtaMeta _meta; /* 当前会话元数据 (RAM 副本) */ +static uint32_t _slot_base; /* 当前 Slot 数据区基址 */ +static uint32_t _meta_watermark; /* 上次落盘元数据时的 received (节流) */ +static uint8_t _init_done; /* ota_init 完成 */ + +/* 刷写状态机 (非阻塞 tick 驱动) */ +static uint8_t _fs; /* OTA_FLASH_* */ +static uint8_t _fs_retry; /* 当前步重试计数 */ +static uint32_t _fs_start_ms; /* 当前步起始时刻 (超时判断) */ +static uint16_t _fs_sub; /* 当前块序号 (首=总块数, 末=1) */ +static uint32_t _fs_sent; /* 已送 Loop 字节数 */ +static uint32_t _fs_block_crc; /* 当前块 CRC (调试/审计) */ + +uint8_t g_ota_flash_active = 0; /* 本地刷写进行中 → uart_srv 把 ACK 交给 ota_srv */ + +/* 块缓冲 (static: 栈紧张教训) */ +static uint8_t _chunk_buf[OTA_CHUNK_SIZE]; /* 下载片缓冲 */ +static uint8_t _fs_frame[5 + OTA_FLASH_BLOCK + 1]; /* A7 帧: 9F+SubL+SubH+LEN+CMD+DATA+CHECK */ + +/*=========================================================================== + * CRC-32/ISO-HDLC (协议 §4.19.1; 逐位实现, 零查表 RAM) + * poly=0x04C11DB7 (reflected 0xEDB88320), init=0xFFFFFFFF, xorout=0xFFFFFFFF + * 校验向量: crc32("123456789") = 0xCBF43926 + *===========================================================================*/ +/* 增量更新: 调用方初始 crc=0xFFFFFFFF, 分块续算, 最后 ^0xFFFFFFFF + (全镜像 96KB 校验分块读, 不能一次入 RAM) */ +static uint32_t ota_crc32_update(uint32_t crc, const uint8_t *data, uint32_t len) +{ + uint32_t i; + uint8_t b; + for (i = 0; i < len; i++) { + crc ^= data[i]; + for (b = 0; b < 8; b++) { + crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u))); + } + } + return crc; +} + +uint32_t ota_crc32(const uint8_t *data, uint32_t len) +{ + return ota_crc32_update(0xFFFFFFFFu, data, len) ^ 0xFFFFFFFFu; +} + +/*=========================================================================== + * hex 工具 + *===========================================================================*/ +static uint8_t ota_hex_val(char c) +{ + if (c >= '0' && c <= '9') return (uint8_t)(c - '0'); + if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10); + if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10); + return 0xFF; +} + +int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n) +{ + uint32_t i; + for (i = 0; i < n; i++) { + uint8_t hi = ota_hex_val(hex[i * 2]); + uint8_t lo = ota_hex_val(hex[i * 2 + 1]); + if (hi == 0xFF || lo == 0xFF) return -1; + out[i] = (uint8_t)((hi << 4) | lo); + } + return 0; +} + +/*=========================================================================== + * 元数据读写 (双备份, 同扇区 0x010000~0x0103FF) + * 写: 显式擦扇区 (45ms) + NoCheck 写主 + 写备份 + * 读: 主 magic 有效优先, 否则读备份; 双无效 → 默认 IDLE + *===========================================================================*/ +static void ota_meta_write(const OtaMeta *m) +{ + uint8_t buf[64]; + memset(buf, 0xFF, sizeof(buf)); + memcpy(buf, m, sizeof(OtaMeta)); + + SPI_Flash_Erase_Sector((uint32_t)OTA_META_SECTOR); + SPI_Flash_Write_NoCheck(buf, OTA_META_BASE, 64); + SPI_Flash_Write_NoCheck(buf, OTA_META_BACKUP, 64); + PRINT("OTA: meta write state=%lu received=%lu\n", + (unsigned long)m->state, (unsigned long)m->received); +} + +static int ota_meta_read(OtaMeta *m) +{ + uint8_t buf[64]; + memset(buf, 0xFF, sizeof(buf)); + SPI_Flash_Read(buf, OTA_META_BASE, 64); + memcpy(m, buf, sizeof(OtaMeta)); + if (m->magic == OTA_MAGIC) return 0; + memset(buf, 0xFF, sizeof(buf)); + SPI_Flash_Read(buf, OTA_META_BACKUP, 64); + memcpy(m, buf, sizeof(OtaMeta)); + if (m->magic == OTA_MAGIC) return 0; + return -1; +} + +/* 节流落盘: 每累计 4KB 或关键节点写元数据 (擦 45ms 不能每片做) */ +static void ota_meta_flush(void) +{ + if (_meta.received - _meta_watermark >= 4096u) { + ota_meta_write(&_meta); + _meta_watermark = _meta.received; + } +} + +/*=========================================================================== + * 状态工具 + *===========================================================================*/ +const char *ota_state_str(uint8_t state) +{ + static const char *names[] = { + "idle", "downloading", "ready", "flashing", "flash_failed", "aborted" + }; + return (state < 6) ? names[state] : "idle"; +} + +uint8_t ota_silent_active(void) +{ + return (_meta.state == OTA_STATE_DOWNLOADING || _meta.state == OTA_STATE_FLASHING); +} + +const OtaMeta *ota_meta(void) +{ + return &_meta; +} + +/*=========================================================================== + * 初始化 + *===========================================================================*/ +void ota_init(void) +{ + OtaMeta m; + memset(&m, 0, sizeof(m)); + if (ota_meta_read(&m) == 0) { + /* 恢复上次会话状态 (掉电/复位后) */ + _meta = m; + if (_meta.state == OTA_STATE_DOWNLOADING) { + /* 下载中断: 保持 downloading, 平台 ota_begin 时续传 */ + PRINT("OTA: resume downloading received=%lu\n", (unsigned long)_meta.received); + } else if (_meta.state == OTA_STATE_FLASHING) { + /* 刷写中断 (掉电): 置失败态, 保留镜像可重试 */ + _meta.state = OTA_STATE_FLASH_FAILED; + _meta.last_result = OTA_ERR_READY_TIMEOUT; + PRINT("OTA: flash interrupted on boot, mark failed\n"); + ota_meta_write(&_meta); + } + /* ready/aborted/flash_failed/idle: 原样保留 */ + } else { + memset(&_meta, 0, sizeof(_meta)); + _meta.magic = OTA_MAGIC; + _meta.state = OTA_STATE_IDLE; + _meta.slot = 0; + PRINT("OTA: fresh meta (idle)\n"); + } + _slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE; + _meta_watermark = _meta.received; + _fs = OTA_FLASH_IDLE; + _init_done = 1; + PRINT("OTA: init done state=%s slot=%d received=%lu\n", + ota_state_str(_meta.state), _meta.slot, (unsigned long)_meta.received); +} + +/*=========================================================================== + * 会话命令 — ota_begin (开启会话 / 断点续传定位) + * 返回: 0=成功 (data.offset = _meta.received 或 size), 非0=错误 + *===========================================================================*/ +int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force) +{ + (void)force; /* 版本冲突判断暂不校验 (bootloader 不校验版本), 预留 */ + if (size == 0 || size > OTA_MAX_IMAGE) { + return 1; /* 参数错误 */ + } + + /* 已有镜像且 size+crc32 一致 → 续传或直接 ready */ + if (_meta.magic == OTA_MAGIC && _meta.size == size && _meta.crc32 == crc32) { + _slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE; + if (_meta.state == OTA_STATE_READY) { + /* 镜像已就绪, 平台可直接 ota_flash */ + return 0; /* data.offset = size */ + } + if (_meta.state == OTA_STATE_DOWNLOADING) { + _meta.state = OTA_STATE_DOWNLOADING; + PRINT("OTA: begin resume received=%lu\n", (unsigned long)_meta.received); + return 0; /* data.offset = received */ + } + /* flash_failed / aborted: 重新走下载 */ + } + + /* 新会话: 始终用 Slot A (B 保留上一版镜像做回滚) */ + _slot_base = OTA_SLOT_A_BASE; + memset(&_meta, 0, sizeof(_meta)); + _meta.magic = OTA_MAGIC; + _meta.state = OTA_STATE_DOWNLOADING; + _meta.target = 0; + _meta.slot = 0; + _meta.size = size; + _meta.crc32 = crc32; + _meta.received = 0; + _meta.last_result = 0; + _meta.begin_ts = 0; + memset(_meta.version, 0, sizeof(_meta.version)); + if (version) strncpy(_meta.version, version, sizeof(_meta.version) - 1); + _meta.flash_cnt = 0; + _meta_watermark = 0; + /* 先擦目标 Slot 数据区 (100KB = 25 扇区 ~1.1s, 主循环上下文可接受 < IWDG 4s) */ + { + uint32_t sec; + for (sec = 0; sec < OTA_SLOT_SIZE / 4096UL; sec++) { + SPI_Flash_Erase_Sector((uint32_t)(_slot_base / 4096UL) + sec); + } + } + ota_meta_write(&_meta); + PRINT("OTA: begin new session size=%lu crc32=0x%08lX slot=%d\n", + (unsigned long)size, (unsigned long)crc32, _meta.slot); + return 0; +} + +/*=========================================================================== + * 会话命令 — ota_data (分片下发, 256B/片) + * 返回: 0=成功 (幂等), 1=参数/CRC/乱序 (err_code 由调用方区分), 3=会话状态错 + *===========================================================================*/ +int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex) +{ + uint32_t clen; + + if (_meta.state != OTA_STATE_DOWNLOADING) { + return 3; /* 会话状态不允许 (err_code=2 由调用方映射) */ + } + if (offset % OTA_CHUNK_SIZE != 0 || offset >= _meta.size) { + return 1; /* 参数错误 */ + } + if (offset < _meta.received) { + return 0; /* 重复片: 幂等 */ + } + if (offset > _meta.received) { + return 1; /* 乱序缺片: 调用方用 data.offset=received 指示续传 */ + } + + clen = _meta.size - offset; + if (clen > OTA_CHUNK_SIZE) clen = OTA_CHUNK_SIZE; + + if (ota_hex_decode(hex, _chunk_buf, clen) != 0) { + return 1; /* hex 格式错误 */ + } + if (ota_crc32(_chunk_buf, clen) != crc32) { + return 1; /* 单片 CRC 失败 */ + } + + /* 落盘: SPI_Flash_Write 自带"目标区非 0xFF 则读-改-写擦扇区" (256B 页对齐) */ + SPI_Flash_Write(_chunk_buf, _slot_base + offset, (uint16_t)clen); + _meta.received = offset + clen; + ota_meta_flush(); + PRINT("OTA: data@%lu crc_ok received=%lu\n", + (unsigned long)offset, (unsigned long)_meta.received); + return 0; +} + +/*=========================================================================== + * 会话命令 — ota_end (全镜像 CRC32 复核) + *===========================================================================*/ +int ota_cmd_end(uint32_t crc32) +{ + if (_meta.state != OTA_STATE_DOWNLOADING) { + return 3; + } + if (_meta.received != _meta.size) { + return 1; /* 不完整: data.offset=received 续传 */ + } + if (crc32 != _meta.crc32) { + return 1; /* 声明值不一致 */ + } + + /* 读回暂存区计算全镜像 CRC32 (分块增量, 96KB ≈ 40ms @120MHz, 主循环上下文) */ + { + uint32_t total = 0xFFFFFFFFu; + uint32_t i = 0; + while (i < _meta.size) { + uint32_t n = _meta.size - i; + if (n > OTA_CHUNK_SIZE) n = OTA_CHUNK_SIZE; + SPI_Flash_Read(_chunk_buf, _slot_base + i, (uint16_t)n); + total = ota_crc32_update(total, _chunk_buf, n); + i += n; + } + total ^= 0xFFFFFFFFu; + if (total != _meta.crc32) { + _meta.state = OTA_STATE_DOWNLOADING; /* 保留已下载数据, 可重发错片 */ + PRINT("OTA: end CRC mismatch 0x%08lX != 0x%08lX\n", + (unsigned long)total, (unsigned long)_meta.crc32); + return 5; /* 内部错误 + data.crc_ok=false */ + } + } + + _meta.state = OTA_STATE_READY; + _meta.last_result = 0; + ota_meta_write(&_meta); + PRINT("OTA: end OK crc_ok → ready (slot %d)\n", _meta.slot); + return 0; +} + +/*=========================================================================== + * 会话命令 — ota_abort + *===========================================================================*/ +int ota_cmd_abort(void) +{ + if (_meta.state == OTA_STATE_IDLE) { + return 0; /* 无会话: 幂等 */ + } + _meta.state = OTA_STATE_ABORTED; + ota_meta_write(&_meta); + PRINT("OTA: abort → aborted\n"); + return 0; +} + +/*=========================================================================== + * 会话命令 — ota_flash (触发本地 ISP 刷写, 同步检查 + 异步执行) + * 安全窗口: 4 通道任一有车 → 拒绝 (err_code=3); force 跳过 + *===========================================================================*/ +int ota_cmd_flash(uint8_t slot, uint8_t force) +{ + uint8_t any_car; + extern uint8_t iot_any_car(void); + + if (_meta.state != OTA_STATE_READY) { + return 3; /* 非 ready: 先 ota_end */ + } + if (slot != 0 && slot != 1) { + return 1; + } + if (slot != _meta.slot) { + /* 指定了非当前 ready 槽: 读该槽元数据? 简化: 仅允许当前槽 */ + return 1; + } + /* 与 BLE OTA 透传互斥: g_flag_counter_ota.flag=1 且非本地刷写 → 拒绝 */ + if (g_flag_counter_ota.flag != 0 && !g_ota_flash_active) { + return 3; + } + /* 安全窗口: 有车压线圈拒绝 (force 跳过) */ + any_car = iot_any_car(); + if (!force && any_car) { + PRINT("OTA: flash rejected, car on loop\n"); + return 3; + } + + /* 启动刷写 (异步): offlog 开始事件先写 (会话静默前) */ + offlog_ota_start(_meta.slot, _meta.size); + _meta.state = OTA_STATE_FLASHING; + _meta.flash_cnt++; + _meta.last_result = 0; + ota_meta_write(&_meta); + + _fs = OTA_FLASH_START; + _fs_retry = 0; + _fs_sent = 0; + _fs_sub = 0; + g_ota_flash_active = 1; + g_flag_counter_ota.flag = 1; /* UART2 切 0x9F 解析 */ + g_flag_counter_ota.tick = 0; + PRINT("OTA: flash start slot=%d size=%lu (force=%d)\n", + _meta.slot, (unsigned long)_meta.size, force); + return 0; +} + +/*=========================================================================== + * 刷写状态机 (非阻塞 tick 驱动, ota_poll 每主循环调用) + * + * A5 启动 → pre_ok → A6 地址 → addr_ok → A7 数据块×N → 末块(序号=1) → 完成 + * 每步超时 1s 重试 ×3; 失败 → state=flash_failed + event_report ota_error + *===========================================================================*/ + +/* 0x9F 帧发送: 9F SubL SubH LEN CMD DATA... CHECK(SUM 不含 magic) */ +static void ota_send_9f(uint8_t sub_l, uint8_t sub_h, uint8_t cmd, + const uint8_t *data, uint16_t datalen) +{ + uint8_t frame[5 + OTA_FLASH_BLOCK + 1]; + uint16_t i; + uint8_t sum = 0; + frame[0] = 0x9F; + frame[1] = sub_l; + frame[2] = sub_h; + frame[3] = (uint8_t)(1 + datalen); /* LEN 含 CMD */ + frame[4] = cmd; + if (data && datalen) memcpy(&frame[5], data, datalen); + for (i = 1; i < 5 + datalen; i++) sum += frame[i]; /* SubL..DATA */ + frame[5 + datalen] = sum; + UART2_SendString(frame, (uint16_t)(5 + datalen + 1)); +} + +/* 刷写失败收尾: 状态落盘 + event_report 告警 + 恢复 0x7F 模式 */ +static void ota_flash_fail(uint32_t err) +{ + PRINT("OTA: flash FAILED err=0x%lX\n", (unsigned long)err); + _meta.state = OTA_STATE_FLASH_FAILED; + _meta.last_result = err; + ota_meta_write(&_meta); + offlog_ota_result(err); + /* 恢复 UART2 0x7F 模式 */ + g_ota_flash_active = 0; + g_flag_counter_ota.flag = 0; + lup_frame_reset(); + _fs = OTA_FLASH_IDLE; + /* event_report ota_error 告警 (平台必答, 由 iot_mqtt_srv 发送) */ + extern void iot_evt_report_ota_error(uint32_t err); + iot_evt_report_ota_error(err); +} + +/* 刷写成功收尾: 保留镜像 (state=ready 可重刷), 恢复 0x7F 模式 */ +static void ota_flash_done(void) +{ + PRINT("OTA: flash DONE sent=%lu\n", (unsigned long)_fs_sent); + _meta.state = OTA_STATE_READY; + _meta.last_result = 0; + ota_meta_write(&_meta); + offlog_ota_result(0); + g_ota_flash_active = 0; + g_flag_counter_ota.flag = 0; + lup_frame_reset(); + _fs = OTA_FLASH_IDLE; +} + +/* UART2 收到 bootloader 0x9F ACK 帧 (uart_srv OTA 分支调用, 主循环上下文) + * ACK 帧: 9F SubL SubH 02 CMD Status CHECK + * pre_ok = 9F 01 00 02 A5 00 A8 + * addr_ok = 9F 01 00 02 A6 00 A9 | addr_err = 9F 01 00 02 A6 01 AA + * data ack= 9F SubL SubH 02 A7 00 CK | data err = 9F SubL SubH 02 A7 01 CK */ +void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len) +{ + uint8_t cmd, status; + if (len < 7 || pkg[0] != 0x9F) return; + if (pkg[3] != 0x02) return; /* ACK 帧 LEN=2 */ + cmd = pkg[4]; + status = pkg[5]; + PRINT("OTA: ack cmd=0x%02X status=%d sub=%u\n", cmd, status, pkg[1]); + + switch (_fs) { + case OTA_FLASH_WAIT_READY: + if (cmd == 0xA5 && status == 0x00) { + _fs = OTA_FLASH_ADDR; + _fs_retry = 0; + } + break; + case OTA_FLASH_WAIT_ADDR: + if (cmd == 0xA6 && status == 0x00) { + /* 地址 OK → 进入数据阶段: 首块序号 = 总块数 (末块=1) */ + uint32_t blocks = (_meta.size + OTA_FLASH_BLOCK - 1) / OTA_FLASH_BLOCK; + _fs_sub = (uint16_t)(blocks > 0xFFFF ? 0xFFFF : blocks); + _fs_sent = 0; + _fs = OTA_FLASH_DATA; + _fs_retry = 0; + } else if (cmd == 0xA6 && status != 0x00) { + ota_flash_fail(OTA_ERR_ADDR_FAIL); + } + break; + case OTA_FLASH_WAIT_ACK: + if (cmd == 0xA7) { + if (status == 0x00) { + _fs_sent += OTA_FLASH_BLOCK; + if (_fs_sent >= _meta.size) _fs_sent = _meta.size; + _fs_retry = 0; + if (_fs_sub <= 1) { + /* 末块已 ACK → 完成 (bootloader 清 flag 复位跑新 APP) */ + ota_flash_done(); + } else { + _fs_sub--; /* 序号递减: 首=总块数, 末=1 */ + _fs = OTA_FLASH_DATA; + } + } else { + /* 数据块错误: 重发同块 (序号不变) */ + _fs_retry++; + if (_fs_retry > 3) { + ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT); + } else { + _fs = OTA_FLASH_DATA; + } + } + } + break; + default: + break; + } +} + +uint32_t ota_flash_sent(void) { return _fs_sent; } +uint32_t ota_flash_total(void) { return _meta.size; } + +/* 主循环轮询: 推进刷写状态机 (每轮最多发 1~2 块, 绝不阻塞) */ +void ota_poll(void) +{ + uint32_t now; + + if (!_init_done || _fs == OTA_FLASH_IDLE) return; + + now = mstick(); + if (_fs == OTA_FLASH_START) { + /* 发 A5 启动帧: 9F 01 00 01 A5 A7 → Loop APP 写 flag 复位 → bootloader 回 pre_ok */ + static const uint8_t start_frame[6] = {0x9F, 0x01, 0x00, 0x01, 0xA5, 0xA7}; + UART2_SendString((uint8_t *)start_frame, 6); + _fs = OTA_FLASH_WAIT_READY; + _fs_start_ms = now; + /* 重试计数保持 (首发由 ota_cmd_flash 置 0; 重发不清零, 否则超时重试永不失败) */ + PRINT("OTA: sent A5 start\n"); + return; + } + if (_fs == OTA_FLASH_WAIT_READY) { + if (now - _fs_start_ms > 1000u) { + _fs_retry++; + if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_READY_TIMEOUT); return; } + _fs = OTA_FLASH_START; /* 重发启动帧 */ + } + return; + } + if (_fs == OTA_FLASH_ADDR) { + /* 发 A6 地址帧: 9F 01 00 05 A6 08 00 34 00 SUM (0x08003400 大端) */ + uint8_t addr[4] = {0x08, 0x00, 0x34, 0x00}; /* APP_START_ADDR */ + ota_send_9f(0x01, 0x00, 0xA6, addr, 4); + _fs = OTA_FLASH_WAIT_ADDR; + _fs_start_ms = now; + /* 重试计数保持 (ACK 成功路径在 ota_flash_feed_ack 清 0) */ + return; + } + if (_fs == OTA_FLASH_WAIT_ADDR) { + if (now - _fs_start_ms > 1000u) { + _fs_retry++; + if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_ADDR_FAIL); return; } + _fs = OTA_FLASH_ADDR; /* 重发地址帧 */ + } + return; + } + if (_fs == OTA_FLASH_DATA) { + /* 从 W25Qxx 读一块 → 组 A7 帧 → 发 (停等 ACK) */ + uint32_t read_off = _fs_sent; + uint32_t remain = _meta.size - read_off; + uint16_t datalen = (remain > OTA_FLASH_BLOCK) ? OTA_FLASH_BLOCK : (uint16_t)remain; + SPI_Flash_Read(_fs_frame + 5, _slot_base + read_off, datalen); + ota_send_9f((uint8_t)(_fs_sub & 0xFF), (uint8_t)(_fs_sub >> 8), 0xA7, + _fs_frame + 5, datalen); + _fs = OTA_FLASH_WAIT_ACK; + _fs_start_ms = now; + PRINT("OTA: sent A7 sub=%u off=%lu datalen=%u\n", + _fs_sub, (unsigned long)read_off, datalen); + return; + } + if (_fs == OTA_FLASH_WAIT_ACK) { + if (now - _fs_start_ms > 1000u) { + _fs_retry++; + if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT); return; } + _fs = OTA_FLASH_DATA; /* 重发同块 */ + } + return; + } +} diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c index 3996932..82191ed 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c @@ -27,6 +27,7 @@ #include "tcp_json_srv.h" #include "loop_uart_proto.h" #include "iot_mqtt_srv.h" +#include "ota_srv.h" #include "fault_diag.h" /********************************************************************* @@ -274,6 +275,7 @@ void Main_Circulation(void) FAULT_MARKER(MK_UART_SRV_IN); uart_srv(); FAULT_MARKER(MK_UART_SRV_OUT); + ota_poll(); /* OTA 刷写状态机 (V1.08): 非阻塞 tick 驱动, 先消费 UART2 ACK 再推进 */ snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */ FAULT_MARKER(MK_POLL_BLE_IN); poll_dbn_ble(); @@ -328,6 +330,8 @@ int main(void) PRINT("INIT: storage ok\n"); offlog_init(); PRINT("INIT: offlog ok\n"); + ota_init(); /* OTA 元数据恢复 (V1.08): 下载中断续传 / 刷写中断标记失败 */ + PRINT("INIT: ota ok\n"); /* 快照区: 延后初始化 (2026-08-17) — snap_init 移到主循环 3s 后 (snap_delayed_init), 避开启动早期 SPI 重负载窗口 (擦+整扇区写回 ~80ms); 3s 内的传感帧由 snap_enqueue/snap_flush 的 !_ready 门控自动丢弃不落盘 */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/usart_biz.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/usart_biz.c index 5c5aad3..374cd77 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/usart_biz.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/usart_biz.c @@ -11,6 +11,7 @@ #include "loop_uart_proto.h" #include #include "dbn_ble_srv.h" +#include "ota_srv.h" void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); @@ -291,10 +292,14 @@ void uart_srv(void) } } else{ - /* OTA 模式 (2026-08-19): Loop bootloader 回的 0x9F 帧 - (back_9F_pre_ok / addr_ok / data ACK) 透传回 BLE 工具, + /* OTA 模式: Loop bootloader 回的 0x9F ACK 帧 (pre_ok/addr_ok/data ACK) 分发: + - 本地远程 OTA (g_ota_flash_active, V1.08) → 交给 ota_srv 刷写状态机 + - BLE 透传 (现状, V1.02.03) → 透传回 BLE 工具 否则停等协议 (0xA7 WITH_BACK) 永远等不到 ACK */ - if(g_flag_bt_state){ + if(g_ota_flash_active){ + ota_flash_feed_ack(g_pkg_uart_2.pkg, g_pkg_uart_2.offset); + } + else if(g_flag_bt_state){ g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp); } /* OTA 模式同样清 flag — 残留帧会在 OTA 结束后被误处理 */ diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index 85acbe6..e1341ad 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -4,6 +4,49 @@ > > 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 +## 2026-08-20 — Loop MCU 远程 OTA 固件实现(MQTT V1.08,ROADMAP P1.4 ①) + +### 背景 + +协议设计稿 V1.01 拍板 + 并入 MQTT 协议 V1.08 + DBNMQTTool OTA 页签/协议单测先行。本次固件实现**先存后刷**:MQTT 分片下载 → W25Qxx OTA 暂存区(0x010000 起 512KB)→ 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU(AT32F421)。 + +### 变更 + +| 文件 | 变更 | +|------|------| +| `ota_srv.c/h`(新增) | CRC32(ISO-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 + 单片 CRC32(hex 512 字符 < RECV_BUF 1024) | +| 镜像上限 | 96KB(Slot 100KB - 4KB 边界余量) | +| 刷写块 | 248B(0x9F LEN 为 uint8 → DATA ≤254B;bootloader 2KB 扇区累积写) | +| 分区 | 元数据 4KB + Slot A/B 各 100KB(0x011000/0x02A000)+ 预留 308KB | +| 断点续传 | 元数据 received 节流 4KB flush,掉电丢 ≤4KB(平台重发幂等,协议已定义) | +| 刷写窗口 | 64KB ≈ 265 块 × ~20ms ≈ 6~8s;非阻塞调度保 MQTT 保活/IWDG | + +### 单测(gcc 隔离,`tests/test_ota_srv.c`,mock 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 置 0,ACK 成功由 feed_ack 清 0)。 + +### 待板上验证 + +- UART2 TX 阻塞轮询(248B ≈ 13ms)与主循环时序、擦除 45ms×N 分散阻塞对 MQTT 保活影响 +- Loop 实际 bin 大小(验证 96KB 余量)、bootloader 真实 ACK 时序 +- 会话静默后 event_report 补发行为(队列积压恢复) + +--- + ## 2026-08-19 — 固件版本 V1.02.04(SPI 存储适配两项修复) ### 内容 diff --git a/vd960DBN/tests/ota_mock/CONFIG.h b/vd960DBN/tests/ota_mock/CONFIG.h new file mode 100644 index 0000000..4fba298 --- /dev/null +++ b/vd960DBN/tests/ota_mock/CONFIG.h @@ -0,0 +1,5 @@ +/* mock CONFIG.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */ +#ifndef __MOCK_CONFIG_H__ +#define __MOCK_CONFIG_H__ + +#endif diff --git a/vd960DBN/tests/ota_mock/cmcng.h b/vd960DBN/tests/ota_mock/cmcng.h new file mode 100644 index 0000000..1755343 --- /dev/null +++ b/vd960DBN/tests/ota_mock/cmcng.h @@ -0,0 +1,22 @@ +/* mock cmcng.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */ +#ifndef __MOCK_CMCNG_H__ +#define __MOCK_CMCNG_H__ + +#include +#include + +typedef struct { + uint32_t flag; + uint32_t timeout; + uint32_t tick; +} Flag_Counter; + +extern Flag_Counter g_flag_counter_key; +extern Flag_Counter g_flag_counter_ota; + +uint32_t mstick(void); +void UART2_SendString(uint8_t *buf, uint16_t len); + +#define PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__) + +#endif diff --git a/vd960DBN/tests/test_ota_srv.c b/vd960DBN/tests/test_ota_srv.c new file mode 100644 index 0000000..3332eaf --- /dev/null +++ b/vd960DBN/tests/test_ota_srv.c @@ -0,0 +1,450 @@ +/* + * ota_srv.c 隔离单测 (gcc, 无 WCH 环境) + * + * 策略: mock CONFIG.h/cmcng.h + mock 外设 (NOR flash 模拟 / UART2 发送记录 / + * 可推进时钟 / offlog 记录) → #include 真实 ../APP/ota_srv.c (同编译单元, + * static 函数可见) → 断言会话状态机 + 刷写协议帧 + 超时重试。 + * + * 编译: + * gcc -I tests/ota_mock -I APP/include -I APP -o /tmp/test_ota_srv \ + * tests/test_ota_srv.c + * 运行: /tmp/test_ota_srv + */ +#include +#include +#include +#include + +#include "cmcng.h" /* mock: Flag_Counter / PRINT / mstick / UART2_SendString */ + +/* ============================================================================ + * mock 外设 + * ============================================================================*/ +Flag_Counter g_flag_counter_key = {0, 0, 0}; +Flag_Counter g_flag_counter_ota = {0, 0, 0}; + +static uint32_t _mock_ms = 0; +uint32_t mstick(void) { return _mock_ms; } +static void mock_advance(uint32_t ms) { _mock_ms += ms; } + +/* UART2 发送记录: 保存最近一帧 */ +static uint8_t _tx_buf[512]; +static uint16_t _tx_len = 0; +static uint16_t _tx_count = 0; +void UART2_SendString(uint8_t *buf, uint16_t len) +{ + _tx_len = (len <= sizeof(_tx_buf)) ? len : (uint16_t)sizeof(_tx_buf); + memcpy(_tx_buf, buf, _tx_len); + _tx_count++; +} +static void tx_reset(void) { _tx_len = 0; _tx_count = 0; } + +/* ---- mock NOR flash (W25Q32 语义: 写=AND, 擦=0xFF) ---- */ +#define MOCK_FLASH_SIZE (4 * 1024 * 1024) +static uint8_t flash[MOCK_FLASH_SIZE]; +static void flash_init(void) { memset(flash, 0xFF, sizeof(flash)); } + +void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size) +{ + assert(ReadAddr + size <= MOCK_FLASH_SIZE); + memcpy(pBuffer, &flash[ReadAddr], size); +} +void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size) +{ + uint16_t i; + assert(WriteAddr + size <= MOCK_FLASH_SIZE); + for (i = 0; i < size; i++) flash[WriteAddr + i] &= pBuffer[i]; +} +void SPI_Flash_Erase_Sector(uint32_t Dst_Addr) +{ + assert(Dst_Addr * 4096UL + 4096UL <= MOCK_FLASH_SIZE); + memset(&flash[Dst_Addr * 4096UL], 0xFF, 4096); +} +/* 与 storage.c 语义一致: 目标区非 0xFF → 擦扇区 → 读-改-写保留同扇区其他数据 */ +void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size) +{ + uint32_t secpos, secoff, secremain; + uint16_t i; + while (size > 0) { + secpos = WriteAddr / 4096; + secoff = WriteAddr % 4096; + secremain = 4096 - secoff; + if (size <= secremain) secremain = size; + /* 目标区是否全 0xFF */ + { + uint8_t need_erase = 0; + for (i = 0; i < secremain; i++) { + if (flash[secpos * 4096 + secoff + i] != 0xFF) { need_erase = 1; break; } + } + if (need_erase) { + uint8_t sector[4096]; + memcpy(sector, &flash[secpos * 4096], 4096); + for (i = 0; i < secremain; i++) sector[secoff + i] = pBuffer[i]; + memset(&flash[secpos * 4096], 0xFF, 4096); + for (i = 0; i < 4096; i++) flash[secpos * 4096 + i] &= sector[i]; + } else { + for (i = 0; i < secremain; i++) flash[WriteAddr + i] &= pBuffer[i]; + } + } + pBuffer += secremain; + WriteAddr += (uint32_t)secremain; + size -= (uint16_t)secremain; + } +} +uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* W25Q32 */ + +/* ---- mock offlog (记录最近 OTA 事件) ---- */ +static uint32_t _ota_start_cnt = 0, _ota_result_cnt = 0; +static uint32_t _last_ota_result = 0; +void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) { (void)type; (void)payload; (void)len; } +void offlog_ota_start(uint8_t slot, uint32_t size) { _ota_start_cnt++; (void)slot; (void)size; } +void offlog_ota_result(uint32_t code) { _ota_result_cnt++; _last_ota_result = code; } +void offlog_boot(uint32_t r) { (void)r; } + +/* ---- mock loop_uart_proto ---- */ +void lup_frame_reset(void) { } + +/* ---- mock iot_mqtt_srv ---- */ +static uint8_t _mock_any_car = 0; +static uint32_t _ota_err_report = 0, _ota_err_cnt = 0; +uint8_t iot_any_car(void) { return _mock_any_car; } +void iot_evt_report_ota_error(uint32_t err) { _ota_err_report = err; _ota_err_cnt++; } + +/* ============================================================================ + * 嵌入真实 ota_srv.c (同一编译单元, static 可见) + * ============================================================================*/ +#include "../APP/ota_srv.c" + +/* ============================================================================ + * 测试 + * ============================================================================*/ +#define CHK(cond) do { if (!(cond)) { \ + printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; } } while (0) + +/* 构造测试镜像 (5120B = 20 片) */ +static void make_image(uint8_t *buf, uint32_t size) +{ + uint32_t i; + for (i = 0; i < size; i++) buf[i] = (uint8_t)(i * 7 + 3); +} + +static int test_crc32(void) +{ + /* CRC-32/ISO-HDLC 标准向量 */ + CHK(ota_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL); + CHK(ota_crc32(NULL, 0) == 0x00000000UL); + printf("PASS test_crc32\n"); + return 0; +} + +static int test_hex_decode(void) +{ + uint8_t out[4]; + CHK(ota_hex_decode("0102aBff", out, 4) == 0); + CHK(out[0] == 0x01 && out[1] == 0x02 && out[2] == 0xAB && out[3] == 0xFF); + CHK(ota_hex_decode("01z2", out, 2) != 0); /* 非法字符 */ + CHK(ota_hex_decode("010", out, 2) != 0); /* 长度不足 */ + printf("PASS test_hex_decode\n"); + return 0; +} + +static int test_meta_restore(void) +{ + flash_init(); + tx_reset(); + ota_init(); + CHK(_meta.state == OTA_STATE_IDLE); + /* 写一份 downloading 元数据 → 重新 init → 恢复续传状态 */ + _meta.magic = OTA_MAGIC; + _meta.state = OTA_STATE_DOWNLOADING; + _meta.size = 512; + _meta.received = 256; + ota_meta_write(&_meta); + ota_init(); + CHK(_meta.state == OTA_STATE_DOWNLOADING); + CHK(_meta.received == 256); + printf("PASS test_meta_restore\n"); + return 0; +} + +static int test_session_flow(void) +{ + uint8_t img[5120]; + uint32_t i; + make_image(img, sizeof(img)); + flash_init(); + tx_reset(); + ota_init(); + + /* begin */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0); + CHK(_meta.state == OTA_STATE_DOWNLOADING); + CHK(_meta.received == 0); + + /* 20 片下发 */ + for (i = 0; i < sizeof(img); i += 256) { + uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i); + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + memcpy(chunk, &img[i], clen); + for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0); + } + CHK(_meta.received == sizeof(img)); + + /* end → ready */ + CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0); + CHK(_meta.state == OTA_STATE_READY); + + /* 暂存区与镜像一致 (读回比对) */ + { + uint8_t back[5120]; + SPI_Flash_Read(back, OTA_SLOT_A_BASE, sizeof(back)); + CHK(memcmp(back, img, sizeof(img)) == 0); + } + printf("PASS test_session_flow\n"); + return 0; +} + +static int test_begin_resume(void) +{ + uint8_t img[5120]; + uint32_t i; + make_image(img, sizeof(img)); + flash_init(); + ota_init(); + + /* 下载 16 片 (4096B, 触发元数据节流 flush) 后"断电重启" */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); + for (i = 0; i < 16 * 256; i += 256) { + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + memcpy(chunk, &img[i], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(i, ota_crc32(chunk, 256), hex) == 0); + } + CHK(_meta.received == 16 * 256); /* 4096B, 已 flush */ + + /* 重启 */ + ota_init(); + CHK(_meta.state == OTA_STATE_DOWNLOADING); + CHK(_meta.received == 16 * 256); /* 节流点精确续传 */ + + /* 重新 begin → 续传点 */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); + CHK(_meta.received == 16 * 256); + + /* 继续下载剩余 → ready */ + for (i = 16 * 256; i < sizeof(img); i += 256) { + uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i); + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + memcpy(chunk, &img[i], clen); + for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0); + } + CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0); + CHK(_meta.state == OTA_STATE_READY); + printf("PASS test_begin_resume\n"); + return 0; +} + +static int test_dup_and_gap(void) +{ + uint8_t img[1024]; + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + make_image(img, sizeof(img)); + flash_init(); + ota_init(); + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); + + memcpy(chunk, &img[0], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0); + + /* 重复片幂等 */ + CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0); + CHK(_meta.received == 256); + + /* 乱序缺片 (跳 256 直接发 512) → 拒绝 */ + memcpy(chunk, &img[512], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(512, ota_crc32(chunk, 256), hex) == 1); + CHK(_meta.received == 256); + + /* 单片 CRC 错 → 拒绝 */ + memcpy(chunk, &img[256], 256); + for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(256, 0xDEADBEEFUL, hex) == 1); + CHK(_meta.received == 256); + printf("PASS test_dup_and_gap\n"); + return 0; +} + +/* 模拟 bootloader: 根据收到的 A7 块回 data ACK */ +static void flash_step_until_ack(uint32_t *blocks_done, uint32_t total_blocks) +{ + /* ota_poll 推进一帧发送 + 喂 ACK, 直到收到末块 */ + uint32_t guard = 0; + while (*blocks_done < total_blocks && guard++ < 10000) { + ota_poll(); /* 发 A7 块 */ + CHK(_tx_len >= 7 && _tx_buf[0] == 0x9F); + CHK(_tx_buf[4] == 0xA7); + /* 构造 data ACK: 9F SubL SubH 02 A7 00 CHECK */ + { + uint8_t ack[7]; + uint8_t sum = 0; + ack[0] = 0x9F; ack[1] = _tx_buf[1]; ack[2] = _tx_buf[2]; + ack[3] = 0x02; ack[4] = 0xA7; ack[5] = 0x00; + sum = (uint8_t)(ack[1] + ack[2] + ack[3] + ack[4] + ack[5]); + ack[6] = sum; + ota_flash_feed_ack(ack, 7); + } + (*blocks_done)++; + } + CHK(*blocks_done == total_blocks); +} + +static int test_flash_flow(void) +{ + uint8_t img[5120]; + make_image(img, sizeof(img)); + flash_init(); + tx_reset(); + _ota_err_cnt = 0; + ota_init(); + + /* 下载 → ready */ + CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0); + { + uint32_t i; + for (i = 0; i < sizeof(img); i += 256) { + uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i); + uint8_t chunk[256]; + char hex[513]; + uint32_t j; + memcpy(chunk, &img[i], clen); + for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]); + CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0); + } + } + CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0); + CHK(_meta.state == OTA_STATE_READY); + + /* flash: 无车 → 启动 */ + _mock_any_car = 0; + tx_reset(); + CHK(ota_cmd_flash(0, 0) == 0); + CHK(_meta.state == OTA_STATE_FLASHING); + CHK(g_ota_flash_active == 1); + CHK(g_flag_counter_ota.flag == 1); + CHK(_ota_start_cnt >= 1); /* offlog 升级开始已写 */ + + /* START → 发 A5 启动帧 */ + ota_poll(); + CHK(_tx_len == 6 && _tx_buf[0] == 0x9F && _tx_buf[4] == 0xA5 && _tx_buf[5] == 0xA7); + + /* pre_ok → ADDR */ + { + uint8_t pre_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8}; + ota_flash_feed_ack(pre_ok, 7); + } + ota_poll(); + /* A6 地址帧: 9F 01 00 05 A6 08 00 34 00 CHECK */ + CHK(_tx_len == 10 && _tx_buf[4] == 0xA6); + CHK(_tx_buf[5] == 0x08 && _tx_buf[6] == 0x00 && _tx_buf[7] == 0x34 && _tx_buf[8] == 0x00); + /* 校验和: SubL+SubH+LEN+CMD+ADDR = 01+00+05+A6+08+00+34+00 */ + CHK(_tx_buf[9] == (uint8_t)(0x01 + 0x00 + 0x05 + 0xA6 + 0x08 + 0x00 + 0x34 + 0x00)); + + /* addr_ok → DATA 阶段 (总块数 = ceil(5120/248) = 21) */ + { + uint8_t addr_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9}; + ota_flash_feed_ack(addr_ok, 7); + } + CHK(_fs_sub == 21); /* 首块序号 = 总块数 */ + + /* 逐块 A7 + ACK 直到完成 */ + flash_step_until_ack(&(uint32_t){0}, 21); + + /* 完成后: 恢复 0x7F 模式, 元数据 ready (镜像保留可重刷), 结果已记 */ + CHK(_meta.state == OTA_STATE_READY); + CHK(g_ota_flash_active == 0); + CHK(g_flag_counter_ota.flag == 0); + CHK(_ota_result_cnt >= 1); + CHK(_last_ota_result == 0); + CHK(_ota_err_cnt == 0); /* 无失败告警 */ + printf("PASS test_flash_flow (21 blocks)\n"); + return 0; +} + +static int test_flash_reject_car(void) +{ + flash_init(); + ota_init(); + /* 直接构造 ready 态 */ + _meta.state = OTA_STATE_READY; + _meta.size = 512; + _mock_any_car = 1; + CHK(ota_cmd_flash(0, 0) == 3); /* 有车 → 拒绝 */ + CHK(g_ota_flash_active == 0); + _mock_any_car = 1; + CHK(ota_cmd_flash(0, 1) == 0); /* force → 跳过 */ + CHK(g_ota_flash_active == 1); + printf("PASS test_flash_reject_car\n"); + return 0; +} + +static int test_flash_ready_timeout(void) +{ + flash_init(); + tx_reset(); + _ota_err_cnt = 0; + ota_init(); + _meta.state = OTA_STATE_READY; + _meta.size = 512; + _mock_any_car = 0; + + CHK(ota_cmd_flash(0, 0) == 0); + ota_poll(); /* 发 A5 (首发) */ + /* 不喂 ACK: 首发后 3 次超时重发 + 第 4 个超时周期 → 失败 + 轮次: 超时1(→START) A5(2) 超时2(→START) A5(3) 超时3(→START) A5(4) 超时4(→fail) */ + { + int k; + for (k = 0; k < 7; k++) { mock_advance(1001); ota_poll(); } + } + CHK(_meta.state == OTA_STATE_FLASH_FAILED); + CHK(_meta.last_result == OTA_ERR_READY_TIMEOUT); + CHK(_ota_err_cnt == 1); /* event_report ota_error 告警 */ + CHK(_ota_err_report == OTA_ERR_READY_TIMEOUT); + CHK(g_ota_flash_active == 0); + CHK(g_flag_counter_ota.flag == 0); + CHK(_ota_result_cnt >= 1); + CHK(_last_ota_result == OTA_ERR_READY_TIMEOUT); + printf("PASS test_flash_ready_timeout\n"); + return 0; +} + +int main(void) +{ + int fails = 0; + fails += test_crc32(); + fails += test_hex_decode(); + fails += test_meta_restore(); + fails += test_session_flow(); + fails += test_begin_resume(); + fails += test_dup_and_gap(); + fails += test_flash_flow(); + fails += test_flash_reject_car(); + fails += test_flash_ready_timeout(); + if (fails == 0) { + printf("\nALL PASS\n"); + return 0; + } + printf("\n%d TEST(S) FAILED\n", fails); + return 1; +}