遥控器控制麦克拉姆底盘

全环节概览

整个控制的流程可以分为三大步骤:信息传入,数据解算,电流信号输出
图解
接下来将介绍所有环节的细节

信息传入

无线信号发送

DR16遥控器开启并且与其配对的DR16无线接收机也通电时,两者就可以建立无线连接了
此时遥控器和接收机都亮绿灯

串口1通信

无线接收机和单片机的串口1连接,它们进行串口通信时具体采取的协议可以查阅RoboMaster 机器人专用遥控器(接收机)用户手册得知:

通信参数 数值
波特率 100000bps
单元数据长度 8
校验位 1
校验方式 偶校验
结束位 1
流控

为了两者通信正常,我们必须在STM32CubeMX中修改串口1的设置,如图
串口1配置

Word Length项值为9而不是8,因为这一项的值是单位数据长度+校验位,也就是8+1=9

我们战队所使用的板子比较特殊,串口1需要手动绑定TXRXPA9PA10

数据解包

串口1从无线接收机那里得到的数据是18字节的控制帧数据,需要解包成每个按键,摇杆的数据才能进一步使用
好在我们不用自己手搓这一段代码,使用学长写好的解包代码就可以实现这一过程
向项目中新建两个文件dr16.hdr16.c,并复制以下代码内容:

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include "stm32h7xx_hal.h"
#include "usart.h"

#ifndef DR16_H
#define DR16_H

#define DR16_rxBufferLengh 18 //< dr16接收缓存区数据长度
#define Key_Filter_Num 3 //< 按键检测消抖滤波时间(ms)
#define ACE_SENSE 0.1f //0.05f
#define ACE_SHACHE 0.003f
//extern const float ACE_SHACHE = 0.005f;
#define PARK_SENSE 0.0055f

/**
* @brief 详细按键定义,补充接收机结构体内容
*/
#define key_W key[0]
#define key_A key[1]
#define key_S key[2]
#define key_D key[3]
#define key_shift key[4]
#define key_ctrl key[5]
#define key_Q key[6]
#define key_E key[7]
#define key_V key[8]
#define key_F key[9]
#define key_G key[10]
#define key_C key[11]
#define key_R key[12]
#define key_B key[13]
#define key_Z key[14]
#define key_X key[15]

#define key_W_flag keyflag[0]
#define key_A_flag keyflag[1]
#define key_S_flag keyflag[2]
#define key_D_flag keyflag[3]
#define key_shift_flag keyflag[4]
#define key_ctrl_flag keyflag[5]
#define key_Q_flag keyflag[6]
#define key_E_flag keyflag[7]
#define key_V_flag keyflag[8]
#define key_F_flag keyflag[9]
#define key_G_flag keyflag[10]
#define key_C_flag keyflag[11]
#define key_R_flag keyflag[12]
#define key_B_flag keyflag[13]
#define key_Z_flag keyflag[14]
#define key_X_flag keyflag[15]

#define W_Num 0
#define A_Num 1
#define S_Num 2
#define D_Num 3
#define shift_Num 4
#define ctrl_Num 5
#define Q_Num 6
#define E_Num 7
#define V_Num 8
#define F_Num 9
#define G_Num 10
#define C_Num 11
#define R_Num 12
#define B_Num 13
#define Z_Num 14
#define X_Num 15


