S-BUS协议及其衍生协议

S.BUS协议是一个串行通信协议,是由FUTABA提出的舵机控制总线

S-BUS使用RS232C串口的硬件协议作为自己的硬件运行基础。使用TTL电平,即3.3V。使用负逻辑,即低电平为”1“,高电平为”0“。波特率为100000(100k),不兼容115200。

对于串口相关知识,欲知RS232,RS485等内容,详见 电控 | UART串口通信

硬件电路

Robomaster的C板和A板都有D-BUS电路,根据 无人机 | A3 PRO 外设进阶 可以知道,D-BUS和S-BUS实际上可以理解为是相同的。(注:好像发现了不同,详见下文)

image.png

这其实是一个非常简单的三极管电路,具备一个取反功能。S-BUS信号从基极(B)输入,从集电极(C)输出。基极输入‘0’,集电极上拉输出‘1’;基极输入’1‘,三极管被导通,则输出被下拉为’0‘,实现取反。

UART3_RX即为串口的输出端。

关于三极管相关知识,详见 电控 | 基础理论详解——三极管

通信协议

S-BUS拥有很简洁的协议帧,一帧包括25byte的数据:

首部1byte + 数据22byte + 标志位1byte + 结束符1byte

上面即为协议帧的格式。具体协议规则如下:

  • 首部:0000 1111b (0x0f)

  • 数据:22byte分别代表16个通道数据。我们进行简单的计算:

    也就是说,每个通道用11位来表示。因此,每个通道取值范围位0~2047。低位在前,高位在后

  • 标志位:1byte,

    • bit7:CH17数字通道

    • bit6:CH16数字通道

    • bit5:丢失帧

    • bit4:安全保护,即失控保护激活位,判断无人机是否失控

    • bit3~bit0:低四位不使用

  • 结束符:0x00

协议解析

通道解析表

将数据解析为通道的方法如下:

16通道 解析方法
通道0 byte[1] >> 0 | byte[2] << 8 & 0x7ff
通道1 byte[2] >> 3 | byte[3] << 5 & 0x7ff
通道2 byte[3] >> 6 | byte[4] << 2 | byte[5] << 10 & 0x7ff
通道3 byte[5] >> 1 | byte[6] << 7 & 0x7ff
通道4 byte[6] >> 4 | byte[7] << 4 & 0x7ff
通道5 byte[7] >> 7 | byte[8] << 1 | byte[9] << 9 & 0x7ff
通道6 byte[9] >> 2 | byte[10] << 6 & 0x7ff
通道7 byte[10] >> 5 | byte[11] << 3 & 0x7ff
通道8 byte[12] >> 0 | byte[13] << 8 & 0x7ff
通道9 byte[13] >> 3 | byte[14] << 5 & 0x7ff
通道10 byte[14] >> 6 | byte[15] << 2 | byte[16] << 10 & 0x7ff
通道11 byte[16] >> 1 | byte[17] << 7 & 0x7ff
通道12 byte[17] >> 4 | byte[18] << 4 & 0x7ff
通道13 byte[18] >> 7 | byte[19] << 1 | byte[20] << 9 & 0x7ff
通道14 byte[20] >> 2 | byte[21] << 6 & 0x7ff
通道15 byte[21] >> 5 | byte[22] << 3 & 0x7ff

解析逻辑

由上文可知,每个通道数据为11字节。但是我们在发送的时候,只能16字节16字节的发送。为了节约空间,提高传输效率,减少时延,在发送和接收的时候必须要把空出来的5位数补齐。

因此,在接收到信号之后,进行解码,就要把补齐后首尾相连的内容分离出来。这就叫做数据的解析。

  • 0x07FF

    十六进制的0x07FF是十进制中的2047,其二进制表示为11位全为1的掩码。通过&来操作,以确保每个通道值不会超过11位。

  • byte[1] >> 0 | byte[2] << 8

    假设,这些数据都是8位的(通常而言,都是8位的)

    byte[1]的最低8位直接使用,无需移动。

    byte[2]的8位左移8位

    将两者相或,再与上0x07FF,就是我们要的Ch[0]的数据。

