双向 DSHOT(BDShot)收发实现概要设计

目录

  1. 双向 DSHOT 协议要点
  2. 总体架构:单线半双工
  3. 发送:命令帧(PWMV2 输出比较 + DMAV2 突发)
  4. 方向切换与时序预算
  5. 接收:回传帧(2 种采集方案)
  6. 回传帧软件解码流水线
  7. 关键寄存器映射
  8. 资源占用与选型
  9. Bring-up 步骤与常见坑

1. 双向 DSHOT 协议要点

双向 DSHOT(Bidirectional DSHOT,简称 BDShot / DShot-Telemetry)在同一根信号线上、以时分半双工的方式完成两件事:

  1. 命令帧:飞控(FC)→ 电调(ESC),发送 16 bit 油门/命令帧;
  2. 回传帧: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 槽),接收端无法建立位槽栅格,解码必然失败。

回传帧线序(差分/边沿编码,idle high)—— 示例逻辑位 1(起始位) 0 1 1 0 0 1 1 0 1,逻辑 1=位槽内翻转、逻辑 0=保持

图 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].CFGshadow_val[0] = 0)→ 每个位周期起点(count 0)产生上升沿;
    • 可变比较点(CMP[2i+1].CFGshadow_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].CFG0OUT_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 方向切换流程

  1. TX 结束:等 BURSTEND0x424),确认 16 位已全部移出、线路回到 idle 高;
  2. 切输入(① 换向死区,~2 μs):把承载回传信号的 pad 从 PWM 输出功能切到 RX 输入——方案 1 切到 GPIO 输入(或 PWM capture 输入),方案 2 切到 TRGM.Pxx(iomux ALT 17) 功能 (注意:建议发送命令帧的PWM引脚也使用TRGM.Pxx引脚,这样引脚就不用切换了);并使能上拉 (注意:建议外部上拉,这里就不需要内部上拉了),保证 idle 高、不干扰 ESC 驱动;
  3. 布防捕获 + 等 ESC(② 响应间隙):切输入后立即开启 RX 采集/捕获,并启动一个 ~70 μs 超时;ESC 标称在 ~30 μs 后开始回传,采集缓冲覆盖"等待 + 回传"整段最稳;超时未见首个边沿 → 本帧无遥测;
  4. RX 结束:采足样本 / 收满 21 个位槽 / 超时后停止;
  5. 切回输出: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_EN0x480)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_Pxx0x60~0x7F,P00~P31)进入 TRGM;若走 PWM0 的捕获输入,则用 HPM_TRGM0_INPUT_SRC_PWM0_CAPIN0/10x8/0x9);
  • TRGM 输出分别路由到 GPTMR 捕获:HPM_TRGM0_OUTPUT_SRC_GPTMR0_IN20x68,"GPTMR0 channel2 capture in")、HPM_TRGM0_OUTPUT_SRC_GPTMR0_IN30x69,"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].CRCAPMODE(位 0~2)001=上升沿、010=下降沿、011=双边沿;本方案将 ch2 配上升沿、ch3 配下降沿,得到每个边沿的独立时间戳,电平极性不丢失;
  • 捕获时间戳锁存到 CHANNEL[x].CAPPOS0x20,上升沿)/ CAPNEG0x24,下降沿);
  • 软件帧末把"上升沿时间戳序列 + 下降沿时间戳序列"按时间归并,得到单调的边沿时间戳流,再进 §6;
  • DMA 路径(可选,性能等同方案 1)CR.DMASEL(位 6~7)= 10(Input signal toggle captured)→ 每捕获一个边沿发一次 DMA 请求;经 DMAMUX HPM_DMA_SRC_GPTMR0_20x46)/ HPM_DMA_SRC_GPTMR0_30x47)搬进 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=0xF0420000MAP_BASE_HDMAMAP_BASE_GPTMR0=0xF0000000MAP_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+0x400dma_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 同理)。

0
0

订阅

发表回复 0

Your email address will not be published. Required fields are marked *

captcha
Enter the characters shown in the image:
Reload

This CAPTCHA helps ensure that you are human. Please enter the requested characters.