双向 DSHOT(BDShot)收发实现概要设计
目录
- 双向 DSHOT 协议要点
- 1.1 命令帧(沿用脉宽编码,但反相)
- 1.2 回传帧的线序:逻辑 1 = 有沿,逻辑 0 = 无沿
- 1.3 GCR 与 21 bit 线序
- 1.4 eRPM 载荷格式
- 1.5 取反 CRC 算法(命令帧与回传帧通用)
- 总体架构:单线半双工
- 发送:命令帧(PWMV2 输出比较 + DMAV2 突发)
- 方向切换与时序预算
- 接收:回传帧(2 种采集方案)
- 回传帧软件解码流水线
- 关键寄存器映射
- 资源占用与选型
- Bring-up 步骤与常见坑
1. 双向 DSHOT 协议要点
双向 DSHOT(Bidirectional DSHOT,简称 BDShot / DShot-Telemetry)在同一根信号线上、以时分半双工的方式完成两件事:
- 命令帧:飞控(FC)→ 电调(ESC),发送 16 bit 油门/命令帧;
- 回传帧:ESC → FC,在收到命令帧后约 30 μs(标称)开始,把电机转速(eRPM)编码为 16 bit 回传;慢速 ESC 固件此间隙可达 ~70 μs,FC 侧应据此设置接收超时(见 §4)。
与单向 DSHOT 相比有三处关键差异:
| 差异点 | 单向 DSHOT | 双向 DSHOT(BDShot) |
|---|---|---|
| 线路极性 | 空闲低,高脉冲有效 | 线路反相:空闲高,低脉冲为命令帧有效电平 |
| CRC | crc = (v ^ (v>>4) ^ (v>>8)) & 0xF(不取反) |
取反:crc = ~(v ^ (v>>4) ^ (v>>8)) & 0xF,命令帧与回传帧通用(见 §1.5) |
| 回传帧 | 无 | ESC 回传 21 bit 线序(差分/边沿编码 + GCR),速率 = DSHOT 速率 × 5/4 |
1.1 命令帧(沿用脉宽编码,但反相)
命令帧仍是 16 bit = 11 bit 油门 + 1 bit 遥测请求 + 4 bit CRC,MSB 先发,每个 bit 用脉宽编码(bit1 ≈ 75% T、bit0 ≈ 37.5% T)。唯一区别是整条线反相:空闲为高电平,"有效脉冲"是拉低。硬件实现上,只需在单向方案基础上把 PWM 输出置为反相(OUT_POLARITY = 1),波形发生逻辑与单向完全一致。
命令帧速率与位周期(与单向相同):
| 速率等级 | 命令帧位速率 | 命令帧位周期 T | 一帧(16 bit) | 回传帧位速率 (×5/4) | 回传帧位周期 |
|---|---|---|---|---|---|
| DSHOT150 | 150 kbps | 6.67 μs | 106.7 μs | 187.5 kbps | 5.33 μs |
| DSHOT300 | 300 kbps | 3.33 μs | 53.3 μs | 375 kbps | 2.67 μs |
| DSHOT600 | 600 kbps | 1.67 μs | 26.7 μs | 750 kbps | 1.33 μs |
| DSHOT1200 | 1200 kbps | 0.83 μs | 13.3 μs | 1500 kbps | 0.67 μs |
本文正文以 DSHOT600 为默认例子(命令帧 600 kbps、回传帧 750 kbps),代码中所有时间参数都参数化,可一处改动切换速率等级。
1.2 回传帧的线序:逻辑 1 = 有沿,逻辑 0 = 无沿(差分编码)
回传帧不是脉宽编码,而是差分/边沿编码(与命令帧本质不同):
- 空闲电平 = 高(反相线路);
- 回传帧被拆成固定长度的位槽(bit slot),每个位槽时长 = 回传帧位周期(DSHOT600 → 1.333 μs);
- 逻辑 1:该位槽内电平发生一次翻转(有边沿);
- 逻辑 0:该位槽内电平保持不变(无边沿);
- 起始位固定为 1(有沿):21 bit 线序的第一个位槽永远是逻辑 1,因此帧开始处必然产生一个下降沿——线路被 ESC 从 idle high 拉低。这是接收端唯一可靠的帧同步标志(RX 就是靠"第一个下降沿"锁定位槽栅格,见 §6)。
⚠ 易错点:不要把"电平域的 0"和"差分域的 0"混为一谈。
常见说法"回传帧以 0 开头"指的是物理电平——线路确实从 idle high 被拉低到低电平(0)。但在差分/逻辑域,这个起始位是 1(发生了翻转)。
若误把起始位当作差分域的 0(不翻转),帧起点就没有边沿,首个下降沿的位置会随载荷数据漂移(实测:电机停转帧落在第 2 槽、其它帧落在第 1 槽),接收端无法建立位槽栅格,解码必然失败。

