feat(vd960DBN): Loop MCU 远程 OTA 固件实现 (MQTT V1.08, ROADMAP P1.4 ①)

先存后刷: MQTT 分片下载 → W25Qxx OTA 暂存区 (0x010000 512KB) → 全镜像 CRC32
复核 → 本地 0x9F ISP 透传刷写 Loop MCU (AT32F421)

- 新增 ota_srv.c/h: CRC32(ISO-HDLC 逐位零表)/OtaMeta 双备份(节流 4KB flush)/
  会话状态机(begin/data/end/abort/flash)/本地刷写状态机(非阻塞 tick: A5→A6→A7
  停等 ACK 1s×3)/安全窗口(有车拒绝 force 跳过)/失败 event_report ota_error
- iot_mqtt_srv.c: 6 个 ota_* 命令分发 + 会话静默(event_report 积压/offlog·快照
  落盘暂停, 结束补发) + IOT_EVT_OTA_ERROR + iot_any_car
- usart_biz.c: uart_srv OTA 分支 ACK 分发 (本地刷写→ota_flash_feed_ack, BLE 透传不变)
- peripheral_main.c: ota_init + ota_poll 挂主循环
- offlog: OTA_START 0x60/OTA_RESULT 0x61 审计事件
- tests/test_ota_srv.c: gcc 隔离单测 9 组全过 (mock NOR/UART2/时钟, 嵌入真实实现)
  抓到 2 个真 bug: ①A7 序号未递减(bootloader 等不到末包) ②重发时 retry 清零(超时永不失败)
- 语法检查: ota_srv/iot_mqtt_srv/usart_biz/offlog 0 新增错误 (基线 interrupt 假阳性除外)
- devlog 置顶条目 + 待板上验证清单
This commit is contained in:
wangfq
2026-08-20 12:09:13 +08:00
parent 6c1f276002
commit 49736c341c
12 changed files with 1505 additions and 9 deletions
@@ -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 (主循环调用)
@@ -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); /* 有效记录数 */
@@ -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 <stdint.h>
/*===========================================================================
* 分区常量 (与 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__ */
@@ -26,6 +26,7 @@
#include "offlog.h"
#include "snapshot.h"
#include "fault_diag.h"
#include "ota_srv.h"
#include "ch32v20x_iwdg.h"
#include <string.h>
#include <stdlib.h>
@@ -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];
@@ -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";
}
@@ -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 <string.h>
/*===========================================================================
*
*===========================================================================*/
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;
}
}
@@ -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 */
@@ -11,6 +11,7 @@
#include "loop_uart_proto.h"
#include <string.h>
#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 结束后被误处理 */