3. ROS2 TF2核心概念
这一章我们直接动手。静态广播器,说白了就是发布一个永远不变的坐标变换。比如机器人底盘 base_link 和激光雷达 laser_frame 之间的位置关系——装上去就不会动了。我刚开始用ROS2的时候,觉得这玩意儿太简单,结果在参数上翻过车。嗯,咱们一步步来,把坑填平。
为什么需要静态广播器?
机器人身上有很多“焊死”的坐标系。激光雷达装在底盘前方10厘米,摄像头固定在云台上——这些相对关系不会随时间改变。你当然可以用动态广播器每毫秒发一次同样的数据,但浪费CPU,也增加网络负担。静态广播器只发一次,TF树就记住了。我个人习惯:所有固定安装的传感器,一律用静态广播器,省心。
方法一:编写静态广播节点(C++)
咱们先写一个正经的节点。虽然ROS2提供了现成的工具,但有时候你需要把静态变换嵌入到自己的启动流程里。比如在读取完配置文件后,再发布变换。
// static_broadcaster.cpp
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/static_transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>
class StaticFramePublisher : public rclcpp::Node
{
public:
StaticFramePublisher() : Node("static_broadcaster_node")
{
broadcaster_ = std::make_shared<tf2_ros::StaticTransformBroadcaster>(this);
publish_transform();
}
private:
void publish_transform()
{
geometry_msgs::msg::TransformStamped t;
t.header.stamp = this->get_clock()->now();
t.header.frame_id = "base_link";
t.child_frame_id = "laser_frame";
t.transform.translation.x = 0.10;
t.transform.translation.y = 0.0;
t.transform.translation.z = 0.05;
t.transform.rotation.x = 0.0;
t.transform.rotation.y = 0.0;
t.transform.rotation.z = 0.0;
t.transform.rotation.w = 1.0;
broadcaster_->sendTransform(t);
RCLCPP_INFO(this->get_logger(), "已发布 base_link → laser_frame 静态变换");
}
std::shared_ptr<tf2_ros::StaticTransformBroadcaster> broadcaster_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<StaticFramePublisher>());
rclcpp::shutdown();
return 0;
}
这段代码里,我特意把平移设成了 x=0.10, z=0.05——模拟激光雷达装在底盘前方10厘米、高5厘米。旋转用的是单位四元数,表示没有旋转。你想想看,如果激光雷达是倒着装的,就得调四元数了。
tf2::Quaternion 辅助生成。比如绕Z轴转90度:q.setRPY(0, 0, 1.5708)。别手算,容易错。
方法二:static_transform_publisher 命令行
这是我最常用的方式——尤其是调试的时候。一行命令,不用编译,不用写节点。ROS2 内置了这个工具,参数格式很灵活。
# 标准格式:x y z yaw pitch roll frame_id child_frame_id
ros2 run tf2_ros static_transform_publisher 0.10 0 0.05 0 0 0 base_link laser_frame
# 或者用四元数格式(更精确)
ros2 run tf2_ros static_transform_publisher 0.10 0 0.05 0 0 0 1 base_link laser_frame
注意看,第一种格式最后六个数字:平移xyz + 欧拉角(yaw, pitch, roll)。第二种格式平移xyz + 四元数xyzw。我个人更推荐四元数版本,因为欧拉角容易遇到万向锁,虽然静态变换很少碰到,但养成好习惯嘛。
static_transform_publisher 的欧拉角顺序是 yaw pitch roll(即 ZYX)。记住:偏航-俯仰-横滚。
两种方式对比
| 特性 | 编写节点 | 命令行工具 |
|---|---|---|
| 灵活性 | 高(可加逻辑、读参数) | 低(固定参数) |
| 启动集成 | 适合嵌入 launch 文件 | 适合快速测试 |
| 依赖 | 需要编译 | 无需编译,直接运行 |
| 发布频率 | 默认只发一次(静态) | 默认只发一次(静态) |
验证你的静态变换
运行之后,用 tf2_echo 看一眼:
ros2 run tf2_ros tf2_echo base_link laser_frame
如果看到平移 (0.10, 0.00, 0.05) 和单位四元数,说明成功了。我记得第一次用这个命令时,等了半天没输出——原来是我忘了先启动节点。嗯,检查一下节点是否在运行。
/tf_static 话题上,而不是普通的 /tf。你可以用 ros2 topic echo /tf_static 查看原始消息。这样做的好处是:不会随时间重复广播,节省带宽。
把静态变换写进 launch 文件
实际项目中,你不会每次都敲命令行。把它塞进 launch 文件才是正道。下面是一个 Python launch 示例:
# static_broadcaster_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='tf2_ros',
executable='static_transform_publisher',
arguments=['0.10', '0', '0.05', '0', '0', '0', '1', 'base_link', 'laser_frame'],
output='screen'
)
])
启动方式:ros2 launch your_package static_broadcaster_launch.py。这样你的团队就不用记那一长串参数了。
常见问题 & 避坑
- 变换没生效? 检查 frame_id 和 child_frame_id 是否拼写正确。大小写敏感。
- 多个静态变换冲突? 同一个父子关系只能有一个静态变换。如果重复发布,后面的会覆盖前面的。
- 时间戳问题? 静态变换的时间戳通常设为0或当前时间。设为0表示“永远有效”,我一般用当前时间,避免TF树出现负时间。
好了,静态广播器就这些内容。说白了就是“一次发布,终身受用”。