Files
DCU_APP/GD32E508_DCU_APP/sys/motor_ota_relay.cpp
T
cbb 46ef19d882 feat(build): 优化构建配置并引入日志分级系统
- 将优化级别从balanced调整为size,减少Flash占用
- 添加hardfault.cpp文件以增强故障诊断能力
- 关闭RTTI和异常处理以移除libcppabi运行时(节省约2.5KB Flash)
- 添加链接器参数输出未引用段与总量报告用于Flash尺寸分析

refactor(logging): 统一日志输出并实现编译时日志分级

- 将printf调用替换为模块化的日志宏(ECAT_LOGI、CAN_LOGI等)
- 实现CONFIG_*_LOG_ENABLE编译开关控制各模块INFO级日志
- 错误和关键事件使用LOG_ERR恒定保留,其他日志可编译时裁剪
- 保持原有日志内容不变,但支持链接器自动回收未使用的格式字符串
2026-07-27 01:49:18 +08:00

1097 lines
53 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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 <cstring>
#include <cstdio>
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<uint16_t>((s_buf_head - s_buf_tail + kBufSize) % kBufSize);
}
static inline uint16_t bufFree() {
return static_cast<uint16_t>(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<uint16_t>((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<uint16_t>((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<uint8_t>((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<uint8_t>(crc & 0xFFU);
output[1] = static_cast<uint8_t>((crc >> 8) & 0xFFU);
output[2] = static_cast<uint8_t>((crc >> 16) & 0xFFU);
output[3] = static_cast<uint8_t>((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<uint8_t>(s_state);
payload[1] = s_error;
payload[2] = static_cast<uint8_t>(s_packets_acked & 0xFF);
payload[3] = static_cast<uint8_t>(s_packets_acked >> 8);
payload[4] = static_cast<uint8_t>(s_total_packets & 0xFF);
payload[5] = static_cast<uint8_t>(s_total_packets >> 8);
payload[6] = static_cast<uint8_t>(s_fw_size & 0xFF);
payload[7] = static_cast<uint8_t>((s_fw_size >> 8) & 0xFF);
payload[8] = static_cast<uint8_t>((s_fw_size >> 16) & 0xFF);
payload[9] = static_cast<uint8_t>((s_fw_size >> 24) & 0xFF);
payload[10] = static_cast<uint8_t>(bufUsed() & 0xFF);
payload[11] = static_cast<uint8_t>(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;
/* 预取消等待(ms):发 CANCEL 后等电机 BOOT 主循环处理完(复位到 IDLE),再进握手。
* 电机 BOOT 主循环周期 ~1-10ms,100ms 留足余量。 */
static constexpr uint32_t kBootSettleMs = 100U; /* 检测到0x103心跳后再等这么久,让电机APP残留[00FA]CAN帧先消耗完
* 原 2000ms→800ms→100ms:0x103 出现即电机已在 BOOT IDLE,
* 残帧实测 <4 帧(~40ms),100ms 足够。 */
/* 步骤名,用于“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)";
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<uint8_t>(s_fw_size & 0xFF);
size_le[1] = static_cast<uint8_t>((s_fw_size >> 8) & 0xFF);
size_le[2] = static_cast<uint8_t>((s_fw_size >> 16) & 0xFF);
size_le[3] = static_cast<uint8_t>((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<uint8_t>(s_session_num & 0xFF);
frame[1] = static_cast<uint8_t>((s_session_num >> 8) & 0xFF);
frame[2] = static_cast<uint8_t>((s_session_num >> 16) & 0xFF);
frame[3] = static_cast<uint8_t>((s_session_num >> 24) & 0xFF);
/* packet_num LE */
frame[4] = static_cast<uint8_t>(pkt_num & 0xFF);
frame[5] = static_cast<uint8_t>((pkt_num >> 8) & 0xFF);
frame[6] = static_cast<uint8_t>((pkt_num >> 16) & 0xFF);
frame[7] = static_cast<uint8_t>((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<uint16_t>(
(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<uint16_t>((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<uint32_t>(data[2])
| (static_cast<uint32_t>(data[3]) << 8)
| (static_cast<uint32_t>(data[4]) << 16)
| (static_cast<uint32_t>(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,relay 直接进 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] FINISHED 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());
enterState(State::kFinished);
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<uint16_t>(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<uint16_t>(
(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。
* 但不立即握手——电机 APP 跳 BOOT 前可能在 CAN TX 队列残留 [00 FA](fail)帧,
* 这帧延迟发到总线会被 BOOT 的 handleLinkMessage catch-all 命中
* → 重置状态机 → 0x05。等 kBootSettleMs 让残帧在 BOOT IDLE 期间消耗完,
* 再开始握手就不会被干扰。 */
if (s_state == State::kPreCancel) {
if (s_boot_heartbeat_seen && stateTimedOut(kBootSettleMs)) {
OTA_LOGI("[MOTOR-OTA] 0x103 ok, residue-win %ums passed, handshake\r\n",
(unsigned)kBootSettleMs);
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;
}
/* 超时检查 */
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<uint32_t>(s_nonce[0])
| (static_cast<uint32_t>(s_nonce[1]) << 8)
| (static_cast<uint32_t>(s_nonce[2]) << 16)
| (static_cast<uint32_t>(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<uint8_t>(s_fw_size & 0xFF);
size_le[1] = static_cast<uint8_t>((s_fw_size >> 8) & 0xFF);
size_le[2] = static_cast<uint8_t>((s_fw_size >> 16) & 0xFF);
size_le[3] = static_cast<uint8_t>((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<uint8_t>(0x30U + static_cast<uint8_t>(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<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;
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<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);
}
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<uint16_t>(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 时电机已跳转 APP 或已中止,绝不能再发 CAN 取消帧!
* kOtaCancelId=0x00000000 与电机 APP 的 CANFD_BOOT_ID(base_id=0x0000) 冲突,
* 电机 APP 收到会当作 BOOT 命令 → enter_silent_mode 10s,APP 看似卡死。
* 仅在活跃传输态(kPreCancel..kFinishOta)发取消帧通知电机 BOOT 中止。 */
const bool was_active = (s_state != State::kFinished && s_state != State::kError);
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 */