Files
ESP32_COMM/main/storage/gd32_flash_store.h
T
小龙虾 ebb3dcdf38 feat(debug_log): 新增UART0 RX泵任务支持串口日志转发
新增UART0 RX泵任务功能,将GPIO20接收到的串口数据按切行、剥离后以tag=UART推入ring buffer,通过HTTP /logs接口显示在网页日志面板。
接线方式:任意串口源TX连接至H1/H2的ESP_RX引脚,波特率3M 8N1。

feat(motor_ota): GD32 OTA增加包级防呆机制防止错误固件刷入

中继触发前校验gd32_ota镜像向量表身份(Gd32FlashStore::validateStoredImageIdentity),
对Bootloader包或非APP包在WiFi /ota/relay与BLE 0xF7均拒绝中继,
避免GD32 APP区被错误包擦除。新增0x87完成码用于固件身份验证失败场景。

feat(motor_state): 扩展CAN状态数据结构支持CONFIG负载

扩展motor_slot_t结构体,新增config_rx_count、config_seq和config_data字段
用于存储CONFIG帧的相关信息。CAN状态数据包大小从44B扩展到76B,
页面总数从6调整为9,支持目标值回报的配置数据传输。

fix(motor_ota): 修复OTA取消流程中的信用度管理和任务清理问题

★ 先复位credit再发送CANCEL命令,防止传输卡住时背压导致CANCEL被拒绝。
延长取消等待时间至10s覆盖最坏情况,确保feedTask正常退出而非强制删除。
取消完成后复位credit防止锁存死锁,确保后续通信正常进行。

fix(http_server): 修复HTTP请求处理中的边界检查和安全验证

拒绝chunked请求(content_len<=0)防止无限循环,添加通道参数范围验证(ch 0~2),
在OTA中继触发前验证固件身份防止错误包刷入,使用http_json_append避免缓冲区越界。

refactor(gd32_flash): 实现GD32固件身份验证算法确保刷写安全

基于Cortex-M向量表内容实现包级身份校验,通过MSP和复位向量位置
区分APP包、Bootloader包和无效固件,为OTA安全提供基础保障。

BREAKING CHANGE: 电机状态API数据格式变更,CAN通道参数验证更严格
2026-09-01 02:49:34 +08:00

