Files
ESP32_COMM/main/transport/spi_master.h
T
小龙虾 1b50ce1657 feat(can): 新增CAN通道收发器Silent模式功能
- 新增SPI 0xC8命令用于切换CAN通道收发器Silent模式(只听不发)
- 在canFdSetTxSilent函数中实现静默模式控制逻辑
- 在HTTP API中添加POST /can/silent端点
- 更新CAN协议文档,记录新命令和错误码E6
- 修改bus_state位图,添加bit5表示静默状态

fix(can): 优化CAN反馈任务内存使用和链路离线处理

- 将任务栈上的临时缓冲区改为静态分配,防止栈溢出
- 添加链路在线检测,在离线状态下进入休眠退避
- 修复queryMotorStatePages和queryAdcFeedback函数返回值类型
- 优化UDP日志发送的冷却退避机制

chore(config): 更新调试日志配置和版本信息

- 在VSCode设置中添加ESP-IDF串口配置
- 添加SPI调试日志级别编译期开关CONFIG_SPI_DEBUG_LOG_LEVEL
- 更新固件版本至0.5.2,记录相关变更内容
- 调整SPI和CAN反馈任务的栈大小配置
2026-10-10 02:25:12 +08:00

620 lines
30 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.
/*
* spi_master.h - SPI Master 驱动头文件
* CANFD 3通道发送接口 + 底层 SPI 总线
*
* SpiMaster 类封装 SPI 总线初始化、CANFD 通信和 SPI OTA 协议。
*
* 握手架构(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
#include <stdint.h>
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#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 时钟来自 menuconfig;其余可调项在 app_config.h
#define SPI_CLOCK_HZ CONFIG_DCU_SPI_CLOCK_HZ
// GD32 SPI 业务 TYPE(与 SpiDefs / v2 TYPE 字段一致)
#define SPI_OTA_CMD_REQ (0x01)
#define SPI_OTA_CMD_DATA (0x02)
#define SPI_OTA_CMD_CANCEL (0x03) /* 中止 OTA 会话 → GD32 resetToIdle */
#define SPI_APP_ENTER_BOOT_CMD (0xB0)
#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)
// GD32 版本信息(从 APP PING 响应解析)
typedef struct {
uint16_t app_major;
uint16_t app_minor;
uint16_t app_patch;
uint16_t app_build;
uint16_t boot_major;
uint16_t boot_minor;
uint16_t boot_patch;
uint16_t boot_build;
} gd32_version_t;
/* GD32 APP init_state 编码(身份块 [15] 与 0xA3 payload[9],见 spi_link_v2.h) */
#define GD32_INIT_STATE_OK (0x00) /* 全部模块初始化成功 */
#define GD32_INIT_STATE_DEGRADED (0x01) /* critical 全成,非 critical 有失败/跳过 */
#define GD32_INIT_STATE_CRITICAL_FAIL (0x02) /* 有 critical 模块失败 */
#define GD32_INIT_STATE_UNKNOWN (0xFF) /* 本端尚未收到身份块 */
// GD32 APP 初始化状态详情(SPI 0xA3 INIT_STATUS 响应解析结果)
#define GD32_INIT_MODULE_COUNT 13 /* 模块数(fault_codes 长度,与 DCU_APP kModuleCount 一致) */
typedef struct {
uint32_t module_mask; /* 成功初始化的模块位图(DCU_APP AppInitializer::ModuleBit) */
uint32_t critical_fail_mask; /* 失败的 critical 模块位图(0=无 critical 失败) */
uint8_t reset_cause; /* 复位源位图(bit0 POR/bit1 EPR/bit2 FWDGT/bit3 WWDGT/bit4 SW/bit5 LP) */
uint8_t init_state; /* GD32_INIT_STATE_* */
/* 每模块 1B 故障码(GD32 1.5.36+,响应 25B 时填充;0=成功。
* 编码见 DCU_APP sys/initial.h;canfd = 原因码|(首个失败通道<<4)) */
uint8_t fault_codes[GD32_INIT_MODULE_COUNT];
bool has_fault_codes; /* fault_codes 是否有效(响应 ≥25B) */
bool valid; /* 本结构是否已成功查询填充 */
} gd32_init_status_t;
/**
* @brief SPI Master 驱动类(单例)
*
* 封装 SPI 总线初始化、同步传输、CANFD 通信和 SPI OTA 协议。
*/
class SpiMaster {
public:
/**
* @brief 获取 SpiMaster 单例实例。
* @return SpiMaster 单例引用。
*/
static SpiMaster& instance();
/** @brief 初始化 SPI2 总线与目标从设备。 */
esp_err_t init();
// ============================================================
// SPI OTA 协议
// ============================================================
/** @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 */
esp_err_t otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
/** @brief ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。
*
* 解决 GD32 "延迟一拍"协议:transact 首笔 CS 读到过期响应,
* otaPollResponse 读取本笔命令的真实响应。命令只发送一次,不重发。
* 适用于所有 ESP32→GD32 方向的命令(CAN 发送/模式切换/ID 快照等)。
*
* @param type 请求帧类型(如 0xC0/0xC2/0xC3)。
* @param payload 请求负载数据。
* @param payload_len 请求负载长度。
* @param resp_type 输出响应类型(ACK/WAIT/ERROR)。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应负载长度。
* @param timeout_ms 总超时(发送 + 轮询),内部分配。
* @return ESP_OK 成功读到响应;其他为 SPI 错误或超时。
*/
esp_err_t otaSendAndPoll(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);
/** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */
uint8_t remoteCredit() const { return m_remoteCredit; }
/** @brief 读取本端剩余可接收 credit(SPEC §10.1,/system/status 展示用)。 */
uint8_t localCredit() const { return m_localCredit; }
/** @brief credit 窗口复位到初始值(电机 OTA 取消等半会话终止后调用)。
* credit 只随帧头传递:对端转空闲后不再发帧,传输期锁存的 credit=0
* 无法自愈 → 所有 transact 被 CREDIT 背压拒绝(死锁)。
* GD32 端 resetToIdle 已恢复接收窗口(clearOtaSessionQueues 置满),
* 本侧同步复位与之对齐;此后每帧帧头 credit 会覆盖为真实值。 */
void resetCredit() { m_localCredit = 8; m_remoteCredit = 8; }
/** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */
TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; }
/** @brief 最近一次 worker job 实际执行耗时(µs,worker 内计时)。
* 测速页用 RTT − 本值拆出"提交/调度/唤醒"开销,定位抖动归属。 */
uint32_t lastWorkerExecUs() const { return m_lastWorkerExecUs; }
/**
* @brief 单次 transact 的分段耗时剖析(测速页诊断用)。
* 5 段:提交/调度(t_submit0→t_w0)、发送帧(t_send0→t_send1)、
* 等应答(t_wait0→t_wait1)、读响应(t_wait1→t_read1)、唤醒(t_w1→t_submit1)。
* 写入顺序由 done 信号量保证 happens-before(worker 先写全再 give,调用方 take 后读)。
*/
struct SpiProfile {
uint64_t t_submit0 = 0; ///< 调用方 transact 入口
uint64_t t_w0 = 0; ///< worker executeJob 开始
uint64_t t_send0 = 0; ///< 发送帧开始
uint64_t t_send1 = 0; ///< 发送帧结束
uint64_t t_wait0 = 0; ///< 进入 poll(等 READY)
uint64_t t_wait1 = 0; ///< READY 就绪(wait 段结束)
uint64_t t_read1 = 0; ///< 响应帧读取完成
uint64_t t_w1 = 0; ///< worker executeJob 结束
uint64_t t_submit1 = 0; ///< 调用方 submitJob 返回
};
/** @brief 最近一次 transact 的剖析快照(仅测速/诊断用)。 */
SpiProfile lastProfile() const { return m_lastProfile; }
/** @brief SPI 任务队列当前待处理 job 数(固定 Job pool 槽,无堆分配)。
* 供 /system/heap_low_log 堆低水位快照采样,定位"低谷是不是 SPI 队列堆积引起"。 */
UBaseType_t getPendingJobCount() const { return m_jobQueue ? uxQueueMessagesWaiting(m_jobQueue) : 0; }
/**
* @brief OTA 中继期间禁止 worker 将 DATA_READY 当 EVENT 读取。
* 中继开始设 true,结束/复位设 false。
* 实现为引用计数:GD32 中继与电机 OTA feed 可并发持有,任一持有者
* 退出都不会提前解除抑制(避免电机 OTA 收尾把 GD32 中继的抑制误清)。
*/
void setSuppressRingService(bool suppress);
/**
* @brief ESP32 自我 OTA 期间暂停全部 SPI 活动(GD32 按铃读 + 业务 transact)。
* 暂停后 worker 空转不读 SPI,submitJob 快速返回 ESP_ERR_INVALID_STATE
* (业务立即失败,不阻塞排队);恢复时唤醒 worker 恢复正常。幂等。
*/
void setSpiPaused(bool paused);
/**
* @brief 业务层确认当前请求已成功时推进/对齐 m_txSeq。
*/
void confirmTxSeqAccepted();
/**
* @brief 统一请求入口:经 SPI worker 串行执行(send+poll)。
*/
esp_err_t transact(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);
/**
* @brief 取出 GD32 EVENT 帧(worker 已读入事件队列)。
* @param timeout_ms 等待超时(ms)。0 = 非阻塞(原语义)。
* @return true 取到事件;false 队列空且超时未到达。
*/
bool takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len,
uint32_t timeout_ms = 0U);
// ============================================================
// SPI Link v2 链路层(阶段1:帧格式 + 状态成员就位)
// ============================================================
/** @brief v2 链路状态统计(SPEC/spi_link_v2.md §12)。 */
struct SpiLinkStats {
uint32_t crc_errors; ///< CRC 校验失败计数
uint32_t duplicate_requests; ///< 重复请求计数(序号回退)
uint32_t seq_errors; ///< 序号跳变计数
uint32_t retries; ///< 重试总数
uint32_t ready_timeouts; ///< DATA_READY 超时计数
uint32_t rx_overruns; ///< RX overrun 计数
uint32_t resyncs; ///< HELLO 重同步次数
uint32_t max_resp_latency_us; ///< 最大响应延迟(微秒)
/* 0.4.3 诊断:GD32 回 0x82 的量化归因(/system/status 直读,不依赖 /logs) */
uint32_t rsp_err_0x82; ///< 收到 GD32 0x82 ERROR 响应总数
uint8_t rsp_err_last_req; ///< 最近一笔 0x82 对应的主动请求 TYPE
uint8_t rsp_err_last_p0; ///< 最近一笔 0x82 的 payload[0](GD32 错误码)
uint8_t rsp_err_last_p1; ///< 0xE8 时=GD32 收到的 rxLen
uint8_t rsp_err_last_p2; ///< 0xE8 时=GD32 rxBuf[0](期望 SOF0=0xA5)
uint8_t rsp_err_last_p3; ///< 0xE8 时=GD32 rxBuf[1](期望 SOF1=0x5A)
uint8_t rsp_err_last_p4; ///< 0xE8 时=GD32 SpiParseResult 解析失败原因枚举
uint8_t rsp_err_last_p5; ///< 0xE8 时=GD32 SOF offset (0=正常, >0=漂移, 255=未找到)
uint32_t ring_steals; ///< serviceRing 偷读迟到 RESPONSE 总数
uint8_t ring_steal_last_type; ///< 最近一笔偷读帧的 TYPE(0.4.5 归因)
uint8_t ring_steal_last_ack; ///< 最近一笔偷读帧 ack_seq 是否匹配 lastSent
uint8_t ring_steal_last_p0; ///< 最近一笔偷读帧 payload[0](0.4.6)
uint8_t ring_steal_last_req; ///< 偷读时刻的 m_lastReqType(0.4.6)
uint8_t ring_steal_last_p1; ///< 0xE8 时=GD32 诊断 rxLen(0.4.8)
uint8_t ring_steal_last_p2; ///< 0xE8 时=GD32 诊断 rxBuf[0](0.4.8)
uint8_t ring_steal_last_p3; ///< 0xE8 时=GD32 诊断 rxBuf[1](0.4.8)
uint8_t ring_steal_last_p4; ///< 0xE8 时=GD32 诊断 parse_res
uint8_t ring_steal_last_p5; ///< 0xE8 时=GD32 诊断 sof_offset
/* 0.4.9 诊断:透明重试会掩盖真实失败率(DBG 被编译裁掉、无计数)。
* tx_total 进入事务;tx_retry 走到重发;tx_fail 耗尽重试仍失败返回。 */
uint32_t tx_total; ///< otaSendAndPoll 事务总数
uint32_t tx_retry; ///< 因 send/poll 超时触发的重发总次数
uint32_t tx_fail; ///< 重试后仍失败(返回非 OK)的事务数
uint8_t tx_fail_last_type; ///< 最近一笔最终失败的命令 TYPE
};
/** @brief v2 完整链路状态(SPEC §10.1,会话/credit + 统计)。 */
struct SpiLinkStatus {
uint8_t version; ///< 协议版本(固定 2)
uint8_t gd32_role; ///< 0=unknown, 1=app, 2=boot
uint32_t session_id; ///< 当前会话 ID
uint8_t local_credit; ///< 本端剩余 credit
uint8_t remote_credit; ///< 对端剩余 credit
SpiLinkStats stats; ///< 8 个统计计数器
};
/** @brief 当前会话 ID(0 = 未建立 session)。 */
uint32_t sessionId() const { return m_sessionId; }
/** @brief v2 session 是否已激活。 */
bool v2Active() const { return m_v2Active; }
/** @brief SPI 链路是否判定在线(连续失败未触发熔断,防 SPI 不通时死循环阻塞与堆栈耗尽)。 */
bool isLinkOnline() const { return m_linkOnline; }
/** @brief 当前连续失败次数。 */
uint32_t consecutiveFailures() const { return m_consecutiveFailures; }
/** @brief 检查当前是否允许发起常规业务 SPI 事务(离线熔断冷却期内快速失败)。 */
bool allowRegularTransact(uint8_t type) const;
/** @brief 最近一次 HELLO 得到的 GD32 角色(0=unknown, 1=app, 2=boot)。 */
uint8_t gd32Role() const { return m_gd32Role; }
/**
* @brief 是否已从 HELLO_ACK/PING 身份块拿到功能位图。
* @note false=尚未建连身份或旧固件。
*/
bool gd32FeatKnown() const { return m_gd32FeatKnown; }
/** @brief GD32 功能位图(V2_FEAT_*);未知时为 0。 */
uint32_t gd32FeatFlags() const { return m_gd32FeatFlags; }
/** @brief 便捷:GD32 是否开启 ADC 0xC4(需 gd32FeatKnown)。 */
bool gd32FeatAdcFeedback() const {
return m_gd32FeatKnown && (m_gd32FeatFlags & V2_FEAT_ADC_FEEDBACK) != 0U;
}
/** @brief 是否已从 HELLO_ACK/PING 拿到 APP/BOOT 版本。 */
bool gd32IdKnown() const { return m_gd32IdKnown; }
/** @brief 最近身份块中的版本(HELLO 或 PING)。 */
gd32_version_t gd32VersionCached() const { return m_gd32Version; }
/** @brief 获取链路统计快照(线程安全,持锁读取)。 */
SpiLinkStats linkStats();
/** @brief 获取完整链路状态(SPEC §10.1,会话/credit + 统计)。 */
SpiLinkStatus linkStatus();
/** @brief 发起 HELLO 握手建立 v2 session(SPEC §5.1)。
* 生成随机非零 SESSION_ID,发送 LINK_HELLO,等待 HELLO_ACK。
* 成功后 m_v2Active=true,序号归零。
* @param timeout_ms 总超时(毫秒),默认 3000。
* @param role_hint 期望的对端角色(0=any, 1=app, 2=boot)。
* @return ESP_OK 成功;ESP_ERR_TIMEOUT 超时;ESP_ERR_INVALID_RESPONSE 响应异常。 */
esp_err_t hello(uint32_t timeout_ms = 3000U, uint8_t role_hint = 0U);
/** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */
esp_err_t appRequestBootloader();
/** @brief 通过 HELLO 轮询等待 Bootloader 就绪(v2 方式,不再依赖 PB6 序列)。
* App→Boot 切换后,ESP32 每 20ms 发送 HELLO(role_hint=boot),
* 等待 HELLO_ACK 且 role=boot,最长等待 timeout_ms。
* 超时返回 ESP_ERR_TIMEOUT。 */
esp_err_t waitForBootloaderReady(uint32_t timeout_ms);
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
/** @brief 查询 GD32 APP 初始化状态(SPI 0xA3,12B:模块位图/失败位图/复位源/init_state)。
* 仅 APP 角色支持(BOOT 无此命令);成功时填充 out 并置 valid=true,同时更新本端缓存。 */
esp_err_t appInitStatus(uint32_t timeout_ms, gd32_init_status_t *out);
/** @brief 身份块 [15] 带来的 init_state(HELLO/PING 自动刷新;0xFF=未知)。 */
uint8_t gd32InitState() const { return m_gd32InitState; }
/** @brief 读取缓存的 0xA3 查询结果;valid=false 表示尚未成功查询过。 */
bool gd32InitStatusCached(gd32_init_status_t *out) const;
/**
* @brief L0 本机自检(不依赖 GD32):SPI 设备/互斥/worker/队列已就绪。
*/
bool selfTestL0() const;
/**
* @brief L0 对端轻检:短超时 HELLO(role=any),确认 SPI 链路可达 GD32。
* @note GD32 仍在启动延时中时可能失败,属非关键项。
*/
bool selfTestPeerL0();
private:
/**
* @brief 私有默认构造函数。
*/
SpiMaster() = default;
/**
* @brief 私有析构函数。
*/
~SpiMaster() = default;
/**
* @brief 禁止复制构造。
*/
SpiMaster(const SpiMaster&) = delete;
/**
* @brief 禁止复制赋值。
*/
SpiMaster& operator=(const SpiMaster&) = delete;
spi_device_handle_t m_spiDevice = NULL;
SemaphoreHandle_t m_mutex = NULL;
// ── 本端事务态(非 PB6 电平门控;PB6 仅 DATA_READY 通知)──
/** 本端 SPI 事务状态:仅用于 acceptRing(忙时不接按铃)。 */
enum class HandshakeState : uint8_t {
IDLE = 0, ///< 空闲,可接受 GD32 按铃
MASTER_SENDING, ///< 主机正在 CS 发送
WAITING_RESPONSE, ///< 主机正在 poll/读响应
SERVING_GD32_RING, ///< 正在读 GD32 EVENT
};
/**
* @brief 本端事务占用标记(v2)。
*
* DATA_READY(PB6)由 GD32 独占:低=有待读,高=队列空。
* 本状态机只表示 ESP32 是否正占用 SPI 总线,避免与按铃读并发。
*/
class SpiLinkState {
public:
/** 进入忙碌态。 */
void enterBusy(HandshakeState hs) { m_hsState = hs; }
/** 退出忙碌态,回到 IDLE。 */
void exitBusy() { m_hsState = HandshakeState::IDLE; }
/** 当前是否空闲(可接受新按铃下降沿)。 */
bool isIdle() const { return m_hsState == HandshakeState::IDLE; }
/** 当前握手状态。 */
HandshakeState state() const { return m_hsState; }
/** ISR 检测下降沿时调用:仅 IDLE 态接受按铃。 */
bool acceptRing() const { return isIdle(); }
private:
volatile HandshakeState m_hsState = HandshakeState::IDLE;
};
SpiLinkState m_linkState;
/** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,仅 SPI worker 消费。 */
volatile bool m_gd32RequestPending = false;
/** ring service 抑制引用计数(setSuppressRingService 内部使用,临界区保护)。 */
uint32_t m_ringSuppressRefs = 0;
/** OTA 中继时为 true:worker 不 serviceRing,避免偷读 OTA ACK/WAIT。 */
volatile bool m_suppressRingService = false;
/** ESP32 自我 OTA 期间为 true:worker 空转、submitJob 快速拒绝。 */
volatile bool m_spiPaused = false;
/** GD32 重启检测(响应帧 session=0)置位:worker 空闲时 HELLO 重同步。 */
volatile bool m_resyncPending = false;
/** 上次 HELLO 重同步时刻(冷却 5s,防 GD32 不响应时反复 HELLO)。 */
volatile uint32_t m_lastResyncTick = 0;
/** 链路在线状态及熔断状态:防 SPI 不通时死循环阻塞与堆栈耗尽 */
volatile bool m_linkOnline = true;
volatile uint32_t m_consecutiveFailures = 0;
volatile uint32_t m_lastFailureTick = 0;
static constexpr uint32_t kCircuitBreakerFailThreshold = 3U; ///< 连续 3 次失败触发熔断
static constexpr uint32_t kCircuitBreakerCooloffMs = 2000U; ///< 熔断后冷却 2 秒
void recordSpiSuccess();
void recordSpiFailure();
/** 最近一次主动请求类型(otaTransferUnlocked 发送时记录;poll 读预算按它收紧)。 */
volatile uint8_t m_lastReqType = 0xFFU;
/** SPI worker 任务句柄(ISR 唯一通知目标)。 */
TaskHandle_t m_spiTaskHandle = NULL;
TaskHandle_t m_workerTask = NULL;
QueueHandle_t m_jobQueue = NULL;
/** 最近一次 job 执行耗时(µs,worker 写入、业务只读;单核 32 位读原子)。 */
uint32_t m_lastWorkerExecUs = 0;
/** 最近一次 transact 分段剖析(worker 与调用方交叉写入,done 信号量同步)。 */
SpiProfile m_lastProfile;
/** 事件计数信号量(worker push / takeEvent pop)。 */
SemaphoreHandle_t m_eventSem = NULL;
// ── 多槽事件队列(worker 写入,业务 takeEvent 读出)──
static constexpr uint8_t kEventSlots = 4U;
struct EventSlot {
uint8_t type;
uint16_t len;
uint8_t data[SPI_OTA_FRAME_MAX];
};
EventSlot m_eventQ[kEventSlots] = {};
uint8_t m_eventHead = 0;
uint8_t m_eventTail = 0;
uint8_t m_eventCount = 0;
// ── 作业提交(跨任务:固定 Job pool + 静态 binary done;worker 串行执行)──
enum class JobKind : uint8_t {
Transact = 0,
Poll,
Hello,
EnterBoot,
Ping,
WaitBoot,
InitStatus,
};
struct Job {
JobKind kind = JobKind::Transact;
uint32_t timeout_ms = 0;
uint8_t type = 0;
uint8_t expected_type = 0xFFU;
uint8_t role_hint = 0;
size_t payload_len = 0;
uint8_t payload[SPI_OTA_FRAME_MAX] = {};
uint8_t *out_type = nullptr;
uint8_t *out_payload = nullptr;
size_t out_max = 0;
size_t *out_len = nullptr;
uint32_t *out_uptime = nullptr;
gd32_version_t *out_version = nullptr;
gd32_init_status_t *out_init_status = nullptr;
esp_err_t result = ESP_FAIL;
SemaphoreHandle_t done = nullptr; ///< 池槽静态 binary,init 创建,永不 delete
volatile bool finished = false; ///< worker 已 execute + give
volatile bool abandoned = false; ///< 提交方已超时返回,worker 负责 release 槽
volatile bool executing = false; ///< worker 已 dequeue(即将/正在写 out_*;超时放弃分型用)
bool in_use = false; ///< 池槽占用(critical 保护)
};
/** 队列深度;池 = 队列 + 1(在途执行槽),覆盖 queued + executing。 */
static constexpr UBaseType_t kJobQueueLen = 6;
static constexpr UBaseType_t kJobPoolSize = kJobQueueLen + 1U;
Job m_jobPool[kJobPoolSize] = {};
bool m_jobPoolReady = false;
bool onWorker() const;
/** @brief 从固定池取空闲 Job;池耗尽返回 nullptr(ESP_ERR_NO_MEM)。 */
Job *acquireJobSlot();
/** @brief 归还池槽(清空业务字段/标志,drain done 信号量)。 */
void releaseJobSlot(Job *job);
esp_err_t submitJob(Job &job);
void executeJob(Job &job);
/** @brief worker 完成池 Job 后的收尾(give done;abandoned 时 release 槽)。 */
void finishPoolJob(Job *job);
/** @brief FreeRTOS 任务入口,转到 workerLoop。 */
static void workerEntry(void *arg);
/** @brief SPI worker 主循环:EVENT 按铃 + Job 队列。 */
void workerLoop();
/** @brief 单次服务 GD32 按铃读帧并入 EVENT 队列。 */
esp_err_t serviceRingOnce();
/** @brief EVENT 入队(队列满丢最旧)。 */
bool pushEvent(uint8_t type, const uint8_t *payload, size_t len);
/** @brief EVENT 出队(非阻塞)。 */
bool popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len);
/**
* @brief 解析并缓存 APP 身份 16B(HELLO_ACK 附加块 / PING body 同布局)。
* @param id 身份块
* @param out_version 可选输出版本
* @param out_uptime_sec 可选输出 uptime 秒
*/
void applyAppIdentity16(const uint8_t *id, gd32_version_t *out_version,
uint32_t *out_uptime_sec);
// ── 帧内同步状态机 ──
volatile uint8_t m_localCredit = 8; ///< 本端剩余可接收帧数(信用窗口)
volatile uint8_t m_remoteCredit = 8; ///< 对端上报的剩余信用
// ── SPI Link v2 会话/序号状态(阶段1 就位,阶段2 启用校验)──
volatile uint32_t m_sessionId = 0; ///< 会话 ID(0 = 未建立,HELLO 时由 ESP32 生成)
volatile uint8_t m_txSeq = 0; ///< 本端发送序号(每发一帧递增,重发不变)
volatile uint8_t m_lastSentSeq = 0; ///< 上次发送的序号(用于响应归属匹配)
volatile uint8_t m_rxAckedSeq = 0; ///< 已确认的对端最新序号
volatile uint8_t m_expectedRxSeq = 0; ///< 期望收到的对端序号
volatile uint8_t m_gd32Role = 0; ///< GD32 角色(0=unknown, 1=app, 2=boot)
volatile bool m_gd32FeatKnown = false; ///< HELLO/PING 身份是否带 feat_map
volatile uint32_t m_gd32FeatFlags = 0; ///< V2_FEAT_*
volatile bool m_gd32IdKnown = false; ///< 是否已缓存版本号
gd32_version_t m_gd32Version = {}; ///< HELLO/PING 缓存的版本
volatile uint8_t m_gd32InitState = GD32_INIT_STATE_UNKNOWN; ///< 身份块 [15] init_state
gd32_init_status_t m_gd32InitStatus = {}; ///< 0xA3 查询缓存(valid 标志内置)
volatile bool m_v2Active = false; ///< v2 session 是否激活
/** 最近一次 otaFindFrame 解析到的 RESPONSE 是否与 m_lastSentSeq 归属匹配 */
volatile bool m_lastRxAckMatched = false;
SpiLinkStats m_stats = {}; ///< 链路统计
// RAII 锁守卫
class LockGuard {
public:
explicit LockGuard(SemaphoreHandle_t lock) : m_lock(lock) {
if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); }
}
~LockGuard() {
if (m_lock) { xSemaphoreGive(m_lock); }
}
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
private:
SemaphoreHandle_t m_lock;
};
/** @brief GPIO 边沿中断回调(注册到 ESP-IDF GPIO ISR 服务)。 */
static void IRAM_ATTR onReadyEdgeISR(void *arg);
/** @brief APP 独有业务 TYPE:见到即可纠正 m_gd32Role(BOOT 不会发这些)。 */
static bool isAppOnlySpiType(uint8_t type);
// ── 去重辅助 ──
/** @brief 把 otaFindFrame 找到的帧拷贝到调用方输出缓冲(安全截断)。 */
static void copyFoundFrame(uint8_t *resp_type, uint8_t found_type,
uint8_t *resp_payload, size_t resp_max,
const uint8_t *found_payload, size_t found_len,
size_t *resp_len);
/** @brief 见到 APP 独有 TYPE 时纠正粘滞的 boot role(3 处调用合并)。 */
void correctRoleFromType(uint8_t found_type, const char *src);
// 内部 SPI 传输
/**
* @brief 执行底层 SPI 时钟同步传输。
* @param tx_buf 发送数据缓冲区。
* @param tx_len 发送数据长度。
* @param rx_buf 接收数据缓冲区。
* @param rx_len 接收数据长度。
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms = 0U);
/**
* @brief 底层传输,支持 SPI 事务 flags(如 SPI_TRANS_CS_KEEP_ACTIVE)。
* 多段同 CS 变长读时:先 spi_device_acquire_bus,段间 KEEP_ACTIVE,末段清 flag 后 release_bus。
*/
esp_err_t transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms, uint32_t trans_flags);
/**
* @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 budget_hint 读预算提示(0=按角色默认;>0 时取 min(hint, 角色预算),
* 主动 poll 按最近请求类型收紧,省 0xFF 填充时钟)
*/
esp_err_t readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
uint32_t timeout_ms, size_t budget_hint = 0U);
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
/**
* @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms,
uint8_t expected_type = 0xFFU);
/** @brief 无锁版本的 otaTransfer(调用者须已持有锁)。
* @param expect_stale true 表示调用方预期读到过期/空响应(如 otaSendAndPoll 步骤1),
* 找不到帧时降级为 DBG 日志,不打 WARN。 */
esp_err_t otaTransferUnlocked(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,
bool expect_stale = false);
// OTA 帧工具(静态,无状态)
static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *out, size_t *out_len);
static bool otaFindFrame(const uint8_t *buf, size_t buf_len,
uint8_t *type, const uint8_t **payload, size_t *payload_len,
size_t *frame_offset, uint8_t *out_flags = nullptr);
};
#endif // SPI_MASTER_H