在 ROS 2 时代,底盘控制架构发生了翻天覆地的变化。原先在 ROS 1 中广泛使用的 ros_control_boilerplate 已经被彻底废弃,取而代之的是基于生命周期(Lifecycle)和插件机制(Pluginlib)的全新 ros2_control 框架。

本文将以一个基于 CANopen 协议(使用 controlcan 封装)的真实双轮差速底盘(CgbotII)为例,详细记录如何将 ROS 1 的老驱动一步步移植到 ROS 2 Humble 中,并完美解决期间遇到的环境冲突、CMake 链接等经典“天坑”。


核心架构差异分析

在动手之前,我们需要明确 ROS 1 与 ROS 2 硬件接口设计的核心差异:

  1. 去节点化 (No Node):硬件接口不再是一个独立运行的 Node,而是必须被编译为动态链接库插件 (.so),由 controller_manager 统一加载。
  2. 生命周期管理:摒弃了在构造函数中初始化硬件的做法,改为使用严格的 on_init, on_configure, on_activate 等回调函数。
  3. 参数配置迁移:不再通过 NodeHandle 读取 yaml,底层硬件参数必须写入机器人模型的 URDF (Xacro) <ros2_control> 标签 中。

Step 1: 封装底层驱动为纯依赖库

首先,我们将底层的 CAN 收发封装代码(base_driver.cpp, controlcan_wrapper.cpp)独立成一个纯驱动库包(motor_driver)。

关键点:CMakeLists.txt 中,不生成任何可执行文件,而是将其编译为 SHARED 库,并通过 ament_export_* 暴露给上层插件使用。

# motor_driver/CMakeLists.txt 节选
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(Boost REQUIRED COMPONENTS thread system)

# 编译为动态库
add_library(${PROJECT_NAME} SHARED
  src/base_driver.cpp
  src/controlcan_wrapper.cpp
)

target_link_libraries(${PROJECT_NAME} controlcan Boost::thread Boost::system)
ament_target_dependencies(${PROJECT_NAME} rclcpp)

# 导出库,供硬件接口包 find_package() 使用
ament_export_include_directories(include)
ament_export_libraries(${PROJECT_NAME} controlcan)
ament_export_dependencies(rclcpp Boost)

Step 2: 编写 ros2_control 硬件接口插件

创建一个新包 rt_cgbot_hardware_interface。我们需要继承 hardware_interface::SystemInterface 来编写我们的控制插件。

1. 插件头文件定义 (.hpp)

声明生命周期回调以及标准的 readwrite 函数:

#include "hardware_interface/system_interface.hpp"
#include "motor_driver/base_driver.h" // 引入底层驱动库

namespace rt_cgbot_hardware_interface {
class RTCgbotSystemHardware : public hardware_interface::SystemInterface {
public:
  hardware_interface::CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override;
  hardware_interface::CallbackReturn on_configure(const rclcpp_lifecycle::State & previous_state) 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;

  std::vector<hardware_interface::StateInterface> export_state_interfaces() override;
  std::vector<hardware_interface::CommandInterface> export_command_interfaces() 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:
  std::shared_ptr<rt_cgbot::BaseDriver> base_driver_;
  std::vector<double> hw_commands_;
  std::vector<double> hw_states_position_;
  std::vector<double> hw_states_velocity_;
};
}

2. 核心逻辑实现 (.cpp)

  • on_init:从 URDF 中解析硬件参数(info_.hardware_parameters)。
  • on_configure:实例化 BaseDriver
  • on_activate:下发 CANopen 使能指令,启动电机。
  • read / write:与驱动器进行数据交互。

最后,千万别忘了注册插件宏

#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(
  rt_cgbot_hardware_interface::RTCgbotSystemHardware, hardware_interface::SystemInterface)

3. 创建插件描述文件 (XML)

在包根目录下创建 rt_cgbot_hardware_interface_plugin.xml,这是 controller_manager 找到你的类的唯一凭证:

<library path="rt_cgbot_hardware_interface">
  <class name="rt_cgbot_hardware_interface/RTCgbotSystemHardware"
         type="rt_cgbot_hardware_interface::RTCgbotSystemHardware"
         base_class_type="hardware_interface::SystemInterface">
    <description>ROS 2 hardware interface plugin for CgbotII</description>
  </class>
</library>

Step 3: 配置 URDF 与 Controllers

