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:
cbb
2026-07-26 20:39:59 +08:00
parent 934836618a
commit 079c6442d4
25 changed files with 2503 additions and 103 deletions
@@ -18,6 +18,10 @@ project:
- file: ./sys/task_runner.cpp
- file: ./sys/ota_boot_request.h
- file: ./sys/ota_boot_request.cpp
- file: ./sys/aes128.h
- file: ./sys/aes128.cpp
- file: ./sys/motor_ota_relay.h
- file: ./sys/motor_ota_relay.cpp
- file: ./sys/cpp_stubs.cpp
- file: ./sys/config.h
- group: device
@@ -32,6 +36,10 @@ project:
- file: ./device/usart0.cpp
- file: ./device/watchdog.h
- file: ./device/watchdog.cpp
- file: ./device/motor_state.h
- file: ./device/motor_state.cpp
- file: ./device/motor_version.h
- file: ./device/motor_version.cpp
# GigaDevice DFP 未把 EXMC 注册为 CMSIS 组件(仅有 FMC=片内 Flash),
# 但外设源文件存在于 Pack;LAN9252 并行总线需要 EXMC,故本地编入。
- group: device_exmc
@@ -51,6 +59,9 @@ project:
- file: ./device/spi/app_spi_cmd_can_feedback.cpp
- file: ./device/spi/app_spi_cmd_id_snapshot.cpp
- file: ./device/spi/app_spi_cmd_adc_feedback.cpp
- file: ./device/spi/app_spi_cmd_motor_state.cpp
- file: ./device/spi/app_spi_cmd_motor_version.cpp
- file: ./device/spi/app_spi_cmd_motor_ota.cpp
- group: spi_link
files:
- file: ../../spi_link/spi_link_v2.cpp
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* @file esc_exmc_bus.h
* @brief LAN9252 ESC 的 EXMC 16-bit 并行总线抽象接口。
* @note 通过 EXMC NOR/SRAM Region1(NE1,基址 0x60000000)以内存映射方式访问 LAN9252。
* @note 通过 EXMC NOR/SRAM Region1(NE1,基址 0x64000000)以内存映射方式访问 LAN9252。
* 16-bit 数据总线 + A16 地址线 + NOE/NWE/NE1 控制线。
*/
#ifndef COMM_ESC_EXMC_BUS_H
+55 -35
View File
@@ -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) {
+6
View File
@@ -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();
/**
+148
View File
@@ -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
+87
View File
@@ -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 */
+105
View File
@@ -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
+68
View File
@@ -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;
+5 -2
View File
@@ -16,8 +16,9 @@ enum system_task_t {
TASK_ADC_TEMP_SAMPLE, /**< 采样内部温度通道原始值任务。 */
TASK_ADC_PROCESS, /**< 对 ADC 原始值进行电压与温度计算任务。 */
TASK_CAN_RX_PARSE, /**< 解析并消费已接收 CANFD 帧任务。 */
TASK_CAN_TX_SEND, /**< CANFD 周期发送任务,每 1ms 每通道发送一帧。 */
TASK_CAN_TX_SEND, /**< CANFD 周期发送任务,每 10ms 每通道发送一帧。 */
TASK_HEARTBEAT, /**< 系统心跳维护任务。 */
TASK_MOTOR_STATE_PUSH, /**< 多电机状态 0xC5 主动推送启动任务(250ms)。 */
TASK_DIAGNOSTIC_PRINT, /**< 诊断信息打印任务。 */
TASK_ETHERCAT_PROCESS, /**< EtherCAT 周期/事件处理任务(非实时应用层 PDO 映射)。 */
TASK_COUNT /**< 任务总数,用于边界或统计场景。 */
@@ -41,10 +42,12 @@ struct task_scheduler_t {
volatile uint16_t div_temp_sample;
/** @brief 调度分频计数:CAN 接收处理。 */
volatile uint16_t div_can_rx;
/** @brief 调度分频计数:CAN 周期发送(1ms)。 */
/** @brief 调度分频计数:CAN 周期发送(10ms)。 */
volatile uint16_t div_can_tx;
/** @brief 调度分频计数:心跳(1s)。 */
volatile uint16_t div_heartbeat;
/** @brief 调度分频计数:多电机状态推送(250ms)。 */
volatile uint16_t div_motor_state;
/** @brief 调度分频计数:APP 状态/诊断打印(5s)。 */
volatile uint16_t div_diag_print;
};
+183
View File
@@ -0,0 +1,183 @@
/**
* @file aes128.cpp
* @brief AES-128 ECB 加解密实现(移植自电机 BOOT 工程 sys/aes128.cpp)。
*/
#include "sys/aes128.h"
/* S盒 */
static const uint8_t sbox[256] = {
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
};
/* 逆S盒 */
static const uint8_t inv_sbox[256] = {
0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d
};
/* 轮常量 */
static const uint8_t rcon[10] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36};
static uint8_t xtime(uint8_t x) { return static_cast<uint8_t>((x << 1) ^ (((x >> 7) & 1) * 0x1b)); }
static uint8_t gf_mul(uint8_t a, uint8_t b) {
uint8_t p = 0;
while (b) {
if (b & 1) p ^= a;
const uint8_t hi = a & 0x80;
a <<= 1;
if (hi) a ^= 0x1b;
b >>= 1;
}
return p;
}
static void add_round_key(uint8_t *state, const uint8_t *rk) {
for (uint8_t i = 0; i < 16; i++) state[i] ^= rk[i];
}
static void sub_bytes(uint8_t *state) {
for (uint8_t i = 0; i < 16; i++) state[i] = sbox[state[i]];
}
static void inv_sub_bytes(uint8_t *state) {
for (uint8_t i = 0; i < 16; i++) state[i] = inv_sbox[state[i]];
}
static void shift_rows(uint8_t *s) {
uint8_t t;
t=s[1]; s[1]=s[5]; s[5]=s[9]; s[9]=s[13]; s[13]=t;
t=s[2]; s[2]=s[10]; s[10]=t; t=s[6]; s[6]=s[14]; s[14]=t;
t=s[3]; s[3]=s[15]; s[15]=s[11]; s[11]=s[7]; s[7]=t;
}
static void inv_shift_rows(uint8_t *s) {
uint8_t t;
t=s[13]; s[13]=s[9]; s[9]=s[5]; s[5]=s[1]; s[1]=t;
t=s[2]; s[2]=s[10]; s[10]=t; t=s[6]; s[6]=s[14]; s[14]=t;
t=s[7]; s[7]=s[11]; s[11]=s[15]; s[15]=s[3]; s[3]=t;
}
static void mix_columns(uint8_t *s) {
for (uint8_t c = 0; c < 4; c++) {
const uint8_t i = c * 4;
const uint8_t a0=s[i], a1=s[i+1], a2=s[i+2], a3=s[i+3];
s[i] = xtime(a0) ^ xtime(a1) ^ a1 ^ a2 ^ a3;
s[i+1] = a0 ^ xtime(a1) ^ xtime(a2) ^ a2 ^ a3;
s[i+2] = a0 ^ a1 ^ xtime(a2) ^ xtime(a3) ^ a3;
s[i+3] = xtime(a0) ^ a0 ^ a1 ^ a2 ^ xtime(a3);
}
}
static void inv_mix_columns(uint8_t *s) {
for (uint8_t c = 0; c < 4; c++) {
const uint8_t i = c * 4;
const uint8_t a0=s[i], a1=s[i+1], a2=s[i+2], a3=s[i+3];
s[i] = gf_mul(0x0e,a0)^gf_mul(0x0b,a1)^gf_mul(0x0d,a2)^gf_mul(0x09,a3);
s[i+1] = gf_mul(0x09,a0)^gf_mul(0x0e,a1)^gf_mul(0x0b,a2)^gf_mul(0x0d,a3);
s[i+2] = gf_mul(0x0d,a0)^gf_mul(0x09,a1)^gf_mul(0x0e,a2)^gf_mul(0x0b,a3);
s[i+3] = gf_mul(0x0b,a0)^gf_mul(0x0d,a1)^gf_mul(0x09,a2)^gf_mul(0x0e,a3);
}
}
static void key_expansion(aes128_context *ctx) {
memcpy(ctx->round_keys[0], ctx->key, 16);
for (uint8_t i = 1; i <= 10; i++) {
uint8_t temp[4];
temp[0] = ctx->round_keys[i-1][12];
temp[1] = ctx->round_keys[i-1][13];
temp[2] = ctx->round_keys[i-1][14];
temp[3] = ctx->round_keys[i-1][15];
const uint8_t t = temp[0];
temp[0]=temp[1]; temp[1]=temp[2]; temp[2]=temp[3]; temp[3]=t;
temp[0]=sbox[temp[0]]; temp[1]=sbox[temp[1]];
temp[2]=sbox[temp[2]]; temp[3]=sbox[temp[3]];
temp[0] ^= rcon[i-1];
ctx->round_keys[i][0] = ctx->round_keys[i-1][0] ^ temp[0];
ctx->round_keys[i][1] = ctx->round_keys[i-1][1] ^ temp[1];
ctx->round_keys[i][2] = ctx->round_keys[i-1][2] ^ temp[2];
ctx->round_keys[i][3] = ctx->round_keys[i-1][3] ^ temp[3];
for (uint8_t j = 1; j < 4; j++) {
ctx->round_keys[i][j*4+0] = ctx->round_keys[i][(j-1)*4+0] ^ ctx->round_keys[i-1][j*4+0];
ctx->round_keys[i][j*4+1] = ctx->round_keys[i][(j-1)*4+1] ^ ctx->round_keys[i-1][j*4+1];
ctx->round_keys[i][j*4+2] = ctx->round_keys[i][(j-1)*4+2] ^ ctx->round_keys[i-1][j*4+2];
ctx->round_keys[i][j*4+3] = ctx->round_keys[i][(j-1)*4+3] ^ ctx->round_keys[i-1][j*4+3];
}
}
}
void aes128_set_key(aes128_context *ctx, const uint8_t *key) {
memcpy(ctx->key, key, AES128_KEY_LEN);
key_expansion(ctx);
}
void aes128_encrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output) {
memcpy(ctx->state, input, AES_BLOCK_SIZE);
add_round_key(ctx->state, ctx->round_keys[0]);
for (uint8_t r = 1; r <= 9; r++) {
sub_bytes(ctx->state);
shift_rows(ctx->state);
mix_columns(ctx->state);
add_round_key(ctx->state, ctx->round_keys[r]);
}
sub_bytes(ctx->state);
shift_rows(ctx->state);
add_round_key(ctx->state, ctx->round_keys[10]);
memcpy(output, ctx->state, AES_BLOCK_SIZE);
}
void aes128_decrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output) {
memcpy(ctx->state, input, AES_BLOCK_SIZE);
add_round_key(ctx->state, ctx->round_keys[10]);
for (uint8_t r = 9; r >= 1; r--) {
inv_shift_rows(ctx->state);
inv_sub_bytes(ctx->state);
add_round_key(ctx->state, ctx->round_keys[r]);
inv_mix_columns(ctx->state);
}
inv_shift_rows(ctx->state);
inv_sub_bytes(ctx->state);
add_round_key(ctx->state, ctx->round_keys[0]);
memcpy(output, ctx->state, AES_BLOCK_SIZE);
}
void aes128_ecb_encrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output) {
aes128_encrypt_block(ctx, input, output);
aes128_encrypt_block(ctx, input + 16, output + 16);
aes128_encrypt_block(ctx, input + 32, output + 32);
}
void aes128_ecb_decrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output) {
aes128_decrypt_block(ctx, input, output);
aes128_decrypt_block(ctx, input + 16, output + 16);
aes128_decrypt_block(ctx, input + 32, output + 32);
}
+28
View File
@@ -0,0 +1,28 @@
/**
* @file aes128.h
* @brief AES-128 ECB 加解密(纯软件实现,移植自电机 BOOT 工程)。
* 用于电机 OTA relay 中对固件数据加密及密钥协商。
*/
#ifndef SYS_AES128_H
#define SYS_AES128_H
#include <cstdint>
#include <cstddef>
#include <cstring>
static constexpr uint8_t AES128_KEY_LEN = 16U;
static constexpr uint8_t AES_BLOCK_SIZE = 16U;
struct aes128_context {
uint8_t key[AES128_KEY_LEN];
uint8_t round_keys[11][16];
uint8_t state[AES_BLOCK_SIZE];
};
void aes128_set_key(aes128_context *ctx, const uint8_t *key);
void aes128_encrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output);
void aes128_decrypt_block(aes128_context *ctx, const uint8_t *input, uint8_t *output);
void aes128_ecb_encrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output);
void aes128_ecb_decrypt_48bytes(aes128_context *ctx, const uint8_t *input, uint8_t *output);
#endif /* SYS_AES128_H */
+60 -18
View File
@@ -14,31 +14,62 @@
* 功能开关(调试/特性总闸 — 改这里即可开关,改完重编)
* 0/false=关,1/true=开。需要 #if 的保留 #define。
* ============================================================ */
/** @brief 周期温度/电压 UART 打印(5s,task_manager #if)。 */
#define CONFIG_UART_TEMP_PERIODIC_PRINT 1
/** @brief UART 诊断详细级别(task_runner #if;0=精简 1=含 CAN 统计)。 */
#define CONFIG_UART_DIAG_LEVEL 1
/**
* @brief 5s 诊断任务是否输出全量 UART 摘要(task_runner)。
* @note 任务本身始终 5s 调度:0=默认仅状态变化/错误增量;1=周期打印 ADC/CAN/ECAT 全量行。
* 现场抓吞吐/温度时改回 1 后重编即可。
*/
#define CONFIG_UART_TEMP_PERIODIC_PRINT 0
/**
* @brief 全量摘要详细级别(仅 PERIODIC_PRINT=1 时生效;0=ADC 精简 1=含 CAN 统计)。
* @note 默认 0;需要 can_tx/rx/drop 等列时与 PERIODIC_PRINT 一并打开。
*/
#define CONFIG_UART_DIAG_LEVEL 0
/**
* @brief SPI 调试反馈 0xC4:GD32 ADC 温度/VDDA/VIN 1Hz 主动推送。
* @note 调试用;0=不注册 handler、不推送。与 ESP32 GD32_ADC_FEEDBACK_ENABLE 配对。
*/
#define CONFIG_SPI_ADC_FEEDBACK_ENABLE 1
/**
* @brief 多电机状态表 0xC5:CAN STATUS 帧缓存 + 250ms 主动推送。
* @note 0=不注册 handler、不推送、不解析 STATUS 帧。与 ESP32 端 motor_state_task 配对。
*/
#define CONFIG_MOTOR_STATE_ENABLE 1
/**
* @brief 多电机版本 0xC6:CAN 版本应答帧(0x00FF)缓存 + 事件驱动推送。
* @note 0=不识别版本帧、不推送。与 ESP32 端 motor_version 缓存配对。
* 版本为按需查询的低频数据,独立于 0xC5 周期推送。
*/
#define CONFIG_MOTOR_VERSION_ENABLE 1
/** @brief 最大电机节点数 (node ID 1-9)。 */
constexpr uint8_t CONFIG_MOTOR_MAX_NODES = 9U;
/** @brief 电机离线判定超时 (ms)。超过此时间未收到 STATUS 帧则标记 offline。 */
constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U;
/** @brief SPI 命令逐帧日志(#if)。 */
#define CONFIG_SPI_APP_LOG_ENABLE 0
/** @brief CAN_SEND 成功帧详细日志(#if)。 */
#define CONFIG_SPI_CAN_SEND_VERBOSE_LOG 0
/** @brief CAN-FD BRS 使能(canfd.cpp #if)。 */
#define CONFIG_CAN_FD_BRS_ENABLE 1U
/** @brief CAN-FD BRS 使能(canfd.cpp #if)。
* 关闭 BRS: 数据段用仲裁段同速率,牢牢性更高。
* 电机 BOOT 无法正确接收 BRS 高速段→nonce(16B FD+BRS)被拒绝→握手失败。 */
#define CONFIG_CAN_FD_BRS_ENABLE 0U
/** @brief 启动时总线探测帧;正常固件关。 */
#define CONFIG_CAN_STARTUP_BUS_TEST_ENABLE 0
/** @brief 启动时内部回环自检;正常固件关。 */
#define CONFIG_CAN_STARTUP_LOOPBACK_TEST_ENABLE 0
/** @brief CAN 启动详细诊断 dump;默认关。 */
#define CONFIG_CAN_STARTUP_DIAG_VERBOSE 0
/** @brief CAN0/1/2 通道使能。 */
/**
* @brief 周期测试发送任务(taskCanTxSend,发 0x18FF5000 64B FD 帧)。
* @note 调试用:该 PGN 无业务接收节点 ACK。auto_retrans=ENABLE 时无 ACK 被
* 无限重发吞进 TEC(TEC 一直涨);DISABLE 时单发失败即 MTEn→error_count++。
* 电机拓扑下无对端应答,默认关以消除持续错误帧。需要回放测试波形时打开。
*/
#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 = true;
constexpr bool CONFIG_CAN_CH2_ENABLE = true;
constexpr bool CONFIG_CAN_CH1_ENABLE = false;
constexpr bool CONFIG_CAN_CH2_ENABLE = false;
/** @brief 主循环 WFI(main.cpp #if)。 */
#define CONFIG_MAIN_LOOP_WFI_ENABLE 0
/** @brief EtherCAT SYNC0 同步模式(#if)。 */
@@ -48,13 +79,13 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* APP 固件版本号
* ============================================================ */
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
constexpr uint32_t APP_FW_VERSION_MINOR = 3U;
constexpr uint32_t APP_FW_VERSION_PATCH = 0U;
constexpr uint32_t APP_FW_VERSION_MINOR = 4U;
constexpr uint32_t APP_FW_VERSION_PATCH = 75U; /* 1.4.75:尾段fed>=fw_size旁路水位门控,修复末6包死锁 */
constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
/** @brief USART0 默认波特率。 */
constexpr uint32_t CONFIG_USART0_BAUDRATE = 115200U;
constexpr uint32_t CONFIG_USART0_BAUDRATE = 3000000U; /* 3M:诊断打印高速输出,printf 阻塞极小、不拖慢 relay */
/** @brief ADC 外设实例。 */
constexpr uint32_t CONFIG_ADC_INSTANCE = ADC0;
@@ -100,8 +131,13 @@ constexpr uint16_t CONFIG_SPI_RX_BUF_SIZE = 256U;
constexpr uint16_t CONFIG_SPI_LOG_BUF_SIZE = 256U;
/** @brief 主动按铃超时:ESP32 未在该时间内来读则 cancelRing(ms)。 */
constexpr uint32_t CONFIG_SPI_RING_TIMEOUT_MS = 50U;
/** @brief SPI 命令 handler 注册表容量。 */
constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 8U;
/** @brief SPI 命令 handler 注册表容量。
*
* 当前 init 注册 9 个:boot_request / ota_wait / ping / can_bridge /
* can_feedback / id_snapshot / adc_feedback / motor_state / motor_ota。
* registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。
* 取 12 留余量;新增 handler 时须确认本容量足够。 */
constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 12U;
/** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */
constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_MAX = 16U;
/** @brief 快照条目最大数据字节数(CAN FD DLC 上限)。 */
@@ -176,10 +212,15 @@ constexpr uint32_t CONFIG_TASK_PERIOD_VREF_SAMPLE_TICKS = 5U;
constexpr uint32_t CONFIG_TASK_PERIOD_TEMP_SAMPLE_TICKS = 10U;
/** @brief 调度基准节拍:10ms 计数周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_CAN_RX_TICKS = 100U;
/** @brief 调度基准节拍:CAN 周期发送任务周期,10 tick = 1ms。 */
constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 10U;
/** @brief 调度基准节拍:CAN 周期发送任务周期,100 tick = 10ms。
* @note 原 1ms/帧过密,改为 10ms/帧以降低 CAN 总线负载与 GD32 邮箱压力。 */
constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 100U;
/** @brief 调度基准节拍:心跳周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_HEARTBEAT_TICKS = 10000U;
/** @brief 调度基准节拍:多电机状态 0xC5 主动推送启动周期,2500 tick = 250ms。
* @note 独立于 1Hz heartbeat,避免连带加速 OTA Boot 轮询 / CAN 反馈快照刷新 /
* ADC 推送 / checkOffline 离线判定(3s 阈值)。前端 age 应稳定在 ~120ms。 */
constexpr uint32_t CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS = 2500U;
/** @brief 调度基准节拍:APP 状态/诊断打印周期,50000 tick = 5s。 */
constexpr uint32_t CONFIG_TASK_PERIOD_DIAG_PRINT_TICKS = 50000U;
@@ -225,8 +266,9 @@ constexpr uint32_t CONFIG_ESC_PDI_OUTPUT_OFFSET = 0x1200U;
constexpr size_t CONFIG_ETHERCAT_PDO_SIZE = 32U;
/* ---------- EXMC 引脚配置(GD32E508 EXMC 标准映射) ---------- */
constexpr uint32_t CONFIG_ESC_EXMC_BASE_ADDR = 0x60000000U;
/* Region1 = NE1 @ 0x60000000;数值与 gd32e50x_exmc.h 中 EXMC_BANK0_NORSRAM_REGION1 一致 */
constexpr uint32_t CONFIG_ESC_EXMC_BASE_ADDR = 0x64000000U;
/* Region1 = NE1 @ 0x64000000(GD32E50x 用户手册 Rev2.2 图25-3: Bank0 四 Region 各 64MB,
* HADDR[27:26]=01 选中 NE1)。LAN9252 CS 接 PD7=EXMC_NE1,故基址为 0x64000000。 */
#ifndef EXMC_BANK0_NORSRAM_REGION1
#define EXMC_BANK0_NORSRAM_REGION1 ((uint32_t)0x00000001U)
#endif
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
/**
* @file motor_ota_relay.h
* @brief 电机 OTA relay 状态机 — GD32 本地完成与电机 Bootloader 的全部交互。
*
* 职责:
* - AES-128 密钥协商(K_master 存本地 flash)
* - 固件数据 AES-ECB 加密 + keyed-CRC32
* - CAN 上逐包 stop-and-wait(发 0x101 → 等 0x102 ACK → 按包号继续)
* - 进度/完成/错误通过 SPI ring push 报 ESP32
*
* ESP32 通过 SPI 0xD0(start) / 0xD1(data) / 0xD3(cancel) 喂数据,
* GD32 通过 0xD2 ring push 报进度。
*/
#ifndef SYS_MOTOR_OTA_RELAY_H
#define SYS_MOTOR_OTA_RELAY_H
#include <cstdint>
namespace MotorOtaRelay {
/* 电机 Bootloader CAN ID(固定,非节点相关) */
static constexpr uint32_t kOtaReqId = 0x00000100U; /* Host→Motor 握手 */
static constexpr uint32_t kOtaDataId = 0x00000101U; /* Host→Motor 数据 */
static constexpr uint32_t kOtaAckId = 0x00000102U; /* Motor→Host ACK */
static constexpr uint32_t kOtaWaitId = 0x00000103U; /* Motor→Host IDLE心跳 */
static constexpr uint32_t kOtaCancelId = 0x00000000U; /* Host→Motor 取消 */
static constexpr uint32_t kPacketPayload = 48U; /* 每包有效载荷 */
static constexpr uint32_t kFrameDlc = 64U; /* CAN FD DLC */
static constexpr uint32_t kMaxFwSize = 0xC000U; /* 48 KB */
static constexpr uint32_t kAckTimeoutMs = 5000U; /* 单包 ACK 超时 */
static constexpr uint32_t kHandshakeTimeoutMs = 3000U;
enum class State : uint8_t {
kIdle = 0,
kHandshakeReq, /* 已发 [00 AC],等 ACK */
kHandshakeNonce, /* 已发 nonce,等 36B ACK */
kHandshakeConfirm, /* 已发 confirm,等 [BA C0] */
kErase, /* 已发 size,等擦除完成 ACK */
kTransfer, /* 逐包传输中 */
kFinishOta, /* 收到 0xF0F0F0F0,已发 [0A AC],等 FINISH ACK */
kFinished, /* 完成 */
kError, /* 错误 */
kPreCancel = 9 /* 等待电机 BOOT 心跳(0x103)确认已进入 Bootloader,然后进 kHandshakeReq */
};
struct Status {
State state;
uint8_t error_code;
uint16_t packets_acked;
uint16_t total_packets;
uint32_t fw_size;
};
/* 初始化(注册 CAN RX 回调等) */
void init();
/* 主循环推进(从 task_runner 周期调用,建议 1~5ms 一次) */
void process();
/* 数据发送推进:由主循环 TaskRunner 每圈调用,按 SysTick 钟表控制每包间隔
* (不再走 ISR 计时 + TASK 标志,消除跨上下文计数器竞争)。 */
void onTxTick();
/* CAN RX 分发入口:匹配 0x102 ACK 帧时消费,返回 true 表示已处理 */
bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len);
/* SPI 命令接口 */
bool start(uint8_t can_channel, uint32_t fw_size);
/**
* @brief 喂入固件明文。payload 布局:[offset:u32 LE][data...]
* offset 为固件绝对字节偏移;重复/重叠区间幂等 ACK,缺口拒绝(→SPI WAIT)。
* 无 offset 的旧纯流会在 SPI 超时重传时把同一 chunk 追加进环缓(BLF 见前 236B 循环)。
*/
bool feedData(const uint8_t *data, uint16_t len);
void cancel();
/* 状态查询 */
Status getStatus();
bool isActive();
} /* namespace MotorOtaRelay */
#endif /* SYS_MOTOR_OTA_RELAY_H */
+9 -3
View File
@@ -32,6 +32,7 @@ void TaskManager::init()
g_scheduler.div_can_rx = 0U;
g_scheduler.div_can_tx = 0U;
g_scheduler.div_heartbeat = 0U;
g_scheduler.div_motor_state = 0U;
g_scheduler.div_diag_print = 0U;
}
@@ -95,7 +96,6 @@ void TaskManager::tick100us()
if(++g_scheduler.div_can_tx >= CONFIG_TASK_PERIOD_CAN_TX_TICKS) {
g_scheduler.div_can_tx = 0U;
g_scheduler.tick_1ms++;
set(1UL << TASK_CAN_TX_SEND);
}
@@ -106,12 +106,18 @@ void TaskManager::tick100us()
set(1UL << TASK_HEARTBEAT);
}
#if (CONFIG_UART_TEMP_PERIODIC_PRINT != 0)
/* 多电机状态 0xC5 推送:250ms 独立节拍。
* 不挂在 1Hz heartbeat 上,避免连带加速 OTA Boot 轮询/CAN快照/ADC推送/checkOffline。 */
if(++g_scheduler.div_motor_state >= CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS) {
g_scheduler.div_motor_state = 0U;
set(1UL << TASK_MOTOR_STATE_PUSH);
}
/* 始终 5s 调度诊断任务:默认静默检查错误增量,PERIODIC_PRINT=1 时打全量摘要 */
if(++g_scheduler.div_diag_print >= CONFIG_TASK_PERIOD_DIAG_PRINT_TICKS) {
g_scheduler.div_diag_print = 0U;
set(1UL << TASK_DIAGNOSTIC_PRINT);
}
#endif
}
/**
+182 -42
View File
@@ -8,8 +8,11 @@
#include "device/watchdog.h"
#include "device/spi/app_spi_command.h"
#include "device/canfd.h"
#include "device/motor_state.h"
#include "device/motor_version.h"
#include "sys/config.h"
#include "sys/ota_boot_request.h"
#include "sys/motor_ota_relay.h"
#include "sys/task_manager.h"
#include "sys/task_runner.h"
#include "comm/ethercat_slave.h"
@@ -134,7 +137,7 @@ void TaskRunner::taskAdcProcess()
}
/**
* @brief 执行 CAN 周期发送任务:每 1ms 在每个使能通道上发送一帧 CANFD 测试帧。
* @brief 执行 CAN 周期发送任务:每 10ms 在每个使能通道上发送一帧 CANFD 测试帧。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
@@ -150,6 +153,7 @@ void TaskRunner::taskAdcProcess()
*/
void TaskRunner::taskCanTxSend()
{
#if (CONFIG_CAN_PERIODIC_TEST_TX_ENABLE != 0)
static uint32_t seq = 0U;
for (uint8_t ch = 0U; ch < 3U; ++ch) {
@@ -176,6 +180,11 @@ void TaskRunner::taskCanTxSend()
seq++;
}
}
#else
/* CONFIG_CAN_PERIODIC_TEST_TX_ENABLE=0:测试帧无业务接收节点 ACK,
* 单发模式下会持续产生 MTEn 错误帧,整任务空跑。 */
(void)0;
#endif
}
/**
@@ -199,6 +208,25 @@ void TaskRunner::taskCanRxParse()
OtaBootRequest::process();
return;
}
if(MotorOtaRelay::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len)) {
consumed++;
continue;
}
#if (CONFIG_MOTOR_STATE_ENABLE != 0)
MotorState::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len);
#endif
#if (CONFIG_MOTOR_VERSION_ENABLE != 0)
MotorVersion::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len);
#endif
consumed++;
}
@@ -221,6 +249,22 @@ void TaskRunner::taskHeartbeat()
canFeedbackStartRound();
/* ADC 调试反馈 1Hz:CONFIG_SPI_ADC_FEEDBACK_ENABLE=0 时为空操作 */
adcFeedbackStartRound();
#if (CONFIG_MOTOR_STATE_ENABLE != 0)
MotorState::checkOffline();
#endif
}
/**
* @brief 启动多电机状态 0xC5 推送轮次(250ms 独立节拍)。
* @note 与 taskHeartbeat 解耦:不连带加速 OTA Boot 轮询 / CAN 快照 / ADC 推送 /
* checkOffline 离线判定(3s 阈值)。仅触发本轮 P0/P1 分页推送,
* 实际 SPI 传输由 motorStatePushStep() 在主循环按铃协议推进。
*/
void TaskRunner::taskMotorStatePush()
{
#if (CONFIG_MOTOR_STATE_ENABLE != 0)
motorStateStartRound();
#endif
}
/**
@@ -238,60 +282,62 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out)
}
/**
* @brief 输出当前温度、电压与 CAN 诊断信息。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 由 5 秒节拍触发,通过 UART 打印当前温度、电压;采用定点整数格式,避免依赖浮点 `printf` 支持。
* @brief 5s 诊断任务:默认仅状态变化/错误增量;可选全量 UART 摘要。
* @note CONFIG_UART_TEMP_PERIODIC_PRINT=1 时打印 ADC/CAN/ECAT 全量行(调试用)。
* 默认=0:相对上次快照有 CAN 错误计数增量、ADC valid 跳变、ECAT ready 变化或 ISR 超阈时才打印。
* 采用定点整数格式,避免依赖浮点 printf。
*/
void TaskRunner::taskDiagnosticPrint()
{
const auto *diag = CanFd::diag();
const auto &s = s_runtime.sample;
const int32_t temp_x100 = static_cast<int32_t>(s.temperature * 100.0f);
const uint32_t vdda_mV = static_cast<uint32_t>(s.vdda * 1000.0f);
const uint32_t vin_adc_mV = static_cast<uint32_t>(s.vin_adc_v * 1000.0f);
#if (CONFIG_UART_TEMP_PERIODIC_PRINT != 0)
/* ---- Debug 全量摘要:周期刷 ADC / CAN 吞吐 / ECAT ISR 统计 ---- */
{
const int32_t temp_x100 = static_cast<int32_t>(s.temperature * 100.0f);
const uint32_t vdda_mV = static_cast<uint32_t>(s.vdda * 1000.0f);
const uint32_t vin_adc_mV = static_cast<uint32_t>(s.vin_adc_v * 1000.0f);
#if (CONFIG_UART_DIAG_LEVEL == 0)
printf("[APP] uptime=%lus valid=%u, temp_raw=%u, vref_raw=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V\r\n",
(unsigned long)g_app_uptime_sec,
static_cast<unsigned int>(s.valid),
static_cast<unsigned int>(s.temp_raw),
static_cast<unsigned int>(s.vref_raw),
static_cast<long>(temp_x100 / 100),
static_cast<long>((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100),
static_cast<unsigned long>(vdda_mV / 1000U),
static_cast<unsigned long>(vdda_mV % 1000U),
static_cast<unsigned long>(vin_adc_mV / 1000U),
static_cast<unsigned long>(vin_adc_mV % 1000U));
printf("[APP] uptime=%lus valid=%u, temp_raw=%u, vref_raw=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V\r\n",
(unsigned long)g_app_uptime_sec,
static_cast<unsigned int>(s.valid),
static_cast<unsigned int>(s.temp_raw),
static_cast<unsigned int>(s.vref_raw),
static_cast<long>(temp_x100 / 100),
static_cast<long>((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100),
static_cast<unsigned long>(vdda_mV / 1000U),
static_cast<unsigned long>(vdda_mV % 1000U),
static_cast<unsigned long>(vin_adc_mV / 1000U),
static_cast<unsigned long>(vin_adc_mV % 1000U));
#else
printf("[APP] uptime=%lus valid=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V, rx_ch=%u, rx_id=0x%08lX, rx_len=%u, tx_ch=%u, can_tx=%lu, can_rx=%lu, sw_drop=%lu, hw_full=%lu, hw_over=%lu, tx_abort=%lu, can_err=%lu\r\n",
(unsigned long)g_app_uptime_sec,
static_cast<unsigned int>(s.valid),
static_cast<long>(temp_x100 / 100),
static_cast<long>((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100),
static_cast<unsigned long>(vdda_mV / 1000U),
static_cast<unsigned long>(vdda_mV % 1000U),
static_cast<unsigned long>(vin_adc_mV / 1000U),
static_cast<unsigned long>(vin_adc_mV % 1000U),
static_cast<unsigned int>(s_runtime.last_rx_channel),
static_cast<unsigned long>(s_runtime.last_rx_id),
static_cast<unsigned int>(s_runtime.last_rx_len),
static_cast<unsigned int>(diag->last_tx_channel),
static_cast<unsigned long>(diag->tx_count),
static_cast<unsigned long>(diag->rx_count),
static_cast<unsigned long>(diag->rx_drop_count),
static_cast<unsigned long>(diag->rx_fifo_full_count),
static_cast<unsigned long>(diag->rx_fifo_overfull_count),
static_cast<unsigned long>(diag->tx_abort_count),
static_cast<unsigned long>(diag->error_count));
printf("[APP] uptime=%lus valid=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V, rx_ch=%u, rx_id=0x%08lX, rx_len=%u, tx_ch=%u, can_tx=%lu, can_rx=%lu, sw_drop=%lu, hw_full=%lu, hw_over=%lu, tx_abort=%lu, can_err=%lu\r\n",
(unsigned long)g_app_uptime_sec,
static_cast<unsigned int>(s.valid),
static_cast<long>(temp_x100 / 100),
static_cast<long>((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100),
static_cast<unsigned long>(vdda_mV / 1000U),
static_cast<unsigned long>(vdda_mV % 1000U),
static_cast<unsigned long>(vin_adc_mV / 1000U),
static_cast<unsigned long>(vin_adc_mV % 1000U),
static_cast<unsigned int>(s_runtime.last_rx_channel),
static_cast<unsigned long>(s_runtime.last_rx_id),
static_cast<unsigned int>(s_runtime.last_rx_len),
static_cast<unsigned int>(diag->last_tx_channel),
static_cast<unsigned long>(diag->tx_count),
static_cast<unsigned long>(diag->rx_count),
static_cast<unsigned long>(diag->rx_drop_count),
static_cast<unsigned long>(diag->rx_fifo_full_count),
static_cast<unsigned long>(diag->rx_fifo_overfull_count),
static_cast<unsigned long>(diag->tx_abort_count),
static_cast<unsigned long>(diag->error_count));
#endif
{
if (EthercatSlave::isEscReady()) {
uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U;
EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt);
/* 180MHz: cycles / 180 = µs;用 *1000/180 获得毫微秒精度避免浮点 */
/* 180MHz: cycles / 180 = µs;用 *1000/180 获得亚微秒精度避免浮点 */
const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U;
const uint32_t avgUs_x1000 = avgCyc * 1000U / 180U;
printf("[ECAT] periodicIsr cnt=%lu max=%lu.%03luus avg=%lu.%03luus\r\n",
@@ -303,6 +349,88 @@ void TaskRunner::taskDiagnosticPrint()
(unsigned long)EthercatSlave::getChipId());
}
}
#else
/* ---- 默认静默路径:仅错误增量 / 状态变化,无 uptime 心跳 ---- */
{
static uint32_t s_prev_rx_drop = 0U;
static uint32_t s_prev_rx_fifo_full = 0U;
static uint32_t s_prev_rx_fifo_overfull = 0U;
static uint32_t s_prev_tx_abort = 0U;
static uint32_t s_prev_error = 0U;
static uint8_t s_prev_adc_valid = 0xFFU; /* 0xFF=尚未采样 */
static bool s_prev_esc_ready = false;
static bool s_esc_ready_inited = false;
static uint32_t s_prev_ecat_max_cyc = 0U;
const uint32_t rx_drop = diag->rx_drop_count;
const uint32_t rx_full = diag->rx_fifo_full_count;
const uint32_t rx_over = diag->rx_fifo_overfull_count;
const uint32_t tx_abort = diag->tx_abort_count;
const uint32_t can_err = diag->error_count;
if (rx_drop > s_prev_rx_drop || rx_full > s_prev_rx_fifo_full
|| rx_over > s_prev_rx_fifo_overfull || tx_abort > s_prev_tx_abort
|| can_err > s_prev_error) {
printf("[CAN-DIAG] uptime=%lus err=%lu(+%lu) drop=%lu(+%lu) full=%lu(+%lu) over=%lu(+%lu) abort=%lu(+%lu)\r\n",
(unsigned long)g_app_uptime_sec,
(unsigned long)can_err,
(unsigned long)(can_err - s_prev_error),
(unsigned long)rx_drop,
(unsigned long)(rx_drop - s_prev_rx_drop),
(unsigned long)rx_full,
(unsigned long)(rx_full - s_prev_rx_fifo_full),
(unsigned long)rx_over,
(unsigned long)(rx_over - s_prev_rx_fifo_overfull),
(unsigned long)tx_abort,
(unsigned long)(tx_abort - s_prev_tx_abort));
s_prev_rx_drop = rx_drop;
s_prev_rx_fifo_full = rx_full;
s_prev_rx_fifo_overfull = rx_over;
s_prev_tx_abort = tx_abort;
s_prev_error = can_err;
}
if (s_prev_adc_valid == 0xFFU) {
s_prev_adc_valid = s.valid;
} else if (s.valid != s_prev_adc_valid) {
printf("[APP] adc valid %u -> %u (uptime=%lus)\r\n",
static_cast<unsigned int>(s_prev_adc_valid),
static_cast<unsigned int>(s.valid),
(unsigned long)g_app_uptime_sec);
s_prev_adc_valid = s.valid;
}
const bool esc_ready = EthercatSlave::isEscReady();
if (!s_esc_ready_inited) {
s_prev_esc_ready = esc_ready;
s_esc_ready_inited = true;
} else if (esc_ready != s_prev_esc_ready) {
printf("[ECAT] ESC ready %u -> %u chip_id=0x%08lX (uptime=%lus)\r\n",
static_cast<unsigned int>(s_prev_esc_ready ? 1U : 0U),
static_cast<unsigned int>(esc_ready ? 1U : 0U),
(unsigned long)EthercatSlave::getChipId(),
(unsigned long)g_app_uptime_sec);
s_prev_esc_ready = esc_ready;
}
if (esc_ready) {
uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U;
EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt);
/* 阈值单位 µs → CPU 周期(180 MHz) */
const uint32_t threshCyc = CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD * 180U;
if (maxCyc > threshCyc && maxCyc > s_prev_ecat_max_cyc) {
const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U;
printf("[ECAT] ISR max over threshold: max=%lu.%03luus thr=%luus cnt=%lu (uptime=%lus)\r\n",
(unsigned long)(maxUs_x1000 / 1000U),
(unsigned long)(maxUs_x1000 % 1000U),
(unsigned long)CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD,
(unsigned long)cnt,
(unsigned long)g_app_uptime_sec);
s_prev_ecat_max_cyc = maxCyc;
}
}
}
#endif
}
/**
@@ -359,12 +487,23 @@ void TaskRunner::runPending()
OtaBootRequest::process();
}
/* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。
* 非活跃时立即返回,开销可忽略。 */
MotorOtaRelay::process();
/* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔
* (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */
MotorOtaRelay::onTxTick();
/* CAN 反馈主动推送:GD32 APP 循环推送 3 通道 × 2 页反馈帧到 ESP32,
* 替代 ESP32 主动轮询。非阻塞:上一帧未读走时立即返回。
* 每次主循环迭代调用一次,ESP32 读走一帧后 CS 释放中断唤醒主循环继续推送。 */
canFeedbackPushStep();
/* 多电机状态(0xC5):优先级介于 CAN 反馈与 ADC 之间;ring 忙则跳过。 */
motorStatePushStep();
/* ADC 调试反馈(0xC4):优先级低于 CAN;ring 忙则跳过。可 config 关闭。 */
adcFeedbackPushStep();
/* 多电机版本(0xC6):事件驱动、低频,优先级最低;无 dirty 版本时立即返回。 */
motorVersionPushStep();
/* 周期路径( SYNC0 ISR preempt=0 )用 s_periodicSeq 序列号通知主循环,
* 不再走 task_flags,避免 BASEPRI 临界区与 preempt=0 的 RMW 竞态。
@@ -385,6 +524,7 @@ void TaskRunner::runPending()
if(pending & (1UL << TASK_CAN_RX_PARSE)) taskCanRxParse();
if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend();
if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat();
if(pending & (1UL << TASK_MOTOR_STATE_PUSH)) taskMotorStatePush();
if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint();
if(pending & (1UL << TASK_ETHERCAT_PROCESS)) taskEthercatProcess();
}
+3 -1
View File
@@ -46,10 +46,12 @@ private:
static void taskAdcProcess();
/** @brief 解析并消费接收帧。 */
static void taskCanRxParse();
/** @brief CAN 周期发送任务;每 1ms 每通道发送一帧测试报文。 */
/** @brief CAN 周期发送任务;每 10ms 每通道发送一帧测试报文。 */
static void taskCanTxSend();
/** @brief 心跳维护任务。 */
static void taskHeartbeat();
/** @brief 多电机状态 0xC5 主动推送启动(250ms 独立节拍)。 */
static void taskMotorStatePush();
/** @brief 诊断打印任务。 */
static void taskDiagnosticPrint();
/** @brief EtherCAT 周期/事件处理(非实时应用层 PDO 映射)。 */