Nav2(全称Navigation2)是ROS 2(Robot Operating System 2)的官方导航框架,可以理解为机器人导航领域的“操作系统”

    • 定位 (Localization):在地图上确定机器人自身的位置。
    • 路径规划 (Path Planning):规划一条从起点到终点的最优路径。
    • 控制 (Control):生成控制指令(如电机速度),驱动机器人沿着规划路径行驶,并动态躲避障碍物。
    • 建图 (Mapping):加载、提供和存储环境地图。

Nav2的模块化架构

Nav2的架构非常灵活,采用“乐高积木”式的模块化设计。其核心思想是通过行为树 (Behavior Tree, BT) 来组织和编排各个独立的功能模块(称为“服务器”)。

主要的功能模块(服务器)包括:

模块 (Server)

功能描述

Map Server

负责加载、提供和存储环境地图。

AMCL

负责在地图上定位机器人。

Planner Server

负责全局路径规划(如使用A*算法)。

Controller Server

负责局部路径规划和控制,让机器人跟随路径并避障。

Behavior Tree (BT) Navigator

作为“总指挥”,通过行为树来编排和调用其他服务器,完成复杂的导航任务。

Recoveries

在导航失败(如被困)时,执行恢复行为(如后退、旋转)。

Waypoint Follower

使机器人能够按顺序访问一系列目标点。

为什么需要 ros2_control(先建立问题意识)

在你没有 ros2_control 时,"让机械臂动一下"的朴素做法是:写一个节点打开串口,把舵机协议拼成字节帧发出去。这条路在一个关节、一个 demo 时没问题,一旦规模化就会出现下面这些问题:

  1. 仿真和真机要写两套代码:Gazebo 里是 topic 订阅,真机是串口协议,上层算法没法在两者间无缝迁移。
  2. 没有统一的"接口契约":每个模块自己发明话题名和消息格式,换硬件(换个舵机型号、换控制器品牌)就要改上层代码。
  3. 没有统一的状态管理:控制器要"启动/暂停/重启",硬件要"初始化/使能/报错恢复",全靠自己写状态标志位,极易出错。
  4. 没有实时性/确定性保证:控制循环和一堆普通 ROS 节点抢 CPU,loop 抖动大,关节轨迹会毛糙。
  5. 上层框架接不进来:MoveIt、行为树、RL 策略都要一套标准化的"把期望关节指令写进去、把当前关节状态读出来"的通道。

ros2_control 就是 ROS2 官方给的答案,它提供三样东西:

它解决什么

对应机制

统一的硬件抽象层

硬件组件(Hardware Component)把"真机串口 / 仿真 / mock"都包装成同一种东西,对外只暴露接口

统一的控制器运行时

Controller Manager 负责加载、配置、激活/停用、调度所有控制器

配置驱动

机器人长什么样(URDF 里写)、用什么硬件、跑哪些控制器,全部由配置文件决定,不改一行 C++ 就能切换

一句话定位:ros2_control 是"机器人底层控制层"的中间件,它把 控制算法(控制器)具体硬件(硬件组件) 解耦,中间由 Controller Manager 做调度。

一个类比(面试暖场用):把机械臂想象成家里的电路。

  • 硬件组件 = 墙里的线路和插座(每家每户的布线不同,但插座标准统一);
  • 控制器 = 插在插座上的电器(电视机、空调——换电器不用改墙里的线);
  • Controller Manager = 家里的配电箱/管家,负责"哪个电器用电(claim 接口)、什么时候开(激活)、什么时候关(停用)"。

对应到你自己的项目叙事:本科你写 STM32 是"自己拉电线、自己造电器";现在用 ros2_control,是"把自家布线(Feetech 串口)接成标准插座(SystemInterface),让 MoveIt / RL 这些电器插上就能用"。这就是简历里"上层运动规划与底层执行器解耦,支持仿真与真机无缝切换"这句话的底层含义。

核心概念逐层拆

2.1 三个角色:Controller Manager / 硬件组件 / 控制器

