feat(transport): 添加SPI Ready握手引脚支持并优化GPIO驱动依赖

添加ESP32-C3 GPIO驱动组件以支持SPI Ready握手机制,新增
SPI_READY_GPIO(10)引脚配置用于监测GD32 PB6就绪状态,避免SPI
通信时从机未准备好的丢帧问题。
This commit is contained in:
小龙虾
2026-07-06 19:14:13 +08:00
parent 2fe5393769
commit 069ce8e6a5
17 changed files with 251 additions and 76 deletions
+8 -2
View File
@@ -54,16 +54,23 @@ inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4; ///< AP 模
// ============================================================
inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服务器监听端口
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< HTTP 服务器任务栈大小(字节)
// ★ 曾降至 6142 省 2KB,但 system_status_handler→sample_cpu_usage() 在栈上分配
// TaskStatus_t tasks[24](~864B)+previous_samples[24](192B)+CpuUsageSnapshot(~608B)≈1.7KB,
// 加上 spi_send_handler 的 buf[512]+resp[1024]+SPI 调用链 ≈3.5KB,
// 再加 httpd 框架开销 ~2KB → 总需求逼近 6KB,6144 栈导致栈溢出 → TCB 被破坏
// → "Stack protection fault"(栈边界被 "http" ASCII 覆盖)。恢复 8192 留足余量。
// ============================================================
// OTA 配置
// ============================================================
inline constexpr size_t OTA_BLOCK_SIZE = 8192;
inline constexpr size_t OTA_BLOCK_SIZE = 4096; ///< OTA 上传静态接收缓冲区,从 8192 降到 4096 省 4KB
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB = 221184B = 0x35F90,固件大小上限
inline constexpr size_t GD32_OTA_PACKET_SIZE = 48; // GD32 OTA 实际转发包载荷
inline constexpr uint32_t OTA_POLL_RETRIES = 40; // 每包 ACK 轮询次数
inline constexpr uint32_t OTA_RESEND_RETRIES = 10; // 每包重发次数上限
inline constexpr uint32_t OTA_PACKET_DELAY_MS = 5; // 每包发送后延时(ms),给 GD32 处理时间
inline constexpr uint32_t OTA_POST_SEND_DELAY_MS = 3; // 发送请求后首次轮询前的延迟(ms),无 READY 拉低时防止读到 stale 数据
inline constexpr uint32_t OTA_POLL_NO_FRAME_DELAY_MS = 8; // 轮询未找到帧时的重试延迟(ms),无 READY 拉低时需更长
// ============================================================
// 健康监控阈值
@@ -127,4 +134,3 @@ inline constexpr uint8_t GD32_CHIP_UID[] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
};
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 36
#define FW_VERSION_STRING "0.3.36"
#define FW_VERSION_PATCH 37
#define FW_VERSION_STRING "0.3.37"
// 编译时间
#define BUILD_TIME "2026-07-02 01:59:27"
#define BUILD_TIME "2026-07-06 19:07:54"
// Git 信息
#define GIT_HASH "6335d05"
#define GIT_HASH "2fe5393"
+19 -4
View File
@@ -118,10 +118,12 @@ char* DebugLog::ringDrainJson(int *out_len)
if (!out_len) return NULL;
*out_len = 0;
int count = ringCount();
// ★ 限制单次导出条数,避免 count=64 时 malloc(32KB) 在堆紧张时失败
// 前端每秒轮询,16 条/次足够保持实时性;超出的旧条目下一轮再拉
static constexpr int kMaxDrain = 16;
if (count > kMaxDrain) count = kMaxDrain;
int capacity = count * (LOG_RING_SIZE * 2) + 16;
if (capacity < 16) capacity = 16;
// 当 count 较大(如 64)时 capacity 可能达 32KB+,单次 malloc 有风险。
// 当前为简化实现保留单次 malloc;若后续日志量增大,可改为按 8KB 分块 realloc。
char *buf = (char*)malloc(capacity);
if (!buf) return NULL;
@@ -390,14 +392,27 @@ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ..
}
snprintf(prefix, sizeof(prefix), "[%s][%s]", level_str, tag);
// 控制单条日志栈缓冲大小,避免调用任务(如 can_fb)因日志格式化爆栈。
va_list args;
va_start(args, fmt);
char msg[512];
char msg[256];
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
char log_buf[600];
char log_buf[320];
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
// ★ snprintf 截断时返回"本应写入的长度"(可能 > sizeof(log_buf)),
// 直接传给 send() 会越界读取栈外内存 → 破坏相邻任务 TCB → "Stack protection fault"
// (崩溃栈中出现 HTTP User-Agent / "/ota/relay" 等字符串即此症状)。
// 截断到实际缓冲区大小,避免越界读。
if (total < 0) {
return;
}
if ((size_t)total >= sizeof(log_buf)) {
total = (int)sizeof(log_buf) - 1;
log_buf[total] = '\0';
}
send((const uint8_t *)log_buf, total);
}
+19 -32
View File
@@ -224,19 +224,6 @@ void HealthMonitor::taskEntry(void *pvParameters)
}
}
// ============================================================
// 健康事件日志
// ============================================================
/**
* @brief 记录一条健康事件日志。
* @param event 事件名称。
* @param value 事件值。
*/
void HealthMonitor::logEvent(const char *event, int value)
{
DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value);
}
// ============================================================
// OTA 健康检查
// ============================================================
@@ -353,30 +340,30 @@ void HealthMonitor::setWifiConnected(bool connected)
*/
health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
{
health_state_t cur = getState();
health_state_t next = SYS_NORMAL;
if (rssi < WIFI_RSSI_CRITICAL) {
setState(SYS_CRITICAL);
DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
logEvent("WIFI危险", rssi);
return HEALTH_MON_CRITICAL;
next = SYS_CRITICAL;
} else if (rssi < WIFI_RSSI_NORMAL) {
health_state_t cur = getState();
if (cur < SYS_WARNING) {
setState(SYS_WARNING);
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
logEvent("WIFI警告", rssi);
}
return HEALTH_MON_WARNING;
next = SYS_WARNING;
}
// RSSI 已恢复正常(>= WIFI_RSSI_NORMAL):从 WARNING 或 CRITICAL 任一状态降级到 NORMAL。
// 历史缺陷:原仅当 cur == SYS_WARNING 时降级,导致 CRITICAL 状态下 RSSI 恢复正常无法降级。
// 注意:SAFE_SHUTDOWN 不应被 RSSI 恢复降级,仅处理 WARNING/CRITICAL。
health_state_t cur = getState();
if ((cur == SYS_WARNING || cur == SYS_CRITICAL) && rssi >= WIFI_RSSI_NORMAL) {
setState(SYS_NORMAL);
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm (从 %d 降级)", rssi, WIFI_RSSI_NORMAL, (int)cur);
logEvent("WIFI恢复", rssi);
// 仅在状态发生转换时打印一次,避免同一阈值下反复刷屏。
if (next != cur) {
if (next == SYS_CRITICAL) {
setState(SYS_CRITICAL);
DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
} else if (next == SYS_WARNING) {
setState(SYS_WARNING);
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
} else { // next == SYS_NORMAL,从 WARNING/CRITICAL 恢复
setState(SYS_NORMAL);
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm (从 %d 降级)", rssi, WIFI_RSSI_NORMAL, (int)cur);
}
}
if (next == SYS_CRITICAL) return HEALTH_MON_CRITICAL;
if (next == SYS_WARNING) return HEALTH_MON_WARNING;
return HEALTH_MON_OK;
}
-2
View File
@@ -65,8 +65,6 @@ public:
/** @brief OTA 中继转发阶段执行健康检查(跳过 WiFi 检查)。 */
health_status_t checkBeforeOtaRelay();
/** @brief 记录一条健康事件日志。 */
void logEvent(const char *event, int value);
/** @brief 获取当前系统健康状态(线程安全)。 */
health_state_t getCurrent();
+2
View File
@@ -456,6 +456,7 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING (epoch=%u, channel=%d)",
total_size, m_otaEpoch, (int)channel);
pauseCanFeedbackTask(true);
} else {
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
}
@@ -582,6 +583,7 @@ void OtaCoordinator::doAbortLocked()
Esp32OtaCore::instance().abort();
}
pauseCanFeedbackTask(false);
m_sysState = OTA_SYS_IDLE;
m_gd32StorageDone = false;
m_busySinceTick = 0;
+25 -8
View File
@@ -418,7 +418,7 @@ esp_err_t WifiOta::httpServerStart()
config.server_port = HTTP_SERVER_PORT;
config.stack_size = HTTP_SERVER_STACK_SIZE;
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
config.max_open_sockets = 4; // LWIP_MAX_SOCKETS 默认 7,HTTP 内部占 3,最多 4
config.max_open_sockets = 4; // 配合 LWIP_MAX_SOCKETS=12: 4(httpd) + 1(UDP) + ~3(LWIP内部) = 8, 余量 4; 空闲时前端 status/logs/上传可能并发,3 个易被 LRU 踢掉导致连接中断
config.lru_purge_enable = true; // socket 满时自动清理最久未用的连接
config.recv_wait_timeout = 30; // GD32 上传前会擦除 Flash,5 秒默认值过紧
config.send_wait_timeout = 30;
@@ -734,6 +734,30 @@ void WifiOta::taskEntry(void *pvParameters)
DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
}
// ★ 开机清理:如果 NVS 中有不完整的 OTA 元数据(上次 OTA 中断),
// GD32 可能还卡在 Bootloader 中,导致 SPI 全面超时 → HTTP socket 饥荒。
// 主动清除残留元数据并尝试恢复 GD32 APP。
{
ota_meta_t boot_meta;
if (OtaCoordinator::instance().loadGd32Meta(&boot_meta) == ESP_OK &&
boot_meta.total_size > 0 && boot_meta.stored_size < boot_meta.total_size) {
DEBUG_WARN("WIFI", "检测到不完整 OTA 缓存 (stored=%u total=%u),清除残留状态",
(unsigned)boot_meta.stored_size, (unsigned)boot_meta.total_size);
OtaCoordinator::instance().clearGd32Meta();
OtaCoordinator::instance().abortCurrent();
// 给 GD32 时间从 Bootloader 超时退出并重启到 APP
vTaskDelay(pdMS_TO_TICKS(3000));
// 尝试探测 GD32 APP 是否恢复
gd32_version_t ver = {};
if (OtaCoordinator::instance().probeGd32AppOnce(&ver, nullptr) == ESP_OK) {
DEBUG_INFO("WIFI", "GD32 APP 已恢复: v%d.%d.%d.%d",
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build);
} else {
DEBUG_WARN("WIFI", "GD32 APP 未恢复,SPI 链路可能不可用");
}
}
}
// ★ WiFi 已连接,UDP 从这里开始能通
// 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
vTaskDelay(pdMS_TO_TICKS(500));
@@ -772,16 +796,9 @@ void WifiOta::taskEntry(void *pvParameters)
int8_t rssi = WifiOta::getRssi();
if (rssi != -127) {
HealthMonitor::instance().updateFromWifiRssi(rssi);
if (rssi < WIFI_RSSI_CRITICAL) {
DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm (阈值: %d)", rssi, WIFI_RSSI_CRITICAL);
} else if (rssi < WIFI_RSSI_WARNING) {
DEBUG_WARN("WIFI", "WiFi RSSI 较弱: %d dBm", rssi);
}
}
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
+12 -1
View File
@@ -4,6 +4,7 @@
#include <string.h>
#include "canfd_bridge.h"
#include "spi_master.h"
#include "monitor/debug_log.h"
namespace {
@@ -39,7 +40,16 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
&resp_len,
timeout_ms);
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
return -1;
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));
return -2; /* SPI 通信失败 */
}
/* resp_type != ACK: 读取 GD32 返回的错误码 */
uint8_t gd32_err = (resp_len > 0 && recv_buf) ? recv_buf[0] : 0xFF;
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 */
}
if (recv_buf == nullptr || recv_max == 0U) {
@@ -67,6 +77,7 @@ static inline uint32_t readU32Le(const uint8_t *p) {
/**
* @brief 发送一帧 CANFD 数据,可选等待并读取响应。
* 通过 SPI 协议帧发送给 GD32 APP,并读取 ACK/ERROR 响应。
* 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。
*/
int canFdSend(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc,
+61
View File
@@ -12,6 +12,7 @@
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "soc/spi_struct.h"
#include "esp_err.h"
#include "esp_log.h"
@@ -35,6 +36,44 @@ SpiMaster& SpiMaster::instance()
return inst;
}
/**
* @brief 等待 GD32 PB6 SPI Ready 握手引脚拉高(就绪)。
* GD32 从机在 CS 断言瞬间拉低 PB6(忙),在响应帧就绪后拉高(就绪)。
* 主机每次发起 CS 前调用,确保从机已准备好接收下一笔事务。
* @param timeout_ms 超时时间(毫秒),0 表示不等待直接返回。
* @return ESP_OK 表示从机就绪;ESP_ERR_TIMEOUT 表示超时(退化为继续发起)。
*/
esp_err_t SpiMaster::waitSpiReady(uint32_t timeout_ms)
{
if (timeout_ms == 0U) {
return ESP_OK;
}
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
for (;;) {
int level = gpio_get_level((gpio_num_t)SPI_READY_GPIO);
if (level == 1) {
/* 从机就绪:复位超时计数器,下次掉线时首次超时又能立即告警 */
m_spiReadyTimeoutCount = 0U;
return ESP_OK;
}
if (xTaskGetTickCount() >= deadline) {
/* 限频:首次超时立即告警,之后每 kSpiReadyWarnEvery 次打印一条,
* 避免 GD32 掉线/卡死时高频日志刷屏。计数器无锁:仅在持锁的
* transferClocked 路径调用,单任务访问,无需保护。 */
++m_spiReadyTimeoutCount;
if (m_spiReadyTimeoutCount == 1U ||
(m_spiReadyTimeoutCount % kSpiReadyWarnEvery) == 0U) {
DEBUG_WARN("SPI", "waitSpiReady 超时: GPIO%d 仍为 LOW(从机忙), timeout=%ums, 累计=%lu",
SPI_READY_GPIO, (unsigned)timeout_ms,
(unsigned long)m_spiReadyTimeoutCount);
}
return ESP_ERR_TIMEOUT;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
// ============================================================
// 内部 SPI 传输
// ============================================================
@@ -69,6 +108,11 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
return ESP_ERR_INVALID_SIZE;
}
/* CS 断言前等待 GD32 PB6 拉高(就绪):从机在上一笔 onCsRelease/
* processCommand 末尾才会拉高 PB6,提前发起 CS 会因从机未装好
* DMA/响应帧而丢失字节。超时退化为继续发起,保留原有容错路径。 */
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
uint8_t tx_staging[kSpiTransferMaxBytes];
uint8_t rx_staging[kSpiTransferMaxBytes];
const uint8_t *tx_ptr = tx_buf;
@@ -336,6 +380,23 @@ esp_err_t SpiMaster::init()
DEBUG_INFO("SPI", "SPI2 总线初始化完成: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO%d, CLK=%d Hz",
SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ);
/* 配置 SPI Ready 握手输入引脚(GPIO10),监测 GD32 PB6 电平。
* GD32 在 CS 下降沿拉低 PB6(忙),在 onCsRelease/processCommand 末尾拉高(就绪)。
* 主机每次发起 CS 前须等待 PB6=HIGH,避免从机未就绪时抢跑丢帧。 */
gpio_config_t ready_cfg = {};
ready_cfg.pin_bit_mask = (1ULL << SPI_READY_GPIO);
ready_cfg.mode = GPIO_MODE_INPUT;
ready_cfg.pull_up_en = GPIO_PULLUP_ENABLE;
ready_cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
ready_cfg.intr_type = GPIO_INTR_DISABLE;
esp_err_t gpio_ret = gpio_config(&ready_cfg);
if (gpio_ret != ESP_OK) {
DEBUG_WARN("SPI", "SPI Ready 握手引脚 GPIO%d 配置失败: %s", SPI_READY_GPIO, esp_err_to_name(gpio_ret));
/* 非致命:握手失败时退化为不等待,保持原有行为 */
} else {
DEBUG_INFO("SPI", "SPI Ready 握手输入已配置: GPIO%d (监测 GD32 PB6)", SPI_READY_GPIO);
}
return ESP_OK;
}
+8 -1
View File
@@ -1,4 +1,4 @@
/*
/*
* spi_master.h - SPI Master 驱动头文件
* CANFD 3通道发送接口 + 底层 SPI 总线
*
@@ -17,6 +17,8 @@
#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
#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 单帧最大字节数
// GD32 SPI OTA 帧类型
@@ -153,6 +155,11 @@ 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; ///< 累计超时次数(用于限频)
// 内部 SPI 传输
/**
* @brief 执行底层 SPI 时钟同步传输。