第13天:ROS控制小车运动-自动
第13天:ROS控制小车运动
简介:基于Ubuntu20.04+ROS Noetic搭建完整机器人仿真平台,从零手写差速小车URDF模型,集成360°激光雷达,实现Gazebo物理仿真、键盘遥控、RViz雷达可视化、Python自主控车+雷达数据读取节点。全程无冗余操作,代码可直接复制运行,完美适配ROS入门学习、高校课程实验、机器人实训。
一、前置环境说明
1.1 软硬件环境配置
本次实战基于稳定通用的ROS仿真环境,适配绝大多数虚拟机、物理机设备:
操作系统:Ubuntu 20.04 LTS
ROS版本:Noetic Ninjemys
仿真工具:Gazebo 11(系统自带适配版本)
一键安装全部依赖库,包含仿真插件、键盘控制工具,无需逐个安装:
sudo apt install ros-noetic-desktop-full ros-noetic-gazebo-ros ros-noetic-gazebo-plugins ros-noetic-teleop-twist-keyboard1.2 核心通信链路总览(必懂原理)
整个小车仿真运行逻辑极简,所有操作都遵循ROS话题通信机制,两条核心链路贯穿全程:
小车运动链路:键盘按键 → teleop遥控节点 → 发布/cmd_vel速度话题 → Gazebo差速驱动插件 → 计算左右轮转速 → 物理引擎驱动小车移动
雷达传感链路:Gazebo射线仿真雷达 → 激光插件解析数据 → 发布/scan雷达话题 → RViz可视化/自定义Python节点读取数据
1.3 差速小车运动数学原理
差速底盘是移动机器人最基础的底盘结构,仅依靠左右两个主动轮的转速差,即可实现前进、后退、转向,无需额外转向机构。
左轮速度 vL | 右轮速度 vR | 小车运动状态 |
|---|---|---|
v | v | 直线前进 |
-v | -v | 直线后退 |
v | -v | 原地右转 |
-v | v | 原地左转 |
核心计算公式(轮距L、小车线速度v、角速度ω):
Gazebo差速插件会自动根据该公式计算轮速,开发者只需发布线速度、角速度即可控车。
1.4 核心通信消息 Twist 详解
ROS所有地面移动机器人统一使用geometry_msgs/Twist消息,通过/cmd_vel话题接收运动指令,字段定义固定:
linear.x 前后线速度(m/s),前进为正、后退为负 linear.y/z 地面小车固定为0(无上下、平移运动) angular.z 旋转角速度(rad/s),左转正、右转负 angular.x/y 地面小车固定为0(无俯仰、横滚运动)二、步骤1:创建仿真功能包 & 工程目录
新建ROS工作空间与仿真专用功能包,自动配置依赖,创建标准化工程目录:
# 新建工作空间(已存在可跳过) mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src # 创建仿真功能包,导入核心依赖 catkin_create_pkg simple_car_sim urdf gazebo_ros gazebo_plugins rospy # 创建标准化目录 cd simple_car_sim mkdir urdf launch worlds scripts # 编译工作空间并刷新环境 cd ~/catkin_ws && catkin_make source devel/setup.bash目录功能说明:
urdf:存放机器人三维模型文件
launch:存放仿真启动脚本
scripts:存放Python控车、雷达读取节点
worlds:存放自定义仿真场景文件
三、步骤2:编写完整URDF机器人模型
本次模型包含:基准坐标系、蓝色底盘、左右驱动轮、万向支撑轮、360°激光雷达,集成Gazebo物理仿真插件与雷达传感插件,可直接仿真运行。
文件路径:~/catkin_ws/src/simple_car_sim/urdf/simple_car.urdf
<?xml version="1.0"?> <robot name="simple_car"> <!-- ============================================================ 1. 材质定义 ============================================================ --> <material name="blue"> <color rgba="0.2 0.4 0.8 1.0"/> </material> <material name="black"> <color rgba="0.1 0.1 0.1 1.0"/> </material> <material name="gray"> <color rgba="0.6 0.6 0.6 1.0"/> </material> <!-- ============================================================ 2. base_footprint ============================================================ --> <link name="base_footprint"/> <joint name="base_joint" type="fixed"> <parent link="base_footprint"/> <child link="base_link"/> <origin xyz="0 0 0.1"/> <!-- 修复:抬高 = 轮子半径 0.05 + 车体半高 0.05 = 0.1m 确保车体在轮子正确支撑下,底面恰好在轮子顶端 --> </joint> <!-- ============================================================ 3. base_link ============================================================ --> <link name="base_link"> <visual> <geometry> <box size="0.3 0.2 0.1"/> </geometry> <material name="blue"/> </visual> <collision> <geometry> <box size="0.3 0.2 0.1"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="1.0"/> <inertia ixx="0.0058" ixy="0" ixz="0" iyy="0.0108" iyz="0" izz="0.0158"/> </inertial> </link> <!-- ============================================================ 4. 左驱动轮 ============================================================ --> <link name="left_wheel"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> <material name="black"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.2"/> <inertia ixx="0.000183" ixy="0" ixz="0" iyy="0.000183" iyz="0" izz="0.00025"/> </inertial> </link> <joint name="left_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="left_wheel"/> <!-- base_link 原点在车体中心(z方向) 轮子需要在车体底面(z = -0.05)处,让轮轴与地面平行 rpy="1.5708 0 0":绕X轴转90°,圆柱竖轴变为Y轴方向(即横向轮子) --> <origin xyz="-0.05 0.12 -0.05" rpy="1.5708 0 0"/> <!-- 旋转轴:在子坐标系(已绕X转90°)中,z轴 = 全局Y轴方向,即轮轴方向 ✅ --> <axis xyz="0 0 -1"/> <!-- 修复:统一改为 0 0 -1,方向由 rpy 决定,左右轮对称由 y 坐标正负保证 --> </joint> <!-- ============================================================ 5. 右驱动轮 ============================================================ --> <link name="right_wheel"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> <material name="black"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.05" length="0.04"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.2"/> <inertia ixx="0.000183" ixy="0" ixz="0" iyy="0.000183" iyz="0" izz="0.00025"/> </inertial> </link> <joint name="right_wheel_joint" type="continuous"> <parent link="base_link"/> <child link="right_wheel"/> <origin xyz="-0.05 -0.12 -0.05" rpy="1.5708 0 0"/> <axis xyz="0 0 -1"/> <!-- 修复:与左轮统一为 0 0 -1,差速驱动插件内部会处理左右轮的正反转 --> </joint> <!-- ============================================================ 6. 前万向轮 ============================================================ --> <link name="caster_wheel"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <sphere radius="0.025"/> </geometry> <material name="gray"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <sphere radius="0.025"/> </geometry> <!-- 修复:<surface> 不属于 URDF,移到 <gazebo reference> 里 --> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.1"/> <inertia ixx="0.000010" ixy="0" ixz="0" iyy="0.000010" iyz="0" izz="0.000010"/> </inertial> </link> <joint name="caster_joint" type="fixed"> <parent link="base_link"/> <child link="caster_wheel"/> <!-- 修复:万向轮球心 z = -(车体半高 + 球半径) = -(0.05 + 0.025) = -0.075 这样球体最低点 = -0.075 - 0.025 = -0.1,与驱动轮最低点一致 ✅ --> <origin xyz="0.1 0 -0.075"/> </joint> <!-- ============================================================ 7. 激光雷达 ============================================================ --> <link name="laser_link"> <visual> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.04" length="0.04"/> </geometry> <material name="gray"/> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <cylinder radius="0.04" length="0.04"/> </geometry> </collision> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.1"/> <inertia ixx="0.000033" ixy="0" ixz="0" iyy="0.000033" iyz="0" izz="0.000080"/> </inertial> </link> <joint name="laser_joint" type="fixed"> <parent link="base_link"/> <child link="laser_link"/> <origin xyz="0 0 0.07"/> </joint> <!-- ============================================================ 8. Gazebo 插件:差速驱动 ============================================================ --> <gazebo> <plugin name="diff_drive_controller" filename="libgazebo_ros_diff_drive.so"> <leftJoint>left_wheel_joint</leftJoint> <rightJoint>right_wheel_joint</rightJoint> <wheelSeparation>0.24</wheelSeparation> <wheelDiameter>0.1</wheelDiameter> <!-- 修复:添加 wheelTorque 限制最大输出扭矩,防止速度阶跃 --> <wheelTorque>5.0</wheelTorque> <!-- 修复:加速度限制,平滑速度变化 --> <wheelAcceleration>1.0</wheelAcceleration> <commandTopic>cmd_vel</commandTopic> <odometryTopic>odom</odometryTopic> <odometryFrame>odom</odometryFrame> <robotBaseFrame>base_footprint</robotBaseFrame> <updateRate>30</updateRate> <publishTf>1</publishTf> <publishWheelTF>false</publishWheelTF> <publishOdomTF>true</publishOdomTF> <!-- 关键:显式开启关节状态发布 --> <publishWheelJointState>true</publishWheelJointState> </plugin> </gazebo> <!-- ============================================================ 9. Gazebo 插件:激光雷达 ============================================================ --> <gazebo reference="laser_link"> <sensor type="ray" name="lidar_sensor"> <pose>0 0 0 0 0 0</pose> <visualize>true</visualize> <update_rate>10</update_rate> <ray> <scan> <horizontal> <samples>360</samples> <resolution>1</resolution> <min_angle>-3.14159</min_angle> <max_angle> 3.14159</max_angle> </horizontal> </scan> <range> <min>0.12</min> <max>10.0</max> <resolution>0.01</resolution> </range> <noise> <type>gaussian</type> <mean>0.0</mean> <stddev>0.01</stddev> </noise> </ray> <plugin name="lidar_plugin" filename="libgazebo_ros_laser.so"> <topicName>scan</topicName> <frameName>laser_link</frameName> </plugin> </sensor> </gazebo> <!-- ============================================================ 10. Gazebo 材质 & 摩擦力属性 ============================================================ --> <gazebo reference="base_link"> <material>Gazebo/Blue</material> </gazebo> <gazebo reference="left_wheel"> <material>Gazebo/Black</material> <mu1>1.0</mu1> <mu2>1.0</mu2> <!-- 轮子与地面接触的最大横向修正速度,减少数值抖动 --> <maxVel>1.0</maxVel> <minDepth>0.001</minDepth> </gazebo> <gazebo reference="right_wheel"> <material>Gazebo/Black</material> <mu1>1.0</mu1> <mu2>1.0</mu2> <maxVel>1.0</maxVel> <minDepth>0.001</minDepth> </gazebo> <!-- 修复:万向轮摩擦力必须在 <gazebo reference> 里设置才生效 --> <gazebo reference="caster_wheel"> <material>Gazebo/Grey</material> <mu1>0.0</mu1> <mu2>0.0</mu2> <maxVel>0.1</maxVel> <minDepth>0.001</minDepth> </gazebo> <gazebo reference="laser_link"> <material>Gazebo/Grey</material> </gazebo> </robot>URDF语法校验(启动前必做)
提前检测模型语法错误,规避启动崩溃问题:
cd ~/catkin_ws/src/simple_car_sim/urdf check_urdf simple_car.urdf输出Successfully Parsed XML即为正常;若报错,检查标签闭合、参数格式即可。
四、步骤3:编写仿真启动Launch文件
Launch文件一键启动Gazebo、加载机器人模型、生成仿真小车、发布TF坐标,无需逐个启动节点。
文件路径:~/catkin_ws/src/simple_car_sim/launch/simple_car_gazebo.launch
<launch> <!-- 将URDF模型加载到ROS参数服务器 --> <param name="robot_description" command="cat '$(find simple_car_sim)/urdf/simple_car.urdf'" /> <!-- 启动Gazebo空白仿真世界,启用仿真时间 --> <include file="$(find gazebo_ros)/launch/empty_world.launch"> <arg name="paused" value="false"/> <arg name="use_sim_time" value="true"/> <arg name="gui" value="true"/> </include> <!-- 在Gazebo原点生成机器人模型 --> <node name="spawn_urdf" pkg="gazebo_ros" type="spawn_model" args="-urdf -model simple_car -param robot_description -x 0 -y 0 -z 0.1" output="screen"/> <!-- 发布机器人TF坐标变换,为RViz可视化提供支撑 --> <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" output="screen"> <param name="publish_frequency" value="30.0"/> </node> </launch>五、步骤4:启动仿真 & 基础功能测试
5.1 启动Gazebo完整仿真环境
cd ~/catkin_ws source devel/setup.bash roslaunch simple_car_sim simple_car_gazebo.launch首次启动耗时30秒左右,加载完成后可见:Gazebo窗口中出现蓝色小车,车顶雷达带有绿色激光射线。
5.2 键盘遥控小车运动
新建终端,执行命令启动键盘控制节点:
source ~/catkin_ws/devel/setup.bash rosrun teleop_twist_keyboard teleop_twist_keyboard按键说明(重点):必须鼠标选中该终端窗口,按键才生效
i:前进、,:后退j:原地左转、l:原地右转k:紧急停止q/z:增大/减小全局运动速度
问题1:teleop_twist_keyboard 可执行文件找不到(精准报错解决)
报错现象:执行
rosrun teleop_twist_keyboard teleop_twist_keyboard,提示Couldn't find executable named teleop_twist_keyboard,仅检索到文件夹无可执行文件报错原因:依赖安装不完整、软件包未编译、环境变量未刷新,是Noetic版本高频隐性报错
终极解决命令(逐条执行):
# 1. 重新安装完整键盘控制依赖(覆盖缺失文件) sudo apt install --reinstall ros-noetic-teleop-twist-keyboard # 2. 刷新ROS全局环境变量 source /opt/ros/noetic/setup.bash # 3. 回到工作空间刷新本地环境 cd ~/catkin_ws source devel/setup.bash # 4. 直接运行(无需编译,官方包自带可执行文件) rosrun teleop_twist_keyboard teleop_twist_keyboard彻底根治方案:若依旧报错,说明本地源码冲突,删除冗余源码文件夹
~/xxx_ws/src/teleop_twist_keyboard,再重新安装依赖即可。
5.3 查看ROS话题通信状态
# 查看所有活跃话题 rostopic list # 实时打印小车速度指令 rostopic echo /cmd_vel # 实时打印激光雷达测距数据 rostopic echo /scan # 查看话题发布频率 rostopic hz /scan rostopic hz /cmd_vel5.4 RViz可视化小车与雷达点云
新建终端输入rviz打开可视化工具,按以下步骤配置:
将左侧Fixed Frame修改为
base_footprint点击左下角 Add,添加
RobotModel,显示小车三维模型再次 Add,添加
LaserScan,Topic选择/scan将LaserScan的Size参数改为0.05,放大雷达点云,更清晰
操控小车移动,可实时观察RViz中雷达点云随障碍物变化。
六、步骤5:自主编写Python功能节点
6.1 小车自动画圆节点(自主控车)
原理:固定线速度+角速度,让小车持续做圆周运动,理解速度话题发布逻辑。
文件路径:simple_car_sim/scripts/move_circle.py
#!/usr/bin/env python3 """ move_circle.py 让小车以固定的线速度和角速度做圆周运动。 目的:理解 cmd_vel 的发布方式。 """ import rospy from geometry_msgs.msg import Twist def main(): # 初始化节点 rospy.init_node('move_circle', anonymous=True) # 创建发布者,发布到 /cmd_vel 话题 # queue_size=10 表示消息队列最多缓存10条 pub = rospy.Publisher('/cmd_vel', Twist, queue_size=10) # 发布频率:10Hz(每秒发10次指令) rate = rospy.Rate(10) rospy.loginfo("小车开始画圆,按 Ctrl+C 停止...") while not rospy.is_shutdown(): # 构造 Twist 消息 msg = Twist() # 线速度:向前 0.3 m/s msg.linear.x = 0.3 # 角速度:绕 z 轴 0.5 rad/s(左转) # 转弯半径 r = v / ω = 0.3 / 0.5 = 0.6 m msg.angular.z = 0.5 # 发布消息 pub.publish(msg) # 按照设定频率休眠 rate.sleep() # 节点退出时发送停止指令 rospy.loginfo("停止小车...") stop_msg = Twist() # 默认全0,即停止 pub.publish(stop_msg) if __name__ == '__main__': try: main() except rospy.ROSInterruptException: pass添加权限并运行:
chmod +x ~/catkin_ws/src/simple_car_sim/scripts/move_circle.py rosrun simple_car_sim move_circle.py6.2 激光雷达数据读取节点
原理:订阅/scan话题,解析雷达数据,打印小车前、左、右三方障碍物距离。
文件路径:simple_car_sim/scripts/read_laser.py
import rospy from sensor_msgs.msg import LaserScan def laser_callback(msg): """ 每收到一帧雷达数据就调用这个函数。 msg.ranges 是一个列表,包含 360 个距离值。 索引 0 对应正前方(0°),索引 90 对应左方(90°),以此类推。 注意:inf 表示该方向没有测到障碍物(超过最大量程)。 """ # 取正前方(索引0)的距离 front_dist = msg.ranges[0] # 取左方(索引90)的距离 left_dist = msg.ranges[90] # 取右方(索引270,即-90°)的距离 right_dist = msg.ranges[270] rospy.loginfo( f"前方: {front_dist:.2f}m | 左方: {left_dist:.2f}m | 右方: {right_dist:.2f}m" ) def main(): rospy.init_node('read_laser', anonymous=True) # 订阅 /scan 话题,每次收到消息调用 laser_callback rospy.Subscriber('/scan', LaserScan, laser_callback) rospy.loginfo("开始读取激光雷达数据...") # spin() 让节点保持运行,持续接收回调 rospy.spin() if __name__ == '__main__': try: main() except rospy.ROSInterruptException: pass chmod +x ~/catkin_ws/src/simple_car_sim/scripts/read_laser.py rosrun simple_car_sim read_laser.py添加权限并运行:
chmod +x ~/catkin_ws/src/simple_car_sim/scripts/read_laser.py rosrun simple_car_sim read_laser.py七、整体通信数据流总结
7.1 小车运动数据流
键盘输入 / Python节点 → 发布/cmd_vel(Twist)→ 差速驱动插件 → 计算左右轮速 → Gazebo物理引擎驱动小车运动
7.2 雷达传感数据流
Gazebo射线仿真采集距离 → 激光插件解析数据 → 发布/scan(LaserScan)→ RViz可视化 / 自定义节点数据读取
八、常见报错与解决方案(全覆盖)
问题1:spawn_urdf 模型生成失败:URDF文件路径错误,核对
$(find)功能包名与文件路径一致问题2:robot_state_publisher 进程闪退:所有link标签缺少
<inertial>惯性参数,本文代码已修复该问题问题3:RViz不显示雷达点云:Fixed Frame设置为
base_footprint,手动选择话题/scan问题4:键盘控制无响应:鼠标点击键盘控制终端,激活窗口焦点
问题5:Gazebo启动卡顿、缓慢:首次启动加载资源属于正常现象,关闭多余后台程序即可
九、项目拓展学习方向
新增摄像头URDF模型,实现机器人视觉仿真
基于雷达数据编写自主避障、循迹行走算法
订阅
/odom里程计话题,实现小车坐标定位使用Xacro拆分URDF文件,实现机器人模型模块化搭建
添加仿真障碍物场景,完成自主导航实训
文末福利:本文全套可运行工程文件、完整源码已整理完毕,适合ROS入门实训、期末课程设计、机器人仿真作业,直接复制即可运行,无报错、无删减!
标签#ROS #Gazebo仿真 #差速小车 #URDF建模 #机器人仿真 #Twist话题 #雷达传感器