本文介绍了控制器如何 独占地使用硬件资源,同时保持尽可能的灵活性,以避免被严格绑定为“位置(position)”、“速度(velocity)”和“力/力矩(effort)”这三种类型的限制(参见 Flexible Joint States Message)。

文章首先描述了硬件资源是如何 分类和加载 的,然后提供了一种 在控制器中安全访问这些资源的实时方法

硬件资源(Hardware Resources)

硬件资源描述了在 ros2_control 中被管理的 物理组件

在这里,我们区分三类硬件资源:执行器(Actuators)传感器(Sensor)系统(System)。每个硬件资源在运行时加载,这使得被控制系统的组成既 灵活又动态。硬件的组成和配置完全通过 URDF 来完成。


关节(Joint / Interface)

关节被视为一个逻辑组件,由至少一个执行器驱动(根据实际硬件,可能存在欠驱动或过驱动的情况)。

关节的作用是作为 控制器实例与底层硬件之间的抽象层。这种抽象可以屏蔽硬件的复杂性,使控制器只需关注关节的值即可,即使一个关节可能由多个电机通过复杂的传动机构驱动。

关节的配置是与硬件资源(如执行器或系统)配合进行的。关节组件通过 命令接口(command interfaces)状态接口(state interfaces) 使用,这些接口在 URDF 中声明:

  • 命令接口(command interfaces):描述控制关节时可以设置的值类型,例如力/力矩(effort)或速度(velocity)
  • 状态接口(state interfaces):只读接口,用于获取关节反馈信息

接口本身还可以通过传入参数进行进一步配置。

例如,一个 URDF 配置示例如下:

...
<joint name="my_joint">
  <command_interface name="joint_command_interface">
    <param name="my_joint_command_param">1.5</param>
  </command_interface>
  <state_interface name="joint_state_interface1" />
  <state_interface name="joint_state_interface2" />
</joint>
...

传感器(Sensor / Interface)

传感器是另一种逻辑组件,它表示 只读状态反馈的硬件资源接口

类似于关节接口(Joint Interface),传感器接口也对底层物理硬件进行了抽象,使控制器可以通过统一接口访问硬件状态。

<sensor name="my_sensor">
  <state_interface name="sensor_state_interface1" />
  <state_interface name="sensor_state_interface2" />
  <state_interface name="sensor_state_interface3" />
</sensor>

传感器接口只能在 Sensor 或 System 硬件标签内进行配置。

执行器(硬件)

执行器(Actuator)描述了一个具有最多 1 个自由度(1DoF) 的单一物理执行器实例,并且它与某个 关节(Joint) 严格绑定。
它通常接收一个单一的控制命令值,用于对应的工作模式,例如期望的关节速度(velocity)或力矩/推力(effort);在少数情况下,执行器也可能直接接收一个精确的关节位置(position)值。

该执行器的实现可能会将这些期望值转换为 PWM 信号 或其他硬件特定的控制命令,从而驱动物理硬件。同样地,执行器也可以提供状态反馈。根据具体设置,电机编码器(motor encoder)可能会提供位置、速度、力矩或电流等反馈信息。

执行器在 URDF 中的定义片段可能如下所示:

<ros2_control name="my_simple_servo_motor" type="actuator">
  <hardware>
    <class>simple_servo_motor_pkg/SimpleServoMotor</class>
    <param name="serial_port">/dev/tty0</param>
  ...
  </hardware>
  <joint name="joint1">
    <command_interface name="position">
      <param name="min">-1.57</param>
      <param name="max">1.57</param>
    </command>

    <state_interface name="position"/>
    ...
  </joint>
</ros2_control>

上面的代码片段展示了一个简单的硬件配置:
它包含一个执行器(Actuator),用于控制一个逻辑关节(Joint)。
在这个例子中,关节被配置为使用位置值(position)进行控制命令,同时其状态反馈也是位置(position)

如果一个关节在配置中声明了某种控制接口或状态接口,而底层硬件并不支持该接口,那么在启动时会触发运行时错误(runtime error)

相反,如果关节只配置了最基本、必要的接口(例如 position),即使硬件实际上还支持其他接口(例如 “current” 或 “voltage”),这些额外接口也不会被实例化,而是被忽略

传感器(Sensor,硬件)

传感器是一种仅提供状态反馈的硬件组件。
它可以被视为一种只读的硬件资源,因此不需要独占访问管理——也就是说,多个控制器可以同时共享使用该传感器。

<ros2_control name="my_simple_sensor">
  <hardware type="sensor">
    <class>simple_sensor_pkg/SimpleSensor</class>
    <param name="serial_port">/dev/tty0</param>
    ...
  </hardware>
  <sensor name="my_sensor">
    <state_interface name="roll" />
    <state_interface name="pitch" />
    <state_interface name="yaw" />
  </sensor>
