基于stm32和富斯遥控器的SBUS波形分析和通讯实现
- 简介
- 软件环境和硬件搭建
- 软件环境
- 硬件搭建
- SBUS协议
- SBUS协议
- SBUS波形分析
- 程序部分
- 程序流程
- 核心程序
- 总结
简介
最近一个小项目用到了富斯的遥控器(使用的SBUS协议),目的是实现通过遥控器的各个通道对小车进行简单控制(移动、灯光、不同工作模式等),一点小经验和大家分享下。SBUS网上的资料很多,本篇更偏向于新人对SBUS的快速理解和直接应用,对一些不太常用的细则不再进行介绍。
因为是第一次使用SBUS协议,根据个人习惯在学习通讯协议时喜欢对照着实际波形理解,如果有朋友对硬件有简单了解,建议接触新的通讯协议时也用示波器配合实际波形来学习,能发现很多细节。当然这个不是必须的,仅是个人建议而已,实际波形我也会贴出供感兴趣的朋友参考。
其他细节如有疏漏还请各位指出,共同进步。
软件环境和硬件搭建
软件环境
编译软件:KEIL MDK
库:STM32标准库
单片机I/O使用:PC11(串口USART4 RX端,TX端不接即可)
单片机外设使用:USART4(接收遥控数据)、TIM3(定时验证数据正确性)
硬件搭建
发射装置:富斯遥控器FS-I6S
接收装置:接收机IA10B
MCU控制板:STM32F407电路板
外接电路:简单的三极管反向电路(必须)
发射装置和接收装置之间只要是SBUS通讯方式,不同型号理论来说影响不大,程序可以通用。
因为只需要用到单片机的串口(为了验证数据的正确性笔者多用了个定时器TIM3),所以只是实现通讯的话电路要求比较简单,只要能正常工作并带有串口外设的单片机板即可,比如某宝上卖的STM32F103最小系统板。
由于SBUS逻辑电平和常用的串口通讯极性刚好相反,所以需要搭建一个简单的三极管反向电路,电路参考下图。
遥控器需要配置为SBUS输出模式:
接收机接线如下:
绿线为信号线-----接三极管反向电路的输入端(Single)
黄线为电源+线-----接5V电源
蓝线为电源地线 -----接电源GND
总体连接如下:
SBUS协议
SBUS协议
SBUS协议其实就是串口通讯(USART)的应用层协议,它的本质还是USART通讯。可以粗暴理解为一帧SBUS数据是由连续发送或接收25个字节(即25次)的串口数据构成,第一个字节固定为0x0F,最后一个字节固定为0x00,中间23个字节和起来构成了所需数据。所以使用它在程序上还是使用串口,只不过在串口配置上必须按照以下参数配置:
串口波特率为100000,数据位为8位,2个停止位,偶校验,无硬件控流。
Sbus的编码方式为每11位为一个数据,除去第一个字节和第25个字节,需要把中间23个字节的常规8位数据合在一起,并按每11位为一组的格式进行解析处理。具体解析方法网上教程较多,不再赘述。如果不想了解具体解析方法,可直接引用下文的解析函数得出解析后的结果即可。
SBUS波形分析
位长度:
SBUS的波特率固定为100K,所以每传输一位的时间为:1/100K=10us,
随机用示波器抓取了一位,实测结果略微有误差为11.7us,在接受范围内。
字节长度:
SBUS一帧由25次串口接收或发送构成(25个字节),每次串口发送有12位组成:1个起始位+8个数据位+1个偶校验位+2个停止位。下图为截取一帧SBUS前几个数据字节波形。由于发送顺序遵循LSB(低位优先)原则,所以需要注意每个字节高位和低位的波形和实际结果颠倒的。如波形第一个字节为0xF0,实际数据为0x0F。
帧长度
SBUS一帧由25个字节构成,每个字节12位,每位长度10us,总长度=10us12位25个字节=3000us(纠正:图中3000us单位错打成了3000ms)。
帧间隔
SBUS两帧间间隔约4.68ms,如果要求不能漏掉任何一帧,则需要注意其他程序处理时间必须在4.68ms内,不能影响一下帧的接收。
程序部分
程序流程
程序执行流程:上电-----配置外设(USART4、TIM3,默认使能都为关闭状态,TIM3定时3ms)-----等待PC11出现持续一段时间的高电平后使能USART4,等待接收第一个字节(等待的持续高电平即为两帧间的高电平间隔部分,确保能从第一个字节接收)-----当串口收到数据后使能TIM3-----当TIM3时间到后关闭TIM3和USART4判断串口是否是刚好收到25个字节-----是则执行解析函数,不是则为接收错误-----重新等待持续的高电平。
核心程序
程序是基于STM32F407的,如果是103可能在系统头文件名上报错和USART配置时会有点小差别。
USART4配置及其中断函数:
一定要注意因为有一个偶校验位,数据长度要写为9:
USART_InitStructure.USART_WordLength = USART_WordLength_9b。
中断内的函数功能为:进中断开TIM3定时器,把收到的串口数据进行保存。
#include "sys.h"
#include "usart.h" u8 rec_buff[30]={0};
u8 rec_cnt=0;
extern u32 WaitRec_cnt;void Uart4_Init(u32 bound){//GPIO端口设置 PC10 PC11GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC,ENABLE); //使能GPIOC时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4,ENABLE); //使能USART1时钟//串口4对应引脚复用映射GPIO_PinAFConfig(GPIOC,GPIO_PinSource10,GPIO_AF_UART4); //GPIOC10复用为USART4GPIO_PinAFConfig(GPIOC,GPIO_PinSource11,GPIO_AF_UART4); //GPIOA11复用为USART4//USART1端口配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; //GPIOc10与GPIOc11GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHzGPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉GPIO_Init(GPIOC,&GPIO_InitStructure); //初始化C10 C11//Usart1 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel = UART4_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器//USART 初始化设置USART_InitStructure.USART_BaudRate = bound;//串口波特率USART_InitStructure.USART_WordLength = USART_WordLength_9b;//字长为8位数据格式USART_InitStructure.USART_StopBits = USART_StopBits_2;//2个停止位USART_InitStructure.USART_Parity = USART_Parity_Even;//偶校验位USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式USART_Init(UART4, &USART_InitStructure); //初始化串口1USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);//开启串口接受中断USART_Cmd(UART4, DISABLE); //使能串口1 }
void UART4_IRQHandler(void)
{if(USART_GetITStatus(UART4, USART_IT_RXNE) != RESET){rec_buff[rec_cnt]=UART4->DR;rec_cnt++;WaitRec_cnt=0;TIM_Cmd(TIM3, ENABLE); }USART_ClearITPendingBit(UART4, USART_IT_RXNE);
}
TIM3配置及其中断函数:
TIM3时间在实际应用时是3ms进定时器中断,理论上3ms能刚好把一帧SBUS(25个字节)接收完毕。因为是已经接收到第一个串口数据后才开的定时器,后续只会有24个字节的时间,所以实际上定时器3ms时间还留有一个字节的时间裕量。
TIM3的中断函数功能:即为判断串口是否正确接收了一帧SBUS(25个字节)数据,是则进行数据解析函数SbusDataParsing(u8 buf[]) ;,不是则错误位RecErr_Flag+1。
#include "tim.h"
#include "usart.h"
#include "sbus.h"
u8 RecErr_Flag;
extern u8 rec_cnt;
u32 WaitRec_cnt;
extern u8 rec_buff[30];
void TIM3_Init(u16 arr,u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能TIM_DeInit(TIM3);//定时器TIM3初始化TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_timTIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位TIM_ClearITPendingBit(TIM3, TIM_IT_Update);TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断//中断优先级NVIC设置NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级0级NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //从优先级3级NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器TIM_Cmd(TIM3, DISABLE); //使能TIMx
}
void TIM3_IRQHandler(void) //TIM3中断
{if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查TIM3更新中断发生与否{USART_Cmd(UART4, DISABLE); TIM_Cmd(TIM3, DISABLE);TIM3->CNT=0;if(rec_cnt==25){SbusDataParsing(rec_buff); //SBUS解析}else RecErr_Flag++;rec_cnt=0;WaitRec_cnt=0;TIM_ClearITPendingBit(TIM3, TIM_IT_Update); //清除TIMx更新中断标志 }
}
解析函数:
SbusDataParsing(u8 buf[])为解析函数,如果TIM3判断正确接收了25个字节的数据,则把串口接收到的25个数据放入buf[]数组内,执行完的数组结果ch[]就是我们需要的最终结果。
#include "sbus.h"u16 ch[16]={0};
void SbusDataParsing(u8 buf[]) //S-BUS解析
{ch[0] = ((u16)buf[ 1] >> 0 | ((int16_t)buf[ 2] << 8 )) & 0x07FF;ch[1] = ((u16)buf[ 2] >> 3 | ((int16_t)buf[ 3] << 5 )) & 0x07FF;ch[2] = ((u16)buf[ 3] >> 6 | ((int16_t)buf[ 4] << 2 ) | (int16_t)buf[ 5] << 10 ) & 0x07FF;ch[3] = ((u16)buf[ 5] >> 1 | ((int16_t)buf[ 6] << 7 )) & 0x07FF;ch[4] = ((u16)buf[ 6] >> 4 | ((int16_t)buf[ 7] << 4 )) & 0x07FF;ch[5] = ((u16)buf[ 7] >> 7 | ((int16_t)buf[ 8] << 1 ) | (int16_t)buf[9] << 9 ) & 0x07FF;ch[6] = ((u16)buf[ 9] >> 2 | ((int16_t)buf[10] << 6 )) & 0x07FF;ch[7] = ((u16)buf[10] >> 5 | ((int16_t)buf[11] << 3 )) & 0x07FF;ch[8] = ((u16)buf[12] << 0 | ((int16_t)buf[13] << 8 )) & 0x07FF;ch[9] = ((u16)buf[13] >> 3 | ((int16_t)buf[14] << 5 )) & 0x07FF;ch[10] = ((u16)buf[14] >> 6 | ((int16_t)buf[15] << 2 ) | (int16_t)buf[16] << 10 ) & 0x07FF;ch[11] = ((u16)buf[16] >> 1 | ((int16_t)buf[17] << 7 )) & 0x07FF;ch[12] = ((u16)buf[17] >> 4 | ((int16_t)buf[18] << 4 )) & 0x07FF;ch[13] = ((u16)buf[18] >> 7 | ((int16_t)buf[19] << 1 ) | (int16_t)buf[20] << 9 ) & 0x07FF;ch[14] = ((u16)buf[20] >> 2 | ((int16_t)buf[21] << 6 )) & 0x07FF;ch[15] = ((u16)buf[21] >> 5 | ((int16_t)buf[22] << 3 )) & 0x07FF;
}
主函数:
主函数的主要功能:上电配置串口USART4和定时器TIM3,然后while循环检查串口USART4的RX引脚PC11是否出现连续的高电平。每次while循环一次检测是高则WaitRec_cnt+1,是低则清0,直到出现一段连续的高电平就表明进入了两帧SBUS中间的帧间隔中,再开启串口确保能从第一个字节开始接收。实际的WaitRec_cnt时间不用特别精确但需要大家进行调试,不同单片机主频不同,执行while的时间也不同。STM32F407主频168M,WaitRec_cnt执行到3000时大概700多us。同时需要自行考虑持续多久开启串口中断比较好,不要影响到其他程序的运行。
#include "stm32f4xx.h"
#include "usart.h"
#include "tim.h"
extern u32 WaitRec_cnt;int main(void)
{ Uart4_Init(100000); //遥控器TIM3_Init(29,8399);while(1){ if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_11)==1) //等待一段高电平,确保从第一个字节开始。{WaitRec_cnt++;}else WaitRec_cnt=0;if(WaitRec_cnt>3000) //3000时大概为几百us,持续几百us都为高则开启串口{USART_Cmd(UART4, ENABLE);}}
}
其他.h文件
sbus.h
#ifndef __SBUS_H
#define __SBUS_H
#include "sys.h"
#define RightRocker_Horizontal ch[0] //left:242 center:1033 right:1804
#define RightRocker_Vertical ch[1] //up:1807 center:1024 down:240
#define LeftRocker_Vertical ch[2] //up:1805 center:1024 down:240
#define LeftRocker_Horizontal ch[3] //left:240 center:1025 right:1807
#define SWA ch[4] //up:240 down: 1807
#define SWB ch[5] //up:240 center:1024 down: 1807
#define SWC ch[6] //up:240 center:1024 down: 1807
#define SWD ch[7] //up:240 down: 1807
#define VAA ch[8] //left:240 center:1024 right:1807
#define VAB ch[9] //left:1807 center:1024 right:240
void SbusDataParsing(u8 buf[]);
#endif
usart.h
#ifndef __UART_H
#define __UART_H
#include "stdio.h"
#include "sys.h" void Uart4_Init(u32 bound);
void Uart1_Init(u32 bound);
#endif
tim.h
#ifndef __TIMER_H
#define __TIMER_H
#include "sys.h"void TIM3_Init(u16 arr,u16 psc);#endif
总结
至此,整个SBUS的通讯已经完成,通讯的最终结果存放在CH[]数组里以方便调用。遥控器上不同的摇杆和拨动开关对应不同的CH[]通道,sbus.h里也有进行宏定义以便大家进行遥控的按钮和CH[]通道的对应:
如
#define RightRocker_Horizontal ch[0] //left:242 center:1033 right:1804
实际就是遥控器右边摇杆水平拨动时对应的是ch[0]中的值的变化。不拨动时ch[0]值是1033,右摇杆拨到最左边时ch[0]是242,最右边时ch[0]是1804.不同的遥控器中间值和最大最小值会有小范围的偏差,一般不会超过几十。其他摇杆和按键的对应关系请自行体会。也可以去B站看实际控制遥控器对应的CH[]变化,不过是16进制的看着不是很方便:
https://www.bilibili.com/video/BV1Kv411k7fQ
最后附一张刚买的一个遥控器的初始值调试结果的截图: