专栏首页张国平_玩转树莓派树莓派基础实验39:解析无线电接收机PWM、SBUS信号

树莓派基础实验39:解析无线电接收机PWM、SBUS信号

一、介绍

  虽然如今或者将来,5G网络的建设带来人工智能和工业自动化的全面升级,生产活动中劳动力的需求大大减少,大量的劳动力将向内容生产行业和服务行业转移。教育、医疗、娱乐、公共管理等诸多领域,乃至整个社会都将迎来巨大变革。可参阅我的一篇读书笔记5G社会:万物互联新时代

  但是,使用传统无线电通信设备通信仍然是非常重要的通信方式,比如无线电台、对讲机,航模、车模、船模遥控等等。与手机移动网络、WIFI连接相比,无线电连接有它独特的优势。

  在树莓派基础实验38:逻辑分析仪分析PWM、UART信号中使用逻辑分析仪,对树莓派的PWM信号和UART信号进行分析,从中可以详细了解逻辑分析仪分析的使用方法及PWM信号和UART信号。

  本实验中将使用逻辑分析仪、树莓派,对航模无线电接收机输出的PWM信号、SBUS信号进行采集分析,以便树莓派能够接收无线电控制信号,进而可以开发基于无线电控制的树莓派航模飞行控制系统、或者智能小车的无人驾驶系统。

二、组件

★Raspberry Pi 3 B+全套*1

★睿思凯Frsky X8R 接收机*1

★电平反向器模块*1

★睿思凯Frsky Taranis X9D PLUS SE2019遥控器*1

★国产梦源DSLogic Plus逻辑分析仪*1

★面包板*1(可选)

★40P软排线*1

★跳线若干

三、实验原理

(一)航模无线电遥控系统

本实验中使用的遥控系统可以自行选择其它品牌的产品,如国产的天地飞还不错。

航模的遥控器就是像电视机遥控器、空调遥控器一样可以不用接触到被控设备,而通过一个手持器件,使用无线电与被控设备进行通信,从而达到对设备的控制。

遥控器想到达到与无人机通信的功能需要有两部分配合完成。即:发射器(遥控器)与接收机。遥控器上的控制杆转为无线电波发送给接收机,而接收机通过接收无线电波,读取遥控器上控制杆的读数,并转为数字信号发送到航模的控制器中。

发射器与接收机

目前用于无人机遥控器主流的无线电频率是2.4G,这样的无线电波的波长更长,可以通信的距离较远,普通2.4G遥控器与接收机的通信距离在空旷的地方大概在1km以内。2.4GHz无线技术如今已经成为了无线产品的主流传输技术。所谓的2.4GHz所指的是一个工作频段2400M-2483M范围,这个频段是全世界免申请使用。

常见的Wifi、蓝牙、ZigBee都是使用的2.4G频率段,只不过他们采用的协议不同,导致其传输速率不同,所以运用的范围就不同。同样是采用2.4G频率作为载波,但不同的通讯协议衍生出的通讯方式会有着天壤之别;仅仅在传输数据量上,就有着从1M每秒到100M每秒的差别。

关于遥控器与无人机的通信协议也有很多种,常见的数据协议如下: 1.pwm:需要在接收机上接上全部pwm输出通道,每一个通道就要接一组线,解析程序需要根据每一个通道的pwm高电平时长(即占空比)计算通道数值。

2.ppm:按固定周期发送所有通道pwm脉宽的数据格式,一组接线,一个周期内发送所有通道的pwm值,解析程序需要自行区分每一个通道的pwm时长。PPM的频率通常是50Hz,周期长度20ms,每一个周期中可以存放最多10路PWM信号,每一路PWM的周期为2ms。

3.sbus:每11个bit位表示一个通道数值的协议,串口通信,但是sbus的接收机通常是反向电平,连接到航模时需要接电平反向器,大部分支持sbus的飞行控制板已经集成了反向器。

4.xbus:常规通信协议,支持18个通道,数据包较大,串口通信有两种模式,可以在遥控器的配置选项中配置。接收机无需做特殊配置。

睿思凯Frsky Taranis X9D PLUS SE2019遥控器

