本人是参加过第二十四届,第二十五届RoboCon比赛的参赛队员,本篇文章分享今年我们机器人用的遥控器与SBUS通信代码。
发布这篇文章前所学习研究的文章:
STM32 Futaba SBUS协议解析:STM32 Futaba SBUS协议解析_接收机与bus-CSDN博客
Robomaster大疆遥控器DT7&DR16介绍与C型开发板通信教程:Robomaster大疆遥控器DT7&DR16介绍与C型开发板通信教程-CSDN博客
一.遥控器介绍
我们机器人采用的是HotRC的 HT-10A十通道遥控器+SBUS接收机,接收机自带反向电平电路。
遥控器除了两个摇杆以外,还有两个三位开关与两个二位开关,还有两个旋钮,通道丰富;遥控器中间下方有个1.7寸的彩屏,能够直接看到各个通道发送的数值,便于操作;最重要的是便宜!遥控器+SBUS接收机总共不超过200元。遥控器支持2S锂电池供电或者四节5号电池供电 (个人推荐使用锂电池,续航能力更好一些)。注意!遥控器很不耐摔,切记要保护好!
二.工程配置
1.CubeMX配置
我们采用的是RoboMasterC型开发板的串口3,因为串口3本身带有反向电平电路,所以在接收机与串口之间又加了个反向器,本不用这么麻烦,但我们资源有限所以是无奈之举,大家选用普通串口就好。
配置串口3如图所示并开启串口3NVIC
然后开启DMA接收通道,采用轮询(Circular)
2.头文件解析
是整个驱动的对外门面,统一管理协议常量、数据结构与 API 声明,业务层仅需包含此头文件即可使用全部功能,完全屏蔽底层解码与 DMA 实现细节。
2.1 宏定义
在.h文件中我们定义协议核心常量宏,同时定义遥控器量程映射的宏,将 SBUS 原始 200~1800 的通道值,线性映射为 1000~2000 标准量程
//协议基础常量 #define MAX_SBUS_CHANNEL 16 // 最大支持通道数 #define SBUS_FRAME_SIZE 25 // SBUS 单帧固定字节长度 #define SBUS_START_SYMBOL 0x0f // 帧头固定标识 0x0F #define SBUS_INPUT_CHANNELS 16 // 标准通道数量 #define SBUS_FLAGS_BYTE 23 // 标志位所在字节索引(第24字节) #define SBUS_FAILSAFE_BIT 3 // 失控保护标志位 #define SBUS_FRAMELOST_BIT 2 // 丢帧标志位 //量程映射 #define SBUS_RANGE_MIN 200.0f #define SBUS_RANGE_MAX 1800.0f #define SBUS_TARGET_MIN 1000.0f #define SBUS_TARGET_MAX 2000.0f #define SBUS_SCALE_FACTOR ((SBUS_TARGET_MAX - SBUS_TARGET_MIN) / (SBUS_RANGE_MAX - SBUS_RANGE_MIN)) #define SBUS_SCALE_OFFSET (int)(SBUS_TARGET_MIN - (SBUS_SCALE_FACTOR * SBUS_RANGE_MIN + 0.5f))配置DMA 缓冲区大小
// 循环 DMA 接收缓冲区大小需大于单次轮询间隔的最大接收字节数; // SBUS 波特率 100000 下每秒约 8333 字节,10ms 轮询间隔最多约 83 字节,因此取 128 字节留有充足余量 #define SBUS_DMA_BUF_SIZE 1282.2核心数据结构
配置解码状态枚举 SBUS_DECODE_STATE,定义有限状态机的全部状态,状态机用于同步帧,解决乱码、断帧、不同步问题
typedef enum { SBUS_DECODE_STATE_DESYNC = 0xFFF, // 失同步,寻找帧头 SBUS_DECODE_STATE_SBUS_START = 0x2FF,// 找到帧头,开始接收 SBUS_DECODE_STATE_SBUS1_SYNC = 0x00, // SBUS1 标准帧同步 SBUS_DECODE_STATE_SBUS2_SYNC = 0x1FF,// SBUS2 扩展帧同步 SBUS_DECODE_STATE_SBUS2_RX_VOLTAGE = 0x04, // SBUS2 电压帧 SBUS_DECODE_STATE_SBUS2_GPS = 0x14, // SBUS2 GPS 帧 SBUS_DECODE_STATE_SBUS2_DATA1 = 0x24, // 预留拓展,当前版本未实现 SBUS_DECODE_STATE_SBUS2_DATA2 = 0x34 // 预留拓展,当前版本未实现 } SBUS_DECODE_STATE;配置对外输出结构体sbus_info_t获取通道值和内部解析器结构体sbus_decoder_t存储解码全部中间状态、缓存、统计信息
typedef struct { // 摇杆基础通道 int16_t ch1; int16_t ch2; int16_t ch3; int16_t ch4; // 拨杆开关 int16_t tsw1; int16_t ssw1; int16_t ssw2; int16_t tsw2; // 旋钮通道 int16_t roll_1; int16_t roll_2; int16_t ch11; int16_t ch12; int16_t ch13; int16_t ch14; int16_t ch15; int16_t ch16; uint8_t frame_lost; // 丢帧标志 uint8_t failsafe; // 失控保护标志 } __attribute__((packed)) sbus_info_t;typedef struct { uint16_t rc_count; // 当前解析出的有效通道数 uint16_t max_channels; // 最大通道数配置 bool sbus_failsafe; // 失控保护标志 bool sbus_frame_drop; // 当前帧丢帧标志 uint32_t sbus_frame_drops; // 累计丢帧计数 uint32_t partial_frame_count; // 当前已接收的帧字节数 uint32_t last_rx_time; // 上次收到字节的时间戳 uint32_t last_frame_time; // 上次解析到完整帧的时间戳 bool sbus_data_ready; // 有效数据就绪标志 SBUS_DECODE_STATE sbus_decode_state; // 状态机当前状态 uint8_t sbus_frame[SBUS_FRAME_SIZE + (SBUS_FRAME_SIZE / 2)]; // 帧接收缓存 uint16_t sbus_val[MAX_SBUS_CHANNEL]; // 映射后的通道值 } sbus_decoder_t;三,SBUS代码解析
SBUS 协议最核心的特点是16 个 11bit 通道连续打包在 22 字节中,每个通道的数据都会跨 2~3 个字节,不存在字节对齐,因此解码的本质就是按位拆分、拼接还原。
3.1预定义位提取表
我们设计一段纯静态常量数组,也是整个解码逻辑的 “规则手册,采用预计算好的位提取规则表,每个通道对应 3 组参数,解码时只需循环查表、移位、按位或,即可拼接出完整的 11bit 原始值
struct sbus_bit_pick { uint8_t byte; // 目标 bit 位于帧数据区的第几个字节 uint8_t rshift; // 取出字节后,向右移位的位数 uint8_t mask; // 位掩码,用于截取有效 bit uint8_t lshift; // 截取后向左移位的位数,用于拼接到最终结果 }; static const struct sbus_bit_pick sbus_decoder[SBUS_INPUT_CHANNELS][3] = { { { 0, 0, 0xff, 0 }, { 1, 0, 0x07, 8 }, { 0, 0, 0x00, 0 } }, { { 1, 3, 0x1f, 0 }, { 2, 0, 0x3f, 5 }, { 0, 0, 0x00, 0 } }, { { 2, 6, 0x03, 0 }, { 3, 0, 0xff, 2 }, { 4, 0, 0x01, 10 } }, { { 4, 1, 0x7f, 0 }, { 5, 0, 0x0f, 7 }, { 0, 0, 0x00, 0 } }, { { 5, 4, 0x0f, 0 }, { 6, 0, 0x7f, 4 }, { 0, 0, 0x00, 0 } }, { { 6, 7, 0x01, 0 }, { 7, 0, 0xff, 1 }, { 8, 0, 0x03, 9 } }, { { 8, 2, 0x3f, 0 }, { 9, 0, 0x1f, 6 }, { 0, 0, 0x00, 0 } }, { { 9, 5, 0x07, 0 }, { 10, 0, 0xff, 3 }, { 0, 0, 0x00, 0 } }, { { 11, 0, 0xff, 0 }, { 12, 0, 0x07, 8 }, { 0, 0, 0x00, 0 } }, { { 12, 3, 0x1f, 0 }, { 13, 0, 0x3f, 5 }, { 0, 0, 0x00, 0 } }, { { 13, 6, 0x03, 0 }, { 14, 0, 0xff, 2 }, { 15, 0, 0x01, 10 } }, { { 15, 1, 0x7f, 0 }, { 16, 0, 0x0f, 7 }, { 0, 0, 0x00, 0 } }, { { 16, 4, 0x0f, 0 }, { 17, 0, 0x7f, 4 }, { 0, 0, 0x00, 0 } }, { { 17, 7, 0x01, 0 }, { 18, 0, 0xff, 1 }, { 19, 0, 0x03, 9 } }, { { 19, 2, 0x3f, 0 }, { 20, 0, 0x1f, 6 }, { 0, 0, 0x00, 0 } }, { { 20, 5, 0x07, 0 }, { 21, 0, 0xff, 3 }, { 0, 0, 0x00, 0 } } };3.2完整SBUS帧解码
定义sbus_decode 函数,其将会对一帧收齐的 25 字节 SBUS 数据,完成帧合法性校验、通道值拼接、量程映射与标志位解析
static bool sbus_decode(sbus_decoder_t* decoder, uint32_t frame_time) { if ((decoder->sbus_frame[0] != SBUS_START_SYMBOL)) { decoder->sbus_frame_drops++; decoder->sbus_decode_state = SBUS_DECODE_STATE_DESYNC; return false; } switch (decoder->sbus_frame[24]) { case 0x00: decoder->sbus_decode_state = SBUS_DECODE_STATE_SBUS1_SYNC; break; case 0x04: decoder->sbus_decode_state = SBUS_DECODE_STATE_SBUS2_RX_VOLTAGE; break; case 0x14: decoder->sbus_decode_state = SBUS_DECODE_STATE_SBUS2_GPS; break; case 0x24: case 0x34: decoder->sbus_decode_state = SBUS_DECODE_STATE_SBUS2_SYNC; break; default: decoder->sbus_decode_state = SBUS_DECODE_STATE_DESYNC; return false; } decoder->last_frame_time = frame_time; unsigned chancount = (decoder->max_channels > SBUS_INPUT_CHANNELS) ? SBUS_INPUT_CHANNELS : decoder->max_channels; for (unsigned channel = 0; channel < chancount; channel++) { unsigned value = 0; for (unsigned pick = 0; pick < 3; pick++) { const struct sbus_bit_pick* decode = &sbus_decoder[channel][pick]; if (decode->mask != 0) { unsigned piece = decoder->sbus_frame[1 + decode->byte]; piece >>= decode->rshift; piece &= decode->mask; piece <<= decode->lshift; value |= piece; } } decoder->sbus_val[channel] = (uint16_t)(value * SBUS_SCALE_FACTOR + .5f) + SBUS_SCALE_OFFSET; } decoder->rc_count = chancount; if (decoder->sbus_frame[SBUS_FLAGS_BYTE] & (1 << SBUS_FAILSAFE_BIT)) { decoder->sbus_failsafe = true; decoder->sbus_frame_drop = true; } else if (decoder->sbus_frame[SBUS_FLAGS_BYTE] & (1 << SBUS_FRAMELOST_BIT)) { decoder->sbus_failsafe = false; decoder->sbus_frame_drop = true; } else { decoder->sbus_failsafe = false; decoder->sbus_frame_drop = false; } return true; }定义sbus_parse 函数,其会逐字节处理流式输入数据,通过有限状态机完成帧头搜索、数据拼接、解码触发与失步自动恢复功能
static bool sbus_parse(sbus_decoder_t* decoder, uint8_t* frame, unsigned len) { unsigned i; decoder->last_rx_time = HAL_GetTick(); bool decode_ret = false; for (i = 0; i < len; i++) { if (decoder->partial_frame_count == sizeof(decoder->sbus_frame) / sizeof(decoder->sbus_frame[0])) { decoder->partial_frame_count = 0; decoder->sbus_decode_state = SBUS_DECODE_STATE_DESYNC; } if (decoder->partial_frame_count == SBUS_FRAME_SIZE) { decoder->partial_frame_count = 0; decoder->sbus_decode_state = SBUS_DECODE_STATE_DESYNC; } switch (decoder->sbus_decode_state) { case SBUS_DECODE_STATE_DESYNC: if (frame[i] == SBUS_START_SYMBOL) { decoder->sbus_decode_state = SBUS_DECODE_STATE_SBUS_START; decoder->partial_frame_count = 0; decoder->sbus_frame[decoder->partial_frame_count++] = frame[i]; } break; case SBUS_DECODE_STATE_SBUS_START: case SBUS_DECODE_STATE_SBUS1_SYNC: case SBUS_DECODE_STATE_SBUS2_SYNC: { decoder->sbus_frame[decoder->partial_frame_count++] = frame[i]; if (decoder->partial_frame_count < SBUS_FRAME_SIZE) { break; } decode_ret = sbus_decode(decoder, decoder->last_rx_time); unsigned start_index = 0; if (!decode_ret && decoder->sbus_decode_state == SBUS_DECODE_STATE_DESYNC) { for (unsigned j = 1; j < decoder->partial_frame_count; j++) { if (decoder->sbus_frame[j] == SBUS_START_SYMBOL) { start_index = j; break; } } if (start_index != 0) { for (unsigned j = 0; j < decoder->partial_frame_count - start_index; j++) { decoder->sbus_frame[j] = decoder->sbus_frame[j + start_index]; } decoder->partial_frame_count -= start_index; decoder->sbus_decode_state = SBUS_DECODE_STATE_SBUS_START; } } if (start_index == 0) { decoder->partial_frame_count = 0; } } break; case SBUS_DECODE_STATE_SBUS2_RX_VOLTAGE: case SBUS_DECODE_STATE_SBUS2_GPS: decoder->sbus_frame[decoder->partial_frame_count++] = frame[i]; if (decoder->partial_frame_count == 1 && decoder->sbus_frame[0] == SBUS_START_SYMBOL) { decoder->sbus_decode_state = SBUS_DECODE_STATE_SBUS2_SYNC; } break; default: decode_ret = false; } } decoder->sbus_data_ready = decode_ret && !decoder->sbus_failsafe && !decoder->sbus_frame_drop; return decode_ret; }3.4初始化相关函数
static int sbus_decoder_init(sbus_decoder_t* decoder) { if (decoder == NULL) { return -1; } memset(decoder, 0, sizeof(sbus_decoder_t)); decoder->sbus_decode_state = SBUS_DECODE_STATE_DESYNC; decoder->max_channels = MAX_SBUS_CHANNEL; return 0; } // 绑定串口并启动循环 DMA 接收(内部使用)。 static void sbus_recv_start(UART_HandleTypeDef* huart) { s_sbus_huart = huart; s_sbus_rd_pos = 0; HAL_UART_Receive_DMA(huart, s_sbus_dma_buf, SBUS_DMA_BUF_SIZE); // 关掉 UART 的校验(PE)和错误(ERR: FE/NE/ORE)中断:SBUS 偶发坏字节是常态,关掉后 // 坏字节静默进缓冲,由 sbus_parse() 靠帧头/状态机过滤,DMA 永不被打断。 __HAL_UART_DISABLE_IT(huart, UART_IT_PE); __HAL_UART_DISABLE_IT(huart, UART_IT_ERR); // 使能 IDLE 空闲中断 __HAL_UART_CLEAR_IDLEFLAG(huart); __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); } // 初始化函数 void sbus_init(void) { sbus_decoder_init(&s_decoder); sbus_recv_start(&huart3); }在main.c中调用sbus_init()
在stm32f4xx_it.c中找到串口中断服务函数,使用串口DMA 空闲中断作为触发开关来调用轮询函数获取通道数据
void sbus_receive_callback(UART_HandleTypeDef* huart) {//用于检查UART接收状态并在接收到空闲状态时调用相应的回调函数 if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) && //检查UART是否设置了空闲标志,表示UART接收完成并进入空闲状态 __HAL_UART_GET_IT_SOURCE(huart, UART_IT_IDLE)) //检查UART空闲中断是否被使能,只有在中断使能的情况下,才会处理空闲状态 { __HAL_UART_CLEAR_IDLEFLAG(huart); //清除IDLE中断标志 sbus_recv_poll(); } }四.效果展示
五,最后
该文档仅供个人学习交流使用,不得用于其他用途,禁止商用, 转载或公开使用请联系作者授权。