对黑羊、Elrs等使用的crsf协议的简单解析

 一、前言

        crsf是在航模中常用的协议,在想使用Elrs 900接收机(使用crsf协议)的时候发现找不到对crsf协议直接免费的文字说明,计划从代码出发看一看crsl协议的校验和数据帧格式。下面从github上找了一个crsf转pwm的代码,原地址如下:GitHub – CapnBry/CRServoF: CRSF to PWM Servo converter for STM32F103https://github.com/CapnBry/CRServoF

二、分析

        文中主要关注到以下两个结构体和一四个代码段。(阅读本文时无需仔细阅读代码段)

/*来自crsf_protocol.h*/
#define CRSF_BAUDRATE           420000
CrsfSerial(HardwareSerial &port, uint32_t baud = CRSF_BAUDRATE);

/*接口初始化_来自CrsfSerial.cpp*/
CrsfSerial::CrsfSerial(HardwareSerial &port, uint32_t baud) :
    _port(port), _crc(0xd5), _baud(baud),
    _lastReceive(0), _lastChannelsPacket(0), _linkIsUp(false),
    _passthroughMode(false)
{
    //8N1 Serial
    _port.begin(_baud);
}

/*结构体1_来自crsf_protocol.h*/
typedef struct crsf_header_s
{
    uint8_t device_addr; 
    uint8_t frame_size;  
    uint8_t type;        
    uint8_t data[0];
} PACKED crsf_header_t;

/*结构体2_来自crsf_protocol.h*/
typedef struct crsf_channels_s
{
    unsigned ch0 : 11;
    unsigned ch1 : 11;
    unsigned ch2 : 11;
    unsigned ch3 : 11;
    unsigned ch4 : 11;
    unsigned ch5 : 11;
    unsigned ch6 : 11;
    unsigned ch7 : 11;
    unsigned ch8 : 11;
    unsigned ch9 : 11;
    unsigned ch10 : 11;
    unsigned ch11 : 11;
    unsigned ch12 : 11;
    unsigned ch13 : 11;
    unsigned ch14 : 11;
    unsigned ch15 : 11;
} PACKED crsf_channels_t;

/*代码段1_来自CrsfSerial.cpp*/
void CrsfSerial::handleSerialIn()
{
    while (_port.available())
    {
        uint8_t b = _port.read();
        _lastReceive = millis();

        if (_passthroughMode)
        {
            if (onShiftyByte)
                onShiftyByte(b);
            continue;
        }

        _rxBuf[_rxBufPos++] = b;
        handleByteReceived();

        if (_rxBufPos == (sizeof(_rxBuf)/sizeof(_rxBuf[0])))
        {
            _rxBufPos = 0;
        }
    }

    checkPacketTimeout();
    checkLinkDown();
}

/*代码段2_来自CrsfSerial.cpp*/
void CrsfSerial::handleByteReceived()
{
    bool reprocess;
    do
    {
        reprocess = false;
        if (_rxBufPos > 1)
        {
            uint8_t len = _rxBuf[1];
            // Sanity check the declared length, can't be shorter than Type, X, CRC
            if (len < 3 || len > CRSF_MAX_PACKET_LEN)
            {
                shiftRxBuffer(1);
                reprocess = true;
            }

            else if (_rxBufPos >= (len + 2))
            {
                uint8_t inCrc = _rxBuf[2 + len - 1];
                uint8_t crc = _crc.calc(&_rxBuf[2], len - 1);
                if (crc == inCrc)
                {
                    processPacketIn(len);
                    shiftRxBuffer(len + 2);
                    reprocess = true;
                }
                else
                {
                    shiftRxBuffer(1);
                    reprocess = true;
                }
            } 
        } 
    } while (reprocess);
}

