ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ROS2智能轮椅自主导航实战:从URDF到Nav2调参避坑指南

ROS2智能轮椅自主导航实战:从URDF到Nav2调参避坑指南

简介:这份资源是面向机器人方向毕业设计、课程设计与期末大作业场景的完整工程包,基于ROS2构建智能轮椅自主导航系统,适合具备一定ROS基础、希望快速搭建可运行导航项目的学生与开发者。压缩包共42个文件,约1.34MB,涵盖urdf机器人模型描述、yaml参数配置、launch启动脚本、rviz可视化配置、world仿真环境、stl与dae网格模型、csv数据及md说明文档等,覆盖建模、建图、定位、路径规划与仿真验证的完整链路。已有80人学习下载。读者可据此获得一套结构清晰的ROS2工程模板,直接复用其模块化目录组织、导航参数与仿真世界配置,在Gazebo中复现轮椅运动与避障过程,并借助持续集成配置理解工程化测试思路,为二次开发与算法调优提供可靠起点。

1. 从一台轮椅的转向延迟说起:ROS2 智能轮椅自主导航到底在做什么

一台普通电动轮椅加上激光雷达和 Jetson,从按下“去护士站”到轮子真正动起来,中间要经过定位、全局规划、局部避障、速度平滑、串口下发五道关。我见过太多团队卡在第三关:RViz2 里路径画得漂漂亮亮,轮子却原地画龙。问题往往不在算法,而在 ROS2 的节点拆分和话题频率没对齐。基于 ROS2 的智能轮椅自主导航系统,本质是把 Nav2 这套导航栈裁剪到轮椅的差速底盘上,用激光雷达或深度相机做 SLAM 建图,再通过 micro-ROS 或串口桥接把速度指令送到下位机。它适合做康复辅具、医院物流、园区接驳的团队,也适合想拿一个完整 ROS2 项目实例练手的人。这一篇不讲空泛概念,只讲怎么把轮椅跑起来、参数怎么设、哪里会翻车。

2. 轮椅底盘与 ROS2 导航栈的对接:从 URDF 到差速控制器

2.1 为什么轮椅不用阿克曼而用差速驱动

轮椅的机械结构决定了它几乎都是两轮差速加前后万向轮。阿克曼转向需要转向机构,轮椅没有;麦克纳姆轮成本高且对地面要求苛刻。差速驱动在 ROS2 里对应diff_drive_controller,它订阅/cmd_vel的geometry_msgs/Twist,输出左右轮角速度。这里第一个坑是轮距和轮径的标定:URDF 里写的wheel_separation如果比实际大 2 厘米,转 90 度就会偏 5 度以上。我一般先用卷尺量轮子中心距,再在 Gazebo 里跑直线验证,最后实车微调。

轮椅的 URDF 要包含base_footprint、base_link、左右驱动轮、前后万向轮、激光雷达安装位。雷达的frame_id必须和 URDF 里的 link 名一致,否则 Nav2 的代价地图会报Timed out waiting for transform。下面是一个最小差速轮椅 URDF 片段,只保留关键关节。

<!-- wheelchair.urdf.xacro 关键片段 --> <link name="base_footprint"/> <link name="base_link"> <visual> <geometry><box size="0.6 0.5 0.3"/></geometry> </visual> </link> <joint name="base_joint" type="fixed"> <parent link="base_footprint"/> <child link="base_link"/> <origin xyz="0 0 0.15"/> </joint> <link name="left_wheel_link"> <visual><geometry><cylinder radius="0.1" length="0.04"/></geometry></visual> </link> <joint name="left_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="left_wheel_link"/> <origin xyz="0 0.25 -0.05" rpy="-1.5708 0 0"/> <axis xyz="0 0 1"/> </joint> <!-- 右轮、万向轮同理,注意 origin 的 y 取负 -->

逻辑说明:base_footprint贴地,base_link抬高 0.15 米作为车体中心,轮子半径 0.1 米、轮距 0.5 米。参数说明:origin xyz的 y 值决定轮距,左右轮必须对称;axis对连续关节要写旋转轴,差速轮绕 z 轴转。改完 URDF 后跑ros2 run robot_state_publisher robot_state_publisher加joint_state_publisher_gui,在 RViz2 里看模型是否散架。

2.2 用 ros2_control 配置差速控制器

