← 返回目录

案例 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。


一、结论(给研发/售后)

  1. 15265 与研发表述一致,且不矛盾: 失控末段(10:35:01 起)轨迹上 油门连续为 0、ECU 已置 ESC 通信故障;按 MotorSpeedControlTask 逻辑,此时 MotorSpeed 必为 0 且仍周期调用 SendSetSpeedRequest(0),但 CAN 已异常,零速指令无法可靠送达 ESC,ESC 可保持失控前最后一档 PWM(研发确认满功率)。
  2. 日志里的 motorSpeed=11175、electricCurrent=69.2A 在通信故障段是「冻结快照」(49 个点数值完全相同),不能当作实时转速/电流;但 冻结前一刻(10:34:54–56)确为 thr=1000 + 69.2A + motor≈11175,说明失控前主机曾收到过 大电流/高转速反馈。
  3. 异常转速簇 -727678xxx 在 OSS 中共 13 点,与研发列举数值 完全吻合;其 uint32 表示 稳定落在 0xD4A081xx,与电调协议 5.1.4 设速请求示例 CAN ID 0x00D4A081 前三字节 D4 A0 81 一致——可基本判定为 libcanard 内存池问题,使 TX 请求帧 ID/池内残留 被 RX 响应解码 误读为 int32 speed(见 §五)。
  4. 根因分层:
  5. 感知层(P5): canard 池损坏 → 偶发异常反馈 → UpdateESCErrorCodeByRunningStatus 误置位且仍上行;
  6. 执行层(P3/P4): 池耗尽/总线错误后 零速 SetSpeed 发不出或 ESC 不收 → 电机保持满功率;
  7. 不是「ECU MotorSpeedControlTask 仍算出非零转速」。
  8. 项目逻辑缺口(与池问题叠加): 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

正常周期:


四、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 内存池出问题」一致):

  1. 2KB 池 在「20ms 周期 TX 设速 + 多帧 RX 遥测 + 其它服务」下易 耗尽/碎片化;
  2. RX/TX 未共锁,canardHandleRxFrame 与 canardRequestOrRespondObj 并发访问同一 canard 实例与池;
  3. 池内链式块(libcanard 用链表管理的 CanardRxState payload 块)被提前回收或越界写入,导致多帧响应拼接时 payload 指针或 bit_offset 指向相邻 TX 描述符内存区域(该区域残留 TX 请求 CAN 扩展 ID,高 3 字节即 D4 A0 81);
  4. 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;
  5. 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 驱动层

9.6 ESC 固件(0551,需电调配合)


十、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 分析脚本

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;现象口径以研发纠正版为准。