feat(sys): 添加AES加密和电机OTA中继功能模块
添加了AES128加密算法实现和电机OTA中继相关文件,包括: - ./sys/aes128.h 和 ./sys/aes128.cpp - AES128加密算法头文件和实现 - ./sys/motor_ota_relay.h 和 ./sys/motor_ota_relay.cpp - 电机OTA中继功能 - 更新了项目配置文件包含这些新模块
This commit is contained in:
@@ -267,7 +267,7 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
|
||||
can_parameter.time_triggered = DISABLE;
|
||||
can_parameter.auto_bus_off_recovery = ENABLE;
|
||||
can_parameter.auto_wake_up = DISABLE;
|
||||
can_parameter.auto_retrans = ENABLE;
|
||||
can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */
|
||||
can_parameter.rec_fifo_overwrite = ENABLE;
|
||||
can_parameter.trans_fifo_order = ENABLE;
|
||||
can_parameter.working_mode = CAN_NORMAL_MODE;
|
||||
@@ -936,40 +936,19 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
|
||||
return CanTxResult::kErrInvalid;
|
||||
}
|
||||
|
||||
/* 总线健康检查:BUS_OFF 或 TEC 过高时跳过发送,防止测试帧把通道推向 BUS_OFF */
|
||||
/* 诊断:捕获任何通过 transmitFrame 发 0x100 DLC=16 的帧(排查幽灵帧来源) */
|
||||
if (ext_id == 0x00000100U && dlc == 16U) {
|
||||
printf("[CAN-DIAG] TX 0x100 DLC=16 via transmitFrame ch=%u\r\n",
|
||||
(unsigned)channelIndex);
|
||||
}
|
||||
|
||||
/* BUS_OFF 不再软件拦截——CAN_ERR_BOERR 是粘性标志,控制器恢复后仍置位,
|
||||
* 导致总线正常时仍拒绝发送(用户确认其他设备能收到 GD32 帧)。
|
||||
* CAN 硬件自带恢复机制(128×11隐性位后自动回 Normal);
|
||||
* 若确实发不出,can_message_transmit 会返回 NOMAILBOX → kErrMailbox。 */
|
||||
const uint32_t canPeriph = kChannels[channelIndex].canPeriph;
|
||||
const uint32_t errReg = CAN_ERR(canPeriph);
|
||||
if (errReg & CAN_ERR_BOERR) {
|
||||
static uint32_t s_lastBusOffLogSeq[kChannelCount] = {0xFFFFFFFFU, 0xFFFFFFFFU, 0xFFFFFFFFU};
|
||||
if (s_lastBusOffLogSeq[channelIndex] != s_feedbackSeq) {
|
||||
printf("[CAN-TX-REJECT] CH%u BUS_OFF ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n",
|
||||
static_cast<unsigned>(channelIndex),
|
||||
static_cast<unsigned long>(errReg),
|
||||
static_cast<unsigned long>((errReg >> 4U) & 0x07U),
|
||||
canLastErrorName(errReg),
|
||||
static_cast<unsigned long>((errReg >> 16U) & 0xFFU),
|
||||
static_cast<unsigned long>((errReg >> 24U) & 0xFFU),
|
||||
static_cast<unsigned long>(CAN_TSTAT(canPeriph)));
|
||||
s_lastBusOffLogSeq[channelIndex] = s_feedbackSeq;
|
||||
}
|
||||
return CanTxResult::kErrBusOff; /* 通道处于 BUS_OFF,不发送 */
|
||||
}
|
||||
const uint32_t tec = (errReg >> 16) & 0xFFU;
|
||||
if (tec > 96U && !periodic) {
|
||||
static uint32_t s_lastTecLogSeq[kChannelCount] = {0xFFFFFFFFU, 0xFFFFFFFFU, 0xFFFFFFFFU};
|
||||
if (s_lastTecLogSeq[channelIndex] != s_feedbackSeq) {
|
||||
printf("[CAN-TX-REJECT] CH%u TEC_HIGH ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n",
|
||||
static_cast<unsigned>(channelIndex),
|
||||
static_cast<unsigned long>(errReg),
|
||||
static_cast<unsigned long>((errReg >> 4U) & 0x07U),
|
||||
canLastErrorName(errReg),
|
||||
static_cast<unsigned long>(tec),
|
||||
static_cast<unsigned long>((errReg >> 24U) & 0xFFU),
|
||||
static_cast<unsigned long>(CAN_TSTAT(canPeriph)));
|
||||
s_lastTecLogSeq[channelIndex] = s_feedbackSeq;
|
||||
}
|
||||
return CanTxResult::kErrTecHigh; /* TEC 过高(>75% 错误被动阈值),暂停发送保护通道 */
|
||||
}
|
||||
/* CAN 硬件自有 error-passive(TEC>127 自动加间隔)
|
||||
* 和 bus-off(TEC=256 → 自动恢复) 保护。软件不再做任何发送拦截。 */
|
||||
|
||||
/* 周期发送只允许一个邮箱保持自动重发。无 ACK 时同一帧会持续产生
|
||||
* 总线波形;不要每 1ms 再填满其余邮箱,更不能主动 abort 旧帧,否则
|
||||
@@ -1045,6 +1024,44 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
|
||||
return CanTxResult::kErrMailbox;
|
||||
}
|
||||
|
||||
void CanFd::abortPendingTx(uint8_t channelIndex)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
return;
|
||||
}
|
||||
const uint32_t canPeriph = kChannels[channelIndex].canPeriph;
|
||||
for (uint8_t mb = CAN_MAILBOX0; mb <= CAN_MAILBOX2; ++mb) {
|
||||
if (can_transmit_states(canPeriph, mb) == CAN_TRANSMIT_PENDING) {
|
||||
(void)can_transmission_stop(canPeriph, mb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CanFd::setChannelAutoRetrans(uint8_t channelIndex, bool enable)
|
||||
{
|
||||
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
|
||||
return;
|
||||
}
|
||||
const uint32_t canPeriph = kChannels[channelIndex].canPeriph;
|
||||
/* ARD 位仅在 init mode 可写:进 init mode → 改 ARD → 退 init mode。 */
|
||||
CAN_CTL(canPeriph) |= CAN_CTL_IWMOD;
|
||||
for (volatile uint32_t t = 0U; t < 200000U; ++t) {
|
||||
if (CAN_STAT(canPeriph) & CAN_STAT_IWS) break;
|
||||
}
|
||||
if (enable) {
|
||||
CAN_CTL(canPeriph) &= ~CAN_CTL_ARD; /* 自动重发 ON(业务默认) */
|
||||
} else {
|
||||
CAN_CTL(canPeriph) |= CAN_CTL_ARD; /* 自动重发 OFF(单发,停等式 OTA 用) */
|
||||
}
|
||||
CAN_CTL(canPeriph) &= ~CAN_CTL_IWMOD;
|
||||
for (volatile uint32_t t = 0U; t < 200000U; ++t) {
|
||||
if ((CAN_STAT(canPeriph) & CAN_STAT_IWS) == 0U) break;
|
||||
}
|
||||
printf("[CAN] CH%u set auto_retrans=%u -> ARD=%u\r\n",
|
||||
(unsigned)channelIndex, (unsigned)enable,
|
||||
(CAN_CTL(canPeriph) & CAN_CTL_ARD) ? 1U : 0U);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取 CANFD 诊断统计信息只读指针。
|
||||
* @param[in] 无。
|
||||
@@ -1319,13 +1336,16 @@ void CanFd::refreshFeedbackSnapshot()
|
||||
}
|
||||
|
||||
s_feedbackSeq++;
|
||||
/* 每 5 秒打印一次 seq 心跳;诊断期间同时输出实际错误通道的最后错误类型。 */
|
||||
/* 每 5 秒采样硬件错误态;正常心跳不打印(统计走 SPI 0xC1)。
|
||||
* 仅当 TEC/REC 或告警位非零时输出 [CAN-HW-ERR];全量 seq 心跳见 CONFIG_UART_DIAG_LEVEL。 */
|
||||
if (s_feedbackSeq % 5U == 0U) {
|
||||
#if (CONFIG_UART_DIAG_LEVEL != 0)
|
||||
printf("[CAN-FB] refresh seq=%lu rx0=%lu rx1=%lu rx2=%lu\r\n",
|
||||
(unsigned long)s_feedbackSeq,
|
||||
(unsigned long)s_channelStats[0].rx_count,
|
||||
(unsigned long)s_channelStats[1].rx_count,
|
||||
(unsigned long)s_channelStats[2].rx_count);
|
||||
#endif
|
||||
for (size_t i = 0; i < kChannelCount; ++i) {
|
||||
if (hw_tec[i] == 0U && hw_rec[i] == 0U
|
||||
&& (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) == 0U) {
|
||||
|
||||
@@ -144,6 +144,12 @@ public:
|
||||
static bool consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel = nullptr);
|
||||
static CanTxResult transmitFrame(uint8_t channelIndex, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc, bool periodic = false);
|
||||
|
||||
/** @brief 立即停掉指定通道所有 PENDING 发送邮箱(阻止 auto_retrans 残留重发,供停等式协议用)。 */
|
||||
static void abortPendingTx(uint8_t channelIndex);
|
||||
|
||||
/** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */
|
||||
static void setChannelAutoRetrans(uint8_t channelIndex, bool enable);
|
||||
static const Diag *diag();
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @file motor_state.cpp
|
||||
* @brief 多电机状态表实现 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。
|
||||
*
|
||||
* 所有函数仅在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep),
|
||||
* 无 ISR 竞态,不需要临界区保护。
|
||||
*/
|
||||
|
||||
#include "device/motor_state.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "main.h"
|
||||
#include <cstring>
|
||||
|
||||
/* ── 全局状态表实例 ── */
|
||||
static MotorStateTable s_motorTable = {};
|
||||
|
||||
namespace MotorState {
|
||||
|
||||
MotorStateTable &table()
|
||||
{
|
||||
return s_motorTable;
|
||||
}
|
||||
|
||||
void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
|
||||
{
|
||||
/* BOOT 心跳 0x103(固定 ID,不带节点号):电机在 Bootloader IDLE 每 ~10ms 发一次。
|
||||
* 记录最近收到时刻与通道,供网页显示独立 BOOT 状态(不归属任何 Nx)。
|
||||
* 注意:必须在 STATUS 掩码过滤之前拦截,否则 0x103 会被下面的掩码判断丢弃。 */
|
||||
if (id == kOtaBootHeartbeatId) {
|
||||
s_motorTable.boot_hb_sec = g_app_uptime_sec;
|
||||
s_motorTable.boot_channel = channel;
|
||||
s_motorTable.boot_active = 1U;
|
||||
/* 0x103 仅更新 boot_active 状态,不再串口打印(高频心跳会刷屏)。 */
|
||||
return;
|
||||
}
|
||||
|
||||
/* 掩码匹配 STATUS ID: 0x000N0100 */
|
||||
if ((id & kMotorStatusIdMask) != kMotorStatusIdMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t node = static_cast<uint8_t>((id >> 16) & 0x0FU);
|
||||
if (node < 1U || node > kMotorMaxNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
MotorSlot &slot = s_motorTable.slots[node - 1U];
|
||||
|
||||
slot.node_id = node;
|
||||
slot.channel = channel;
|
||||
slot.online = 1U;
|
||||
slot.dlc = (len <= kMotorStatusDlc) ? len : kMotorStatusDlc;
|
||||
slot.rx_count++;
|
||||
slot.last_rx_sec = g_app_uptime_sec;
|
||||
slot.seq = ++s_motorTable.global_seq;
|
||||
|
||||
/* 拷贝原始 payload */
|
||||
std::memcpy(slot.data, data, slot.dlc);
|
||||
if (slot.dlc < kMotorStatusDlc) {
|
||||
std::memset(&slot.data[slot.dlc], 0, kMotorStatusDlc - slot.dlc);
|
||||
}
|
||||
}
|
||||
|
||||
void checkOffline()
|
||||
{
|
||||
const uint32_t now = g_app_uptime_sec;
|
||||
uint8_t active = 0U;
|
||||
|
||||
for (uint8_t i = 0U; i < kMotorMaxNodes; ++i) {
|
||||
MotorSlot &slot = s_motorTable.slots[i];
|
||||
if (slot.node_id == 0U) {
|
||||
continue; /* 空槽 */
|
||||
}
|
||||
if ((now - slot.last_rx_sec) >= kMotorOfflineSec) {
|
||||
slot.online = 0U;
|
||||
}
|
||||
if (slot.online != 0U) {
|
||||
active++;
|
||||
}
|
||||
}
|
||||
s_motorTable.active_count = active;
|
||||
|
||||
/* BOOT 心跳超时:>kBootHeartbeatTimeoutSec 无 0x103 → 电机已退出 BOOT。 */
|
||||
if (s_motorTable.boot_active != 0U &&
|
||||
(now - s_motorTable.boot_hb_sec) >= kBootHeartbeatTimeoutSec) {
|
||||
s_motorTable.boot_active = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen)
|
||||
{
|
||||
if (page >= kMotorTotalPages || buf == nullptr) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/* 需要的最小空间: header + 至少 1 entry */
|
||||
const uint16_t needed = kMotorPageHeaderLen + kMotorEntrySize;
|
||||
if (maxLen < needed) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
const uint8_t startIdx = static_cast<uint8_t>(page * kMotorEntriesPerPage);
|
||||
uint8_t count = 0U;
|
||||
|
||||
/* Header (4B) */
|
||||
buf[0] = page;
|
||||
buf[1] = kMotorTotalPages;
|
||||
buf[2] = 0U; /* entry_count, 稍后回填 */
|
||||
/* bit[6:0]=在线电机数,bit7=BOOT 心跳(0x103)活跃标志(供网页显示独立 BOOT 状态)。
|
||||
* active_count 上限 9,bit7 空闲可复用,旧解析方读到会多算 128,故 ESP32 侧需掩 0x7F。 */
|
||||
buf[3] = static_cast<uint8_t>((s_motorTable.active_count & 0x7FU)
|
||||
| (s_motorTable.boot_active ? kMotorActiveBootBit : 0U));
|
||||
|
||||
uint16_t offset = kMotorPageHeaderLen;
|
||||
|
||||
for (uint8_t i = 0U; i < kMotorEntriesPerPage; ++i) {
|
||||
const uint8_t slotIdx = static_cast<uint8_t>(startIdx + i);
|
||||
if (slotIdx >= kMotorMaxNodes) {
|
||||
break;
|
||||
}
|
||||
|
||||
const MotorSlot &slot = s_motorTable.slots[slotIdx];
|
||||
if (slot.node_id == 0U) {
|
||||
continue; /* 跳过空槽 */
|
||||
}
|
||||
|
||||
/* 检查剩余空间 */
|
||||
if (offset + kMotorEntrySize > maxLen) {
|
||||
break;
|
||||
}
|
||||
|
||||
buf[offset + 0U] = slot.node_id;
|
||||
buf[offset + 1U] = slot.channel;
|
||||
buf[offset + 2U] = slot.online;
|
||||
buf[offset + 3U] = slot.dlc;
|
||||
writeU32Le(&buf[offset + 4U], slot.rx_count);
|
||||
writeU32Le(&buf[offset + 8U], slot.seq);
|
||||
std::memcpy(&buf[offset + 12U], slot.data, kMotorStatusDlc);
|
||||
|
||||
offset += kMotorEntrySize;
|
||||
count++;
|
||||
}
|
||||
|
||||
buf[2] = count;
|
||||
return offset;
|
||||
}
|
||||
|
||||
} // namespace MotorState
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file motor_state.h
|
||||
* @brief 多电机状态表 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。
|
||||
*
|
||||
* 设计要点:
|
||||
* - 仅在主循环上下文 (taskCanRxParse / taskHeartbeat / pushStep) 中读写,
|
||||
* 无 ISR 竞态,不需要临界区。
|
||||
* - GD32 不做浮点解码,只缓存原始 32B STATUS payload,由前端按 DBC 解码。
|
||||
* - 离线判定:3s 无 STATUS 帧 → online=0。
|
||||
*/
|
||||
|
||||
#ifndef DEVICE_MOTOR_STATE_H
|
||||
#define DEVICE_MOTOR_STATE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "sys/config.h"
|
||||
|
||||
/* ── CAN ID 匹配规则 (29-bit 扩展帧) ──
|
||||
* 物理 ID = 0x000N_TT_SS
|
||||
* N = 节点号 (1-9), 位于 bits[19:16]
|
||||
* TT = 消息类型: 01=STATUS, 02=CMD, 03=CONFIG
|
||||
* SS = 子类型
|
||||
* STATUS = 0x000N0100
|
||||
*/
|
||||
static constexpr uint32_t kMotorStatusIdMask = 0xFFF0FFFFU; /* 屏蔽 node nibble [19:16] */
|
||||
static constexpr uint32_t kMotorStatusIdMatch = 0x00000100U; /* STATUS 类型码 */
|
||||
|
||||
static constexpr uint8_t kMotorMaxNodes = CONFIG_MOTOR_MAX_NODES;
|
||||
static constexpr uint8_t kMotorStatusDlc = 32U;
|
||||
static constexpr uint16_t kMotorOfflineSec = CONFIG_MOTOR_OFFLINE_MS / 1000U;
|
||||
|
||||
/* ── BOOT 心跳(0x103)──
|
||||
* 电机在 Bootloader IDLE 每 ~10ms 发一次 0x00000103(固定 ID,不带节点号)。
|
||||
* 网页据此显示一个独立 BOOT 状态(不归属任何 Nx)。 */
|
||||
static constexpr uint32_t kOtaBootHeartbeatId = 0x00000103U;
|
||||
static constexpr uint32_t kBootHeartbeatTimeoutSec = 3U; /* >3s 无 0x103 → 视为退出 BOOT */
|
||||
static constexpr uint8_t kMotorActiveBootBit = 0x80U; /* 0xC5 page0 header active 字节 bit7 = boot_active */
|
||||
|
||||
/* ── 序列化常量 (0xC5 payload) ── */
|
||||
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 */
|
||||
|
||||
/// 单电机状态槽
|
||||
struct MotorSlot {
|
||||
uint8_t node_id; ///< 1-9, 0=空槽
|
||||
uint8_t channel; ///< 最后收到帧的 CAN 通道 (0/1/2)
|
||||
uint8_t online; ///< 1=在线, 0=离线
|
||||
uint8_t dlc; ///< 最后帧 DLC (正常=32)
|
||||
uint32_t rx_count; ///< 累计收到 STATUS 帧数
|
||||
uint32_t last_rx_sec; ///< 最后收到帧时的 g_app_uptime_sec
|
||||
uint32_t seq; ///< 更新序号 (每次刷新 +1)
|
||||
uint8_t data[kMotorStatusDlc]; ///< 原始 STATUS payload
|
||||
};
|
||||
|
||||
/// 全局电机状态表
|
||||
struct MotorStateTable {
|
||||
MotorSlot slots[kMotorMaxNodes]; ///< 按 node_id-1 索引
|
||||
uint32_t global_seq; ///< 全局刷新序号
|
||||
uint32_t last_push_sec; ///< 上次推送完成时间
|
||||
uint8_t active_count; ///< 当前在线电机数
|
||||
uint32_t boot_hb_sec; ///< 最后收到 0x103 BOOT 心跳时的 uptime(0=从未)
|
||||
uint8_t boot_channel; ///< 最后 0x103 所在 CAN 通道
|
||||
uint8_t boot_active; ///< 1=最近 kBootHeartbeatTimeoutSec 内有 0x103
|
||||
};
|
||||
|
||||
/// 电机状态管理命名空间
|
||||
namespace MotorState {
|
||||
|
||||
/// 获取全局状态表引用
|
||||
MotorStateTable &table();
|
||||
|
||||
/// CAN 帧到达时调用 (主循环上下文, taskCanRxParse 内)
|
||||
void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len);
|
||||
|
||||
/// 1Hz 离线检测 (taskHeartbeat 内)
|
||||
void checkOffline();
|
||||
|
||||
/// 序列化一页电机状态到 payload 缓冲区
|
||||
/// @return 实际写入字节数, 0=参数错误
|
||||
uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen);
|
||||
|
||||
} // namespace MotorState
|
||||
|
||||
#endif /* DEVICE_MOTOR_STATE_H */
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @file motor_version.cpp
|
||||
* @brief 多电机版本缓存实现 — CAN 版本应答帧(0x00FF)识别、缓存、0xC6 序列化。
|
||||
*
|
||||
* 所有函数仅在主循环上下文调用(taskCanRxParse / pushStep),无 ISR 竞态。
|
||||
* 与 motor_state.cpp 不同:版本是事件驱动(收到应答才缓存+推送),无周期任务、
|
||||
* 无离线判定;pushStep 在 app_spi_cmd_motor_version.cpp(含 SPI 依赖)。
|
||||
*/
|
||||
|
||||
#include "device/motor_version.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include <cstring>
|
||||
|
||||
/* ── 全局版本表实例 ── */
|
||||
static MotorVersionTable s_verTable = {};
|
||||
|
||||
namespace MotorVersion {
|
||||
|
||||
MotorVersionTable &table()
|
||||
{
|
||||
return s_verTable;
|
||||
}
|
||||
|
||||
void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
|
||||
{
|
||||
(void)channel;
|
||||
|
||||
/* 掩码匹配版本应答 ID: 0x000N00FF */
|
||||
if ((id & kMotorVersionIdMask) != kMotorVersionIdMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t node = static_cast<uint8_t>((id >> 16) & 0x0FU);
|
||||
if (node < 1U || node > kMotorVersionMaxNodes) {
|
||||
return;
|
||||
}
|
||||
if (data == nullptr || len < 1U) {
|
||||
return;
|
||||
}
|
||||
|
||||
MotorVersionSlot &slot = s_verTable.slots[node - 1U];
|
||||
|
||||
slot.node_id = node;
|
||||
slot.seq = ++s_verTable.global_seq;
|
||||
|
||||
/* 拷贝原始 payload(不足补 0,超长截断) */
|
||||
const uint8_t n = (len <= kMotorVersionDlc) ? len : kMotorVersionDlc;
|
||||
std::memcpy(slot.version, data, n);
|
||||
if (n < kMotorVersionDlc) {
|
||||
std::memset(&slot.version[n], 0, kMotorVersionDlc - n);
|
||||
}
|
||||
|
||||
slot.dirty = 1U; /* 标记待推送 */
|
||||
}
|
||||
|
||||
bool hasPending()
|
||||
{
|
||||
for (uint8_t i = 0U; i < kMotorVersionMaxNodes; ++i) {
|
||||
if (s_verTable.slots[i].dirty != 0U) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t buildPayload(uint8_t *buf, uint16_t maxLen)
|
||||
{
|
||||
if (buf == nullptr || maxLen < kMotorVersionHeaderLen) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/* Header (2B): count 稍后回填, rsvd=0 */
|
||||
buf[0] = 0U;
|
||||
buf[1] = 0U;
|
||||
|
||||
uint16_t offset = kMotorVersionHeaderLen;
|
||||
uint8_t count = 0U;
|
||||
|
||||
for (uint8_t i = 0U; i < kMotorVersionMaxNodes; ++i) {
|
||||
MotorVersionSlot &slot = s_verTable.slots[i];
|
||||
if (slot.dirty == 0U) {
|
||||
continue;
|
||||
}
|
||||
/* 检查剩余空间;装不下则保留 dirty 待下轮(不丢) */
|
||||
if (offset + kMotorVersionEntrySize > maxLen) {
|
||||
break;
|
||||
}
|
||||
|
||||
buf[offset + 0U] = slot.node_id;
|
||||
writeU32Le(&buf[offset + 1U], slot.seq);
|
||||
std::memcpy(&buf[offset + 5U], slot.version, kMotorVersionDlc);
|
||||
|
||||
offset += kMotorVersionEntrySize;
|
||||
count++;
|
||||
slot.dirty = 0U; /* 已打包进本次推送 */
|
||||
}
|
||||
|
||||
if (count == 0U) {
|
||||
return 0U;
|
||||
}
|
||||
buf[0] = count;
|
||||
return offset;
|
||||
}
|
||||
|
||||
} // namespace MotorVersion
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file motor_version.h
|
||||
* @brief 多电机版本缓存 — CAN 版本应答帧(0x00FF)识别、内存缓存、SPI 0xC6 推送序列化。
|
||||
*
|
||||
* 设计要点(与 motor_state 同款模型):
|
||||
* - 仅在主循环上下文 (taskCanRxParse / pushStep) 中读写,无 ISR 竞态,不需要临界区。
|
||||
* - GD32 不做语义解码,只缓存原始 24B 版本 payload,由前端解码。
|
||||
* - 事件驱动:电机回报版本(0x000N00FF)时缓存并置 dirty,pushStep 经 ring 推 0xC6。
|
||||
* 版本是按需查询、低频数据,不进 250ms 周期推送(0xC5),避免占用周期带宽。
|
||||
*/
|
||||
|
||||
#ifndef DEVICE_MOTOR_VERSION_H
|
||||
#define DEVICE_MOTOR_VERSION_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "sys/config.h"
|
||||
|
||||
/* ── CAN ID 匹配规则 (29-bit 扩展帧) ──
|
||||
* 版本应答 = 0x000N00FF(DBC CANFD_GET_VERSION,发送方 Motor,与请求同 ID)。
|
||||
* 电机 APP 仅在收到 base_id=0x00FF 时回报(CANFD_VERSION_ID)。
|
||||
*/
|
||||
static constexpr uint32_t kMotorVersionIdMask = 0xFFF0FFFFU; /* 屏蔽 node nibble [19:16] */
|
||||
static constexpr uint32_t kMotorVersionIdMatch = 0x000000FFU; /* 版本应答类型码 */
|
||||
|
||||
static constexpr uint8_t kMotorVersionMaxNodes = CONFIG_MOTOR_MAX_NODES;
|
||||
static constexpr uint8_t kMotorVersionDlc = 24U;
|
||||
|
||||
/* ── 序列化常量 (0xC6 payload) ──
|
||||
* Header 2B: count + rsvd;每 entry 29B: node(1)+seq(4)+version(24)。
|
||||
* (236-2)/29 = 8.06 → 单帧最多 8 个节点,版本按需查询、通常单节点,足够。 */
|
||||
static constexpr uint8_t kMotorVersionHeaderLen = 2U;
|
||||
static constexpr uint8_t kMotorVersionEntrySize = 29U; /* 1+4+24 */
|
||||
|
||||
/// 单电机版本槽
|
||||
struct MotorVersionSlot {
|
||||
uint8_t node_id; ///< 1-9, 0=空槽
|
||||
uint8_t dirty; ///< 1=已缓存待推送, 0=已推送/空
|
||||
uint8_t rsvd[2];
|
||||
uint32_t seq; ///< 更新序号 (每次收到新应答 +1)
|
||||
uint8_t version[kMotorVersionDlc]; ///< 原始版本 payload
|
||||
};
|
||||
|
||||
/// 全局电机版本表
|
||||
struct MotorVersionTable {
|
||||
MotorVersionSlot slots[kMotorVersionMaxNodes]; ///< 按 node_id-1 索引
|
||||
uint32_t global_seq; ///< 全局更新序号
|
||||
};
|
||||
|
||||
/// 电机版本管理命名空间
|
||||
namespace MotorVersion {
|
||||
|
||||
/// 获取全局版本表引用
|
||||
MotorVersionTable &table();
|
||||
|
||||
/// CAN 帧到达时调用 (主循环上下文, taskCanRxParse 内);识别 0x00FF 并缓存
|
||||
void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len);
|
||||
|
||||
/// 是否有待推送的版本更新 (任一槽 dirty)
|
||||
bool hasPending();
|
||||
|
||||
/// 序列化所有待推送版本到 payload 缓冲区,并清除已打包槽的 dirty。
|
||||
/// @return 实际写入字节数, 0=无待推送/参数错误
|
||||
uint16_t buildPayload(uint8_t *buf, uint16_t maxLen);
|
||||
|
||||
} // namespace MotorVersion
|
||||
|
||||
#endif /* DEVICE_MOTOR_VERSION_H */
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "device/adc.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/task_runner.h"
|
||||
#include "main.h"
|
||||
#include <cstring>
|
||||
@@ -130,6 +131,11 @@ void adcFeedbackPushStep()
|
||||
s_needPush = false;
|
||||
return;
|
||||
}
|
||||
/* 电机 OTA relay 活跃期间停止 ADC 反馈推送,避免按铃干扰 0xD1 数据流
|
||||
* (详见 canFeedbackPushStep 同款注释)。 */
|
||||
if (MotorOtaRelay::isActive()) {
|
||||
return;
|
||||
}
|
||||
/* 与 CAN 反馈共用 ring:未读走则等下一主循环,不抢占 */
|
||||
if (AppSpiCommandBus.isRingPending()) {
|
||||
return;
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -281,6 +282,15 @@ void canFeedbackPushStep()
|
||||
return;
|
||||
}
|
||||
|
||||
/* 电机 OTA relay 活跃期间停止 CAN 反馈主动推送:
|
||||
* OTA 数据流(0xD1)在 SPI 上是紧循环 stop-and-wait,本推送会拉低 PB6 按
|
||||
* 铃、占用 m_txBuf,与 0xD1 的 ACK 响应争用单缓冲;ESP32 的 send/poll
|
||||
* 两步法会把 0xC1 EVENT 帧当作 0xD1 响应误读,导致 feedTask 误判失败。
|
||||
* OTA 结束后 s_roundActive 由 taskHeartbeat 重新激活。 */
|
||||
if (MotorOtaRelay::isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 1Hz 节流:当前轮次未激活则等待 taskHeartbeat() 触发 */
|
||||
if (!s_roundActive) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @file app_spi_cmd_motor_ota.cpp
|
||||
* @brief SPI 命令处理:电机 OTA relay(0xD0 命令 / 0xD1 数据)。
|
||||
*
|
||||
* 0xD0 payload: [cmd(1B)] + 参数
|
||||
* cmd=0 (START): [ch(1B) + fw_size(4B LE)]
|
||||
* cmd=1 (CANCEL): 无参数
|
||||
* cmd=2 (QUERY): 无参数 → 返回 12B 状态
|
||||
*
|
||||
* 0xD1 payload: [offset:u32 LE][固件明文…](总长≤236,数据≤232),
|
||||
* GD32 按绝对 offset 幂等写入环缓后本地加密发 CAN。
|
||||
*/
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
class SpiMotorOtaHandler final : public ISpiCommandHandler {
|
||||
public:
|
||||
bool canHandle(uint8_t frame_type) const override
|
||||
{
|
||||
return (frame_type == SpiDefs::kTypeMotorOtaCmd ||
|
||||
frame_type == SpiDefs::kTypeMotorOtaData);
|
||||
}
|
||||
|
||||
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
|
||||
{
|
||||
if (ctx.frame_type == SpiDefs::kTypeMotorOtaCmd) {
|
||||
handleCmd(bus, ctx);
|
||||
} else {
|
||||
handleData(bus, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void handleCmd(AppSpiCommand &bus, const SpiCommandContext &ctx)
|
||||
{
|
||||
if (ctx.payload_len < 1U) {
|
||||
const uint8_t err = 0xE0U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t cmd = ctx.payload[0];
|
||||
switch (cmd) {
|
||||
case 0U: { /* START */
|
||||
if (ctx.payload_len < 6U) {
|
||||
const uint8_t err = 0xE0U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
const uint8_t ch = ctx.payload[1];
|
||||
const uint32_t fw_size =
|
||||
static_cast<uint32_t>(ctx.payload[2])
|
||||
| (static_cast<uint32_t>(ctx.payload[3]) << 8)
|
||||
| (static_cast<uint32_t>(ctx.payload[4]) << 16)
|
||||
| (static_cast<uint32_t>(ctx.payload[5]) << 24);
|
||||
|
||||
printf("[MOTOR-OTA] SPI 0xD0 START recv ch=%u fw=%lu\r\n",
|
||||
(unsigned)ch, (unsigned long)fw_size);
|
||||
if (MotorOtaRelay::start(ch, fw_size)) {
|
||||
const uint8_t ack = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &ack, 1U);
|
||||
} else {
|
||||
const uint8_t err = 0xE5U; /* 已在进行中或参数无效 */
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1U: { /* CANCEL */
|
||||
MotorOtaRelay::cancel();
|
||||
const uint8_t ack = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &ack, 1U);
|
||||
break;
|
||||
}
|
||||
case 2U: { /* QUERY */
|
||||
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
|
||||
uint8_t resp[12];
|
||||
resp[0] = static_cast<uint8_t>(st.state);
|
||||
resp[1] = st.error_code;
|
||||
resp[2] = static_cast<uint8_t>(st.packets_acked & 0xFF);
|
||||
resp[3] = static_cast<uint8_t>(st.packets_acked >> 8);
|
||||
resp[4] = static_cast<uint8_t>(st.total_packets & 0xFF);
|
||||
resp[5] = static_cast<uint8_t>(st.total_packets >> 8);
|
||||
resp[6] = static_cast<uint8_t>(st.fw_size & 0xFF);
|
||||
resp[7] = static_cast<uint8_t>((st.fw_size >> 8) & 0xFF);
|
||||
resp[8] = static_cast<uint8_t>((st.fw_size >> 16) & 0xFF);
|
||||
resp[9] = static_cast<uint8_t>((st.fw_size >> 24) & 0xFF);
|
||||
resp[10] = 0; /* reserved */
|
||||
resp[11] = 0;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, resp, 12U);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const uint8_t err = 0xE1U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleData(AppSpiCommand &bus, const SpiCommandContext &ctx)
|
||||
{
|
||||
if (ctx.payload_len == 0U) {
|
||||
const uint8_t ack = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &ack, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MotorOtaRelay::feedData(ctx.payload, ctx.payload_len)) {
|
||||
const uint8_t ack = 0x00U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, &ack, 1U);
|
||||
} else {
|
||||
/* 缓冲满或状态不对:返回 WAIT,ESP32 稍后重试 */
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspWait, nullptr, 0U);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static SpiMotorOtaHandler s_handler;
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerSpiMotorOtaCommand(AppSpiCommand &bus)
|
||||
{
|
||||
bus.registerHandler(&s_handler);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @file app_spi_cmd_motor_state.cpp
|
||||
* @brief 多电机状态 250ms 主动推送 (TYPE 0xC5) 与被动查询处理器。
|
||||
*
|
||||
* 与 0xC1 CAN 反馈 / 0xC4 ADC 反馈同款模型:
|
||||
* - 主循环 pushStep 非阻塞;ring 未读走则跳过。
|
||||
* - 优先级:CAN feedback > Motor state > ADC(runPending 调用顺序决定)。
|
||||
* - 启动节拍:taskMotorStatePush()(250ms 独立任务位,与 1Hz heartbeat 解耦)。
|
||||
* - 总开关:sys/config.h 中 CONFIG_MOTOR_STATE_ENABLE
|
||||
*
|
||||
* payload 格式 (每页):
|
||||
* [0] page 页号 (0/1)
|
||||
* [1] total_pages 总页数 (2)
|
||||
* [2] entry_count 本页有效电机数
|
||||
* [3] active_count 全局在线电机数
|
||||
* [4..] 每个 entry 44B:
|
||||
* [0] node_id, [1] channel, [2] online, [3] dlc,
|
||||
* [4..7] rx_count (LE), [8..11] seq (LE),
|
||||
* [12..43] data[32] (原始 STATUS payload)
|
||||
*/
|
||||
|
||||
#include "device/spi/spi_command_handler.h"
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/motor_state.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include <cstring>
|
||||
|
||||
#if (CONFIG_MOTOR_STATE_ENABLE != 0)
|
||||
|
||||
/* ---- 被动查询 handler ---- */
|
||||
|
||||
namespace {
|
||||
|
||||
static constexpr uint16_t kMotorPayloadMax =
|
||||
static_cast<uint16_t>(AppSpiCommand::kTxBufSize - V2_OVERHEAD);
|
||||
|
||||
class SpiMotorStateHandler final : public ISpiCommandHandler {
|
||||
public:
|
||||
bool canHandle(uint8_t frame_type) const override
|
||||
{
|
||||
return frame_type == SpiDefs::kTypeMotorState;
|
||||
}
|
||||
|
||||
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
|
||||
{
|
||||
/* 请求 payload[0] = page (可选, 默认 0) */
|
||||
const uint8_t page = (ctx.payload_len >= 1U) ? ctx.payload[0] : 0U;
|
||||
|
||||
uint8_t payload[kMotorPayloadMax] = {};
|
||||
const uint16_t n = MotorState::buildPayload(page, payload, sizeof(payload));
|
||||
|
||||
if (n == 0U) {
|
||||
uint8_t err = 0xE1U; /* 非法页号 */
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, payload, n);
|
||||
}
|
||||
};
|
||||
|
||||
static SpiMotorStateHandler s_handler;
|
||||
|
||||
} // namespace
|
||||
|
||||
/* ---- 主动推送状态机 ---- */
|
||||
|
||||
static volatile bool s_roundActive = true; /* 首次允许启动,无需等 1s */
|
||||
static uint8_t s_page = 0U;
|
||||
|
||||
/**
|
||||
* @brief 激活新一轮 0xC5 推送。
|
||||
* @note 若上一轮仍在进行中(s_roundActive=true),不重置 s_page,
|
||||
* 避免在 250ms 节拍 < 单轮 P0+P1 排空时间时反复打断、
|
||||
* 导致 P1 永远没机会推(前端只看到 P0 的 5 个节点)。
|
||||
* 新一轮请求会被合并到当前轮次,推完 P1 后自然进入下一轮。
|
||||
* 这与 0xC1 canFeedbackStartRound() 的语义不同:CAN 反馈 1Hz
|
||||
* 远大于排空时间,不会发生自打断;0xC5 在 250ms 节拍下需要这层保护。
|
||||
*/
|
||||
void motorStateStartRound()
|
||||
{
|
||||
s_roundActive = true;
|
||||
/* 仅在轮次已结束时重置页号;进行中保持 s_page 不动 */
|
||||
if (s_page >= kMotorTotalPages) {
|
||||
s_page = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
void motorStatePushStep()
|
||||
{
|
||||
if (!s_roundActive) {
|
||||
return;
|
||||
}
|
||||
if (OtaBootRequest::isRebootPending()) {
|
||||
s_roundActive = false;
|
||||
return;
|
||||
}
|
||||
/* 电机 OTA relay 活跃期间停止 motor state 推送,避免按铃干扰 0xD1 数据流
|
||||
* (详见 canFeedbackPushStep 同款注释)。 */
|
||||
if (MotorOtaRelay::isActive()) {
|
||||
return;
|
||||
}
|
||||
if (AppSpiCommandBus.isRingPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t payload[kMotorPayloadMax] = {};
|
||||
const uint16_t n = MotorState::buildPayload(s_page, payload, sizeof(payload));
|
||||
|
||||
if (n > 0U) {
|
||||
(void)AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorState, payload, n);
|
||||
}
|
||||
|
||||
/* 推进页号;到末页后结束本轮 */
|
||||
s_page++;
|
||||
if (s_page >= kMotorTotalPages) {
|
||||
s_page = 0U;
|
||||
s_roundActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
void registerSpiMotorStateCommand(AppSpiCommand &bus)
|
||||
{
|
||||
bus.registerHandler(&s_handler);
|
||||
}
|
||||
|
||||
#else /* CONFIG_MOTOR_STATE_ENABLE == 0 */
|
||||
|
||||
void motorStateStartRound() {}
|
||||
void motorStatePushStep() {}
|
||||
void registerSpiMotorStateCommand(AppSpiCommand & /*bus*/) {}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file app_spi_cmd_motor_version.cpp
|
||||
* @brief 多电机版本 0xC6 事件驱动推送(pushStep)。
|
||||
*
|
||||
* 与 0xC1/0xC4/0xC5 同款 ring 按铃模型,但触发方式不同:
|
||||
* - 0xC5 是 250ms 周期轮次;0xC6 是事件驱动——电机回报版本(0x00FF)时
|
||||
* MotorVersion::onCanFrame() 缓存并置 dirty,本 pushStep 在主循环发现
|
||||
* 有待推送(dirty)且 ring 空闲时,打包一帧 0xC6 按铃推给 ESP32。
|
||||
* - 版本是按需查询的低频数据,推送优先级最低(runPending 中排在 0xC1/0xC5/0xC4 之后)。
|
||||
* - 总开关:sys/config.h 中 CONFIG_MOTOR_VERSION_ENABLE
|
||||
*
|
||||
* payload 格式(单帧,Header 2B + 每 entry 29B):
|
||||
* [0] count 本帧节点数
|
||||
* [1] rsvd 保留
|
||||
* [2..] 每个 entry 29B:
|
||||
* [0] node_id, [1..4] seq (LE), [5..28] version[24] (原始版本 payload)
|
||||
*
|
||||
* 仅推送(无被动查询 handler):ESP32 收到后自行缓存,Web 从 ESP32 缓存读取。
|
||||
*/
|
||||
|
||||
#include "device/spi/app_spi_command.h"
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/motor_version.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include <cstring>
|
||||
|
||||
#if (CONFIG_MOTOR_VERSION_ENABLE != 0)
|
||||
|
||||
namespace {
|
||||
static constexpr uint16_t kVerPayloadMax =
|
||||
static_cast<uint16_t>(AppSpiCommand::kTxBufSize - V2_OVERHEAD);
|
||||
}
|
||||
|
||||
void motorVersionPushStep()
|
||||
{
|
||||
/* 无待推送 → 直接返回(事件驱动,绝大多数主循环迭代在此短路) */
|
||||
if (!MotorVersion::hasPending()) {
|
||||
return;
|
||||
}
|
||||
if (OtaBootRequest::isRebootPending()) {
|
||||
return;
|
||||
}
|
||||
/* 电机 OTA relay 活跃期间停止版本推送,避免按铃干扰 0xD1 数据流
|
||||
* (详见 canFeedbackPushStep 同款注释)。dirty 保留不丢,OTA 结束后继续推。 */
|
||||
if (MotorOtaRelay::isActive()) {
|
||||
return;
|
||||
}
|
||||
if (AppSpiCommandBus.isRingPending()) {
|
||||
return; /* 上一帧未被 ESP32 读走,下轮再试(dirty 保留不丢) */
|
||||
}
|
||||
|
||||
uint8_t payload[kVerPayloadMax] = {};
|
||||
const uint16_t n = MotorVersion::buildPayload(payload, sizeof(payload));
|
||||
if (n > 0U) {
|
||||
/* ring 空闲时 requestEsp32Read 必成功(单线程主循环,检查与调用之间无并发);
|
||||
* buildPayload 已清掉已打包槽的 dirty。 */
|
||||
(void)AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorVersion, payload, n);
|
||||
}
|
||||
}
|
||||
|
||||
#else /* CONFIG_MOTOR_VERSION_ENABLE == 0 */
|
||||
|
||||
void motorVersionPushStep() {}
|
||||
|
||||
#endif
|
||||
@@ -386,6 +386,8 @@ extern void registerSpiCanBridgeCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiMotorStateCommand(AppSpiCommand &bus);
|
||||
extern void registerSpiMotorOtaCommand(AppSpiCommand &bus);
|
||||
AppSpiCommand AppSpiCommandBus;
|
||||
void AppSpiCommand::init()
|
||||
{
|
||||
@@ -407,6 +409,8 @@ void AppSpiCommand::init()
|
||||
registerSpiCanFeedbackCommand(AppSpiCommandBus);
|
||||
registerSpiIdSnapshotCommand(AppSpiCommandBus);
|
||||
registerSpiAdcFeedbackCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
|
||||
registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
|
||||
registerSpiMotorOtaCommand(AppSpiCommandBus);
|
||||
|
||||
configureGpio();
|
||||
configureDma();
|
||||
@@ -467,7 +471,14 @@ void AppSpiCommand::armSpiDmasForNextCs()
|
||||
|
||||
void AppSpiCommand::registerHandler(ISpiCommandHandler *handler)
|
||||
{
|
||||
if (!handler || m_commandCount >= kMaxCommands) {
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
if (m_commandCount >= kMaxCommands) {
|
||||
/* 表满静默丢弃会导致命令无声失效(motor_ota 曾因此 0xD0 回 0xEE)。
|
||||
* init 在任务上下文,printf 走 USART0 中断环,启动日志可见。 */
|
||||
printf("[SPI-APP] registerHandler: 注册表满(%u),handler 被丢弃! 请增大 CONFIG_SPI_MAX_COMMANDS\r\n",
|
||||
static_cast<unsigned>(kMaxCommands));
|
||||
return;
|
||||
}
|
||||
m_commands[m_commandCount++] = handler;
|
||||
@@ -811,6 +822,13 @@ void AppSpiCommand::onCsRelease()
|
||||
m_txLen = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
|
||||
/* ★ 关键:在 processCommand 之前同步 g_localCredit。
|
||||
* 此时 m_txLen=0, m_ringPending=false → g_localCredit=1。
|
||||
* 若不做,buildResponse 会读到上一次 pushStatus/requestEsp32Read 留下的
|
||||
* g_localCredit=0 → 响应帧携带 credit=0 → ESP32 m_remoteCredit=0 → 后续发送死锁。
|
||||
* 这是电机 OTA 吐量只有 1 chunk/2s 的根因。 */
|
||||
updateDataReady();
|
||||
|
||||
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
|
||||
* 消除主循环延迟。处理完成后 processCommand() 末尾再 re-arm DMA + 释放 SPIready。 */
|
||||
processCommand();
|
||||
|
||||
@@ -173,6 +173,21 @@ void adcFeedbackPushStep();
|
||||
/** @brief 激活一次 ADC 调试反馈推送(1Hz taskHeartbeat)。 */
|
||||
void adcFeedbackStartRound();
|
||||
|
||||
/**
|
||||
* @brief 多电机状态(0xC5)主动推送一步。
|
||||
* @note CONFIG_MOTOR_STATE_ENABLE=0 时为空操作。
|
||||
*/
|
||||
void motorStatePushStep();
|
||||
|
||||
/** @brief 激活新一轮多电机状态推送(1Hz taskHeartbeat)。 */
|
||||
void motorStateStartRound();
|
||||
|
||||
/**
|
||||
* @brief 多电机版本(0xC6)事件驱动推送步进:有 dirty 版本且 ring 空闲时按铃推一帧。
|
||||
* @note CONFIG_MOTOR_VERSION_ENABLE=0 时为空操作。优先级低于 0xC1/0xC5/0xC4。
|
||||
*/
|
||||
void motorVersionPushStep();
|
||||
|
||||
/** @brief 全局 SPI 命令总线实例。 */
|
||||
extern AppSpiCommand AppSpiCommandBus;
|
||||
|
||||
|
||||
@@ -27,6 +27,21 @@ static constexpr uint8_t kTypeIdSnapshot = 0xC3U; /**< ESP32 -> GD32 拉取不
|
||||
/** @brief GD32 ADC 调试反馈(温度/VDDA/VIN);1Hz 主动 EVENT 或被动查询。
|
||||
* 由 CONFIG_SPI_ADC_FEEDBACK_ENABLE 门控,调试用可随时关。 */
|
||||
static constexpr uint8_t kTypeAdcFeedback = 0xC4U;
|
||||
/** @brief 多电机状态推送;GD32 1Hz 主动 EVENT,将 CAN STATUS 帧缓存表推给 ESP32。
|
||||
* 由 CONFIG_MOTOR_STATE_ENABLE 门控。 */
|
||||
static constexpr uint8_t kTypeMotorState = 0xC5U;
|
||||
/** @brief 多电机版本推送;GD32 收到电机版本应答(0x00FF)后事件驱动 EVENT,
|
||||
* 将缓存的 24B 版本推给 ESP32(按需查询、低频,不进周期推送)。
|
||||
* 由 CONFIG_MOTOR_VERSION_ENABLE 门控。 */
|
||||
static constexpr uint8_t kTypeMotorVersion = 0xC6U;
|
||||
|
||||
/** @brief 电机 OTA relay 命令(ESP32→GD32):start / cancel / query。
|
||||
* payload[0]=cmd(0=start,1=cancel,2=query), start 时 payload[1]=ch, payload[2..5]=fw_size LE */
|
||||
static constexpr uint8_t kTypeMotorOtaCmd = 0xD0U;
|
||||
/** @brief 电机 OTA relay 数据(ESP32→GD32):固件明文块,GD32 本地加密后发 CAN。 */
|
||||
static constexpr uint8_t kTypeMotorOtaData = 0xD1U;
|
||||
/** @brief 电机 OTA relay 状态推送(GD32→ESP32 ring push):12B 进度。 */
|
||||
static constexpr uint8_t kTypeMotorOtaStatus = 0xD2U;
|
||||
|
||||
/* 响应类型 */
|
||||
static constexpr uint8_t kRspAck = 0x80U;
|
||||
|
||||
Reference in New Issue
Block a user