第四节:基础运动控制接口

各位同学好,我是老张。今天咱们聊聊无人车最基础、也是最核心的一块——怎么让车动起来。

你想想看,不管你的车规划得多么天花乱坠,最终都得落实到电机转不转、轮子滚不滚。说白了,运动控制就是无人车的"手脚"。今天这一节,我就带你把这双手脚练利索。

4.1 认识 /cmd_vel 话题

在ROS里,控制机器人运动的标准接口就是 /cmd_vel 话题。它的消息类型是 geometry_msgs/Twist。这个结构很简单,就两个字段:

  • linear.x:线速度,单位 m/s。正数前进,负数后退。
  • angular.z:角速度,单位 rad/s。正数左转,负数右转。

嗯,这里要注意:linear 其实有 x、y、z 三个分量,但咱们差速轮式车一般只用 x。y 和 z 留给全向轮或者飞行器用。我刚开始做项目时,有次不小心把速度发到了 linear.y 上,车纹丝不动,排查了半天……

核心原则:发布 /cmd_vel 话题,车就会动。停止运动?发布全零速度即可。

4.2 实现前进、后退与原地旋转

咱们直接上代码。我个人习惯把运动控制封装成一个类,这样后面加功能也方便。

import rospy
from geometry_msgs.msg import Twist

class MotionController:
    def __init__(self):
        rospy.init_node('motion_controller')
        self.pub = rospy.Publisher('/cmd_vel', Twist, queue_size=10)
        self.rate = rospy.Rate(10)  # 10Hz 控制频率

    def move_forward(self, speed=0.5, duration=2.0):
        """前进"""
        twist = Twist()
        twist.linear.x = speed
        twist.angular.z = 0.0
        self._publish_for_duration(twist, duration)

    def move_backward(self, speed=0.3, duration=1.5):
        """后退"""
        twist = Twist()
        twist.linear.x = -speed
        twist.angular.z = 0.0
        self._publish_for_duration(twist, duration)

    def rotate(self, angular_speed=0.5, duration=3.0):
        """原地旋转,正数为左转"""
        twist = Twist()
        twist.linear.x = 0.0
        twist.angular.z = angular_speed
        self._publish_for_duration(twist, duration)

    def stop(self):
        """急停"""
        twist = Twist()
        self.pub.publish(twist)

    def _publish_for_duration(self, twist, duration):
        """持续发布一段时间"""
        start = rospy.Time.now()
        while rospy.Time.now() - start < rospy.Duration(duration):
            self.pub.publish(twist)
            self.rate.sleep()
        self.stop()

这段代码看着简单,但有个坑——持续发布。ROS的 /cmd_vel 话题没有"保持"机制,你必须以一定频率持续发送,车才会一直动。我曾经在测试时只发了一次速度指令,结果车动了一下就停了,我还以为是电机坏了……

避坑指南:控制频率建议 10Hz~50Hz。太低车会一卡一卡,太高浪费CPU。我一般用20Hz,兼顾平滑和性能。

4.3 速度斜坡控制与平滑启停

直接给目标速度会怎样?车会"哐"一下冲出去,或者"嘎"一下刹停。这在真实场景里很危险,货物会倒、乘客会晕。所以我们需要速度斜坡——让速度慢慢变化。

说白了,就是加一个加速度限制。我常用的方法有两种:

方法一:梯形速度曲线

把速度变化分成三段:匀加速、匀速、匀减速。适合点对点运动。

def ramp_move(self, target_speed, accel=0.3, total_duration=3.0):
    """
    带斜坡控制的前进
    target_speed: 目标速度 (m/s)
    accel: 加速度 (m/s²)
    total_duration: 总运动时间 (s)
    """
    twist = Twist()
    current_speed = 0.0
    start = rospy.Time.now()
    
    while rospy.Time.now() - start < rospy.Duration(total_duration):
        elapsed = (rospy.Time.now() - start).to_sec()
        
        # 加速阶段
        if elapsed < 1.0:
            current_speed = min(target_speed, accel * elapsed)
        # 减速阶段(最后1秒)
        elif elapsed > total_duration - 1.0:
            remaining = total_duration - elapsed
            current_speed = max(0.0, accel * remaining)
        # 匀速阶段
        else:
            current_speed = target_speed
            
        twist.linear.x = current_speed
        self.pub.publish(twist)
        self.rate.sleep()
    
    self.stop()

方法二:指数平滑(低通滤波)

这个方法更简单,适合连续控制场景。每次只朝目标速度靠近一点点。

class SmoothMotionController:
    def __init__(self):
        self.current_linear = 0.0
        self.current_angular = 0.0
        self.smooth_factor = 0.2  # 0~1,越小越平滑

    def set_target(self, target_linear, target_angular):
        """设置目标速度,内部做平滑"""
        # 线性插值
        self.current_linear += (target_linear - self.current_linear) * self.smooth_factor
        self.current_angular += (target_angular - self.current_angular) * self.smooth_factor
        
        twist = Twist()
        twist.linear.x = self.current_linear
        twist.angular.z = self.current_angular
        self.pub.publish(twist)

我个人更推荐第二种方法。为什么?因为它在连续导航中表现更自然。你想想看,导航规划器每100ms发一个新速度,如果直接跳变,车就会来回晃。用了平滑后,车就像老司机开车一样稳。

注意:平滑因子不能太小,否则车反应太迟钝。我一般设0.15~0.3。具体值取决于你的控制频率和底盘响应速度。

4.4 本章知识体系

下面这张图总结了运动控制的核心逻辑,我画了个流程图,方便你理解整个链路:

运动控制核心流程 用户指令 速度斜坡控制 发布 /cmd_vel 话题 底盘电机执行 关键参数 • 线速度:0.1~1.0 m/s • 角速度:0.2~1.0 rad/s • 控制频率:10~50 Hz • 平滑因子:0.15~0.3 • 加速度:0.2~0.5 m/s² 避坑提醒 • 必须持续发布话题 • 停止时发全零速度 • 斜坡防止机械冲击

4.5 实战经验总结

最后,我把自己踩过的坑和心得整理成一张表,你写代码时可以参考:

场景 推荐做法 常见错误
直线前进 只设 linear.x,angular.z 置零 忘记清零角速度,车走弧线
原地旋转 linear.x 置零,只调 angular.z 线速度不为零,车画圈
启停控制 用斜坡或平滑滤波 直接跳变,机械冲击大
长时间运动 循环发布,频率≥10Hz 只发一次,车动一下就停
紧急停止 立即发布全零速度 依赖自然减速,响应慢

嗯,这一节的内容就这些。运动控制看似简单,但它是所有上层功能的基础。你想想看,如果车都走不稳,导航、避障全是白搭。所以,我建议你把今天这几个函数多跑几遍,感受一下不同参数对运动的影响。

记住一句话:让车动起来不难,让车优雅地动起来才是本事。


公众号:蓝海资料掘金营,微信deep3321