4、机器人运动学基础:刚体运动描述、旋转矩阵与四元数、坐标变换(TF2)原理

各位同学,欢迎来到运动学基础这一章。

说实话,这部分内容看起来有点数学,但它是整个机器人控制的基石。你想想看,机器人要抓个杯子,首先得知道杯子在哪儿,自己的手在哪儿,对吧?这背后就是刚体运动描述和坐标变换。

我个人习惯把这一章叫做“机器人的世界观”。搞懂了它,你写出来的代码才靠谱。

4.1 刚体运动描述:位置与姿态

一个刚体在三维空间里,有6个自由度。3个是位置(x, y, z),3个是姿态(绕x, y, z轴的旋转)。

位置很好理解,就是坐标。姿态呢?说白了就是“它朝哪边看”。

描述姿态,我们常用的有三种方式:旋转矩阵、欧拉角、四元数。我一个个说。

4.1.1 旋转矩阵

旋转矩阵是一个3x3的矩阵。它能把一个向量从一个坐标系,转到另一个坐标系。

比如,我有一个点P,在坐标系A里的坐标是P_A。我想知道它在坐标系B里的坐标P_B。如果我知道坐标系B相对于A的旋转矩阵R_BA,那直接乘就行:

P_B = R_BA * P_A

旋转矩阵有个特点:它是正交矩阵,行列式为1。这意味着它不会拉伸或压缩向量,只会旋转。

核心要点:

  • 旋转矩阵有9个元素,但只有3个自由度(冗余)。
  • 它没有万向锁问题,但计算量大。
  • 组合多个旋转时,矩阵乘法顺序很重要。

我在项目中遇到过一个问题:用旋转矩阵做连续旋转,结果数值误差越来越大,矩阵慢慢变得不正交了。嗯,这里要注意,长时间运行后,最好做一次正交化处理。

4.1.2 欧拉角

欧拉角用三个角度来描述旋转:绕x轴转(roll)、绕y轴转(pitch)、绕z轴转(yaw)。

直观,好理解。但有个致命问题——万向锁。

避坑指南:

我曾经在做一个机械臂的逆解时,用了欧拉角。结果在pitch接近90度时,roll和yaw突然变得无法区分,机械臂直接乱跳。这就是万向锁。从那以后,我在内部计算中再也不敢用欧拉角了。

ROS2里,欧拉角主要用于人机交互的输入输出。内部计算,我建议用四元数。

4.1.3 四元数

四元数是个很神奇的东西。它用四个数(w, x, y, z)来表示旋转。

为什么用四个数?因为它避免了万向锁,而且插值平滑。

四元数的基本形式:

q = w + x*i + y*j + z*k

其中,i² = j² = k² = ijk = -1。

单位四元数表示旋转。w是实部,代表旋转角的一半的余弦。x, y, z是虚部,代表旋转轴的方向。

我的小技巧:

ROS2里,四元数用 geometry_msgs::msg::Quaternion 表示。你不需要手算四元数,直接用TF2的API转换就行。但你要理解它的含义,不然调试时一脸懵。

四元数乘法:

q1 * q2 表示先做q2旋转,再做q1旋转

注意顺序!和矩阵乘法一样,不满足交换律。

4.2 坐标变换(TF2)原理

好了,数学基础有了。现在说说ROS2里怎么用——TF2。

TF2是ROS2的坐标变换库。它维护了一棵“坐标变换树”。

举个例子:

  • 世界坐标系:map
  • 机器人基座:base_link
  • 激光雷达:laser
  • 摄像头:camera

这些坐标系之间,通过TF2发布变换关系。你只需要知道两个坐标系的名字,就能随时获取它们之间的变换。

// 获取 laser 到 base_link 的变换
geometry_msgs::msg::TransformStamped t;
t = tf_buffer->lookupTransform("base_link", "laser", rclcpp::Time(0));

rclcpp::Time(0) 表示获取最新的一帧变换。

TF2的核心概念:

  • 广播器(Broadcaster):发布坐标系之间的变换关系。
  • 监听器(Listener):接收并缓存所有变换关系。
  • 变换树(Transform Tree):所有坐标系构成一棵树,根节点通常是map或odom。
  • 时间戳:每个变换都有时间戳,支持历史查询。

我个人习惯,在机器人启动时,就把所有固定坐标系(比如base_link到laser)的变换发布好。动态的(比如机械臂末端)则实时发布。

4.2.1 TF2的工作流程

下面这张图,是我自己总结的TF2工作流程。你看一遍就明白了。

传感器节点 (发布 laser→base_link) TF2广播器 (TransformBroadcaster) TF2缓存 (Buffer) TF2监听器 (TransformListener) 用户节点 (查询变换) 发布变换 存储 更新缓存 查询

流程很简单:

  1. 传感器节点(比如激光雷达驱动)计算出laser到base_link的变换。
  2. 它通过TF2广播器发布这个变换。
  3. TF2监听器收到后,存入缓存(Buffer)。
  4. 用户节点(比如导航模块)从缓存中查询需要的变换。

你看,整个过程是解耦的。每个节点只关心自己知道的变换,不用管全局。

4.2.2 静态变换与动态变换

类型 特点 示例 发布方式
静态变换 固定不变 base_link → laser static_transform_publisher
动态变换 随时间变化 odom → base_link TransformBroadcaster

静态变换,我建议用launch文件里的static_transform_publisher。省事,而且不会因为节点重启而丢失。

# 在launch文件中声明静态变换
static_transform_publisher x y z yaw pitch roll frame_id child_frame_id

动态变换,比如里程计到基座的变换,就得在代码里实时发布了。

我的经验:

TF2的缓存默认保留10秒的历史数据。如果你需要查询历史变换,记得设置合适的缓存时间。我曾经调试一个延时问题,查了半天才发现是缓存时间太短,历史数据被清掉了。

4.3 旋转矩阵与四元数的互转

在实际开发中,你经常需要在旋转矩阵和四元数之间转换。ROS2的tf2提供了现成的函数。

#include <tf2/LinearMath/Quaternion.h>
#include <tf2/LinearMath/Matrix3x3.h>

// 四元数转旋转矩阵
tf2::Quaternion q(0.707, 0.0, 0.0, 0.707);
tf2::Matrix3x3 m(q);

// 旋转矩阵转四元数
tf2::Matrix3x3 m2;
m2.setRPY(1.57, 0.0, 0.0);  // roll=90度
tf2::Quaternion q2;
m2.getRotation(q2);

我个人习惯,在代码内部统一用四元数。只在需要显示给用户看时,才转成欧拉角。

注意:

四元数有“双倍覆盖”特性。q和-q表示同一个旋转。这在插值时要注意,不然可能走远路。

好了,这一章的内容就这些。运动学基础是后面所有控制算法的基础。你花时间搞懂它,后面写代码会顺畅很多。