图 1.2 回传帧线序(差分/边沿编码,idle high)。示例逻辑位序列 1(SB) 0 1 1 0 0 1 1 0 1:起始位在差分域固定为 1,所以帧开始处必有一个下降沿(红色竖线),作为接收端与数据无关的帧同步点;随后逻辑 1 = 该位槽内有一次电平翻转,逻辑 0 = 电平保持。每个位槽宽 = 回传帧位周期(DSHOT600 → 1.333 μs)。
因此 RX 端的核心动作是:测量每个位槽内"有没有发生电平翻转",翻转→1、不变→0。注意电平高低本身不表示逻辑 1/0,边沿的有无才表示逻辑位;当前位槽结束后的绝对电平,由前面所有位的累积翻转决定。
1.3 GCR 与 21 bit 线序
ESC 侧的编码链路:16 bit 逻辑值 → 20 bit GCR → 前置 1 个起始位 → 21 bit 差分线序。
- GCR(Gray Code Reversed):把 16 bit 拆成 4 个 nibble,每个 nibble 用 5 bit 五元组(quintet)表示,共 20 bit。GCR 的作用是限制连续 0 的个数(最多 2 个连 0),保证线上有足够多的边沿供接收端定位位槽边界;
- 差分展开:在 20 bit GCR 前补一个起始位 1,从空闲高电平起,按"遇 1 翻转、遇 0 保持"逐位生成 21 bit 线上电平序列。起始位为 1 保证了第一个位槽必定把线路拉低,给接收端一个与数据无关的固定同步沿(见 §1.2 易错点)。
RX 解码就是这条链路的逆过程(见 §6)。GCR 五元组映射表(nibble ↔ quintet):
| nibble | 0x0 | 0x1 | 0x2 | 0x3 | 0x4 | 0x5 | 0x6 | 0x7 |
|---|---|---|---|---|---|---|---|---|
| quintet | 0x19 | 0x1B | 0x12 | 0x13 | 0x1D | 0x15 | 0x16 | 0x17 |
| 二进制 | 11001 | 11011 | 10010 | 10011 | 11101 | 10101 | 10110 | 10111 |
| nibble | 0x8 | 0x9 | 0xA | 0xB | 0xC | 0xD | 0xE | 0xF |
|---|---|---|---|---|---|---|---|---|
| quintet | 0x1A | 0x09 | 0x0A | 0x0B | 0x1E | 0x0D | 0x0E | 0x0F |
| 二进制 | 11010 | 01001 | 01010 | 01011 | 11110 | 01101 | 01110 | 01111 |
观察二进制列可以验证 GCR 的设计意图:16 个 quintet 中任意一个内部、以及任意两个相邻拼接处,连续 0 都不超过 2 个——这正是"保证线上边沿密度、供接收端定位位槽边界"的来源。
1.4 eRPM 载荷格式
回传的 16 bit = 12 bit 遥测载荷 + 4 bit CRC。12 bit 载荷格式为 eee mmmmmmmmm:
- 高 3 bit = 指数
e(左移位数 0~7); - 低 9 bit = 尾数
m; - 周期
period_us = m << e; eRPM = 60_000_000 / period_us;机械转速RPM = eRPM × 2 / 电机极对数;- 特例:12 bit 全 1(
0x0FFF)表示电机停转(eRPM = 0)。
载荷后跟的 4 bit CRC 与命令帧使用同一套取反 CRC,算法见 §1.5。
1.5 取反 CRC 算法(命令帧与回传帧通用)
双向 DSHOT 的命令帧和回传帧使用同一种 4 bit CRC,且都是取反式。这是与单向 DSHOT 最容易踩错的差异之一:单向不取反、双向取反,用错式子会导致 ESC 拒收命令帧、FC 判所有回传帧无效。
算法:对参与校验的 12 bit 数据,按 nibble(每 4 bit)做异或折叠得到 4 bit 校验和,再取反、取低 4 bit:
csum = data ^ (data >> 4) ^ (data >> 8) // 三个 nibble 异或折叠为 4 bit
crc = (~csum) & 0x0F // 双向:取反后取低 4 bit(单向不取反)
两种帧的差别仅在于"参与 CRC 的 12 bit 数据是什么",算法完全一致:
| 帧 | 参与 CRC 的 12 bit 数据 | 拼帧方式(16 bit) | CRC 用法 |
|---|---|---|---|
| 命令帧(TX) | (throttle << 1) \| tlm_req(11 bit 油门 + 1 bit 遥测请求,见 §1.1) |
data << 4 \| crc |
发送端计算并附加,MSB 先发 |
| 回传帧(RX) | 遥测载荷 eee mmmmmmmmm(GCR 解码后得到,见 §1.4) |
data << 4 \| crc |
接收端对载荷重算,与收到的低 4 bit 比对 |
为什么要取反:单向与双向的多项式折叠部分完全相同,唯一区别是"是否取反"。取反能让全 0 数据也产生非全 0 的 CRC——线路长时间静默(全 0)或 GCR 解出全 0 时不会被误判为合法帧,对差分/GCR 编码的回传帧尤为重要。
编码 / 校验参考实现(命令帧与回传帧共用 dshot_crc_bidir()):
/* 通用:对 12 bit 数据计算双向取反 CRC(命令帧、回传帧都用它) */
static inline uint8_t dshot_crc_bidir(uint16_t data12)
{
return (uint8_t)((~(data12 ^ (data12 >> 4) ^ (data12 >> 8))) & 0x0F);
}
/* 命令帧编码:throttle 0..2047;telemetry_request 双向须为 1 才要求回传 */
static uint16_t dshot_make_frame(uint16_t throttle, uint8_t telemetry_request)
{
uint16_t v = (uint16_t)((throttle << 1) | (telemetry_request & 1)); /* 12 bit */
uint16_t crc = dshot_crc_bidir(v); /* 取反 CRC */
return (uint16_t)((v << 4) | crc); /* 16 bit,MSB 先发 */
}
/* 回传帧校验:val16 = 12 bit 载荷 << 4 | 4 bit CRC */
static inline bool dshot_check_crc(uint16_t val16)
{
uint16_t tele = (uint16_t)(val16 >> 4); /* 12 bit 遥测载荷 */
uint16_t crc = (uint16_t)(val16 & 0x0F); /* 线上收到的 4 bit CRC */
return dshot_crc_bidir(tele) == crc; /* 相等即该帧有效 */
}
本节是协议层定义,TX(§3)与 RX 解码(§6)均直接复用
dshot_crc_bidir(),全文只维护这一份实现。
2. 总体架构:单线半双工
一根线,一个 PWMV2 通道,在每次 DSHOT 更新里按 ① 发送命令帧 → ② 换向死区 → ③ 等待 ESC(空闲高)→ ④ 接收回传帧 → ⑤ 复位切回 五个阶段循环:

