Files
ESP32_COMM/main/transport/spi_master.cpp
T
小龙虾 770ae14156
SPI Link v2 Self-Test / selftest (push) Has been cancelled
feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
添加SPI Link v2的PC端自测功能,包含交叉校验基准测试,覆盖HELLO/OTA/CAN/PING等
多种协议场景。同时重构了health monitor中的多字段读取逻辑,消除了volatile变量
的撕裂读问题,并增强了BLE OTA的数据传输可靠性。

refactor(health_monitor): 修复heap hook多字段撕裂读问题

采用portMUX_CRITICAL临界区实现多字段一致快照读取,消除任务上下文与heap hook
之间的数据竞争。修改了HookSnap结构体设计,通过一次性拷贝volatile变量避免
跨字段组合时的数据不一致。JSON输出中明确标注estimated值的置信度。

fix(ble_ota): 实现DATA_IN丢包负反馈机制

当malloc失败或队列满时发送缺口ACK通知前端按completed_bytes续传,避免
静默丢包导致的8秒超时重传风暴。增加节流机制防止负ACK在重传叠加时打爆
NimBLE mbuf池,提升低内存环境下的OTA传输稳定性。

refactor(spi_master): 从堆分配改为固定Job池管理

将SPI worker的动态堆分配Job改为固定大小的静态池管理,避免频繁的
calloc/free操作影响实时性。新增acquireJobSlot/releaseJobSlot机制
管理池槽生命周期,提升SPI通信的可靠性和内存使用效率。
2026-07-21 01:16:43 +08:00