睿思凯Frsky X8R 接收机

然后,就是电调通过接收接收机输出的这些信号,来将输入的电源转为不同的电压,并输出到电机,从而达到使电机产生不同的转速的目的。有刷电调可以改变电流方向,从而可以改变电机转动方向。而无刷电调却不能改变电机的转动方向,但是可以将直流电转为三相交流电,从而输出到无刷电机上。

所谓电调就是电压调节器,也可以通俗的说成是电机调节器,这里不做过多讲解。

(二)接收机的PWM信号

PWM英文全称为(Pulse-width modulation)。也称占空比信号,它表示高电平时长占整个信号周期的比例。

PWM周期

PWM信号的频率是通常是没有规定的,可以是50hz、100hz、200hz或500hz等等。控制频率越高,其周期越短,控制间隔也就越短,电调和电机响应速度也就越快。反之,控制频率越低,其周期就越长,控制间隔就越长,电调和电机的响应速度就越慢。早期电调响应PWM信号的频率是50hz,但随着科技的发展和对控制流畅度的要求,现在多数电调都支持500hz以上的PWM信号,并且电调内部自带滤波器,可以很好的响应并控制电机的转动。

传统的遥控器接收机是采用多路PWM的方式进行输出的,遥控器中有多少个通道,接收机中就有多少路PWM输出,睿思凯Frsky X8R接收机的1-8个PWM输出通道,都是以PWM的形式输出的,这就需要飞控能够采集并解析这些PWM信号,并为飞控所用。

那么,睿思凯Frsky X8R接收机的PWM信号到底是怎样的呢?我们使用逻辑分析仪看看吧,连接好遥控器、接收机、连接逻辑分析仪。

接收机连接逻辑分析仪

这里我只采集了1、3、5号通道的PWM信号。1号通道是右手油门摇杆左右晃动,会自动回中;3号通道是右手油门摇杆油门控制,由低到高表示油门由小到大,不会回中;5号通道是SA开关,有上中下3个档位。

首先来看1号通道,当摇杆往左摇到底时,占空比约为5.5%,高电平时长为0.99ms,信号周期为18ms。

1号通道摇杆往左摇到底时

1号通道,当摇杆往右摇到底时,占空比约为11.2%,高电平时长为2.01ms,信号周期为18ms。

1号通道摇杆往右摇到底时

再看3号通道,当摇杆往下摇到底时,油门为0,占空比约为5.5%,高电平时长为0.99ms,信号周期为18ms。

3号通道油门为0,摇杆往下摇到底时

3号通道,当摇杆往上摇到底时,油门为最大,占空比约为11.2%,高电平时长为2.01ms,信号周期为18ms。

pwm_acc_100.jpg

那当他们居中时呢?占空比约为8.3%,高电平时长为1.50ms,信号周期还是为18ms。

pwm_center.jpg

5号通道为开关,上中下三档,与1/3通道的高中低三档时的数值一样,占空比依次约为11.2%、8.3%、5.5%,高电平时长依次约为2ms、1.5ms、1ms,信号周期一直是稳定的18ms。

开关为下档时

开关为中档时

开关为上档时

在采集接收机PWM信号时发现,当接收机刚通电时,接收机不输出PWM信号,当遥控器连接成功接收机后,接收机就立马输出遥控器的即时状态信号,所以请注意,连接之前请注意将油门调至0,否则如果电调没有保护机制,螺旋桨会立马飞起来。

无线电波在传输过程中可能受到干扰或是数据丢失等等问题,当接收机无法接收到发射器的数据时,通常会进入保护状态,也就是仍旧向无人机发送控制信号,此时的信号就是接收机收到遥控器发射器最后一次的有效数据。这样因为信号丢失而发送的保护数数据通常叫做failsafe数据。

如果遥控器没有设置failsafe mode,X8R接收机默认HOLD模式,即保持断联之前的信号一直输出;可以在遥控器上设置No pulses模式,指断联后接收机不输出信号;可以在遥控器上设置Custom模式,定制断联后接收机要输出的控制信号,比如降低油门到比较低的程度,以便飞机自动降落。

