第4章 TF与速度控制:速度指令在TF下的表达
速度控制,说白了就是让机器人动起来,而且动得听话。但光发一个「往前走」的指令远远不够——你得告诉它:往哪个坐标系走?速度怎么分解到每个关节?轮子转多快?这些问题的答案,全都绕不开TF。
我个人习惯把速度控制拆成三层来看:指令层(Twist消息)、映射层(关节空间分配)、融合层(TF+里程计)。这一章咱们就把这三层挨个捋一遍。
4.1 速度指令在TF下的表达
你给机器人发一个 cmd_vel,里面装的是 Twist 消息。但机器人怎么理解这个速度?它得知道:这个速度是相对于哪个坐标系的。
我见过不少新手直接往 base_link 里塞速度,结果机器人原地打转——因为没考虑TF变换。其实,速度指令天然带有坐标系属性。比如你发 linear.x = 0.5,如果是在 base_link 下,那就是朝车头方向走;如果在 odom 下,那就是朝世界坐标系的x轴走。
base_link,因为关节运动学计算通常基于机器人本体。
举个例子,如果你从导航栈收到一个 odom 坐标系下的速度,你需要用TF把它转到 base_link:
// 伪代码:速度从 odom 变换到 base_link
geometry_msgs::TwistStamped twist_odom, twist_base;
tf_listener.transformTwist("base_link", twist_odom, twist_base);
嗯,这里要注意:transformTwist 不仅会旋转速度向量,还会考虑平移带来的角速度耦合。说白了,如果两个坐标系原点不重合,线速度也会受影响。
4.2 Twist消息详解
Twist 消息是ROS里最常用的速度容器。结构很简单,就6个分量:
| 字段 | 类型 | 说明 | 典型值 |
|---|---|---|---|
| linear.x | float64 | 前后方向线速度 (m/s) | 0.5 |
| linear.y | float64 | 左右方向线速度 (m/s) | 0.0 |
| linear.z | float64 | 垂直方向线速度 (m/s) | 0.0 |
| angular.x | float64 | 绕X轴角速度 (rad/s) | 0.0 |
| angular.y | float64 | 绕Y轴角速度 (rad/s) | 0.0 |
| angular.z | float64 | 绕Z轴角速度 (rad/s) | 0.3 |
你想想看,对于差速轮机器人,其实只用得到 linear.x 和 angular.z。但全向轮或者机械臂,六个自由度全都要用上。
linear.y 填成了正值,结果车子横着走反了——因为坐标系定义和电机映射方向不一致。后来我养成了一个习惯:先在仿真里用 rviz 把Twist向量可视化出来,确认方向再上路。
4.3 将速度指令映射到关节空间
拿到Twist之后,下一步就是把它变成每个关节的速度。这一步说白了就是运动学逆解算。对于移动机器人,通常用 运动学模型 来算轮子转速。
差速轮模型是最经典的:
// 差速轮逆运动学
double v = twist.linear.x;
double w = twist.angular.z;
double wheel_base = 0.5; // 轮距,单位米
double wheel_radius = 0.1;
double left_speed = (v - w * wheel_base / 2.0) / wheel_radius;
double right_speed = (v + w * wheel_base / 2.0) / wheel_radius;
你看,这里隐含了一个假设:速度是在 base_link 坐标系下表达的。如果Twist是在其他坐标系,必须先变换,否则算出来的轮速就是错的。
对于机械臂,映射就更复杂了。你需要用雅可比矩阵把末端速度映射到关节速度:
// 伪代码:关节速度 = 雅可比逆 × 末端Twist
Eigen::VectorXd joint_velocities = jacobian_inverse * twist_end_effector;
我建议你在做这一步时,一定要检查雅可比矩阵的奇异性。我曾经在某个奇异位型附近硬算,结果关节速度直接飞了,差点把电机烧了。
clamp 函数,把每个关节速度限制在安全范围内。
4.4 TF与里程计融合
速度控制不能开环跑,你得知道机器人实际走了多少。里程计就是干这个的——它通过轮子编码器或者视觉/激光数据,估算出机器人的位姿变化。
TF在这里扮演了桥梁的角色。典型的做法是:
- odom → base_link 变换由里程计节点发布
- 速度控制节点监听这个变换,得到实际位移
- 把实际位移和期望速度做比较,形成闭环
说白了,里程计就是TF树里那个不断更新的动态分支。我习惯在代码里这样融合:
// 从TF获取里程计位移
tf::StampedTransform transform;
tf_listener.lookupTransform("odom", "base_link", ros::Time(0), transform);
// 提取位移和旋转
double dx = transform.getOrigin().x();
double dy = transform.getOrigin().y();
double dyaw = tf::getYaw(transform.getRotation());
// 与期望速度做差,送入PID
double v_error = v_desired - (dx / dt);
double w_error = w_desired - (dyaw / dt);
ekf_localization 做最优融合。
嗯,这里还要注意时间戳。TF的 lookupTransform 如果时间对不齐,拿到的变换可能是滞后的。我个人的做法是:用 ros::Time(0) 获取最新变换,但配合 waitForTransform 确保数据到位。
最后总结一句:速度控制不是发个Twist就完事了。你得让TF、运动学、里程计三者协同工作。任何一个环节脱节,机器人都会跑偏。