具体,我们可以举个例子。

让我们做一个假设:

1
2
3
4
buf[1] = 0101 0110
buf[2] = 1010 1001
buf[3] = 0111 1010
buf[4] = 1011 1110

现在,我们来对上面的代码进行运算。

buf[1] >> 0:buf[1] = 000 0101 0110

buf[2] << 8:buf[2] = 101 0100 1000

buf[1] | buf[2]:Ch[0] = 001 0101 0110

此时,Ch[0]已经解析完成。

让我们继续:

buf[2] >> 3:buf[2] = 000 0001 0101

buf[3] << 5:buf[3] = 111 0100 0000

buf[2] | buf[3]:Ch[1] = 111 0101 0101

通过对比Ch[1]和Ch[0]两个数据,我们将他们写在一起:

111 0101 0101 001 0101 0110

再将他们每八位一划分:

11 1010 | 1010 1001 | 0101 0110

可以发现,这就是buf[3]一部分 + buf[2] + buf[1]

其解码的数理逻辑,如上所言。

解析代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Sbus_Data_Count(uint8_t *buf)
{
CH[ 0] = ((int16_t)buf[ 1] >> 0 | ((int16_t)buf[ 2] << 8 )) & 0x07FF;
CH[ 1] = ((int16_t)buf[ 2] >> 3 | ((int16_t)buf[ 3] << 5 )) & 0x07FF;
CH[ 2] = ((int16_t)buf[ 3] >> 6 | ((int16_t)buf[ 4] << 2 ) | (int16_t)buf[ 5] << 10 ) & 0x07FF;
CH[ 3] = ((int16_t)buf[ 5] >> 1 | ((int16_t)buf[ 6] << 7 )) & 0x07FF;
CH[ 4] = ((int16_t)buf[ 6] >> 4 | ((int16_t)buf[ 7] << 4 )) & 0x07FF;
CH[ 5] = ((int16_t)buf[ 7] >> 7 | ((int16_t)buf[ 8] << 1 ) | (int16_t)buf[ 9] << 9 ) & 0x07FF;
CH[ 6] = ((int16_t)buf[ 9] >> 2 | ((int16_t)buf[10] << 6 )) & 0x07FF;
CH[ 7] = ((int16_t)buf[10] >> 5 | ((int16_t)buf[11] << 3 )) & 0x07FF;

CH[ 8] = ((int16_t)buf[12] << 0 | ((int16_t)buf[13] << 8 )) & 0x07FF;
CH[ 9] = ((int16_t)buf[13] >> 3 | ((int16_t)buf[14] << 5 )) & 0x07FF;
CH[10] = ((int16_t)buf[14] >> 6 | ((int16_t)buf[15] << 2 ) | (int16_t)buf[16] << 10 ) & 0x07FF;
CH[11] = ((int16_t)buf[16] >> 1 | ((int16_t)buf[17] << 7 )) & 0x07FF;
CH[12] = ((int16_t)buf[17] >> 4 | ((int16_t)buf[18] << 4 )) & 0x07FF;
CH[13] = ((int16_t)buf[18] >> 7 | ((int16_t)buf[19] << 1 ) | (int16_t)buf[20] << 9 ) & 0x07FF;
CH[14] = ((int16_t)buf[20] >> 2 | ((int16_t)buf[21] << 6 )) & 0x07FF;
CH[15] = ((int16_t)buf[21] >> 5 | ((int16_t)buf[22] << 3 )) & 0x07FF;
}

