
简介这份资源面向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 摄像头 / CSIUSB / CSI发布/image_raw电机带编码器直流减速电机 ×4驱动板执行 测速接线顺序有个容易翻车的点激光雷达和底盘驱动板往往都是 USB 串口设备插上后谁先枚举谁就是ttyUSB0重启一次可能就换了。血泪经验是别在代码里写死ttyUSB0用 udev 规则按设备序列号绑定固定名字后面 2.4 节会给具体做法。2.2 ROS 节点拓扑谁发布、谁订阅整个系统的数据流可以拆成三条主线。第一条是控制线遥控节点或导航节点发布/cmd_velgeometry_msgs/Twist底盘驱动节点订阅后转成左右轮目标速度经串口下发给驱动板。第二条是感知线雷达节点发布/scansensor_msgs/LaserScan摄像头节点发布/image_rawsensor_msgs/Image。第三条是状态线驱动板回传编码器计数底盘节点换算出/odomnav_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 生成固定软链接 SUBSYSTEMtty, ATTRS{idVendor}1a86, ATTRS{idProduct}7523, ATTRS{serial}底盘板序列号, SYMLINKcar_base SUBSYSTEMtty, ATTRS{idVendor}10c4, ATTRS{idProduct}ea60, ATTRS{serial}雷达序列号, SYMLINKcar_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, timeout0.1) # 订阅速度指令队列长度 1 保证只处理最新指令 rospy.Subscriber(/cmd_vel, Twist, self.cmd_cb, queue_size1) 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_size1是有意为之速度指令是“最新有效”语义积压旧指令会让车在急停后还往前冲一段。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_size10) 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 namerplidar_node pkgrplidar_ros typerplidarNode outputscreen param nameserial_port value/dev/car_lidar/ param nameserial_baudrate value115200/ param nameframe_id valuelaser_link/ param nameangle_compensate valuetrue/ /node !-- 摄像头用 uvc_camera 或 usb_cam按实际驱动改 -- node nameusb_cam pkgusb_cam typeusb_cam_node outputscreen param namevideo_device value/dev/video0/ param nameimage_width value640/ param nameimage_height value480/ param namepixel_format valueyuyv/ /node /launchframe_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/ /launchdescription.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 对应版本的 ROSMelodic/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 的pyserialpip install pyserial补上。source要写进~/.bashrc否则每开一个终端都得重来。4.2 分步验证先雷达、再底盘、最后合体别一上来就roslaunch bringup.launch出问题根本不知道是哪一环。分三步第一步单独起雷达确认/scan有数据roslaunch car_sensors sensors.launch # 另开终端 rostopic hz /scan # 应看到约 5-10Hz rosrun rviz rviz # 添加 LaserScanFixed 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 从“看得懂”变成“跑得动”。本文还有配套的精品资源点击获取