树莓派输出PWM信号很简单,但是如果我们需要使用树莓派来读取接收机输出的PWM信号值怎么办呢?

我们以第一个通道的PWM为例,讲述树莓派对其处理的具体方法: (1)检测引脚由低点平变为高电平的时刻,并记录当前时间t0,表示高电平开始; (2)检测引脚由高电平变为低点平的时刻,并记录当前时间t1,表示高电平结束; (3)继续检测引脚由低点平变为高电平的时刻,并记录当前时间t2,表示一个PWM周期结束; (4)计算高电平时长 = t1 - t0; (5)计算整个PWM周期 = t2 - t0; (6)计算PWM占空比 = 高电平时长 / PWM周期

每一个遥控器通道都需要一个PWM采集器进行采集,但是对于树莓派来说不可能使用多个定时器来采集多个通道的PWM,这对于树莓派的资源来说十分浪费,因此我优先采用的就是SBUS编码,可以在一个管脚中传输多路控制信号。

(三)SBUS信号

1.介绍

S.BUS是FUTABA提出的舵机控制总线,全称Serial Bus,别名S-BUS或SBUS,也称 Futaba S.BUS。

S-BUS其实是一种串口通信协议,采用100000的波特率,数据位点8bits,停止位点2bits,偶效验,即8E2的串口通信。但是S-BUS采用的是反向电平传输,也就是说,在S-BUS的发送端高低电平是反向的,协议中的所有高电平都被转换成低电平,协议中的所有低电平都被转换成高电平。所以在S-BUS的接收端需要增加一个高低电平反向器来进行电平反转。

高低电平反向器

实际上,有的飞控板上已经集成了反向器,所以对于使用这种飞控的用户来说,可以忽略掉S-BUS的反向机制,但是对于其它没有集成S-BUS反向器的硬件平台上,就需要使用者增加一个反向器来处理数据,否则将无法读取协议数据。

另外,100000的波特率并不是标准的波特率,这在一些只支持标准波特率的系统上无法实现,我们可以通过对设备节点的配置实现波特率的设定。

通信接口:USART(TTL)

通信参数:1个起始位+8个数据位+偶校验位+2个停止位,无控流,25个字节,波特率=100000bit/s,电平逻辑反转。

X6R的SBUS通信速率:每6ms间隔发送数据,每数据帧时长为3ms。

数据帧格式:

需要注意的是S-BUS中用11bits来表示一个遥控器通道的数值,22个字节就可以表示16通道(8 × 22 = 11 ×16)。11个bit可以表示的数值范围为0~2047。

每帧25个字节,排列如下:

[start byte] [data1] [data2] [data3] ... [data22] [flag] [end byte]

SBUS帧格式

简单来说就是,通道1数据在前,通道16数据最后;每通道的数据,低位在前面的字节中,高位在后面的字节中;每8bit数据中,低位是上一通道的数据,高位是下一通道的数据。

start byte = 0x0F CH1 = [data2]的低3位 + [data1]的8位     (678 + 12345678 = 678,12345678) CH2 = [data3]的低6位 + [data2]的高5位     (345678 + 12345 = 345678,12345 ) CH3 = [data5]的低1位 + [data4]的8位 + [data3]的高2位     (8    +   12345678    +   12 = 8,12345678,12) ... ... flag(由高位到低位:N/A N/A N/A N/A 故障保护激活位 帧丢失位 数字通道CH18 数字通道CH17 ) end byte = 0x00

2.未做电平反向时的SBUS信号

未做电平反向时的SBUS信号

可以看出字节数不对,只解析出23字节,起始字节不是正确的0x0F,而是0xF8,还有红色的PE(Frame error)帧错误,即是乱码。

未做电平反向时的起始字节

未做电平反向时的结束字节

3.电平反向后的SBUS信号

电平反向后的SBUS信号

可以看出一帧数据为25字节,起始字节是正确的0x0F,结束字节为0x00。

再详细分析起始字节,要搞清楚每个字节的含义,先弄清UART的数据通信的字节格式:

其中各位的意义如下: 起始位:先发出一个逻辑”0”信号,表示传输字符的开始。 数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位),小端传输。 校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验) 停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 空闲位:处于逻辑“1”状态,表示当前线路上没有资料传送。 传输方向:即数据是从高位(MSB)开始传输还是从低位(LSB)开始传输,X8R是从低位开始传输的。

电平反向后的起始字节

波特率:上图中可以看出每位的时长是10us,意思就是每秒传输100000比特位数(bit),即波特率为100000。

起始位:先发出一个逻辑”0”的信号,即低电平,表示传输数据的开始。

数据位:SBUS信号明显为8位。

校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。SBUS为偶校验,起始字节数据位中已有4个“1”,所以偶校验位为0。

停止位:它是一帧数据的结束标志。可以是1bit、1.5bit、2bit的空闲电平。SBUS信号是2位停止位,即2位高电平。

空闲位:没有数据传输时线路上的电平状态。为逻辑1。

传输方向:uart传输数据的顺序就是:刚开始传输一个起始位,接着传输数据位,接着传输校验位(可不需要此位),最后传输停止位。这样一帧的数据就传输完了。所以上图中Bits显示的11110000,是从左到右是由低到高位显示的,其值实际上是B00001111=0x0F。

帧间隔:即传送数据的帧与帧之间的间隔大小,这里的间隔为6ms,每帧的周期可以以位为计量也可以用时间,(起始1位+数据8位+校验1位+中止2位=12位) x 25字节=300位,每位时长为10us x 300位=3000us=3ms。

帧与帧之间的间隔为6ms

每帧数据时长为2.990ms,有10us的误差,应该是3ms:

每帧数据时长为3ms

4. 关闭遥控器后接收机的反应

为模拟接收机与遥控器失联后的状态,关闭遥控器的过程中,用逻辑分析仪分析了第24个字节的变化情况,在断开连接的前900ms内,帧丢失位由0变为1,即第24个字节值为0x04。

帧丢失位由0变为1

之后,故障保护激活位由0变为1,帧丢失位仍为1,即第24个字节值为0x0C,此时如果设置了failsafe数据,接收机就按照failsafe数据输出信号。

故障保护激活位也变为1

比如我设置的为No pulses(无脉冲),所有的通道值变为0。

无脉冲

四、实验步骤

(一) 树莓派解析接收机PWM信号

  1. 连接线路。将接收机的1/3/5通道分别连接到树莓派面包板上的G17、G18、G19上,接收机的电源+、-接5V和GND。

树莓派(name)

T型转接板(BCM)

接收机

GPIO.0

G17

Channel 1(SIG)

GPIO.1

G18

Channel 3(SIG)

GPIO.24

G19

Channel 5(SIG)

5V

5V

+

GND

GND

-

连线很简单,电路图就没画了,接收机上端接出的两个黑色细长薄片是天线。

树莓派解析接收机PWM信号接线图

  1. 编写树莓派解析PWM信号的程序。为了不至于结果刷新太快,为了便于观察,我设置了每次采集信号0.5秒的延迟,在实际信号使用过程中,显然是不用的。
#!/usr/bin/env python

import RPi.GPIO as GPIO
import time

channel_1 = 17 #接收机1通道连接树莓派G17针脚
channel_3 = 18 #接收机3通道连接树莓派G18针脚
channel_5 = 19 #接收机5通道连接树莓派G19针脚

def setup():
    GPIO.setmode(GPIO.BCM)
    
    GPIO.setup(channel_1, GPIO.IN)
    GPIO.setup(channel_3, GPIO.IN)
    GPIO.setup(channel_5, GPIO.IN)

def duty_cycle_collect(pin):

    #等待低电平结束,然后记录时间
    while GPIO.input(pin) == 0:  #捕捉信号端输出上升沿
        pass
    time1 = time.time()  
    
    #等待高电平结束,然后记录时间
    while GPIO.input(pin) == 1:  #捕捉信号端输出下降沿
        pass
    time2 = time.time() 
    #等待低电平结束,然后记录时间
    while GPIO.input(pin) == 0:  #捕捉信号端输出上升沿
        pass
    time3 = time.time()
    
    period = time3 - time1
    high_time = time2 - time1
    low_time = time3 - time2
    duty_cycle = high_time * 100 / period
    #print period
    
    return duty_cycle   