① Controller Manager(controller_manager 节点,核心)

  • 一个机器人通常只跑一个(多 controller manager 属于进阶,见 §8)。
  • 职责:
    1. 持有 ResourceManager(资源管理器):ResourceManager 解析 URDF 里所有 <ros2_control> 硬件块,加载硬件插件,维护"有哪些关节、每个关节有哪些 command/state interface"这张总表。
    2. 生命周期管家:用 ROS2 Lifecycle 机制统一管理每个硬件组件和每个控制器的状态迁移(见 §2.4)。
    3. 实时控制循环:以固定频率(update_rate,默认 100 Hz)驱动整条链:read 硬件 → update 所有激活控制器 → write 硬件(见 §2.5)。
    4. 接口仲裁(claim):命令接口(command interface)同一时刻只能被一个控制器占用(防止两个控制器同时抢着指挥同一个关节);状态接口(state interface)可被多个控制器/广播器同时读。
    5. 对外提供 ros2 control 系列命令行和 /controller_manager/... 服务接口,供你动态加载/卸载控制器。

② 硬件组件(Hardware Component)

  • 是"对下通信、对上暴露接口"的插件。类型三种:SystemInterface(整机,多关节/一主多从总线)、ActuatorInterface(单个执行器)、SensorInterface(非关节传感器,如力/力矩传感器、IMU)。
  • 对 SoArm101:一个串口挂 6 个舵机 → 用 SystemInterface,一次性暴露 6 个关节的接口。
  • 对仿真:仿真后端(gz_ros2_control 等)就是一个"把仿真模型当硬件"的硬件组件。
  • 生命周期:硬件组件自己也是被 Controller Manager 管理的 lifecycle 对象(configure 时初始化、activate 时打开通信)。

③ 控制器(Controller)

  • 是"控制算法",也是被 Controller Manager 加载的插件/节点。职责:订阅外部指令(话题 / action / service),结合读取到的状态接口,计算出要写入命令接口的值。
  • 控制器本身不直接碰硬件——它只读写接口(这正是解耦的体现)。
  • 例子:joint_state_broadcaster 只读状态接口并广播成 /joint_statesjoint_trajectory_controller 订阅 MoveIt 的轨迹 action,跟踪轨迹、输出位置命令;forward_command_controller 把话题上的原始指令直通命令接口。
记忆锚点:Controller Manager 管"人"(控制器)和"设备"(硬件),硬件组件管"设备",控制器管"算法";三者在接口处汇合。

2.2 接口契约:Command Interface 与 State Interface

这是 ros2_control 的"货币",几乎所有配置、报错、面试追问都围绕它,必须一次理解透:

  • Command Interface(命令接口,下发给硬件的期望值):名字通常是 position / velocity / effort。表示"我希望这个关节走到哪 / 转多快 / 使多大劲"。对 SoArm101:Feetech 舵机内部自带位置闭环和 PID,所以你只下 position 命令 → 关节上只有 position 这一个命令接口(没有 velocity/effort 命令接口,因为你发不了)。
  • State Interface(状态接口,从硬件读回的测量值):名字同样可能是 position / velocity / effort,指"测量到的实际位置/速度/力矩"。对 SoArm101:舵机能回传位置(多数也能回传速度),即至少一个 position 状态接口。
  • 一个关节 = 一组 状态接口(给上层读)+ 一组 命令接口(给上层写)。一个"接口"的完整名字 = 关节名 + 接口类型 + 单位(单位统一为 rad、rad/s、N·m 等 SI 制——原始舵机单位必须在硬件组件内部换算,不让上层看到)。

三条铁律(面试高频):

  1. 接口必须先"声明"(在 URDF 里列出来 + 硬件组件导出),控制器才能用。报错 interface ... not found 基本都是这里漏了。
  2. 命令接口是独占的:同一时刻只能被一个已激活控制器 claim。第二个控制器想激活并写同一个 position 命令接口 → 激活失败,日志会提示 conflict。这是面试必考:为什么 joint_state_broadcaster 和 joint_trajectory_controller 能同时跑? —— 因为前者只读状态接口,后者才写命令接口,两者不冲突。
  3. 状态接口是共享的:广播器、控制器、诊断节点都可以读同一个状态接口。
对理解你自己的项目:你现在"会配"但"讲不清",十有八九是没把接口这张总表在脑子里建起来。D2 实验任务就是:从 ros2 control list_hardware_interfaces 输出里,把你的臂的接口总表抄一遍。

