feat(spi): 更新SPI通信协议为5-wire SPI Link v2并优化握手流程

- 添加DATA_READY引脚(GPIO10)支持5-wire SPI拓扑
- 将otaTransfer调用替换为更通用的transact方法
- 更新SPI引脚定义和通信协议文档
- 修改README.md详细说明新的SPI Link v2协议

docs: 标记旧版OTA中继设计文档为已废弃

- 在esp32_ota_relay_design.md添加废弃警告
- 指向新的SPI Link v2协议文档作为权威来源
- 保留历史文档供参考但停止维护

refactor(can): 增强CANFD桥接错误处理机制

- 扩展canFdSend函数增加gd32_err_out参数
- 透传GD32细分错误码(0xE0-0xE5)到HTTP响应
- 更新HTTP处理器输出完整的错误信息

perf(monitor): 优化按铃事件等待机制

- 修改can_feedback_task.cpp中的事件等待逻辑
- 统一使用takeEvent方法替代checkGd32Request
- 减少不必要的中间变量和函数调用

fix(status): 改进系统状态监控和CPU使用率统计

- 增加CPU任务快照缓冲区容量从16到32
- 添加CPU统计无效时的调试日志输出
- 更新SPI Link统计接口使用新的linkStatus方法

refactor(ota): 优化OTA中继传输和启动等待逻辑

- 调整APP启动等待重试策略(10次->20次, 500ms->300ms)
- 移除数据包传输进度的日志输出减少冗余信息
- 统一使用transact方法进行SPI事务处理

bump(version): 更新固件版本到0.3.44

