/****************************************************************************** * @file ota_ab.c * @brief APP 侧 OTA 下载实现:收固件 → 写入备份区 → CRC 校验 → 通知 Bootloader * @author cyWu <1917507415@qq.com> * @date 2026.08.18 * @version V1.0.0 * @history * - V1.0.0, 2026.08.18, cyWu, 补充注释(逻辑与原先一致) * * 本文件只负责「下载阶段」,不负责把新固件切到运行区。 * 切换/拷贝由复位后的 Bootloader 根据状态区 PENDING 完成: * * 协议 0x30 ota_start() 擦备份区,准备接收 * 协议 0x32 ota_receive_chunk() 分片写入备份区(页缓冲) * ota_finish() 长度+CRC 通过后写 PENDING * 复位 * Bootloader 读到 PENDING 单备份:拷贝到运行区 * 双备份:A/B 交换后写 BOOT_NEW * 新 APP ota_boot_check_confirm() 双备份首次启动确认(单备份恒 false) ******************************************************************************/ #include "ota_ab.h" #include "flash.h" #include "crc32.h" #include "log.h" /*============================================================================*/ /* 私有变量 */ /*============================================================================*/ static ota_param_t g_ota; /**< 本次升级参数(长度/CRC/状态),结束时写入状态区 */ static uint32_t g_crc_acc; /**< 流式 CRC32 累加器,边收边算,不必整包再读 Flash */ static uint32_t g_recv_len; /**< 已接收且计入固件的字节数(不含尾包填充) */ static bool g_running; /**< true=正在收包;abort/finish 后清掉 */ /* * 页缓冲:协议分片约 96B,Flash 页编程必须整页 256B。 * 分片先攒进 s_page_buf,满页或收尾时再 APP_FlashWrite。 */ static uint8_t s_page_buf[OTA_PAGE_SIZE]; static uint32_t s_page_addr; /**< 当前缓冲对应的 Flash 页起始地址 */ static uint32_t s_page_fill; /**< 当前页已填字节数,0 表示缓冲空 */ /*============================================================================*/ /* 私有函数 */ /*============================================================================*/ /** * @brief 把页缓冲写入 Flash,然后清空缓冲 * @note 尾页不足 256B 时先补 0xFF(擦除态),满足整页编程要求。 * 补的 0xFF 不参与 CRC(CRC 在收包时已按原始固件字节累计)。 * @return 0 成功;缓冲为空时直接返回 0 */ static int ota_flush_page(void) { if (s_page_fill == 0) { return 0; } if (s_page_fill < OTA_PAGE_SIZE) { memset(&s_page_buf[s_page_fill], 0xFF, OTA_PAGE_SIZE - s_page_fill); } APP_FlashWrite(s_page_addr, s_page_buf, OTA_PAGE_SIZE); s_page_fill = 0; return 0; } /*============================================================================*/ /* 公有函数:下载流程 */ /*============================================================================*/ /** * @brief 开始 OTA:擦备份区并初始化接收状态(协议 0x30 接受升级后调用) * @param total_len 固件总长度,来自 0x30 的 fwLength * @param fw_crc32 固件 CRC32,来自 0x30 的 fwCrc32,收完后与流式 CRC 比对 * @return 0 成功;-1 长度非法 * @note 目标地址固定 OTA_BAK_ADDR_BASE: * 单备份 = BAK 下载缓冲;双备份 = B 区。运行区固件此时不动。 * 下载期间状态保持 IDLE,避免半包掉电被 Bootloader 误当成待升级。 */ int ota_start(uint32_t total_len, uint32_t fw_crc32) { uint32_t base = OTA_BAK_ADDR_BASE; if ((total_len == 0) || (total_len > OTA_SLOT_MAX_SIZE)) { Log("[OTA] invalid length %lu\r\n", (unsigned long)total_len); return -1; } Log("[OTA] start len=%lu crc=%08X target=%08X\r\n", (unsigned long)total_len, (unsigned int)fw_crc32, (unsigned int)base); memset(&g_ota, 0, sizeof(g_ota)); g_ota.magic = OTA_MAGIC; g_ota.state = OTA_STATE_IDLE; g_ota.fw_size = total_len; g_ota.fw_crc32 = fw_crc32; g_crc_acc = crc32_init(); g_recv_len = 0; g_running = true; s_page_addr = 0; s_page_fill = 0; /* 整槽擦除。调用点在 0x30 应答之前,擦完才让 BLE 开始发 0x32 */ APP_FlashEraseWithCheck(base, OTA_SLOT_MAX_SIZE); return 0; } /** * @brief 写入一个 OTA 数据分片(协议 0x32 每包调用) * @param data 本包固件数据 * @param len 本包长度;尾包可能按 MaxDataLen 补齐,内部会截到 fw_size * @return 0 成功(含已收满后的多余包,直接忽略) * -1 未处于下载中 * -2 参数非法 * -3 超出备份区容量 * @note 边收边做两件事:更新流式 CRC;按 256B 页对齐写入备份区。 */ int ota_receive_chunk(const uint8_t *data, uint32_t len) { uint32_t base; uint32_t offset; const uint8_t *p; uint32_t remain; if (!g_running) { return -1; } if ((data == NULL) || (len == 0)) { return -2; } if (g_recv_len >= g_ota.fw_size) { return 0; /* 已收满,忽略多余分片 */ } /* 尾包若带协议填充,只取固件剩余字节,避免把填充算进 CRC / 写入 Flash */ if ((g_recv_len + len) > g_ota.fw_size) { len = g_ota.fw_size - g_recv_len; } if ((g_recv_len + len) > OTA_SLOT_MAX_SIZE) { Log("[OTA] overflow\r\n"); return -3; } base = OTA_BAK_ADDR_BASE; offset = g_recv_len; p = data; remain = len; g_crc_acc = crc32_update(g_crc_acc, data, len); while (remain > 0) { /* 当前字节所属 Flash 页的起始地址(256B 对齐) */ uint32_t page_addr = base + (offset & ~(OTA_PAGE_SIZE - 1)); uint32_t in_page; uint32_t n; /* 跨页:先把上一页写进去,再开新页缓冲 */ if (page_addr != s_page_addr) { ota_flush_page(); s_page_addr = page_addr; s_page_fill = 0; } /* 本页还能塞多少:页大小 - 页内偏移 */ in_page = OTA_PAGE_SIZE - (offset & (OTA_PAGE_SIZE - 1)); n = (remain < in_page) ? remain : in_page; memcpy(&s_page_buf[s_page_fill], p, n); s_page_fill += n; if (s_page_fill == OTA_PAGE_SIZE) { ota_flush_page(); } p += n; offset += n; remain -= n; } g_recv_len = offset; return 0; } /** * @brief 是否已收满 0x30 声明的 fwLength * @return true 已收满,可调用 ota_finish();不依赖分片序号从 0 还是从 1 起 */ bool ota_is_download_complete(void) { return (g_running && (g_recv_len >= g_ota.fw_size) && (g_ota.fw_size > 0)); } /** * @brief 收包结束:刷尾页、核对长度和 CRC,通过则把状态写成 PENDING * @return 0 校验通过,已写 PENDING,调用方可复位进 Bootloader * 1 长度或 CRC 失败(备份区数据作废,运行区仍是旧固件) * -1 当前不在下载中 * @note 只有这里才会把状态区改成 PENDING。半包掉电时仍是 IDLE,下次启动正常跑旧固件。 */ int ota_finish(void) { uint32_t crc; if (!g_running) { return -1; } ota_flush_page(); g_running = false; Log("[OTA] recv %lu bytes, expect %lu\r\n", (unsigned long)g_recv_len, (unsigned long)g_ota.fw_size); if (g_recv_len != g_ota.fw_size) { Log("[OTA] length mismatch, abort\r\n"); return 1; } crc = crc32_final(g_crc_acc); Log("[OTA] crc=%08X expect=%08X\r\n", (unsigned int)crc, (unsigned int)g_ota.fw_crc32); if (crc != g_ota.fw_crc32) { Log("[OTA] CRC32 FAILED, abort\r\n"); return 1; } /* 告诉 Bootloader:备份区固件有效,复位后请拷贝/交换 */ g_ota.state = OTA_STATE_PENDING; ota_param_store(&g_ota); /* 回读确认状态区已落盘,避免写失败后仍去复位 */ ota_param_load(&g_ota); Log("[OTA] state=%d\r\n", g_ota.state); Log("[OTA] CRC32 OK, state=PENDING, ready to reboot\r\n"); return 0; } /** * @brief 中止本次 OTA(协议 0x36 / 0x38) * @note 只停接收、丢页缓冲。不改状态区(仍为 IDLE),运行区不受影响。 */ void ota_abort(void) { Log("[OTA] abort\r\n"); g_running = false; s_page_fill = 0; } /*============================================================================*/ /* 公有函数:新固件首次启动确认(仅双备份有意义) */ /*============================================================================*/ /** * @brief APP 启动时检查是否需要确认「已切到新固件」 * @return true 本次是双备份切换后的首次启动,已把 BOOT_NEW 改成 IDLE * false 普通启动,或单备份(不会出现 BOOT_NEW) * @note 应在 APP 自检通过后调用。若新固件崩溃、未跑到这里, * Bootloader 下次启动仍看到 BOOT_NEW,会回滚旧固件。 */ bool ota_boot_check_confirm(void) { ota_param_t p; ota_param_load(&p); if (p.state == OTA_STATE_BOOT_NEW) { p.state = OTA_STATE_IDLE; ota_param_store(&p); Log("[OTA] new fw confirmed\r\n"); return true; } return false; }