☰
STM32F103解析富斯i6接收机ibus协议:串口DMA接收14通道数据
2026/9/25 6:32:54 网站建设 项目流程

简介:基于STM32F103解析富斯i6遥控器IBUS通信的完整工程资源,面向嵌入式开发者、遥控模型爱好者及需要实现无线遥控控制的项目人员。资源围绕IBUS单线串行协议,给出从GPIO配置、定时器输入捕获、上升沿与下降沿中断读取计数值,到根据脉冲宽度还原二进制数据、按通道地址重组为油门/方向/副翼等控制指令,并最终输出PWM驱动小车、无人机或云台的完整方案。工程内包含49个h头文件与44个c源码文件,覆盖STM32标准外设库、定时器、I2C、ADC、Flash等底层驱动;同时提供hex固件、pdf说明文档与Keil工程配置文件,可直接编译烧录,并借助清理脚本等工具快速梳理项目结构。压缩包共109个文件,整体仅1.89MB,轻量清晰,适合边读边调试,也可作为课程设计与毕业设计的参考模板。已有1703人学习下载,对理解单线协议解析、中断服务程序设计及嵌入式遥控应用均有直接参考价值。 手边有一套飞行器拆下来的富斯i6遥控器加FS-iA6B接收机,是很多玩航模、做机器人或者搞无人车的人都会入手的第一套遥控设备。但真到了要把接收机接到STM32上用的环节,我发现网上大量教程还在教你用PWM方式逐通道采集,一根线读一个通道,6通道就得接6个IO口,还得配合输入捕获中断,程序又长又容易乱。其实FS-iA6B背后还有一根“I-BUS”信号线,这一个引脚就能把全部14个通道数据用串口方式送到stm32f103。这个做法的本质很简单:接收机内部已经把各个通道打包成一帧串行数据,我们只要用USART按115200波特率收下来,按协议拆包,就能直接拿到每个通道对应的数值。这篇文章会把基于stm32f103解析富斯i6遥控器和FS-iA6B接收机ibus通信的完整过程,从协议层面到实际代码再到我踩过的坑,全部摆出来。不管你是给小车加无线控制,还是想做一个低成本遥控手柄,这篇都能帮你直接落地。

1. 方案选型:为什么绕开PWM去碰ibus

1.1 PWM、PPM、ibus三种模式怎么选

FS-iA6B接收机本身支持PWM、PPM两种传统输出,同时也支持ibus串行输出。很多教程默认教PWM方式,因为每个通道对应一根信号线,看起来最直观:遥控器推杆,对应引脚上就有脉宽变化,用定时器输入捕获就能把脉宽读出来。

但PWM方式的实际体验不太行。6个通道至少要占6个输入IO,接收机上面的排针密密麻麻;而且每个通道要单独配置一个输入捕获通道,中断也多,主循环动不动被打断。更麻烦的是,如果后面想扩展机械臂或云台,8通道、10通道一上来,IO根本不够用。

PPM是一个引脚输出所有通道,但它本质是模拟式的脉宽串行信号,一个周期内按时间顺序编码多个通道。解码时要用定时器不停测量脉宽,对中断实时性要求高,一旦程序里有关中断的临界区,就容易丢脉冲。

ibus则完全是数字协议,把14个通道的数据打包成固定帧通过串口发出来,UART天然适合收这种东西。用DMA接收的话,CPU基本不用管,一帧数据到了自己就进内存,解析也只是简单的字节切片和校验。三种方式对比如下:

方式接线数量CPU占用实时性通道数
PWM每通道1根高,需输入捕获/外部中断一般受IO数量限制
PPM1根中,需要定时器测量脉宽中最多8/9通道
ibus1根低,USART+DMA后台接收约7ms刷新一帧14通道

1.2 整套系统的数据链路

从遥控器到单片机,数据是这么走的:富斯i6遥控器把摇杆、开关、旋钮的状态通过2.4G无线发送给FS-iA6B接收机,接收机在内部把各通道数据整理成ibus帧格式,从I-BUS引脚用串口电平发出来。STM32F103要做的就是把这一帧收下来,从帧里还原出通道值。

