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:
@@ -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,9 +114,11 @@ 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++;
|
||||
/* 事件日志: 线圈事件 (主循环上下文) */
|
||||
/* 事件日志: 线圈事件 (主循环上下文); 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 结束后被误处理 */
|
||||
|
||||
@@ -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 存储适配两项修复)
|
||||
|
||||
### 内容
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* mock CONFIG.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
|
||||
#ifndef __MOCK_CONFIG_H__
|
||||
#define __MOCK_CONFIG_H__
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
/* mock cmcng.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
|
||||
#ifndef __MOCK_CMCNG_H__
|
||||
#define __MOCK_CMCNG_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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
|
||||
@@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user