完整代码(基于HAL库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// sbus.h
#ifndef SBUS_H
#define SBUS_H

#include "sys.h"

#define USART_BUF_SIZE 8 // HAL库USART接收Buffer大小
#define SBUS_DATA_SIZE 25 // 25字节

#define SBUS_PIN GPIO_PIN_2 | GPIO_PIN_3 // PA2--TX, PA3--RX
#define SBUS_GPIO GPIOA
#define SBUS_ENCLK() __HAL_RCC_GPIOA_CLK_ENABLE(); \
__HAL_RCC_USART2_CLK_ENABLE(); //使能GPIOA时钟//使能USART2时钟

struct SBUS_t{
uint8_t head; // 1字节首部
uint16_t ch[16]; // 16个字节数据
uint8_t flag; // 1字节标志位
uint8_t end; // 1字节结束
};

void SBUS_Init(void);
void SbusParseTask(void *arg);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// sbus.c
#include "sbus.h"
#include "delay.h"

uint8_t usart_buf[USART_BUF_SIZE];
uint8_t sbus_rx_head = 0; // 发现起始字节 0x0F
uint8_t sbus_rx_sta = 0; // sbus 接收状态,0:未完成,1:已完成一帧接收
uint8_t sbus_rx_index; // 接收字节计数
uint8_t sbus_rx_buf[SBUS_DATA_SIZE]; // 接收sbus数据缓冲区

struct SBUS_t sbus; // SBUS 结构体实例化

UART_HandleTypeDef UART2_Handler; // 串口2配置句柄

void SBUS_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;

// 时钟使能
SBUS_ENCLK();

// 串口初始化配置
// 波特率100kbps,8位数据,偶校验(even),2位停止位,无流控。
UART2_Handler.Instance = USART2;
UART2_Handler.Init.BaudRate = 100000;
UART2_Handler.Init.WordLength = UART_WORDLENGTH_8B;
UART2_Handler.Init.StopBits = UART_STOPBITS_2;
UART2_Handler.Init.Parity = UART_PARITY_EVEN;
UART2_Handler.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UART2_Handler.Init.Mode = UART_MODE_TX_RX;

// 引脚 配置
GPIO_Initure.Pin = SBUS_PIN; // PA2--TX, PA3--RX
GPIO_Initure.Mode = GPIO_MODE_AF_PP;
GPIO_Initure.Pull = GPIO_PULLUP;
GPIO_Initure.Speed = GPIO_SPEED_HIGH;
GPIO_Initure.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(GPIOA, &GPIO_Initure);

// 中断配置
HAL_NVIC_EnableIRQ(USART2_IRQn);
HAL_NVIC_SetPriority(USART1_IRQn, 3, 4);

HAL_UART_Init(&UART2_Handler); //HAL_UART_Init()会使能UART2
HAL_UART_Receive_IT(&UART2_Handler, (uint8_t *)usart_buf, USART_BUF_SIZE); //该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
}

/* USART2 中断服务函数 */
/* 实现对S.BUS协议缓存,头部为 0x0F,结尾为 0x00, 中间22Bytes16通道数据,1Byte标志符 */
void USART2_IRQHandler(void) //中断函数
{
uint8_t chr;
if ((__HAL_UART_GET_FLAG(&UART2_Handler, UART_FLAG_RXNE) != RESET)) // 接收中断
{

HAL_UART_Receive(&UART2_Handler, &chr, 1, 1000); // 接收一个字符

if (sbus_rx_sta == 0) // 接收未完成
{
if ((chr == 0x0F) || sbus_rx_head) // 找到首字节或已经找到首字节
{
sbus_rx_head = 1; // 标明已经找到首字母
if (sbus_rx_index < SBUS_DATA_SIZE) // 未接收到25个字符
{
sbus_rx_buf[sbus_rx_index] = chr; // 不断接收
sbus_rx_index ++;
}
else // 接收到25个字符了
{
sbus_rx_sta = 1; // 接收完成
sbus_rx_head = 0; // 清零,准备下一次接收
sbus_rx_index = 0;
}
}
}
}
HAL_UART_IRQHandler(&UART2_Handler);
}

