4. TF广播器(Broadcaster)编程
写一个简单的TF广播节点,发布静态变换。听起来很简单对吧?但这里头有不少细节,我当年第一次写的时候也踩过坑。今天咱们就手撸一个最干净的静态TF广播器,把原理和代码都掰开揉碎讲清楚。
4.1 为什么需要广播器?
TF广播器,说白了就是坐标系的“发言人”。你的机器人有激光雷达、底盘、机械臂……每个部件都有自己的坐标系。广播器的任务就是告诉整个系统:“激光雷达坐标系相对于底盘坐标系的位置和姿态是多少”。
我个人习惯把广播器想象成一个实时电台——它不断播报坐标关系,而其他节点(监听器)只要调对频道就能收到。静态变换尤其简单:雷达装在底盘前方10厘米,这个关系几乎不变,所以我们只发布一次,或者以很低的频率重复发布。
4.2 编写静态TF广播节点
咱们用C++写一个最干净的节点。假设有个激光雷达(laser_frame)固定在机器人底盘(base_link)前方0.3米,高度0.2米,无旋转。
// 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 的输出变化。