4. TF广播器(Broadcaster)编程

写一个简单的TF广播节点,发布静态变换。听起来很简单对吧?但这里头有不少细节,我当年第一次写的时候也踩过坑。今天咱们就手撸一个最干净的静态TF广播器,把原理和代码都掰开揉碎讲清楚。

TF 广播器节点 初始化节点 ros::init + NodeHandle 创建广播器 tf::TransformBroadcaster 发布变换 sendTransform() 静态变换:父→子 固定位姿

4.1 为什么需要广播器?

TF广播器,说白了就是坐标系的“发言人”。你的机器人有激光雷达、底盘、机械臂……每个部件都有自己的坐标系。广播器的任务就是告诉整个系统:“激光雷达坐标系相对于底盘坐标系的位置和姿态是多少”。

我个人习惯把广播器想象成一个实时电台——它不断播报坐标关系,而其他节点(监听器)只要调对频道就能收到。静态变换尤其简单:雷达装在底盘前方10厘米,这个关系几乎不变,所以我们只发布一次,或者以很低的频率重复发布。

4.2 编写静态TF广播节点

咱们用C++写一个最干净的节点。假设有个激光雷达(laser_frame)固定在机器人底盘(base_link)前方0.3米,高度0.2米,无旋转。

核心步骤: 初始化ROS → 创建广播器 → 填充变换 → 循环发布。
// static_tf_broadcaster.cpp
#include <ros/ros.h>
#include <tf/transform_broadcaster.h>

int main(int argc, char** argv) {
    ros::init(argc, argv, "static_tf_broadcaster");
    ros::NodeHandle nh;

    tf::TransformBroadcaster broadcaster;
    tf::Transform transform;

    // 设置平移:x=0.3, y=0.0, z=0.2 (单位:米)
    transform.setOrigin(tf::Vector3(0.3, 0.0, 0.2));
    // 设置旋转:无旋转(单位四元数)
    transform.setRotation(tf::Quaternion(0, 0, 0, 1));

    ros::Rate rate(10);  // 10Hz 发布,静态变换其实1Hz也够
    while (ros::ok()) {
        // 发布变换:父坐标系 base_link → 子坐标系 laser_frame
        broadcaster.sendTransform(
            tf::StampedTransform(transform, ros::Time::now(),
                                 "base_link", "laser_frame"));
        rate.sleep();
    }
    return 0;
}

嗯,这里要注意:sendTransform 的第一个参数是 StampedTransform,里面包含了变换本身、时间戳、父坐标系和子坐标系的名字。我见过不少新手把父和子搞反——记住,第一个字符串是父,第二个是子。

💡 我的小习惯: 静态变换我通常用 tf2_ros::StaticTransformBroadcaster,只发布一次,系统会自动记住。但为了让你看清广播器的底层逻辑,上面用了最原始的 TransformBroadcaster。两种都能用,看场景。

4.3 编译与运行

在 CMakeLists.txt 里加上:

add_executable(static_tf_broadcaster src/static_tf_broadcaster.cpp)
target_link_libraries(static_tf_broadcaster ${catkin_LIBRARIES})

然后编译:

cd ~/catkin_ws
catkin_make
source devel/setup.bash
rosrun tf_demo static_tf_broadcaster

运行之后,用 tf_echo 工具验证一下:

rosrun tf tf_echo base_link laser_frame

你应该会看到类似这样的输出:

At time 1698123456.123
- Translation: [0.300, 0.000, 0.200]
- Rotation: in Quaternion [0.000, 0.000, 0.000, 1.000]
⚠️ 我曾经踩过的坑: 有一次我忘了调用 ros::Time::now(),而是传了一个默认时间戳。结果 tf 树一直报“extrapolation error”。记住,时间戳一定要用 ros::Time::now() 或者当前有效时间,否则监听器那边会抱怨“未来数据”或“过期数据”。

4.4 代码拆解——每一行都在干什么

代码片段作用
tf::TransformBroadcaster broadcaster;创建广播器对象,内部连接TF话题
transform.setOrigin(...)设置平移向量 (x, y, z) 米
transform.setRotation(...)设置旋转四元数 (x, y, z, w)
tf::StampedTransform(...)把变换+时间戳+坐标系名打包
broadcaster.sendTransform(...)发布到 /tf 话题,所有监听器都能收到

你想想看,整个流程其实就三步:造变换 → 打时间戳 → 扔出去。静态变换尤其简单,因为旋转和平移都是固定的。但如果你做动态变换(比如机械臂关节角度变化),就需要在循环里不断更新 transform 的内容。

4.5 静态变换 vs 动态变换——什么时候用哪个?

我个人经验是:固定安装的传感器、结构件,一律用静态变换。比如摄像头、激光雷达、imu 的安装位置。而关节、轮子、滑动机构,这些会动的部件才用动态广播器。

  • 静态变换:发布一次,或者低频发布(1~10Hz),不占用带宽。
  • 动态变换:每个周期都要更新,频率通常 50~100Hz,适合关节状态。

ROS 里还有一个专门的 tf2_ros::StaticTransformBroadcaster,它会把变换发布到 /tf_static 话题,订阅者只需要接收一次,之后自动缓存。如果你做静态广播,我建议直接用那个——省心。

一句话总结: 静态广播器 = 固定坐标关系 + 低频发布;动态广播器 = 变化坐标关系 + 高频更新。今天咱们写的这个节点,虽然用了普通广播器,但发布的是固定变换,所以本质上还是静态广播。

好了,现在你已经会写一个最基础的TF广播节点了。试着改改平移和旋转,看看 tf_echo 的输出变化。