291 lines
9.6 KiB
C
291 lines
9.6 KiB
C
/******************************************************************************
|
||
* @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;
|
||
}
|