feat(core): 升级固件至v0.4.1并增加GD32初始化状态监控功能
- 修复GD32 OTA中继全程ring抑制问题:handleIdle起点reset()曾立即解除 taskEntry设置的suppressRingService(true),导致中继全程worker偷读OTA ACK/WAIT - 增加reset(bool release_ring_suppress)参数控制ring抑制释放时机 - 新增GD32 APP初始化状态可视化功能(配套GD32 1.5.34+): * 通过SPI 0xA3查询模块位图/关键失败位图/复位源 * /system/status接口新增gd32_init字段提供状态详情 * 支持故障码数组显示,前端可直接显示中文失败原因 - 添加GD32模块级故障码支持(配套GD32 1.5.36): * 解析INIT_STATUS响应中的fault_codes数组 * 每模块1B故障码,支持CANFD通道号显示 - 电机状态协议升级:STATUS DLC从32字节扩展到48字节, 二进制协议版本升级到v2,entry大小从76字节变为92字节 - 修复SPI响应类型表,将0xA3加入短响应和APP ISR快速响应类型
This commit is contained in:
@@ -13,9 +13,9 @@
|
||||
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 82
|
||||
#define FW_VERSION_STRING "0.3.82" // DebugLog 新增 UART0 RX 泵任务(GPIO20, 3M 8N1):收到的串口行按 \n 切行、剥 \r,以 tag=UART 推入 ring → HTTP /logs 出现在网页日志面板;接线=任意串口源 TX→H1/H2 的 ESP_RX。0.3.81 GD32 OTA 包级防呆:中继触发前校验 gd32_ota 镜像向量表身份(Gd32FlashStore::validateStoredImageIdentity),BOOT 包/非 APP 包在 WiFi /ota/relay 与 BLE 0xF7 均拒绝,GD32 APP 区不再被错误包擦除;BLE 新增 0x87 完成码
|
||||
#define FW_VERSION_MINOR 4
|
||||
#define FW_VERSION_PATCH 1
|
||||
#define FW_VERSION_STRING "0.4.1" // GD32 OTA 中继全程 ring 抑制修复:handleIdle 起点调 reset() 曾把 taskEntry 刚置位的 setSuppressRingService(true) 立即解除,导致中继全程 worker 偷读 OTA ACK/WAIT("serviceRing 读到 RESPONSE"告警,log#449),poll 空等→50ms 重发→包失败;reset() 增加 release_ring_suppress 参数,状态机起点 reset(false) 只清状态不清抑制,结束/中止路径仍统一释放。0.3.85 INIT_STATUS 日志补打 fault_codes hex 一行(13B 模块故障码,编码见 GD32 sys/initial.h)——排查 GD32 init 失败无需解码位图;配套 GD32 1.5.38 看门狗 LSI 修复验证。0.3.84 GD32 模块级故障码(配套 GD32 1.5.36):0xA3 INIT_STATUS 响应 25B 时解析 [12..24] fault_codes(每模块 1B,canfd 高 4 位为通道号),gd32_init_status_t 增 fault_codes/has_fault_codes;/system/status gd32_init 新增 fault_codes 数组,前端「GD32 自检」悬停/值直接显示中文失败原因。0.3.83 GD32 APP 初始化状态可视(配套 GD32 1.5.34):身份块[15]解析 init_state(0=OK/1=degraded/2=critical_fail,HELLO/PING 自动刷新,异常打 WARN);新增 SpiMaster::appInitStatus() 经 SPI 0xA3 查询模块位图/critical 失败位图/复位源(仅 APP 角色,成功即缓存,session 重建自清);/system/status 新增 gd32_init 字段(state/state_known/detail_known/module_mask/critical_fail_mask/reset_cause,惰性查询+10s 失败退避);0xA3 加入短响应/ISR 快速响应类型表。0.3.82 DebugLog 新增 UART0 RX 泵任务(GPIO20, 3M 8N1):收到的串口行按 \n 切行、剥 \r,以 tag=UART 推入 ring → HTTP /logs 出现在网页日志面板;接线=任意串口源 TX→H1/H2 的 ESP_RX。0.3.81 GD32 OTA 包级防呆:中继触发前校验 gd32_ota 镜像向量表身份(Gd32FlashStore::validateStoredImageIdentity),BOOT 包/非 APP 包在 WiFi /ota/relay 与 BLE 0xF7 均拒绝,GD32 APP 区不再被错误包擦除;BLE 新增 0x87 完成码
|
||||
|
||||
// Git 信息(手动填写)
|
||||
#define GIT_HASH "770ae14"
|
||||
@@ -65,9 +65,10 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
slot.rx_count = rdU32Le(&e[4]);
|
||||
slot.seq = rdU32Le(&e[8]);
|
||||
memcpy(slot.data, &e[12], MOTOR_STATUS_DLC);
|
||||
slot.config_rx_count = rdU32Le(&e[44]);
|
||||
slot.config_seq = rdU32Le(&e[48]);
|
||||
memcpy(slot.config_data, &e[52], MOTOR_CONFIG_DLC);
|
||||
/* v0.74: STATUS DLC 48,CONFIG 字段整体后移 16B */
|
||||
slot.config_rx_count = rdU32Le(&e[60]);
|
||||
slot.config_seq = rdU32Le(&e[64]);
|
||||
memcpy(slot.config_data, &e[68], MOTOR_CONFIG_DLC);
|
||||
|
||||
offset += MOTOR_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
*
|
||||
* payload 格式(每页):
|
||||
* [0] page, [1] total_pages, [2] entry_count, [3] active_count
|
||||
* [4..] 每个 entry 76B:
|
||||
* [4..] 每个 entry 92B (v0.74: STATUS DLC 48):
|
||||
* [0] node_id, [1] channel, [2] online, [3] dlc,
|
||||
* [4..7] rx_count (LE), [8..11] seq (LE),
|
||||
* [12..43] data[32] (原始 STATUS payload)
|
||||
* [44..47] config_rx_count (LE), [48..51] config_seq (LE),
|
||||
* [52..75] config_data[24] (原始 CONFIG payload, 目标值回报)
|
||||
* [12..59] data[48] (原始 STATUS payload, 字节 32~35 = InitStatus)
|
||||
* [60..63] config_rx_count (LE), [64..67] config_seq (LE),
|
||||
* [68..91] config_data[24] (原始 CONFIG payload, 目标值回报)
|
||||
*
|
||||
* 注意:STATUS DLC 48 与页大小/页数变更(76→92、9→14 页)需 DCU GD32 与
|
||||
* ESP32 固件成对升级,旧解析方会因长度校验失败拒收(不误解析)。
|
||||
*
|
||||
* 同一节点号可能同时挂在多个 CAN 通道(两台电机同号异线),
|
||||
* 缓存表与 GD32 端一致按 node×channel 双键存储(27 槽)。
|
||||
@@ -30,18 +33,18 @@ extern "C" {
|
||||
#define MOTOR_MAX_NODES 9
|
||||
#define MOTOR_CHANNELS 3
|
||||
#define MOTOR_MAX_SLOTS (MOTOR_MAX_NODES * MOTOR_CHANNELS)
|
||||
#define MOTOR_STATUS_DLC 32
|
||||
#define MOTOR_STATUS_DLC 48
|
||||
#define MOTOR_CONFIG_DLC 24
|
||||
#define MOTOR_ENTRY_SIZE 76
|
||||
#define MOTOR_ENTRY_SIZE 92
|
||||
#define MOTOR_PAGE_HEADER 4
|
||||
#define MOTOR_TOTAL_PAGES 9
|
||||
#define MOTOR_TOTAL_PAGES 14
|
||||
|
||||
/** @brief 单电机状态快照。 */
|
||||
typedef struct {
|
||||
uint8_t node_id; ///< 1-9, 0=空槽
|
||||
uint8_t channel; ///< CAN 通道 (0/1/2)
|
||||
uint8_t online; ///< 1=在线, 0=离线
|
||||
uint8_t dlc; ///< STATUS 帧 DLC (正常=32)
|
||||
uint8_t dlc; ///< STATUS 帧 DLC (正常=48)
|
||||
uint32_t rx_count; ///< 累计收到帧数
|
||||
uint32_t seq; ///< GD32 侧更新序号
|
||||
uint8_t data[MOTOR_STATUS_DLC]; ///< 原始 STATUS payload
|
||||
|
||||
@@ -226,7 +226,11 @@ ota_result_t OtaRelay::handleIdle(relay_event_t event, const uint8_t *payload, s
|
||||
|
||||
if (event == RELAY_EVT_START) {
|
||||
DEBUG_INFO("OTA", "=== 中继状态机启动: IDLE → HANDSHAKING ===");
|
||||
reset();
|
||||
/* 只清状态不清 ring 抑制:taskEntry 收到调度时已 setSuppressRingService(true),
|
||||
* 此处若解除,中继全程(握手/协商/传包)worker 都会偷读 OTA ACK/WAIT
|
||||
* (serviceRing 读到 RESPONSE 告警、poll 空等→重发→失败)。
|
||||
* 抑制由中继结束时的 reset()/abortRemoteAndReset() 统一释放。 */
|
||||
reset(false);
|
||||
setState(RELAY_HANDSHAKING);
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
@@ -532,7 +536,7 @@ void OtaRelay::clearSessionCryptoState()
|
||||
// ============================================================
|
||||
// 状态机复位
|
||||
// ============================================================
|
||||
void OtaRelay::reset()
|
||||
void OtaRelay::reset(bool release_ring_suppress)
|
||||
{
|
||||
StateLockGuard lock;
|
||||
relayState = RELAY_IDLE;
|
||||
@@ -543,7 +547,9 @@ void OtaRelay::reset()
|
||||
m_transferFlashOffset = 0;
|
||||
m_transferDeadline = 0;
|
||||
clearSessionCryptoState();
|
||||
SpiMaster::instance().setSuppressRingService(false);
|
||||
if (release_ring_suppress) {
|
||||
SpiMaster::instance().setSuppressRingService(false);
|
||||
}
|
||||
DEBUG_INFO("OTA", "状态机已复位 → IDLE");
|
||||
}
|
||||
|
||||
|
||||
@@ -144,8 +144,11 @@ public:
|
||||
/**
|
||||
* @brief 重置本端中继状态机到 IDLE(不主动通知 GD32)。
|
||||
* 中继失败/中止时请优先用 abortRemoteAndReset()。
|
||||
* @param release_ring_suppress true(默认)解除 SPI ring 抑制引用;
|
||||
* 状态机起点(IDLE→HANDSHAKING)必须传 false——taskEntry 在调度时
|
||||
* 取的抑制引用要覆盖整个中继,否则中继全程 worker 会偷读 OTA ACK/WAIT。
|
||||
*/
|
||||
void reset();
|
||||
void reset(bool release_ring_suppress = true);
|
||||
|
||||
/**
|
||||
* @brief 通知 GD32 取消 OTA 会话(SPI CANCEL),再复位本端到 IDLE。
|
||||
|
||||
@@ -1996,6 +1996,58 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
(unsigned long)ls.stats.max_resp_latency_us);
|
||||
}
|
||||
|
||||
/* ── GD32 APP 初始化状态(GD32 1.5.34+)──
|
||||
* state 来自身份块 [15](HELLO/PING 自动刷新,零成本);
|
||||
* 详情(模块位图/失败位图/复位源)惰性查询 0xA3:仅 APP 角色、缓存无效、
|
||||
* 距上次尝试 ≥10s(失败退避,老固件不认识 0xA3 时每 10s 才多发一帧)。
|
||||
* init 状态静态不变,成功一次即终身缓存(session 重建时 SpiMaster 自清)。 */
|
||||
{
|
||||
const uint8_t init_state = SpiMaster::instance().gd32InitState();
|
||||
const bool state_known = (init_state != GD32_INIT_STATE_UNKNOWN);
|
||||
gd32_init_status_t ist = {};
|
||||
bool detail_known = SpiMaster::instance().gd32InitStatusCached(&ist);
|
||||
|
||||
static uint32_t s_initStatusLastTryMs = 0;
|
||||
const uint32_t now_ms = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||
if (!detail_known && SpiMaster::instance().gd32Role() == V2_ROLE_APP &&
|
||||
(now_ms - s_initStatusLastTryMs) >= 10000U) {
|
||||
s_initStatusLastTryMs = now_ms;
|
||||
if (SpiMaster::instance().appInitStatus(300U, nullptr) == ESP_OK) {
|
||||
detail_known = SpiMaster::instance().gd32InitStatusCached(&ist);
|
||||
}
|
||||
}
|
||||
|
||||
if (detail_known) {
|
||||
len = http_json_append(resp, HTTP_JSON_WORK_CAP, len,
|
||||
",\"gd32_init\":{\"state\":%u,\"state_known\":%s,\"detail_known\":true,"
|
||||
"\"module_mask\":%lu,\"critical_fail_mask\":%lu,\"reset_cause\":%u",
|
||||
(unsigned)ist.init_state,
|
||||
state_known ? "true" : "false",
|
||||
(unsigned long)ist.module_mask,
|
||||
(unsigned long)ist.critical_fail_mask,
|
||||
(unsigned)ist.reset_cause);
|
||||
/* GD32 1.5.36+:每模块 1B 故障码数组(前端按位序解码中文原因) */
|
||||
if (len > 0 && ist.has_fault_codes) {
|
||||
len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, ",\"fault_codes\":[");
|
||||
for (uint8_t fi = 0; fi < GD32_INIT_MODULE_COUNT && len > 0; ++fi) {
|
||||
len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, "%s%u",
|
||||
fi == 0 ? "" : ",", (unsigned)ist.fault_codes[fi]);
|
||||
}
|
||||
if (len > 0) {
|
||||
len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, "]");
|
||||
}
|
||||
}
|
||||
if (len > 0) {
|
||||
len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, "}");
|
||||
}
|
||||
} else {
|
||||
len = http_json_append(resp, HTTP_JSON_WORK_CAP, len,
|
||||
",\"gd32_init\":{\"state\":%u,\"state_known\":%s,\"detail_known\":false}",
|
||||
(unsigned)(state_known ? init_state : 0U),
|
||||
state_known ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
gd32_adc_feedback_t ga = {};
|
||||
const bool have = getLatestGd32AdcFeedback(&ga);
|
||||
@@ -2207,18 +2259,18 @@ esp_err_t motor_state_handler(httpd_req_t *req)
|
||||
const uint32_t age = motorStateAgeMs();
|
||||
|
||||
/* 二进制模式:?bin=1(前端默认走此路径,体积比 JSON 省 66%)。
|
||||
* 协议(LE,Header 12B + 每节点 Entry 44B):
|
||||
* 协议(LE,Header 12B + 每节点 Entry 92B,v0.74: STATUS DLC 48):
|
||||
* Header: magic u16 = 0x4D53 ('MS')
|
||||
* ver u8 = 1
|
||||
* ver u8 = 2 (v0.74: entry 76→92 / data 32→48)
|
||||
* flags u8 = 0
|
||||
* age_ms u32 (0xFFFFFFFF = 无数据)
|
||||
* active u8 (在线电机数)
|
||||
* count u8 (有效 entry 数)
|
||||
* rsvd u16 = 0
|
||||
* Entry: node u8, ch u8, online u8, dlc u8,
|
||||
* rx_count u32, seq u32, data[32],
|
||||
* rx_count u32, seq u32, data[48],
|
||||
* config_rx_count u32, config_seq u32, config_data[24]
|
||||
* 27 槽(9 node × 3 ch,node×channel 双键)最大 12 + 27*76 = 2064B。
|
||||
* 27 槽(9 node × 3 ch,node×channel 双键)最大 12 + 27*92 = 2508B。
|
||||
* 无 have_data 时仍发 header(count=0),让前端区分「无数据」与「空总线」。 */
|
||||
bool want_bin = http_query_bool(req, "bin");
|
||||
if (want_bin) {
|
||||
@@ -2229,7 +2281,7 @@ esp_err_t motor_state_handler(httpd_req_t *req)
|
||||
size_t off = 0;
|
||||
/* magic 'MS' (LE: 0x53, 0x4D) */
|
||||
buf[off++] = 0x53U; buf[off++] = 0x4DU;
|
||||
buf[off++] = 1U; /* version */
|
||||
buf[off++] = 2U; /* version (v0.74: entry 92 / data 48) */
|
||||
/* flags bit0 = boot_active(总线有电机在 BOOT,0x103 心跳) */
|
||||
buf[off++] = (have && tbl.boot_active) ? 0x01U : 0x00U;
|
||||
/* age_ms LE u32 */
|
||||
|
||||
@@ -78,7 +78,7 @@ static size_t spiRespReadBudget(void)
|
||||
static bool isShortRespType(uint8_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case 0xA1U: case 0x02U: case 0x01U: case 0xF7U:
|
||||
case 0xA1U: case 0xA3U: case 0x02U: case 0x01U: case 0xF7U:
|
||||
case 0xB0U: case 0xC0U: case 0xC2U: case 0xD1U:
|
||||
case 0xC4U: case 0xC7U:
|
||||
return true;
|
||||
@@ -98,7 +98,7 @@ static constexpr size_t kShortRespReadBudget = 96U;
|
||||
static bool isAppIsrFastRespType(uint8_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case 0xA1U: case 0xC0U: case 0xC1U: case 0xC2U:
|
||||
case 0xA1U: case 0xA3U: case 0xC0U: case 0xC1U: case 0xC2U:
|
||||
case 0xC3U: case 0xC4U: case 0xC5U: case 0xC7U: case 0xD1U:
|
||||
case 0xF7U:
|
||||
return true;
|
||||
@@ -414,6 +414,9 @@ void SpiMaster::executeJob(Job &job)
|
||||
case JobKind::Ping:
|
||||
job.result = appPing(job.timeout_ms, job.out_uptime, job.out_version);
|
||||
break;
|
||||
case JobKind::InitStatus:
|
||||
job.result = appInitStatus(job.timeout_ms, job.out_init_status);
|
||||
break;
|
||||
case JobKind::WaitBoot:
|
||||
job.result = waitForBootloaderReady(job.timeout_ms);
|
||||
break;
|
||||
@@ -451,6 +454,7 @@ SpiMaster::Job *SpiMaster::acquireJobSlot()
|
||||
slot->out_len = nullptr;
|
||||
slot->out_uptime = nullptr;
|
||||
slot->out_version = nullptr;
|
||||
slot->out_init_status = nullptr;
|
||||
slot->result = ESP_FAIL;
|
||||
slot->done = done;
|
||||
slot->finished = false;
|
||||
@@ -489,6 +493,7 @@ void SpiMaster::releaseJobSlot(Job *job)
|
||||
job->out_len = nullptr;
|
||||
job->out_uptime = nullptr;
|
||||
job->out_version = nullptr;
|
||||
job->out_init_status = nullptr;
|
||||
job->payload_len = 0U;
|
||||
job->result = ESP_FAIL;
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
@@ -2014,7 +2019,7 @@ esp_err_t SpiMaster::waitForBootloaderReady(uint32_t timeout_ms)
|
||||
*
|
||||
* 布局与 HELLO_ACK 附加块 / APP PING body 相同(见 spi_link_v2.h V2_APP_IDENTITY_LEN):
|
||||
* [0..3] APP ver · [4..7] BOOT ver · [8..11] uptime_ms LE
|
||||
* [12] feat_map_ver · [13..14] feature_flags LE · [15] rsv
|
||||
* [12] feat_map_ver · [13..14] feature_flags LE · [15] init_state
|
||||
*
|
||||
* @param id 身份块指针(至少 16 字节)
|
||||
* @param out_version 可选,输出解析后的版本结构
|
||||
@@ -2051,6 +2056,16 @@ void SpiMaster::applyAppIdentity16(const uint8_t *id, gd32_version_t *out_versio
|
||||
feat_known = true;
|
||||
}
|
||||
|
||||
/* [15] init_state(GD32 1.5.34+;旧固件恒 0=OK,与未知区分靠 feat_known 同帧到达) */
|
||||
const uint8_t init_state = id[15];
|
||||
if (init_state != m_gd32InitState) {
|
||||
if (init_state != GD32_INIT_STATE_OK) {
|
||||
DEBUG_WARN("SPI", "GD32 init_state=%u (0=OK 1=degraded 2=critical_fail)",
|
||||
(unsigned)init_state);
|
||||
}
|
||||
m_gd32InitState = init_state;
|
||||
}
|
||||
|
||||
/* HELLO 与 PING 都会带同一份 16B 身份:仅版本/feat 变化时打 INFO,避免连打两遍 */
|
||||
const bool ver_changed =
|
||||
!m_gd32IdKnown ||
|
||||
@@ -2153,6 +2168,114 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 查询 GD32 APP 初始化状态(SPI 0xA3 INIT_STATUS)。
|
||||
*
|
||||
* 两步法(持锁),机制与 appPing 相同。应答 ACK:
|
||||
* [0..3] module_mask LE · [4..7] critical_fail_mask LE
|
||||
* [8] reset_cause · [9] init_state · [10..11] reserved
|
||||
* [12..24] fault_codes[13](GD32 1.5.36+,每模块 1B 故障码)
|
||||
* 成功时填充 out(valid=true)并更新本端缓存 m_gd32InitStatus。
|
||||
* 仅 APP 角色支持:BOOT 不认识 0xA3,角色不符时快速拒绝不打 SPI。
|
||||
*/
|
||||
esp_err_t SpiMaster::appInitStatus(uint32_t timeout_ms, gd32_init_status_t *out)
|
||||
{
|
||||
if (!onWorker() && m_workerTask != nullptr) {
|
||||
Job job{};
|
||||
job.kind = JobKind::InitStatus;
|
||||
job.timeout_ms = timeout_ms;
|
||||
job.out_init_status = out;
|
||||
return submitJob(job);
|
||||
}
|
||||
|
||||
if (m_gd32Role != V2_ROLE_APP) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
LockGuard lock(m_mutex);
|
||||
constexpr uint8_t kInitStatusType = 0xA3U;
|
||||
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[32];
|
||||
size_t resp_len = 0U;
|
||||
|
||||
const uint32_t send_timeout = (timeout_ms > 100U) ? 100U : timeout_ms;
|
||||
esp_err_t send_ret = otaTransferUnlocked(kInitStatusType, nullptr, 0U,
|
||||
nullptr, nullptr, 0U, nullptr, send_timeout,
|
||||
/*expect_stale=*/true);
|
||||
if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) {
|
||||
DEBUG_DBG("SPI", "INIT_STATUS send failed: %s", esp_err_to_name(send_ret));
|
||||
return send_ret;
|
||||
}
|
||||
|
||||
{
|
||||
const TickType_t gap = (pdMS_TO_TICKS(1) > 0) ? pdMS_TO_TICKS(1) : 1;
|
||||
vTaskDelay(gap);
|
||||
}
|
||||
|
||||
const uint32_t poll_timeout = (timeout_ms > send_timeout)
|
||||
? (timeout_ms - send_timeout) : 500U;
|
||||
esp_err_t err = otaPollResponseUnlocked(&resp_type, resp_payload, sizeof(resp_payload),
|
||||
&resp_len, poll_timeout, SPI_OTA_RSP_ACK);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_DBG("SPI", "INIT_STATUS poll failed: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
if (resp_type != SPI_OTA_RSP_ACK || resp_len < 10U) {
|
||||
DEBUG_DBG("SPI", "INIT_STATUS bad resp type=0x%02X len=%u", resp_type, (unsigned)resp_len);
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
gd32_init_status_t st = {};
|
||||
st.module_mask = v2_get_u32le(&resp_payload[0]);
|
||||
st.critical_fail_mask = v2_get_u32le(&resp_payload[4]);
|
||||
st.reset_cause = resp_payload[8];
|
||||
st.init_state = resp_payload[9];
|
||||
/* GD32 1.5.36+ 响应 25B:附带每模块 1B 故障码;1.5.34 仅 12B 无此数组 */
|
||||
if (resp_len >= 25U) {
|
||||
memcpy(st.fault_codes, &resp_payload[12], GD32_INIT_MODULE_COUNT);
|
||||
st.has_fault_codes = true;
|
||||
}
|
||||
st.valid = true;
|
||||
|
||||
m_gd32InitStatus = st;
|
||||
m_gd32InitState = st.init_state;
|
||||
if (out) {
|
||||
*out = st;
|
||||
}
|
||||
DEBUG_INFO("SPI", "INIT_STATUS state=%u mod=0x%08lX crit_fail=0x%08lX rst=0x%02X",
|
||||
(unsigned)st.init_state, (unsigned long)st.module_mask,
|
||||
(unsigned long)st.critical_fail_mask, (unsigned)st.reset_cause);
|
||||
/* 故障码 hex 一行(编码见 GD32 sys/initial.h;canfd 高 4 位=通道号) */
|
||||
if (st.has_fault_codes) {
|
||||
char fc_hex[GD32_INIT_MODULE_COUNT * 2 + 1];
|
||||
for (uint8_t fi = 0; fi < GD32_INIT_MODULE_COUNT; ++fi) {
|
||||
fc_hex[fi * 2] = "0123456789ABCDEF"[(st.fault_codes[fi] >> 4) & 0xFU];
|
||||
fc_hex[fi * 2 + 1] = "0123456789ABCDEF"[st.fault_codes[fi] & 0xFU];
|
||||
}
|
||||
fc_hex[GD32_INIT_MODULE_COUNT * 2] = '\0';
|
||||
DEBUG_INFO("SPI", "INIT_STATUS fault_codes=%s", fc_hex);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 读取缓存的 0xA3 查询结果(不打 SPI)。
|
||||
* @param out 输出结构
|
||||
* @return true=缓存有效(已至少成功查询一次);false=无有效缓存
|
||||
*/
|
||||
bool SpiMaster::gd32InitStatusCached(gd32_init_status_t *out) const
|
||||
{
|
||||
if (!m_gd32InitStatus.valid) {
|
||||
return false;
|
||||
}
|
||||
if (out) {
|
||||
*out = m_gd32InitStatus;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SPI Link v2 链路状态
|
||||
// ============================================================
|
||||
@@ -2299,6 +2422,10 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
|
||||
/* payload[5]/hdr.credit 均为对端 credit_max → 写入 m_remoteCredit。
|
||||
* 勿写入 m_localCredit(本端 RX 窗口,init 为 8)。 */
|
||||
m_remoteCredit = (pload[5] != 0U) ? pload[5] : hdr.credit;
|
||||
/* 新 session = GD32 可能重启过:0xA3 缓存与 init_state 失效,
|
||||
* 由下方身份块 / 后续 PING 刷新。 */
|
||||
m_gd32InitStatus = gd32_init_status_t{};
|
||||
m_gd32InitState = GD32_INIT_STATE_UNKNOWN;
|
||||
/* APP:HELLO_ACK 带 16B 身份 → 建连即完成开机身份(与 PING 同布局) */
|
||||
if (m_gd32Role == V2_ROLE_APP &&
|
||||
hdr.payload_len >= V2_HELLO_ACK_APP_LEN) {
|
||||
|
||||
@@ -47,6 +47,26 @@ typedef struct {
|
||||
uint16_t boot_build;
|
||||
} gd32_version_t;
|
||||
|
||||
/* GD32 APP init_state 编码(身份块 [15] 与 0xA3 payload[9],见 spi_link_v2.h) */
|
||||
#define GD32_INIT_STATE_OK (0x00) /* 全部模块初始化成功 */
|
||||
#define GD32_INIT_STATE_DEGRADED (0x01) /* critical 全成,非 critical 有失败/跳过 */
|
||||
#define GD32_INIT_STATE_CRITICAL_FAIL (0x02) /* 有 critical 模块失败 */
|
||||
#define GD32_INIT_STATE_UNKNOWN (0xFF) /* 本端尚未收到身份块 */
|
||||
|
||||
// GD32 APP 初始化状态详情(SPI 0xA3 INIT_STATUS 响应解析结果)
|
||||
#define GD32_INIT_MODULE_COUNT 13 /* 模块数(fault_codes 长度,与 DCU_APP kModuleCount 一致) */
|
||||
typedef struct {
|
||||
uint32_t module_mask; /* 成功初始化的模块位图(DCU_APP AppInitializer::ModuleBit) */
|
||||
uint32_t critical_fail_mask; /* 失败的 critical 模块位图(0=无 critical 失败) */
|
||||
uint8_t reset_cause; /* 复位源位图(bit0 POR/bit1 EPR/bit2 FWDGT/bit3 WWDGT/bit4 SW/bit5 LP) */
|
||||
uint8_t init_state; /* GD32_INIT_STATE_* */
|
||||
/* 每模块 1B 故障码(GD32 1.5.36+,响应 25B 时填充;0=成功。
|
||||
* 编码见 DCU_APP sys/initial.h;canfd = 原因码|(首个失败通道<<4)) */
|
||||
uint8_t fault_codes[GD32_INIT_MODULE_COUNT];
|
||||
bool has_fault_codes; /* fault_codes 是否有效(响应 ≥25B) */
|
||||
bool valid; /* 本结构是否已成功查询填充 */
|
||||
} gd32_init_status_t;
|
||||
|
||||
/**
|
||||
* @brief SPI Master 驱动类(单例)
|
||||
*
|
||||
@@ -252,6 +272,16 @@ public:
|
||||
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
||||
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
|
||||
|
||||
/** @brief 查询 GD32 APP 初始化状态(SPI 0xA3,12B:模块位图/失败位图/复位源/init_state)。
|
||||
* 仅 APP 角色支持(BOOT 无此命令);成功时填充 out 并置 valid=true,同时更新本端缓存。 */
|
||||
esp_err_t appInitStatus(uint32_t timeout_ms, gd32_init_status_t *out);
|
||||
|
||||
/** @brief 身份块 [15] 带来的 init_state(HELLO/PING 自动刷新;0xFF=未知)。 */
|
||||
uint8_t gd32InitState() const { return m_gd32InitState; }
|
||||
|
||||
/** @brief 读取缓存的 0xA3 查询结果;valid=false 表示尚未成功查询过。 */
|
||||
bool gd32InitStatusCached(gd32_init_status_t *out) const;
|
||||
|
||||
/**
|
||||
* @brief L0 本机自检(不依赖 GD32):SPI 设备/互斥/worker/队列已就绪。
|
||||
*/
|
||||
@@ -366,6 +396,7 @@ private:
|
||||
EnterBoot,
|
||||
Ping,
|
||||
WaitBoot,
|
||||
InitStatus,
|
||||
};
|
||||
struct Job {
|
||||
JobKind kind = JobKind::Transact;
|
||||
@@ -381,6 +412,7 @@ private:
|
||||
size_t *out_len = nullptr;
|
||||
uint32_t *out_uptime = nullptr;
|
||||
gd32_version_t *out_version = nullptr;
|
||||
gd32_init_status_t *out_init_status = nullptr;
|
||||
esp_err_t result = ESP_FAIL;
|
||||
SemaphoreHandle_t done = nullptr; ///< 池槽静态 binary,init 创建,永不 delete
|
||||
volatile bool finished = false; ///< worker 已 execute + give
|
||||
@@ -438,6 +470,8 @@ private:
|
||||
volatile uint32_t m_gd32FeatFlags = 0; ///< V2_FEAT_*
|
||||
volatile bool m_gd32IdKnown = false; ///< 是否已缓存版本号
|
||||
gd32_version_t m_gd32Version = {}; ///< HELLO/PING 缓存的版本
|
||||
volatile uint8_t m_gd32InitState = GD32_INIT_STATE_UNKNOWN; ///< 身份块 [15] init_state
|
||||
gd32_init_status_t m_gd32InitStatus = {}; ///< 0xA3 查询缓存(valid 标志内置)
|
||||
volatile bool m_v2Active = false; ///< v2 session 是否激活
|
||||
/** 最近一次 otaFindFrame 解析到的 RESPONSE 是否与 m_lastSentSeq 归属匹配 */
|
||||
volatile bool m_lastRxAckMatched = false;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# 墨染浮生
|
||||
|
||||
[Intro]
|
||||
嗯...
|
||||
(雨打芭蕉,檐下滴水声)
|
||||
啊...
|
||||
|
||||
[Verse 1]
|
||||
夜半灯残宣纸白
|
||||
研一砚松烟,墨色渐开
|
||||
提笔落笔皆是梦
|
||||
点染之间,山水自归来
|
||||
二十层晕染不着急
|
||||
浓淡干湿,墨分五色意
|
||||
每一幅都像旧时故纸堆里翻出的残卷
|
||||
模糊了轮廓,却记得那年
|
||||
|
||||
[Instrumental]
|
||||
(古琴泛音,远处箫声悠长)
|
||||
|
||||
[Verse 2]
|
||||
铺开下一尺素绢
|
||||
笔锋流转如落花时节
|
||||
一笔接一笔似檐外细雨
|
||||
宣纸上的人物便活了气息
|
||||
有些留白是刻意,有些是随意
|
||||
收好的折几叠,随它去的便由它去
|
||||
留得住的,都像深秋午后那缕闲光
|
||||
笔锋转...转...转...意到便成章
|
||||
|
||||
[Chorus]
|
||||
嗯... 任笔墨流淌
|
||||
(慢慢来,不急)
|
||||
啊... 到了天亮,画卷自会收
|
||||
(再添一笔,再添一笔)
|
||||
|
||||
[Instrumental]
|
||||
(古筝与箫对答,雨声渐浓)
|
||||
|
||||
[Bridge]
|
||||
窗外的雨还在芭蕉上画着圈...
|
||||
案头的墨还在自行晕开...
|
||||
都不急...
|
||||
都不赶...
|
||||
|
||||
[Chorus]
|
||||
嗯... 任笔墨流淌
|
||||
(慢慢来,不急)
|
||||
啊... 到了天亮,画卷自会收
|
||||
(再添一笔,再添一笔)
|
||||
|
||||
[Instrumental]
|
||||
(编磬轻叩,古琴余韵)
|
||||
|
||||
[Outro]
|
||||
嗯...
|
||||
(笔锋转...)
|
||||
啊... 夜安
|
||||
(墨香渐散,一盏孤灯缓缓暗下)
|
||||
Reference in New Issue
Block a user