JetWave ECU 动力模组深度程序分析报告
feature_motorstuck 分支专项
文档版本: v1.4(§8.3 分层安全停板 LSS + §11.8 停板实现)
生成日期: 2026-05-26
代码基线: JetWaveECU-feature_motorstuck/(分支 feature_motorstuck)
对比基线: JetWaveECU-master-2d55e90f4a54d61a83efab89195082109acd3472(原 master 分析)
前置文档:
- JETWAVE_ECU_POWERTRAIN_ANALYSIS_PLAN.md
- JETWAVE_ECU_POWERTRAIN_DEEP_ANALYSIS_REPORT.md(master 版)
执行摘要
核心问题(与 master 相同,用户仍会遇到)
失速 + 「断联」+ 遥控器推油门无动力
在 feature_motorstuck 上,根因结构未变:ESC 真保护 / ECU 不拉闸 / 通信超时误报 / 断链不发零速。该分支 没有 实现完整的「电机卡死(motor stuck)安全状态机」,仅改了 ECU 二次故障合成的一处逻辑,且存在 故障位映射错误 风险。
本分支相对 master 的关键结论
| 项 | master (2d55e90f) |
feature_motorstuck |
评价 |
|---|---|---|---|
| STALL 后限扭/零速 | ❌ 无 | ❌ 仍无 | 未解决主诉 |
BLE 断链 mode=255 零速 |
❌ 无 SendSetSpeedRequest(0) |
❌ 仍无 | P0 未修 |
| 转速异常合成位 | running_error bit1 → esc_event 0x20000 (STALL) |
bit6 → 0x400000 (过压) | ⚠️ 语义错误 |
| 转速异常条件 | speed <= -20000 |
\|speed\| >= 20000 |
略扩宽 |
| 空转保护执行 | MotorSpeed=0 注释掉 |
bit9 已启用 MotorSpeed=0 |
✅ 小幅改进 |
| ALC 俯仰自稳 | 有 | 已删除 | 简化固件 |
| NearLink 双链路 | 有 | 无(仅 ESP32 BLE) | 产品形态不同 |
| RC Ulog | 注释 | 已打开 UlogWriteRcMessage |
✅ 利于复盘 |
| 堵转 bit 清除滞回 | 无 | 仍无 | 误报可锁死 |
分支命名 feature_motorstuck 与代码效果不完全一致: 注释写「堵转」,但置位的是 running_error bit6,上报为 ESC_FAULT_OVERVOLTAGE_PROTECTION (4194304),不是 现场主分析的 ESC_FAULT_STALL_PROTECTION (131072)。若荷兰等问题会话以 131072 为主,则该分支 不能替代 ESC 固件 STALL,且可能 新增误报过压。
〇、用户感知再确认:「断联 + 收不到指令 + 自己疯狂运行」
重要前提(方法论): 失速/通信异常之后,云端轨迹与 OSS 明细有可能根本回传不全甚至中断,不能以「失速后
motorSpeed=0」等日志形态否定现场口述。
本节 仅依据固件因果链 判断:您描述的现象 在现有代码下可以成立,且恰恰是 日志最可能丢失 的那类故障。
0.1 为何不能依赖失速后的日志
| 链路环节 | 失速/异常后可能发生什么 | 对云端数据的影响 |
|---|---|---|
| ESC→ECU(CAN) | 电调保护或 Bus 异常 → ReceiveEscRunningDataFromQueue 长期失败 |
latestEscRunningData 不更新,但 不会被清零 |
| ECU→App(BLE 心跳 500ms) | 仍用 上一份 latestEscRunningData 填 motor_speed 上报 |
云端看到 过时转速,或 App 停记会话 |
| ECU→App(事件) | FAULT_ESC_COMMUNICATION 置位后用户感知断联 |
会话可能 提前结束,失速后段不上传 |
| App→云端 | 断联、崩溃、弱网 | OSS 缺口/截断,不能反推「当时电机已停」 |
| 板载 Ulog(SD) | 依赖文件系统;通信故障期未必落盘完整 | 售后若未拉 SD,仍 无据可查 |
代码依据(心跳在收不到 ESC 新数据时仍发旧值):
// ecu_manager.c:队列失败时 latestEscRunningData 保持上次 memcpy 结果,不清零
if (ReceiveEscRunningDataFromQueue(...) == pdPASS) {
memcpy(&latestEscRunningData, ...);
} else if (GetRcConnectionStatus()) {
ESC_CommunicationFailure++; // 12s 后置 FAULT_ESC_COMMUNICATION
}
// 500ms 仍执行:
UpdateEcuHeatbeatDataByDevice(latestEscRunningData, ...); // motor_speed = 陈旧值
SendMavlinkToBleRC(&HeatbeatMsg);
因此:历史上 OSS 里「STALL 后 motorSpeed≈0」最多说明 「有上传的那几帧」 的电调反馈;不能证明 失速瞬间及之后物理电机状态,更不能排除 「断联 + 还在转」。
0.2 从固件推导:您描述的三句话 可以同时成立
现场口述:板体与遥控器断联 → 板体收不到遥控指令 → 自己疯狂运行。
在 不查日志 的前提下,按当前 feature_motorstuck 代码,这是一条 合法故障轨迹:
sequenceDiagram
participant U as 遥控器
participant ECU as ECU
participant ESC as 电调/电机
Note over U,ESC: ① 正常运行
U->>ECU: 油门(BLE 正常)
ECU->>ESC: SetSpeed(高) 每 20ms
Note over U,ESC: ② 失速/堵转(ESC 固件)
ESC->>ESC: STALL 或保护态
ESC--xECU: CAN 反馈变少或中断
Note over U,ESC: ③ ECU 仍按油门发 CAN(缺陷)
U->>ECU: 油门可能仍在发
ECU->>ESC: SetSpeed(非零) 仍每 20ms
Note over ESC: 物理上:可能继续转/抖动/保持 — 日志不可见
Note over U,ESC: ④ ~12s 后 ECU 判通信故障
ECU->>U: FAULT_ESC_COMMUNICATION
U->>U: 用户感知「断联」
Note over U,ESC: ⑤ BLE 真断或 RF 恶化
U--xECU: 蓝牙断
ECU->>ECU: mode=255,MotorSpeed=0 但不发 CAN 零速
ESC->>ESC: 若保持最后一帧转速指令
Note over U: 「收不到指令 + 还在转」
| 用户感受 | 不依赖日志的代码解释 |
|---|---|
| 断联 | FAULT_ESC_COMMUNICATION 弹窗/埋点;和/或 BLE 真断(ble_server → mode=255) |
| 收不到指令 | 真断链:不再更新 ThrottleAndGear;假断联:收到了但 电机不跟,用户也会说「控不住」 |
| 疯狂运行 | ① 失速前 ECU 持续发高转速;② 断链后 未 SendSetSpeedRequest(0),ESC 可能维持末指令;③ STALL 后 ECU 不拉闸 仍发非零 CAN — 物理结果 必须以台架/示波器为准,不能由缺失的 OSS 否定 |
0.3 与「仅有日志时」结论的差异(务必读)
| 论断 | 若 只看 OSS 失速后点位 | 若 承认日志可缺失(应采用) |
|---|---|---|
| 失速后电机一定停转 | 常被统计为 motorSpeed=0 |
无法证明;应用台架验证 |
| 失速后 RC 一定还连着 | 心跳里仍有 throttle | BLE 可能随后断;断在日志最后一帧之后 |
| 用户「疯狂运行」是误判 | 与部分轨迹矛盾 | 与 mode255 不零速 + 持续 SetSpeed 不矛盾,且 更危险 |
| 修复优先级 | 可能低估 M3(断链零速) | M2+M3 同为 P0 |
对程序员: 分析、定责、验收 不得以「OSS 失速后 motorSpeed=0」为通过标准;必须通过 CAN 抓包 + 断 BLE + 堵转台架 验证 §十一。
0.4 仅用固件逻辑的失败模式分类(供复现)
| 模式 | 触发 | 遥控链路 | CAN 输出 | 物理电机(需台架) | 日志可信度 |
|---|---|---|---|---|---|
| F1 | ESC STALL,BLE 仍通 | 通 | 仍发非零 SetSpeed | 停/抖/保持 — 不确定 | 失速后 低/无 |
| F2 | STALL → 12s → ECU 通信故障 | 可能仍通 | 同 F1 | 同 F1 | 通信故障后 常缺失 |
| F3 | BLE 断 mode=255 |
断 | 不发 零速指令 | 最像「断联还在转」 | 会话 常中断 |
| F4 | F1+F3 串联 | 先通后断 | 高转速 → 断链不拉闸 | 用户口述 最常见叙事 | 几乎无法靠 OSS 还原 |
您描述的现象与模式 F3、F4 一致性最高;模式 F1 表现为「还在发指令但像失控」——用户也会说成「疯狂」。
0.5 现场核对(不依赖云端轨迹)
- 问题当时 蓝牙是否已断?(设置页 + 板子指示灯)
- 听/看螺旋桨:断链后是否仍转 ≥1s?(验证 F3)
- 能否导出 板载 Ulog(SD) 或 App 本地缓存?(比 OSS 完整)
- 返厂 CAN 抓包:断链/失速瞬间是否仍有 非零 SetSpeed?
不应再问:「后台 motorSpeed 是不是 0」——失速后后台 可能没有有效后段。
一、问题定义(与 master 报告一致)
| 用户说法 | 日志/App | 代码枚举 | 本分支是否处理 |
|---|---|---|---|
| 失速 | escEventCode & 131072 |
ESC_FAULT_STALL_PROTECTION 0x20000 |
❌ 仅透传 ESC;ECU 合成不再置位 0x20000 |
| 断联 | ecuEventCode & 1073741824 |
FAULT_ESC_COMMUNICATION |
❌ 逻辑同 master(12s 无队列数据) |
| 无法控制 | 日志中或见油门>0、转速≈0(失速后日志可能不回传,不作硬依据) | — | ❌ STALL 未拉闸 |
| 真蓝牙断链 | mode=255 |
ECU 私有 | ❌ 仍不发 CAN 零速 |
位域(未改):
esc_event = ((uint32_t)runningError << 16) | selfcheckError;
// STALL 0x20000 <=> runningError bit1
// OV 0x400000 <=> runningError bit6 ← 本分支误用此位表示「堵转」
二、分支差异总览(第一性原理:改了什么、没改什么)
2.1 架构差异
flowchart LR
subgraph FM["feature_motorstuck"]
BLE_ONLY[仅 ESP32 BLE]
ESC_M2[esc_manager 无 ALC]
SYN[UpdateESC: bit6 转速异常]
end
subgraph MS["master 2d55e90f"]
NL[NearLink + BLE 可选]
ESC_M1[esc_manager + ALC PID]
SYN2[UpdateESC: bit1 转速异常]
end
FM --> SAME[相同缺陷层]
MS --> SAME
SAME --> GAP[无 SafetySupervisor<br/>STALL 不拉闸<br/>mode255 不零速]
| 模块 | feature_motorstuck | master |
|---|---|---|
main.c |
仅 BleInit(),日志默认 ESP_LOG_NONE |
NearLink 分支 + ESP_LOG_INFO |
esc_manager.c |
~553 行,无 ALC/无 ReadAutoLevelControlParams |
~666 行,含 ALC |
ecu_manager.c |
无 NearLink 回调注册 | 有 SLE/BLE 动态注册 |
uavcan_twai.c |
10:1 降频入队 | 相同 |
JetWave.xml 故障枚举 |
与 master 一致 | 一致 |
2.2 本分支唯一与「motorstuck」相关的代码变更
```225:231:JetWaveECU-feature_motorstuck/App/src/esc_manager.c // 转速异常 if (escSpeedInfo->speed <= -20000 || escSpeedInfo->speed >= 20000) { runningErrorState |= (uint16_t)(0x0001 << 6); // 置位堵转故障码 }
// 更新 ESC 的运行错误码 escSpeedInfo->runningError = runningErrorState;
**master 对应逻辑:**
```c
if (escSpeedInfo->speed <= -20000) {
runningErrorState |= (uint16_t)(0x0001 << 1); // 注释为堵转,实际映射到 STALL 位
}
2.3 故障位映射分析(本报告最重要的新增发现)
running_error 位 |
左移 16 位后 esc_event |
JetWave.xml 名称 |
现场/App 常用值 |
|---|---|---|---|
bit 1 (0x0002) |
0x00020000 | ESC_FAULT_STALL_PROTECTION | 131072 |
bit 6 (0x0040) |
0x00400000 | ESC_FAULT_OVERVOLTAGE_PROTECTION | 4194304 |
因此:
- master:ECU 在
speed <= -20000时 误用 STALL 位 上报(与 ESC 原生 STALL 混用,至少 数值上对得上 131072)。 - feature_motorstuck:注释写堵转,但置 bit6 → App/云端会判为 过压 4194304,与荷兰报告里「STALL+ECU、很少 OV」的统计 不一致,售后按 131072 筛 会漏掉 纯 ECU 合成路径。
- 两种分支均未:在
MotorSpeedControlTask里根据0x20000或 bit6 限制扭矩。
单片机/底层视角: runningErrorState 为静态变量,整体覆盖 escSpeedInfo->runningError,且 bit6 无滞回清除 → 一旦触发可能 永久 OR 在状态字,直到重启。
三、根因场景(feature_motorstuck 分支逐项复核)
Scenario A:ESC 真失速(STALL 131072)— 仍为第一主因
- 来源: ESC 固件
running_errorbit1 或selfcheck(以电调为准),不是 本分支 bit6 逻辑。 - ECU 行为: 与 master 相同,每 20ms
SendSetSpeedRequest(MotorSpeed),不检查LastEscSpeedData的 STALL 位。 - 现场: 荷兰 15349/15353、
231AAAA5C001688等仍以 131072 为主 → 必须继续查 ESC 固件,不能指望本分支修复。
Scenario B:FAULT_ESC_COMMUNICATION 假断联 — 未改
```488:497:JetWaveECU-feature_motorstuck/App/src/ecu_manager.c } else if (GetRcConnectionStatus()) { ESC_CommunicationFailure ++; if (ESC_CommunicationFailure >= 240) { // 12S ecuEventCode |= FAULT_ESC_COMMUNICATION; } } else if (!GetRcConnectionStatus()) { ESC_CommunicationFailure = 0; ecuEventCode &= ~FAULT_ESC_COMMUNICATION;
叠加因素仍为:UAVCAN **10:1 入队**、`ProcessEscDataTask` 100ms、STALL 后反馈稀疏。
**额外注意(本分支):** BMS 通信故障判断使用 `GetRcConnectionStatus()`(510 行),与 master 的 `GetBmsConnectionStatus()` **不同** → 可能 **误用 RC 连接状态判断 BMS**,属 **回归风险**,需单独评审。
### Scenario C:BLE 断链 mode=255 — **未修,P0**
```372:376:JetWaveECU-feature_motorstuck/App/src/esc_manager.c
case 255: // 蓝牙断链
ESP_LOGD(LOG_TAG, "NOBLUETOOTH");
ThrottleAndGear.throttle = 0;
MotorSpeed = 0;
break; // 仍无 SendSetSpeedRequest(0)
Scenario D:油门丢失 1s → mode 254 — 仍有效
```365:370:JetWaveECU-feature_motorstuck/App/src/esc_manager.c case 254: ... SendSetSpeedRequest(MotorSpeed); // MotorSpeed=0
### Scenario E:本分支「转速异常 → bit6」— **新路径,可能误导**
| 条件 | 后果 |
|------|------|
| `\|speed\| >= 20000`(0.1rpm 即 2000rpm) | 置 `running_error` bit6 → 上报 **过压** |
| 高速工况(进阶档满速 15000=1500rpm) | 通常不触发;若 ESC 反馈溢出/符号错误 | **误报过压** |
| 触发后 | **无清除逻辑**;**不限扭**;与 STALL 131072 **并存时** App 若用 `==` 判断可能只显示一种 |
### Scenario F:空转保护 bit9 — **本分支改进点**
```358:363:JetWaveECU-feature_motorstuck/App/src/esc_manager.c
if (LastEscSpeedData.runningError & (uint16_t)(0x0001 << 9)) {
MotorSpeed = 0;
ESP_LOGD(LOG_TAG, "Idle protection in progress, speed forced to 0");
}
SendSetSpeedRequest(MotorSpeed);
- master 同段 注释掉
MotorSpeed = 0;本分支 已启用。 - 但空转 置位 仍在
UpdateESCErrorCodeByRunningStatus里 注释掉(207 行)→ bit9 主要靠 ESC 上报,ECU 侧空转检测仍不完整。
Scenario G:OTA 挂起控制任务 — 同 master
StartUpgradeNotification → MotorSpeedControlTaskSuspend(),表现仍为「无法控制」。
四、通信与子系统(与 master 相同部分摘要)
| 项目 | 状态 | 说明 |
|---|---|---|
SendSetSpeedRequest |
每 20ms(正常模式) | rpm=0 时 CAN 最高优先级 |
| ESC 反馈 | 10 帧入队 1 次 | 通信故障检测欠采样 |
| TWAI Bus-Off | 自动 recovery | 恢复窗口无强制零速 |
| 心跳 | 500ms | motor_speed 滞后于控制环 |
| 事件上报 | EventCodeProcessByBit ~12s/bit |
复合故障感知慢 |
| Ulog RC | 本分支已写盘 | 利于对齐 OSS「有油门无电机」 |
五、控制逻辑(C/控制工程师视角)
5.1 模式与转速表
```66:71:JetWaveECU-feature_motorstuck/App/src/esc_manager.c static float maxSpeedForModes[3][16] = { {7600.0, ... 11200.0, 10000.0}, // BEGINNER {11400.0, ... 15000.0, 10000.0}, // 注释 SPORT,枚举仍为 INTERMEDIATE {15000.0, ... 15000.0, 10000.0} // ADVANCED };
- 无 ALC:高阶模式 **满速更激进**,STALL 触发概率可能 **高于** master(需台架对比)。
- 仍含 **20/40 rpm per 20ms** 斜坡、`LowPassFilter` 油门。
### 5.2 安全相关缺失(两分支共性)
1. 无 **STALL / 通信故障 / 断链** 统一 `torque_enable`
2. 无 **松油门恢复** 状态机
3. 无 **MAVLink 清除故障** 指令
---
## 六、FMEA(feature_motorstuck 增量)
| ID | 风险 | 本分支变化 | 严重度 |
|----|------|------------|--------|
| R0 | 堵转改 bit6 | 上报 **过压** 而非 STALL | **高**(误导排查) |
| R1 | STALL 后不拉闸 | 无变化 | 高 |
| R2 | mode255 不零速 | 无变化 | 高 |
| R3 | bit6 无清除 | 新增锁死风险 | 中 |
| R4 | BMS 通信用 RC 状态 | 新增/回归 | 中 |
| R5 | 去掉 ALC | 高阶模式操控不同 | 低~中 |
---
## 七、与现场数据的关系
| 现场现象 | feature_motorstuck 解释 |
|----------|-------------------------|
| 荷兰 STALL **131072** | 仍主要来自 **ESC 固件**;本分支 ECU 合成 **不再** 打 0x20000 |
| 会话「STALL+ECU 通信」 | Scenario A+B 叠加,逻辑未改 |
| 若升级本分支后出现 **4194304** 增多 | 怀疑 **bit6 转速异常误报过压** |
| `throttle>0, motorSpeed=0` | 仍无 ECU 侧强制零速(除 bit9 空转) |
**判定树(在本分支固件上):**
推油门无动力? ├─ esc & 131072 → ESC 真 STALL(与本分支 bit6 无关)→ 查 ESC + 仍缺 ECU 拉闸 ├─ esc & 4194304 且近期升级 feature_motorstuck → 先查 speed 是否 |speed|>=20000 误触 bit6 ├─ ecu & 0x40000000 → ECU↔ESC 12s 无数据(同 master) ├─ mode 255 → 断链未 SendSetSpeed(0)(同 master) └─ 仅 bit9 空转 → 本分支会强制 MotorSpeed=0(小幅缓解)
---
## 八、修改建议(针对 feature_motorstuck 的评审意见)
### 8.1 优先级总表
| ID | 优先级 | 项 | 主要文件 |
|----|--------|-----|----------|
| M1 | **P0** | 修正转速异常 → **bit1(STALL)** + 滞回清除 | `App/src/esc_manager.c` |
| M2 | **P0** | 扭矩安全门:STALL/通信/断链 → 强制零速 | `App/src/esc_manager.c`(可选新建 `App/src/powertrain_safety.c`) |
| M3 | **P0** | `mode=255` 断链发 `SendSetSpeedRequest(0)` | `App/src/esc_manager.c` |
| M4 | **P0** | `running_error` 与 ESC 原生字段 **OR 合并** | `App/src/esc_manager.c` |
| M5 | **P1** | BMS 通信判断改回 `GetBmsConnectionStatus()` | `App/src/ecu_manager.c` |
| M6 | **P1** | ESC 通信故障:降频/置位时联动零速 | `Protocol/src/uavcan_twai.c`、`App/src/ecu_manager.c` |
| M7 | **P1** | 安全状态机 + 恢复条件(松油门) | `App/inc/powertrain_safety.h`、`App/src/powertrain_safety.c` |
| M8 | **P0** | 零速看门狗 50ms(LSS L3) | `App/src/powertrain_safety.c` |
| M9 | **P0** | ESC 反馈丢失 500ms 停板(与 12s 报障分离) | `ecu_manager.c` + `powertrain_safety.c` |
| M10 | P2 | LIMP 超 3s 可选 `SendEscRebootRequest` | `powertrain_safety.c` |
### 8.2 不建议合并到量产前的状态
当前 `feature_motorstuck` **不适合**作为「失速/卡死修复」量产分支,除非 **M1~M4 + M8 全部合入** 且台架通过 §九 用例。
### 8.3 安全停板总体方案(断联 / 失速 / 不可控故障)— **推荐采用**
> **目标:** 一旦 **与遥控器断联**,或出现 **不可控类故障(含失速 STALL)**,在 **不依赖云端日志、不依赖用户松手** 的前提下,**尽快、持续、可验证** 地让板体停止输出(CAN 上为零转速指令;物理停转由 ESC 执行)。
> **原则:** 失联 / 失控 → **默认零扭(fail-safe)**;恢复必须 **显式条件**,不能靠「再发一次大油门」碰运气。
#### 8.3.1 推荐方案名:**分层安全停板(Layered Safe Stop, LSS)**
在现有架构上增加 **「安全监督层」**,与 `MotorSpeedControlTask`(20ms 控制)并行,**专责停板**,避免「控制任务还在发非零转速」或「断链未发 CAN 零速」。
```mermaid
flowchart TB
subgraph TRIG["停板触发(任一满足即 LIMP)"]
T1[RC 蓝牙断链 mode=255]
T2[RC 连接但 1s 无油门 → mode254]
T3[ESC STALL 0x20000]
T4[ESC 严重故障 OV/OC/OT 等]
T5[ESC 反馈丢失 ≥500ms 且此前在驱动]
T6[ECU FAULT_ESC_COMM 置位]
end
subgraph LSS["安全监督层 LIMP"]
Z1[safe_rpm = 0]
Z2[每 20ms SetSpeed 0 最高优先级]
Z3[独立看门狗 50ms 重复发 0]
end
subgraph REC["恢复 RECOVERY_WAIT"]
R1[油门≤死区 持续 2s]
R2[ESC 无 STALL 位]
R3[ESC 通信恢复]
end
TRIG --> LSS
LSS --> REC
REC -->|满足| NORMAL[正常控制]
8.3.2 停板触发条件(建议固件统一配置)
| 触发类 | 检测方式 | 建议动作延迟 | 说明 |
|---|---|---|---|
| A. 遥控断联 | GetRcConnectionStatus()==false 或 mode==255 |
立即(≤20ms) | ble_server 断开时同步调用 PowertrainSafetyOnRcLinkLost() |
| B. 遥控失联等效 | 1s 无 ThrottleAndGear(现有 mode254) |
1s 后发零速 | 已有 SendSetSpeed(0),保留 |
| C. 失速 STALL | esc_event & 0x20000 或 running_error bit1 |
立即 | 不等 12s 通信故障 |
| D. 不可控/严重 ESC 故障 | esc_event 掩码:STALL、过流 0x10000、过温 0x40000 等(可配置表) |
立即 | 与 App 弹窗掩码对齐 |
| E. ESC 反馈中断 | 连续 500ms(建议 25×20ms)未收到 EscRunningData 且上一周期 safe_rpm≠0 |
500ms 发零速 | 不要等 12s 才停;12s 仅用于 上报 FAULT_ESC_COMMUNICATION |
| F. 通信故障已置位 | FAULT_ESC_COMMUNICATION |
保持 LIMP | 上报与停板解耦 |
关键设计: 停板(E)要快于报障(F 的 12s)。当前固件 12s 才置通信故障,但 停板应在亚秒级。
8.3.3 停板执行层(三层,缺一不可)
| 层级 | 做什么 | 解决什么问题 |
|---|---|---|
| L1 控制门 | MotorSpeedControlTask 出口统一 ApplySpeedWithSafety(),LIMP 下 只发 0 |
失速后仍发非零 CAN |
| L2 断链拉闸 | mode=255 必须 SendSetSpeedRequest(0);ble_server 断开回调 再发一次 0 |
断联后 ESC 保持末指令 →「疯狂运行」 |
| L3 零速看门狗 | 独立任务 SafeStopWatchdogTask 50ms,在 LIMP 或 !GetRcConnectionStatus() 时 周期性 SendSetSpeedRequest(0) |
OTA 挂起 MotorSpeedControl、任务异常时仍能拉闸 |
为何需要 L3: StartUpgradeNotification 会 vTaskSuspend(MotorSpeedControlTask),若仅依赖控制任务发零速,升级/异常挂起时板体可能不停。
8.3.4 方案对比(为何不用「单点修补」)
| 方案 | 做法 | 断联停板 | 失速停板 | 任务挂起时 | 推荐 |
|---|---|---|---|---|---|
| S0 现状 | 无统一策略 | ❌ mode255 不发 CAN | ❌ 仍发非零 | ❌ | 否 |
| S1 仅修 mode255 | 断链加一行 SendSetSpeed(0) |
⚠️ 仅一次 | ❌ | ❌ | 不足 |
| S2 仅 STALL 限扭 | if (stall) MotorSpeed=0 |
❌ | ⚠️ 仅控制任务内 | ❌ | 不足 |
| S3 等 12s 通信故障再停 | 依赖 FAULT_ESC_COMM |
❌ 太晚 | ❌ 太晚 | ❌ | 否 |
| S4 LSS(推荐) | L1+L2+L3 + 快触发 E + 状态机恢复 | ✅ | ✅ | ✅ | 是 |
| S5 LSS + ESC 重启 | L4 下 LIMP 3s 仍无反馈则 SendEscRebootRequest() |
✅ | ✅ | ✅ | 可选增强 |
8.3.5 恢复策略(停板之后如何「能再次开」)
| 条件 | 必须同时满足 | 原因 |
|---|---|---|
进入 RECOVERY_WAIT |
LIMP 下油门 ≤ 死区 | 用户明确卸负载 |
回到 NORMAL |
松油门 ≥2s + ESC 无 STALL + 收到 ESC 反馈 | 避免顶着油门再次堵转;通信未恢复不加扭 |
| 禁止 | 仅 App 点「确定」即恢复 | 与现场「不可控」体验不符 |
断联恢复:须 BLE 重新连接(mode 从 255→254/正常档)+ 上述条件。
8.3.6 与 App / 现场的边界
| 责任方 | 停板相关职责 |
|---|---|
| ECU 固件(必须) | LSS 三层停板;不依赖日志回传 |
| ESC 固件(应确认) | 收到 SetSpeed(0) 后多少 ms 内卸扭;断 CAN 后是否超时自停 |
| App(建议) | 断联/STALL 弹窗;不在 LIMP 期间向用户暗示「仍可满油门」;可选「长按 3s 强制恢复」仅发 查询/复位 指令,不直接绕过 ECU 安全层 |
| 验收 | CAN 抓包 + 断 BLE 台架;不以 OSS 为准 |
8.4 修改项补充(停板专项)
| ID | 优先级 | 项 | 文件 |
|---|---|---|---|
| M8 | P0 | SafeStopWatchdogTask 50ms 周期发零速 |
App/src/powertrain_safety.c |
| M9 | P0 | ESC 反馈丢失 500ms 进 LIMP(与 12s 报障分离) | App/src/ecu_manager.c + powertrain_safety.c |
| M10 | P2 | LIMP 持续 3s 且无 ESC 应答 → SendEscRebootRequest() |
powertrain_safety.c |
十一、程序员实施指南(伪代码 / 文件 / 修改后流程)
本节供固件工程师直接对照改代码。伪代码与现有 FreeRTOS / MAVLink 风格一致,合入时需保留现有命名与头文件包含关系。
11.1 涉及文件与改动范围
| 文件 | 操作 | 改动摘要 |
|---|---|---|
App/inc/powertrain_safety.h |
新建 | 安全状态枚举、阈值宏、对外 API |
App/src/powertrain_safety.c |
新建 | 状态机、IsTorqueAllowed()、OnEscFeedback() |
App/CMakeLists.txt |
修改 | 将 powertrain_safety.c 加入编译 |
App/src/esc_manager.c |
修改 | M1/M2/M3/M4;MotorSpeedControlTask 调用安全层 |
App/src/ecu_manager.c |
修改 | M5/M6;通信故障时通知安全层 |
Protocol/src/uavcan_twai.c |
修改 | M6;故障帧或每帧入队策略 |
Ble/src/ble_server.c |
可选 | 断链时已发 mode=255,与安全层重复校验无妨 |
不建议改: JetWave.xml(沿用 ESC_FAULT_STALL_PROTECTION=0x20000)、SendSetSpeedRequest 签名。
11.2 新建头文件 App/inc/powertrain_safety.h(伪代码)
#ifndef POWERTRAIN_SAFETY_H
#define POWERTRAIN_SAFETY_H
#include <stdint.h>
#include <stdbool.h>
/* 与 JetWave.xml / 现场日志一致 */
#define ESC_RUN_ERR_BIT_STALL ((uint16_t)(1u << 1)) /* -> esc_event 0x20000 */
#define ESC_EVT_MASK_STALL (0x00020000u)
#define ECU_EVT_MASK_ESC_COMM (0x40000000u)
#define THROTTLE_DEADBAND 5 /* 与 esc_manager 死区一致 */
#define STALL_RECOVERY_HOLD_MS 2000 /* 松油门持续时间 */
#define SPEED_ANOMALY_LOW (-20000)
#define SPEED_ANOMALY_HIGH (20000) /* 仅用于异常检测,非满速工况 */
typedef enum {
PWR_SAFE_NORMAL = 0,
PWR_SAFE_LIMP, /* 强制零扭:STALL / 通信丢失 / 断链 */
PWR_SAFE_RECOVERY_WAIT, /* 等待用户松油门 + ESC 故障清除 */
} PowertrainSafetyState_t;
void PowertrainSafetyInit(void);
/* 每 20ms 在 MotorSpeedControlTask 调用 */
bool PowertrainSafetyFilterSpeed(int32_t requested_rpm, uint16_t throttle,
uint8_t throttle_mode, int32_t *out_rpm);
/* 每 100ms 在 ProcessEscDataTask 收到 ESC 反馈后调用 */
void PowertrainSafetyOnEscFeedback(uint16_t running_error, uint16_t selfcheck_error,
int32_t esc_speed);
/* ecu_manager 置位 FAULT_ESC_COMMUNICATION 时调用 */
void PowertrainSafetyOnEscCommFault(bool active);
/* ble 断链 / mode255 时调用 */
void PowertrainSafetyOnRcLinkLost(void);
PowertrainSafetyState_t PowertrainSafetyGetState(void);
#endif
11.3 M1:修正 UpdateESCErrorCodeByRunningStatus(esc_manager.c)
替换当前 225~231 行(bit6 无清除)为:
/* ----- 文件: App/src/esc_manager.c ----- */
/* ----- 函数: UpdateESCErrorCodeByRunningStatus ----- */
#define FAULT_STALL_SETTLE_CYCLES (500 / 200) /* 500ms 置位,ProcessEscDataTask ~200ms/次 */
#define FAULT_STALL_CLEAR_CYCLES (3000 / 200) /* 3s 清除滞回 */
static void UpdateESCErrorCodeByRunningStatus(EscSetSpeedResponseData_t *escSpeedInfo) {
static uint32_t faultStallSettleCycle = 0;
static uint32_t faultStallClearCycle = 0;
static uint16_t runningErrorState = 0;
static uint16_t escNativeRunningError = 0;
/* ... 保留现有过温 bit8、过流 bit7、空转 bit9 逻辑 ... */
/* [M1] 转速异常 -> bit1(STALL),禁止 bit6(过压) */
bool speed_anomaly = (escSpeedInfo->speed <= SPEED_ANOMALY_LOW)
|| (escSpeedInfo->speed >= SPEED_ANOMALY_HIGH);
if (speed_anomaly) {
faultStallSettleCycle++;
faultStallClearCycle = 0;
if (faultStallSettleCycle >= FAULT_STALL_SETTLE_CYCLES) {
runningErrorState |= ESC_RUN_ERR_BIT_STALL; /* bit1 -> 0x20000 */
}
} else {
if (faultStallSettleCycle > 0) {
faultStallSettleCycle--;
}
if (runningErrorState & ESC_RUN_ERR_BIT_STALL) {
faultStallClearCycle++;
if (faultStallClearCycle >= FAULT_STALL_CLEAR_CYCLES) {
runningErrorState &= ~ESC_RUN_ERR_BIT_STALL;
faultStallClearCycle = 0;
}
} else {
faultStallClearCycle = 0;
}
}
/* [M4] 保留 ESC 固件上报位,再叠加 ECU 合成位 */
escNativeRunningError = escSpeedInfo->runningError;
escSpeedInfo->runningError = escNativeRunningError | runningErrorState;
}
正当性:
- bit1 与
JetWave.xml/ App131072/ OSSescEventCode同口径。 - 置位/清除带滞回,避免单次噪声 speed 跳变 锁死。
|合并保留 ESC 原生 STALL,ECU 只做 补充 不 覆盖。
11.4 M2 + M7:安全层 powertrain_safety.c(核心伪代码)
/* ----- 文件: App/src/powertrain_safety.c ----- */
static PowertrainSafetyState_t s_state = PWR_SAFE_NORMAL;
static uint32_t s_throttle_low_ms = 0;
static bool s_esc_comm_fault = false;
static bool EscReportsStall(uint16_t running_error, uint16_t selfcheck_error) {
uint32_t esc_event = ((uint32_t)running_error << 16) | selfcheck_error;
return (esc_event & ESC_EVT_MASK_STALL) != 0
|| (running_error & ESC_RUN_ERR_BIT_STALL) != 0;
}
void PowertrainSafetyOnEscFeedback(uint16_t running_error, uint16_t selfcheck_error,
int32_t esc_speed) {
(void)esc_speed;
if (s_state == PWR_SAFE_NORMAL && EscReportsStall(running_error, selfcheck_error)) {
s_state = PWR_SAFE_LIMP;
ESP_LOGW("PWR_SAFE", "Enter LIMP: STALL detected");
}
if (s_state == PWR_SAFE_RECOVERY_WAIT && !EscReportsStall(running_error, selfcheck_error)) {
/* ESC 已清除,仍须等松油门计时 */
}
}
void PowertrainSafetyOnEscCommFault(bool active) {
s_esc_comm_fault = active;
if (active && s_state == PWR_SAFE_NORMAL) {
s_state = PWR_SAFE_LIMP;
ESP_LOGW("PWR_SAFE", "Enter LIMP: ESC comm fault");
}
}
void PowertrainSafetyOnRcLinkLost(void) {
s_state = PWR_SAFE_LIMP;
ESP_LOGW("PWR_SAFE", "Enter LIMP: RC link lost");
}
bool PowertrainSafetyFilterSpeed(int32_t requested_rpm, uint16_t throttle,
uint8_t throttle_mode, int32_t *out_rpm) {
/* [M3] 断链:mode 255 与 ble 回调双路径均会进 LIMP */
if (throttle_mode == 255) {
s_state = PWR_SAFE_LIMP;
}
switch (s_state) {
case PWR_SAFE_NORMAL:
*out_rpm = requested_rpm;
return true;
case PWR_SAFE_LIMP:
*out_rpm = 0;
if (throttle <= THROTTLE_DEADBAND && !s_esc_comm_fault) {
s_state = PWR_SAFE_RECOVERY_WAIT;
s_throttle_low_ms = 0;
}
return false; /* 表示已限扭 */
case PWR_SAFE_RECOVERY_WAIT:
*out_rpm = 0;
if (throttle > THROTTLE_DEADBAND) {
s_throttle_low_ms = 0; /* 用户未松油门,不允许恢复 */
} else {
s_throttle_low_ms += 20; /* 调用周期 20ms */
if (s_throttle_low_ms >= STALL_RECOVERY_HOLD_MS) {
s_state = PWR_SAFE_NORMAL;
ESP_LOGI("PWR_SAFE", "Exit to NORMAL after throttle release");
}
}
return false;
default:
*out_rpm = 0;
return false;
}
}
在 MotorSpeedControlTask 末尾统一发 CAN(替换多处 SendSetSpeedRequest):
/* ----- 文件: App/src/esc_manager.c ----- */
/* ----- 函数: MotorSpeedControlTask,在计算出 MotorSpeed 之后 ----- */
static void ApplySpeedWithSafety(int32_t raw_motor_speed, uint16_t throttle, uint8_t mode) {
int32_t safe_rpm = raw_motor_speed;
PowertrainSafetyFilterSpeed(raw_motor_speed, throttle, mode, &safe_rpm);
/* 空转 bit9:与安全层叠加,取更严格者 */
if (LastEscSpeedData.runningError & (uint16_t)(1u << 9)) {
safe_rpm = 0;
}
SendSetSpeedRequest(safe_rpm);
}
/* case BEGINNER/INTERMEDIATE/ADVANCED: */
/* ... 计算 MotorSpeed ... */
ApplySpeedWithSafety(MotorSpeed, ThrottleAndGear.throttle, ThrottleAndGear.mode);
break;
/* case 254: */
ApplySpeedWithSafety(0, 0, 254);
break;
/* case 255: [M3] 必须发零速 */
ApplySpeedWithSafety(0, 0, 255);
break;
正当性(为何能解决「有油门无电机仍狂发转速」):
| 阶段 | 旧逻辑 | 新逻辑 |
|---|---|---|
| ESC STALL | 仍 SendSetSpeed(N) |
LIMP → 恒发 0 |
| 用户继续推油门 | ECU 与 ESC 对抗 | out_rpm=0,直至进入 RECOVERY_WAIT |
| 恢复 | 无定义 | 松油门 ≥2s 且(通信正常后)回 NORMAL |
| 断链 | 不发 CAN | 强制 0 |
11.5 M3:mode=255 补丁(最小改动,不建安全层时)
若暂不引入 powertrain_safety.c,至少改 esc_manager.c:
case 255:
ThrottleAndGear.throttle = 0;
MotorSpeed = 0;
SendSetSpeedRequest(0); /* ← 新增这一行 */
break;
并在 Ble/src/ble_server.c 的 ESP_GATTS_DISCONNECT_EVT 中可选调用 SendSetSpeedRequest(0) 双保险(断链回调与 20ms 任务可能存在 ≤20ms 窗口)。
11.6 M5:ecu_manager.c BMS 通信判断
/* ----- 文件: App/src/ecu_manager.c ----- */
/* 约 510 行:将 GetRcConnectionStatus() 改回 ----- */
} else if (GetBmsConnectionStatus()) { /* 未收到电池数据且 BMS 蓝牙已连接 */
BMS_CommunicationFailure++;
...
}
正当性: BMS 链路状态应由 BMS BLE Client 决定;用 RC Server 连接态会 误清零或误置位 BMS 故障。
11.7 M6:ESC 通信故障与 UAVCAN 降频
6a — uavcan_twai.c:故障时全帧入队
static void HandleEscSetSpeedResponse(CanardRxTransfer *transfer) {
static uint8_t sendCount = 0;
if (esc_SetSpeedResponse_decode(transfer, &escSetSpeedResponseData)) {
return;
}
bool has_fault = (escSetSpeedResponseData.running_error != 0)
|| (escSetSpeedResponseData.selfcheck_error != 0);
if (has_fault || (++sendCount >= 10)) {
SendEscSpeedResponseDataToQueue(escSetSpeedResponseData, pdMS_TO_TICKS(10));
sendCount = 0;
}
}
6b — ecu_manager.c:置位通信故障时通知安全层
if (ESC_CommunicationFailure >= 240) {
ecuEventCode |= FAULT_ESC_COMMUNICATION;
PowertrainSafetyOnEscCommFault(true); /* 新增 */
} else if (收到 ESC 数据) {
ecuEventCode &= ~FAULT_ESC_COMMUNICATION;
PowertrainSafetyOnEscCommFault(false);
}
正当性: 降低「STALL 后反馈变慢 → 12s 假断联」概率;假断联时 同样拉闸,避免用户在「通信异常」下继续推油门。
11.8 安全停板专项实现(M8 / M9 / LSS 三层)
对应 §8.3 分层安全停板(LSS);解决 断联、失速、不可控故障 后如何 更好停止板体。
11.8.1 停板触发统一入口(powertrain_safety.c)
/* ----- 文件: App/src/powertrain_safety.c ----- */
#define ESC_FEEDBACK_LOST_STOP_MS 500 /* M9:500ms 无反馈即停板(≠12s 报障) */
#define ESC_FEEDBACK_LOST_REPORT_MS 12000 /* 仍用 ecu_manager 12s 置 FAULT_ESC_COMM */
#define CRITICAL_ESC_EVENT_MASK (ESC_EVT_MASK_STALL | 0x00010000u | 0x00040000u)
static bool s_limp = false;
static bool s_was_driving = false;
static uint32_t s_esc_feedback_lost_ms = 0;
void PowertrainSafetySetLimp(const char *reason) {
if (!s_limp) {
s_limp = true;
ESP_LOGW("PWR_SAFE", "LIMP: %s", reason);
}
}
bool PowertrainSafetyIsLimp(void) {
return s_limp;
}
/* 每 100ms:ProcessEscDataTask 收到 ESC 时调用 */
void PowertrainSafetyOnEscFeedback(uint16_t running_error, uint16_t selfcheck_error,
int32_t esc_speed) {
uint32_t esc_event = ((uint32_t)running_error << 16) | selfcheck_error;
s_esc_feedback_lost_ms = 0;
if (esc_event & CRITICAL_ESC_EVENT_MASK) {
PowertrainSafetySetLimp("esc_critical_fault");
}
}
/* 每 50ms:ecu_manager 未收到 EscRunningData 时调用 */
void PowertrainSafetyOnEscFeedbackTimeout(bool rc_connected, bool had_recent_drive) {
if (!rc_connected) {
return;
}
s_esc_feedback_lost_ms += 50;
if (had_recent_drive && s_esc_feedback_lost_ms >= ESC_FEEDBACK_LOST_STOP_MS) {
PowertrainSafetySetLimp("esc_feedback_lost"); /* M9:快停,不等 12s */
}
}
/* 断链:ble_server DISCONNECT 与 mode255 双路径 */
void PowertrainSafetyOnRcLinkLost(void) {
PowertrainSafetySetLimp("rc_link_lost");
SendSetSpeedRequest(0); /* L2:立即拉闸,不等待 20ms 任务 */
}
11.8.2 L1:控制任务出口(esc_manager.c)
static void ApplySpeedWithSafety(int32_t raw_rpm, uint16_t throttle, uint8_t mode) {
int32_t safe_rpm = raw_rpm;
if (mode == 255 || !GetRcConnectionStatus()) {
PowertrainSafetyOnRcLinkLost();
}
if (PowertrainSafetyIsLimp()) {
safe_rpm = 0;
} else {
PowertrainSafetyFilterSpeed(raw_rpm, throttle, mode, &safe_rpm);
if (safe_rpm != 0) {
s_was_driving = true;
}
}
if (LastEscSpeedData.runningError & (1u << 9)) {
safe_rpm = 0;
}
SendSetSpeedRequest(safe_rpm);
}
11.8.3 L3:零速看门狗任务(M8,新建)
/* ----- 文件: App/src/powertrain_safety.c ----- */
#define SAFE_STOP_WATCHDOG_PERIOD_MS 50
static void SafeStopWatchdogTask(void *arg) {
while (1) {
if (PowertrainSafetyIsLimp() || !GetRcConnectionStatus()) {
/* 周期性重复零速:覆盖 ESC 丢帧、OTA 挂起控制任务等情况 */
SendSetSpeedRequest(0);
}
vTaskDelay(pdMS_TO_TICKS(SAFE_STOP_WATCHDOG_PERIOD_MS));
}
}
void PowertrainSafetyInit(void) {
xTaskCreatePinnedToCore(SafeStopWatchdogTask, "SafeStopWatchdog",
2048, NULL, 16, NULL, 1); /* 优先级 ≥ MotorSpeedControl */
}
正当性:
| 场景 | 仅 L1 | 加 L3 看门狗 |
|---|---|---|
| 失速后控制任务正常 | 每 20ms 发 0 | 20ms + 50ms 双保险 |
OTA 挂起 MotorSpeedControl |
❌ 可能无零速 | ✅ 50ms 仍发 0 |
| 断链只发一次 0 但 ESC 未执行 | ❌ | ✅ 50ms 重试 |
| CAN Bus-Off 恢复窗口 | 间歇 | 恢复后持续补 0 |
11.8.4 ecu_manager.c:快停与慢报障分离(M9)
/* ProcessEcuDataTask 内,ReceiveEscRunningDataFromQueue 失败分支 */
static bool HadRecentDrive(void) {
return PowertrainSafetyIsLimp() || (GetLatestEcuHeatbeatData()->motor_speed != 0);
/* 或维护「过去 2s 内发过非零 SetSpeed」标志 */
}
if (ReceiveEscRunningDataFromQueue(...) == pdPASS) {
...
PowertrainSafetyOnEscFeedback(esc.runningError, esc.selfcheckError, esc.speed);
ESC_CommunicationFailure = 0;
ecuEventCode &= ~FAULT_ESC_COMMUNICATION;
} else if (GetRcConnectionStatus()) {
PowertrainSafetyOnEscFeedbackTimeout(true, HadRecentDrive()); /* 500ms → LIMP */
ESC_CommunicationFailure++;
if (ESC_CommunicationFailure >= 240) { /* 12s:仅上报,停板已由 M9 完成 */
ecuEventCode |= FAULT_ESC_COMMUNICATION;
}
}
11.8.5 ble_server.c:断链双保险(L2)
/* ESP_GATTS_DISCONNECT_EVT 内,SendRcThrottleAndGearCmd 之后 */
PowertrainSafetyOnRcLinkLost(); /* 内已 SendSetSpeedRequest(0) */
11.8.6 停板后逻辑流(修改后 · 正当性)
flowchart TD
START[20ms 控制周期] --> CHK_LINK{RC 连接?}
CHK_LINK -->|否| LIMP[进入 LIMP + 立即 CAN 0]
CHK_LINK -->|是| CHK_FB{ESC 反馈 500ms 内?}
CHK_FB -->|否且曾在驱动| LIMP
CHK_FB -->|是| CHK_STALL{STALL/严重故障?}
CHK_STALL -->|是| LIMP
CHK_STALL -->|否| NORMAL[按油门计算 rpm]
NORMAL --> GATE[ApplySpeedWithSafety]
LIMP --> ZERO[safe_rpm=0]
ZERO --> CAN[SendSetSpeed 0]
CAN --> WD[看门狗 50ms 再发 0]
WD --> REC{松油门2s且故障清除?}
REC -->|是| NORMAL
REC -->|否| LIMP
为何这是「更好停止板体」的方案:
- 断联:不等用户、不等日志 — 断链 立即 + 周期 发 CAN 零速。
- 失速/不可控:不依赖 12s 通信故障 — 500ms 无反馈 或 STALL 位 即停。
- 不依赖单任务 — 看门狗在控制任务挂起时仍拉闸。
- 恢复可预期 — 必须松油门 + 故障清除,避免「停不下来」和「起不来」两种极端。
11.8.7 可选增强 M10:长时间 LIMP 后重启 ESC
static uint32_t s_limp_duration_ms = 0;
/* 在 SafeStopWatchdogTask 中 */
if (PowertrainSafetyIsLimp()) {
SendSetSpeedRequest(0);
s_limp_duration_ms += SAFE_STOP_WATCHDOG_PERIOD_MS;
if (s_limp_duration_ms >= 3000 && !EscFeedbackSeenRecently()) {
SendEscRebootRequest(); /* 仅当产品/ESC 手册允许 */
s_limp_duration_ms = 0;
}
} else {
s_limp_duration_ms = 0;
}
需与电调厂家确认:重启是否 Safe、水上是否允许。
11.9 修改后端到端逻辑流程(正当性说明)
11.8.1 总体数据流(修改后)
flowchart TB
RC[RC ThrottleAndGear 20ms]
MSC[MotorSpeedControlTask]
SAFE[PowertrainSafetyFilterSpeed]
CAN[SendSetSpeedRequest]
ESC[ESC 固件]
PED[ProcessEscDataTask 100ms]
UPD[UpdateESCErrorCodeByRunningStatus]
SAFE_FB[PowertrainSafetyOnEscFeedback]
ECU[ProcessEcuDataTask 50ms]
HB[Heartbeat / Event → App]
RC --> MSC
MSC -->|raw MotorSpeed| SAFE
SAFE -->|safe_rpm 可能强制 0| CAN
CAN --> ESC
ESC --> PED
PED --> UPD
UPD --> SAFE_FB
SAFE_FB --> SAFE
PED --> ECU
ECU -->|FAULT_ESC_COMM| SAFE
ECU --> HB
设计原则对照:
| 原则 | 修改后如何满足 |
|---|---|
| 失效安全 fail-safe | LIMP 默认 rpm=0;断链/通信/STALL 均进入 LIMP |
| 单点写 CAN 转速 | 仅 ApplySpeedWithSafety → SendSetSpeedRequest |
| 控制与解耦 | 安全层只输出 safe_rpm,不修改油门协议 |
| 可恢复 | RECOVERY_WAIT 要求松油门 2s,避免「顶着油门」反复触发 STALL |
| 协议一致 | STALL 仍报 0x20000,与 App/OSS 一致 |
11.8.2 安全状态机(修改后)
stateDiagram-v2
[*] --> NORMAL
NORMAL --> LIMP: ESC STALL 或 ECU_COMM 或 RC 断链 mode255
LIMP --> RECOVERY_WAIT: 油门≤死区 且 非通信故障阻塞
RECOVERY_WAIT --> NORMAL: 松油门≥2s
RECOVERY_WAIT --> LIMP: 油门再次推高 或 再次 STALL
LIMP --> LIMP: 持续 SendSetSpeed 0
NORMAL --> NORMAL: 无故障且 safe_rpm=requested
为何需要 RECOVERY_WAIT(正当性):
- 现场「失速不可控」常因用户 持续满油门;ESC 保护要求 先卸负载 才能清除。
- 若 STALL 清除后立即回
NORMAL而油门仍为 1000,会 瞬间再次堵转。 - 通信故障 (
s_esc_comm_fault) 时暂不允许进入恢复,避免 链路未恢复就加扭。
11.8.3 时序:STALL 场景(修改后预期)
sequenceDiagram
participant U as 用户
participant ECU as MotorSpeedControl + Safety
participant ESC as 电调
U->>ECU: 油门 800
ECU->>ESC: SetSpeed(N)
ESC-->>ECU: running_error STALL
ECU->>ECU: state=LIMP
ECU->>ESC: SetSpeed(0) 每20ms
U->>ECU: 油门仍 800
ECU->>ESC: SetSpeed(0) 忽略请求
U->>ECU: 油门回 0 持续2s
ECU->>ECU: state=NORMAL
U->>ECU: 油门 300
ECU->>ESC: SetSpeed(n) 恢复
对比 修改前:用户在步骤「油门仍 800」时 ECU 仍发 SetSpeed(N),与 ESC 保护冲突。
11.8.4 时序:BLE 断链(修改后预期)
sequenceDiagram
participant BLE as ble_server
participant ECU as MotorSpeedControl
participant ESC as 电调
BLE->>ECU: mode=255
ECU->>ECU: LIMP
ECU->>ESC: SetSpeed(0)
Note over ESC: ≤20ms 内收到零速指令
11.10 合入顺序与自测清单
| 步骤 | 内容 | 验证(不以 OSS 为准) |
|---|---|---|
| 1 | M3 + L2 断链零速 | 断 BLE,CAN 立即且持续 SetSpeed=0 |
| 2 | M8 看门狗 | OTA 挂起 MotorSpeedControl 后 CAN 仍每 50ms 为 0 |
| 3 | M9 500ms 快停 | 拔 CAN 模拟反馈丢失,<1s 内零速(不必等 12s) |
| 4 | M1 bit1 + 清除 | 堵转语义对齐 131072 |
| 5 | M4 OR 合并 | ESC 原生 STALL 能进 LIMP |
| 6 | M2/M7 安全层 | 堵转/失速后持续推油门,CAN 恒为 0 |
| 7 | M5 BMS | BMS 断开不误用 RC 状态 |
| 8 | M6 降频 | 故障帧全量入队;12s 仅作 App 上报 |
停板专项台架(§8.3):
| 用例 | 操作 | 通过标准 |
|---|---|---|
| T-STOP-1 | 满速运行中断 BLE | 200ms 内 CAN 零速;50ms 看门狗持续补 0 |
| T-STOP-2 | 台架堵转 STALL | 检测到 STALL ≤1 周期 进入 LIMP,CAN 恒 0 |
| T-STOP-3 | 运行中拔 ESC CAN | 500ms 内 停板(与 12s 弹窗无关) |
| T-STOP-4 | LIMP 中持续推油门 | 电机不得再次加扭,直至松油门 2s |
11.11 与当前 feature_motorstuck 的差异小结(给 Code Review)
| 位置 | 当前分支 | 建议修改后 |
|---|---|---|
esc_manager.c:227 |
bit6 堵转注释 |
bit1 + 滞回 |
esc_manager.c:231 |
runningError = runningErrorState 覆盖 |
native \| state |
esc_manager.c:363 |
直接 SendSetSpeedRequest |
ApplySpeedWithSafety |
esc_manager.c:376 |
255 无 CAN | SendSetSpeed(0) |
| 无 | — | powertrain_safety.c + SafeStopWatchdogTask |
| 无 | — | 500ms 反馈丢失快停(M9) |
九、测试用例(分支专项)
| ID | 步骤 | 期望(合入前) | 期望(修完后) |
|---|---|---|---|
| T-M1 | 台架堵转至 ESC 报 131072 | 仍可能持续发转速 | 零速 + 松油门恢复 |
| T-M2 | 注入 speed=25000 | 误报 4194304(当前) | 应报 STALL 或专用码,且拉闸 |
| T-M3 | 对比 master/bit1 路径 | master 可能 131072 | 行为一致且可恢复 |
| T-M4 | BLE 断开 | CAN 可能保持旧转速 | 200ms 内零速 |
| T-M5 | Ulog RC+ESC | 本分支可记录 | 对齐 OSS 15349 时间轴 |
十、结论
feature_motorstuck未解决用户主诉「失速 + 断联 + 遥控器无法控制」的三大结构性缺陷(STALL 不拉闸、假断联逻辑、断链不零速)。- 分支 实质改动 是:转速异常从 bit1(STALL) 改为 bit6(过压),并启用 空转 bit9 限扭、打开 RC Ulog、移除 ALC/NearLink。
- 从协议语义看,bit6 标注「堵转」是错误的,会导致与现场 131072 分析口径 脱节,并可能 误触发过压告警。
- 建议:不要将当前分支当作 motor stuck 修复版合并;断联/失速如何更好停板见 §8.3 分层安全停板(LSS) 与 §11.8 实现。
- 合入后必须以 §11.10 T-STOP-1~4(CAN 抓包 + 断 BLE 台架)为提测门槛,不以 OSS 失速后数据为准。
附录 A:master vs feature_motorstuck 对照表
| 文件/逻辑 | master | feature_motorstuck |
|---|---|---|
| 转速异常 ECU 合成 | speed<=-20000 → bit1 → STALL |
\|speed\|>=20000 → bit6 → OV |
| STALL 后限扭 | 无 | 无 |
| 空转 bit9 执行 | 注释 | MotorSpeed=0 |
| mode255 零速 | 无 | 无 |
| ALC | 有 | 无 |
| NearLink | 有 | 无 |
| Ulog RC | 注释 | 开启 |
| BMS 通信条件 | GetBmsConnectionStatus |
GetRcConnectionStatus ⚠️ |
esc_manager 行数 |
~666 | ~553 |
附录 B:关键源码索引(本分支)
| 主题 | 路径 |
|---|---|
| 转速异常 bit6 | App/src/esc_manager.c 225~231 |
| 电机控制任务 | 同上 282~390 |
| ESC 通信故障 | App/src/ecu_manager.c 479~497 |
| UAVCAN 降频 | Protocol/src/uavcan_twai.c 58~62 |
| 入口 BLE only | main/main.c 124~139 |
附录 C:相关报告
| 文档 | 说明 |
|---|---|
JETWAVE_ECU_POWERTRAIN_DEEP_ANALYSIS_REPORT.md |
master 深度报告 |
NETHERLANDS_ABNORMAL_REPORT_20260526_190035.md |
荷兰 STALL 样本 |
CODE_REVIEW_交互架构与问题导读.md |
App 131072/1073741824 |
附录 D:§十一 修改项速查
| ID | 文件 | 函数/位置 |
|---|---|---|
| M1 | App/src/esc_manager.c |
UpdateESCErrorCodeByRunningStatus |
| M2/M7 | App/src/powertrain_safety.c(新) |
PowertrainSafetyFilterSpeed |
| M2/M7 | App/src/esc_manager.c |
ApplySpeedWithSafety / MotorSpeedControlTask |
| M3 | App/src/esc_manager.c |
case 255 |
| M4 | App/src/esc_manager.c |
UpdateESCErrorCodeByRunningStatus 末尾合并 |
| M5 | App/src/ecu_manager.c |
BMS 通信 ~510 行 |
| M6a | Protocol/src/uavcan_twai.c |
HandleEscSetSpeedResponse |
| M6b | App/src/ecu_manager.c |
FAULT_ESC_COMMUNICATION 置位/清除 |
| M8 | App/src/powertrain_safety.c |
SafeStopWatchdogTask(停板 L3) |
| M9 | ecu_manager.c + powertrain_safety.c |
OnEscFeedbackTimeout 500ms 快停 |
| M10 | powertrain_safety.c |
可选 SendEscRebootRequest |
附录 E:停板方案速查(§8.3)
| 场景 | 推荐动作 |
|---|---|
| 蓝牙断联 | 立即 SendSetSpeed(0) + 看门狗 50ms 重复 |
| 失速 STALL | 立即 LIMP,safe_rpm=0 |
| ESC 反馈丢失 | 500ms 停板;12s 仅上报通信故障 |
| OTA 挂起控制任务 | 看门狗仍发零速 |
| 恢复 | 松油门 2s + 无 STALL + 通信恢复 |
静态审计结论;ESC 固件 STALL 阈值与恢复需 CAN 抓包与厂家文档确认。实施细节以 §十一 为准。