def loop():
    while True:
        #调用占空比采集函数duty_cycle_collect()获得各通道的信号占空比
        duty_cycle_channel_1 = duty_cycle_collect(channel_1)
        print 'duty_cycle_channel_1 =',duty_cycle_channel_1 
        
        duty_cycle_channel_3 = duty_cycle_collect(channel_3)
        print 'duty_cycle_channel_3 =',duty_cycle_channel_3
        
        duty_cycle_channel_5 = duty_cycle_collect(channel_5)
        print 'duty_cycle_channel_5 =',duty_cycle_channel_5
        
        print '' 
        time.sleep(0.5) #为了便于观察结果设置了延迟

def destroy():
    GPIO.cleanup()

if __name__ == "__main__":
    setup()
    try:
        loop()
    except KeyboardInterrupt:
        destroy()
  1. 测试成功获取接收机PWM信号的占空比。1/5通道的遥控均在中位,所以占空比约为8.3%,与逻辑分析仪的结果一致;3通道是油门,由大到小滑动时,得到的占空比结果11.2%降至5.5%,与逻辑分析仪的结果一致。但是少数测量结果会有偏差,极少数情况偏差较大。

pwm_analyse

  1. 当遥控器与接收机失联时,我定制了failsafe数据,油门降低。3号通道的占空比在开始失联的时候有抖动,约3秒钟后稳定在设置的6.3%左右。

pwm_analyse_mistake

(二) 分析接收机SBUS信号

  1. 连接电路。与树莓派基础实验36:通用串口通信实验一样设置树莓派的串口为通用串口,恢复硬件串口(/dev/ttyAMA0)与GPIO 14/15的映射关系,使得我们能够通过GPIO使用高性能的硬件串口来连接我们的SBUS信号输入。

T型转接板(BCM)

接收机

电平反向模块

DSlogic逻辑分析仪

-

SBUS

A6

-

-

-

B6

Channel 1(SIG)

3.3V

-

3.3V

-

5V

5V

-

-

GND

GND

GND

Channel 0(GND)

电平反相模块很便宜,某宝5元一个能买到6路的电平反相器。注意反向后的高电平是几伏,反相器的VCC就接几伏的电源,树莓派GPIO接收3.3V高电平,不能接收5V高电平,所以这里电平反向模块的VCC只能接3.3V电源。

电平反相器

解析SBUS信号接线图

  1. 这里树莓派要使用pyserial模块编程接收SBUS信号,有关基础可以参考树莓派基础实验37:pyserial模块通信实验。下面的程序我做了详细注释,如果有更快更好的代码,请留言。
#!/usr/bin/env python
#-*- coding: utf-8 -*-

import array #array模块是python中实现的一种高效的数组存储类型
import serial #serial模块封装了对串行端口的访问
import codecs #Python中专门用作编码转换的模块
import time