/**
* @brief 详细按键定义,补充接收机结构体内容
*/
typedef struct {
struct {
uint16_t sw;
uint16_t ch0;
uint16_t ch1;
uint16_t ch2;
uint16_t ch3;
uint8_t s1;
uint8_t s2;
uint8_t s1_last;
uint8_t s2_last;
}rc;

struct {
int16_t x;
int16_t y;
int16_t z;
uint8_t press_l;
uint8_t press_r;
uint8_t press_l_flag;
uint8_t press_r_flag;
}mouse;

struct {
uint16_t v;
}keyboard;

uint8_t key[18];
uint8_t keyflag[18];
uint32_t key_filter_cnt[18];
uint8_t isUnpackaging; //< 解算状态标志位,解算过程中不读取数据
uint8_t isOnline;
uint32_t onlineCheckCnt;
int16_t OneShoot;
int16_t ThreeShoot;
uint16_t c;
uint16_t d;
float Chassis_Y_Integ;//斜坡积分变量
float Chassis_X_Integ;
int8_t ShootNumber;
uint8_t ShootNumberCut;
uint8_t Cruise_Mode;
uint8_t ch0_PushedFlag;
uint8_t ch1_PushedFlag;
}RC_Ctrl;


/**
* @brief dr16数据拆分解算函数
* @param[in] rc_ctrl 为该结构体赋值
* @param[in] recBuffer 串口接收缓存区
* @param[in] len 缓存区数组数据长度,未使用,仅用来匹配函数类型
*/


/**
* @brief dr16数据拆分解算函数
* @param[in] recBuffer 串口接收缓存区
* @param[in] len 缓存区数组数据长度,未使用,仅用来匹配函数类型
* @retval RC_Ctl 接收机数据类型,储存杆量和按键信息
*/
uint8_t DR16_callback(uint8_t * recBuffer, uint16_t len);

/**
* @brief 初始化接收机数据类型的数据,将杆量和按键信息归零
* @param[in] RC_Ctl 接收机数据类型首地址,储存杆量和按键信息
* @retval RC_Ctl 接收机数据类型首地址,储存杆量和按键信息
*/
void DR16Init(RC_Ctrl* RC_Ctl);
void DR16_DataUnpack(RC_Ctrl* rc_ctrl, uint8_t * recBuffer);


/**
* @brief 按键消抖,检测是否为有效按下
* @param[in] RC_Ctl 接收机数据类型首地址
* @retval RC_Ctl 接收机数据类型首地址
*/
void PC_keybroad_filter(RC_Ctrl* RC_Ctl);

extern int Check_receiver;
extern RC_Ctrl rc_Ctrl;


#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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**@file  dr16.c
* @brief 设备层
* @details 主要包括构建串口管理器,提供串口初始化和用户回调重定义
* @author RyanJiao any question please send mail to 1095981200@qq.com
* @date 2021-10-9
* @version V1.0
* @copyright Copyright (c) 2021-2121 中国矿业大学CUBOT战队
**********************************************************************************
* @attention
* 硬件平台: STM32H750VBT \n
* SDK版本:-++++
* @par 修改日志:
* <table>
* <tr><th>Date <th>Version <th>Author <th>Description
* <tr><td>2021-8-12 <td>1.0 <td>RyanJiao <td>创建初始版本
* </table>
*
**********************************************************************************
==============================================================================
How to use this driver
==============================================================================


********************************************************************************
* @attention
* 硬件平台: STM32H750VBT \n
* SDK版本:-++++
* if you had modified this file, please make sure your code does not have many
* bugs, update the version NO., write dowm your name and the date, the most
* important is make sure the users will have clear and definite understanding
* through your new brief.
********************************************************************************
*/

#include "dr16.h"
#include <stdlib.h>

RC_Ctrl rc_Ctrl={
.isUnpackaging = 0,
.isOnline = 0

};

/**
* @brief 创建dr16的接收机缓存数组
*/
uint8_t DR16_recData[DR16_rxBufferLengh];

int Check_receiver=31;


//将下面的代码加入到定时器中断中,一旦isOnline为0,就把电机输出置零(必须加,不然可能会疯车)
/** if(Check_receiver > 100)
Check_receiver = 100;
if(Check_receiver>30)
rc_Ctrl.isOnline = 0;
else
rc_Ctrl.isOnline = 1;
*/