ROS2 Humble 里推荐用ros2_control管理硬件接口。轮椅下位机如果是 STM32 或 ESP32,常见做法是写一个hardware_interface::SystemInterface插件,通过串口读写编码器和 PWM。如果只是快速验证,可以用diff_drive_controller加一个假的system插件先跑仿真。配置文件wheelchair_controllers.yaml如下。

controller_manager: ros__parameters: update_rate: 50 # Hz,轮椅建议 50,太低会抖 diff_drive_controller: type: diff_drive_controller/DiffDriveController joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster diff_drive_controller: ros__parameters: left_wheel_names: ["left_wheel_joint"] right_wheel_names: ["right_wheel_joint"] wheel_separation: 0.50 wheel_radius: 0.10 publish_rate: 50.0 base_frame_id: base_footprint odom_frame_id: odom enable_odom_tf: true cmd_vel_timeout: 0.5 # 超时停车,安全关键 linear.x.max_velocity: 0.8 linear.x.min_velocity: -0.3 angular.z.max_velocity: 1.0

逻辑说明:update_rate是控制器管理器的循环频率,轮椅惯性大,50 Hz 足够;cmd_vel_timeout设 0.5 秒,一旦导航节点卡死,轮子会在半秒内停,这是轮椅必须有的后悔药。参数说明:wheel_separation和wheel_radius必须和 URDF 一致,否则里程计会漂;linear.x.max_velocity限制到 0.8 m/s,医院场景建议 0.5 以下。启动顺序是先ros2 launch控制器,再ros2 control load_controller,最后ros2 control switch_controllers激活。

2.3 串口桥接下位机:micro-ROS 还是自写节点

热词里常出现 micro-ROS agent 和串口桥接 ESP32 小车,轮椅也一样。两条路:一是下位机跑 micro-ROS,直接发布/odom和订阅/cmd_vel,上位机只跑 agent;二是下位机只发原始编码器计数,上位机用 Python 节点算里程计。前者实时性好但烧录麻烦,后者灵活但占 CPU。我一般用后者做原型,因为改协议不用重新烧固件。下面是一个最小串口桥接节点,读编码器、发 odom、收 cmd_vel。

import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist from nav_msgs.msg import Odometry import serial, struct, math class WheelchairBridge(Node): def __init__(self): super().__init__('wheelchair_bridge') self.ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.01) self.pub = self.create_publisher(Odometry, '/odom', 10) self.sub = self.create_subscription(Twist, '/cmd_vel', self.cmd_cb, 10) self.timer = self.create_timer(0.02, self.read_serial) # 50Hz self.x = self.y = self.th = 0.0 self.last_time = self.get_clock().now() def cmd_cb(self, msg): # 协议:0xAA 左轮速度 右轮速度 0x55,单位 mm/s left = int(msg.linear.x * 1000 - msg.angular.z * 250) right = int(msg.linear.x * 1000 + msg.angular.z * 250) self.ser.write(struct.pack('<BhhB', 0xAA, left, right, 0x55)) def read_serial(self): if self.ser.in_waiting >= 10: data = self.ser.read(10) if data[0] == 0xBB: left_ticks, right_ticks = struct.unpack('<hh', data[1:5]) # 省略积分计算,按轮径和轮距更新 x,y,th odom = Odometry() odom.header.stamp = self.get_clock().now().to_msg() odom.header.frame_id = 'odom' odom.child_frame_id = 'base_footprint' odom.pose.pose.position.x = self.x odom.pose.pose.position.y = self.y self.pub.publish(odom) def main(): rclpy.init() rclpy.spin(WheelchairBridge()) rclpy.shutdown()

逻辑说明:cmd_cb把线速度和角速度解算成左右轮线速度,再乘 1000 转成毫米每秒下发;read_serial每 20 毫秒读一次编码器,积分出位姿。参数说明:/dev/ttyUSB0要换成实际设备,权限用sudo usermod -aG dialout $USER解决;0xAA和0x55是帧头帧尾,防止半包错位。这个节点跑起来后,ros2 topic hz /odom应该稳定在 50 左右,ros2 topic echo /cmd_vel能看到导航发的指令。

3. 建图与定位:用 slam_toolbox 和 AMCL 把轮椅钉在地图上

3.1 激光雷达选型与 slam_toolbox 参数

