feat(protocol): 更新SPI Link协议至v2版本并优化CAN通信

- 将SPI OTA帧格式升级为SPI Link v2,采用新的帧格式:
  | SOF0 0xA5 | SOF1 0x5A | VER 0x02 | FLAGS | SESSION 4B LE |
  TX_SEQ | ACK_SEQ | CREDIT | TYPE | LEN 2B LE | RSV 2B | PAYLOAD |
  CRC32 4B LE |
- 新增V2_OVERHEAD=20字节开销,APP最大payload为236字节,BOOT为108字节
- 修改帧类型定义:ENTER_BOOT从0xB5合并至0xB0,新增CAN业务类型0xC0~C3
- 更新CAN协议文档,支持SPI Link v2-only模式,调整最大业务payload计算
- 扩展0xC1帧功能,支持双向通信和1Hz主动推送ID快照
- 添加0xC3完整ID快照帧类型,包含seq/rx_count等完整字段
- 更新协议文档中的错误码定义和反馈payload格式

refactor(main): 重构主程序入口并添加CAN反馈任务

- 在main.cpp中添加CAN反馈任务,更新启动流程注释
- 创建createCriticalTask辅助函数,统一关键任务创建逻辑
- 调整SPI初始化顺序,确保CAN反馈任务依赖正确
- 优化错误处理机制,关键任务失败时立即重启
- 将GD32版本探测移至ota_relay任务处理
- 更新版本号至0.3.48,调整编译时间和Git哈希

config: 更新SPI和BLE配置参数

