尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

AM32电调Telemetry开发实战:DMA+USART+CRC8避坑指南

发布时间:2026/9/25 4:25:33

资讯中心
01
ARTICLE

AM32电调Telemetry开发实战:DMA+USART+CRC8避坑指南

AM32电调Telemetry开发实战:DMA+USART+CRC8避坑指南
1. 为什么要在AM32上折腾Telemetry如果你正在看这篇文章大概率是手里攥着一块刷了AM32固件的电调想让它把转速、电压、电流、温度这些实时数据吐回飞控结果发现官方文档语焉不详社区里的帖子东一句西一句串口能通但数据全是乱码或者干脆连不上。这不是你一个人的问题AM32的Telemetry功能在STM32F103这类资源紧张的MCU上实现起来确实有几个容易翻车的点。AM32是一款开源的电调固件主要跑在STM32F051、STM32F103、STM32G071这类芯片上支持BLHeli风格的PWM输入、DShot、串口通信等多种协议。Telemetry就是它的遥测回传功能通过USART把电调的状态数据打包发给飞控飞控再转发给地面站显示。听起来简单但实际做的时候你会遇到协议格式不统一、DMA和中断打架、CRC8校验算不对、串口发送阻塞导致电机控制抖动等一系列问题。这篇文章面向的是有一定嵌入式基础的开发者最好你已经在用STM32CubeMX或者CubeIDE做开发对USART、DMA、中断这些外设有基本概念。如果你刚接触STM32建议先把串口收发和DMA搬运数据这两件事搞明白再来看。我会从协议解析开始一步步讲到怎么在AM32的代码框架里把Telemetry报文稳定地发出去中间踩过的坑、试过的方案、最终为什么选这个方案都会说清楚。核心关键词就这几个AM32、Telemetry、DMA、USART、CRC8。整篇文章围绕它们展开不跑题。2. Telemetry协议到底长什么样2.1 常见电调遥测协议的格式对比电调遥测没有统一的国际标准不同厂商、不同固件用的格式不一样。BLHeli32用的是自己的Telemetry格式KISS也有自己的一套AM32在早期版本里兼容了BLHeli的部分格式后来也支持了更通用的串口遥测。你在做开发之前第一件事是确认你的飞控端期望收到什么格式的数据。协议类型典型帧长校验方式波特率数据内容BLHeli32 Telemetry10字节CRC8115200温度、电压、电流、转速、状态KISS Telemetry10字节CRC8115200类似BLHeli字节序不同自定义ASCII不定长无或简单校验9600-115200可读文本调试方便MSP遥测不定长校验和115200多传感器融合数据AM32的Telemetry实现主要参考BLHeli32的格式因为它的目标用户群体大量使用Betaflight和BLHeliSuite生态。BLHeli32的Telemetry帧结构大致是这样的帧头、温度、电压、电流、转速、状态标志、CRC8校验。具体字节序和缩放系数在不同版本里有细微差别你需要对着你用的AM32版本源码里的telemetry.c或者serial_telemetry.c来确认。2.2 帧结构逐字节拆解假设我们用的是BLHeli32兼容格式一帧10个字节结构如下Byte 0帧头通常是0x2E或者0x3F取决于具体实现Byte 1温度单位摄氏度直接取整Byte 2-3电压小端序单位0.01V比如12.6V存为1260Byte 4-5电流小端序单位0.01AByte 6-7转速小端序单位是电周期每秒钟需要根据电机极对数换算成RPMByte 8状态标志位bit0表示电机运转bit1表示方向等等Byte 9CRC8校验值对前9个字节计算这里有个容易搞错的地方电压和电流的缩放系数。有些版本用0.1V有些用0.01V如果你发出去的数据飞控显示电压只有实际值的十分之一大概率就是缩放系数搞反了。我的建议是先用一个已知电压比如12.6V的电池测试看飞控显示多少反推缩放系数。CRC8的计算多项式通常是0x07初始值0x00不反转输入输出。但BLHeli32用的是CRC8-DVB-S2多项式0x07初始值0x00结果异或0x00。这个细节在协议文档里经常被忽略但算错了飞控就直接丢弃整帧数据。2.3 为什么选USART而不是其他接口AM32跑在STM32F103上芯片资源有限。Telemetry需要持续回传数据对实时性有要求但又不能占用太多CPU时间。可选方案有几种USART最常用硬件简单两根线TX/RX波特率115200足够。缺点是如果不用DMA每个字节都要中断CPU开销大。I2C速度慢不适合持续数据流而且电调上I2C总线容易被电机噪声干扰。SPI速度快但需要额外的片选线飞控端也要支持SPI从机模式兼容性差。单线半双工BLHeli的Telemetry就是单线但AM32的硬件设计通常把USART的TX和RX分开用单线需要额外配置。综合来看USART是AM32上最现实的选择。STM32F103的USART1挂在APB2上最高可以跑到4.5Mbps115200绰绰有余。关键是怎么把数据高效地搬进USART的发送寄存器这就引出了DMA。3. DMA和USART的配合方式3.1 为什么不用中断发送如果你用HAL库的HAL_UART_Transmit()函数它是阻塞发送每个字节都要等TXE标志置位发完一帧10个字节大概需要10*(1/115200)*10 ≈ 0.87ms。这0.87ms里CPU什么都干不了对于电调来说电机换相控制是微秒级的阻塞0.87ms可能导致换相延迟电机发出异响甚至失步。用中断发送呢每发一个字节进一次中断10个字节就是10次中断。STM32F103的NVIC中断响应大概需要12个时钟周期72MHz主频下约0.17us10次中断加上中断服务程序的执行时间总开销也不小。而且如果Telemetry发送频率高中断会频繁打断电机控制循环。DMA发送就优雅多了你只需要把一帧数据准备好放到一个缓冲区里然后告诉DMA“从这个地址搬10个字节到USART的DR寄存器”DMA硬件会自动完成搬运每搬一个字节不需要CPU干预。搬完之后DMA产生一个传输完成中断你在中断里准备下一帧数据。CPU只在帧与帧之间介入中间过程完全解放。3.2 DMA通道选择和配置要点STM32F103的DMA1有7个通道USART1_TX通常映射到DMA1_Channel4USART1_RX映射到DMA1_Channel5。这个映射关系是固定的查参考手册的DMA请求映射表就能确认。如果你用的是USART2TX是DMA1_Channel7RX是DMA1_Channel6。配置DMA的时候有几个关键参数传输方向外设到存储器接收或存储器到外设发送数据宽度外设端和存储器端都设为8位Byte因为USART的DR寄存器是8位的循环模式发送用Normal模式发完一帧就停接收可以用Circular模式配合空闲中断优先级建议设为Medium或者High但不要设Very High避免抢占电机控制的DMA如果有的话中断使能使能传输完成中断TC在中断里处理下一帧在CubeMX里配置的时候注意DMA的“Mode”要选Normal不要选Circular否则DMA会一直循环发送同一个缓冲区数据会重复。这个坑我踩过飞控端收到的数据一直是第一帧的内容查了半天才发现是DMA模式选错了。3.3 双缓冲和空闲中断的取舍对于接收方向DMA加空闲中断IDLE是STM32串口接收的经典方案。空闲中断在总线空闲一个字节时间后触发你可以在中断里计算收到了多少字节然后处理数据。这个方案的好处是不需要知道对方发多少字节来多少收多少。但Telemetry主要是发送方向接收方向在AM32上通常用于接收飞控的配置命令。如果你只需要发送遥测接收方向可以简单用中断或者轮询。不过为了完整性我还是建议把接收也配上DMA加空闲中断这样飞控发来的参数调整命令不会丢。双缓冲Double Buffer在STM32F103的DMA上支持有限F4系列有真正的双缓冲模式F103需要用两个DMA通道或者手动切换缓冲区。对于Telemetry发送来说单缓冲加传输完成中断就够了没必要上双缓冲增加复杂度。4. CRC8校验的实现细节4.1 CRC8的多种变体CRC8不是只有一个标准不同的多项式、初始值、输入反转、输出反转组合起来有几十种变体。BLHeli32 Telemetry用的是CRC8-DVB-S2参数如下多项式0x07初始值0x00输入反转否输出反转否结果异或0x00但有些AM32的版本用的是CRC8-MAXIM多项式0x31初始值0x00结果异或0x00。这两个算出来的校验值完全不同。如果你发现飞控丢弃了所有帧第一件事就是确认CRC参数。4.2 查表法还是逐位计算CRC8的计算有两种实现方式逐位计算和查表法。逐位计算代码简单但每个字节要循环8次10个字节就是80次循环在72MHz的F103上大概消耗几微秒。查表法需要一个256字节的表计算时直接查表速度快但占用Flash空间。对于Telemetry这种每秒发送几十到几百帧的场景逐位计算完全够用。我的建议是先用逐位计算实现确认功能正常后再考虑优化。下面是一个逐位计算的CRC8实现uint8_t crc8_dvb_s2(uint8_t crc, uint8_t data) { crc ^ data; for (uint8_t i 0; i 8; i) { if (crc 0x80) { crc (crc 1) ^ 0x07; } else { crc 1; } } return crc; }调用的时候初始crc传0x00然后对前9个字节依次调用最后得到的crc就是第10个字节。4.3 校验失败的常见原因CRC算不对除了参数搞错还有几个隐蔽的坑字节序问题电压、电流、转速是多字节的小端序和大端序搞反了CRC自然不对。确认你的飞控端期望的是小端序还是大端序。数据类型截断比如电压是uint16_t你传的时候强转成uint8_t高位丢了CRC算出来和飞控端不一致。缓冲区越界如果你发送的缓冲区比实际数据长CRC计算包含了未初始化的字节结果随机。CRC计算范围有些协议CRC只算数据字节不算帧头有些算全部。确认你的协议里CRC覆盖哪些字节。我遇到过一次CRC随机失败的问题最后发现是DMA发送完成中断里我在准备下一帧数据时没有等DMA完全停止新数据覆盖了正在发送的缓冲区。解决办法是在传输完成中断里先关闭DMA再填充数据最后重新使能DMA。5. 在AM32代码框架里落地Telemetry5.1 AM32的代码结构概览AM32的源码结构比较清晰主要文件包括main.c主循环和初始化motor.c电机换相控制adc.c电压电流采样serial_telemetry.c遥测发送eeprom.c参数存储dshot.cDShot协议解析Telemetry相关的代码主要在serial_telemetry.c里它负责组帧、计算CRC、通过USART发送。你需要关注的是它怎么和主循环配合怎么在不影响电机控制的前提下把数据发出去。5.2 发送时机和频率控制Telemetry不能发得太频繁否则占用太多CPU和总线时间也不能太慢否则地面站显示卡顿。BLHeli32的默认遥测频率大概是每秒30到50帧具体取决于飞控的请求频率。在AM32里Telemetry发送通常由两种方式触发一种是定时器中断里定时发送另一种是收到飞控的遥测请求后回复。我建议用定时器触发比如每20ms发一帧这样频率稳定不会因为飞控请求的抖动导致数据忽快忽慢。定时器可以用TIM3或者TIM4配置成20ms周期中断在中断里设置一个标志位主循环检测到标志位后组帧并启动DMA发送。注意不要在定时器中断里直接组帧和启动DMA因为组帧需要读ADC值、算CRC耗时较长会阻塞其他中断。5.3 组帧函数的实现组帧函数的核心任务是把各个传感器的值读出来按照协议格式填充到发送缓冲区计算CRC然后启动DMA。下面是一个简化的实现示例#define TELEMETRY_FRAME_LEN 10 uint8_t telemetry_tx_buf[TELEMETRY_FRAME_LEN]; void telemetry_build_frame(void) { uint16_t voltage adc_get_voltage(); // 单位0.01V uint16_t current adc_get_current(); // 单位0.01A uint16_t rpm motor_get_erpm(); // 电周期每秒 uint8_t temp adc_get_temperature(); // 摄氏度 uint8_t status motor_get_status(); telemetry_tx_buf[0] 0x2E; // 帧头 telemetry_tx_buf[1] temp; telemetry_tx_buf[2] voltage 0xFF; telemetry_tx_buf[3] (voltage 8) 0xFF; telemetry_tx_buf[4] current 0xFF; telemetry_tx_buf[5] (current 8) 0xFF; telemetry_tx_buf[6] rpm 0xFF; telemetry_tx_buf[7] (rpm 8) 0xFF; telemetry_tx_buf[8] status; uint8_t crc 0; for (int i 0; i 9; i) { crc crc8_dvb_s2(crc, telemetry_tx_buf[i]); } telemetry_tx_buf[9] crc; }这个函数里adc_get_voltage()这些函数需要你根据AM32的ADC采样代码来实现。注意电压和电流的缩放系数要和飞控端一致否则显示值不对。5.4 DMA发送的启动和完成处理组帧完成后启动DMA发送void telemetry_send(void) { HAL_UART_Transmit_DMA(huart1, telemetry_tx_buf, TELEMETRY_FRAME_LEN); }HAL库的HAL_UART_Transmit_DMA()会自动配置DMA并启动传输。传输完成后会调用HAL_UART_TxCpltCallback()回调函数你可以在里面设置一个标志位表示上一帧发送完成可以准备下一帧了。但这里有个问题如果你在上一帧还没发完的时候就调用HAL_UART_Transmit_DMA()HAL库会返回HAL_BUSY新数据发不出去。所以你需要一个状态机来管理发送过程空闲状态等待定时器触发组帧状态填充缓冲区计算CRC发送状态DMA正在搬运数据完成状态DMA传输完成回到空闲状态用状态机可以避免发送冲突也能在发送失败时重试。6. 实战中遇到的坑和解决方案6.1 串口DMA发送不能连续发送这是HAL库的一个经典问题。你调用HAL_UART_Transmit_DMA()发送第一帧成功发送第二帧返回HAL_BUSY。原因是HAL库在传输完成后没有正确清除内部状态或者你的回调函数里没有重新使能发送。解决办法有几个一是用LL库直接操作寄存器绕过HAL的状态管理二是在HAL_UART_TxCpltCallback()里手动清除huart-gState三是用DMA的传输完成中断直接操作不用HAL的封装。我最终选的是第三种方案直接配置DMA和USART寄存器不经过HAL的UART发送函数。这样代码更精简也更容易控制发送时机。具体做法是使能USART1的DMA发送请求设置CR3寄存器的DMAT位配置DMA1_Channel4的源地址、目的地址、传输长度然后使能DMA通道。传输完成后DMA产生TC中断在中断里清除标志位准备下一帧。6.2 电机噪声导致串口误码电调上电机换相会产生强烈的电磁干扰串口线如果走线不好很容易误码。表现是飞控端偶尔收到CRC错误的帧或者数据跳变。硬件上的解决办法串口线尽量短远离电机线TX和RX之间加磁珠或者小电容滤波如果条件允许用屏蔽线。软件上的解决办法降低波特率115200不行就降到57600增加发送重复次数同一帧发两次飞控端取正确的那个在CRC校验失败时请求重发。我实测下来115200波特率下如果串口线长度小于10cm并且和电机线保持一定距离误码率可以接受。如果线长了降到57600会稳定很多。6.3 DMA传输完成中断不触发有时候DMA配置好了数据也发出去了但传输完成中断就是不触发。排查思路确认DMA的TC中断使能位CCR寄存器的TCIE位置1了确认NVIC里对应的DMA通道中断使能了确认在中断服务程序里清除了TC标志位写1到IFCR寄存器确认DMA没有因为传输错误而停止检查TE标志位还有一个隐蔽的原因如果你在DMA传输过程中修改了传输长度或者源地址DMA可能会异常终止。确保在DMA使能之前配置好所有参数使能之后不要动。6.4 常见问题速查表现象可能原因排查方法解决方案飞控收不到数据TX/RX接反交换TX和RX试试更正接线数据全是乱码波特率不匹配确认双方波特率统一为115200CRC校验失败CRC参数错误对比协议文档改用正确的多项式和初始值电压显示为0ADC通道配置错误检查ADC采样代码更正通道和缩放系数发送几帧后停止DMA状态未清除检查TC中断在中断里清除标志并重启DMA电机抖动发送阻塞控制循环测量发送耗时改用DMA发送降低频率数据跳变电磁干扰观察误码率缩短线长加滤波降波特率7. 调试工具和验证方法7.1 用逻辑分析仪抓串口波形调试串口通信逻辑分析仪是最直接的工具。把探头接到TX线上设置波特率115200触发方式设为下降沿就能抓到每一帧的波形。你可以直接看到帧头、数据字节、CRC字节和你的代码对照确认发送的内容对不对。我用的是一款几十块钱的USB逻辑分析仪配合开源的Sigrok/PulseView软件解码串口协议很方便。抓到的波形可以直接导出成文本和代码里的缓冲区对比。7.2 用飞控地面站验证数据如果你有Betaflight飞控可以把电调的Telemetry线接到飞控的某个串口上在Betaflight配置里打开遥测然后在地面站软件里看电调数据。这是最接近实际使用场景的验证方法。注意Betaflight对Telemetry的格式有要求不是所有格式都支持。你需要确认你的AM32固件输出的格式和Betaflight期望的一致。如果不一致可能需要在飞控端做转换或者修改AM32的代码。7.3 用串口助手做单元测试在把Telemetry接到飞控之前先用USB转串口模块把电调的TX接到电脑上用串口助手接收数据。设置好波特率看能不能收到完整的帧。你可以写一个简单的Python脚本解析收到的数据验证CRC和数值范围。import serial import struct def crc8_dvb_s2(data): crc 0 for byte in data: crc ^ byte for _ in range(8): if crc 0x80: crc (crc 1) ^ 0x07 else: crc 1 crc 0xFF return crc ser serial.Serial(COM3, 115200, timeout1) while True: frame ser.read(10) if len(frame) 10: if crc8_dvb_s2(frame[:9]) frame[9]: temp frame[1] voltage struct.unpack(H, frame[2:4])[0] / 100.0 current struct.unpack(H, frame[4:6])[0] / 100.0 rpm struct.unpack(H, frame[6:8])[0] print(f温度:{temp}C 电压:{voltage}V 电流:{current}A 转速:{rpm}) else: print(CRC错误)这个脚本可以快速验证你的Telemetry输出是否正确比接飞控方便得多。8. 性能优化和进阶玩法8.1 降低CPU占用Telemetry发送本身占用CPU不多但如果你在组帧的时候做了浮点运算或者复杂的换算就会增加CPU负载。优化方向用整数运算代替浮点运算比如电压用mV为单位避免除以100.0把CRC计算表放在Flash里用查表法代替逐位计算减少组帧频率从50Hz降到30Hz对显示效果影响不大我实测过在72MHz的F103上逐位CRC计算10个字节大概消耗5us查表法只要1us。如果每秒发50帧逐位计算占用0.025%的CPU时间其实可以忽略。但如果你的主循环还有其他耗时操作优化一下也无妨。8.2 支持多种遥测格式如果你想让AM32的Telemetry兼容更多飞控可以在代码里实现多种格式通过参数选择。比如BLHeli32格式、KISS格式、自定义ASCII格式。这样同一个固件可以适配不同的飞控提高通用性。实现方式是用函数指针或者switch-case根据配置参数调用不同的组帧函数。注意不同格式的帧长和CRC参数不同发送缓冲区要足够大。8.3 遥测数据的扩展除了基本的电压、电流、转速、温度你还可以扩展其他数据电机堵转检测通过电流突变判断温度保护超过阈值时在状态位里标记运行时间统计记录电机累计运行时间错误码记录过流、过温、失步等错误这些数据可以放在状态字节或者扩展帧里。但要注意帧长不能超过飞控端的接收缓冲区否则会丢数据。9. 一些个人经验我在AM32上做Telemetry开发前后折腾了大概两周大部分时间花在调试CRC和DMA上。最开始用HAL库的阻塞发送电机一跑起来就抖动后来换成DMA发送问题解决。CRC算不对的时候我用逻辑分析仪抓了波形和飞控端的解析代码逐字节对比才发现是多项式搞错了。还有一次Telemetry数据在电机低速时正常高速时全是乱码。查了半天发现是电机高速时电流大电源纹波导致MCU的USART时钟不稳定。解决办法是在USART的时钟线上加了一个小电容问题缓解。如果你也在做类似的项目我的建议是先把协议搞清楚用串口助手验证数据格式再接飞控。不要一上来就接飞控调试那样出了问题你分不清是电调的问题还是飞控的问题。另外逻辑分析仪真的很有用几百块钱的投资能省你很多时间。最后分享一个小技巧在Telemetry帧里加一个递增的序号字节飞控端可以据此判断是否丢帧。这个字节不参与CRC计算或者参与也行只要双方约定好。这样调试的时候能快速发现是发送端丢帧还是接收端丢帧。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。