轮椅建图常用 2D 单线雷达,比如 RPLIDAR A1 或思岚 S2,价格低、驱动成熟。ROS2 Humble 里slam_toolbox是标配,支持在线异步建图。启动文件里要配base_frame、odom_frame、map_frame,以及雷达话题。关键参数是resolution和max_laser_range。医院走廊窄,分辨率设 0.03 米,最大距离设 12 米,太远会引入玻璃反射噪声。

slam_toolbox: ros__parameters: solver_plugin: solver_plugins::CeresSolver ceres_linear_solver: SPARSE_NORMAL_CHOLESKY resolution: 0.03 max_laser_range: 12.0 map_update_interval: 2.0 minimum_time_interval: 0.2 transform_timeout: 0.2 use_scan_matching: true use_scan_barycenter: true mode: mapping # 建图模式,定位时改 localization

逻辑说明:use_scan_matching打开后,雷达帧间匹配能减少里程计漂移;map_update_interval设 2 秒,避免地图频繁刷新导致 RViz2 卡顿。参数说明:resolution越小地图越精细但内存越大,0.03 米在 100 米走廊约 30 MB,Jetson 能扛;mode切到localization时配合 AMCL 使用。建图时手动推轮椅走一圈,速度别超过 0.5 m/s,否则匹配容易丢。

3.2 AMCL 定位参数怎么调才不飘

建好图后切到定位模式,用nav2_amcl。AMCL 是粒子滤波,轮椅低速且激光特征明显,粒子数不用太多。min_particles设 500,max_particles设 2000,laser_model_type用likelihood_field。最关键的参数是update_min_d和update_min_a:走 0.1 米或转 0.2 弧度才更新一次,太频繁会抖,太稀疏会丢。

amcl: ros__parameters: alpha1: 0.2 alpha2: 0.2 alpha3: 0.2 alpha4: 0.2 alpha5: 0.2 base_frame_id: base_footprint beam_skip_distance: 0.5 laser_model_type: likelihood_field max_particles: 2000 min_particles: 500 update_min_d: 0.1 update_min_a: 0.2 set_initial_pose: true initial_pose: x: 0.0 y: 0.0 z: 0.0 yaw: 0.0

逻辑说明:alpha1到alpha5是里程计噪声模型,轮椅差速底盘打滑少,设 0.2 偏保守;set_initial_pose让 AMCL 启动时直接用配置的初始位姿,省去 RViz2 里点 2D Pose Estimate。参数说明:beam_skip_distance跳过 0.5 米内的无效激光点,避免轮椅扶手遮挡;update_min_a设 0.2 弧度,约 11 度,转弯时不会频繁重采样。如果定位飘,先看/amcl_pose的协方差,再检查雷达安装是否水平。

3.3 八叉树地图与代价地图的取舍

热词里提到八叉树地图导航,那是 3D 场景用的。轮椅在平面跑,2D 代价地图足够,但如果有低矮障碍(比如输液架底座),可以用spatio_temporal_voxel_layer做 3D 体素层。不过 Jetson 上跑 3D 体素很吃 CPU,我一般先用 2D 激光加超声波,成本低还稳。代价地图的inflation_radius设 0.35 米,轮椅宽 0.5 米,留 0.1 米余量。cost_scaling_factor设 3.0,让路径远离墙壁。

local_costmap: ros__parameters: update_frequency: 5.0 publish_frequency: 2.0 global_frame: odom robot_base_frame: base_footprint rolling_window: true width: 3 height: 3 resolution: 0.05 plugins: ["obstacle_layer", "inflation_layer"] obstacle_layer: plugin: "nav2_costmap_2d::ObstacleLayer" observation_sources: scan scan: topic: /scan max_obstacle_height: 2.0 clearing: true marking: true inflation_layer: plugin: "nav2_costmap_2d::InflationLayer" inflation_radius: 0.35 cost_scaling_factor: 3.0

逻辑说明:局部代价地图用rolling_window跟随轮椅,3 米见方够用;obstacle_layer订阅/scan,clearing和marking都开,动态障碍才能被清除。参数说明:update_frequency5 Hz 对轮椅足够,再高浪费 CPU;inflation_radius必须大于轮椅半宽,否则会蹭墙。全局代价地图类似,但global_frame用map,rolling_window关。

4. Nav2 导航调参:让轮椅在走廊里不画龙

4.1 全局规划器选 NavFn 还是 Smac