/**
* @brief 初始化接收机数据类型的数据,将杆量和按键信息归零
*/
void DR16Init(RC_Ctrl* RC_Ctl)
{
RC_Ctl->rc.ch0=1024;
RC_Ctl->rc.ch1=1024;
RC_Ctl->rc.ch2=1024;
RC_Ctl->rc.ch3=1024;
RC_Ctl->rc.s1=3;
RC_Ctl->rc.s2=3;
RC_Ctl->rc.sw=1024;
RC_Ctl->mouse.x=0;
RC_Ctl->mouse.y=0;
RC_Ctl->mouse.z=0;
RC_Ctl->key_Q_flag=0;
RC_Ctl->key_E_flag=0; //< 上电关弹舱
RC_Ctl->key_R_flag=0;
RC_Ctl->key_F_flag=0;
RC_Ctl->key_G_flag=0;
RC_Ctl->key_Z_flag=0;
RC_Ctl->key_X_flag=0;
RC_Ctl->key_C_flag=0;
RC_Ctl->key_V_flag=0;
RC_Ctl->key_B_flag=0;
RC_Ctl->key_ctrl_flag=0;
RC_Ctl->Chassis_Y_Integ=0;//斜坡积分变量
RC_Ctl->Chassis_X_Integ=0;
RC_Ctl->ShootNumber=1;
RC_Ctl->Cruise_Mode = 0;
}


