案例 15265:研发纠正现象下的动力链失控分析报告
| 项 | 内容 |
|---|---|
| 轨迹 ID | 15265 |
| 固件分支 | JetWaveECU-feature_motorstuck(ESC 0551 / ECU 0022) |
| OSS | 明细 |
| 分析页 | https://manager.jetwave.cn/motion/trajectoryHistory/analysis/15265 |
| 关联 | STALL_CASE_15265.md、JETWAVE_ECU_POWERTRAIN_DEEP_ANALYSIS_REPORT_feature_motorstuck.md |
〇、研发纠正后的现象表述(本报告唯一对齐口径)
| # | 研发确认现象 | 技术含义 |
|---|---|---|
| P1 | 遥控器油门为 0 | App/BLE 侧 ThrottleAndGear.throttle == 0 |
| P2 | MotorSpeedControlTask 内 MotorSpeed == 0 |
ECU 指令意图 为零转速/停转 |
| P3 | ECU 与 ESC 的 CAN 总线通讯异常 | ecuEventCode & FAULT_ESC_COMMUNICATION (0x40000000),队列长期收不到 EscRunningData |
| P4 | ESC 仍控制电机满功率运行 | 物理层观测;主机侧已无法通过 CAN 有效约束 ESC |
| P5 | 失控前 ECU 偶发收到 ESC 异常转速(如 -727678635 等) | libcanard 内存池异常 导致 esc_SetSpeedResponse_decode 解出非法 int32 speed;数值与 ECU→ESC 设速请求 CAN ID 0x00D4A081 同源,不是 物理 RPM |
本报告目标: 用 15265 日志 + 电调协议 5.1.4 + feature_motorstuck 源码,证明 P1–P5 可同时成立,交叉验证 canard 内存池根因,并给出控制流转、风险与修复(含伪代码)。
版本: v1.2(2026-05-28)— 全文审校;修正伪代码 API 错误(pool_peak_usage / twai_in_bus_off / SendSetSpeedRequest 返回类型);新增 §5.4 队列返回值 bug;精确化 §5.3 内存池机制描述;补充 §9.0 锁竞争分析、§9.1 streak 联动、§9.4 ESC_STALL_MASK 定义、§十 T-RND-0。
一、结论(给研发/售后)
- 15265 与研发表述一致,且不矛盾: 失控末段(10:35:01 起)轨迹上 油门连续为 0、ECU 已置 ESC 通信故障;按
MotorSpeedControlTask逻辑,此时MotorSpeed必为 0 且仍周期调用SendSetSpeedRequest(0),但 CAN 已异常,零速指令无法可靠送达 ESC,ESC 可保持失控前最后一档 PWM(研发确认满功率)。 - 日志里的
motorSpeed=11175、electricCurrent=69.2A在通信故障段是「冻结快照」(49 个点数值完全相同),不能当作实时转速/电流;但 冻结前一刻(10:34:54–56)确为 thr=1000 + 69.2A + motor≈11175,说明失控前主机曾收到过 大电流/高转速反馈。 - 异常转速簇
-727678xxx在 OSS 中共 13 点,与研发列举数值 完全吻合;其uint32表示 稳定落在0xD4A081xx,与电调协议 5.1.4 设速请求示例 CAN ID0x00D4A081前三字节 D4 A0 81 一致——可基本判定为 libcanard 内存池问题,使 TX 请求帧 ID/池内残留 被 RX 响应解码 误读为int32 speed(见 §五)。 - 根因分层:
- 感知层(P5): canard 池损坏 → 偶发异常反馈 →
UpdateESCErrorCodeByRunningStatus误置位且仍上行; - 执行层(P3/P4): 池耗尽/总线错误后 零速 SetSpeed 发不出或 ESC 不收 → 电机保持满功率;
- 不是「ECU
MotorSpeedControlTask仍算出非零转速」。 - 项目逻辑缺口(与池问题叠加): RX/TX 未共锁、池仅 2048B、反馈 无范围校验、通信故障后 仍信陈旧
latestEscRunningData。
二、概念区分(读日志前必须看)
| 名称 | 来源 | 案例中的值 |
|---|---|---|
MotorSpeed(指令) |
MotorSpeedControlTask 局部变量 → SendSetSpeedRequest |
油门 0 时为 0(P2) |
LastEscSpeedData.speed(反馈) |
CAN esc_SetSpeedResponse.speed → UpdateEscSpeedInfo |
正常约 ±15000;异常 -727678720;通信故障后 冻结 11175 |
轨迹字段 motorSpeed |
ECU 心跳 motor_speed(= 反馈,不是指令) |
≠ MotorSpeed;断联后易误导为「还在转」 |
```386:386:JetWaveECU-feature_motorstuck/App/src/esc_manager.c UlogWriteRcMessage(ThrottleAndGear.throttle, ThrottleAndGear.gear, ThrottleAndGear.mode, MotorSpeed, ThrottleLostCnt, intervalUs);
```117:118:JetWaveECU-feature_motorstuck/App/src/ecu_manager.c
latestEcuHeatbeatData.motor_current = EscData.current;
latestEcuHeatbeatData.motor_speed = EscData.speed;
三、系统任务与数据流(正常)
flowchart TB
subgraph RC["遥控 App / BLE"]
TG[ThrottleAndGear MAVLink]
end
subgraph ECU_Tasks["ECU FreeRTOS"]
MST[MotorSpeedControlTask 20ms]
PET[ProcessEscDataTask]
EMT[EcuManagerTask 50ms]
end
subgraph CAN["TWAI / UAVCAN"]
TX[SendSetSpeedRequest]
RX[HandleEscSetSpeedResponse]
end
subgraph ESC_HW["电调 ESC 0551"]
MOT[电机 PWM]
end
TG -->|RcCmd 队列| MST
MST -->|MotorSpeed| TX
TX --> CAN
CAN --> RX
RX -->|10:1 入队| Q1[EscSpeedResponseQueue]
Q1 --> PET
PET --> LES[LastEscSpeedData]
LES --> Q2[EscRunningDataQueue]
Q2 --> EMT
EMT --> HB[ECU Heartbeat → App/OSS]
CAN --> MOT
MOT -->|SetSpeedResponse| CAN
正常周期:
MotorSpeedControlTask:每 20ms 读油门 → 算MotorSpeed→SendSetSpeedRequest。ProcessEscDataTask:收 CAN 响应 →UpdateEscSpeedInfo→SendEscRunningDataToQueue。EcuManagerTask:每 50ms 尝试ReceiveEscRunningDataFromQueue;成功则刷新latestEscRunningData并组心跳。
四、15265 分阶段时间轴(UTC)
| 阶段 | 时间 | 索引 | 日志特征 | 对应现象 |
|---|---|---|---|---|
| A 正常 | 09:34–10:09 | 0–2084 | 无 STALL | — |
| B STALL | 10:09:34 起 | 2085+ | esc=0x20000 |
ESC 失速保护 |
| C 池污染反馈 | 10:10:20–10:34:09 | 2131…3560 | 13 点 motor=-727678xxx(0xD4A081xx) |
P5 / canard 池异常前兆 |
| D 高功率末帧 | 10:34:54–56 | 3605–3607 | thr=1000, motor=11175, cur=69.2A, STALL | 失控前最后一帧有效反馈 |
| E 用户收油 | 10:34:57–11 | 3608–3611 | thr=0, motor/cur 仍 11175/69.2 | P1;指令将变 P2 |
| F CAN 异常 | 10:35:01 起 | 3612–3702 | ecu=0x40000000,49 点 motor/cur 恒为 11175/69.2 |
P3;轨迹为 冻结快照 |
| G 物理满功率 | 阶段 E–F(研发现场确认) | — | 主机 P2+P1,ESC P4 | 执行层脱钩 |
阶段 E→F 关键 8 行(与 P1–P4 一一对应):
| 时间 | thr | motor(反馈) | cur | esc | ecu |
|---|---|---|---|---|---|
| 10:34:54 | 1000 | 11175 | 69.2 | STALL | 0 |
| 10:34:57 | 0 | 11175 | 69.2 | STALL | 0 |
| 10:35:00 | 0 | 11175 | 69.2 | STALL | 0 |
| 10:35:01 | 0 | 11175 | 69.2 | 0 | 0x40000000 |
从 10:34:57 起油门已为 0;10:35:01 ECU 判定 ESC 通信故障——与研发 「油门 0 + CAN 异常 + ECU 认为在发 0」 对齐。
五、异常转速簇(P5)— 协议 5.1.4 × canard 内存池 × 日志交叉验证
5.1 电调协议 5.1.4(设速)与固件 DSDL 对齐
| 协议字段 | 协议文档值 | 工程代码 |
|---|---|---|
| Data ID | 212 | ESC_SETSPEED_REQUEST_ID / ESC_SETSPEED_RESPONSE_ID == 212 |
| Data Signature | 0x4461BB2B55B3F410 | ESC_SETSPEED_*_SIGNATURE 一致 |
| 请求 payload | int32 speed(rpm) |
SendSetSpeedRequest → pkt.set_speed |
| 响应 payload | 第 3 字段 int32 speed(rpm) |
esc_SetSpeedResponse.speed → LastEscSpeedData.speed |
协议示例(Priority 0,主机 ID=1,ESC ID=0x20,设速 504 rpm):
| 方向 | 协议示例 CAN ID | 说明 |
|---|---|---|
| 请求(ECU→ESC) | 0x00D4A081 |
MotorSpeedControlTask 每 20ms 触发,总线上最繁忙的 ID 之一 |
| 响应(ESC→ECU) | 0x00D401A0 起(多帧) |
HandleEscSetSpeedResponse 解码对象 |
504 rpm 的请求数据示例:0xF8 0x01 0x00 0x00(小端 int32)+ CRC 尾字节 —— 与异常簇 0xD4A081xx 无直接关系,说明异常值 不是 把 504 误读,而是 把 CAN ID 片段当成了 speed 字段。
5.2 日志十六进制与 0x00D4A081 重合(核心证据)
研发列举值与 15265 OSS 13 点逐一命中。将其按 uint32 展开:
异常 int32(样例) |
十六进制 | 字节序列(高→低) | 与请求 CAN ID 关系 |
|---|---|---|---|
| -727678720 | 0xD4A08100 |
D4 A0 81 00 | 与 0x00D4A081 同族 |
| -727678635 | 0xD4A08155 |
D4 A0 81 55 | 同上,低字节为池内相邻脏数据 |
| -727678551 | 0xD4A081A9 |
D4 A0 81 A9 | 同上 |
结论(与研发一致): 异常转速 不是 随机总线噪声,而是 ECU 发给 ESC 的设速请求 CAN ID(D4A081)渗入 canard 解析结果。正常物理转速不可能稳定落在 -727678000 量级。
反证: 若为 响应 ID 污染,应更接近协议中的 0x00D401A0 模式;实测异常簇集中在 D4A081,指向 TX 请求路径/内存池残留,而非 ESC 响应 ID 本身。
5.3 项目逻辑:canard 内存池与并发(根因机制)
```31:35:JetWaveECU-feature_motorstuck/MiddleWare/src/twai_ops_esp32.c static CanardInstance canard; EXT_RAM_BSS_ATTR static uint8_t memory_pool[2048] = {0}; // 内存池 2KB static CanardTxTransfer tx_transfer;
| 路径 | 任务 | 是否持 `canardTransmitMutex` | 行为 |
|------|------|------------------------------|------|
| **TX** | `MotorSpeedControlTask` 等 → `canardRequestWithMutex` | **是**(10ms 超时) | 每 20ms `SendSetSpeedRequest` → 池内分配 TX 描述符,CAN ID **`0x00D4A081`** |
| **RX** | `TwaiReceiveTask` → `canardHandleRxFrame` | **否** | 多帧拼 `SetSpeedResponse`(最大 33B),回调 `HandleEscSetSpeedResponse` |
```174:174:JetWaveECU-feature_motorstuck/MiddleWare/src/twai_ops_esp32.c
canardHandleRxFrame(&canard, &rxFrame, timeStamp); // 无互斥
推断链(与研发「canard 内存池出问题」一致):
- 2KB 池 在「20ms 周期 TX 设速 + 多帧 RX 遥测 + 其它服务」下易 耗尽/碎片化;
- RX/TX 未共锁,
canardHandleRxFrame与canardRequestOrRespondObj并发访问同一canard实例与池; - 池内链式块(libcanard 用链表管理的
CanardRxStatepayload 块)被提前回收或越界写入,导致多帧响应拼接时 payload 指针或 bit_offset 指向相邻 TX 描述符内存区域(该区域残留 TX 请求 CAN 扩展 ID,高 3 字节即D4 A0 81); esc_SetSpeedResponse_decode→_esc_SetSpeedResponse_decode调用canardDecodeScalar(transfer, *bit_ofs, 32, true, &msg->speed)时,实际读取的 payload 字节来自损坏块,而非 ESC 真实回复 —— 解码形式合法(字节足够、无 CRC 失败),但内容为 CAN ID 残留D4 A0 81 xx,解为有符号int32即-727678xxx;HandleEscSetSpeedResponse解码返回 false 即视为成功,且 无 rpm 范围校验 → 写入LastEscSpeedData→ 心跳/OSS 出现「假转速」。
```49:53:JetWaveECU-feature_motorstuck/Protocol/src/uavcan_twai.c if (esc_SetSpeedResponse_decode(transfer, &escSetSpeedResponseData)) { return; // true = 失败才 return;成功则继续 }
**时序:** 首次异常 **10:10:20**(STALL 后 46s),末次 **10:34:09**;共 13 次 **偶发**,符合 **池竞争/损坏间歇出现**,而非稳定传感器漂移。
### 5.4 异常反馈进入业务链(项目逻辑确认)
```494:503:JetWaveECU-feature_motorstuck/App/src/esc_manager.c
UpdateEscSpeedInfo(SpeedInfo); // speed 无校验
UpdateESCErrorCodeByRunningStatus(&LastEscSpeedData);
SendEscRunningDataToQueue(LastEscSpeedData, 0, pdMS_TO_TICKS(50));
```225:228:JetWaveECU-feature_motorstuck/App/src/esc_manager.c if (escSpeedInfo->speed <= -20000 || escSpeedInfo->speed >= 20000) { runningErrorState |= (uint16_t)(0x0001 << 6); // 误映射为过压类 esc_event }
- 异常 speed **仍会** 进入 `LastEscSpeedData` 并上行(仅多打错误 runningError 位,**不阻断**控制)。
- **10:1 反馈降频**(`sendCount >= 10`)使池上压力与异常窗口更难从日志对齐。
- 异常簇之后约 **25min** 出现 **通信故障 + 冻结 11175**(§七),符合 **池/总线逐步恶化** 路径。
### 5.5 <mark>新发现 bug:`SendEscSpeedResponseDataToQueue` 始终返回 `pdFAIL`</mark>
```301:323:JetWaveECU-feature_motorstuck/Protocol/src/protocol_common_interface.c
BaseType_t SendEscSpeedResponseDataToQueue(... ) {
if (EscSpeedResponseDataQueue != NULL) {
EscSetSpeedResponseData_t *data = calloc(...);
...
if (xQueueSend(...) != pdPASS) {
free(data);
return pdFAIL;
}
}
return pdFAIL; // ← 无论入队成功与否,均返回 pdFAIL
}
影响分析: 数据流本身不受影响(消费端 ProcessEscDataTask 通过 ReceiveEscSpeedResponseDataFromQueue 正常出队),但调用侧无法判断是否入队成功,异常转速帧在此处不会被拦截,且监控/日志统计失真。建议修复:入队成功路径 return pdPASS。
5.6 交叉验证小结
| 证据源 | 结论 |
|---|---|
| 协议 5.1.4 截图 | 请求 CAN ID = 0x00D4A081;响应 ID 不同 |
| 15265 OSS | 13 个异常值 全部 0xD4A081xx |
| 研发最新描述 | canard 内存池 + ID 重合 |
twai_ops_esp32.c |
池 2048B;RX 无锁 |
esc_manager.c |
解码后 不校验、不丢弃 |
六、P1+P2:油门 0 时 ECU 指令必为 0 的程序推导
MotorSpeedControlTask 在 mode=0/1/2 且 throttle==0 时:
```353:363:JetWaveECU-feature_motorstuck/App/src/esc_manager.c MotorSpeed = CurrentMaxSpeed * (throttleRatio / 1000.0); // 或倒挡公式 ... if (LastEscSpeedData.runningError & (uint16_t)(0x0001 << 9)) { MotorSpeed = 0; } SendSetSpeedRequest(MotorSpeed);
- 阶段 E(10:34:57 起)轨迹 **thr=0** → **`MotorSpeed == 0`**(P2)。
- **仅** 空转 bit9 会强制清零;**STALL (bit1) 不会**——但本阶段油门已为 0,指令同样为 0。
- `mode=254` 也会 `SendSetSpeedRequest(0)`;**`mode=255` 不发 CAN**(本会话未出现 255)。
```372:376:JetWaveECU-feature_motorstuck/App/src/esc_manager.c
case 255: // 蓝牙断链
ThrottleAndGear.throttle = 0;
MotorSpeed = 0;
break; // 无 SendSetSpeedRequest
七、P3+P4:CAN 异常时为何 ESC 仍可满功率
7.1 通信故障判定
```480:494:JetWaveECU-feature_motorstuck/App/src/ecu_manager.c if (ReceiveEscRunningDataFromQueue(&EscRunningData, 0) == pdPASS) { ... ESC_CommunicationFailure = 0; ecuEventCode &= ~FAULT_ESC_COMMUNICATION; } else if (GetRcConnectionStatus()) { ESC_CommunicationFailure ++; if (ESC_CommunicationFailure >= 240) { // 12S → 50ms×240 ecuEventCode |= FAULT_ESC_COMMUNICATION;
- **12s** 无新 ESC 数据才置位——15265 在 **10:35:01** 置位,说明此前已长时间收不到可靠反馈。
- 置位后 **`latestEscRunningData` 不清零**,心跳继续上报 **最后一帧**:
```560:560:JetWaveECU-feature_motorstuck/App/src/ecu_manager.c
UpdateEcuHeatbeatDataByDevice(latestEscRunningData, latestBmsRunningData, 0);
→ OSS 上 motor=11175、cur=69.2 恒定 49 次 = 冻结快照,不是实时采样。
7.2 「指令 0、ESC 满功率」机理链(含 canard 池恶化)
sequenceDiagram
participant MST as MotorSpeedControlTask
participant Pool as canard memory_pool 2KB
participant CAN as TWAI/UAVCAN
participant ESC as 电调
loop 每 20ms
MST->>Pool: TX SetSpeed 分配( CAN ID D4A081 )
MST->>CAN: SendSetSpeedRequest(MotorSpeed)
end
Note over Pool: 池耗尽/块复用 RX 与 TX 竞态
CAN->>Pool: RX 多帧 Response 拼接
Pool-->>MST: speed 误读为 0xD4A081xx
Note over MST: thr=0 → MotorSpeed=0
loop 每 20ms
MST->>CAN: SendSetSpeedRequest(0)
end
Note over CAN: Bus Off / mutex 超时 / 池失败
CAN--xESC: 零速帧丢失
Note over ESC: 保持最后一档非零 PWM
ESC->>ESC: 电机满功率 P4
辅助因素:
| 因素 | 代码位置 | 说明 |
|---|---|---|
| canard 池 2048B + RX 无锁 | twai_ops_esp32.c |
P5 根因;与 D4A081 异常簇一致 |
| 发送无确认 | SendSetSpeedRequest |
不检查 canardRequestWithMutex 返回值 |
| Mutex 仅保护 TX | twai_ops_esp32.c |
canardHandleRxFrame 未 纳入同一互斥 |
| Bus Off 只恢复驱动 | TwaiBusStatusCheckTask |
不 触发 ESC 安全停机 |
| STALL 不拉闸 | MotorSpeedControlTask |
失控前 STALL 段仍可能发过非零指令;末段虽为 0,ESC 可能已进入保护态不理会主机 |
| 反馈无校验 | HandleEscSetSpeedResponse |
0xD4A081xx 仍写入 LastEscSpeedData |
八、风险与隐患清单
| ID | 风险 | 严重度 | 15265 / 协议证据 |
|---|---|---|---|
| R0 | libcanard 内存池损坏/耗尽,TX CAN ID 渗入 RX speed |
致命 | 13 点 0xD4A081xx ≡ 协议请求 ID |
| R1 | 指令/反馈混用,心跳 motor_speed 误导 |
高 | 油门 0 仍见 11175 |
| R2 | 解码后无 speed 合法性/指纹校验 | 高 | 异常值仍进 LastEscSpeedData |
| R3 | 通信丢失后仍信陈旧 latestEscRunningData |
高 | 49 点完全相同 |
| R4 | 停板依赖 12s 报障,不依赖 500ms 无反馈 | 高 | 10:35:01 才置位 |
| R5 | SendSetSpeedRequest 失败静默 |
中 | 代码无处理 |
| R6 | 反馈 10:1 降频 | 中 | 池压力与异常更难追溯 |
| R7 | STALL 不禁止 SendSetSpeedRequest |
中 | 10:34:54 仍 thr=1000 |
| R8 | mode=255 不发零速 |
中 | 本会话未触发 |
| R9 | 转速异常置 bit6→过压事件混淆 | 低 | 诊断干扰 |
| R10 | RX/TX 并发访问 canard 实例无共锁 |
高 | canardHandleRxFrame 无 mutex |
| R11 | SendEscSpeedResponseDataToQueue 始终返回 pdFAIL(入队成功也如此) |
中 | protocol_common_interface.c 第 323 行;调用侧无法判断成功,监控失真 |
| R12 | SendSetSpeedRequest 丢弃 canardRequestWithMutex 返回值,发送失败无感知 |
高 | uavcan_twai.c 第 186 行;需通过外部计数器间接监控 |
九、修复建议(分层)与伪代码
9.0 L-1 — canard 内存池与并发(堵 P5 根因,优先于校验)
// twai_ops_esp32.c — 建议改动要点
// 1) 扩大池(按峰值帧估算,建议 ≥8KB,需压测后确定)
// 原:static uint8_t memory_pool[2048];
EXT_RAM_BSS_ATTR static uint8_t memory_pool[8192];
// 2) RX/TX 共用同一把锁,重命名为 canardMutex(原 canardTransmitMutex 仅覆盖 TX)
// ⚠ 超时设计:TX 侧原为 10ms,RX 侧建议 ≤5ms,
// 以避免 RX 任务长期阻塞丢帧(RX 丢帧比 TX 延迟代价更低)
static SemaphoreHandle_t canardMutex; // 替换原 canardTransmitMutex
// TwaiReceiveTask 内,原 canardHandleRxFrame 改为:
static uint32_t canard_rx_lock_fail_cnt = 0;
if (xSemaphoreTake(canardMutex, pdMS_TO_TICKS(5)) == pdTRUE) {
canardHandleRxFrame(&canard, &rxFrame, timeStamp);
xSemaphoreGive(canardMutex);
} else {
canard_rx_lock_fail_cnt++;
// 连续失败可作为池压力预警指标
}
// 3) 池水位监控(⚠ libcanard CanardInstance 无 pool_peak_usage 字段,
// 需自行在 canardRequestOrRespondObj 前后用 canard.allocated_blocks 追踪峰值)
static uint32_t canard_pool_peak = 0;
// 每次 TX 分配后:
uint32_t cur_blocks = canard.allocated_blocks; // libcanard 内部计数
if (cur_blocks > canard_pool_peak) canard_pool_peak = cur_blocks;
if (canard_pool_peak > POOL_HIGH_WATERMARK) {
ESP_LOGW(TAG, "canard pool high water: %lu blocks", canard_pool_peak);
}
// 4) canardRequestOrRespondObj 返回值检测
int16_t tx_ret = canardRequestOrRespondObj(&canard, destination_node_id, &tx_transfer);
if (tx_ret < 0) {
canard_tx_fail_streak++;
if (canard_tx_fail_streak >= 3) {
EnterPowertrainLimpMode(); // 连续 3 次池分配/发送失败 → 限速停板
canard_tx_fail_streak = 0;
}
} else {
canard_tx_fail_streak = 0;
}
审校说明(已修正 2 处):
- 原报告 canard.pool_peak_usage 字段在 libcanard/dronecan 中 不存在;已改为 canard.allocated_blocks(libcanard 实际维护的块计数字段),仍需压测确认字段名与版本对应。
- RX 加锁超时 5ms < TX 10ms,确保 RX 任务不会因等锁阻塞过久导致 twai_receive 超时丢帧;若 5ms 仍超时频繁,优先扩池降低 TX 持锁时长。
说明: 即使加校验,不修池/锁 仍可能间歇性解出 0xD4A081xx;校验是 第二道防线。
9.1 L0 — 反馈合法性 + D4A081 指纹(堵 P5 表象)
// esc_manager.h 或 powertrain_safety.h
#define ESC_SPEED_ABS_MAX_RPM 20000u
// 协议请求 CAN ID 高 3 字节 = 0xD4A081;
// 渗入 int32 speed 后,任意低字节 xx 均满足:(uint32)(speed) >> 8 == 0xD4A081
// 15265 全部 9 种取值均命中此条件,用移位比较比 mask 更直观
#define CORRUPT_SPEED_HIGH3 0xD4A081u // 对应请求 CAN ID 高 3 字节
static uint32_t canard_corrupt_speed_cnt = 0; // 供 Ulog/心跳上报
static bool EscSpeedFeedbackValid(int32_t rpm) {
if (rpm == INT32_MIN) return false; // 极值保护
if ((uint32_t)rpm > ESC_SPEED_ABS_MAX_RPM &&
(uint32_t)(-rpm) > ESC_SPEED_ABS_MAX_RPM) return false; // |rpm| > 20000
// 拦截 canard 池损坏产生的 CAN ID 渗入特征(15265 全簇命中)
if (((uint32_t)rpm >> 8) == CORRUPT_SPEED_HIGH3) return false;
return true;
}
// HandleEscSetSpeedResponse 内,esc_SetSpeedResponse_decode 成功后立即校验:
if (!EscSpeedFeedbackValid(escSetSpeedResponseData.speed)) {
canard_corrupt_speed_cnt++;
ESP_LOGW(TAG, "Reject corrupt ESC speed: %ld (0x%08lx), pool_corrupt_cnt=%lu",
escSetSpeedResponseData.speed,
(uint32_t)escSetSpeedResponseData.speed,
canard_corrupt_speed_cnt);
return; // 丢弃本帧,不更新 LastEscSpeedData,不入队
}
// 校验通过后才更新 last_valid_esc_rx_us(供 L2 健康度检测)
last_valid_esc_rx_us = esp_timer_get_time();
审校说明(已修正 2 处):
- 原 CORRUPT_SPEED_PREFIX 0xD4A08100u 命名歧义:0xD4A08100 是 mask 后的值,而非真正的「前缀」。改为 CORRUPT_SPEED_HIGH3 = 0xD4A081 + 右移 8 位比较,语义更清晰,同时覆盖所有低字节变体(0xD4A081xx)。
- 新增 last_valid_esc_rx_us 更新:校验通过后才刷新此时间戳,供 §9.3 L2 EscLinkHealthy() 使用;若仍用旧位置(入队降频后),健康度超时窗口被人为延长 10×。
9.2 L1 — 动力安全门(堵 P4,与 MotorSpeed 并联)
// powertrain_safety.c — 50ms 看门狗任务
// 共享变量(需 volatile 或 FreeRTOS atomic)
volatile bool force_limpmode = false;
static uint32_t esc_invalid_streak = 0; // 由 L0 校验失败时递增,通过时清零
void SafeStopWatchdogTask(void *arg) {
twai_status_info_t twai_status; // ESP-IDF TWAI 状态结构
for (;;) {
bool link_ok = EscLinkHealthy(); // 见 §9.3 L2
if (!link_ok || esc_invalid_streak >= 3) {
force_limpmode = true;
}
if (force_limpmode) {
SendSetSpeedRequest(0); // priority=HIGHEST,每 50ms 保底发一次零速
// 检查总线状态(ESP-IDF 正确 API)
if (twai_get_status_info(&twai_status) == ESP_OK &&
twai_status.state == TWAI_STATE_BUS_OFF) {
// 总线离线:触发恢复(需 128 次总线空闲,通常 ≥ 1ms)
twai_initiate_recovery();
// 等待恢复完成后再尝试发送(恢复后 TwaiBusStatusCheckTask 会收到 TWAI_ALERT_BUS_RECOVERED)
vTaskDelay(pdMS_TO_TICKS(200));
SendSetSpeedRequest(0); // 恢复后补发一次
}
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
审校说明(已修正 2 处):
- 原 GetTwaiBusOff() 不是 ESP-IDF API,已改为 twai_get_status_info(&twai_status) + 判断 twai_status.state == TWAI_STATE_BUS_OFF(driver/twai.h 标准接口)。
- 原 TwaiRecoverAndFlushRx() 不存在,已改为 twai_initiate_recovery() + 延迟补发;总线恢复需等待 128 个空闲位,不可立即重发,否则再次 Bus Off。
9.3 L2 — CAN 健康度(堵 P3,与 12s 报障解耦)
#define ESC_FEEDBACK_TIMEOUT_MS 500
// last_valid_esc_rx_us 由 §9.1 L0 在校验通过后刷新:
// last_valid_esc_rx_us = esp_timer_get_time(); ← 在 HandleEscSetSpeedResponse 内
// 注:⚠ 不要放在 10:1 降频入队后再更新,那会把超时窗口扩大 10 倍(约 5s)
extern volatile int64_t last_valid_esc_rx_us; // 在 uavcan_twai.c 中定义
bool EscLinkHealthy(void) {
int64_t now = esp_timer_get_time();
// 1) 反馈超时(§9.1 L0 校验通过才算有效帧)
if ((now - last_valid_esc_rx_us) > (int64_t)ESC_FEEDBACK_TIMEOUT_MS * 1000LL)
return false;
// 2) 总线离线(ESP-IDF 正确 API)
twai_status_info_t status;
if (twai_get_status_info(&status) == ESP_OK &&
status.state == TWAI_STATE_BUS_OFF)
return false;
return true;
}
// EcuManagerTask:500ms 不健康即 limp,不等待原 12s(240 周期)
if (!EscLinkHealthy() && GetRcConnectionStatus()) {
EnterPowertrainLimpMode();
latestEcuHeatbeatData.motor_speed = 0; // 心跳置零,防止 App 误判「还在转」
latestEcuHeatbeatData.ecu_status |= ECU_STATUS_ESC_FEEDBACK_INVALID; // 需在 MAVLink 定义新标志位
}
审校说明(已修正 2 处):
- 原 twai_in_bus_off() 不是 ESP-IDF API,已改为 twai_get_status_info() 判断 TWAI_STATE_BUS_OFF。
- last_valid_esc_rx_us 必须在 §9.1 L0 校验通过后才更新(在 HandleEscSetSpeedResponse 里、decode+校验成功后),若放在 ProcessEscDataTask 出队处或降频后入队处,超时窗口被放大为 500ms×10 = 5s,失去保护意义。
9.4 L3 — MotorSpeedControlTask 修改要点
// esc_manager.h 补充掩码定义(runningError 字段位定义,非上报的 esc_event)
// runningError bit1 = 失速保护(上报后经 <<16 成为 esc_event 0x20000)
#define ESC_RUNNING_ERROR_STALL_MASK ((uint16_t)(0x0001u << 1))
// runningError bit9 = 空转保护(已有判断,保持一致命名)
#define ESC_RUNNING_ERROR_IDLE_MASK ((uint16_t)(0x0001u << 9))
// MotorSpeedControlTask 内,计算完 MotorSpeed 后、发送前:
// 原有空转保护(保留)
if (LastEscSpeedData.runningError & ESC_RUNNING_ERROR_IDLE_MASK) {
MotorSpeed = 0;
}
// 新增:失速保护期间禁止继续发非零转速
if (LastEscSpeedData.runningError & ESC_RUNNING_ERROR_STALL_MASK) {
MotorSpeed = 0;
}
// 新增:链路不健康或限速模式
if (!EscLinkHealthy() || force_limpmode) {
MotorSpeed = 0;
}
// 修复:mode=255 也必须发 CAN 零速(原代码 255 分支无 SendSetSpeedRequest)
if (ThrottleAndGear.mode == 255) {
MotorSpeed = 0;
SendSetSpeedRequest(0);
continue; // 或 break,跳过下方统一发送
}
// 统一发送(⚠ SendSetSpeedRequest 返回 void,无法直接检测失败)
// 需通过 L-1 中的 canard_tx_fail_streak 间接监控
SendSetSpeedRequest(MotorSpeed);
审校说明(已修正 2 处):
- 原 ESC_STALL_MASK 未定义,且需与 runningError 字段(非上报的 esc_event)对应;已明确为 bit1 = 0x0002,并与现有 bit9 空转保护命名保持一致风格(ESC_RUNNING_ERROR_xxx_MASK)。
- 原伪代码 int16_t tx = SendSetSpeedRequest(...):SendSetSpeedRequest 在 uavcan_twai.c 实际声明为 void(内部 canardRequestWithMutex 的返回值未透传),不能直接检测发送结果;失败监控需通过 §9.0 L-1 的 canard_tx_fail_streak 计数器间接实现。
9.5 L4 — CAN 驱动层
- Bus Off / Error Passive 时:暂停非零转速请求,仅允许 高优先级零速。
- 取消正常运行态 10:1 反馈降频;或故障时强制 1:1。
- 可选:对
esc_SetSpeedResponse增加 CRC/序号/两次采样一致 再采纳。
9.6 ESC 固件(0551,需电调配合)
- 主机 500ms 未收到合法 SetSpeed 帧 → 独立降功率至 0(硬件看门狗)。
- STALL 保护态明确 是否忽略主机转速;若忽略,需文档化并与 ECU 策略一致。
十、15265 台架/路测验收(对齐 P1–P5)
| 编号 | 步骤 | 通过标准 |
|---|---|---|
| T-RND-0 | [池修复专项] 压测 20ms SetSpeed + 满负载多帧 RX,监控 canard.allocated_blocks 峰值及 canard_rx_lock_fail_cnt |
无 0xD4A081xx 上行;canard_corrupt_speed_cnt 保持 0;allocated_blocks 不超高水位 |
| T-RND-1 | [指纹校验] 注入 CAN 位错误帧 / 人工制造池污染(如注入内存填充脏值) | EscSpeedFeedbackValid 拦截;不更新 LastEscSpeedData |
| T-RND-1b | [返回值 bug] 验证修复后 SendEscSpeedResponseDataToQueue 成功返回 pdPASS |
调用侧可正确区分入队成功/失败 |
| T-RND-2 | 正常骑行后拔 CAN/H 线,遥控油门拉 0 | 500ms 内电流下降;MotorSpeed 指令=0(Ulog) |
| T-RND-3 | 复现 15265 末段:高负荷后收油 | FAULT_ESC_COMMUNICATION 前后,ESC 物理 不再 69A 级满载 |
| T-RND-4 | 对比修复前后 Ulog MotorSpeed vs ESC 反馈 |
通信异常时反馈标 无效,心跳 motor_speed=0 |
十一、日志使用边界(严谨性)
| 说法 | 是否可由 15265 OSS 单独证明 |
|---|---|
| 油门为 0 | ✅ 阶段 E–F |
| ECU 指令 MotorSpeed=0 | ✅ 由代码推导 + 油门 0(Ulog 可再证) |
| CAN 通讯异常 | ✅ ecu=0x40000000 |
| ESC 满功率 | ⚠️ 研发现场确认;OSS 末段电流为 冻结的 69.2A |
| 异常转速列表 | ✅ 13 点完全命中 |
异常值 ≡ 设速请求 CAN ID D4A081 |
✅ 协议 5.1.4 示例 + 日志 0xD4A081xx |
| canard 池为根因 | ✅ 研发确认 + 工程 RX/TX 竞态与 2KB 池 |
建议取证优先级: 板载 Ulog(MotorSpeed 指令 + UlogWriteEscMessage 反馈) > CAN 抓包对比 TX ID D4A081 与异常上报时刻 > canard 池峰值日志 > OSS 心跳。
十二、附录
12.1 分析脚本
analysis/_analyze_15265_rnd.py— 异常转速簇、油门 0、通信故障段统计analysis/_deep_15265.py— 通用 STALL 统计
12.2 协议与代码索引(5.1.4 设速)
| 项 | 值 |
|---|---|
| 服务 ID | 212 / Signature 0x4461BB2B55B3F410 |
| 请求 CAN ID(文档示例) | 0x00D4A081 |
| 响应 CAN ID(文档示例) | 0x00D401A0(多帧) |
| ECU 发送 | uavcan_twai.c → SendSetSpeedRequest |
| ECU 接收 | HandleEscSetSpeedResponse → esc.SetSpeed_res.h |
十三、本次审校改动汇总(v1.1 → v1.2)
| 章节 | 改动类型 | 说明 |
|---|---|---|
| §5.3 推断链 3、4 | 精确化 | canard payload 是链式内存块,损坏时指针/长度越界,非「直接读 CAN ID 字节」 |
| §5.4 → §5.5(新增) | 新发现 bug | SendEscSpeedResponseDataToQueue 成功路径也返回 pdFAIL(R11) |
| §5.5 → §5.6(重编号) | — | 原交叉验证小结顺延 |
| §9.0 L-1 伪代码 | API 修正 | canard.pool_peak_usage 不存在 → canard.allocated_blocks;补充 RX 加锁超时策略 |
| §9.1 L0 伪代码 | 命名修正 + 联动补充 | CORRUPT_SPEED_PREFIX/MASK → CORRUPT_SPEED_HIGH3 + 右移;last_valid_esc_rx_us 在此更新 |
| §9.2 L1 伪代码 | API 修正 | GetTwaiBusOff() → twai_get_status_info();TwaiRecoverAndFlushRx() → twai_initiate_recovery() + 延迟补发 |
| §9.3 L2 伪代码 | API 修正 + 更新位置 | twai_in_bus_off() → twai_get_status_info();明确 last_valid_esc_rx_us 应在 L0 校验后更新 |
| §9.4 L3 伪代码 | 定义补充 + 类型修正 | 补充 ESC_RUNNING_ERROR_STALL_MASK (bit1);SendSetSpeedRequest 实为 void,不可检返回值 |
| §八 风险表 | 新增 R11、R12 | 队列返回值 bug 与发送失败静默 |
| §十 台架验收 | 新增 T-RND-0、T-RND-1b | 池修复专项与队列 bug 验收 |
报告版本:v1.2(2026-05-28)— 全文审校,修正伪代码 API 错误,新增 §5.5 queue bug、R11/R12、T-RND-0;现象口径以研发纠正版为准。