</ros2_control>

需要注意的是,从技术上讲,物理硬件资源<hardware type="sensor">)与逻辑组件<sensor name="my_sensor">)在概念上是分开的,虽然它们都被称为 Sensor(传感器)
不过,在本文档中我们不会对它们进行进一步区分,因为对于用户而言,这种区分没有太大的语义意义

系统(System,硬件)

System 用于表示一种更复杂的硬件结构,它包含多个关节(joints)和传感器(sensors)
这种类型通常用于第三方机器人系统,例如机械臂工业自动化设备,这些设备通常拥有自有(专有)的 API 接口

System 类型硬件资源的实现,起到一个中间层接口的作用——它连接底层物理硬件与上层控制器(controller),以便控制器能够通过该接口访问和操作硬件提供的 API。

<ros2_control name="MyComplexRobot" type="system">
  <hardware>
    <class>complex_robot_pkg/ComplexRobot</class>
    ...
    </hardware>
    <joint name="joint1">
      <command_interface name="velocity" />
      <command_interface name="effort" />
      <state_interface name="position" />
      <state_interface name="velocity" />
      <state_interface name="effort" />
    </joint>
    <joint name="joint2">
      <command_interface name="velocity" />
      <command_interface name="effort" />
      <state_interface name="position" />
      <state_interface name="velocity" />
      <state_interface name="effort" />
    </joint>
</ros2_control>

资源管理器(Resource Manager)

资源管理器负责 解析 URDF 文件 并实例化相应的硬件资源(hardware resources)和逻辑组件(logical components)。
它管理这些硬件资源及其关联逻辑关节的生命周期,并作为控制器管理器(Controller Manager)的存储后端,为控制器提供硬件资源的使用权限。

资源管理器会维护一个控制器与其所占用资源的账本

  • 当控制器不再需要某个资源时,该资源会被释放并返回给资源管理器(Resource Manager),可以提供给其他控制器使用。
    在内部,资源管理器会维护每个硬件资源及其接口的映射关系。

  • 这个映射可以通过简单的 逻辑组件名 / 接口名 查找。

  • 资源管理器将物理硬件资源与逻辑组件进行抽象分离,使控制器无需知道具体哪个硬件负责控制哪条关节(joint)。

在之前的例子中:

  • 执行器(Actuator)的控制接口被映射到 joint1/position,状态接口同样映射到 joint1/position
  • 对于传感器(Sensor),它的接口被映射到 my_sensor/rollmy_sensor/pitchmy_sensor/yaw

控制器接口(Controller Interface)

系统启动并加载控制器后,控制器可以**声明逻辑组件(logical components)**并访问它们的接口。


通用访问(Generic Access)

控制器可以通过向资源管理器(Resource Manager)查询对应的键(key)来访问单个接口的值,例如 joint1/effort

  • 如果该接口可用,控制器就会获得一个 handle,可以在控制器执行过程中设置 joint1 的力矩(effort)值。

示例代码:

void MyController::init(... resource_manager)
{
  InterfaceCommandHandle joint1_effort_cmd = resource_manager->claim_command_interface("joint1/effort");
  InterfaceStateHandle joint1_position_state = resource_manager->claim_state_interface("joint1/position");
}

语义组件(Semantic Components)

上面的例子对于简单系统可能已经足够,但在处理复杂传感器(如 6D 力/扭矩传感器、IMU 等)时,会积累大量的 handle。
因此,引入了 语义组件(Semantic Components),它们可以封装多个键(keys),并在此基础上提供更有意义的 API,方便控制器调用和操作。

void MyController::init(... resource_manager)
{
  FTSensor6D ft_sensor(resource_manager,
    "sensor1/fx", "sensor1/fy", "sensor1/fz",  // force values
    "sensor1/tx", "sensor1/ty", "sensor1/tz"); // torque values

  std::vector<double> torque = ft_sensor.get_torque_values();
  geometry_msgs::msg::Wrench wrench_msg = ft_sensor.as_wrench_msg();
}

作为展望(Outlook),可以考虑让 语义组件(Semantic Components) 为硬件资源提供更多有意义的信息。

例如,对于一个摄像头传感器(Camera Sensor),为每个像素提供一个键(key)几乎没有实际意义。
一种解决方案是提供能够
充分描述摄像头的键
,例如 通用数据键(generic data key)尺寸键(size key)

在这种情况下,Camera 类的实现需要知道如何解释 camera1/data 指针:

  • 不能把它当作单个 double 值处理,
  • 而应当将其视为一个指针地址或类似的数据结构,从而正确访问整个图像数据。
Camera cam(resource_manager, "camera1/data", "camera1/size");
cv::Mat img = cam.get_image();

更多推荐