ROS2系列教程:ROS2-Control与控制器(原理)
本文摘要
官方控制器不够用时,可以写自定义控制器。
本文是 ROS2 系列教程的第 28 篇
本文是 ROS2 系列教程的第 28 篇:ROS2-Control 与控制器(原理)。第 27 篇我们通过 MicroROS 让单片机变成了 ROS2 节点,实现了“能通信”。但机器人关节怎么被统一管理?电机怎么按标准接口转动?这就是 ros2_control 框架的职责。本篇讲解 ros2_control 的架构(Controller Manager / Hardware Interface / Controller)、核心概念(Command Interface / State Interface / Transmission),并用仿真环境跑通一个完整的关节控制流程。这是硬件控制阶段最核心的一篇。
一、为什么需要 ros2_control
1.1 没有标准的混乱局面
在 ros2_control 出现之前,每个机器人都有自己的控制代码:有人直接订阅 /cmd_vel 驱动电机,有人写自己的 PID 控制器,有人为每种电机写一套驱动。结果就是:换硬件要重写控制逻辑,换控制器要重写驱动,代码无法复用。
1.2 ros2_control 的定位
ros2_control 是 ROS2 官方的机器人硬件控制框架,它解决的问题:
- 硬件抽象:把“电机驱动”和“控制算法”解耦。上层控制器只面对统一的接口(关节位置/速度/力矩),不关心底层是直流电机还是舵机。
- 控制器标准化:官方提供开箱即用的控制器(关节轨迹控制器、差速驱动控制器等),也可自定义。
- 状态反馈统一:所有传感器状态(编码器、IMU)通过标准接口上报。
一句话:ros2_control 是“机器人关节的中间件”,上接控制器,下接硬件,中间全是标准接口。
1.3 与 MicroROS 的分工
第 27 篇的 MicroROS 负责“MCU 如何成为 ROS2 节点”,而 ros2_control 负责“关节/电机如何被统一管理”。两者可以结合:ros2_control 的硬件接口(hardware_interface)可以基于串口/MicroROS 与下位机通信。典型链路:
/controller_manager(总管家) ├── 控制器:diff_drive_controller(订阅 /cmd_vel → 计算轮速) ├── 控制器:joint_trajectory_controller(执行轨迹) └── 硬件接口:RobotHW(把目标轮速写到电机,读回编码器) ↓ 串口 / MicroROS / CAN 真实电机 + 编码器二、ros2_control 核心架构(原理)
2.1 三大组件
ros2_control 由三个核心组件组成:
| 组件 | 角色 | 类比 |
|---|---|---|
| Controller Manager | 总管所有控制器的生命周期:加载、配置、激活、切换 | 操作系统 |
| Controller(控制器) | 订阅目标(如 /cmd_vel),计算关节命令,发布状态 | 应用程序 |
| Hardware Interface(硬件接口) | 真正读写电机/传感器,把“关节命令”变成“电流/电压” | 设备驱动 |
2.2 硬件接口的两类接口
硬件接口与控制器之间通过资源接口交互,分两类:
- Command Interface(命令接口):控制器→硬件,比如“关节速度命令”(
wheel_left/velocity)。 - State Interface(状态接口):硬件→控制器,比如“关节位置反馈”(
wheel_left/position)。
每个接口用 资源名/接口名 标识,例如 joint_1/position、wheel_left/velocity。控制器通过接口名读写硬件,硬件通过接口名暴露能力。
2.3 控制循环(Control Loop)
ros2_control 的核心是一个周期控制循环:
读取状态(State Interface:读编码器) ↓控制器计算(diff_drive:把 /cmd_vel 换算成轮速) ↓写入命令(Command Interface:写目标轮速) ↓等待下一个周期(默认 100Hz / 10ms)这个循环由 Controller Manager 的 update() 驱动,周期在 YAML 中配置。
三、环境准备
3.1 安装 ros2_control(Humble)
# 安装 ros2_control 全家桶(Humble 默认已含部分组件,补装控制器)sudo apt updatesudo apt install -y ros-humble-ros2-control ros-humble-ros2-controllers
# 验证安装ros2 pkg list | grep control# 应看到:# ros2_control# ros2_controllers# controller_manager# ...(若干 hardware_interface 等)3.2 使用仿真硬件(Mock Hardware)
为了让没有真实电机的学习者能跑通流程,ros2_control 提供 mock_components(仿真硬件):它在内存中模拟电机行为,编码器读数会响应命令变化。这样我们可以在纯软件环境里完整测试控制链路。
# 安装仿真硬件组件sudo apt install -y ros-humble-ros2-controllers-test-fixtures# 或包含在 ros-humble-ros2-control 中四、示例 1:最小 ros2_control 系统(仿真单关节)
4.1 URDF 中声明硬件接口
ros2_control 的硬件配置写在 URDF 的 <ros2_control> 标签里。创建一个单关节(joint1)的仿真系统:
<!-- robot.urdf.xacro:单关节机器人 + ros2_control 仿真硬件 --><?xml version="1.0"?><robot name="single_joint_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<link name="base_link"/> <link name="joint1_link"/>
<joint name="joint1" type="revolute"> <parent link="base_link"/> <child link="joint1_link"/> <origin xyz="0 0 0.1" rpy="0 0 0"/> <axis xyz="0 0 1"/> <limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/> </joint>
<!-- ros2_control 硬件声明 --> <ros2_control name="SingleJointHW" type="system"> <hardware> <plugin>mock_components/GenericSystem</plugin> <param name="example_param">1.0</param> </hardware> <joint name="joint1"> <command_interface name="position"/> <command_interface name="velocity"/> <state_interface name="position"/> <state_interface name="velocity"/> </joint> </ros2_control>
<!-- 连接 ros2_control 与 robot_state_publisher --> <gazebo> <plugin name="gazebo_ros2_control" filename="libgazebo_ros2_control.so"> <parameters>$(find single_joint_robot)/config/ros2_control.yaml</parameters> </plugin> </gazebo></robot>要点:
<hardware><plugin>mock_components/GenericSystem</plugin>:声明用仿真硬件,不碰真实电机。<joint>里声明该关节暴露哪些接口:命令接口(position/velocity)+ 状态接口(position/velocity)。- 这就是“硬件抽象”:换成真实电机驱动,只需改 plugin 名和相关参数,上层完全不变。
4.2 Controller Manager 配置
创建 config/ros2_control.yaml,配置 Controller Manager 和控制器:
controller_manager: ros__parameters: update_rate: 100 # 控制循环 100Hz # 声明要加载的控制器 joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster joint_trajectory_controller: type: joint_trajectory_controller/JointTrajectoryController
joint_state_broadcaster: ros__parameters: interface_names: - joint1/position - joint1/velocity
joint_trajectory_controller: ros__parameters: joints: - joint1 command_interfaces: - position state_interfaces: - position - velocity state_publish_rate: 50.0 action_monitor_rate: 20.0这里声明了两个控制器:
- joint_state_broadcaster:负责把关节状态广播成
/joint_states话题(RViz 可视化依赖它)。 - joint_trajectory_controller:接收轨迹目标(FollowJointTrajectory Action),逐步执行关节位置命令。
4.3 启动 launch 文件
# launch/control.launch.py:启动整个 ros2_control 系统from launch import LaunchDescriptionfrom launch_ros.actions import Nodefrom launch.actions import ExecuteProcessfrom launch.substitutions import Command, FindExecutableimport os
def generate_launch_description(): # 解析 URDF(含 ros2_control 声明) robot_description = Command([ FindExecutable(name='xacro'), ' ', os.path.join('src/single_joint_robot/urdf/robot.urdf.xacro') ])
return LaunchDescription([ # 1. 发布机器人模型 Node( package='robot_state_publisher', executable='robot_state_publisher', parameters=[{'robot_description': robot_description}], output='screen', ), # 2. 启动 Controller Manager(读取 YAML 配置) Node( package='controller_manager', executable='ros2_control_node', parameters=['config/ros2_control.yaml'], output='screen', ), # 3. 启动 RViz(可选,可视化) ExecuteProcess( cmd=['rviz2', '-d', 'config/view.rviz'], output='screen', ), ])4.4 运行与验证
# 终端 1:构建并启动cd ~/single_joint_ws && colcon build && source install/setup.bashros2 launch single_joint_robot control.launch.py
# 终端 2:查看控制器状态ros2 control list_controllers# 输出示例:# joint_state_broadcaster[joint_state_broadcaster/JointStateBroadcaster] active# joint_trajectory_controller[joint_trajectory_controller/JointTrajectoryController] active
# 终端 3:查看关节状态话题ros2 topic echo /joint_states# 能看到 joint1 的 position/velocity 在不断刷新(仿真硬件在"运行")五、示例 2:控制关节运动(发送轨迹)
5.1 用 Action 发送轨迹目标
joint_trajectory_controller 通过 FollowJointTrajectory Action 接收目标。用 ros2 action send_goal 让 joint1 转到 1.57 弧度(90 度):
# 发送轨迹目标:joint1 在 2 秒内转到 1.57 radros2 action send_goal /joint_trajectory_controller/follow_joint_trajectory \ control_msgs/action/FollowJointTrajectory \ "{trajectory: {joint_names: ['joint1'], points: [{positions: [1.57], time_from_start: {sec: 2}}]}}" \ --feedback运行后可以观察到:
- joint1 的 position 从 0 平滑运动到 1.57(RViz 里能看到关节转动)。
- 反馈里有
desired和actual,体现“期望轨迹 vs 实际跟踪”。
5.2 用 Python 控制(Action 客户端)
实际项目中用代码发送轨迹更常见:
# send_trajectory.py:发送关节轨迹的 Action 客户端import rclpyfrom rclpy.action import ActionClientfrom rclpy.node import Nodefrom control_msgs.action import FollowJointTrajectoryfrom trajectory_msgs.msg import JointTrajectoryPoint
class TrajectoryClient(Node): def __init__(self): super().__init__('trajectory_client') self.client = ActionClient( self, FollowJointTrajectory, '/joint_trajectory_controller/follow_joint_trajectory') self.client.wait_for_server() self.get_logger().info('trajectory server connected')
def send(self, positions, seconds=2.0): goal = FollowJointTrajectory.Goal() point = JointTrajectoryPoint() point.positions = list(positions) point.time_from_start.sec = int(seconds) goal.trajectory.joint_names = ['joint1'] goal.trajectory.points = [point] future = self.client.send_goal_async(goal) future.add_done_callback(self._goal_done)
def _goal_done(self, future): goal_handle = future.result() if goal_handle.accepted: self.get_logger().info('goal accepted') goal_handle.get_result_async() else: self.get_logger().warn('goal rejected')
def main(): rclpy.init() node = TrajectoryClient() node.send([1.57]) # 转到 90 度 rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()运行:
python3 send_trajectory.py# 日志:trajectory server connected → goal accepted5.3 C++ 版本(Action 客户端)
// send_trajectory.cpp:C++ 发送关节轨迹#include <rclcpp/rclcpp.hpp>#include <rclcpp_action/rclcpp_action.hpp>#include <control_msgs/action/follow_joint_trajectory.hpp>#include <trajectory_msgs/msg/joint_trajectory_point.hpp>
using FollowJointTrajectory = control_msgs::action::FollowJointTrajectory;
class TrajectoryClient : public rclcpp::Node {public: using Client = rclcpp_action::Client<FollowJointTrajectory>;
TrajectoryClient() : Node("trajectory_client") { client_ = rclcpp_action::create_client<FollowJointTrajectory>( this, "/joint_trajectory_controller/follow_joint_trajectory"); while (!client_->wait_for_action_server(std::chrono::seconds(1))) { RCLCPP_INFO(get_logger(), "waiting for action server..."); } RCLCPP_INFO(get_logger(), "action server connected"); send(1.57); }
void send(double position, double seconds = 2.0) { auto goal = FollowJointTrajectory::Goal(); trajectory_msgs::msg::JointTrajectoryPoint point; point.positions.push_back(position); point.time_from_start = rclcpp::Duration::from_seconds(seconds); goal.trajectory.joint_names = {"joint1"}; goal.trajectory.points.push_back(point);
auto send_opt = rclcpp_action::Client<FollowJointTrajectory>::SendGoalOptions(); send_opt.goal_response_callback = [this](auto future) { if (future.get()) RCLCPP_INFO(get_logger(), "goal accepted"); else RCLCPP_WARN(get_logger(), "goal rejected"); }; client_->async_send_goal(goal, send_opt); }
private: std::shared_ptr<Client> client_;};
int main(int argc, char **argv) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<TrajectoryClient>()); rclcpp::shutdown(); return 0;}CMakeLists.txt 关键配置:
find_package(control_msgs REQUIRED)find_package(trajectory_msgs REQUIRED)find_package(rclcpp_action REQUIRED)add_executable(send_trajectory src/send_trajectory.cpp)ament_target_dependencies(send_trajectory rclcpp rclcpp_action control_msgs trajectory_msgs)六、示例 3:差速驱动控制器(机器人底盘)
单关节太简单,机器人真正常用的是 diff_drive_controller:订阅 /cmd_vel(速度指令),自动换算成左右轮速度。这是两轮差速底盘的标配。
6.1 URDF 声明差速底盘硬件
<ros2_control name="DiffDriveHW" type="system"> <hardware> <plugin>mock_components/GenericSystem</plugin> </hardware> <joint name="wheel_left_joint"> <command_interface name="velocity"/> <state_interface name="position"/> <state_interface name="velocity"/> </joint> <joint name="wheel_right_joint"> <command_interface name="velocity"/> <state_interface name="position"/> <state_interface name="velocity"/> </joint></ros2_control>6.2 配置 diff_drive_controller
controller_manager: ros__parameters: update_rate: 100 diff_drive_controller: type: diff_drive_controller/DiffDriveController joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller: ros__parameters: left_wheel_names: ['wheel_left_joint'] right_wheel_names: ['wheel_right_joint'] wheel_separation: 0.4 # 轮距 0.4m wheels_per_side: 1 wheel_radius: 0.1 # 轮半径 0.1m command_interfaces: ['velocity'] state_interfaces: ['position', 'velocity'] publish_rate: 50.0 odom_frame_id: odom base_frame_id: base_link pose_covariance_diagonal: [0.001, 0.001, 0.0, 0.0, 0.0, 0.01] twist_covariance_diagonal: [0.001, 0.0, 0.0, 0.0, 0.0, 0.01]关键参数:wheel_separation(轮距)、wheel_radius(轮半径)——这两项决定了 /cmd_vel 到轮速的换算。差速模型:
左轮速度 = (v - ω × wheel_separation / 2) / wheel_radius右轮速度 = (v + ω × wheel_separation / 2) / wheel_radius其中 v=线速度,ω=角速度6.3 启动并控制小车
# 启动(launch 里加载 diff_drive_controller)ros2 launch diff_drive_robot control.launch.py
# 查看控制器ros2 control list_controllers# diff_drive_controller[DiffDriveController] active
# 让小车向前走 1 秒ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \ "{linear: {x: 0.5}, angular: {z: 0.0}}" --once
# 查看里程计ros2 topic echo /odom# position.x 不断增大,说明底盘在"动"(仿真里程计)
# 原地旋转ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \ "{linear: {x: 0.0}, angular: {z: 0.5}}" --once# /odom 的 yaw 角不断变化为什么重要:/cmd_vel → /odom 的闭环,正是建图(第 21 篇)和导航(第 24 篇)的基础。Nav2 发的 /cmd_vel,就是被这个 diff_drive_controller 消费的。
七、自定义控制器(原理)
官方控制器不够用时,可以写自定义控制器。核心是继承 controller_interface::ControllerInterface,实现四个方法:
// my_controller.cpp 骨架:自定义控制器#include <controller_interface/controller_interface.hpp>#include <rclcpp_lifecycle/state.hpp>
namespace my_robot {
class MyController : public controller_interface::ControllerInterface {public: // 1. 声明参数(如控制增益) controller_interface::CallbackReturn on_init() override { auto_declare<std::vector<std::string>>("joints", {}); auto_declare<double>("gain", 1.0); return controller_interface::CallbackReturn::SUCCESS; }
// 2. 配置:获取硬件接口引用(命令 + 状态) controller_interface::CallbackReturn on_configure( const rclcpp_lifecycle::State &) override { joint_ = get_node()->get_parameter("joints").as_string_array()[0]; gain_ = get_node()->get_parameter("gain").as_double(); command_itf_ = std::find_if( command_interfaces_.begin(), command_interfaces_.end(), [this](const auto &itf) { return itf.get_interface_name() == "position" && itf.get_prefix_name() == joint_; }); return controller_interface::CallbackReturn::SUCCESS; }
// 3. 激活 controller_interface::CallbackReturn on_activate( const rclcpp_lifecycle::State &) override { return controller_interface::CallbackReturn::SUCCESS; }
// 4. 控制循环:读取参考,写命令(核心逻辑) controller_interface::return_type update( const rclcpp::Time &, const rclcpp::Duration &) override { // 简化:命令 = 固定值 × 增益(实际应订阅目标并计算) command_itf_->get().set_value(gain_ * 0.5); return controller_interface::return_type::OK; }
private: std::string joint_; double gain_; std::vector<controller_interface::InterfaceReference>::iterator command_itf_;};
} // namespace my_robot
#include "pluginlib/class_list_macros.hpp"PLUGINLIB_EXPORT_CLASS(my_robot::MyController, controller_interface::ControllerInterface)配套插件描述文件:
<library path="my_controller"> <class name="my_robot/MyController" type="my_robot::MyController" base_class_type="controller_interface::ControllerInterface"> <description>My custom controller</description> </class></library>然后在 YAML 中引用:
controller_manager: ros__parameters: my_controller: type: my_robot/MyControllermy_controller: ros__parameters: joints: ['joint1'] gain: 2.0八、常见问题与排障
| 现象 | 原因 | 解决 |
|---|---|---|
controller_manager 启动报找不到插件 |
插件描述文件未导出 | pluginlib_export_plugin_description_file(controller_interface my_controller_plugin.xml) 后重新编译 |
| 控制器状态 inactive | 未激活 | ros2 control load_controller + ros2 control switch_controllers --activate |
| 接口名不匹配报错 | URDF 接口与 YAML 接口不一致 | 检查命令/状态接口的名称和类型 |
| 关节不动但无报错 | 命令被限制(limit)或控制器未接管 | 检查 joint limit 配置、ros2 control list_controllers 状态 |
| /odom 无输出 | diff_drive 未激活或参数错误 | 检查 wheel_separation/radius 和命名空间 |
| 控制频率太低 | update_rate 太低 | 提高 update_rate(如 200Hz) |
九、小结与下一篇预告
本篇核心:
- ros2_control 三大组件:Controller Manager(管家)、Controller(算法)、Hardware Interface(驱动),标准接口解耦硬件与控制。
- 两类接口:Command Interface(写命令)+ State Interface(读状态),资源接口名格式
关节/接口。 - 控制循环:读状态 → 计算 → 写命令,100Hz 周期驱动。
- 仿真硬件(mock_components)让无硬件也能跑通:单关节轨迹控制、差速底盘
/cmd_vel → /odom闭环。
至此我们理解了“机器人关节如何被 ROS2 统一管理”。下一篇(第 29 篇)进入 控制硬件实战(电机驱动,原理):从原理到代码,讲透直流电机驱动(H 桥、PWM)、编码器测速、PID 闭环控制,并把它们接到 ros2_control 的硬件接口上,让“纸面上的关节”变成“真正转动的电机”。硬件控制阶段最实战的一篇即将展开。
ROS2系列教程:ROS2-Control与控制器(原理)
作者:Wang Shijie
本文链接:https://wsj-blog.pages.dev/posts/ros2-xi-lie-jiao-cheng-ros2-control-yu-kong-zhi-qi-yuan-li/
本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论
评论资源加载失败,请重试。