DShot600 协议原理与实现-学习笔记

DShot600 协议原理与实现 ------ 学习笔记

1. 协议概述

DShot(Digital Shot)是一种用于无人机飞控(FC)与电调(ESC)之间的数字通信协议,由 Flyduino 在 2016 年发布。与传统的 PWM/Oneshot/Multishot 等模拟协议不同,DShot 以二进制位的形式直接传输油门值,天然抗噪声和抖动。

DShot 各版本对比

参数 DShot150 DShot300 DShot600 DShot1200
比特率 150 kbit/s 300 kbit/s 600 kbit/s 1200 kbit/s
位周期 6.67 µs 3.33 µs 1.67 µs 0.833 µs
T0H (逻辑0高电平) 2.50 µs 1.25 µs 625 ns 313 ns
T1H (逻辑1高电平) 5.00 µs 2.50 µs 1.25 µs 625 ns
16位帧时长 106.72 µs 53.28 µs 26.72 µs 13.28 µs
理论最大更新率 ~9.4 kHz ~18.8 kHz ~37.5 kHz ~75 kHz
双向DShot(遥测) 有限支持 ✅ 支持 ✅ 支持 ❌ 不支持

物理层与信号传输

DShot 采用单根信号线 + 共用地线传输,每台电机一条独立信号线,四轴共需 4 根信号线。

电气层面

复制代码
飞控 MCU 的 GPIO 引脚 ──── 信号线 ──── ESC 的 MCU GPIO 引脚
    3.3V TTL 电平                       3.3V / 5V 容受
              (同一条线,双向模式下分时半双工)
              
                       GND ──────────────────── GND

核心特征

特征 说明
电平标准 TTL 数字电平,飞控侧 3.3V(STM32/ESP32),ESC 侧多数 3.3V 兼 5V 容受
传输方式 单端(非差分),非隔离,直接 GPIO 驱动
通信方向 单工 (FC → ESC)常态;双向模式下为分时半双工(同一线上 ESC 回传)
空闲态 线路上拉保持高电平(MCU 内部上拉或外部电阻)
线路数量 每台电机 1 根信号线 + 共用 GND,无独立时钟线

与模拟协议(PWM / Oneshot / Multishot)的物理层差异

复制代码
PWM / Oneshot / Multishot:
  飞控输出: ▁▁▁▁▁█▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▁▁▁▁▁▁▁▁▁▁▁▁▁  (单个宽脉冲,周期可变)
           |←──── 脉宽代表油门 ────→|
  没有校验位,模拟量传输

DShot:
  飞控输出: ▁▁█▆ ▁▁█▆ █▆ ▁▁█▆ ▁▁█▆ █▆ █▆ ▁▁█▆ ▁▁█▆ █▆ █▆ ▁▁█▆ ▁▁█▆ █▆ █▆ █▆ ▁▁
           0   0   1  0   0   0  1  1   0   0  1  1   0   1  1  1   0  ← 16 bits
           |←────────── 26.72 µs (DShot600) ──────────→|
  带 CRC 校验位,数字量传输

双向模式下的线路状态切换

复制代码
         FC 发送 DShot 帧          ESC 回传 GCR 遥测帧
         (取反信号, bit11=1)        (21-bit, 5/4 倍速率)
         |←── 26.72µs ──→|  ~30µs  |←── ~28µs ──→|
         ┌────────────────┐        ┌──────────────┐
GPIO 线  │    FC 驱动      │ HIGH   │  ESC 驱动     │ HIGH (空闲)
─────────┘                └────────┘              └──────────
                FC GPIO 方向切换:          ESC GPIO 方向切换:
                OUTPUT → INPUT            INPUT → OUTPUT
  • 飞控发送完 16-bit 帧 + 2-bit 间隔后,立即将 GPIO 方向从 OUTPUT 切换为 INPUT
  • ESC 检测到取反的 DShot 帧后,等待约 30--40µs,将自身 GPIO 从 INPUT 切换为 OUTPUT,回传 GCR 遥测帧
  • 在约 30µs 的间隙内完成方向切换是双向 DShot 实现的关键难点,要求 MCU 能快速操作 GPIO 方向寄存器
