基于stm32和富斯遥控器的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个字节构成,其波特率为100K,所以可在这里插入图片描述
帧长度
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个字节-----是则执行解析函数,不是则为接收错误-----重新等待持续的高电平。

Created with Raphaël 2.2.0 上电 初始化USAER4、TIM3 读取PC11电平 是否出现持续的高电平? 使能USART4,等待接收第一个字节 收到第一个字节则使能TIM3 TIM3时间满后(3ms)判断是否完整收到25个字节 对收到的数据进行解析 yes no yes no

核心程序

程序是基于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 PC11
  GPIO_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复用为USART4
	GPIO_PinAFConfig(GPIOC,GPIO_PinSource11,GPIO_AF_UART4); //GPIOA11复用为USART4
	
 	//USART1端口配置 
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; //GPIOc10与GPIOc11
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//速度50MHz
	GPIO_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 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_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); //初始化串口1
  USART_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_tim
	TIM_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
最后附一张刚买的一个遥控器的初始值调试结果的截图:在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_49980537/article/details/109380389