### 运动算法设计
#### 概览
工程使用的算法大概可以分为三种,分别是底**盘控制**算法,**机械臂控制**算法,5阶**插值**算法。
#### 底盘控制算法
关于底盘的控制算法,不同的驱动方式有不同的算法,经过考虑,整车选择了全向轮作为驱动机构,四个全向轮组成**O**形,实现全向移动。算法原理很简单,只需要用到速度矢量的分解,三个速度矢量分别是方向x速度、y方向速度和角速度w,其中x和y垂直且与组成的平面平行于地面,分解为四个全向轮的速度。
具体推导可以参考以下文章:`https://blog.csdn.net/weixin_42722140/article/details/134621461`
需要注意的是文章通过一个**旋转矩阵**实现了从物流车坐标系到世界坐标系的转换。

#### 机械臂控制算法
机械臂运动学解算分为机械臂运动学正解算和逆解算,正解算是由各个关节的角度推算机械臂末端坐标,逆解算是由机械臂末端坐标推算各个机械臂关节的角度。逆解算用于**控制**,正解算用于机械臂**示教**(校赛暂未实现)。
* 值得注意的是,机械臂算法并没有刻意的去解算计算xy的平面(机械臂底部电机)的角度,仅解算yz平面的角度(大臂角和小臂角度)。xy平面的角度将分离为一个单独调整的变量来控制,这样做的**目的**是更方便的调试动作。
机械臂运动学逆解算算法可以参考以下视频:
```
https://www.bilibili.com/video/BV1mY411476h/?spm_id_from=333.337.search-card.all.click&vd_source=0b74b2b69095521c3b4b6fbfdcd7eae6
```
机械臂正解算的思路就像点在平面坐标系移动,已知机械臂的各个臂长和角度,通过三角函数可以推算出每一段的点的x和y所走的距离,叠加各段x和各段的y,最终得到末端坐标。
#### 5阶插值
关于五阶插值的原理可以参考以下文章:`https://www.zhihu.com/tardis/zm/art/269230598?source_id=1005`
简单来说,插值在本工程运动算法的作用是让一段运动变得丝滑,特别是底盘控制,若合理的设置节点的时刻、位移、速度、加速度,5阶线性插值算法能自动解算出一段路径的每个时刻的速度,让物流车运行更加的平稳,**防止车轮打滑**。
前文提到,本次物流车底盘控制算法在底层就是控制x、y、w方向上的速度,在此基础上,分别对这三个方向进行插值,即可实现对一段平滑的轨迹进行控制。下图展示的使用 `matlab`仿真设定x和y方向的每个节点位移、速度、加速度,自动解算出中间过程数值的结果。可以看出,在速度、加速度都是连续的,意味着运动的丝滑。
把x和y上的位移作为坐标系的横轴和纵轴,我们可以得出**模拟轨迹**图如下。