uint8_t correct_num=0;
/**
* @brief 创建dr16的接收机缓存数组, 并对全局变量rc_Ctrl赋值,以供其他函数调用
*/
void DR16_DataUnpack(RC_Ctrl* rc_ctrl, uint8_t * recBuffer)
{
// tim14_FPS.Receiver_cnt++;
rc_ctrl->isUnpackaging = 1; //< 解算期间不允许读取数据

correct_num=0;
if(((recBuffer[0] | (recBuffer[1] << 8)) & 0x07ff)<=1684 && ((recBuffer[0] | (recBuffer[1] << 8)) & 0x07ff)>=364)
correct_num++;
if((((recBuffer[1] >> 3) | (recBuffer[2] << 5)) & 0x07ff)<=1684 && (((recBuffer[1] >> 3) | (recBuffer[2] << 5)) & 0x07ff)>=364)
correct_num++;
if((((recBuffer[2] >> 6) | (recBuffer[3] << 2) |(recBuffer[4] << 10)) & 0x07ff)<=1684 && (((recBuffer[2] >> 6) | (recBuffer[3] << 2) |(recBuffer[4] << 10)) & 0x07ff)>=364)
correct_num++;
if((((recBuffer[4] >> 1) | (recBuffer[5] << 7)) & 0x07ff)<=1684 && (((recBuffer[4] >> 1) | (recBuffer[5] << 7)) & 0x07ff)>=364)
correct_num++;
if((((recBuffer[5] >> 4)& 0x000C) >> 2)==1 || (((recBuffer[5] >> 4)& 0x000C) >> 2)==2 || (((recBuffer[5] >> 4)& 0x000C) >> 2)==3)
correct_num++;
if(((recBuffer[5] >> 4)& 0x0003)==1 || ((recBuffer[5] >> 4)& 0x0003)==2 || ((recBuffer[5] >> 4)& 0x0003)==3)
correct_num++;
if(correct_num==6) //< 数据校验通过
{
rc_ctrl->rc.ch0 = (recBuffer[0]| (recBuffer[1] << 8)) & 0x07ff; //< Channel 0 高8位与低3位
rc_ctrl->rc.ch1 = ((recBuffer[1] >> 3) | (recBuffer[2] << 5)) & 0x07ff; //< Channel 1 高5位与低6位
rc_ctrl->rc.ch2 = ((recBuffer[2] >> 6) | (recBuffer[3] << 2) |(recBuffer[4] << 10)) & 0x07ff; //< Channel 2
rc_ctrl->rc.ch3 = ((recBuffer[4] >> 1) | (recBuffer[5] << 7)) & 0x07ff; //< Channel 3
rc_ctrl->rc.s1 = ((recBuffer[5] >> 4)& 0x000C) >> 2; //!< Switch left
rc_ctrl->rc.s2 = ((recBuffer[5] >> 4)& 0x0003); //提取数组5的3,4位 //!< Switch right
rc_ctrl->rc.sw=(uint16_t)(recBuffer[16]|(recBuffer[17]<<8))&0x7ff;

if((rc_ctrl->rc.ch0>1020)&&(rc_ctrl->rc.ch0<1028)) //遥控器零飘
rc_ctrl->rc.ch0=1024;
if((rc_ctrl->rc.ch1>1020)&&(rc_ctrl->rc.ch1<1028))
rc_ctrl->rc.ch1=1024;
if((rc_ctrl->rc.ch2>1020)&&(rc_ctrl->rc.ch2<1028))
rc_ctrl->rc.ch2=1024;
if((rc_ctrl->rc.ch3>1020)&&(rc_ctrl->rc.ch3<1028))
rc_ctrl->rc.ch3=1024;

/***********鼠标信息*************/
rc_ctrl->mouse.x = recBuffer[6] | (recBuffer[7] << 8); //< Mouse X axis
rc_ctrl->mouse.y = recBuffer[8] | (recBuffer[9] << 8); //< Mouse Y axis
rc_ctrl->mouse.z = recBuffer[10] | (recBuffer[11] << 8); //< Mouse Z axis
rc_ctrl->mouse.press_l = recBuffer[12]; //< Mouse Left Is Press ?
rc_ctrl->mouse.press_r = recBuffer[13]; //< Mouse Right Is Press ?

if(rc_ctrl->mouse.x>25000) rc_ctrl->mouse.x=25000; //< 限幅
if(rc_ctrl->mouse.x<-25000) rc_ctrl->mouse.x=-25000;
if(rc_ctrl->mouse.y>25000) rc_ctrl->mouse.y=25000;
if(rc_ctrl->mouse.y<-25000) rc_ctrl->mouse.y=-25000;

rc_ctrl->keyboard.v = recBuffer[14]| (recBuffer[15] << 8); //< 共16个按键值

rc_ctrl->key_W=recBuffer[14]&0x01;
rc_ctrl->key_S=(recBuffer[14]>>1)&0x01;
rc_ctrl->key_A=(recBuffer[14]>>2)&0x01;
rc_ctrl->key_D=(recBuffer[14]>>3)&0x01;
rc_ctrl->key_B=(recBuffer[15]>>7)&0x01;
rc_ctrl->key_V=(recBuffer[15]>>6)&0x01;
rc_ctrl->key_C=(recBuffer[15]>>5)&0x01;
rc_ctrl->key_X=(recBuffer[15]>>4)&0x01;
rc_ctrl->key_Z=(recBuffer[15]>>3)&0x01;
rc_ctrl->key_G=(recBuffer[15]>>2)&0x01;
rc_ctrl->key_F=(recBuffer[15]>>1)&0x01;
rc_ctrl->key_R=(recBuffer[15])&0x01;
rc_ctrl->key_E=(recBuffer[14]>>7)&0x01;
rc_ctrl->key_Q=(recBuffer[14]>>6)&0x01;
rc_ctrl->key_ctrl=(recBuffer[14]>>5)&0x01;
rc_ctrl->key_shift=(recBuffer[14]>>4)&0x01;


Check_receiver = 0;

}
else{}
rc_ctrl->isUnpackaging = 0; //< 解算完成标志位,允许读取
}

接收到数据时就可以进行数据的解包了,所以在串口接收中断回调函数中调用解包函数DR16_DataUnpack()就可以把数据存储到结构体rc_Ctrl中了

1
2
3
4
5
6
7
8
9
10
extern RC_Ctrl rc_Ctrl;// 引入外部变量一般写在 USER 0 所有函数的前面
//串口接收中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
DR16_DataUnpack(&rc_Ctrl,dr16_rxbuffer);
HAL_UART_Receive_DMA(huart,dr16_rxbuffer,sizeof(dr16_rxbuffer));
}
}