2.3 谁加载了谁:URDF 的 ros2_control 标签 → ResourceManager

  • 机器人的结构描述在 URDF/xacro(links/joints/几何/惯性),控制配置也在 URDF——通过一个专门的 <ros2_control> 标签块(注意:它和普通 link/joint 平级,是给 Controller Manager 看的,不是给 rviz 看的)。
  • Controller Manager 启动时读取 robot_description 参数(URDF 字符串),ResourceManager 扫描其中所有 <ros2_control> 块:
    1. <hardware><plugin>...</plugin> 找到硬件插件(真机插件 / 仿真插件 / mock 插件);
    2. 读取该硬件下的 <joint> 列表,为每个 joint 登记声明的 command/state interfaces;
    3. 读取 <param> 透传给硬件组件的 on_init()
  • 启动顺序(重要):先有 robot_description(robot_state_publisher 发布),再启动 controller_manager,controller_manager 才有东西可解析。你以前可能遇到"controller_manager 报找不到任何硬件"的错,多半是顺序/参数没对上。

2.4 生命周期状态机

ROS2 的 Lifecycle 把节点状态做成显式状态机,ros2_control 用它统一管理硬件与控制器。五个常用状态:

                    configure                     activate
 [unconfigured] ────────────► [inactive] ◄──────────── [active]
       ▲                          │   ▲                   │
       │        cleanup           │   │    deactivate      │
       └──────────────────────────┘   └────────────────────┘
        (shutdown / error → finalized,错误处理见下)

每个状态迁移会回调对应函数(硬件组件/控制器的 on_configureon_activate……),这正是 §4.2 你代码里那些 override 的来由。

状态

含义(对控制器)

含义(对硬件组件)

unconfigured

已加载但什么都没初始化

插件刚实例化

inactive

已初始化、参数就绪,但不参与控制循环(不写命令)

已分配内存、读了参数,但未打开通信/未使能

active

每个周期都执行 update(),写命令接口

每个周期都执行 read()/write(),通信已建立

为什么搞这么复杂?因为"把节点拉起来"和"让它真正接管硬件"必须分开——这样你可以:先启动全部东西热热身、检查一遍,再让控制器逐个接管;出问题时先 deactivate(让硬件进入安全状态,例如舵机锁住不动而不是乱跑),再排查。

  • 硬件组件的激活时机:当第一个需要它的控制器激活时,ResourceManager 才会激活对应硬件;当所有控制器停用后,硬件会停用(通信关闭)。这也是"仿真/真机切换"时你只重启 controller_manager 就行的原因之一。
  • spawner(加载器)本质就是替你执行 configure → activate 的小工具;也可以手动 ros2 control load_controller --set-state active

2.5 控制循环与实时性

Controller Manager 里跑一个实时线程(RT 线程),按 update_rate(默认 100 Hz,即 10 ms 一个周期)执行:

每个控制周期 t:
  ├─ 对所有激活的硬件组件: hardware.read()   ← 从舵机总线/仿真里读回当前状态 → 更新 state interfaces
  ├─ 对所有激活的控制器:   controller.update() ← 各控制器消费状态接口、算出期望值、写入 command interfaces
  └─ 对所有激活的硬件组件: hardware.write()  ← 把 command interfaces 的期望值一次性下发到硬件

要点(面试可深挖):

  • read/update/write 三者同周期、有严格顺序:先用上一帧的命令、读到最新状态,再算新命令,最后下发。
  • 硬件 read/write 每周期只能各一次,且必须快、不能阻塞(RT 线程里做阻塞 I/O 是灾难)。这对舵机总线有直接推论:不要在一个 write() 里逐关节"发一帧、等应答、再发下一帧"——应该攒成一帧批量下发;read() 同理要批量读或异步处理。你的 6 个舵机是一主多从同一条总线,天然适合"一帧广播/一次轮询"。
  • 控制器 update() 的触发方式分两种:被 CM 同步调用的普通控制器(绝大多数);以及"链式控制器"(chainable,输出又作为另一个控制器的输入,见 §8)。
  • 为什么强调实时:轨迹平滑性依赖周期抖动小。RL 策略 30 Hz 够用,但 JTC 做插值时 100 Hz 写命令会让运动更顺;把 update_rate 从 100 改成 20,你会立刻看到轨迹变"一顿一顿"——D3/D5 实验可做。

