384 lines
14 KiB
C++
384 lines
14 KiB
C++
/**
|
||||
|
|
* @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" */
|