Nav2 默认全局规划器是NavFn,基于 Dijkstra,路径平滑但绕障不够灵活。Smac Planner支持 Hybrid-A*,考虑轮椅运动学,路径更贴合差速约束。轮椅转弯半径小,用 Smac 的SmacPlanner2D就够,不用上SmacPlannerHybrid。配置里tolerance设 0.1 米,max_iterations设 1000000,cost_penalty设 2.0。

planner_server: ros__parameters: expected_planner_frequency: 1.0 planner_plugins: ["GridBased"] GridBased: plugin: "nav2_smac_planner/SmacPlanner2D" tolerance: 0.1 downsample_costmap: false allow_unknown: true max_iterations: 1000000 max_on_approach_iterations: 1000 cost_penalty: 2.0

逻辑说明:expected_planner_frequency1 Hz,轮椅不需要高频重规划;allow_unknown允许走未知区域,建图不全时有用。参数说明:tolerance是目标点容差,0.1 米对轮椅合适;cost_penalty越大路径越远离障碍,但太大可能找不到路。如果规划失败,先看/plan话题有没有输出,再查代价地图里目标点是否被膨胀层覆盖。

4.2 局部控制器 DWB 的参数血泪经验

DWB 是 Nav2 默认局部控制器,参数多到让人头大。轮椅最关键是max_vel_x、max_vel_theta、acc_lim_x、decel_lim_x。医院场景max_vel_x设 0.5,max_vel_theta设 0.8,acc_lim_x设 0.5,decel_lim_x设 -0.5。vx_samples设 20,vtheta_samples设 40,太多会卡。sim_time设 1.5 秒,太短来不及避障,太长会犹豫。

controller_server: ros__parameters: controller_frequency: 10.0 min_x_velocity_threshold: 0.001 progress_checker_plugin: "progress_checker" goal_checker_plugins: ["general_goal_checker"] controller_plugins: ["FollowPath"] progress_checker: plugin: "nav2_controller::SimpleProgressChecker" required_movement_radius: 0.3 movement_time_allowance: 10.0 general_goal_checker: stateful: True plugin: "nav2_controller::SimpleGoalChecker" xy_goal_tolerance: 0.15 yaw_goal_tolerance: 0.2 FollowPath: plugin: "dwb_core::DWBLocalPlanner" debug_trajectory_details: True min_vel_x: -0.2 max_vel_x: 0.5 max_vel_theta: 0.8 acc_lim_x: 0.5 decel_lim_x: -0.5 vx_samples: 20 vtheta_samples: 40 sim_time: 1.5 xy_goal_tolerance: 0.15 trans_stopped_velocity: 0.05

逻辑说明:progress_checker监控 10 秒内是否移动 0.3 米,卡住就报错;goal_checker容差 0.15 米和 0.2 弧度,轮椅停准。参数说明:min_vel_x设负值允许倒车,但轮椅倒车危险,建议设 0;trans_stopped_velocity0.05 米每秒,低于此值算停。DWB 调参先调sim_time和采样数,再调加速度限制。如果轮椅画龙,多半是max_vel_theta太大或vtheta_samples太少。

4.3 行为树与恢复行为:卡住时轮椅该干什么

Nav2 用行为树编排导航流程,默认navigate_to_pose_w_replanning_and_recovery.xml。轮椅卡住时,恢复行为依次是清代价地图、旋转、后退。旋转对轮椅可行,后退要小心。我一般把Spin的spin_dist设 1.57 弧度,BackUp的backup_dist设 0.3 米,backup_speed设 0.1。如果轮椅在窄走廊,旋转会撞墙,可以改成先Wait再ClearCostmap。

<RecoveryNode number_of_retries="6" name="NavigateRecovery"> <SequenceStar name="RecoveryActions"> <ClearEntireCostmap name="ClearLocalCostmap" service_name="local_costmap/clear_entirely_local_costmap"/> <Spin name="Spin" spin_dist="1.57"/> <Wait name="Wait" wait_duration="5.0"/> <BackUp name="BackUp" backup_dist="0.3" backup_speed="0.1"/> </SequenceStar> </RecoveryNode>

逻辑说明:number_of_retries6 次,每次失败后依次执行恢复;ClearEntireCostmap清局部代价地图,把动态障碍清掉。参数说明:spin_dist1.57 弧度约 90 度,够重新观察;wait_duration5 秒给行人通过时间。行为树改完用ros2 run nav2_bt_navigator bt_navigator加载,在 RViz2 里看/behavior_tree_log。

