套件介绍

Robomaster中遥控机器人的手段是固定的,只能使用大疆提供的DR16&DT7套件进行操控数据的发送和接受。这个套件的手册可以在Robomaster的官网上下载到,里面有详细的说明,以及官方给的解码demo。

https://www.robomaster.com/zh-CN/products/components/detail/122

操控数据可以通过DT7遥控器直接发送,也可以将遥控器通过数据线连接到电脑上,然后打开Robomaster客户端操作界面,遥控器会将客户端内的键鼠操作发送给接收器。

使用DT7&DR16套件前需要保证接收机和遥控器之间已经成功配对。

DR16上的LED指示灯一般有三种状态:红灯常亮,绿灯闪烁,绿灯常亮,对应的状态如下:

指示灯状态 对应状态
红灯常亮 未检测到遥控器
绿灯闪烁 检测到遥控器,但未配对
绿灯常亮 已与遥控器配对

配对方法为打开需要配对的遥控器和接收机,长按对频按键10s左右,然后松开。

套件参数如下,通信距离足足可以达到1km,实际使用中也确实很稳,DJI还是牛逼的。但是需要注意的是DT7遥控器很不耐摔,天线也容易断,切记要保护好。

DBUS协议

接收机与接收机之间采用DBUS协议进行通信,这个DBUS协议和常用的SBUS协议应该是一样的东西,不知道为什么要改个名。

信号电平为TTL电平,但是和UART是反相的,需要过一个反相器再输入到单片机的串口上进行接收。这个反相器可以自己搭,一个三极管加两个电阻就可以了。

接收时对照着参数表进行串口的参数设置。

当然也可以直接连接Robomaster的开发板上专门留给DR16接收机的接口,一般是串口1,直接将接收机上引出的线对应连接即可,接口内置了反相器。DBUS与UART之间电平标准为反相关系,所以不能随意连接到其他串口。

数据帧解析

下面开始说重头戏——遥控器发送给接收机的数据帧。在实际使用时,我们需要将接收到的数据流进行解码,将接收到的数据流“翻译”成对应的遥控器/键盘/鼠标的数值。DBUS数据以18个字节为一帧.

可以将接收机通过反相器,接给USB转TTL模块,然后接到PC上,开串口助手调成16进制显示看一下。

一个典型的数据帧长下面这样

00 04 20 00 01 78 00 00 00 00 00 00 00 00 00 00 00 00

将其对应到官方给的手册上,很容易就可以理解数据帧的含义。

以通道0的数值为例,将上面的数据帧的前两个字节写成二进制的形式,得到

0000 0000 0000 0100

按照规则,将第二个字节的后3位作为高八位,第一个字节整体作为低8位拼接起来,即可得到

010 0000 0000

这个二进制数转化成十进制后,得到的值恰好就是2的10次方,即1024。

通过同样的方式可以分离-拼接出所有的数据值,实际上我们的遥控器解码函数所作的也正是这个工作。

DMA接收&&解码示例

这里以官方开源代码为例来讲解遥控器接收+解码的过程。

https://github.com/RoboMaster/RoboRTS-Firmware

在dbus.c下面,我们可以看到官方的解码函数,其完成的工作包括——数据的分离和拼接;为了防止遥控器数据的零漂,设置了一个正负5的死区;数据溢出的处理

static void get_dr16_data(rc_device_t rc_dev, uint8_t *buff)
{

  memcpy(&(rc_dev->last_rc_info), &rc_dev->rc_info, sizeof(struct rc_info));

  rc_info_t rc = &rc_dev->rc_info;
  
  //satori:这里完成的是数据的分离和拼接,减去1024是为了让数据的中间值变为0
  rc->ch1 = (buff[0] | buff[1] << 8) & 0x07FF;
  rc->ch1 -= 1024;
  rc->ch2 = (buff[1] >> 3 | buff[2] << 5) & 0x07FF;
  rc->ch2 -= 1024;
  rc->ch3 = (buff[2] >> 6 | buff[3] << 2 | buff[4] << 10) & 0x07FF;
  rc->ch3 -= 1024;
  rc->ch4 = (buff[4] >> 1 | buff[5] << 7) & 0x07FF;
  rc->ch4 -= 1024;
  
  //satori:防止数据零漂,设置正负5的死区
  /* prevent remote control zero deviation */
  if(rc->ch1 <= 5 && rc->ch1 >= -5)
    rc->ch1 = 0;
  if(rc->ch2 <= 5 && rc->ch2 >= -5)
    rc->ch2 = 0;
  if(rc->ch3 <= 5 && rc->ch3 >= -5)
    rc->ch3 = 0;
  if(rc->ch4 <= 5 && rc->ch4 >= -5)
    rc->ch4 = 0;
  
  rc->sw1 = ((buff[5] >> 4) & 0x000C) >> 2;
  rc->sw2 = (buff[5] >> 4) & 0x0003;
  
  //satori:防止数据溢出
  if ((abs(rc->ch1) > 660) || \
      (abs(rc->ch2) > 660) || \
      (abs(rc->ch3) > 660) || \
      (abs(rc->ch4) > 660))
  {
    memset(rc, 0, sizeof(struct rc_info));
    return ;
  }

  rc->mouse.x = buff[6] | (buff[7] << 8); // x axis
  rc->mouse.y = buff[8] | (buff[9] << 8);
  rc->mouse.z = buff[10] | (buff[11] << 8);

  rc->mouse.l = buff[12];
  rc->mouse.r = buff[13];

  rc->kb.key_code = buff[14] | buff[15] << 8; // key borad code
  rc->wheel = (buff[16] | buff[17] << 8) - 1024;
}