这个方案里,遥控器端不用做任何额外设置,接收机默认在I-BUS引脚输出iber帧。只要STM32端的串口参数对得上、帧格式理解正确,数据就能稳定拿到。整条链路里面,唯一需要自己动手的,就是写STM32这一端的接收和解析程序。

2. ibus协议数据帧完全拆解

2.1 一帧32字节,结构非常固定

FS-iA6B的ibus输出是标准UART串口协议:115200波特率,8个数据位,1个停止位,无校验。一帧固定32个字节,结构如下表:

偏移(字节)长度内容
02帧头,固定0x20 0x40
22814个通道数据,每个通道2字节,小端模式
302校验和,低字节在前

通道数据的排布方式:从第2字节开始,每2个字节是一个通道的数值。比如第2、3字节是CH1,第4、5字节是CH2,依此类推。每个通道是一个16位无符号整数,按小端存储,也就是低字节在前高字节在后。读取时的C语言写法是:

ch_value = buf[2 + ch_index * 2] | (buf[3 + ch_index * 2] << 8);

通道值的物理意义对应脉宽微秒数,范围一般在1000到2000左右。摇杆在中位时约1500,推到底是1000或2000,开关通道一般直接落在1000和2000两端。这个数值可以直接拿来映射舵机角度、电机油门输出。

2.2 校验和算法和帧同步

校验和放在第30和第31字节,算法是:把第0到第29字节(共30字节)按8位逐字节累加,得到一个16位累加和,然后把累加和的低8位放在第30字节,高8位放在第31字节。

举个例子,假设前30字节累加和是0x1234,那么第30字节就是0x34,第31字节就是0x12。接收端计算同样的累加和,和帧尾的16位数据比较,一致就说明这一帧有效。

帧同步这个问题容易被忽略。ibus每帧约7毫秒发一次,接收机和STM32之间如果出现电压毛刺、串口干扰,或者STM32复位后DMA还没对齐,就很可能出现中间少收一个字节、整帧错位的情况。这时候如果还按固定位置取通道数据,解出来的全是乱的。所以解析程序里必须先检查帧头是否为0x20 0x40,不满足就说明这一帧没有对齐,要丢弃重新搜索。我在实际项目里用的是“IDLE中断判断一帧结束 + 帧头二次确认 + 累加和校验”三层保障,跑下来基本不会出问题。

3. 硬件接线与电平匹配

3.1 找FS-iA6B的I-BUS引脚

FS-iA6B接收机机身侧边会有一排通道排针,上面一般印着CH1到CH6,侧面有一个独立的排针位置或者丝印标注“I-BUS”。不同批次、不同版本的接收机丝印位置略有区别,有的放在排针末尾,有的单独一列。判断方式很简单,用万用表量这个引脚,在接收机上电后通常能看到3.3V左右的高电平,而且实际输出波形是一连串不规则的串行数据。没有万用表的话,也可以用逻辑分析仪或示波器去点各个引脚,能抓到一串密集波形的那个就是I-BUS。

3.2 STM32F103这边的接线

FS-iA6B的I-BUS输出的是3.3V逻辑电平,可以直接接到STM32F103的串口RX引脚。我习惯用USART1,RX是PA10,接线就三根:

  • FS-iA6B的I-BUS信号端子 → STM32的PA10
  • FS-iA6B的GND → STM32的GND
  • 接收机供电用5V独立电源,不占用STM32的供电

特别提醒一下,别把接收机的5V直接接到STM32板子的5V引脚给整块板供电。接收机工作瞬间电流不小,如果共用一颗LDO,容易造成STM32上电复位。我一开始图省事直接从开发板的5V引脚给接收机供电,结果每次推油门舵机一动作,STM32就重启。后来改成独立5V给接收机,两边的GND共地,问题立刻消失。信号线上可以串一个1kΩ电阻做保护,PA10虽然是FT引脚能容忍5V,但长期接高电平还是不推荐。

3.3 关于“FS-iA6B是不是5V电平”的误解

