第二章 ROS驱动框架:从零搭建底盘通信
好,咱们进入正题。这一章要聊的是ROS驱动框架的核心——节点与话题。说白了,就是让底盘和上位机“说上话”。
我记得刚接触ROS那会儿,最困惑的就是:为什么非得搞个节点?直接写个main函数不行吗?后来踩了坑才明白,节点化设计是为了让系统能“拆开玩”。你想想看,一个底盘驱动、一个激光雷达、一个导航算法,如果都写在一起,改一处就得重新编译整个工程。用ROS节点,每个模块独立运行,互不干扰——这才是工程化的思路。
2.1 ROS节点与话题机制
ROS里,节点就是一个可执行程序。它通过话题(Topic)来收发数据。话题就像广播频道,发布者喊话,订阅者收听。
我个人的习惯是:每个硬件设备对应一个节点。比如底盘驱动节点、IMU节点、激光雷达节点。这样出了问题,一眼就能定位到是哪个节点挂了。
核心概念速览:
- 节点(Node):最小计算单元,一个进程
- 话题(Topic):数据总线,异步通信
- 消息(Message):数据结构,定义通信内容
- 发布/订阅(Pub/Sub):一对多通信模式
这里有个关键点:话题通信是异步的。发布者只管发,不管有没有人收。订阅者只管收,不管谁发的。这种松耦合设计,让系统扩展性极强。我在做多机器人协同项目时,就是靠这个机制,轻松把三台底盘的里程计数据汇总到一个节点里。
2.2 底盘驱动节点设计
底盘驱动节点,是整个系统的“心脏”。它负责两件事:
- 接收速度指令:从上层(如导航节点)订阅/cmd_vel话题
- 发布里程计数据:向/odom话题发布位姿信息
我一般把节点设计成三个线程:
- 主线程:ROS spin循环,处理回调
- 控制线程:定时发送PWM给电机驱动器
- 里程计线程:定时读取编码器,计算位姿
为什么要分线程?因为电机控制和里程计计算都是实时任务。如果放在一个循环里,控制频率和计算频率会互相干扰。我曾经在项目里偷懒,把两者写在一起,结果电机控制频率被里程计计算拖慢,底盘跑起来一卡一卡的——教训啊。
我的经验:控制线程频率建议50-100Hz,里程计线程频率建议10-50Hz。太高了CPU吃不消,太低了控制精度不够。
2.3 电机控制话题定义
电机控制话题,通常用Twist消息。它定义在geometry_msgs包里,包含线速度和角速度。
# Twist.msg 标准定义
geometry_msgs/Vector3 linear # 线速度 (x, y, z)
geometry_msgs/Vector3 angular # 角速度 (x, y, z)
对于两轮差速底盘,我们只关心linear.x(前进速度)和angular.z(旋转速度)。y和z方向通常置零。
但这里有个坑:Twist消息的单位是m/s和rad/s。而电机驱动器通常接收的是PWM占空比(0-100%)。所以节点里必须做转换。我一般这样处理:
# 速度转PWM的简化示例
def twist_to_pwm(linear_x, angular_z):
# 左轮速度 = 线速度 - 角速度 * 轮距/2
# 右轮速度 = 线速度 + 角速度 * 轮距/2
left_speed = linear_x - angular_z * wheel_base / 2.0
right_speed = linear_x + angular_z * wheel_base / 2.0
# 限幅处理
left_speed = max(min(left_speed, max_speed), -max_speed)
right_speed = max(min(right_speed, max_speed), -max_speed)
# 映射到PWM
left_pwm = int(left_speed / max_speed * 100)
right_pwm = int(right_speed / max_speed * 100)
return left_pwm, right_pwm
注意:wheel_base(轮距)这个参数必须准确测量。差1厘米,转弯半径就偏了。我见过有人用尺子量,结果差了2毫米,导航绕圈圈绕了半小时没找到路。
2.4 速度指令订阅
订阅速度指令,用ROS的Subscriber就行。代码很简单:
import rospy
from geometry_msgs.msg import Twist
def cmd_vel_callback(msg):
# 收到速度指令
linear_x = msg.linear.x
angular_z = msg.angular.z
# 调用转换函数
left_pwm, right_pwm = twist_to_pwm(linear_x, angular_z)
# 发送给电机
motor_control(left_pwm, right_pwm)
rospy.init_node('chassis_driver')
rospy.Subscriber('/cmd_vel', Twist, cmd_vel_callback)
rospy.spin()
这里有个容易被忽略的点:回调函数里不能做耗时操作。比如写文件、打印日志、复杂计算。因为ROS的回调是单线程的,你卡住了,后续消息就丢了。我一般只在回调里做数据转换和缓存,真正的控制逻辑放到另一个线程里。
另外,建议加个超时保护。如果超过一定时间没收到/cmd_vel消息,自动让电机停止。防止上位机挂了,底盘还在狂奔:
def check_timeout():
if (rospy.Time.now() - last_cmd_time) > rospy.Duration(0.5):
motor_stop() # 超时停车
本章知识体系总览:
嗯,这一章的内容就这些。核心就是:节点怎么设计、话题怎么定义、速度指令怎么收。把这些搞明白,底盘驱动的基本框架就搭起来了。
可可精品资料库