# 案例 15265：研发纠正现象下的动力链失控分析报告

| 项 | 内容 |
|----|------|
| 轨迹 ID | **15265** |
| 固件分支 | `JetWaveECU-feature_motorstuck`（ESC **0551** / ECU **0022**） |
| OSS | [明细](https://surf-oss.oss-accelerate.aliyuncs.com/surf-oss/file/231AAAA5G000728_1_10000251_20260524113448.txt) |
| 分析页 | 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 内存池根因**，并给出控制流转、风险与修复（含伪代码）。

**版本：** <mark>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。</mark>

---

## 一、结论（给研发/售后）

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. **根因分层：**
   - **感知层（P5）：** canard 池损坏 → 偶发异常反馈 → `UpdateESCErrorCodeByRunningStatus` 误置位且仍上行；
   - **执行层（P3/P4）：** 池耗尽/总线错误后 **零速 SetSpeed 发不出或 ESC 不收** → 电机保持满功率；
   - **不是**「ECU `MotorSpeedControlTask` 仍算出非零转速」。
5. **项目逻辑缺口（与池问题叠加）：** 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;
```

---

## 三、系统任务与数据流（正常）

```mermaid
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 内存池出问题」一致）：**

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

<mark>**影响分析：** 数据流本身不受影响（消费端 `ProcessEscDataTask` 通过 `ReceiveEscSpeedResponseDataFromQueue` 正常出队），但**调用侧无法判断是否入队成功**，异常转速帧在此处不会被拦截，且监控/日志统计失真。建议修复：入队成功路径 `return pdPASS`。</mark>

### 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 池恶化）

```mermaid
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 |
| <mark>**R11**</mark> | <mark>**`SendEscSpeedResponseDataToQueue` 始终返回 `pdFAIL`**（入队成功也如此）</mark> | <mark>中</mark> | <mark>`protocol_common_interface.c` 第 323 行；调用侧无法判断成功，监控失真</mark> |
| <mark>**R12**</mark> | <mark>**`SendSetSpeedRequest` 丢弃 `canardRequestWithMutex` 返回值**，发送失败无感知</mark> | <mark>高</mark> | <mark>`uavcan_twai.c` 第 186 行；需通过外部计数器间接监控</mark> |

---

## 九、修复建议（分层）与伪代码

### 9.0 L-1 — canard 内存池与并发（堵 P5 根因，优先于校验）

```c
// 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;
}
```

<mark>**审校说明（已修正 2 处）：**</mark>
- <mark>**原报告 `canard.pool_peak_usage`** 字段在 libcanard/dronecan 中 **不存在**；已改为 `canard.allocated_blocks`（libcanard 实际维护的块计数字段），仍需压测确认字段名与版本对应。</mark>
- <mark>**RX 加锁超时 5ms < TX 10ms**，确保 RX 任务不会因等锁阻塞过久导致 `twai_receive` 超时丢帧；若 5ms 仍超时频繁，优先扩池降低 TX 持锁时长。</mark>

**说明：** 即使加校验，**不修池/锁** 仍可能间歇性解出 `0xD4A081xx`；校验是 **第二道防线**。

### 9.1 L0 — 反馈合法性 + `D4A081` 指纹（堵 P5 表象）

```c
// 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();
```

<mark>**审校说明（已修正 2 处）：**</mark>
- <mark>**原 `CORRUPT_SPEED_PREFIX 0xD4A08100u` 命名歧义**：`0xD4A08100` 是 mask 后的值，而非真正的「前缀」。改为 `CORRUPT_SPEED_HIGH3 = 0xD4A081` + 右移 8 位比较，语义更清晰，同时覆盖所有低字节变体（`0xD4A081xx`）。</mark>
- <mark>**新增 `last_valid_esc_rx_us` 更新**：校验通过后才刷新此时间戳，供 §9.3 L2 `EscLinkHealthy()` 使用；若仍用旧位置（入队降频后），健康度超时窗口被人为延长 10×。</mark>

### 9.2 L1 — 动力安全门（堵 P4，与 MotorSpeed 并联）

```c
// 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));
    }
}
```

<mark>**审校说明（已修正 2 处）：**</mark>
- <mark>**原 `GetTwaiBusOff()` 不是 ESP-IDF API**，已改为 `twai_get_status_info(&twai_status)` + 判断 `twai_status.state == TWAI_STATE_BUS_OFF`（`driver/twai.h` 标准接口）。</mark>
- <mark>**原 `TwaiRecoverAndFlushRx()` 不存在**，已改为 `twai_initiate_recovery()` + 延迟补发；总线恢复需等待 128 个空闲位，不可立即重发，否则再次 Bus Off。</mark>

### 9.3 L2 — CAN 健康度（堵 P3，与 12s 报障解耦）

```c
#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 定义新标志位
}
```

<mark>**审校说明（已修正 2 处）：**</mark>
- <mark>**原 `twai_in_bus_off()` 不是 ESP-IDF API**，已改为 `twai_get_status_info()` 判断 `TWAI_STATE_BUS_OFF`。</mark>
- <mark>**`last_valid_esc_rx_us` 必须在 §9.1 L0 校验通过后才更新**（在 `HandleEscSetSpeedResponse` 里、decode+校验成功后），若放在 `ProcessEscDataTask` 出队处或降频后入队处，超时窗口被放大为 500ms×10 = **5s**，失去保护意义。</mark>

### 9.4 L3 — `MotorSpeedControlTask` 修改要点

```c
// 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);
```

<mark>**审校说明（已修正 2 处）：**</mark>
- <mark>**原 `ESC_STALL_MASK` 未定义**，且需与 `runningError` 字段（非上报的 `esc_event`）对应；已明确为 **`bit1 = 0x0002`**，并与现有 bit9 空转保护命名保持一致风格（`ESC_RUNNING_ERROR_xxx_MASK`）。</mark>
- <mark>**原伪代码 `int16_t tx = SendSetSpeedRequest(...)`**：`SendSetSpeedRequest` 在 `uavcan_twai.c` 实际声明为 **`void`**（内部 `canardRequestWithMutex` 的返回值未透传），不能直接检测发送结果；失败监控需通过 §9.0 L-1 的 `canard_tx_fail_streak` 计数器间接实现。</mark>

### 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）

| 编号 | 步骤 | 通过标准 |
|------|------|----------|
| <mark>**T-RND-0**</mark> | <mark>**[池修复专项]** 压测 20ms SetSpeed + 满负载多帧 RX，监控 `canard.allocated_blocks` 峰值及 `canard_rx_lock_fail_cnt`</mark> | <mark>无 `0xD4A081xx` 上行；`canard_corrupt_speed_cnt` 保持 0；`allocated_blocks` 不超高水位</mark> |
| 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` |