class SBUSReceiver():
    def __init__(self, _uart_port='/dev/ttyAMA0'):

        #初始化树莓派串口参数
        self.ser = serial.Serial(
            port=_uart_port,            #树莓派的硬件串口/dev/ttyAMA0
            baudrate = 100000,          #波特率为100k
            parity=serial.PARITY_EVEN,  #偶校验
            stopbits=serial.STOPBITS_TWO,#2个停止位
            bytesize=serial.EIGHTBITS,   #8个数据位
            timeout = 0,
        )

        # 常数
        self.START_BYTE = b'\x0f'  #起始字节为0x0f
        self.END_BYTE = b'\x00'     #结束字节为0x00
        self.SBUS_FRAME_LEN = 25    #SBUS帧有25个字节
        self.SBUS_NUM_CHAN = 18     #18个通道
        self.OUT_OF_SYNC_THD = 10
        self.SBUS_NUM_CHANNELS = 18 #18个通道
        self.SBUS_SIGNAL_OK = 0     #信号正常为0
        self.SBUS_SIGNAL_LOST = 1       #信号丢失为1
        self.SBUS_SIGNAL_FAILSAFE = 2   #输出failsafe信号时为2

        # 堆栈变量初始化
        self.isReady = True
        self.lastFrameTime = 0
        self.sbusBuff = bytearray(1)  # 用于同步的单个字节
        #bytearray(n) 方法返回一个长度为n的初始化数组;
        self.sbusFrame = bytearray(25)  # 单个SBUS数据帧,25个字节
        self.sbusChannels = array.array('H', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])  # 接收到的各频道值
        #array.array(typecode,[initializer]) --typecode:元素类型代码;initializer:初始化器,若数组为空,则省略初始化器
        self.failSafeStatus = self.SBUS_SIGNAL_FAILSAFE


    def get_rx_channels(self):
        """
        用于读取最后的SBUS通道值
        返回:由18个无符号短元素组成的数组,包含16个标准通道值+ 2个数字(ch17和18)
        """
        return self.sbusChannels

    def get_rx_channel(self, num_ch):
        """
        用于读取最后的SBUS某一特定通道的值
        num_ch: 要读取的某个通道的通道序号
        返回:某一通道的值
        """
        return self.sbusChannels[num_ch]

    def get_failsafe_status(self):
        """
        用于获取最后的FAILSAFE状态
        返回: FAILSAFE状态值
        """
        return self.failSafeStatus


    def decode_frame(self):
        """
        对每帧数据进行解码,每个通道的值在两个或三个不同的字节之间,要读取出来很麻烦
        不过futaba已经发布了下面的解码代码
        """
        def toInt(_from):
            #encode() 方法以指定的编码格式编码字符串。
            #int() 函数用于将一个字符串或数字转换为整型。
            return int(codecs.encode(_from, 'hex'), 16) 
            
        #CH1 = [data2]的低3位 + [data1]的8位(678+12345678 = 678,12345678)
        self.sbusChannels[0]  = ((toInt(self.sbusFrame[1])      |toInt(self.sbusFrame[2])<<8)                                   & 0x07FF);
        #CH2 = [data3]的低6位 + [data2]的高5位(345678+12345 = 345678,12345 )
        self.sbusChannels[1]  = ((toInt(self.sbusFrame[2])>>3   |toInt(self.sbusFrame[3])<<5)                                   & 0x07FF);
        #CH3 = [data5]的低1位 + [data4]的8位 + [data3]的高2位(8+12345678+12 = 8,12345678,12)
        self.sbusChannels[2]  = ((toInt(self.sbusFrame[3])>>6   |toInt(self.sbusFrame[4])<<2 |toInt(self.sbusFrame[5])<<10)     & 0x07FF);
        
        self.sbusChannels[3]  = ((toInt(self.sbusFrame[5])>>1   |toInt(self.sbusFrame[6])<<7)                                   & 0x07FF);
        self.sbusChannels[4]  = ((toInt(self.sbusFrame[6])>>4   |toInt(self.sbusFrame[7])<<4)                                   & 0x07FF);
        self.sbusChannels[5]  = ((toInt(self.sbusFrame[7])>>7   |toInt(self.sbusFrame[8])<<1 |toInt(self.sbusFrame[9])<<9)      & 0x07FF);
        self.sbusChannels[6]  = ((toInt(self.sbusFrame[9])>>2   |toInt(self.sbusFrame[10])<<6)                                  & 0x07FF);
        self.sbusChannels[7]  = ((toInt(self.sbusFrame[10])>>5  |toInt(self.sbusFrame[11])<<3)                                  & 0x07FF);
        self.sbusChannels[8]  = ((toInt(self.sbusFrame[12])     |toInt(self.sbusFrame[13])<<8)                                  & 0x07FF);
        self.sbusChannels[9]  = ((toInt(self.sbusFrame[13])>>3  |toInt(self.sbusFrame[14])<<5)                                  & 0x07FF);
        self.sbusChannels[10] = ((toInt(self.sbusFrame[14])>>6  |toInt(self.sbusFrame[15])<<2|toInt(self.sbusFrame[16])<<10)    & 0x07FF);
        self.sbusChannels[11] = ((toInt(self.sbusFrame[16])>>1  |toInt(self.sbusFrame[17])<<7)                                  & 0x07FF);
        self.sbusChannels[12] = ((toInt(self.sbusFrame[17])>>4  |toInt(self.sbusFrame[18])<<4)                                  & 0x07FF);
        self.sbusChannels[13] = ((toInt(self.sbusFrame[18])>>7  |toInt(self.sbusFrame[19])<<1|toInt(self.sbusFrame[20])<<9)     & 0x07FF);
        self.sbusChannels[14] = ((toInt(self.sbusFrame[20])>>2  |toInt(self.sbusFrame[21])<<6)                                  & 0x07FF);
        self.sbusChannels[15] = ((toInt(self.sbusFrame[21])>>5  |toInt(self.sbusFrame[22])<<3)                                  & 0x07FF);

        #17频道,第24字节的最低一位
        if toInt(self.sbusFrame[23])  & 0x0001 :
            self.sbusChannels[16] = 2047
        else:
            self.sbusChannels[16] = 0
        #18频道,第24字节的低第二位,所以要右移一位
        if (toInt(self.sbusFrame[23]) >> 1) & 0x0001 :
            self.sbusChannels[17] = 2047
        else:
            self.sbusChannels[17] = 0

        #帧丢失位为1时,第24字节的低第三位,与0x04进行与运算
        self.failSafeStatus = self.SBUS_SIGNAL_OK
        if toInt(self.sbusFrame[23]) & (1 << 2):
            self.failSafeStatus = self.SBUS_SIGNAL_LOST
        #故障保护激活位为1时,第24字节的低第四位,与0x08进行与运算   
        if toInt(self.sbusFrame[23]) & (1 << 3):
            self.failSafeStatus = self.SBUS_SIGNAL_FAILSAFE


    def update(self):
        """
        我们需要至少2帧大小,以确保找到一个完整的帧
        所以我们取出所有的缓存(清空它),读取全部数据,直到捕获新的数据
        首先找到END BYTE并向后查找SBUS_FRAME_LEN,看看它是否是START BYTE
        """

        #我们是否有足够的数据在缓冲区和有没有线程在后台?
        if self.ser.inWaiting() >= self.SBUS_FRAME_LEN*2 and self.isReady: #inWaiting()返回接收缓存中的字节数
            self.isReady = False    #表明有线程在运行,isReady = False
            
            # 读取所有临时帧数据
            tempFrame = self.ser.read(self.ser.inWaiting())
            # 在缓冲区帧的每个字符中,我们寻找结束字节
            for end in range(0, self.SBUS_FRAME_LEN):
                #寻找结束字节,从后向前查找
                if tempFrame[len(tempFrame)-1-end] == self.END_BYTE :
                
                    #从最后的命中点减去SBUS_FRAME_LEN寻找起始字节
                    if tempFrame[len(tempFrame)-end-self.SBUS_FRAME_LEN] == self.START_BYTE :
                        # 如果相等,则帧数据正确,数据以8E2包到达,因此它已经被校验过

                        # 从临时帧数据中取出刚验证正确的一段正确帧数据
                        lastUpdate = tempFrame[len(tempFrame)-end-self.SBUS_FRAME_LEN:len(tempFrame)-1-end]
                        if not self.sbusFrame == lastUpdate: #相等即表示没有操作,不用再次解码
                            self.sbusFrame = lastUpdate
                            self.decode_frame() #调用解码函数

                        self.lastFrameTime = time.time() # 跟踪最近的更新时间
                        self.isReady = True
                        break