找到对应的数据结构rc_info,其内容和手册上的内容完全一一对应,可以看到官方代码为了方便后续代码的编写,在键盘的数据处使用了联合体。

struct rc_info
{
  /* rocker channel information */
  int16_t ch1;
  int16_t ch2;
  int16_t ch3;
  int16_t ch4;
  /* left and right lever information */
  uint8_t sw1;
  uint8_t sw2;
  /* mouse movement and button information */
  struct
  {
    int16_t x;
    int16_t y;
    int16_t z;

    uint8_t l;
    uint8_t r;
  } mouse;
  /* keyboard key information */
  union {
    uint16_t key_code;
    struct
    {
      uint16_t W : 1;
      uint16_t S : 1;
      uint16_t A : 1;
      uint16_t D : 1;
      uint16_t SHIFT : 1;
      uint16_t CTRL : 1;
      uint16_t Q : 1;
      uint16_t E : 1;
      uint16_t R : 1;
      uint16_t F : 1;
      uint16_t G : 1;
      uint16_t Z : 1;
      uint16_t X : 1;
      uint16_t C : 1;
      uint16_t V : 1;
      uint16_t B : 1;
    } bit;
  } kb;
  int16_t wheel;
};

关于遥控器数据的接收,我们可以直接去寻找解码函数在何处被调用,按照

get_dr16_data -> rc_device_date_update -> dr16_rx_data_by_uart -> dr16_rx_callback -> dr16_uart_rx_data_handle -> USART1_IRQHandler

可以发现其是在串口1的接收中断中被调用的,DMA初始化函数如下

void dr16_uart_init(void)
{
  UART_Receive_DMA_No_IT(&huart1, dr16_uart_rx_buff, DR16_RX_BUFFER_SIZE);
  
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

接收处理函数如下

抛除一些HAL库为了驱动DMA进行的一些寄存器操作,其实我们可以看到整个DMA接收逻辑实际上还是很简单的,即通过dr16_uart_rx_buff这个数组存储DMA获取的数据,每次完成接收之后对接收数据的长度进行判断,如果确认了是18个字节则判定为合法数据,传入dr16_rx_callback进行下一步的处理。

虽然官方的代码为了各种目的进行了很多层的封装,但是实际上在dma接收处理函数中进行数据合法性的判断之后,已经可以直接将数据送入解码函数了。

uint32_t dr16_uart_rx_data_handle(UART_HandleTypeDef *huart)
{
  if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE))
  {
    /* clear idle it flag avoid idle interrupt all the time */
    __HAL_UART_CLEAR_IDLEFLAG(huart);

    /* clear DMA transfer complete flag */
    __HAL_DMA_DISABLE(huart->hdmarx);

    /* handle dbus data dbus_buf from DMA */
    if ((DR16_RX_BUFFER_SIZE - huart->hdmarx->Instance->NDTR) == DR16_DATA_LEN)
    {
      if (dr16_rx_callback != NULL)
      {
        dr16_rx_callback(dr16_uart_rx_buff, DR16_DATA_LEN);
      }

      if (dr16_forword_callback != NULL)
      {
        dr16_forword_callback(dr16_uart_rx_buff, DR16_DATA_LEN);
      }
    }

    /* restart dma transmission */
    __HAL_DMA_SET_COUNTER(huart->hdmarx, DR16_RX_BUFFER_SIZE);
    __HAL_DMA_ENABLE(huart->hdmarx);
  }
  return 0;
}

结语

那么本讲就到此为之了,实际上DR16&DT7遥控器套件接收是一个很容易的工作,需要注意的是由于机器人对于任务执行的实时性需求,一般不会采用耗时长,占用资源多的串口中断的方式进行接收,而是使用DMA方式进行接收。