/* * spi_master.cpp - SPI Master 驱动实现 * T1.1 - T1.4 * ESP-IDF 6.0 适配版本 * CANFD 3通道发送接口 * * 所有功能封装为 SpiMaster 类方法。 */ #include #include #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(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(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; }