CRSF协议说明以及基于STM32duino的CRSF(Crossfire)协议的ELRS接收机遥控通道数据解析与发送指南
本文详细介绍了在STM32duino平台上实现CRSF(Crossfire)协议以解析ELRS(ExpressLRS)接收机遥控通道数据的方法。文档首先概述了CRSF协议的特性和数据包结构,然后提供了一个完整的代码实现,包括串口初始化、CRC校验和数据包解析等核心功能。代码采用模块化设计,易于理解和移植。文章深入解释了如何正确解码16个遥控通道的11位数据,并详细阐述了每个函数的作用和实现逻辑。此
《基于STM32duino的CRSF协议实现:ELRS接收机遥控通道数据解析与发送指南》
本文详细介绍了如何在STM32duino平台上实现CRSF(Crossfire)协议,以解析来自ELRS(ExpressLRS)接收机的遥控通道数据,并演示如何构建和发送CRSF遥测数据包。CRSF协议是一种用于遥控器和飞控之间低延迟、双向通信的协议,广泛应用于FPV无人机等遥控飞行器中。
文档首先概述了CRSF协议的特性和数据包结构,然后提供了一个完整的代码实现,包括串口初始化、CRC校验、数据包解析与构建等核心功能。代码采用模块化设计,易于理解和移植。
本指南不仅展示了如何正确解码16个遥控通道的11位数据,还详细解释了每个函数的作用和实现逻辑。此外,文档还提供了测试验证方法和潜在的应用扩展建议,使读者能够将这一实现应用到实际的飞控开发项目中。
1. CRSF协议简介
CRSF(Crossfire)协议是一种用于遥控器和飞控之间低延迟、双向通信的协议,广泛应用于遥控飞行器(例如FPV无人机)。CRSF协议具备以下特性:
- 低延迟、高速数据传输:CRSF通常使用高达420,000bps的波特率,确保了遥控器和飞控之间的低延迟。
- 支持16个通道数据传输:每个通道的数据被压缩成11位,可以传输16个通道的信息。CRSF原始通道值范围为172到1811。
- 双向通信:可以传输飞行控制器的状态数据(如GPS数据、链路状态)到遥控器,同时也传输遥控通道数据到飞控。
- 帧校验:使用CRC-8 DVB-S2校验确保数据完整性。
2. 硬件串口和波特率配置
在STM32duino中,通过硬件串口(例如 Serial2)实现与ELRS接收机或目标设备的通信。CRSF协议通常要求串口波特率设置为 420,000bps。串口的其他配置为:8数据位、无校验位、1停止位(8N1),这是标准的串行通信设置。
代码中的initCRSFSerial()函数负责初始化串口通信:
// CRSF协议标准波特率
#define CRSF_BAUDRATE 420000
// 假设使用Serial2作为CRSF通信串口
HardwareSerial &crsfSerial = Serial2;
// 初始化串口,波特率设置为CRSF协议要求的420000
void initCRSFSerial() {
crsfSerial.begin(CRSF_BAUDRATE);
}
该函数调用了STM32duino的串口库,设置波特率为420,000bps。在STM32中,硬件串口可以根据项目需求选择不同的Serial端口,例如Serial1, Serial2等。
3. CRSF 数据包结构 (以RC Channels Packed为例)
CRSF协议的数据包结构具有以下特征。以最常见的遥控通道数据包(RC Channels Packed, 类型 0x16)为例:
| 字节偏移 | 长度 (字节) | 内容 | 说明 |
|---|---|---|---|
| 0 | 1 | 同步字节 (Sync Byte) |
固定为 0xC8 |
| 1 | 1 | 长度 (Length) |
类型(1) + 负载(22) + CRC(1) = 24 (十六进制 0x18) |
| 2 | 1 | 类型 (Type) |
表示数据帧的类型,RC通道数据类型为0x16 |
| 3-24 | 22 | 负载 (Payload) |
实际的16个通道数据,每个通道用11位表示,共176位 (22字节) |
| 25 | 1 | CRC校验 (CRC) |
CRC-8 DVB-S2校验,校验范围从**类型(Type)字节到负载(Payload)**结束 |
| 总计 | 26 |
负载 (Payload) 部分说明 (RC Channels Packed)
负载部分存储了16个通道的值,每个通道值使用11位来表示。这些11位数据的原始值范围是 172 到 1811。这些原始值在飞控中通常会被映射到标准的PWM脉宽(如1000µs-2000µs或988µs-2012µs)。这176位数据被紧密打包到22字节中进行传输。
例如:
- 通道1: 11位(例如
00010101100对应原始值 172) - 通道2: 11位,紧接着通道1
- …
数据的解码与编码需要考虑到每个通道数据是连续的11位,并需要适当的移位和掩码操作。
4. CRC校验
CRSF协议使用 CRC-8 DVB-S2 校验算法,校验值附加在数据包的最后。CRC的作用是确保数据在传输过程中没有被损坏。CRSF协议使用的多项式是 0xD5,初始值为0x00,输入和输出均非反射。
在代码中,calcCRC()函数负责计算校验值:
// 计算CRC-8 DVB-S2校验值,使用多项式0xD5
uint8_t calcCRC(const uint8_t *data, uint8_t len) {
uint8_t crc = 0; // 初始值为0
for (uint8_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0xD5;
} else {
crc = crc << 1;
}
}
}
return crc;
}
- 该函数接收一个数据数组和其长度,使用多项式
0xD5来迭代计算校验值。 - CRC的计算范围是从数据包的“类型(Type)”字节开始,到“负载(Payload)”的最后一个字节结束。
5. 打包和发送CRSF数据包 (以RC Channels Packed为例)
sendCRSFFrame() 函数负责打包16个通道的原始CRSF数据(172-1811范围),并通过串口发送。打包过程涉及到将16个通道的11位数据依次存储到22字节的负载缓冲区中。
// CRSF协议相关常量
#define CRSF_SYNC_BYTE 0xC8
#define CRSF_FRAMETYPE_RC_CHANNELS_PACKED 0x16
#define CRSF_PAYLOAD_SIZE_RC_CHANNELS 22
#define CRSF_MAX_CHANNEL 16
// 假设的遥控通道原始值 (172-1811范围)
uint16_t rcChannelValues[CRSF_MAX_CHANNEL]; // 应填充172-1811范围的值
// 发送CRSF RC Channels Packed帧
void sendCRSFFrame() {
// 总帧长度: Sync(1) + Length(1) + Type(1) + Payload(22) + CRC(1) = 26字节
uint8_t crsfData[26];
int offset = 0;
// 1. 同步字节
crsfData[offset++] = CRSF_SYNC_BYTE;
// 2. 长度字节 (Type + Payload + CRC)
crsfData[offset++] = 1 + CRSF_PAYLOAD_SIZE_RC_CHANNELS + 1; // 1 + 22 + 1 = 24
// 3. 类型字节
crsfData[offset++] = CRSF_FRAMETYPE_RC_CHANNELS_PACKED;
// 4. 打包16个通道的11位数据到负载区
uint64_t bitBuffer = 0; // 使用64位缓冲区以容纳足够多的位
uint8_t bitsInBuffer = 0;
int payloadOffset = offset; // 记录负载区的起始位置
for (int i = 0; i < CRSF_MAX_CHANNEL; i++) {
// 确保通道值在11位范围内 (0-2047), CRSF实际有效范围是172-1811
uint16_t channel_val = rcChannelValues[i] & 0x07FF;
bitBuffer |= ((uint64_t)channel_val << bitsInBuffer);
bitsInBuffer += 11;
while (bitsInBuffer >= 8) {
crsfData[payloadOffset++] = bitBuffer & 0xFF;
bitBuffer >>= 8;
bitsInBuffer -= 8;
}
}
// 处理剩余的位 (如果有)
if (bitsInBuffer > 0) {
crsfData[payloadOffset++] = bitBuffer & 0xFF;
}
offset = payloadOffset; // 更新当前偏移到CRC位置
// 5. 计算CRC校验值
// CRC计算范围: 从Type字节开始,到Payload结束 (共 1 + 22 = 23字节)
// crsfData[2] 是Type字节的起始位置
uint8_t crc = calcCRC(&crsfData[2], 1 + CRSF_PAYLOAD_SIZE_RC_CHANNELS);
crsfData[offset++] = crc; // 将CRC校验字节加到数据帧末尾
// 通过硬件串口发送打包后的数据帧
crsfSerial.write(crsfData, offset); // offset 此时应为 26
}
该函数的工作流程为:
- 帧头设置:设置同步字节、计算并设置长度字节、设置类型字节。
- 打包16个通道数据:将每个通道的11位原始CRSF值压缩存储到数据帧的22字节负载区中。
- 计算CRC校验:调用
calcCRC()函数计算从**类型(Type)字节开始到负载(Payload)**结束的CRC值,并将其附加到数据帧末尾。 - 发送数据包:使用
crsfSerial.write()通过硬件串口将完整数据包发送出去。
6. 主程序流程 (发送示例)
setup()函数初始化硬件串口,loop()函数则定期更新通道数据并调用sendCRSFFrame()发送数据包:
void setup() {
// 初始化CRSF串口
initCRSFSerial();
// 示例:初始化遥控通道值 (172-1811范围)
for(int i=0; i<CRSF_MAX_CHANNEL; i++) {
if (i < 4) rcChannelValues[i] = 992; // 中间值 (对应1500us)
else if (i == 4) rcChannelValues[i] = 172; // 最小值 (对应约1000us)
else if (i == 5) rcChannelValues[i] = 1811; // 最大值 (对应约2000us)
else rcChannelValues[i] = 992;
}
}
void loop() {
// 在此处可以动态更新各个通道的原始CRSF数据 (172-1811),例如:
// rcChannelValues[0] = some_sensor_reading_mapped_to_crsf_range;
// 每10ms发送一次CRSF数据包 (发送频率根据实际需求调整)
sendCRSFFrame();
delay(10);
}
setup()函数确保在系统启动时配置串口。loop()函数可以根据实际的遥控输入或飞控状态来更新各个通道的原始CRSF值 (172-1811)。然后,定期调用sendCRSFFrame()函数,发送更新后的CRSF数据包。
7. 模块化与移植 (发送示例)
由于代码模块化设计,函数如initCRSFSerial()、calcCRC()和sendCRSFFrame()可以很方便地移植到其他项目中。你只需修改串口配置和通道数据的来源,就可以将此代码嵌入到你的飞控开发环境中。
- 串口选择:根据硬件情况,可以将
crsfSerial修改为任意可用的硬件串口(例如Serial1,Serial2)。
8. 其他CRSF数据包类型简介
在 CRSF 协议中,除了遥控通道数据包(RC Channels Packed)外,还有多种其他类型的数据包,它们用于传递设备状态、传感器数据以及控制命令等。下面简要介绍这些不同的数据包类型、它们的内容及使用场景。
(注:以下单位和偏移量已根据CRSF协议规范进行修正)
8.1. GPS 数据包(Frame Type: 0x02)
描述:传递GPS模块的位置信息。
数据包格式 (部分字段):
| 字段 | 大小 | 内容 |
|---|---|---|
| Latitude | 4字节 | 纬度,单位:度 * 1e7 |
| Longitude | 4字节 | 经度,单位:度 * 1e7 |
| Ground Speed | 2字节 | 地速,单位:0.1 km/h |
| GPS Heading | 2字节 | 航向,单位:0.01 度 (centi-degrees) |
| Altitude | 2字节 | 海拔高度,单位:米,通常有1000米偏移量 |
| Satellites in use | 1字节 | 当前使用的卫星数量 |
使用场景:将飞行器的定位信息传送给遥控器或地面站。
8.2. 电池状态数据包(Frame Type: 0x08)
描述:传递飞行器电池的电压、电流、容量以及电量剩余情况。
数据包格式 (部分字段):
| 字段 | 大小 | 内容 |
|---|---|---|
| Voltage | 2字节 | 电压,单位:0.1V (deci-volts) |
| Current | 2字节 | 电流,单位:0.1A (deci-amps) |
| Capacity | 3字节 | 已消耗或剩余容量,单位:毫安时(mAh) |
| Battery Remaining | 1字节 | 电池剩余电量,百分比表示 (0-100%) |
使用场景:提供电池的实时状态,确保操作员能够及时了解电量并进行合理的电池管理。
8.3. 链路统计数据包(Frame Type: 0x14)
描述:提供当前遥控器与飞控之间的通信链路的质量信息。
数据包格式 (部分字段):
| 字段 | 大小 | 内容 |
|---|---|---|
| Uplink RSSI Ant. 1 | 1字节 | 上行RSSI信号强度(天线1),单位为-dBm |
| Uplink RSSI Ant. 2 | 1字节 | 上行RSSI信号强度(天线2),单位为-dBm |
| Uplink Link Quality (LQ) | 1字节 | 上行链路质量,单位为百分比 (0-100%) |
| Uplink SNR | 1字节 | 上行信噪比,单位为dB |
| RF Mode | 1字节 | 当前射频模式 (e.g., 50Hz, 150Hz) |
| Downlink RSSI | 1字节 | 下行RSSI信号强度,单位为-dBm |
| Downlink Link Quality (LQ) | 1字节 | 下行链路质量,单位为百分比 (0-100%) |
| Downlink SNR | 1字节 | 下行信噪比,单位为dB |
使用场景:监控飞控与遥控器之间的通信质量。
8.4. 姿态数据包(Frame Type: 0x1E)
描述:传递飞行器的航向、俯仰角和滚转角。
数据包格式:
| 字段 | 大小 | 内容 |
|---|---|---|
| Pitch | 2字节 | 俯仰角,单位:弧度 * 10000 (0.1 millirad) |
| Roll | 2字节 | 滚转角,单位:弧度 * 10000 (0.1 millirad) |
| Yaw | 2字节 | 航向角,单位:弧度 * 10000 (0.1 millirad) |
使用场景:用于在地面站或遥控器显示飞行器的实时姿态信息。
8.5. 飞行模式文本数据包(Frame Type: 0x21)
描述:传递当前飞行器的飞行模式文本。
数据包格式:
| 字段 | 大小 | 内容 |
|---|---|---|
| Flight Mode | 可变长度 | 飞行模式名称,空终止的ASCII字符串 |
使用场景:在遥控器或地面站显示当前飞行模式。
(其他不常用的或特定用途的数据包类型在此省略,可参考CRSF协议规范)
9. ELRS接收机遥控通道数据解析示例代码
以下是如何在STM32duino平台上接收并解析来自ELRS接收机的CRSF遥控通道数据的示例。
#include <HardwareSerial.h>
// 定义CRSF协议相关常量 (与发送部分共用)
#define CRSF_SYNC_BYTE 0xC8
#define CRSF_MAX_CHANNEL 16
#define CRSF_FRAMETYPE_RC_CHANNELS_PACKED 0x16
#define CRSF_MIN_FRAME_SIZE 4 // Sync + Length + Type + CRC (最短帧,如ping)
#define CRSF_MAX_FRAME_SIZE 64 // CRSF帧最大长度限制 (包括扩展帧)
// 假设使用Serial2作为CRSF通信串口
HardwareSerial &crsfSerial = Serial2;
// 用于存储解码后的16个通道原始CRSF值 (172-1811)
uint16_t rcChannels[CRSF_MAX_CHANNEL];
// 转换CRSF原始通道值 (172-1811) 到 PWM值 (例如 988-2012us)
uint16_t convertCrsfToPwm(uint16_t crsf_val) {
// 线性映射: (val - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
// CRSF: 172 (min) -> 992 (mid) -> 1811 (max)
// PWM: 988 (min) -> 1500 (mid) -> 2012 (max)
if (crsf_val <= 172) return 988;
if (crsf_val >= 1811) return 2012;
return (uint16_t)(988.0f + (crsf_val - 172.0f) * (2012.0f - 988.0f) / (1811.0f - 172.0f));
}
// CRC校验函数 (与发送部分共用)
uint8_t calcCRC(const uint8_t *data, uint8_t len) {
uint8_t crc = 0;
for (uint8_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80) crc = (crc << 1) ^ 0xD5;
else crc = crc << 1;
}
}
return crc;
}
// 处理接收到的CRSF RC Channels Packed 数据包
void processCRSFRcChannelsPacket(const uint8_t *payload) {
uint64_t bitBuffer = 0;
uint8_t bitsInBuffer = 0;
int byteIndex = 0;
for (int i = 0; i < CRSF_MAX_CHANNEL; i++) {
while (bitsInBuffer < 11) {
// 从payload中读取字节
bitBuffer |= ((uint64_t)payload[byteIndex++] << bitsInBuffer);
bitsInBuffer += 8;
}
rcChannels[i] = bitBuffer & 0x07FF; // 取低11位作为通道值
bitBuffer >>= 11;
bitsInBuffer -= 11;
}
// 打印解析出的各个通道原始值和转换后的PWM值
// Serial.println("CRSF Channels Received:");
// for (int i = 0; i < CRSF_MAX_CHANNEL; i++) {
// Serial.print("CH");
// Serial.print(i + 1);
// Serial.print(": ");
// Serial.print(rcChannels[i]); // 原始CRSF值 (172-1811)
// Serial.print(" (PWM: ");
// Serial.print(convertCrsfToPwm(rcChannels[i]));
// Serial.println("us)");
// }
}
// 从串口接收并解析CRSF数据包
void receiveCRSF() {
static uint8_t rxBuffer[CRSF_MAX_FRAME_SIZE];
static uint8_t rxPos = 0;
static uint8_t expectedLength = 0;
while (crsfSerial.available()) {
uint8_t byteReceived = crsfSerial.read();
if (rxPos == 0) { // 等待同步字节
if (byteReceived == CRSF_SYNC_BYTE) {
rxBuffer[rxPos++] = byteReceived;
}
} else if (rxPos == 1) { // 接收长度字节
if (byteReceived >= (CRSF_MIN_FRAME_SIZE - 2) && byteReceived <= (CRSF_MAX_FRAME_SIZE - 2) ) { // 长度字节范围校验 (Type+Payload+CRC)
rxBuffer[rxPos++] = byteReceived;
expectedLength = byteReceived + 2; // 完整帧长度 = Length_Field + SyncByte + LengthByte
} else { // 无效的长度字节,重置
rxPos = 0;
}
} else { // 接收剩余数据 (Type, Payload, CRC)
rxBuffer[rxPos++] = byteReceived;
if (rxPos == expectedLength) { // 接收到完整帧
// CRC校验范围: 从Type字节(rxBuffer[2])到Payload结束
// CRC本身位于帧尾 rxBuffer[expectedLength-1]
// 校验的数据长度 = expectedLength - 3 (排除Sync, Length, CRC本身)
// 或者等于 Length_Field - 1 (排除CRC本身)
uint8_t calculatedCRC = calcCRC(&rxBuffer[2], expectedLength - 3);
if (calculatedCRC == rxBuffer[expectedLength - 1]) {
// CRC校验成功,根据类型处理数据包
uint8_t frameType = rxBuffer[2];
const uint8_t *payload = &rxBuffer[3]; // 负载数据从第3个字节开始
// uint8_t payloadLength = expectedLength - 4; // Sync, Length, Type, CRC
if (frameType == CRSF_FRAMETYPE_RC_CHANNELS_PACKED) {
processCRSFRcChannelsPacket(payload);
} else {
// 在此处理其他类型的CRSF帧
// Serial.print("Received CRSF Frame Type: 0x");
// Serial.println(frameType, HEX);
}
} else {
// Serial.println("CRC Error!");
}
rxPos = 0; // 重置缓冲区,准备接收下一帧
} else if (rxPos >= CRSF_MAX_FRAME_SIZE) { // 缓冲区溢出,通常不应发生
rxPos = 0;
}
}
}
}
void setup() {
// 初始化调试串口
Serial.begin(115200); // 用于打印调试信息
while(!Serial); // 等待串口连接 (某些板子需要)
Serial.println("CRSF Parser Initializing...");
// 初始化CRSF串口
initCRSFSerial();
Serial.println("CRSF Serial Initialized at 420000bps.");
}
void loop() {
receiveCRSF(); // 循环接收并解析CRSF数据包
// 可以在这里使用解析到的 rcChannels[] 数据
// 例如,每秒打印一次通道1的值
// static unsigned long lastPrintTime = 0;
// if (millis() - lastPrintTime > 1000) {
// lastPrintTime = millis();
// Serial.print("CH1 (Raw): ");
// Serial.print(rcChannels[0]);
// Serial.print(" (PWM): ");
// Serial.println(convertCrsfToPwm(rcChannels[0]));
// }
}
10. 代码说明 (解析示例)
10.1. initCRSFSerial() 和 calcCRC()
这两个函数与发送部分的实现相同。
10.2. convertCrsfToPwm()
一个辅助函数,用于将CRSF的原始11位通道值(172-1811)线性映射到标准的PWM脉宽值(例如988-2012µs)。这对于在飞控中直接使用或调试非常有用。
10.3. processCRSFRcChannelsPacket()
这是解析CRSF遥控通道数据包负载的核心函数。它接收指向负载数据区的指针,并从中提取16个11位的通道值,存储到全局数组 rcChannels[] 中。
10.4. receiveCRSF()
该函数是CRSF协议的接收状态机:
- 等待同步字节 (
CRSF_SYNC_BYTE):如果接收到的第一个字节不是同步字节,则忽略。 - 接收长度字节:第二个字节是长度字段,它决定了期望接收的完整帧的长度。对长度字节进行范围校验。
- 接收剩余数据:继续接收类型、负载和CRC字节,直到达到期望的帧长度。
- CRC校验:当接收到完整帧后,计算从**类型(Type)字节到负载(Payload)**结束的CRC值,并与接收到的CRC字节进行比较。
- 处理数据包:如果CRC校验通过,则根据帧类型调用相应的处理函数(例如
processCRSFRcChannelsPacket())。 - 重置状态:处理完一帧或发生错误后,重置接收状态,准备接收下一帧。
10.5. setup() 和 loop()
setup() 初始化调试串口和CRSF通信串口。loop() 持续调用 receiveCRSF() 来检查和处理传入的CRSF数据。
11. 测试与验证 (解析示例)
将ELRS接收机的CRSF输出连接到STM32配置的crsfSerial(例如TX引脚连接到STM32的RX2)。上传代码后,打开Arduino IDE的串口监视器(设置为115200波特率),你应该能看到解析出的遥控通道数据。操作遥控器的摇杆和开关,观察串口监视器上对应通道值的变化,以验证解析是否正确。
12. 结论
本文档提供了一个基于STM32duino的CRSF协议发送与接收的模块化实现。通过理解CRSF协议的帧结构、CRC校验机制以及通道数据的打包/解包方式,开发者可以在自己的飞控项目中有效地集成ELRS或其他CRSF兼容设备。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)