不能忘记写防疯车代码
如果遥控器离线,那么串口1中接收到的数据可能会异常,严重时会导致”疯车”,也就是机器不受控制乱跑的情况
为了防止这种情况,我们引入Check_receiver来计算遥控器的延迟,如果延迟过高(比如超过了30ms)就判断车辆离线,此时立即将电机速度置0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(Check_receiver > 100)
Check_receiver = 100;
if(Check_receiver>30)
rc_Ctrl.isOnline = 0;
else
rc_Ctrl.isOnline = 1;
if (rc_Ctrl.isOnline){
// 底盘控制
// 不要忘了掉线置0
}
Check_receiver++;
}

数据解算

我们已经得到了摇杆数据,但是我们要发送给电机的是四个电机的电流数据,因此我们还需要对数据进行多层处理

摇杆数据换算

摇杆的位置数据是存储在rc_Ctrl.rc.chN,其中N取0,1,2,3
四个通道对应哪一个摇杆自行看用户手册的配图
通道的数值值域为[364,1684],以ch0为例换算目标速度:
首先算出通道的比值(满油门的百分比,只不过没有百分号)

1
ch0_pct = ((int32_t)rc_Ctrl.rc.ch0-1024.0)/(1684.0-364)*2;

然后将百分比乘以设计的最大速度,就得到目标速度
比如这里假设ch0映射目标角速度的大小,那么

1
w = ch0_pct * w_MAX;// w_MAX是你自己设置的常量,表示角速度的最大值  

同理,可以得到底盘运动的目标平动速度(vx,vy)

麦克拉姆轮运动学逆解算

上一步得到了底盘的目标速度(vx, vy, w),接下来我们将计算四个麦克拉姆轮各自的目标转速

麦轮长这样:

这种轮子可以实现侧向的运动
麦轮底盘中麦轮的组装方式也有讲究,CUBOT中采取图中的组装方式:

图中数字表示驱动对应麦轮的电机的CAN ID

以1号麦轮为例,记该麦轮的动力方向为m,无滚动方向为n,并作出以下假设:

  • 自由轮滚动方向可以无阻力地跟随底盘的运动
  • 自由轮的轴向方向不会打滑

为了得到1号麦轮的转速(蓝色向量m),我们可以根据下面的步骤来计算:

  1. 由底盘的平动速度和角速度(红色向量)计算出1号麦轮的平动方向(紫色向量的矢量和)
  2. 由1号麦轮的平动方向(紫色向量的矢量和)向无滚动方向的方向向量投影,得到由电机驱动的速度(蓝色向量n)
  3. 由n方向速度的大小反向解出m方向速度的大小









得出四轮的速度表达式后可以发现,所有式子都包含+(b-a)w,因此不妨去掉(b-a)

1
2
3
4
5
6
7
8
vx *= vxy_MAX;
vy *= vxy_MAX;
w *= w_MAX;

float v1 = vx + vy + w ;
float v2 = -vx + vy + w ;
float v3 = -vx - vy + w ;
float v4 = vx - vy + w ;

PID 速度环控制

由于我们得到的是电机的转速,这和我们需要的电机电流不是一个量
这里用之前我们写的pid来实时控制电机电流
将求得的速度作为pid的目标值传入,pid控制对象就可以返回所需的输出量了

1
2
3
4
5
6
7
8
9
10
// vn算出的是转轴的速度,需要转化为转子的速度才行
PID_target(&motor_speed_pid_1, v1*(3591.0f/187.0f));
PID_target(&motor_speed_pid_2, v2*(3591.0f/187.0f));
PID_target(&motor_speed_pid_3, v3*(3591.0f/187.0f));
PID_target(&motor_speed_pid_4, v4*(3591.0f/187.0f));