对比维度 模拟协议(PWM/Oneshot/Multishot) DShot
信号本质 脉冲宽度代表油门(模拟量) 16 位二进制比特流(数字量)
抗干扰能力 弱 --- 脉宽被噪声拉伸即产生错误油门 强 --- 每位只有 0/1 判断,容错空间大
校验机制 ❌ 无校验 ✅ 4-bit CRC 校验
双向通信 ❌ 不支持 ✅ DShot300/600 支持 ESC 回传遥测
油门分辨率 通常 ~1000 级(受限于定时器时钟) 2000 级(48--2047)
是否需要校准 需要(PWM 行程校准) 不需要(数字协议,固定映射)
帧长一致性 可变(油门不同脉宽不同) 固定(每帧始终 16 bit ≈ 26.72µs)

信号完整性注意事项

  • 信号线建议 ≤ 30cm,过长时高频脉冲衰减严重,建议降级为 DShot300
  • 信号线应远离电机相线(高电流 PWM)和电源线,防止电磁耦合导致误码
  • 信号线上不要加滤波电容 --- 电容会吃掉 625ns 的窄脉冲
  • 长距离场景可在 ESC 端串联 22--47Ω 终端电阻抑制信号反射
  • 多轴场景下,信号线以星形从飞控引出,避免串扰

2. 核心原理

2.1 信号编码方式

DShot 采用脉冲宽度编码:每个位的周期固定(DShot600 为 1.67 µs),通过高电平持续时间区分逻辑 0 和 1。

复制代码
逻辑 "0":  ┌───┐                    ┌───┐
           │   │  ~625ns             │   │
           │   └──────────────────────│   └───
           └──────────────────────────┘
           |←────── 1.67µs ─────────→|

逻辑 "1":  ┌──────────┐             ┌──────────┐
           │          │  ~1250ns    │          │
           │          └─────────────│          └──
           └────────────────────────┘
           |←────── 1.67µs ────────→|
  • 逻辑 0: 高电平 ≈ 625ns (37.5% 占空比), 低电平 ≈ 1.042µs
  • 逻辑 1: 高电平 ≈ 1250ns (75% 占空比), 低电平 ≈ 417ns

空闲态为高电平,帧间隔至少需要 2 bit 周期的低电平。

2.2 帧格式 (16-bit)

DShot 每帧 16 位,分为三个字段:

位范围 长度 含义 说明
bit 10:0 11 位 油门值 0--2047,0=停转,1--47=特殊命令,48--2047=油门
bit 11 1 位 遥测请求标志 0=不请求遥测,1=请求遥测数据
bit 15:12 4 位 CRC 校验 对前 12 位数据计算的 4 位 CRC

最终发送的 16 位帧组合方式:

复制代码
frame = ((throttle << 1) | telemetry_bit) << 4 | crc

帧间隔(reset):帧结束后至少 2 bit 周期的低电平(约 3.33µs),用于 ESC 检测帧边界。

2.3 油门值范围详解

油门值(11 位,bit10:0)共 2048 个离散值(0--2047),分为三个功能区间:

复制代码
油门值:  0        1            48                              2047
         │  Disarm │  特殊命令  │        正常油门 (0%--100%)      │
         │  停转   │  1--47      │      48--2047                  │
         └─────────┴────────────┴───────────────────────────────┘
区间一:油门值 = 0 ------ Disarm(停转)
  • ESC 完全解除武装,电机立即停止转动
  • 需要重新走 ARM 流程才能再次驱动电机
  • 安全用途:解锁开关关闭、失控保护、急停