/*代码段3_来自CrsfSerial.cpp*/
void CrsfSerial::processPacketIn(uint8_t len)
{
    const crsf_header_t *hdr = (crsf_header_t *)_rxBuf;
    if (hdr->device_addr == CRSF_ADDRESS_FLIGHT_CONTROLLER)
    {
        switch (hdr->type)
        {
        case CRSF_FRAMETYPE_GPS:
            packetGps(hdr);
            break;
        case CRSF_FRAMETYPE_RC_CHANNELS_PACKED:
            packetChannelsPacked(hdr);
            break;
        case CRSF_FRAMETYPE_LINK_STATISTICS:
            packetLinkStatistics(hdr);
            break;
        }
    } 
}

/*代码段4_来自CrsfSerial.cpp*/
void CrsfSerial::packetChannelsPacked(const crsf_header_t *p)
{
    crsf_channels_t *ch = (crsf_channels_t *)&p->data;
    _channels[0] = ch->ch0;
    _channels[1] = ch->ch1;
    _channels[2] = ch->ch2;
    _channels[3] = ch->ch3;
    _channels[4] = ch->ch4;
    _channels[5] = ch->ch5;
    _channels[6] = ch->ch6;
    _channels[7] = ch->ch7;
    _channels[8] = ch->ch8;
    _channels[9] = ch->ch9;
    _channels[10] = ch->ch10;
    _channels[11] = ch->ch11;
    _channels[12] = ch->ch12;
    _channels[13] = ch->ch13;
    _channels[14] = ch->ch14;
    _channels[15] = ch->ch15;

    for (unsigned int i=0; i<CRSF_NUM_CHANNELS; ++i)
        _channels[i] = map(_channels[i], CRSF_CHANNEL_VALUE_1000, CRSF_CHANNEL_VALUE_2000, 1000, 2000);

    if (!_linkIsUp && onLinkUp)
        onLinkUp();
    _linkIsUp = true;
    _lastChannelsPacket = millis();

    if (onPacketChannels)
        onPacketChannels();
}

对于串口的配置和校验:

        由接口初始化代码段可知,接收机波特率为420000,crc校验常数poly=0xD5,数据位、校验位、停止位没有找到具体代码,后来查到是8N1。

波特率 数据位 校验位 停止位 校验方式 校验常数
420000 8 None 1 crc8 0XD5

对于与数据包解析:

        原始数据转换为通道值的过程可以分为以下四个步骤,分析部分篇幅较长,可以查看结论。

1.接收原始数据单个字节存放到变量b,然后放入缓冲区_rxBuf。

/*接收原始数据的一个字节*/
//来自代码段1
uint8_t b = _port.read();
/*将原始数据放入缓冲区*/
//来自代码段1
_rxBuf[_rxBufPos++] = b;

2.调用handleByteReceived()函数(既代码段2)对_rxBuf内接收到的数据进行校验。

          CRSF采用cec8校验,当crc == inCrc通过校验,校验通过调用代码段3,校验方法下文题及。

//来自代码段2
//上下文......
uint8_t len = _rxBuf[1];
//上下文......
else if (_rxBufPos >= (len + 2))
{
    uint8_t inCrc = _rxBuf[2 + len - 1];
    uint8_t crc = _crc.calc(&_rxBuf[2], len - 1);
    if (crc == inCrc)
    {
        processPacketIn(len);
        shiftRxBuffer(len + 2);
        reprocess = true;
    }
}
//上下文......

 校验方法和校验函数:

        (1)数据包中的CRC字节:数据包中第len + 1字节。

//来自代码段2
uint8_t len = _rxBuf[1];
//来自代码段2
uint8_t inCrc = _rxBuf[2 + len - 1];

        (2)计算收到的数据包实际CRC值(校验值):其中poly为crc校验常数,在前文提到其值为0xD5。

//来自文件crc8.h
uint8_t _lut[256];

//来自文件crc8.cpp
void Crc8::init(uint8_t poly)
{
    for (int idx=0; idx<256; ++idx)
    {
        uint8_t crc = idx;
        for (int shift=0; shift<8; ++shift)
        {
            crc = (crc << 1) ^ ((crc & 0x80) ? poly : 0);
        }
        _lut[idx] = crc & 0xff;
    }
}

//来自文件crc8.cpp
uint8_t Crc8::calc(uint8_t *data, uint8_t len)
{
    uint8_t crc = 0;
    while (len--)
    {
        crc = _lut[crc ^ *data++];
    }
    return crc;
}

