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:
@@ -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 |
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
@@ -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()) {
|
||||
|
||||
@@ -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 尾随逗号叠加
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 + ')';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user