/* 对SBUS协议数据进行解析 */
/* 实现对S.BUS协议缓存,头部为 0x0F,结尾为 0x00, 中间22Bytes16通道数据,1Byte标志符 */
void SbusParseTask(void *arg)
{
while (1)
{
if(sbus_rx_sta==1) // 接收完一帧
{

NVIC_DisableIRQ(USART2_IRQn); // 要关闭中断,防止读写混乱

sbus.head = sbus_rx_buf[0]; // 首部
sbus.flag = sbus_rx_buf[23]; // 标志符
sbus.end = sbus_rx_buf[24]; // 结尾

sbus.ch[0] =((sbus_rx_buf[2]<<8) + (sbus_rx_buf[1])) & 0x07ff;
sbus.ch[1] =((sbus_rx_buf[3]<<5) + (sbus_rx_buf[2]>>3)) & 0x07ff;
sbus.ch[2] =((sbus_rx_buf[5]<<10) + (sbus_rx_buf[4]<<2) + (sbus_rx_buf[3]>>6)) & 0x07ff;
sbus.ch[3] =((sbus_rx_buf[6]<<7) + (sbus_rx_buf[5]>>1)) & 0x07ff;
sbus.ch[4] =((sbus_rx_buf[7]<<4) + (sbus_rx_buf[6]>>4)) & 0x07ff;
sbus.ch[5] =((sbus_rx_buf[9]<<9) + (sbus_rx_buf[8]<<1) + (sbus_rx_buf[7]>>7)) & 0x07ff;
sbus.ch[6] =((sbus_rx_buf[10]<<6) + (sbus_rx_buf[9]>>2)) & 0x07ff;
sbus.ch[7] =((sbus_rx_buf[11]<<3) + (sbus_rx_buf[10]>>5)) & 0x07ff;
sbus.ch[8] =((sbus_rx_buf[13]<<8) + sbus_rx_buf[12]) & 0x07ff;
sbus.ch[9] =((sbus_rx_buf[14]<<5) + (sbus_rx_buf[13]>>3)) & 0x07ff;
sbus.ch[10]=((sbus_rx_buf[16]<<10) + (sbus_rx_buf[15]<<2) + (sbus_rx_buf[14]>>6)) & 0x07ff;
sbus.ch[11]=((sbus_rx_buf[17]<<7) + (sbus_rx_buf[16]>>1)) & 0x07ff;
sbus.ch[12]=((sbus_rx_buf[18]<<4) + (sbus_rx_buf[17]>>4)) & 0x07ff;
sbus.ch[13]=((sbus_rx_buf[20]<<9) + (sbus_rx_buf[19]<<1) + (sbus_rx_buf[18]>>7)) & 0x07ff;
sbus.ch[14]=((sbus_rx_buf[21]<<6) + (sbus_rx_buf[20]>>2)) & 0x07ff;
sbus.ch[15]=((sbus_rx_buf[22]<<3) + (sbus_rx_buf[21]>>5)) & 0x07ff;

printf("======================================\r\n");
printf("正常: head=0x0F, flag=0x00, end=0x00\r\n\r\n");
printf("head: %d\r\n", sbus.head);
printf(" %d, %d, %d, %d\r\n", sbus.ch[0], sbus.ch[1], sbus.ch[2], sbus.ch[3]);
printf(" %d, %d, %d, %d\r\n", sbus.ch[4], sbus.ch[5], sbus.ch[6], sbus.ch[7]);
printf(" %d, %d, %d, %d\r\n", sbus.ch[8], sbus.ch[9], sbus.ch[10], sbus.ch[11]);
printf(" %d, %d, %d, %d\r\n", sbus.ch[12], sbus.ch[13], sbus.ch[14], sbus.ch[15]);
printf("flag: %d\r\n", sbus.flag);
printf("end: %d\r\n", sbus.end);
printf("======================================\r\n\r\n");

delay_ms(500); // 先做完延时再开启中断与下一次捕获,否则延时期间中断到来,没有达到预期效果

NVIC_EnableIRQ(USART2_IRQn); // 打开串口中断
sbus_rx_sta = 0; // 准备下一次接收
}
else
{
delay_ms(500); // 免得异常时,到此处使得低优先级任务无法执行
}
}
}

