2. 运动学基础:底盘坐标系定义,平移与旋转的数学表示
各位同学,欢迎来到第二章。这一章是底盘控制的“地基”。
说实话,我见过太多人一上来就调PID、写运动学代码,结果车子跑起来歪歪扭扭,找半天原因——坐标系定义反了。嗯,这种坑我踩过不止一次。所以这一章,咱们把坐标系和数学基础打扎实。
2.1 底盘坐标系:世界系 vs 机器人系
做全向轮控制,脑子里必须同时装着两个坐标系:世界坐标系和机器人坐标系。
- 世界坐标系:固定在地面上的参考系。通常用 \( X_W, Y_W \) 表示。你给底盘下达“向东移动1米”,这个“东”就是世界坐标系下的方向。
- 机器人坐标系:固定在底盘上的参考系。用 \( X_R, Y_R \) 表示。原点在底盘中心,\( X_R \) 指向车头方向,\( Y_R \) 指向左侧。
我个人习惯,在ROS里把机器人坐标系命名为 base_link,世界坐标系叫 odom 或 map。这个命名规范,后面写代码会省很多事。
2.2 平移运动的数学表示
平移,说白了就是“从A点走到B点”。在二维平面上,我们用向量来表示位置。
假设底盘在世界坐标系下的位置是 \( (x, y) \),那么它的位置向量就是:
P = [x, y]^T
如果底盘移动了一段距离 \( \Delta x \) 和 \( \Delta y \),新位置就是:
P_new = P_old + [Δx, Δy]^T
我在项目中遇到过一个问题:用麦克纳姆轮做全向移动时,如果只给 \( \Delta x \) 不给 \( \Delta y \),车子会原地打转。后来发现是坐标系搞反了——我把机器人坐标系下的指令直接发给了世界坐标系。所以,平移向量一定要明确是在哪个坐标系下计算的。
2.3 旋转运动的数学表示
旋转比平移稍微复杂一点。底盘绕自身中心旋转,我们用角度 \( \theta \) 来表示姿态。
为什么用角度?因为全向轮底盘可以原地自转,这个自由度必须单独控制。
旋转的数学表示,最常用的是旋转矩阵。二维平面下的旋转矩阵长这样:
R(θ) = [cosθ -sinθ]
[sinθ cosθ]
如果你有一个点 \( (x, y) \) 在机器人坐标系下,想把它转到世界坐标系下,就乘上这个旋转矩阵:
P_world = R(θ) * P_robot
反过来,从世界坐标系转到机器人坐标系,就用逆矩阵 \( R^{-1}(\theta) \),其实就是转置矩阵:
P_robot = R(θ)^T * P_world
tf2 库做转换非常方便。
2.4 平移 + 旋转:齐次坐标
把平移和旋转写在一起,就引出了齐次坐标。说白了,就是把旋转矩阵和平移向量拼成一个3x3的矩阵:
T = [R(θ) | t]
[0 0 1 | 1]
其中 \( t = [x, y]^T \) 是平移向量。这样,一次矩阵乘法就能同时完成旋转和平移:
P_world_homogeneous = T * P_robot_homogeneous
为什么要用齐次坐标?因为方便。你想想看,如果不用齐次坐标,你得先旋转再平移,写两行代码。用齐次坐标,一行搞定。而且ROS里的 tf 树就是基于齐次变换的。
cos(30) 默认是弧度,而我传了30度进去。结果底盘转得乱七八糟。所以,所有三角函数计算,务必用弧度制。
2.5 知识体系结构图
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个“思维导图”来理解:
从图中可以看到,坐标系定义是基础,平移和旋转是两大核心操作,最后通过齐次变换矩阵统一起来,应用到ROS的tf树和运动学解算中。
2.6 实战小贴士:代码里的坐标系管理
在ROS里,我推荐用 tf2 库来管理坐标系变换。下面是一个简单的C++示例,演示如何监听 odom 到 base_link 的变换:
#include <tf2_ros/transform_listener.h>
#include <tf2_geometry_msgs/tf2_geometry_msgs.h>
tf2_ros::Buffer tf_buffer;
tf2_ros::TransformListener listener(tf_buffer);
geometry_msgs::TransformStamped transform;
try {
transform = tf_buffer.lookupTransform("odom", "base_link", ros::Time(0));
} catch (tf2::TransformException &ex) {
ROS_WARN("Could not get transform: %s", ex.what());
}
// 提取平移和旋转
double x = transform.transform.translation.x;
double y = transform.transform.translation.y;
double yaw = tf2::getYaw(transform.transform.rotation);
这段代码我几乎每个项目都会用。注意 ros::Time(0) 表示获取最新可用的变换,实时性要求高的场景下很实用。
好了,这一章就到这里。记住,数学是工具,不是负担。多用几次就熟了。