1982 lines
73 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.cpp - SPI Master 驱动实现
* T1.1 - T1.4
* ESP-IDF 6.0 适配版本
* CANFD 3通道发送接口
*
* 所有功能封装为 SpiMaster 类方法。
*/
#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "soc/gpio_reg.h"
#include "soc/spi_struct.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_random.h" // esp_random() for HELLO session_id
#include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM
#include "spi_master.h"
#include "spi_link_v2.h"
#include "app_config.h"
#include "monitor/debug_log.h"
static constexpr size_t kSpiTransferMaxBytes = 256U;
/** @brief submitJob 池 Job 与 worker 之间的 finished/abandoned/in_use 交接。 */
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(256)。
* APP 侧 TX 满缓冲 256:poll/ring 读预算见 spiRespReadBudget()。
*/
static size_t spiXferBytes(size_t frame_len)
{
size_t n = frame_len;
if (n < 64U) {
n = 64U;
}
if (n > SPI_OTA_FRAME_MAX) {
n = SPI_OTA_FRAME_MAX;
}
return n;
}
/**
* @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)
{
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)。
* 用于 SPIready 边沿中断,避免在 cache 被禁用期间访问 Flash 代码。
*/
static int IRAM_ATTR readReadyLevelRaw(void)
{
return (REG_READ(GPIO_IN_REG) >> SPI_READY_GPIO) & 0x1;
}
/* 前向声明:waitSpiDataReady 定义在文件下方,
* 但 serviceRingOnce / otaPollResponseUnlocked 等更早出现的方法需要它。 */
static esp_err_t waitSpiDataReady(uint32_t timeout_ms);
// ============================================================
// 单例
// ============================================================
/**
* @brief 获取 SpiMaster 单例实例(单例模式)。
* @return SpiMaster 单例引用。
*/
SpiMaster& SpiMaster::instance()
{
static SpiMaster inst;
return inst;
}
// ============================================================
// SPI Worker:唯一持 SPI 的任务;ISR 只通知本任务
// ============================================================
/** @brief 当前任务是否为 SPI worker(仅 worker 可直接走无锁路径)。 */
bool SpiMaster::onWorker() const
{
return m_workerTask != nullptr && xTaskGetCurrentTaskHandle() == m_workerTask;
}
/**
* @brief 将 GD32 EVENT 帧入队(worker 上下文)。
* @param type 业务 TYPE(如 0xC1/0xC4)
* @param payload 载荷指针,可为 nullptr
* @param len 载荷长度
* @return 始终 true;队列满时丢最旧一帧再写入
*/
bool SpiMaster::pushEvent(uint8_t type, const uint8_t *payload, size_t len)
{
if (m_eventCount >= kEventSlots) {
/* 队列满:丢最旧一帧,保证新事件可进 */
m_eventTail = (uint8_t)((m_eventTail + 1U) % kEventSlots);
m_eventCount--;
if (m_eventSem) {
(void)xSemaphoreTake(m_eventSem, 0);
}
}
EventSlot &slot = m_eventQ[m_eventHead];
slot.type = type;
slot.len = (uint16_t)((len > sizeof(slot.data)) ? sizeof(slot.data) : len);
if (payload && slot.len > 0U) {
memcpy(slot.data, payload, slot.len);
}
m_eventHead = (uint8_t)((m_eventHead + 1U) % kEventSlots);
m_eventCount++;
if (m_eventSem) {
(void)xSemaphoreGive(m_eventSem);
}
return true;
}
/**
* @brief 从事件队列弹出一帧(非阻塞)。
* @param[out] type 业务 TYPE
* @param[out] payload 拷贝载荷;可为 nullptr
* @param max_len payload 缓冲容量
* @param[out] len 实际拷贝长度
* @return true 取到一帧;false 队列空
*/
bool SpiMaster::popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len)
{
if (m_eventCount == 0U) {
return false;
}
EventSlot &slot = m_eventQ[m_eventTail];
if (type) {
*type = slot.type;
}
size_t copy_len = slot.len;
if (copy_len > max_len) {
copy_len = max_len;
}
if (len) {
*len = copy_len;
}
if (payload && copy_len > 0U) {
memcpy(payload, slot.data, copy_len);
}
m_eventTail = (uint8_t)((m_eventTail + 1U) % kEventSlots);
m_eventCount--;
return true;
}
/**
* @brief 暂停/恢复对 GD32 主动按铃的 serviceRing 处理。
* @param suppress true=OTA 等关键路径禁止偷读 PB6;false=恢复
*/
void SpiMaster::setSuppressRingService(bool suppress)
{
m_suppressRingService = suppress;
if (suppress) {
/* 丢弃已锁存的按铃标志,避免解除后立刻误读残留边沿 */
m_gd32RequestPending = false;
}
}
/**
* @brief 业务侧确认本端 TX 序号已被对端接受,推进 m_txSeq。
* @note 在收到明确业务 ACK 后调用,避免重发与序号失配。
*/
void SpiMaster::confirmTxSeqAccepted()
{
/* 业务 ACK 已确认:保证下一包使用 lastSentSeq+1,即使头 ack_seq 曾失配 */
m_txSeq = (uint8_t)(m_lastSentSeq + 1U);
m_rxAckedSeq = m_lastSentSeq;
m_lastRxAckMatched = true;
}
/**
* @brief worker 单次服务 GD32 按铃:读一帧并入 EVENT 队列。
* @return ESP_OK 入队成功;ESP_ERR_NOT_FOUND 无线/被抑制;其它为读失败
*/
esp_err_t SpiMaster::serviceRingOnce()
{
/* OTA 中继期间禁止按铃读:OTA ACK/WAIT 也拉低 PB6,偷读会导致 poll 空等。 */
if (m_suppressRingService) {
m_gd32RequestPending = false;
return ESP_ERR_NOT_FOUND;
}
/* 仅 worker 调用。有 pending 或 DATA_READY 低时读一帧 EVENT。 */
if (!m_gd32RequestPending) {
/* 无 ISR 标志时,若线已低仍尝试读(丢边沿兜底) */
if (readReadyLevelRaw() != 0) {
return ESP_ERR_NOT_FOUND;
}
}
m_gd32RequestPending = false;
m_linkState.enterBusy(HandshakeState::SERVING_GD32_RING);
if (waitSpiDataReady(3U) != ESP_OK) {
m_linkState.exitBusy();
return ESP_ERR_NOT_FOUND;
}
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 500U);
m_linkState.exitBusy();
if (ret != ESP_OK) {
return ret;
}
uint8_t found_type = 0;
const uint8_t *found_payload = nullptr;
size_t found_len = 0;
size_t frame_offset = 0;
if (!otaFindFrame(rx_raw, rx_len, &found_type, &found_payload,
&found_len, &frame_offset)) {
return ESP_ERR_INVALID_RESPONSE;
}
if (found_type == V2_TYPE_CREDIT_UPDATE) {
DEBUG_DBG("SPI", "worker CREDIT_UPDATE credit=%u", (unsigned)m_remoteCredit);
return ESP_ERR_NOT_FOUND;
}
/* RESPONSE(OTA ACK/WAIT/ERROR)绝不能当 EVENT 入队;否则 OTA poll 丢响应。
* 此处帧已被时钟读走,只能记统计;真正防护靠 m_suppressRingService。 */
if (found_type == SPI_OTA_RSP_ACK || found_type == SPI_OTA_RSP_WAIT ||
found_type == SPI_OTA_RSP_ERROR || found_type == V2_TYPE_RSP_ACK ||
found_type == V2_TYPE_RSP_WAIT || found_type == V2_TYPE_RSP_ERROR) {
static uint32_t s_steal_logs = 0U;
if ((++s_steal_logs % 16U) == 1U) {
DEBUG_WARN("SPI", "serviceRing 读到 RESPONSE type=0x%02X(应禁止,log#%lu)",
found_type, (unsigned long)s_steal_logs);
}
return ESP_ERR_INVALID_RESPONSE;
}
{
LockGuard lock(m_mutex);
(void)pushEvent(found_type, found_payload, found_len);
}
return ESP_OK;
}
/**
* @brief worker 执行一条排队 Job(Transact/Hello/Ping/EnterBoot 等)。
* @param job 入出参:写入 job.result
*/
void SpiMaster::executeJob(Job &job)
{
/* 已在 worker 上下文;下列 public API 见 onWorker() 后走实现体,不再二次入队。 */
switch (job.kind) {
case JobKind::Transact:
job.result = otaSendAndPoll(job.type,
job.payload_len ? job.payload : nullptr,
job.payload_len,
job.out_type, job.out_payload, job.out_max,
job.out_len, job.timeout_ms);
break;
case JobKind::Poll:
job.result = otaPollResponse(job.out_type, job.out_payload, job.out_max,
job.out_len, job.timeout_ms);
break;
case JobKind::Hello:
job.result = hello(job.timeout_ms, job.role_hint);
break;
case JobKind::EnterBoot:
job.result = appRequestBootloader();
break;
case JobKind::Ping:
job.result = appPing(job.timeout_ms, job.out_uptime, job.out_version);
break;
case JobKind::WaitBoot:
job.result = waitForBootloaderReady(job.timeout_ms);
break;
default:
job.result = ESP_ERR_INVALID_ARG;
break;
}
}
/**
* @brief 从固定 Job 池取空闲槽;池耗尽返回 nullptr。
* @note in_use 在 critical 内置 true 并保持,禁止整体 Job{} 赋值清掉占用标志。
*/
SpiMaster::Job *SpiMaster::acquireJobSlot()
{
if (!m_jobPoolReady) {
return nullptr;
}
Job *slot = nullptr;
taskENTER_CRITICAL(&s_spiJobMux);
for (UBaseType_t i = 0; i < kJobPoolSize; ++i) {
if (!m_jobPool[i].in_use) {
slot = &m_jobPool[i];
/* 清业务字段但 in_use 全程 true,避免与其它 acquire 竞态 */
SemaphoreHandle_t done = slot->done;
slot->kind = JobKind::Transact;
slot->timeout_ms = 0U;
slot->type = 0U;
slot->expected_type = 0xFFU;
slot->role_hint = 0U;
slot->payload_len = 0U;
slot->out_type = nullptr;
slot->out_payload = nullptr;
slot->out_max = 0U;
slot->out_len = nullptr;
slot->out_uptime = nullptr;
slot->out_version = nullptr;
slot->result = ESP_FAIL;
slot->done = done;
slot->finished = false;
slot->abandoned = false;
slot->in_use = true;
break;
}
}
taskEXIT_CRITICAL(&s_spiJobMux);
if (slot == nullptr) {
return nullptr;
}
/* 仅本槽所有者 drain;不可在 critical 内 take */
if (slot->done != nullptr) {
while (xSemaphoreTake(slot->done, 0) == pdTRUE) {
}
}
return slot;
}
/**
* @brief 归还池槽(调用方已独占该 Job,不再与 worker 共享)。
*/
void SpiMaster::releaseJobSlot(Job *job)
{
if (job == nullptr) {
return;
}
if (job->done != nullptr) {
while (xSemaphoreTake(job->done, 0) == pdTRUE) {
}
}
job->out_type = nullptr;
job->out_payload = nullptr;
job->out_len = nullptr;
job->out_uptime = nullptr;
job->out_version = nullptr;
job->payload_len = 0U;
job->result = ESP_FAIL;
taskENTER_CRITICAL(&s_spiJobMux);
job->finished = false;
job->abandoned = false;
job->in_use = false;
taskEXIT_CRITICAL(&s_spiJobMux);
}
/**
* @brief worker 完成池 Job 后的收尾:give done;abandoned 时由 worker release 槽。
* @param job 池内 Job 指针
*/
void SpiMaster::finishPoolJob(Job *job)
{
if (job == nullptr) {
return;
}
/* 先 give,再标 finished;提交方 take 成功后会 spin 等 finished 再 release。 */
if (job->done) {
(void)xSemaphoreGive(job->done);
}
taskENTER_CRITICAL(&s_spiJobMux);
job->finished = true;
const bool release_it = job->abandoned;
taskEXIT_CRITICAL(&s_spiJobMux);
if (release_it) {
releaseJobSlot(job);
}
}
/**
* @brief 将 Job 提交给 SPI worker 串行执行(非 worker 线程走队列)。
* @param job 任务描述(栈上字段拷入固定池槽再排队)
* @return 业务结果码;池耗尽 ESP_ERR_NO_MEM;队列满/超时 ESP_ERR_TIMEOUT
*/
esp_err_t SpiMaster::submitJob(Job &job)
{
if (m_jobQueue == nullptr || m_workerTask == nullptr || onWorker()) {
executeJob(job);
return job.result;
}
/* 跨任务:固定池 Job + 静态 binary 完成信号量(无 calloc/xSemaphoreCreate) */
Job *pool_job = acquireJobSlot();
if (pool_job == nullptr) {
DEBUG_ERR("SPI", "submitJob: job pool exhausted kind=%u", (unsigned)job.kind);
return ESP_ERR_NO_MEM;
}
/* 逐字段拷贝业务参数;禁止整体赋值覆盖 done/in_use */
pool_job->kind = job.kind;
pool_job->timeout_ms = job.timeout_ms;
pool_job->type = job.type;
pool_job->expected_type = job.expected_type;
pool_job->role_hint = job.role_hint;
pool_job->payload_len = job.payload_len;
if (job.payload_len > 0U) {
const size_t n = (job.payload_len > sizeof(pool_job->payload))
? sizeof(pool_job->payload)
: job.payload_len;
memcpy(pool_job->payload, job.payload, n);
pool_job->payload_len = n;
} else {
pool_job->payload_len = 0U;
}
pool_job->out_type = job.out_type;
pool_job->out_payload = job.out_payload;
pool_job->out_max = job.out_max;
pool_job->out_len = job.out_len;
pool_job->out_uptime = job.out_uptime;
pool_job->out_version = job.out_version;
pool_job->result = ESP_FAIL;
pool_job->finished = false;
pool_job->abandoned = false;
/* done / in_use 保持 acquire 时状态 */
const TickType_t qwait = pdMS_TO_TICKS(
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 2000U);
if (xQueueSend(m_jobQueue, &pool_job, qwait) != pdTRUE) {
DEBUG_WARN("SPI", "worker job queue full/timeout kind=%u", (unsigned)job.kind);
releaseJobSlot(pool_job);
return ESP_ERR_TIMEOUT;
}
xTaskNotify(m_workerTask, 0x10U, eSetBits);
const TickType_t dwait = pdMS_TO_TICKS(
(job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 10000U);
if (xSemaphoreTake(pool_job->done, dwait) == pdTRUE) {
const esp_err_t result = pool_job->result;
/* worker 在 give 后才写 finished;等其退出临界区后再由本侧独占 release。 */
for (;;) {
taskENTER_CRITICAL(&s_spiJobMux);
const bool fin = pool_job->finished;
taskEXIT_CRITICAL(&s_spiJobMux);
if (fin) {
break;
}
vTaskDelay(1);
}
releaseJobSlot(pool_job);
return result;
}
/* 超时:标记 abandoned;若 worker 已跑完则本侧 release,否则 finishPoolJob release。 */
taskENTER_CRITICAL(&s_spiJobMux);
pool_job->abandoned = true;
const bool already_done = pool_job->finished;
taskEXIT_CRITICAL(&s_spiJobMux);
if (already_done) {
(void)xSemaphoreTake(pool_job->done, 0);
const esp_err_t result = pool_job->result;
releaseJobSlot(pool_job);
return result;
}
DEBUG_ERR("SPI", "worker job done timeout kind=%u(池槽交 worker 释放)",
(unsigned)job.kind);
return ESP_ERR_TIMEOUT;
}
/** @brief FreeRTOS 任务入口,转到 workerLoop。 */
void SpiMaster::workerEntry(void *arg)
{
static_cast<SpiMaster *>(arg)->workerLoop();
}
/**
* @brief SPI worker 主循环:处理按铃 EVENT + 执行 job 队列。
* @note 全栈唯一允许直接访问 SPI 总线的任务上下文。
*/
void SpiMaster::workerLoop()
{
m_workerTask = xTaskGetCurrentTaskHandle();
m_spiTaskHandle = m_workerTask;
DEBUG_INFO("SPI", "SPI worker started, handle=%p", (void *)m_workerTask);
for (;;) {
/* ★ 只在 ISR 置位 m_gd32RequestPending 时读 EVENT。
* 禁止见 DATA_READY 低就 serviceRing:命令 HELLO_ACK/OTA 响应也会拉低
* PB6,会被偷走当事件 → 业务 poll 全 0xFF,OTA/ENTER_BOOT 全挂。
* OTA 中继期间 m_suppressRingService 屏蔽按铃读。 */
if (m_gd32RequestPending && !m_suppressRingService) {
(void)serviceRingOnce();
} else if (m_suppressRingService) {
m_gd32RequestPending = false;
}
Job *job = nullptr;
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) {
executeJob(*job);
finishPoolJob(job);
continue;
}
/* 无 job:等 GPIO / 提交唤醒。
* 超时只是无事件时的兜底回扫周期;ISR 的 xTaskNotifyFromISR 会立即唤醒
* 本任务,响应延迟不由这里决定。过短会令 spi_worker(优先级6) 忙转压住
* httpd(优先级5),导致 /system/status、/logs 响应不来 → 网页收不到消息。 */
uint32_t bits = 0U;
(void)xTaskNotifyWait(0U, 0xFFFFFFFFU, &bits, pdMS_TO_TICKS(25));
if (m_suppressRingService) {
m_gd32RequestPending = false;
} else if ((bits & 0x01U) || m_gd32RequestPending) {
(void)serviceRingOnce();
}
}
}
/**
* @brief GPIO 边沿中断回调(DATA_READY / PB6)。
*
* 仅处理下降沿:GD32 有待读数据(按铃)。上升沿不再使用(旧的"等上升沿
* ready"逻辑已删除,现由 waitSpiDataReady 做电平轮询)。
* ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。
*
* 本端忙时(acceptRing()==false):记下 pending 但不 notify,避免抢断
* 当前 SPI 事务;事务结束 worker 下一轮会立即消费此标志,省去最多 25ms
* 的回扫延迟。PB6 在被读走前保持低、不会产生新下降沿,不记 pending 即丢唤醒。
*/
void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
{
SpiMaster *self = static_cast<SpiMaster *>(arg);
if (!self) { return; }
/* 只关心低电平(下降沿后采样):有数据待读 */
if (readReadyLevelRaw() != 0) {
return;
}
if (self->m_suppressRingService) {
/* OTA 中继:PB6 低是 WAIT/ACK 就绪,留给业务 poll,不按 EVENT 处理 */
return;
}
if (!self->m_linkState.acceptRing()) {
/* 本端忙:记下 pending,不 notify 抢断当前事务;
* 事务结束 worker 下一轮会消费此标志(PB6 保持低、无新下降沿,
* 不记则只能等 25ms 回扫兜底)。 */
self->m_gd32RequestPending = true;
return;
}
self->m_gd32RequestPending = true;
if (self->m_spiTaskHandle) {
BaseType_t hp = pdFALSE;
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x01U, eSetBits, &hp);
portYIELD_FROM_ISR(hp);
}
}
/**
* @brief 等 GD32 拉低 PB6(有待读响应)。
* v2 DATA_READY:低 = 有数据;高 = 队列空。TX 前不等 HIGH(CS 即互斥)。
* @return ESP_OK 已低;ESP_ERR_TIMEOUT 超时仍为高(调用方可再决定是否盲 poll)。
*/
static esp_err_t waitSpiDataReady(uint32_t timeout_ms)
{
if (readReadyLevelRaw() == 0) { return ESP_OK; } /* 已低 = 有数据 */
if (timeout_ms == 0U) { return ESP_ERR_TIMEOUT; }
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
while (xTaskGetTickCount() < deadline) {
if (readReadyLevelRaw() == 0) { return ESP_OK; }
vTaskDelay(pdMS_TO_TICKS(1) > 0 ? pdMS_TO_TICKS(1) : 1);
}
return ESP_ERR_TIMEOUT;
}
// ============================================================
// 内部 SPI 传输
// ============================================================
/**
* @brief 执行底层 SPI 时钟同步传输。
* @param tx_buf 发送数据缓冲区,可为 NULL。
* @param tx_len 发送数据长度。
* @param rx_buf 接收数据缓冲区,可为 NULL。
* @param rx_len 接收数据长度。
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms)
{
return transferClockedFlags(tx_buf, tx_len, rx_buf, rx_len, timeout_ms, 0U);
}
esp_err_t SpiMaster::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)
{
if (m_spiDevice == NULL) {
return ESP_ERR_INVALID_STATE;
}
if ((tx_len > 0U && tx_buf == NULL) || (rx_len > 0U && rx_buf == NULL)) {
DEBUG_WARN("SPI", "xfer INVALID_ARG tx=%u rx=%u dev=%p",
(unsigned)tx_len, tx_buf, (unsigned)rx_len, rx_buf, m_spiDevice);
return ESP_ERR_INVALID_ARG;
}
const size_t total_len = (tx_len > rx_len) ? tx_len : rx_len;
if (total_len == 0U) {
return ESP_OK;
}
if (total_len > kSpiTransferMaxBytes) {
return ESP_ERR_INVALID_SIZE;
}
uint8_t tx_staging[kSpiTransferMaxBytes];
uint8_t rx_staging[kSpiTransferMaxBytes];
const uint8_t *tx_ptr = tx_buf;
uint8_t *rx_ptr = rx_buf;
if (tx_len < total_len) {
memset(tx_staging, 0xFF, total_len);
if (tx_buf != NULL && tx_len > 0U) {
memcpy(tx_staging, tx_buf, tx_len);
}
tx_ptr = tx_staging;
}
if (rx_len > 0U && rx_len < total_len) {
rx_ptr = rx_staging;
}
spi_transaction_t t = {};
t.flags = trans_flags;
t.length = total_len * 8U;
t.rxlength = (rx_len > 0U) ? (total_len * 8U) : 0U;
t.tx_buffer = tx_ptr;
t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL;
/* SPI 事务等待使用有界超时 */
const uint32_t effective_ms = (timeout_ms == 0U) ? 500U
: (timeout_ms < 50U ? 50U : timeout_ms);
const TickType_t queue_wait = pdMS_TO_TICKS(effective_ms);
esp_err_t ret = spi_device_queue_trans(m_spiDevice, &t, queue_wait);
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "spi_device_queue_trans fail: %s len=%u",
esp_err_to_name(ret), (unsigned)(total_len * 8U));
return ret;
}
spi_transaction_t *ret_trans = NULL;
/* 结果等待:调用方超时 + 余量,硬上限 2s,绝不用无限等 */
uint32_t result_ms = effective_ms + 200U;
if (result_ms < 100U) {
result_ms = 100U;
}
if (result_ms > 2000U) {
result_ms = 2000U;
}
const TickType_t result_wait = pdMS_TO_TICKS(result_ms);
ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, result_wait);
if (ret != ESP_OK) {
/* 再给一次有界抢救(最多 1s),清掉可能残留的 in-flight 事务。
* 使用有界超时,避免对端无响应时 worker 永久阻塞。 */
DEBUG_WARN("SPI", "spi_device_get_trans_result timeout: %s len=%u, bounded retry 1s",
esp_err_to_name(ret), (unsigned)(total_len * 8U));
ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, pdMS_TO_TICKS(1000));
if (ret != ESP_OK) {
DEBUG_ERR("SPI", "trans_result放弃:%s 或复位SPI",
esp_err_to_name(ret));
/* 尽力排空 ret_queue,避免后续 queue_trans 永久堵死 */
spi_transaction_t *junk = nullptr;
while (spi_device_get_trans_result(m_spiDevice, &junk, 0) == ESP_OK) {
(void)junk;
}
return ESP_ERR_TIMEOUT;
}
(void)ret_trans;
return ESP_ERR_TIMEOUT;
}
(void)ret_trans;
if (rx_len > 0U && rx_ptr != rx_buf) {
memcpy(rx_buf, rx_ptr, rx_len);
}
return ESP_OK;
}
/**
* @brief 读一帧响应(单 CS 固定预算,再 v2_parse 截真实长度)。
*
* 不用两段 KEEP_ACTIVE:Boot 在 CS 释放时无论是否读完都清 s_txPending,
* 中途断 CS 会丢 HELLO_ACK,后续 MISO 全 0xFF(线上已复现)。
* 时钟预算见 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)
{
if (out_frame == nullptr || out_len == nullptr || out_max < V2_HEADER_LEN) {
return ESP_ERR_INVALID_ARG;
}
if (m_spiDevice == nullptr) {
return ESP_ERR_INVALID_STATE;
}
*out_len = 0U;
size_t budget = spiRespReadBudget();
if (budget > out_max) {
budget = out_max;
}
if (budget > kSpiTransferMaxBytes) {
budget = kSpiTransferMaxBytes;
}
uint8_t dummy[kSpiTransferMaxBytes];
memset(dummy, 0xFF, budget);
memset(out_frame, 0xFF, budget);
esp_err_t ret = transferClocked(dummy, budget, out_frame, budget, timeout_ms);
if (ret != ESP_OK) {
return ret;
}
v2_header_t hdr;
const uint8_t *pload = nullptr;
if (!v2_parse_frame(out_frame, budget, &hdr, &pload)) {
return ESP_ERR_INVALID_RESPONSE;
}
const size_t total = V2_HEADER_LEN + (size_t)hdr.payload_len + V2_CRC_LEN;
if (total > out_max) {
return ESP_ERR_INVALID_SIZE;
}
/* parse 可能从 offset>0 找到帧:搬到缓冲区头,方便调用方 */
if (hdr.frame_offset > 0U && hdr.frame_offset + total <= budget) {
memmove(out_frame, out_frame + hdr.frame_offset, total);
}
*out_len = total;
return ESP_OK;
}
// ============================================================
// OTA 帧工具
// ============================================================
/*
* otaBuildFrame:使用当前 session/seq 成员填充 v2 帧头,FLAGS=REQUEST。
* 记录 m_lastSentSeq 供响应归属匹配。
*/
bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *out, size_t *out_len)
{
if (!out || !out_len) return false;
if (payload_len > 0U && payload == NULL) return false;
SpiMaster &self = SpiMaster::instance();
const uint8_t flags = V2_FLAG_REQUEST;
const uint8_t tx_seq = self.m_txSeq;
const uint8_t ack_seq = self.m_rxAckedSeq;
const uint8_t credit = self.m_localCredit;
self.m_lastSentSeq = tx_seq; /* 记录发送序号供响应归属匹配 */
return v2_build_frame(type, flags, self.m_sessionId,
tx_seq, ack_seq, credit,
payload, payload_len, out, out_len);
}
/*
* otaFindFrame:委托 v2_parse_frame + SESSION/SEQ 校验。
* ESP32 是主机,收到 GD32 的 RESPONSE/EVENT 帧:
* - ack_seq 携带"GD32 确认收到的 ESP32 请求序号",用于响应归属匹配
* - tx_seq 是 GD32 的发送序号
*/
bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
uint8_t *type, const uint8_t **payload, size_t *payload_len,
size_t *frame_offset)
{
if (!buf) return false;
v2_header_t hdr;
const uint8_t *pload = nullptr;
if (!v2_parse_frame(buf, buf_len, &hdr, &pload)) {
return false;
}
SpiMaster &self = SpiMaster::instance();
/* 更新对端 credit */
self.m_remoteCredit = hdr.credit;
if (self.m_v2Active) {
/* SESSION_ID 校验 */
if (hdr.session_id != self.m_sessionId) {
self.m_stats.seq_errors++;
const bool is_event = (hdr.flags & V2_FLAG_EVENT) != 0U;
/* EVENT 按铃帧在 GD32 session 未激活时 session_id=0(见 requestEsp32Read)。
* 若此处直接拒绝:按铃被读走却解析失败 → 狂刷 WARN,CAN 反馈全丢。
* 策略:EVENT 一律放行;REQUEST/RESPONSE 仍拒绝错 session。 */
if (!is_event) {
static uint32_t s_sess_mm_logs = 0U;
if ((++s_sess_mm_logs % 32U) == 1U) {
DEBUG_WARN("SPI", "v2 SES mismatch got=%08lX exp=%08lX t=%02X #%lu",
(unsigned long)hdr.session_id, (unsigned long)self.m_sessionId,
hdr.type, (unsigned long)s_sess_mm_logs);
}
return false;
}
static uint32_t s_evt_sess0_logs = 0U;
if ((++s_evt_sess0_logs % 64U) == 1U) {
DEBUG_DBG("SPI", "EVENT session mismatch accepted: got=0x%08lX exp=0x%08lX type=0x%02X",
(unsigned long)hdr.session_id, (unsigned long)self.m_sessionId, hdr.type);
}
}
/* 响应归属:ack_seq 应等于 m_lastSentSeq。
*
* ★ 关键:RESPONSE(ACK/WAIT/ERROR)在 CS 上已被读走,GD32 会清 s_txPending。
* 若因 ack_seq 失配直接 return false,业务层永远看不到该帧 → 空等/重发/卡死。
* 策略:CRC+session 合法的 RESPONSE 一律交给调用方;用 m_lastRxAckMatched
* 标记是否归属当前请求。业务层以结构化 payload(BA C0+seq)做最终判定。 */
if (hdr.flags & V2_FLAG_RESPONSE) {
self.m_lastRxAckMatched = (hdr.ack_seq == self.m_lastSentSeq);
if (!self.m_lastRxAckMatched) {
static uint32_t s_stale_logs = 0U;
if ((++s_stale_logs % 32U) == 1U) {
DEBUG_DBG("SPI", "v2 stale rsp t=0x%02X ack=%u exp=%u #%lu",
hdr.type, hdr.ack_seq, self.m_lastSentSeq,
(unsigned long)s_stale_logs);
}
} else {
self.m_rxAckedSeq = self.m_lastSentSeq;
}
} else {
self.m_lastRxAckMatched = false;
}
/* 更新期望的对端序号(EVENT 帧的 tx_seq) */
if (hdr.flags & V2_FLAG_EVENT) {
self.m_expectedRxSeq = hdr.tx_seq + 1U;
}
}
if (type) *type = hdr.type;
if (payload) *payload = pload;
if (payload_len) *payload_len = hdr.payload_len;
if (frame_offset) *frame_offset = hdr.frame_offset;
return true;
}
// ============================================================
// 初始化
// ============================================================
/**
* @brief 初始化 SPI2 总线与目标从设备。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::init()
{
/* 先创建互斥锁,再进入临界区 */
if (!m_mutex) {
m_mutex = xSemaphoreCreateMutex();
if (m_mutex == NULL) {
DEBUG_ERR("SPI", "SPI 互斥锁创建失败");
return ESP_ERR_NO_MEM;
}
}
LockGuard lock(m_mutex);
DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ===");
spi_bus_config_t bus_cfg = {};
bus_cfg.miso_io_num = 7;
bus_cfg.mosi_io_num = 5;
bus_cfg.sclk_io_num = 6;
bus_cfg.quadwp_io_num = -1;
bus_cfg.quadhd_io_num = -1;
bus_cfg.max_transfer_sz = kSpiTransferMaxBytes * SPI_QUEUE_SIZE;
esp_err_t ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
DEBUG_ERR("SPI", "SPI 总线初始化失败: %s", esp_err_to_name(ret));
return ret;
}
spi_device_interface_config_t dev_cfg = {};
dev_cfg.clock_speed_hz = SPI_CLOCK_HZ;
dev_cfg.mode = SPI_MODE;
dev_cfg.spics_io_num = SPI_MASTER_CS_GPIO;
dev_cfg.queue_size = SPI_QUEUE_SIZE;
dev_cfg.command_bits = 0;
dev_cfg.address_bits = 0;
dev_cfg.dummy_bits = 0;
/* 加长 CS 建立时间,给 GD32 EXTI/预装 DMA 留余量(单位:SPI 半周期相关) */
dev_cfg.cs_ena_posttrans = 32;
dev_cfg.cs_ena_pretrans = 64;
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &m_spiDevice);
if (ret != ESP_OK) {
DEBUG_ERR("SPI", "SPI 设备添加失败: %s", esp_err_to_name(ret));
return ret;
}
DEBUG_INFO("SPI", "SPI2 ok CS=GPIO%d CLK=%dHz",
SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ);
/* 配置 SPI Ready 握手引脚(GPIO10),v2 DATA_READY 模式:仅输入。
* PB6 仅由 GD32 下拉(有数据待发时拉低,队列空时释放高)。
* ESP32 不再下拉 PB6,只检测边沿:下降沿=GD32 有数据按铃。 */
gpio_config_t ready_cfg = {};
ready_cfg.pin_bit_mask = (1ULL << SPI_READY_GPIO);
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_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", "DATA_READY 输入已配置: GPIO%d (← GD32 PB6,低=有数据)", SPI_READY_GPIO);
}
/* 安装 GPIO ISR 服务(全局仅安装一次)并注册边沿回调 */
static bool s_isrInstalled = false;
if (!s_isrInstalled) {
gpio_ret = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
if (gpio_ret != ESP_OK && gpio_ret != ESP_ERR_INVALID_STATE) {
DEBUG_WARN("SPI", "gpio_install_isr_service 失败: %s", esp_err_to_name(gpio_ret));
} else {
s_isrInstalled = true;
}
}
if (s_isrInstalled) {
gpio_ret = gpio_isr_handler_add((gpio_num_t)SPI_READY_GPIO,
onReadyEdgeISR, this);
if (gpio_ret != ESP_OK) {
DEBUG_WARN("SPI", "gpio_isr_handler_add GPIO%d 失败: %s",
SPI_READY_GPIO, esp_err_to_name(gpio_ret));
}
}
m_linkState.exitBusy();
m_gd32RequestPending = false;
m_localCredit = 8;
m_remoteCredit = 8;
/* v2 链路状态初始化:session 未建立,序号从 0 开始 */
m_sessionId = 0;
m_txSeq = 0;
m_lastSentSeq = 0;
m_rxAckedSeq = 0;
m_expectedRxSeq = 0;
m_gd32Role = 0;
m_v2Active = false;
m_stats = SpiLinkStats{};
m_eventHead = m_eventTail = m_eventCount = 0;
/* 创建 job 队列、固定 Job pool(静态 done 信号量)、事件信号量与 SPI worker */
if (m_jobQueue == nullptr) {
m_jobQueue = xQueueCreate(kJobQueueLen, sizeof(Job *));
if (m_jobQueue == nullptr) {
DEBUG_ERR("SPI", "SPI job queue create failed");
return ESP_ERR_NO_MEM;
}
}
if (!m_jobPoolReady) {
for (UBaseType_t i = 0; i < kJobPoolSize; ++i) {
m_jobPool[i] = Job{};
m_jobPool[i].done = xSemaphoreCreateBinary();
if (m_jobPool[i].done == nullptr) {
DEBUG_ERR("SPI", "SPI job pool done sem create failed idx=%u",
(unsigned)i);
for (UBaseType_t j = 0; j < i; ++j) {
vSemaphoreDelete(m_jobPool[j].done);
m_jobPool[j].done = nullptr;
}
return ESP_ERR_NO_MEM;
}
m_jobPool[i].in_use = false;
}
m_jobPoolReady = true;
DEBUG_INFO("SPI", "SPI job pool ready size=%u queue=%u",
(unsigned)kJobPoolSize, (unsigned)kJobQueueLen);
}
if (m_eventSem == nullptr) {
m_eventSem = xSemaphoreCreateCounting(kEventSlots, 0);
if (m_eventSem == nullptr) {
DEBUG_ERR("SPI", "SPI event sem create failed");
return ESP_ERR_NO_MEM;
}
}
if (m_workerTask == nullptr) {
BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", TASK_STACK_SPI_WORKER, this, 6, &m_workerTask);
if (ok != pdPASS || m_workerTask == nullptr) {
DEBUG_ERR("SPI", "SPI worker task create failed");
m_workerTask = nullptr;
return ESP_ERR_NO_MEM;
}
DEBUG_INFO("SPI", "SPI worker task created");
}
m_spiTaskHandle = m_workerTask;
return ESP_OK;
}
/**
* @brief L0 本机自检:SPI 设备、互斥、worker、job 队列是否就绪。
* @return true 本机 SPI 栈可用
*/
bool SpiMaster::selfTestL0() const
{
return (m_spiDevice != nullptr) && (m_mutex != nullptr) &&
(m_workerTask != nullptr) && (m_jobQueue != nullptr);
}
/**
* @brief L0 对端轻检:短超时 HELLO,确认链路可达 GD32。
* @return true 至少一次 HELLO 成功
*/
bool SpiMaster::selfTestPeerL0()
{
/* 短超时、最多 2 次;不拉长整体启动 */
for (uint32_t attempt = 0U; attempt < 2U; ++attempt) {
if (hello(600U, 0U) == ESP_OK) {
return true;
}
vTaskDelay(pdMS_TO_TICKS(50));
}
return false;
}
// ============================================================
// SPI OTA 协议
// ============================================================
/**
* @brief 无锁版本的 otaTransfer(调用者须已持有锁)。
*/
esp_err_t SpiMaster::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)
{
uint8_t tx_frame[SPI_OTA_FRAME_MAX];
size_t tx_len = 0;
if (!otaBuildFrame(type, payload, payload_len, tx_frame, &tx_len)) {
return ESP_ERR_INVALID_ARG;
}
DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u", type, (unsigned)tx_len);
if (tx_len > SPI_OTA_FRAME_MAX) {
DEBUG_ERR("SPI", "OTA frame too large: %u", (unsigned)tx_len);
return ESP_ERR_INVALID_SIZE;
}
/* CREDIT 背压(包间节奏):
* remoteCredit==0 禁止发新 REQUEST;等 CREDIT_UPDATE / 帧头 credit。
* HELLO / 链路控制不受 credit 限制(session 建立前 credit 可能为 0)。 */
if (type != V2_TYPE_LINK_HELLO && m_remoteCredit == 0U) {
const TickType_t cred_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(
timeout_ms > 0U ? timeout_ms : 500U);
while (m_remoteCredit == 0U && xTaskGetTickCount() < cred_deadline) {
/* 有 DATA_READY 时变长读一帧(可能是 CREDIT_UPDATE 或陈旧响应) */
if (waitSpiDataReady(5U) == ESP_OK) {
uint8_t drain[SPI_OTA_FRAME_MAX];
size_t drain_len = 0U;
if (readV2FrameVarlen(drain, sizeof(drain), &drain_len, 100U) == ESP_OK) {
(void)otaFindFrame(drain, drain_len, nullptr, nullptr, nullptr, nullptr);
}
} else {
vTaskDelay(pdMS_TO_TICKS(5));
}
}
if (m_remoteCredit == 0U) {
DEBUG_WARN("SPI", "remoteCredit=0, refuse TX type=0x%02X", type);
return ESP_ERR_INVALID_STATE;
}
}
/* 同步单 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);
uint8_t tx_padded[SPI_OTA_FRAME_MAX];
if (tx_len > sizeof(tx_padded) || xfer_len > sizeof(tx_padded)) {
DEBUG_ERR("SPI", "OTA frame %u exceeds tx_padded %u", (unsigned)tx_len, (unsigned)sizeof(tx_padded));
m_linkState.exitBusy();
return ESP_ERR_INVALID_SIZE;
}
memset(tx_padded, 0xFF, xfer_len);
memcpy(tx_padded, tx_frame, tx_len);
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
memset(rx_raw, 0xFF, xfer_len);
esp_err_t ret = transferClocked(tx_padded, xfer_len, rx_raw, xfer_len, timeout_ms);
m_linkState.exitBusy();
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "OTA sync transfer failed: %s", esp_err_to_name(ret));
return ret;
}
uint8_t found_type = 0;
const uint8_t *found_payload = nullptr;
size_t found_len = 0;
size_t frame_offset = 0;
if (otaFindFrame(rx_raw, xfer_len, &found_type, &found_payload, &found_len, &frame_offset)) {
if (resp_type) *resp_type = found_type;
if (resp_payload && resp_max > 0) {
size_t copy_len = (found_len < resp_max) ? found_len : resp_max;
if (found_payload && copy_len > 0) {
memcpy(resp_payload, found_payload, copy_len);
}
}
if (resp_len) *resp_len = found_len;
/* expect_stale(两步法 step1)读到的是延迟一拍陈旧帧,序号由 poll 成功时推进。
* 此处若自增会导致 job 间隙/超时后 resend 使用错误 TX_SEQ。 */
if (!expect_stale) {
m_txSeq = (uint8_t)(m_txSeq + 1U);
}
return ESP_OK;
}
if (expect_stale) {
DEBUG_DBG("SPI", "OTA step1 stale response (expected, delayed-by-one)");
} else {
DEBUG_WARN("SPI", "OTA sync transfer response not found");
}
return ESP_ERR_INVALID_RESPONSE;
}
/**
* @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
{
if (!onWorker() && m_workerTask != nullptr) {
Job job{};
job.kind = JobKind::Poll;
job.timeout_ms = timeout_ms;
job.out_type = resp_type;
job.out_payload = resp_payload;
job.out_max = resp_max;
job.out_len = resp_len;
return submitJob(job);
}
LockGuard lock(m_mutex);
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
/**
* @brief 无锁版本的 OTA 响应轮询实现。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::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)
{
if (timeout_ms == 0U) {
timeout_ms = 1U;
}
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
uint32_t attempt = 0;
bool did_spi_read = false;
while (xTaskGetTickCount() <= deadline) {
++attempt;
/* 优先等 DATA_READY;超时仍做有限次盲 poll。
* 短 timeout(OTA 紧循环 50~150ms)必须保证至少 1 次 SPI 读,
* 否则 PB6 偶发未拉低时整段切片空转 → 只见 “timeout: 30 ms” 然后失败。 */
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
TickType_t now = xTaskGetTickCount();
uint32_t remain_ms = (deadline > now) ? ((deadline - now) * portTICK_PERIOD_MS) : 0U;
if (remain_ms == 0U && did_spi_read) {
break;
}
if (remain_ms == 0U) {
remain_ms = 1U; /* 强制末次读 */
}
const uint32_t ready_slice = (attempt == 1U)
? ((timeout_ms >= 80U) ? 40U : timeout_ms)
: 15U;
const uint32_t wait_ms = (ready_slice < remain_ms) ? ready_slice : remain_ms;
const bool ready = (waitSpiDataReady(wait_ms) == ESP_OK);
now = xTaskGetTickCount();
remain_ms = (deadline > now) ? ((deadline - now) * portTICK_PERIOD_MS) : 0U;
const bool last_chance = (remain_ms == 0U) || (attempt >= 6U && !did_spi_read);
/* 无 ready:前 2 轮盲读;之后每 8 轮盲读;deadline 末强制盲读一次 */
if (!ready && attempt > 2U && (attempt % 8U) != 0U && !last_chance) {
vTaskDelay(pdMS_TO_TICKS(CONFIG_DCU_OTA_POLL_BACKOFF_MS));
continue;
}
if (remain_ms == 0U) {
remain_ms = 20U; /* 末次 SPI 事务给足一点时钟时间 */
}
const uint32_t spi_timeout = (remain_ms > 200U) ? 200U : remain_ms;
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, spi_timeout);
did_spi_read = true;
if (ret != ESP_OK) {
if (ret == ESP_ERR_INVALID_RESPONSE) {
vTaskDelay(pdMS_TO_TICKS(CONFIG_DCU_OTA_POLL_BACKOFF_MS));
} else {
vTaskDelay(pdMS_TO_TICKS(1));
}
if (xTaskGetTickCount() > deadline) {
break;
}
continue;
}
/* DEBUG: 限频,避免 OTA 紧循环刷屏 */
{ static uint32_t s_pdbg = 0;
if ((++s_pdbg % 40) == 1 || (rx_raw[0] != 0xFF && rx_raw[0] != 0x00)) {
v2_header_t dh; const uint8_t *dp = nullptr;
bool parsed = v2_parse_frame(rx_raw, rx_len, &dh, &dp);
if (parsed) {
DEBUG_DBG("SPI", "poll att=%u type=0x%02X ack_seq=%u lastSent=%u",
(unsigned)attempt, (unsigned)dh.type, (unsigned)dh.ack_seq,
(unsigned)m_lastSentSeq);
}
}
}
uint8_t found_type = 0;
const uint8_t *found_payload = nullptr;
size_t found_len = 0;
size_t frame_offset = 0;
if (otaFindFrame(rx_raw, rx_len, &found_type, &found_payload, &found_len, &frame_offset)) {
if (found_type == V2_TYPE_CREDIT_UPDATE) {
DEBUG_DBG("SPI", "poll got CREDIT_UPDATE credit=%u", (unsigned)m_remoteCredit);
vTaskDelay(pdMS_TO_TICKS(1));
continue;
}
if (expected_type != 0xFFU && found_type != expected_type &&
found_type != SPI_OTA_RSP_WAIT) {
DEBUG_DBG("SPI", "poll type mismatch: got=0x%02X want=0x%02X",
found_type, expected_type);
vTaskDelay(pdMS_TO_TICKS(1));
continue;
}
if (resp_type) *resp_type = found_type;
if (resp_payload && resp_max > 0) {
size_t copy_len = (found_len < resp_max) ? found_len : resp_max;
if (found_payload && copy_len > 0) {
memcpy(resp_payload, found_payload, copy_len);
} else if (copy_len > 0) {
memset(resp_payload, 0, copy_len);
}
}
if (resp_len) *resp_len = found_len;
/* WAIT 立刻返回,由 OTA 紧循环再 poll(勿在此空转) */
if (found_type == SPI_OTA_RSP_WAIT) {
m_linkState.exitBusy();
return ESP_OK;
}
if (m_lastRxAckMatched) {
m_txSeq = (uint8_t)(m_lastSentSeq + 1U);
}
m_linkState.exitBusy();
return ESP_OK;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
m_linkState.exitBusy();
m_stats.ready_timeouts++;
/* 短切片超时是 OTA 正常紧循环,勿 WARN 刷爆 ring/网页 */
if (timeout_ms >= 200U) {
DEBUG_WARN("SPI", "OTA response timeout: %lu ms", (unsigned long)timeout_ms);
} else {
DEBUG_DBG("SPI", "OTA poll slice timeout: %lu ms", (unsigned long)timeout_ms);
}
return ESP_ERR_TIMEOUT;
}
/**
* @brief ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。
*
* 解决 GD32 "延迟一拍"协议:transact 首笔 CS 读到过期响应,
* otaPollResponse 读取本笔命令的真实响应。命令只发送一次,不重发。
* 适用于所有 ESP32→GD32 方向的命令(CAN 发送/模式切换/ID 快照等)。
*/
esp_err_t SpiMaster::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)
{
if (payload_len > SPI_OTA_FRAME_MAX) {
return ESP_ERR_INVALID_SIZE;
}
if (!onWorker() && m_workerTask != nullptr) {
Job job{};
job.kind = JobKind::Transact;
job.timeout_ms = timeout_ms;
job.type = type;
job.payload_len = payload_len;
if (payload && payload_len > 0U) {
memcpy(job.payload, payload, payload_len);
}
job.out_type = resp_type;
job.out_payload = resp_payload;
job.out_max = resp_max;
job.out_len = resp_len;
return submitJob(job);
}
return otaSendAndPoll(type, payload, payload_len,
resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
bool SpiMaster::takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len,
uint32_t timeout_ms)
{
/* 快速路径:先非阻塞尝试一次 */
{
LockGuard lock(m_mutex);
if (popEvent(type, payload, max_len, len)) {
if (m_eventSem) {
(void)xSemaphoreTake(m_eventSem, 0);
}
return true;
}
}
/* 队列空:若 timeout_ms==0 直接返回,否则在信号量上等待 */
if (timeout_ms == 0U || m_eventSem == nullptr) {
return false;
}
/* 用绝对截止时间:虚假唤醒/抢事件后按剩余时间等,避免每次重新等满 timeout。 */
const TickType_t start = xTaskGetTickCount();
const TickType_t total = pdMS_TO_TICKS(timeout_ms);
while (true) {
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 抢走),继续等剩余时间 */
}
}
esp_err_t SpiMaster::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)
{
/* 非 worker 调用:转 transact 入队,避免多任务直接碰 SPI */
if (!onWorker() && m_workerTask != nullptr) {
return transact(type, payload, payload_len,
resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
LockGuard lock(m_mutex);
/* 步骤 1: 发送命令(忽略延迟一拍的过期响应)。
* otaTransferUnlocked 可能返回:
* ESP_OK - 读到过期帧(正常,忽略)
* ESP_ERR_INVALID_RESPONSE - 读到 0xFF(正常,延迟一拍)
* 其他 - SPI 真实错误 */
/* 总超时在 send 与 poll 之间分配,二者之和不超过 timeout_ms */
uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
if (send_timeout == 0U) {
send_timeout = 1U;
}
uint32_t poll_timeout;
if (timeout_ms > send_timeout) {
poll_timeout = timeout_ms - send_timeout;
} else if (timeout_ms >= 40U) {
send_timeout = timeout_ms / 2U;
if (send_timeout == 0U) {
send_timeout = 1U;
}
poll_timeout = timeout_ms - send_timeout;
} else {
poll_timeout = timeout_ms;
}
if (poll_timeout == 0U) {
poll_timeout = 1U;
}
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) {
return send_ret;
}
/* CS 释放后短延时,再 poll 应答 */
vTaskDelay(pdMS_TO_TICKS(5));
/* 步骤 2: 轮询读取真实响应(变长读,不重发命令) */
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, poll_timeout,
/*expected_type=*/0xFFU);
}
/**
* @brief 通知 GD32 APP 设置 OTA 标志并软复位进入 Bootloader(v2 标准帧)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::appRequestBootloader()
{
if (!onWorker() && m_workerTask != nullptr) {
Job job{};
job.kind = JobKind::EnterBoot;
job.timeout_ms = 5000U;
return submitJob(job);
}
LockGuard lock(m_mutex);
DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader (v2)...");
m_gd32RequestPending = false;
/* 先变长读最多 2 次,吞掉残留 0xC1(仅 DATA_READY 低时读,禁止固定长度盲读)。 */
for (uint32_t d = 0U; d < 2U; ++d) {
if (waitSpiDataReady(5U) != ESP_OK) {
break;
}
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
(void)readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 20U);
m_linkState.exitBusy();
vTaskDelay((pdMS_TO_TICKS(5) > 0) ? pdMS_TO_TICKS(5) : 1);
}
/* 最多 3 次 0xB0。只认 type=0xB0 + status=0xB5 为真 ACK。
* 勿把 session=0 的 0x80 当成功(常见为过期/错帧,导致“假 ack、真没进 BOOT”)。 */
constexpr uint32_t kMaxSends = 3U;
bool command_sent = false;
bool got_ack = false;
const TickType_t gap = (pdMS_TO_TICKS(10) > 0) ? pdMS_TO_TICKS(10) : 1;
for (uint32_t retry = 0U; retry < kMaxSends; ++retry) {
if (retry > 0U) {
vTaskDelay(pdMS_TO_TICKS(2));
/* 序号/会话错乱时重同步一次再发 0xB0 */
if (retry == 1U) {
m_v2Active = false;
m_sessionId = esp_random() | 0x00000001U;
m_txSeq = 0;
m_lastSentSeq = 0;
m_rxAckedSeq = 0;
m_expectedRxSeq = 0;
uint8_t hf[V2_MAX_FRAME];
size_t hl = 0;
if (v2_build_hello_frame(m_sessionId, V2_ROLE_APP, hf, &hl)) {
/* 延迟一拍:连发 2 次 HELLO(≥64B 时钟),第 2 次 RX 为 ACK */
const size_t xlen = spiXferBytes(hl);
for (uint32_t hi = 0U; hi < 2U; ++hi) {
uint8_t tx_p[SPI_OTA_FRAME_MAX];
uint8_t rx_r[SPI_OTA_FRAME_MAX];
memset(tx_p, 0xFF, xlen);
memcpy(tx_p, hf, hl);
memset(rx_r, 0xFF, xlen);
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
(void)transferClocked(tx_p, xlen, rx_r, xlen, 200U);
m_linkState.exitBusy();
v2_header_t h;
const uint8_t *pl = nullptr;
if (v2_parse_frame(rx_r, xlen, &h, &pl) &&
h.type == V2_TYPE_LINK_HELLO && (h.flags & V2_FLAG_RESPONSE) &&
pl && h.payload_len >= 6U &&
v2_get_u32le(&pl[0]) == m_sessionId) {
m_gd32Role = pl[4];
m_v2Active = true;
DEBUG_INFO("SPI", "ENTER_BOOT 前 HELLO 重同步 ok role=%u", m_gd32Role);
break;
}
vTaskDelay(gap);
}
}
}
}
esp_err_t ret = otaTransferUnlocked(SPI_APP_ENTER_BOOT_CMD, nullptr, 0U,
nullptr, nullptr, 0U, nullptr, 400U,
/*expect_stale=*/true);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_RESPONSE) {
DEBUG_WARN("SPI", "ENTER_BOOT transfer failed: %s", esp_err_to_name(ret));
continue;
}
command_sent = true;
m_txSeq = (uint8_t)(m_lastSentSeq + 1U);
vTaskDelay(gap);
/* poll 最多 ~200ms:只认 0xB0+0xB5。优先 DATA_READY,超时仍单 CS 盲读。 */
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(200U);
uint8_t last_type = 0xFFU;
uint32_t poll_i = 0U;
while (xTaskGetTickCount() <= deadline) {
++poll_i;
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
(void)waitSpiDataReady(poll_i <= 2U ? 20U : 5U);
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 40U);
m_linkState.exitBusy();
if (ret != ESP_OK) {
vTaskDelay(gap);
continue;
}
v2_header_t hdr;
const uint8_t *pload = nullptr;
if (!v2_parse_frame(rx_raw, rx_len, &hdr, &pload) ||
!(hdr.flags & V2_FLAG_RESPONSE)) {
vTaskDelay(gap);
continue;
}
last_type = hdr.type;
if (hdr.type == SPI_APP_ENTER_BOOT_CMD) {
const uint8_t status = (hdr.payload_len > 0U && pload)
? pload[0] : 0x00U;
if (status == SPI_APP_ENTER_BOOT_ACK_OK) {
DEBUG_INFO("SPI", "ENTER_BOOT ACK type=0xB0 status=0xB5 sess=0x%08lX",
(unsigned long)hdr.session_id);
got_ack = true;
break;
}
DEBUG_WARN("SPI", "ENTER_BOOT ACK status=0x%02X (want 0xB5)", status);
} else if (hdr.type == V2_TYPE_LINK_ERROR) {
const uint8_t errc = (hdr.payload_len > 0U && pload) ? pload[0] : 0xFFU;
const uint8_t exp = (hdr.payload_len > 1U && pload) ? pload[1] : 0xFFU;
DEBUG_WARN("SPI", "ENTER_BOOT拒 code=0x%02X exp=%u last=%u",
errc, exp, (unsigned)m_lastSentSeq);
break;
} else if (hdr.type == SPI_OTA_RSP_ERROR) {
DEBUG_WARN("SPI", "ENTER_BOOT 收到 RSP_ERROR(0x82),将重发");
break;
} else {
/* 0xC1 事件 / 过期 0x80 等:继续 poll,不记成功 */
DEBUG_DBG("SPI", "ENTER_BOOT poll 忽略 type=0x%02X flags=0x%02X",
hdr.type, hdr.flags);
}
vTaskDelay(gap);
}
if (got_ack) {
break;
}
if (last_type != 0xFFU) {
DEBUG_INFO("SPI", "ENTER_BOOT poll 结束 last_type=0x%02X (未获 0xB0/0xB5)", last_type);
}
}
if (command_sent) {
/* 静默:APP 擦配置页(可达数十 ms) + 软复位 + BOOT 起机。
* 此间禁止 HELLO,否则 role 会一直像卡在 APP。 */
const uint32_t kQuietMs = got_ack ? 100U : 30U;
DEBUG_INFO("SPI", "ENTER_BOOT ok ack=%s waitAPP %lums",
got_ack ? "yes" : "no", (unsigned long)kQuietMs);
vTaskDelay(pdMS_TO_TICKS(kQuietMs));
return got_ack ? ESP_OK : ESP_ERR_INVALID_RESPONSE;
}
DEBUG_WARN("SPI", "ENTER_BOOT 未能发出 SPI 帧");
return ESP_ERR_TIMEOUT;
}
/**
* @brief 通过 HELLO 轮询等待 Bootloader 就绪(v2 方式)。
*
* App→Boot 切换后轮询 HELLO,直到 role=boot 或超时。
* APP 仍在线时拉大间隔、降噪日志,避免 role=1 刷屏。
*/
esp_err_t SpiMaster::waitForBootloaderReady(uint32_t timeout_ms)
{
if (!onWorker() && m_workerTask != nullptr) {
Job job{};
job.kind = JobKind::WaitBoot;
job.timeout_ms = timeout_ms;
return submitJob(job);
}
DEBUG_INFO("SPI", "等待 Bootloader 就绪 (HELLO 轮询, timeout=%lums)...",
(unsigned long)timeout_ms);
/* 复位/起机再留白;未走 appRequestBootloader 的调用方也需要 */
{
const TickType_t settle = (pdMS_TO_TICKS(200) > 0) ? pdMS_TO_TICKS(200) : 1;
vTaskDelay(settle);
}
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
constexpr uint32_t kHelloAttemptMs = 400U;
uint32_t app_hits = 0U;
uint32_t rounds = 0U;
while (xTaskGetTickCount() < deadline) {
TickType_t now = xTaskGetTickCount();
uint32_t remain_ms = (deadline > now)
? (uint32_t)((deadline - now) * portTICK_PERIOD_MS) : 0U;
if (remain_ms == 0U) {
break;
}
const uint32_t attempt_ms = (remain_ms < kHelloAttemptMs) ? remain_ms : kHelloAttemptMs;
++rounds;
esp_err_t ret = hello(attempt_ms, V2_ROLE_BOOT);
if (ret == ESP_OK && m_gd32Role == V2_ROLE_BOOT) {
DEBUG_INFO("SPI", "Bootloader 就绪 (HELLO role=boot, round=%lu)",
(unsigned long)rounds);
return ESP_OK;
}
if (ret == ESP_OK && m_gd32Role == V2_ROLE_APP) {
++app_hits;
/* 仍在 APP:复位未完成或 ENTER_BOOT 未生效;放慢 HELLO 给 Flash/复位时间 */
if (app_hits == 1U || (app_hits % 5U) == 0U) {
DEBUG_INFO("SPI", "仍为 APP (role=1),等待复位进 BOOT... (app_hits=%lu)",
(unsigned long)app_hits);
}
const uint32_t app_gap_ms = (app_hits < 3U) ? 200U : 150U;
const TickType_t app_gap = (pdMS_TO_TICKS(app_gap_ms) > 0)
? pdMS_TO_TICKS(app_gap_ms) : 1;
vTaskDelay(app_gap);
continue;
}
/* 无响应 / 0xC1 噪声:中等等待 */
const TickType_t miss_gap = (pdMS_TO_TICKS(60) > 0) ? pdMS_TO_TICKS(60) : 1;
vTaskDelay(miss_gap);
}
DEBUG_WARN("SPI", "BOOT就绪超时 %lums r=%lu app=%lu",
(unsigned long)timeout_ms, (unsigned long)rounds, (unsigned long)app_hits);
return ESP_ERR_TIMEOUT;
}
/**
* @brief 解析 GD32 APP 身份 16B 并写入本端缓存。
*
* 布局与 HELLO_ACK 附加块 / APP PING body 相同(见 spi_link_v2.h V2_APP_IDENTITY_LEN):
* [0..3] APP ver · [4..7] BOOT ver · [8..11] uptime_ms LE
* [12] feat_map_ver · [13..14] feature_flags LE · [15] rsv
*
* @param id 身份块指针(至少 16 字节)
* @param out_version 可选,输出解析后的版本结构
* @param out_uptime_sec 可选,输出 uptime(秒)
*/
void SpiMaster::applyAppIdentity16(const uint8_t *id, gd32_version_t *out_version,
uint32_t *out_uptime_sec)
{
if (!id) {
return;
}
gd32_version_t ver{};
ver.app_major = id[0];
ver.app_minor = id[1];
ver.app_patch = id[2];
ver.app_build = id[3];
ver.boot_major = id[4];
ver.boot_minor = id[5];
ver.boot_patch = id[6];
ver.boot_build = id[7];
const uint32_t uptime_ms = (uint32_t)id[8]
| ((uint32_t)id[9] << 8)
| ((uint32_t)id[10] << 16)
| ((uint32_t)id[11] << 24);
if (out_uptime_sec) {
*out_uptime_sec = uptime_ms / 1000U;
}
uint16_t feat_flags = 0U;
bool feat_known = false;
if (id[12] == V2_FEAT_MAP_VER) {
feat_flags = v2_get_u16le(&id[13]);
feat_known = true;
}
/* HELLO 与 PING 都会带同一份 16B 身份:仅版本/feat 变化时打 INFO,避免连打两遍 */
const bool ver_changed =
!m_gd32IdKnown ||
m_gd32Version.app_major != ver.app_major ||
m_gd32Version.app_minor != ver.app_minor ||
m_gd32Version.app_patch != ver.app_patch ||
m_gd32Version.app_build != ver.app_build ||
m_gd32Version.boot_major != ver.boot_major ||
m_gd32Version.boot_minor != ver.boot_minor ||
m_gd32Version.boot_patch != ver.boot_patch ||
m_gd32Version.boot_build != ver.boot_build ||
m_gd32FeatKnown != feat_known ||
(feat_known && m_gd32FeatFlags != feat_flags);
m_gd32Version = ver;
m_gd32IdKnown = true;
if (out_version) {
*out_version = ver;
}
m_gd32FeatKnown = feat_known;
m_gd32FeatFlags = feat_known ? feat_flags : 0U;
if (ver_changed && feat_known) {
DEBUG_INFO("SPI", "ID APP=%u.%u.%u.%u feat=%04lX",
(unsigned)ver.app_major, (unsigned)ver.app_minor,
(unsigned)ver.app_patch, (unsigned)ver.app_build,
(unsigned long)m_gd32FeatFlags);
DEBUG_INFO("SPI", "ID BOOT=%u.%u.%u.%u ch=%u",
(unsigned)ver.boot_major, (unsigned)ver.boot_minor,
(unsigned)ver.boot_patch, (unsigned)ver.boot_build,
(unsigned)((m_gd32FeatFlags & V2_FEAT_CAN_CH_MASK) >> V2_FEAT_CAN_CH_SHIFT));
}
}
/**
* @brief 向 GD32 APP 发送存活探测(帧长发送 + DATA_READY 变长读)。
*
* 两步法(持锁):otaTransferUnlocked(expect_stale) + otaPollResponseUnlocked。
* 成功时 body 为身份 16B,与 HELLO_ACK 附加块同布局,经 applyAppIdentity16 入库。
*/
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version)
{
if (!onWorker() && m_workerTask != nullptr) {
Job job{};
job.kind = JobKind::Ping;
job.timeout_ms = timeout_ms;
job.out_uptime = uptime_sec;
job.out_version = version;
return submitJob(job);
}
LockGuard lock(m_mutex);
constexpr uint8_t kAppPingType = 0xA1U;
uint8_t resp_type = 0;
uint8_t resp_payload[32];
size_t resp_len = 0U;
/* PING 无 payload,send 只发 1 帧 SPI(<50ms),不需要 500ms 预留。
* 省下的时间给 poll,让 APP 命令层有更长窗口响应。 */
const uint32_t send_timeout = (timeout_ms > 100U) ? 100U : timeout_ms;
esp_err_t send_ret = otaTransferUnlocked(kAppPingType, nullptr, 0U,
nullptr, nullptr, 0U, nullptr, send_timeout,
/*expect_stale=*/true);
if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) {
DEBUG_DBG("SPI", "APP PING send failed: %s", esp_err_to_name(send_ret));
return send_ret;
}
{
const TickType_t gap = (pdMS_TO_TICKS(1) > 0) ? pdMS_TO_TICKS(1) : 1;
vTaskDelay(gap);
}
const uint32_t poll_timeout = (timeout_ms > send_timeout)
? (timeout_ms - send_timeout) : 500U;
esp_err_t err = otaPollResponseUnlocked(&resp_type, resp_payload, sizeof(resp_payload),
&resp_len, poll_timeout, SPI_OTA_RSP_ACK);
if (err != ESP_OK) {
DEBUG_DBG("SPI", "APP PING poll failed: %s", esp_err_to_name(err));
return err;
}
if (resp_type != SPI_OTA_RSP_ACK || resp_len < 12U) {
DEBUG_DBG("SPI", "APP PING bad resp type=0x%02X len=%u", resp_type, (unsigned)resp_len);
return ESP_ERR_INVALID_RESPONSE;
}
const uint8_t *pload = resp_payload;
if (resp_len >= V2_APP_IDENTITY_LEN) {
applyAppIdentity16(pload, version, uptime_sec);
} else if (version) {
memset(version, 0, sizeof(gd32_version_t));
version->app_major = pload[0];
version->app_minor = pload[1];
version->app_patch = pload[2];
version->app_build = pload[3];
}
DEBUG_DBG("SPI", "APP PING 成功 len=%u", (unsigned)resp_len);
return ESP_OK;
}
// ============================================================
// SPI Link v2 链路状态
// ============================================================
/**
* @brief 拷贝链路统计快照(有界取锁,失败返回零值)。
* @return 统计结构副本
*/
SpiMaster::SpiLinkStats SpiMaster::linkStats()
{
/* OTA 中继期间 SPI worker 在 otaSendAndPoll 全程持有 m_mutex(发送+轮询 ACK,
* 每包最长 6s)。若用 portMAX_DELAY 等锁,/system/status 会被阻塞整个传输阶段,
* 前端进度完全不同步直到传输结束。
* 统计仅为展示用途,拿不到锁时返回零值,绝不阻塞 status handler。 */
SpiLinkStats ret{};
if (m_mutex && xSemaphoreTake(m_mutex, pdMS_TO_TICKS(5)) == pdTRUE) {
ret = m_stats;
xSemaphoreGive(m_mutex);
}
return ret;
}
/**
* @brief 完整链路状态(session/role/credit + 统计),供 /system/status。
* @return 状态快照
*/
SpiMaster::SpiLinkStatus SpiMaster::linkStatus()
{
/* 状态字段为 volatile 单字节/双字读,原子性足够;统计复用 linkStats() 的有界取锁。 */
SpiLinkStatus st{};
st.version = 2U;
st.gd32_role = m_gd32Role;
st.session_id = m_sessionId;
st.local_credit = m_localCredit;
st.remote_credit = m_remoteCredit;
st.stats = linkStats();
return st;
}
/**
* @brief 发起 HELLO 建 session;APP 侧 ACK 可附带身份 16B(版本+开关)。
* @param timeout_ms 总超时
* @param role_hint 0=any,1=app,2=boot
* @return ESP_OK 成功;超时/坏响应见错误码
*/
esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
{
if (!onWorker() && m_workerTask != nullptr) {
Job job{};
job.kind = JobKind::Hello;
job.timeout_ms = timeout_ms;
job.role_hint = role_hint;
return submitJob(job);
}
LockGuard lock(m_mutex);
/* 1. 生成随机非零 SESSION_ID(整次 hello 内固定,禁止每 CS 换 session) */
m_sessionId = esp_random() | 0x00000001U;
m_txSeq = 0;
m_lastSentSeq = 0;
m_rxAckedSeq = 0;
m_expectedRxSeq = 0;
m_gd32Role = 0;
m_v2Active = false;
/* 2. 构建 HELLO 帧 */
uint8_t hello_frame[V2_MAX_FRAME];
size_t hello_len = 0;
if (!v2_build_hello_frame(m_sessionId, role_hint, hello_frame, &hello_len)) {
DEBUG_ERR("SPI", "HELLO build failed");
return ESP_ERR_INVALID_RESPONSE;
}
/* HELLO 必须走延迟一拍全双工连发(与 Boot 实测一致):
* 第 N 次 CS 读到第 N-1 次 HELLO 的 ACK。不可只等 DATA_READY 再短读:
* Boot TX 按 128 预装,过短/两段 CS 会清 pending → MISO 全 0xFF。 */
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
uint32_t attempt = 0U;
uint8_t last_type = 0xFFU;
uint8_t last_flags = 0U;
bool saw_any_frame = false;
uint8_t last_rx_head[16];
memset(last_rx_head, 0xFF, sizeof(last_rx_head));
const TickType_t gap_ticks = (pdMS_TO_TICKS(10) > 0) ? pdMS_TO_TICKS(10) : 1;
const size_t xfer_len = spiXferBytes(hello_len); /* ≥64,盖住 HELLO_ACK */
while (xTaskGetTickCount() <= deadline) {
++attempt;
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
uint8_t tx_padded[SPI_OTA_FRAME_MAX];
memset(tx_padded, 0xFF, xfer_len);
memcpy(tx_padded, hello_frame, hello_len);
memset(rx_raw, 0xFF, xfer_len);
esp_err_t ret = transferClocked(tx_padded, xfer_len, rx_raw, xfer_len, 200U);
m_linkState.exitBusy();
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "HELLO transfer failed[%lu]: %s",
(unsigned long)attempt, esp_err_to_name(ret));
vTaskDelay(gap_ticks);
continue;
}
memcpy(last_rx_head, rx_raw, sizeof(last_rx_head));
v2_header_t hdr;
const uint8_t *pload = nullptr;
if (!v2_parse_frame(rx_raw, xfer_len, &hdr, &pload)) {
vTaskDelay(gap_ticks);
continue;
}
saw_any_frame = true;
last_type = hdr.type;
last_flags = hdr.flags;
if (hdr.type != V2_TYPE_LINK_HELLO || !(hdr.flags & V2_FLAG_RESPONSE)) {
vTaskDelay(gap_ticks);
continue;
}
if (hdr.payload_len < 6U || !pload) {
vTaskDelay(gap_ticks);
continue;
}
const uint32_t session_echo = v2_get_u32le(&pload[0]);
if (session_echo != m_sessionId) {
/* 延迟一拍旧 ACK:同 session 再发一次即可对齐 */
DEBUG_DBG("SPI", "HELLO lag echo=0x%08lX expected=0x%08lX attempt=%lu",
(unsigned long)session_echo, (unsigned long)m_sessionId,
(unsigned long)attempt);
vTaskDelay(gap_ticks);
continue;
}
m_gd32Role = pload[4];
/* payload[5]/hdr.credit 均为对端 credit_max → 写入 m_remoteCredit。
* 勿写入 m_localCredit(本端 RX 窗口,init 为 8)。 */
m_remoteCredit = (pload[5] != 0U) ? pload[5] : hdr.credit;
/* APP:HELLO_ACK 带 16B 身份 → 建连即完成开机身份(与 PING 同布局) */
if (m_gd32Role == V2_ROLE_APP &&
hdr.payload_len >= V2_HELLO_ACK_APP_LEN) {
applyAppIdentity16(&pload[V2_HELLO_ACK_BASE_LEN], nullptr, nullptr);
}
m_v2Active = true;
m_stats.resyncs++;
return ESP_OK;
}
if (!saw_any_frame) {
/* 不打全包 hex:只记 attempts + 前 4 字节便于判 SOF */
DEBUG_WARN("SPI",
"HELLO no v2 frame att=%lu rx0-3=%02X %02X %02X %02X",
(unsigned long)attempt,
last_rx_head[0], last_rx_head[1], last_rx_head[2], last_rx_head[3]);
} else {
DEBUG_WARN("SPI", "HELLO: bad/lag response type=0x%02X flags=0x%02X attempts=%lu",
last_type, last_flags, (unsigned long)attempt);
}
return ESP_ERR_INVALID_RESPONSE;
}