From e06f5eb9a92b8d630c0cec606f8337d2ae856500 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Thu, 30 Jul 2026 02:11:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(motor=5Fstate):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E9=80=9A=E9=81=93=E7=94=B5=E6=9C=BA=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E5=8A=A8=E6=80=81=E8=B0=83=E8=8A=82=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 node×channel 双键槽位管理,支持同一节点号在不同CAN通道独立缓存 - 扩展电机状态表容量从9个槽位到27个槽位(9节点×3通道) - 添加SPI命令子命令0xFE用于动态设置0xC5推送周期(50-5000ms) - 增加运行时可调推送周期功能,默认250ms,支持50ms-5s范围调节 - 更新CAN通道配置,启用CH1和CH2通道支持 - 版本号更新至1.5.4 --- GD32E508_DCU_APP/device/motor_state.cpp | 19 +++-- GD32E508_DCU_APP/device/motor_state.h | 19 ++++- .../device/spi/app_spi_cmd_motor_state.cpp | 16 ++++- GD32E508_DCU_APP/main.h | 5 +- GD32E508_DCU_APP/sys/config.h | 6 +- GD32E508_DCU_APP/sys/motor_ota_relay.cpp | 70 +++++++++++++------ GD32E508_DCU_APP/sys/motor_ota_relay.h | 3 +- GD32E508_DCU_APP/sys/task_manager.cpp | 3 +- 8 files changed, 107 insertions(+), 34 deletions(-) diff --git a/GD32E508_DCU_APP/device/motor_state.cpp b/GD32E508_DCU_APP/device/motor_state.cpp index 502c644..5392e80 100644 --- a/GD32E508_DCU_APP/device/motor_state.cpp +++ b/GD32E508_DCU_APP/device/motor_state.cpp @@ -40,11 +40,12 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) } const uint8_t node = static_cast((id >> 16) & 0x0FU); - if (node < 1U || node > kMotorMaxNodes) { + if (node < 1U || node > kMotorMaxNodes || channel >= kMotorChannels) { return; } - MotorSlot &slot = s_motorTable.slots[node - 1U]; + /* node×channel 双键槽位:同节点号在不同通道各自独立缓存,互不覆盖 */ + MotorSlot &slot = s_motorTable.slots[(node - 1U) * kMotorChannels + channel]; slot.node_id = node; slot.channel = channel; @@ -66,7 +67,7 @@ void checkOffline() const uint32_t now = g_app_uptime_sec; uint8_t active = 0U; - for (uint8_t i = 0U; i < kMotorMaxNodes; ++i) { + for (uint8_t i = 0U; i < kMotorSlotCount; ++i) { MotorSlot &slot = s_motorTable.slots[i]; if (slot.node_id == 0U) { continue; /* 空槽 */ @@ -115,7 +116,7 @@ uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen) for (uint8_t i = 0U; i < kMotorEntriesPerPage; ++i) { const uint8_t slotIdx = static_cast(startIdx + i); - if (slotIdx >= kMotorMaxNodes) { + if (slotIdx >= kMotorSlotCount) { break; } @@ -145,4 +146,14 @@ uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen) return offset; } +uint16_t setPushPeriodMs(uint16_t ms) +{ + if (ms < kMotorPeriodMinMs) { ms = kMotorPeriodMinMs; } + if (ms > kMotorPeriodMaxMs) { ms = kMotorPeriodMaxMs; } + /* ms→tick:100us/tick,每 ms 10 tick。写 volatile uint16_t 在 Cortex-M 上为原子, + * tick100us(ISR) 读该值无擕裂风险。 */ + g_scheduler.motor_state_period_ticks = static_cast(ms * kMotorTicksPerMs); + return ms; +} + } // namespace MotorState diff --git a/GD32E508_DCU_APP/device/motor_state.h b/GD32E508_DCU_APP/device/motor_state.h index 138fc09..89b7851 100644 --- a/GD32E508_DCU_APP/device/motor_state.h +++ b/GD32E508_DCU_APP/device/motor_state.h @@ -27,9 +27,19 @@ static constexpr uint32_t kMotorStatusIdMask = 0xFFF0FFFFU; /* 屏蔽 node nibb static constexpr uint32_t kMotorStatusIdMatch = 0x00000100U; /* STATUS 类型码 */ static constexpr uint8_t kMotorMaxNodes = CONFIG_MOTOR_MAX_NODES; +/* 同一节点号可能同时挂在多个 CAN 通道上(两台电机同号异线), + * 状态表按 node×channel 双键存储,各通道独立槽位互不覆盖。 */ +static constexpr uint8_t kMotorChannels = 3U; +static constexpr uint8_t kMotorSlotCount = kMotorMaxNodes * kMotorChannels; /* 9×3 = 27 */ static constexpr uint8_t kMotorStatusDlc = 32U; static constexpr uint16_t kMotorOfflineSec = CONFIG_MOTOR_OFFLINE_MS / 1000U; +/* ── 0xC5 推送周期运行时可调(ESP32 经 0xC5 子命令 0xFE 下发)── */ +static constexpr uint16_t kMotorPeriodMinMs = 50U; /* 最快 20Hz */ +static constexpr uint16_t kMotorPeriodMaxMs = 5000U; /* 最慢 0.2Hz */ +static constexpr uint16_t kMotorTicksPerMs = 10U; /* 100us/tick → 每 ms 10 tick */ +static constexpr uint8_t kMotorSubCmdSetPeriod = 0xFEU; /* 0xC5 请求 payload[0]=0xFE:设推送周期 */ + /* ── BOOT 心跳(0x103)── * 电机在 Bootloader IDLE 每 ~10ms 发一次 0x00000103(固定 ID,不带节点号)。 * 网页据此显示一个独立 BOOT 状态(不归属任何 Nx)。 */ @@ -41,7 +51,8 @@ static constexpr uint8_t kMotorActiveBootBit = 0x80U; /* 0xC5 page0 header static constexpr uint8_t kMotorEntrySize = 44U; /* 1+1+1+1+4+4+32 */ static constexpr uint8_t kMotorPageHeaderLen = 4U; /* page+total+count+active */ static constexpr uint8_t kMotorEntriesPerPage = 5U; /* (236-4)/44 = 5.27 → 5 */ -static constexpr uint8_t kMotorTotalPages = 2U; /* ceil(9/5) = 2 */ +static constexpr uint8_t kMotorTotalPages = + (kMotorSlotCount + kMotorEntriesPerPage - 1U) / kMotorEntriesPerPage; /* ceil(27/5) = 6 */ /// 单电机状态槽 struct MotorSlot { @@ -57,7 +68,7 @@ struct MotorSlot { /// 全局电机状态表 struct MotorStateTable { - MotorSlot slots[kMotorMaxNodes]; ///< 按 node_id-1 索引 + MotorSlot slots[kMotorSlotCount]; ///< 按 (node_id-1)*kMotorChannels+channel 索引 uint32_t global_seq; ///< 全局刷新序号 uint32_t last_push_sec; ///< 上次推送完成时间 uint8_t active_count; ///< 当前在线电机数 @@ -82,6 +93,10 @@ void checkOffline(); /// @return 实际写入字节数, 0=参数错误 uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen); +/// 设置 0xC5 主动推送周期(毫秒,钳制到 [kMotorPeriodMinMs, kMotorPeriodMaxMs])。 +/// @return 实际应用的毫秒值。 +uint16_t setPushPeriodMs(uint16_t ms); + } // namespace MotorState #endif /* DEVICE_MOTOR_STATE_H */ diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_state.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_state.cpp index 9ee0c45..193f8d7 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_state.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_motor_state.cpp @@ -9,8 +9,8 @@ * - 总开关:sys/config.h 中 CONFIG_MOTOR_STATE_ENABLE * * payload 格式 (每页): - * [0] page 页号 (0/1) - * [1] total_pages 总页数 (2) + * [0] page 页号 (0..kMotorTotalPages-1) + * [1] total_pages 总页数 (6,27 槽 = 9 node × 3 ch) * [2] entry_count 本页有效电机数 * [3] active_count 全局在线电机数 * [4..] 每个 entry 44B: @@ -47,6 +47,18 @@ public: void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override { + /* 子命令:payload[0]=0xFE 且 len>=3 → 设置 0xC5 推送周期(payload[1..2]=毫秒 LE)。 + * 有效页号仅 0..kMotorTotalPages-1,0xFE 作子操作码不冲突。回 ACK 携实际应用毫秒 LE。 */ + if (ctx.payload_len >= 3U && ctx.payload[0] == kMotorSubCmdSetPeriod) { + const uint16_t ms = static_cast(ctx.payload[1] | (ctx.payload[2] << 8)); + const uint16_t applied = MotorState::setPushPeriodMs(ms); + uint8_t ack[2] = { static_cast(applied & 0xFFU), + static_cast((applied >> 8) & 0xFFU) }; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, ack, sizeof(ack)); + return; + } + /* 请求 payload[0] = page (可选, 默认 0) */ const uint8_t page = (ctx.payload_len >= 1U) ? ctx.payload[0] : 0U; diff --git a/GD32E508_DCU_APP/main.h b/GD32E508_DCU_APP/main.h index bf700d3..4b8b4f7 100644 --- a/GD32E508_DCU_APP/main.h +++ b/GD32E508_DCU_APP/main.h @@ -46,8 +46,11 @@ struct task_scheduler_t { volatile uint16_t div_can_tx; /** @brief 调度分频计数:心跳(1s)。 */ volatile uint16_t div_heartbeat; - /** @brief 调度分频计数:多电机状态推送(250ms)。 */ + /** @brief 调度分频计数:多电机状态推送(默认 250ms)。 */ volatile uint16_t div_motor_state; + /** @brief 0xC5 推送周期(100us tick 数,运行时可由 ESP32 SPI 命令调整)。 + * 默认 CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS,范围 500(50ms)~50000(5000ms)。 */ + volatile uint16_t motor_state_period_ticks; /** @brief 调度分频计数:APP 状态/诊断打印(5s)。 */ volatile uint16_t div_diag_print; }; diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index d190dd8..a0aec46 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -68,8 +68,8 @@ constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U; #define CONFIG_CAN_PERIODIC_TEST_TX_ENABLE 0 /** @brief CAN0/1/2 通道使能。CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI 带宽。 */ constexpr bool CONFIG_CAN_CH0_ENABLE = true; -constexpr bool CONFIG_CAN_CH1_ENABLE = false; -constexpr bool CONFIG_CAN_CH2_ENABLE = false; +constexpr bool CONFIG_CAN_CH1_ENABLE = true; +constexpr bool CONFIG_CAN_CH2_ENABLE = true; /** @brief 主循环 WFI(main.cpp #if)。 */ #define CONFIG_MAIN_LOOP_WFI_ENABLE 0 /** @brief EtherCAT SYNC0 同步模式(#if)。 */ @@ -115,7 +115,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = false; * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MINOR = 5U; -constexpr uint32_t APP_FW_VERSION_PATCH = 0U; /* 1.5.0:大版本:Flash瘦身-34.5%(-fno-rtti/异常+size优化+日志分级宏),启用HardFault诊断,新增尺寸护栏 */ +constexpr uint32_t APP_FW_VERSION_PATCH = 4U; /* 1.5.4:0xC5 推送周期运行时可调(子命令 0xFE 设周期,50~5000ms) */ constexpr uint32_t APP_FW_VERSION_BUILD = 0U; diff --git a/GD32E508_DCU_APP/sys/motor_ota_relay.cpp b/GD32E508_DCU_APP/sys/motor_ota_relay.cpp index 281f3d5..9a2d118 100644 --- a/GD32E508_DCU_APP/sys/motor_ota_relay.cpp +++ b/GD32E508_DCU_APP/sys/motor_ota_relay.cpp @@ -293,11 +293,14 @@ static constexpr uint8_t kAckMaxResends = 4U; * 100ms 仅为电机内部状态转换留余量(BOOT 主循环~10ms)。 * 之前 500ms 导致电机 RECEIVING_FIRMWARE 超时(~2s)预算被压缩。 */ static constexpr uint32_t kEraseWaitMs = 100U; -/* 预取消等待(ms):发 CANCEL 后等电机 BOOT 主循环处理完(复位到 IDLE),再进握手。 - * 电机 BOOT 主循环周期 ~1-10ms,100ms 留足余量。 */ -static constexpr uint32_t kBootSettleMs = 100U; /* 检测到0x103心跳后再等这么久,让电机APP残留[00FA]CAN帧先消耗完 - * 原 2000ms→800ms→100ms:0x103 出现即电机已在 BOOT IDLE, - * 残帧实测 <4 帧(~40ms),100ms 足够。 */ +/* 等新 APP 运行确认(ms):尾包 ACK 后电机复位跳 APP,新 APP 上电初始化完 CAN 会 + * 自主发 [0A AC](刷写结束/APP 运行有效,见协议表 15/16 行)。 + * 复位+初始化实测 <1s,3s 留足余量;超时 = 新 APP 未启动 → error 0x25。 */ +static constexpr uint32_t kAppRunTimeoutMs = 3000U; +/* 残帧窗口 kBootSettleMs 已移除(2026-07-27):收到 0x103 即直接进握手。 + * 旧担忧:电机 APP 跳转前 TX 队列残留 [00 FA] 帧延迟上总线被 BOOT catch-all 命中 → 0x05。 + * 现行握手阶段对垃圾帧一律忽略,靠 kStepTimeoutMs=100ms 步进重发(≤2次)自愈, + * 窗口不再必要;若 0x05 复现优先怀疑此处。 */ /* 步骤名,用于“xxx 未收到”日志,让用户/上位机知道卡在协议哪一步。 */ static const char *stepName(State st) { @@ -309,6 +312,7 @@ static const char *stepName(State st) { case State::kErase: return "ERASE(0x101:fw_size)"; case State::kTransfer: return "TRANSFER(0x101:data)"; case State::kFinishOta: return "FINISH_OTA(0x100:0AAC)"; + case State::kWaitAppRun: return "WAIT_APP_RUN(0x102:0AAC)"; default: return "?"; } } @@ -572,19 +576,20 @@ static void processTransferAck(const uint8_t *data, uint8_t len) { * 自行跳转 APP,不需要上位机再发 FINISH_OTA。 * 旧注释里"0xF0F0F0F0 表示等 FINISH_OTA"是错误假设——电机 BOOT 代码里根本 * 没有 0xF0F0F0F0,尾帧 ACK 就是 0xFFFFFFFF,发完即跳转。 - * 因此收到 0xFFFFFFFF 即表示电机已跳转 APP,relay 直接进 kFinished。 - * 若再发 [0x0A,0xAC] 反而会被已运行的电机 APP 当作握手开始信号干扰。 */ + * 收到 0xFFFFFFFF = 电机已跳转,但不证明新 APP 能跑 → 进 kWaitAppRun + * 等新 APP 上电自主发的 [0A AC] 运行确认(协议表 15/16 行),收到才 kFinished。 + * 此阶段绝不能主动发 [0x0A,0xAC]——会被已运行的电机 APP 当作握手开始信号干扰。 */ if (next_pkt == 0xFFFFFFFFU) { - /* 末包已被电机确认并跳转:把进度钉到 total,便于 QUERY/网页识别 100%。 */ + /* 末包已被电机确认:把进度钉到 total,便于 QUERY/网页识别 100%。 */ s_packets_acked = s_total_packets; s_last_acked_pkt = s_total_packets; s_pkt_pending = false; - LOG_ERR("[MOTOR-OTA] FINISHED pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu\r\n", + LOG_ERR("[MOTOR-OTA] tail-ACK pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu, wait APP-run 0AAC\r\n", (unsigned)s_packets_acked, (unsigned)s_total_packets, (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count, (unsigned long)s_ack_resend_total, (unsigned long)s_fast_resend_total, (unsigned long)SysTickTimer::getMs()); - enterState(State::kFinished); + enterState(State::kWaitAppRun); pushStatus(); return; } @@ -742,15 +747,11 @@ void process() { } /* kPreCancel:等待电机 BOOT 心跳(0x103)确认已进入 Bootloader。 - * 电机 BOOT IDLE 每 ~10ms 发一次 0x103;收到即证明电机已在 BOOT IDLE。 - * 但不立即握手——电机 APP 跳 BOOT 前可能在 CAN TX 队列残留 [00 FA](fail)帧, - * 这帧延迟发到总线会被 BOOT 的 handleLinkMessage catch-all 命中 - * → 重置状态机 → 0x05。等 kBootSettleMs 让残帧在 BOOT IDLE 期间消耗完, - * 再开始握手就不会被干扰。 */ + * 电机 BOOT IDLE 每 ~10ms 发一次 0x103;收到即证明电机已在 BOOT IDLE, + * 直接进握手(残帧窗口已移除,握手阶段对干扰帧忽略+步进重发兜底)。 */ if (s_state == State::kPreCancel) { - if (s_boot_heartbeat_seen && stateTimedOut(kBootSettleMs)) { - OTA_LOGI("[MOTOR-OTA] 0x103 ok, residue-win %ums passed, handshake\r\n", - (unsigned)kBootSettleMs); + if (s_boot_heartbeat_seen) { + OTA_LOGI("[MOTOR-OTA] 0x103 ok, handshake\r\n"); enterState(State::kHandshakeReq); pushStatus(); } else if (stateTimedOut(kHandshakeTimeoutMs)) { @@ -760,6 +761,33 @@ void process() { return; } + /* kWaitAppRun:尾包 ACK 后电机已复位跳 APP,等新 APP 自主发的 [0A AC] 运行确认。 + * GD32 此阶段不发任何帧(不能走下方首发/补发路径,否则 sendCurrentStep 空转 + * kMaxResends 次后误报 0x3A);启动期杂帧一律忽略,只认 [0A AC]。 + * kAppRunTimeoutMs 无确认 → error 0x25(新 APP 未启动)。 */ + if (s_state == State::kWaitAppRun) { + if (s_ack_pending) { + s_ack_pending = false; + uint8_t ack_data[64]; + memcpy(ack_data, s_ack_data, 64); + const uint8_t ack_len = s_ack_len; + if (ack_len >= 2 && ack_data[0] == 0x0A && ack_data[1] == 0xAC) { + LOG_ERR("[MOTOR-OTA] APP-run 0AAC ok, FINISHED t=%lu\r\n", + (unsigned long)SysTickTimer::getMs()); + enterState(State::kFinished); + pushStatus(); + return; + } + /* 非 [0A AC](APP 启动期其他帧)——忽略继续等 */ + } + if (stateTimedOut(kAppRunTimeoutMs)) { + LOG_ERR("[MOTOR-OTA] no APP-run 0AAC in %lums, new APP not running\r\n", + (unsigned long)kAppRunTimeoutMs); + setError(0x25); /* 0x25 = 刷写传完但新 APP 未发运行确认 */ + } + return; + } + /* 超时检查 */ const uint32_t timeout = (s_state == State::kTransfer || s_state == State::kFinishOta) ? kAckTimeoutMs : kHandshakeTimeoutMs; @@ -1062,11 +1090,13 @@ bool feedData(const uint8_t *data, uint16_t len) { void cancel() { if (s_state == State::kIdle) return; LOG_ERR("[MOTOR-OTA] CANCEL received in state %s\r\n", stepName(s_state)); - /* ★ kFinished/kError 时电机已跳转 APP 或已中止,绝不能再发 CAN 取消帧! + /* ★ kFinished/kError/kWaitAppRun 时电机已跳转 APP 或已中止,绝不能再发 CAN 取消帧! * kOtaCancelId=0x00000000 与电机 APP 的 CANFD_BOOT_ID(base_id=0x0000) 冲突, * 电机 APP 收到会当作 BOOT 命令 → enter_silent_mode 10s,APP 看似卡死。 + * kWaitAppRun = 尾包已确认、电机已复位跳 APP,同样不能发。 * 仅在活跃传输态(kPreCancel..kFinishOta)发取消帧通知电机 BOOT 中止。 */ - const bool was_active = (s_state != State::kFinished && s_state != State::kError); + const bool was_active = (s_state != State::kFinished && s_state != State::kError && + s_state != State::kWaitAppRun); if (was_active) { canSend(kOtaCancelId, nullptr, 0); } diff --git a/GD32E508_DCU_APP/sys/motor_ota_relay.h b/GD32E508_DCU_APP/sys/motor_ota_relay.h index 1fcbd2a..f0cd487 100644 --- a/GD32E508_DCU_APP/sys/motor_ota_relay.h +++ b/GD32E508_DCU_APP/sys/motor_ota_relay.h @@ -41,7 +41,8 @@ enum class State : uint8_t { kFinishOta, /* 收到 0xF0F0F0F0,已发 [0A AC],等 FINISH ACK */ kFinished, /* 完成 */ kError, /* 错误 */ - kPreCancel = 9 /* 等待电机 BOOT 心跳(0x103)确认已进入 Bootloader,然后进 kHandshakeReq */ + kPreCancel = 9, /* 等待电机 BOOT 心跳(0x103)确认已进入 Bootloader,然后进 kHandshakeReq */ + kWaitAppRun = 10 /* 尾包 ACK 后电机已跳转,等新 APP 上电自主发的 [0A AC] 运行确认,收到才进 kFinished */ }; struct Status { diff --git a/GD32E508_DCU_APP/sys/task_manager.cpp b/GD32E508_DCU_APP/sys/task_manager.cpp index 5c0a8bc..5231017 100644 --- a/GD32E508_DCU_APP/sys/task_manager.cpp +++ b/GD32E508_DCU_APP/sys/task_manager.cpp @@ -33,6 +33,7 @@ void TaskManager::init() g_scheduler.div_can_tx = 0U; g_scheduler.div_heartbeat = 0U; g_scheduler.div_motor_state = 0U; + g_scheduler.motor_state_period_ticks = CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS; g_scheduler.div_diag_print = 0U; } @@ -108,7 +109,7 @@ void TaskManager::tick100us() /* 多电机状态 0xC5 推送:250ms 独立节拍。 * 不挂在 1Hz heartbeat 上,避免连带加速 OTA Boot 轮询/CAN快照/ADC推送/checkOffline。 */ - if(++g_scheduler.div_motor_state >= CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS) { + if(++g_scheduler.div_motor_state >= g_scheduler.motor_state_period_ticks) { g_scheduler.div_motor_state = 0U; set(1UL << TASK_MOTOR_STATE_PUSH); }