2.6 一帧数据流全景

把前面所有概念串成一条链(这也是面试白板题"画数据流"的标准答案):

【指令源】 MoveIt2 规划出的轨迹(action)  /  RL 策略动作(话题)  /  你的话题指令
                    │
                    ▼
        【控制器】 e.g. joint_trajectory_controller / forward_command_controller
                    对每个关节把期望值写入 → position command interface(claim 独占)
                    │
                    ▼
        【Controller Manager】update_rate 周期:write 阶段调用
                    │
                    ▼
        【硬件组件 write()】SystemInterface:rad → raw(舵机单位) 换算 → 拼串口帧 → 发 Feetech 总线
                    │
        舵机实际运动,回传位置
                    ▼
        【硬件组件 read()】 解析应答帧 → raw → rad → 更新 position state interface
                    │
                    ▼
        【广播器】 joint_state_broadcaster 读 state interface → 发 /joint_states
                    │
                    ▼
        【消费方】 rviz2 显示 / MoveIt 状态反馈(闭环) / 你的监控节点

对照你自己的项目回忆一下:P1 里"Rviz 拖拽目标点 → MoveIt 规划 → 下发执行"这条路,中间"执行"二字落点正是 joint_trajectory_controller + 你的 SystemInterface。你之前照教程配通的部分(/joint_trajectory_controller/follow_joint_trajectory/joint_states 话题)在这张图里各占一个格子——现在你能说出它们各自的角色了。


ros2_control 硬件插件代码

伪代码.cpp
// =====================================================================
// St3215SystemHardware —— Feetech ST3215 舵机机械臂的 ros2_control 硬件插件
// =====================================================================
// 角色:本文件实现一个 ros2_control 的 SystemInterface 插件,充当
//   "上层控制器" 与 "底层舵机" 之间的翻译官:
//   把控制器给的抽象指令(弧度) 编成 ST3215 能懂的串口帧发出去;
//   把舵机回传的编码器值(counts) 读回并换算成弧度(rad) 供控制器使用。
//
// 在 ros2_control 的每个控制周期里,本插件被反复调用:
//   controller_manager
//     ├─ controller 算好目标值 → 写入 command interface(内存)
//     ├─ read()  把舵机真实位置读进 state interface
//     └─ write() 把 command interface 里的目标位置写到舵机
//
// 生命周期(由 controller_manager 在启动/停止时依次触发):
//   on_init(解析参数+校验接口)
//     -> on_configure(开串口+锁力矩) -> on_activate(激活进入运行)
//     -> [read/write 循环...]
//     -> on_deactivate/on_cleanup/on_shutdown(关闭)
// =====================================================================

第一步:写舵机寄存器地址、单位换算(counts——rad)
第二步:on_init:读参数(从 info_.hardware_parameters 读 port/baudrate/servo_ids/teach_mode    xacro文件中<params>)、校验每个关节接口定义(command interface---position   state interface----pos/vel)
第三步:分配缓冲,resize所有缓冲,设置零位和方向
第四步:export将内部缓冲给ros_control(export_state_interface  export_command_interfaces)
第五步:生命周期
   1、on_configure: 配置阶段,打开串口、给舵机使能/关闭力矩(unconfigured → inactive)
   2、on_activate: 激活阶段,进入正式受控运行前的最后准备,再次确保力矩状态正确(inactive → active,正式受控)
   3、on_deactivate: 停止受控时触发 —— 一律松开所有舵机力矩,防止机械臂僵在原地(active → inactive)
   4、on_cleanup: 回到 unconfigured 状态时触发 —— 关闭串口,释放资源(inactive → unconfigured)
   5、on_shutdown: 节点关闭时触发 —— 同样关串口(任何状态 → 关闭)
第六步:
       read: 从舵机读位置 → 弧度 → 状态缓冲(把 6 个舵机的真实位置读回来,换算成弧度,填进状态缓冲;顺带用相邻两次位置差分算出速度填进 hw_states_vel_)
       write: 命令缓冲 → 弧度 → 舵机位置值 → 发串口(把控制器写入 hw_commands_ 的目标位置(弧度) 换算回 counts)
