目录
一、引言
1.传统的单缓冲区和双缓冲区面临的问题
2.基于单缓冲区的定长帧循环快照法
二、准备工作
1.大疆DT7遥控器&DR16接收机
2.DBUS协议
3.DR16数据帧解析
三、开发过程
1.cubemx配置
2.代码实现和调试
四、结语
定长帧循环快照法的适用范围
一、引言
1.传统的单缓冲区和双缓冲区面临的问题
在 RoboMaster 电控开发中,读取遥控器数据是最高频、最基础的操作之一。DT7 遥控器通过 DR16 接收机输出 DBUS 协议信号,STM32 需要以 100kbps 的波特率、每帧 18 字节、约 14ms 一帧的速度持续接收数据。如果处理不当,这个看似简单的任务就会给CPU 带来沉重负担甚至接收不到数据。在编者的学习过程中接触过两种DBUS接收实现方式,但在我看来它们都存在一些问题
首先是传统的单缓冲区方案:它采用了“DMA + 空闲中断”的思路:定义一个18字节的数组用来接收,DR16 发完一帧数据(18字节)触发空闲中断,此时失能DMA停止接收的同时解析数据,解析完成后清空数组再使能DMA开启接收。这个方案看似逻辑严密:它为防止新数据覆盖正在解析的一帧,选择关闭DMA停止接收数据,解析完成后再重新接收。但它存在一个致命问题:数据的发送并不会因为DMA的关闭而停止。在数据解析的时间里若干个字节的数据将会丢失。而由于数组的大小仅仅正好是一帧数据的大小,后续收到的每一帧数据都会错位。这将意味着这种方案在一次丢帧后将永远不会再收到完整的一帧。
双缓冲区方案解决了上述问题。它的核心思路是用两个缓冲区轮流接客,一个给 DMA 写,一个给 CPU 读,交替使用,互不干扰。DMA不停,数据也将被完整的接收。但编者认为它仍然存在一些问题:
1.双缓冲区方案本质上是硬件解决方法,对硬件资源要求高,部分芯片不支持。这意味着它的移植性一般。
2.内存占用大。
3.只能缓存两帧,一旦数据错位甚至无法回溯,容错小
4.调试维护难度大
2.基于单缓冲区的定长帧循环快照法
综上所述,本文提供一种巧妙的新方案。它只需一个缓冲区且能做到不停用DMA。编者将它命名为“定长帧循环快照法,下面简述本方案逻辑:
用一块能容纳 5 帧 DBUS 数据的大缓冲区作为 “循环跑道”,DMA 硬件全程不停歇地循环往里写数据;每收到完整一帧就触发串口空闲中断,我们通过 DMA 硬件计数器精准定位刚写完的帧位置,把这 18 字节完整 “快照拷贝” 出来再解析(经编者查阅,memcpy拷贝的执行速度是微秒级,不影响数据接收的实时性)。全程 DMA 不停车、无接收空档,用极简单的逻辑实现了高可靠的单缓冲接收。
二、准备工作
1.大疆DT7遥控器&DR16接收机
DT7 遥控器是一款工作于2.4 GHz 频段的无线电通信设备,该遥控器仅能与DR16 接收机配合使用。DR16 接收机是一款工作频率为2.4 GHz的16通道接收机,可配合DT7遥控器使用。
配对方法为开启需要配对的遥控器,靠近正常工作的DR16,长按对频按键5s左右,然后松开,当指示灯常亮绿灯时便代表连接成功。(连接时确保周围没有其它开启的遥控器,防止误连)
2.DBUS协议
接收机与接收机之间采用DBUS协议进行通信,信号电平为TTL电平,但是和UART是反相的,但是可以直接连接 RobomasterC型开发板上的DBUS接口(即USART3),因为接口内置了反相器。DBUS与UART之间电平标准为反相关系,因而不能随意连接到其他串口。
当我们后续配置cubemx串口参数时,按照DBUS通信参数进行配置
3.DR16数据帧解析
三、开发过程
1.cubemx配置
芯片选择C板使用的STM32F407IGH6,本文略去常规配置操作仅展示关键步骤
DBUS接口即USART3
串口模式选择异步模式;参数对照上文DBUS通信参数填入(100k8E1)
开启中断
为串口3添加一个DMA通道(注意是接收通道rx而不是发送);模式设置为轮询模式;优先级设置为高
2.代码实现和调试
我们先处理头文件,大致了解一下框架
宏定义。这一步我们给串口定义了用户标签;定义了一帧为18;定义了缓冲区大小为5帧
#defineDBUS_HUARThuart3#defineDBUS_BUFLEN18U// 标准DBUS帧长#defineDBUS_BUF_SIZE90U// 循环缓冲区总大小,可存5帧,50ms容错窗口定义一个结构体,并把它初始化,保证每个通道上电时默认是0
由上文“DR16数据帧解析”可知,左右摇杆是11bit;左右拨杆是2bit。所以我们摇杆通道使用int16_t;拨杆通道使用uint8_t。留有一定内存余量但不至于浪费
typedefstruct{int16_tch0;// 右摇杆水平int16_tch1;// 右摇杆垂直int16_tch2;// 左摇杆水平int16_tch3;// 左摇杆垂直uint8_tsw1;// 右拨杆uint8_tsw2;// 左拨杆}rc_info_t;#definerc_Init{0,0,0,0,0,0}声明两个我们后续要用的变量
externrc_info_trc;externvolatileuint8_tdbus_rx_flag;// 一帧接收完成标志声明两个我们后续要调用的函数
voiddbus_uart_init(void);voiduart_receive_handler(UART_HandleTypeDef*huart);接下来我们处理源文件
把接收缓冲区初始化为0;初始化结构体rc;dbus_rx_flag是同步标志位,空闲中断里解析完一帧数据后置1,主循环检测到1就消费数据并清零
uint8_tdbus_rx_buf[DBUS_BUF_SIZE]={0};rc_info_trc=rc_Init;volatileuint8_tdbus_rx_flag=0U;DBUS 串口初始化函数
voiddbus_uart_init(void){// 清除空闲标志,避免上电误触发__HAL_UART_CLEAR_IDLEFLAG(&DBUS_HUART);// 开启空闲中断__HAL_UART_ENABLE_IT(&DBUS_HUART,UART_IT_IDLE);// 用HAL标准函数启动DMA循环接收,DMA全程不停HAL_UART_Receive_DMA(&DBUS_HUART,dbus_rx_buf,DBUS_BUF_SIZE);}DBUS协议解析函数。其中复杂解算过程直接复制粘贴即可
staticvoidrc_callback_handler(rc_info_t*rc,uint8_t*buff){// 摇杆通道解析,11位有效位,中点1024rc->ch0=(int16_t)(((uint16_t)buff[0]|((uint16_t)buff[1]<<8))&0x07FFU)-1024;rc->ch1=(int16_t)(((uint16_t)(buff[1]>>3)|((uint16_t)buff[2]<<5))&0x07FFU)-1024;rc->ch2=(int16_t)(((uint16_t)(buff[2]>>6)|((uint16_t)buff[3]<<2)|((uint16_t)buff[4]<<10))&0x07FFU)-1024;rc->ch3=(int16_t)(((uint16_t)(buff[4]>>1)|((uint16_t)buff[5]<<7))&0x07FFU)-1024;rc->sw1=(buff[5]>>6)&0x03U;// bit6~bit7rc->sw2=(buff[5]>>4)&0x03U;// bit4~bit5// 异常校验:DR16满杆约±660,超出则判定为数据错乱if((abs(rc->ch0)>660)||(abs(rc->ch1)>660)||(abs(rc->ch2)>660)||(abs(rc->ch3)>660)){memset(rc,0,sizeof(rc_info_t));//清零}}核心代码:空闲中断回调函数
1.第一件事一定要清除空闲标志,若不清除硬件持续触发中断,主循环无法运行
2.串口过滤判断是不是UART3,若不是需过滤掉,以免影响DBUS解析
3.计算写指针位置
DMA 硬件有一个NDTR寄存器,记录 “剩余可写字节数”。用缓冲区总长度减去剩余数量,就能算出 DMA 当前写到了缓冲区的第几个字节,这个位置就是刚写完的那一帧的末尾。
4.快照取完整帧,分正常情况和回卷情况两种
5.调用协议解析函数,然后把dbus_rx_flag置为1,通知主循环新一帧的数据已就绪
staticvoiduart_rx_idle_callback(UART_HandleTypeDef*huart){__HAL_UART_CLEAR_IDLEFLAG(huart);//第一件事先清除串口空闲标志if(huart!=&DBUS_HUART){return;}// 计算当前写指针位置// NDTR = 剩余可写字节数,总大小 - 剩余 = 已写入位置uint16_twrite_pos=DBUS_BUF_SIZE-__HAL_DMA_GET_COUNTER(huart->hdmarx);uint8_tframe_buf[DBUS_BUFLEN];//从循环缓冲区快照出完整一帧if(write_pos>=DBUS_BUFLEN){// 正常情况:帧未跨缓冲区首尾,直接倒推18字节拷贝memcpy(frame_buf,&dbus_rx_buf[write_pos-DBUS_BUFLEN],DBUS_BUFLEN);}else{// 回卷情况:帧跨了缓冲区结尾和开头,分两段拼接uint8_ttail_len=write_pos;uint8_thead_len=DBUS_BUFLEN-tail_len;// 先拷缓冲区尾部的部分memcpy(frame_buf,&dbus_rx_buf[DBUS_BUF_SIZE-head_len],head_len);// 再拷缓冲区开头的部分memcpy(frame_buf+head_len,dbus_rx_buf,tail_len);}// 帧长校验 + 解析// 空闲中断触发=一帧结束,正常帧长必为18字节,校验通过才解析// 这里额外校验是防止干扰、误触发导致的错误解析rc_callback_handler(&rc,frame_buf);dbus_rx_flag=1U;}中断入口分发函数。检查是否是有效的中断空闲事件,避免其他串口中断事件触发
voiduart_receive_handler(UART_HandleTypeDef*huart){// 同时检查标志位和中断使能,确保是有效空闲中断if(__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE)&&__HAL_UART_GET_IT_SOURCE(huart,UART_IT_IDLE)){uart_rx_idle_callback(huart);}}源文件处理完毕,不要忘了挂接串口中断:
打开stm32f4xx_it.c,找到 USART3_IRQHandler 函数做如下修改,同时别忘了引用我们前文写的头文件
最后处理主函数main.c,记得引用头文件
调用DBUS串口初始化函数,启动接收
主循环处理如下,先清零、后处理,防止处理期间新来的帧被覆盖丢失
至此代码实现部分结束
调试指南:
实验现象:监视结构体“rc”,推动摇杆,对应通道数值同步变化
若排查中断工作:监视dbus_rx_flag,数值在0和1之间快速跳转即中断正常触发;恒为0说明中断未能触发;恒为1说明主循环未能正常消费,可能是主循环卡死
若排查接收线路:监视原始数组dbus_rx_buf(十六进制查看),若有数据被接收,说明数据解算部分有问题;若没有数据,继续往初始配置和硬件方面排查(波特率、校验位、DMA、接线、设备工作状态等等)
四、结语
本文提供的定长帧循环快照法以循环 DMA + 串口空闲中断为核心,用一块大缓冲区实现 5 帧数据容错,DMA全程不停,解决了漏接数据的问题,非常适合DBUS这类定长帧协议。同时逻辑简单,非常易于理解,调试也很简易。但是定长帧循环快照法仍然不适用于以下几种场景:
1.不适合实时性要求极高的场景。定长帧循环快照法在回卷情况下需要拷贝两次,持续占用CPU时间。而双缓冲区方案无需面临这一问题
2.不适合不定长协议,兼容性远弱于双缓冲方案。一旦一帧的长度不固定,本方案完全失效
3.不适用串口需双向通信的场景
编者rm电控小登一枚,能力有限请多多批评指正