CRC校验相关资料:CRC校验 – yuxi_o – 博客园 (cnblogs.com)https://www.cnblogs.com/embedded-linux/p/5664194.html

3.对数据包进行分类。

_rxBuf将_rxBuf地址放入crsf_header_t 型的指针:hdr,这样_rxBuf第0字节对应device_addr,第1字节对应frame_size,第2字节对应type。到这里可以看出CRSF协议数据帧的第0字节为device_addr(设备地址),第1字节为frame_size(帧大小),第2字节对应type(类型)。然后根据该帧数据的type对数据包进行分类处理。可以看到数据包有CRSF_FRAMETYPE_GPS、CRSF_FRAMETYPE_RC_CHANNELS_PACKED、CRSF_FRAMETYPE_LINK_STATISTICS三种类型,我们需要的是遥控通道数据包。 

//来自代码段3
void CrsfSerial::processPacketIn(uint8_t len)
{
    const crsf_header_t *hdr = (crsf_header_t *)_rxBuf;
    if (hdr->device_addr == CRSF_ADDRESS_FLIGHT_CONTROLLER)
    {
        switch (hdr->type)
        {
        case CRSF_FRAMETYPE_GPS:
            packetGps(hdr);
            break;
        case CRSF_FRAMETYPE_RC_CHANNELS_PACKED: //遥控通道数据包
            packetChannelsPacked(hdr);//调用代码段4
            break;
        case CRSF_FRAMETYPE_LINK_STATISTICS:
            packetLinkStatistics(hdr);
            break;
        }
    } 
}

4.获得通道值。

        上一步中代码段3调用 packetChannelsPacked()函数(代码段4)传入参数hdr,在packetChannelsPacked()函数中,crsf_channels_t 型结构体hdr的data元素的地址被复赋值给crsf_channels_t 型指针ch。因此ch结构体首地址为原hdr的第25位(前3*8位为device_addr,frame_size,type),也就是说_rxBuf[3]及以后为通道数据。

//来自代码段4
crsf_channels_t *ch = (crsf_channels_t *)&p->data;

//来自结构体2
typedef struct crsf_channels_s
{
    unsigned ch0 : 11;//表示该元素占用内存为11位
    unsigned ch1 : 11;
    unsigned ch2 : 11;
    unsigned ch3 : 11;
    unsigned ch4 : 11;
    unsigned ch5 : 11;
    unsigned ch6 : 11;
    unsigned ch7 : 11;
    unsigned ch8 : 11;
    unsigned ch9 : 11;
    unsigned ch10 : 11;
    unsigned ch11 : 11;
    unsigned ch12 : 11;
    unsigned ch13 : 11;
    unsigned ch14 : 11;
    unsigned ch15 : 11;
} PACKED crsf_channels_t;

        可以看到在结构体crsf_channels_t中,每个通道占用11位。因此CRSF数据包结构如下:

0 Byte 1 Byte 2 Byte 3Byte、4Byte、5Byte ……
 device_addr frame_size type channals
设备地址 帧大小 帧类型 通道值,11位为一个通道

三、测试程序

        直接使用Python测试比较方便所以这里使用Python进行测试,有时间移植到单片机,放上C语言版本。由代码来看CRSF协议支持16个通道,这里取前五个通道进行测试。

测试Python代码:

import serial
import os
import time
from serial.tools import list_ports

#获取端口列表
plist = list(serial.tools.list_ports.comports())
for port in plist:
    print('端口号:' + port[0] + '   端口名:' + port[1])
#打开端口
port = serial.Serial(port = 'COM5', baudrate = 420000, bytesize = 8
                         , parity = 'N', stopbits = 1 , timeout= 1.0)
#校验初始化
lut = []
poly = 0xD5
idx = 0
for CNT in range(256):
    lut.append(0)
while idx<256:
    crc = idx
    shift = 0
    while shift<8:
        shift += 1
        if crc & 0x80:
            crc_temp = poly
        else:
            crc_temp = 0
        crc = (crc << 1) ^ crc_temp
    lut[idx] = crc & 0xff
    idx += 1
