先存后刷: 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 置顶条目 + 待板上验证清单
451 lines
15 KiB
C
451 lines
15 KiB
C
/*
|
|
* 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;
|
|
}
|