车辆运动学模型:从自行车到阿克曼
各位同学好,我是你们的老朋友。今天咱们聊聊自动驾驶仿真里最基础、也最核心的一块——车辆运动学模型。
说实话,我刚入行那会儿,总觉得运动学模型太简单,不就是个几何关系嘛。直到有一次,我在做路径跟踪仿真时,发现车辆怎么都跑不直,方向盘明明打了,车却像喝醉了一样。排查了半天,原来是模型里少考虑了阿克曼几何的约束。嗯,从那以后,我再也不敢小看这个“简单”的模型了。
为什么是自行车模型?
你想想看,一辆真车有四个轮子,每个轮子都有自己的转向角、滑移角,建模起来非常复杂。但在很多场景下,我们其实不需要那么精细。
自行车模型的核心思想很简单:把左右两个前轮合并成一个虚拟前轮,左右后轮合并成一个虚拟后轮。这样一来,四轮车就变成了两轮车。
适用场景:
- 低速行驶(通常 < 10 m/s)
- 路径跟踪控制
- 轨迹规划中的状态预测
- 仿真验证的快速原型
注意:自行车模型假设轮胎没有侧偏,也就是说不考虑轮胎的侧向力。高速过弯时,这个模型就不太准了。我在做高速避障仿真时吃过这个亏,后来换成了动力学模型才搞定。
阿克曼转向几何
好,问题来了。既然我们把两个前轮合并成一个,那这个虚拟前轮的转向角该怎么算?
这就引出了阿克曼转向几何。说白了,它解决的是:车辆转弯时,内侧轮和外侧轮的转弯半径不一样,所以它们的转向角也不一样。
你看,如果左右前轮平行转向,车辆会怎么走?它会侧滑,因为内侧轮转的弯更急,外侧轮转的弯更缓。阿克曼几何就是让内侧轮多转一点,外侧轮少转一点,这样所有轮子都绕着同一个圆心转。
数学上,阿克曼转向关系可以表示为:
δ_inner = arctan(L / (R - W/2))
δ_outer = arctan(L / (R + W/2))
其中:
- L:轴距(前后轮中心的距离)
- W:轮距(左右轮中心的距离)
- R:转弯半径
而在自行车模型中,我们通常使用一个等效的前轮转向角 δ,它近似等于内外轮转向角的平均值。
我的经验:实际项目中,阿克曼几何的偏差会导致路径跟踪误差。我曾经在一个泊车项目中,因为忽略了阿克曼修正,车辆倒车入库时总是偏右20厘米。后来加上阿克曼补偿,一把就进去了。
车辆状态表示:x, y, yaw, velocity
有了模型,我们还得知道车辆当前在哪儿、朝哪儿、跑多快。这就是车辆状态表示。
在二维平面中,我们用四个量来描述车辆状态:
| 状态量 | 符号 | 单位 | 含义 |
|---|---|---|---|
| 横向位置 | x | m | 车辆后轴中心在全局坐标系下的x坐标 |
| 纵向位置 | y | m | 车辆后轴中心在全局坐标系下的y坐标 |
| 航向角 | yaw | rad | 车辆纵轴与全局x轴的夹角 |
| 速度 | velocity | m/s | 车辆后轴中心的瞬时速度 |
这里有个细节:为什么选后轴中心作为参考点?因为后轴中心在自行车模型中就是后轮的位置,而且后轮没有转向角,计算起来最方便。
运动学方程
有了状态,我们还得知道状态怎么随时间变化。这就是运动学方程:
dx/dt = v * cos(yaw)
dy/dt = v * sin(yaw)
dyaw/dt = v * tan(δ) / L
dv/dt = a
其中:
- δ:前轮转向角(控制输入)
- a:加速度(控制输入)
- L:轴距
你看,这个方程组其实很直观。x和y的变化就是速度在x和y方向上的投影。航向角的变化率,说白了就是角速度,它等于速度除以转弯半径,而转弯半径 R = L / tan(δ)。
核心公式记忆法:
我个人的习惯是这么记的:
- 位置变化 = 速度 × 方向余弦
- 方向变化 = 速度 × 曲率
- 曲率 = tan(转向角) / 轴距
离散化:从连续到离散
仿真嘛,肯定是在离散时间步上跑的。所以我们需要把上面的连续方程离散化。
最常用的方法是欧拉法:
x(k+1) = x(k) + v(k) * cos(yaw(k)) * dt
y(k+1) = y(k) + v(k) * sin(yaw(k)) * dt
yaw(k+1) = yaw(k) + v(k) * tan(δ(k)) / L * dt
v(k+1) = v(k) + a(k) * dt
dt 是仿真步长,一般取 0.01s 到 0.1s 之间。
避坑指南:我曾经在仿真中把dt设得太大(0.5s),结果车辆状态发散得一塌糊涂。后来才意识到,欧拉法对步长很敏感。如果你发现仿真结果不稳定,先检查dt是不是太大了。
知识体系总览
为了让大家更直观地理解本章的知识结构,我画了一张图:
代码实现:一个简单的运动学仿真
光说不练假把式。咱们写个简单的 Python 代码,把自行车模型跑起来:
import numpy as np
class BicycleModel:
def __init__(self, L=2.5, dt=0.05):
self.L = L # 轴距,单位米
self.dt = dt # 仿真步长,单位秒
self.x = 0.0
self.y = 0.0
self.yaw = 0.0
self.v = 0.0
def update(self, delta, a):
"""
更新车辆状态
delta: 前轮转向角 (rad)
a: 加速度 (m/s^2)
"""
self.x += self.v * np.cos(self.yaw) * self.dt
self.y += self.v * np.sin(self.yaw) * self.dt
self.yaw += self.v * np.tan(delta) / self.L * self.dt
self.v += a * self.dt
return self.x, self.y, self.yaw, self.v
# 使用示例
car = BicycleModel(L=2.5, dt=0.05)
for t in range(100):
# 给一个恒定的转向角和加速度
x, y, yaw, v = car.update(delta=0.1, a=0.5)
if t % 20 == 0:
print(f"t={t*0.05:.2f}s: x={x:.2f}, y={y:.2f}, yaw={yaw:.2f}, v={v:.2f}")
小技巧:实际仿真中,我习惯把车辆状态封装成一个类,这样方便扩展。比如后面要加传感器噪声、路面坡度等,直接在 update 方法里加就行,不用改调用处的代码。
总结一下
这一章我们聊了三个核心内容:
- 自行车模型:把四轮简化为两轮,适用于低速场景
- 阿克曼转向几何:保证车辆转弯时所有轮子绕同一圆心转动
- 车辆状态表示:用 (x, y, yaw, velocity) 描述车辆位姿和速度
这些内容看起来简单,但它们是整个路径跟踪仿真的基石。我见过不少同学一上来就搞复杂的动力学模型,结果连最基本的运动学都没搞明白,仿真结果自然一塌糊涂。
记住:先把简单的跑通,再往复杂里加。这是我在项目中摔过跟头后总结出来的经验。
一句话总结:自行车模型 + 阿克曼几何 + 状态表示 = 路径跟踪仿真的起点。把这个基础打牢了,后面的控制算法才能站得住脚。