第七步:串口层--通信底层
第八步:SCS协议(Feetech/ST 串行总线舵机协议)
       每帧格式(十六进制): FF FF ID LEN INST PARAM... CHK
       FF FF : 帧头(所有舵机据此识别"要发指令了")
       ID    : 目标舵机号(0xFE=广播)
       LEN   : 后面数据长度
       INST  : 指令(0x02 READ / 0x03 WRITE)
       PARAM : 寄存器地址+数据
       CHK   : 校验和 = ~(从 ID 到倒数第 2 字节求和) & 0xFF
       计算校验和:从第 3 字节(不含 FF FF 帧头)累加,取反+1字节

三件套 = ① URDF 里的 <ros2_control> 块(声明硬件和接口) + ② controllers.yaml(Controller Manager 和控制器参数) + ③ launch 文件(加载顺序)。缺一不可。

3.1 URDF / xacro 里的 <ros2_control>

位置:与 <link>/<joint> 平级,通常写在文件末尾、</robot> 之前。一个块 = 一个硬件组件。结构:

ros2_control.xacro文件

<?xml version="1.0"?>
<!-- ================================================================
 这个 xacro 文件用于描述机械臂的 ros2_control 硬件配置。
 它被 load_yaml 引用进 robot_description,最终展开成一份
 <ros2_control> 描述,controller_manager 启动时会读取它,从而知道:
   - 用哪个硬件插件控制舵机 (arm_hardware/St3215SystemHardware)
   - 打开哪个串口、波特率多少
   - 管理哪些关节,每个关节有哪些 指令接口/状态接口
 ================================================================ -->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
    <!-- 定义一个可复用的 xacro 宏
         参数说明:
           name                   -> <ros2_control name="...">,给这组硬件起个名字
           initial_positions_file -> 存放各关节"初始角度"的 yaml 文件 -->
    <xacro:macro name="so101_new_follower_ros2_control" params="name initial_positions_file">
        <!-- 从 yaml 里读出 'initial_positions' 这一节存成变量 initial_positions,
             供下面每个关节的 <param name="initial_value"> 取值使用 -->
        <xacro:property name="initial_positions" value="${xacro.load_yaml(initial_positions_file)['initial_positions']}"/>

        <!-- ros2_control 根标签
             type="system":表示"一个硬件实例管多个关节"(本机械臂 = 一条舵机总线管 6 个舵机);
             如果是一对一(一个执行器带一个关节),可用 type="actuator"  比如舵机云台 / 单舵机(只控制 1 个关节) -->
        <ros2_control name="${name}" type="system">
            <!-- hardware:声明硬件插件及其参数 -->
            <hardware>
                <!-- 插件:加载 arm_hardware 包里导出的 St3215SystemHardware 类 -->
                <plugin>arm_hardware/St3215SystemHardware</plugin>
                <!-- 串口设备:舵机控制板在 PC 上对应的设备号 -->
                <param name="port">/dev/ttyACM0</param>
                <!-- 波特率:ST3215 串行总线舵机的通信速率(1 Mbps) -->
                <param name="baudrate">1000000</param>
                <!-- 舵机 ID 列表:顺序与下方 6 个 <joint> 一一对应 -->
                <param name="servo_ids">1,2,3,4,5,6</param>
                <!-- 示教模式:true 时允许手掰定位、以当前位置作初始值;调试阶段常先开 true -->
                <param name="teach_mode">true</param>
            </hardware>
            <!-- 关节:shoulder_pan(基座左右旋转/偏航)
                 每个关节都有两类接口:
                   command_interface = 控制器"发给"硬件的指令
                   state_interface   = 硬件"回报"给控制器的状态
                 joint 的 name 必须和 URDF 里的关节名一致 -->
            <joint name="shoulder_pan">
                <!-- 位置指令接口:接收目标角度(弧度)指令 -->
                <command_interface name="position"/>
                <!-- 位置状态接口:回报当前位置;initial_value 取 yaml 里的初始角度 -->
                <state_interface name="position">
                  <param name="initial_value">${initial_positions['shoulder_pan']}</param>
                </state_interface>
                <!-- 速度状态接口:回报当前速度 -->
                <state_interface name="velocity"/>
            </joint>
            <!-- 关节:shoulder_lift(肩部俯仰,抬/落大臂) -->
            <joint name="shoulder_lift">
                <command_interface name="position"/>
                <state_interface name="position">
                  <param name="initial_value">${initial_positions['shoulder_lift']}</param>
                </state_interface>
                <state_interface name="velocity"/>
            </joint>
            <!-- 关节:elbow_flex(肘部弯曲) -->
            <joint name="elbow_flex">
                <command_interface name="position"/>
                <state_interface name="position">
                  <param name="initial_value">${initial_positions['elbow_flex']}</param>
                </state_interface>
                <state_interface name="velocity"/>
            </joint>
            <!-- 关节:wrist_flex(腕部俯仰,抬头/低头) -->
            <joint name="wrist_flex">
                <command_interface name="position"/>
                <state_interface name="position">
                  <param name="initial_value">${initial_positions['wrist_flex']}</param>
                </state_interface>
                <state_interface name="velocity"/>
            </joint>
            <!-- 关节:wrist_roll(腕部自转/滚转) -->
            <joint name="wrist_roll">
                <command_interface name="position"/>
                <state_interface name="position">
                  <param name="initial_value">${initial_positions['wrist_roll']}</param>
                </state_interface>
                <state_interface name="velocity"/>
            </joint>
            <!-- 关节:gripper(夹爪开合) -->
            <joint name="gripper">
                <command_interface name="position"/>
                <state_interface name="position">
                  <param name="initial_value">${initial_positions['gripper']}</param>
                </state_interface>
                <state_interface name="velocity"/>
            </joint>

            <!-- 小结:上面 6 个 <joint> 的声明顺序与 hardware 中
                 servo_ids=1,2,3,4,5,6 的顺序一一对应;
                 每个关节的 name 都必须和 URDF/xacro 模型中的关节名完全一致,
                 否则 controller_manager 无法把控制器绑定到对应关节上。 -->
        </ros2_control>
    </xacro:macro>