区间二:油门值 = 1--47 ------ 特殊命令
  • 此时 bit11(遥测标志位)必须置 1,ESC 才会将帧解释为命令
  • 共 47 条命令,涵盖蜂鸣、电机方向、3D 模式、LED、保存设置等
  • 详见 [第 4 节:特殊命令](#第 4 节:特殊命令)
区间三:油门值 = 48--2047 ------ 正常油门驱动

这是 DShot 协议的核心功能区间,油门值与推力输出的映射关系如下:

复制代码
油门值    百分比       说明
────────────────────────────────────────────────
48        0.0%        最小油门 / 怠速(ARM 后电机起转的起始点)
   ↓         ↓         线性映射
   ↓         ↓         percentage = (value - 48) / 1999 × 100%
   ↓         ↓
1024      ≈48.8%      中间点
   ↓         ↓
2047      100.0%      最大油门 / 满油

映射公式

复制代码
percentage = (throttle_value - 48) / 1999 × 100%

throttle_value = 48 + percentage / 100 × 1999

实例

复制代码
油门值    换算                                   约等于
───────────────────────────────────────────────────────
  48     (48-48)/1999 × 100% = 0.000%   →  怠速/0%油门
 100     (100-48)/1999 × 100% = 2.60%   →  低油门
 500     (500-48)/1999 × 100% = 22.6%   →  悬停油门(5寸机约此值)
1024     (1024-48)/1999 × 100% = 48.8%  →  半油
1500     (1500-48)/1999 × 100% = 72.6%  →  高油门
2000     (2000-48)/1999 × 100% = 97.6%  →  接近满油
2047     (2047-48)/1999 × 100% = 100%   →  满油

为什么 2047 是满油而不是 2048?

11 位二进制最大值为 2¹¹ - 1 = 2047。DShot 用完整的 11 位(0--2047)表示油门域,前 48 个值(0--47)被特殊命令和 Disarm 占用,留给实际油门的范围是 48--2047,共 2000 个步进 。这意味着 DShot 的油门分辨率为 2000 级,远超传统 PWM(通常约 1000 级),控制精度更高。

bit11 在油门模式下的作用

在油门区间(48--2047)中,bit11 的含义切换为遥测请求

  • bit[11] = 0:不请求遥测,ESC 只接收不回复
  • bit[11] = 1:请求遥测,ESC 在收到帧后约 30--40µs 回传 GCR 遥测数据(仅双向 DShot 模式)

油门帧构造示例(油门值 = 500,不请求遥测):

复制代码
throttle = 500     = 0b_001_1111_0100  (11 bits)
telem    = 0       = 0b_0

packet = (500 << 1) | 0 = 1000 = 0b_0011_1110_1000  (12 bits)

CRC = 1000 ^ (1000>>4) ^ (1000>>8) = 1000 ^ 62 ^ 3
    = 0b_0011_1110_1000 ^ 0b_0000_0011_1110 ^ 0b_0000_0000_0011
    = 0b_0011_1101_0100
    & 0x0F = 0b_0100 = 4

frame = (1000 << 4) | 4 = 16004 = 0x3E84

油门值与实际推力的关系

DShot 传输的是线性油门值 ,但电机实际输出的推力与油门值非线性 。ESC 内部可以配置油门曲线来映射,飞控端也可以做 expo 补偿。飞控的 PID 控制器工作在归一化的油门空间中,DShot 的 2000 级精度确保了控制量的细腻度。

2.4 CRC 计算方法

CRC 为 4 位,通过对 12 位数据(11位油门 + 1位遥测标志)按 4 位一组逐级异或计算:

c 复制代码
// 方法一:异或法
uint8_t calc_crc(uint16_t packet) {
    // packet = ((throttle & 0x7FF) << 1) | (telem & 1);  // 12 bits
    uint8_t crc = 0;
    uint16_t data = packet;
    for (int i = 0; i < 3; i++) {
        crc ^= data;        // XOR by nibble
        data >>= 4;
    }
    return crc & 0x0F;
}

// 方法二:移位异或法(等价)
crc = (packet ^ (packet >> 4) ^ (packet >> 8)) & 0x0F;

计算示例 --- 油门值 100、无遥测请求:

复制代码
throttle = 100 = 0b00001100100
telem    = 0

前12位数据构造:
  packet = (100 << 1) | 0 = 200 = 0b000011001000

CRC 计算:
  crc = 200 ^ (200>>4) ^ (200>>8) = 200 ^ 12 ^ 0 = 196
  crc & 0x0F = 196 & 0x0F = 4

最终帧 = (200 << 4) | 4 = 3204 = 0x0C84

3. DShot600 关键时序

参数 数值 公式
比特率 600 kbit/s ---
位周期 1.67 µs T = 1/600000
T0H 625 ns T × 0.375
T1H 1250 ns T × 0.75
16位帧时间 26.72 µs 16 × 1.67µs
含间隔的帧时间 ~30 µs 18 × 1.67µs
帧间隔 (低电平) ≥ 3.33 µs ≥ 2 bit 周期

定时器配置的关键:在 STM32 上实现 DShot600,需要将定时器的 ARR(自动重装值)设置得足够大以获得足够的分辨率。例如:72 MHz 定时器时钟,预分频 3 → 18 MHz 计数频率,ARR = 29 → 30 个 tick = 1.67 µs。此时 BIT_0 的高电平约为 11 个 tick,BIT_1 的高电平约为 22 个 tick。


4. 特殊命令(油门值 0--47)

当油门值为 1--47 且遥测标志位(bit 11)置 1 时,帧被解析为特殊命令而非油门信号。

4.1 完整命令表

复制代码
 值    名称                    说明                       发送要求
─────────────────────────────────────────────────────────────────
 0   MOTOR_STOP               停转(安全切断)
 1   BEEP1 / BEACON1          蜂鸣音 1(寻机)           间隔 ≥380ms
 2   BEEP2 / BEACON2          蜂鸣音 2                   间隔 ≥380ms
 3   BEEP3 / BEACON3          蜂鸣音 3                   间隔 ≥400ms
 4   BEEP4 / BEACON4          蜂鸣音 4                   间隔 ≥400ms
 5   BEEP5 / BEACON5          蜂鸣音 5                   间隔 ≥400ms
 6   ESC_INFO                 查询 ESC 信息
 7   SPIN_DIRECTION_1         正向旋转(永久)           连续 ≥6次
 8   SPIN_DIRECTION_2         反向旋转(永久)           连续 ≥6次
 9   3D_MODE_OFF              关闭 3D 模式               连续 ≥6次
10   3D_MODE_ON               开启 3D 模式               连续 ≥6次
11   SETTINGS_REQUEST         请求 ESC 设置
12   SAVE_SETTINGS            保存当前设置              连续 ≥6次, 等待 ≥12ms
20   SPIN_DIRECTION_NORMAL    临时正向旋转(断电恢复)   连续 ≥6次
21   SPIN_DIRECTION_REVERSED  临时反向旋转(断电恢复)   连续 ≥6次
22-29 LED0_ON ~ LED3_OFF      控制 LED (BLHeli_32)      
30   AUDIO_STREAM_MODE        KISS 音频流模式
31   SILENT_MODE              KISS 静音模式
47   MAX                      最大值(保留)

4.2 命令帧构造示例

以**命令 8(设置反向旋转)**为例:

复制代码
命令值 = 8 = 0b_00000_01000
遥测标志 = 1

原数据12位 = (8 << 1) | 1 = 17 = 0b_0000_0001_0001

CRC = (0x011 ^ 0x00 ^ 0x00) & 0xF = 0

最终帧 = (17 << 4) | 0 = 0x0110

4.3 应用场景

  • Turtle Mode(反乌龟模式): 用命令 21 临时反转两个电机,帮助翻倒的飞机自行翻正
  • Props Out: 用命令 7/8 永久改变电机方向,实现桨叶外旋配置
  • Beeper: 用命令 1--5 驱动电机线圈发声,替代外置蜂鸣器寻机

5. 双向 DShot(Bidirectional DShot)

双向 DShot 允许 ESC 在同一条信号线上回传遥测数据(eRPM),用于 RPM 陷波滤波,提升飞控性能。

5.1 工作原理

复制代码
┌─────────┐      发送 DShot 命令帧(bit11=1)      ┌─────────┐
│  飞控   │ ─────────────────────────────────────→ │  电调   │
│  (FC)   │                                        │  (ESC)  │
│         │ ←───────────────────────────────────── │         │
└─────────┘      回传 GCR 遥测帧(21 bit)          └─────────┘
                     ≈30--40 µs 后开始
  • 飞控将正常的 DShot 帧取反后发送(CRC 按位取反),告诉 ESC "请求遥测"
  • ESC 接收到取反帧后,在 ~30--40µs 后将信号线切换为输出模式
  • ESC 以 5/4 倍 DShot 比特率回传 21 位 GCR 编码的遥测帧
  • 飞控在半双工模式下接收并解码

关键约束 : 双向模式仅 DShot300 和 DShot600 可靠支持。DShot150 兼容性有限,DShot1200 因时序过于紧张无法支持。

5.2 GCR 编码(Group Coded Recording)

遥测数据使用 GCR(4→5) 编码,即每个 4 位半字节(nibble)映射为一个 5 位值。GCR 编码保证不会出现超过两个连续的 0,便于接收端做时钟恢复。

GCR 编码查找表
复制代码
gcr_encode_table[16] = {
    0b_00001, // 0  → 1
    0b_00010, // 1  → 2
    0b_00100, // 2  → 4
    0b_01000, // 3  → 8
    0b_10000, // 4  → 16
    0b_10001, // 5  → 17
    0b_10010, // 6  → 18
    0b_10011, // 7  → 19
    0b_10100, // 8  → 20
    0b_10101, // 9  → 21
    0b_10110, // 10 → 22
    0b_11000, // 11 → 24
    0b_11001, // 12 → 25
    0b_11010, // 13 → 26
    0b_11100, // 14 → 28
    0b_11101, // 15 → 29
};
GCR 解码查找表(5→4)
复制代码
gcr_decode_table[32] = {
    [0b_00001] = 0,  [0b_00010] = 1,  [0b_00100] = 2,  [0b_01000] = 3,
    [0b_10000] = 4,  [0b_10001] = 5,  [0b_10010] = 6,  [0b_10011] = 7,
    [0b_10100] = 8,  [0b_10101] = 9,  [0b_10110] = 10, [0b_11000] = 11,
    [0b_11001] = 12, [0b_11010] = 13, [0b_11100] = 14, [0b_11101] = 15,
    // 非法码字 = 0xFF (无效)
};

5.3 遥测帧编码流程

复制代码
16 bit 原始遥测数据 (12位有效 + 4位 CRC)
  ↓
拆分: N0 N1 N2 N3 (4个 nibble)
  ↓
GCR 编码: G0 G1 G2 G3 (4×5 = 20 bit)
  ↓
DMA 差分编码 + 加 start bit = 21 bit 最终帧
  ↓
以 5/4 × DShot 比特率串行输出

5.4 遥测帧解码流程

c 复制代码
// 步骤1: 捕获21位原始帧
uint32_t gcr21 = capture_21_bits();

// 步骤2: 差分解码 (NRZI-like) → 恢复20位GCR
uint32_t gcr20 = gcr21 ^ (gcr21 >> 1);

// 步骤3: GCR 5→4 解码 → 得到16位数据
uint16_t raw_value = 0;
uint32_t mask = 0x1F;  // 5-bit groups
for (int i = 0; i < 4; i++) {
    uint8_t gcr_code = (gcr20 >> (i * 5)) & 0x1F;
    uint8_t nibble = gcr_decode_table[gcr_code];
    if (nibble == 0xFF) { /* GCR decode error */ }
    raw_value |= (nibble << (i * 4));
}

// 步骤4: 验证 CRC
uint8_t received_crc = raw_value & 0x0F;
uint16_t data = raw_value >> 4;
uint8_t calc_crc = (data ^ (data >> 4) ^ (data >> 8)) & 0x0F;
if (calc_crc != received_crc) { /* CRC error, discard */ }

// 步骤5: 解析 eRPM
uint16_t exponent  = (data >> 9) & 0x07;  // 3 bits
uint16_t period_us = data & 0x01FF;        // 9 bits mantissa
period_us = period_us << exponent;          // 实际周期 (µs)

// 步骤6: 计算 eRPM(电转速 × 100)
// eRPM = (1000000 / period_us) × 60 / 100
//      = (600000 / period_us)
uint16_t erpm_x100 = (6000000 + period_us / 2) / period_us;

// 步骤7: 机械 RPM (考虑磁极对数)
// RPM = eRPM × 100 / 磁极对数
uint16_t rpm = (erpm_x100 * 100) / motor_poles;

5.5 遥测数据帧格式

复制代码
16-bit 原始遥测数据:
  [eee] [mmmmmmmmm] [cccc]
   ↑        ↑         ↑
 指数位  周期尾数    CRC校验
 (3 bit)  (9 bit)   (4 bit)

eRPM 周期 = mantissa × 2^exponent  (单位: µs)

5.6 扩展遥测 (EDT --- Extended DShot Telemetry)

现代 ESC 固件(AM32、Bluejay)通过前缀位区分帧类型:

复制代码
前缀位 (4 bit)     帧类型
────────────────────────────
xxx1 或 0000       eRPM 帧(标准)
0010               温度 (°C)
0100               电压 (×0.25V)
0110               电流 (A)
1000               调试数据 1
1010               调试数据 2
1100               应力水平
1110               状态帧

6. STM32 实现方案(Timer + DMA)

6.1 整体架构

复制代码
┌───────────────┐       DMA (内存→外设)       ┌──────────────┐
│  DMA缓冲区     │ ──────────────────────────→ │  TIMx CCRx   │ ──→ GPIO: PWM 输出
│  [18个值]      │    自动传输                 │ (PWM模式)     │
└───────────────┘                             └──────────────┘

以 STM32F103C8T6(72 MHz)为例,DShot600 的定时器配置:

c 复制代码
// TIM2 配置,用于 DShot600
// 72 MHz / 4 = 18 MHz 定时器时钟
TIM_TimeBaseStruct.TIM_Prescaler = 3;   // 72/4 = 18 MHz
TIM_TimeBaseStruct.TIM_Period    = 29;  // 30 ticks = 1.67 µs

// DShot600 位定义(以 18MHz 定时器时钟下的 tick 数表示)
#define DSHOT600_BIT_PERIOD  30    // 1.67 µs = 30 ticks
#define DSHOT600_BIT_0_HIGH  11    // ~625 ns
#define DSHOT600_BIT_1_HIGH  22    // ~1250 ns
#define DSHOT600_BIT_LOW      0    // 0 = 低电平

6.2 DMA 缓冲区构造

c 复制代码
uint16_t dma_buffer[18];  // 16位数据 + 2位帧间隔

void build_dma_buffer(uint16_t packet) {
    for (int i = 0; i < 16; i++) {
        if (packet & (1 << (15 - i))) {
            dma_buffer[i] = DSHOT600_BIT_1_HIGH;  // T1H
        } else {
            dma_buffer[i] = DSHOT600_BIT_0_HIGH;  // T0H
        }
    }
    // 帧间隔: 至少 2 bit 周期低电平
    dma_buffer[16] = 0;
    dma_buffer[17] = 0;
}

注意:缓冲区中的值是定时器 CCR 值(不是占空比)。PWM 模式配置为:到达 CCR 值时翻转输出,到达 ARR 时复位。因此每个 DMA 传输对应一个完整的位周期。

实际上更常用的方式是将定时器配置为单脉冲模式 (One Pulse Mode) ,并利用 OC 预装载 (OCxPE) 确保 DMA 写入的 CCR 值在下一个更新事件才生效。

6.3 完整发送流程

c 复制代码
// 1. 构造帧(含 CRC)
uint16_t dshot_encode(uint16_t throttle, uint8_t telem) {
    uint16_t packet = ((throttle & 0x7FF) << 1) | (telem & 1);
    uint8_t crc = (packet ^ (packet >> 4) ^ (packet >> 8)) & 0x0F;
    return (packet << 4) | crc;
}

// 2. 构造 DMA 缓冲区
void dshot_prepare_buffer(uint16_t frame) {
    for (int i = 0; i < 16; i++) {
        dma_buffer[i] = (frame & (1 << (15 - i)))
                        ? DSHOT600_BIT_1_HIGH
                        : DSHOT600_BIT_0_HIGH;
    }
    dma_buffer[16] = 0;  // 间隔位1
    dma_buffer[17] = 0;  // 间隔位2
}

// 3. 发送
void dshot_send(void) {
    DMA_Cmd(DMA1_Channel7, DISABLE);
    DMA_SetCurrDataCounter(DMA1_Channel7, 18);
    DMA_Cmd(DMA1_Channel7, ENABLE);
    TIM_Cmd(TIM2, ENABLE);
    // DMA 传输完成后会在 ISR 中自动停止定时器
}

6.4 定时器配置要点(STM32 HAL)

c 复制代码
// 关键点:
// 1. 使用 OC_Preload (TIM_OC2PreloadConfig) 防止 DMA 写入时出现毛刺
// 2. 配置为 PWM1 模式
// 3. 使能 DMA 传输到 CCR 寄存器
// 4. DMA 传输完成中断中禁用定时器输出

TIM_OCInitStructure.TIM_OCMode       = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState  = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse        = 0;
TIM_OCInitStructure.TIM_OCPolarity   = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OCIdleState  = TIM_OCIdleState_Set;  // 空闲态高电平

6.5 常见 STM32 系列定时器时钟计算

MCU 主频 推荐预分频 定时器时钟 ARR BIT_0 BIT_1
STM32F103 72 MHz 3 18 MHz 29 11 22
STM32F4xx 168 MHz 7 21 MHz 34 13 26
STM32F7xx 216 MHz 11 18 MHz 29 11 22
STM32G4xx 170 MHz 7 21.25 MHz 34 13 26

7. 其他 MCU 实现方案

7.1 ESP32 --- RMT 方式

ESP32 的 RMT(Remote Control Transceiver)外设天然适合生成精确的脉冲序列:

c 复制代码
// RMT 每项 { level, duration },单位为 RMT tick (80 MHz = 12.5ns)
// DShot600: 1.67µs / 12.5ns = 133 ticks/bit
// T0H: 625ns / 12.5ns = 50 ticks
// T1H: 1250ns / 12.5ns = 100 ticks

rmt_item32_t dshot_items[18];
for (int i = 0; i < 16; i++) {
    bool bit_val = (frame >> (15 - i)) & 1;
    dshot_items[i].level0 = 1;
    dshot_items[i].duration0 = bit_val ? 100 : 50;  // T0H or T1H
    dshot_items[i].level1 = 0;
    dshot_items[i].duration1 = 133 - dshot_items[i].duration0; // 剩余 = 低电平
}
dshot_items[16] = (rmt_item32_t){{0, 133, 0, 0}};  // 间隔位1
dshot_items[17] = (rmt_item32_t){{0, 133, 0, 0}};  // 间隔位2

rmt_write_items(channel, dshot_items, 18, true); // true = 等待发送完成

7.2 Teensy --- UART 方式

利用硬件 UART 的起始位/停止位来合成脉冲波形:

c 复制代码
// UART 在 8N1 模式下,每个字节 = 1 start bit (0) + 8 data bits + 1 stop bit (1)
// 通过设置波特率使每个 UART 帧 = 1 DShot 位周期
// 波特率 = 1 / 1.67µs ≈ 600kbaud
// 逻辑0: 发送 0x00 (只有起始位=低, 其余=高) → 短高脉冲
// 逻辑1: 发送 0xF0 (起始位+4高+4低) → 长高脉冲

7.3 方案对比

方案 精度 CPU 占用 适用场景
Timer + DMA ★★★★★ 极低 STM32,推荐
RMT ★★★★★ ESP32,推荐
UART ★★★★ Teensy / 通用 MCU
软件位脉冲 (bit-banging) ★★ 极高 应急/调试

8. ARM / 启动流程

DShot 上电后 ESC 不立即响应油门,需要完成上电→检测→ARM→运行流程:

复制代码
上电 ──→ 3声短鸣 (初始化成功)
   │
   ▼
检测到 DShot 信号 ──→ 1声低鸣 (ARM 开始)
   │
   ▼
油门归零 ──→ 1声高鸣 (ARM 完成)
   │
   ▼
正常油门响应 (48--2047)

ARM 注意事项

  • 油门值 0 = 完全解除 ARM(Disarm)
  • 油门值 48 = 已 ARM + 零油门(可在此基础上增加油门值)
  • 上电后需持续发送 DShot 帧(帧间隔 ≤ 50ms),否则 ESC 会超时判定信号丢失并 Disarm
  • 常规更新频率:8--32 kHz,即每 31.25--125 µs 发送一帧

9. CRC 容错与信号质量

9.1 4-bit CRC 的局限性

4 位 CRC 只能检测有限范围内的位翻转错误,无法检测所有组合错误。Betaflight 在实践中对每帧计算 CRC,若 CRC 错误则丢弃该帧(保留上一帧油门值),并对连续错误帧计数。

9.2 信号完整性建议

  • 移除信号线上的滤波电容: DShot600 的 625ns 高脉冲会被 RC 滤波严重衰减
  • 信号线长度 ≤ 30cm: 超过此长度建议降级为 DShot300
  • 避免并行走线: DShot 信号线远离电源线和电机相线,防止耦合噪声
  • 地线分离: 信号地与功率地分开走线,单点接地
  • 终端匹配 (可选): 在 ESC 端串联 22--47Ω 电阻,抑制反射

10. 学习总结

DShot600 协议精华

要点 核心内容
信号本质 脉冲宽度编码,1.67µs/bit,T0H=625ns,T1H=1250ns
帧结构 16-bit = 11(油门) + 1(遥测) + 4(CRC),含间隔约30µs
油门范围 0=停转, 1-47=命令, 48-2047=油门(0-100%)
CRC 算法 (data ^ (data>>4) ^ (data>>8)) & 0xF
双向遥测 GCR 5→4编码,21-bit 回传帧,eRPM = mantissa << exponent
MCU实现 STM32用TIM+DMA,ESP32用RMT,Teensy用UART
ARM流程 上电3短鸣→检测信号1低鸣→油门归零1高鸣→可正常运行
适用场景 高速PID回路(16-32kHz),竞速级无人机

速记公式

复制代码
帧构造:  frame = ((throttle << 1) | telem) << 4 | crc
CRC计算: crc   = (data ^ (data>>4) ^ (data>>8)) & 0xF
eRPM计算: erpm = 600000 / period_us
机械RPM:  rpm  = erpm × 100 / 磁极对数

关键参考