---

---

## 十三、<mark>本次审校改动汇总（v1.1 → v1.2）</mark>

| 章节 | 改动类型 | 说明 |
|------|----------|------|
| §5.3 推断链 3、4 | <mark>精确化</mark> | canard payload 是链式内存块，损坏时指针/长度越界，非「直接读 CAN ID 字节」 |
| §5.4 → §5.5（新增） | <mark>新发现 bug</mark> | `SendEscSpeedResponseDataToQueue` 成功路径也返回 `pdFAIL`（R11） |
| §5.5 → §5.6（重编号） | — | 原交叉验证小结顺延 |
| §9.0 L-1 伪代码 | <mark>API 修正</mark> | `canard.pool_peak_usage` 不存在 → `canard.allocated_blocks`；补充 RX 加锁超时策略 |
| §9.1 L0 伪代码 | <mark>命名修正 + 联动补充</mark> | `CORRUPT_SPEED_PREFIX`/`MASK` → `CORRUPT_SPEED_HIGH3` + 右移；`last_valid_esc_rx_us` 在此更新 |
| §9.2 L1 伪代码 | <mark>API 修正</mark> | `GetTwaiBusOff()` → `twai_get_status_info()`；`TwaiRecoverAndFlushRx()` → `twai_initiate_recovery()` + 延迟补发 |
| §9.3 L2 伪代码 | <mark>API 修正 + 更新位置</mark> | `twai_in_bus_off()` → `twai_get_status_info()`；明确 `last_valid_esc_rx_us` 应在 L0 校验后更新 |
| §9.4 L3 伪代码 | <mark>定义补充 + 类型修正</mark> | 补充 `ESC_RUNNING_ERROR_STALL_MASK (bit1)`；`SendSetSpeedRequest` 实为 `void`，不可检返回值 |
| §八 风险表 | <mark>新增 R11、R12</mark> | 队列返回值 bug 与发送失败静默 |
| §十 台架验收 | <mark>新增 T-RND-0、T-RND-1b</mark> | 池修复专项与队列 bug 验收 |

*报告版本：<mark>v1.2（2026-05-28）</mark>— 全文审校，修正伪代码 API 错误，新增 §5.5 queue bug、R11/R12、T-RND-0；现象口径以研发纠正版为准。*
