ARTICLE DETAIL

资讯详情

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

STM32解析IBUS协议:一根线读取14通道遥控数据

STM32解析IBUS协议:一根线读取14通道遥控数据 玩单片机和航模的朋友应该都有过这种经历手里一堆遥控器、接收机好不容易把PWM信号读进来了一看占用的引脚数量心凉了半截。尤其是做遥控小车、机械臂、船模这类项目既要收遥控通道又要接电机驱动、编码器、显示屏引脚根本不够用。我当年做基于STM32的遥控小车时就被这个问题卡了很久直到换成了富斯i6遥控器加IBUS协议一根信号线把14个通道全部读回来整个项目瞬间清爽了。这篇内容就把我从零搭建富斯i6与STM32之间IBUS通信的完整过程写出来包括协议原理、硬件接线、接收机配置、STM32代码实现、调试方法和踩坑记录。无论你是刚开始玩STM32的新手还是准备做遥控类毕业设计的同学照着操作就能把遥控数据读进单片机跑通自己的第一个遥控项目。1. 项目背景为什么选了富斯i6和IBUS协议先说结论富斯i6FS-i6是航模圈里性价比很高的入门级6通道遥控器两百块以内能拿到手而且支持AFHDS 2A协议配上FS-iA6B接收机之后可以输出PWM、PPM、IBUS、SBUS四种信号玩法非常灵活。IBUS是富斯自家定义的串行总线协议把遥控器所有通道数据打包成一帧通过一根信号线发给接收机再从接收机传给飞控或者单片机。1.1 遥控器与接收机怎么选这套方案的核心设备就是富斯i6遥控器和FS-iA6B接收机两者是配套的。富斯i6有一个容易被忽略的点它的固件版本决定了菜单里有没有IBUS选项。我手上这台出厂固件是1.6版本进接收机设置菜单里压根找不到IBUS后来升级到1.7以上才出现这个选项。如果你买的是二手遥控器建议先确认固件版本不然配置时会一脸懵。接收机方面FS-iA6B是必须的因为它带独立的IBUS/S.Bus输出引脚而老款的FS-iA6只支持PWM和PPM没法直接输出IBUS。1.2 IBUS相对PWM和PPM的优势在哪里很多新手一开始接触的是PWM直连方案接收机每一个通道引出一根信号线接在STM32的定时器输入捕获引脚上用测频法或者测脉宽法把每个通道的PWM脉宽读出来。这样做最大的问题是引脚消耗太大6个通道就要占6个引脚而且每个通道都需要一个定时器通道去捕获代码写起来也啰嗦。PPM虽然能一根线传8个通道但PPM信号本质上是把多个通道的脉宽按时间顺序拼接成一帧模拟信号解析时对定时器输入捕获的精度要求比较高稍有干扰就会出现通道值跳变。IBUS走的是数字串口协议半双工波特率1152008位数据位、无校验、1位停止位也就是常说的115200 8N1。刷新率大约是14ms一帧换算下来约70Hz控制舵机、电机、云台都够用。最关键的是它只有一根信号线接STM32的任意一个串口RX引脚就能收14个通道全在一帧里解析也不难。对比下来IBUS在引脚占用、通道数量、抗干扰能力三个方面优势非常明显。2. IBUS协议拆解帧格式、校验和与实际数值IBUS协议不复杂难在很多人没搞懂它的帧结构和校验算法直接抄代码容易翻车。我这里把协议拆开讲清楚你理解了之后哪怕换一个平台、换一种单片机也能自己写出解析代码。2.1 一帧IBUS数据到底长什么样IBUS的每一帧固定是32个字节不是变长的。帧结构如下字节偏移内容说明00x20固定帧头10x40固定标志可以理解为通道数识别码2-2914个通道数据每通道2字节小端模式低字节在前30校验和低字节checksum 0xFF31校验和高字节checksum 8这里的“小端模式”是很多新手容易搞错的地方。每个通道值占用两个字节比如第一个通道的值是0x05DC对应的十进制是1500在帧里的存储顺序是字节2存0xDC低位字节3存0x05高位也就是“低字节在前”。读取时必须组装成ch1 frame[2] | (frame[3] 8)如果写成ch1 frame[3] | (frame[2] 8)数值就会完全不对。为什么要固定32字节因为IBUS总线上不仅接收机会发数据飞控也可能给接收机回传数据固定的帧长让接收端可以很轻松地用状态机或者DMA加空闲中断来切帧。每次收到一帧完整的32字节就是一个完整的控制周期。2.2 校验和的计算方法IBUS的校验算法说穿了很简单把第0到第29字节也就是帧头、标志位和14个通道数据全部累加再用0xFFFF减去这个累加和得到的结果就是校验值。计算方式是uint16_t ibus_checksum(uint8_t *buf) { uint16_t sum 0; for (int i 0; i 30; i) { sum buf[i]; } return 0xFFFF - sum; }计算出来的校验值低字节存在帧的第30字节高字节存在第31字节。解析时用同样的方法重新计算一遍然后和帧里最后两个字节对比uint16_t checksum_recv frame[30] | (frame[31] 8); uint16_t checksum_calc ibus_checksum(frame); if (checksum_recv checksum_calc) { // 校验通过 }校验算法本身不复杂但它起到了两个作用一是验证这一帧在传输过程中有没有被干扰二是帮你确认帧头找对了。如果经常出现校验失败先别怀疑代码检查一下接线和共地问题后面我会专门讲。2.3 通道值和摇杆位置的对应关系IBUS协议里每个通道的数值范围大致是1000到2000中位在1500左右。这个数值对应的是遥控器摇杆的位置和PWM脉宽的微秒数刚好一致只是用数字方式传输而已。富斯i6默认情况下油门摇杆在中位时输出约1500推到最高约2000拉到最低约1000。方向舵、升降舵、副翼同理。但要注意遥控器本身可以设置通道反向和行程量如果发现通道数值反了或者范围偏小可以在遥控器的“通道设置”菜单里调整。IBUS帧里通道顺序和遥控器上的通道编号是严格对应的第1个通道对应遥控器CH1第2个通道对应CH2以此类推前4个通道通常默认分配给副翼、升降舵、油门、方向舵。如果做小车一般把油门接到CH3方向接到CH1这个看个人习惯。3. 硬件准备与接线没那么玄乎但细节决定成败这部分的坑比代码还要多。硬件接线错了要么串口收不到数据要么收到一堆乱码最难受的是收数据时好时坏这就大概率是接线或者供电的问题。3.1 需要准备哪些材料我的硬件清单如下都是很常见的物料某宝随便买富斯i6遥控器一个FS-iA6B接收机一个STM32F103C8T6最小系统板一块也就是大家常说的“蓝板”ST-Link V2下载器一个用于烧录程序USB转TTL模块一个用来把STM32的串口数据打印到电脑上调试杜邦线若干舵机或者带驱动板的直流电机用于验证控制效果STM32F103C8T6的串口资源足够我用的是USART1对应的引脚是PA9TX和PA10RX。IBUS接收机输出只需要接PA10PA9留给调试打印。3.2 接收机接线和引脚规划FS-iA6B接收机上除了CH1到CH6的PWM输出排针之外还有一个独立的接口上面标注了“I-BUS/S.Bus”这个接口通常有三根线红色是5V电源黑色或棕色是GND白色或黄色是信号线。接线表如下FS-iA6B接收机STM32F103C8T6说明红色线VCC5V引脚给接收机供电黑色线GNDGND必须共地白色线I-BUSPA10USART1_RXIBUS信号线这里最容易被忽视的是共地。接收机的GND和STM32的GND必须连在一起否则串口信号没有统一的参考电平数据必然乱码严重时甚至可能损坏引脚。我曾经在一次调试中把GND给忘了结果串口读回来的数据全是0xFF、0x00这种毫无规律的值排查了半天才发现是共地问题。除了接线电调或者舵机的供电也要留意。FS-iA6B接收机工作电压一般是4到6V如果直接用STM32板载的5V引脚供电注意这个5V是从USB来的还是从稳压芯片来的电流不够的话会导致接收机重启或者输出异常。如果后面接了舵机最好单独给舵机供电接收机只取信号和共地不要把所有负载都压在STM32的5V上。3.3 富斯i6遥控器菜单怎么配置遥控器和接收机需要对频这一步一般在出厂时商家已经帮你做完了但如果你买的是二手设备或者换了新接收机就需要重新对频。对频方法遥控器关机状态下按住“BIND”键在遥控器背面再开机然后给接收机上电接收机上的LED灯会闪烁过几秒变成常亮说明对频成功。接下来设置IBUS输出模式。开机后长按“OK”键进入菜单依次选择“System Setup”-“RX Setup”在里面找到接收机输出方式把选项从PWM或者PPM切换为IBUS。设置完成后接收机重新上电IBUS信号就开始在白色信号线上输出了。我特别提醒一下如果你的遥控器菜单里根本没有RX Setup这一项或者里面没有IBUS选项大概率是固件版本太旧。富斯官网有升级工具需要用一个USB线连接遥控器背后的USB口接到电脑上升级固件升到1.7版本以上就有了。这个操作不难但是需要留意升级过程中不要断电。4. STM32端代码实现从串口中断到协议解析代码是整套方案的核心我直接用标准库来写因为网上很多STM32教程还是以标准库为主而且代码看起来更直观。HAL库的思路是一样的只是初始化函数名不同照着逻辑迁移过去不难。4.1 串口接收的两种方案对比接收IBUS数据的核心是单片机串口每收到一个字节就要把它存进缓冲区凑够32个字节后进行解析。最简单的实现是用串口接收中断每进一次中断存一个字节。这种方案代码好写但是每个字节都会打断CPU一次115200波特率下大约每86微秒进一次中断对于STM32F103来说完全扛得住只要中断服务函数里不做耗时操作就没问题。另一种更优雅的方案是DMA加串口空闲中断。让DMA自动把串口收到的数据搬运到内存缓冲区CPU完全不用管等一帧数据接收完毕串口会产生一个空闲中断这时在中断里检查DMA收到的字节数如果正好是32字节就解析。这种方案CPU负载更低代码稍微复杂一点。我这里两种方案都会讲新手可以先跑通中断收包版本再尝试DMA版本感受一下区别。4.2 基于串口接收中断的完整代码先定义一个全局的接收结构体#define IBUS_FRAME_LEN 32 volatile uint8_t ibus_rx_buf[IBUS_FRAME_LEN]; // 当前帧缓存 volatile uint8_t ibus_rx_index 0; // 接收字节计数 volatile uint8_t ibus_frame_ready 0; // 一帧接收完成标志串口初始化void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }串口中断服务函数void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); // 找帧头 if (ibus_rx_index 0 data ! 0x20) { return; // 没收到帧头丢弃 } // 第二个字节必须是0x40否则重新同步 if (ibus_rx_index 1 data ! 0x40) { ibus_rx_index 0; return; } ibus_rx_buf[ibus_rx_index] data; if (ibus_rx_index IBUS_FRAME_LEN) { ibus_rx_index 0; ibus_frame_ready 1; } } }这段代码有几个关键点。第一个是找帧头如果第一个字节不是0x20说明当前字节是两帧之间的噪声或者上一帧的残留数据直接扔掉。第二个是检查第二个字节IBUS帧的第二个字节固定是0x40如果不匹配说明帧头找错了需要把索引清零重新同步。第三步才是收满32字节置标志位。你可能会问为什么不在中断里直接解析因为解析涉及循环和校验计算中断里做这些操作会占用较长时间影响其他中断的实时性。正确做法是中断里只负责收数据、置标志位主循环里检测到标志位后再解析。4.3 主循环里的帧解析与校验主循环里的解析代码uint16_t channel_value[14]; uint8_t ibus_parse_frame(void) { uint16_t checksum_calc 0; uint16_t checksum_recv 0; // 计算校验和 for (int i 0; i 30; i) { checksum_calc ibus_rx_buf[i]; } checksum_calc 0xFFFF - checksum_calc; // 帧里存的校验和 checksum_recv ibus_rx_buf[30] | (ibus_rx_buf[31] 8); if (checksum_calc ! checksum_recv) { return 0; // 校验失败 } // 解析14个通道 for (int i 0; i 14; i) { channel_value[i] ibus_rx_buf[2 i * 2] | (ibus_rx_buf[2 i * 2 1] 8); } return 1; }主函数int main(void) { SystemInit(); USART1_Init(); while (1) { if (ibus_frame_ready) { ibus_frame_ready 0; if (ibus_parse_frame()) { // 到这里channel_value[] 就是14个通道的最新值 // 可用于控制舵机、电机等 } } } }这个逻辑已经足够在项目中使用了。但要提醒一点ibus_rx_buf和ibus_frame_ready都是volatile修饰的全局变量中断和主循环共享解析时最好关闭中断保护一下临界区防止主循环正在读数据时中断又在往里面写新帧。简单的做法是解析前临时候一份数据uint8_t temp_buf[IBUS_FRAME_LEN]; if (ibus_frame_ready) { __disable_irq(); memcpy(temp_buf, (uint8_t *)ibus_rx_buf, IBUS_FRAME_LEN); ibus_frame_ready 0; __enable_irq(); // 对temp_buf进行解析 }这样做以后即使新数据到来也不会影响正在解析的帧。4.4 进阶方案DMA加串口空闲中断如果觉得每收一个字节就进一次中断太繁琐可以用DMA搬运数据。思路是把串口1的接收映射到DMA1的通道5DMA自动把收到的字节写入缓冲区。当一帧数据发送完毕串口总线处于空闲状态串口的IDLE标志位置1触发中断这时检查DMA剩余传输量就能知道这一帧收了多少字节。初始化DMA的关键代码#define IBUS_RX_BUF_SIZE 32 uint8_t ibus_dma_buf[IBUS_RX_BUF_SIZE]; void IBUS_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)ibus_dma_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize IBUS_RX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }串口中断里检测空闲标志void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART1); // 读DR清IDLE标志 uint16_t recv_len IBUS_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); if (recv_len IBUS_RX_BUF_SIZE) { ibus_frame_ready 1; } DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, IBUS_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }DMA方案的好处是一帧数据收完才触发一次中断而且DMA在后台搬运数据CPU可以专心干别的活。对于后续要跑PID、控制电机、刷OLED的项目来说这个方案更从容。4.5 通道数据怎么用起来拿到channel_value[14]之后就可以做各种事情了。我建议先把每个通道映射到PWM输出驱动舵机或者电调验证效果这是最直观的测试方式。STM32的定时器可以输出多路PWM比如用TIM2的四个通道输出四路PWM把通道值直接映射到定时器的比较寄存器// 假设TIM2已经配置成PWM输出模式频率50Hz TIM_SetCompare1(TIM2, channel_value[0]); TIM_SetCompare2(TIM2, channel_value[1]); TIM_SetCompare3(TIM2, channel_value[2]); TIM_SetCompare4(TIM2, channel_value[3]);舵机的控制信号就是50Hz的PWM脉宽1000到2000微秒刚好和IBUS通道值对应所以不需要做任何转换。电调也一样前提是电调已经完成油门行程校准。如果不做校准油门通道输出可能导致电机转速异常这一点在航模项目里特别重要。5. 调试验证与常见问题排查这部分是我最想写的因为整个过程中踩过的坑比写代码的时间还长。把这些问题整理成速查表以后遇到类似情况可以少走很多弯路。5.1 第一步用串口助手验证驱动如果你手头有USB转TTL模块建议先不要把接收机直接接到STM32上而是把IBUS信号线接到USB转TTL的RX引脚打开电脑上的串口助手波特率设置成115200观察有没有数据。正常情况下串口助手会以约14ms的间隔收到一帧32字节的数据帧头是0x20 0x40。如果串口助手能稳定收到这样的数据说明遥控器、接收机、接线都没问题剩下的就是STM32端代码的事了。我习惯把原始数据打印到串口助手上看这样能直接判断是协议问题还是接线问题。如果收到的数据里帧头不是0x20 0x40而是乱码先检查波特率、电平、共地这三项。如果数据偶尔丢帧检查一下接收机供电是否稳定特别是用了劣质杜邦线的时候接触不良会导致帧结构被破坏。5.2 常见问题排查速查表现象可能原因解决办法串口完全收不到数据接线错误、接收机没设置IBUS、未共地检查信号线是否接在RX脚确认遥控器RX Setup里选了IBUS确认GND相连收到数据全是0xFF或0x00共地问题或者接收机未上电把接收机和STM32的GND连起来检查5V供电能收到数据但校验总失败帧不同步、干扰、波特率不匹配检查是否从帧头开始解析换质量好一点的杜邦线确认波特率是115200第二个字节不是0x40协议理解错误或者接收机输出不是IBUS确认接收机型号和固件支持IBUS确认遥控器菜单设置正确通道值固定不变遥控器未开、没有对频、通道线序搞错开机对频检查通道顺序在遥控器上推动摇杆观察数值变化通道数值跳变严重供电不足、电磁干扰接收机单独供电信号线远离电机驱动线加一个100nF电容滤波5.3 实际调试中容易被忽略的坑第一个坑是串口空闲中断的清除方式。STM32的IDLE标志位需要用“先读SR再读DR”的方式清除或者直接读一次DR寄存器。我在写DMA方案时没有清IDLE标志导致中断反复进入整个程序卡死在中断里看起来就像死机了一样。这个问题很隐蔽排查了很久才找到。第二个坑是接收机的PPM/IBUS引脚并不是所有固件版本都默认输出IBUS信号。FS-iA6B接收机上那个白色信号线接口既可以输出PPM也可以输出IBUS到底输出什么取决于遥控器里的设置。如果遥控器里没有IBUS选项即使你代码写得再对也收不到IBUS帧。第三个坑是有些STM32板子的串口引脚默认被JTAG占用了。我用的PA13、PA14、PA15就是JTAG引脚如果这几个引脚被当作普通IO使用需要先禁用JTAG功能。好在PA9和PA10不涉及这个问题但如果你用的是SWD下载也要注意PB3、PB4这些特殊引脚。第四个坑是接收机输出的信号电平。FS-iA6B的信号电平是3.3V和STM32F103的IO电平正好匹配可以直接连接。但如果你用的是Arduino Uno这类5V单片机最好做一下电平转换否则长期使用可能损坏接收机。反过来如果接收机是5V输出的型号接到STM32上也需要分压或者电平转换。6. 项目扩展方向与个人经验收尾跑通IBUS通信之后整个项目就打开了新世界的大门。一根线读14个通道意味着你可以轻易地扩展出很多功能用第5通道的开关控制灯用第6通道控制机械臂夹爪用剩余通道做云台控制全都很方便。我后来在这个基础上加了OLED显示屏实时显示各通道值调试上位机的时候非常直观。做一个双路电机驱动的小车时我用CH3做油门、CH1做转向通道值先做了一次中位死区判断再映射到PWM输出。这里有个细节遥控器摇杆回中时通道值不一定刚好是1500可能会有几十微秒的偏差。这个偏差直接控制电机的话会导致小车在摇杆回中时仍然缓慢前进或转向。解决办法是在代码里做校准把回中值记下来使用时减去这个偏移量。如果想把数据范围从1000到2000映射成更通用的0到100可以用map函数思路int16_t map_value(int16_t x, int16_t in_min, int16_t in_max, int16_t out_min, int16_t out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) out_min; }不过要注意遥控器摇杆的物理行程可能达不到全范围也就是推到底可能只有1980而不是2000所以要根据实际读到的最大最小值来校准不要盲信协议里的1000到2000。还有一个好用的扩展方向是无线回传也就是微控制器反向给接收机发数据通过遥控器屏幕显示电池电压、转速等信息。这部分需要用到IBUS的回传协议帧结构类似但是数据含义不同。我在一个四轴项目中试过用回传功能显示电池电压比外接电压表方便得多。最后再分享一个小技巧如果你手头有逻辑分析仪调试IBUS协议会轻松很多。把逻辑分析仪的通道夹在IBUS信号线上解码方式选择UART或者自定义的115200 8N1就能在电脑上完整看到一帧数据的波形和内容。相比在代码里打印日志这种方式直观得多。没有逻辑分析仪的话用串口助手打印HEX数据也能凑合但效率确实差一些。我自己做完这个项目最大的感受是IBUS协议本身不复杂复杂的是整个链路上的各种细节从固件版本、接线共地、中断标志清除到电平匹配任何一个环节出问题都会让人抓狂。但只要你按照这篇的思路一步一步来花一个下午的时间就能把整套流程跑通。后面再做遥控类项目换再高级的接收机核心思路都是一样的。
返回列表