第四节:基础运动控制接口
各位同学好,我是老张。今天咱们聊聊无人车最基础、也是最核心的一块——怎么让车动起来。
你想想看,不管你的车规划得多么天花乱坠,最终都得落实到电机转不转、轮子滚不滚。说白了,运动控制就是无人车的"手脚"。今天这一节,我就带你把这双手脚练利索。
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 本章知识体系
下面这张图总结了运动控制的核心逻辑,我画了个流程图,方便你理解整个链路:
4.5 实战经验总结
最后,我把自己踩过的坑和心得整理成一张表,你写代码时可以参考:
| 场景 | 推荐做法 | 常见错误 |
|---|---|---|
| 直线前进 | 只设 linear.x,angular.z 置零 | 忘记清零角速度,车走弧线 |
| 原地旋转 | linear.x 置零,只调 angular.z | 线速度不为零,车画圈 |
| 启停控制 | 用斜坡或平滑滤波 | 直接跳变,机械冲击大 |
| 长时间运动 | 循环发布,频率≥10Hz | 只发一次,车动一下就停 |
| 紧急停止 | 立即发布全零速度 | 依赖自然减速,响应慢 |
嗯,这一节的内容就这些。运动控制看似简单,但它是所有上层功能的基础。你想想看,如果车都走不稳,导航、避障全是白搭。所以,我建议你把今天这几个函数多跑几遍,感受一下不同参数对运动的影响。
记住一句话:让车动起来不难,让车优雅地动起来才是本事。
公众号:蓝海资料掘金营,微信deep3321