#主循环
while(True):
    time.sleep(0.01)
    #读端口
    data = port.read_all()
    #校验
    len_data = data[1]
    data_idx = 2
    inCrc = data[2+len_data-1]
    crc = 0
    while len_data-1:
        crc = lut[crc ^ data[data_idx]]
        data_idx += 1
        len_data -= 1
    if (data[2] == 22) & (len(data) >= (len_data+2)) & (inCrc == crc):
        #解析,从地四个字节开始,每个字节占用11位
        ch1 = ((data[3]>>0) | (data[4]<<8)) & 0x07FF
        ch2 = ((data[4]>>3) | (data[5]<<5)) & 0x07FF
        ch3 = ((data[5]>>6) | (data[6]<<2) | (data[7]<<10)) & 0x07FF
        ch4 = ((data[7]>>1) | (data[8]<<7)) & 0x07FF
        ch5 = ((data[8]>>4) | (data[9]<<4)) & 0x07FF

#结果显示(可不看)
#清屏
        os.system('cls')
#通道数值
        print("ch1->"+ str(ch1))
        print("ch2->"+ str(ch2))
        print("ch3->"+ str(ch3))
        print("ch4->"+ str(ch4))
        print("ch5->"+ str(ch5))
#比例条
        print("\n**********\n")
        #ch1
        draw1 = ch1
        print("ch1->", end="")
        while (draw1-172)>0:
            draw1 -= 20
            print("=", end="")
        print("")
        #ch2
        draw2 = ch2
        print("ch2->", end="")
        while (draw2 - 172) > 0:
            draw2 -= 20
            print("=", end="")
        print("")
        #ch3
        draw3 = ch3
        print("ch3->", end="")
        while (draw3 - 172) > 0:
            draw3 -= 20
            print("=", end="")
        print("")
        #ch4
        draw4 = ch4
        print("ch4->", end="")
        while (draw4 - 172) > 0:
            draw4 -= 20
            print("=", end="")
        print("")
        # ch5
        draw5 = ch5
        print("ch5->", end="")
        while (draw5 - 191) > 0:
            draw5 -= 20
            print("=", end="")
        print("")
        print("\n**********\n")

 测试视频:

CRSF协议解析效果视频

四、结论

1.串口配置和数据包校验:

波特率 数据位 校验位 停止位 校验方式 校验常数
420000 8 None 1 crc8 0XD5

2.数据包格式:

0 Byte 1 Byte 2 Byte 3Byte、4Byte、5Byte ……
 device_addr frame_size type channals
设备地址 帧大小 帧类型 通道值,11位为一个通道

注意:不要忘记筛选数据包类型(2 Byte: type),遥控通道数据包type = 22。

 3.校验方法和校验函数:

        当数据包中的CRC字节==计算出的CRC值时校验通过。

        (1)数据包中的CRC字节:数据包中第len + 1字节。

//来自代码段2
uint8_t len = _rxBuf[1];
//来自代码段2
uint8_t inCrc = _rxBuf[2 + len - 1];

        (2)计算收到的数据包实际CRC值(校验值):其中poly为crc校验常数,在前文提到其值为0xD5。

//来自文件crc8.h
uint8_t _lut[256];

//来自文件crc8.cpp
void Crc8::init(uint8_t poly)
{
    for (int idx=0; idx<256; ++idx)
    {
        uint8_t crc = idx;
        for (int shift=0; shift<8; ++shift)
        {
            crc = (crc << 1) ^ ((crc & 0x80) ? poly : 0);
        }
        _lut[idx] = crc & 0xff;
    }
}

//来自文件crc8.cpp
uint8_t Crc8::calc(uint8_t *data, uint8_t len)
{
    uint8_t crc = 0;
    while (len--)
    {
        crc = _lut[crc ^ *data++];
    }
    return crc;
}

物联沃分享整理
物联沃-IOTWORD物联网 » 对黑羊、Elrs等使用的crsf协议的简单解析

发表评论