</robot>

逐字段含义:

  • name:这块硬件的名字(日志/调试里区分多硬件用)。
  • typesystem(整机,多关节同一总线)/ actuator(单执行器)/ sensor(只读传感器,可没有 <joint>,改有 <gpio>/传感器描述)。
  • <plugin>:真正干活的类。换仿真/真机就是换这一行(这就是"无缝切换"的物理位置)。常见值:你的自写插件、gz_ros2_control/GzSystem(Gazebo Harmonic)、gazebo_ros2_control/GazeboSystem(Gazebo Classic,见 §4.3 的版本坑)、mock 测试插件。
  • <param>:硬件私有参数,编译期无关,运行时在硬件 on_init() 里读取——所以换串口、换波特率不用改代码。
  • <joint><command_interface>/<state_interface>:接口声明。声明了才存在,控制器只能用它声明的接口。漏声明 = "接口找不到"报错;多声明 = 硬件必须真的提供(export)否则报错。

3.2 controllers.yaml(Controller Manager 与各控制器参数)

一个 yaml 文件,分两大段:

controller_manager:
  ros__parameters:
    update_rate: 100          # 控制循环频率 Hz(见 §2.5)
    # 每个要加载的控制器在这里"挂号",键名=控制器实例名
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster
    joint_trajectory_controller:
      type: joint_trajectory_controller/JointTrajectoryController
    forward_position_controller:
      type: forward_command_controller/ForwardCommandController

# 下面为各控制器实例的具体参数(与 controller_manager 并列,属于各自节点)
joint_state_broadcaster:
  ros__parameters:
    # 通常空即可:广播"全部"状态接口。也可显式列 joints 限制广播范围
    joints: [joint1, joint2, joint3, joint4, joint5, joint6]

joint_trajectory_controller:
  ros__parameters:
    joints: [joint1, joint2, joint3, joint4, joint5, joint6]  # 顺序=轨迹消息里的顺序
    command_interfaces: [position]    # 本控制器写哪些命令接口(你的臂只有 position)
    state_interfaces: [position]      # 本控制器读哪些状态接口(无 velocity 就只写 position)
    state_publish_rate: 50.0          # 额外状态话题发布频率
    action_monitor_rate: 20.0

forward_position_controller:
  ros__parameters:
    joints: [joint1, joint2, joint3, joint4, joint5, joint6]
    interface_name: position          # 把收到的指令直通到哪个命令接口

