第二章 ROS驱动框架:从零搭建底盘通信

好,咱们进入正题。这一章要聊的是ROS驱动框架的核心——节点与话题。说白了,就是让底盘和上位机“说上话”。

我记得刚接触ROS那会儿,最困惑的就是:为什么非得搞个节点?直接写个main函数不行吗?后来踩了坑才明白,节点化设计是为了让系统能“拆开玩”。你想想看,一个底盘驱动、一个激光雷达、一个导航算法,如果都写在一起,改一处就得重新编译整个工程。用ROS节点,每个模块独立运行,互不干扰——这才是工程化的思路。

2.1 ROS节点与话题机制

ROS里,节点就是一个可执行程序。它通过话题(Topic)来收发数据。话题就像广播频道,发布者喊话,订阅者收听。

我个人的习惯是:每个硬件设备对应一个节点。比如底盘驱动节点、IMU节点、激光雷达节点。这样出了问题,一眼就能定位到是哪个节点挂了。

核心概念速览:

  • 节点(Node):最小计算单元,一个进程
  • 话题(Topic):数据总线,异步通信
  • 消息(Message):数据结构,定义通信内容
  • 发布/订阅(Pub/Sub):一对多通信模式

这里有个关键点:话题通信是异步的。发布者只管发,不管有没有人收。订阅者只管收,不管谁发的。这种松耦合设计,让系统扩展性极强。我在做多机器人协同项目时,就是靠这个机制,轻松把三台底盘的里程计数据汇总到一个节点里。

2.2 底盘驱动节点设计

底盘驱动节点,是整个系统的“心脏”。它负责两件事:

  1. 接收速度指令:从上层(如导航节点)订阅/cmd_vel话题
  2. 发布里程计数据:向/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()  # 超时停车

本章知识体系总览:

ROS底盘驱动框架 导航节点 /move_base /cmd_vel (Twist) 底盘驱动节点 chassis_driver 速度转换 电机控制 里程计 电机驱动器 + 编码器 + 底盘硬件 ROS节点 话题通信 驱动节点 硬件层

嗯,这一章的内容就这些。核心就是:节点怎么设计、话题怎么定义、速度指令怎么收。把这些搞明白,底盘驱动的基本框架就搭起来了。

可可精品资料库