ROS2 Humble下,手把手教你为DIY机械臂编写ros2_control硬件插件(C++实战)

张开发
2026/4/30 14:47:08 15 分钟阅读

分享文章

ROS2 Humble下,手把手教你为DIY机械臂编写ros2_control硬件插件(C++实战)
ROS2 Humble下DIY机械臂的ros2_control硬件插件开发实战机械臂开发者在将自定义或非标机械臂接入ROS2生态时最关键的挑战之一是实现硬件与ROS2控制框架的无缝对接。本文将深入探讨如何基于ros2_control框架从零开始开发一个完整的硬件接口插件让你的DIY机械臂能够流畅运行MoveIt生成的轨迹规划。1. ros2_control框架核心概念解析ros2_control是ROS2中负责硬件抽象和实时控制的框架它定义了硬件与控制器之间的标准接口。理解其架构对于开发硬件插件至关重要。核心组件关系图[用户指令] → [MoveIt] → [轨迹控制器] → [ros2_control] → [硬件插件] → [实际硬件] ↑____________反馈循环__________↓硬件插件需要实现的关键接口包括命令接口接收控制器发送的位置/速度/力距指令状态接口向控制器反馈关节实际状态生命周期管理硬件初始化、激活和关闭典型的开发流程中我们需要重点关注以下几个文件my_arm_hardware/ ├── include/my_arm_hardware/my_arm_hardware.hpp # 插件头文件 ├── src/my_arm_hardware.cpp # 插件实现 ├── CMakeLists.txt # 构建配置 └── config/controllers.yaml # 控制器配置2. 创建硬件接口插件工程首先创建一个新的ROS2包来承载我们的硬件插件ros2 pkg create --build-type ament_cmake my_arm_hardware \ --dependencies hardware_interface rclcpp pluginlib在CMakeLists.txt中需要添加以下关键配置# 查找依赖 find_package(hardware_interface REQUIRED) find_package(pluginlib REQUIRED) # 声明插件库 add_library( my_arm_hardware SHARED src/my_arm_hardware.cpp ) # 链接依赖 target_include_directories( my_arm_hardware PUBLIC include ) ament_target_dependencies( my_arm_hardware hardware_interface rclcpp ) # 导出插件 pluginlib_export_plugin_description_file( hardware_interface my_arm_hardware_plugin.xml ) # 安装目标 install( TARGETS my_arm_hardware ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin )同时需要创建插件描述文件my_arm_hardware_plugin.xmllibrary pathmy_arm_hardware class namemy_arm_hardware/MyArmHardware typemy_arm_hardware::MyArmHardware base_class_typehardware_interface::SystemInterface description Hardware interface for DIY robotic arm /description /class /library3. 实现硬件接口类硬件插件的核心是继承hardware_interface::SystemInterface并实现其纯虚函数。以下是关键方法的实现要点3.1 类定义与接口声明#include hardware_interface/system_interface.hpp #include hardware_interface/types/hardware_interface_type_values.hpp namespace my_arm_hardware { class MyArmHardware : public hardware_interface::SystemInterface { public: // 必须实现的接口 CallbackReturn on_init(const hardware_interface::HardwareInfo info) override; std::vectorStateInterface export_state_interfaces() override; std::vectorCommandInterface export_command_interfaces() override; CallbackReturn on_activate(const rclcpp_lifecycle::State previous_state) override; CallbackReturn on_deactivate(const rclcpp_lifecycle::State previous_state) override; return_type read(const rclcpp::Time time, const rclcpp::Duration period) override; return_type write(const rclcpp::Time time, const rclcpp::Duration period) override; private: // 硬件通信相关成员变量 std::vectordouble joint_position_commands_; std::vectordouble joint_position_states_; std::vectordouble joint_velocity_states_; // 硬件通信接口如串口、Socket等 int serial_fd_; }; } // namespace my_arm_hardware3.2 硬件初始化实现on_init方法负责解析URDF中的硬件配置信息CallbackReturn MyArmHardware::on_init(const hardware_interface::HardwareInfo info) { if (hardware_interface::SystemInterface::on_init(info) ! CallbackReturn::SUCCESS) { return CallbackReturn::ERROR; } // 初始化命令和状态变量 joint_position_commands_.resize(info_.joints.size(), 0.0); joint_position_states_.resize(info_.joints.size(), 0.0); joint_velocity_states_.resize(info_.joints.size(), 0.0); // 检查URDF配置 for (const auto joint : info_.joints) { if (joint.command_interfaces.size() ! 1 || joint.command_interfaces[0].name ! hardware_interface::HW_IF_POSITION) { RCLCPP_ERROR(rclcpp::get_logger(MyArmHardware), Joint %s has invalid command interfaces, joint.name.c_str()); return CallbackReturn::ERROR; } if (joint.state_interfaces.size() 2) { RCLCPP_ERROR(rclcpp::get_logger(MyArmHardware), Joint %s has insufficient state interfaces, joint.name.c_str()); return CallbackReturn::ERROR; } } // 初始化硬件通信以串口为例 serial_fd_ open(/dev/ttyACM0, O_RDWR | O_NOCTTY | O_SYNC); if (serial_fd_ 0) { RCLCPP_ERROR(rclcpp::get_logger(MyArmHardware), Failed to open serial port: %s, strerror(errno)); return CallbackReturn::ERROR; } // 配置串口参数 struct termios tty; if (tcgetattr(serial_fd_, tty) ! 0) { RCLCPP_ERROR(rclcpp::get_logger(MyArmHardware), Error %d from tcgetattr: %s, errno, strerror(errno)); return CallbackReturn::ERROR; } cfsetospeed(tty, B115200); cfsetispeed(tty, B115200); tty.c_cflag ~PARENB; // 无奇偶校验 tty.c_cflag ~CSTOPB; // 1位停止位 tty.c_cflag ~CSIZE; tty.c_cflag | CS8; // 8位数据位 tty.c_cflag ~CRTSCTS; // 无硬件流控 tty.c_cflag | CREAD | CLOCAL; // 启用接收忽略控制线 tty.c_lflag ~ICANON; // 非规范模式 tty.c_lflag ~ECHO; // 禁用回显 tty.c_lflag ~ISIG; // 禁用信号字符 tty.c_iflag ~(IXON | IXOFF | IXANY); // 禁用软件流控 tty.c_oflag ~OPOST; // 原始输出 if (tcsetattr(serial_fd_, TCSANOW, tty) ! 0) { RCLCPP_ERROR(rclcpp::get_logger(MyArmHardware), Error %d from tcsetattr: %s, errno, strerror(errno)); return CallbackReturn::ERROR; } return CallbackReturn::SUCCESS; }3.3 接口导出实现export_state_interfaces和export_command_interfaces定义了硬件提供的接口std::vectorStateInterface MyArmHardware::export_state_interfaces() { std::vectorStateInterface state_interfaces; for (size_t i 0; i info_.joints.size(); i) { state_interfaces.emplace_back( hardware_interface::StateInterface( info_.joints[i].name, hardware_interface::HW_IF_POSITION, joint_position_states_[i])); state_interfaces.emplace_back( hardware_interface::StateInterface( info_.joints[i].name, hardware_interface::HW_IF_VELOCITY, joint_velocity_states_[i])); } return state_interfaces; } std::vectorCommandInterface MyArmHardware::export_command_interfaces() { std::vectorCommandInterface command_interfaces; for (size_t i 0; i info_.joints.size(); i) { command_interfaces.emplace_back( hardware_interface::CommandInterface( info_.joints[i].name, hardware_interface::HW_IF_POSITION, joint_position_commands_[i])); } return command_interfaces; }4. 实现实时读写方法read()和write()方法是硬件插件的核心负责与真实硬件通信4.1 read()方法实现return_type MyArmHardware::read(const rclcpp::Time /*time*/, const rclcpp::Duration /*period*/) { // 从串口读取硬件状态 char buffer[256]; int n ::read(serial_fd_, buffer, sizeof(buffer)); if (n 0) { buffer[n] \0; // 解析硬件返回的状态信息 // 假设格式为J1:pos,vel;J2:pos,vel;... std::string data(buffer); std::vectorstd::string joint_data; size_t pos 0; while ((pos data.find(;)) ! std::string::npos) { joint_data.push_back(data.substr(0, pos)); data.erase(0, pos 1); } for (size_t i 0; i joint_data.size() i joint_position_states_.size(); i) { size_t colon_pos joint_data[i].find(:); if (colon_pos ! std::string::npos) { std::string values joint_data[i].substr(colon_pos 1); size_t comma_pos values.find(,); joint_position_states_[i] std::stod(values.substr(0, comma_pos)); joint_velocity_states_[i] std::stod(values.substr(comma_pos 1)); } } } else if (n 0) { RCLCPP_ERROR(rclcpp::get_logger(MyArmHardware), Serial read error: %s, strerror(errno)); return return_type::ERROR; } return return_type::OK; }4.2 write()方法实现return_type MyArmHardware::write(const rclcpp::Time /*time*/, const rclcpp::Duration /*period*/) { // 构造发送给硬件的命令字符串 std::string command CMD; for (size_t i 0; i joint_position_commands_.size(); i) { command std::to_string(joint_position_commands_[i]); } command \n; // 通过串口发送命令 if (::write(serial_fd_, command.c_str(), command.length()) 0) { RCLCPP_ERROR(rclcpp::get_logger(MyArmHardware), Serial write error: %s, strerror(errno)); return return_type::ERROR; } return return_type::OK; }5. 控制器配置与系统集成完成硬件插件开发后需要在URDF中声明硬件接口并在控制器配置文件中定义控制器5.1 URDF中的硬件接口声明ros2_control nameMyArmSystem typesystem hardware pluginmy_arm_hardware/MyArmHardware/plugin /hardware joint namejoint1 command_interface nameposition/ state_interface nameposition/ state_interface namevelocity/ /joint !-- 其他关节类似 -- /ros2_control5.2 控制器配置文件创建config/controllers.yamlcontroller_manager: ros__parameters: update_rate: 500 # Hz joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster joint_trajectory_controller: type: joint_trajectory_controller/JointTrajectoryController joints: - joint1 - joint2 - joint3 - joint4 - joint5 - joint6 command_interfaces: - position state_interfaces: - position - velocity gains: # PID参数根据实际硬件调整 joint1: {p: 100.0, i: 0.01, d: 1.0} joint2: {p: 100.0, i: 0.01, d: 1.0} joint3: {p: 100.0, i: 0.01, d: 1.0} joint4: {p: 100.0, i: 0.01, d: 1.0} joint5: {p: 100.0, i: 0.01, d: 1.0} joint6: {p: 100.0, i: 0.01, d: 1.0}5.3 Launch文件配置创建启动文件加载控制器from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packagecontroller_manager, executableros2_control_node, parameters[{robot_description: }, config/controllers.yaml], outputscreen ), Node( packagecontroller_manager, executablespawner, arguments[joint_state_broadcaster], outputscreen ), Node( packagecontroller_manager, executablespawner, arguments[joint_trajectory_controller], outputscreen ) ])6. 调试技巧与性能优化开发硬件插件时以下几个调试技巧可以节省大量时间使用模拟硬件测试在开发初期可以先实现一个模拟硬件版本验证接口逻辑正确性实时性检查使用rqt_plot监控/joint_states话题确保控制频率稳定延迟测量通过硬件时间戳和ROS时间戳对比测量通信延迟错误恢复实现硬件断连检测和自动重连机制性能优化建议通信协议优化使用二进制协议而非文本协议实现数据压缩如只发送变化量批量读写减少IO次数实时性保障# 设置线程优先级需要root权限 chrt -f 99 ros2 run my_arm_hardware my_arm_hardware_node缓存策略实现命令缓冲减少通信延迟影响使用预测算法平滑运动轨迹7. 与MoveIt的集成测试完成硬件插件开发后可以通过以下步骤测试与MoveIt的集成启动MoveIt配置ros2 launch my_arm_moveit_config demo.launch.py在RViz中添加MotionPlanning显示设置规划组为arm拖动交互式标记设置目标位置点击Plan Execute测试运动通过命令行发送测试指令ros2 action send_goal /joint_trajectory_controller/follow_joint_trajectory \ control_msgs/action/FollowJointTrajectory \ { trajectory: { joint_names: [joint1, joint2, joint3, joint4, joint5, joint6], points: [ {positions: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], time_from_start: {sec: 1}}, {positions: [1.0, 0.5, -0.5, 0.2, 0.3, 0.1], time_from_start: {sec: 3}} ] } }在开发过程中遇到硬件响应延迟或轨迹跟踪误差时可以尝试以下调整降低控制器更新频率调整PID参数检查硬件供电是否充足优化通信协议减少数据传输量

更多文章