ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32 SBUS协议解析:DMA循环+IDLE中断+三段式状态机实战

STM32 SBUS协议解析:DMA循环+IDLE中断+三段式状态机实战 1. 项目概述为什么SBUS解析不能只靠普通串口接收SBUS是Futaba、FrSky等主流航模遥控器广泛采用的串行通信协议它以100kHz波特率、反向逻辑、8位数据位、2位停止位、无校验的方式每7ms发送一帧25字节的数据包。这25字节里包含16路通道值每路11位共176位、1路数字开关状态、1路信号丢失标志和1字节帧尾。如果你用传统方式——比如HAL_UART_Receive_IT()配合一个全局缓冲区——去接收会立刻掉进三个坑里第一7ms一帧而串口空闲中断IDLE触发延迟在普通中断模式下可能高达几十微秒甚至上百微秒一旦错过IDLE边沿整帧数据就错位第二16路通道值跨字节边界排列比如第1路的bit0~bit7在byte1bit8~bit10在byte2的低3位这种非对齐打包方式靠简单memcpy根本没法安全提取第三遥控器断电、线缆抖动、电源噪声都会导致帧头错乱或字节丢失没有状态机兜底程序很容易卡死在某个中间状态飞控直接失联。我最早在STM32F103C8T6上试过纯中断接收结果实测连续飞行12分钟就出现一次通道跳变排查发现是某次IDLE中断晚到了43μs导致DMA把下一帧的前3个字节当成了当前帧的结尾后续所有通道值全偏移。后来改用HAL库下的DMA循环接收IDLE中断组合再叠上三段式状态机做帧同步与解包连续压测72小时没出一次错帧。这个方案的核心不是堆砌技术名词而是让每个环节各司其职DMA负责“不丢字节”IDLE中断负责“精准切帧”状态机负责“认得清谁是谁”。你不需要懂Verilog也能写好状态机——它就是一套带记忆的if-else流程图只不过我们把它拆成“等待帧头”、“接收有效载荷”、“校验与提交”三个稳定阶段每个阶段只响应特定输入其他输入一律忽略。关键词里反复出现的“dma continuous requests”和“dma加空闲中断”说的就是这个组合拳的底层支撑点DMA必须配置为循环模式Circular否则缓冲区满后自动停摆IDLE中断必须在串口空闲线检测到高电平持续1字符时间后立即触发这是切分数据流的唯一可靠锚点。这套方案特别适合刚从标准库转HAL库的朋友。很多人抱怨HAL库“封装太深、不好调试”其实问题不在HAL而在没吃透它的设计哲学HAL把硬件操作抽象成“初始化→启动→回调→处理”的四步闭环而DMAIDLE正是把“启动”和“回调”两个环节用到了极致。你不用手动清TC标志、不用反复调用HAL_UART_Receive_DMA只要在MX_USARTx_UART_Init()里把huart-hdmarx-Init.Mode DMA_CIRCULAR设对在HAL_UART_RxCpltCallback()里啥都不干因为循环模式下它根本不会进这个回调只专注处理HAL_UARTEx_RxEventCallback()里传来的IDLE事件——这才是HAL库该有的用法。网上那些教你在HAL_UART_RxCpltCallback()里重装DMA地址的写法本质上还是在用标准库思维套HAL壳子既绕弯又容易出错。2. 整体架构设计DMA循环缓冲区 IDLE中断 三段式状态机的协同逻辑2.1 为什么必须用DMA循环模式而非普通模式先说清楚一个关键误区很多教程说“DMA接收要配成Normal模式收到一帧后手动重启”这是对HAL库DMA机制的根本性误读。Normal模式下DMA传输完成一次后自动关闭你需要在IDLE中断里调用HAL_UART_Receive_DMA()重新启动但这个函数内部会先检查DMA是否忙再配置寄存器、启动传输——这一套操作耗时约12~18μsKeil5 -O2优化下实测。而SBUS帧间隔只有7ms看似充裕可问题在于如果IDLE中断刚触发你还在执行HAL_UART_Receive_DMA()此时新一帧数据已涌入RX FIFODMA还没来得及把FIFO里的字节搬走就会发生溢出ORE标志置位导致第一个字节丢失。我用逻辑分析仪抓过波形溢出后紧接着的帧头0x0F直接被吞掉状态机永远等不到起始符。循环模式Circular彻底规避了这个问题。配置时设置DMA缓冲区为256字节远大于SBUS单帧25字节启用循环模式后DMA像一条永不停歇的传送带RX FIFO只要有数据就自动搬入缓冲区填满后自动回到起点继续覆盖。这样IDLE中断的角色就从“搬运工调度员”降级为“切帧质检员”它不负责搬数据只负责在串口线空闲时快速计算出“上一帧数据在缓冲区里的起止位置”然后通知状态机来处理。计算方法很简单IDLE中断触发瞬间读取DMA的当前数据寄存器CDR这个值就是DMA已经搬走的总字节数用它对缓冲区长度取模就得到最后一个字节在缓冲区中的索引再往前推25个字节就是当前帧的起始索引。整个过程只需3条指令耗时1μs完全不会漏字节。提示缓冲区长度必须是2的幂次如256、512否则取模运算无法用位运算优化。STM32F1系列DMA的CDR寄存器是16位的所以缓冲区最大不能超过65536字节256字节是兼顾内存占用与安全余量的黄金值。2.2 IDLE中断的精确触发时机与硬件依赖IDLE中断的可靠性取决于STM32的USART外设对“空闲线检测”的实现方式。手册里明确写着IDLE标志在RX引脚检测到连续1个字符时间的高电平后置位。这里的关键是“1个字符时间”怎么算它等于1数据位校验位停止位×位时间。SBUS是100kHz波特率位时间10μs8N2格式180211位所以空闲检测窗口是110μs。这意味着只要两帧数据之间有≥110μs的高电平间隙IDLE中断就必然触发。而实际SBUS协议规定帧间隔最小为7ms远大于110μs所以理论上100%可靠。但现实很骨感。我遇到过三次IDLE失灵第一次是PCB上RX引脚附近铺了大片地铜分布电容把下降沿拖慢导致空闲检测窗口被压缩到95μs偶尔漏触发第二次是使用CH340G USB转TTL模块其内部电平转换电路引入了约20μs的传播延迟叠加后总延迟达130μs超出了110μs窗口第三次最隐蔽——电源纹波过大RX引脚在空闲期出现毫伏级振荡被误判为“非空闲”。解决方案很务实PCB布线时RX走线远离电源层和高速信号线长度控制在3cm内USB转TTL模块必须选FT232RL或CP2102这类低延迟芯片电源处加一颗10μF钽电容0.1μF陶瓷电容滤波。这些细节在HAL库中文手册里根本找不到全是用示波器一帧帧抓出来的血泪教训。2.3 三段式状态机的设计哲学与状态迁移表状态机不是炫技是应对SBUS协议不确定性的防御性编程。SBUS数据流本质是“确定帧长不确定起始位置”的混合体我们知道每帧25字节但不知道哪一字节是帧头0x0F。传统做法是每收到一个字节就检查是否为0x0F找到后连续读24字节——这在理想环境下可行但遇到干扰时某个字节被噪声翻转成0x0F状态机就会错误同步后续所有通道值全错。三段式状态机通过“验证确认”双保险解决此问题。当前状态输入事件下一状态动作说明WAIT_SYNC收到字节 0x0FRECV_PAYLOAD记录当前缓冲区索引为start_pos启动payload计数器WAIT_SYNC收到字节 ! 0x0FWAIT_SYNC忽略继续等待RECV_PAYLOADpayload计数器 24RECV_PAYLOAD累加计数器不处理数据RECV_PAYLOADpayload计数器 24VERIFY_FRAME调用校验函数检查最后1字节是否为0x00VERIFY_FRAME校验通过SUBMIT_DATA解析16路通道值更新全局结构体触发用户回调VERIFY_FRAME校验失败WAIT_SYNC丢弃整帧重置状态机这个表里藏着两个关键设计第一“WAIT_SYNC”状态只响应0x0F其他任何输入都无视杜绝了误同步第二“VERIFY_FRAME”状态必须校验最后一字节0x00SBUS帧尾因为0x0F作为帧头可能出现在payload中间比如通道值恰好是0x0F但0x00作为帧尾几乎不可能自然出现16路通道值范围是0~2047最高位是bit100x00只会在所有通道为0时出现概率极低。我统计过10万帧真实数据帧尾0x00的出现率是99.998%足以作为强校验依据。注意状态机变量必须声明为static且所有状态迁移必须在IDLE中断的同一上下文中完成。切忌在状态机中调用HAL_Delay()或任何可能阻塞的函数——中断服务程序里执行耗时操作是嵌入式开发的大忌。3. 核心实现细节从CubeMX配置到状态机代码逐行解析3.1 CubeMX中的关键配置项与陷阱CubeMX是HAL库的起点但默认配置离SBUS需求差得很远。以下是必须手动修改的5个关键点USARTx参数波特率设为100000不是115200Word Length选8 BitsStop Bits选2Parity选NoneMode选Asynchronous。特别注意必须取消勾选“Hardware Flow Control”SBUS是单向通信RTS/CTS引脚要留给其他外设。DMA配置在“Configuration”页点击USARTx → “DMA Settings”添加RX ChannelMode选“Circular”Data Width选“Byte”Priority选“High”避免被ADC等DMA抢占。Buffer Size填256Address Increment选“Memory”Memory Data Width选“Byte”。这里有个隐藏陷阱CubeMX生成的MX_DMA_Init()函数里hdma_usartx_rx.Init.PeriphInc DMA_PINC_DISABLE;这行必须保留因为USART外设地址是固定的如0x40004404不能自增。NVIC设置在“System Core” → “NVIC”页勾选“USARTx global interrupt”和“DMAx streamy global interrupt”但不要勾选“USARTx wake-up interrupt”——这个中断和IDLE无关是给低功耗模式用的勾选了反而会干扰。HAL库版本务必使用STM32Cube_FW_F1_V1.8.4及以上版本。早期版本如V1.6.0的HAL_UARTEx_ReceiveToIdle_DMA()函数存在bug当DMA缓冲区未满时触发IDLE它会错误地认为传输已完成导致RxXferSize被清零。V1.8.4修复了此问题函数名也改为HAL_UARTEx_ReceiveToIdle_IT()更准确地反映了其基于中断的本质。时钟树SBUS对时序敏感APB2USART1挂载于此时钟必须≥36MHz。F103C8T6的默认HSE是8MHz经PLL倍频后APB272MHz完全满足但如果用HSI8MHz做PLL源需确保PLL_MULL9否则APB2可能只有36MHz导致100kHz波特率误差超±3%实测误差达4.2%帧同步失败。配置完成后生成代码打开main.c你会看到MX_USARTx_UART_Init()和MX_DMA_Init()函数。现在要做的不是改它们而是找到HAL_UART_MspInit()函数在里面添加一行关键代码// 在HAL_UART_MspInit()函数末尾添加 __HAL_UART_ENABLE_IT(huartx, UART_IT_IDLE); // 使能IDLE中断CubeMX默认不生成这行必须手写。否则IDLE中断永远不会触发。3.2 IDLE中断服务程序ISR的精简实现IDLE中断的唯一任务是“快准狠”地定位帧位置其他事一概不管。以下是经过10次逻辑分析仪验证的ISR代码void USARTx_IRQHandler(void) { uint32_t isrflags READ_REG(huartx.Instance-SR); uint32_t cr1its READ_REG(huartx.Instance-CR1); // 检查是否为IDLE中断仅当IDLEIE1且IDLE1时触发 if (((isrflags USART_SR_IDLE) ! RESET) ((cr1its USART_CR1_IDLEIE) ! RESET)) { // 清除IDLE标志读SR后必须读DR否则标志不复位 __IO uint8_t dummy READ_REG(huartx.Instance-DR); // 获取DMA当前传输字节数CDR寄存器 uint16_t dma_counter huartx.hdmarx-Instance-NDTR; // 计算已接收总字节数 缓冲区长度 - CDR值 uint16_t total_received SBUS_RX_BUFFER_SIZE - dma_counter; // 计算当前帧起始索引取模运算因缓冲区长度为256可用位运算优化 uint16_t start_index (total_received - SBUS_FRAME_LENGTH) (SBUS_RX_BUFFER_SIZE - 1); // 将帧数据指针和长度传递给状态机处理函数 sbus_process_frame(huartx.pRxBuffPtr[start_index], SBUS_FRAME_LENGTH); } }这段代码有三个精妙之处第一用READ_REG()宏直接读寄存器绕过HAL库的函数调用开销实测比__HAL_UART_GET_FLAG()快3倍第二清除IDLE标志的操作严格遵循手册要求先读SR再读DR缺一不可否则下次中断不触发第三 (SBUS_RX_BUFFER_SIZE - 1)替代% SBUS_RX_BUFFER_SIZE因为256是2的幂位运算比除法快一个数量级。我测试过这段ISR在F103上执行时间稳定在0.82μs为状态机留足了处理时间。3.3 三段式状态机的C语言实现与通道解包逻辑状态机代码放在sbus_parser.c中核心是sbus_process_frame()函数。它接收一帧25字节的原始数据输出16路通道值到全局结构体sbus_data_ttypedef struct { uint16_t channel[16]; // 16路通道值范围0~2047 uint8_t failsafe; // 1信号丢失0正常 uint8_t frame_lost; // 1本帧校验失败0成功 } sbus_data_t; sbus_data_t sbus_data; void sbus_process_frame(uint8_t *frame, uint8_t len) { static enum { WAIT_SYNC, RECV_PAYLOAD, VERIFY_FRAME } state WAIT_SYNC; static uint8_t payload[24]; static uint8_t payload_idx 0; if (len ! SBUS_FRAME_LENGTH) return; // 长度不对直接丢弃 // 状态机主循环 switch(state) { case WAIT_SYNC: if (frame[0] 0x0F) { // 找到帧头复制后续24字节到payload缓冲区 memcpy(payload, frame[1], 24); payload_idx 0; state RECV_PAYLOAD; } break; case RECV_PAYLOAD: // 此状态不在此处处理因为frame已是完整25字节 // 直接跳转到校验 state VERIFY_FRAME; break; case VERIFY_FRAME: // 校验帧尾必须是0x00 if (frame[24] ! 0x00) { state WAIT_SYNC; sbus_data.frame_lost 1; return; } // 解包16路通道值核心算法 for (int i 0; i 16; i) { uint8_t bit_offset i * 11; // 第i路通道起始比特位 uint8_t byte_idx bit_offset / 8; // 所在字节索引 uint8_t bit_in_byte bit_offset % 8; // 在字节内的起始位 // 从payload中提取11位跨越最多2个字节 uint16_t value 0; value | (uint16_t)(payload[byte_idx]) bit_in_byte; // 如果跨越字节补上高位 if (bit_in_byte 11 8) { uint8_t high_bits 11 - (8 - bit_in_byte); value | (uint16_t)(payload[byte_idx 1]) (8 - bit_in_byte); } // 取低11位范围0~2047 sbus_data.channel[i] value 0x07FF; } // 提取数字开关和信号丢失标志 sbus_data.failsafe (payload[23] 0x40) ? 1 : 0; // bit6 of byte23 state WAIT_SYNC; sbus_data.frame_lost 0; break; } }解包逻辑是SBUS协议的难点。以第0路通道为例bit0~bit7在payload[0]bit8~bit10在payload[1]的bit0~bit2。代码中bit_offset 0*11 0byte_idx 0/8 0bit_in_byte 0%8 0所以value | payload[0] 0接着判断011 8成立high_bits 11-(8-0)3所以value | payload[1] (8-0) payload[1] 8——等等这不对payload[1] 8永远是0。这里有个经典错误右移位数应该是8 - bit_in_byte但bit_in_byte是0所以是payload[1] 8显然错了。正确做法是high_bits表示需要从下一个字节取多少位所以右移位数是8 - high_bits。修正后的代码应为if (bit_in_byte 11 8) { uint8_t high_bits 11 - (8 - bit_in_byte); // 需要从下一字节取high_bits位 value | (uint16_t)(payload[byte_idx 1]) (8 - bit_in_byte); // 左移补齐低位 } value 0x07FF; // 最终取11位这个bug我在江科大的STM32教程视频里也看到过很多初学者照抄就踩坑。实测修正后第15路通道bit165~bit175能正确解出2047的最大值。4. 实操过程与性能验证从硬件连接到72小时压力测试4.1 硬件连接与信号调理实战要点SBUS信号是反向逻辑idle高电平数据低电平而STM32的USART RX引脚默认是正向接收。直接连接会导致帧头0x0F二进制00001111被识别为0xF011110000全盘错乱。必须加一级反相电路。最稳妥的方案是用SN74LVC1G04单路反相器供电3.3V输入接遥控器SBUS输出输出接STM32 RX。我试过用三极管搭建简易反相器但开关速度不够100kHz下波形畸变严重也试过软件反相在DMA接收后对每个字节取反但这样IDLE中断的触发时机就乱了——因为IDLE检测的是物理线上的电平不是软件里的数值。PCB布局上反相器必须紧挨着STM32的RX引脚走线长度5mm。我曾把反相器放在板子另一端走线长达3cm结果在电机全速运转时电磁干扰耦合进走线RX引脚出现毛刺IDLE中断频繁误触发。解决方案是在反相器输入输出端各加一颗100pF瓷片电容到地滤除高频噪声同时在STM32的VDDA引脚模拟电源处用10μF钽电容0.1μF瓷片电容滤波降低电源噪声对USART基准电压的影响。USB转TTL模块的选择至关重要。CH340G的驱动能力弱输出高电平仅2.8V标称3.3V在长线传输下易被噪声淹没CP2102输出高电平稳定在3.25V以上且内置ESD保护。我用示波器对比过两者在1米杜邦线上的波形CH340G的上升沿有明显回沟CP2102则干净利落。因此调试阶段务必用CP2102量产时可换用成本更低的CH9102F国产替代性能接近CP2102。4.2 Keil5工程配置与编译优化技巧在Keil5中必须开启两项关键优化才能保证状态机实时性第一在“Options for Target” → “C/C”页将Optimization Level设为“Level 3”并勾选“Optimize for Time”第二在“Target”页将“Use MicroLIB”打钩。MicroLIB是ARM专为嵌入式优化的C库memcpy等函数用汇编重写比标准C库快40%。实测开启后sbus_process_frame()函数执行时间从3.2μs降至1.9μs。链接脚本scatter file也要调整。默认的STM32F103CB_FLASH.sct把RAM分配给堆栈和全局变量但SBUS解析需要一块256字节的DMA缓冲区必须确保它位于RAM的低地址区域0x20000000起因为某些STM32型号的DMA控制器对内存地址有访问限制。我在RW_IRAM1段里显式定义LR_IROM1 0x08000000 0x00020000 { ; load region size_region ER_IROM1 0x08000000 0x00020000 { ; load address execution address *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { ; RW data .ANY (RW ZI) sbus_rx_buffer.o (RW) ; 强制将DMA缓冲区放在这里 } }然后在sbus_parser.c中定义缓冲区__attribute__((section(.sbus_rx_buffer))) uint8_t sbus_rx_buffer[256];这样链接器会把缓冲区精准放在0x20000000起始的RAM中避免DMA访问异常。4.3 72小时压力测试方案与失效分析验证方案不能只看“能不能跑”要看“极端条件下稳不稳”。我的测试分三阶段阶段一信号完整性测试用信号发生器模拟SBUS波形注入不同幅度噪声±100mV、±200mV观察帧丢失率。结果在±150mV噪声下帧丢失率0.001%10万帧丢1帧主要发生在噪声恰好覆盖帧头0x0F的bit3~bit4时。解决方案是在状态机中加入“软同步”当连续3帧校验失败强制进入WAIT_SYNC状态并丢弃接下来5ms内的所有数据等待信道恢复。阶段二电源扰动测试用电子负载在3.3V电源线上注入1A/100Hz的脉冲电流模拟电机启停。此时VDDA电压跌落至2.95VUSART的采样点偏移。现象IDLE中断触发延迟从110μs增至135μs偶尔漏帧。对策是修改IDLE检测逻辑不依赖硬件IDLE改用定时器捕获RX引脚电平变化。用TIM2的CH1通道配置为输入捕获检测RX引脚由低到高跳变当高电平持续时间100μs即判定为空闲。虽然增加了定时器资源占用但可靠性提升一个数量级。阶段三长期老化测试将设备置于60℃恒温箱中连续运行72小时每小时记录一次16路通道值的标准差。结果通道值波动范围始终在±2LSB内1LSB0.5无累积误差。唯一发现的问题是长时间运行后sbus_data_t结构体中的frame_lost标志偶尔被置1后不自动清零。排查发现是状态机在VERIFY_FRAME状态中sbus_data.frame_lost 0;这行代码被编译器优化掉了——因为前面有return语句。修正方法是把frame_lost清零移到状态机入口处void sbus_process_frame(uint8_t *frame, uint8_t len) { sbus_data.frame_lost 0; // 统一在此清零 // ... 后续状态机逻辑 }5. 常见问题与独家排查技巧从IDLE不触发到通道值跳变的全链路诊断5.1 IDLE中断不触发的5种原因与速查表现象可能原因排查步骤解决方案示波器看到RX有空闲高电平但IDLE中断从不触发NVIC未使能IDLE中断检查HAL_UART_MspInit()中是否调用__HAL_UART_ENABLE_IT(huartx, UART_IT_IDLE)手动添加该行代码IDLE中断偶发触发频率远低于7msRX引脚存在弱上拉/下拉用万用表测RX对地电阻正常应为浮空1MΩ移除PCB上误加的10kΩ上拉电阻IDLE中断频繁触发每帧触发2次串口线接触不良或噪声大用示波器看RX波形检查空闲期是否有毛刺加100pF滤波电容更换屏蔽线IDLE中断触发但READ_REG(huartx.Instance-SR)读不到IDLE标志USART外设时钟未开启检查RCC-APB2ENR寄存器确认USARTxEN位为1在MX_GPIO_Init()前调用__HAL_RCC_USARTx_CLK_ENABLE()IDLE中断触发但READ_REG(huartx.Instance-DR)读出0xFFRX引脚悬空或反相器损坏测反相器输入输出电压正常应为输入高3.3V输出低0V更换SN74LVC1G04芯片最隐蔽的问题是第五种。我曾花两天时间排查示波器显示RX波形完美IDLE中断也正常触发但DR寄存器总是0xFF。最终发现是反相器的VCC引脚虚焊导致输出恒为高阻态MCU的RX内部上拉电阻将其拉高DR读出全1。用热风枪重焊VCC引脚后问题消失。5.2 通道值跳变与错位的根因分析通道值跳变通常不是软件bug而是硬件同步问题。典型场景遥控器刚上电时第一帧数据不完整只有前10字节状态机在WAIT_SYNC状态收到0x0F开始接收但后续14字节是下一帧的开头导致解包错位。此时16路通道值会出现“阶梯式跳变”前几路正常中间几路是上一帧的高位后几路是下一帧的低位。解决方案是增加“同步确认”机制状态机在RECV_PAYLOAD状态不直接处理而是缓存最近3帧数据只有当连续3帧的帧尾都是0x00且帧头都是0x0F时才认为同步成功开始提交数据。代码实现很简单static uint8_t sync_counter 0; // 在VERIFY_FRAME状态中 if (frame[24] 0x00 frame[0] 0x0F) { sync_counter; if (sync_counter 3) { // 同步成功提交数据 submit_sbus_data(); sync_counter 0; // 重置计数器 } } else { sync_counter 0; // 同步失败重置 }这个机制牺牲了首帧响应时间约21ms但换来100%的同步可靠性。在飞控应用中21ms的延迟完全可以接受毕竟人类操作响应时间在100ms量级。5.3 HAL库常见陷阱与避坑指南HAL库的坑往往藏在函数文档的角落里。以下是三个血泪教训陷阱一HAL_UARTEx_ReceiveToIdle_IT()的缓冲区大小限制该函数要求缓冲区大小必须≥帧长但实际测试发现当缓冲区25字节时DMA的NDTR寄存器在IDLE触发时可能还未减到0导致total_received计算错误。必须留足余量256字节是经过验证的安全值。陷阱二HAL_UART_IRQHandler()会清IDLE标志如果你在HAL_UART_IRQHandler()里调用HAL_UART_IRQHandler(huartx)它内部会执行__HAL_UART_CLEAR_IDLEFLAG()这会把IDLE标志清掉导致你的自定义ISR收不到中断。解决方案是绝对不要调用HAL_UART_IRQHandler()自己写裸ISR只处理IDLE事件。陷阱三DMA缓冲区地址必须字节对齐huartx.pRxBuffPtr必须是uint8_t类型指针且地址必须是4字节对齐虽然SBUS是字节流但DMA控制器要求。如果用malloc()动态分配可能返回非对齐地址。务必用静态数组或__align(4)修饰uint8_t __align(4) sbus_rx_buffer[256];最后分享一个小技巧在调试时把sbus_data.channel[0]映射到一个GPIO引脚用示波器看其电平变化。正常情况下摇杆满行程时channel[0]从0线性变化到2047对应引脚PWM占空比从0%到100%。如果看到跳变或平台说明解包逻辑有问题如果看到缓慢漂移说明电源或参考电压不稳。这个方法比串口打印快10倍是定位实时性问题的利器。
返回列表