我们已经学习了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()                      │
└─────────────────────────────────────────┘
Logo

更多推荐