---
### 软件设计
#### 概览
在软件设计上,设计具有层级的代码结构,各层之间**耦合度底**,易于维护升级,可以单独维护升级每一层。在**用户层**,创新性的使用节点的编程思想,将物流车的运动抽象成一个个节点,并通过控制两个节点的状态,自动解算出两个节点间每个时刻的状态,实现稳定快速的物流车控制。调车时,我们只需要修改节点及其节点对应的任务,**其他什么都不需要动**,就可以实现对物流车运动状态进行修改。
#### 文件架构
* 代码文件结构概览(细节的文件暂不展示,后文会详细叙述)
```c
GC_25/
│
└── code/
├── .vscode/
│ └── launch.json //调试配置文件
└── remote_control/
│ └── test.py //串口通讯测试文件
│ └── UART.py //手柄+串口透传遥控
└── robot_control/
│ └── CORE //cotex-m4内核文件
│ └── FWLIB //STM32F4外设库
│ └── DSP //ARM算法加速库
│ └── SYSTEM //底层驱动配置
│ └── HARDWARE //硬件驱动层
│ └── KINEMATICS //算法层
│ └── USER //用户层
└── digital_tube/ //任务码显示模块工程
└── led_control/
└── led_control //视觉补光灯工程
```
代码的code-workspace文件在code\robot_control\USER\GC_25.code-workspace,通过这个文件可以打开工程。
**任务码显示模块**使用keil5开发,打开烧录程序即可,通过串口发送类似 `(123312)`指令即可显示,默认波特率115200。
**视觉补光灯模块**使用vscode+EIDE开发,编译后和51单片机一样烧录hex文件就好,使用方法很简单,两个按钮分别是亮度+和亮度-,带掉电存储当前亮度。
主要讲解robot_control工程。
工程共使用了两个库,一个是**stm32标准库**,另外一个是ARM提供的**DSP**库,可以加速一些数学运算,例如COS、SIN函数。
cotex-m4具备**FPU**,可以加速**单精度浮点数**的运算,本工程开启了FPU,开启的方式很简单,添加以下预处理宏定义即可
```c
__CC_ARM
__TARGET_FPU_VFP
__FPU_PRESENT
```
添加之后可以检查以下**宏定义** `__FPU_USED`是否为1,若为1,则表示启用了FPU,本工程启用了FPU。
工程所有代码都存放在**code**文件夹里面
* **.vscode**是cotex-debug插件自动生成的lauch.json文件,当我们需要调试时,需要在里面配置调试参数,如目标芯片型号,调试器型号等。
* ~~remote_control~~文件夹存放了早期调试底盘时写的使用PS5手柄操控物流车的驱动程序,现已不使用。具体原理是使用电脑读取手柄数据,然后通过调试器自带的远程串口传输给物流车,实现对物流车的控制,代码使用python编写。
* **robot_control**是本工程核心文件,该文件夹存放了所有STM32代码,其中CORE和FWLIB和DSP不做过多介绍,都是官方库文件。**SYSTEM**文件夹存放了芯片外设驱动文件,包括使用到的定时器、串口和DMA。**HARDWARE**文件夹存放了硬件驱动文件,包括步进电机驱动、陀螺仪驱动、通讯协议驱动、扫码模块驱动、舵机驱动和系统时钟驱动。**KINEMATICS**文件夹存放底盘控制算法、**5阶线性插值算法**和机械臂控制算法文件。最后是**USER**文件夹,存放了平时常用的文件,包括主程序main文件、物流车动作文件、路径控制文件、~~远程遥控文件~~和一些官方的中断服务函数文件等。
在robot_control中,除官方库外,SYSTEM文件是工程**最底层**,描述了片内外设如何初始化,HARDWARE内文件调用SYSTEM层和官方库文件,封装了一层**硬件驱动层**,负责硬件控制。KINEMTICS层负责**整车运动算法**,仅调用HARDWARE层和官方库文件。USER层调用HARDWARE最终实现**运动控制**。
整个工程的**层级结构**为:SYSTEM->HARDWARE->KINEMTICS->USER(SYSTEM为最底层)
另外需要注意的是,部分文件存在**跨层**或者**同层**调用的情况,分别是USER层的 `motion.c`调用了HARDWARE层的 `protocols.h`,和HARDWARE层的 `protocols.h`调用了同层的 `scanner_driver.h`。
#### 开发环境
电控的开发环境是vscode的插件EIDE,具体使用可以参考下面链接:
```
https://www.bilibili.com/video/BV1nr4y1R7Jb/?spm_id_from=333.337.search-card.all.click&vd_source=0b74b2b69095521c3b4b6fbfdcd7eae6
```
里面有详细的EIDE配置方法,但是想要使用调试功能,视频里的方法已经不适用,可以参考下面链接:
```
https://blog.csdn.net/qq_39854159/article/details/124511433
```
建议使用vscode的**插件**EIDE和cotex-debug加上openocd。EIDE写代码,编译烧录,cotex-debug负责调试。想要使用调试功能,要注意整个**工程不能有中文路径**,cotex-debug不支持中文路径。配置cotex-debug时需要注意的是**lauch.json**文件,该文件位于code\.vscode\launch.json。需要配置的路径在文件已经标明,目前配置了dap-link和stlink。
jlink也可以使用,支持实时查看变量对于电机或者运动控制等一些需要快速查看变量的场合很方便,使用串口输出调试信息会让整个程序运行效率变慢,对于一些高速任务如需要20KHz控制频率的FOC控制技术,加了一个串口输出函数会极大影响运行效果,这时如果我们还想要监控数据,可以考虑使用jlink。
* 以下为cotex-debug调试的配置文件 `launch.json`的示例(仅需修改注释提示的地方)
```json
"configurations": [
{
"cwd": "${workspaceRoot}",
//需要配置以下elf文件路径
"executable": "F:\\project_no_cn\\GC_25\\code\\robot_control\\USER\\build\\Template\\GC_25.elf",
//dap为调试名称,可以更改
"name": "dap",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
//需要配置以下openocd文件路径,选择调试设备和目标
"configFiles": [
"F:\\vscode_debug\\openocd-0.12.0\\openocd-0.12.0\\tcl\\interface\\cmsis-dap.cfg",
"F:\\vscode_debug\\openocd-0.12.0\\openocd-0.12.0\\tcl\\target\\stm32f4x.cfg"
],
"searchDir": [],
"runToEntryPoint": "main",
"showDevDebugOutput": "none"
},
```
#### SYSTEM文件
SYSTEM文件主要是负责底层芯片外设驱动。
* delay
`delay.c/.h`文件直接使用的正点原子的delay文件,这里不做过多叙述。
* usart+dma
`usart.c/.h`文件是配置串口1~6,串口2**重定向**到printf()函数。`dma.c/.h`文件是配置串口DMA,包括发送和接收。这两个文件共同组成了硬件通讯最底层。
下表为串口与硬件对应关系(rx_dma表示开启了串口的**dma接收**+串口**空闲中断**)
| usart1 | usart2 | usart3 | uart4 | uart5 | usart6 |
| :------: | :------: | :--------: | :------: | :------: | :----: |
| rx_dma | rx_dma | NULL | NULL | NULL | rx_dma |
| 视觉模块 | 调试信息 | 机械臂电机 | 底盘电机 | 扫码模块 | 陀螺仪 |
* timer
`timer.c/.h`文件是配置系统定时器文件,主要存放定时器初始化函数。
下表为定时器和功能对应关系
| tim2 | tim4 | tim5 |
| :---------: | :---------: | :--------: |
| 电机PWM控制 | 舵机PWM控制 | 系统时间戳 |
#### HARDWARE文件
HARDWARE文件主要负责硬件驱动和系统时间控制。
* hwt101_driver
`hwt101_driver.c/.h`文件是陀螺仪**HWT101**的驱动,对数据的解包均由中断函数 `USART6_IRQHandler`完成,最后存放于两个全局变量,加速度为 `hwt101_acceleration_data`变量,世界坐标角度为 `abs_degree_data`。另外还有常见的陀螺仪功能,如陀螺仪复位、校准和设置输出速率。
* motor_driver
`motor_driver.c/.h`文件时整车电机驱动,使用以下结构体描述电机:
```c
//电机控制结构体
typedef struct
{
char ID; //电机ID
char direction; //电机运动方向
char synchronous; //同步设定
int speed; //速度
int acc_speed; //加速度
int pulse_num; //总脉冲数
} steeping_motor;
```
工程创建了7个全局变量**steeping_motor**结构体,用于描述整车7个电机。
```c
extern steeping_motor motor[7];
```
其中 `motor[0]~motor[3]`为**底盘**电机,`motor[4]`为**机械臂小臂**电机,`motor[5]`为**云台底座**电机,`motor[6]`为**机械臂大臂**电机。
另外,除基础的电机速度控制,位置控制外,motor_driver还具有设置电机**零点**,电机回零功能。选择对应的**电机结构体**,设定好结构体参数,**作为参数**传入相应的控制函数即可控制
* protocols
`protocols.c/.h`文件为通讯协议,主要功能处理与视觉模块通讯协议,扫码模块通讯协议~~和远程遥控协议~~。
```c
/**
*@brief 初始化软件数据包
*@param 无
*@return 无
*/
void soft_packet_init(void)
{
head.next_usart_pack = &tail;
tail.next_usart_pack = &head;
pi_location_data.package_header = 'L';
pi_location_data.package_tail = 'E';
pi_location_data.packet = pi_location;
pi_location_data.package_len = 20;
pi_location_data.package_flag = rec_unfinish;
create_usart_package(&pi_location_data);
}
```
以上代码创建了一个数据包,该数据包由'L'开头,‘E’结尾,最长20byte个长度。当串口空闲中断触发时,`packet_scan()`内自动索检包头包尾,当包头包尾有创建时,如接收到’L‘开头‘E’结尾数据包,会自动将该数据包的 `pi_location_data.package_flag = rec_finish`用户可以判断该标志位判断是否接收完成,当读取完之后,要手动将该数据包重新置为 `rec_unfinish`,实现以上功能主要使用到**链表**操作。
* scanner_driver
`scanner_driver.c/.h`为扫码模块驱动文件,扫码模块使用串口通讯,由于扫码内容是固定的类似与123+321的数据格式所以我们的驱动也很简单。
* servo_driver
`servo_driver.c/.h`为舵机驱动文件,比较简单,这里也不详细叙述。
* system_clock
`system_clock.c/.h`实现了系统时间戳功能,使用tim5定时中断,系统计时以10ms为步进,每次中断使变量 `sys_time_ms+=10`,然后把 `timer_10ms_flag=1`。若要以10ms运行一个任务,可以参考以下代码:
```c
while(1)
{
if(timer_10ms_flag)
{
timer_10ms_flag = 0;
task();
}
}
```
`sys_time_ms`与 `timer_10ms_flag`均为全局变量,`sys_time_ms`是系统运行时间戳。
另外,该文件还实现了计时功能,计时器抽象为如下结构体:
```c
typedef struct _sys_count_time_t
{
char count_time_state;
long long int start_time;
long long int count_now_time;
long long int count_end_time;
}sys_count_time_t;
```
若要使用计时功能,需要创建结构体,创建结构体时只需要设定 `count_end_time`的值,开始计时要调用库函数内的 `start_count_time`函数。**注意**,要定期调用 `count_time`函数与系统时间戳同步,调用越频繁计时越准确,最小精确到10ms。
system_clock还集成了时间戳格式化输出功能,可以格式化输出分钟和秒数。
#### KINEMTICS文件
KINEMTICS文件主要负责**运动学算法**的实现。
* chassis_control
`chassis_control.c/.h`负责底盘运动学解算,兼容麦克纳姆轮和全向轮。`set_car_speed_Omni_Wheel`是物流车坐标系下的物流车速度控制,`chassis_control_Omni_Wheel`是世界坐标系下的物流车速度控制,内有角度环控制、世界坐标系转换、全向轮转速转换。
全向轮物流车坐标系下速度解算如下所示
```c
motor[0].speed = - x + y - w;
motor[1].speed = - x + y + w;
motor[2].speed = x + y - w;
motor[3].speed = x + y + w;
```
麦轮不赘述。
* robot_arm_control
`robot_arm_control.c/.h`负责机械臂运动学解算,包含机械臂运动学正解算 `robot_arm_calculate_forward`,逆解算 `robot_arm_calculate_inverse`,机械臂底座控制robot_arm_control_botton_position,机械臂臂控制 `robot_arm_control_arm_poosition`。
另外宏定义还定义了以下机械臂规格,使用的时候可以修改
```
//机械臂关节减速比
#define reduction_ratio 4.0f
//机械臂规格,单位为mm
#define base_height 92.3f //底座高度
#define first_arm_lenght 180.0f //第一个关节长度
#define second_arm_lenght 180.0f //第二个关节长度
#define gripper_lenght 0.0f //机械爪长度
#define gripper_height 0.0f //机械爪高度
```
* linear_interpolation
`linear_interpolation.c/.h`负责5阶插值算法的实现,文件以节点的形式描述。每个阶段有对应的输入参数、5阶方程解、节点队列分别用以下结构体表示:
```c
typedef struct _input_li5_t
{
float t0; //开始时间
float s0; //开始位移
float v0; //开始速度
float a0; //开始加速度
float t1; //结束时间
float s1; //结束加速度
float v1; //结束速度
float a1; //结束加速度
}input_li5_t;
typedef struct _output_li5_t
{
//解算出五阶方程的7个参数
float t0;
float a0;
float a1;
float a2;
float a3;
float a4;
float a5;
}output_li5_t;
typedef struct _li5_t
{
float time; //节点时间
float dis; //节点位移
float vel; //节点速度
float acc; //节点加速度
void * task; //节点任务
output_li5_t solve; //5阶计算解算结果
}li5_t;w
```
文件中 `li5th_solve`根据推导出的公式求解每一个节点的函数的解,`get_displacement_from_li5`、`get_speed_from_li5`、`get_acceleration_from_li5`分别从已经求解的节点获得该时刻的位移,速度,加速度。
文件中 `solve_li5th_node`自动求解一个5阶插值队列(**li5_t**)的所有函数的解。
#### USER文件
最终调车所需要的文件都存在这里。
* path_control
`path_control.c/.h`是路径控制文件,该文件的存在是为了更快速的编辑物流车任务,达到增大调车效率的效果。`path_control`以队列的形式描述物流车任务。每个队列有单独的路径队列、有单独的路径计时器(获取5阶段插值的结果需要以时间为参数)、有单独的变量指示当前队列运行到什么位置。具体形式以结构体表示,如下所示:
```
typedef struct _path_quene_t
{
li5_t* quene; //路径队列
sys_count_time_t timer; //当前路径节点计时器
char p_node; //当前路径节点
}path_quene_t;
```
基于以上结构体,我们可以创建出一个队列对象,控制x方向上的速度,如下所示:
```c
//---------------------------time------dis---speed-----acc--------------//
//创建x轴速度队列
static li5_t node_x_speed[]={{0.0f, 0.0f, 0.0f, 0.0f, NULL}, //出发到扫码区域
{1500.0f, 500.0f, 0.5f, 0.0f, NULL},
{3000.0f, 740.0f, 0.0f, 0.0f, NULL},
{.task = qr_code_task}, //扫码区域到转盘区域
{3000.0f, 1000.0f, 0.0f, 0.0f, NULL},
{.task = rotator_task}, //转盘区域到粗加工区域
{1500.0f, -550.0f, 0.0f, 0.0f, NULL},
{6000.0f, -450.0f, 0.0f, 0.0f, NULL},
{.task = processing_area_task}, //粗加工区域到精加工区域
{2500.0f, 950.0f, 0.1f, 0.0f, NULL},
{4000.0f, 1000.0f, 0.0f, 0.0f, NULL},
{.task = processing_area_task}, //精加工区到转盘
{2000.0f, -100.0f, -0.2f, 0.0f, NULL},
{4000.0f, -600.0f, 0.0f, 0.0f, NULL},
//二程
{.task = rotator_task}, //转盘区域到粗加工区
{1500.0f, -550.0f, 0.0f, 0.0f, NULL},
{6000.0f, -450.0f, 0.0f, 0.0f, NULL},
{.task = processing_area_task}, //粗加工区域到精加工区域
{2500.0f, 950.0f, 0.1f, 0.0f, NULL},
{4000.0f, 1000.0f, 0.0f, 0.0f, NULL},
//回家
{.task = processing_area_task},
{6000.0f, -2000.0f, 0.0f, 0.0f, NULL},
{9000.0f, -2100.0f, 0.0f, 0.0f, NULL},
{.task = &end_quene}};
static path_quene_t x_speed = {.quene = node_x_speed, .timer.count_end_time = 0xFFFFFFFF};
```
可以看到,以上创建了一个名为x_speed的结构体,包含了一个计时时间被设置为0XFFFFFFFF的定时器,还有一个node_x_speed的队列。需要注意的是,创建队列时,不能连续两次创建节点任务。
文件中 `chassis_path_control`是推演函数,推演每个时刻5阶插值的速度输出,函数具体内容较为复杂,感兴趣的可以看一下,函数具体用法如下所示(此段代码在main.c:
```c
while(1)
{
if(timer_10ms_flag)
{
timer_10ms_flag = 0;
_x_speed = chassis_path_control(&x_speed, out_type_speed);
_y_speed = chassis_path_control(&y_speed, out_type_speed);
_w_dis = chassis_path_control(&w_dis, out_type_displacement);
chassis_control_Omni_Wheel(_x_speed, _y_speed, _w_dis);
robot_arm_control_botton_speed_CL(_abs_robot_arm_angle);
}
}
```
`robot_arm_control_botton_speed_CL`函数是在底盘运行轨迹时使机械臂保持在 `_abs_robot_arm_angle`角度,该角度为世界坐标系的角度。
* motion
`motion.c/.h`文件是除底盘外动作文件,是车动作的具体实现,其中 `motion.h`定义了许多位置坐标,修改时主要修改此位置坐标。
* main
`main.c/.h`文件有着程序的**入口**,程序开始先进行各种设备的初始化,接着创建三个方向的插值速度队列,接着求解三个方向每个节点的解,最后每**10ms推演一次**路径。每推演一次路径解算一次该时刻三个方向的队列的速度,若推演到该节点有任务,则优先完成该节点的任务。整个程序以底盘运动三轴速度为主线,其中添加各种任务节点。
在使用的时候,比如需要调整x速度队列某个节点的速度,我们只需要往node_x_speed结构体数组**添加、修改、删除节点**即可。添加任务时,节点可单独使用.task成员指向对应函数,即可在这个节点添加一个自定义任务。
* ~~remote_car_control~~
已弃用,不做介绍。
---
### TODO
无