网上有些说法把I-BUS当成5V TTL,这是不对的。FS-iA6B内部用3.3V单片机做信号处理,I-BUS引脚输出的也是3.3V电平,和STM32F103的IO电平天然兼容。有的人把I-BUS接到USB转TTL模块的5V RX脚上去看数据,也能看到,因为5V TTL对3.3V高电平的判断阈值大概在1.5V以上,能识别出来,但你千万别在STM32这边把I-BUS当成5V来硬接。

4. STM32F103解析ibus的代码实现

4.1 串口IDLE空闲中断 + DMA接收

ibus一帧32字节,115200波特率下大约2.8ms就能收完一帧。如果每次都靠RXNE中断逐字节接收,CPU会被频繁打断,而且两个字节之间的间隔波动会导致判断“一帧结束”很麻烦。我推荐的做法是DMA接收 + USART空闲中断(IDLE)。

思路是这样的:DMA空闲时一直在内存缓冲区里待命,串口每收到一个字节就自动存进缓冲区。当串口线上出现空闲(也就是一帧数据结束)时,USART会产生IDLE中断,我在中断里读DMA当前剩余计数,推算出这一帧收到了多少个字节。因为是接收机主动连续发帧,两次帧之间有空闲间隔,IDLE中断正好能在每帧结束后及时告诉我们“数据到了”。

初始化的标准库代码:

#define IBUS_FRAME_SIZE 32 uint8_t rx_buf[IBUS_FRAME_SIZE]; uint8_t ibus_frame[IBUS_FRAME_SIZE]; volatile uint8_t frame_ready = 0; void USART1_GPIO_DMA_Init(void) { GPIO_InitTypeDef gpio_init; USART_InitTypeDef usart_init; DMA_InitTypeDef dma_init; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA10 作为USART1的RX输入 gpio_init.GPIO_Pin = GPIO_Pin_10; gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio_init); // USART1 115200 8N1 usart_init.USART_BaudRate = 115200; usart_init.USART_WordLength = USART_WordLength_8b; usart_init.USART_StopBits = USART_StopBits_1; usart_init.USART_Parity = USART_Parity_No; usart_init.USART_Mode = USART_Mode_RX; usart_init.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_Init(USART1, &usart_init); // DMA1通道5对应USART1_RX dma_init.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; dma_init.DMA_MemoryBaseAddr = (uint32_t)rx_buf; dma_init.DMA_DIR = DMA_DIR_PeripheralSRC; dma_init.DMA_BufferSize = IBUS_FRAME_SIZE; dma_init.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma_init.DMA_MemoryInc = DMA_MemoryInc_Enable; dma_init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; dma_init.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; dma_init.DMA_Mode = DMA_Mode_Normal; dma_init.DMA_Priority = DMA_Priority_High; dma_init.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &dma_init); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); USART_Cmd(USART1, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); }

这里DMA模式用的Normal而不是Circular,是因为每次IDLE中断里我会重新设置DMA计数器。Normal模式能让每一帧数据都在缓冲区里从0位置开始,解析时逻辑最清晰。Circular模式会不停覆盖缓冲区,中间容易出现帧头错位,反而增加复杂度。

4.2 IDLE中断处理函数

USART1的中断服务函数里,关键是清掉IDLE标志位。标准库的写法是先后读SR和DR寄存器来清除IDLE标志。