5. 避坑与排查:轮椅导航翻车现场记录

5.1 现象:RViz2 里路径正常,轮子不动

原因:/cmd_vel话题类型不匹配,或者diff_drive_controller没激活。Nav2 发的是geometry_msgs/Twist,如果下位机桥接节点订阅的是TwistStamped,就收不到。解决:ros2 topic info /cmd_vel看类型,ros2 control list_controllers看控制器状态,用ros2 control switch_controllers激活。

5.2 现象:轮椅走直线往一边偏

原因:轮径或轮距标定不准,或者左右轮摩擦力不一致。解决:先量实际轮距,改 URDF 和控制器 yaml;再在平整地面跑 5 米直线,用卷尺量偏差,按比例微调wheel_separation。如果还偏,检查轮胎气压和电机编码器线序。

5.3 现象:AMCL 定位突然跳到地图外

原因:激光雷达被遮挡,或者update_min_d太大导致粒子跟不上。解决:检查雷达前方是否有轮椅扶手或背包;把update_min_d降到 0.05,update_min_a降到 0.1;在 RViz2 里看/particle_cloud,粒子散开就手动给初始位姿。

5.4 现象:DWB 报 “No valid trajectories”

原因:max_vel_x或max_vel_theta设太大,采样轨迹都撞障碍;或者sim_time太长,轨迹末端超出代价地图。解决:降max_vel_x到 0.3,max_vel_theta到 0.5,sim_time到 1.0;检查局部代价地图width和height是否够大,至少 3 米。

5.5 现象:串口桥接节点 CPU 占用 100%

原因:read_serial定时器频率太高,或者serial.read阻塞。解决:定时器设 50 Hz 足够,serial.Serial加timeout=0.01避免阻塞;用ros2 topic hz /odom确认频率,如果超过 100 就降。另外 Python 节点用rclpy的MultiThreadedExecutor会好一些。

6. 进阶技巧:用零拷贝和生命周期节点把延迟压到 20 毫秒

轮椅导航对延迟敏感,尤其是避障。ROS2 Humble 支持零拷贝(loaned messages),但需要发布者和订阅者在同一进程。实际部署时,把雷达驱动、SLAM、Nav2 放在同一 composable 容器里,用ros2 component load加载,能省掉序列化开销。我实测把/scan从独立进程改成组件后,AMCL 更新延迟从 35 毫秒降到 18 毫秒。

# 启动 composable 容器 ros2 run rclcpp_components component_container # 加载雷达驱动组件 ros2 component load /ComponentManager rplidar_ros RplidarNode # 加载 slam_toolbox 组件 ros2 component load /ComponentManager slam_toolbox SlamToolbox

逻辑说明:component_container是一个进程,所有组件共享内存;component load把节点动态加载进去。参数说明:组件名要查ros2 component types,不同包不一样。零拷贝需要消息类型支持unique_ptr,sensor_msgs/LaserScan在 Humble 里已支持。

另一个技巧是用生命周期节点管理导航栈。Nav2 的节点都是生命周期节点,启动时按configure、activate顺序来。写一个 Python 脚本按顺序激活,避免手动敲命令。

from lifecycle_msgs.srv import ChangeState import rclpy def change_state(node, node_name, transition): client = node.create_client(ChangeState, f'/{node_name}/change_state') req = ChangeState.Request() req.transition.id = transition # 1=configure, 3=activate client.call_async(req) rclpy.spin_until_future_complete(node, client)

逻辑说明:transition.id1 是 configure,3 是 activate,2 是 cleanup,4 是 deactivate。参数说明:节点名要和启动文件里一致,比如controller_server、planner_server。按map_server、amcl、controller_server、planner_server、bt_navigator顺序激活,能避免 TF 树没建好就规划。

最后说一个验证方法:在轮椅前方 1 米放一个纸箱,发一个目标点让轮椅绕过去。用ros2 topic echo /local_costmap/costmap看障碍是否被标记,用ros2 topic hz /cmd_vel看指令频率。如果轮椅犹豫,调小sim_time;如果撞上去,调大inflation_radius。我习惯每次改完参数先跑 10 次,记录成功率和平均耗时,再决定是否上车。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进