距离第一次写 ROS1 的笔记已经过去很久最近因为项目需要把整套机器人底盘控制链路从 ROS1 迁移到 ROS2正好把这一周踩过的坑、梳理过的思路完整整理出来。如果你正准备入门 ROS2或者想在仿真环境里跑通“底盘里程计 激光雷达”这套机器人基本功这篇文章会比较适合你。我不打算堆概念而是按一天一天的实战节奏把“远程开发环境 → 底盘里程计驱动 → 激光雷达仿真”这条链路拆开讲清楚每个环节都有可复制的代码和配置。本文不是 ROS2 的完整手册更像是一份“能跟着做的训练营笔记”。所有示例基于 Ubuntu 22.04 与 ROS2 Humble 版本功能包以 Python 和 C 混合编写为主包含工作空间组织、节点通信、里程计发布、Gazebo 仿真和 RViz2 可视化等核心内容。最终目标是让你掌握一套可用于机器人硬件控制系统的闭环实践方法环境能搭起来、底盘能算出里程、雷达能在仿真里出数据、RViz2 里能看到机器人模型正确运动。1. 为什么 ROS2 是当前机器人开发绕不开的底座1.1 ROS2 要解决什么问题先补一点背景。ROSRobot Operating System本质上不是操作系统而是一套分布式通信中间件它帮我们把传感器驱动、导航定位、运动控制、可视化这些模块拆成独立节点节点之间通过话题Topic、服务Service、动作Action进行通信。这样做的最大好处是写底盘驱动的人不需要关心导航模块怎么调用它只要把消息发到约定好的话题上整个系统就能协同工作。ROS1 时代很多新人都从 Melodic、Noetic 开始学但 ROS1 有一个明显短板master 节点一旦挂了整个系统就会瘫痪而且在不同机器之间通信非常麻烦。ROS2 则基于 DDSData Distribution Service通信协议去掉了中心节点每个节点自带发现机制天然支持分布式、多机协同也更适合真实机器人产品落地。对于大多数开发者来说选择 ROS2 已经不是“要不要追新”的问题而是当前官方主推、社区生态逐渐迁移、招聘要求也在全面覆盖的现实选择。与其学完 ROS1 再迁移不如直接以 ROS2 为起点。1.2 机器人硬件控制系统的典型层次如果拆开一台差速底盘的移动机器人从软件到硬件大概分为这样几层应用层导航、建图、路径规划、视觉识别 ↓ ROS2 中间层节点通信、TF 坐标变换、参数服务 ↓ 驱动层里程计发布、速度指令订阅、激光雷达/IMU 数据接入 ↓ 硬件层主控板STM32/ESP32/Jetson、电机驱动器、编码器、激光雷达本文训练营重点落在“驱动层”与“中间层”也就是底盘的里程计驱动、速度指令控制、激光雷达数据接入以及如何在仿真里验证整条链路。导航、规划、建图是再往上层走的内容但如果没有正确的里程计和雷达数据上层算法基本是空中楼阁。1.3 一周训练营的整体路线我按一周的节奏把内容拆分如下天次核心任务产出Day 1搭建 ROS2 远程开发环境Ubuntu ROS2 Humble VS Code 远程开发Day 2创建工作空间与功能包自定义 topic/service 通信Day 3编写底盘里程计节点发布 Odometry TF 变换Day 4仿真环境接入差速模型URDF 模型 Gazebo 加载Day 5配置激光雷达插件发布 LaserScan 数据并在 RViz2 中显示Day 6串口控制真实底盘可选速度指令下发与闭环调试Day 7整体联调与复盘完整跑通硬件控制系统基础流程这个安排并不要求每天都从早写到晚关键是每个阶段都有明确的验证结果。接下来我会按这条路线展开每个环节都给出配置和核心代码。2. 环境准备与远程开发方案2.1 操作系统与 ROS2 版本选择目前在 PC 端最稳妥的组合是 Ubuntu 22.04 LTS ROS2 Humble Hawksbill。Humble 是长期支持版本生命周期比较久教程也多遇到问题容易找到解决方案。如果你的机器是 Ubuntu 24.04可以选择 ROS2 Jazzy但部分第三方包和仿真插件可能还在适配过程中所以对新手而言我建议 Humble 优先。如果你只有 Windows 电脑常见方案有两种安装 VMware/VirtualBox 虚拟机运行 Ubuntu 22.04。使用 WSL2Windows Subsystem for Linux 2并在里面安装 Ubuntu 22.04 ROS2。WSL2 的好处是资源占用比虚拟机低启动快配合 VS Code Remote-WSL 非常顺手。不过如果你要跑 Gazebo 仿真WSL2 的图形界面支持虽然已经进步很多但偶尔还是会有 OpenGL 渲染问题所以仿真为主的话我更推荐虚拟机或者双系统。2.2 安装 ROS2 Humble安装 ROS2 Humble 非常简单主要分四步设置软件源、添加密钥、更新软件源、安装桌面版。这里贴出最简洁的步骤# 1. 设置编码 sudo apt update sudo apt install -y locales sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALLen_US.UTF-8 LANGen_US.UTF-8 export LANGen_US.UTF-8 # 2. 添加 ROS2 软件源 sudo apt update sudo apt install -y software-properties-common curl sudo add-apt-repository universe sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release echo $UBUNTU_CODENAME) main | sudo tee /etc/apt/sources.list.d/ros2.list /dev/null # 3. 安装桌面版 ROS2 Humble sudo apt update sudo apt install -y ros-humble-desktop # 4. 配置环境变量 echo source /opt/ros/humble/setup.bash ~/.bashrc source ~/.bashrc安装完成后检查版本ros2 --version预期输出类似ros2 2.x.x。如果这个命令能正常输出说明 ROS2 核心环境已经完成。2.3 远程开发环境搭建SSH VS Code Remote我这一周大部分时间是通过 SSH 连接到一台 Ubuntu 服务器上开发的笔记本上只用 VS Code 作为 IDE。这种远程开发模式非常值得推广尤其是后续要在 Jetson 这类嵌入式平台写代码时你不可能抱着显示器去调试。宿主机需要安装 SSH 服务sudo apt install -y openssh-server sudo systemctl enable ssh sudo systemctl restart ssh本地 Windows/Mac 上安装 VS Code然后安装扩展“Remote - SSH”。安装后通过命令面板连接Ctrl Shift P → Remote-SSH: Connect to Host也可以直接在终端里用 SSH 登录验证ssh 用户名远程IP在远程服务器上建议创建专门的开发目录比如~/ros2_ws后续所有工作空间、功能包都在这个目录下组织。远程开发还有一个好处代码在服务器上运行不占用本地资源Gazebo 和 RViz2 这类图形程序也可以通过 X11 转发或 VNC 方式显示到本地具体方式可以在踩坑阶段根据自己的网络环境调整。2.4 创建工作空间与功能包ROS2 的工作空间一般用src、build、install、log四个目录组成。src放功能包源码build是编译中间产物install是安装后的可运行文件log是日志。手动创建也没问题但更推荐用colcon工具来初始化。mkdir -p ~/ros2_ws/src cd ~/ros2_ws colcon build功能包的创建则根据语言不同分为两种# Python 功能包 cd ~/ros2_ws/src ros2 pkg create robot_base_pkg --build-type ament_python --dependencies rclpy std_msgs geometry_msgs nav_msgs tf2_ros # C 功能包 ros2 pkg create robot_base_cpp --build-type ament_cmake --dependencies rclcpp geometry_msgs nav_msgs tf2_ros创建功能包时最好把会用到的依赖一次性加进去避免后续频繁修改package.xml和CMakeLists.txt。建议的目录结构如下~/ros2_ws/ ├── src/ │ ├── robot_base_pkg/ │ │ ├── robot_base_pkg/ │ │ │ ├── __init__.py │ │ │ └── odometry_node.py │ │ ├── launch/ │ │ ├── urdf/ │ │ ├── config/ │ │ ├── package.xml │ │ └── setup.py │ └── robot_base_cpp/ └── install/关于“功能包组织”这件事多说一句ROS2 中一个功能包应该只负责一个相对独立的功能底盘驱动、激光雷达驱动、导航配置最好分开放。不要把所有代码都堆在一个包里否则后续调试和复用会非常痛苦。3. 底盘里程计驱动的核心原理与实现3.1 差速底盘的运动学模型进入 Day 3 之前先明确一个问题底盘里程计到底在算什么对于常见的两轮差速底盘底盘上有左右两个驱动轮外加若干万向轮支撑。我们想知道的量是机器人本体坐标系下的线速度 v机器人本体坐标系下的角速度 ω已知量是左右轮的实际速度 v_L、v_R以及两个驱动轮之间的轮距 d左右轮中心距。差速模型公式v (v_L v_R) / 2 ω (v_R - v_L) / d如果前进方向为 X 轴左转为正方向那么机器人在地图坐标系中的位姿更新可以表示为x x v * cos(θ) * Δt y y v * sin(θ) * Δt θ θ ω * Δt其中 θ 是机器人的航向角。这个计算过程就叫航迹推演Odometry它并不完美会因为轮子打滑、编码器噪声等因素累积误差但在短距离、室内场景中已经足够支撑建图和导航使用。3.2 里程计消息与 TF 变换在 ROS2 中里程计数据通过nav_msgs/msg/Odometry消息发布其中包含pose.pose.position机器人在地图中的位置pose.pose.orientation机器人姿态四元数twist.twist.linear线速度twist.twist.angular角速度同时还要发布坐标变换TF告诉系统odom坐标系和base_link坐标系之间的关系。导航栈和其他算法通过 TF 来理解机器人各个部件之间的相对位姿缺少 TF 会导致 RViz2 里模型位置异常或导航无法启动。代码里发布 TF 用的是tf2_ros.TransformBroadcaster发布的消息类型是geometry_msgs/msg/TransformStamped。3.3 里程计节点 Python 实现下面的示例模拟一个差速底盘通过读取左右轮速度计算里程计数据并发布。这个代码可以直接在仿真环境或接入真实编码器后运行。# 文件路径~/ros2_ws/src/robot_base_pkg/robot_base_pkg/odometry_node.py import math import rclpy from rclpy.node import Node from nav_msgs.msg import Odometry from geometry_msgs.msg import Point, Pose, Quaternion, Twist, Vector3, TransformStamped from tf2_ros import TransformBroadcaster class DiffDriveOdometryNode(Node): def __init__(self): super().__init__(diff_drive_odometry_node) # 机器人参数 self.wheel_base 0.4 # 轮距单位 m self.wheel_radius 0.1 # 轮子半径单位 m # 初始位姿 self.x 0.0 self.y 0.0 self.theta 0.0 # 订阅左轮和右轮的速度话题 self.left_wheel_sub self.create_subscription( Float64, /left_wheel_speed, self.left_wheel_callback, 10) self.right_wheel_sub self.create_subscription( Float64, /right_wheel_speed, self.right_wheel_callback, 10) # 发布里程计和 TF self.odom_pub self.create_publisher(Odometry, /odom, 10) self.tf_broadcaster TransformBroadcaster(self) # 控制频率 20Hz self.timer self.create_timer(0.05, self.update_odometry) self.left_speed 0.0 self.right_speed 0.0 def left_wheel_callback(self, msg): self.left_speed msg.data def right_wheel_callback(self, msg): self.right_speed msg.data def update_odometry(self): # 线速度与角速度 v (self.left_speed self.right_speed) / 2.0 omega (self.right_speed - self.left_speed) / self.wheel_base dt 0.05 # 与定时器周期保持一致 # 位姿更新 delta_x v * math.cos(self.theta) * dt delta_y v * math.sin(self.theta) * dt delta_theta omega * dt self.x delta_x self.y delta_y self.theta delta_theta # 四元数表示姿态 qx 0.0 qy 0.0 qz math.sin(self.theta / 2.0) qw math.cos(self.theta / 2.0) # 发布里程计消息 odom Odometry() odom.header.stamp self.get_clock().now().to_msg() odom.header.frame_id odom odom.child_frame_id base_link odom.pose.pose Pose( positionPoint(xself.x, yself.y, z0.0), orientationQuaternion(xqx, yqy, zqz, wqw) ) odom.twist.twist Twist( linearVector3(xv, y0.0, z0.0), angularVector3(x0.0, y0.0, zomega) ) self.odom_pub.publish(odom) # 发布 TFodom - base_link tf_msg TransformStamped() tf_msg.header.stamp self.get_clock().now().to_msg() tf_msg.header.frame_id odom tf_msg.child_frame_id base_link tf_msg.transform.translation.x self.x tf_msg.transform.translation.y self.y tf_msg.transform.translation.z 0.0 tf_msg.transform.rotation.x qx tf_msg.transform.rotation.y qy tf_msg.transform.rotation.z qz tf_msg.transform.rotation.w qw self.tf_broadcaster.sendTransform(tf_msg) self.get_logger().info( f更新里程计: x{self.x:.3f}, y{self.y:.3f}, theta{self.theta:.3f} ) def main(argsNone): rclpy.init(argsargs) node DiffDriveOdometryNode() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ __main__: main()这个节点里有两个细节很关键一是create_timer的周期和delta_t要一致否则里程计会越算越偏。如果你希望更精确可以在回调里用self.get_clock().now()计算真实时间差而不是写死0.05。二是 TF 的frame_id和child_frame_id必须与 URDF 模型中的坐标系命名保持一致。很多新手在 RViz2 里看不到模型或警告“No transform from [odom] to [base_link]”基本就是这两个名字对不上。为了让代码可以编译安装记得在setup.py中把入口点配置好# 文件路径~/ros2_ws/src/robot_base_pkg/setup.py entry_points{ console_scripts: [ odometry_node robot_base_pkg.odometry_node:main, ], },然后在工作空间编译cd ~/ros2_ws colcon build --symlink-install source install/setup.bash ros2 run robot_base_pkg odometry_node如果此时我们手动发布左右轮速度ros2 topic pub /left_wheel_speed std_msgs/msg/Float64 {data: 0.5} -r 10 ros2 topic pub /right_wheel_speed std_msgs/msg/Float64 {data: 0.5} -r 10那么底盘会保持直线前进里程计数值会持续增加。用下面的命令即可验证ros2 topic echo /odom这是一个非常直观的自我验证方式不需要真实机器人就能看到里程计是否正确。4. 激光雷达仿真与机器人模型描述4.1 用 URDF 描述机器人结构Gazebo 仿真相对于真实硬件的好处非常明显可以在没有实体机器人时验证驱动逻辑、调试参数避免直接操作电机带来的风险。而要在 Gazebo 中运行机器人首先得用 URDFUnified Robot Description Format描述机器人的外形、关节、传感器。这里给出一个简化的差速机器人 URDF包含底盘、左右轮和激光雷达传感器。!-- 文件路径~/ros2_ws/src/robot_base_pkg/urdf/robot_base.urdf -- ?xml version1.0? robot namerobot_base !-- 底盘 -- link namebase_link visual geometry box size0.4 0.3 0.12/ /geometry material nameblue/ /visual collision geometry box size0.4 0.3 0.12/ /geometry /collision inertial mass value2.0/ inertia ixx0.02 ixy0.0 ixz0.0 iyy0.02 iyz0.0 izz0.03/ /inertial /link !-- 左轮 -- link nameleft_wheel visual geometry cylinder radius0.1 length0.03/ /geometry material namegray/ /visual collision geometry cylinder radius0.1 length0.03/ /geometry /collision inertial mass value0.2/ inertia ixx0.0002 ixy0.0 ixz0.0 iyy0.0001 iyz0.0 izz0.0002/ /inertial /link !-- 右轮 -- link nameright_wheel visual geometry cylinder radius0.1 length0.03/ /geometry material namegray/ /visual collision geometry cylinder radius0.1 length0.03/ /geometry /collision inertial mass value0.2/ inertia ixx0.0002 ixy0.0 ixz0.0 iyy0.0001 iyz0.0 izz0.0002/ /inertial /link !-- 雷达 -- link namelaser_link visual geometry cylinder radius0.03 length0.05/ /geometry material namered/ /visual collision geometry cylinder radius0.03 length0.05/ /geometry /collision /link !-- 左轮关节 -- joint nameleft_wheel_joint typecontinuous parent linkbase_link/ child linkleft_wheel/ origin xyz0.0 0.2 -0.06 rpy-1.5708 0.0 0.0/ axis xyz0 1 0/ /joint !-- 右轮关节 -- joint nameright_wheel_joint typecontinuous parent linkbase_link/ child linkright_wheel/ origin xyz0.0 -0.2 -0.06 rpy-1.5708 0.0 0.0/ axis xyz0 1 0/ /joint !-- 雷达支架 -- joint namelaser_joint typefixed parent linkbase_link/ child linklaser_link/ origin xyz0.15 0.0 0.06 rpy0.0 0.0 0.0/ /joint material nameblue color rgba0.0 0.2 0.8 1.0/ /material material namegray color rgba0.5 0.5 0.5 1.0/ /material material namered color rgba0.8 0.1 0.1 1.0/ /material /robot再配合robot_state_publisher功能包ROS2 就能自动发布机器人的静态坐标变换比如base_link到laser_link之间的 TF。4.2 在 Gazebo 中加载模型并发布雷达数据URDF 只描述机器人的几何外观和关节信息要让 Gazebo 真正模拟物理碰撞、轮子转动还需要在 URDF 中加入 Gazebo 插件。对于激光雷达我们通常使用gazebo_ros_ray_sensor插件或者更现代的gazebo_ros2_control搭配传感器插件。下面以最常见的差速驱动机器人为例在 URDF 中追加 Gazebo 控制插件和雷达插件。轮式底盘在 Gazebo 中要使用差速控制器插件这样我们向/cmd_vel话题发布速度指令时Gazebo 能自动计算左右轮速度。!-- 追加在 URDF 中 -- gazebo plugin namegazebo_ros_diff_drive filenamelibgazebo_ros_diff_drive.so ros namespace//namespace remappingcmd_vel:/cmd_vel/remapping remappingodom:/odom/remapping /ros left_jointleft_wheel_joint/left_joint right_jointright_wheel_joint/right_joint wheel_separation0.4/wheel_separation wheel_diameter0.2/wheel_diameter max_wheel_torque20.0/max_wheel_torque max_wheel_acceleration1.0/max_wheel_acceleration publish_odomtrue/publish_odom update_rate20/update_rate /plugin /gazebo gazebo referencelaser_link sensor namelaser_sensor typeray pose0 0 0 0 0 0/pose visualizetrue/visualize update_rate10/update_rate ray scan horizontal samples360/samples resolution1/resolution min_angle-3.14159/min_angle max_angle3.14159/max_angle /horizontal /scan range min0.10/min max20.0/max resolution0.01/resolution /range /ray plugin namelaser_scan filenamelibgazebo_ros_ray_sensor.so ros namespace//namespace remappingscan:/scan/remapping /ros output_typesensor_msgs/LaserScan/output_type /plugin /sensor /gazebo配置里最需要注意的字段是wheel_separation和wheel_diameter它们必须与 URDF 中的实际几何尺寸一致否则 Gazebo 发布的里程计会和自定义运动学模型产生冲突。4.3 启动仿真并在 RViz2 中可视化为了方便启动我们写一个 launch 文件一次性启动 robot_state_publisher、Gazebo 和 RViz2。# 文件路径~/ros2_ws/src/robot_base_pkg/launch/simulation.launch.py import os from launch import LaunchDescription from launch.actions import ExecuteProcess from launch_ros.actions import Node from ament_index_python.packages import get_package_share_directory def generate_launch_description(): pkg_share get_package_share_directory(robot_base_pkg) urdf_path os.path.join(pkg_share, urdf, robot_base.urdf) return LaunchDescription([ # 发布机器人模型和 TF Node( packagerobot_state_publisher, executablerobot_state_publisher, outputscreen, parameters[{robot_description: open(urdf_path).read()}] ), # 启动 Gazebo ExecuteProcess( cmd[gazebo, --verbose, -s, libgazebo_ros_factory.so], outputscreen ), # 在 Gazebo 中生成机器人模型 Node( packagegazebo_ros, executablespawn_entity.py, arguments[-entity, robot_base, -file, urdf_path], outputscreen ), # 启动 RViz2 Node( packagerviz2, executablerviz2, outputscreen ), ])运行cd ~/ros2_ws colcon build --symlink-install source install/setup.bash ros2 launch robot_base_pkg simulation.launch.py启动后向底盘的/cmd_vel话题发布一个速度指令ros2 topic pub /cmd_vel geometry_msgs/msg/Twist {linear: {x: 0.3, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.3}} -r 10此时你应该能在 Gazebo 里看到机器人转动在 RViz2 中添加LaserScan显示插件后能看到雷达扫描出的环境轮廓。同时执行ros2 topic echo /odom能看到里程计的数据在变化。这整条链路验证通过就意味着机器人硬件控制系统的基础数据流已经打通。5. 从仿真到真实硬件串口与下位机联动很多新手学 ROS2 死活学不到“硬件控制”这一层总觉得机器人跑起来靠的是 Gazebo 里的物理引擎。真实底盘跟仿真最大的区别在于你要通过串口或 CAN 把速度指令发给电机驱动板然后从电机编码器读回真实转速再喂给 ROS2 里程计节点。这个过程涉及协议设计和数据校验。5.1 通信协议设计示例假设我们的底盘下位机使用 STM32 控制串口波特率 115200每 20ms 发送一帧数据。上位机发给下位机的速度指令帧帧头 长度 指令 左轮速度 右轮速度 校验 0xAA 0x55 0x06 0x01 int16 int16 CRC8下位机反馈的编码器数据帧帧头 长度 指令 左轮转速 右轮转速 校验 0xAA 0x55 0x0A 0x02 int32 int32 CRC8转速单位换算需要额外说明通常编码器输出的是“每秒脉冲数”需要根据减速比和编码器线数换算成“m/s”。换算公式轮子线速度 (脉冲数 / 编码器线数 / 减速比) * 2 * π * 轮子半径我在实际项目里吃过这个亏减速比算错一位导致里程计速度比真实速度大了 30 倍机器人建图直接乱掉。所以建议在调试时先用尺子量一段距离对比里程计读数标定轮子周长和减速比。5.2 ROS2 串口节点实现ROS2 中串口通信可以使用pyserial库在功能包的setup.py中添加依赖即可。# 文件路径~/ros2_ws/src/robot_base_pkg/robot_base_pkg/serial_bridge.py import rclpy from rclpy.node import Node import serial import struct from geometry_msgs.msg import Twist from std_msgs.msg import Float64 class SerialBridgeNode(Node): def __init__(self): super().__init__(serial_bridge_node) self.declare_parameter(port, /dev/ttyUSB0) self.declare_parameter(baudrate, 115200) port self.get_parameter(port).value baudrate self.get_parameter(baudrate).value try: self.ser serial.Serial(port, baudrate, timeout0.1) self.get_logger().info(f串口已打开: {port}) except Exception as e: self.get_logger().error(f串口打开失败: {e}) self.ser None self.cmd_vel_sub self.create_subscription( Twist, /cmd_vel, self.cmd_vel_callback, 10) self.left_pub self.create_publisher(Float64, /left_wheel_speed, 10) self.right_pub self.create_publisher(Float64, /right_wheel_speed, 10) self.timer self.create_timer(0.02, self.read_serial_data) def cmd_vel_callback(self, msg): if self.ser is None: return v msg.linear.x w msg.angular.z # 差速解算 left_speed (v - w * 0.4 / 2.0) / 0.1 right_speed (v w * 0.4 / 2.0) / 0.1 left_int int(left_speed * 1000) right_int int(right_speed * 1000) # 组帧发送 data struct.pack(BBBBhhB, 0xAA, 0x55, 0x06, 0x01, left_int, right_int, 0x00) self.ser.write(data) self.get_logger().debug(f发送速度指令: L{left_speed:.2f}, R{right_speed:.2f}) def read_serial_data(self): if self.ser is None: return if self.ser.in_waiting 10: raw self.ser.read(10) # 这里只做最简单的帧头校验 if raw[0] 0xAA and raw[1] 0x55: left_pulse struct.unpack(i, raw[4:8])[0] right_pulse struct.unpack(i, raw[6:10])[0] left_msg Float64() left_msg.data left_pulse / 1000.0 self.left_pub.publish(left_msg) right_msg Float64() right_msg.data right_pulse / 1000.0 self.right_pub.publish(right_msg) def destroy_node(self): if self.ser: self.ser.close() super().destroy_node() def main(argsNone): rclpy.init(argsargs) node SerialBridgeNode() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ __main__: main()真实串口调试时有一个高频问题串口权限不足。需要把当前用户加入dialout组sudo usermod -aG dialout $USER然后重新登录否则会报Permission denied: /dev/ttyUSB0。另外真实硬件调试时务必先在“空载状态”下测试也就是把底盘架起来让轮子悬空确认速度指令方向、量程都正确后再放到地面上跑。这一步能防止电机倒转导致机器人冲出去非常重要。6. 常见问题与排查思路这一周里遇到的高频问题我整理成了一张表多数问题在 ROS2 开发中出现频率相当高。问题现象常见原因解决思路colcon build后找不到功能包没有 source install/setup.bash每次终端重新打开后都要 source或写入 .bashrc节点启动报 ModuleNotFoundError入口点配置错误或依赖未安装检查 setup.py 的 entry_points确认依赖使用 rosdep 安装ros2 topic echo没有任何数据节点没有运行或话题名拼写有误用ros2 topic list查看实际话题名RViz2 报 No transform from odom to base_linkTF 没有发布或 frame_id 不一致检查 odometry 节点 TF 发布逻辑检查 URDF 中坐标系拼写Gazebo 模型下沉或抖动碰撞体或惯性参数不正确给每个 link 配置合适的 inertia调整碰撞几何LaserScan 数据显示位置不对雷达的 frame_id 与 TF 不一致统一 laser_link 的命名在 RViz2 中 Fixed Frame 设为 odom串口报 Permission denied当前用户不在 dialout 组执行 usermod 并重新登录里程计速度与真实速度相差过大编码器线数、减速比或轮径配置错误用卷尺实测一段距离标定轮径和减速比机器人突然原地转圈左右轮方向相反检查电机接线或在驱动节点中对调左右轮方向单独说一下高频报错的完整排查步骤。错误找不到功能包如果执行ros2 run robot_base_pkg odometry_node提示找不到包先确认你已经执行过cd ~/ros2_ws colcon build source install/setup.bash注意每个新终端都需要 source 一次。如果不希望每次手动执行可以在~/.bashrc添加echo source ~/ros2_ws/install/setup.bash ~/.bashrc错误RViz2 中 Fixed Frame 报错RViz2 左下角会出现红色警告需要把全局选项里的Fixed Frame改成odom或map而不是默认的base_link。然后再添加RobotModel和LaserScan插件才能看到机器人模型和雷达数据。错误Gazebo 中机器人没有生成Gazebo 启动时偶尔因为模型加载顺序问题导致空场景。最稳妥的方式是先把 Gazebo 手动打开再执行 spawn 命令gazebo --verbose -s libgazebo_ros_factory.so然后另开终端ros2 run gazebo_ros spawn_entity.py -entity robot_base -file ~/ros2_ws/src/robot_base_pkg/urdf/robot_base.urdf这类问题的核心思路是把大问题拆成小步骤逐个验证。先用ros2 node list和ros2 topic list确认节点和话题是否存在再进一步查数据内容。7. 最佳实践与工程化建议7.1 工作空间与代码组织多人协作或长期项目开发时工作空间规范直接决定效率。建议遵循以下几条规则功能包按模块拆分robot_base_driver、robot_lidar、robot_navigation互相独立。不要把所有 launch 文件堆在一个包每个功能包维护自己的 launch 目录。常量统一放在config/*.yaml中通过参数服务器加载不要在源码里写死。提交代码前删除build、install、log目录只提交src源码。7.2 遥控与安全机制机器人硬件控制系统最怕的是“跑去哪了都不知道”。在实际开发中至少要提供一个“急停”机制。用键盘遥控时可以安装teleop_twist_keyboardsudo apt install -y ros-humble-teleop-twist-keyboard ros2 run teleop_twist_keyboard teleop_twist_keyboard它会把键盘输入转换成/cmd_vel话题发布。但要注意键盘松开时节点并不会自动发布零速度所以最好在接收/cmd_vel时做超时保护如果超过 200ms 没有收到新指令底盘自动停车。这个保护逻辑对安全运行至关重要建议在驱动节点的回调中加入时间戳判断。7.3 日志与数据记录ROS2 提供了ros2 bag工具可以把话题数据录制下来回放这在排查问题时非常有用。# 录制所有话题 ros2 bag record -a -o robot_test.bag # 回放 ros2 bag play robot_test.bag比如底盘出现里程计跳变问题时先录制一段编码器原始数据和里程计输出数据再离线分析两者关系比对着终端输出猜原因高效得多。7.4 坐标变换规范TF 坐标变换是机器人系统最容易出问题但又最容易被忽视的部分。建议所有新项目都遵循以下约定map地图坐标系表示全局地图中的位置由 SLAM 或建图模块维护。odom里程计坐标系以机器人启动位置为原点由里程计模块维护。base_link机器人本体中心坐标系固定在底盘上。laser_link、imu_link传感器坐标系通过静态 TF 与base_link关联。静态坐标变换可以用static_transform_publisher发布不需要在里程计节点中手动处理。ros2 run tf2_ros static_transform_publisher 0.15 0.0 0.06 0.0 0.0 0.0 base_link laser_link在实际工程中如果发现 RViz2 中传感器数据与模型位置不重合先从静态 TF 查起确认传感器在机器人上的安装位置是否与 URDF 一致。8. 从训练营到真实项目还需要补什么一周训练营做的是“最小闭环”能让你理解机器人硬件控制系统的工作原理。但如果想真正把它用在产品级项目里还要继续补充以下内容导航栈Nav2做自主导航、路径规划、避障时需要基于已经跑通的底盘和雷达数据安装并配置 Nav2。定位与建图SLAM 建图需要融合里程计、激光雷达数据常用方案有 Cartographer、SLAM Toolbox。多传感器融合加入 IMU 后里程计精度会进一步提升常见的融合方式有 robot_localization 包中的 EKF 滤波器。机器人控制框架如果想做更复杂的电机控制可以学习 ros2_control它提供了更加标准的硬件抽象层。不过我不建议你马上把所有框架都装上而是先把一个最简单、最可靠的闭环跑熟ROS2 环境 → 遥控底盘 → 里程计输出 → 雷达数据 → RViz2 实时观察。这个闭环是一切上层功能的基础也是排查问题时的“安全网”。9. 一周训练营总结与行动清单最后把这一周的关键动作浓缩成一份清单可以直接作为自己的检查项[ ] Ubuntu 22.04 ROS2 Humble 安装完成ros2 --version正常输出[ ] VS Code Remote-SSH 远程开发环境搭建成功代码可在远程编译执行[ ] 创建工作空间和至少一个自定义功能包colcon build成功[ ] 理解差速底盘运动学v、ω 与左右轮速度的换算关系[ ] 编写并运行里程计节点ros2 topic echo /odom能看到数据变化[ ] 使用 URDF 描述机器人模型Gazebo 能正常加载并模拟物理效果[ ] 雷达插件配置成功RViz2 中能显示 LaserScan 数据[ ] 存在一个“急停”方案超时停车或急停话题避免失控风险[ ] 将里程计和雷达数据用 ros2 bag 录制并回放成功ROS2 的入门曲线比 ROS1 陡峭一些但核心逻辑是相通的节点之间只靠消息沟通只要你把消息结构、坐标变换和通信质量把握好后续加导航、加视觉、加机械臂都只是扩展新节点的问题。如果这篇文章对你有帮助可以收藏备用遇到具体报错时欢迎在评论区带上环境版本和错误日志一起讨论这样能更快定位问题。