【ROS2】驱动开发-通过控制器访问硬件(Hardware Access through Controllers)
本文介绍了控制器如何 独占地使用硬件资源,同时保持尽可能的灵活性,以避免被严格绑定为“位置(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/roll、my_sensor/pitch、my_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 valuesstd::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();
