feat(ota): 电机 OTA 支持 EtherCAT 模式(CoE 邮箱进料 + 通道独占锁)
进料口从「只有 ESP32 SPI 一条」扩成两条,中继引擎不变:主站经 CoE 0x2511 数据窗 把电机固件明文灌进 GD32 的 48KB SRAM 暂存区,再由 comm/ecat_motor_ota 泵进原有 4KB 环缓;AES 握手、CAN 单发停等、重发、0AAC 运行确认全部沿用 MotorOtaRelay。 纯网线(ESP32 WiFi 关断、/motor/ota 打不通)场合因此也能刷电机。 偏移/幂等判据仍只有一份:feedData 拆出 feedAt,两个进料口共用。 同时把审计报告 VULN-12/13/14 落地(此前只存在于文档,一行未实现):start() 第一条 语句在 PRIMASK 内原子置位 s_channel_lock_mask,EtherCAT 桥接 sendBatch/processOutput 与 SPI 0xC0/0xC7 对锁定通道回新码 0xE7=「被电机 OTA 独占」,不冒充「邮箱满」0xE4。 反向互斥一并补上:电机 OTA 活跃时 requestEnterBoot() 直接拒绝——SPI 0xB0、CoE 0x2502、 CAN 0x320 三条交接入口共用这一个判据,避免复位把刷写腰斩在 erase 之后。 完整性口径按链路分开,不互相冒充:块级 CRC32 + 整镜像 CRC32 都在从站核,且核过才 让电机进 BOOT——失败时电机仍跑旧 APP、擦除没开始,不存在「先擦再验」的变砖路径。 CRC 为 CRC-32/B(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射无末尾异或),与 v2_crc32 同算法(用公开向量 123456789=0x0376E6E7 对过)。 块长取 1024 而不是 BOOT 用的 4096:SSC 的分段下载缓冲是全工程唯一的 malloc 用户, 而 APP 的 __HEAP_SIZE 只有 3KB,4096 会被从站当场回 abort 0x05040005 Out of memory (BOOT 的堆是 16KB 才撑得住 4096——块长能抄,堆抄不了)。这是跨仓常量,须与 tools/ecat 的 MFO_CHUNK_OBJ 同步。 APP 1.7.4(RAM ZI 97156)。已实测:0x2510 参数写与一块 1016B 数据写均被从站接受 (staged=1016、phase bit4);端到端电机刷写待复测(native 块长修复后未跑完整一轮)。
This commit is contained in:
@@ -176,7 +176,7 @@ constexpr uint32_t CONFIG_CAN_QUIET_AFTER_SEC = 10U;
|
||||
* ============================================================ */
|
||||
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
|
||||
constexpr uint32_t APP_FW_VERSION_MINOR = 7U;
|
||||
constexpr uint32_t APP_FW_VERSION_PATCH = 2U; /* 1.7.2: CAN 通道收发器 Silent(只听不发)——S 脚 API + 0xC8 命令 + 0xC1 bus_state bit5;静默期发送入口回 0xE6 且不计错误 */
|
||||
constexpr uint32_t APP_FW_VERSION_PATCH = 4U; /* 1.7.4: 电机 OTA 邮箱进料块长 4096→1024——APP __HEAP_SIZE 只有 3KB,SSC 分段缓冲是唯一 malloc 用户,4096 会被从站回 abort 0x05040005 Out of memory(BOOT 堆 16KB 所以能用 4096) */
|
||||
constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
|
||||
|
||||
|
||||
|
||||
@@ -59,4 +59,19 @@
|
||||
*/
|
||||
#define RT_CRITICAL_RESTORE(val) __set_BASEPRI(val)
|
||||
|
||||
/**
|
||||
* @brief Preempt-0 专用临界区:PRIMASK 全局屏蔽,用于与 preempt=0 中断共享的
|
||||
* 单字标志(读-改-写)。
|
||||
* @note 严格限定 O(1) 状态操作(置位/清位/单字赋值)。**严禁**在其中调用
|
||||
* printf/LOG_*:串口入队实测 36us,TX FIFO 满时可达 123us,会把 EtherCAT
|
||||
* SYNC0 抖动顶出 DC 容差。日志一律放临界区外。
|
||||
*/
|
||||
#define RT_CRITICAL_PREEMPT0_SAVE() \
|
||||
({ \
|
||||
uint32_t __primask_save = __get_PRIMASK(); \
|
||||
__disable_irq(); \
|
||||
__primask_save; \
|
||||
})
|
||||
#define RT_CRITICAL_PREEMPT0_RESTORE(val) __set_PRIMASK(val)
|
||||
|
||||
#endif /* SYS_CRITICAL_H */
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/aes128.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/critical.h"
|
||||
#include "sys/systick.h"
|
||||
#include "sys/task_manager.h"
|
||||
#include "main.h"
|
||||
@@ -29,11 +30,23 @@ static const uint8_t K_MASTER[16] = {
|
||||
* 提到 4KB≈85 包(~420ms)给 WiFi/HTTP 抖动更大缓冲。 */
|
||||
static constexpr uint16_t kBufSize = 4096U;
|
||||
|
||||
/* ═══════════════════ 通道独占锁 ═══════════════════ */
|
||||
/* 位 0..2 = CAN 通道 0..2 正被 OTA 独占。置位于 start() 第一条语句(早于任何
|
||||
* 状态清零与串口日志),释位于 enterState() 进终态时。
|
||||
* EtherCAT 的 PDO→CAN 在 Sync0_Isr(preempt 0) 里发,OTA 的 START 可能来自主循环
|
||||
* 也可能来自 EXTI10_15(preempt 0),因此置位/判忙必须原子完成——否则两者之间
|
||||
* 的窗口(含 LOG 的百微秒级耗时)足够让一拍运动帧灌进正在升级的通道。
|
||||
* 同一时刻只跑一个会话,故写入用整字赋值/在 PRIMASK 里做单次 RMW,不做 RMW 于中断。 */
|
||||
static volatile uint32_t s_channel_lock_mask = 0U;
|
||||
|
||||
/* 本次会话的进料来源:环缓只认一路,交叉写入会拼出「一半 WiFi 一半 ECAT」的镜像。 */
|
||||
static FeedSource s_feed_src = FeedSource::kSpi;
|
||||
|
||||
/* ═══════════════════ 内部状态 ═══════════════════ */
|
||||
|
||||
static State s_state = State::kIdle;
|
||||
static volatile State s_state = State::kIdle;
|
||||
static uint8_t s_error = 0;
|
||||
static uint8_t s_channel = 0;
|
||||
static volatile uint8_t s_channel = 0;
|
||||
static uint32_t s_fw_size = 0;
|
||||
static uint32_t s_fw_fed = 0; /* 已接受的固件字节数(绝对流位置,防 0xD1 重传重复追加) */
|
||||
static uint16_t s_total_packets = 0;
|
||||
@@ -214,6 +227,12 @@ static void enterState(State st) {
|
||||
s_last_send_ms = 0;
|
||||
s_crc_resend_pending = false;
|
||||
|
||||
/* 通道锁释放:进终态(含 kIdle/kFinished/kError)即解锁,让 EtherCAT/SPI 恢复
|
||||
* 对该通道发帧。整字清零——同一时刻只可能有一个会话,无需 RMW。 */
|
||||
if (st == State::kIdle || st == State::kFinished || st == State::kError) {
|
||||
s_channel_lock_mask = 0U;
|
||||
}
|
||||
|
||||
/* auto_retrans 切换:OTA 是应用层停等协议,硬件自动重发会打穿电机单缓冲 RX
|
||||
* (重发的前一帧被当成新帧处理 → receive_packet_num 错位 → CRC 错)。
|
||||
* 进入 kHandshakeReq(start() 唯一入口)即关 ARD 切单发;
|
||||
@@ -1067,18 +1086,33 @@ bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
|
||||
return true; /* 已消费,不再传递给其他 handler */
|
||||
}
|
||||
|
||||
bool start(uint8_t can_channel, uint32_t fw_size) {
|
||||
if (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError) {
|
||||
return false; /* 已在进行中 */
|
||||
}
|
||||
bool start(uint8_t can_channel, uint32_t fw_size, FeedSource src, uint8_t node_id) {
|
||||
if (fw_size == 0 || fw_size > kMaxFwSize) return false;
|
||||
if (can_channel >= 3) return false;
|
||||
|
||||
s_channel = can_channel;
|
||||
s_fw_size = fw_size;
|
||||
s_fw_fed = 0U;
|
||||
s_total_packets = static_cast<uint16_t>((fw_size + kPacketPayload - 1U) / kPacketPayload);
|
||||
s_packets_acked = 0;
|
||||
/* ★ 第一条触碰共享状态的语句就完成「判忙 + 上锁」,且两者原子。
|
||||
* 下面的二十余项清零和 START 日志(UART 格式化,百微秒级)都在锁之后;
|
||||
* EtherCAT 的 PDO→CAN 由 preempt 0 的 Sync0_Isr 驱动,随时能插进这些空隙,
|
||||
* 只要漏一拍运动帧进这条通道,单发模式(ARD=1)的 0x101 固件包就被硬件 abort,
|
||||
* 电机会回 0x05/0x06 并在擦除后半路变砖。 */
|
||||
{
|
||||
const uint32_t save = RT_CRITICAL_PREEMPT0_SAVE();
|
||||
const bool busy = (s_channel_lock_mask != 0U) ||
|
||||
(s_state != State::kIdle && s_state != State::kFinished &&
|
||||
s_state != State::kError);
|
||||
if (!busy) {
|
||||
s_channel_lock_mask = (1U << can_channel);
|
||||
}
|
||||
RT_CRITICAL_PREEMPT0_RESTORE(save);
|
||||
if (busy) return false; /* 已有会话在跑(WiFi 或 EtherCAT,两者共用这一道门) */
|
||||
}
|
||||
|
||||
s_channel = can_channel;
|
||||
s_feed_src = src;
|
||||
s_fw_size = fw_size;
|
||||
s_fw_fed = 0U;
|
||||
s_total_packets = static_cast<uint16_t>((fw_size + kPacketPayload - 1U) / kPacketPayload);
|
||||
s_packets_acked = 0;
|
||||
s_error = 0;
|
||||
s_session_num = 0; /* 由 processNonceAck 根据 s_nonce 前4字节设置 */
|
||||
s_buf_head = 0;
|
||||
@@ -1093,81 +1127,102 @@ s_channel = can_channel;
|
||||
s_ack_resend_total = 0;
|
||||
s_fast_resend_total = 0;
|
||||
s_last_pad_valid = 0xFFFFU;
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算 SPI 喂数超时 */
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算喂数超时 */
|
||||
s_auto_retry = 0;
|
||||
|
||||
/* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。
|
||||
* 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */
|
||||
LOG_ERR("[MOTOR-OTA] START ch=%u fw=%lu total=%u\r\n",
|
||||
(unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets);
|
||||
LOG_ERR("[MOTOR-OTA] START ch=%u fw=%lu total=%u src=%s\r\n",
|
||||
(unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets,
|
||||
(src == FeedSource::kEcat) ? "ECAT" : "SPI");
|
||||
s_boot_heartbeat_seen = false;
|
||||
enterState(State::kPreCancel);
|
||||
|
||||
/* 进 BOOT 帧(0x000N0000, DLC=0)由本机在**已加锁**的通道上发:EtherCAT 链路必须
|
||||
* 走这里——那条链路的运动帧就是同一批 CAN 邮箱发出去的,先解锁再让上位机发帧
|
||||
* 等于把窗口留给运动帧。WiFi 链路保持 node_id=0(上位机先发帧的既有顺序),
|
||||
* 不动那条已验证的通路。 */
|
||||
if (node_id >= 1U && node_id <= 9U) {
|
||||
const uint32_t bootId = static_cast<uint32_t>(node_id) << 16;
|
||||
const bool sent = canSend(bootId, nullptr, 0U);
|
||||
LOG_ERR("[MOTOR-OTA] ENTER-BOOT id=0x%08lX ch=%u %s\r\n",
|
||||
(unsigned long)bootId, (unsigned)can_channel, sent ? "queued" : "retry-by-state");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool feedData(const uint8_t *data, uint16_t len) {
|
||||
/* 接受任何活跃状态的数据(包括握手期间)——让 ESP32 在 kPreCancel/握手
|
||||
* 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。
|
||||
*
|
||||
* payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。
|
||||
* ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加,
|
||||
* CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */
|
||||
if (s_state == State::kIdle || s_state == State::kError ||
|
||||
s_state == State::kFinished) {
|
||||
return false;
|
||||
}
|
||||
if (data == nullptr || len < 4U) {
|
||||
return false;
|
||||
}
|
||||
/* payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。
|
||||
* ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加,
|
||||
* CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */
|
||||
if (s_feed_src != FeedSource::kSpi) {
|
||||
/* 本次会话由 ECAT 暂存泵进料:SPI 侧(ESP32 还在线时)的喂数一律拒收,
|
||||
* 两路交叉写环缓会拼出「半份 WiFi、半份 EtherCAT」的镜像。 */
|
||||
return false;
|
||||
}
|
||||
if (data == nullptr || len < 4U) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t offset = static_cast<uint32_t>(data[0])
|
||||
| (static_cast<uint32_t>(data[1]) << 8)
|
||||
| (static_cast<uint32_t>(data[2]) << 16)
|
||||
| (static_cast<uint32_t>(data[3]) << 24);
|
||||
const uint8_t *chunk = data + 4U;
|
||||
uint16_t chunk_len = static_cast<uint16_t>(len - 4U);
|
||||
if (chunk_len == 0U) {
|
||||
return true;
|
||||
}
|
||||
if (s_fw_size == 0U) {
|
||||
return false;
|
||||
}
|
||||
if (offset >= s_fw_size) {
|
||||
return true; /* 越界尾:幂等 ACK */
|
||||
}
|
||||
/* 截到固件末尾 */
|
||||
if (offset + chunk_len > s_fw_size) {
|
||||
chunk_len = static_cast<uint16_t>(s_fw_size - offset);
|
||||
}
|
||||
const uint32_t offset = static_cast<uint32_t>(data[0])
|
||||
| (static_cast<uint32_t>(data[1]) << 8)
|
||||
| (static_cast<uint32_t>(data[2]) << 16)
|
||||
| (static_cast<uint32_t>(data[3]) << 24);
|
||||
return feedAt(offset, data + 4U, static_cast<uint16_t>(len - 4U));
|
||||
}
|
||||
|
||||
if (offset + chunk_len <= s_fw_fed) {
|
||||
/* 完全已收过:幂等 ACK,不写环缓 */
|
||||
return true;
|
||||
}
|
||||
if (offset > s_fw_fed) {
|
||||
/* 缺口:拒收,ESP32 按 s_fw_offset 重发正确位置 */
|
||||
static uint32_t s_last_gap_ms = 0;
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
if (now - s_last_gap_ms >= 200U) {
|
||||
LOG_ERR("[MOTOR-OTA] feed gap off=%lu fed=%lu len=%u\r\n",
|
||||
(unsigned long)offset, (unsigned long)s_fw_fed,
|
||||
(unsigned)chunk_len);
|
||||
s_last_gap_ms = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool feedAt(uint32_t offset, const uint8_t *chunk, uint16_t chunk_len) {
|
||||
/* 接受任何活跃状态的数据(包括握手期间)——让上位机在 kPreCancel/握手
|
||||
* 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。 */
|
||||
if (s_state == State::kIdle || s_state == State::kError ||
|
||||
s_state == State::kFinished) {
|
||||
return false;
|
||||
}
|
||||
if (chunk == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (chunk_len == 0U) {
|
||||
return true;
|
||||
}
|
||||
if (s_fw_size == 0U) {
|
||||
return false;
|
||||
}
|
||||
if (offset >= s_fw_size) {
|
||||
return true; /* 越界尾:幂等 ACK */
|
||||
}
|
||||
/* 截到固件末尾 */
|
||||
if (offset + chunk_len > s_fw_size) {
|
||||
chunk_len = static_cast<uint16_t>(s_fw_size - offset);
|
||||
}
|
||||
|
||||
/* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */
|
||||
const uint32_t skip = s_fw_fed - offset;
|
||||
const uint16_t add_len = static_cast<uint16_t>(chunk_len - skip);
|
||||
if (bufFree() < add_len) {
|
||||
return false; /* 环缓满 → SPI WAIT */
|
||||
}
|
||||
bufPush(chunk + skip, add_len);
|
||||
s_fw_fed += add_len;
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* SPI 断流检测:记录有效喂数时刻 */
|
||||
return true;
|
||||
}
|
||||
if (offset + chunk_len <= s_fw_fed) {
|
||||
/* 完全已收过:幂等 ACK,不写环缓 */
|
||||
return true;
|
||||
}
|
||||
if (offset > s_fw_fed) {
|
||||
/* 缺口:拒收,上位机按 s_fw_fed 重发正确位置 */
|
||||
static uint32_t s_last_gap_ms = 0;
|
||||
const uint32_t now = SysTickTimer::getMs();
|
||||
if (now - s_last_gap_ms >= 200U) {
|
||||
LOG_ERR("[MOTOR-OTA] feed gap off=%lu fed=%lu len=%u\r\n",
|
||||
(unsigned long)offset, (unsigned long)s_fw_fed,
|
||||
(unsigned)chunk_len);
|
||||
s_last_gap_ms = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */
|
||||
const uint32_t skip = s_fw_fed - offset;
|
||||
const uint16_t add_len = static_cast<uint16_t>(chunk_len - skip);
|
||||
if (bufFree() < add_len) {
|
||||
return false; /* 环缓满 → SPI WAIT / ECAT 下拍再泵 */
|
||||
}
|
||||
bufPush(chunk + skip, add_len);
|
||||
s_fw_fed += add_len;
|
||||
s_last_feed_ms = SysTickTimer::getMs(); /* 喂数断流检测:记录有效喂数时刻 */
|
||||
return true;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (s_state == State::kIdle) return;
|
||||
@@ -1198,11 +1253,32 @@ Status getStatus() {
|
||||
st.packets_acked = s_packets_acked;
|
||||
st.total_packets = s_total_packets;
|
||||
st.fw_size = s_fw_size;
|
||||
st.fw_fed = s_fw_fed;
|
||||
return st;
|
||||
}
|
||||
|
||||
uint16_t feedFreeSpace() {
|
||||
return bufFree();
|
||||
}
|
||||
|
||||
bool isActive() {
|
||||
return (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError);
|
||||
}
|
||||
|
||||
bool isChannelLocked(uint8_t channel) {
|
||||
if (channel >= 3U) return false;
|
||||
/* 单次 32 位读:位与本身就是原子的,不需要临界区。
|
||||
* 判据只看锁位,不看状态机——状态机那几个变量是在锁之后逐步赋的,
|
||||
* 拿它当门会让出几十微秒的空窗(审计报告里的 88.53% 漏包就是这么来的)。 */
|
||||
return ((s_channel_lock_mask & (1U << channel)) != 0U);
|
||||
}
|
||||
|
||||
int8_t lockedChannel() {
|
||||
const uint32_t mask = s_channel_lock_mask;
|
||||
for (uint8_t ch = 0U; ch < 3U; ++ch) {
|
||||
if ((mask & (1U << ch)) != 0U) return static_cast<int8_t>(ch);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} /* namespace MotorOtaRelay */
|
||||
|
||||
@@ -51,6 +51,16 @@ struct Status {
|
||||
uint16_t packets_acked;
|
||||
uint16_t total_packets;
|
||||
uint32_t fw_size;
|
||||
/** 已被环缓接受的明文字节数(流位置)。ECAT 暂存泵用它对账:
|
||||
* 电机侧自动重试会把环缓与这个位置一起清零,泵必须跟着回退。
|
||||
* 注意:只是内部 C++ 结构,SPI 0xD0 QUERY 的线格式没有改动。 */
|
||||
uint32_t fw_fed;
|
||||
};
|
||||
|
||||
/** 固件明文进料来源。同一时刻只允许一个来源,环缓不接受两路交叉写入。 */
|
||||
enum class FeedSource : uint8_t {
|
||||
kSpi = 0U, /* ESP32 经 SPI 0xD1 喂(WiFi/BLE 链路的既有通路) */
|
||||
kEcat = 1U, /* EtherCAT 主站经 CoE 0x2511 灌进暂存区,再由 ecat_motor_ota 泵入环缓 */
|
||||
};
|
||||
|
||||
/* 初始化(注册 CAN RX 回调等) */
|
||||
@@ -67,19 +77,51 @@ void init();
|
||||
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 启动一次电机 OTA。
|
||||
* @param can_channel 目标 CAN 通道 0..2。
|
||||
* @param fw_size 固件明文字节数。
|
||||
* @param src 进料来源(SPI / ECAT 暂存泵)。
|
||||
* @param node_id 1..9:由 GD32 自己在**已加锁**的通道上发 0x000N0000 进 BOOT 帧;
|
||||
* 0:不进 BOOT(WiFi 链路沿用上位机先发帧的老顺序)。
|
||||
* @note 入口第一条语句即置通道锁,早于任何状态清零与串口日志。
|
||||
*/
|
||||
bool start(uint8_t can_channel, uint32_t fw_size,
|
||||
FeedSource src = FeedSource::kSpi, uint8_t node_id = 0U);
|
||||
/**
|
||||
* @brief 喂入固件明文。payload 布局:[offset:u32 LE][data...]
|
||||
* offset 为固件绝对字节偏移;重复/重叠区间幂等 ACK,缺口拒绝(→SPI WAIT)。
|
||||
* 无 offset 的旧纯流会在 SPI 超时重传时把同一 chunk 追加进环缓(BLF 见前 236B 循环)。
|
||||
*/
|
||||
bool feedData(const uint8_t *data, uint16_t len);
|
||||
/**
|
||||
* @brief 已解析过的进料写入(feedData 的核心,偏移/幂等/环缓满判据只有一份)。
|
||||
* @note 供 ECAT 暂存泵调用:那边已按 offset 寻址,无需再拼 4 字节头,
|
||||
* 也避免在仅 512B 的主栈上放临时头缓冲。
|
||||
*/
|
||||
bool feedAt(uint32_t offset, const uint8_t *chunk, uint16_t chunk_len);
|
||||
/**
|
||||
* @brief 环缓当前还能接受多少明文字节。
|
||||
* @note 给进料方按空位切片用(否则一次投过大永远排不进队首)。不改变任何判据:
|
||||
* feedAt 内部仍会自己再查一次空位。
|
||||
*/
|
||||
uint16_t feedFreeSpace(void);
|
||||
void cancel();
|
||||
|
||||
/* 状态查询 */
|
||||
Status getStatus();
|
||||
bool isActive();
|
||||
|
||||
/**
|
||||
* @brief 指定 CAN 通道当前是否被电机 OTA 独占。
|
||||
* @note 单条 32 位位与,可在 preempt=0(EXTI0/SYNC0、EXTI10_15)里直接调用。
|
||||
* EtherCAT 桥接与 SPI 桥接/突发在发送前必须查它,否则周期运动帧会
|
||||
* abort 掉 OTA 的 0x101 包(单发模式 ARD=1 不重传),电机会回 0x05/0x06。
|
||||
*/
|
||||
bool isChannelLocked(uint8_t channel);
|
||||
/** @brief 当前被锁定的通道号;-1 表示无锁。 */
|
||||
int8_t lockedChannel();
|
||||
|
||||
} /* namespace MotorOtaRelay */
|
||||
|
||||
#endif /* SYS_MOTOR_OTA_RELAY_H */
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
#include "gd32e50x.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/systick.h"
|
||||
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h),本文件原先没有 stdio */
|
||||
|
||||
namespace {
|
||||
/** Bootloader OTA 配置页地址与 word 布局:唯一真源 shared/ota_layout.h。 */
|
||||
@@ -151,6 +153,16 @@ void OtaBootRequest::maybeClearCrashCount(uint32_t uptime_sec)
|
||||
*/
|
||||
bool OtaBootRequest::requestEnterBoot()
|
||||
{
|
||||
/* 电机 OTA 正在跑(WiFi 与 EtherCAT 两条进料路都算)时绝不交回 BOOT:
|
||||
* 复位会把电机的刷写腰斩在 erase 之后——旧 APP 已抹、新 APP 没收完,直接变砖。
|
||||
* 判据只放这一个执行点:SPI 0xB0、EtherCAT CoE 0x2502、CAN 0x320 三条入口
|
||||
* 最终都要过这里,不在调用方各抄一份。 */
|
||||
if (MotorOtaRelay::isActive()) {
|
||||
LOG_ERR("[OTA-BOOT] enter-boot refused: motor OTA active ch=%d\r\n",
|
||||
(int)MotorOtaRelay::lockedChannel());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
ok = otaConfigSet(kAppLatestCheckAddr, kOtaAval) && ok;
|
||||
ok = otaConfigSet(kAppLatestAvalAddr, kOtaAval) && ok;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "comm/ethercat_slave.h"
|
||||
#include "comm/ethercat_sync.h"
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "comm/ecat_motor_ota.h"
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
#include "comm/esc_lan9252.h"
|
||||
|
||||
@@ -171,6 +172,9 @@ void TaskRunner::taskCanTxSend()
|
||||
if (!CanFd::isChannelEnabled(ch)) {
|
||||
continue;
|
||||
}
|
||||
if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
continue; /* 电机 OTA 独占:诊断帧也不许挤这条通道 */
|
||||
}
|
||||
|
||||
uint8_t data[64];
|
||||
data[0] = 0xC0U;
|
||||
@@ -591,10 +595,14 @@ void TaskRunner::runPending()
|
||||
* 非活跃时立即返回,开销可忽略。 */
|
||||
/* 负载率挂钩:relay 活跃时才计 busy——isActive() 是廉价枚举判断,
|
||||
* 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */
|
||||
const bool motorOtaActive = MotorOtaRelay::isActive();
|
||||
const bool motorOtaActive = MotorOtaRelay::isActive() || EcatMotorOta::sessionActive();
|
||||
if (motorOtaActive) {
|
||||
CpuLoad::beginWork();
|
||||
}
|
||||
/* EtherCAT 邮箱进料(comm/ecat_motor_ota.cpp):把本地 48KB 暂存区按环缓空位
|
||||
* 泵进 relay。放在 relay::process() 之前,同圈刚泵进去的字节立刻能被弹包。
|
||||
* WiFi/SPI 链路下 s_started=false,这里第一次判断就返回,开销可忽略。 */
|
||||
EcatMotorOta::process();
|
||||
MotorOtaRelay::process();
|
||||
/* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔
|
||||
* (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */
|
||||
|
||||
Reference in New Issue
Block a user