void USART1_IRQHandler(void) { uint16_t rx_len; if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { // 读SR和DR清除IDLE标志 (void)USART1->SR; (void)USART1->DR; // 先停DMA,再算长度 DMA_Cmd(DMA1_Channel5, DISABLE); rx_len = IBUS_FRAME_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); if (rx_len == IBUS_FRAME_SIZE) { memcpy(ibus_frame, rx_buf, IBUS_FRAME_SIZE); frame_ready = 1; } // 重置DMA,继续接收下一帧 DMA_SetCurrDataCounter(DMA1_Channel5, IBUS_FRAME_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }

为什么在IDLE中断里要先DMA_Cmd(DISABLE)再取计数器?因为DMA计数器在运行过程中是实时变化的,如果不禁用直接DMA_GetCurrDataCounter,可能会在DMA正写内存的瞬间读到不一致的数据,轻微但存在风险。加上禁用的动作后,DMA停在当前状态,计数稳定,拷贝出来的帧也是完整对齐的。我的习惯是只把拷贝进ibus_frame这件事放在中断里做,解析放到主循环,这样中断服务函数的处理时间尽可能短。

4.3 解析函数:帧头确认、校验、抽通道

主循环里只要判断frame_ready标志位,然后调用解析函数:

uint8_t ibus_parse(uint8_t *buf, int16_t *channels) { uint16_t sum = 0; uint16_t checksum = 0; int i; // 1. 帧头确认 if (buf[0] != 0x20 || buf[1] != 0x40) return 0; // 2. 累加前30字节 for (i = 0; i < 30; i++) sum += buf[i]; // 3. 取帧尾校验值 checksum = buf[30] | (buf[31] << 8); if (sum != checksum) return 0; // 4. 解析14个通道 for (i = 0; i < 14; i++) channels[i] = buf[2 + i * 2] | (buf[3 + i * 2] << 8); return 1; }

主循环的用法:

int16_t channels[14]; while (1) { if (frame_ready) { frame_ready = 0; if (ibus_parse(ibus_frame, channels)) { // channels[0]~channels[13] 就是遥控器14个通道的值 printf("CH1:%d CH2:%d CH3:%d CH4:%d\r\n", channels[0], channels[1], channels[2], channels[3]); } } }

校验通过后,channels数组里每个元素的取值范围大约在1000~2000。串口打印出来就能观察到:推摇杆,对应通道值跟着变。到这里,接收机的数据已经完整落到单片机内存里了。

5. 调试中的坑和排查技巧

5.1 一串乱码或者完全收不到数据

先别动代码。把I-BUS引脚从STM32拆下来,接到USB转TTL模块的RX上,波特率115200,打开串口助手,看有没有以20 40开头、连续32字节的帧出现。如果有,说明接收机输出正常,问题出在STM32这一侧;如果没有,检查接收机是否成功绑定遥控器。富斯i6要对频很简单,接收机上电后按住对频键,遥控器进入对频模式,几秒钟后接收机指示灯常亮就完成了。

STM32这一侧最常见的问题是RX接错。有人把I-BUS接在PA9上,那个是USART1_TX,不是RX,自然什么都收不到。另外检查DMA用的是不是DMA1_Channel5,USART1_RX对应的是这个通道,不是Channel4,搞错了数据永远进不了内存。

5.2 帧头对但校验和经常失败

这种情况多发生在接收机到单片机之间线路较长,或者供电不够干净的时候。FS-iA6B输出的串口波形如果受到干扰,某个字节电平翻转,就会导致累加和不对。排查时先用示波器看I-BUS引脚的波形,确认高低电平是否干净,边沿是否陡峭。波形毛刺多的话,优先在接收机电源引脚并一个100uF电容,信号线尽量短,必要时用双绞线或屏蔽线。

另外有一种隐蔽的情况,接收机绑定时设置的失控保护导致某些通道数值超出正常范围,虽然不影响帧格式,但要注意是否你的程序把通道值当成了有符号数来处理。1000~2000这个范围正好在int16_t正数区间内,但如果你用int8_t去接收,马上就会出问题。我在解析函数里统一用int16_t,避免符号扩展的坑。

5.3 偶尔会卡住一两秒,然后才恢复

这是典型的数据帧失去同步。原因可能是STM32复位瞬间接收机已经在发数据,DMA从半路开始收,导致收到的数据不是从帧头开始的。虽然我在IDLE中断里只接收长度为32的帧,但如果错位后的32字节恰好包含0x20 0x40的组合,校验也可能碰巧通过,解析出错误的通道值。

解决办法是在解析函数里加一重检查:通道值必须落在合理范围内(比如800~2200之间),超过这个范围就判定为无效帧,丢弃。ibus正常通道值不会超过这个区间,所以这个过滤很有效。更严谨的做法是连续收到N帧校验失败后,把DMA重置一遍,强制让数据从下个完整的0x20 0x40开始重新累积,我的工程里就用了这个逻辑,效果很明显。

5.4 接收机一上电,STM32板子就工作不正常

优先查共地和供电。之前说过,接收机启动瞬间电流大,如果供电是从STM32板上引的,瞬时压降会导致单片机复位。标准做法是接收机独立5V供电,两边GND连起来,信号线再串联一个1kΩ电阻。实际测量的时候,接收机工作电流通常在几十毫安,但启动瞬间可能冲到上百毫安,板载LDO不一定扛得住。

5.5 通道值漂移、微调后范围不对

富斯i6遥控器本身有摇杆校准和微调功能。如果解析出的通道中位不在1500附近,先到遥控器的“功能设置”里做摇杆校准,再检查通道显示界面。ibus输出的数值本质上是遥控器内部换算后的结果,所以校准实际上是在遥控器端完成的,STM32这边只是忠实还原。

6. 再进一步:把ibus解析变成项目的一部分

6.1 通道值直接驱动PWM输出

解析出来的通道值,天然就是脉宽微秒数,可以直接映射到舵机或电调。STM32F103的定时器可以同时输出多路PWM,如果只是单一通道点对点赋值,逐个改CCR寄存器就够了。但项目里往往要同时控制多个舵机,逐个赋值会带来通道之间的相位误差。我后来用定时器PWM + DMA的方式,先把14个通道值写进一个数组,然后由DMA在定时器更新事件时一次性载入所有CCR寄存器,这样多路PWM几乎同时更新,控制机械臂时动作更同步。用到的引脚就是PA1、PA3这类定时器通道引脚,和ibus接收的串口引脚不冲突。

6.2 上FreeRTOS以后怎么组织

如果工程里用了FreeRTOS,ibus解析建议单独做成一个任务,优先级中等。串口IDLE中断里只置一个事件标志位或发一个二值信号量,不直接做解析。解析任务等信号量,拿到后调用ibus_parse,再把校验通过的数据发布给其他任务使用。这样遥控器数据流和业务逻辑解耦,后面加云台控制、路径规划都不互相阻塞。

这里有个细节:FreeRTOS的临界区或调度器锁可能会影响串口中断响应,但IDLE中断很短,DMA接收本身不依赖CPU,所以影响很小。真遇到帧校验偶发失败,我一般先查是不是任务里关了中断时间太长,而不是怀疑协议本身。

6.3 掉电保存校准值

遥控器摇杆的物理行程偶尔有偏差,我习惯在第一次开机时让用户把拨杆打满一圈,采集每个通道的最大最小值,然后存到STM32F103内部的Flash里。之后运行时用这两个极值做归一化,把通道值映射成0%~100%的百分比。这样即使遥控器微调有变化,或者不同遥控器之间有差异,程序也能自适应。Flash写入次数有限,所以只有在校准模式下才写,正常运行不擦写。

6.4 关于内部晶振的提醒

有些DIY板子为了省成本,用的不是外部8MHz晶振,而是STM32F103内部RC振荡器。内部RC的精度在常温下还可以,但温度变化时频率会飘,串口波特率跟着飘。ibus固定115200,STM32端波特率误差太大会导致长时间运行后偶发丢字节、校验错误。如果确认板子上没有外部晶振,建议先用定时器测一下实际系统时钟,或者干脆换一块带外部晶振的最小系统板。我早期用内部RC调试,短时间正常,放一晚上第二天上电就出现校验失败,排查半天才发现是晶振精度问题。

这套方案跑通之后,后续无论是做无线遥控小车、机械臂示教还是FPV云台,都只需要把接收机数据源接上,然后按自己的业务逻辑去处理那14个通道值就行。ibus相比PWM方式最大的优势就是省心,一根线解决所有通道,帧结构固定,还有校验和兜底,稳定性和可维护性都高一个档次。踩过几次坑之后,我现在拿到一颗新的STM32F103,第一件事就是把串口空闲中断+DMA这套框架搭好,ibus解析只是其中一个很典型的小应用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询