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:
+116
-43
@@ -1,5 +1,14 @@
|
||||
/*
|
||||
* canfd_bridge.cpp - CANFD 3通道通信桥接实现
|
||||
*
|
||||
* SPI 业务类型:
|
||||
* 0xC0 发 CAN 帧(canFdSend)
|
||||
* 0xC1 反馈页解析(canFdParseFeedbackPage;由 GD32 主动推送,本文件不轮询)
|
||||
* 0xC2 设 RX 模式(canFdSetRxMode)
|
||||
*
|
||||
* 0xC1 线格式与 GD32 app_spi_cmd_can_feedback.cpp 对齐:
|
||||
* 固定头 28B + 条目 id(4LE)+dlc(1)+flags(1)+rsvd(2)+data(dlc)
|
||||
* 业务 payload 上限 256 - V2_OVERHEAD(20) = 236
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "canfd_bridge.h"
|
||||
@@ -8,8 +17,19 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kSpiCanFrameType = 0xC0U;
|
||||
constexpr uint8_t kSpiCanFrameType = 0xC0U;
|
||||
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
||||
constexpr uint8_t kFeedbackFixedHeader = 28U;
|
||||
constexpr uint8_t kFeedbackMaxPage = 1U; /* GD32 仅 page 0/1 */
|
||||
|
||||
/**
|
||||
* @brief 经 SPI 发送一帧 CAN,并解析 ACK/ERROR。
|
||||
*
|
||||
* 两步法:transact 发命令(丢弃延迟一拍陈旧响应)+ poll 真实响应。
|
||||
* CAN 帧非幂等,命令只发一次,不重发。
|
||||
*
|
||||
* @return >=0 ACK 时响应负载长度;-2 SPI 失败;-3 GD32 ERROR。
|
||||
*/
|
||||
static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *recv_buf, uint8_t recv_max,
|
||||
@@ -24,61 +44,71 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
tx_buf[tx_len++] = static_cast<uint8_t>((ext_id >> 16) & 0xFFU);
|
||||
tx_buf[tx_len++] = static_cast<uint8_t>((ext_id >> 8) & 0xFFU);
|
||||
tx_buf[tx_len++] = static_cast<uint8_t>(ext_id & 0xFFU);
|
||||
if (dlc > 0U) {
|
||||
if (dlc > 0U && data != nullptr) {
|
||||
memcpy(&tx_buf[tx_len], data, dlc);
|
||||
tx_len += dlc;
|
||||
}
|
||||
|
||||
/* 独立缓冲收响应:调用方 recv_buf 为空时也能拿到 ERROR 细分码。 */
|
||||
uint8_t local_resp[72];
|
||||
uint8_t *spi_rx = (recv_buf != nullptr && recv_max > 0U) ? recv_buf : local_resp;
|
||||
const size_t spi_rx_max = (recv_buf != nullptr && recv_max > 0U)
|
||||
? static_cast<size_t>(recv_max)
|
||||
: sizeof(local_resp);
|
||||
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
/* 两步法:transact 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。
|
||||
* CAN 帧非幂等,命令只发一次,不重发。 */
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
kSpiCanFrameType,
|
||||
tx_buf, tx_len,
|
||||
&resp_type, recv_buf, recv_max, &resp_len, timeout_ms);
|
||||
&resp_type, spi_rx, spi_rx_max, &resp_len, timeout_ms);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s",
|
||||
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret));
|
||||
if (gd32_err_out) *gd32_err_out = 0U; /* SPI 失败时清零 */
|
||||
return -2; /* SPI 通信失败 */
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
if (resp_type == SPI_OTA_RSP_ACK) {
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
if (recv_buf == nullptr || recv_max == 0U) {
|
||||
if (gd32_err_out) *gd32_err_out = 0U;
|
||||
return static_cast<int>(resp_len);
|
||||
}
|
||||
const size_t copied_len = (resp_len < recv_max) ? resp_len : recv_max;
|
||||
if (gd32_err_out) *gd32_err_out = 0U;
|
||||
return static_cast<int>(copied_len);
|
||||
}
|
||||
/* resp_type == ERROR: 读取 GD32 返回的错误码 */
|
||||
uint8_t gd32_err = (resp_len > 0 && recv_buf) ? recv_buf[0] : 0xFF;
|
||||
if (gd32_err_out) *gd32_err_out = gd32_err; /* 透传细分错误码 */
|
||||
|
||||
/* ERROR:细分码在 payload[0];不依赖调用方是否提供 recv_buf */
|
||||
const uint8_t gd32_err = (resp_len > 0U) ? spi_rx[0] : 0xFFU;
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = gd32_err;
|
||||
}
|
||||
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X",
|
||||
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err);
|
||||
return -3; /* GD32 返回 ERROR */
|
||||
return -3;
|
||||
}
|
||||
|
||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
||||
constexpr size_t kFeedbackPayloadLen = 123U; // 与 GD32 协议表对齐 (kTxBufSize=128 - overhead=5)
|
||||
|
||||
static inline uint32_t readU32Le(const uint8_t *p) {
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
/** @brief 小端读 uint32(0xC1 固定头/条目 id)。 */
|
||||
static inline uint32_t readU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 发送一帧 CANFD 数据,可选等待并读取响应。
|
||||
* 通过 SPI 协议帧发送给 GD32 APP,并读取 ACK/ERROR 响应。
|
||||
* 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。
|
||||
* gd32_err_out 在返回 -3 时输出 GD32 细分错误码,其它情况清零。
|
||||
*
|
||||
* 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。
|
||||
* gd32_err_out 在返回 -3 时输出 GD32 细分错误码,其它情况清零。
|
||||
* dlc==0 时允许 data 为 nullptr(仅发 ID 的远程帧/空数据帧)。
|
||||
*/
|
||||
int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
@@ -86,21 +116,31 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
uint32_t timeout_ms,
|
||||
uint8_t *gd32_err_out)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
|
||||
if (gd32_err_out) *gd32_err_out = 0U;
|
||||
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC) {
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms, gd32_err_out);
|
||||
/* dlc>0 时 data 必填;dlc==0 允许空指针 */
|
||||
if (dlc > 0U && data == nullptr) {
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return sendCanFrameOverSpi(channel, ext_id, data, dlc,
|
||||
recv_buf, recv_max, timeout_ms, gd32_err_out);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
||||
*
|
||||
* GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后
|
||||
* 调用本函数解析:
|
||||
* page=0 → 重置 out,填充 28 字节固定头 + 首批条目;
|
||||
* page=1 → 在已有条目后追加。
|
||||
* 调用方按通道维护 entryIdx,page=0 时会被重置为 0。
|
||||
* page=0 → 清空 entries、重置 entryIdx,填充 28 字节固定头 + 首批条目;
|
||||
* page=1 → 校验 channel 一致后在已有条目后追加。
|
||||
* 调用方按通道维护 entryIdx。
|
||||
*
|
||||
* @return 0 成功;-1 参数/页号/通道/截断导致无法解析。
|
||||
*/
|
||||
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx)
|
||||
@@ -109,8 +149,7 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
return -1;
|
||||
}
|
||||
|
||||
constexpr uint8_t kFixedHeader = 28U;
|
||||
if (len < kFixedHeader) {
|
||||
if (len < kFeedbackFixedHeader) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -119,10 +158,15 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
if (channel >= CAN_CHANNEL_MAX) {
|
||||
return -1;
|
||||
}
|
||||
/* GD32 仅 page 0/1;非法页拒绝,避免当追加污染缓冲 */
|
||||
if (page > kFeedbackMaxPage) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* page=0:重置并解析固定头 */
|
||||
if (page == 0U) {
|
||||
/* 防 total 变少时 HTTP 读到上一轮残留 ID */
|
||||
*entryIdx = 0U;
|
||||
memset(out->entries, 0, sizeof(out->entries));
|
||||
out->stats.channel = channel;
|
||||
out->stats.mode = payload[4];
|
||||
out->stats.snapshot_seq = readU32Le(&payload[5]);
|
||||
@@ -134,13 +178,21 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
out->stats.rec = payload[26];
|
||||
out->stats.bus_state = payload[27];
|
||||
out->total_count = payload[2];
|
||||
} else {
|
||||
/* page>=1:必须与 page0 已建立的通道一致 */
|
||||
if (out->stats.channel != channel) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 解析本页条目并追加 */
|
||||
const uint8_t entryCount = payload[3];
|
||||
uint16_t offset = kFixedHeader;
|
||||
const uint8_t idx_before = *entryIdx;
|
||||
uint16_t offset = kFeedbackFixedHeader;
|
||||
|
||||
for (uint8_t i = 0U; i < entryCount && *entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) {
|
||||
if (offset + 8U > len) break;
|
||||
if (offset + 8U > len) {
|
||||
break;
|
||||
}
|
||||
|
||||
id_snapshot_entry_t &e = out->entries[*entryIdx];
|
||||
e.id = readU32Le(&payload[offset]);
|
||||
@@ -149,11 +201,16 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
|
||||
e.brs = (flags & 0x02U) ? 1U : 0U;
|
||||
e.esi = (flags & 0x04U) ? 1U : 0U;
|
||||
/* 线协议不携带 per-ID seq/rx_count,显式置 0 避免脏读 */
|
||||
e.seq = 0U;
|
||||
e.rx_count = 0U;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
if (offset + entrySize > len) break;
|
||||
if (offset + entrySize > len) {
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(e.data, &payload[offset + 8U], dataLen);
|
||||
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
|
||||
@@ -164,15 +221,26 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
(*entryIdx)++;
|
||||
}
|
||||
|
||||
/* 声明了条目但一个都没完整解析 → 截断/坏帧 */
|
||||
if (entryCount > 0U && *entryIdx == idx_before) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out->valid = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。
|
||||
* @return 0 成功;-1 参数非法或 SPI/应答失败。
|
||||
*/
|
||||
int canFdSetRxMode(can_rx_mode_t mode,
|
||||
can_rx_mode_t *actual,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (mode != CAN_RX_MODE_ALL_PASS && mode != CAN_RX_MODE_FILTER_ID && mode != CAN_RX_MODE_BRIDGE_0_TO_12) {
|
||||
if (mode != CAN_RX_MODE_ALL_PASS && mode != CAN_RX_MODE_FILTER_ID
|
||||
&& mode != CAN_RX_MODE_BRIDGE_0_TO_12) {
|
||||
DEBUG_WARN("CAN", "SET_MODE 非法 mode=%u", (unsigned)mode);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -181,8 +249,7 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
|
||||
/* 两步法破解延迟一拍:发命令一次 + poll 真实响应。
|
||||
* 0xC2 幂等,但两步法避免无效重试。 */
|
||||
/* 两步法:发命令一次 + poll 真实响应。0xC2 幂等,但避免无效重试。 */
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
kSpiCanSetModeType,
|
||||
&req, 1U,
|
||||
@@ -194,5 +261,11 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_WARN("CAN", "SET_MODE 失败: want=%u spi=%s resp=0x%02X len=%u",
|
||||
(unsigned)mode,
|
||||
esp_err_to_name(ret),
|
||||
(unsigned)resp_type,
|
||||
(unsigned)resp_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ typedef struct {
|
||||
can_channel_t channel; ///< CAN 通道 (0~2)
|
||||
uint32_t ext_id; ///< 扩展帧 ID (29bit, Big-Endian 存储)
|
||||
uint8_t data[CAN_MAX_DLC]; ///< CAN 数据负载 (最大 64 字节)
|
||||
uint8_t dlc; ///< 数据长度 (1~64)
|
||||
uint8_t dlc; ///< 数据长度 (0~64)
|
||||
} can_frame_t;
|
||||
|
||||
/**
|
||||
@@ -38,8 +38,8 @@ typedef struct {
|
||||
* 内部通过 SpiMaster 的公共 SPI 接口完成传输。
|
||||
* @param channel CAN 通道编号(0~2)。
|
||||
* @param ext_id 扩展帧 ID(29 位)。
|
||||
* @param data CAN 数据负载。
|
||||
* @param dlc 数据长度。
|
||||
* @param data CAN 数据负载;dlc==0 时可为 nullptr。
|
||||
* @param dlc 数据长度(0~64);0 表示空数据帧,仍发送 ID。
|
||||
* @param recv_buf 接收数据缓冲区(可为 nullptr)。
|
||||
* @param recv_max 接收缓冲区最大长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
@@ -61,11 +61,14 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
|
||||
/**
|
||||
* @brief 单个不重复 ID 的最新帧快照条目。
|
||||
*
|
||||
* 0xC1 线协议条目:id(4LE)+dlc(1)+flags(1)+rsvd(2)+data(dlc)。
|
||||
* seq / rx_count 未上线,解析后恒为 0(保留字段便于日后扩协议)。
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
uint32_t seq;
|
||||
uint32_t rx_count;
|
||||
uint32_t id; ///< CAN ID
|
||||
uint32_t seq; ///< 线协议未携带,解析后恒 0
|
||||
uint32_t rx_count; ///< 线协议未携带,解析后恒 0
|
||||
uint8_t dlc;
|
||||
uint8_t fd_flag;
|
||||
uint8_t brs;
|
||||
@@ -77,8 +80,6 @@ typedef struct {
|
||||
// CAN 接收过滤模式 & 1Hz 反馈快照
|
||||
// 与 GD32 DCU_APP SPI 协议帧 0xC1/0xC2 配套
|
||||
// ============================================================
|
||||
#define SPI_CAN_FEEDBACK_LAST_DATA_BYTES 93 ///< 反馈快照中最近一帧数据字节数(受 SPI 128B 应答帧约束(kTxBufSize=128 - overhead=5 - 前缀30 = 93))
|
||||
|
||||
typedef enum {
|
||||
CAN_RX_MODE_ALL_PASS = 0, ///< 默认:禁用 ID 过滤,三路全部接收
|
||||
CAN_RX_MODE_FILTER_ID = 1, ///< 按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧过滤
|
||||
@@ -115,24 +116,27 @@ typedef struct {
|
||||
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
||||
*
|
||||
* GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后
|
||||
* 调用本函数解析。page=0 时重置 out 并填充固定头 + 首批条目;
|
||||
* page=1 时在已有条目后追加。每页解析后置 out->valid=1。
|
||||
* 调用本函数解析:
|
||||
* - page=0:清空 entries、重置 entryIdx,填固定头 + 首批条目;
|
||||
* - page=1:校验 channel 与 page0 一致后追加;
|
||||
* - page>1 或声明条目却截断无法解析 → 失败。
|
||||
* 成功时置 out->valid=1。
|
||||
*
|
||||
* @param payload GD32 推送帧的 payload([channel, page, total, count, mode, seq, ...])。
|
||||
* @param len payload 长度。
|
||||
* @param out 累积结果(跨 page 0/1 累积)。
|
||||
* @param entryIdx 当前已填充条目数(page 0 时被重置为 0,调用方按通道维护)。
|
||||
* @return 0 成功,<0 失败(payload 过短或通道非法)。
|
||||
* @return 0 成功,<0 失败(payload 过短、页号/通道非法或条目截断)。
|
||||
*/
|
||||
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx);
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式。
|
||||
* @param mode 目标模式(0=全收,1=按 ID 过滤)。
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。
|
||||
* @param mode 目标模式(0=全收,1=按 ID 过滤,2=CAN0 桥接到 1/2)。
|
||||
* @param actual 可选输出,GD32 实际生效模式。
|
||||
* @param timeout_ms SPI 事务超时。
|
||||
* @return 0 成功,<0 失败。
|
||||
* @return 0 成功,<0 失败(失败时打 WARN 日志)。
|
||||
*/
|
||||
int canFdSetRxMode(can_rx_mode_t mode,
|
||||
can_rx_mode_t *actual,
|
||||
|
||||
+81
-31
@@ -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], ×tamp, 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);
|
||||
}
|
||||
|
||||
+60
-27
@@ -1,75 +1,108 @@
|
||||
/*
|
||||
* protocol.h - 帧格式、CRC16、AES 加密解密
|
||||
* T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
* protocol.h - OTA 握手辅助(nonce 扰码 + 会话密钥生命周期)
|
||||
*
|
||||
* Protocol 类封装协议层功能。
|
||||
* 本模块刻意保持轻量(原「帧格式 / CRC16 / AES-CBC」中间件已迁出):
|
||||
* - 不负责 SPI Link v2 帧 / CRC32
|
||||
* - 不负责 AES 加解密(OTA 数据包 AES-ECB 在 ota_relay 内)
|
||||
* - 仅提供:RNG 扰码常数、server nonce 生成、session key 存清
|
||||
*
|
||||
* AES 主密钥 MASTER_KEY[] 见 app_config.h。
|
||||
*/
|
||||
#ifndef PROTOCOL_H
|
||||
#define PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "app_config.h"
|
||||
|
||||
// 安全常量(定义在 app_config.h 中)
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16; ///< AES-128 主密钥长度(字节)
|
||||
inline constexpr size_t CHIP_UID_LEN = 12; ///< GD32 芯片 UID 长度(字节)
|
||||
/** @brief AES-128 / 会话密钥长度(字节)。 */
|
||||
inline constexpr size_t SESSION_KEY_LEN = 16;
|
||||
|
||||
// AES 会话上下文
|
||||
/**
|
||||
* @brief OTA 会话密钥上下文。
|
||||
*
|
||||
* 仅缓存协商得到的 K_session;真正加解密在 ota_relay 中用该 key 做 AES-ECB。
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t key[16]; // 会话密钥 K_session
|
||||
uint8_t key[SESSION_KEY_LEN]; ///< 会话密钥 K_session(16 字节)
|
||||
} session_ctx_t;
|
||||
|
||||
/**
|
||||
* @brief 协议层类(单例)
|
||||
* @brief OTA 握手辅助类(单例)
|
||||
*
|
||||
* 封装 CRC16、AES-128-CBC、帧构建/解析、会话管理等功能。
|
||||
* 大部分方法为静态工具方法,单例仅持有初始化标志。
|
||||
* 持有 per-boot 扰码常数 m_scrambleConst;
|
||||
* sessionKeyInit / sessionKeyClear 为无状态静态工具,不依赖单例成员。
|
||||
*/
|
||||
class Protocol {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取 Protocol 单例实例。
|
||||
* @brief 获取 Protocol 单例实例。
|
||||
* @return Protocol 单例引用。
|
||||
*/
|
||||
static Protocol& instance();
|
||||
|
||||
/**
|
||||
* @brief 初始化协议层。
|
||||
* @brief 初始化协议层(幂等)。
|
||||
*
|
||||
* 使用硬件 RNG 填充 m_scrambleConst,供 scrambleGenerate 混合使用。
|
||||
* 可重复调用;已初始化则直接返回。
|
||||
*/
|
||||
void init();
|
||||
|
||||
/**
|
||||
* @brief 生成基于时间戳、地址和随机种子的扰码 nonce。
|
||||
* @param timestamp 时间戳。
|
||||
* @param mem_addr 内存地址。
|
||||
* @brief 生成基于时间戳、地址和随机种子的 16 字节 server nonce。
|
||||
* @param timestamp 时间戳(如 FreeRTOS tick)。
|
||||
* @param mem_addr 内存地址(增加熵,通常取栈/缓冲区地址)。
|
||||
* @param rand_seed 随机种子。
|
||||
* @param nonce_out 输出的 16 字节 nonce 缓冲区。
|
||||
* @param nonce_out 输出缓冲区,至少 16 字节;为 nullptr 时直接返回。
|
||||
*
|
||||
* @note 非标准 KDF,仅作 OTA 握手 nonce,勿当通用密钥派生。
|
||||
* @note 若尚未 init(),内部会自动调用 init()。
|
||||
*/
|
||||
void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
|
||||
/**
|
||||
* @brief 会话密钥初始化,将密钥拷贝到会话上下文。
|
||||
* @brief 会话密钥初始化,将密钥拷贝到会话上下文。
|
||||
* @param sess 会话上下文指针。
|
||||
* @param k_session 会话密钥数据。
|
||||
* @param k_session 会话密钥数据(至少 SESSION_KEY_LEN 字节)。
|
||||
* @return 成功返回 0,参数无效返回 -1。
|
||||
*/
|
||||
static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session);
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥数据。
|
||||
* @param sess 会话上下文指针。
|
||||
* @brief 清除会话上下文中的密钥数据(抗优化擦除)。
|
||||
* @param sess 会话上下文指针;为 nullptr 时忽略。
|
||||
*/
|
||||
static void sessionKeyClear(session_ctx_t *sess);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 私有默认构造函数。
|
||||
*/
|
||||
Protocol() = default;
|
||||
/**
|
||||
* @brief 私有析构函数。
|
||||
*/
|
||||
~Protocol() = default;
|
||||
/**
|
||||
* @brief 禁止复制构造。
|
||||
*/
|
||||
Protocol(const Protocol&) = delete;
|
||||
/**
|
||||
* @brief 禁止复制赋值。
|
||||
*/
|
||||
Protocol& operator=(const Protocol&) = delete;
|
||||
|
||||
uint8_t m_scrambleConst[SCRAMBLE_CONST_LEN] = {0};
|
||||
bool m_initialized = false;
|
||||
/**
|
||||
* @brief 抗编译器优化的内存擦除(volatile 逐字节写 0)。
|
||||
* @param p 起始地址;nullptr 忽略。
|
||||
* @param n 字节数。
|
||||
*/
|
||||
static void secureWipe(void *p, size_t n);
|
||||
|
||||
uint8_t m_scrambleConst[SCRAMBLE_CONST_LEN] = {0}; ///< per-boot 扰码常数(init 时 RNG 填充)
|
||||
bool m_initialized = false; ///< 是否已 init
|
||||
};
|
||||
|
||||
#endif // PROTOCOL_H
|
||||
#endif // PROTOCOL_H
|
||||
|
||||
+135
-206
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/queue.h"
|
||||
@@ -29,13 +31,16 @@
|
||||
|
||||
static constexpr size_t kSpiTransferMaxBytes = 256U;
|
||||
|
||||
/** @brief submitJob 堆 Job 与 worker 之间的 finished/abandoned 交接。 */
|
||||
static portMUX_TYPE s_spiJobMux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
/**
|
||||
* @brief 发送路径 CS 时钟字节数。
|
||||
* Boot TX DMA 按满缓冲预装(kTxBufSize=128),主机过短时钟会:
|
||||
* - 截断延迟一拍的响应;
|
||||
* - 两段变长读若中途释放 CS,Boot 会把 s_txPending 当已交付清掉 → 后续全 0xFF。
|
||||
* 下限 64:覆盖 HELLO_ACK(~26)/短 ACK;上限 SPI_OTA_FRAME_MAX。
|
||||
* App 大帧仍可由 poll 路径按 128/256 读。
|
||||
* 下限 64:覆盖 HELLO_ACK(~26)/短 ACK;上限 SPI_OTA_FRAME_MAX(256)。
|
||||
* APP 侧 TX 满缓冲 256:poll/ring 读预算见 spiRespReadBudget()。
|
||||
*/
|
||||
static size_t spiXferBytes(size_t frame_len)
|
||||
{
|
||||
@@ -49,10 +54,19 @@ static size_t spiXferBytes(size_t frame_len)
|
||||
return n;
|
||||
}
|
||||
|
||||
/** @brief 读响应时单 CS 时钟预算(Boot 满缓冲 128;App 可达 256)。 */
|
||||
/**
|
||||
* @brief 读响应时单 CS 时钟预算(按最近 HELLO 的 GD32 role 分支)。
|
||||
* - BOOT:TX DMA 满缓冲 128 → 读 128,避免过长时钟 underrun
|
||||
* - APP / unknown:TX DMA 满缓冲 256 → 读 256,避免 0xC1 等长 EVENT 被截断 CRC
|
||||
* unknown 偏 APP:宁长勿短(BOOT 短帧在 256 时钟下尾部 0xFF,仍可 parse)。
|
||||
*/
|
||||
static size_t spiRespReadBudget(void)
|
||||
{
|
||||
return 128U;
|
||||
const uint8_t role = SpiMaster::instance().gd32Role();
|
||||
if (role == V2_ROLE_BOOT) {
|
||||
return SPI_BOOT_TX_BUF_SIZE;
|
||||
}
|
||||
return SPI_APP_TX_BUF_SIZE;
|
||||
}
|
||||
|
||||
/** @brief ISR-safe GPIO 电平读取(不调用 Flash 中的 gpio_get_level)。
|
||||
@@ -63,7 +77,7 @@ static int IRAM_ATTR readReadyLevelRaw(void)
|
||||
return (REG_READ(GPIO_IN_REG) >> SPI_READY_GPIO) & 0x1;
|
||||
}
|
||||
|
||||
/* 前向声明:waitSpiDataReady 定义在文件下方(waitSpiReady 之后),
|
||||
/* 前向声明:waitSpiDataReady 定义在文件下方,
|
||||
* 但 serviceRingOnce / otaPollResponseUnlocked 等更早出现的方法需要它。 */
|
||||
static esp_err_t waitSpiDataReady(uint32_t timeout_ms);
|
||||
|
||||
@@ -244,15 +258,37 @@ void SpiMaster::executeJob(Job &job)
|
||||
case JobKind::WaitBoot:
|
||||
job.result = waitForBootloaderReady(job.timeout_ms);
|
||||
break;
|
||||
case JobKind::ServiceRing:
|
||||
job.result = serviceRingOnce();
|
||||
break;
|
||||
default:
|
||||
job.result = ESP_ERR_INVALID_ARG;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SpiMaster::finishHeapJob(Job *job)
|
||||
{
|
||||
if (job == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* 先 give,再标 finished;提交方 take 成功后会 spin 等 finished 再 free。 */
|
||||
if (job->done) {
|
||||
(void)xSemaphoreGive(job->done);
|
||||
}
|
||||
if (!job->heap_allocated) {
|
||||
return;
|
||||
}
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
job->finished = true;
|
||||
const bool free_it = job->abandoned;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
if (free_it) {
|
||||
if (job->done) {
|
||||
vSemaphoreDelete(job->done);
|
||||
job->done = nullptr;
|
||||
}
|
||||
free(job);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t SpiMaster::submitJob(Job &job)
|
||||
{
|
||||
if (m_jobQueue == nullptr || m_workerTask == nullptr || onWorker()) {
|
||||
@@ -260,31 +296,72 @@ esp_err_t SpiMaster::submitJob(Job &job)
|
||||
return job.result;
|
||||
}
|
||||
|
||||
StaticSemaphore_t done_buf;
|
||||
job.done = xSemaphoreCreateBinaryStatic(&done_buf);
|
||||
if (job.done == nullptr) {
|
||||
executeJob(job);
|
||||
return job.result;
|
||||
/* 堆 Job + 动态 binary sem:避免调用方超时返回后栈上 Job/done 被 worker UAF。 */
|
||||
Job *heap_job = static_cast<Job *>(calloc(1, sizeof(Job)));
|
||||
if (heap_job == nullptr) {
|
||||
DEBUG_ERR("SPI", "submitJob: calloc failed kind=%u", (unsigned)job.kind);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
*heap_job = job;
|
||||
heap_job->heap_allocated = true;
|
||||
heap_job->finished = false;
|
||||
heap_job->abandoned = false;
|
||||
heap_job->result = ESP_FAIL;
|
||||
heap_job->done = xSemaphoreCreateBinary();
|
||||
if (heap_job->done == nullptr) {
|
||||
free(heap_job);
|
||||
DEBUG_ERR("SPI", "submitJob: done sem create failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
(void)xSemaphoreTake(job.done, 0);
|
||||
|
||||
Job *ptr = &job;
|
||||
const TickType_t qwait = pdMS_TO_TICKS(
|
||||
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 2000U);
|
||||
if (xQueueSend(m_jobQueue, &ptr, qwait) != pdTRUE) {
|
||||
if (xQueueSend(m_jobQueue, &heap_job, qwait) != pdTRUE) {
|
||||
DEBUG_WARN("SPI", "worker job queue full/timeout kind=%u", (unsigned)job.kind);
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
free(heap_job);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
/* 唤醒可能阻塞在 notify wait 的 worker */
|
||||
xTaskNotify(m_workerTask, 0x10U, eSetBits);
|
||||
|
||||
const TickType_t dwait = pdMS_TO_TICKS(
|
||||
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 10000U);
|
||||
if (xSemaphoreTake(job.done, dwait) != pdTRUE) {
|
||||
DEBUG_ERR("SPI", "worker job done timeout kind=%u", (unsigned)job.kind);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
if (xSemaphoreTake(heap_job->done, dwait) == pdTRUE) {
|
||||
const esp_err_t result = heap_job->result;
|
||||
/* worker 在 give 后才写 finished;等其退出临界区后再由本侧独占 free。 */
|
||||
for (;;) {
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
const bool fin = heap_job->finished;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
if (fin) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
heap_job->done = nullptr;
|
||||
free(heap_job);
|
||||
return result;
|
||||
}
|
||||
return job.result;
|
||||
|
||||
/* 超时:标记 abandoned;若 worker 已跑完则本侧 free,否则 worker finishHeapJob free。 */
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
heap_job->abandoned = true;
|
||||
const bool already_done = heap_job->finished;
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
|
||||
if (already_done) {
|
||||
(void)xSemaphoreTake(heap_job->done, 0);
|
||||
const esp_err_t result = heap_job->result;
|
||||
vSemaphoreDelete(heap_job->done);
|
||||
heap_job->done = nullptr;
|
||||
free(heap_job);
|
||||
return result;
|
||||
}
|
||||
|
||||
DEBUG_ERR("SPI", "worker job done timeout kind=%u(堆 Job 交 worker 释放)",
|
||||
(unsigned)job.kind);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
void SpiMaster::workerEntry(void *arg)
|
||||
@@ -312,9 +389,7 @@ void SpiMaster::workerLoop()
|
||||
Job *job = nullptr;
|
||||
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) {
|
||||
executeJob(*job);
|
||||
if (job->done) {
|
||||
xSemaphoreGive(job->done);
|
||||
}
|
||||
finishHeapJob(job);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -333,76 +408,38 @@ void SpiMaster::workerLoop()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief GPIO 边沿中断回调(SPIready 线跳变)。
|
||||
*
|
||||
* - 下降沿:GD32 主动按铃(请求 ESP32 来读)。若当前空闲则置
|
||||
* m_gd32RequestPending 并唤醒 SPI 任务。
|
||||
* - 上升沿:GD32 释放 SPIready(WAITING_RESPONSE 态),唤醒等待任务。
|
||||
* @brief GPIO 边沿中断回调(DATA_READY / PB6)。
|
||||
*
|
||||
* 仅处理下降沿:GD32 有待读数据(按铃)。若本端 IDLE 则置
|
||||
* m_gd32RequestPending 并唤醒 SPI worker。上升沿不再使用(旧 waitSpiReady 已删)。
|
||||
* ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。
|
||||
*/
|
||||
void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
|
||||
{
|
||||
SpiMaster *self = static_cast<SpiMaster *>(arg);
|
||||
if (!self) { return; }
|
||||
int level = readReadyLevelRaw();
|
||||
if (level == 0) {
|
||||
/* 下降沿 */
|
||||
if (self->m_suppressRingService) {
|
||||
/* OTA 中继:PB6 低是 WAIT/ACK 就绪,留给业务 poll,不按 EVENT 处理 */
|
||||
} else if (self->m_linkState.acceptRing()) {
|
||||
/* GD32 按铃:仅 IDLE 态接受 */
|
||||
self->m_gd32RequestPending = true;
|
||||
if (self->m_spiTaskHandle) {
|
||||
BaseType_t hp = pdFALSE;
|
||||
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x01U,
|
||||
eSetBits, &hp);
|
||||
portYIELD_FROM_ISR(hp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 上升沿 */
|
||||
if (self->m_linkState.state() == HandshakeState::WAITING_RESPONSE) {
|
||||
/* GD32 释放 SPIready,可发起新 CS */
|
||||
if (self->m_spiTaskHandle) {
|
||||
BaseType_t hp = pdFALSE;
|
||||
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x02U,
|
||||
eSetBits, &hp);
|
||||
portYIELD_FROM_ISR(hp);
|
||||
}
|
||||
}
|
||||
/* 只关心低电平(下降沿后采样):有数据待读 */
|
||||
if (readReadyLevelRaw() != 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 等 SPIready 适合发起下一笔主机事务(v2 DATA_READY 语义)。
|
||||
*
|
||||
* v2:PB6 低 = GD32 有待读数据;高 = 队列空/空闲。
|
||||
* - 发新命令前:若 PB6 已高,立即返回;若仍低,可能是未读走的响应,
|
||||
* 短暂等待其升高(BOOT 交付后应拉高)。超时不阻塞——CS 本身互斥。
|
||||
* - 注意:poll 读响应路径不应调用本函数等 HIGH;应等 LOW 或直接读。
|
||||
*/
|
||||
esp_err_t SpiMaster::waitSpiReady(uint32_t timeout_ms)
|
||||
{
|
||||
if (timeout_ms == 0U) { return ESP_OK; }
|
||||
|
||||
/* 已高,立即返回 */
|
||||
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
||||
|
||||
/* PB6 低:可能有未读响应。短等升高;BOOT 修好交付清 pending 后应很快。
|
||||
* 超时返回 ESP_ERR_TIMEOUT,禁止调用方假装“已就绪”后盲发。 */
|
||||
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||
while (xTaskGetTickCount() < deadline) {
|
||||
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
||||
vTaskDelay(pdMS_TO_TICKS(1) > 0 ? pdMS_TO_TICKS(1) : 1);
|
||||
if (self->m_suppressRingService) {
|
||||
/* OTA 中继:PB6 低是 WAIT/ACK 就绪,留给业务 poll,不按 EVENT 处理 */
|
||||
return;
|
||||
}
|
||||
if (!self->m_linkState.acceptRing()) {
|
||||
return;
|
||||
}
|
||||
self->m_gd32RequestPending = true;
|
||||
if (self->m_spiTaskHandle) {
|
||||
BaseType_t hp = pdFALSE;
|
||||
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x01U, eSetBits, &hp);
|
||||
portYIELD_FROM_ISR(hp);
|
||||
}
|
||||
m_stats.ready_timeouts++;
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 等 GD32 拉低 PB6(有待读响应)。
|
||||
* v2 DATA_READY:低 = 有数据,与 waitSpiReady(等高)语义相反。
|
||||
* v2 DATA_READY:低 = 有数据;高 = 队列空。TX 前不等 HIGH(CS 即互斥)。
|
||||
* @return ESP_OK 已低;ESP_ERR_TIMEOUT 超时仍为高(调用方可再决定是否盲 poll)。
|
||||
*/
|
||||
static esp_err_t waitSpiDataReady(uint32_t timeout_ms)
|
||||
@@ -538,7 +575,7 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
|
||||
*
|
||||
* 不用两段 KEEP_ACTIVE:Boot 在 CS 释放时无论是否读完都清 s_txPending,
|
||||
* 中途断 CS 会丢 HELLO_ACK,后续 MISO 全 0xFF(线上已复现)。
|
||||
* 预算 128B 对齐 Boot TX DMA 满缓冲;App 大 EVENT 仍可到 256(out_max)。
|
||||
* 时钟预算见 spiRespReadBudget():BOOT=128,APP=256。
|
||||
*/
|
||||
esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
|
||||
uint32_t timeout_ms)
|
||||
@@ -762,12 +799,12 @@ esp_err_t SpiMaster::init()
|
||||
ready_cfg.mode = GPIO_MODE_INPUT; /* v2: 仅输入,不再开漏输出 */
|
||||
ready_cfg.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||
ready_cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
ready_cfg.intr_type = GPIO_INTR_ANYEDGE; /* 双边沿中断 */
|
||||
ready_cfg.intr_type = GPIO_INTR_NEGEDGE; /* 仅下降沿:有数据待读 */
|
||||
esp_err_t gpio_ret = gpio_config(&ready_cfg);
|
||||
if (gpio_ret != ESP_OK) {
|
||||
DEBUG_WARN("SPI", "SPI Ready 握手引脚 GPIO%d 配置失败: %s", SPI_READY_GPIO, esp_err_to_name(gpio_ret));
|
||||
} else {
|
||||
DEBUG_INFO("SPI", "SPI Ready 开漏握手已配置: GPIO%d (线与 GD32 PB6)", SPI_READY_GPIO);
|
||||
DEBUG_INFO("SPI", "DATA_READY 输入已配置: GPIO%d (← GD32 PB6,低=有数据)", SPI_READY_GPIO);
|
||||
}
|
||||
|
||||
/* 安装 GPIO ISR 服务(全局仅安装一次)并注册边沿回调 */
|
||||
@@ -883,15 +920,9 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
|
||||
}
|
||||
}
|
||||
|
||||
/* 同步单 CS 传输:按帧长发送;延迟一拍下本笔 RX 为陈旧响应。 */
|
||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||
/* v2 延迟一拍:PB6 低表示 GD32 有上一笔响应待读,step1 的 transferClocked
|
||||
* 会自然把它当陈旧响应读走,不需要额外 drain(drain 会多消耗一个 CS 周期,
|
||||
* 可能误吃掉主循环刚排好的 ACK 导致后续包永久丢 ACK)。
|
||||
*
|
||||
* ★ 不调 waitSpiReady(等 PB6 变高):v2 DATA_READY 下 PB6 低 = GD32 有待发
|
||||
* 响应,OTA 中继期间几乎持续为低。旧代码每包忙等 200ms 超时,21 包累计
|
||||
* ~4.2s 纯浪费,最终拖垮转发预算导致卡死。CS 本身即互斥,无需额外就绪等待。 */
|
||||
/* 同步单 CS 传输:按帧长发送;延迟一拍下本笔 RX 为陈旧响应。
|
||||
* v2:PB6 低 = 有待读;陈旧响应由本笔 transferClocked 自然读走(勿额外 drain,
|
||||
* 以免误吃主循环刚排好的 ACK)。CS 即互斥,TX 前不等 PB6 HIGH。 */
|
||||
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
|
||||
|
||||
const size_t xfer_len = spiXferBytes(tx_len);
|
||||
@@ -1161,17 +1192,23 @@ bool SpiMaster::takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 循环等待:信号量到达后重新持锁取,取空则继续等(防虚假唤醒) */
|
||||
const TickType_t deadline_ticks = pdMS_TO_TICKS(timeout_ms);
|
||||
/* 用绝对截止时间:虚假唤醒/抢事件后按剩余时间等,避免每次重新等满 timeout。 */
|
||||
const TickType_t start = xTaskGetTickCount();
|
||||
const TickType_t total = pdMS_TO_TICKS(timeout_ms);
|
||||
while (true) {
|
||||
if (xSemaphoreTake(m_eventSem, deadline_ticks) != pdTRUE) {
|
||||
return false; /* 超时 */
|
||||
const TickType_t elapsed = xTaskGetTickCount() - start;
|
||||
if (elapsed >= total) {
|
||||
return false;
|
||||
}
|
||||
const TickType_t remain = total - elapsed;
|
||||
if (xSemaphoreTake(m_eventSem, remain) != pdTRUE) {
|
||||
return false;
|
||||
}
|
||||
LockGuard lock(m_mutex);
|
||||
if (popEvent(type, payload, max_len, len)) {
|
||||
return true;
|
||||
}
|
||||
/* 信号量被消费但队列空(可能被并发 takeEvent 抢走),继续等 */
|
||||
/* 信号量被消费但队列空(可能被并发 takeEvent 抢走),继续等剩余时间 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,112 +1250,6 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
/*expected_type=*/0xFFU);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 带重试退避的事务(SPEC §9.2)。
|
||||
*
|
||||
* 同序号重试最多 3 次,退避 10/30/90ms;
|
||||
* 3 次失败后 HELLO 重同步,再试 1 次。
|
||||
* 调用者须已持有锁。
|
||||
*/
|
||||
esp_err_t SpiMaster::transactWithRetry(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||||
{
|
||||
/* 保存原始序号用于重发时恢复 */
|
||||
const uint8_t saved_tx_seq = m_txSeq;
|
||||
static constexpr uint32_t kBackoffMs[] = { 10U, 30U, 90U };
|
||||
static constexpr uint32_t kMaxRetries = 3U;
|
||||
|
||||
for (uint32_t retry = 0U; retry <= kMaxRetries; ++retry) {
|
||||
if (retry > 0U) {
|
||||
const uint32_t backoff = kBackoffMs[(retry - 1U) < 3U ? (retry - 1U) : 2U];
|
||||
vTaskDelay(pdMS_TO_TICKS(backoff));
|
||||
m_stats.retries++;
|
||||
/* 重发时恢复原序号 */
|
||||
m_txSeq = saved_tx_seq;
|
||||
}
|
||||
|
||||
/* 使用两步法(持锁已由调用者完成,用 Unlocked 版本) */
|
||||
const uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
|
||||
esp_err_t send_ret = otaTransferUnlocked(type, payload, payload_len,
|
||||
nullptr, nullptr, 0U, nullptr, send_timeout,
|
||||
/*expect_stale=*/true);
|
||||
if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) {
|
||||
if (retry < kMaxRetries) { continue; }
|
||||
return send_ret;
|
||||
}
|
||||
|
||||
const uint32_t poll_timeout = (timeout_ms > send_timeout)
|
||||
? (timeout_ms - send_timeout) : 500U;
|
||||
esp_err_t ret = otaPollResponseUnlocked(resp_type, resp_payload,
|
||||
resp_max, resp_len, poll_timeout,
|
||||
/*expected_type=*/0xFFU);
|
||||
if (ret == ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (ret == ESP_ERR_TIMEOUT) {
|
||||
m_stats.ready_timeouts++;
|
||||
}
|
||||
|
||||
/* 3 次重试后尝试 HELLO 重同步(最后一次循环) */
|
||||
if (retry == kMaxRetries - 1U) {
|
||||
DEBUG_WARN("SPI", "transactWithRetry: %u 次失败, HELLO 重同步...", (unsigned)retry);
|
||||
m_v2Active = false;
|
||||
/* HELLO 需要锁——但调用者已持锁。
|
||||
* hello() 内部有 LockGuard,会递归死锁。
|
||||
* 因此用 hello 的核心逻辑但不加锁:两阶段(发 + dummy 收)。 */
|
||||
m_sessionId = esp_random() | 0x00000001U;
|
||||
m_txSeq = 0;
|
||||
m_lastSentSeq = 0;
|
||||
m_rxAckedSeq = 0;
|
||||
m_expectedRxSeq = 0;
|
||||
|
||||
uint8_t hello_frame[V2_MAX_FRAME];
|
||||
size_t hello_len = 0;
|
||||
if (v2_build_hello_frame(m_sessionId, 0U, hello_frame, &hello_len)) {
|
||||
/* 延迟一拍连发 2 次,避免 DATA_READY/短读导致全 0xFF */
|
||||
const size_t xfer_len = spiXferBytes(hello_len);
|
||||
for (uint32_t hi = 0U; hi < 2U; ++hi) {
|
||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
|
||||
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
|
||||
uint8_t tx_p[SPI_OTA_FRAME_MAX];
|
||||
uint8_t rx_r[SPI_OTA_FRAME_MAX];
|
||||
memset(tx_p, 0xFF, xfer_len);
|
||||
memcpy(tx_p, hello_frame, hello_len);
|
||||
memset(rx_r, 0xFF, xfer_len);
|
||||
esp_err_t hr = transferClocked(tx_p, xfer_len, rx_r, xfer_len, 2000U);
|
||||
m_linkState.exitBusy();
|
||||
if (hr != ESP_OK) {
|
||||
continue;
|
||||
}
|
||||
v2_header_t hdr;
|
||||
const uint8_t *pl = nullptr;
|
||||
if (v2_parse_frame(rx_r, xfer_len, &hdr, &pl) &&
|
||||
hdr.type == V2_TYPE_LINK_HELLO &&
|
||||
(hdr.flags & V2_FLAG_RESPONSE) &&
|
||||
hdr.payload_len >= 6U && pl &&
|
||||
v2_get_u32le(&pl[0]) == m_sessionId) {
|
||||
m_gd32Role = pl[4];
|
||||
m_localCredit = pl[5];
|
||||
m_remoteCredit = hdr.credit;
|
||||
m_v2Active = true;
|
||||
m_stats.resyncs++;
|
||||
DEBUG_INFO("SPI", "transactWithRetry: HELLO 重同步成功 role=%u",
|
||||
m_gd32Role);
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief 通知 GD32 APP 设置 OTA 标志并软复位进入 Bootloader(v2 标准帧)。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
@@ -1713,8 +1644,6 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
|
||||
while (xTaskGetTickCount() <= deadline) {
|
||||
++attempt;
|
||||
|
||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
|
||||
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
|
||||
|
||||
uint8_t tx_padded[SPI_OTA_FRAME_MAX];
|
||||
|
||||
+31
-53
@@ -4,12 +4,10 @@
|
||||
*
|
||||
* SpiMaster 类封装 SPI 总线初始化、CANFD 通信和 SPI OTA 协议。
|
||||
*
|
||||
* 握手架构(线与 SPIready,GPIO10<->GD32 PB6 开漏单线):
|
||||
* - 空闲态线 HIGH(双方释放,靠上拉)。
|
||||
* - ESP32 主动发起:下拉 SPIready -> 立刻 CS 发请求 -> CS 释放后保持下拉,
|
||||
* GD32 在 onCsRelease() ISR 内完成帧处理+响应装载后释放 SPIready(高),
|
||||
* ESP32 GPIO 上升沿中断唤醒后发起 CS#2 读响应。
|
||||
* - GD32 主动按铃:下拉 SPIready -> ESP32 下降沿中断唤醒后发起读事务。
|
||||
* 握手架构(v2 DATA_READY,GPIO10 ← GD32 PB6 开漏):
|
||||
* - PB6 仅由 GD32 驱动:低 = 有待读数据,高 = 队列空。ESP32 只输入、不拉线。
|
||||
* - 帧同步/许可在帧内 SEQ/ACK_SEQ/CREDIT;CS 本身即互斥,TX 前不再等 HIGH。
|
||||
* - GD32 有数据:拉低 DATA_READY → ESP32 下降沿/轮询 → CS 读响应。
|
||||
*/
|
||||
#ifndef SPI_MASTER_H
|
||||
#define SPI_MASTER_H
|
||||
@@ -22,21 +20,16 @@
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include "app_config.h" /* SPI_MASTER_CS_GPIO / SPI_READY_GPIO / SPI_OTA_FRAME_MAX 等 */
|
||||
|
||||
// SPI 配置常量
|
||||
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
||||
#define SPI_CLOCK_HZ CONFIG_DCU_SPI_CLOCK_HZ // 默认 4MHz,可在 menuconfig 手动提速
|
||||
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
||||
#define SPI_QUEUE_SIZE (4) ///< SPI 事务队列深度
|
||||
#define SPI_READY_GPIO (10) // 线与 SPIready 握手引脚(开漏,GPIO10<->GD32 PB6)
|
||||
#define SPI_READY_TIMEOUT_MS (200) // 等待 SPIready 上升沿超时(毫秒)
|
||||
#define SPI_OTA_FRAME_MAX (256) ///< SPI 同步帧最大字节数
|
||||
// SPI 时钟来自 menuconfig;其余可调项在 app_config.h
|
||||
#define SPI_CLOCK_HZ CONFIG_DCU_SPI_CLOCK_HZ
|
||||
|
||||
// GD32 SPI 同步帧类型
|
||||
// GD32 SPI 业务 TYPE(与 SpiDefs / v2 TYPE 字段一致)
|
||||
#define SPI_OTA_CMD_REQ (0x01)
|
||||
#define SPI_OTA_CMD_DATA (0x02)
|
||||
#define SPI_APP_ENTER_BOOT_CMD (0xB0)
|
||||
#define SPI_APP_ENTER_BOOT_ACK_OK (0xB5) // GD32 APP 固件须同步修改 ACK 字节为 0xB5
|
||||
#define SPI_APP_ENTER_BOOT_ACK_OK (0xB5)
|
||||
#define SPI_OTA_RSP_ACK (0x80)
|
||||
#define SPI_OTA_RSP_WAIT (0x81)
|
||||
#define SPI_OTA_RSP_ERROR (0x82)
|
||||
@@ -103,6 +96,9 @@ public:
|
||||
/** @brief 读取本端剩余可接收 credit(SPEC §10.1,/system/status 展示用)。 */
|
||||
uint8_t localCredit() const { return m_localCredit; }
|
||||
|
||||
/** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */
|
||||
TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; }
|
||||
|
||||
/**
|
||||
* @brief OTA 中继期间禁止 worker 按铃读。
|
||||
*
|
||||
@@ -215,24 +211,20 @@ private:
|
||||
spi_device_handle_t m_spiDevice = NULL;
|
||||
SemaphoreHandle_t m_mutex = NULL;
|
||||
|
||||
// ── 线与 SPIready 握手状态机 ──
|
||||
/** 握手状态机枚举。 */
|
||||
// ── 本端事务态(非 PB6 电平门控;PB6 仅 DATA_READY 通知)──
|
||||
/** 本端 SPI 事务状态:仅用于 acceptRing(忙时不接按铃)。 */
|
||||
enum class HandshakeState : uint8_t {
|
||||
IDLE = 0, ///< 空闲,线 HIGH
|
||||
MASTER_SENDING, ///< ESP32 已下拉 SPIready,正在 CS#1 发送请求
|
||||
WAITING_RESPONSE, ///< CS#1 完成,等待 GD32 释放 SPIready(上升沿唤醒)
|
||||
SERVING_GD32_RING, ///< 响应 GD32 按铃,正在读 GD32 请求帧
|
||||
IDLE = 0, ///< 空闲,可接受 GD32 按铃
|
||||
MASTER_SENDING, ///< 主机正在 CS 发送
|
||||
WAITING_RESPONSE, ///< 主机正在 poll/读响应
|
||||
SERVING_GD32_RING, ///< 正在读 GD32 EVENT
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief SPI 链路状态机:封装握手状态 + 完成信号逻辑。
|
||||
* @brief 本端事务占用标记(v2)。
|
||||
*
|
||||
* 线状态语义(对齐 OTA):
|
||||
* - 线 HIGH = 空闲(双方释放),可发起新事务
|
||||
* - 线 LOW = 忙碌(某方下拉占用),禁止发起新事务
|
||||
*
|
||||
* ESP32 作为主动方(transact/appPing):enterBusy() 占用线 → transferClocked → exitBusy() 释放线
|
||||
* ESP32 作为按铃响应方(takeEvent):同上
|
||||
* DATA_READY(PB6)由 GD32 独占:低=有待读,高=队列空。
|
||||
* 本状态机只表示 ESP32 是否正占用 SPI 总线,避免与按铃读并发。
|
||||
*/
|
||||
class SpiLinkState {
|
||||
public:
|
||||
@@ -279,7 +271,7 @@ private:
|
||||
uint8_t m_eventTail = 0;
|
||||
uint8_t m_eventCount = 0;
|
||||
|
||||
// ── 作业提交(调用方栈上 Job + StaticSemaphore,worker 执行)──
|
||||
// ── 作业提交(跨任务:堆 Job + binary done;worker 串行执行)──
|
||||
enum class JobKind : uint8_t {
|
||||
Transact = 0,
|
||||
Poll,
|
||||
@@ -287,7 +279,6 @@ private:
|
||||
EnterBoot,
|
||||
Ping,
|
||||
WaitBoot,
|
||||
ServiceRing,
|
||||
};
|
||||
struct Job {
|
||||
JobKind kind = JobKind::Transact;
|
||||
@@ -305,12 +296,18 @@ private:
|
||||
gd32_version_t *out_version = nullptr;
|
||||
esp_err_t result = ESP_FAIL;
|
||||
SemaphoreHandle_t done = nullptr;
|
||||
/** submitJob 堆分配的 Job:超时放弃后由 worker 释放,成功路径由提交方释放。 */
|
||||
bool heap_allocated = false;
|
||||
volatile bool finished = false; ///< worker 已 execute + give
|
||||
volatile bool abandoned = false; ///< 提交方已超时返回,worker 负责 free
|
||||
};
|
||||
static constexpr UBaseType_t kJobQueueLen = 6;
|
||||
|
||||
bool onWorker() const;
|
||||
esp_err_t submitJob(Job &job);
|
||||
void executeJob(Job &job);
|
||||
/** @brief worker 完成堆 Job 后的生命周期收尾(give done + 可能 free)。 */
|
||||
void finishHeapJob(Job *job);
|
||||
static void workerEntry(void *arg);
|
||||
void workerLoop();
|
||||
esp_err_t serviceRingOnce();
|
||||
@@ -348,18 +345,6 @@ private:
|
||||
SemaphoreHandle_t m_lock;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 等 GD32 释放 SPIready(上升沿),中断驱动非忙等。
|
||||
*
|
||||
* 调用前须将 m_linkState 置为 WAITING_RESPONSE(enterBusy),ISR 上升沿检测到该状态
|
||||
* 后发 0x02 通知唤醒本任务。GD32 写 Flash 期间保持 PB6 低,本函数阻塞,
|
||||
* 写完拉高 → 唤醒 → 继续,自然避免 overrun。
|
||||
*
|
||||
* @param timeout_ms 超时(毫秒),0 表示不等待。
|
||||
* @return ESP_OK 线已高;ESP_ERR_TIMEOUT 超时。
|
||||
*/
|
||||
esp_err_t waitSpiReady(uint32_t timeout_ms);
|
||||
|
||||
/** @brief GPIO 边沿中断回调(注册到 ESP-IDF GPIO ISR 服务)。 */
|
||||
static void IRAM_ATTR onReadyEdgeISR(void *arg);
|
||||
|
||||
@@ -386,12 +371,12 @@ private:
|
||||
uint32_t timeout_ms, uint32_t trans_flags);
|
||||
|
||||
/**
|
||||
* @brief 变长读:同 CS 先读 16B Header,再按 LEN 读 payload+CRC。
|
||||
* 调用者须已持锁;仅在 DATA_READY 已低(有数据)时调用,禁止盲读。
|
||||
* @brief 读一帧响应:单 CS 固定时钟预算(BOOT=128 / APP=256),再 v2_parse 截真实长度。
|
||||
* 调用者须已持锁。不用两段 KEEP_ACTIVE(Boot 中途断 CS 会清 pending → 全 0xFF)。
|
||||
* @param out_frame 完整帧输出(含头+payload+CRC)
|
||||
* @param out_max 输出缓冲容量
|
||||
* @param out_len 实际帧长
|
||||
* @param timeout_ms 单段 SPI 超时
|
||||
* @param timeout_ms 单次 SPI 超时
|
||||
*/
|
||||
esp_err_t readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
|
||||
uint32_t timeout_ms);
|
||||
@@ -417,13 +402,6 @@ private:
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms,
|
||||
bool expect_stale = false);
|
||||
|
||||
/** @brief 带重试退避的事务(SPEC §9.2)。
|
||||
* 同序号重试最多 3 次,退避 10/30/90ms;
|
||||
* 3 次失败后 HELLO 重同步,再试 1 次。
|
||||
* 调用者须已持有锁。 */
|
||||
esp_err_t transactWithRetry(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||
// OTA 帧工具(静态,无状态)
|
||||
static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *out, size_t *out_len);
|
||||
|
||||
Reference in New Issue
Block a user