玩单片机和航模的朋友应该都有过这种经历:手里一堆遥控器、接收机,好不容易把PWM信号读进来了,一看占用的引脚数量,心凉了半截。尤其是做遥控小车、机械臂、船模这类项目,既要收遥控通道,又要接电机驱动、编码器、显示屏,引脚根本不够用。我当年做基于STM32的遥控小车时,就被这个问题卡了很久,直到换成了富斯i6遥控器加IBUS协议,一根信号线把14个通道全部读回来,整个项目瞬间清爽了。
这篇内容就把我从零搭建富斯i6与STM32之间IBUS通信的完整过程写出来,包括协议原理、硬件接线、接收机配置、STM32代码实现、调试方法和踩坑记录。无论你是刚开始玩STM32的新手,还是准备做遥控类毕业设计的同学,照着操作就能把遥控数据读进单片机,跑通自己的第一个遥控项目。
1. 项目背景:为什么选了富斯i6和IBUS协议
先说结论:富斯i6(FS-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走的是数字串口协议,半双工,波特率115200,8位数据位、无校验、1位停止位,也就是常说的115200 8N1。刷新率大约是14ms一帧,换算下来约70Hz,控制舵机、电机、云台都够用。最关键的是,它只有一根信号线,接STM32的任意一个串口RX引脚就能收,14个通道全在一帧里,解析也不难。对比下来,IBUS在引脚占用、通道数量、抗干扰能力三个方面优势非常明显。
2. IBUS协议拆解:帧格式、校验和与实际数值
IBUS协议不复杂,难在很多人没搞懂它的帧结构和校验算法,直接抄代码容易翻车。我这里把协议拆开讲清楚,你理解了之后,哪怕换一个平台、换一种单片机,也能自己写出解析代码。
2.1 一帧IBUS数据到底长什么样
IBUS的每一帧固定是32个字节,不是变长的。帧结构如下:
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| 0 | 0x20 | 固定帧头 |
| 1 | 0x40 | 固定标志,可以理解为通道数识别码 |
| 2-29 | 14个通道数据 | 每通道2字节,小端模式,低字节在前 |
| 30 | 校验和低字节 | checksum & 0xFF |
| 31 | 校验和高字节 | 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,对应的引脚是PA9(TX)和PA10(RX)。IBUS接收机输出只需要接PA10,PA9留给调试打印。
3.2 接收机接线和引脚规划
FS-iA6B接收机上除了CH1到CH6的PWM输出排针之外,还有一个独立的接口,上面标注了“I-BUS/S.Bus”,这个接口通常有三根线:红色是5V电源,黑色或棕色是GND,白色或黄色是信号线。
接线表如下:
| FS-iA6B接收机 | STM32F103C8T6 | 说明 |
|---|---|---|
| 红色线(VCC) | 5V引脚 | 给接收机供电 |
| 黑色线(GND) | GND | 必须共地 |
| 白色线(I-BUS) | PA10(USART1_RX) | IBUS信号线 |
这里最容易被忽视的是共地。接收机的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的通道5,DMA自动把收到的字节写入缓冲区。当一帧数据发送完毕,串口总线处于空闲状态,串口的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协议本身不复杂,复杂的是整个链路上的各种细节,从固件版本、接线共地、中断标志清除到电平匹配,任何一个环节出问题都会让人抓狂。但只要你按照这篇的思路,一步一步来,花一个下午的时间就能把整套流程跑通。后面再做遥控类项目,换再高级的接收机,核心思路都是一样的。