ROS 1 到 ROS 2 底盘驱动移植实战:基于 ros2_control 的 CAN 硬件插件全指南
在 ROS 2 时代,底盘控制架构发生了翻天覆地的变化。原先在 ROS 1 中广泛使用的 ros_control_boilerplate 已经被彻底废弃,取而代之的是基于生命周期(Lifecycle)和插件机制(Pluginlib)的全新 ros2_control 框架。
本文将以一个基于 CANopen 协议(使用 controlcan 封装)的真实双轮差速底盘(CgbotII)为例,详细记录如何将 ROS 1 的老驱动一步步移植到 ROS 2 Humble 中,并完美解决期间遇到的环境冲突、CMake 链接等经典“天坑”。
核心架构差异分析
在动手之前,我们需要明确 ROS 1 与 ROS 2 硬件接口设计的核心差异:
- 去节点化 (No Node):硬件接口不再是一个独立运行的 Node,而是必须被编译为动态链接库插件 (.so),由
controller_manager统一加载。 - 生命周期管理:摒弃了在构造函数中初始化硬件的做法,改为使用严格的
on_init,on_configure,on_activate等回调函数。 - 参数配置迁移:不再通过
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)
声明生命周期回调以及标准的 read 和 write 函数:
#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_controller 和 joint_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)和真实硬件的切换变得前所未有的丝滑。希望这份实战记录能帮正在移植驱动的你少走弯路!
更多推荐
所有评论(0)