套件介绍

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

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

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

java对接大疆无人机 大疆无人机配对_串口

java对接大疆无人机 大疆无人机配对_java对接大疆无人机_02

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

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

指示灯状态

对应状态

红灯常亮

未检测到遥控器

绿灯闪烁

检测到遥控器,但未配对

绿灯常亮

已与遥控器配对

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

java对接大疆无人机 大疆无人机配对_数据帧_03

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

java对接大疆无人机 大疆无人机配对_java对接大疆无人机_04

DBUS协议

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

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

java对接大疆无人机 大疆无人机配对_java对接大疆无人机_05

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

java对接大疆无人机 大疆无人机配对_串口_06

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

java对接大疆无人机 大疆无人机配对_数据帧_07

数据帧解析

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

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

java对接大疆无人机 大疆无人机配对_RoboMaster电控入门_08

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

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

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

java对接大疆无人机 大疆无人机配对_串口_09

java对接大疆无人机 大疆无人机配对_java对接大疆无人机_10

java对接大疆无人机 大疆无人机配对_数据帧_11

以通道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方式进行接收。