易错点(对应你"会配但讲不清"):

  • type 字段 = "包名/类名",不是随便起的名字。类名以 ros2 control list_controller_types 输出为准。
  • JTC 的 joints 顺序 = 订阅到的轨迹消息中关节顺序,MoveIt 生成的轨迹顺序与其一致才行,否则报"关节不在列表"。
  • command_interfaces/state_interfaces 子集不能超出硬件声明。你只有 position 命令接口,若写 [position, velocity],JTC 配置必失败——这是最常见的"我按教程抄了 RRBot(它带 velocity)但我的臂配置失败"的原因,D4 实验会验证。
  • controller_manager 段里只挂号(声明要加载谁);参数段在各自的节点名下。缩进错位是 yaml 报错重灾区。

3.3 launch:谁先谁后,为什么

标准 launch 骨架(以 ros2_control_demos 为范本):

# 1. 机器人模型:robot_state_publisher 把 URDF 发成 /robot_description
robot_state_publisher = Node(package='robot_state_publisher',
    executable='robot_state_publisher',
    parameters=[{'robot_description': urdf_xacro}])

# 2. 控制核心:controller_manager 节点(可执行名 ros2_control_node),
#    参数 = 上面的 controllers.yaml(它据此挂号并创建 controller manager 实例)
controller_manager = Node(package='controller_manager',
    executable='ros2_control_node',
    parameters=[controllers_yaml])

# 3. 广播器先启动,其他控制器随后(用 spawner 逐步 activate)
joint_state_broadcaster_spawner = Node(package='controller_manager',
    executable='spawner',
    arguments=['joint_state_broadcaster', '--controller-manager', '/controller_manager',
               '--controller-manager-timeout', '30'])

# 4. 真机/仿真都起来后再 activate 轨迹控制器(顺序取决于你想先看状态还是直接动)
traj_controller_spawner = Node(package='controller_manager',
    executable='spawner',
    arguments=['joint_trajectory_controller', ...])

顺序逻辑:

  1. 先有 robot_description:controller_manager 解析 URDF 需要它(§2.3)。
  2. 再起 controller_manager:它创建 ResourceManager、解析 URDF 硬件块、按 yaml 挂号控制器;硬件此时还只是 configured/inactive(未开串口)——所以"controller_manager 起来了但串口还没开"是正常的,不要慌。
  3. spawner 逐个激活:spawner 本质是"load + configure + activate",并等待 controller manager 就绪(--controller-manager-timeout 防 launch 竞态)。
  4. joint_state_broadcaster 永远最先激活:没有它就没有 /joint_states,MoveIt/rviz 拿不到状态,会报 joint state 超时。

3.4 调试命令全家桶(背下来)

这套命令 = 面试"你怎么调试 ros2_control" + 日常排障的全部工具:

# ── 查状态(最常用,先跑这三个)──
ros2 control list_controllers            # 每个控制器的 名称/类型/状态(active?)/claimed interfaces
ros2 control list_hardware_interfaces    # 每个关节的 command/state 接口 + 当前被谁 claim + 数值
ros2 control list_controller_types       # 本机装了哪些控制器类型(type 字段以这里为准)

# ── 动态管理控制器 ──
ros2 control load_controller joint_trajectory_controller        # 只加载(→unconfigured)
ros2 control load_controller joint_trajectory_controller --set-state configure
ros2 control load_controller joint_trajectory_controller --set-state active
ros2 control unload_controller joint_trajectory_controller
ros2 control switch_controllers --deactivate old_ctrl --activate new_ctrl

# ── 生命周期(spawner 之外的另一种控制)──
ros2 lifecycle get /controller_manager
ros2 lifecycle set /joint_trajectory_controller deactivate
ros2 lifecycle list /joint_trajectory_controller

# ── 观察数据 ──
ros2 topic echo /joint_states           # 状态回传是否正常(臂应该在动/有值)
ros2 topic echo /joint_trajectory_controller/joint_trajectory   # JTC 收到的轨迹
ros2 node list && ros2 node info /controller_manager            # 谁在跑、有啥服务
ros2 param get /controller_manager update_rate

# ── 日志(激活失败时必看)──
ros2 topic echo /rosout                        # 或 rqt_console

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