- 版本号从0.3.43提升至0.3.44
- 更新编译时间和Git哈希值
This commit is contained in:
小龙虾
2026-07-15 00:35:02 +08:00
parent adb3386da4
commit 164855a36c
12 changed files with 209 additions and 336 deletions
+2 -1
View File
@@ -20,7 +20,8 @@ ESP32-C3FH4 OTA 中继固件,作为 **GD32E508VET6** 的 OTA 升级桥梁。
|------|------|
| **芯片** | ESP32-C3FH4 (RISC-V, 单核, 160MHz) |
| **Flash** | 4MB |
| **SPI 引脚** | MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO4 |
| **SPI 引脚** | MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO4, DATA_READY=GPIO10(GD32 PB6 开漏输出,ESP32 仅输入) |
| **SPI 拓扑** | 5-wire SPI(4 线 SPI + 1 DATA_READY 通知线),SPI Link v2 协议(详见 `SPEC/spi_link_v2.md`) |
| **SPI 速率** | 4MHz, Mode 0 |
| **UART 调试** | TX=GPIO21, RX=GPIO20, 115200 baud |
| **编译工具链** | ESP-IDF v6.0, riscv32-esp-elf |
+8
View File
@@ -1,5 +1,13 @@
# ESP32-C3FH4 OTA 中继架构设计
> **⚠️ v1 已废弃**(2026-07-14 标注)
>
> 本文档描述的是 v1 OTA 中继设计(旧帧格式 0xEB/6B/CRC16、旧引脚 GPIO6-9、旧命令 0xF0-0xF7),
> 与当前代码实现不符。当前 SPI 通信协议以 **`SPEC/spi_link_v2.md`** 为单一权威来源
> (SPI Link v2:16B 头帧、SESSION/SEQ/CREDIT、5-wire SPI、HELLO 握手)。
>
> 本文档保留作历史参考,不再维护。所有新代码与文档改动请参照 SPEC。
## 1. 系统概述
```
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 43
#define FW_VERSION_STRING "0.3.43"
#define FW_VERSION_PATCH 44
#define FW_VERSION_STRING "0.3.44"
// 编译时间
#define BUILD_TIME "2026-07-14 12:05:28"
#define BUILD_TIME "2026-07-15 00:28:19"
// Git 信息
#define GIT_HASH "7678234"
#define GIT_HASH "adb3386"
+3 -8
View File
@@ -65,15 +65,10 @@ static void taskLoop(void *)
continue;
}
/* 阻塞等待 GD32 按铃并读取一帧(超时则重试) */
esp_err_t ret = SpiMaster::instance().checkGd32Request(kWaitRingMs);
if (ret != ESP_OK) {
continue;
}
/* 取出 checkGd32Request 存入的 type/payload */
/* 阻塞等待 GD32 按铃事件(超时则重试) */
if (!SpiMaster::instance().takeEvent(&ring_type, ring_payload,
sizeof(ring_payload), &ring_len)) {
sizeof(ring_payload), &ring_len,
kWaitRingMs)) {
continue;
}
+14 -19
View File
@@ -856,9 +856,9 @@ ota_result_t OtaRelay::doHandshakeReq(uint32_t attempts)
esp_err_to_name(hr), SpiMaster::instance().gd32Role());
}
/* otaTransfer 已含延迟一拍两步法;再经 pollUntil 处理 WAIT/陈旧 ACK。 */
/* transact 已含延迟一拍两步法;再经 pollUntil 处理 WAIT/陈旧 ACK。 */
const uint32_t total_timeout_ms = kHandshakeReqTimeoutMs + kHandshakePollTimeoutMs;
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, payload, sizeof(payload),
esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, payload, sizeof(payload),
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
total_timeout_ms);
last_err = err;
@@ -906,7 +906,7 @@ ota_result_t OtaRelay::doChallengeAndAuth()
DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---");
DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08" PRIX32, timestamp);
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, m_nonceServer, 16,
esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, m_nonceServer, 16,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000);
logRespPayload("CHALLENGE", resp_type, resp_payload, resp_len);
err = pollUntilExpectedAckShape("CHALLENGE", &resp_type, resp_payload,
@@ -937,7 +937,7 @@ ota_result_t OtaRelay::doChallengeAndAuth()
DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY/CONFIRM ---");
resp_len = 0;
err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16,
err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, encrypted_confirm, 16,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
logRespPayload("CONFIRM", resp_type, resp_payload, resp_len);
err = pollUntilExpectedAckShape("CONFIRM", &resp_type, resp_payload,
@@ -976,7 +976,7 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
size_payload[2] = (uint8_t)((size >> 16) & 0xFF);
size_payload[3] = (uint8_t)((size >> 24) & 0xFF);
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload),
esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload),
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
logRespPayload("SEND_SIZE", resp_type, resp_payload, resp_len);
/* GD32 收到 SIZE 后擦 Flash,先 WAIT 再 ACK(seq=0);进度由前端读 status,不打过程 INFO */
@@ -1070,7 +1070,7 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
} else {
resp_len = 0;
resp_type = 0;
esp_err_t err = SpiMaster::instance().otaTransfer(
esp_err_t err = SpiMaster::instance().transact(
SPI_OTA_CMD_DATA, tx_buf, 64,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1200);
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
@@ -1081,10 +1081,6 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
SpiMaster::instance().confirmTxSeqAccepted();
m_transferFlashOffset += (uint32_t)chunk_size;
if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) {
DEBUG_INFO("OTA", "转发进度 %" PRIu32 "/%" PRIu32,
seq + 1U, m_transferTotalPackets);
}
return false;
}
}
@@ -1110,10 +1106,6 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
SpiMaster::instance().confirmTxSeqAccepted();
m_transferFlashOffset += (uint32_t)chunk_size;
if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) {
DEBUG_INFO("OTA", "转发进度 %" PRIu32 "/%" PRIu32,
seq + 1U, m_transferTotalPackets);
}
return false;
}
uint32_t err_seq = 0;
@@ -1256,7 +1248,7 @@ ota_result_t OtaRelay::doSendFinishOta()
resp_len = 0;
memset(resp_payload, 0, sizeof(resp_payload));
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload),
esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload),
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
CONFIG_DCU_OTA_FINISH_TIMEOUT_MS);
logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len);
@@ -1317,15 +1309,18 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
// 以 SESSION mismatch 拒绝(旧 session_id vs 新 APP session_id=0)。
bool session_established = false;
for (uint32_t attempt = 0; attempt < 10; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(500));
/* APP 完整初始化(含 EthercatSlave::init)需 5-6 秒,偶发 >10 秒。
* 增加重试次数到 20、缩短延迟到 300ms,总窗口 ~26 秒,
* APP 启动后最快 300ms 内检测到(原 500ms)。 */
for (uint32_t attempt = 0; attempt < 20; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(300));
if (!session_established) {
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP);
if (hr == ESP_OK) {
session_established = true;
} else {
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/10)", attempt + 1);
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
continue;
}
}
@@ -1333,7 +1328,7 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
if (probeAndCacheAppAlive(timeout_ms)) {
break;
}
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/10)", attempt + 1);
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
}
if (!getGd32AppAlive()) {
+33 -18
View File
@@ -776,8 +776,9 @@ esp_err_t can_send_handler(httpd_req_t *req)
cJSON_Delete(root);
uint8_t resp_data[CAN_MAX_DLC] = {0};
uint8_t gd32_err = 0U;
int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc,
resp_data, sizeof(resp_data), 2000);
resp_data, sizeof(resp_data), 2000, &gd32_err);
char resp[512];
if (resp_len >= 0) {
@@ -790,7 +791,8 @@ esp_err_t can_send_handler(httpd_req_t *req)
ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
} else {
snprintf(resp, sizeof(resp),
"{\"status\": \"error\", \"code\": %d}", resp_len);
"{\"status\": \"error\", \"code\": %d, \"gd32_err\": %u}",
resp_len, (unsigned)gd32_err);
}
httpd_resp_set_type(req, "application/json");
@@ -878,7 +880,7 @@ esp_err_t spi_send_handler(httpd_req_t *req)
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 6144;
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 16;
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
static char s_status_json[SYSTEM_STATUS_JSON_CAP];
static SemaphoreHandle_t s_status_json_mux = nullptr;
static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
@@ -974,6 +976,8 @@ static void sample_cpu_usage(CpuUsageSnapshot& result)
UBaseType_t captured_count = uxTaskGetSystemState(tasks, task_count, &total_runtime);
int64_t now_us = esp_timer_get_time();
if (captured_count == 0 || captured_count > task_count) {
DEBUG_WARN("STATUS", "CPU stats: uxTaskGetSystemState 返回 0 (actual_tasks=%lu, buf_slots=%lu)",
(unsigned long)uxTaskGetNumberOfTasks(), (unsigned long)task_count);
free(tasks);
return;
}
@@ -1038,6 +1042,15 @@ static void sample_cpu_usage(CpuUsageSnapshot& result)
result.usage_pct = std::max(0.0, 100.0 - result.idle_pct);
}
if (!result.valid) {
DEBUG_DBG("STATUS", "CPU stats: valid=false had_prev=%d total=%lu prev_total=%lu "
"can_compute=%d delta=%lu captured=%lu",
(int)had_previous,
(unsigned long)total_runtime, (unsigned long)previous_total,
(int)can_compute, (unsigned long)total_delta,
(unsigned long)captured_count);
}
free(previous_samples);
free(tasks);
#else
@@ -1236,34 +1249,36 @@ esp_err_t system_status_handler(httpd_req_t *req)
len = append_cpu_status_json(resp, SYSTEM_STATUS_JSON_CAP, len, *cpu);
/* SPI Link v2 统计(SPEC §12.1)— linkStats 用 5ms 有界超时取 SPI 锁,
/* SPI Link v2 统计(SPEC §12.1)— linkStatus 内部用 5ms 有界超时取 SPI 锁,
* OTA 中继期间拿不到锁时返回零值统计,绝不阻塞 /system/status 响应。 */
{
SpiMaster::SpiLinkStats ls = SpiMaster::instance().linkStats();
SpiMaster::SpiLinkStatus ls = SpiMaster::instance().linkStatus();
const char *role_str = "unknown";
switch (SpiMaster::instance().gd32Role()) {
switch (ls.gd32_role) {
case 1: role_str = "app"; break;
case 2: role_str = "boot"; break;
default: break;
}
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len,
",\"spi_link\":{\"version\":2,\"gd32_role\":\"%s\",\"session_id\":\"0x%08lX\","
"\"v2_active\":%s,\"remote_credit\":%u,"
",\"spi_link\":{\"version\":%u,\"gd32_role\":\"%s\",\"session_id\":\"0x%08lX\","
"\"v2_active\":%s,\"local_credit\":%u,\"remote_credit\":%u,"
"\"crc_errors\":%lu,\"duplicate_requests\":%lu,\"seq_errors\":%lu,"
"\"retries\":%lu,\"ready_timeouts\":%lu,\"rx_overruns\":%lu,"
"\"resyncs\":%lu,\"max_resp_latency_us\":%lu}",
(unsigned)ls.version,
role_str,
(unsigned long)SpiMaster::instance().sessionId(),
(unsigned long)ls.session_id,
SpiMaster::instance().v2Active() ? "true" : "false",
(unsigned)SpiMaster::instance().remoteCredit(),
(unsigned long)ls.crc_errors,
(unsigned long)ls.duplicate_requests,
(unsigned long)ls.seq_errors,
(unsigned long)ls.retries,
(unsigned long)ls.ready_timeouts,
(unsigned long)ls.rx_overruns,
(unsigned long)ls.resyncs,
(unsigned long)ls.max_resp_latency_us);
(unsigned)ls.local_credit,
(unsigned)ls.remote_credit,
(unsigned long)ls.stats.crc_errors,
(unsigned long)ls.stats.duplicate_requests,
(unsigned long)ls.stats.seq_errors,
(unsigned long)ls.stats.retries,
(unsigned long)ls.stats.ready_timeouts,
(unsigned long)ls.stats.rx_overruns,
(unsigned long)ls.stats.resyncs,
(unsigned long)ls.stats.max_resp_latency_us);
}
// 前导逗号与前一块分隔;之前因 append_cpu_status_json 尾随逗号叠加
+12 -4
View File
@@ -13,7 +13,8 @@ constexpr uint8_t kSpiCanFrameType = 0xC0U;
static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
uint8_t *recv_buf, uint8_t recv_max,
uint32_t timeout_ms)
uint32_t timeout_ms,
uint8_t *gd32_err_out)
{
uint8_t tx_buf[72];
size_t tx_len = 0;
@@ -30,7 +31,7 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
uint8_t resp_type = 0U;
size_t resp_len = 0U;
/* 两步法:otaTransfer 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。
/* 两步法:transact 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。
* CAN 帧非幂等,命令只发一次,不重发。 */
esp_err_t ret = SpiMaster::instance().transact(
kSpiCanFrameType,
@@ -40,17 +41,21 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
if (ret != ESP_OK) {
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s",
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret));
if (gd32_err_out) *gd32_err_out = 0U; /* SPI 失败时清零 */
return -2; /* SPI 通信失败 */
}
if (resp_type == SPI_OTA_RSP_ACK) {
if (recv_buf == nullptr || recv_max == 0U) {
if (gd32_err_out) *gd32_err_out = 0U;
return static_cast<int>(resp_len);
}
const size_t copied_len = (resp_len < recv_max) ? resp_len : recv_max;
if (gd32_err_out) *gd32_err_out = 0U;
return static_cast<int>(copied_len);
}
/* resp_type == ERROR: 读取 GD32 返回的错误码 */
uint8_t gd32_err = (resp_len > 0 && recv_buf) ? recv_buf[0] : 0xFF;
if (gd32_err_out) *gd32_err_out = gd32_err; /* 透传细分错误码 */
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X",
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err);
return -3; /* GD32 返回 ERROR */
@@ -73,16 +78,19 @@ static inline uint32_t readU32Le(const uint8_t *p) {
* @brief 发送一帧 CANFD 数据,可选等待并读取响应。
* 通过 SPI 协议帧发送给 GD32 APP,并读取 ACK/ERROR 响应。
* 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。
* gd32_err_out 在返回 -3 时输出 GD32 细分错误码,其它情况清零。
*/
int canFdSend(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
uint8_t *recv_buf, uint8_t recv_max,
uint32_t timeout_ms)
uint32_t timeout_ms,
uint8_t *gd32_err_out)
{
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
if (gd32_err_out) *gd32_err_out = 0U;
return -1;
}
return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms);
return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms, gd32_err_out);
}
/**
+5 -2
View File
@@ -43,12 +43,15 @@ typedef struct {
* @param recv_buf 接收数据缓冲区(可为 nullptr)。
* @param recv_max 接收缓冲区最大长度。
* @param timeout_ms 超时时间(毫秒)。
* @return 成功时返回接收数据长度,失败返回 -1。
* @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5);
* 仅在返回 -3 时有效,其它情况清零。可为 nullptr。
* @return 成功时返回接收数据长度,失败返回 -1/-2/-3。
*/
int canFdSend(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
uint8_t *recv_buf, uint8_t recv_max,
uint32_t timeout_ms);
uint32_t timeout_ms,
uint8_t *gd32_err_out = nullptr);
// ============================================================
// 不重复 ID 快照表(SPI 0xC3 协议配套)
+58 -222
View File
@@ -18,7 +18,6 @@
#include "soc/spi_struct.h"
#include "esp_err.h"
#include "esp_log.h"
#include "rom/ets_sys.h" // esp_rom_delay_us (ets_delay_us)
#include "esp_random.h" // esp_random() for HELLO session_id
#include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM
@@ -183,15 +182,9 @@ esp_err_t SpiMaster::serviceRingOnce()
return ESP_ERR_NOT_FOUND;
}
driveReadyLow();
if (SPI_READY_DRIVE_DELAY_US > 0U) {
esp_rom_delay_us(SPI_READY_DRIVE_DELAY_US);
}
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);
releaseReady();
m_linkState.exitBusy();
if (ret != ESP_OK) {
@@ -341,30 +334,12 @@ void SpiMaster::workerLoop()
}
}
/**
* @brief 下拉 SPIready(v2 DATA_READY 模式下 ESP32 不再操作 PB6)。
* 保留函数体兼容调用方,实际为空操作。
*/
void SpiMaster::driveReadyLow()
{
/* v2 DATA_READY: PB6 仅 GD32 下拉,ESP32 GPIO10 只输入。 */
}
/**
* @brief 释放 SPIready(v2 DATA_READY 模式下空操作)。
*/
void SpiMaster::releaseReady()
{
/* v2 DATA_READY: ESP32 不操作 PB6。 */
}
/**
* @brief GPIO 边沿中断回调(SPIready 线跳变)。
*
* - 下降沿:GD32 主动按铃(请求 ESP32 来读)。若当前空闲则置
* m_gd32RequestPending 并唤醒 SPI 任务。
* - 上升沿:Bootloader init 完成(释放 SPIready),唤醒等待
* WAITING_BOOT_READY 的任务。
* - 上升沿:GD32 释放 SPIready(WAITING_RESPONSE 态),唤醒等待任务。
*
* ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。
*/
@@ -386,14 +361,6 @@ void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
eSetBits, &hp);
portYIELD_FROM_ISR(hp);
}
} else if (self->m_linkState.state() == HandshakeState::WAITING_BOOT_READY) {
/* 方案A: Bootloader 已接管(APP 拉低或 BOOT 极早拉低) */
if (self->m_spiTaskHandle) {
BaseType_t hp = pdFALSE;
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x04U,
eSetBits, &hp);
portYIELD_FROM_ISR(hp);
}
}
} else {
/* 上升沿 */
@@ -405,14 +372,6 @@ void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg)
eSetBits, &hp);
portYIELD_FROM_ISR(hp);
}
} else if (self->m_linkState.state() == HandshakeState::WAITING_BOOT_READY) {
/* 方案A: Bootloader init 完成,释放 SPIready */
if (self->m_spiTaskHandle) {
BaseType_t hp = pdFALSE;
xTaskNotifyFromISR(self->m_spiTaskHandle, 0x08U,
eSetBits, &hp);
portYIELD_FROM_ISR(hp);
}
}
}
}
@@ -640,26 +599,9 @@ esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_
// ============================================================
// OTA 帧工具
// ============================================================
/**
* @brief 计算 CRC32(MPEG-2,与 GD32 三端一致)。
* 委托给 v2_crc32,算法完全相同。
*/
uint32_t SpiMaster::otaCrc32(const uint8_t *data, size_t len)
{
return v2_crc32(data, len);
}
/* v1 同步帧头已升级为 v2 16B 头(见 spi_link_v2.h)。
* 旧 6B 头常量保留为兼容注释,不再用于构建/解析。 */
static constexpr size_t kSyncHeaderLen = V2_HEADER_LEN; /* 16 */
static constexpr size_t kSyncCrcLen = V2_CRC_LEN; /* 4 */
static constexpr size_t kSyncOverhead = V2_OVERHEAD; /* 20 */
/* v1 LE 工具已迁移至 spi_link_v2.h 的 v2_put/v2_get 内联函数。 */
/*
* v2 shim:旧 otaBuildFrame 接口委托给 v2_build_frame。
* 使用当前 session/seq 成员填充 v2 帧头,FLAGS=REQUEST。
* otaBuildFrame:使用当前 session/seq 成员填充 v2 帧头,FLAGS=REQUEST。
* 记录 m_lastSentSeq 供响应归属匹配。
*/
bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
@@ -681,7 +623,7 @@ bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t paylo
}
/*
* v2 shim:旧 otaFindFrame 接口委托给 v2_parse_frame + SESSION/SEQ 校验。
* otaFindFrame:委托 v2_parse_frame + SESSION/SEQ 校验。
* ESP32 是主机,收到 GD32 的 RESPONSE/EVENT 帧:
* - ack_seq 携带"GD32 确认收到的 ESP32 请求序号",用于响应归属匹配
* - tx_seq 是 GD32 的发送序号
@@ -734,9 +676,7 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
* 策略:CRC+session 合法的 RESPONSE 一律交给调用方;用 m_lastRxAckMatched
* 标记是否归属当前请求。业务层以结构化 payload(BA C0+seq)做最终判定。 */
if (hdr.flags & V2_FLAG_RESPONSE) {
const bool ack_match = (hdr.ack_seq == self.m_lastSentSeq);
const bool legacy_zero_ack = (hdr.ack_seq == 0U);
self.m_lastRxAckMatched = (ack_match || legacy_zero_ack);
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) {
@@ -745,10 +685,6 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
(unsigned long)s_stale_logs);
}
} else {
if (!ack_match && legacy_zero_ack) {
DEBUG_DBG("SPI", "v2 legacy ack_seq=0 accepted: type=0x%02X expected=%u",
hdr.type, self.m_lastSentSeq);
}
self.m_rxAckedSeq = self.m_lastSentSeq;
}
} else {
@@ -843,9 +779,6 @@ esp_err_t SpiMaster::init()
DEBUG_INFO("SPI", "SPI Ready 开漏握手已配置: GPIO%d (线与 GD32 PB6)", SPI_READY_GPIO);
}
/* 释放 SPIready(开漏高阻),确保空闲态线 HIGH */
releaseReady();
/* 安装 GPIO ISR 服务(全局仅安装一次)并注册边沿回调 */
static bool s_isrInstalled = false;
if (!s_isrInstalled) {
@@ -914,27 +847,6 @@ esp_err_t SpiMaster::init()
// ============================================================
// SPI OTA 协议
// ============================================================
/**
* @brief 按 GD32 SPI OTA 最小协议执行一次请求加响应读取。
* @param type 请求帧类型。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::otaTransfer(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)
{
/* 公开 otaTransfer = transact(L4 worker 串行)。 */
return transact(type, payload, payload_len,
resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
/**
* @brief 无锁版本的 otaTransfer(调用者须已持有锁)。
*/
@@ -966,11 +878,9 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
if (waitSpiDataReady(20U) == ESP_OK) {
uint8_t drain[SPI_OTA_FRAME_MAX];
size_t drain_len = 0U;
driveReadyLow();
if (readV2FrameVarlen(drain, sizeof(drain), &drain_len, 100U) == ESP_OK) {
(void)otaFindFrame(drain, drain_len, nullptr, nullptr, nullptr, nullptr);
}
releaseReady();
} else {
vTaskDelay(pdMS_TO_TICKS(5));
}
@@ -992,16 +902,10 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
* ~4.2s 纯浪费,最终拖垮转发预算导致卡死。CS 本身即互斥,无需额外就绪等待。 */
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
driveReadyLow();
if (SPI_READY_DRIVE_DELAY_US > 0U) {
esp_rom_delay_us(SPI_READY_DRIVE_DELAY_US);
}
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));
releaseReady();
m_linkState.exitBusy();
return ESP_ERR_INVALID_SIZE;
}
@@ -1012,7 +916,6 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
memset(rx_raw, 0xFF, xfer_len);
esp_err_t ret = transferClocked(tx_padded, xfer_len, rx_raw, xfer_len, timeout_ms);
releaseReady();
m_linkState.exitBusy();
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "OTA sync transfer failed: %s", esp_err_to_name(ret));
@@ -1130,9 +1033,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
driveReadyLow();
esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, spi_timeout);
releaseReady();
did_spi_read = true;
if (ret != ESP_OK) {
if (ret == ESP_ERR_INVALID_RESPONSE) {
@@ -1219,7 +1120,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
/**
* @brief ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。
*
* 解决 GD32 "延迟一拍"协议:otaTransfer 首笔 CS 读到过期响应,
* 解决 GD32 "延迟一拍"协议:transact 首笔 CS 读到过期响应,
* otaPollResponse 读取本笔命令的真实响应。命令只发送一次,不重发。
* 适用于所有 ESP32→GD32 方向的命令(CAN 发送/模式切换/ID 快照等)。
*/
@@ -1249,33 +1150,49 @@ esp_err_t SpiMaster::transact(uint8_t type, const uint8_t *payload, size_t paylo
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)
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)) {
/* 与 pushEvent 的 counting sem 对齐 */
if (m_eventSem) {
(void)xSemaphoreTake(m_eventSem, 0);
/* 快速路径:先非阻塞尝试一次 */
{
LockGuard lock(m_mutex);
if (popEvent(type, payload, max_len, len)) {
if (m_eventSem) {
(void)xSemaphoreTake(m_eventSem, 0);
}
return true;
}
if (m_ringRxLen != 0U) {
/* 兼容单槽回落 */
if (type) { *type = m_ringRxType; }
size_t copy_len = (m_ringRxLen <= max_len) ? m_ringRxLen : max_len;
if (len) { *len = copy_len; }
if (payload && copy_len > 0U) {
memcpy(payload, m_ringRxBuf, copy_len);
}
m_ringRxLen = 0U;
m_ringRxType = 0U;
return true;
}
return true;
}
/* 兼容:事件队列空时回落单槽 */
if (m_ringRxLen == 0U) {
/* 队列空:若 timeout_ms==0 直接返回,否则在信号量上等待 */
if (timeout_ms == 0U || m_eventSem == nullptr) {
return false;
}
if (type) {
*type = m_ringRxType;
/* 循环等待:信号量到达后重新持锁取,取空则继续等(防虚假唤醒) */
const TickType_t deadline_ticks = pdMS_TO_TICKS(timeout_ms);
while (true) {
if (xSemaphoreTake(m_eventSem, deadline_ticks) != pdTRUE) {
return false; /* 超时 */
}
LockGuard lock(m_mutex);
if (popEvent(type, payload, max_len, len)) {
return true;
}
/* 信号量被消费但队列空(可能被并发 takeEvent 抢走),继续等 */
}
size_t copy_len = (m_ringRxLen <= max_len) ? m_ringRxLen : max_len;
if (len) {
*len = copy_len;
}
if (payload && copy_len > 0U) {
memcpy(payload, m_ringRxBuf, copy_len);
}
m_ringRxLen = 0U;
m_ringRxType = 0U;
return true;
}
esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t payload_len,
@@ -1386,14 +1303,12 @@ esp_err_t SpiMaster::transactWithRetry(uint8_t type, const uint8_t *payload, siz
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
driveReadyLow();
uint8_t tx_p[SPI_OTA_FRAME_MAX];
uint8_t rx_r[SPI_OTA_FRAME_MAX];
memset(tx_p, 0xFF, xfer_len);
memcpy(tx_p, hello_frame, hello_len);
memset(rx_r, 0xFF, xfer_len);
esp_err_t hr = transferClocked(tx_p, xfer_len, rx_r, xfer_len, 2000U);
releaseReady();
m_linkState.exitBusy();
if (hr != ESP_OK) {
continue;
@@ -1450,9 +1365,7 @@ esp_err_t SpiMaster::appRequestBootloader()
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
driveReadyLow();
(void)readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 50U);
releaseReady();
m_linkState.exitBusy();
vTaskDelay((pdMS_TO_TICKS(5) > 0) ? pdMS_TO_TICKS(5) : 1);
}
@@ -1487,9 +1400,7 @@ esp_err_t SpiMaster::appRequestBootloader()
memcpy(tx_p, hf, hl);
memset(rx_r, 0xFF, xlen);
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
driveReadyLow();
(void)transferClocked(tx_p, xlen, rx_r, xlen, 200U);
releaseReady();
m_linkState.exitBusy();
v2_header_t h;
const uint8_t *pl = nullptr;
@@ -1530,9 +1441,7 @@ esp_err_t SpiMaster::appRequestBootloader()
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
size_t rx_len = 0U;
driveReadyLow();
ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 80U);
releaseReady();
m_linkState.exitBusy();
if (ret != ESP_OK) {
vTaskDelay(gap);
@@ -1691,7 +1600,9 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver
uint8_t resp_payload[32];
size_t resp_len = 0U;
const uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
/* 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);
@@ -1745,92 +1656,6 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver
return ESP_OK;
}
/**
* @brief 检查 GD32 是否有主动按铃请求,若有则发起读事务读取请求帧。
*
* GD32 下拉 SPIready 表示「我有请求帧要给你读」。此函数检测到该状态后
* 发起一次 SPI 读事务(发 dummy 字节,读回 GD32 准备的请求帧)。
*
* @param timeout_ms 等待 GD32 请求的超时(毫秒),0 表示不等待仅检查。
* @return ESP_OK 成功读到请求帧;ESP_ERR_NOT_FOUND 当前无请求;其余为错误码。
*/
esp_err_t SpiMaster::checkGd32Request(uint32_t timeout_ms)
{
/* L4:不改 m_spiTaskHandle(必须始终是 worker)。
* 已有事件则直接 OK;否则提交 ServiceRing 或等待 eventSem。 */
if (m_suppressRingService) {
return ESP_ERR_NOT_FOUND;
}
{
LockGuard lock(m_mutex);
if (m_eventCount > 0U || m_ringRxLen > 0U) {
return ESP_OK;
}
}
if (onWorker()) {
return serviceRingOnce();
}
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(
timeout_ms > 0U ? timeout_ms : 1U);
for (;;) {
{
LockGuard lock(m_mutex);
if (m_eventCount > 0U || m_ringRxLen > 0U) {
return ESP_OK;
}
}
if (timeout_ms == 0U) {
/* 单次尝试:让 worker 立刻 service 一次 */
Job job{};
job.kind = JobKind::ServiceRing;
job.timeout_ms = 50U;
esp_err_t r = submitJob(job);
if (r == ESP_OK) {
return ESP_OK;
}
return ESP_ERR_NOT_FOUND;
}
if (xTaskGetTickCount() >= deadline) {
return ESP_ERR_NOT_FOUND;
}
/* 唤醒 worker 读按铃;再等事件信号 */
if (m_workerTask) {
xTaskNotify(m_workerTask, 0x01U, eSetBits);
}
Job job{};
job.kind = JobKind::ServiceRing;
job.timeout_ms = 100U;
if (submitJob(job) == ESP_OK) {
return ESP_OK;
}
if (m_eventSem) {
TickType_t now = xTaskGetTickCount();
TickType_t left = (deadline > now) ? (deadline - now) : 0;
if (left == 0) {
return ESP_ERR_NOT_FOUND;
}
if (xSemaphoreTake(m_eventSem, left) == pdTRUE) {
/* 信号量被 take 后计数减一,但事件仍在队列里;补回计数给 takeEvent */
(void)xSemaphoreGive(m_eventSem);
return ESP_OK;
}
} else {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
bool SpiMaster::takeRingRx(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len)
{
return takeEvent(type, payload, max_len, len);
}
// ============================================================
// SPI Link v2 链路状态
// ============================================================
@@ -1848,6 +1673,19 @@ SpiMaster::SpiLinkStats SpiMaster::linkStats()
return ret;
}
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;
}
esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
{
if (!onWorker() && m_workerTask != nullptr) {
@@ -1898,7 +1736,6 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
m_linkState.enterBusy(HandshakeState::MASTER_SENDING);
driveReadyLow();
uint8_t tx_padded[SPI_OTA_FRAME_MAX];
memset(tx_padded, 0xFF, xfer_len);
@@ -1906,7 +1743,6 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
memset(rx_raw, 0xFF, xfer_len);
esp_err_t ret = transferClocked(tx_padded, xfer_len, rx_raw, xfer_len, 200U);
releaseReady();
m_linkState.exitBusy();
if (ret != ESP_OK) {
DEBUG_WARN("SPI", "HELLO transfer failed[%lu]: %s",
+24 -52
View File
@@ -31,12 +31,8 @@
#define SPI_READY_GPIO (10) // 线与 SPIready 握手引脚(开漏,GPIO10<->GD32 PB6)
#define SPI_READY_TIMEOUT_MS (200) // 等待 SPIready 上升沿超时(毫秒)
#define SPI_OTA_FRAME_MAX (256) ///< SPI 同步帧最大字节数
// ESP32 下拉 SPIready 后到拉 CS 之间的微秒延时(零延时起步,按需测算调大)
#define SPI_READY_DRIVE_DELAY_US (0)
// GD32 SPI 同步帧类型
#define SPI_SYNC_SOF0 (0xA5)
#define SPI_SYNC_SOF1 (0x5A)
#define SPI_OTA_CMD_REQ (0x01)
#define SPI_OTA_CMD_DATA (0x02)
#define SPI_APP_ENTER_BOOT_CMD (0xB0)
@@ -79,19 +75,13 @@ public:
// SPI OTA 协议
// ============================================================
/** @brief 按 GD32 SPI OTA 协议执行一次请求 + 响应读取(延迟一拍两步法)。
* 内部等价于 otaSendAndPoll:首笔 CS 发请求(忽略旧响应),次笔 CS 收真实响应。 */
esp_err_t otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
/** @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 */
esp_err_t otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
/** @brief ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。
*
* 解决 GD32 "延迟一拍"协议:otaTransfer 首笔 CS 读到过期响应,
* 解决 GD32 "延迟一拍"协议:transact 首笔 CS 读到过期响应,
* otaPollResponse 读取本笔命令的真实响应。命令只发送一次,不重发。
* 适用于所有 ESP32→GD32 方向的命令(CAN 发送/模式切换/ID 快照等)。
*
@@ -112,6 +102,9 @@ public:
/** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */
uint8_t remoteCredit() const { return m_remoteCredit; }
/** @brief 读取本端剩余可接收 credit(SPEC §10.1,/system/status 展示用)。 */
uint8_t localCredit() const { return m_localCredit; }
/**
* @brief OTA 中继期间禁止 worker 按铃读。
*
@@ -136,8 +129,11 @@ public:
/**
* @brief L4 取出 GD32 EVENT 帧(worker 已读入事件队列)。
* @param timeout_ms 等待超时(ms)。0 = 非阻塞(原语义)。
* @return true 取到事件;false 队列空且超时未到达。
*/
bool takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len);
bool takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len,
uint32_t timeout_ms = 0U);
// ============================================================
// SPI Link v2 链路层(阶段1:帧格式 + 状态成员就位)
@@ -155,6 +151,16 @@ public:
uint32_t max_resp_latency_us; ///< 最大响应延迟(微秒)
};
/** @brief v2 完整链路状态(SPEC §10.1,会话/credit + 统计)。 */
struct SpiLinkStatus {
uint8_t version; ///< 协议版本(固定 2)
uint8_t gd32_role; ///< 0=unknown, 1=app, 2=boot
uint32_t session_id; ///< 当前会话 ID
uint8_t local_credit; ///< 本端剩余 credit
uint8_t remote_credit; ///< 对端剩余 credit
SpiLinkStats stats; ///< 8 个统计计数器
};
/** @brief 当前会话 ID(0 = 未建立 session)。 */
uint32_t sessionId() const { return m_sessionId; }
@@ -167,8 +173,8 @@ public:
/** @brief 获取链路统计快照(线程安全,持锁读取)。 */
SpiLinkStats linkStats();
/** @brief L4 链路状态(linkStats 别名)。 */
SpiLinkStats linkStatus() { return linkStats(); }
/** @brief 获取完整链路状态(SPEC §10.1,会话/credit + 统计)。 */
SpiLinkStatus linkStatus();
/** @brief 发起 HELLO 握手建立 v2 session(SPEC §5.1)。
* 生成随机非零 SESSION_ID,发送 LINK_HELLO,等待 HELLO_ACK。
@@ -192,34 +198,6 @@ public:
// ── CANFD 接口已迁移至 transport/canfd_bridge.h(M4 解耦)──
/**
* @brief 检查 GD32 是否有主动按铃请求,若有则发起读事务读取请求帧。
*
* GD32 下拉 SPIready 表示「我有请求帧要给你读」。此函数检测到该状态后
* 发起一次 SPI 读事务(发 dummy 字节,读回 GD32 准备的请求帧)。
* 读取成功后按帧 TYPE 交给上层处理(如 CAN feedback 轮询、重启请求等)。
*
* @param timeout_ms 等待 GD32 请求的超时(毫秒),0 表示不等待仅检查。
* @return ESP_OK 成功读到请求帧;ESP_ERR_NOT_FOUND 当前无请求;其余为错误码。
*
* @note 应在主循环或 OTA 空闲时周期调用,以响应 GD32 主动上报。
*/
esp_err_t checkGd32Request(uint32_t timeout_ms = 0U);
/**
* @brief 取出最近一次 GD32 按铃请求帧(线程安全,取出后清空内部缓冲)。
*
* checkGd32Request() 成功读取一帧后将其 type/payload 存入内部缓冲,
* 上层调用本函数取出并消费。取出后缓冲清空,避免重复消费。
*
* @param[out] type 请求帧 TYPE(如 0xC1)。
* @param[out] payload 输出 payload 拷贝缓冲。
* @param[in] max_len payload 缓冲最大长度。
* @param[out] len 实际 payload 长度。
* @return true 表示有数据被取出;false 表示当前无数据。
*/
bool takeRingRx(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len);
private:
/**
* @brief 私有默认构造函数。
@@ -248,7 +226,6 @@ private:
MASTER_SENDING, ///< ESP32 已下拉 SPIready,正在 CS#1 发送请求
WAITING_RESPONSE, ///< CS#1 完成,等待 GD32 释放 SPIready(上升沿唤醒)
SERVING_GD32_RING, ///< 响应 GD32 按铃,正在读 GD32 请求帧
WAITING_BOOT_READY, ///< 方案A: 等待 Bootloader 就绪(LOW→HIGH 序列)
};
/**
@@ -258,15 +235,15 @@ private:
* - 线 HIGH = 空闲(双方释放),可发起新事务
* - 线 LOW = 忙碌(某方下拉占用),禁止发起新事务
*
* ESP32 作为主动方(otaTransfer/appPing):enterBusy() 占用线 → transferClocked → exitBusy() 释放线
* ESP32 作为按铃响应方(checkGd32Request):同上,releaseReady 上升沿通知 GD32 完成
* ESP32 作为主动方(transact/appPing):enterBusy() 占用线 → transferClocked → exitBusy() 释放线
* ESP32 作为按铃响应方(takeEvent):同上
*/
class SpiLinkState {
public:
/** 进入忙碌态(driveReadyLow 前/后调用)。 */
/** 进入忙碌态。 */
void enterBusy(HandshakeState hs) { m_hsState = hs; }
/** 退出忙碌态(releaseReady 后调用),回到 IDLE。 */
/** 退出忙碌态,回到 IDLE。 */
void exitBusy() { m_hsState = HandshakeState::IDLE; }
/** 当前是否空闲(可接受新按铃下降沿)。 */
@@ -380,10 +357,6 @@ private:
SemaphoreHandle_t m_lock;
};
/** @brief 下拉 SPIready(开漏驱动低)。 */
void driveReadyLow();
/** @brief 释放 SPIready(开漏高阻,靠上拉拉高)。 */
void releaseReady();
/**
* @brief 等 GD32 释放 SPIready(上升沿),中断驱动非忙等。
*
@@ -461,7 +434,6 @@ private:
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
// OTA 帧工具(静态,无状态)
static uint32_t otaCrc32(const uint8_t *data, size_t len);
static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *out, size_t *out_len);
static bool otaFindFrame(const uint8_t *buf, size_t buf_len,
+1 -1
View File
@@ -213,6 +213,6 @@
<span id="toastMsg">操作成功</span>
</div>
<script src="can.js?v=20260701a"></script>
<script src="can.js?v=20260715c"></script>
</body>
</html>
+45 -5
View File
@@ -185,6 +185,7 @@
var messageMap = {};
var comments = { messages: {}, signals: {} };
var valueTables = {};
var sigDefaults = {};
var nodeNames = [];
var current = null;
@@ -234,6 +235,7 @@
comment: '',
valueType: 'int',
enumMap: {},
defaultValue: null,
multiplexRole: muxInfo.role,
multiplexValue: muxInfo.value
});
@@ -272,6 +274,15 @@
if (signal) {
signal.valueType = valTypeMatch[3] === '1' ? 'float' : valTypeMatch[3] === '2' ? 'double' : 'int';
}
continue;
}
/* BA_ "GenSigDefaultValue" SG_ <msgId> <signalName> <value>;
* 注意该值是物理值(已应用 factor/offset),与 GenSigStartValue(raw)不同 */
var sigDefaultMatch = line.match(/^BA_\s+"GenSigDefaultValue"\s+SG_\s+(\d+)\s+([A-Za-z0-9_]+)\s+(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*;$/);
if (sigDefaultMatch) {
sigDefaults[sigDefaultMatch[1] + ':' + sigDefaultMatch[2]] = sigDefaultMatch[3];
continue;
}
}
@@ -280,6 +291,8 @@
msg.signals.forEach(function (signal) {
signal.comment = comments.signals[String(msg.idRaw) + ':' + signal.name] || '';
signal.enumMap = valueTables[String(msg.idRaw) + ':' + signal.name] || {};
var dv = sigDefaults[String(msg.idRaw) + ':' + signal.name];
signal.defaultValue = (dv != null && Number.isFinite(Number(dv))) ? dv : null;
});
});
@@ -704,10 +717,10 @@
}
function getDefaultSignalValue(signal) {
var enumKeys = Object.keys(signal.enumMap || {});
if (enumKeys.length) return enumKeys[0];
if (Number.isFinite(signal.min)) return String(signal.min);
if (signal.factor === 0) return '0';
/* 优先使用 DBC 中 BA_ "GenSigDefaultValue" 声明的物理值,否则回退为 0 */
if (signal && signal.defaultValue != null && Number.isFinite(Number(signal.defaultValue))) {
return String(signal.defaultValue);
}
return '0';
}
@@ -1086,6 +1099,10 @@
updateBatchUi();
var channel = parseInt(el.defaultChannel.value, 10) || 0;
var ok = 0, fail = 0;
/* BATCH 间隔:GD32 SPI 从机用 RBNE 中断逐字节接收,无 RX DMA,
* 连续高频 CS 事务易触发 RXORERR -> 帧字节丢失 -> CRC 失败 -> 0xEE。
* 30ms 间隔给 GD32 足够时间完成 onCsRelease 清理 + EXTI 重新待命。 */
var BATCH_INTERVAL_MS = 30;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var msg = null;
@@ -1144,6 +1161,10 @@
resultText: msg.name + ' · ' + err.message
});
}
/* 帧间间隔,避免 GD32 SPI RXORERR(最后一帧不需要等) */
if (i < keys.length - 1) {
await new Promise(function (r) { setTimeout(r, BATCH_INTERVAL_MS); });
}
}
state.batchInFlight = false;
updateBatchUi();
@@ -1470,7 +1491,17 @@
var CAN_SEND_ERR_DESC = {
'-1': '参数错误(通道/ID/DLC 无效)',
'-2': 'SPI 通信失败(ESP32 与 GD32 通信异常)',
'-3': 'GD32 CAN 总线拒绝(BUS_OFF / TEC 过高 / 邮箱满)',
'-3': 'GD32 CAN 总线拒绝',
};
var GD32_ERR_DESC = {
0xE0: 'payload 过短',
0xE1: '通道号非法',
0xE2: 'BUS_OFF(总线离线,检查接线/终端电阻/对端上电)',
0xE3: 'TEC 过高(错误计数 >96,对端 ACK 失败率高)',
0xE4: '邮箱满(3 个硬件邮箱全 PENDING,对端不 ACK)',
0xE5: '参数无效(通道未启用/DLC 越界)',
0xEE: 'SPI 帧解析失败(GD32 收到的帧 CRC/SOF 校验失败,通常是 SPI 字节丢失或 CS 时序冲突)'
};
function buildSendResponseText(result) {
@@ -1479,6 +1510,15 @@
}
var code = String(result.code);
var desc = CAN_SEND_ERR_DESC[code] || '未知错误';
/* 用 GD32 细分错误码补充具体原因 */
if (result.gd32_err != null && result.gd32_err !== 0) {
var gd32Desc = GD32_ERR_DESC[result.gd32_err];
if (gd32Desc) {
desc = desc + ' · ' + gd32Desc;
} else {
desc = desc + ' · gd32_err=0x' + result.gd32_err.toString(16).toUpperCase();
}
}
return '发送失败: ' + desc + ' (code=' + result.code + ')';
}