In the RoboMaster competition, robot control is standardized around the DJI DR16 receiver and DT7 transmitter. The official documentation and demo code are available from the RoboMaster website. Communication can be initiated either directly via the DT7 remote or by connecting it to a PC and using the RoboMaster client software to forward keyboard and mouse inputs.
Before use, the receiver and transmitter must be paired. The DR16 LED indicates its state:
Pairing is done by powering on both devices and holding the pairing button on the receiver for about 10 seconds. The link range reaches 1 km, providing reliable control in typical environments.
Understanding the DBUS Protocol
The DR16 communicates using the DBUS protocol, which is functionally equivalent to SBUS but operates with an inverted TTL logic level compared to standard UART. Direct connection to a microcontroller UART requires a hardware inverter (a simple circuit with a transistor and two resistors) or the use of a dedicated port on the RoboMaster development board. Most official boards provide a connector for the DR16 that includes an onboard inverter, typically wired to USART1, so the serial lines can be connected directly without additional components.
Data Frame Structure
The transmitter sends a stream of 18-byte frames. Each frame encodes joystick channels, switches, mouse movements, and keyboard state. As an illustration, a typical hex dump might look like:
00 04 20 00 01 78 00 00 00 00 00 00 00 00 00 00 00 00
Channel data is packed into 11-bit values across byte boundaries. Taking the first channel as an example, the first two bytes (00 04) represent bits 0000 0000 0000 0100. The final 3 bits of the second byte are combined with the first byte to form an 11-bit value: 100 0000 0000 (binary) = 1024 (decimal). This 1024 represents the neutral (center) position for the sticks, as the protocol uses a range from 0 to 2047.
Decoding Implementation
The receiver data must be parsed into a structured format. Below is a rewritten decoding routine that extracts stick positions, switches, mouse and keyboard data. In this version, the raw buffer is copied into a packet array, and bit manipulation is performed to assemble the registers.
typedef struct {
int16_t ch0;
int16_t ch1;
int16_t ch2;
int16_t ch3;
uint8_t sw_left;
uint8_t sw_right;
struct {
int16_t x;
int16_t y;
int16_t scroll;
uint8_t left_button;
uint8_t right_button;
} mouse;
union {
uint16_t keycode;
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;
} bits;
} keyboard;
int16_t dial;
} RemoteFrame;
void decode_dr16_packet(const uint8_t packet[18], RemoteFrame* const frame) {
// Assemble 11-bit channels, offset by -1024 to center around zero
uint16_t word0 = ((packet[1] & 0x07) << 8) | packet[0];
frame->ch0 = (int16_t)(word0 - 1024);
uint16_t word1 = ((packet[2] & 0x3F) << 5) | (packet[1] >> 3);
frame->ch1 = (int16_t)(word1 - 1024);
uint16_t word2 = ((packet[4] & 0x01) << 10) | (packet[3] << 2) | (packet[2] >> 6);
frame->ch2 = (int16_t)(word2 - 1024);
uint16_t word3 = ((packet[5] & 0x7F) << 7) | (packet[4] >> 1);
frame->ch3 = (int16_t)(word3 - 1024);
// Apply a dead zone of ±5 to filter out stick drift
if (frame->ch0 <= 5 && frame->ch0 >= -5) frame->ch0 = 0;
if (frame->ch1 <= 5 && frame->ch1 >= -5) frame->ch1 = 0;
if (frame->ch2 <= 5 && frame->ch2 >= -5) frame->ch2 = 0;
if (frame->ch3 <= 5 && frame->ch3 >= -5) frame->ch3 = 0;
// Safety check: if any channel exceeds reasonable bounds, invalidate the frame
if (abs(frame->ch0) > 660 || abs(frame->ch1) > 660 ||
abs(frame->ch2) > 660 || abs(frame->ch3) > 660) {
memset(frame, 0, sizeof(RemoteFrame));
return;
}
// Extract switches
uint8_t sw_byte = packet[5] >> 4;
frame->sw_right = (sw_byte >> 2) & 0x03;
frame->sw_left = sw_byte & 0x03;
// Mouse axes and buttons
frame->mouse.x = (int16_t)(packet[6] | (packet[7] << 8));
frame->mouse.y = (int16_t)(packet[8] | (packet[9] << 8));
frame->mouse.scroll = (int16_t)(packet[10] | (packet[11] << 8));
frame->mouse.left_button = packet[12];
frame->mouse.right_button = packet[13];
// Keyboard key mapping
frame->keyboard.keycode = (uint16_t)(packet[14] | (packet[15] << 8));
// Dial (also centered around 1024)
uint16_t dial_raw = (packet[16] | (packet[17] << 8));
frame->dial = (int16_t)(dial_raw - 1024);
}
DMA-Based Reception
For real-time control, interrupt-driven UART reception is generally avoided due to its overhead. Instead, the DMA controller is used alongside the UART’s IDLE line detection to efficiently capture complete frames. The UART is configured to receive via DMA into a buffer, and an IDLE interrupt signals the end of a transmission burst. In the interrupt handler, the difference between the total buffer size and the DMA’s remaining counter (NDTR) gives the number of bytes received. If that count equals 18, the packet is valid and forwarded to the decoding function.
The initialization routine looks like this:
void dr16_dma_init(void) {
HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, DR16_BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
In the UART interrupt handler, the following logic retrieves the frame:
void dr16_idle_handler(UART_HandleTypeDef *huart) {
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
__HAL_DMA_DISABLE(huart->hdmarx);
uint32_t received_bytes = DR16_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
if (received_bytes == 18) {
decode_dr16_packet(rx_dma_buffer, &latest_frame);
}
__HAL_DMA_SET_COUNTER(huart->hdmarx, DR16_BUFFER_SIZE);
__HAL_DMA_ENABLE(huart->hdmarx);
}
}
This method ensures that the main control loop has immediate access to the latest remote data without wasting CPU cycles on byte-by-byte interrupts. The only hardware requirement is an appropriate UART with DMA support and an inverting circuit if the microcontroller does not provide a dedicated DR16 port.