if __name__ == '__main__':

    sbus = SBUSReceiver('/dev/ttyAMA0')

    while True:
        time.sleep(0.005)

        # X8R的SBUS信号是间隔6ms发送一次,一次持续发送3ms;
        # 不要调用sbus.update()太快,如果sbus.ser.inWaiting()>50,且增长很多,可以调用sbus.update()快点,即time.sleep()延迟短点;
        # 如果sbus.ser.inWaiting()<50,可以调用sbus.update()慢点,即time.sleep()延迟长点;
        sbus.update()

        #在您的代码中,您可以调用sbus.get_rx_channels()来获取所有数据,或者调用sbus.get_rx_channels()[n]来获取第n个通道的值;
        #或get_rx_channel(self, num_ch)来获得第num_ch个通道的值;
        print sbus.get_failsafe_status(), sbus.get_rx_channels(), str(sbus.ser.inWaiting()).zfill(4) , (time.time()-sbus.lastFrameTime)
        #str() 函数将对象转化为适于人阅读的形式,将指定的值转换为字符串。
        #zfill() 方法返回指定长度的字符串,原字符串右对齐,前面填充0。
        #(time.time()-sbus.lastFrameTime)用于展示得到最近这次数据的延迟
  1. 运行程序后,依次打印出了failsafe状态值、所有通道的10进制数组、读取缓存中的字节数、当次数据更新的延迟时间。控制遥控器摇杆晃动,能够及时得到该通道的数值变化。
  1. 从实验数据中可以看出,三个档位的通道的下档值为172,中间档位时值为992,上档位时值为1811;2个档位的下档值为172,上档值为1811,摇杆在中间位置时值为992,向其它方向摇动时,数值向172或1811变化。
  2. 使用上面的数值,通过函数转换,就可以输出相应通道的PWM控制信号,或者其它开关控制信号了!为什么不直接使用PWM输出呢?因为这样可以通过无线电远距离控制树莓派了,再通过树莓派编程,控制其他设备,比如树莓派无人机或者树莓派智能小车,特别是在没有移动网络信号的时候。

