/** * @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 #include /* 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(p[0]) | (static_cast(p[1]) << 8) | (static_cast(p[2]) << 16) | (static_cast(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(s_channel), s_total, MotorOtaRelay::FeedSource::kEcat, static_cast(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(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(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(st.state), static_cast(st.error_code), static_cast(st.packets_acked), static_cast(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(st.state) : static_cast(s_final_state); case 8U: return st.packets_acked; case 9U: return st.total_packets; case 10U: return (static_cast(s_sess_err) << 8) | (live ? static_cast(st.error_code) : static_cast(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(s_channel))) { phase |= 0x0020U; } if (MotorOtaRelay::isActive()) { phase |= 0x0040U; } return phase; } default: return 0U; } } } /* extern "C" */