206 lines
8.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* gd32_flash_store.h - GD32 OTA 固件 Flash 存储抽象层
*
* 封装 Flash 分区读写、NVS 元数据持久化和 SHA-256 校验。
* 传输层(WiFi/BLE)和 OtaRelay 均通过本类操作 GD32 固件存储。
*
* 线程模型:
* - 通过 instance() 单例访问,NVS 元数据读写由内部 mutex 保护
* - metaLoad/metaSave/metaUpdate/metaClear 串行化,读-改-写原子
* - 分区指针懒初始化,首次使用时查找并缓存
*/
#ifndef GD32_FLASH_STORE_H
#define GD32_FLASH_STORE_H
#include <stdint.h>
#include "esp_err.h"
#include "esp_partition.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#define OTA_META_NVS_NAMESPACE "ota_meta" ///< OTA 元数据 NVS 命名空间
/** @brief OTA 传输元数据结构,持久化到 NVS 用于断点续传和完整性校验。 */
typedef struct {
uint32_t total_size; ///< 固件总大小(字节)
uint32_t stored_size; ///< 已存储到 Flash 的大小(字节)
uint32_t last_seq; ///< 最后一次成功写入的包序号
uint8_t checksum[32]; ///< 固件 SHA-256 校验值
} ota_meta_t;
/**
* @brief gd32_ota 分区中存储镜像的身份分类(包级防呆)。
*
* 判定依据是镜像自身内容(Cortex-M 向量表前两个字),不依赖文件名:
* SHA-256 只能证明"传对了",不能证明"传的是对的包"。
* 把 BOOT 包(或任意非 APP 包)中继进 GD32 APP 区会擦掉好固件,
* 因此触发中继前必须确认镜像身份为 kApp。
*/
enum class Gd32ImageIdentity : uint8_t {
kApp = 0, ///< 复位向量落在 APP 区 → 合法 GD32 APP 固件
kBootloader, ///< 复位向量落在 BOOT 区(0x08000000~0x08011FFF)→ Bootloader 包,禁止刷入 APP
kInvalid, ///< 向量表非法 → 非 GD32 APP 固件(ESP32 镜像 / 文本 / 损坏数据)
kReadFailed, ///< 分区读取失败,无法判定
};
/**
* @brief GD32 OTA 固件 Flash 存储抽象层(纯静态工具类)。
*
* 封装 gd32_ota 分区的读写、NVS 元数据持久化、SHA-256 校验。
* 传输层通过 OtaCoordinator 或直接调用本类完成存储操作,
* OtaRelay 仅负责 SPI 中继状态机,不再承担存储职责。
*/
class Gd32FlashStore {
public:
/**
* @brief 获取单例实例。
* @return Gd32FlashStore 单例引用。
*/
static Gd32FlashStore& instance();
// ── Flash 分区读写 ──
/**
* @brief 向 gd32_ota 分区写入数据块(支持非 4 字节对齐写入)。
* @param offset 分区内写入偏移(字节)。
* @param data 待写入数据缓冲区。
* @param len 数据长度(字节)。
* @return ESP_OK 成功,其他错误码表示失败。
*/
esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
/**
* @brief 从 gd32_ota 分区读取数据块。
* @param offset 分区内读取偏移(字节)。
* @param data 输出数据缓冲区。
* @param len 读取长度(字节)。
* @return ESP_OK 成功,其他错误码表示失败。
*/
esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
/**
* @brief 擦除 gd32_ota 分区指定范围。
* @param size 擦除大小(字节),从偏移 0 开始。
* @return ESP_OK 成功,其他错误码表示失败。
*/
esp_err_t erasePartition(uint32_t size);
/**
* @brief 新一轮 GD32 固件写入会话开始时调用:复位擦除水位。
* @param full_erase true=强制擦除整个分区(motor OTA 用,杜绝旧固件残留导致的位错误);
* false=只重置水位,由 flashWriteChunk 按需擦扇区(GD32 OTA 用,与收流交织)。
*/
void beginWriteSession(bool full_erase = false);
// ── NVS 元数据持久化 ──
/**
* @brief 从 NVS 加载 OTA 元数据。
* @param meta 输出元数据结构体指针(任何错误路径下都会被清零)。
* @return ESP_OK 成功(含命名空间不存在的兼容路径,返回全零结构);
* ESP_ERR_TIMEOUT 加锁超时;
* 其他错误码 = nvs_open 真实失败(如 NVS 未初始化/损坏),不再一律当 OK。
*/
esp_err_t metaLoad(ota_meta_t *meta);
/**
* @brief 将 OTA 元数据写入 NVS。
* @param meta 待保存的元数据结构体指针。
* @return ESP_OK 成功,NVS 写入失败时返回错误码。
*/
esp_err_t metaSave(const ota_meta_t *meta);
/**
* @brief 传输过程中更新元数据的 stored_size 和 last_seq。
* @param stored_size 已存储大小(字节)。
* @param last_seq 最后成功写入的包序号。
* @return ESP_OK 成功,其他错误码表示失败。
*/
esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
/**
* @brief OTA 完成或中止时清除元数据(全零写入)。
* @return ESP_OK 成功。
*/
esp_err_t metaClear();
// ── SHA-256 校验 ──
/**
* @brief 计算 gd32_ota 分区中已存储固件的 SHA-256。
* @param out_sha256 输出 32 字节 SHA-256 哈希。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 元数据无效。
*/
esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
/**
* @brief 固件接收完毕后计算 SHA-256 并写入元数据。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 固件不完整。
*/
esp_err_t metaFinalizeChecksum();
/**
* @brief 校验固件完整性(重新计算 SHA-256 并与 meta.checksum 比对)。
* @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败。
*/
esp_err_t metaVerifyChecksum();
/**
* @brief 校验前端提供的 SHA-256 是否与设备端计算的一致。
* @param frontend_sha32 前端 32 字节原始 SHA(非 hex)。
* @param out_device_sha 可为 nullptr;非空时输出设备端 32 字节 SHA。
* @return ESP_OK 匹配;ESP_ERR_INVALID_CRC 不匹配;其他 = 元数据/计算失败。
*/
esp_err_t verifyFrontendSha(const uint8_t frontend_sha32[32],
uint8_t out_device_sha[32]);
/**
* @brief 读取 gd32_ota 分区镜像开头的 Cortex-M 向量表并判定包身份。
*
* 检查初始 MSP 是否落在 GD32E508 SRAM、复位向量是否落在 APP 区
* (0x08048000~0x0807DFFB,与 BOOT 侧 validateApplication() 同一判据)。
* 复位向量落在 BOOT 区(0x08000000~0x08011FFF)则判为 Bootloader 包。
* 供中继触发前的包级防呆使用(WiFi /ota/relay 与 BLE 0xF7 共用)。
* @return 镜像身份分类;调用方仅应在 kApp 时允许中继。
*/
Gd32ImageIdentity validateStoredImageIdentity();
/**
* @brief 获取分区指针(懒初始化)。
* @return 分区指针,找不到时返回 nullptr。
*/
const esp_partition_t* getPartition();
// ── OTA 期对齐缓冲生命周期 ──
/** @brief GD32 OTA begin 时调用:预分配对齐缓冲(消除 flashWriteChunk 逐块 malloc)。 */
static void acquireAlignBufForOta();
/** @brief GD32 OTA end/abort 时调用:释放对齐缓冲(非 OTA 期零占用)。 */
static void releaseAlignBufForOta();
private:
Gd32FlashStore();
~Gd32FlashStore() = default;
Gd32FlashStore(const Gd32FlashStore&) = delete;
Gd32FlashStore& operator=(const Gd32FlashStore&) = delete;
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
// ── 擦除水位:beginWriteSession 后从 0 起;
// flashWriteChunk → ensureErasedForWrite 按需推进(不再依赖 prepare 整区预擦)。
static constexpr uint32_t kFlashSectorSize = 4096U;
uint32_t m_erasedUpTo = 0;
esp_err_t ensureErasedForWrite(uint32_t offset, size_t len);
// NVS 互斥:lockNvs 返回 take 是否成功;unlockNvs(handle) 仅成功时 give
SemaphoreHandle_t m_nvsMutex = nullptr;
bool lockNvs(uint32_t timeout_ms);
void unlockNvs(bool handle);
static constexpr uint32_t kNvsLockTimeoutMs = 100; ///< NVS 加锁超时(ms)
// 已持锁版本(调用方已持 m_nvsMutex)
esp_err_t metaLoadLocked(ota_meta_t *meta);
esp_err_t metaSaveLocked(const ota_meta_t *meta);
};
#endif // GD32_FLASH_STORE_H