目录
[1.1 普通函数是主动调用](#1.1 普通函数是主动调用)
[1.2 回调函数是被动调用](#1.2 回调函数是被动调用)
[1.3 ROS2 为什么大量使用回调函数?](#1.3 ROS2 为什么大量使用回调函数?)
[1.4 回调函数和 rclcpp::spin() 的关系](#1.4 回调函数和 rclcpp::spin() 的关系)
[二、Topic 订阅回调函数详解](#二、Topic 订阅回调函数详解)
[2.1 Topic 回调函数的作用](#2.1 Topic 回调函数的作用)
[2.2 订阅 /cmd_vel 的完整代码](#2.2 订阅 /cmd_vel 的完整代码)
[2.3 这段代码的执行流程](#2.3 这段代码的执行流程)
[2.4 SharedPtr msg 是什么意思?](#2.4 SharedPtr msg 是什么意思?)
[2.5 为什么是 msg->linear.x?](#2.5 为什么是 msg->linear.x?)
[三、Timer 定时器回调函数详解](#三、Timer 定时器回调函数详解)
[3.1 Timer 回调函数的作用](#3.1 Timer 回调函数的作用)
[3.2 定时发布 /cmd_vel 的完整代码](#3.2 定时发布 /cmd_vel 的完整代码)
[3.3 Timer 回调函数什么时候执行?](#3.3 Timer 回调函数什么时候执行?)
[3.4 Timer 回调为什么没有参数?](#3.4 Timer 回调为什么没有参数?)
[四、Service 和 Action 中的回调函数](#四、Service 和 Action 中的回调函数)
[4.1 Service 服务回调函数的作用](#4.1 Service 服务回调函数的作用)
[4.2 自定义服务接口示例](#4.2 自定义服务接口示例)
[4.3 Service 回调函数代码示例](#4.3 Service 回调函数代码示例)
[4.4 Service 回调函数什么时候触发?](#4.4 Service 回调函数什么时候触发?)
[4.5 Action 中的回调函数简单理解](#4.5 Action 中的回调函数简单理解)
[五、std::bind、this、_1、_2 和常见问题总结](#五、std::bind、this、_1、_2 和常见问题总结)
[5.1 std::bind 是什么?](#5.1 std::bind 是什么?)
[5.2 &LimoTopicSub::cmd_callback 是什么意思?](#5.2 &LimoTopicSub::cmd_callback 是什么意思?)
[5.3 this 是什么意思?](#5.3 this 是什么意思?)
[5.4 _1 和 _2 是什么意思?](#5.4 _1 和 _2 是什么意思?)
[5.5 Timer 为什么不需要 _1?](#5.5 Timer 为什么不需要 _1?)
[5.6 也可以使用 Lambda 表达式写回调](#5.6 也可以使用 Lambda 表达式写回调)
[5.7 常见错误总结](#5.7 常见错误总结)
[(1)忘记写 rclcpp::spin(node)](#(1)忘记写 rclcpp::spin(node))
[(2)Topic 回调参数类型写错](#(2)Topic 回调参数类型写错)
[(4)指针访问成员时写成了 .](#(4)指针访问成员时写成了 .)
[(5)Service 回调少写 _2](#(5)Service 回调少写 _2)
摘要
在 ROS2 C++ 开发中,回调函数****是一个非常核心的概念。
无论是订阅话题、定时发布消息、处理服务请求,还是执行 Action 任务,都会大量用到回调函数。
很多初学者刚开始学习 ROS2 时,经常会看到下面这种代码:
cpp
std::bind(&LimoTopicSub::cmd_callback, this, std::placeholders::_1)
或者:
cpp
this->create_wall_timer(
500ms,
std::bind(&LimoTopicCmd::timer_callback, this)
);
看起来比较复杂,其实本质并不难。
一句话理解:
回调函数就是提前写好的函数,等某个事件发生时,由 ROS2 自动帮我们调用。
本篇文章就围绕 ROS2 C++ 中的回调函数展开,结合 Topic、Timer、Service、Action 几种常见场景,详细讲清楚:
- 回调函数是什么
- 什么时候会触发
std::bind是什么this是什么std::placeholders::_1又是什么意思
一、回调函数到底是什么?
1.1 普通函数是主动调用
先看一个普通函数:
cpp
#include <iostream>
void say_hello()
{
std::cout << "Hello ROS2" << std::endl;
}
int main()
{
say_hello();
return 0;
}
这段代码中,say_hello() 是我们在 main() 函数中主动调用的。
执行流程是:
main 函数开始执行
↓
主动调用 say_hello()
↓
say_hello() 执行
↓
程序结束
也就是说:
普通函数一般是我们自己主动调用。
1.2 回调函数是被动调用
回调函数不一样。
回调函数通常不是我们自己直接调用,而是提前把函数交给 ROS2,当某个事件发生时,ROS2 自动帮我们调用这个函数。
例如订阅 /cmd_vel 话题:
cpp
sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel",
10,
std::bind(&LimoTopicSub::cmd_callback, this, std::placeholders::_1)
);
这里的 cmd_callback() 并不是我们自己手动调用的。
它的触发条件是:
/cmd_vel 话题收到新消息
↓
ROS2 自动调用 cmd_callback()
↓
回调函数解析收到的消息
所以可以这样理解:
- 普通函数:我主动调用函数
- 回调函数:事件发生后,系统自动调用函数
1.3 ROS2 为什么大量使用回调函数?
ROS2 是机器人通信框架。
机器人程序和普通程序不太一样。
1. 普通程序很多时候是从上往下执行一遍:
第一步
第二步
第三步
程序结束
2. 但是机器人程序需要一直运行,并且随时响应各种事件。
例如:
有没有新的速度指令?
有没有新的雷达数据?
有没有客户端发送服务请求?
有没有新的 Action 目标?
定时器时间到了没有?
这些事件什么时候发生是不固定的。
所以ROS2 采用了事件驱动的方式:
- 收到话题消息 → 触发 Topic 回调函数
- 定时器到时间 → 触发 Timer 回调函数
- 收到服务请求 → 触发 Service 回调函数
- 收到 Action 目标 → 触发 Action 回调函数
这就是 ROS2 中回调函数非常重要的原因。
1.4 回调函数和 rclcpp::spin() 的关系
很多初学者写完回调函数后,发现程序没有反应。
其中一个常见原因就是没有写:
cpp
rclcpp::spin(node);
一个完整的 ROS2 节点主函数一般是这样:
cpp
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<LimoTopicSub>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
其中:
rclcpp::spin(node);
可以理解为:
让节点一直运行,并且不断检查有没有事件需要处理。
比如:
有没有新的 Topic 消息?
有没有 Timer 到时间?
有没有 Service 请求?
有没有 Action 目标?
如果有事件发生,ROS2 就会自动调用对应的回调函数。
所以:
1. 没有 spin:
节点创建完可能就结束了,回调函数没有机会执行。2. 有 spin:
节点持续运行,等待事件发生,然后触发回调函数。
二、Topic 订阅回调函数详解
2.1 Topic 回调函数的作用
Topic 订阅回调函数的作用是:
当订阅的话题收到新消息时,自动执行对应的处理逻辑。
例如小车控制中经常使用的话题:
/cmd_vel
消息类型是:
geometry_msgs/msg/Twist
这个话题通常用来发送小车速度指令。
2.2 订阅 /cmd_vel 的完整代码
下面是一个 ROS2 C++ 订阅者节点,用来订阅 /cmd_vel 话题。
cpp
#include <memory>
#include <functional>
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp"
using std::placeholders::_1;
class LimoTopicSub : public rclcpp::Node
{
public:
LimoTopicSub() : Node("limo_topic_sub")
{
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel",
10,
std::bind(&LimoTopicSub::cmd_callback, this, _1)
);
RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started.");
}
private:
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(),
"收到速度指令:linear.x = %.2f, angular.z = %.2f",
msg->linear.x,
msg->angular.z);
}
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<LimoTopicSub>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
2.3 这段代码的执行流程
代码运行后,整体流程如下:
cpp
程序启动
↓
创建 LimoTopicSub 节点
↓
执行构造函数 LimoTopicSub()
↓
创建 /cmd_vel 订阅者
↓
打印:limo_topic_sub node has started.
↓
进入 rclcpp::spin(node)
↓
等待 /cmd_vel 话题消息
↓
一旦收到 Twist 消息
↓
ROS2 自动调用 cmd_callback()
↓
打印 linear.x 和 angular.z
所以如果一直没有任何节点往 /cmd_vel 发布消息,终端通常只会看到:
[INFO] [limo_topic_sub]: limo_topic_sub node has started.
然后程序不会结束,而是一直卡在这里等待:
rclcpp::spin(node);
因此:
cmd_callback()
不需要我们自己手动调用。
只要 /cmd_vel 收到新消息,ROS2 就会自动调用它。
2.4 SharedPtr msg 是什么意思?
回调函数中有这样一行:
cpp
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
可以拆开理解:
geometry_msgs::msg::Twist
表示消息类型是 Twist。
SharedPtr
表示智能指针。
msg
表示变量名。
所以这一行整体意思是:
- 定义一个回调函数 cmd_callback
- 它接收一个 Twist 类型的消息指针
- 这个消息指针的名字叫 msg
其中 msg 只是变量名,可以修改。
例如:
cpp
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr data)
{
RCLCPP_INFO(this->get_logger(), "linear.x = %.2f", data->linear.x);
}
这样写也是可以的。
只不过ROS2 代码中习惯写成 msg,因为它一看就知道代表接收到的消息。
2.5 为什么是 msg->linear.x?
因为 msg 是指针,所以访问里面的成员变量时要用:
msg->linear.x
msg->angular.z
而不是:
msg.linear.x
msg.angular.z
简单记:
普通对象访问成员:用 .
指针访问成员:用 ->
所以:
msg->linear.x
可以理解为:
访问 msg 指向的 Twist 消息里面的 linear.x 字段
三、Timer 定时器回调函数详解
3.1 Timer 回调函数的作用
Timer 定时器回调函数用于周期性执行某个任务。
例如:
- 每 500ms 发布一次速度指令
- 每 1 秒打印一次状态
- 每 100ms 读取一次传感器数据
- 每隔一段时间检查一次任务状态
在 ROS2 小车控制中,经常会用 Timer 定时发布 /cmd_vel 速度指令。
3.2 定时发布 /cmd_vel 的完整代码
下面这个节点会每隔 500ms 执行一次 timer_callback(),并向 /cmd_vel 发布速度消息。
cpp
#include <memory>
#include <functional>
#include <chrono>
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp"
using namespace std::chrono_literals;
class LimoTopicCmd : public rclcpp::Node
{
public:
LimoTopicCmd() : Node("limo_topic_cmd"), count_(0)
{
pub_vel_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel",
10
);
timer_ = this->create_wall_timer(
500ms,
std::bind(&LimoTopicCmd::timer_callback, this)
);
RCLCPP_INFO(this->get_logger(), "limo_topic_cmd node has started.");
}
private:
void timer_callback()
{
geometry_msgs::msg::Twist vel_cmd;
if (count_ < 4)
{
vel_cmd.linear.x = 0.1;
vel_cmd.angular.z = 0.0;
RCLCPP_INFO(this->get_logger(), "小车前进中...");
}
else
{
vel_cmd.linear.x = 0.0;
vel_cmd.angular.z = 0.0;
RCLCPP_INFO(this->get_logger(), "小车停止。");
}
pub_vel_->publish(vel_cmd);
count_++;
}
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_vel_;
rclcpp::TimerBase::SharedPtr timer_;
int count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<LimoTopicCmd>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
3.3 Timer 回调函数什么时候执行?
核心代码是:
cpp
timer_ = this->create_wall_timer(
500ms,
std::bind(&LimoTopicCmd::timer_callback, this)
);
这段代码的意思是:
- 创建一个定时器
- 每隔 500ms
- 自动调用一次 timer_callback()
执行流程如下:
启动 limo_topic_cmd 节点
↓
创建 /cmd_vel 发布者
↓
创建 500ms 定时器
↓
进入 rclcpp::spin(node)
↓
每隔 500ms 自动触发 timer_callback()
↓
发布一次 Twist 速度消息
所以 Timer 回调函数的特点是:
- 不需要外部发送消息
- 只要时间到了,就自动执行
3.4 Timer 回调为什么没有参数?
Topic 订阅回调一般有参数:
cpp
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
因为Topic 回调需要接收外部传来的消息。
但是 Timer 回调一般没有参数:
void timer_callback()
因为它只是时间到了自动执行,不需要 ROS2 额外传入消息。
所以可以这样区分:
- Topic 回调:收到消息后触发,通常有 msg 参数
- Timer 回调:定时时间到了触发,通常没有参数
四、Service 和 Action 中的回调函数
4.1 Service 服务回调函数的作用
Service 是一种请求 / 响应通信方式。
它的通信流程是:
客户端发送请求
↓
服务端处理请求
↓
服务端返回响应
在服务端中,负责处理请求的函数就是 Service 回调函数。
例如我们定义一个小车控制服务:
客户端发送 x、y、z
服务端收到后发布 /cmd_vel
最后返回 success
4.2 自定义服务接口示例
假设自定义服务文件为:
limo_msgs/srv/LimoSrv.srv
内容如下:
float32 x
float32 y
float32 z
---
bool success
其中:
--- 上面是请求 Request
--- 下面是响应 Response
也就是说:
客户端发送:
x
y
z
服务端返回:
success
4.3 Service 回调函数代码示例
cpp
#include <memory>
#include <functional>
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp"
#include "limo_msgs/srv/limo_srv.hpp"
using std::placeholders::_1;
using std::placeholders::_2;
class LimoSrvServer : public rclcpp::Node
{
public:
LimoSrvServer() : Node("limo_srv_server")
{
cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel",
10
);
srv_ = this->create_service<limo_msgs::srv::LimoSrv>(
"/limo_srv",
std::bind(&LimoSrvServer::srv_callback, this, _1, _2)
);
RCLCPP_INFO(this->get_logger(), "limo_srv_server node has started.");
}
private:
void srv_callback(
const std::shared_ptr<limo_msgs::srv::LimoSrv::Request> request,
std::shared_ptr<limo_msgs::srv::LimoSrv::Response> response)
{
geometry_msgs::msg::Twist vel_cmd;
vel_cmd.linear.x = request->x;
vel_cmd.linear.y = request->y;
vel_cmd.angular.z = request->z;
cmd_pub_->publish(vel_cmd);
response->success = true;
RCLCPP_INFO(this->get_logger(),
"收到服务请求:x = %.2f, y = %.2f, z = %.2f",
request->x,
request->y,
request->z);
}
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr srv_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<LimoSrvServer>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
4.4 Service 回调函数什么时候触发?
这一行代码创建了服务端:
cpp
srv_ = this->create_service<limo_msgs::srv::LimoSrv>(
"/limo_srv",
std::bind(&LimoSrvServer::srv_callback, this, _1, _2)
);
意思是:
- 创建一个 /limo_srv 服务
- 当客户端调用 /limo_srv 时
- 自动执行 srv_callback()
执行流程如下:
服务端启动
↓
创建 /limo_srv 服务
↓
进入 rclcpp::spin(node)
↓
客户端发送请求
↓
ROS2 自动调用 srv_callback()
↓
服务端读取 request
↓
服务端填写 response
↓
结果返回给客户端
4.5 Action 中的回调函数简单理解
Action 适合处理长时间任务。
例如:
导航到目标点
巡检一段路线
让小车持续运动一段时间
机械臂执行抓取任务
Action 和 Service 的区别是:
- Service:一次请求,一次响应
- Action:发送目标,持续反馈,最终返回结果
Action 服务端常见回调函数有:
- handle_goal:收到目标时触发
- handle_cancel:收到取消请求时触发
- handle_accepted:目标被接受后触发
- execute:真正执行任务的函数
Action 客户端常见回调函数有:
- goal_response_callback:服务端是否接受目标
- feedback_callback:接收执行过程反馈
- result_callback:接收最终执行结果
所以Action 中的回调更多,是因为 Action 需要处理完整的任务生命周期。
可以简单理解为:
- Topic 回调:收到消息就触发
- Timer 回调:时间到了就触发
- Service 回调:客户端请求来了就触发
- Action 回调:目标、反馈、结果等不同阶段都会触发
五、std::bind、this、_1、_2 和常见问题总结
5.1 std::bind 是什么?
在 ROS2 C++ 中,经常会看到这种写法:
cpp
std::bind(&LimoTopicSub::cmd_callback, this, _1)
它的作用是:
把类里面的成员函数绑定成 ROS2 可以调用的回调函数。
- 因为
cmd_callback()是类LimoTopicSub里面的成员函数。- 成员函数不能像普通函数一样直接传给 ROS2。
所以需要告诉 ROS2:
这个回调函数属于哪个类?
用哪个对象来调用?
将来要接收几个参数?
std::bind 就是用来完成这件事的。
5.2 &LimoTopicSub::cmd_callback 是什么意思?
这一部分:
&LimoTopicSub::cmd_callback
表示:
取出 LimoTopicSub 类中的 cmd_callback 成员函数地址
可以理解为:
我要把 LimoTopicSub 这个类里面的 cmd_callback 函数交给 ROS2
5.3 this 是什么意思?
这一部分:
this
表示当前对象。
因为 cmd_callback() 是类里面的成员函数,它必须依赖某一个具体对象才能调用。
例如:
std::bind(&LimoTopicSub::cmd_callback, this, _1)
可以理解为:
使用当前这个 LimoTopicSub 节点对象
去调用它自己的 cmd_callback 函数
也就是说,this 指向当前节点对象。
5.4 _1 和 _2 是什么意思?
_1 完整写法是:
std::placeholders::_1
它是一个占位符。
意思是:
- 这里先占一个位置
- 等 ROS2 真正调用回调函数时
- 把第 1 个参数传进来
例如 Topic 回调函数:
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
这个函数需要一个参数 msg。
所以绑定时写:
std::bind(&LimoTopicSub::cmd_callback, this, _1)
意思是:
- ROS2 收到消息后
- 把收到的消息作为第 1 个参数
- 传给 cmd_callback()
Service 回调函数一般有两个参数:
request
response
所以绑定时写:
std::bind(&LimoSrvServer::srv_callback, this, _1, _2)
其中:
_1 对应 request
_2 对应 response
也就是:
客户端请求来了
↓
ROS2 把请求数据放到 _1
↓
ROS2 把响应对象放到 _2
↓
调用 srv_callback(request, response)
5.5 Timer 为什么不需要 _1?
Timer 回调函数一般没有参数:
void timer_callback()
所以绑定时不需要 _1:
std::bind(&LimoTopicCmd::timer_callback, this)
因为 Timer 回调不是接收消息,也不是处理请求。
它只是:
时间到了
↓
自动执行 timer_callback()
所以不需要 ROS2 传入额外参数。
5.6 也可以使用 Lambda 表达式写回调
除了 std::bind,ROS2 C++ 中也可以使用 Lambda 表达式。
Topic 回调可以写成:
cpp
```cpp
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel",
10,
[this](const geometry_msgs::msg::Twist::SharedPtr msg)
{
this->cmd_callback(msg);
}
);
```
对比:
cpp
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel",
10,
std::bind(&LimoTopicSub::cmd_callback, this, _1)
Timer 回调可以写成:
cpp
```cpp
timer_ = this->create_wall_timer(
500ms,
[this]()
{
this->timer_callback();
}
);
```
对比:
cpp
timer_ = this->create_wall_timer(
500ms,
std::bind(&LimoTopicCmd::timer_callback, this)
);
这两种写法本质上都可以。
对初学者来说:
std::bind 写法:ROS2 官方示例中比较常见
Lambda 写法:看起来更直观,参数更清楚
实际项目中,两种写法都能用。
5.7 常见错误总结
(1)忘记写 rclcpp::spin(node)
如果没有写:
rclcpp::spin(node);
节点可能创建完就结束,回调函数不会被触发。
(2)Topic 回调参数类型写错
订阅 /cmd_vel 时,消息类型是:
geometry_msgs::msg::Twist
那么回调函数参数也要对应:
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
如果类型不一致,就会编译报错。
(3)忘记包含 <functional>
如果使用了:
std::bind
std::placeholders::_1
就需要包含头文件:
#include <functional>
否则可能会出现 std::bind 未定义的问题。
(4)指针访问成员时写成了 .
错误写法:
msg.linear.x
正确写法:
msg->linear.x
因为 msg 是指针,所以要用 ->。
(5)Service 回调少写 _2
Service 回调一般有两个参数:
request
response
所以绑定时一般要写:
std::bind(&LimoSrvServer::srv_callback, this, _1, _2)
如果只写 _1,参数数量对不上,也会出错。
六、总结
本篇文章主要讲解了ROS2 C++ 中非常重要的回调函数。
回调函数可以简单理解为:
提前写好的函数,等某个事件发生时,由 ROS2 自动调用。
在 ROS2 中,常见的回调场景包括:
- Topic 回调:收到话题消息时触发
- Timer 回调:定时时间到了触发
- Service 回调:客户端发送请求时触发
- Action 回调:目标、反馈、结果等阶段触发
其中:
std::bind
用于把类成员函数绑定成 ROS2 可以调用的回调函数。
this
表示当前节点对象。
_1
表示第一个参数。
_2
表示第二个参数。
再简单总结一句:
- Topic 收消息靠回调
- Timer 定时执行靠回调
- Service 处理请求靠回调
- Action 管理任务过程也靠回调
理解了回调函数,后面学习 ROS2 的 Publisher、Subscriber、Service、Action,就会轻松很多。