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:
@@ -87,6 +87,9 @@ project:
|
||||
- file: ./comm/ethercat_sync.cpp
|
||||
- file: ./comm/ethercat_can_bridge.h
|
||||
- file: ./comm/ethercat_can_bridge.cpp
|
||||
# 电机 OTA 的 EtherCAT 邮箱进料通路(CoE 0x2510/0x2511 → MotorOtaRelay 环缓)
|
||||
- file: ./comm/ecat_motor_ota.h
|
||||
- file: ./comm/ecat_motor_ota.cpp
|
||||
- file: ./comm/ethercat_pdo.h
|
||||
- file: ./comm/ethercat_pdo.cpp
|
||||
# [DIFF-STATS] 临时仪表:差分写取证,删除时连同本两行与 comm/ethercat_diff_stats.* 一并移除
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* @file ecat_motor_ota.cpp
|
||||
* @brief EtherCAT 邮箱进料通路实现(职责与时序前提见 ecat_motor_ota.h)。
|
||||
*
|
||||
* 上下文:三个 SDO 钩子只在 MainLoop(ECAT_Application 调用链)里被调,泵 process()
|
||||
* 也在主循环,因此 48KB 暂存区没有并发写者。真正的硬保证仍是通道锁:即便主站循环
|
||||
* 被别的路径(页面自愈 / 守护重连)拉回来,运动帧也进不了正在升级的通道。
|
||||
*/
|
||||
#include "comm/ecat_motor_ota.h"
|
||||
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include "sys/systick.h"
|
||||
|
||||
#include "spi_link_v2.h" /* v2_crc32:CRC32 MPEG-2(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射、无末尾异或) */
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h) */
|
||||
|
||||
namespace {
|
||||
|
||||
/*============================================================================
|
||||
* 尺寸常量与守卫
|
||||
*==========================================================================*/
|
||||
constexpr uint32_t kChunkObjBytes = ECAT_MOTA_CHUNK_OBJ_BYTES;
|
||||
constexpr uint32_t kChunkHdrBytes = ECAT_MOTA_CHUNK_HDR_BYTES;
|
||||
constexpr uint32_t kImgMaxBytes = ECAT_MOTA_IMG_MAX_BYTES;
|
||||
constexpr uint8_t kCtlSubs = ECAT_MOTA_CTL_SUBS;
|
||||
|
||||
static_assert(kImgMaxBytes == MotorOtaRelay::kMaxFwSize,
|
||||
"暂存区上限必须与 MotorOtaRelay::kMaxFwSize 一致");
|
||||
static_assert(kChunkObjBytes * 8U <= 0xFFFFU,
|
||||
"0x2511 块长超出 SDO 单条目 BitSize(UINT16)上限");
|
||||
static_assert(kChunkObjBytes > kChunkHdrBytes, "数据窗要留出净载荷");
|
||||
|
||||
/** APP 的堆容量(RTE/Device/GD32E508VE/regions_GD32E508VE.h 的 __HEAP_SIZE)。
|
||||
* SSC 每笔分段下载都要 malloc(块长)(sdoserv.c:751),堆不够就从站直接回
|
||||
* abort 0x05040005「Out of memory」——2026-10-11 就是拿 4096 撞出来的。
|
||||
* 这里留 3 倍余量:谁要把块长往上抬,先抬 __HEAP_SIZE 再来改这一行。 */
|
||||
constexpr uint32_t kHeapBytes = 3072U;
|
||||
static_assert(kChunkObjBytes * 3U <= kHeapBytes,
|
||||
"块长受 APP 堆限制:malloc(块长) 必须留足余量,否则从站 OOM 拒收");
|
||||
|
||||
/** 单次 process() 最多搬运的字节数:把一圈的占用钳住,别在 MainLoop 里长时间拷 48KB。 */
|
||||
constexpr uint16_t kPumpSlice = 512U;
|
||||
|
||||
/*============================================================================
|
||||
* 会话状态
|
||||
*==========================================================================*/
|
||||
uint32_t s_cmd = 0U; /**< sub1: 最近一次执行的命令 */
|
||||
uint32_t s_total = 0U; /**< sub2: 声明的镜像总字节数 */
|
||||
uint32_t s_channel = 0U; /**< sub3: 目标 CAN 通道 */
|
||||
uint32_t s_node = 0U; /**< sub4: 目标节点 1..9 */
|
||||
uint32_t s_img_crc = 0U; /**< sub5: 整镜像期望 CRC32 */
|
||||
|
||||
uint32_t s_staged = 0U; /**< 已连续暂存字节数(水位,写块时前移) */
|
||||
uint8_t s_sess_err = ECAT_MOTA_OK;
|
||||
|
||||
bool s_armed = false; /**< 已声明总长,可收块 */
|
||||
bool s_started = false; /**< 已交给 relay(暂存区冻结,只剩泵) */
|
||||
bool s_crc_ok = false; /**< 整镜像 CRC 已核对通过 */
|
||||
bool s_last_chunk_ok = false;
|
||||
bool s_end_logged = false;/**< 终态日志只打一次 */
|
||||
|
||||
/* 终态锁存:relay 会话结束后可能很快回到 kIdle(主站下一拍才来读),
|
||||
* 不做锁存就会把「刷成功了」读成 kIdle,主站据此报的结论就成了猜的。 */
|
||||
bool s_final_valid = false;
|
||||
uint8_t s_final_state = 0U;
|
||||
uint8_t s_final_err = 0U;
|
||||
|
||||
uint32_t s_pump_pos = 0U; /**< 泵在暂存区里的读游标 */
|
||||
|
||||
/** 48KB 暂存区(.bss:启动零耗时;APP 当前 RAM 用量 35KB/128KB,放得下)。 */
|
||||
uint8_t s_img[kImgMaxBytes];
|
||||
|
||||
inline uint32_t loadU32(const uint8_t *p) {
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
void resetSession()
|
||||
{
|
||||
s_armed = false;
|
||||
s_started = false;
|
||||
s_crc_ok = false;
|
||||
s_staged = 0U;
|
||||
s_pump_pos = 0U;
|
||||
s_total = 0U;
|
||||
s_last_chunk_ok = false;
|
||||
s_end_logged = false;
|
||||
s_final_valid = false;
|
||||
s_final_state = 0U;
|
||||
s_final_err = 0U;
|
||||
}
|
||||
|
||||
/** 只认「已连续暂存水位之内」的偏移:留洞会让泵卡住,而且说明主站发料顺序错了。 */
|
||||
uint8_t acceptChunk(uint32_t offset, uint32_t crc, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
if (!s_armed || s_total == 0U) {
|
||||
s_sess_err = ECAT_MOTA_E_NOTARMED;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (s_started) {
|
||||
s_sess_err = ECAT_MOTA_E_STARTED;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (offset > s_staged || offset + len > s_total) {
|
||||
s_sess_err = ECAT_MOTA_E_GAP;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (len > 0U) {
|
||||
/* 块级校验:EtherCAT 数据报**头**有 CRC,载荷没有;邮箱分段拼装还是软件路径。
|
||||
* 这里挡一次,坏块就地重发,不会等到电机回 0x06 才发现(那时 flash 已擦)。 */
|
||||
if (v2_crc32(data, len) != crc) {
|
||||
s_sess_err = ECAT_MOTA_E_CHUNKCRC;
|
||||
s_last_chunk_ok = false;
|
||||
return s_sess_err;
|
||||
}
|
||||
std::memcpy(&s_img[offset], data, len);
|
||||
if (offset + len > s_staged) {
|
||||
s_staged = offset + len;
|
||||
}
|
||||
}
|
||||
s_last_chunk_ok = true;
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
return ECAT_MOTA_OK;
|
||||
}
|
||||
|
||||
/** 整镜像核对 + 交给 relay。所有失败路径都必须在电机进 BOOT **之前**。 */
|
||||
bool beginTransfer()
|
||||
{
|
||||
if (s_total == 0U || s_total > kImgMaxBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return false;
|
||||
}
|
||||
if (s_staged != s_total) {
|
||||
s_sess_err = ECAT_MOTA_E_GAP;
|
||||
return false;
|
||||
}
|
||||
if (s_channel >= 3U || s_node < 1U || s_node > 9U) {
|
||||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||||
return false;
|
||||
}
|
||||
/* DCU 自己正在回 BOOT(配置页写 + 复位在路上)时绝不能起电机会话:
|
||||
* 复位会把半程的刷写腰斩在 erase 之后。 */
|
||||
if (OtaBootRequest::isRebootPending()) {
|
||||
s_sess_err = ECAT_MOTA_E_BOOTPEND;
|
||||
return false;
|
||||
}
|
||||
/* 整镜像 CRC 先行:这一刻电机还在 APP 正常跑、擦除没开始,校验不过就是零代价失败。
|
||||
* 反过来「先进 BOOT 再验」才是变砖路径。 */
|
||||
if (v2_crc32(s_img, s_total) != s_img_crc) {
|
||||
s_sess_err = ECAT_MOTA_E_IMGCRC;
|
||||
return false;
|
||||
}
|
||||
s_crc_ok = true;
|
||||
|
||||
if (!MotorOtaRelay::start(static_cast<uint8_t>(s_channel), s_total,
|
||||
MotorOtaRelay::FeedSource::kEcat,
|
||||
static_cast<uint8_t>(s_node))) {
|
||||
s_sess_err = ECAT_MOTA_E_BUSY;
|
||||
s_crc_ok = false;
|
||||
return false;
|
||||
}
|
||||
s_started = true;
|
||||
s_end_logged = false;
|
||||
s_pump_pos = 0U;
|
||||
s_final_valid = false;
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
LOG_ERR("[ECAT-MOTA] start ch=%lu node=%lu size=%lu crc=%08lX\r\n",
|
||||
(unsigned long)s_channel, (unsigned long)s_node,
|
||||
(unsigned long)s_total, (unsigned long)s_img_crc);
|
||||
return true;
|
||||
}
|
||||
|
||||
} /* namespace */
|
||||
|
||||
/*============================================================================
|
||||
* EcatMotorOta(C++ 侧:主循环钩子与查询)
|
||||
*==========================================================================*/
|
||||
namespace EcatMotorOta {
|
||||
|
||||
void process()
|
||||
{
|
||||
if (!s_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
|
||||
|
||||
/* 电机侧自动重试会清环缓并把 fw_fed 归零:泵必须跟着回退,否则会把
|
||||
* 「后半段」当成已喂过,喂出一个前段缺失的镜像。 */
|
||||
if (st.fw_fed < s_pump_pos) {
|
||||
s_pump_pos = st.fw_fed;
|
||||
}
|
||||
|
||||
/* 泵:按环缓空位切片,搬不动就下一圈再来(不阻塞、不忙等、不碰 CAN)。 */
|
||||
while (s_pump_pos < s_total) {
|
||||
const uint16_t freeSpace = MotorOtaRelay::feedFreeSpace();
|
||||
if (freeSpace == 0U) {
|
||||
break;
|
||||
}
|
||||
const uint32_t remain = s_total - s_pump_pos;
|
||||
uint16_t n = static_cast<uint16_t>(remain < kPumpSlice ? remain : kPumpSlice);
|
||||
if (n > freeSpace) {
|
||||
n = freeSpace;
|
||||
}
|
||||
if (!MotorOtaRelay::feedAt(s_pump_pos, &s_img[s_pump_pos], n)) {
|
||||
break;
|
||||
}
|
||||
s_pump_pos += n;
|
||||
}
|
||||
|
||||
if (!MotorOtaRelay::isActive()) {
|
||||
if (!s_end_logged) {
|
||||
s_end_logged = true;
|
||||
s_final_valid = true;
|
||||
s_final_state = static_cast<uint8_t>(st.state);
|
||||
s_final_err = st.error_code;
|
||||
LOG_ERR("[ECAT-MOTA] end state=%u err=0x%02X acked=%u/%u fed=%lu/%lu\r\n",
|
||||
static_cast<unsigned>(st.state), static_cast<unsigned>(st.error_code),
|
||||
static_cast<unsigned>(st.packets_acked),
|
||||
static_cast<unsigned>(st.total_packets),
|
||||
(unsigned long)st.fw_fed, (unsigned long)st.fw_size);
|
||||
}
|
||||
s_started = false;
|
||||
s_crc_ok = false;
|
||||
/* 会话已结束:不再收块。主站要开下一轮必须重新写 0x2510:02 声明总长,
|
||||
* 那一步复位全部会话状态(包括上面的终态锁存)。 */
|
||||
s_armed = false;
|
||||
s_last_chunk_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool sessionActive()
|
||||
{
|
||||
return s_started;
|
||||
}
|
||||
|
||||
} /* namespace EcatMotorOta */
|
||||
|
||||
/*============================================================================
|
||||
* SDO 钩子(extern "C":dc_app.c 的对象表直接引用)
|
||||
*==========================================================================*/
|
||||
extern "C" {
|
||||
|
||||
uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes)
|
||||
{
|
||||
if (bytes == nullptr) {
|
||||
return ECAT_MOTA_E_BADLEN;
|
||||
}
|
||||
if (dataSize < kChunkHdrBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (dataSize > kChunkObjBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return s_sess_err;
|
||||
}
|
||||
|
||||
const uint32_t offset = loadU32(&bytes[0]);
|
||||
const uint32_t crc = loadU32(&bytes[4]);
|
||||
const uint32_t len = dataSize - kChunkHdrBytes;
|
||||
|
||||
const uint8_t rc = acceptChunk(offset, crc, &bytes[kChunkHdrBytes], len);
|
||||
if (rc != ECAT_MOTA_OK) {
|
||||
/* 一次失败只打一行:主站会重发同一块,串口刷屏会拖慢 MainLoop。 */
|
||||
LOG_ERR("[ECAT-MOTA] chunk reject off=%lu len=%lu err=0x%02X\r\n",
|
||||
(unsigned long)offset, (unsigned long)len, (unsigned)rc);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize, const uint8_t *bytes)
|
||||
{
|
||||
if (bytes == nullptr || dataSize < 4U) {
|
||||
return ECAT_MOTA_E_BADLEN;
|
||||
}
|
||||
const uint32_t v = loadU32(bytes);
|
||||
|
||||
switch (subindex) {
|
||||
case 1U: {
|
||||
/* sub1 cmd:1 = 校验并启动,3 = 中止会话 */
|
||||
s_cmd = v;
|
||||
if (v == 1U) {
|
||||
if (!beginTransfer()) {
|
||||
return s_sess_err;
|
||||
}
|
||||
} else if (v == 3U) {
|
||||
if (s_started) {
|
||||
MotorOtaRelay::cancel();
|
||||
LOG_ERR("[ECAT-MOTA] cancel by master\r\n");
|
||||
}
|
||||
resetSession();
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
} else {
|
||||
return ECAT_MOTA_E_PARAM;
|
||||
}
|
||||
return ECAT_MOTA_OK;
|
||||
}
|
||||
case 2U: {
|
||||
/* sub2 总长:声明即复位会话(主站重开一轮不需要先 abort) */
|
||||
if (v == 0U || v > kImgMaxBytes) {
|
||||
s_sess_err = ECAT_MOTA_E_BADLEN;
|
||||
return s_sess_err;
|
||||
}
|
||||
if (s_started) {
|
||||
s_sess_err = ECAT_MOTA_E_STARTED;
|
||||
return s_sess_err;
|
||||
}
|
||||
resetSession();
|
||||
s_total = v;
|
||||
s_armed = true;
|
||||
s_sess_err = ECAT_MOTA_OK;
|
||||
return ECAT_MOTA_OK;
|
||||
}
|
||||
case 3U:
|
||||
if (v >= 3U) {
|
||||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||||
return s_sess_err;
|
||||
}
|
||||
s_channel = v;
|
||||
return ECAT_MOTA_OK;
|
||||
case 4U:
|
||||
if (v < 1U || v > 9U) {
|
||||
s_sess_err = ECAT_MOTA_E_PARAM;
|
||||
return s_sess_err;
|
||||
}
|
||||
s_node = v;
|
||||
return ECAT_MOTA_OK;
|
||||
case 5U:
|
||||
s_img_crc = v;
|
||||
return ECAT_MOTA_OK;
|
||||
default:
|
||||
/* sub0 是条目数;sub6..sub11 只读 */
|
||||
return ECAT_MOTA_E_PARAM;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t EcatMotorOtaCtlRead(uint8_t subindex)
|
||||
{
|
||||
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
|
||||
/* live = 会话还在跑,或从没跑过(那就照实反映 relay 当前状态);
|
||||
* 会话刚结束时返回锁存值,避免主站把「成功」读成 kIdle。 */
|
||||
const bool live = (s_started || !s_final_valid);
|
||||
|
||||
switch (subindex) {
|
||||
case 0U: return kCtlSubs;
|
||||
case 1U: return s_cmd;
|
||||
case 2U: return s_total;
|
||||
case 3U: return s_channel;
|
||||
case 4U: return s_node;
|
||||
case 5U: return s_img_crc;
|
||||
case 6U: return s_staged;
|
||||
case 7U: return live ? static_cast<uint32_t>(st.state)
|
||||
: static_cast<uint32_t>(s_final_state);
|
||||
case 8U: return st.packets_acked;
|
||||
case 9U: return st.total_packets;
|
||||
case 10U: return (static_cast<uint32_t>(s_sess_err) << 8)
|
||||
| (live ? static_cast<uint32_t>(st.error_code)
|
||||
: static_cast<uint32_t>(s_final_err));
|
||||
case 11U: {
|
||||
/* phase 位图:只报能被板子真实触发的事实,不做任何推测性状态。
|
||||
* bit0 已声明总长 · bit1 暂存齐 · bit2 整镜像 CRC 过 · bit3 会话在跑
|
||||
* bit4 上一块被接受 · bit5 本会话目标通道当前持锁 · bit6 relay 活跃 */
|
||||
uint32_t phase = 0U;
|
||||
if (s_armed) { phase |= 0x0001U; }
|
||||
if (s_total != 0U && s_staged == s_total) { phase |= 0x0002U; }
|
||||
if (s_crc_ok) { phase |= 0x0004U; }
|
||||
if (s_started) { phase |= 0x0008U; }
|
||||
if (s_last_chunk_ok) { phase |= 0x0010U; }
|
||||
if (MotorOtaRelay::isChannelLocked(static_cast<uint8_t>(s_channel))) { phase |= 0x0020U; }
|
||||
if (MotorOtaRelay::isActive()) { phase |= 0x0040U; }
|
||||
return phase;
|
||||
}
|
||||
default: return 0U;
|
||||
}
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @file ecat_motor_ota.h
|
||||
* @brief EtherCAT(CoE 邮箱)电机 OTA 进料通路 —— 主站把电机固件灌进 DCU 本地暂存区,
|
||||
* 再由本模块泵进 MotorOtaRelay 现有的 4KB 环缓。
|
||||
*
|
||||
* 为什么要有它(不是重复造轮子):电机 OTA 的引擎一直是 MotorOtaRelay
|
||||
* (AES 握手 + CAN 停等 + 单发模式),只有一条进料路:ESP32 经 SPI 0xD1 喂。
|
||||
* 纯 EtherCAT 场合(只插网线、ESP32 WiFi 关断)那条 HTTP 打不通,
|
||||
* 于是前端把能力标成 supportsOta=false。本模块只换**进料口**,不换引擎:
|
||||
* - 分块写入仍走 MotorOtaRelay::feedAt(),offset/幂等/环缓满的判据只有一份;
|
||||
* - 加密、停等、重发、终态判定全部沿用原状态机;
|
||||
* - 通道独占锁同样沿用(EtherCAT 自己的 PDO 运动帧也被它挡在 OTA 之外)。
|
||||
*
|
||||
* 与 BOOT 的 0x2500/0x2501(刷 DCU 自身)的区别:那边落 flash,这边只落 SRAM 暂存区,
|
||||
* 全程不碰 fmc_*,因此不需要关中断擦写那套优雅关停。
|
||||
*
|
||||
* 对象(APP 字典,表项与钩子包装在 comm/ssc/port/dc_app.c):
|
||||
* 0x2510 会话控制(REC of UINT32,读写钩子返回实时值)
|
||||
* 0x2511 数据窗(VAR OCTETSTRING,4096B,载荷 [offset:u32 LE][crc32:u32 LE][data...])
|
||||
*
|
||||
* 本头刻意只用 <stdint.h> 类型:dc_app.c 是 C 且靠相对路径取 ecat_def.h,
|
||||
* 而 comm/ 下的 C++ 拿不到那套 SSC 类型。SDO abort 码的映射留在 dc_app.c 那一层。
|
||||
*/
|
||||
#ifndef COMM_ECAT_MOTOR_OTA_H
|
||||
#define COMM_ECAT_MOTOR_OTA_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* ── 尺寸常量(宏:C 与 C++ 两侧共用同一真源) ──
|
||||
* 0x2511 对象字节数上限,含 8 字节自描述头。
|
||||
* 单条目 BitSize 是 UINT16 ⇒ 8191 字节是天花板,但**真正的上限是 APP 的堆**:
|
||||
* SSC 的分段下载缓冲是 `ALLOCMEM(ROUNDUPBYTE2WORD(size))` 即 malloc(size)
|
||||
* (comm/ssc/src/sdoserv.c:751,池不够就回 abort 0x05040005 Out of memory),
|
||||
* 而 APP 的 `__HEAP_SIZE` 只有 0xC00=3072B(BOOT 是 0x4000,所以 BOOT 的
|
||||
* 0x2501 能用 4096 —— 照抄块长不照抄堆就会当场失败,2026-10-11 实测)。
|
||||
* 取 1024:48KB 镜像 = 47 块,每块 1 initiate + 4 段 ≈ 5 个邮箱帧,
|
||||
* 总邮箱流量仍在亚秒级,且给 malloc 留出 3 倍余量。
|
||||
* 改这里必须同步 tools/ecat/ecat_native.c 的 MFO_CHUNK_OBJ(两边各自断言不了对方)。
|
||||
*/
|
||||
#define ECAT_MOTA_CHUNK_OBJ_BYTES 1024U
|
||||
#define ECAT_MOTA_CHUNK_HDR_BYTES 8U
|
||||
/** 电机固件上限:必须与 MotorOtaRelay::kMaxFwSize(48KB)一致,C++ 侧有 static_assert 守着。 */
|
||||
#define ECAT_MOTA_IMG_MAX_BYTES 0xC000U
|
||||
/** 0x2510 的子索引个数(sub1..sub11)。 */
|
||||
#define ECAT_MOTA_CTL_SUBS 11U
|
||||
|
||||
/**
|
||||
* ── 会话结果码 ──
|
||||
* 与 MotorOtaRelay 的协议错误码(0x01..0x3x,来自电机 BOOT)刻意分段:
|
||||
* 0x8x 永远是「DCU 本端把料挡了」,不是电机报错。看到 0x8x 查链路与参数,
|
||||
* 看到 0x0x/0x1x/0x3x 查电机与 CAN。同一份码既写进 0x2510:10 高字节(主站可读),
|
||||
* 也作为钩子的返回值(dc_app.c 再换成 SDO abort 码)。
|
||||
*/
|
||||
#define ECAT_MOTA_OK 0x00U
|
||||
#define ECAT_MOTA_E_BADLEN 0x81U /* 声明总长或块长非法 */
|
||||
#define ECAT_MOTA_E_CHUNKCRC 0x82U /* 块 CRC32 不符:邮箱搬坏了,主站重发该块 */
|
||||
#define ECAT_MOTA_E_GAP 0x83U /* 块偏移越界/留洞:主站发料顺序错 */
|
||||
#define ECAT_MOTA_E_IMGCRC 0x84U /* 整镜像 CRC32 不符:拒绝进 BOOT,一个字节都没发 */
|
||||
#define ECAT_MOTA_E_BUSY 0x85U /* relay 已在跑(另一条链路或上一会话未结束) */
|
||||
#define ECAT_MOTA_E_NOTARMED 0x86U /* 没声明总长就写数据窗 */
|
||||
#define ECAT_MOTA_E_STARTED 0x87U /* 已 start,暂存区冻结,不再收块 */
|
||||
#define ECAT_MOTA_E_PARAM 0x88U /* 通道/节点参数非法 */
|
||||
#define ECAT_MOTA_E_BOOTPEND 0x89U /* DCU 自己正在交接回 BOOT,此时不许刷电机 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
namespace EcatMotorOta {
|
||||
|
||||
/** 主循环每圈调用:把暂存区泵进环缓,并在会话结束时收标志。 */
|
||||
void process(void);
|
||||
/** 本模块的会话是否活跃(已 start 且 relay 未进终态)。 */
|
||||
bool sessionActive(void);
|
||||
|
||||
} /* namespace EcatMotorOta */
|
||||
|
||||
#define ECAT_MOTA_LINKAGE extern "C"
|
||||
#else
|
||||
#define ECAT_MOTA_LINKAGE extern
|
||||
#endif /* __cplusplus */
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* SDO 钩子(dc_app.c 的对象表引用这三个,实现在 ecat_motor_ota.cpp)
|
||||
*
|
||||
* 返回 ECAT_MOTA_OK 或上面的 ECAT_MOTA_E_*;dc_app.c 把非 0 换成 SDO abort 码,
|
||||
* 主站既拿得到 abort,也能从 0x2510:10 读到具体是哪一条,不用猜。
|
||||
*---------------------------------------------------------------------------*/
|
||||
/** @brief 0x2510 写入:sub1=cmd(1=start / 3=abort)、sub2=总长、sub3=通道、sub4=node、sub5=整镜像 CRC32。 */
|
||||
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize,
|
||||
const uint8_t *bytes);
|
||||
|
||||
/** @brief 0x2510 读取:返回该 sub 的实时值(进度不落地缓存,避免读到旧数)。 */
|
||||
ECAT_MOTA_LINKAGE uint32_t EcatMotorOtaCtlRead(uint8_t subindex);
|
||||
|
||||
/** @brief 0x2511 写入:一块镜像(自描述 offset + 块 CRC32),失败即 SDO abort。 */
|
||||
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes);
|
||||
|
||||
#endif /* COMM_ECAT_MOTOR_OTA_H */
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "device/canfd.h"
|
||||
#include "device/motor_state.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include "sys/systick.h"
|
||||
|
||||
#include "RTE_Components.h"
|
||||
@@ -108,6 +109,12 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
CanFd::CanTxResult r;
|
||||
if (!CanFd::isChannelEnabled(ch)) {
|
||||
r = CanFd::CanTxResult::kErrInvalid;
|
||||
} else if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
/* 该通道正被电机 OTA 独占:PDO 运动帧一律不投递。
|
||||
* OTA 用单发模式(ARD=1),一旦挤进同一个邮箱就会 abort 掉在途的
|
||||
* 0x101 固件包,或让电机 BOOT 收到非预期长度直接回 0x05/0x06。
|
||||
* 计数与 lastTxResult 走 0xE7,主站侧能看到「被 OTA 挡了」而不是「邮箱满」。 */
|
||||
r = CanFd::CanTxResult::kErrOtaLocked;
|
||||
} else {
|
||||
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
|
||||
&f[ECAT_FRM_HDR_SIZE], dlc);
|
||||
@@ -322,6 +329,9 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
if (dlc > 0U) {
|
||||
if (!CanFd::isChannelEnabled(ch)) {
|
||||
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid;
|
||||
} else if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
/* 同 sendBatch:OTA 独占通道,周期运动帧不投邮箱(0xE7 上报主站)。 */
|
||||
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrOtaLocked;
|
||||
} else {
|
||||
const CanFd::CanTxResult r =
|
||||
CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]),
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "objdef.h"
|
||||
#include "dc_app.h"
|
||||
#include "../../ethercat_pdo_size.h" /* ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES */
|
||||
#include "../../ecat_motor_ota.h" /* 0x2510/0x2511:电机 OTA 邮箱进料的尺寸、会话码与钩子 */
|
||||
|
||||
#include <string.h>
|
||||
#include <stddef.h> /* offsetof:下方布局守卫断言使用 */
|
||||
@@ -391,6 +392,141 @@ static UINT8 ObjEsp32EnWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* 0x2510 / 0x2511 电机 OTA 邮箱进料通路(主站 → APP)
|
||||
*
|
||||
* 为什么要有它:电机 OTA 的引擎在 GD32(MotorOtaRelay:AES 握手 + CAN 停等),
|
||||
* 但固件明文只有一条进料路 —— ESP32 经 SPI 0xD1 喂。只插网线的场合那条 HTTP
|
||||
* 打不通,于是前端把电机 OTA 标成「本链路不支持」。这两个对象把进料口换成
|
||||
* EtherCAT 邮箱,引擎一行没动:数据窗的写回调最终调的还是 MotorOtaRelay::feedAt()。
|
||||
*
|
||||
* 形状完全照 BOOT 已验证的 0x2500/0x2501(comm/ecat/boot_ecat_app.cpp):
|
||||
* - REC 的条目统一 UINT32,偏移才和 SSC「si0 占 2 字节 + 按 BitLength 紧排」一致;
|
||||
* - 数据窗建成 VAR(不是 ARR):VAR 没有 sub0 计数字段,主站按 `:00` 整块写;
|
||||
* VAR 的 maxSubindex 恒为 0 ⇒ 写 `:1` 会直接 ABORTIDX_SUBINDEX_NOT_EXISTING;
|
||||
* complete access 对 VAR 一律 UNSUPPORTED ⇒ 主站必须传 CA=FALSE;
|
||||
* - 条目描述必须带写权限位,objdef.c:1325 的通用写权限门早于 1352 的回调分发,
|
||||
* 少了它回调一次都不会进(BOOT 的 0x2500 当初就这么静默躺平过)。
|
||||
* 业务逻辑(暂存、CRC、泵、终态锁存)在 comm/ecat_motor_ota.cpp,本层只做
|
||||
* SDO 字节 ↔ 纯整型参数的翻译。
|
||||
*---------------------------------------------------------------------------*/
|
||||
|
||||
typedef struct OBJ_STRUCT_PACKED_START {
|
||||
UINT16 bSubIndex; /**< sub0:条目数,SSC 按 UINT16 读 */
|
||||
UINT32 u32Cmd; /**< sub1:W 1=start 3=abort;R 最近一次命令 */
|
||||
UINT32 u32Total; /**< sub2:镜像总字节数 */
|
||||
UINT32 u32Channel; /**< sub3:目标 CAN 通道 0..2 */
|
||||
UINT32 u32Node; /**< sub4:目标节点 1..9 */
|
||||
UINT32 u32ImgCrc; /**< sub5:整镜像 CRC32(MPEG-2,与 SPI 链路同款) */
|
||||
UINT32 u32Staged; /**< sub6:已连续暂存字节数(只读) */
|
||||
UINT32 u32RelayState; /**< sub7:MotorOtaRelay 状态(只读) */
|
||||
UINT32 u32PkAcked; /**< sub8:已确认包数(只读) */
|
||||
UINT32 u32PkTotal; /**< sub9:总包数(只读) */
|
||||
UINT32 u32Err; /**< sub10:高字节=本模块会话码 0x8x,低字节=电机协议错误码 */
|
||||
UINT32 u32Phase; /**< sub11:位图,见 ecat_motor_ota.cpp */
|
||||
}
|
||||
OBJ_STRUCT_PACKED_END
|
||||
TOBJ2510;
|
||||
|
||||
_Static_assert(offsetof(TOBJ2510, u32Cmd) == 2, "TOBJ2510 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
_Static_assert(sizeof(TOBJ2510) == 2 + 4 * 11, "TOBJ2510 尺寸应为 2+44");
|
||||
|
||||
static TOBJ2510 s_motorOtaCtl = {ECAT_MOTA_CTL_SUBS, 0U, 0U, 0U, 0U, 0U,
|
||||
0U, 0U, 0U, 0U, 0U, 0U};
|
||||
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaCtlEntryDesc[] = {
|
||||
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 11 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub1 : cmd(必须带写位,否则回调进不来) */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub2 : 镜像总长 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub3 : CAN 通道 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub4 : 节点号 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub5 : 整镜像 CRC32 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub6 : 已暂存字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub7 : relay 状态 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub8 : 已 ACK 包数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub9 : 总包数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub10 : 会话码<<8 | 协议错误码 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub11 : phase 位图 */
|
||||
};
|
||||
static OBJCONST UCHAR aName0x2510[] = "Motor OTA control";
|
||||
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaChunkEntryDesc[] = {
|
||||
/* 唯一条目 = 一整块 [offset:u32][crc32:u32][data...]。
|
||||
* 写回调在 objdef.c:1352 早分发,SSC 的默认拷贝路径不会执行。 */
|
||||
{DEFTYPE_OCTETSTRING, (UINT16)(ECAT_MOTA_CHUNK_OBJ_BYTES * 8U), ACCESS_READWRITE},
|
||||
};
|
||||
static OBJCONST UCHAR aName0x2511[] = "Motor firmware chunk";
|
||||
|
||||
/** 数据窗的落地缓冲:写路径不依赖它(回调自己取字节),保留是给默认读路径的落点。 */
|
||||
static UINT8 s_motorOtaChunk[ECAT_MOTA_CHUNK_OBJ_BYTES];
|
||||
|
||||
static UINT8 ObjMotorOtaCtlWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT8 rc = EcatMotorOtaCtlWrite(subindex, dataSize, (const UINT8 *)pData);
|
||||
|
||||
(void)index;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (subindex == 0U) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* sub0 是条目数,不可写 */
|
||||
}
|
||||
if (subindex >= 2U + ECAT_MOTA_CTL_SUBS) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING;
|
||||
}
|
||||
if (rc != ECAT_MOTA_OK) {
|
||||
/* 具体原因不靠 abort 码分类:主站读 0x2510:10 的高字节拿真码。 */
|
||||
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/** 0x2510 读取:逐 subindex 从 ecat_motor_ota 取**实时**值,不读 s_motorOtaCtl 的镜像。 */
|
||||
static UINT8 ObjMotorOtaCtlRead(UINT16 index, UINT8 subindex, UINT32 objSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT32 v = EcatMotorOtaCtlRead(subindex);
|
||||
UINT8 *p8 = (UINT8 *)pData;
|
||||
|
||||
(void)index;
|
||||
(void)objSize;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (pData == NULL) {
|
||||
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
|
||||
}
|
||||
if (subindex == 0U) {
|
||||
/* sub0 是 UINT8(条目数),只占载荷一个字节 */
|
||||
p8[0] = (UINT8)v;
|
||||
} else {
|
||||
p8[0] = (UINT8)(v & 0xFFU);
|
||||
p8[1] = (UINT8)((v >> 8) & 0xFFU);
|
||||
p8[2] = (UINT8)((v >> 16) & 0xFFU);
|
||||
p8[3] = (UINT8)((v >> 24) & 0xFFU);
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
static UINT8 ObjMotorOtaChunkWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
|
||||
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
|
||||
{
|
||||
const UINT8 rc = EcatMotorOtaChunkWrite(dataSize, (const UINT8 *)pData);
|
||||
|
||||
(void)index;
|
||||
(void)bCompleteAccess;
|
||||
|
||||
if (subindex != 0U) {
|
||||
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* VAR 只有 :00 */
|
||||
}
|
||||
if (rc == ECAT_MOTA_E_GAP || rc == ECAT_MOTA_E_PARAM || rc == ECAT_MOTA_E_BADLEN) {
|
||||
return ABORTIDX_VALUE_EXCEEDED;
|
||||
}
|
||||
if (rc != ECAT_MOTA_OK) {
|
||||
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
* Object Names
|
||||
*---------------------------------------------------------------------------*/
|
||||
@@ -444,6 +580,16 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
|
||||
{NULL, NULL, 0x2503, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
|
||||
sEntryDesc0x2503, aName0x2503, &s_esp32EnVal, NULL, ObjEsp32EnWrite, 0x0000},
|
||||
|
||||
/* 0x2510: 电机 OTA 会话控制(写 sub2 声明总长 → 反复写 0x2511 灌块 → 写 sub1=1 启动) */
|
||||
{NULL, NULL, 0x2510, {DEFTYPE_RECORD, ECAT_MOTA_CTL_SUBS | (OBJCODE_REC << 8)},
|
||||
asMotorOtaCtlEntryDesc, aName0x2510, &s_motorOtaCtl,
|
||||
ObjMotorOtaCtlRead, ObjMotorOtaCtlWrite, 0x0000},
|
||||
|
||||
/* 0x2511: 电机固件数据窗(VAR OCTETSTRING,主站分段下载攒完才回调一次) */
|
||||
{NULL, NULL, 0x2511, {DEFTYPE_OCTETSTRING, 0 | (OBJCODE_VAR << 8)},
|
||||
asMotorOtaChunkEntryDesc, aName0x2511, &s_motorOtaChunk,
|
||||
NULL, ObjMotorOtaChunkWrite, 0x0000},
|
||||
|
||||
/* 终止条目 */
|
||||
{NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000}
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
* @note 0xE0/0xE1 保留给 handler 入口校验(payload 过短/通道非法),
|
||||
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因;
|
||||
* 0xE6=通道处于收发器 Silent(只听不发),在入口即拒绝,不计任何错误计数。
|
||||
* 0xE7=通道被电机 OTA 独占锁定(EtherCAT/SPI 跨总线排他),同样不计错误计数。
|
||||
*/
|
||||
enum class CanTxResult : uint8_t {
|
||||
kOk = 0x00U, /**< 发送成功,已进入硬件邮箱。 */
|
||||
@@ -43,6 +44,7 @@ public:
|
||||
kErrMailbox = 0xE4U, /**< 3 个硬件邮箱全 PENDING,abort+重投仍失败。 */
|
||||
kErrInvalid = 0xE5U, /**< 入口参数无效(channelIndex/dlc/data 等)。 */
|
||||
kErrSilent = 0xE6U, /**< 通道收发器处于 Silent(驱动器关断),拒绝发送。 */
|
||||
kErrOtaLocked = 0xE7U, /**< 通道被电机 OTA 独占(跨总线排他),拒绝发送,不计错误计数。 */
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
@@ -76,6 +77,17 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
/* 通道正被电机 OTA 独占(WiFi 与 EtherCAT 两条链路可能同时在跑,锁是共用的):
|
||||
* 这一帧挤进同一通道会 abort 掉在途的 0x101 固件包,单发模式下永不重传。
|
||||
* 回 0xE7,上位机据此知道「是被 OTA 挡了」而不是「邮箱满」。 */
|
||||
if (MotorOtaRelay::isChannelLocked(can_ch)) {
|
||||
postCanSendLog(bus, "LOCKED(motor-OTA)", can_ch, can_id, can_data, can_dlc);
|
||||
const uint8_t err = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 0xC0 单帧发送改走 enqueueTx:
|
||||
* 1) allow_abort=false 杜绝硬件邮箱被后续发送主动 abort 抹杀;
|
||||
* 2) 电机高频上报抢占仲裁导致邮箱全占时,帧自动进入 13 深度软件队列,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/motor_ota_relay.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -81,6 +82,11 @@ public:
|
||||
resp[1U + i] = 0xE1U; /* 通道非法:跳过该帧,其余照常发送 */
|
||||
continue;
|
||||
}
|
||||
if (MotorOtaRelay::isChannelLocked(ch)) {
|
||||
/* 电机 OTA 独占该通道:整帧拒绝,不挤邮箱(0xE7)。 */
|
||||
resp[1U + i] = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
|
||||
continue;
|
||||
}
|
||||
const CanFd::CanTxResult r =
|
||||
CanFd::enqueueTx(ch, id, (dlc > 0U) ? &p[1] : nullptr, dlc);
|
||||
resp[1U + i] = static_cast<uint8_t>(r);
|
||||
|
||||
@@ -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