简介:这份资源面向ROS入门与机器人建模学习者,提供一套带摄像头与激光雷达的四轮小车URDF描述模型,用于解决移动机器人平台在仿真与可视化环境中的结构定义与传感器声明问题。压缩包共6个文件,约22KB,包含urdf模型文件、launch启动配置、package.xml与CMakeLists.txt工程描述、config参数目录,以及mbot.gv与mbot.pdf结构图,便于快速理解小车连杆与关节的组成关系。资源已有2765人学习下载,适合作为ROS工作空间搭建与模型加载的练手素材。读者可从中获得完整的机器人描述工程结构,参考车轮、车身与摄像头、激光雷达在URDF中的声明方式,并结合launch文件完成模型可视化启动,为后续接入差速控制、图像传输与激光数据处理节点打下基础。
1. 一台四轮小车,凭什么成了 ROS 入门的“标准教具”
实验室角落里那台落灰的四轮小车,摄像头歪着、激光雷达支架用扎带绑着,插上电还能跑。很多做 ROS 的人第一次真正理解tf变换、/scan话题、/cmd_vel控制,就是在这种小车上完成的。它把移动机器人最核心的三件事——感知(摄像头 + 激光雷达)、决策(ROS 节点与话题通信)、执行(四轮差速底盘)——压进一个能放在桌面上的物理系统里。你不需要仿真器里那些理想化的假设,传感器有噪声、轮子会打滑、USB 会掉线,这些“不完美”恰恰是它比 Gazebo 更值钱的地方。这份资源适合两类人:刚学完 ROS 基础概念、想找个实物把topic、service、tf串起来的新手;以及需要一套可改可扩的差速底盘原型、往上叠 SLAM 或视觉巡线的开发者。下面按“它是什么 → 怎么跑起来 → 怎么改”的顺序拆。
2. 系统架构与硬件组成:从传感器到轮子的完整链路
2.1 硬件清单与接线逻辑
这类四轮小车的硬件配置在同类项目里高度趋同,常见做法是:主控用一块树莓派(或同级别的 ARM 板)跑 Ubuntu + ROS,底盘驱动板(如基于 STM32 或 Arduino 的电机控制板)通过串口与主控通信,摄像头走 USB 或 CSI 接口,激光雷达走 USB 转串口。四轮采用两两并联的差速结构——左侧两轮同速、右侧两轮同速,靠左右速度差实现转向,没有舵机。
| 部件 | 典型型号/规格 | 接口 | 在 ROS 中的角色 |
|---|---|---|---|
| 主控 | 树莓派 4B / 同级 ARM 板 | — | 运行 ROS 节点 |
| 底盘驱动板 | STM32 / Arduino 类 | USB 串口 | 接收速度指令、回传轮速 |
| 激光雷达 | 2D 单线雷达 | USB 转串口 | 发布/scan |
| 摄像头 | USB 摄像头 / CSI | USB / CSI | 发布/image_raw |
| 电机 | 带编码器直流减速电机 ×4 | 驱动板 | 执行 + 测速 |
接线顺序有个容易翻车的点:激光雷达和底盘驱动板往往都是 USB 串口设备,插上后谁先枚举谁就是ttyUSB0,重启一次可能就换了。血泪经验是别在代码里写死ttyUSB0,用 udev 规则按设备序列号绑定固定名字,后面 2.4 节会给具体做法。
2.2 ROS 节点拓扑:谁发布、谁订阅
整个系统的数据流可以拆成三条主线。第一条是控制线:遥控节点或导航节点发布/cmd_vel(geometry_msgs/Twist),底盘驱动节点订阅后转成左右轮目标速度,经串口下发给驱动板。第二条是感知线:雷达节点发布/scan(sensor_msgs/LaserScan),摄像头节点发布/image_raw(sensor_msgs/Image)。第三条是状态线:驱动板回传编码器计数,底盘节点换算出/odom(nav_msgs/Odometry),同时通过tf广播odom → base_link的变换。
/cmd_vel ──► [base_driver] ──串口──► 驱动板 ──► 电机 │ └──► /odom + tf(odom→base_link) [lidar_node] ──► /scan [camera_node] ──► /image_raw这里的关键设计是底盘节点同时承担“指令下发”和“里程计发布”两个职责,因为它们共享同一个串口连接,拆成两个节点反而要处理串口互斥。base_link到各传感器的tf通常写在 URDF 里,由robot_state_publisher统一广播,这样雷达点云和摄像头图像才能变换到车体坐标系下对齐。
2.3 工作空间目录结构
一个能跑起来的 ROS 工作空间,目录大致长这样:
catkin_ws/ └── src/ ├── car_base/ # 底盘驱动包 │ ├── launch/ │ │ └── base.launch │ ├── scripts/ │ │ └── base_driver.py │ └── config/ │ └── serial.yaml ├── car_description/ # URDF 模型与 tf │ ├── urdf/ │ │ └── car.urdf │ └── launch/ │ └── description.launch ├── car_sensors/ # 雷达与摄像头启动封装 │ └── launch/ │ └── sensors.launch └── car_bringup/ # 总启动包 └── launch/ └── bringup.launch把底盘、模型、传感器、总启动分成四个包,是为了让“换雷达只动car_sensors、换底盘只动car_base”这件事成立。新手常把所有 launch 塞进一个包,改一处牵连一片,后期叠 SLAM 时非常难受。
2.4 串口权限与设备名固定
树莓派上第一次插串口设备,十有八九会遇到Permission denied。原因是当前用户不在dialout组。解决分两步,先加组:
# 把当前用户加入 dialout 组,之后需要重新登录生效 sudo usermod -aG dialout $USER # 查看当前用户所属组,确认 dialout 已出现 groups再加 udev 规则固定设备名。先查设备序列号:
# 列出所有串口设备的详细属性,找到 idVendor 和 idProduct udevadm info -a -n /dev/ttyUSB0 | grep -E "idVendor|idProduct|serial"然后写规则文件/etc/udev/rules.d/99-car.rules:
# 按 VID/PID 和序列号绑定,SYMLINK 生成固定软链接 SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ATTRS{serial}=="底盘板序列号", SYMLINK+="car_base" SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ATTRS{serial}=="雷达序列号", SYMLINK+="car_lidar"重载规则后,代码里统一用/dev/car_base和/dev/car_lidar,再也不用猜ttyUSB编号。idVendor和idProduct要换成你手上设备的实际值,serial字段不是所有设备都有,没有就只靠 VID/PID 区分,但同型号两块板会冲突,这时必须靠序列号。
3. 核心代码拆解:底盘驱动节点怎么写
3.1 速度指令到轮速的换算
差速底盘的运动学模型不复杂,但参数写错就是原地打转或者跑偏。核心公式:设车体线速度v、角速度w、轮距L,则左轮线速度v_left = v - w*L/2,右轮v_right = v + w*L/2。再除以轮半径r得到角速度,乘减速比得到电机转速。下面是一个订阅/cmd_vel并下发的节点骨架:
#!/usr/bin/env python import rospy from geometry_msgs.msg import Twist import serial class BaseDriver: def __init__(self): # 从参数服务器读取标定值,避免硬编码 self.wheel_base = rospy.get_param('~wheel_base', 0.20) # 轮距,米 self.wheel_radius = rospy.get_param('~wheel_radius', 0.033) # 轮半径,米 self.serial_port = rospy.get_param('~port', '/dev/car_base') self.ser = serial.Serial(self.serial_port, 115200, timeout=0.1) # 订阅速度指令,队列长度 1 保证只处理最新指令 rospy.Subscriber('/cmd_vel', Twist, self.cmd_cb, queue_size=1) def cmd_cb(self, msg): v = msg.linear.x w = msg.angular.z # 差速运动学:左右轮线速度 v_left = v - w * self.wheel_base / 2.0 v_right = v + w * self.wheel_base / 2.0 # 线速度转轮子角速度(rad/s) w_left = v_left / self.wheel_radius w_right = v_right / self.wheel_radius self.send_to_board(w_left, w_right) def send_to_board(self, wl, wr): # 协议示例:帧头 + 左右轮速度 + 校验,具体按驱动板固件来 frame = "S%+.3f,%+.3f\n" % (wl, wr) self.ser.write(frame.encode()) if __name__ == '__main__': rospy.init_node('base_driver') BaseDriver() rospy.spin()wheel_base和wheel_radius必须实测,不能抄默认值。轮距量左右轮接地点中心距离,轮半径量轮胎实际滚动半径(不是直径的一半那么简单,充气胎要算负载下的形变)。这两个参数偏 10%,跑直线就会明显画龙。queue_size=1是有意为之:速度指令是“最新有效”语义,积压旧指令会让车在急停后还往前冲一段。
3.2 里程计发布与 tf 广播
里程计靠编码器计数累加。每个控制周期读左右轮编码器增量,换算成位移和转角,再积分出位姿。这里有个常见误区:直接用/cmd_vel积分当里程计,轮子打滑时位姿就飘了,必须用编码器实测值。
import math from nav_msgs.msg import Odometry from tf.broadcaster import TransformBroadcaster class OdomPublisher: def __init__(self): self.x = self.y = self.theta = 0.0 self.last_time = rospy.Time.now() self.odom_pub = rospy.Publisher('/odom', Odometry, queue_size=10) self.tf_br = TransformBroadcaster() def update(self, dl, dr, dt): # dl/dr 为左右轮本周期位移(米) d = (dl + dr) / 2.0 # 中心位移 dtheta = (dr - dl) / self.wheel_base # 航向变化 self.x += d * math.cos(self.theta + dtheta / 2.0) self.y += d * math.sin(self.theta + dtheta / 2.0) self.theta += dtheta self.publish(dt) def publish(self, dt): odom = Odometry() odom.header.stamp = rospy.Time.now() odom.header.frame_id = 'odom' odom.child_frame_id = 'base_link' odom.pose.pose.position.x = self.x odom.pose.pose.position.y = self.y # 四元数只绕 z 轴旋转 odom.pose.pose.orientation.z = math.sin(self.theta / 2.0) odom.pose.pose.orientation.w = math.cos(self.theta / 2.0) # 协方差按经验填,给 SLAM 一个可信度参考 odom.pose.covariance[0] = 0.01 odom.pose.covariance[7] = 0.01 odom.pose.covariance[35] = 0.03 self.odom_pub.publish(odom) self.tf_br.sendTransform( (self.x, self.y, 0), (0, 0, math.sin(self.theta/2), math.cos(self.theta/2)), rospy.Time.now(), 'base_link', 'odom')dtheta用中点积分(theta + dtheta/2)而不是直接用旧航向,圆弧运动时误差更小。协方差矩阵那几个下标是固定位置:[0]是 x 方差、[7]是 y 方差、[35]是偏航角方差,填太小 SLAM 会过度信任里程计,填太大又会频繁重定位,一般从 0.01 量级起步再调。
3.3 传感器启动封装
雷达和摄像头的启动文件本质是把厂商驱动节点包一层,统一参数。以雷达为例:
<launch> <!-- 雷达驱动节点,端口用 udev 固定名 --> <node name="rplidar_node" pkg="rplidar_ros" type="rplidarNode" output="screen"> <param name="serial_port" value="/dev/car_lidar"/> <param name="serial_baudrate" value="115200"/> <param name="frame_id" value="laser_link"/> <param name="angle_compensate" value="true"/> </node> <!-- 摄像头用 uvc_camera 或 usb_cam,按实际驱动改 --> <node name="usb_cam" pkg="usb_cam" type="usb_cam_node" output="screen"> <param name="video_device" value="/dev/video0"/> <param name="image_width" value="640"/> <param name="image_height" value="480"/> <param name="pixel_format" value="yuyv"/> </node> </launch>frame_id必须和 URDF 里雷达 link 的名字一致,否则tf树断链,点云没法变换到base_link。angle_compensate打开后雷达在转速不稳时角度数据更均匀,代价是轻微延迟,做建图时建议开,做高速避障时看情况。
3.4 总启动文件与 tf 树
bringup.launch把底盘、模型、传感器串起来:
<launch> <include file="$(find car_base)/launch/base.launch"/> <include file="$(find car_description)/launch/description.launch"/> <include file="$(find car_sensors)/launch/sensors.launch"/> </launch>description.launch里加载 URDF 并启动robot_state_publisher,它负责广播base_link → laser_link、base_link → camera_link这些静态变换。整棵 tf 树是odom → base_link → {laser_link, camera_link},map → odom由 SLAM 节点在建图时补上。用rosrun tf view_frames生成 pdf 检查,树断了会直接显示成两棵孤立的树。
4. 跑起来:从零到小车动起来
4.1 环境准备与依赖安装
假设主控已装好 Ubuntu 18.04/20.04 对应版本的 ROS(Melodic/Noetic)。先建工作空间并拉依赖:
# 创建工作空间 mkdir -p ~/catkin_ws/src && cd ~/catkin_ws/src # 把项目源码包解压到 src 下,然后回到根目录编译 cd ~/catkin_ws rosdep install --from-paths src --ignore-src -r -y catkin_make source devel/setup.bashrosdep那步会按package.xml自动装缺失依赖,比手动一个个apt install靠谱。编译报错先看是不是缺serial库(Python 的pyserial),pip install pyserial补上。source要写进~/.bashrc,否则每开一个终端都得重来。
4.2 分步验证:先雷达、再底盘、最后合体
别一上来就roslaunch bringup.launch,出问题根本不知道是哪一环。分三步:
第一步,单独起雷达,确认/scan有数据:
roslaunch car_sensors sensors.launch # 另开终端 rostopic hz /scan # 应看到约 5-10Hz rosrun rviz rviz # 添加 LaserScan,Fixed Frame 设为 laser_link第二步,单独起底盘,手动发速度指令看轮子转不转:
roslaunch car_base base.launch rostopic pub /cmd_vel geometry_msgs/Twist '{linear: {x: 0.1}, angular: {z: 0.0}}' -r 10车应该缓慢直行。如果原地转或不动,回去查 3.1 的轮距、轮半径和串口协议。第三步,合体启动,在 rviz 里同时看/scan、/odom和 tf 树,推着车走一段,看odom位姿是否跟着变。
4.3 用 rviz 检查 tf 与传感器对齐
rviz 里最容易暴露的问题是“雷达点云和车体模型对不上”。把 Fixed Frame 设成base_link,同时显示 RobotModel 和 LaserScan,如果点云整体偏移或旋转,说明 URDF 里laser_link相对base_link的origin写错了——xyz是安装位置偏移,rpy是安装角度。拿尺子量雷达中心到车体中心的距离填进去,角度用水平仪校。这一步做不准,后面建图墙会歪。
4.4 键盘遥控与建图初体验
装teleop_twist_keyboard做手动遥控:
rosrun teleop_twist_keyboard teleop_twist_keyboard.py按提示用i前进、j左转。确认能遥控后,叠一个gmapping或cartographer建图,roslaunch对应建图包,遥控车绕房间一圈,在 rviz 里看地图逐渐成形。这一步能跑通,说明感知、控制、tf 三条链路全通了,后面叠导航只是加节点的事。
5. 避坑与排查:那些让车“抽风”的细节
5.1 现象:车走直线画龙,越走越偏
原因通常是左右轮实际半径不一致,或者轮距参数偏小。解决:让车直行 2 米,量左右轮实际走过的距离,按比例修正wheel_radius;轮距用卷尺量接地点中心距,别量轮毂外侧。修正后重测,偏差应控制在 5% 以内。
5.2 现象:/odom漂移严重,转一圈回来位姿对不上
编码器分辨率低或积分周期不稳是主因。检查驱动板回传的编码器计数是否在每个控制周期都读到,串口timeout设太大会导致读阻塞、dt不准。把控制周期固定成 20ms 定时器触发,别用rospy.sleep凑合。另外确认没有把/cmd_vel积分当里程计用。
5.3 现象:rviz 里雷达点云不显示或报 tf 错误
先rostopic echo /scan看header.frame_id是什么,再rosrun tf tf_echo base_link laser_link看变换是否存在。常见原因是 URDF 里 link 名和驱动参数里的frame_id大小写或拼写不一致,或者robot_state_publisher没启动。tf 树断链时 rviz 会明确提示 “No transform from [xxx]”。
5.4 现象:串口偶尔掉线,节点崩溃退出
USB 供电不足或线材质量差是高频原因,尤其雷达和底盘板同时插在树莓派上时。解决:用带供电的 USB Hub,或者把雷达单独供电;代码里给串口读写加try/except,掉线后自动重连而不是让节点直接挂掉。重连逻辑放在主循环里,每 1 秒尝试一次打开端口。
5.5 现象:编译通过但roslaunch报找不到包
多半是没source devel/setup.bash,或者包放错了层级(必须在src下且含package.xml)。用rospack find 包名验证,找不到就检查ROS_PACKAGE_PATH。另一个隐蔽原因是包名和文件夹名不一致,rospack认的是package.xml里的<name>。
6. 二次开发:把小车改成你要的样子
改造的第一步永远是先让原系统稳定跑通再动刀,我见过太多人一上来就换底盘、改协议,结果连原始状态都回不去,排查时没有基准。具体到这台车,几个高性价比的改造方向:
换传感器不改架构。加一个 IMU 做里程计融合,只需在car_sensors里加启动项,再写一个robot_localization的配置做ekf融合/odom和/imu/data。配置文件里odom0和imu0两个输入源,two_d_mode: true锁死平面运动,融合后/odometry/filtered直接替代原/odom喂给导航栈,底盘代码一行不用动。
改控制协议。如果驱动板固件换了帧格式,只改base_driver.py里的send_to_board和解析回传的部分,把协议封装成独立函数,方便以后换板子时对照。建议在config/serial.yaml里把帧头、波特率、校验方式都参数化。
叠视觉巡线。摄像头已经发布/image_raw,写一个节点订阅图像、做颜色阈值分割、输出/cmd_vel,和键盘遥控互斥即可。调试时把处理后的二值图用cv_bridge转成 ROS 图像发布出来,在 rviz 里实时看阈值效果,比盲调参数快得多。
验证改造是否成功,我习惯用一套固定动作:直行 2 米测偏差、原地转 360 度测航向回零误差、绕一个已知尺寸的矩形走一圈对比odom轨迹和实际轨迹。三个动作都过关,才认为改动没破坏基础运动学。
| 改造项 | 改动文件 | 验证动作 |
|---|---|---|
| IMU 融合 | car_sensors/launch、ekf.yaml | 直行 + 转弯对比融合前后漂移 |
| 换驱动协议 | base_driver.py、serial.yaml | 手动发速度看轮子响应 |
| 视觉巡线 | 新增line_follow.py | 地面贴线跑完整圈不脱轨 |
从那以后我每次改底盘参数,都强制先跑一遍这三个固定动作再往下叠功能,因为运动学一旦有隐性偏差,上层 SLAM 和导航的报错会把你带到完全错误的方向去查。希望这台小车能帮你把 ROS 从“看得懂”变成“跑得动”。
本文还有配套的精品资源,点击获取