float m1 = PID_Calc(&motor_speed_pid_1,v1,0);
float m2 = PID_Calc(&motor_speed_pid_2,v2,0);
float m3 = PID_Calc(&motor_speed_pid_3,v3,0);
float m4 = PID_Calc(&motor_speed_pid_4,v4,0);

电流信号输出

CAN通信控制电机

有了以前的CAN通信的经验,我们打开对应的can口并发送电流数据即可

1
CAN_Send(&hfdcan2,m1,m2,m3,m4)

代码汇总

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef DIPAN_H
#define DIPAN_H

#include "main.h"
#include "motor.h"
#include "pid.h"

void Dipan_Init(void);

uint8_t Dipan_Mecanum(float vx, float vy, float w);

#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
#include "dipan.h"

const uint16_t vxy_MAX = 1000;
const uint16_t w_MAX = 500;

PID motor_speed_pid_1;
PID motor_speed_pid_2;
PID motor_speed_pid_3;
PID motor_speed_pid_4;

void Dipan_Init(void){
PID_Init(&motor_speed_pid_1,0.6f,0.4f,0.0f, 0,1000,16384,500.0f,600);
PID_Init(&motor_speed_pid_2,0.6f,0.4f,0.0f, 0,1000,16384,500.0f,600);
PID_Init(&motor_speed_pid_3,0.6f,0.4f,0.0f, 0,1000,16384,500.0f,600);
PID_Init(&motor_speed_pid_4,0.6f,0.4f,0.0f, 0,1000,16384,500.0f,600);
}

/**
* @brief 麦克拉姆论轮底盘运动学计算
*
* @attention 支持麻神www.bilibili.com/video/BV1toH6ekEfJ
*
* @param vx 速度x分量
* @param vy 速度y分量
* @param w 角速度
* @return 发送是否成功
*/
uint8_t Dipan_Mecanum(float vx, float vy, float w){
vx *= vxy_MAX;
vy *= vxy_MAX;
w *= w_MAX;

float v1 = vx + vy + w ;
float v2 = -vx + vy + w ;
float v3 = -vx - vy + w ;
float v4 = vx - vy + w ;

PID_target(&motor_speed_pid_1, v1*(3591.0f/187.0f));
PID_target(&motor_speed_pid_2, v2*(3591.0f/187.0f));
PID_target(&motor_speed_pid_3, v3*(3591.0f/187.0f));
PID_target(&motor_speed_pid_4, v4*(3591.0f/187.0f));

float m1 = PID_Calc(&motor_speed_pid_1,v1,0);
float m2 = PID_Calc(&motor_speed_pid_2,v2,0);
float m3 = PID_Calc(&motor_speed_pid_3,v3,0);
float m4 = PID_Calc(&motor_speed_pid_4,v4,0);

return CAN_Send(&hfdcan2,m1,m2,m3,m4);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(Check_receiver > 100)
Check_receiver = 100;
if(Check_receiver>30)
rc_Ctrl.isOnline = 0;
else
rc_Ctrl.isOnline = 1;
if (rc_Ctrl.isOnline){
float vx,vy,w;
vx = ((int32_t)rc_Ctrl.rc.ch2-1024.0)/(1684.0-364)*2;
vy = ((int32_t)rc_Ctrl.rc.ch3-1024.0)/(1684.0-364)*2;
w = ((int32_t)rc_Ctrl.rc.ch0-1024.0)/(1684.0-364)*2;
Dipan_Mecanum( vx, vy, w);
}
else{
Dipan_Mecanum(0,0,0);
}
Check_receiver++;
}

—END—

参考:
【中科大RM电控合集】各种底盘各种解算一网打尽
附件:
RoboMaster 机器人专用遥控器(接收机)用户手册


遥控器控制麦克拉姆底盘
http://chose-b-log.netlify.app/遥控器和麦克拉姆底盘/
作者
B
发布于
2025年11月6日
许可协议