② 与 ③ 是两个不同的"死区"概念,容易混淆:② 是 FC 主动把引脚从输出切成输入的换向死区(~2 μs,由 MCU 决定);③ 是命令帧发完后被动等待 ESC 开始回传的空闲间隙(标称 ~30 μs,慢速 ESC 最坏 ~70 μs,由 ESC 固件决定)。二者都是绝对 μs、与 DSHOT 速率无关;随速率缩放的只有帧长(① 命令帧、④ 回传帧)。工程上 FC 不应"硬等 30 μs",而是切输入后立即布防捕获并挂一个 ~70 μs 超时(见 §4)。
| 阶段 | 引脚方向 | PWMV2 + HDMA(TX / 方案1 RX) | GPTMR0 + TRGM0(方案2 RX,可选) |
|---|---|---|---|
| ① 发送命令帧 | 输出(PWM ALT) | TX:8 通道输出比较、突发 16 位;HDMA 搬 HRAM→CMP 影子 | 不使用 |
| ② 方向切换 | 高阻/输入 | 切 GPIO 输入、上拉(idle 高) | 切 TRGM.Pxx(iomux ALT 17)输入、上拉 |
| ③ 接收回传帧 | 输入 | 方案1:PWMV2 计数器定时作 DMA 节拍 + HDMA 采 GPIO DI(过采样) | 方案2:TRGM 路由 → GPTMR0 ch2/ch3 捕获边沿时间戳(可 DMA 搬) |
| ④ 复位切回 | 输出 | 复位 PWMV2 计数器、重配 TX 缓冲 | 复位并重配 TX 缓冲(TRGM 路由可保持) |
TX 部分直接复用单向 DSHOT 的方案四(PWMV2 + DMAV2 FIX_BURST,见 dshot_oneway_design.md §6),仅加"输出反相"。本设计的新增内容集中在方向切换与RX 回传帧的采集与解码。
3. 发送:命令帧(PWMV2 输出比较 + DMAV2 突发)
- 计数器
RELOAD= 命令帧位周期 T;每路输出用双比较点:- 固定比较点(
CMP[2i].CFG选shadow_val[0] = 0)→ 每个位周期起点(count 0)产生上升沿; - 可变比较点(
CMP[2i+1].CFG选shadow_val[2+i])→ 由 DMA 逐位更新,决定下降沿(脉宽);
- 固定比较点(
- DMA 请求源 =
cmp0(==0)(DMA_EN寄存器,见 §7),每个位周期 1 次请求; - DMAV2 FIX_BURST:一次突发写入连续的 8 个
shadow_val(8 路同 bit),burst 结束目的地址回卷; - 源缓冲 HRAM 按"bit 主序 / 路次序"交织:
HRAM[bit_k*0x20 + ch_i*4]; - 计数器
CNT_BURST = 16:发满 16 位自动停,帧边界由突发长度界定(无需空闲填充字)。
3.1 与单向的差异:输出反相(idle 高)
双向 DSHOT 线路反相。实现上把每个 TX 通道的 PWM[i].CFG0 的 OUT_POLARITY(bit1,mask 0x2)置 1:
/* 单向:REG32_WRITE(MAP_BASE_PWM0+0x104+i*0x10, 0x10000000); // 仅 set oe (CFG1.HIGHZ_EN_N) */
/* 双向:CFG0 置反相;CFG1 置输出使能 */
REG32_WRITE(MAP_BASE_PWM0 + 0x100 + i*0x10, PWMV2_PWM_CFG0_OUT_POLARITY_SET(1)); /* 0x2 */
REG32_WRITE(MAP_BASE_PWM0 + 0x104 + i*0x10, 0x10000000); /* CFG1.HIGHZ_EN_N=1 */
这样:空闲时输出反相为高(idle 高),命令帧的每个"高脉冲"被反相成"低脉冲",占空比不变,ESC 侧判决逻辑不变。
待发送的 16 bit 命令帧由
dshot_make_frame()生成(含取反 CRC,算法见 §1.5),随后按 bit 展开成脉宽写入 HRAM 交织缓冲。
4. 方向切换与时序预算
半双工的关键是"发完就撒手,让出总线给 ESC 回传"。
4.1 两个"死区"必须区分
| 概念 | 是什么 | 数量级 | 谁决定 | 随速率变化? |
|---|---|---|---|---|
| ① FC 换向死区 | FC 把 pad 从 output 重配为 input 的硬件切换时间 | ~ 1~2 μs | MCU 硬件 | ❌ 不变 |
| ② ESC 响应间隙 | 命令帧结束 → ESC 开始回传之间的总线空闲(拉高) | 标称 ~30 μs,慢速 ESC 最坏 ~70 μs | ESC 固件 | ❌ 不变 |
- 30 μs = ESC 的标称/期望间隙(典型固件收到命令帧后算 eRPM + GCR 编码所需的固定处理耗时);
- 70 μs = 应当设置的接收超时窗口(worst-case),用于兜住慢速/老款 ESC,不是常态;
- FC 侧不应
delay(30μs)硬等,正确做法是:切输入后(②,~2 μs)立即布防捕获,同时挂一个 ~70 μs 超时——第一个边沿到来即开始解码,超时未到则判定"本帧无遥测"。 - 会随速率缩放的是帧长(命令帧 16 bit、回传帧 21 bit),而 ② 这段间隙用绝对 μs 度量、不按 bit 数缩放(见 §4.3)。
4.2 方向切换流程
- TX 结束:等
BURSTEND(0x424),确认 16 位已全部移出、线路回到 idle 高; - 切输入(① 换向死区,~2 μs):把承载回传信号的 pad 从 PWM 输出功能切到 RX 输入——方案 1 切到 GPIO 输入(或 PWM capture 输入),方案 2 切到 TRGM.Pxx(iomux ALT 17) 功能 (注意:建议发送命令帧的PWM引脚也使用TRGM.Pxx引脚,这样引脚就不用切换了);并使能上拉 (注意:建议外部上拉,这里就不需要内部上拉了),保证 idle 高、不干扰 ESC 驱动;
- 布防捕获 + 等 ESC(② 响应间隙):切输入后立即开启 RX 采集/捕获,并启动一个 ~70 μs 超时;ESC 标称在 ~30 μs 后开始回传,采集缓冲覆盖"等待 + 回传"整段最稳;超时未见首个边沿 → 本帧无遥测;
- RX 结束:采足样本 / 收满 21 个位槽 / 超时后停止;
- 切回输出:pad 切回 PWM ALT,重配 TX 缓冲,进入下一循环。
4.3 时序预算(DSHOT600、8 kHz 更新率示例)
| 段 | 时长 | 随速率变化 | 说明 |
|---|---|---|---|
| ① TX 命令帧 | 26.7 μs | ✅ 缩放 | 16 bit × 1.667 μs |
| ② 换向死区(FC 切输入) | ~2 μs | ❌ 固定 | pad 重配 + 上拉建立(MCU 决定) |
| ③ ESC 响应间隙 | ~30 μs(标称,最坏 ~70) | ❌ 固定 | 总线空闲拉高,ESC 决定;超时按 ~70 设置 |
| ④ RX 回传帧 | ~28 μs | ✅ 缩放 | 21 bit × 1.333 μs |
| ⑤ 切回 + 解码 | ~10 μs | 约固定 | pad 切回 + 软件 GCR/CRC 解码 |
| 合计(标称) | ≈ 97 μs | — | < 125 μs(8 kHz),有余量 |
不同速率下"③ 固定间隙 + 缩放帧长"的组合决定了可达更新率:
| 速率 | ① 命令帧 | ④ 回传帧 | ③ 间隙(固定) | 一次交互合计(标称) | 8 kHz(125μs) 可容纳 |
|---|---|---|---|---|---|
| DSHOT300 | 53.3 μs | 56 μs (@375k) | ~30 μs | ~139 μs | ❌(故双向 DSHOT300 常降到 4 kHz) |
| DSHOT600 | 26.7 μs | 28 μs (@750k) | ~30 μs | ~85 μs | ✅ |
| DSHOT1200 | 13.3 μs | 14 μs (@1.5M) | ~30 μs | ~57 μs | ✅ |
注意:速率越低,那段"固定 ~30 μs 间隙"占比反而不吃紧,真正把周期撑爆的是被拉长的帧长——这正是双向 DSHOT300 通常只能跑 4 kHz、而 DSHOT600 能跑 8 kHz 的原因。
⚠ 引脚方向切换与上拉是平台/板级相关(IOC/PIOC pad 配置、内部上拉或外部上拉电阻),BDShot 线路通常建议板上加外部上拉(比内部上拉更稳),保证死区期间 idle 稳定为高。
5. 接收:回传帧(2 种采集方案)
回传帧采集只有两条可落地路径,二者定位不同,但共用 §6 解码流水线。
- 方案 1:PWMV2 + DMAV2,DMA 过采样 GPIO 输入电平;
- 方案 2:把 PWM 引脚路由到 TRGM.Pxx,用 GPTMR0 的捕获通道 ch2/ch3 直接锁存边沿时间戳。
5.1 方案 1:PWMV2 + DMAV2 过采样 DMA
思路:PWMV2 的 DMA_EN(0x480)DMA 请求源只有比较点(0~23)/ 重载点(24~27),没有"捕获沿"(查 hpm_pwmv2_regs.h)。因此不能像 GPTMR 那样"每捕获一个边沿就发一次 DMA 把时间戳搬走"。变通办法:用 PWMV2 的比较点/重载点当"固定节拍的 DMA 触发器",让 DMAV2 在每个节拍把该引脚的 GPIO 输入电平寄存器(DI)搬到 RAM,对回传帧做高倍过采样。整帧采完后,软件在 RAM 里把过采样电平序列还原成位槽 → 边沿 → 差分 → GCR → 16 bit。这正是 STM32 上 BDShot 常用的"定时器 + DMA 采 GPIO IDR"做法,在 HPM6E00 上用 PWMV2 + HDMA 等价实现,既是 DMA、又用 PWMV2 IP,契合需求。
PWMV2 计数器(自由运行, RELOAD=采样周期)
│ 每个 cmp0(==0) 命中 → 1 次 DMA 请求
▼
DMA_EN.DMA0_SEL = 0 (cmp0) ──► MOT_GPR/DMAMUX ──► HDMA ch
│ src = GPIO_DI(固定) dst = RAM 缓冲(递增)
▼
RAM: [s0,s1,s2, ... sN] 每个样本 = 该时刻引脚电平
└── 软件: 阈值→电平序列→位槽→边沿→GCR→16bit
采样率:取回传帧位速率的 4~8 倍。DSHOT600 回传 750 kbps → 采样率取 6× = 4.5 MHz → 采样周期 ≈ 222 ns(200 MHz 计数时钟 → RELOAD ≈ 44 ticks)。采样窗口覆盖"死区尾 + ESC 延时 + 回传帧",约 70~80 μs → 样本数 ≈ 4.5 MHz × 80 μs ≈ 360 个。每样本可只取 1 个 32 bit DI 字(burst=1),缓冲 ≈ 360 × 4 B ≈ 1.5 KB,放 HRAM。
关键配置(伪代码,详见 bidir_dshot_6e.c):
/* RX 计数器:自由运行,reload = 采样周期 */
REG32_WRITE(MAP_BASE_PWM0+0x008, 0); /* shadow_val[0]=0 → cmp0 命中点 */
REG32_WRITE(MAP_BASE_PWM0+0x00C, HR(SAMPLE_TICKS)-0x100); /* reload = 采样周期 */
/* cmp0 作 DMA 请求源(每采样周期一次) */
REG32_WRITE(MAP_BASE_PWM0+0x480, 0x80 | 0 /*DMA0_SEL=cmp0*/); /* DMA0_EN=1, sel=0 */
/* HDMA: src=GPIO_DI(固定), dst=RX 缓冲(递增), len=样本数 */
REG32_WRITE(MAP_BASE_HDMA+0x040, /*32bit, src fixed, dst incr, handshake*/ ...);
REG32_WRITE(MAP_BASE_HDMA+0x044, RX_SAMPLES*4);
REG32_WRITE(MAP_BASE_HDMA+0x048, DSHOT_GPIO_DI_ADDR); /* src 固定 */
REG32_WRITE(MAP_BASE_HDMA+0x04C, 0);
REG32_WRITE(MAP_BASE_HDMA+0x050, (uint32_t)rx_buf); /* dst 递增 */
/* 路由 PWM DMA 请求 → HDMA(与 TX 相同的 MOT_GPR/DMAMUX 链路) */
✅ 优点:CPU 只在帧末做一次解码;采样节拍严格由 PWMV2 计数器决定、无抖动;对 ESC 时序容差大(过采样天然抗抖动);不额外占用引脚。
⚠ 注意:DSHOT_GPIO_DI_ADDR是该 pad 所在 GPIO 端口的输入数据寄存器地址;解码时按 bit 位取出对应引脚。若一路 DMA 采整个 32 bit 端口,可同端口多电机共享一次采样。
5.2 方案 2:TRGM.Pxx → GPTMR0 捕获 ch2/ch3
思路:RX 段同样要把同一条线的回传帧采进来,但不走"PWMV2 定时 + 采 GPIO DI",而是把 ESC 回传的物理信号引入 TRGM,经路由送到 GPTMR0 的捕获输入 IN2 / IN3,用 GPTMR 的输入捕获直接锁存边沿时间戳。关键优势:GPTMR 的 DMA 请求源(CR.DMASEL)支持"捕获沿触发 DMA"(值 10 = Input signal toggle captured),而 PWMV2 不支持——所以方案 2 也能像方案 1 那样用 DMA 把时间戳批量搬走,且不占用 PWMV2 的定时/计数资源,与 TX 段彻底解耦。
信号路由(板级 / TRGM):
- 把承载 ESC 回传信号的 pad 配置为 iomux 的
TRGM_P_xx功能(IOC_PAxx_FUNC_CTL_TRGM_P_xx,ALT = 17),物理信号作为HPM_TRGM0_INPUT_SRC_TRGM0_Pxx(0x60~0x7F,P00~P31)进入 TRGM;若走 PWM0 的捕获输入,则用HPM_TRGM0_INPUT_SRC_PWM0_CAPIN0/1(0x8/0x9); - TRGM 输出分别路由到 GPTMR 捕获:
HPM_TRGM0_OUTPUT_SRC_GPTMR0_IN2(0x68,"GPTMR0 channel2 capture in")、HPM_TRGM0_OUTPUT_SRC_GPTMR0_IN3(0x69,"GPTMR0 channel3 capture in");一个输入可 fan-out 到两个输出,故同一个边沿信号同时喂给 ch2 与 ch3; - TRGM 配置:把输入源写进对应
TRGOCFG[out]的TRIGOSEL(低 8 位),REDG2PEN/FEDG2PEN(位 16/17)控制上升/下降沿转发,OUTINV(位 18)可按需反相;信号质量差时可用FILTCFG[]做数字滤波(MODE:000 bypass / 100 rapid / 101 delay / 110,111 stable)。
ESC 回传信号
│ pad = iomux TRGM_P_xx (ALT 17)
▼
TRGM0 输入 TRGM0_Pxx (0x60~0x7F)
│ TRGOCFG[0x68].TRIGOSEL ← 0x6x (fan-out)
├──► GPTMR0_IN2 (0x68) ──► GPTMR0 ch2 上升沿捕获
└──► GPTMR0_IN3 (0x69) ──► GPTMR0 ch3 下降沿捕获
(两通道计数器需同步为同一时基:同 reload + GCR.SWSYNCT 同时启动)
GPTMR0 捕获配置(基址 MAP_BASE_GPTMR0 = 0xF0000000):
- 选一路计数器作时间基(自由运行,
CNT实时计数,0x30); - ch2 / ch3 的
CHANNEL[x].CR:CAPMODE(位 0~2)001=上升沿、010=下降沿、011=双边沿;本方案将 ch2 配上升沿、ch3 配下降沿,得到每个边沿的独立时间戳,电平极性不丢失; - 捕获时间戳锁存到
CHANNEL[x].CAPPOS(0x20,上升沿)/CAPNEG(0x24,下降沿); - 软件帧末把"上升沿时间戳序列 + 下降沿时间戳序列"按时间归并,得到单调的边沿时间戳流,再进 §6;
- DMA 路径(可选,性能等同方案 1):
CR.DMASEL(位 6~7)=10(Input signal toggle captured)→ 每捕获一个边沿发一次 DMA 请求;经 DMAMUXHPM_DMA_SRC_GPTMR0_2(0x46)/HPM_DMA_SRC_GPTMR0_3(0x47)搬进 HDMA 通道,把CAPPOS/CAPNEG直接搬进 RAM;也可用IRQ_EN的捕获标志做逐沿中断(bring-up 简单,但边沿最密处 ~1.33 μs 中断开销偏大)。
/* RX 段:GPTMR0 ch2(上升)/ch3(下降) 输入捕获同一回传信号 */
/* 1) TRGM: 输入 Pxx → 输出 IN2 + IN3(fan-out,示例源 P00=0x60) */
REG32_WRITE(MAP_BASE_TRGM0+0x1000+0x68*4, 0x60 /*TRIGOSEL=TRGM0_P00*/ | (1<<16) /*REDG2PEN*/);
REG32_WRITE(MAP_BASE_TRGM0+0x1000+0x69*4, 0x60 /*TRIGOSEL=TRGM0_P00*/ | (1<<17) /*FEDG2PEN*/);
/* 2) GPTMR0 ch2 = 上升沿捕获, ch3 = 下降沿捕获(CHANNEL 间距 0x40;CR 含 CAPMODE/CEN/DMAEN/DMASEL,见 §7.2) */
REG32_WRITE(MAP_BASE_GPTMR0+0x2*0x40+0x0, 0x1 /*CAPMODE=001 上升*/ | (1<<10) /*CEN*/ | (1<<5) /*DMAEN*/ | (2<<6) /*DMASEL=10 捕获沿*/);
REG32_WRITE(MAP_BASE_GPTMR0+0x3*0x40+0x0, 0x2 /*CAPMODE=010 下降*/ | (1<<10) /*CEN*/ | (1<<5) /*DMAEN*/ | (2<<6) /*DMASEL=10 捕获沿*/);
/* 3) 两通道计数器设为同一自由运行时基(reload=满量程),并用 GCR.SWSYNCT 同步启动 */
/* 4) DMAMUX: GPTMR0_2 / GPTMR0_3 → HDMA;源/目的 = CAPPOS/CAPNEG → RAM */
✅ 优点:直接得到边沿时间戳(省去方案 1 的"电平序列→边沿"还原,解码更省 CPU);GPTMR 有捕获沿 DMA 源,可用 DMA 搬数据。
⚠ 缺点: 使用的DMA通道数较多。
6. 回传帧软件解码流水线
无论方案 1(过采样电平序列)还是方案 2(边沿时间戳),最终都归一到"每个位槽有没有翻转",之后流程一致:
① 采集结果
方案 1: 过采样电平序列 s[0..N] 方案 2: 边沿时间戳 t[0..K]
│ │
▼ ▼
② 归一到位槽: 按回传帧位周期切槽,判定每槽"有沿/无沿"
│
▼ 逻辑1=有沿, 逻辑0=无沿
③ 21 bit 线序 → 相邻槽做 XOR 得 21 个差分位
│ (起点用 idle 高作为参考:diff[i] = level[i] ^ level[i-1])
▼ diff[0] 固定为 1(起始位/同步沿),diff[1..20] 即 20 bit GCR
④ 20 bit GCR → 拆成 4 个 5bit quintet(MSB 先)
│
▼ 反查 GCR 表(无效 quintet → 报错丢帧)
⑤ 16 bit 值 = 12 bit 遥测载荷 << 4 | 4 bit CRC
│
▼ 取反 CRC(§1.5):dshot_crc_bidir(t) 与低 4 bit 比对
⑥ 校验通过 → tele(12b) = eee mmmmmmmmm
│
▼ period_us = m << e; (tele==0x0FFF → 停转)
⑦ eRPM = 60e6 / period_us; RPM = eRPM*2 / poles
参考解码实现:
/* GCR 反查表(quintet[0x1F] → nibble,非法置 0xFF) */
static const uint8_t gcr_rev[32] = {
[0x19]=0x0,[0x1B]=0x1,[0x12]=0x2,[0x13]=0x3,[0x1D]=0x4,[0x15]=0x5,
[0x16]=0x6,[0x17]=0x7,[0x1A]=0x8,[0x09]=0x9,[0x0A]=0xA,[0x0B]=0xB,
[0x1E]=0xC,[0x0D]=0xD,[0x0E]=0xE,[0x0F]=0xF,
/* 其余下标默认 0,需另用有效性掩码判定,见下 */
};
static bool dshot_decode_erpm(uint32_t gcr20, uint32_t *erpm_out)
{
/* gcr20: 已由差分序列还原的 20 bit GCR */
uint16_t val = 0;
for (int q = 0; q < 4; q++) {
uint8_t quint = (gcr20 >> ((3 - q) * 5)) & 0x1F; /* MSB 先 */
uint8_t nib = gcr_rev[quint];
if (!gcr_valid(quint)) return false; /* 非法五元组 */
val = (uint16_t)((val << 4) | nib);
}
uint16_t tele = val >> 4;
uint16_t crc = val & 0x0F;
if (dshot_crc_bidir(tele) != crc) return false; /* 取反 CRC,见 §1.5 */
if (tele == 0x0FFF) { *erpm_out = 0; return true; } /* 停转 */
uint16_t e = tele >> 9; /* 3 bit 指数 */
uint16_t m = tele & 0x1FF; /* 9 bit 尾数 */
uint32_t period_us = (uint32_t)m << e;
*erpm_out = period_us ? (60000000UL / period_us) : 0;
return true;
}
位槽判定要点:方案 1 的过采样序列先做多数表决/阈值去毛刺,再用回传帧位周期把样本分桶;每桶若出现电平翻转即该槽为 1。方案 2 已直接拿到边沿时间戳,按相邻边沿间隔判"有沿/无沿"即可。由于 GCR 保证"最多 2 连 0",可用最初若干个边沿校准实际位槽宽度(对 ESC 时钟偏差做自适应),提高在 DSHOT600 下的鲁棒性。
7. 关键寄存器映射(PWMV2 + HDMA + GPTMR0 + TRGM0)
以下偏移来自本仓库 soc/HPM6E00/ip/*.h,基址:MAP_BASE_PWM0=0xF0420000、MAP_BASE_HDMA、MAP_BASE_GPTMR0=0xF0000000、MAP_BASE_TRGM0=0xF047C000。
7.1 PWMV2(TX 输出比较 + 方案 1 RX 计数节拍)
| 功能 | 偏移 / 寄存器 | 位域 | 用途 |
|---|---|---|---|
| 影子值 | 0x008 SHADOW_VAL[0] |
— | val0=0,第一比较点(上升沿/命中点) |
| 影子值 | 0x00C SHADOW_VAL[1] |
— | val1=reload/采样周期 |
| 影子值 | 0x010+ SHADOW_VAL[2+i] |
— | 每路可变脉宽(TX);DMA 目的(TX) |
| PWM 配置 | 0x100+i*0x10 PWM[i].CFG0 |
OUT_POLARITY bit1(0x2) |
双向置 1(反相/idle 高) |
| PWM 配置 | 0x104+i*0x10 PWM[i].CFG1 |
HIGHZ_EN_N bit28 |
输出使能(TX 段=1,RX 段=0 高阻) |
| 比较配置 | 0x800+n*0x10 CMP[n].CFG |
CMP_IN_SEL[21:16] |
选比较源:0~0x1B影子 / 0x30~0x37捕获值 |
| DMA 请求源 | 0x480 DMA_EN |
DMA0_ENbit7 / DMA0_SEL[4:0] |
源仅 0~23比较点 / 24~27重载点(无捕获沿!)—— 方案 1 用 cmp0 作固定节拍 |
| 突发结束 | 0x424 IRQ_STS_BURSTEND |
bit0 | TX 突发完成标志 |
| 重载中断 | 0x414 IRQ_STS_RELOAD |
bit0 | 等一个重载清 burst_en |
| 计数器控制 | 0x540 CNT_GLBCFG |
每路 4bit | 启动/复位/使能计数器 |
| 计数器配置 | 0x500+i*0x10 CNT[i].CFG* |
reload 源 / burst 长度 | TX:burst16;RX(方案 1):自由运行 |
| set-lock | 0x07C WORK_CTRL1 |
bit31 | 首次加载影子/reload |
HDMA(MAP_BASE_HDMA)通道寄存器(沿用参考偏移):0x040 ctrl、0x044 len、0x048 src、0x04C src_h、0x050 dst、0x054..05C 其余。请求路由:MAP_BASE_DMA_MUX+0x000(选 DMA_INDEX_MOT_0)、MAP_BASE_MOT_GPR+0x400(dma_mot0 from dma_pwm0_0)。
TX 与方案 1 RX 的 DMA 方向差异:TX 时
src=HRAM(递增)、dst=PWM CMP 影子(固定/回卷);方案 1 RX 时src=GPIO_DI(固定)、dst=RAM(递增),且DMA0_SEL选一个自由运行计数器的cmp0/reload 点作节拍。两段在循环里重配同一 HDMA 通道即可。
7.2 GPTMR0 捕获(方案 2)
基址 MAP_BASE_GPTMR0=0xF0000000,通道寄存器 CHANNEL[x] 间距 0x40(每通道独立计数器);CR 是每通道寄存器(无全局 CR,CEN/DMAEN/DMASEL/CAPMODE 全部在 CHANNEL[x].CR):
| 功能 | 偏移 / 寄存器 | 位域 | 用途(方案 2) |
|---|---|---|---|
| 通道控制 | CHANNEL[x].CR (x*0x40 + 0x0) |
CAPMODE[2:0]:001上升 / 010下降 / 011双边;CEN[10];DMAEN[5];DMASEL[7:6]=10(捕获沿触发 DMA) |
ch2 上升、ch3 下降捕获 + 使能 + DMA |
| 上升沿时间戳 | CHANNEL[x].CAPPOS (x*0x40 + 0x20) |
32 位 | 上升沿锁存值 |
| 下降沿时间戳 | CHANNEL[x].CAPNEG (x*0x40 + 0x24) |
32 位 | 下降沿锁存值 |
| 实时计数 | CHANNEL[x].CNT (x*0x40 + 0x30) |
32 位 | 自由运行时间基 |
| 中断状态/使能 | SR(0x200) / IRQEN(0x204) |
捕获标志位 | 逐沿中断(bring-up) |
| 全局同步/复位 | GCR(0x208) |
SWSYNCT[3:0] |
多通道计数器同步启动/复位(保证 ch2、ch3 时基同源) |
DMAMUX 源:HPM_DMA_SRC_GPTMR0_2(0x46)、HPM_DMA_SRC_GPTMR0_3(0x47) → 接 HDMA 通道,把 CAPPOS/CAPNEG 搬进 RAM。
7.3 TRGM0 路由(方案 2)
基址 MAP_BASE_TRGM0=0xF047C000:
| 功能 | 偏移 / 寄存器 | 位域 | 用途(方案 2) |
|---|---|---|---|
| 路由输出 | TRGOCFG[out] (0x1000 + out*4) |
TRIGOSEL[7:0] 输入源 / REDG2PEN[16] 上升转发 / FEDG2PEN[17] 下降转发 / OUTINV[18] |
输入→输出路由;out=0x68(IN2)/0x69(IN3) |
| 数字滤波 | FILTCFG[n] (0x0) |
MODE[2:0]:000bypass / 100rapid / 101delay / 110,111stable |
信号去抖 |
输入源枚举:HPM_TRGM0_INPUT_SRC_TRGM0_Pxx(0x60~0x7F,P00~P31)、HPM_TRGM0_INPUT_SRC_PWM0_CAPIN0/1(0x8/0x9);输出目标枚举:HPM_TRGM0_OUTPUT_SRC_GPTMR0_IN2(0x68)、HPM_TRGM0_OUTPUT_SRC_GPTMR0_IN3(0x69)。pad 需配 iomux IOC_PAxx_FUNC_CTL_TRGM_P_xx(ALT=17)。
8. 资源占用与选型
| 项目 | 方案 1 过采样 DMA(PWMV2+HDMA) | 方案 2 捕获时间戳(TRGM+GPTMR0) |
|---|---|---|
| PWMV2 | 1 通道复用:TX 输出比较 + RX 计数节拍 | TX 占用,RX 不占 PWMV2 |
| DMA | TX 1 路 + RX 1 路(可复用同一 HDMA 通道,分时重配) | TX 1 路 + RX 捕获沿 DMA(GPTMR DMASEL=10) |
| CPU | 极低:帧末一次解码 | 极低(DMA 搬时间戳)/ 高(逐沿中断 bring-up) |
| 内存 | 一帧过采样缓冲(DSHOT600 ~1.5 KB/端口) | 边沿时间戳数组(~ 几十~上百字) |
| 速率适应 | 全速率稳 | 全速率(DMA)/ ≤DSHOT300 舒适(中断) |
| 多电机 | 同端口一次采样可覆盖多路(按 DI 位拆分) | 每路各占 TRGM.Pxx + GPTMR 捕获通道 |
选型建议:量产/高速率(DSHOT600+)优先 方案 1——不额外占引脚、与 TX 共用 PWMV2+HDMA,链路最省;若希望 RX 直接拿到边沿时间戳、与 TX 的 PWMV2 彻底解耦,且板级有富余 TRGM 引脚,选 方案 2(DSHOT600+ 走捕获沿 DMA,bring-up 可先逐沿中断)。两方案共用同一套 TX(PWMV2+DMAV2 FIX_BURST)与同一套 §6 解码流水线,切换成本低。
9. 常见问题
- 忘记反相:双向线路 idle 高。TX 不置
OUT_POLARITY会让 ESC 收不到、也不回传; - CRC 用错式:双向是取反 CRC,命令帧和回传帧校验都用取反式(见 §1.5),用了单向式必然全帧失败;
- 误以为 PWMV2 有捕获 DMA 源:
DMA_EN只支持比较点/重载点,务必用方案 1 的"定时+采 GPIO",或方案 2 的"GPTMR 捕获+DMA/中断"(GPTMR 才有捕获沿 DMA 源),见 §5; - 死区不够 / 无上拉:切输入太慢或线上无上拉,死区期间线路电平不稳,ESC 回传起点判不准;建议板上加外部上拉;
- 采样率过低:方案 1 采样率 < 5× 位速率会导致窄槽漏检;DSHOT600 建议 6~8×;
- 回传位槽宽度用理论值:ESC 时钟有偏差,建议用回传帧最初几个边沿自适应校准实际槽宽,再分桶判沿;
- 缓冲放错内存域:DMA 缓冲务必放 HRAM(与 HDMA 同总线域、零等待),放 AXI-SRAM 在高速率下可能来不及(与单向设计 §8.1 同理)。