D-BUS协议

前面说道,D-BUS协议和S-BUS协议是一样的。但是,我始终对此存有怀疑。当我打开了Robomaster官方C板教程,寻找D-BUS解析相关代码的时候,就发现了一些问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @brief 遥控器协议解析
* @param[in] sbus_buf: 原生数据指针
* @param[out] rc_ctrl: 遥控器数据指
* @retval none
*/
static void sbus_to_rc(volatile const uint8_t *sbus_buf, RC_ctrl_t *rc_ctrl)
{
if (sbus_buf == NULL || rc_ctrl == NULL)
{
return;
}

rc_ctrl->rc.ch[0] = (sbus_buf[0] | (sbus_buf[1] << 8)) & 0x07ff; //!< Channel 0
rc_ctrl->rc.ch[1] = ((sbus_buf[1] >> 3) | (sbus_buf[2] << 5)) & 0x07ff; //!< Channel 1
rc_ctrl->rc.ch[2] = ((sbus_buf[2] >> 6) | (sbus_buf[3] << 2) | //!< Channel 2
(sbus_buf[4] << 10)) &0x07ff;
rc_ctrl->rc.ch[3] = ((sbus_buf[4] >> 1) | (sbus_buf[5] << 7)) & 0x07ff; //!< Channel 3
rc_ctrl->rc.s[0] = ((sbus_buf[5] >> 4) & 0x0003); //!< Switch left
rc_ctrl->rc.s[1] = ((sbus_buf[5] >> 4) & 0x000C) >> 2; //!< Switch right
rc_ctrl->mouse.x = sbus_buf[6] | (sbus_buf[7] << 8); //!< Mouse X axis
rc_ctrl->mouse.y = sbus_buf[8] | (sbus_buf[9] << 8); //!< Mouse Y axis
rc_ctrl->mouse.z = sbus_buf[10] | (sbus_buf[11] << 8); //!< Mouse Z axis
rc_ctrl->mouse.press_l = sbus_buf[12]; //!< Mouse Left Is Press ?
rc_ctrl->mouse.press_r = sbus_buf[13]; //!< Mouse Right Is Press ?
rc_ctrl->key.v = sbus_buf[14] | (sbus_buf[15] << 8); //!< KeyBoard value
rc_ctrl->rc.ch[4] = sbus_buf[16] | (sbus_buf[17] << 8); //NULL

rc_ctrl->rc.ch[0] -= RC_CH_VALUE_OFFSET;
rc_ctrl->rc.ch[1] -= RC_CH_VALUE_OFFSET;
rc_ctrl->rc.ch[2] -= RC_CH_VALUE_OFFSET;
rc_ctrl->rc.ch[3] -= RC_CH_VALUE_OFFSET;
rc_ctrl->rc.ch[4] -= RC_CH_VALUE_OFFSET;
}

这是一段Robomaster官方代码,遥控器DMA通信例程中的D-BUS数据解析函数。我们可以很容易地发现,它居然把buf[1]拉高了8位!

这是什么意思呢?难道协议帧的帧头部分,需要被拉高吗?协议帧的帧头前面说不是固定的0x0f吗?

继续往下看,发现还有蹊跷的地方。所有的buffer,到了第17就结束了。而正常的S-BUS是到了第22个。很显然,代码还有残缺的地方。

从解析的逻辑上看,拉高与拉低的位数相加起来确实都是8,而且在按位与的时候,仍然是0x07ff,说明保留的也还是11位,和S-BUS的解析逻辑是一样的。

因此我们可以断定,D-BUS是S-BUS的残缺版。没有头也没有尾,只接收18个字节数据。

W-BUS 协议

W-BUS和S-BUS几乎没有什么区别,唯一的区别在于数据尾部。结束符被去掉,第25个字节变成了一个不断变化的字节,剩下的所有都是兼容S-BUS的。