/** * @file motor_ota_relay.cpp * @brief 电机 OTA relay 状态机实现。 */ #include "sys/motor_ota_relay.h" #include "sys/aes128.h" #include "sys/config.h" #include "sys/systick.h" #include "sys/task_manager.h" #include "main.h" #include "device/canfd.h" #include "device/spi/app_spi_command.h" #include "device/spi/app_spi_command_defs.h" #include #include namespace MotorOtaRelay { /* ═══════════════════ 内部常量 ═══════════════════ */ /* 与电机 BOOT 工程一致的 K_master(预置主密钥) */ static const uint8_t K_MASTER[16] = { 0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF, 0xFE,0xDC,0xBA,0x98,0x76,0x54,0x32,0x10 }; /* 固件环形缓冲:ESP32 喂入明文,状态机消耗加密后发 CAN。 * 2KB≈42 包:CAN 5ms/包时约 210ms 余量;ESP32 SPI 欠载时会出现 ~500ms CAN 空洞。 * 提到 4KB≈85 包(~420ms)给 WiFi/HTTP 抖动更大缓冲。 */ static constexpr uint16_t kBufSize = 4096U; /* ═══════════════════ 内部状态 ═══════════════════ */ static State s_state = State::kIdle; static uint8_t s_error = 0; static 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; static uint16_t s_packets_acked = 0; static uint32_t s_session_num = 0; static bool s_boot_heartbeat_seen = false; /* kPreCancel 期间是否收到 0x103 心跳 */ /* AES */ static aes128_context s_aes_ctx; static uint8_t s_k_session[16]; static uint8_t s_nonce[16]; /* ACK 捕获(由 onCanFrame 在 taskCanRxParse 上下文写入) */ static volatile bool s_ack_pending = false; static uint8_t s_ack_data[64]; static uint8_t s_ack_len = 0; /* 超时 / 发送节奏 */ static uint32_t s_state_enter_ms = 0; /* 上次发送时刻;0 = 当前步骤尚未首发(enterState 置 0,process() 负责首发)。 */ static uint32_t s_last_send_ms = 0; /* 当前步骤已补发次数(进入新步骤 / 弹出新数据包时清零)。 */ static uint8_t s_retry_count = 0; /* CRC 错误后待重发标志:置位后由 onTxTick() 按 kTransferGapMs 间隔重发当前包(不立即重发)。 */ static bool s_crc_resend_pending = false; /* 数据发送节奏完全由主循环 onTxTick() + SysTick 钟表(s_last_send_ms)驱动, * 不再用跨 ISR/主循环的计数器(消除 s_gap_div_100us 竞争)。 */ /* 固件环形缓冲 */ static uint8_t s_buf[kBufSize]; static volatile uint16_t s_buf_head = 0; /* 写入位置(ESP32 feed) */ static volatile uint16_t s_buf_tail = 0; /* 读取位置(状态机消耗) */ /* 当前待发包的明文暂存 + "当前包是否已从缓冲弹出"标志。 * CRC 错误时 BOOT 要求重发同一包,不能再 bufPop 新数据。 * s_pkt_pending=false 表示当前没有待发包数据(需弹新包); * s_pkt_pending=true 表示 s_pkt_plain 已含待发数据,可重发。 */ static uint8_t s_pkt_plain[kPacketPayload]; static bool s_pkt_pending = false; /* 末包 padding 诊断:sendNextPacket 弹末包时记录有效字节数(0xFFFF=非末包/已打印), * 供 sendCurrentPacket 在 TX 日志前一次性打印,避免 padding 日志被紧接的 TX 行吞掉。 */ static uint16_t s_last_pad_valid = 0xFFFFU; /* ═══════════════════ 工具函数 ═══════════════════ */ static inline uint16_t bufUsed() { return static_cast((s_buf_head - s_buf_tail + kBufSize) % kBufSize); } static inline uint16_t bufFree() { return static_cast(kBufSize - 1U - bufUsed()); } static void bufPush(const uint8_t *data, uint16_t len) { for (uint16_t i = 0; i < len; i++) { s_buf[s_buf_head] = data[i]; s_buf_head = static_cast((s_buf_head + 1U) % kBufSize); } } static bool bufPop(uint8_t *out, uint16_t len) { if (bufUsed() < len) return false; for (uint16_t i = 0; i < len; i++) { out[i] = s_buf[s_buf_tail]; s_buf_tail = static_cast((s_buf_tail + 1U) % kBufSize); } return true; } /* 简单伪随机 nonce(SysTick + 未初始化 ADC 噪声足够区分会话) */ static void generateNonce(uint8_t *out) { uint32_t seed = SysTickTimer::getMs(); for (uint8_t i = 0; i < 16; i++) { seed = seed * 1103515245U + 12345U; out[i] = static_cast((seed >> 16) & 0xFF); } } /* keyed-CRC32:与电机 BOOT calculate_crc32_with_key 查表实现完全一致。 * ★ 必须用查表法:BOOT 侧 (crc ^ key[i]) & 0xFF(8位 XOR 再查表), * bit-by-bit 用 crc ^= key[i](32位 XOR),两者不等价,会导致每包 CRC 校验失败。 */ /* CRC32 查找表(标准多项式 0xEDB88320)。 */ static const uint32_t kCrcTable[256] = { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }; static void keyedCrc32(const uint8_t *key, const uint8_t *data, uint8_t *output) { uint32_t crc = 0xFFFFFFFFU; /* 16 字节会话密钥先喂入(与 BOOT 完全一致:查表法) */ for (int i = 0; i < 16; ++i) { crc = kCrcTable[(crc ^ key[i]) & 0xFFU] ^ (crc >> 8); } /* 48 字节有效数据再喂入 */ for (int i = 0; i < (int)kPacketPayload; ++i) { crc = kCrcTable[(crc ^ data[i]) & 0xFFU] ^ (crc >> 8); } /* 取反得到最终结果 */ crc = ~crc; /* 小端格式输出 */ output[0] = static_cast(crc & 0xFFU); output[1] = static_cast((crc >> 8) & 0xFFU); output[2] = static_cast((crc >> 16) & 0xFFU); output[3] = static_cast((crc >> 24) & 0xFFU); } /* ═══════════════════ CAN 发送辅助 ═══════════════════ */ /* OTA 帧发送:periodic 控制是否允许硬件自动重传。 * 所有帧统一用 periodic=false:发一次即止,不靠 CAN 硬件重传。 * - 握手/控制帧:重传由 relay 软件 process() 按 kStepTimeoutMs 超时补发。 * - 数据帧:靠 relay 软件 stop-and-wait(onTxTick)补发。 * 否则电机跳 BOOT 期间 CAN 短暂断开时硬件持续重传,恢复后重复帧到达 BOOT * 被判为非法握手 → INVALID_HANDSHAKE(0x05),或数据帧重复 → CRC 错误(0x06)。 */ static bool canSend(uint32_t id, const uint8_t *data, uint8_t dlc, bool periodic = false) { /* 诊断:kTransfer 期间只应发 0x101,若出现 0x100 则为幽灵帧 */ if (s_state == State::kTransfer && id == kOtaReqId) { LOG_ERR("[MOTOR-OTA] BUG! canSend(0x100) in kTransfer! dlc=%u\r\n", (unsigned)dlc); } const CanFd::CanTxResult r = CanFd::transmitFrame(s_channel, id, data, dlc, periodic); return (r == CanFd::CanTxResult::kOk); } static void enterState(State st) { const State prev = s_state; s_state = st; s_state_enter_ms = SysTickTimer::getMs(); /* 进入新步骤:清补发计数;s_last_send_ms=0 令 process() 先“首发”再进入等待/补发节奏。 */ s_retry_count = 0; s_last_send_ms = 0; s_crc_resend_pending = false; /* auto_retrans 切换:OTA 是应用层停等协议,硬件自动重发会打穿电机单缓冲 RX * (重发的前一帧被当成新帧处理 → receive_packet_num 错位 → CRC 错)。 * 进入 kHandshakeReq(start() 唯一入口)即关 ARD 切单发; * 回到 kIdle/kFinished/kError 即恢复 ARD,避免异常路径漏恢复导致全局卡在单发。 * 注:setChannelAutoRetrans 内部按 init-mode 改 ARD 位,开销在几十 µs 量级。 */ const bool otaActive = (st == State::kPreCancel || st == State::kHandshakeReq || st == State::kHandshakeNonce || st == State::kHandshakeConfirm || st == State::kErase || st == State::kTransfer || st == State::kFinishOta); const bool wasActive = (prev == State::kPreCancel || prev == State::kHandshakeReq || prev == State::kHandshakeNonce || prev == State::kHandshakeConfirm || prev == State::kErase || prev == State::kTransfer || prev == State::kFinishOta); if (otaActive && !wasActive) { CanFd::setChannelAutoRetrans(s_channel, false); /* OTA 开始:单发 */ } else if (!otaActive && wasActive) { CanFd::setChannelAutoRetrans(s_channel, true); /* OTA 结束:恢复重发 */ } } static bool stateTimedOut(uint32_t timeout_ms) { return (SysTickTimer::getMs() - s_state_enter_ms) >= timeout_ms; } static void pushStatus(); /* 前置声明:setError 里要用,定义在下方 */ static void setError(uint8_t code) { s_error = code; enterState(State::kError); /* 任何失败都立即把 kError 推给 ESP32:否则 ESP32 feedTask 看不到错误态, * 会一直喂到 120s 超时才退出。"失败了就报错退出"要求即时上报。 */ pushStatus(); } /* 进度推送给 ESP32(0xD2 ring push)。 * 传输期 ESP32 suppress ring:push 只会占 PB6/m_txBuf 50ms,干扰 0xD1 喂数。 * kTransfer 内禁止 push;终态/错误/握手阶段仍推。 */ static void pushStatus() { if (s_state == State::kTransfer) { return; } uint8_t payload[12]; payload[0] = static_cast(s_state); payload[1] = s_error; payload[2] = static_cast(s_packets_acked & 0xFF); payload[3] = static_cast(s_packets_acked >> 8); payload[4] = static_cast(s_total_packets & 0xFF); payload[5] = static_cast(s_total_packets >> 8); payload[6] = static_cast(s_fw_size & 0xFF); payload[7] = static_cast((s_fw_size >> 8) & 0xFF); payload[8] = static_cast((s_fw_size >> 16) & 0xFF); payload[9] = static_cast((s_fw_size >> 24) & 0xFF); payload[10] = static_cast(bufUsed() & 0xFF); payload[11] = static_cast(bufUsed() >> 8); AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorOtaStatus, payload, 12); } /* ═══════════════════ 握手子步骤 ═══════════════════ */ /* 单步等对应反馈的超时:到点仍无反馈就补发。100ms 加快握手节奏(电机 BOOT 主循环~10ms 足够响应)。 */ static constexpr uint32_t kStepTimeoutMs = 100U; /* 补发次数上限:首发后最多再补发 kMaxResends 次;仍无反馈则判该步失败并打印。 */ static constexpr uint8_t kMaxResends = 2U; /* 数据阶段 ACK 超时重发间隔(ms):正常 ACK 往返 ~11ms(电机 BOOT 主循环 ~10ms), * 100ms 留 10 倍余量且避开电机偶发 flash 写慢。原 500ms 硬编码:11 次丢帧盲等 * 共 ~5.5s,占整场传输过半时间。电机对同包号幂等(只 ACK 不重复写 flash), * 早发导致的双 ACK 由 processTransferAck 的重复 ACK 过滤兜底。 */ static constexpr uint32_t kAckResendMs = 100U; /* 数据阶段每包补发上限:随 kAckResendMs 500→100 同步 2→4,保持总容错时间窗; * 握手阶段仍用 kMaxResends,不动 v1.4.51 抗干扰逻辑。超限后由 process() 整体超时兜底。 */ static constexpr uint8_t kAckMaxResends = 4U; /* 数据传输每包间隔(ms):正式发固件时每包也留间隔,防电机单缓冲 RX 高速下丢包/CRC 错。 * 现行:5ms/包(用户指定)。SPI 喂数必须能跟上;跟不上时靠水位门控暂停 CAN。 */ static constexpr uint32_t kTransferGapMs = 5U; /* 开传前环缓最低水位:至少约 1KB(~21 包 / ~100ms)再发首包, * 避免擦除后立刻按 5ms 抽干仅有的 232B 首 chunk 导致电机超时。 */ static constexpr uint16_t kStartWaterBytes = 1024U; /* 运行中低水位:低于此值暂停发新包,等 ESP32 回填(已在途包仍等 ACK)。 */ static constexpr uint16_t kRunWaterBytes = 256U; /* 擦除后首包等待(ms):电机发 erase ACK = 擦除已完成, * 100ms 仅为电机内部状态转换留余量(BOOT 主循环~10ms)。 * 之前 500ms 导致电机 RECEIVING_FIRMWARE 超时(~2s)预算被压缩。 */ static constexpr uint32_t kEraseWaitMs = 100U; /* 等新 APP 运行确认(ms):尾包 ACK 后电机复位跳 APP,新 APP 上电初始化完 CAN 会 * 自主发 [0A AC](刷写结束/APP 运行有效,见协议表 15/16 行)。 * 复位+初始化实测 <1s,3s 留足余量;超时 = 新 APP 未启动 → error 0x25。 */ static constexpr uint32_t kAppRunTimeoutMs = 3000U; /* 残帧窗口 kBootSettleMs 已移除(2026-07-27):收到 0x103 即直接进握手。 * 旧担忧:电机 APP 跳转前 TX 队列残留 [00 FA] 帧延迟上总线被 BOOT catch-all 命中 → 0x05。 * 现行握手阶段对垃圾帧一律忽略,靠 kStepTimeoutMs=100ms 步进重发(≤2次)自愈, * 窗口不再必要;若 0x05 复现优先怀疑此处。 */ /* 步骤名,用于“xxx 未收到”日志,让用户/上位机知道卡在协议哪一步。 */ static const char *stepName(State st) { switch (st) { case State::kPreCancel: return "WAIT_BOOT(0x103)"; case State::kHandshakeReq: return "HANDSHAKE_REQ(0x100:00AC)"; case State::kHandshakeNonce: return "HANDSHAKE_NONCE(0x100:16B)"; case State::kHandshakeConfirm: return "HANDSHAKE_CONFIRM(0x100:16B)"; case State::kErase: return "ERASE(0x101:fw_size)"; case State::kTransfer: return "TRANSFER(0x101:data)"; case State::kFinishOta: return "FINISH_OTA(0x100:0AAC)"; case State::kWaitAppRun: return "WAIT_APP_RUN(0x102:0AAC)"; default: return "?"; } } static void sendNonce() { /* 生成 nonce 并切状态;首发由 ACK handler 触发(不走 process 首发路径)。 */ generateNonce(s_nonce); enterState(State::kHandshakeNonce); } /* 统一重发当前步骤的帧(超时重发 + kHandshakeReq 首发) */ static void sendCurrentStep(uint32_t now_ms) { OTA_LOGI("[MOTOR-OTA] TX %s try=%u t=%lu\r\n", stepName(s_state), (unsigned)s_retry_count, (unsigned long)now_ms); switch (s_state) { case State::kHandshakeReq: { const uint8_t req[2] = {0x00, 0xAC}; canSend(kOtaReqId, req, 2); break; } case State::kHandshakeNonce: canSend(kOtaReqId, s_nonce, 16); break; case State::kHandshakeConfirm: { uint8_t plain[16]; for (uint8_t i = 0; i < 16; i++) plain[i] = s_nonce[i] ^ 0x55; uint8_t confirm[16]; aes128_set_key(&s_aes_ctx, s_k_session); aes128_encrypt_block(&s_aes_ctx, plain, confirm); canSend(kOtaReqId, confirm, 16); break; } case State::kErase: { uint8_t size_le[4]; size_le[0] = static_cast(s_fw_size & 0xFF); size_le[1] = static_cast((s_fw_size >> 8) & 0xFF); size_le[2] = static_cast((s_fw_size >> 16) & 0xFF); size_le[3] = static_cast((s_fw_size >> 24) & 0xFF); canSend(kOtaDataId, size_le, 4); break; } case State::kFinishOta: { const uint8_t finish_cmd[2] = {0x0A, 0xAC}; canSend(kOtaReqId, finish_cmd, 2); break; } default: break; } s_last_send_ms = now_ms; } /* ═══════════════════ 数据传输 ═══════════════════ */ /* 将 s_pkt_plain 组装成 64B CAN 帧并发送。 * 不弹缓冲,只做加密+CRC+组帧+CAN 发送。 * 供 sendNextPacket(弹新包后发)和 CRC 重发(发同一包)共用。 */ static void sendCurrentPacket() { const uint32_t pkt_num = s_packets_acked; const bool is_last = (pkt_num + 1U >= s_total_packets); /* AES-ECB 加密 48B */ uint8_t encrypted[kPacketPayload]; aes128_set_key(&s_aes_ctx, s_k_session); aes128_ecb_encrypt_48bytes(&s_aes_ctx, s_pkt_plain, encrypted); /* keyed CRC32 */ uint8_t crc[4]; keyedCrc32(s_k_session, s_pkt_plain, crc); /* 组装 64B 帧: session(4) + pkt_num(4) + encrypted(48) + IFEND(4) + CRC(4) * 帧布局对齐 BOOT 工程 OTA.cpp: * data[0-3] session, [4-7] pkt_num, [8-55] encrypted(48B), * [56-59] IFEND (extract_u32_from_4bytes(data+56)), * [60-63] CRC (data+60) */ uint8_t frame[kFrameDlc]; memset(frame, 0, sizeof(frame)); /* session_num LE */ frame[0] = static_cast(s_session_num & 0xFF); frame[1] = static_cast((s_session_num >> 8) & 0xFF); frame[2] = static_cast((s_session_num >> 16) & 0xFF); frame[3] = static_cast((s_session_num >> 24) & 0xFF); /* packet_num LE */ frame[4] = static_cast(pkt_num & 0xFF); frame[5] = static_cast((pkt_num >> 8) & 0xFF); frame[6] = static_cast((pkt_num >> 16) & 0xFF); frame[7] = static_cast((pkt_num >> 24) & 0xFF); /* encrypted payload */ memcpy(&frame[8], encrypted, kPacketPayload); /* IFEND @56 (BOOT 在 data+56 读 IFEND) */ if (is_last) { frame[56] = 0xFF; frame[57] = 0xFF; frame[58] = 0xFF; frame[59] = 0xFF; } /* CRC @60 (BOOT 在 data+60 读 CRC) */ memcpy(&frame[60], crc, 4); s_ack_pending = false; /* periodic=false:数据帧只发一次,不靠 CAN 硬件自动重传。 * 硬件重传会在 10ms 间隔下持续重发同一包,电机 BOOT 处理期间重复收到 * 同一包 → 时序错乱 → CRC 错误(06)/握手错误(05)。 * 超时重发由 onTxTick()(CRC 错误置 s_crc_resend_pending)负责。 */ /* 末包 padding 日志在 TX 前打印(sendNextPacket 设 s_last_pad_valid)。 * 用最短格式 + 一次性打印(打印后置 0xFFFF 防重发重复打印)。 */ if (s_last_pad_valid != 0xFFFFU) { OTA_LOGI("[MOTOR-OTA] PAD v=%u p=%u\r\n", (unsigned)s_last_pad_valid, (unsigned)(kPacketPayload - s_last_pad_valid)); s_last_pad_valid = 0xFFFFU; } /* 逐包 TX 日志在 5ms 节奏下会拖主循环;仅末包/每 64 包打一行。 */ if (is_last || ((pkt_num & 0x3FU) == 0U)) { OTA_LOGI("[MOTOR-OTA] TX pkt=%lu/%u%s t=%lu\r\n", (unsigned long)pkt_num, (unsigned)s_total_packets, is_last ? " LAST" : "", (unsigned long)SysTickTimer::getMs()); } canSend(kOtaDataId, frame, kFrameDlc, /*periodic=*/false); } static void sendNextPacket() { /* 从缓冲取明文到 s_pkt_plain */ const uint16_t used = bufUsed(); const bool is_last = (s_packets_acked + 1U >= s_total_packets); /* 末包真实有效字节:fw_size % 48;整除时末包也是满 48B。 */ const uint16_t last_valid = static_cast( (s_fw_size % kPacketPayload) == 0U ? kPacketPayload : (s_fw_size % kPacketPayload)); if (is_last) { /* ★ 末包无论 used 是否 ≥48,都只取 last_valid 再 0xFF pad。 * 旧逻辑 used≥48 时 bufPop(48) 会把环缓里多喂的重复字节一并发出 * (BLF:末包 padding 变成向量表而非 0xFF)。 */ if (used < last_valid) { static uint32_t s_last_tail_ms = 0; const uint32_t now = SysTickTimer::getMs(); if (now - s_last_tail_ms >= 200U) { OTA_LOGI("[MOTOR-OTA] tail-wait pkt=%u used=%u need=%u fed=%lu t=%lu\r\n", (unsigned)s_packets_acked, (unsigned)used, (unsigned)last_valid, (unsigned long)s_fw_fed, (unsigned long)now); s_last_tail_ms = now; } return; } for (uint16_t i = 0U; i < last_valid; ++i) { s_pkt_plain[i] = s_buf[s_buf_tail]; s_buf_tail = static_cast((s_buf_tail + 1U) % kBufSize); } for (uint16_t i = last_valid; i < kPacketPayload; ++i) { s_pkt_plain[i] = 0xFFU; } s_last_pad_valid = last_valid; } else { if (used < kPacketPayload) { static uint32_t s_last_empty_ms = 0; const uint32_t now = SysTickTimer::getMs(); if (now - s_last_empty_ms >= 200U) { OTA_LOGI("[MOTOR-OTA] buf-empty wait-feed pkt=%u used=%u fed=%lu/%lu t=%lu\r\n", (unsigned)s_packets_acked, (unsigned)used, (unsigned long)s_fw_fed, (unsigned long)s_fw_size, (unsigned long)now); s_last_empty_ms = now; } return; } if (!bufPop(s_pkt_plain, kPacketPayload)) { return; } s_last_pad_valid = 0xFFFFU; } s_pkt_pending = true; s_retry_count = 0; /* 新数据包:重置补发计数 */ s_state_enter_ms = SysTickTimer::getMs(); /* 新数据包:重置整体超时 backstop */ /* 丢弃上一包/握手阶段残留的 TX 错误事件,避免新包刚发就被陈旧标志误触快速重发。 */ (void)CanFd::consumeTxError(s_channel); /* 末包 padding 日志在 sendCurrentPacket 的 TX 行之前打印,且用最短格式, * 避免 10ms 高频 TX 期间被 UART 吞掉(之前放在这里会被紧接的 TX pkt= 行挤掉)。 */ sendCurrentPacket(); s_last_send_ms = SysTickTimer::getMs(); } /* BOOT 错误码(与 OTA.h ota_error_t 对齐,仅列出实际处理的) */ static constexpr uint8_t kBootErrHandshake = 0x05; /* INVALID_HANDSHAKE:直接中止(不重握手) */ static constexpr uint8_t kBootErrCrc = 0x06; /* CRC 错误:重发同一包 */ /* CRC 错误重试上限:BOOT 对 CRC 失败的包做 receive_packet_num -= 1 并 * 发 1 字节 error frame(0x06),期望上位机重发同一包。超过此限判定为 * 真实数据损坏,报错中止,避免无限循环。 */ static constexpr uint8_t kMaxCrcRetries = 3; static uint8_t s_crc_retry_count = 0; static uint16_t s_last_acked_pkt = 0xFFFFU; /* 上次确认的包号,用于识别重复 ACK */ static uint32_t s_dup_ack_count = 0; /* 累计忽略的重复/未前进 ACK 帧数(诊断重传风暴) */ static uint32_t s_crc_err_count = 0; /* 累计收到的 0x06 CRC 错误帧数(含电机重传) */ static uint32_t s_ack_resend_total = 0; /* 累计 ACK 超时重发次数(整场不清零,丢帧率基线) */ static uint32_t s_fast_resend_total = 0; /* 累计 TX 错误快速重发次数(MTE 事件驱动,免等超时) */ static void processTransferAck(const uint8_t *data, uint8_t len) { /* 擦除等待窗口(首包尚未发出:s_packets_acked==0 且 !s_pkt_pending):电机重传的旧 * [BA C0] / 杂散帧一律忽略,绝不能推进 s_packets_acked——否则 onTxTick 的 * need=(s_packets_acked==0?kEraseWaitMs:kTransferGapMs) 会提前用 30ms 抢跑首包, * 且首包被错标成非 0 号(实测首包变 pkt=5) → 从一开始就包号/数据错位,每包 0x06。 */ if (s_packets_acked == 0U && !s_pkt_pending) { s_dup_ack_count++; return; } /* BOOT 错误帧:1 字节 [error_code],发到 0x102 ID。 * CRC 错误(0x06):BOOT 做 receive_packet_num -= 1,期望重发同一包, * 不改变 ota_state,仍处于 RECEIVING_FIRMWARE。 * 其他错误码:BOOT 已进 IDLE,不可恢复。 */ if (len == 1) { if (data[0] == kBootErrCrc) { s_crc_err_count++; /* 去重:已排定重发时,电机 auto_retrans 重传的重复 0x06 不再计数, * 避免一次 CRC 错的多份重传帧把重试计数提前耗尽 → 假性 0x23。 */ if (s_crc_resend_pending) { return; } if (s_crc_retry_count < kMaxCrcRetries) { s_crc_retry_count++; /* 不立即重发:置标志,由 onTxTick() 按 kTransferGapMs 间隔重发当前包, * 避免"挨太近→又 CRC 错"死循环。s_pkt_plain 仍含上次明文,s_pkt_pending 保持。 */ s_crc_resend_pending = true; return; } LOG_ERR("[MOTOR-OTA] CRC retry exhausted pkt=%u/%u dup=%lu crc=%lu\r\n", (unsigned)s_packets_acked, (unsigned)s_total_packets, (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count); setError(0x23); /* CRC 重试耗尽 */ return; } if (data[0] == kBootErrHandshake) { /* INVALID_HANDSHAKE(0x05):电机 BOOT 已退回 IDLE。按用户要求不再做 * 「发 CANCEL + 从包 0 重握手」的自动重试(会白扔进度、坏总线下反复失败), * 直接中止,由上位机决定是否整体重刷。 */ LOG_ERR("[MOTOR-OTA] HANDSHAKE err(0x05) abort pkt=%u/%u\r\n", (unsigned)s_packets_acked, (unsigned)s_total_packets); setError(0x05); return; } /* 其他错误码:BOOT 已中止,relay 也报错 */ setError(0x24); /* BOOT 报致命错误 */ return; } if (len < 6) { setError(0x21); return; } /* ACK: [0xBA, 0xC0, next_pkt(4B LE)] */ if (data[0] != 0xBA || data[1] != 0xC0) { setError(0x22); return; } const uint32_t next_pkt = static_cast(data[2]) | (static_cast(data[3]) << 8) | (static_cast(data[4]) << 16) | (static_cast(data[5]) << 24); /* BOOT 协议:最后一包(IFEND=0xFFFFFFFF)后,BOOT 进 FINISHED 并回 * ACK(0xF0F0F0F0) 表示"全部写入完成,等 FINISH_OTA"。 * 上位机须再发 [0x0A, 0xAC] 到 REQ ID,BOOT 才真正跳转 APP。 * 旧实现误将 0xF0F0F0F0 当普通包号处理,导致 relay 永远等不到 0xFFFFFFFF, * 既不进 kFinished 也不发 FINISH_OTA,BOOT 卡在 FINISHED 状态不跳转。 */ /* BOOT 协议(已对照电机 BOOT OTA.cpp line 430-436 确认): * 最后一包(IFEND=0xFFFFFFFF)后,BOOT 发 ACK(0xFFFFFFFF) 并立即 jumpToApplication() * 自行跳转 APP,不需要上位机再发 FINISH_OTA。 * 旧注释里"0xF0F0F0F0 表示等 FINISH_OTA"是错误假设——电机 BOOT 代码里根本 * 没有 0xF0F0F0F0,尾帧 ACK 就是 0xFFFFFFFF,发完即跳转。 * 收到 0xFFFFFFFF = 电机已跳转,但不证明新 APP 能跑 → 进 kWaitAppRun * 等新 APP 上电自主发的 [0A AC] 运行确认(协议表 15/16 行),收到才 kFinished。 * 此阶段绝不能主动发 [0x0A,0xAC]——会被已运行的电机 APP 当作握手开始信号干扰。 */ if (next_pkt == 0xFFFFFFFFU) { /* 末包已被电机确认:把进度钉到 total,便于 QUERY/网页识别 100%。 */ s_packets_acked = s_total_packets; s_last_acked_pkt = s_total_packets; s_pkt_pending = false; LOG_ERR("[MOTOR-OTA] tail-ACK pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu, wait APP-run 0AAC\r\n", (unsigned)s_packets_acked, (unsigned)s_total_packets, (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count, (unsigned long)s_ack_resend_total, (unsigned long)s_fast_resend_total, (unsigned long)SysTickTimer::getMs()); enterState(State::kWaitAppRun); pushStatus(); return; } /* 兼容:若未来 BOOT 改为回 0xF0F0F0F0 等待 FINISH_OTA,再发 [0x0A,0xAC]。 * 当前电机 BOOT 不会走到这里,保留分支以防协议变体。 */ if (next_pkt == 0xF0F0F0F0U) { const uint8_t finish_cmd[2] = {0x0A, 0xAC}; canSend(kOtaReqId, finish_cmd, 2); s_last_send_ms = SysTickTimer::getMs(); enterState(State::kFinishOta); return; } const uint16_t next16 = static_cast(next_pkt); /* 重复/未前进 ACK:BOOT auto_retrans=ENABLE,relay 侧 CAN 层没回 ACK 时会重传 * “同一个 next_pkt”的 ACK 帧。next_pkt 未前进时必须整帧忽略——绝不能清 * s_pkt_pending:否则会把刚发出、尚未真正确认的在途包误判为已确认,下一次 * onTxTick 多 bufPop 一包却仍沿用旧包号发送 → 包号/数据错位、CRC 错累积, * 正是“看着有很多重复发送”的根源。忽略后在途包的真确认(next_pkt 前进)自然到来。 */ if (s_last_acked_pkt != 0xFFFFU && next16 <= s_last_acked_pkt) { /* 重复/未前进 ACK:只累计计数,不逐帧打印(量极大会打爆 UART 扰乱时序)。 * 汇总见每 32 包进度行的 dup=。 */ s_dup_ack_count++; return; } s_packets_acked = next16; s_last_acked_pkt = next16; s_crc_retry_count = 0; /* 正常前进 ACK,清零 CRC 重试计数 */ s_crc_resend_pending = false; s_pkt_pending = false; /* 本包确认;下一包由主循环 onTxTick 按钟表间隔发出 */ /* 每 32 包只打串口汇总,不再 pushStatus(0xD2)。 * 传输期 ESP32 suppress ring,0xD2 读不到还占 spi_ready/ring, * 与 0xD1 喂数抢总线,加重缓冲欠载(CANlog 可见周期性 ~500ms 空洞)。 * 进度由 ESP32 feedTask 按已喂字节估算;终态再 push。 */ if ((s_packets_acked & 0x1F) == 0) { OTA_LOGI("[MOTOR-OTA] progress pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu\r\n", (unsigned)s_packets_acked, (unsigned)s_total_packets, (unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count, (unsigned long)s_ack_resend_total, (unsigned long)s_fast_resend_total, (unsigned long)SysTickTimer::getMs()); } } /* ═══════════════════ 公开接口 ═══════════════════ */ void init() { /* 无额外硬件初始化;CAN 已由 CanFd 模块管理 */ } /* 数据发送推进:由主循环 TaskRunner 每圈调用(不再走 ISR 计时 + TASK 标志)。 * 全部间隔判定用 SysTick 钟表(s_last_send_ms),与任务调度器的定时器分频解耦, * 从根上消除 s_gap_div_100us 被 ISR(++)与主循环(=0)同时读写的竞争。 * 首包等 BOOT 擦除完成(kEraseWaitMs,从进 kTransfer 起算),后续包按 kTransferGapMs。 */ void onTxTick() { if (s_state != State::kTransfer) return; const uint32_t now = SysTickTimer::getMs(); /* ACK 超时重发:包已发出(s_pkt_pending=true)但超过 kAckResendMs 未收到 ACK,重发。 * 场景:OTA 期间 ARD=1 单发,总线错误(如 STUFF)直接击落数据帧/ACK 丢失, * GD32 等 ACK、电机等下一包 → 双方死锁。 * ★ 注意:检查 s_pkt_pending(包已发等ACK),不是 s_ack_pending(ACK已到等处理)! * 重发后电机幂等处理(同包号只 ACK 不重复写 flash)。 */ if (s_pkt_pending && !s_ack_pending && !s_crc_resend_pending) { /* 快速路径:TX 完成中断已确认当前帧被总线错误击落(MTE),免等 * kAckResendMs 盲等,按 kTransferGapMs 节流立即重发。先判 gap 再 consume, * 避免过早清标志丢事件;重发次数与超时路径共用 kAckMaxResends 上限。 */ if ((now - s_last_send_ms) >= kTransferGapMs && s_retry_count < kAckMaxResends && CanFd::consumeTxError(s_channel)) { s_retry_count++; s_fast_resend_total++; const uint32_t err = CanFd::readChannelErrorReg(s_channel); LOG_ERR("[MOTOR-OTA] TX-err fast-resend pkt=%u retry=%u tec=%lu rec=%lu errn=%lu\r\n", (unsigned)s_packets_acked, (unsigned)s_retry_count, (unsigned long)((err >> 16) & 0xFFU), (unsigned long)((err >> 24) & 0xFFU), (unsigned long)((err >> 4) & 0x07U)); sendCurrentPacket(); s_last_send_ms = SysTickTimer::getMs(); return; } if (now - s_last_send_ms >= kAckResendMs) { if (s_retry_count < kAckMaxResends) { s_retry_count++; s_ack_resend_total++; /* 事件时刻 CAN_ERR 快照:钉死丢帧时总线状态(TEC 涨=数据帧发丢, * REC 高=接收方向损坏),摆脱 1Hz [CAN-BUS] 采样混叠。仅日志,不做发送门控。 */ const uint32_t err = CanFd::readChannelErrorReg(s_channel); LOG_ERR("[MOTOR-OTA] ACK timeout resend pkt=%u retry=%u tec=%lu rec=%lu errn=%lu\r\n", (unsigned)s_packets_acked, (unsigned)s_retry_count, (unsigned long)((err >> 16) & 0xFFU), (unsigned long)((err >> 24) & 0xFFU), (unsigned long)((err >> 4) & 0x07U)); sendCurrentPacket(); s_last_send_ms = SysTickTimer::getMs(); } /* 超过最大重试次数后由 process() 的整体超时处理 */ } return; } /* CRC 错误重发:发同一包(s_pkt_plain 不变、不 bufPop),按 kTransferGapMs 节流; * 优先于 s_pkt_pending 门,否则忽略重复 ACK 后 s_pkt_pending 长期为真会堵死重发。 */ if (s_crc_resend_pending) { if (now - s_last_send_ms < kTransferGapMs) return; s_crc_resend_pending = false; sendCurrentPacket(); s_last_send_ms = SysTickTimer::getMs(); return; } if (s_pkt_pending) return; /* 当前包待确认,不发新包 */ /* 首包(s_packets_acked==0)等 kEraseWaitMs 让 BOOT 擦完,后续包按 kTransferGapMs。 */ const uint32_t need = (s_packets_acked == 0U) ? kEraseWaitMs : kTransferGapMs; if (now - s_last_send_ms < need) return; /* 水位门控:SPI 喂数跟不上时暂停 CAN,避免抽干环缓后电机 1s 超时。 * 首包要求更高水位(开传缓冲);运行中维持低水位即可。 */ const uint16_t water = (s_packets_acked == 0U) ? kStartWaterBytes : kRunWaterBytes; const uint16_t used = bufUsed(); /* 末包:只剩 last_valid 字节时放行 */ const bool is_last = (s_packets_acked + 1U >= s_total_packets); const uint16_t last_need = static_cast( (s_fw_size % kPacketPayload) == 0U ? kPacketPayload : (s_fw_size % kPacketPayload)); /* 尾段:SPI 已喂完全部固件(fed>=fw_size)时缓冲里就是剩余全部数据, * 剩余字节可能 < water(如 248<256),必须旁路水位门控,否则尾段死锁 * → 电机 BOOT 1s 无包超时。 */ const bool all_fed = (s_fw_fed >= s_fw_size); if (!is_last && !all_fed && used < water) { static uint32_t s_last_water_ms = 0; if (now - s_last_water_ms >= 200U) { OTA_LOGI("[MOTOR-OTA] wait-SPI-level used=%u need=%u fed=%lu pkt=%u\r\n", (unsigned)used, (unsigned)water, (unsigned long)s_fw_fed, (unsigned)s_packets_acked); s_last_water_ms = now; } return; } if (is_last && used < last_need) { return; /* sendNextPacket 内也会等,这里省一次调用 */ } sendNextPacket(); /* 内部设 s_last_send_ms */ } void process() { if (s_state == State::kIdle || s_state == State::kFinished || s_state == State::kError) { return; } /* kPreCancel:等待电机 BOOT 心跳(0x103)确认已进入 Bootloader。 * 电机 BOOT IDLE 每 ~10ms 发一次 0x103;收到即证明电机已在 BOOT IDLE, * 直接进握手(残帧窗口已移除,握手阶段对干扰帧忽略+步进重发兜底)。 */ if (s_state == State::kPreCancel) { if (s_boot_heartbeat_seen) { OTA_LOGI("[MOTOR-OTA] 0x103 ok, handshake\r\n"); enterState(State::kHandshakeReq); pushStatus(); } else if (stateTimedOut(kHandshakeTimeoutMs)) { LOG_ERR("[MOTOR-OTA] wait 0x103 timeout, motor not in BOOT\r\n"); setError(0x02); /* 0x02 = 电机未进入 BOOT */ } return; } /* kWaitAppRun:尾包 ACK 后电机已复位跳 APP,等新 APP 自主发的 [0A AC] 运行确认。 * GD32 此阶段不发任何帧(不能走下方首发/补发路径,否则 sendCurrentStep 空转 * kMaxResends 次后误报 0x3A);启动期杂帧一律忽略,只认 [0A AC]。 * kAppRunTimeoutMs 无确认 → error 0x25(新 APP 未启动)。 */ if (s_state == State::kWaitAppRun) { if (s_ack_pending) { s_ack_pending = false; uint8_t ack_data[64]; memcpy(ack_data, s_ack_data, 64); const uint8_t ack_len = s_ack_len; if (ack_len >= 2 && ack_data[0] == 0x0A && ack_data[1] == 0xAC) { LOG_ERR("[MOTOR-OTA] APP-run 0AAC ok, FINISHED t=%lu\r\n", (unsigned long)SysTickTimer::getMs()); enterState(State::kFinished); pushStatus(); return; } /* 非 [0A AC](APP 启动期其他帧)——忽略继续等 */ } if (stateTimedOut(kAppRunTimeoutMs)) { LOG_ERR("[MOTOR-OTA] no APP-run 0AAC in %lums, new APP not running\r\n", (unsigned long)kAppRunTimeoutMs); setError(0x25); /* 0x25 = 刷写传完但新 APP 未发运行确认 */ } return; } /* 超时检查 */ const uint32_t timeout = (s_state == State::kTransfer || s_state == State::kFinishOta) ? kAckTimeoutMs : kHandshakeTimeoutMs; if (stateTimedOut(timeout)) { setError(0x01); /* 超时 */ pushStatus(); return; } /* ===== ACK 优先处理 ===== */ if (s_ack_pending) { s_ack_pending = false; uint8_t ack_data[64]; uint8_t ack_len; memcpy(ack_data, s_ack_data, 64); ack_len = s_ack_len; /* 日志:握手步骤全打印;传输期错误帧限频 */ bool rx_do_log = (s_state != State::kTransfer); if (s_state == State::kTransfer && ack_len == 1) { static uint32_t s_last_rxerr_ms = 0; const uint32_t rnow = SysTickTimer::getMs(); if (rnow - s_last_rxerr_ms >= 200U) { rx_do_log = true; s_last_rxerr_ms = rnow; } } if (rx_do_log) { OTA_LOGI("[MOTOR-OTA] RX %s pkt=%u/%u len=%u b0=%02X b1=%02X t=%lu\r\n", stepName(s_state), (unsigned)s_packets_acked, (unsigned)s_total_packets, (unsigned)ack_len, (ack_len > 0) ? ack_data[0] : 0xFFU, (ack_len > 1) ? ack_data[1] : 0xFFU, (unsigned long)SysTickTimer::getMs()); } switch (s_state) { case State::kHandshakeReq: if (ack_len >= 2 && ack_data[0] == 0x00 && ack_data[1] == 0xAC) { /* echo 正确 —— 立即发 nonce(与 PC 工具一致,不给干扰帧窗口) */ generateNonce(s_nonce); enterState(State::kHandshakeNonce); canSend(kOtaReqId, s_nonce, 16); s_last_send_ms = SysTickTimer::getMs(); OTA_LOGI("[MOTOR-OTA] TX HANDSHAKE_NONCE(0x100:16B) [ACK-driven] t=%lu\r\n", (unsigned long)s_last_send_ms); } /* 不匹配的 ACK 靠掉 —— 忽略,等超时重发 REQ */ break; case State::kHandshakeNonce: if (ack_len < 36) { /* 短帧(0x05/干扰) —— 忽略,等真正 48B 或超时重发 */ LOG_ERR("[MOTOR-OTA] NONCE ACK ignore short frame len=%u b0=0x%02X\r\n", (unsigned)ack_len, (ack_len > 0) ? ack_data[0] : 0xFFU); break; } if (memcmp(ack_data + 12, s_nonce + 8, 8) != 0) { /* nonce echo 不匹配 —— 电机收到了错误的帧当 nonce */ LOG_ERR("[MOTOR-OTA] NONCE ACK echo mismatch, ignore\r\n"); break; } /* ✔ nonce 响应正确 —— 派生 K_session,立即发 confirm */ aes128_set_key(&s_aes_ctx, K_MASTER); aes128_encrypt_block(&s_aes_ctx, s_nonce, s_k_session); s_session_num = static_cast(s_nonce[0]) | (static_cast(s_nonce[1]) << 8) | (static_cast(s_nonce[2]) << 16) | (static_cast(s_nonce[3]) << 24); { enterState(State::kHandshakeConfirm); uint8_t plain[16]; for (uint8_t i = 0; i < 16; i++) plain[i] = s_nonce[i] ^ 0x55; uint8_t confirm[16]; aes128_set_key(&s_aes_ctx, s_k_session); aes128_encrypt_block(&s_aes_ctx, plain, confirm); canSend(kOtaReqId, confirm, 16); s_last_send_ms = SysTickTimer::getMs(); OTA_LOGI("[MOTOR-OTA] TX HANDSHAKE_CONFIRM(0x100:16B) [ACK-driven] t=%lu\r\n", (unsigned long)s_last_send_ms); } break; case State::kHandshakeConfirm: if (ack_len >= 2 && ack_data[0] == 0xBA && ack_data[1] == 0xC0) { /* ✔ 握手成功 —— 立即发 fw_size */ enterState(State::kErase); uint8_t size_le[4]; size_le[0] = static_cast(s_fw_size & 0xFF); size_le[1] = static_cast((s_fw_size >> 8) & 0xFF); size_le[2] = static_cast((s_fw_size >> 16) & 0xFF); size_le[3] = static_cast((s_fw_size >> 24) & 0xFF); canSend(kOtaDataId, size_le, 4); s_last_send_ms = SysTickTimer::getMs(); OTA_LOGI("[MOTOR-OTA] TX ERASE(0x101:fw_size) [ACK-driven] t=%lu\r\n", (unsigned long)s_last_send_ms); } /* 非 [BA C0] —— 忽略(可能是 0x03/0x05 干扰帧) */ break; case State::kErase: if (ack_len >= 6 && ack_data[0] == 0xBA && ack_data[1] == 0xC0) { /* ✔ 擦除完成 —— 进入传输态,等 kEraseWaitMs 后由 onTxTick 发首包 */ s_packets_acked = 0; enterState(State::kTransfer); pushStatus(); /* 清空 CAN TX 邮箱:握手期间快速连发4帧(REQ+nonce+confirm+fw_size), * 可能有帧卡在邮箱未发出,进入 kTransfer 后才发出 → 电机收到 * 过时的 0x100 帧 → catch-all → [05] 杀死 OTA。强制 abort 降低风险。 */ CanFd::abortPendingTx(s_channel); s_last_send_ms = SysTickTimer::getMs(); OTA_LOGI("[MOTOR-OTA] ERASE ACK ok, wait %ums then first pkt\r\n", (unsigned)kEraseWaitMs); } /* 非 [BA C0 ...] —— 忽略,等超时重发 fw_size */ break; case State::kTransfer: processTransferAck(ack_data, ack_len); break; case State::kFinishOta: if (ack_len >= 2 && ack_data[0] == 0x0A && ack_data[1] == 0xAC) { enterState(State::kFinished); pushStatus(); } else { /* BOOT 可能回 WAIT 表示还在处理,重置超时 */ s_state_enter_ms = SysTickTimer::getMs(); } break; default: break; } return; /* ACK 已处理,本轮结束 */ } /* ===== 无 ACK —— 首发或超时重发 ===== */ if (s_state == State::kTransfer) { return; /* kTransfer 由 onTxTick 驱动 */ } const uint32_t now_ms = SysTickTimer::getMs(); if (s_last_send_ms == 0U) { /* 首发(仅 kHandshakeReq 初始进入时走这里,其余步骤由 ACK handler 触发发帧) */ sendCurrentStep(now_ms); } else if ((now_ms - s_last_send_ms) >= kStepTimeoutMs) { /* 超时重发 */ if (s_retry_count >= kMaxResends) { LOG_ERR("[MOTOR-OTA] %s no-ACK after %u tries, abort\r\n", stepName(s_state), (unsigned)(kMaxResends + 1U)); setError(static_cast(0x30U + static_cast(s_state))); pushStatus(); return; } s_retry_count++; sendCurrentStep(now_ms); } } bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) { if (s_state == State::kIdle || s_state == State::kFinished || s_state == State::kError) { return false; } if (channel != s_channel) return false; /* BOOT 的 0x103 帧语义修正: * 本电机 BOOT 仅在 IDLE 状态每 ~10ms 发一次 0x103 心跳(见 OTA.cpp process()), * 擦除/传输忙时并不发 0x103。握手阶段(kHandshakeReq/Nonce/Confirm)收到 0x103 * 只说明电机仍停在 IDLE、尚未接受握手,此时绝不能重置超时——否则握手失败时 * 超时会被 IDLE 心跳无限重置 → relay 静默死循环、UI 永远卡在"握手中"且无错误码。 * 仅在 kErase/kTransfer/kFinishOta(电机确在忙、WAIT 合法)才重置超时, * 使握手失败能在 kHandshakeTimeoutMs 后正常超时报错、可被取消/重试。 */ if (id == kOtaWaitId) { /* kPreCancel 期间:0x103 = 电机 BOOT IDLE 心跳,证明电机已进入 Bootloader */ if (s_state == State::kPreCancel) { s_boot_heartbeat_seen = true; } if (s_state == State::kErase || s_state == State::kTransfer || s_state == State::kFinishOta) { s_state_enter_ms = SysTickTimer::getMs(); } return true; /* 已消费,不再传递给其他 handler */ } if (id != kOtaAckId) return false; /* kPreCancel 期间丢弃残留 ACK(电机可能对上次 OTA 的帧补发 ACK), * 不置 s_ack_pending,确保进入 kHandshakeReq 时从干净状态开始。 */ if (s_state == State::kPreCancel) return true; /* 捕获 ACK:总是覆盖(处理最新帧)。 * 无需 s_last_send_ms guard——新模型里 ACK handler 触发后立即发下一帧, * s_last_send_ms 在任何 ACK 到达前已被设置。 */ /* 捕获 ACK */ const uint8_t copy_len = (len <= 64) ? len : 64; memcpy(s_ack_data, data, copy_len); s_ack_len = copy_len; s_ack_pending = true; 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; /* 已在进行中 */ } 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((fw_size + kPacketPayload - 1U) / kPacketPayload); s_packets_acked = 0; s_error = 0; s_session_num = 0; /* 由 processNonceAck 根据 s_nonce 前4字节设置 */ s_buf_head = 0; s_buf_tail = 0; s_ack_pending = false; s_last_send_ms = 0; /* 0=尚未首发,enterState(kHandshakeReq) 也会置 0,由 process() 首发 00AC */ s_crc_retry_count = 0; s_last_acked_pkt = 0xFFFFU; s_pkt_pending = false; s_dup_ack_count = 0; s_crc_err_count = 0; s_ack_resend_total = 0; s_fast_resend_total = 0; s_last_pad_valid = 0xFFFFU; /* 不在 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); s_boot_heartbeat_seen = false; enterState(State::kPreCancel); 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; } const uint32_t offset = static_cast(data[0]) | (static_cast(data[1]) << 8) | (static_cast(data[2]) << 16) | (static_cast(data[3]) << 24); const uint8_t *chunk = data + 4U; uint16_t chunk_len = static_cast(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(s_fw_size - offset); } 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; } /* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */ const uint32_t skip = s_fw_fed - offset; const uint16_t add_len = static_cast(chunk_len - skip); if (bufFree() < add_len) { return false; /* 环缓满 → SPI WAIT */ } bufPush(chunk + skip, add_len); s_fw_fed += add_len; return true; } void cancel() { if (s_state == State::kIdle) return; LOG_ERR("[MOTOR-OTA] CANCEL received in state %s\r\n", stepName(s_state)); /* ★ kFinished/kError/kWaitAppRun 时电机已跳转 APP 或已中止,绝不能再发 CAN 取消帧! * kOtaCancelId=0x00000000 与电机 APP 的 CANFD_BOOT_ID(base_id=0x0000) 冲突, * 电机 APP 收到会当作 BOOT 命令 → enter_silent_mode 10s,APP 看似卡死。 * kWaitAppRun = 尾包已确认、电机已复位跳 APP,同样不能发。 * 仅在活跃传输态(kPreCancel..kFinishOta)发取消帧通知电机 BOOT 中止。 */ const bool was_active = (s_state != State::kFinished && s_state != State::kError && s_state != State::kWaitAppRun); if (was_active) { canSend(kOtaCancelId, nullptr, 0); } enterState(State::kIdle); s_buf_head = 0; s_buf_tail = 0; s_fw_fed = 0U; s_pkt_pending = false; s_crc_retry_count = 0; pushStatus(); } Status getStatus() { Status st; st.state = s_state; st.error_code = s_error; st.packets_acked = s_packets_acked; st.total_packets = s_total_packets; st.fw_size = s_fw_size; return st; } bool isActive() { return (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError); } } /* namespace MotorOtaRelay */