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
+81 -31
View File
@@ -1,18 +1,16 @@
/*
* protocol.cpp - 协议层实现
* T2.1, T2.2, T1.3
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
* protocol.cpp - OTA 握手辅助实现
*
* 加密实现使用 ESP32-C3 硬件 AES 加速器(通过 mbedtls port)
* 当前保留的接口:
* - init :硬件 RNG 填充扰码常数
* - scrambleGenerate :server nonce 生成
* - sessionKeyInit :会话密钥写入
* - sessionKeyClear :会话密钥安全擦除
*
* 当前保留的接口:init、scrambleGenerate(nonce 生成)、
* sessionKeyInit / sessionKeyClear(会话密钥生命周期管理)。
* 帧构建、AES-CBC 加解密、CRC16 等中间件代码已移除。
* 帧构建、AES-ECB/CBC 加解密、CRC16 等中间件代码已迁出(见 ota_relay / spi_link_v2)。
*/
#include <string.h>
#include <stdio.h>
#include "esp_random.h"
#include "aes/esp_aes.h"
#include "app_config.h"
#include "protocol.h"
#include "monitor/debug_log.h"
@@ -20,8 +18,9 @@
// ============================================================
// Protocol 单例
// ============================================================
/**
* @brief 获取 Protocol 单例实例。
* @brief 获取 Protocol 单例实例(Meyers 单例)。
* @return Protocol 单例引用。
*/
Protocol& Protocol::instance()
@@ -31,23 +30,45 @@ Protocol& Protocol::instance()
}
/**
* @brief 初始化协议层,打印初始化信息并设置初始化标志。
* @brief 抗优化密钥/敏感缓冲擦除。
*
* 普通 memset 可能被编译器当作「无后续读取」而消除;
* 通过 volatile 逐字节写 0,保证运行时真正清零。
*
* @param p 起始地址;nullptr 忽略。
* @param n 字节数。
*/
void Protocol::secureWipe(void *p, size_t n)
{
if (p == nullptr || n == 0U) {
return;
}
volatile uint8_t *v = static_cast<volatile uint8_t *>(p);
while (n > 0U) {
*v++ = 0;
--n;
}
}
/**
* @brief 初始化协议层,打印初始化信息并设置初始化标志。
*
* 使用硬件 RNG 填充 m_scrambleConst。完整常数不再以 INFO 打 hex,
* 避免 UDP/串口日志侧信道;需要核对时开 DEBUG_DBG。
*/
void Protocol::init()
{
if (m_initialized) return;
if (m_initialized) {
return;
}
DEBUG_INFO("PROT", "=== 初始化协议层 ===");
DEBUG_INFO("PROT", "协议层功能: nonce 扰码 + 会话密钥管理");
DEBUG_INFO("PROT", "=== 初始化协议层(nonce 扰码 + 会话密钥管理)===");
esp_fill_random(m_scrambleConst, SCRAMBLE_CONST_LEN);
{
char hex[SCRAMBLE_CONST_LEN * 2 + 1] = {0};
for (size_t i = 0; i < SCRAMBLE_CONST_LEN; ++i)
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", m_scrambleConst[i]);
DEBUG_INFO("PROT", "SCRAMBLE_CONST (RNG): %s", hex);
}
/* 不把完整扰码常数打到 INFO/UDP:减小日志侧信道;需要调试时开 DBG。 */
DEBUG_DBG("PROT", "SCRAMBLE_CONST ready (%u B from HW RNG)",
(unsigned)SCRAMBLE_CONST_LEN);
m_initialized = true;
}
@@ -57,13 +78,28 @@ void Protocol::init()
* @param timestamp 当前时间戳(tick)。
* @param mem_addr 内存地址(增加随机性)。
* @param rand_seed 随机种子。
* @param nonce_out 输出 16 字节 nonce 缓冲区。
* @param nonce_out 输出 16 字节 nonce 缓冲区;为 nullptr 时直接返回。
*
* 混合步骤概要:
* 1) state = timestamp|mem_addr|rand_seed|scrambleConst[0..3](小端)
* 2) 3 轮:逐字节 XOR 全表 scrambleConst + 循环左移 1
* 3) 2 轮:按 32-bit 字 XOR 完整 4B scrambleConst(memcpy 避免未对齐访问)
*
* @note 非标准 KDF;仅 OTA 握手用。未 init 时自动 init()。
*/
void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
uint32_t rand_seed, uint8_t *nonce_out)
{
if (nonce_out == nullptr) {
return;
}
if (!m_initialized) {
init();
}
uint8_t state[16];
/* 宿主小端布局;ESP32 / GD32 均为 LE,与对端一致。 */
memcpy(&state[0], &timestamp, 4);
memcpy(&state[4], &mem_addr, 4);
memcpy(&state[8], &rand_seed, 4);
@@ -72,14 +108,22 @@ void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
for (int round = 0; round < 3; round++) {
for (int i = 0; i < 16; i++) {
state[i] ^= m_scrambleConst[i];
state[i] = (state[i] << 1) | (state[i] >> 7);
state[i] = (uint8_t)((state[i] << 1) | (state[i] >> 7));
}
}
/* 按 32-bit 字再混一轮完整 4B 扰码(memcpy 避免未对齐 uint32_t*)。 */
for (int round = 0; round < 2; round++) {
uint32_t *words = (uint32_t *)state;
for (int i = 0; i < 4; i++) {
words[i] ^= m_scrambleConst[i * 4 % 16] | ((uint32_t)m_scrambleConst[i * 4 + 1] << 8);
uint32_t w = 0;
memcpy(&w, &state[i * 4], 4);
const uint32_t mix =
(uint32_t)m_scrambleConst[i * 4]
| ((uint32_t)m_scrambleConst[i * 4 + 1] << 8)
| ((uint32_t)m_scrambleConst[i * 4 + 2] << 16)
| ((uint32_t)m_scrambleConst[i * 4 + 3] << 24);
w ^= mix;
memcpy(&state[i * 4], &w, 4);
}
}
@@ -91,24 +135,30 @@ void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
* @param sess 会话上下文指针。
* @param k_session 16 字节会话密钥。
* @return 0 成功,-1 参数无效。
*
* 先 secureWipe 再拷贝,避免旧 key 残留。
*/
int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session)
{
if (!sess || !k_session) return -1;
memset(sess, 0, sizeof(session_ctx_t));
memcpy(sess->key, k_session, 16);
if (sess == nullptr || k_session == nullptr) {
return -1;
}
secureWipe(sess, sizeof(*sess));
memcpy(sess->key, k_session, SESSION_KEY_LEN);
return 0;
}
/**
* @brief 清除会话上下文中的密钥数据。
* @param sess 会话上下文指针。
* @param sess 会话上下文指针;为 nullptr 时忽略。
*
* 使用 secureWipe,避免编译器优化掉「无读回的」密钥清零。
*/
void Protocol::sessionKeyClear(session_ctx_t *sess)
{
if (sess) {
memset(sess->key, 0, 16);
if (sess == nullptr) {
return;
}
secureWipe(sess->key, SESSION_KEY_LEN);
}