底盘运动学正逆解算

写在前面
我的底盘控制思路为使用电机的速度模式,通过运动学正逆解算将任务的 X,Y 运动距离转化为四个轮子的转速,从而实现底盘运动。
根据电机编码器返还的值来计算当前底盘位置,从而实现位置闭环控制。 根据陀螺仪的角度来计算当前底盘的角度,从而实现角度闭环控制。
这样可以精细控制电机的运动状态,比如在运动过程中实现边旋转变前进。
底盘运动学正逆解算
关于麦克纳姆轮的运动学正逆解算可以参考:
但是如果想要变旋转边运动我们还需要引入旋转角度 \(\theta\) 影响
运动学正逆解的本质
先回顾一下:
在这里,小车既能 前后移动,又能 左右移动,还可能 原地旋转。
所以我们有两套坐标系:
车体坐标系(局部坐标系)
- 原点在车中心。
- x 轴指前方,y 轴指左方。
- 小车内部的控制、速度、轮子运动都在这个坐标系里计算。
全局坐标系(绝对坐标系)
- 地图坐标,固定不动。
- 小车在地图上的实际位置和方向。
旋转矩阵的作用
假设小车 车头朝东(x轴),它想向前走 1 米,在局部坐标系里就是 \((1, 0)\)。
- 如果地图上北是 y 轴正方向、东是 x 轴正方向,车头朝东 → 不需要旋转。
- 如果车头朝北(局部 x 轴指北) → “局部前进 1 米”在全局坐标里是 \((0, 1)\)。
也就是说:局部坐标系里的前方,在全局坐标系里不一定是同一个方向。
旋转矩阵就是用来把 局部坐标(车体坐标系) 转换到 全局坐标系 的工具,这样我们就可以使得运动过程中的旋转不会影响到我们走直线。
旋转矩阵的原理
二维旋转矩阵:
\[
\begin{bmatrix}
x_\text{global} \\
y_\text{global}
\end{bmatrix}
=
\begin{bmatrix}
\cos\theta & -\sin\theta \\
\sin\theta & \cos\theta
\end{bmatrix}
\begin{bmatrix}
x_\text{local} \\
y_\text{local}
\end{bmatrix}
\]
假设:
- 小车朝向 θ = 45°(前方指向东北)
- 想在全局坐标系里 正东 + 正北方向同时移动,速度分别为
Vx_global = 1, Vy_global = 1
逆解步骤:
旋转矩阵把全局速度换算到局部坐标系:
\[
\begin{aligned}
Vx_{local} &= Vx_{global}\cosθ + Vy_{global}\sinθ = 1*0.707 + 1*0.707 ≈ 1.414 \\
Vy_{local} &= -Vx_{global}\sinθ + Vy_{global}\cosθ = -1*0.707 + 1*0.707 ≈ 0
\end{aligned}
\]
- 结果:在小车自己的坐标系里,它只需要沿前方跑 1.414 米/秒,不需要横向移动
- 旋转矩阵“帮你把全局想走的方向转换成车能直接理解的方向”。
然后加上 角速度 ω,再通过麦克纳姆轮逆解公式算四个轮子的转速。
这样小车就能 同时前进和旋转,边走边转。
代码
运动学逆解算代码:
| C |
|---|
| void Kinematics_Mecanum_CalculateRPM(Kinematics_Struct *Kinematics)
{
float linear_vel_x_mins;
float linear_vel_y_mins;
float angular_vel_z_mins;
float tangential_vel;
float x_rpm;
float y_rpm;
float tan_rpm;
float angle_rad = 0;
angle_rad = HWT101_Struct.CurrentAngle * M_PI / 180.0f;//陀螺仪角度转弧度
float vel_x = Kinematics->Linear_X * cosf(angle_rad) + Kinematics->Linear_Y * sinf(angle_rad);
float vel_y = -Kinematics->Linear_X * sinf(angle_rad) + Kinematics->Linear_Y * cosf(angle_rad);
//convert cm/s to cm/min
linear_vel_x_mins = vel_x * 60;
linear_vel_y_mins = vel_y * 60;
//convert rad/s to rad/min
angular_vel_z_mins = Kinematics->Angular_Z * 0.6 ;
tangential_vel = angular_vel_z_mins * (FR_WHEELS_DISTANCE / 2 + LR_WHEELS_DISTANCE / 2);
x_rpm = linear_vel_x_mins / WHEEL_CIRCUMFERENCE;
y_rpm = linear_vel_y_mins / WHEEL_CIRCUMFERENCE;
tan_rpm = tangential_vel / WHEEL_CIRCUMFERENCE;
//calculate for the target motor RPM and direction
Kinematics->M1_RPM = x_rpm - y_rpm - tan_rpm;//M1电机目标转速 B
Kinematics->M2_RPM = x_rpm + y_rpm + tan_rpm;//M2电机目标转速 C
Kinematics->M3_RPM = x_rpm + y_rpm - tan_rpm;//M3电机目标转速 A
Kinematics->M4_RPM = x_rpm - y_rpm + tan_rpm;//M4电机目标转速 D
}
|
对于正解代码来说,因为张大头电机返回的就是距离值,所以直接可以通过瞬时位移来计算当前速度,而不用再去求导了:
| C |
|---|
| void Kinematics_Mecanum_GetVelocities(Kinematics_Struct *Kinematics)
{
// 计算车体的线速度和角速度
float angle_rad = 0;
angle_rad = HWT101_Struct.CurrentAngle * M_PI / 180.0f;
// 基于麦克纳姆轮正解:车体坐标系下的线速度
float vx = Kinematics->X -Measured_value.old_X;
float vy = Kinematics->Y -Measured_value.old_Y;
Measured_value.old_X = Kinematics->X;
Measured_value.old_Y = Kinematics->Y;
// 变换为全局坐标速度
Kinematics->average_rps_x = vx * cosf(angle_rad) - vy * sinf(angle_rad);
Kinematics->average_rps_y = vx * sinf(angle_rad) + vy * cosf(angle_rad);
Kinematics->average_rps_a = (-Kinematics->M1_RPM + Kinematics->M2_RPM - Kinematics->M3_RPM + Kinematics->M4_RPM) / 4 / 60* WHEEL_CIRCUMFERENCE / (FR_WHEELS_DISTANCE / 2 + LR_WHEELS_DISTANCE / 2);
}
// 函数:计算小车位置
CarPosition_Struct calculateCarPosition(Kinematics_Struct *Kinematics)
{
CarPosition_Struct Measured_Position;
Measured_Position.Valid = SUCCESSRECEIVE;
Measured_Position.x = 0;
Measured_Position.y = 0;
// 假设小车的初始位置为(0, 0)
Measured_value.X += Kinematics->average_rps_x;
Measured_value.Y += Kinematics->average_rps_y;
float angle = Normalize_Angle(HWT101_Struct.CurrentAngle);
if (angle == 0 || angle == 180 || angle == -180) {
Measured_Position.x = Measured_value.X * 0.98;
Measured_Position.y = Measured_value.Y * 0.965;
} else if (angle == 90 || angle == -90) {
Measured_Position.x = Measured_value.X * 0.965;
Measured_Position.y = Measured_value.Y * 0.98;
} else {
Measured_Position.x = Measured_value.X * 0.98;
Measured_Position.y = Measured_value.Y * 0.965;
}
return Measured_Position;
}
|
因为我正解这一部分比较乱在很多地方都有引用,所以知道大概意思就好,注意电机读取的速度一定要快。
一些未经实验的东西
从理论上来说,我们可以通过运动学逆解得到的四个电机速度反带到运动学正解中,计算出当前小车位置。这样我们就可以通过二维卡尔曼滤波来增强整个系统的鲁棒性。
但是实际过程中,因为我们并没有做速度闭环而使用电机本身的速度闭环,所以在一开始时我们系统求出的速度和实际速度会有一个偏差,这个偏差会导致卡尔曼滤波发散。
我其实找到了一些解决方法,比如下面这篇文章中提到的,但是由于时间的关系我并实装在车上,感兴趣的同学可以参考:
关于位置、角度闭环
位置闭环用位置式PID这个没什么好说的。
角度闭环现在我使用的方式还是有一些问题的,主要原因是我直接将陀螺仪的角度进行比例缩放,用位置式 PID 将速度带入逆解公式的角速度中
,这样会导致角速度在开始的时候是突变的,会直接按限值来运行。但是我也没找到比这个更好的方法。
完整代码