免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Dshot协议逆向解析:STM32定时器PWM+DMA实现与逻辑分析仪抓包实践

Dshot协议逆向解析:STM32定时器PWM+DMA实现与逻辑分析仪抓包实践 做无人机飞控或者电调相关开发的朋友十有八九会跟Dshot这个协议打交道。它是目前四轴领域主流的数字电调协议跟传统PWM、Oneshot那一类模拟信号完全不是一回事。我这次因为要给自研飞控调电调时序干脆拿逻辑分析仪直接抓波形从协议层把Dshot这条链路彻底捋了一遍。整个发送端用STM32的定时器PWMDMA实现CubeMX做工程初始化最后波形、CRC、解码全部验证通过。这篇文章适合三类人一是在调飞控、写电调驱动、搞不清Dshot帧结构和CRC的二是手上只有逻辑分析仪、想自己抓包理解协议细节的三是为了快速上手STM32定时器PWMDMA想找个“有挑战性”实例的。我会把帧格式、编码规则、CubeMX配置、DMA参数计算、逻辑分析仪解码技巧全部展开踩过的坑也一并写出来。1. 为什么要逆向解析Dshot协议1.1 Dshot是什么和传统PWM信号的区别Dshot全称Digital Shot是Betaflight团队牵头推出来的数字电调协议。在它之前飞控和电调之间主要靠PWM脉宽传递油门高电平时间在1ms到2ms之间1ms对应最小油门2ms对应最大油门这一点和舵机控制一模一样。后来衍生出Oneshot125、Oneshot42本质还是脉宽映射只是把周期缩短、刷新率提高。Dshot完全不同。它不再用高电平宽度直接映射油门而是把控制信息打包成一串数字帧发送一个Dshot帧固定16个bit包含11位油门、1位遥测请求和4位CRC校验。电调收到后先做CRC校验校验通过才执行对应的油门指令。这样信号传输从“模拟量”变成了“数字量”抗干扰能力、刷新率、可诊断性都上了一个台阶。很多人第一次看到Dshot波形会懵它明明也是一串宽度不一样的高电平脉冲看起来跟PWM差不多。其实关键区别在于Dshot每个bit的时间长度是固定的bit的值靠高电平在这个固定周期内的占比来表达。这相当于用脉宽做数字编码而不是用脉宽模拟连续数值。1.2 逆向解析的两条路径抓波形验证自己的实现我这次做“逆向解析”并不是去破解什么加密协议而是反过来理解它已知协议定义但想亲眼看到波形、亲手验证编码是否正确。路径主要有两条一条是先不看飞控和电调的现成源码直接抓电调收到的信号通过逻辑分析仪把波形还原成数据再对照协议手册反推出帧结构另一条是自己用STM32写Dshot发送端用逻辑分析仪抓自己发出的波形逐bit验证编码、CRC和时序。两条路我实际都走了。先抓现成的飞控输出确认协议理解无误再自己实现STM32发送端对比两边的波形是否一致。这种打法比直接抄代码扎实得多因为抄代码只能知道“怎么发”抓波形才能理解“为什么这么发”。逆向解析的最大价值在于当电调不转、乱转、CRC报错时你能直接判断是协议层的问题还是硬件层的问题。拿逻辑分析仪一抓波形有没有、脉宽对不对、CRC算没算对一目了然。省得拿万用表瞎猜更不用对着电调说明书反复试错。2. Dshot协议帧格式与编码原理2.1 16位帧结构油门、遥测请求与CRC校验Dshot一帧固定16bit从MSB到LSB依次是11位油门值、1位遥测请求位、4位CRC校验码。油门值范围0到2047其中0是电机停止1到46属于“微转”区间47到2047是线性油门区间。实际使用时很多飞控把0直接映射为停止1到46也压缩到最小油门附近避免电调收到极小油门时产生不确定行为。遥测请求位只有0和1两种状态。普通单向后不需要管置0就行。只有需要电调回传eRPM、温度、电压等数据时才把它置1并配合双向Dshot模式使用。电调收到带遥测请求的帧后会在约定的时间窗口内拉低信号线向飞控回传遥测帧。注意这时候同一根信号线就变成了半双工总线飞控发完命令后要立刻切输入模式去读电调回的数据。4位CRC是整个Dshot协议里最容易写错的地方。它的计算对象是前12位数据也就是油门和遥测位组成的那个整数。算法说起来很简单把12位数据按每4位一组切分成3组三组分别异或得到的4位结果就是CRC。比如12位数据是0x258拆成0x2、0x5、0x8CRC就是0x2 ^ 0x5 ^ 0x8 0xF。然后把这4位CRC拼到12位数据后面变成完整的16位帧0x258F发送出去。2.2 脉宽编码与三种速率Dshot150/300/600Dshot在物理层上用脉宽编码表达bit。一个bit占用的时间长度固定由当前速率决定。以Dshot600为例一个bit周期约1.667微秒。bit为1时高电平持续整个周期的2/3约1.111微秒bit为0时高电平只持续1/3周期约0.556微秒。剩余时间拉低。这样每个bit的起始和结束边沿都对齐解码端只需要数高电平宽度就能还原出0和1。三种常用速率分别对应不同的bit周期。Dshot150每个bit约6.667微秒Dshot300约3.333微秒Dshot600约1.667微秒。数字越大刷新率越高、延迟越低但对主控定时器精度和逻辑分析仪采样率的要求也越高。目前飞控主流是Dshot600部分高速竞速机型用Dshot1200但Dshot1200对硬件要求更苛刻普通STM32F103已经很难稳定输出。速率bit周期高电平时间(bit1)高电平时间(bit0)适用场景Dshot1506.667us4.445us2.222us老机型、兼容性调试Dshot3003.333us2.222us1.111us入门穿越机Dshot6001.667us1.111us0.556us主流穿越机、竞速机2.3 为什么发送端必须用DMA看Dshot600的时序就明白了发送一个16bit的帧总共只要26.67微秒。如果靠CPU在定时器中断里逐bit翻转电平每1.667微秒就要进一次中断16次中断发完一帧CPU几乎被拖死。飞控还要跑姿态解算、PID控制、遥控信号解析根本扛不住这么高频率的中断开销。DMA就是用来解决这个问题的。思路很直接提前在内存里准备好16个CCR值分别对应这一帧16个bit的PWM占空比启动定时器PWM后每个bit周期到来时定时器自动触发一次DMA请求把内存中的CCR值搬到定时器比较寄存器里PWM硬件自己完成电平翻转。CPU只负责准备一帧数据、启动DMA发完一帧之后再去处理别的任务。这也是CubeMXDMA方案的核心优势代码简单、时序稳定、CPU占用极低。对比用定时器中断模拟的方式这个方案在Dshot600下几乎不占用额外CPU时间后续加双向Dshot、加遥测解码都有余量。3. 逻辑分析仪实战抓取并手动解析Dshot波形3.1 工具准备与接线采样率、共地和触发设置逻辑分析仪的选择上我手头用的是一款某宝常见的8通道24MHz采样率分析仪配Saleae Logic 2软件几十块钱对Dshot600来说完全够用。如果只有16MHz采样率也能勉强看Dshot600但波形边沿会比较模糊建议有条件还是上24MHz。12MHz及以下采样率的分析仪看Dshot600会非常吃力容易误判脉宽。接线部分有几点必须注意。第一逻辑分析仪的GND必须和飞控、电调共地否则采出来的波形会乱跳严重时根本没法看。第二采样通道接在飞控到电调之间的信号线上也就是电调信号输入端。第三如果只是抓飞控输出可以先不接电调避免电机意外上电转起来安全第一。具体操作是这样的逻辑分析仪CH0接信号线GND接公共地打开Saleae Logic 2软件采样率设为24MHz采样时长设100毫秒左右触发方式选Channel 0上升沿触发。然后给飞控上电发送一个固定油门值比如油门300。点击Start开始采样几秒后停止就能看到一串规律的PWM脉冲。3.2 波形解码实操从脉宽反推16位帧数据抓到波形后放大到能看到每一个bit的程度。Dshot600下一个bit周期是1.667微秒。用Saleae的测量工具量高电平宽度宽度大约1.1微秒的是bit 1宽度0.55微秒左右的是bit 0。找出第一个完整bit的起始边沿按1.667微秒间隔往后切就能把16个bit逐个解出来。打个比方假设我抓到的波形高电平宽度依次是宽窄宽宽窄宽窄窄宽窄宽宽宽窄宽。把宽记作1、窄记作0得到一串16位数据1 0 1 1 0 1 0 0 1 0 1 1 1 0 1 0。这串二进制反过来写成十六进制就是0xB4BA。其中高12位是数据部分低4位是CRC。把高12位算一遍CRC如果和低4位一致说明这帧数据没问题。手动数bit比较费眼睛尤其数据一多容易看花。我的办法是先用Saleae自带的协议解码器如果版本支持Dshot直接选DSHOT协议它能自动把一帧帧解析成油门值和CRC。如果不支持就用我下面写的Python脚本处理导出的CSV逻辑一样但效率高很多。# 将Saleae导出的CSV按bit周期切分统计高电平占比判断0/1 sampling_rate 24_000_000 bit_period 1.667e-6 # Dshot600 data [] # samples为CSV中电平数组time为对应时间数组 for i in range(len(samples)): if time[i] start_time: continue idx int((time[i] - start_time) / bit_period) if idx 16: break # 统计每个bit内高电平采样点数 high_cnt 0 total_cnt 0 t_end start_time (idx 1) * bit_period while time[i] t_end: high_cnt samples[i] total_cnt 1 i 1 bit 1 if high_cnt / total_cnt 0.5 else 0 data.append(bit)这段逻辑不复杂核心就是按固定时间窗切分波形每个窗内高电平占比超过50%判为1否则判为0。采样率够高的情况下这个判断非常可靠。3.3 验证CRC手算与脚本对照解码出16位数据后下一步是验证CRC。以油门300、遥测位0为例我前面算过发送帧应该是0x258F。如果逻辑分析仪解出来的低4位不是0xF说明编码端或者抓取环节出了问题。最直接的验证方式是手算一遍。把解出来的高12位整理成整数拆成三组4位逐组异或得到CRC后与低4位比对。我实际测试时用脚本批量处理几十帧数据全部通过证明整条编码链路是通的。这里有一个小技巧CRC校验不通过时先别急着怀疑CRC算法优先检查位序因为Dshot是MSB先发如果代码里弄成LSB先发解出来的帧就会完全乱套CRC自然也对不上。4. CubeMXDMA方案STM32生成Dshot波形4.1 CubeMX配置细节定时器周期、DMA请求与Continuous RequestsCubeMX里配置Dshot输出核心是“定时器PWM输出DMA传输CCR”。以STM32F103C8T6为例时钟72MHz用TIM2的通道1输出PWM使能定时器的DMA请求让DMA把内存里的CCR数组搬进定时器的比较寄存器。具体配置项如下。定时器部分Clock Source选Internal ClockChannel1选PWM Generation CH1Prescaler设0Counter Period设119这样ARR1120PWM频率72MHz/120600kHz正好对应Dshot600。Pulse初始值设0后面全部由DMA改写。Auto-Reload Preload建议打开让ARR更新在下一个周期生效避免波形毛刺。DMA设置部分在DMA Settings里添加TIM2_CH1_TRIG请求方向Memory To PeripheralData Width选WordMemory Increment打开Mode选Normal。这里有个关键选项叫Continuous Requests翻译过来是连续请求。它的作用是让定时器持续不断地产生DMA请求而不是只在特定边沿来一次。我在实际使用中单帧发送用Normal模式就够每次启动DMA发完16个CCR后自动停止。如果选Circular模式DMA会循环搬运数组适合持续输出同一帧或者连续多帧但更新数据时要注意先停DMA再改缓冲区否则可能出现半帧新数据、半帧旧数据的混合波形电调会直接报错。配置项推荐值说明定时器TIM2选带DMA请求的通用定时器ChannelPWM Generation CH1单端输出不需要互补通道Prescaler0不分频直接用72MHzCounter Period119ARR1120对应600kHzDMA Data WidthWordCCR寄存器是32位DMA ModeNormal单次发送一帧逻辑清晰Continuous Requests按需连续发送时才需要配合Circular模式4.2 代码实现CRC计算、bit编码与DMA发送CubeMX生成工程后自己写的核心代码集中在两个地方一个是如何把油门值打包成16位帧并算CRC另一个是如何把16位帧拆成16个CCR值并启动DMA。打包帧的代码不复杂。油门值先左移1位空出最低位放遥测请求然后按4位一组异或算CRC最后把CRC拼到低4位得到完整的16位发送帧。这一步别偷懒我见过很多人在CRC上翻车算出来的CRC和电调预期不一致电机不转或者乱转排查大半天发现是分组异或的顺序写反了。#define DSHOT_FRAME_SIZE 16 #define DSHOT_BIT0_CCR 40 // ARR119时1/3周期高电平 #define DSHOT_BIT1_CCR 80 // 2/3周期高电平 uint32_t dshot_buffer[DSHOT_FRAME_SIZE]; uint8_t dshot_telemetry_request 0; void DSHOT_Send(uint16_t throttle) { uint16_t packet; uint16_t crc 0; uint16_t tmp; if (throttle 2047) throttle 2047; // 1. 组装12位数据油门值占高11位遥测位占最低位 packet (throttle 1) | (dshot_telemetry_request 0x01); // 2. 计算CRC12位数据拆成3组4位逐组异或 tmp packet; for (int i 0; i 3; i) { crc ^ (tmp 0x0F); tmp 4; } // 3. 拼接成完整的16位发送帧 packet (packet 4) | (crc 0x0F); // 4. 把16位帧编码成16个CCR占空比值 for (int i 0; i DSHOT_FRAME_SIZE; i) { if (packet (0x8000 i)) dshot_buffer[i] DSHOT_BIT1_CCR; else dshot_buffer[i] DSHOT_BIT0_CCR; } // 5. 启动DMA传输定时器按每个bit周期自动更新CCR HAL_TIM_PWM_Start_DMA(htim2, TIM_CHANNEL_1, dshot_buffer, DSHOT_FRAME_SIZE); }发送节奏也要控制好。Dshot没有强制规定帧间隔但一般按飞控控制频率来发比如1kHz到4kHz。每个Dshot帧的实际长度Dshot600下约26.67微秒如果按1kHz的发送频率一帧发完还有近1毫秒的空闲时间足够跑一轮控制算法。4.3 参数计算STM32F103定时器PWM的关键数值以STM32F103C8T6、72MHz定时器时钟为例计算Dshot600的参数。PWM频率就是bit率Dshot600要求600kHz。定时器时钟频率除以目标PWM频率再减1得到ARR值72MHz / 600kHz 120ARR119。bit 1的CCR等于(ARR1)乘以2/3也就是120×0.667≈80bit 0的CCR等于120×0.333≈40。如果换Dshot300目标频率300kHzARR239bit 1的CCR约160bit 0约80。换Dshot150ARR479bit 1的CCR约320bit 0约160。只要把定时器周期和CCR按比例调整同一套代码就能适配不同速率这也是CubeMXDMA方案灵活的地方。我实际测试时CCR取整后对解码影响不大。电调判断bit 1和bit 0依据的是高电平占整个bit周期的比例只要比例明显偏离50%就行。所以即使计算出来是80.5、39.5这种值取整后编码依然稳定。5. 调试经验与常见问题速查5.1 波形发不出来或CRC报错的排查思路如果逻辑分析仪抓不到任何波形先查三件事。第一定时器有没有真的启动PWM输出HAL_TIM_PWM_Start_DMA有没有调用成功第二DMA通道有没有配错CubeMX生成的DMA中断优先级别和别的外设冲突第三GPIO复用功能有没有打开忘了把引脚配置成TIM2_CH1的复用功能波形永远出不来。CRC报错是另一个高频问题。如果解出来的帧低4位和手算结果不一致先看位序。Dshot是MSB先发确认代码里是从0x8000开始逐位判断的而不是从0x0001开始。然后看CRC的分组逻辑必须把12位数据按4位一组从低到高或者从高到低统一处理混用就会算错。最后看数据位是不是有串扰逻辑分析仪的杜邦线太长、地线接触不良会造成波形畸变解出来的bit出错。我调试时遇到过一次诡异现象油门值从0到100很稳定超过200就随机CRC报错。后来发现是DMA缓冲区在PWM周期内被CPU改写了相当于边发边改数据。解决方法是发下一帧之前先调用HAL_TIM_PWM_Stop_DMA改完缓冲区再调用Start或者只在DMA发送完成中断里更新缓冲区绝不在发送中直接改数组。5.2 DMA连续请求与单次请求的区别CubeMX里那个Continuous Requests勾选项很多人搞不明白。简单说勾上它以后定时器的DMA请求是持续有效的配Circular模式可以实现不间断的循环发送不勾的话DMA每次搬运完一轮数据就停下来需要软件重新触发。单帧Dshot传输用Normal模式就足够。发送一帧16个CCR值DMA搬完自动停止CPU在发送完成中断里准备下一帧。这种方式逻辑清晰不容易出现数据错位。如果你想省掉发送完成中断用Circular模式让DMA反复刷同一块缓冲区但更新油门值时必须停DMA、改数据、重启DMA否则DMA可能正在搬前半段CPU改了后半段发出的帧就是新旧混搭电调直接不认。5.3 电调没反应反转阈值、掉线保护与故障安全波形明明正确CRC也对电调却没有任何反应这是最让新手崩溃的情况。先确认电调是不是Dshot模式。很多电调出厂默认是普通PWM或者Oneshot模式需要先通过电调调参软件切换到Dshot模式才能识别数字信号。不同品牌电调切换方式不一样有的是上电听提示音有的是连调参软件设置这点务必先确认。接着检查油门值范围。Dshot的油门值不是0到100而是0到2047。如果你直接传了一个0到100的值电调会以为油门极小电机处于停机状态。我之前就犯过这个错把油门100当成50%输出结果电调纹丝不动。还有一个容易被忽略的是掉线保护和故障安全。飞控如果长时间不发帧电调会进入掉线保护有的电调会持续输出当前油门有的会停机。调试时帧间隔不要拉太长建议用固定频率持续发送比如每1毫秒发一帧。这样不仅电调响应稳定逻辑分析仪抓包也方便。5.4 安全提示PWM飞车、MOS管发热的预防调试电调最怕的就是“飞车”电机突然满速旋转伤到人。测试Dshot输出时务必拆掉螺旋桨电调上电后先用极小油门值验证比如油门48左右确认电机是缓慢转动而不是狂转再逐步加大油门。MOS管发热的问题也值得单独说。Dshot本身就是高频开关信号如果电调MOS管驱动不足、散热不好或者电机相线接触不良电流波形会有尖峰MOS管发热会明显加剧。测试时电机不带桨空转电流比带桨小很多如果这时候MOS管都烫得厉害先别急着怀疑Dshot检查一下电调硬件、电源电压和电机相线连接。我个人的建议是整个调试过程先用稳压电源供电限流比如限制在2安培以内即使出现异常也不会烧电调、伤电机。5.5 常见问题速查表现象可能原因解决方向逻辑分析仪无波形CMSIS引脚复用未配置检查GPIO复用功能确认引脚映射到TIM2_CH1波形有但CRC全错位序反了确认从MSB开始发送CRC间歇性错误DMA缓冲区被CPU改写先停DMA再改缓冲区或等发送完成中断电调不转电调模式还是PWM/Oneshot用调参软件切换到Dshot模式电机乱转油门值映射错误确认油门范围0-20470对应停机MOS管异常发热相线接触不良或电源不稳检查三相连接、限流供电再测帧间隔不稳定发送函数在其他中断里被阻塞把发送调用放到控制循环主路径关掉无关中断最后分享一个我个人的习惯调试Dshot时我会先把逻辑分析仪的采样率调到24MHz触发方式设为上升沿抓一帧后立即停。先确认这一帧16个bit的脉宽都对再连上电调做电机测试。这样做的好处是一旦电机异常能快速区分是协议问题还是电调硬件问题不会两边同时排查浪费大量时间。这套“先抓波形、后带负载”的顺序我已经沿用到现在几乎每次调Dshot都能一次通过。
返回列表