ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 DMA+IDLE中断解析SBUS协议:低CPU占用的16通道遥控数据接收方案

STM32 DMA+IDLE中断解析SBUS协议:低CPU占用的16通道遥控数据接收方案 1. 项目缘起与整体方案选型SBUS 是遥控接收机领域非常常见的一种串行总线协议玩航模、做机器人、搞飞控的朋友基本都绕不开它。它本质上是一路反相串口信号波特率 100000、8 位数据位、偶校验、2 位停止位一帧 25 字节每帧携带 16 个通道的遥控数据外加 2 个标志位。听起来不复杂但真到 STM32 上用 HAL 库把它稳定跑起来坑其实不少。我这次做的项目核心目标就一句话让 STM32 用最省 CPU 的方式把 SBUS 接收机发来的数据实时、稳定地解析成 16 个通道值并且能直接喂给后续的控制逻辑。为什么强调“最省 CPU”因为 SBUS 是 100000 波特率、每 14ms 左右来一帧如果每来一个字节就进一次中断去处理CPU 会被频繁打断稍微复杂一点的主循环就会被拖慢。所以方案上我直接锁定了DMA 循环接收 串口 IDLE 空闲中断 状态机解析这套组合拳。先说 DMA 循环接收。串口 DMA 接收有两种常见模式普通模式和循环模式。普通模式收完指定长度就停了需要手动重启循环模式则是缓冲区收满自动回到开头继续收硬件层面不需要软件干预。SBUS 是连续不断的数据流用循环模式最合适缓冲区就像一个环形传送带数据源源不断往里放我们只需要在合适的时机去“取货”就行。再说 IDLE 空闲中断。串口在接收完一帧数据后总线会空闲一段时间这时候硬件会触发 IDLE 中断。这个中断的好处是它标志着一帧数据的自然结束。SBUS 每帧 25 字节帧与帧之间有明确的空闲间隔用 IDLE 中断来判定“这一帧收完了”非常精准比用定时器超时判断要干净得多。最后是状态机解析。DMA 把原始字节丢进缓冲区IDLE 中断告诉我们“有一帧到了”但缓冲区里的数据不一定刚好从帧头开始。因为 DMA 是循环的你读的时候可能读到的是上一帧的尾巴加下一帧的开头。所以需要一个状态机逐字节扫描找到帧头 0x0F然后连续取 25 个字节校验、解析、输出。状态机的好处是逻辑清晰、容错性强即使中间丢字节或者错位也能自动重新同步。这套方案的优势很明显CPU 占用极低DMA 搬运数据不占 CPUIDLE 中断每帧才触发一次状态机在主循环里跑或者放在中断里跑都行。实测下来主频 72MHz 的 F103 跑这套逻辑CPU 占用不到 2%非常稳。提示SBUS 信号是反相的硬件上需要加一个反相电路比如三极管或者非门或者用串口的反相功能部分 STM32 型号支持 TX/RX 引脚互换和反相配置。软件层面无法直接处理反相信号这一点新手很容易忽略。适合谁来参考这篇内容如果你正在做航模接收机解析、机器人遥控、云台控制、或者任何需要从 SBUS 接收机取通道值的项目这套方案可以直接抄作业。即使你用的是其他协议比如 CRSF、PPM思路也是相通的。下面我会从硬件连接到代码实现再到调试避坑一步步拆开讲。2. 硬件连接与 CubeMX 基础配置2.1 硬件层面的反相与电平匹配SBUS 接收机输出的信号是反相的也就是说空闲时是高电平起始位是低电平跟标准串口正好相反。所以你不能把接收机的信号线直接接到 STM32 的 RX 引脚上必须加反相。常见的做法有两种一种是用一个 NPN 三极管搭一个共射极反相电路基极接接收机信号集电极接 STM32 的 RX发射极接地集电极再通过一个上拉电阻接到 3.3V另一种是直接用 74HC14 这类施密特反相器一片芯片搞定波形还更干净。我个人的习惯是用三极管方案成本低、随手就能搭。但要注意三极管的开关速度SBUS 波特率 100000位宽 10us普通 2N3904 或者 S8050 都能胜任。上拉电阻选 4.7k 到 10k 都行太小了功耗高太大了上升沿变缓。实测 10k 上拉在 100000 波特率下波形依然很干净。电平方面SBUS 接收机一般是 3.3V 或者 5V 供电信号电平也对应。STM32 的 IO 是 3.3V 容忍的如果接收机输出 5V 电平直接接上去短时间可能没事但长期跑建议加一个电平转换或者至少串一个 1k 电阻限流。我自己的接收机是 3.3V 输出的所以直接接省事。2.2 CubeMX 串口与 DMA 配置打开 CubeMX选好你的芯片型号我这次用的是 STM32F103C8T6也就是常说的蓝板。串口方面SBUS 需要配置成波特率100000数据位8 位校验偶校验Even停止位2 位模式RX Only只接收不需要发送这里有个细节HAL 库的串口配置里校验位和停止位的组合要选对。在 CubeMX 的 USART 配置界面Parity 选 EvenStop Bits 选 2Word Length 会自动变成 9 位因为偶校验占一位。这是正常的HAL 库底层会处理。DMA 配置方面在 USART 的 DMA Settings 里点 Add选 USART_RX模式选 Circular循环模式数据宽度选 Byte字节。优先级默认 Medium 就行SBUS 数据量不大不需要太高优先级。中断方面在 NVIC Settings 里勾选 USART 全局中断这样 IDLE 中断才能触发。注意DMA 循环模式下缓冲区大小建议设为 50 或者 64 字节至少是 SBUS 帧长 25 字节的两倍。这样即使你处理不及时也不会被下一帧数据覆盖。我一般设 64留足余量。配置完生成代码HAL 库会自动帮你初始化串口和 DMA。但要注意生成的代码里默认只启动了 DMA 接收没有开启 IDLE 中断。你需要手动在初始化后加上开启 IDLE 中断的代码这个后面会讲。2.3 时钟树与串口波特率误差STM32F103 的串口波特率是由 APB 总线时钟分频得到的。CubeMX 的时钟树配置里USART1 挂在 APB2 上默认 72MHz。100000 波特率的分频系数是 72000000 / 100000 720这个数值很整误差几乎为零。但如果你用的是其他型号比如 F407APB 时钟可能是 84MHz84000000 / 100000 840也很整。所以 SBUS 的波特率在 STM32 上很容易做准不用担心误差累积导致帧错位。不过有一点要留意如果你在 CubeMX 里改了系统时钟一定要回头检查串口的实际波特率。我有一次把主频从 72MHz 超到 128MHz忘了改串口配置结果 SBUS 数据全是乱码排查了半天才发现是波特率偏了。所以时钟树改完串口配置一定要复核。3. DMA 循环接收与 IDLE 中断的代码实现3.1 启动 DMA 接收与 IDLE 中断CubeMX 生成的代码里MX_USART1_UART_Init()会初始化串口但 DMA 接收需要你手动启动。通常我会在main()函数里初始化完成后加上uint8_t sbus_rx_buf[64]; // 启动 DMA 循环接收 HAL_UART_Receive_DMA(huart1, sbus_rx_buf, sizeof(sbus_rx_buf)); // 开启 IDLE 中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);这两行代码是整套方案的核心。HAL_UART_Receive_DMA启动后DMA 就会自动把串口收到的数据搬到sbus_rx_buf里循环模式下一圈接一圈不需要软件干预。__HAL_UART_ENABLE_IT则是打开 IDLE 中断让硬件在总线空闲时通知我们。这里有个 HAL 库的坑HAL_UART_Receive_DMA在循环模式下如果缓冲区满了HAL 库的状态机会变成HAL_UART_STATE_BUSY_RX但它不会自动重启。不过循环模式的 DMA 硬件层面是自动回绕的所以数据其实还在继续搬只是 HAL 库的状态标志没更新。如果你在 IDLE 中断里调用HAL_UART_DMAStop再重启反而会打断数据流。所以我的做法是启动一次永不停止IDLE 中断里只读数据不碰 DMA 配置。3.2 IDLE 中断服务函数的写法IDLE 中断的处理是整个方案的关键。HAL 库默认的中断服务函数USART1_IRQHandler会调用HAL_UART_IRQHandler但这个函数不处理 IDLE 中断。所以你需要自己重写USART1_IRQHandler在里面判断 IDLE 标志并处理。void USART1_IRQHandler(void) { // 判断 IDLE 中断 if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清除 IDLE 标志 // 计算当前 DMA 写指针位置 uint16_t dma_write_ptr sizeof(sbus_rx_buf) - __HAL_DMA_GET_COUNTER(huart1.hdmarx); // 处理数据具体逻辑后面讲 sbus_idle_handler(dma_write_ptr); } HAL_UART_IRQHandler(huart1); // 交给 HAL 处理其他中断 }这里有几个细节要说明。第一__HAL_UART_CLEAR_IDLEFLAG是必须的不清标志会反复进中断。第二__HAL_DMA_GET_COUNTER返回的是 DMA 剩余未搬运的字节数用缓冲区总大小减去它就得到当前 DMA 写到了哪个位置。这个位置是“最新数据”的边界但注意它是循环的可能已经绕了好几圈。第三HAL_UART_IRQHandler要放在最后调用让它处理接收完成、错误等其他中断。如果你不需要 HAL 的其他处理也可以不调但建议保留避免遗漏错误标志。提示IDLE 中断里不要做太耗时的操作。我一般只记录 DMA 写指针和设置一个标志位真正的解析放在主循环里做。这样中断响应快不会丢帧。3.3 环形缓冲区的读写指针管理DMA 循环接收的缓冲区本质上是一个环形缓冲区DMA 是生产者我们的解析代码是消费者。生产者和消费者之间需要一个读指针来记录我们处理到哪里了。static uint16_t sbus_read_ptr 0; // 消费者读指针 volatile uint16_t sbus_write_ptr 0; // 生产者写指针DMA 位置 volatile uint8_t sbus_frame_ready 0; // 帧就绪标志在 IDLE 中断里更新sbus_write_ptr并置位sbus_frame_ready。在主循环里检查sbus_frame_ready如果置位就从sbus_read_ptr开始逐字节扫描直到sbus_write_ptr。扫描过程中如果找到完整帧就解析并输出通道值然后更新sbus_read_ptr到帧尾的下一个位置。这里要注意环形回绕的处理。当sbus_write_ptr小于sbus_read_ptr时说明 DMA 已经绕回缓冲区开头了数据分两段从sbus_read_ptr到缓冲区末尾再从缓冲区开头到sbus_write_ptr。扫描的时候要分两段处理或者用一个临时缓冲区把数据拼起来。我一般用分两段的方式省内存。void sbus_process(void) { if (!sbus_frame_ready) return; sbus_frame_ready 0; uint16_t write_ptr sbus_write_ptr; uint16_t read_ptr sbus_read_ptr; while (read_ptr ! write_ptr) { uint8_t byte sbus_rx_buf[read_ptr]; sbus_state_machine(byte); // 状态机逐字节处理 read_ptr (read_ptr 1) % sizeof(sbus_rx_buf); } sbus_read_ptr read_ptr; }这段代码是消费者逻辑的核心。while循环从读指针走到写指针每个字节喂给状态机。状态机负责找帧头、收帧、校验、解析。这样即使 DMA 绕了好几圈我们也能按顺序处理每个字节不会漏数据。注意sbus_write_ptr和sbus_frame_ready是中断和主循环共享的变量必须加volatile修饰防止编译器优化导致读不到最新值。另外如果主循环处理速度跟不上 DMA 写入速度读指针会永远追不上写指针数据会被覆盖。SBUS 每 14ms 一帧25 字节平均每 560us 一个字节主循环处理 25 个字节的状态机扫描通常不到 10us所以完全跟得上。4. SBUS 协议状态机解析与通道值提取4.1 SBUS 帧格式逐字节拆解SBUS 一帧 25 字节结构如下字节位置内容说明00x0F帧头固定值1-22通道数据16 个通道每个 11 位共 22 字节23标志位bit0丢帧标志bit1失效保护标志240x00帧尾固定值部分接收机为 0x04 或其他通道数据的打包方式比较特别16 个通道每个通道 11 位总共 176 位正好 22 字节。打包顺序是低位在前跨字节拼接。比如通道 1 是字节 1 的 bit0-7 加上字节 2 的 bit0-2共 11 位。通道 2 是字节 2 的 bit3-7 加上字节 3 的 bit0-5以此类推。这种打包方式用代码解析的时候最直接的方法是用一个 32 位的移位寄存器把 22 字节依次移入然后每 11 位取一次。但这样效率不高而且容易出错。我习惯用查表加移位的方式逐通道提取。static uint16_t sbus_channels[16]; void sbus_decode(uint8_t *frame) { sbus_channels[0] ((uint16_t)frame[1] | ((uint16_t)frame[2] 8)) 0x07FF; sbus_channels[1] ((uint16_t)frame[2] 3 | ((uint16_t)frame[3] 5)) 0x07FF; sbus_channels[2] ((uint16_t)frame[3] 6 | ((uint16_t)frame[4] 2) | ((uint16_t)frame[5] 10)) 0x07FF; // ... 依次类推 }手动写 16 个通道的提取表达式容易出错我建议用一个循环加移位的方式void sbus_decode(uint8_t *frame) { uint32_t bitbuf 0; uint8_t bitcount 0; uint8_t ch 0; for (int i 1; i 22; i) { bitbuf | ((uint32_t)frame[i]) bitcount; bitcount 8; while (bitcount 11 ch 16) { sbus_channels[ch] bitbuf 0x07FF; bitbuf 11; bitcount - 11; } } }这个循环的逻辑是把字节逐个拼进一个 32 位缓冲区每当缓冲区里有 11 位以上就取最低 11 位作为一个通道值然后右移。这样写简洁、不易出错而且效率也不错。实测在 72MHz 的 F103 上解析一帧 25 字节不到 5us。4.2 状态机的设计与实现状态机的作用是从连续的字节流中识别出完整的 SBUS 帧。因为 DMA 缓冲区是环形的我们读到的数据可能从帧中间开始也可能包含多个帧。状态机需要能自动同步到帧头并且在帧尾校验失败时重新寻找帧头。我设计的状态机有三个状态等待帧头、接收数据、校验帧尾。typedef enum { SBUS_STATE_WAIT_HEADER, SBUS_STATE_RECEIVING, SBUS_STATE_CHECK_FOOTER } sbus_state_t; static sbus_state_t sbus_state SBUS_STATE_WAIT_HEADER; static uint8_t sbus_frame[25]; static uint8_t sbus_index 0; void sbus_state_machine(uint8_t byte) { switch (sbus_state) { case SBUS_STATE_WAIT_HEADER: if (byte 0x0F) { sbus_frame[0] byte; sbus_index 1; sbus_state SBUS_STATE_RECEIVING; } break; case SBUS_STATE_RECEIVING: sbus_frame[sbus_index] byte; if (sbus_index 25) { sbus_state SBUS_STATE_CHECK_FOOTER; } break; case SBUS_STATE_CHECK_FOOTER: if (sbus_frame[24] 0x00 || sbus_frame[24] 0x04) { sbus_decode(sbus_frame); sbus_update_flags(sbus_frame[23]); } sbus_state SBUS_STATE_WAIT_HEADER; break; } }这个状态机的逻辑很直白等待 0x0F收到后开始收 25 字节收满后检查帧尾。帧尾合法就解析不合法就丢弃重新等待帧头。这样即使中间错位最多丢一帧下一帧就能重新同步。提示有些接收机的帧尾不是 0x00而是 0x04 或者其他值。如果你发现解析一直失败先用逻辑分析仪或者串口助手抓一下原始数据确认帧尾到底是什么。我遇到过一款接收机帧尾是 0x04排查了好久。4.3 通道值范围映射与失效保护处理SBUS 的通道值原始范围是 172 到 1811对应遥控器的 1000us 到 2000us 脉宽。中间值 992 左右对应 1500us。实际使用的时候通常需要把原始值映射到更直观的范围比如 -100 到 100或者 0 到 1000。int16_t sbus_channel_to_percent(uint16_t raw) { // 原始范围 172-1811映射到 -100 到 100 int32_t val ((int32_t)raw - 992) * 100 / 819; if (val 100) val 100; if (val -100) val -100; return (int16_t)val; }这里的 992 是中点819 是半量程1811 - 992 819。映射后摇杆中位对应 0满舵对应 ±100。这个映射不是必须的你可以直接用原始值也可以映射到其他范围看你的控制逻辑需要。失效保护方面SBUS 帧的第 23 字节有两个标志位bit0 是丢帧标志bit1 是失效保护标志。丢帧标志表示接收机没有收到发射机的信号失效保护标志表示接收机进入了失效保护模式通常是输出预设的安全值。这两个标志要单独提取出来在控制逻辑里做相应处理。static uint8_t sbus_frame_lost 0; static uint8_t sbus_failsafe 0; void sbus_update_flags(uint8_t flags) { sbus_frame_lost flags 0x01; sbus_failsafe (flags 1) 0x01; }实际项目中如果sbus_frame_lost或者sbus_failsafe置位我一般会让控制逻辑进入安全状态比如电机停转、舵机回中。这个处理因项目而异但标志位一定要提取出来不能忽略。5. 调试过程中踩过的坑与排查技巧5.1 数据全是乱码的几种可能SBUS 调试最常见的问题就是数据全是乱码串口助手打出来一堆看不懂的字节。这种情况我遇到过好几次原因无非以下几种第一种反相电路没接或者接反了。SBUS 信号是反相的如果你直接接到 RX 引脚收到的数据就是反的帧头 0x0F 会变成 0xF0整个帧都乱了。用示波器或者逻辑分析仪看一下 RX 引脚的波形空闲时应该是高电平起始位是低电平。如果空闲时是低电平说明反相没做对。第二种波特率不对。SBUS 是 100000 波特率不是常见的 9600 或者 115200。如果你在 CubeMX 里设错了波特率收到的数据会全是乱码。检查一下串口配置确保波特率是 100000校验是偶校验停止位是 2。第三种校验位和停止位配置错误。SBUS 是 8 位数据、偶校验、2 位停止位。如果你设成了无校验或者 1 位停止位数据会错位。CubeMX 里 Word Length 会自动变成 9 位这是正常的因为偶校验占一位。第四种DMA 缓冲区太小或者没启动。如果 DMA 没启动或者缓冲区设得太小数据会丢。检查HAL_UART_Receive_DMA是否被调用缓冲区大小是否至少 50 字节。5.2 IDLE 中断不触发的排查思路IDLE 中断不触发通常有以下几个原因第一IDLE 中断没使能。CubeMX 里 NVIC 勾选了 USART 全局中断但 IDLE 中断是单独使能的需要在代码里调用__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE)。如果你忘了这一句IDLE 中断永远不会触发。第二中断服务函数没重写。HAL 库默认的USART1_IRQHandler会调用HAL_UART_IRQHandler但这个函数不处理 IDLE 中断。你需要自己重写USART1_IRQHandler在里面判断 IDLE 标志。如果你没重写IDLE 中断虽然触发了但被 HAL 库忽略掉了。第三IDLE 标志没清除。IDLE 标志是“读后清除”的如果你在中断里没有调用__HAL_UART_CLEAR_IDLEFLAG中断会反复触发但实际上你只处理了一次。更糟糕的是如果标志一直不清后续的 IDLE 中断可能被屏蔽。第四DMA 没启动。IDLE 中断是在串口收到数据后触发的如果 DMA 没启动串口收不到数据自然也不会触发 IDLE 中断。检查HAL_UART_Receive_DMA是否被调用。5.3 帧错位与数据覆盖的解决帧错位是环形缓冲区方案里比较隐蔽的问题。现象是偶尔解析出一帧正确数据但大部分时候通道值跳变或者全是零。原因通常是读指针和写指针的同步出了问题。我遇到过一次读指针在扫描的时候DMA 又写入了新数据导致读到的数据一半是旧帧一半是新帧。解决办法是在扫描之前先把sbus_write_ptr保存到一个局部变量扫描过程中只用这个局部变量作为边界不再读全局的sbus_write_ptr。这样即使 DMA 在扫描过程中写入了新数据也不会影响本次扫描的边界。uint16_t write_ptr sbus_write_ptr; // 保存边界 uint16_t read_ptr sbus_read_ptr; while (read_ptr ! write_ptr) { // 扫描逻辑 }另一个问题是数据覆盖。如果主循环处理太慢读指针追不上写指针DMA 会把还没处理的数据覆盖掉。SBUS 每 14ms 一帧25 字节主循环处理一帧通常不到 10us所以正常情况下不会覆盖。但如果你的主循环里有耗时操作比如 LCD 刷新、SD 卡写入可能会导致处理延迟。解决办法是把 SBUS 处理放在主循环的最前面或者放在一个高优先级的任务里确保每帧都能及时处理。5.4 常见问题速查表现象可能原因排查方法数据全是乱码反相电路没接或接反示波器看 RX 引脚空闲电平数据全是乱码波特率不对检查串口配置是否为 100000数据全是乱码校验位/停止位错误检查是否为偶校验、2 停止位IDLE 中断不触发IDLE 中断没使能检查是否调用__HAL_UART_ENABLE_ITIDLE 中断不触发中断服务函数没重写检查是否重写USART1_IRQHandlerIDLE 中断反复触发IDLE 标志没清除检查是否调用__HAL_UART_CLEAR_IDLEFLAG通道值跳变帧错位检查读指针和写指针的同步逻辑通道值全为零帧尾校验失败抓原始数据确认帧尾值偶尔丢帧主循环处理太慢把 SBUS 处理放在主循环最前面提示调试 SBUS 的时候逻辑分析仪比串口助手好用得多。串口助手只能看解析后的数据逻辑分析仪能看原始波形一眼就能看出反相、波特率、帧间隔是否正常。我用的是一款几十块钱的 8 通道逻辑分析仪配合开源软件抓 SBUS 波形非常方便。6. 性能优化与扩展思路6.1 CPU 占用实测与优化空间这套方案在 STM32F103C8T672MHz上实测CPU 占用不到 2%。具体来说DMA 搬运数据不占 CPUIDLE 中断每 14ms 触发一次中断服务函数里只做指针更新和标志置位耗时不到 1us。主循环里的状态机扫描每帧 25 字节耗时不到 5us。所以平均 CPU 占用 (1us 5us) / 14ms ≈ 0.04%加上主循环其他逻辑整体不到 2%。如果你觉得还不够省可以把状态机扫描也放到 IDLE 中断里做。这样主循环完全不用管 SBUS只需要读解析好的通道值。但要注意中断里做太多事情会增加中断响应时间可能影响其他中断的实时性。我一般不建议这么做除非你的主循环特别忙。另一个优化点是 DMA 缓冲区大小。缓冲区越大抗抖动能力越强但内存占用也越大。64 字节已经足够再大就是浪费。如果你用的是 F4 或者 F7 系列内存充足可以设 128 或者 256但意义不大。6.2 多路 SBUS 接收的实现有些项目需要同时接收多路 SBUS 信号比如双接收机冗余、或者多个遥控器。这时候可以用多个串口每个串口配一个 DMA 和一个 IDLE 中断。STM32F103 有 3 个 USART 和 2 个 UART足够接 3 到 5 路 SBUS。多路接收的代码结构和单路一样只是每个串口有独立的缓冲区、状态机和通道数组。中断服务函数要分别重写比如USART1_IRQHandler、USART2_IRQHandler、USART3_IRQHandler。状态机可以用同一个函数传入不同的缓冲区指针和状态变量。typedef struct { uint8_t rx_buf[64]; uint16_t read_ptr; volatile uint16_t write_ptr; volatile uint8_t frame_ready; sbus_state_t state; uint8_t frame[25]; uint8_t index; uint16_t channels[16]; uint8_t frame_lost; uint8_t failsafe; } sbus_instance_t; sbus_instance_t sbus1, sbus2;用结构体把每个实例的状态打包状态机函数接收sbus_instance_t *指针这样代码复用度高扩展方便。我做过一个双接收机冗余的项目两路 SBUS 同时接收主控根据信号质量自动切换用的就是这个结构。6.3 与飞控/控制逻辑的对接SBUS 解析出来的通道值最终要喂给控制逻辑。对接的时候有几点要注意第一通道值的更新频率。SBUS 每 14ms 更新一次控制逻辑的周期如果是 1ms那通道值在两次更新之间是不变的。这没问题但如果你做的是高动态控制比如穿越机14ms 的延迟可能有点大。这时候可以考虑用更高速的协议比如 CRSF或者用多个接收机做预测。第二失效保护的处理。如果sbus_frame_lost或者sbus_failsafe置位控制逻辑要立即进入安全状态。我一般会在控制逻辑的最前面检查这两个标志如果置位直接输出安全值跳过正常控制。第三通道值的死区处理。遥控器摇杆在中位附近通常有轻微抖动如果直接映射到 -100 到 100会有小幅度跳变。可以在映射后加一个死区比如 ±3 以内归零。这个死区大小因遥控器而异我一般设 3 到 5。int16_t apply_deadzone(int16_t val, int16_t deadzone) { if (val -deadzone val deadzone) return 0; return val; }第四通道值的滤波。如果遥控器信号有噪声可以在软件里加一个简单的低通滤波或者滑动平均。但要注意滤波会引入延迟对于需要快速响应的通道比如油门、副翼延迟越小越好。我一般只对辅助通道比如起落架、灯光做滤波主控制通道不滤波。6.4 从 SBUS 到其他协议的迁移这套 DMA IDLE 状态机的框架其实不限于 SBUS。任何串口协议只要帧长固定或者有明确的帧头帧尾都可以用类似的思路解析。比如CRSF波特率 420000帧头 0xC8帧长可变有 CRC 校验。状态机需要增加长度字段和 CRC 校验。PPM不是串口协议是脉宽调制需要用定时器捕获。但思路类似都是状态机加缓冲。MAVLink帧头 0xFE 或 0xFD帧长可变有 CRC 校验。状态机需要处理长度和校验。迁移的时候主要改的是状态机的帧格式判断和解析函数DMA 和 IDLE 中断的框架不用动。我做过一个项目同时解析 SBUS 和 CRSF两路串口各跑各的状态机代码结构非常清晰。提示如果你要从 SBUS 迁移到 CRSF注意 CRSF 的波特率是 420000比 SBUS 高很多。DMA 缓冲区要相应加大IDLE 中断的频率也会更高。STM32F103 的串口在 420000 波特率下依然能稳定工作但要注意 DMA 的搬运速度是否跟得上。实测 F103 在 420000 波特率下DMA 循环接收没问题CPU 占用依然很低。6.5 实测数据与稳定性验证最后分享一下我的实测数据。用 STM32F103C8T6主频 72MHzSBUS 接收机是某品牌的 8 通道接收机发射机是某品牌遥控器。测试环境是室内距离 5 米无遮挡。连续运行 72 小时解析了大约 1850 万帧 SBUS 数据丢帧率低于 0.01%。丢帧主要发生在遥控器关机或者信号遮挡的时候属于正常现象。CPU 占用稳定在 1.5% 到 2% 之间内存占用方面DMA 缓冲区 64 字节状态机帧缓冲 25 字节通道数组 32 字节总共不到 150 字节对 F103 的 20KB RAM 来说微不足道。稳定性方面我做过一个压力测试在主循环里加了一个 10ms 的延时模拟主循环繁忙的情况。结果 SBUS 解析依然正常因为 DMA 缓冲区有 64 字节足够缓存两帧多的数据。但如果延时超过 28ms两帧时间就会开始丢帧。所以主循环的周期最好控制在 14ms 以内确保每帧都能及时处理。这个方案我后来用在了好几个项目上包括一个四轴飞行器的遥控接收、一个履带机器人的遥控控制、还有一个云台的手动控制。每次都是直接复用这套代码改改通道映射就行非常省事。如果你也在做类似的项目建议先把这套框架跑通后面再根据具体需求调整比从头造轮子快得多。
返回列表