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

这篇具有很好参考价值的文章主要介绍了对黑羊、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。文章来源地址https://www.toymoban.com/news/detail-433157.html

//来自文件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;
}

到了这里,关于对黑羊、Elrs等使用的crsf协议的简单解析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 使用java解析和读取excel表格(EasyExcel的简单使用)

    ** ** Apache POI、JXL、Alibaba EasyExcel等。其中Apache POI基于DOM方式进行解析,将文件直接加载内存,所以速度较快,适合Excel文件数据量不大的应用场景; EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下

    2024年01月16日
    浏览(48)
  • JetCache 使用简单案例到源码解析读这一篇就够

    github.com/alibaba/jet… EnableMethodCache,EnableCreateCacheAnnotation这两个注解分别激活Cached和CreateCache注解,其他和标准的Spring Boot程序是一样的。这个类可以直接main方法运行、 User类: UserService: UserSercviceImpl 测试: 缓存实体必须序列化 使用过程中可能会出现包冲突: 解决方法:

    2024年02月04日
    浏览(39)
  • [Linux] 初识应用层协议: 序列化与反序列化、编码与解码、jsoncpp简单使用...

    有关Linux网络, 之前的文章已经简单演示介绍了 UDP 、 TCP 套接字编程 相关文章: [Linux] 网络编程 - 初见UDP套接字编程: 网络编程部分相关概念、TCP、UDP协议基本特点、网络字节序、socket接口使用、简单的UDP网络及聊天室实现… [Linux] 网络编程 - 初见TCP套接字编程: 实现简单的单

    2024年02月15日
    浏览(74)
  • 蓝桥杯十四天冲刺班 第十四天《考场经验 | 历年考点 | 蓝桥杯押题》《C,JAVA,PY在蓝桥杯中必须要会用的容器 | 集合》(3K+字解析)

     📒博客首页:Sonesang的博客 🎉欢迎关注🔎点赞👍收藏⭐️留言📝 ❤️ :热爱Java与算法学习,期待一起交流! 🙏作者水平很有限,如果发现错误,求告知,多谢! 🌺有问题可私信交流!!!   目录 算法 实力 = 知识点+刷题量+速度+灵活的大脑 C++组知识点 java组知识点

    2023年04月15日
    浏览(42)
  • 最好用的AI创作工具Midjourney怎样使用,AI绘画注册使用教程

    Midjourney 是一个人工智能绘图平台,可以从自然语言描述中生成图像。基于 Transformer 模型和 GAN(生成对抗网络)技术。由一支小型自筹资金团队创立,专注于设计、人类基础设施和人工智能。它由 11 名全职员工和一批优秀的技术顾问组成。 它于 2022 年 3 月 14 日正式以架设在

    2024年02月13日
    浏览(67)
  • 【网安 | 网络协议】ARP协议(地址解析协议)

    在使用nmap时可以使用-PR指令,通过发送ARP请求包进行主机存活探测。 那么这一过程的原理是什么呢? 在了解什么是ARP协议后,问题就迎刃而解了。 地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。 在网络中各个主机相互信任的基础

    2024年02月04日
    浏览(39)
  • ARP协议(地址解析协议)

    MAC 地址的全称是 Media Access Control Address ,即媒体访问控制地址,它是网络上 以太网或网络适配器的唯一标识符 。MAC 地址能够区分不同的网络接口,并用于多种网络技术,尤其是大多数 IEEE 802 网络。 MAC 地址也称为物理地址,硬件地址和老化地址。主要用于识别数据链路中互

    2024年02月15日
    浏览(42)
  • 单片机——通信协议(UART协议解析篇)

            在嵌入式系统设计中,UART通信是一种广泛使用的串行通信协议,它通过 两条信号线实现全双工 的数据传输和接收。UART通信协议以其简单、灵活和易于集成的特点,在嵌入式设备之间以及与外部设备进行通信时发挥着重要作用。本文将详细介绍 UART 通信的 工作原

    2024年02月04日
    浏览(50)
  • 研究开源gpt-2-simple项目,跑一个简单的模型,然后生成一段对话。用的是 Intel(R) Core(TM) i7-9700,8核8线程,训练最小的模型200次跑1个小时20分钟

    本文的原文连接是: https://blog.csdn.net/freewebsys/article/details/108971807 未经博主允许不得转载。 博主CSDN地址是:https://blog.csdn.net/freewebsys 博主掘金地址是:https://juejin.cn/user/585379920479288 博主知乎地址是:https://www.zhihu.com/people/freewebsystem 快速使用docker 镜像进行环境搭建。 相关的

    2024年02月09日
    浏览(64)
  • 【网络协议】聊聊DNS协议如何域名解析和负载均衡

    我们知道如果使用IP地址进行访问网站,很难进行记忆,所以DNS的作用是将域名转换成对应的IP地址。如果全世界都使用同一台DNS服务器,那么DNS服务器本身需要保证服务的高可用、高性能,以及分布式等。最好的方式就是分层。 根DNS服务器:返回顶级域名DNS服务器的IP地址

    2024年02月05日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包