遥控器的数字通道17/18没有搞明白怎么用,所以这里没有能够测试,有知道的同学可以留言。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 树莓派综合项目2:智能小车(三)无线电遥控

    《智能小车(一)四轮驱动》中,实现了代码输入对四个电机的简单控制。《智能小车(二)tkinter图形界面控制》中,实现了本地图形界面控制小车的前进后退、转向和原...

    张国平
  • 树莓派综合项目2:智能小车(四)超声波避障

      阅读本篇文章前建议先参考前期文章: 树莓派基础实验34:L298N模块驱动直流电机实验,学习了单个电机的简单驱动。 树莓派综合项目2:智能小车(一)四轮...

    张国平
  • 树莓派基础实验32:DS1302实时时钟模块实验

      现在有很多流行的串行时钟芯片,如DS1302,DS1307,PCF8485等,由于简单的接口,低成本和易用性,他们被广泛应用于电话、传真、便携式仪器等产品领...

    张国平
  • 反运算(简单的定制)[第十七章]

    关于反运算,这里要注意一点;对于a + b,b的__radd__(self,other),中other是a的对象,self才是b的对象

    天钧
  • 720p实时视频插帧 | 旷视科技&北大提出RIFE

    该文提出一种实时中间流估计(Intermediate Flow Estimation)算法RIFE用于视频插帧。现有视频插帧大多先估计双向光流,然后采用线性组...

    AIWalker
  • PaddlePaddle版Flappy-Bird—使用DQN算法实现游戏智能

    刚刚举行的 WAVE SUMMIT 2019 深度学习开发者峰会上,PaddlePaddle 发布了 PARL 1.1 版本,这一版新增了 IMPALA、A3C...

    用户1386409
  • Mechanize实战二:获取音悦台公告

    有些网站或论坛为了防止暴力破解,在登录框设置了一个验证码,目前针对验证码的解决方案可谓是千奇百怪,考虑到爬虫所需要的只是数据,完全可以绕过验证码,直接使用COO...

    py3study
  • Leetcode 783. 二叉搜索树结点最小距离

    二叉搜索树属于有序树结构,一个可以利用的特点就是中序遍历可以得到有序数组,得到有序数组后遍历一次即可得到两节点最小差值。

    zhipingChen
  • 从PEP-8学习Python编码风格

    Python3中应当总是使用UTF-8。(Python2使用ASCII。)在使用了规定编码后不需要再声明文件编码。

    py3study
  • python第四十二课——__str__(self)函数

    4.__str__(self): 作用: 创建完对象,直接打印对象名/引用名我们得到的是对象的内存信息(十六进制的地址信息), 这串数据我们程序员并不关心...

    hankleo

扫码关注云+社区

领取腾讯云代金券