SPI Link v2 Self-Test / selftest (push) Has been cancelled
添加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通信的可靠性和内存使用效率。
1982 lines
73 KiB
C++
1982 lines
73 KiB
C++
/*
|
||
* 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;
|
||
}
|
||
|