ros_control 中 hardware_interface 教程
各个网站上找了一圈,没发现ROS Hardware_Interface的比较好的教学资料,ROS Wiki写的又看不大懂。索性花了一个下午时间,边和Gemini对话边查找资料,把这部分内容认真梳理了一遍,写成了一篇教程。可能有不完善的地方,也欢迎指教。
hardware_interface 是 ros_control 大框架下的一个重要组成部分。hardware_interface 的架构基于以下几个核心概念:
-
资源 (Resource): 机器人上任何可以被独立控制或读取的单元。最常见的资源是“关节”(Joint),但也可以是其他任何东西,如致动器(Actuator)、传感器(IMU Sensor)、或一个简单的GPIO引脚。
-
句柄 (Handle): 对单个资源的直接访问凭证。控制器通过句柄来读取特定资源的状态(如关节角度)或向其发送指令(如关节目标速度)。句柄确保了资源的访问是受控且唯一的。
-
接口 (Interface): 一组特定类型资源的管理集合。一个 Interface 中拥有并管理一系列同类型的Handel。它为控制器提供了访问和控制这些资源的方法。
下面进行详细讲解。请注意,hardware_interface一般指这个整体框架,而 interface 或 HardwareInterface 则主要指其中的接口部分。
概述
hardware_interface 的架构主要由以下几个C++类和概念构成:
a. hardware_interface::RobotHW
这是用户与 hardware_interface 交互最主要的类。开发者需要继承这个基类,并为自己的特定机器人实现一个具体的硬件接口类。这个派生类是连接 ros_control 框架和物理硬件的桥梁。
RobotHW 类有两个至关重要的虚函数,必须由用户实现:
-
init(): 初始化函数。通常用于设置与硬件的连接,注册下面将要提到的各种硬件接口,以及初始化数据结构。
-
read(const ros::Time& time, const ros::Duration& period): 从硬件读取数据。在此函数中,你需要通过硬件驱动获取机器人的当前状态,并将这些值更新到与 ros_control 共享的内存中。
-
write(const ros::Time& time, const ros::Duration& period): 向硬件写入数据。在此函数中,你需要从 ros_control 共享内存中获取控制器计算出的指令值(,并通过硬件驱动将这些指令发送给物理硬件执行。
b. RobotHW::registerInterface()
在具体代码实现中,我们通过 RobotHW 基类中提供的 registerInterface() 方法,注册和管理机器人支持的所有硬件接口。read、write则是通过这些接口,调用硬件的SDK,对硬件的状态进行读取或者写入。例如:
registerInterface(&hybridJointInterface_);
c. 标准硬件接口 (Standard Hardware Interfaces)
hardware_interface 给定了硬件接口标准形式,基类 hardware_interface::HardwareInterface。
ros_control 框架继承该接口,定义了一系列标准接口:
hardware_interface::JointStateInterface
hardware_interface::ImuSensorInterface
等等.此外,也可以自己定义.
资源 (Resource)
资源 通常对应一个物理实体,比如一个关节电机、一个IMU传感器、一个力传感器或者一个线性的致动器。
资源最重要的是管理,每个资源都必须有一个唯一的字符串名称(例如 “shoulder_pan_joint”, “left_wheel_joint”)。这个名称是整个 ros_control 生态系统(包括URDF、控制器配置文件、RobotHW代码)中识别该资源的关键。例如,ros中,hardware_interface::HardwareInterface 中的 claim_ 用于检测资源的唯一所有性,就是通过维护一个已注册资源的字符串列表,来检查资源是否已经被其他的Handle所有了.再次强调,检查是由Interface进行的.
但是,对于关节 Joint1,我们既需要一个Handle获取其状态,又需要一个Handle来管理发送指令给这个关节,是否会造成冲突呢?答案是不会的.对于一个Interface,如果其定义为:
class JointSomeHandle;
class JointSomeInterface: public HardwareResourceManager<JointSomeHandle, hardware_interface::DontClaimResources>
此时,claim() 和 clearClaims() 方法都是空操作(No-op),它们被调用时什么也不做。也就是不检查其所有权.对于读类型的Interface而言,如JointStateInterface,就是采取的这种定义方式.
而如果采用的是:
class JointSomeHandle;
class JointSomeInterface: public HardwareResourceManager<JointSomeHandle, hardware_interface::ClaimResources>
ClaimResources内部维护着一个私有的资源列表,在初始化的时候会将注册在这个Interface下的Handle所指向的资源加入其私有资源列表中.
现在,当一个控制器 joint_state_controller 启动并请求 JointStateInterface 时,ControllerManager就会先检查这个 Interface 在 RobotHW 中真的已经被创建.如果是,检查该Interface 是 DontClaimResources,就直接将这个 Interface 的指针传给这个控制器了.如果对于某一个Interface(如JointCommadInterface),检查其内部是 ClaimResources,则会将其内部的私有资源列表与中央资源列表对比,如果没有重复 Claim,则将接口给Controller,并将私有资源列表加入到中央资源列表中.
句柄 (Handle)
总结来说,对于hardware_interface而言,句柄是向上的接口,用于与控制器通信;硬件SDK是向下的接口,用于与真实硬件或者仿真环境中的物理资源进行交互。由于一般而言机器人有交互的硬件部分较多,所需的句柄较多,我们需要 HardwareInterface 对向上接口 Handle 进行统一管理。
下面的图像表述了句柄的创建与使用流程。
Handel的创建流程
步骤一:声明数据存储 (The Data Buffers)
// In MyRobotHW.h
class MyRobotHW : public hardware_interface::RobotHW {
private:// 为N个关节声明数据存储// 这些就是“共享内存”std::vector<double> joint_positions_; std::vector<double> joint_velocities_; std::vector<double> joint_efforts_; std::vector<double> joint_commands_; std::vector<std::string> joint_names_; // 资源名称列表int num_joints_; // 资源数量
};
步骤二:绑定数据并实例化 Handle
(The Binding)
值得注意的是,Handle 并没有一个统一的基类.例如下面的JointStateHandle.
// In MyRobotHW.cpp (Constructor or init() method)
MyRobotHW::MyRobotHW() {// 假设我们从参数服务器或配置文件中获取了关节名和数量// num_joints_ = ...; joint_names_ = ...;// 为数据存储分配空间joint_positions_.resize(num_joints_);for (int i = 0; i < num_joints_; ++i) {// 1. 为 "joint_names_[i]" 这个资源创建一个 JointStateHandlehardware_interface::JointStateHandle state_handle(joint_names_[i], &joint_positions_[i], // 指向其位置状态数据的指针&joint_velocities_[i], // 指向其速度状态数据的指针&joint_efforts_[i] // 指向其力矩状态数据的指针);// ... state_handle 被创建 ...// 2. 为 "joint_names_[i]" 这个资源创建一个指令Handle (以Position为例)// 它复用 state_handle 的信息,并额外绑定指令数据变量的地址hardware_interface::JointHandle command_handle(state_handle, // 传入刚才创建的状态句柄&joint_commands_[i] // 指向其指令数据的指针);// ... command_handle 被创建 ...}
}
步骤三:向 Interface
注册 Handle
(The Registration)
// In MyRobotHW.cpp (继续在循环中)
for (int i = 0; i < num_joints_; ++i) {// ... state_handle 和 command_handle 被创建 ...// 3. 将 Handle 注册到对应的 Interface 中// Interface 会以资源名为索引存储这些Handlejnt_state_interface_.registerHandle(state_handle);pos_jnt_interface_.registerHandle(command_handle); // 假设是位置控制
}// 循环结束后,将整个Interface注册到RobotHW基类中
registerInterface(&jnt_state_interface_);
registerInterface(&pos_jnt_interface_);
句柄的使用
上面已经基本将句柄的基本使用方式讲清楚了。下面我们做一个摘要:
Handle的创建
hardware_interface::JointHandle command_handle(state_handle, // 传入刚才创建的状态句柄&joint_commands_[i] // 指向其指令数据的指针
);
Handle的注册
总是需要将Handle先注册到 HardwareInterface 中,此后控制器在通过 HardwareInterface 按照名称获取Handel。
jnt_state_interface_.registerHandle(state_handle);
registerInterface(&jnt_state_interface_);
Handle的调用
joint_handle_ = HwInterface->getHandle(joint_name);
接口 (Interface)
Interface 和 Handle 是典型的 容器与元素 的关系。
- 包含关系: 一个 Interface 包含零个或多个 Handle。
- 访问流程:
- 硬件侧 (RobotHW): Handle 被创建 -> Handle 被注册到 Interface 中。
- 控制器侧 (Controller): 请求 Interface -> 从 Interface 中根据名称请求 Handle -> 使用 Handle 读/写数据。
- 类型约束: 一个 Interface 只能管理一种类型的 Handle。例如,JointStateInterface 只管理 JointStateHandle,不能将一个用于指令的 JointHandle 注册进去。
上面资源部分已经讲解了Interface对资源的Claim操作,保证 具有写操作等互斥操作的Handle 被唯一获取.下面将进一步对 Interface 讲解.
Interface的创建与注册
// MyRobotHW.h
class MyRobotHW : public hardware_interface::RobotHW {
private:hardware_interface::PositionJointInterface pos_jnt_interface_;hardware_interface::JointStateInterface jnt_state_interface_;
};// MyRobotHW.cpp
MyRobotHW::MyRobotHW() {// ... 创建 state_handle 和 command_handle ...jnt_state_interface_.registerHandle(state_handle);pos_jnt_interface_.registerHandle(command_handle);registerInterface(&jnt_state_interface_);registerInterface(&pos_jnt_interface_);
}
在Controller中获取Interface
// MyController.cpp
bool MyController::init(hardware_interface::RobotHW* robot_hw, ...) {// 获取只读接口,总会成功 (只要接口存在)auto* state_if = robot_hw->get<hardware_interface::JointStateInterface>();// 获取独占接口,会触发资源冲突检查auto* pos_if = robot_hw->get<hardware_interface::PositionJointInterface>();if (!pos_if) {ROS_ERROR("Failed to get PositionJointInterface.");return false;}// ... 通过接口获取Handle ...
}