【ROS2】驱动开发-ros2_control模式驱动
我们已经学习了ROS2 中一种最常见的驱动,肯对会有很多人会问, 我们之前Gazebo 仿真章节不是描述了ros2_control么? 硬件的详情都是manager 通过硬件接口抽象层将硬件的数据读上来,发布对应的topic 给应用层。为什么我们在前面的驱动中,没有看到这些驱动和manager 沟通的接口?
原来,在 ROS 2 驱动开发 中,是否需要 “对应的 ros2_control Manager”,要看你的设计目标和复杂度。 对于简单的驱动,通常使用我们前面ROS Wrapper 那种实现形式,这种形式直接使用 topic/service为应用提供服务。
- 1. 对于简单驱动(基础版,直接使用 topic/service)
像我们之前讲的例子(MotorDriverROSWrapper)属于 独立驱动节点(standalone driver node),它直接:- 订阅命令话题 (speed_command)
- 发布状态话题 (motor_status, current_speed)
- 提供控制服务 (stop_motor)
整个节点自己管理硬件对象(MotorDriver),这种结构不需要 ROS 2 Manager,因为这个驱动是单设备、单功能节点。这也是 ROS 官方教程推荐的入门方式。
- 2. 复杂设备系统(使用 Manager)
当你有一个模块,上面有多个硬件(例如电机 + 编码器 + 红外 + 超声波 + 舵机)时,通常需要一个“管理层”, 一般地, 我们都会使用 ros2_control(也可以自己写,绕过ros2_control)框架,进行多种驱动的管理。
ros2_control 是ROS 官方的 硬件抽象管理框架, 提供标准的 Controller Manager(管理控制器)& Hardware Interface(统一硬件接口),适合需要:
- 同步多个硬件更新
- 多个驱动协调、状态监控
- 实时控制 loop(如机械臂、机器人底盘)
在 常见项目中(特别是中小型机器人/嵌入式设备),直接使用 topic + service 的独立驱动节点(无 Manager)更常见。但在 中大型项目或机器人系统集成(例如机械臂、AGV、四轮差速底盘)中,Manager 模式更普遍。
下表是两种方式的对比分析:
| 对比维度 | 直接使用 topic/service | 使用 Manager(如 controller_manager 或自定义) |
|---|---|---|
| 适用规模 | 单个设备、小型项目 | 多设备系统或需要协调的复杂系统 |
| 开发速度 | 🚀 快,代码简单,容易调试 | 🧱 稍复杂,需要统一接口规范 |
| 结构复杂度 | 低 | 中等或高 |
| 可扩展性 | 差,新增设备要改代码 | 好,可以动态加载、统一管理 |
| 实时控制能力 | 一般,依赖 timer | 强,manager 统一调度周期 |
| 维护性 | 中等,节点多可能混乱 | 高,统一配置、统一生命周期管理 |
| 典型代表项目 | ROS 教程、传感器驱动、简单机器人 | MoveIt、Nav2、ros2_control、工业机器人系统 |
我们再来举例对比下:
- 一,直接 topic 模式(最常见)
例如一个小车项目:
/cmd_vel → 速度控制话题
/motor_left/status → 左电机状态
/motor_right/status → 右电机状态
/ultrasonic/data → 超声波距离
每个节点独立运行。在 launch 文件中手动加载所有驱动。
| 分类 | 内容 |
|---|---|
| 优点 | - 学习成本低 - 调试直观 - 可快速验证驱动功能 |
| 缺点 | - 各节点更新频率不同步 - 无法统一启动/停止 - 设备状态监控需要自行实现额外逻辑 |
- 二,Manager 模式(复杂系统)
典型路径规划的例子的manager如:- ROS 2 Control (controller_manager)
- MoveIt2 (planning_manager)
- Nav2 (bt_navigator_manager)
结构示意:
controller_manager
├── diff_drive_controller
├── joint_state_controller
└── motor_hardware_interface
由 Manager 统一调度 update() 和 read()/write(),确保实时控制和状态同步。
| 分类 | 内容 |
|---|---|
| 优点 | - 统一控制频率 - 支持动态加载控制器 - 可在运行时启停模块 - 更容易扩展成多传感器融合系统 |
| 缺点 | - 开发复杂 - 对实时性和接口要求高 - 配置文件较多(YAML + URDF + pluginlib) |
我们再给出一些常见模块驱动的实现方式:
| 模块类型 | 常用方式 | 说明 | 示例 |
|---|---|---|---|
| 底盘驱动 / 电机控制 | Manager | 需要闭环控制、控制器调度、动态加载 | controller_manager + diff_drive_controller / joint_state_controller |
| 机械臂 / 机器人关节 | Manager | 多关节协调控制、规划器接口、状态反馈 | MoveIt2 planning_manager / ros2_control |
| 导航系统 | Manager | 多个行为节点组合、统一任务调度 | Nav2 bt_navigator_manager |
| 雷达 / 激光 / ToF | Topic | 传感器数据采集,发布原始点云或扫描 | /scan (LaserScan)、/points (PointCloud2) |
| 摄像头 / RGB-D / 深度相机 | Topic | 图像/深度流发布,通常不需要闭环控制 | /camera/color/image_raw、/camera/depth/image_raw |
| IMU / 里程计 / GPS | Topic | 发布传感器数据 | /imu/data、/odom、/gps/fix |
| 超声 / 红外 / 简单传感器 | Topic | 小型传感器,频率低,直接发布 | /ultrasonic、/ir_sensor |
| 自定义多传感器融合系统 | Manager | 多传感器联合管理、统一时间戳、统一状态监控 | 自写 sensor_manager 节点 |
ros2_control的学习连接如https://control.ros.org/humble/index.html。
1. 再探 ros2_control 框架
前面我们已经深入探究了直接 topic 模式 的驱动,接下来让我们再浅浅的研究Manager 模式的驱动。要研究该类驱动,ros2_control 就是其核心。我们先贴出一个官方的ros2_control框架图:
1.1 控制器管理器(Controller Manager)
控制器管理器(CM)连接了 ros2_control 框架中的 控制器层 和 硬件抽象层。它同时也通过 ROS 服务为用户提供入口。CM 实现了一个 没有执行器(executor)的节点,你可以在自己写的 ROS2 节点或 Manager 类 中创建 CM 实例,然后自己控制它的循环调用和调度,(而不是依赖 ROS2 默认的 ros2_control_node 提供的循环)。然而,通常建议使用 controller_manager 包中 ros2_control_node 文件实现的默认节点设置。接下来我们总是, 假设你使用的是这个默认节点设置。
一方面,CM 负责管理控制器及其所需的接口,例如 加载、激活、停用和卸载 控制器。另一方面,它通过 资源管理器(Resource Manager) 访问硬件组件及其接口。CM会检查每个Manager需要用哪些硬件接口,并确认这些接口可以使用;当控制器启用时,它就能访问硬件,如果有冲突(比如多个控制器想用同一个接口),就会报错提醒你。
CM 的 update() 方法就是控制器的大脑,每次调用它就完成一次完整控制循环。 该控制循环包括:
- 从硬件组件读取数据
- 更新所有活动控制器的输出
- 将结果写回硬件组件
1.2 资源管理器(Resource Manager,RM)
资源管理器(RM)就是把物理硬件和它的驱动“包装”起来,让Manager可以像操作普通接口一样使用硬件,而不用管硬件的具体细节。我们把** 驱动叫做硬件组件**。RM的作用有如下:
- RM 使用 pluginlib 加载硬件组件;
- 管理硬件组件的生命周期,以及它们的 **状态接口(state interfaces)**和 命令接口(command interfaces);
- 当更换同一类(如底盘)硬件时,不需要重写代码,只需要配置文件或启动参数即可。
- 同时也可以灵活组合不同硬件接口,例如使用不同的电机控制库和编码器读取库
在 控制循环 中,RM 的 read() 和 write() 方法负责与硬件组件进行通信:
- read() 从硬件读取状态数据(如编码器值、传感器数据
- write() 将控制器计算出的命令发送到硬件(如下发电机速度或位置指令)
1.3 控制器(Controllers)
在 ros2_control 框架里,控制器就是负责“控制逻辑”的模块。它会把系统的目标值(参考值)和实际测量值进行比较,然后根据两者的差异(误差)计算出应该发送给硬件的指令,从而让系统达到目标状态。
这些控制器都是从 ControllerInterface 继承的对象,并且可以通过 pluginlib 导出为插件,这样可以灵活地加载或替换不同控制器。比如 ros2_controllers 仓库中的 ForwardCommandController 就是一个示例控制器。
控制器的生命周期基于 LifecycleNode 类,该类实现了 Node 生命周期设计文档中描述的状态机。
在控制循环执行时,会调用 update() 方法。该方法可以访问最新的硬件状态,并允许控制器向硬件命令接口写入数据。
1.4 用户接口(User Interfaces)
用户通过 控制器管理器(Controller Manager) 提供的服务与 ros2_control 框架进行交互。想查看服务列表及其定义,可以参考 controller_manager_msgs 包中的 srv 文件夹。
虽然服务调用可以直接在命令行或通过节点进行,但框架还提供了一个 用户友好的命令行界面(CLI),与 ros2 命令行工具集成。该 CLI 支持自动补全,并提供了一系列常用命令。
CLI 的基础命令是:
ros2 control
有关 CLI 功能的详细说明,可以参考 命令行接口(CLI)文档。
1.4 硬件组件(Hardware Components)
硬件组件负责与实际物理硬件进行通信,并在 ros2_control 框架中对硬件进行抽象表示。
这些组件必须通过 pluginlib 库导出为插件。资源管理器(Resource Manager)会动态加载这些插件,并管理它们的生命周期。
硬件组件主要有三种基本类型:
- 系统(System)
- 适用于复杂的多自由度(multi-DOF,即多个独立运动方向)机器人硬件,例如工业机器人。
- 与执行器(Actuator)组件的主要区别在于,系统组件可以处理复杂的传动机构,比如人形机器人手部所需的关节协调控制。
- 系统组件具有读写能力(可以读取状态,也可以发送命令),通常用于 只有一个逻辑通信通道与硬件交互 的场景,例如 KUKA-RSI 控制接口。
- 传感器(Sensor)
- 用于感知环境的机器人硬件。
- 传感器组件通常关联某个关节(例如编码器)或连杆(例如力/力矩传感器)。
- 这种类型的组件 仅具备读取能力,无法向硬件发送命令
- 执行器(Actuator)
- 适用于简单的一自由度(1 DOF,即一个独立运动方向)机器人硬件,例如电机、阀门等类似设备。
- 每个执行器组件通常只关联 一个关节。
- 这种组件类型具有 读写能力(可以读取状态,也可以发送命令),但如果硬件不支持读取,读取并非强制要求(例如使用 Arduino 控制的直流电机)。
- 如果硬件支持模块化设计,例如每个电机独立通过 CAN 总线通信,执行器类型也可以用于多自由度(multi-DOF)机器人。
关于硬件组件的详细说明,请参见 《通过控制器访问硬件设计文档》(Hardware Access through Controllers Design Document)。
2. 实现一个简单的GPIO controler
2.1 硬件接口类&实现
// include/simple_gpio_interface/simple_gpio_interface.hpp
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <unordered_map>
#include "hardware_interface/system_interface.hpp"
#include "hardware_interface/handle.hpp"
#include "hardware_interface/hardware_info.hpp"
#include "hardware_interface/types/hardware_interface_return_values.hpp"
#include "rclcpp/macros.hpp"
#include "rclcpp/logger.hpp"
#include "rclcpp/logging.hpp"
// 模拟 BSP GPIO 函数(实际使用时替换为你的 BSP)
extern "C" {
void bsp_gpio_init_output(const char* port, int pin);
void bsp_gpio_init_input(const char* port, int pin);
void bsp_gpio_write(const char* port, int pin, bool value);
bool bsp_gpio_read(const char* port, int pin);
void bsp_gpio_deinit(const char* port, int pin);
}
namespace simple_gpio_interface
{
struct GPIOConfig {
std::string port;
int pin;
std::string direction; // "in" or "out"
};
class SimpleGPIOInterface : public hardware_interface::SystemInterface
{
public:
RCLCPP_SHARED_PTR_DEFINITIONS(SimpleGPIOInterface)
// ros2_control 必需接口
hardware_interface::CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override;
std::vector<hardware_interface::StateInterface> export_state_interfaces() override;
std::vector<hardware_interface::CommandInterface> export_command_interfaces() override;
hardware_interface::CallbackReturn on_activate(const rclcpp_lifecycle::State & previous_state) override;
hardware_interface::CallbackReturn on_deactivate(const rclcpp_lifecycle::State & previous_state) override;
hardware_interface::return_type read(const rclcpp::Time & time, const rclcpp::Duration & period) override;
hardware_interface::return_type write(const rclcpp::Time & time, const rclcpp::Duration & period) override;
private:
// GPIO 配置
std::unordered_map<std::string, GPIOConfig> gpio_configs_;
// 状态和命令变量
std::unordered_map<std::string, double> gpio_states_; // 输入状态
std::unordered_map<std::string, double> gpio_commands_; // 输出命令
// 日志
rclcpp::Logger logger_;
};
} // namespace simple_gpio_interface
2.2 GPIO Controller
// include/simple_gpio_controller/simple_gpio_controller.hpp
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "controller_interface/controller_interface.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp"
#include "rclcpp_lifecycle/state.hpp"
namespace simple_gpio_controller
{
class SimpleGPIOController : public controller_interface::ControllerInterface
{
public:
SimpleGPIOController() = default;
controller_interface::InterfaceConfiguration command_interface_configuration() const override;
controller_interface::InterfaceConfiguration state_interface_configuration() const override;
controller_interface::return_type update(const rclcpp::Time & time, const rclcpp::Duration & period) override;
controller_interface::CallbackReturn on_init() override;
controller_interface::CallbackReturn on_configure(const rclcpp_lifecycle::State & previous_state) override;
controller_interface::CallbackReturn on_activate(const rclcpp_lifecycle::State & previous_state) override;
controller_interface::CallbackReturn on_deactivate(const rclcpp_lifecycle::State & previous_state) override;
private:
rclcpp::Logger logger_;
std::vector<std::string> gpio_names_;
};
} // namespace simple_gpio_controller
其完整的系统结构如下:
┌─────────────────────────────────────────┐
│ ROS2 Control 框架 │
├─────────────────────────────────────────┤
│ SimpleGPIOController (控制器) │ ← 控制逻辑
│ - 读取状态接口 │
│ - 设置命令接口 │
│ - 实现业务逻辑 │
├─────────────────────────────────────────┤
│ SimpleGPIOInterface (硬件接口) │ ← 硬件抽象
│ - 初始化/清理硬件 │
│ - 导出状态/命令接口 │
│ - 调用BSP驱动 │
├─────────────────────────────────────────┤
│ BSP GPIO 驱动 │ ← 底层硬件
│ - bsp_gpio_init_output() │
│ - bsp_gpio_write() │
│ - bsp_gpio_read() │
└─────────────────────────────────────────┘
更多推荐

所有评论(0)