在 ROS 2 中,参数不再写在代码或零散的 YAML 里,而是统一整合。

1. URDF (Xacro) 配置

除了必需的物理连杆(Link)和关节(Joint),关键在于 <ros2_control> 标签:

<ros2_control name="RTCgbotHardware" type="system">
  <hardware>
    <plugin>rt_cgbot_hardware_interface/RTCgbotSystemHardware</plugin>
    <param name="mode">3</param>
    <param name="inverse_id">1</param>
    <param name="vel_ratio">95.492967</param>
    <param name="timing1">0x14</param> </hardware>
  <joint name="left_wheel_joint">
    <command_interface name="velocity"/>
    <state_interface name="position"/>
    <state_interface name="velocity"/>
  </joint>
</ros2_control>

2. 控制器配置 (controllers.yaml)

指定 diff_drive_controllerjoint_state_broadcaster

controller_manager:
  ros__parameters:
    update_rate: 25
    mobile_base_controller:
      type: diff_drive_controller/DiffDriveController
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

mobile_base_controller:
  ros__parameters:
    left_wheel_names: ["left_wheel_joint"]
    right_wheel_names: ["right_wheel_joint"]
    wheel_separation: 0.444
    wheel_radius: 0.0845
    # ... 其他速度限制参数 ...

Step 4: 编写 Launch 一键启动脚本

使用 Python 动态解析 Xacro,并按顺序拉起核心节点:

import os
from launch import LaunchDescription
from launch.actions import RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch.substitutions import Command, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():
    pkg_name = 'rt_cgbot_hardware_interface'
    urdf_file = PathJoinSubstitution([FindPackageShare(pkg_name), 'urdf', 'rt_cgbot.urdf.xacro'])
    controllers_file = PathJoinSubstitution([FindPackageShare(pkg_name), 'config', 'controllers.yaml'])

    robot_description = {'robot_description': Command(['xacro ', urdf_file])}

    control_node = Node(package='controller_manager', executable='ros2_control_node', parameters=[robot_description, controllers_file])
    robot_state_pub = Node(package='robot_state_publisher', executable='robot_state_publisher', parameters=[robot_description])
    
    jsb_spawner = Node(package='controller_manager', executable='spawner', arguments=['joint_state_broadcaster'])
    mbc_spawner = Node(package='controller_manager', executable='spawner', arguments=['mobile_base_controller'])

    # 设置启动顺序
    delay_mbc = RegisterEventHandler(
        event_handler=OnProcessExit(target_action=jsb_spawner, on_exit=[mbc_spawner])
    )

    return LaunchDescription([control_node, robot_state_pub, jsb_spawner, delay_mbc])

避坑指南:移植过程中遇到的经典 Error

在编译和测试过程中,我们遇到了几个极其经典的 ROS 2 报错,解决方案如下:

坑 1:Anaconda 环境变量冲突

报错: Could not load library... libstdc++.so.6: version 'GLIBCXX_3.4.30' not found 或者 libboost_thread.so.1.82.0: cannot open shared object file
原因: Conda 环境强行置顶了自己的旧版/自定义版 C++ 库,导致系统核心的 ros2_control 加载底层 .so 时崩溃。
解法: 编译和运行 ROS 2 驱动时,务必退出 Conda 环境 (conda deactivate),并清空原本的 build/ 缓存重新编译。

坑 2:Robot State Publisher 崩溃

报错: No link elements found in urdf file
原因: URDF 里只写了 <ros2_control> 标签,没有写物理模型。
解法: robot_state_publisher 需要完整的运动学链。即使没有真正的外观文件,也必须在 URDF 中用 <link><joint> 标签把底盘和左右轮的层级拓扑定义出来(如上文 URDF 配置所示)。

坑 3:Launch 文件找不到

报错: does not contain the required function 'generate_launch_description()'
原因: colcon build 后才修改了 python 脚本,导致 install 目录下仍是旧文件或空文件。
神仙技巧: 使用软链接编译:colcon build --symlink-install。这样修改 Launch 文件或 yaml 配置后,按 Ctrl+S 保存即可直接生效,无需反复编译!


结语

从 ROS 1 迁移到 ROS 2,硬件接口层的改动是最痛的,但也是最值得的。基于 ros2_control 的插件化设计,让底盘控制、仿真(Gazebo)和真实硬件的切换变得前所未有的丝滑。希望这份实战记录能帮正在移植驱动的你少走弯路!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