feat(ota): 优化 GD32 OTA 流程和 CAN 反馈机制
- 实现 GD32 主动推送 CAN 反馈架构,替代 ESP32 1Hz 轮询方式, 减少 SPI 总线占用提升带宽效率 - 添加 GD32 Bootloader 就绪检测机制,通过 PB6 硬件信号等待 保证 OTA 过程稳定可靠 - 集成 CREDIT 感知功能,当 GD32 写 Flash 缓冲时自动等待, 避免数据包丢失提高传输成功率 - 重构 canFdPollFeedback 为 canFdParseFeedbackPage 函数, 支持被动解析 GD32 主动推送的数据页 - 更新版本号从 0.37 到 0.3.38 并添加 Kconfig 配置选项支持
This commit is contained in:
+2
-1
@@ -1,5 +1,6 @@
|
||||
# ESP32-C3 OTA 中继项目 - 根构建文件
|
||||
# ESP32-C3 OTA 中继项目 - 根构建文件
|
||||
# 要求 ESP-IDF v6.0+,使用 idf.py build 编译
|
||||
# Kconfig.projbuild 提供 DCU_SPI_CLOCK_HZ 等可配置项
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
menu "DCU OTA Configuration"
|
||||
|
||||
config DCU_SPI_CLOCK_HZ
|
||||
int "SPI clock frequency for GD32 (Hz)"
|
||||
default 4000000
|
||||
range 1000000 20000000
|
||||
help
|
||||
SPI clock speed between ESP32-C3 and GD32E50x.
|
||||
Higher value speeds up GD32 OTA relay, but must be
|
||||
validated against actual wiring length/quality and GD32
|
||||
SPI slave timing.
|
||||
|
||||
config DCU_OTA_PACKET_INTERVAL_MS
|
||||
int "Delay between GD32 OTA packets (ms)"
|
||||
default 1
|
||||
range 0 50
|
||||
help
|
||||
Idle delay inserted after each successfully acknowledged
|
||||
firmware packet before sending the next one.
|
||||
Lower value speeds up OTA, but if GD32 cannot finish
|
||||
flash write / prepare next ACK in time, set it back.
|
||||
|
||||
config DCU_OTA_POLL_BACKOFF_MS
|
||||
int "Polling backoff when GD32 is not ready (ms)"
|
||||
default 2
|
||||
range 0 50
|
||||
help
|
||||
Delay between SPI response polls when GD32 returns WAIT
|
||||
or does not drive MISO yet. Lower value reduces per-packet
|
||||
idle time and can significantly speed up OTA, but if GD32
|
||||
cannot finish flash write before the next poll, increase it.
|
||||
|
||||
config DCU_OTA_FINISH_TIMEOUT_MS
|
||||
int "Timeout for FINISH_OTA command (ms)"
|
||||
default 10000
|
||||
range 3000 60000
|
||||
help
|
||||
How long ESP32 waits for GD32 to respond to the FINISH_OTA
|
||||
command. GD32 may need extra time to drain flash buffers,
|
||||
verify firmware, or prepare the APP jump.
|
||||
|
||||
endmenu
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 37
|
||||
#define FW_VERSION_STRING "0.3.37"
|
||||
#define FW_VERSION_PATCH 38
|
||||
#define FW_VERSION_STRING "0.3.38"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-07-06 19:07:54"
|
||||
#define BUILD_TIME "2026-07-08 02:41:52"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "2fe5393"
|
||||
#define GIT_HASH "069ce8e"
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
/*
|
||||
* can_feedback_task.cpp - 周期性轮询 GD32 三路 CAN 1Hz 反馈快照
|
||||
* can_feedback_task.cpp - 被动监听 GD32 主动推送的 CAN 反馈快照
|
||||
*
|
||||
* 通过 SPI 协议帧 0xC1 拉取 GD32 端的通道统计与最近一帧信息,
|
||||
* 用调试日志输出;其它消费者(Web/WebSocket)可直接读取
|
||||
* CanFeedbackTask::snapshot(channel) 拷贝最新一份数据。
|
||||
* 架构(GD32 主动推送,ESP32 被动接收):
|
||||
* GD32 APP 在主循环中调用 canFeedbackPushStep(),循环遍历 3 通道 × 2 页,
|
||||
* 通过 requestEsp32Read(0xC1) 下拉 SPIready 主动按铃,将每页反馈 payload
|
||||
* 推送给 ESP32。ESP32 本任务调用 checkGd32Request() 阻塞等待按铃,读到
|
||||
* 一帧后用 canFdParseFeedbackPage() 解析并累积到 s_work[channel],再
|
||||
* publish() 到 s_latest[channel] 供 Web/WebSocket 读取。
|
||||
*
|
||||
* 替代了原先 ESP32 1Hz 主动发 0xC1 轮询的逻辑,避免占用主线 SPI 带宽。
|
||||
*
|
||||
* OTA 期间通过 pauseCanFeedbackTask(true) 暂停本任务,避免与 OTA 抢占 SPI。
|
||||
*/
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "transport/canfd_bridge.h"
|
||||
#include "transport/spi_master.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
|
||||
@@ -17,17 +25,20 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kPollIntervalMs = 1000U; ///< 1Hz 拉取节拍
|
||||
constexpr uint32_t kPollTimeoutMs = 200U; ///< 单次 SPI 事务超时
|
||||
constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时
|
||||
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
|
||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||
constexpr size_t kRingPayloadMax = 128U; ///< 反馈 payload 最大长度
|
||||
constexpr const char *kTag = "CAN_FB";
|
||||
|
||||
static can_feedback_result_t s_latest[CAN_CHANNEL_MAX];
|
||||
static bool s_valid [CAN_CHANNEL_MAX];
|
||||
static SemaphoreHandle_t s_mutex = nullptr;
|
||||
static volatile bool s_paused = false;
|
||||
static can_channel_feedback_t s_prev_stats[CAN_CHANNEL_MAX];
|
||||
static bool s_prev_valid[CAN_CHANNEL_MAX];
|
||||
static bool s_prev_poll_ok[CAN_CHANNEL_MAX];
|
||||
|
||||
/* 累积工作缓冲:跨 page 0/1 累积每通道的完整反馈 */
|
||||
static can_feedback_result_t s_work [CAN_CHANNEL_MAX];
|
||||
static uint8_t s_workIdx[CAN_CHANNEL_MAX];
|
||||
|
||||
static void publish(uint8_t ch, const can_feedback_result_t &fb)
|
||||
{
|
||||
@@ -41,55 +52,63 @@ static void publish(uint8_t ch, const can_feedback_result_t &fb)
|
||||
|
||||
static void taskLoop(void *)
|
||||
{
|
||||
DEBUG_INFO(kTag, "CAN 反馈轮询任务启动 (1Hz, 0xC1)");
|
||||
DEBUG_INFO(kTag, "CAN 反馈被动监听任务启动 (GD32 主动推送, 0xC1)");
|
||||
|
||||
uint8_t ring_type = 0U;
|
||||
uint8_t ring_payload[kRingPayloadMax] = {};
|
||||
size_t ring_len = 0U;
|
||||
|
||||
can_feedback_result_t fb{};
|
||||
for (;;) {
|
||||
/* OTA 进行中时暂停 CAN 轮询,避免抢占 SPI 导致 OTA 握手失败 */
|
||||
if (s_paused) {
|
||||
vTaskDelay(pdMS_TO_TICKS(kPollIntervalMs));
|
||||
vTaskDelay(pdMS_TO_TICKS(kPauseDelayMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint8_t ch = 0U; ch < CAN_CHANNEL_MAX; ++ch) {
|
||||
const int rc = canFdPollFeedback(static_cast<can_channel_t>(ch),
|
||||
&fb, kPollTimeoutMs);
|
||||
if (rc == 0) {
|
||||
publish(ch, fb);
|
||||
/* 阻塞等待 GD32 按铃并读取一帧(超时则重试) */
|
||||
esp_err_t ret = SpiMaster::instance().checkGd32Request(kWaitRingMs);
|
||||
if (ret != ESP_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fb.stats.error_count != 0U) {
|
||||
DEBUG_INFO(kTag,
|
||||
"CH%u seq=%lu mode=%u rx=%lu tx=%lu drop=%lu err=%lu ids=%u",
|
||||
static_cast<unsigned>(ch),
|
||||
static_cast<unsigned long>(fb.stats.snapshot_seq),
|
||||
static_cast<unsigned>(fb.stats.mode),
|
||||
static_cast<unsigned long>(fb.stats.rx_count),
|
||||
static_cast<unsigned long>(fb.stats.tx_count),
|
||||
static_cast<unsigned long>(fb.stats.rx_drop_count),
|
||||
static_cast<unsigned long>(fb.stats.error_count),
|
||||
static_cast<unsigned>(fb.valid));
|
||||
}
|
||||
/* 取出 checkGd32Request 存入的 type/payload */
|
||||
if (!SpiMaster::instance().takeRingRx(&ring_type, ring_payload,
|
||||
sizeof(ring_payload), &ring_len)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
s_prev_stats[ch] = fb.stats;
|
||||
s_prev_valid[ch] = true;
|
||||
s_prev_poll_ok[ch] = true;
|
||||
} else {
|
||||
if (s_prev_poll_ok[ch]) {
|
||||
DEBUG_WARN(kTag, "CH%u 轮询失败", static_cast<unsigned>(ch));
|
||||
}
|
||||
s_prev_stats[ch] = {};
|
||||
s_prev_valid[ch] = false;
|
||||
s_prev_poll_ok[ch] = false;
|
||||
if (ring_type != kSpiCanFeedbackType || ring_len < 28U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t ch = ring_payload[0];
|
||||
if (ch >= CAN_CHANNEL_MAX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 解析本页并累积到工作缓冲,page=0 会自动重置 */
|
||||
if (canFdParseFeedbackPage(ring_payload, ring_len,
|
||||
&s_work[ch], &s_workIdx[ch]) == 0) {
|
||||
publish(ch, s_work[ch]);
|
||||
|
||||
if (s_work[ch].stats.error_count != 0U) {
|
||||
DEBUG_INFO(kTag,
|
||||
"CH%u seq=%lu mode=%u rx=%lu tx=%lu drop=%lu err=%lu",
|
||||
static_cast<unsigned>(ch),
|
||||
static_cast<unsigned long>(s_work[ch].stats.snapshot_seq),
|
||||
static_cast<unsigned>(s_work[ch].stats.mode),
|
||||
static_cast<unsigned long>(s_work[ch].stats.rx_count),
|
||||
static_cast<unsigned long>(s_work[ch].stats.tx_count),
|
||||
static_cast<unsigned long>(s_work[ch].stats.rx_drop_count),
|
||||
static_cast<unsigned long>(s_work[ch].stats.error_count));
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(kPollIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 启动 1Hz CAN 反馈轮询任务(幂等)。
|
||||
* @brief 启动 CAN 反馈被动监听任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask()
|
||||
@@ -106,6 +125,8 @@ bool startCanFeedbackTask()
|
||||
}
|
||||
memset(s_latest, 0, sizeof(s_latest));
|
||||
memset(s_valid, 0, sizeof(s_valid));
|
||||
memset(s_work, 0, sizeof(s_work));
|
||||
memset(s_workIdx, 0, sizeof(s_workIdx));
|
||||
|
||||
BaseType_t xRet = xTaskCreatePinnedToCore(
|
||||
taskLoop,
|
||||
@@ -127,9 +148,9 @@ void pauseCanFeedbackTask(bool pause)
|
||||
{
|
||||
s_paused = pause;
|
||||
if (pause) {
|
||||
DEBUG_INFO(kTag, "CAN 轮询已暂停(OTA 进行中)");
|
||||
DEBUG_INFO(kTag, "CAN 监听已暂停(OTA 进行中)");
|
||||
} else {
|
||||
DEBUG_INFO(kTag, "CAN 轮询已恢复");
|
||||
DEBUG_INFO(kTag, "CAN 监听已恢复");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+57
-17
@@ -278,6 +278,8 @@ ota_result_t OtaRelay::handleHandshaking(relay_event_t event, const uint8_t *pay
|
||||
DEBUG_INFO("OTA", "=== 阶段1: 握手与认证开始 ===");
|
||||
|
||||
// 步骤1: 请求 GD32 进入 Bootloader
|
||||
// 0xB0 对 APP:触发写标志+复位 → BOOT 启动
|
||||
// 0xB0 对 BOOT:返回 0xB5 + resetToIdle()(IDLE 状态下无害)
|
||||
esp_err_t err = requestGd32AppEnterBootloader();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "GD32 进入 Bootloader 请求未确认,继续尝试握手");
|
||||
@@ -778,31 +780,35 @@ void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48,
|
||||
esp_err_t OtaRelay::requestGd32AppEnterBootloader()
|
||||
{
|
||||
DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口");
|
||||
esp_err_t err = SpiMaster::instance().appRequestBootloader();
|
||||
if (err == ESP_OK) {
|
||||
DEBUG_INFO("OTA", "GD32 已确认进入 Bootloader,等待就绪");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
return ESP_OK;
|
||||
SpiMaster::instance().appRequestBootloader();
|
||||
|
||||
/* 方案A: 通过 PB6 LOW→HIGH 序列等待 Bootloader 就绪
|
||||
* APP 模式: 复位前拉低 PB6 → BOOT 极早拉低 → init 完成释放 HIGH
|
||||
* BOOT 模式: PB6 一直 HIGH,Phase1 超时后直接握手
|
||||
* 超时退化为固定延时(容错旧固件) */
|
||||
esp_err_t wait_err = SpiMaster::instance().waitForBootloaderReady(3000);
|
||||
if (wait_err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "硬件就绪信号超时,退化为固定延时");
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
}
|
||||
DEBUG_WARN("OTA", "GD32 APP 未确认进入 Bootloader: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送握手请求(0x00 0xAC)并等待 GD32 回显 ACK。 */
|
||||
ota_result_t OtaRelay::doHandshakeReq()
|
||||
/** @brief 协议子步骤:发送握手请求(0x00 0xAC)并等待 GD32 回显 ACK。
|
||||
* @param attempts 最大尝试次数,默认 20。先用 3 次快速探测 GD32 是否已在 BOOT。 */
|
||||
ota_result_t OtaRelay::doHandshakeReq(uint32_t attempts)
|
||||
{
|
||||
DEBUG_INFO("OTA", "--- 握手请求 ---");
|
||||
DEBUG_INFO("OTA", "--- 握手请求 (max=%" PRIu32 ") ---", attempts);
|
||||
|
||||
uint8_t payload[] = {0x00, 0xAC};
|
||||
uint8_t last_resp_type = 0U;
|
||||
size_t last_resp_len = 0U;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
constexpr uint32_t kHandshakeAttempts = 10U;
|
||||
constexpr uint32_t kHandshakeReqTimeoutMs = 200U;
|
||||
constexpr uint32_t kHandshakePollTimeoutMs = 200U;
|
||||
constexpr uint32_t kHandshakeRetryGapMs = 100U;
|
||||
|
||||
for (uint32_t attempt = 0U; attempt < kHandshakeAttempts; ++attempt) {
|
||||
for (uint32_t attempt = 0U; attempt < attempts; ++attempt) {
|
||||
uint8_t resp_type = 0U;
|
||||
uint8_t resp_payload[64] = {0};
|
||||
size_t resp_len = 0U;
|
||||
@@ -830,7 +836,7 @@ ota_result_t OtaRelay::doHandshakeReq()
|
||||
}
|
||||
|
||||
DEBUG_WARN("OTA", "HANDSHAKE_REQ 未就绪,重试 %" PRIu32 "/%" PRIu32 ": err=%d resp_type=0x%02X resp_len=%u",
|
||||
attempt + 1U, kHandshakeAttempts, last_err, last_resp_type, (unsigned)last_resp_len);
|
||||
attempt + 1U, attempts, last_err, last_resp_type, (unsigned)last_resp_len);
|
||||
vTaskDelay(pdMS_TO_TICKS(kHandshakeRetryGapMs));
|
||||
}
|
||||
|
||||
@@ -961,7 +967,8 @@ ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta,
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00;
|
||||
/* session_num = nonce_server 前 4 字节,GD32 端做校验防重放/乱序 */
|
||||
memcpy(&tx_buf[0], m_nonceServer, 4);
|
||||
tx_buf[4] = (uint8_t)(seq & 0xFFU);
|
||||
tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU);
|
||||
tx_buf[6] = 0x00;
|
||||
@@ -995,8 +1002,12 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
size_t resp_len = 0;
|
||||
uint32_t retry_count = 0;
|
||||
ota_result_t result = OTA_RESULT_OK;
|
||||
bool got_expected_credit_ok = false;
|
||||
const uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? kLastPacketAckSeq : (seq + 1U);
|
||||
|
||||
// ★ 单包耗时统计,用于定位 OTA 速度瓶颈。
|
||||
TickType_t pkt_start = xTaskGetTickCount();
|
||||
|
||||
// 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。
|
||||
// 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。
|
||||
resend_packet:
|
||||
@@ -1034,7 +1045,30 @@ resend_packet:
|
||||
}
|
||||
}
|
||||
|
||||
/* CREDIT 感知:若 GD32 报 CREDIT=0,说明它在写 Flash 缓冲,
|
||||
* 此时重发只会被丢弃。改为耐心 POLL 等待 CREDIT 恢复,最多等 500ms。 */
|
||||
if (SpiMaster::instance().remoteCredit() == 0U) {
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " GD32 CREDIT=0(写Flash中),POLL 等待...", seq);
|
||||
for (uint32_t cw = 0; cw < 50U; ++cw) {
|
||||
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload,
|
||||
sizeof(resp_payload), &resp_len, 100);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK &&
|
||||
isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
got_expected_credit_ok = true;
|
||||
break;
|
||||
}
|
||||
if (err == ESP_OK && SpiMaster::instance().remoteCredit() > 0U) {
|
||||
break; /* CREDIT 恢复,可以继续轮询或重发 */
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
bool got_expected = false;
|
||||
if (got_expected_credit_ok) {
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
for (uint32_t retry = 0; retry < 10; ++retry) {
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
|
||||
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
|
||||
@@ -1050,6 +1084,7 @@ resend_packet:
|
||||
}
|
||||
// ★ resend 前延迟
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
got_expected_credit_ok = false;
|
||||
goto resend_packet;
|
||||
}
|
||||
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
@@ -1091,6 +1126,10 @@ resend_packet:
|
||||
break;
|
||||
}
|
||||
|
||||
TickType_t pkt_elapsed = xTaskGetTickCount() - pkt_start;
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 完成: elapsed=%" PRIu32 " ms, retries=%" PRIu32, seq,
|
||||
(uint32_t)(pkt_elapsed * portTICK_PERIOD_MS), retry_count);
|
||||
|
||||
if (result != OTA_RESULT_OK) {
|
||||
*out_result = result;
|
||||
return true; // 中止外层循环
|
||||
@@ -1151,8 +1190,8 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
|
||||
// ★ 包间延迟:给 GD32 Bootloader 充足时间完成 flash 写入并准备下一包 ACK。
|
||||
// 无此延迟时,ESP32 立即 poll 会读到上一包的 stale ACK,导致误判为丢包。
|
||||
if (seq < m_transferTotalPackets - 1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
if (seq < m_transferTotalPackets - 1 && CONFIG_DCU_OTA_PACKET_INTERVAL_MS > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(CONFIG_DCU_OTA_PACKET_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -1202,7 +1241,8 @@ ota_result_t OtaRelay::doSendFinishOta()
|
||||
memset(resp_payload, 0, sizeof(resp_payload));
|
||||
|
||||
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload),
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
CONFIG_DCU_OTA_FINISH_TIMEOUT_MS);
|
||||
logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1);
|
||||
|
||||
@@ -317,7 +317,7 @@ private:
|
||||
* @brief 执行握手请求(发送 HANDSHAKE_REQ).
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_TIMEOUT 超时。
|
||||
*/
|
||||
ota_result_t doHandshakeReq();
|
||||
ota_result_t doHandshakeReq(uint32_t attempts = 20U);
|
||||
/**
|
||||
* @brief 执行挑战认证.
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_AUTH 认证失败。
|
||||
|
||||
@@ -106,90 +106,80 @@ int canFdSendNoReply(can_channel_t channel, uint32_t ext_id,
|
||||
|
||||
|
||||
/**
|
||||
* @brief 拉取一路 CAN 通道的 1Hz 反馈 + ID 快照表(GD32 SPI 帧类型 0xC1,分页)。
|
||||
* 连续拉取 page=0 和 page=1,合并为完整结果。
|
||||
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
||||
*
|
||||
* GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后
|
||||
* 调用本函数解析:
|
||||
* page=0 → 重置 out,填充 28 字节固定头 + 首批条目;
|
||||
* page=1 → 在已有条目后追加。
|
||||
* 调用方按通道维护 entryIdx,page=0 时会被重置为 0。
|
||||
*/
|
||||
int canFdPollFeedback(can_channel_t channel,
|
||||
can_feedback_result_t *out,
|
||||
uint32_t timeout_ms)
|
||||
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr) {
|
||||
if (payload == nullptr || out == nullptr || entryIdx == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out->valid = 0;
|
||||
uint8_t entryIdx = 0U;
|
||||
|
||||
constexpr uint8_t kFixedHeader = 28U; /* ch+page+total+cnt+mode+seq+rx+tx+drop+err+tec+rec+bus_state */
|
||||
|
||||
for (uint8_t page = 0U; page < 2U; ++page) {
|
||||
uint8_t req[2] = { static_cast<uint8_t>(channel & CAN_CHANNEL_MASK), page };
|
||||
uint8_t resp_buf[kFeedbackPayloadLen] = {};
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().otaTransfer(
|
||||
kSpiCanFeedbackType,
|
||||
req, 2U,
|
||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
||||
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < kFixedHeader) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 第一页时解析固定头 */
|
||||
if (page == 0U) {
|
||||
out->stats.channel = resp_buf[0];
|
||||
out->stats.mode = resp_buf[4];
|
||||
out->stats.snapshot_seq = readU32Le(&resp_buf[5]);
|
||||
out->stats.rx_count = readU32Le(&resp_buf[9]);
|
||||
out->stats.tx_count = readU32Le(&resp_buf[13]);
|
||||
out->stats.rx_drop_count = readU32Le(&resp_buf[17]);
|
||||
out->stats.error_count = readU32Le(&resp_buf[21]);
|
||||
out->stats.tec = resp_buf[25];
|
||||
out->stats.rec = resp_buf[26];
|
||||
out->stats.bus_state = resp_buf[27];
|
||||
out->total_count = resp_buf[2];
|
||||
}
|
||||
|
||||
const uint8_t entryCount = resp_buf[3];
|
||||
if (entryCount == 0U) break;
|
||||
|
||||
uint16_t offset = kFixedHeader;
|
||||
for (uint8_t i = 0U; i < entryCount && entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) {
|
||||
if (offset + 8U > resp_len) break;
|
||||
|
||||
id_snapshot_entry_t &e = out->entries[entryIdx];
|
||||
e.id = readU32Le(&resp_buf[offset]);
|
||||
e.dlc = resp_buf[offset + 4U];
|
||||
const uint8_t flags = resp_buf[offset + 5U];
|
||||
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
|
||||
e.brs = (flags & 0x02U) ? 1U : 0U;
|
||||
e.esi = (flags & 0x04U) ? 1U : 0U;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
if (offset + entrySize > resp_len) break;
|
||||
|
||||
memcpy(e.data, &resp_buf[offset + 8U], dataLen);
|
||||
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
|
||||
e.data[d] = 0U;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
entryIdx++;
|
||||
}
|
||||
|
||||
if (entryIdx >= out->total_count) break;
|
||||
constexpr uint8_t kFixedHeader = 28U;
|
||||
if (len < kFixedHeader) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t channel = payload[0];
|
||||
const uint8_t page = payload[1];
|
||||
if (channel >= CAN_CHANNEL_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* page=0:重置并解析固定头 */
|
||||
if (page == 0U) {
|
||||
*entryIdx = 0U;
|
||||
out->stats.channel = channel;
|
||||
out->stats.mode = payload[4];
|
||||
out->stats.snapshot_seq = readU32Le(&payload[5]);
|
||||
out->stats.rx_count = readU32Le(&payload[9]);
|
||||
out->stats.tx_count = readU32Le(&payload[13]);
|
||||
out->stats.rx_drop_count = readU32Le(&payload[17]);
|
||||
out->stats.error_count = readU32Le(&payload[21]);
|
||||
out->stats.tec = payload[25];
|
||||
out->stats.rec = payload[26];
|
||||
out->stats.bus_state = payload[27];
|
||||
out->total_count = payload[2];
|
||||
}
|
||||
|
||||
/* 解析本页条目并追加 */
|
||||
const uint8_t entryCount = payload[3];
|
||||
uint16_t offset = kFixedHeader;
|
||||
for (uint8_t i = 0U; i < entryCount && *entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) {
|
||||
if (offset + 8U > len) break;
|
||||
|
||||
id_snapshot_entry_t &e = out->entries[*entryIdx];
|
||||
e.id = readU32Le(&payload[offset]);
|
||||
e.dlc = payload[offset + 4U];
|
||||
const uint8_t flags = payload[offset + 5U];
|
||||
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
|
||||
e.brs = (flags & 0x02U) ? 1U : 0U;
|
||||
e.esi = (flags & 0x04U) ? 1U : 0U;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
if (offset + entrySize > len) break;
|
||||
|
||||
memcpy(e.data, &payload[offset + 8U], dataLen);
|
||||
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
|
||||
e.data[d] = 0U;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
(*entryIdx)++;
|
||||
}
|
||||
|
||||
out->valid = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 接收过滤模式(GD32 SPI 帧类型 0xC2)。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 拉取指定通道的不重复 ID 快照表(GD32 SPI 帧类型 0xC3)。
|
||||
* 分页拉取,每页 8 条,最多拉 2 页(16 条)。
|
||||
|
||||
@@ -121,15 +121,20 @@ typedef struct {
|
||||
} can_feedback_result_t;
|
||||
|
||||
/**
|
||||
* @brief 拉取一路 CAN 通道的 1Hz 反馈快照。
|
||||
* @param channel 目标通道 0/1/2。
|
||||
* @param out 输出快照(不可为 nullptr)。
|
||||
* @param timeout_ms SPI 事务超时。
|
||||
* @return 0 成功,<0 失败(ERROR 应答、payload 长度不足或 SPI 错误)。
|
||||
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
||||
*
|
||||
* GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后
|
||||
* 调用本函数解析。page=0 时重置 out 并填充固定头 + 首批条目;
|
||||
* page=1 时在已有条目后追加。每页解析后置 out->valid=1。
|
||||
*
|
||||
* @param payload GD32 推送帧的 payload([channel, page, total, count, mode, seq, ...])。
|
||||
* @param len payload 长度。
|
||||
* @param out 累积结果(跨 page 0/1 累积)。
|
||||
* @param entryIdx 当前已填充条目数(page 0 时被重置为 0,调用方按通道维护)。
|
||||
* @return 0 成功,<0 失败(payload 过短或通道非法)。
|
||||
*/
|
||||
int canFdPollFeedback(can_channel_t channel,
|
||||
can_feedback_result_t *out,
|
||||
uint32_t timeout_ms);
|
||||
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx);
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式。
|
||||
|
||||
+530
-295
File diff suppressed because it is too large
Load Diff
+99
-104
@@ -1,31 +1,41 @@
|
||||
/*
|
||||
/*
|
||||
* spi_master.h - SPI Master 驱动头文件
|
||||
* CANFD 3通道发送接口 + 底层 SPI 总线
|
||||
*
|
||||
* SpiMaster 类封装 SPI 总线初始化、CANFD 通信和 SPI OTA 协议。
|
||||
*
|
||||
* 握手架构(线与 SPIready,GPIO10<->GD32 PB6 开漏单线):
|
||||
* - 空闲态线 HIGH(双方释放,靠上拉)。
|
||||
* - ESP32 主动发起:下拉 SPIready -> 立刻 CS 发请求 -> CS 释放后保持下拉,
|
||||
* GD32 在 onCsRelease() ISR 内完成帧处理+响应装载后释放 SPIready(高),
|
||||
* ESP32 GPIO 上升沿中断唤醒后发起 CS#2 读响应。
|
||||
* - GD32 主动按铃:下拉 SPIready -> ESP32 下降沿中断唤醒后发起读事务。
|
||||
*/
|
||||
#ifndef SPI_MASTER_H
|
||||
#define SPI_MASTER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "driver/spi_master.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
// SPI 配置常量
|
||||
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
||||
#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
|
||||
#define SPI_CLOCK_HZ CONFIG_DCU_SPI_CLOCK_HZ // 默认 4MHz,可在 menuconfig 手动提速
|
||||
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
||||
#define SPI_QUEUE_SIZE (1) ///< SPI 事务队列深度
|
||||
#define SPI_READY_GPIO (10) // GD32 PB6 SPI Ready 握手输入引脚(HIGH=就绪,LOW=忙)
|
||||
#define SPI_READY_TIMEOUT_MS (100) // waitSpiReady 单次超时(毫秒)
|
||||
#define SPI_OTA_FRAME_MAX (133) ///< SPI OTA 单帧最大字节数
|
||||
#define SPI_QUEUE_SIZE (4) ///< SPI 事务队列深度
|
||||
#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 OTA 帧类型
|
||||
#define SPI_OTA_SOF (0xA5)
|
||||
// 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_OTA_CMD_CANCEL (0x03)
|
||||
#define SPI_APP_ENTER_BOOT_CMD (0xB0)
|
||||
#define SPI_APP_ENTER_BOOT_ACK_OK (0xB5) // GD32 APP 固件须同步修改 ACK 字节为 0xB5
|
||||
#define SPI_OTA_RSP_ACK (0x80)
|
||||
@@ -62,21 +72,6 @@ public:
|
||||
/** @brief 初始化 SPI2 总线与目标从设备。 */
|
||||
esp_err_t init();
|
||||
|
||||
// ============================================================
|
||||
// 底层 SPI 传输
|
||||
// ============================================================
|
||||
|
||||
/** @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。 */
|
||||
esp_err_t syncTransfer(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms);
|
||||
|
||||
/** @brief 仅发送 SPI 请求数据,不关心返回内容。 */
|
||||
esp_err_t send(uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
|
||||
/** @brief 仅接收 SPI 数据,必要时先发送命令字节。 */
|
||||
esp_err_t recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms);
|
||||
|
||||
// ============================================================
|
||||
// SPI OTA 协议
|
||||
// ============================================================
|
||||
@@ -90,21 +85,50 @@ public:
|
||||
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 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */
|
||||
uint8_t remoteCredit() const { return m_remoteCredit; }
|
||||
|
||||
/** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */
|
||||
esp_err_t appRequestBootloader();
|
||||
|
||||
/** @brief 方案A: 通过 PB6 LOW→HIGH 序列等待 Bootloader 就绪。
|
||||
* Phase1 轮询等待 LOW (APP 复位前拉低 / Bootloader 极早拉低);
|
||||
* Phase2 轮询等待 HIGH 并确认稳定 10ms (Bootloader init 完成释放)。
|
||||
* 超时返回 ESP_ERR_TIMEOUT,调用方应退化为固定延时。 */
|
||||
esp_err_t waitForBootloaderReady(uint32_t timeout_ms);
|
||||
|
||||
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
||||
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
|
||||
|
||||
/** @brief 执行带重试的 SPI 同步传输。 */
|
||||
esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, int max_retries);
|
||||
|
||||
// ── CANFD 接口已迁移至 transport/canfd_bridge.h(M4 解耦)──
|
||||
|
||||
/** @brief 获取 SPI 设备句柄(供外部状态查询)。 */
|
||||
spi_device_handle_t deviceHandle() const { return m_spiDevice; }
|
||||
/**
|
||||
* @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:
|
||||
/**
|
||||
@@ -127,39 +151,63 @@ private:
|
||||
spi_device_handle_t m_spiDevice = NULL;
|
||||
SemaphoreHandle_t m_mutex = NULL;
|
||||
|
||||
// ── 线与 SPIready 握手状态机 ──
|
||||
/** 握手状态机枚举。 */
|
||||
enum class HandshakeState : uint8_t {
|
||||
IDLE = 0, ///< 空闲,线 HIGH
|
||||
MASTER_SENDING, ///< ESP32 已下拉 SPIready,正在 CS#1 发送请求
|
||||
WAITING_RESPONSE, ///< CS#1 完成,等待 GD32 释放 SPIready(上升沿唤醒)
|
||||
SERVING_GD32_RING, ///< 响应 GD32 按铃,正在读 GD32 请求帧
|
||||
WAITING_BOOT_READY, ///< 方案A: 等待 Bootloader 就绪(LOW→HIGH 序列)
|
||||
};
|
||||
volatile HandshakeState m_hsState = HandshakeState::IDLE;
|
||||
/** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,SPI 任务消费。 */
|
||||
volatile bool m_gd32RequestPending = false;
|
||||
/** SPI 任务句柄(用于 ISR 唤醒)。 */
|
||||
TaskHandle_t m_spiTaskHandle = NULL;
|
||||
/** GD32 按铃请求帧缓冲区。 */
|
||||
uint8_t m_ringRxBuf[SPI_OTA_FRAME_MAX] = {};
|
||||
size_t m_ringRxLen = 0;
|
||||
uint8_t m_ringRxType = 0; ///< GD32 按铃请求帧 TYPE(checkGd32Request 填充)
|
||||
|
||||
// ── 帧内同步状态机 ──
|
||||
volatile uint8_t m_localCredit = 8; ///< 本端剩余可接收帧数(信用窗口)
|
||||
volatile uint8_t m_remoteCredit = 8; ///< 对端上报的剩余信用
|
||||
|
||||
// RAII 锁守卫
|
||||
class LockGuard {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造加锁守卫,获取互斥锁。
|
||||
* @param mutex 互斥锁句柄。
|
||||
*/
|
||||
explicit LockGuard(SemaphoreHandle_t mutex) : m_lock(mutex) {
|
||||
explicit LockGuard(SemaphoreHandle_t lock) : m_lock(lock) {
|
||||
if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); }
|
||||
}
|
||||
/**
|
||||
* @brief 析构解锁守卫,释放互斥锁。
|
||||
*/
|
||||
~LockGuard() {
|
||||
if (m_lock) { xSemaphoreGive(m_lock); }
|
||||
}
|
||||
/**
|
||||
* @brief 禁止复制构造。
|
||||
*/
|
||||
LockGuard(const LockGuard&) = delete;
|
||||
/**
|
||||
* @brief 禁止复制赋值。
|
||||
*/
|
||||
LockGuard& operator=(const LockGuard&) = delete;
|
||||
private:
|
||||
SemaphoreHandle_t m_lock;
|
||||
};
|
||||
|
||||
/** @brief 等待 GD32 PB6 SPI Ready 握手引脚拉高(就绪),CS 断言前调用。 */
|
||||
esp_err_t waitSpiReady(uint32_t timeout_ms = SPI_READY_TIMEOUT_MS);
|
||||
// waitSpiReady 超时日志限频:每 kSpiReadyWarnEvery 次超时打印一条,避免 GD32 掉线时刷屏
|
||||
static constexpr uint32_t kSpiReadyWarnEvery = 32U; ///< 超时日志打印间隔
|
||||
uint32_t m_spiReadyTimeoutCount = 0U; ///< 累计超时次数(用于限频)
|
||||
/** @brief 下拉 SPIready(开漏驱动低)。 */
|
||||
void driveReadyLow();
|
||||
/** @brief 释放 SPIready(开漏高阻,靠上拉拉高)。 */
|
||||
void releaseReady();
|
||||
/**
|
||||
* @brief 等 GD32 释放 SPIready(上升沿),中断驱动非忙等。
|
||||
*
|
||||
* 调用前须将 m_hsState 置为 WAITING_RESPONSE,ISR 上升沿检测到该状态
|
||||
* 后发 0x02 通知唤醒本任务。GD32 写 Flash 期间保持 PB6 低,本函数阻塞,
|
||||
* 写完拉高 → 唤醒 → 继续,自然避免 overrun。
|
||||
*
|
||||
* @param timeout_ms 超时(毫秒),0 表示不等待。
|
||||
* @return ESP_OK 线已高;ESP_ERR_TIMEOUT 超时。
|
||||
*/
|
||||
esp_err_t waitSpiReady(uint32_t timeout_ms);
|
||||
|
||||
/** @brief GPIO 边沿中断回调(注册到 ESP-IDF GPIO ISR 服务)。 */
|
||||
static void IRAM_ATTR onReadyEdgeISR(void *arg);
|
||||
|
||||
// 内部 SPI 传输
|
||||
/**
|
||||
* @brief 执行底层 SPI 时钟同步传输。
|
||||
@@ -173,21 +221,6 @@ private:
|
||||
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
||||
uint8_t *rx_buf, size_t rx_len,
|
||||
uint32_t timeout_ms = 0U);
|
||||
/**
|
||||
* @brief 执行 SPI 请求-响应组合传输。
|
||||
* @param cmd 命令字节(0 表示无命令)。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param payload_len 负载数据长度。
|
||||
* @param rx_buf 接收数据缓冲区。
|
||||
* @param rx_len 期望接收的数据长度。
|
||||
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *rx_buf, size_t rx_len,
|
||||
uint32_t timeout_ms = 0U);
|
||||
|
||||
|
||||
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
|
||||
/**
|
||||
* @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。
|
||||
@@ -201,50 +234,12 @@ private:
|
||||
esp_err_t otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||
// OTA 帧工具(静态,无状态)
|
||||
/**
|
||||
* @brief 计算 OTA 帧的 8 位 CRC 校验和。
|
||||
* @param data 数据缓冲区。
|
||||
* @param len 数据长度。
|
||||
* @return 计算得到的 CRC 值。
|
||||
*/
|
||||
static uint8_t otaCrc8(const uint8_t *data, size_t len);
|
||||
/**
|
||||
* @brief 构建 OTA 协议帧。
|
||||
* @param type 帧类型。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param payload_len 负载数据长度。
|
||||
* @param out 输出帧缓冲区。
|
||||
* @param out_len 输出实际帧长度。
|
||||
* @return true 表示构建成功,false 表示失败。
|
||||
*/
|
||||
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);
|
||||
/**
|
||||
* @brief 解析 OTA 协议帧。
|
||||
* @param frame 帧数据缓冲区。
|
||||
* @param frame_len 帧数据长度。
|
||||
* @param type 输出解析得到的帧类型。
|
||||
* @param payload 输出指向负载数据的指针。
|
||||
* @param payload_len 输出负载数据长度。
|
||||
* @return true 表示解析成功,false 表示失败。
|
||||
*/
|
||||
static bool otaParseFrame(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *type, const uint8_t **payload, size_t *payload_len);
|
||||
/**
|
||||
* @brief 在数据缓冲区中搜索并解析 OTA 帧。
|
||||
* @param buf 数据缓冲区。
|
||||
* @param buf_len 数据缓冲区长度。
|
||||
* @param type 输出帧类型。
|
||||
* @param payload 输出指向负载数据的指针。
|
||||
* @param payload_len 输出负载数据长度。
|
||||
* @param frame_offset 输出帧在缓冲区中的偏移位置。
|
||||
* @return true 表示找到并成功解析,false 表示未找到。
|
||||
*/
|
||||
static bool otaFindFrame(const uint8_t *buf, size_t buf_len,
|
||||
uint8_t *type, const uint8_t **payload, size_t *payload_len,
|
||||
size_t *frame_offset);
|
||||
};
|
||||
|
||||
#endif // SPI_MASTER_H
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# WiFi perf -- OTA acceleration
|
||||
CONFIG_ESP_WIFI_RX_BA_WIN=6
|
||||
CONFIG_ESP_WIFI_TX_BA_WIN=6
|
||||
CONFIG_DCU_OTA_POLL_BACKOFF_MS=5
|
||||
CONFIG_DCU_OTA_FINISH_TIMEOUT_MS=10000
|
||||
|
||||
# DCU SPI clock -- default 4 MHz, can be raised via menuconfig to speed up GD32 OTA
|
||||
CONFIG_DCU_SPI_CLOCK_HZ=4000000
|
||||
|
||||
# DCU OTA packet interval -- default 1 ms, can be lowered to speed up GD32 OTA
|
||||
CONFIG_DCU_OTA_PACKET_INTERVAL_MS=1
|
||||
|
||||
# LWIP: HTTP server max_open_sockets=4 + WiFi/LWIP 内部 ~3 + UDP debug log + 余量
|
||||
# 原值 7 与 httpd max_open_sockets=4 仅余 3 个 socket,一旦 handler 阻塞即 accept(EMFILE)
|
||||
|
||||
+21
-23
@@ -1,24 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
serial_monitor.py - ESP32 串口日志采集工具
|
||||
import serial, time, sys
|
||||
|
||||
用法: python serial_monitor.py
|
||||
依赖: pip install pyserial
|
||||
功能: 监听 COM15 串口 (115200 波特率),将输出记录到 serial_log.txt,
|
||||
最多运行 5 分钟。
|
||||
"""
|
||||
import serial, sys, time
|
||||
ser = serial.Serial('COM15', 115200, timeout=1) # COM 口和波特率根据实际修改
|
||||
outfile = open('G:\\Robot_v2\\DCU\\ESP32_COMM\\serial_log.txt', 'w', encoding='utf-8') # 输出文件路径
|
||||
start = time.time()
|
||||
while time.time() - start < 300: # 最多 5 分钟
|
||||
line = ser.readline()
|
||||
if line:
|
||||
try:
|
||||
text = line.decode('utf-8', errors='replace').rstrip()
|
||||
outfile.write(text + '\n')
|
||||
outfile.flush()
|
||||
except:
|
||||
pass
|
||||
outfile.close()
|
||||
ser.close()
|
||||
port = 'COM15'
|
||||
baud = 115200
|
||||
outfile = r'G:\Robot_v2\DCU\ESP32_COMM\build\monitor_log.txt'
|
||||
|
||||
print('Monitoring %s at %d baud...' % (port, baud), flush=True)
|
||||
|
||||
try:
|
||||
s = serial.Serial(port, baud, timeout=1)
|
||||
with open(outfile, 'w') as f:
|
||||
while True:
|
||||
data = s.readline()
|
||||
if data:
|
||||
line = data.decode('utf-8', errors='replace').rstrip()
|
||||
ts = time.strftime('%H:%M:%S')
|
||||
msg = '[%s] %s' % (ts, line)
|
||||
print(msg, flush=True)
|
||||
f.write(msg + '\n')
|
||||
f.flush()
|
||||
except Exception as e:
|
||||
print('Error: %s' % e, flush=True)
|
||||
+68
-24
@@ -860,9 +860,12 @@ async function testConnection() {
|
||||
restorePolling();
|
||||
}
|
||||
// 立即发一次请求检测
|
||||
// 手动检测使用独立的 AbortController,避免打断递归调度的 pollStatus
|
||||
var testAbort = new AbortController();
|
||||
var testTimer = setTimeout(function() { try { testAbort.abort(); } catch(_e) {} }, 3000);
|
||||
try {
|
||||
var resp = await fetch(getBaseUrl() + '/system/status', {
|
||||
signal: statusAbortCtrl.signal
|
||||
signal: testAbort.signal
|
||||
});
|
||||
if (resp.ok) {
|
||||
var data = await resp.json();
|
||||
@@ -884,6 +887,8 @@ async function testConnection() {
|
||||
var diag = await diagnoseNetwork();
|
||||
addLog('W', '诊断', diag);
|
||||
showToast('无法连接: ' + diag, 'error');
|
||||
} finally {
|
||||
clearTimeout(testTimer);
|
||||
}
|
||||
}
|
||||
async function refreshSystemStatusNow(event) {
|
||||
@@ -891,10 +896,12 @@ async function refreshSystemStatusNow(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
// ★ ESP32 上传期间也允许日志轮询,让用户看到完整进展
|
||||
// 手动刷新使用独立的 AbortController,避免与递归调度的 pollStatus 互相干扰
|
||||
var manualAbort = new AbortController();
|
||||
var manualTimer = setTimeout(function() { try { manualAbort.abort(); } catch(_e) {} }, 3000);
|
||||
try {
|
||||
var resp = await fetch(getBaseUrl() + '/system/status', {
|
||||
signal: statusAbortCtrl.signal
|
||||
signal: manualAbort.signal
|
||||
});
|
||||
if (!resp.ok) {
|
||||
addLog('E', '状态', '刷新失败: HTTP ' + resp.status);
|
||||
@@ -910,6 +917,8 @@ async function refreshSystemStatusNow(event) {
|
||||
} catch (e) {
|
||||
addLog('E', '状态', '刷新失败: ' + e.message);
|
||||
showToast('状态刷新失败', 'error');
|
||||
} finally {
|
||||
clearTimeout(manualTimer);
|
||||
}
|
||||
}
|
||||
// ==================== CAN 帧发送 ====================
|
||||
@@ -1757,7 +1766,9 @@ var lastDiagTime = 0; // 上次诊断时间
|
||||
var diagResult = ''; // 最近一次诊断结果
|
||||
var lastSysStateCode = -1; // 上次成功的 sys_state_code,用于失败时判断设备是否在忙
|
||||
var lastRelayState = 0; // 上次成功的 relay_state
|
||||
var statusAbortCtrl = null; // 取消上一次 in-flight pollStatus,防止堆积刷新
|
||||
var statusAbortCtrl = null; // 当前 /system/status 请求的 AbortController(3s 超时用)
|
||||
var statusPollIntervalMs = 1000; // 状态轮询目标间隔
|
||||
var statusPollInFlight = false; // 是否正在请求 /system/status,防止重叠
|
||||
// 从设备 IP 推导网关 IP(取前三段 + .1)
|
||||
function getGatewayIp() {
|
||||
var parts = getEspIp().split('.');
|
||||
@@ -1798,64 +1809,88 @@ async function diagnoseNetwork() {
|
||||
diagResult = result.join(';');
|
||||
return diagResult;
|
||||
}
|
||||
/** @description 调度下一次 /system/status 轮询(递归 setTimeout)。
|
||||
* 上一次请求完成(或 3s 超时)后才调度下一次,避免 setInterval 把慢响应
|
||||
* 直接 abort 而导致堆/任务栈/CPU 等卡片刷新率不稳。
|
||||
*/
|
||||
function scheduleStatusPoll(delayMs) {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
statusTimerId = setTimeout(function() {
|
||||
statusTimerId = null;
|
||||
pollStatus();
|
||||
}, delayMs !== undefined ? delayMs : statusPollIntervalMs);
|
||||
}
|
||||
|
||||
/** @description 启动定时轮询(状态 + 日志)。 */
|
||||
|
||||
function startPolling() {
|
||||
consecutivePollFailures = 0;
|
||||
pollSlowMode = false;
|
||||
if (statusTimerId) clearInterval(statusTimerId);
|
||||
statusPollIntervalMs = 1000;
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
if (logsTimerId) clearInterval(logsTimerId);
|
||||
statusTimerId = setInterval(pollStatus, 1000);
|
||||
scheduleStatusPoll(0); // 立即开始第一次,完成后按 interval 递归调度
|
||||
logsTimerId = setInterval(pollLogs, 1000);
|
||||
}
|
||||
function slowDownPolling() {
|
||||
pollSlowMode = true;
|
||||
if (statusTimerId) clearInterval(statusTimerId);
|
||||
statusPollIntervalMs = 10000;
|
||||
// 不中断正在进行的请求;它完成后会按新的 10s 间隔调度下一次
|
||||
if (!statusPollInFlight) {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
scheduleStatusPoll(statusPollIntervalMs);
|
||||
}
|
||||
if (logsTimerId) clearInterval(logsTimerId);
|
||||
statusTimerId = setInterval(pollStatus, 10000);
|
||||
logsTimerId = setInterval(pollLogs, 10000);
|
||||
}
|
||||
function restorePolling() {
|
||||
if (pollSlowMode) {
|
||||
pollSlowMode = false;
|
||||
consecutivePollFailures = 0;
|
||||
if (statusTimerId) clearInterval(statusTimerId);
|
||||
statusPollIntervalMs = 1000;
|
||||
if (logsTimerId) clearInterval(logsTimerId);
|
||||
statusTimerId = setInterval(pollStatus, 1000);
|
||||
logsTimerId = setInterval(pollLogs, 1000);
|
||||
}
|
||||
// 如果当前没有请求在飞,立即触发一次,确保 UI 尽快恢复
|
||||
if (!statusPollInFlight) {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
scheduleStatusPoll(0);
|
||||
}
|
||||
}
|
||||
function pausePollingForUpload() {
|
||||
if (pollingPausedForUpload) return;
|
||||
pollingPausedForUpload = true;
|
||||
uploadPollChip = activeUploadChip || window.flashTarget || null;
|
||||
consecutivePollFailures = 0;
|
||||
if (statusTimerId) clearInterval(statusTimerId);
|
||||
if (logsTimerId) clearInterval(logsTimerId);
|
||||
statusPollIntervalMs = uploadPollChip === "gd32" ? 2500 : 3000;
|
||||
// ★ ESP32/GD32 上传期间均保持轮询,让用户在网页上看到完整 OTA 进展
|
||||
// ESP32 用 3s 间隔(上传为主),GD32 用 2.5s(中继进度依赖 status)
|
||||
statusTimerId = setInterval(pollStatus, uploadPollChip === "gd32" ? 2500 : 3000);
|
||||
logsTimerId = setInterval(pollLogs, uploadPollChip === "gd32" ? 2500 : 3000);
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
if (logsTimerId) clearInterval(logsTimerId);
|
||||
scheduleStatusPoll(statusPollIntervalMs);
|
||||
logsTimerId = setInterval(pollLogs, statusPollIntervalMs);
|
||||
}
|
||||
function resumePollingAfterUpload() {
|
||||
if (!pollingPausedForUpload) return;
|
||||
pollingPausedForUpload = false;
|
||||
uploadPollChip = null;
|
||||
consecutivePollFailures = 0;
|
||||
if (pollSlowMode) {
|
||||
statusTimerId = setInterval(pollStatus, 10000);
|
||||
logsTimerId = setInterval(pollLogs, 10000);
|
||||
statusPollIntervalMs = pollSlowMode ? 10000 : 1000;
|
||||
if (logsTimerId) clearInterval(logsTimerId);
|
||||
logsTimerId = setInterval(pollLogs, statusPollIntervalMs);
|
||||
// 上传结束立即刷新一次状态;若当前已在请求中,则让它完成后按新间隔继续
|
||||
if (statusPollInFlight) {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
} else {
|
||||
statusTimerId = setInterval(pollStatus, 1000);
|
||||
logsTimerId = setInterval(pollLogs, 1000);
|
||||
scheduleStatusPoll(0);
|
||||
}
|
||||
pollStatus();
|
||||
pollLogs();
|
||||
}
|
||||
/** @description 轮询 ESP32 系统状态(GET /status)并更新 UI。 */
|
||||
async function pollStatus() {
|
||||
// ★ 上传期间状态轮询间隔已在 pausePollingForUpload 中缩短,此处不再拦截
|
||||
// 取消上一次 in-flight 请求,防止慢响应堆积导致 UI 瞬间刷新多次
|
||||
if (statusAbortCtrl) { try { statusAbortCtrl.abort(); } catch(_e) {} }
|
||||
// 递归 setTimeout 调度,正常情况下不会重叠;做个保险避免并发请求
|
||||
if (statusPollInFlight) return;
|
||||
statusPollInFlight = true;
|
||||
statusAbortCtrl = new AbortController();
|
||||
var pollTimer = setTimeout(function() { try { statusAbortCtrl.abort(); } catch(_e) {} }, 3000);
|
||||
try {
|
||||
@@ -1942,6 +1977,9 @@ async function pollStatus() {
|
||||
updateConnectionUi(false, '未连接');
|
||||
applyDisconnectedRuntimeState();
|
||||
}
|
||||
} finally {
|
||||
statusPollInFlight = false;
|
||||
scheduleStatusPoll();
|
||||
}
|
||||
}
|
||||
startPolling();
|
||||
@@ -1953,7 +1991,13 @@ document.addEventListener('visibilitychange', function () {
|
||||
pageVisible = !document.hidden;
|
||||
if (!pageVisible) return;
|
||||
consecutivePollFailures = 0;
|
||||
pollStatus();
|
||||
// 切回前台立即刷新状态;若当前已在请求中,取消等待中的下一次让它尽快重排
|
||||
if (statusPollInFlight) {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
} else {
|
||||
if (statusTimerId) clearTimeout(statusTimerId);
|
||||
scheduleStatusPoll(0);
|
||||
}
|
||||
pollLogs();
|
||||
// 之前若在慢速模式(10s),切回前台先用 1s 观察一会儿
|
||||
if (pollSlowMode) restorePolling();
|
||||
|
||||
Reference in New Issue
Block a user