一、前言
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字节。文章来源:https://www.toymoban.com/news/detail-433157.html
//来自代码段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模板网!