- 添加SPI主机相关配置参数,包括CS GPIO、READY GPIO等引脚定义
- 配置SPI缓冲区大小:APP为256字节,BOOT为128字节
- 更新BLE滑动窗口参数,ACK窗口设为32,数据队列深度为72
- 调整SPI工作队列大小至5120字节,增加任务堆栈监控说明
This commit is contained in:
小龙虾
2026-07-16 23:00:15 +08:00
parent 51c89b3065
commit 5db3f66b8e
57 changed files with 4728 additions and 2098 deletions
+52 -144
View File
@@ -1,10 +1,9 @@
/*
* ESP32-C3FH4 OTA 中继 - 程序入口
* 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT
* FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT / CAN 反馈
*
* 使用各模块的单例类进行初始化和任务创建。
* 使用各模块单例初始化并创建任务;入口只做装配,不实现协议。
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
@@ -27,194 +26,103 @@
#include "monitor/can_feedback_task.h"
/**
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
* @brief 创建关键任务;失败则打 ERR 并 ESP_ERROR_CHECK 触发重启。
*/
static void createCriticalTask(TaskFunction_t entry, const char *name,
uint32_t stack, UBaseType_t prio)
{
BaseType_t xRet = xTaskCreatePinnedToCore(
entry, name, stack, NULL, prio, NULL, 0);
if (xRet != pdPASS) {
DEBUG_ERR("MAIN", "创建关键任务 %s 失败", name);
ESP_ERROR_CHECK(ESP_FAIL);
}
DEBUG_INFO("MAIN", "任务已创建: %s (prio=%u stack=%lu)",
name, (unsigned)prio, (unsigned long)stack);
}
/**
* @brief 程序主入口:初始化组件并创建 FreeRTOS 任务。
*/
extern "C" void app_main(void)
{
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
// ESP32 OTA 分区互斥锁内聚到 Esp32OtaCore
Esp32OtaCore::initMutex();
// 初始化网络接口 + 事件循环(必须最早)
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建)
DebugLog::instance().init();
// WiFi 提前初始化(调度器启动前)
bool wifi_sta_ok = WifiOta::instance().earlyInit();
/* earlyInit:true=已启动 STA 连接流程;false=将走 AP(或 CONFIG 直选 AP) */
const bool wifi_early_started_sta = WifiOta::instance().earlyInit();
HealthMonitor::instance().init();
DEBUG_INFO("MAIN", "健康监控状态已初始化");
DEBUG_INFO("MAIN", "========================================");
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay v1.0 启动");
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay %s 启动", FW_VERSION);
DEBUG_INFO("MAIN", "Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip_info.revision);
DEBUG_INFO("MAIN", "Free heap: %lu bytes", esp_get_free_heap_size());
DEBUG_INFO("MAIN", "Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
DEBUG_INFO("MAIN", "CPU Freq: %d MHz", CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ);
DEBUG_INFO("MAIN", "WiFi: %s", wifi_sta_ok ? "STA 连接中" : "将使用 AP 模式");
DEBUG_INFO("MAIN", "WiFi early: %s(最终 STA/AP 由 wifi_ota 任务确认)",
wifi_early_started_sta ? "STA 连接中" : "非 STA 优先/将切 AP");
DEBUG_INFO("MAIN", "========================================");
// 健康监控任务(温度 + WiFi RSSI)
BaseType_t xRet;
xRet = xTaskCreatePinnedToCore(
HealthMonitor::taskEntry,
"health_mon",
TASK_STACK_HEALTH_MON,
NULL,
configMAX_PRIORITIES - 4,
NULL,
0
);
if (xRet != pdPASS) {
// 关键任务:负责温度与 WiFi RSSI 上报,缺失会让运维陷入盲区,
// 无法及时发现硬件异常,必须重启恢复
DEBUG_ERR("MAIN", "创建 health_monitor_task 失败");
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
} else {
DEBUG_INFO("MAIN", "健康监控任务已创建");
}
/* 健康监控:温度/RSSI;缺失则运维盲区 → 关键 */
createCriticalTask(HealthMonitor::taskEntry, "health_mon",
TASK_STACK_HEALTH_MON, configMAX_PRIORITIES - 4);
/* CPU 占用 1Hz 采样(非致命;失败时 /system/status 的 cpu.valid 保持 false) */
/* CPU 1Hz 采样:非致命 */
CpuStats::instance().start();
Protocol::instance().init();
DEBUG_INFO("MAIN", "协议层初始化完成");
// OTA 全局协调器初始化(在 OTA task 创建前,确保 mutex 就绪)
OtaCoordinator::instance().init();
// 注册 BLE 射频共存监听器(WifiOta 实现 IBleCoexListener)
OtaCoordinator::instance().setBleCoexListener(&WifiOta::instance());
DEBUG_INFO("MAIN", "OTA 全局协调器已初始化");
esp_err_t ret = SpiMaster::instance().init();
if (ret != ESP_OK) {
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %d", ret);
} else {
/* SPI 是中继/CAN 反馈根基:失败则无法完成核心职责,按关键失败处理 */
{
esp_err_t ret = SpiMaster::instance().init();
if (ret != ESP_OK) {
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %s", esp_err_to_name(ret));
ESP_ERROR_CHECK(ret);
}
DEBUG_INFO("MAIN", "SPI 总线初始化完成");
}
// OTA 中继任务(中高优先级)
xRet = xTaskCreatePinnedToCore(
OtaRelay::taskEntry,
"ota_relay",
TASK_STACK_OTA_RELAY,
NULL,
configMAX_PRIORITIES - 2,
NULL,
0 // 核0
);
if (xRet != pdPASS) {
// 关键任务:OTA 中继是本设备核心职责(经 SPI 派发固件分片到目标 MCU),
// 缺失则 OTA 刷写主流程彻底瘫痪,必须重启恢复
DEBUG_ERR("MAIN", "创建 ota_relay_task 失败");
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
} else {
DEBUG_INFO("MAIN", "OTA 中继任务已创建");
}
createCriticalTask(OtaRelay::taskEntry, "ota_relay",
TASK_STACK_OTA_RELAY, configMAX_PRIORITIES - 2);
// WiFi OTA 服务器任务
xRet = xTaskCreatePinnedToCore(
WifiOta::taskEntry,
"wifi_ota",
TASK_STACK_WIFI_OTA,
NULL,
configMAX_PRIORITIES - 3,
NULL,
0
);
if (xRet != pdPASS) {
// 关键任务:WiFi OTA 是 OTA 刷写主通道之一,缺失则 WiFi 侧固件升级不可用,
// 系统失去主要刷写路径,必须重启恢复
DEBUG_ERR("MAIN", "创建 wifi_ota_task 失败");
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
} else {
DEBUG_INFO("MAIN", "WiFi OTA 任务已创建");
}
createCriticalTask(WifiOta::taskEntry, "wifi_ota",
TASK_STACK_WIFI_OTA, configMAX_PRIORITIES - 3);
// BLE OTA 任务
xRet = xTaskCreatePinnedToCore(
BleOta::taskEntry,
"ble_ota",
TASK_STACK_BLE_OTA,
NULL,
configMAX_PRIORITIES - 3,
NULL,
0 // ESP32-C3 单核,固定核0
);
if (xRet != pdPASS) {
// 关键任务:BLE OTA 是 OTA 刷写主通道之一(与 WiFi OTA 互为冗余),
// 缺失则 BLE 侧固件升级完全不可用,必须重启恢复
// 注:此处为 BLE OTA 主服务任务,区别于 ble_ota.cpp 内部的
// ble_ota_data_worker 子任务(后者可降级,不在本文件创建)
DEBUG_ERR("MAIN", "创建 ble_ota_task 失败");
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
} else {
DEBUG_INFO("MAIN", "BLE OTA 任务已创建");
}
createCriticalTask(BleOta::taskEntry, "ble_ota",
TASK_STACK_BLE_OTA, configMAX_PRIORITIES - 3);
// CAN 1Hz 反馈轮询任务(依赖 SPI 已初始化)
if (startCanFeedbackTask()) {
DEBUG_INFO("MAIN", "CAN 反馈轮询任务已创建");
} else {
DEBUG_WARN("MAIN", "CAN 反馈轮询任务创建失败(非致命,OTA 不受影响)");
}
// OTA 回滚保护:标记当前固件有效
// 启用 CONFIG_APP_ROLLBACK_ENABLE 后,新固件首次启动必须调用此函数
// 若未调用就崩溃/重启,bootloader 会自动回退到上一个有效分区
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
if (ota_valid_err == ESP_OK) {
DEBUG_INFO("MAIN", "OTA 回滚保护: 当前固件已标记有效");
} else {
DEBUG_WARN("MAIN", "OTA 回滚保护: 标记失败 (%s),可能非 OTA 启动", esp_err_to_name(ota_valid_err));
/* 任务就绪后尽快标记 app 有效,避免后续探测拖太久导致回滚窗口拉长 */
{
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
if (ota_valid_err == ESP_OK) {
DEBUG_INFO("MAIN", "OTA 回滚保护: 当前固件已标记有效");
} else {
DEBUG_WARN("MAIN", "OTA 回滚保护: 标记失败 (%s),可能非 OTA 启动",
esp_err_to_name(ota_valid_err));
}
}
DEBUG_INFO("MAIN", "========================================");
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
// 启动最后步骤:读取 GD32 APP 版本号
// 先 hello() 建立 SPI v2 会话,再 probeGd32AppOnce 读取版本
// GD32 APP 完整初始化需 5-6 秒,使用 20 次重试 × 300ms 窗口
{
DEBUG_INFO("MAIN", "启动 GD32 版本探测...");
gd32_version_t gd32_ver = {};
bool session_established = false;
bool gd32_ok = false;
for (uint32_t attempt = 0; attempt < 20; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(300));
if (!session_established) {
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP);
if (hr == ESP_OK) {
session_established = true;
} else {
DEBUG_INFO("MAIN", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
continue;
}
}
esp_err_t gd32_ret = OtaCoordinator::instance().probeGd32AppOnce(&gd32_ver, nullptr);
if (gd32_ret == ESP_OK) {
DEBUG_INFO("MAIN", "GD32 APP: v%d.%d.%d.%d (BOOT v%d.%d.%d.%d)",
gd32_ver.app_major, gd32_ver.app_minor,
gd32_ver.app_patch, gd32_ver.app_build,
gd32_ver.boot_major, gd32_ver.boot_minor,
gd32_ver.boot_patch, gd32_ver.boot_build);
gd32_ok = true;
break;
}
DEBUG_INFO("MAIN", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
}
if (!gd32_ok) {
DEBUG_WARN("MAIN", "GD32 APP 版本读取失败(OtaRelay 任务将重试)");
}
}
DEBUG_INFO("MAIN", "GD32 版本探测由 ota_relay 任务负责(hello+probe)");
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
DEBUG_LOG_IP, DEBUG_LOG_PORT);
DEBUG_INFO("MAIN", "========================================");