☰
CarSim与CARLA联合仿真平台搭建复盘:车辆动力学与自动驾驶规控测试
2026/11/2 7:44:13 网站建设 项目流程

去年我把自己写的规控算法从 CARLA 往实车上迁移,第一个月就被一个现实问题反复抽打:CARLA 默认动力学模型给不出极限工况下可信的车辆响应。高速变道时车身姿态变化、湿滑路面上的横摆响应都过于理想,算法在仿真里表现良好,一上实车就开始救不回来。那段时间我把网上能搜的联合仿真方案翻了个遍,最终选定了 CarSim 与 CARLA 联合仿真,并围绕 python_carsim_env 搭了一套可以在规控测试和强化学习训练之间复用的平台。这篇文章就是我整个搭建过程的复盘,重点讲三件事:架构怎么分层、状态怎么同步、数据怎么采集,以及那些你翻文档多半找不到的坑。

1. 为什么非要把 CarSim 和 CARLA 绑在一起

1.1 CARLA 的动力学短板与 CarSim 的传感器盲区

CARLA 是当前开源自动驾驶仿真里最常用的工具,地图、交通流、传感器模型都很成熟,Python API 也友好。但它的默认车辆动力学模型是被明显简化过的:轮胎模型做了近似处理,悬架和转向系统也被大幅简化,应付城市道路巡航问题不大,一旦进入高速避障、低附着路面、紧急制动这类工况,车辆响应就会失真。

CarSim 恰好相反。它在汽车行业里是车辆动力学仿真的老牌标准,轮胎非线性、悬架运动学、转向系统迟滞、制动系统这些细节都建模得很细,整车厂和供应商做底盘调校、HIL 测试时常用它作为参考模型。但 CarSim 的场景能力很弱,没有像样的交通流,传感器仿真基本要自己从头搭,想在 CarSim 里完整搞一套自动驾驶闭环测试,体验相当痛苦。

所以这两者天然互补。CarSim 负责车辆本身的动力学精度,CARLA 负责车辆周围的世界。联合仿真的价值不是炫技,而是解决一个非常实际的问题:在软件在环阶段就用上高精度车辆模型,提前发现那些"仿真里看着好、实车一跑就翻车"的隐患。

1.2 联合仿真解决的真实问题:软件在环阶段的可靠性

做规控算法的人应该都有体会,仿真器和实车之间的鸿沟主要有两个来源:传感器噪声分布不真实,以及车辆响应模型不真实。传感器方面 CARLA 做得已经不错,但车辆响应模型的失真,会把控制策略"惯坏"。

举个例子。我在 CARLA 默认模型上调试一个高速变道场景,算法明明在仿真里能稳稳跟住轨迹,但换到 CarSim 模型后,同样的控制指令会导致明显的横摆超调,原因就是默认模型没有准确的轮胎侧偏特性和悬挂载荷转移。这种差异在正常工况下不明显,在极限工况下会被放大到足以判断算法是否可用的程度。

需要这套平台的主要有三类人:第一类做规控算法在环测试,想验证决策规划模块在真实车辆响应下的表现;第二类做强化学习训练,希望策略在更可信的动力学上学会应对极限工况;第三类做底盘控制相关研究,需要同时看到传感器场景和车辆动力学细节,比如 ABS 介入时车身姿态的变化。

1.3 为什么不走 Simulink 路线

提到 CarSim 联合仿真,绝大多数人第一反应是 CarSim 和 Simulink 联合仿真,网上能搜到大量教程。Simulink 路线的优点是和 CarSim 无缝集成、图形化建模直观,缺点是重度依赖 MATLAB/Simulink 环境,License 成本高,实时性不容易控制,部署到训练集群也麻烦。

python_carsim_env 的思路完全不同。CarSim 本身提供动态链接库(VS DLL),有 VS Commands 接口可以直接在 Python 进程里调用;CARLA 也有成熟的 Python API。那就可以写一个 Python 环境类,把 CarSim 的求解循环和 CARLA 的仿真循环统一调度起来,对外暴露类似 Gym 的接口。

我的结论是:只跑一两个固定实验,Simulink 路线没问题;但要反复训练、批量跑场景、甚至部署到多机集群,python_carsim_env 这条纯 Python 路线灵活得多。后文讲的整套架构就是基于这条路线搭起来的。

2. 围绕 python_carsim_env 搭建的联合仿真架构

2.1 一个完整控制周期里的数据流

先看一个控制周期里到底发生了什么。应用层的规控算法或 RL 代理会产出一组控制指令,通常是油门开度、刹车开度、方向盘转角。这条指令经过 python_carsim_env 写入 CarSim 的输入变量,CarSim 求解器根据当前车辆状态积分出新的位置、姿态、速度、横摆角速度、四轮轮速等状态。随后 python_carsim_env 把这些状态做坐标系变换,覆盖写入 CARLA 的 ego vehicle,让 CARLA 里的车辆摆到正确位置,再让 CARLA tick 一个物理步,渲染出这一帧的传感器数据。最后传感器数据和车辆状态被打包返回给应用层。

关键点是:CARLA 里的 ego vehicle 本身的物理属性被"旁路"了,它的运动学状态由 CarSim 每步覆盖写入,CARLA 只负责把它放到正确的地方渲染出来。这样传感器的视角、激光雷达的遮挡关系都基于 CarSim 给的位姿,而车辆自身的动力学行为完全由 CarSim 决定。

从代码层面看,主循环调度大概是这样的逻辑:

# 伪代码:python_carsim_env 主循环核心调度 def step(self, action): # 1. 把控制指令写入 CarSim 输入变量 cs.SetVar("Throttle", action[0]) cs.SetVar("Brake", action[1]) cs.SetVar("SteerL1", action[2]) # 2. 推进 CarSim 内部小步长积分 for _ in range(inner_steps): cs.Step(inner_dt) # 3. 读取 CarSim 状态并做坐标系变换 state = cs.GetState() carla_transform = convert_to_carla(state) # 4. 覆盖写入 CARLA 中的 ego vehicle ego.set_transform(carla_transform) # 5. CARLA tick 一个物理步 self.world.tick(carla_dt) # 6. 收集传感器数据和车辆状态 obs = self.collect_observations() return obs

这个骨架看起来简单,真正写起来最耗精力的不是循环本身,而是每一步里面的细节。后面的状态同步和数据采集章节会逐个展开。

2.2 环境接口设计:把 CarSim 和 CARLA 都封装成 Gym 风格

python_carsim_env 在我看来本质是一个"协议翻译器加调度器"。它要处理四件事:加载并初始化 CarSim 的 VS DLL;连接 CARLA 服务器并创建 ego vehicle 和传感器蓝图;把 CARLA 与 CarSim 都切到同步模式,保证每一步都由 Python 主循环主导;最后实现 reset 和 step 两个核心接口。

按 Gym 风格看,action_space 通常是连续动作空间,比如 [throttle, brake, steer],取值范围 0 到 1、-1 到 1,再映射到 CarSim 的物理量。observation_space 则取决于你的算法。如果是端到端 RL,可以输出一张前视相机图像加车辆状态向量;如果是规控评测,可以输出激光雷达点云或目标级信息,加里程计真值。

接口统一的收益在于上层算法完全感知不到底层仿真器的存在。我在同一个环境上既跑过 PPO 训练,也跑过基于 MPC 的规控评测,切换只是改几行配置,底层代码完全不用动。

2.3 时钟和步长:一个主时钟、两级步进

联合仿真最核心的设计是时钟。CARLA 有内置的仿真时间,用 world.tick(delta_seconds) 控制;CarSim 求解器有自己的积分步长,两者不能直接画等号。

我的做法是以 python_carsim_env 的主循环为中心,设定一个基础控制周期,例如 50Hz 即 20ms。在这个周期内,CarSim 用更小的步长跑多次内部积分,比如 2ms 乘 10 步,保证动力学求解的数值稳定性。CARLA 则每个基础周期 tick 一次。控制指令在每个基础周期开始时写入 CarSim,传感器数据在 tick 结束后采集。

这里有个容易踩的坑:不要为了省事把 CarSim 的步长直接设成和 CARLA 物理步长一致。CarSim 在步长过大时数值误差会显著变大,尤其是急转向和急刹车场景,状态可能直接发散。CarSim 的积分步长留小一点,代价只是多占一些 CPU。

2.4 进程模型与传感器数据解耦

最初我把传感器数据回调直接放在主循环里处理,跑几分钟就发现主循环时钟被渲染拖累,控制频率不稳。后来改成独立线程采集传感器数据,用带锁的环形缓冲区解耦,主循环只负责 CarSim 积分、状态同步和 CARLA tick,传感器数据由采集线程异步落盘或缓存。这个改动之后,主循环的时间抖动从几毫秒降到了亚毫秒级。

CARLA 的传感器数据量很大,如果用多进程方案会引入序列化和进程间通信开销,单车内联合仿真我最终选了单进程多线程。如果你要扩展多车,再考虑把每辆 CarSim 模型放进独立进程,通过共享内存或消息队列和 CARLA 主进程交换数据,单车的环境先跑稳定,扩展才有意义。

3. 状态同步:架构的核心与坑最多的地方

3.1 状态量清单:先定清楚谁产生、谁消费

状态同步最常见的错误是一上来只同步位置和朝向,后面需要加速度、轮速时再临时加,最后代码改得一团糟。我动手前先列了一张完整的状态量清单,把每个变量的来源、消费方、频率要求写清楚。

状态量来源消费方说明
车辆位置 (Xo, Yo, Zo)CarSimCARLA ego transform世界坐标,需坐标系变换
姿态角 (roll, pitch, yaw)CarSimCARLA transform 旋转注意旋转顺序和正方向定义
纵向速度 VxCarSim数据记录、RL 观测注意 m/s 与 km/h 单位换算
横摆角速度 YawRateCarSim数据记录、稳定性分析常用于轨迹跟踪验证
纵向/侧向加速度 Ax, AyCarSim舒适性分析CarSim 可直接输出
车轮转角 SteerL1CarSimCARLA debug 可视化和方向盘转角有转向比关系
四轮轮速 WheelSpeedCarSimABS/TCS 分析四轮独立输出
控制指令应用层CarSim 输入同一份指令要同步给数据记录

清单理清后,同步策略就明确了:位置和姿态必须每个 CARLA tick 都同步,否则渲染会明显卡顿或穿模;Vx、加速度这类记录用状态,10Hz 采样也够,不需要高频传递。

3.2 坐标系变换:Unreal、ISO 和车辆坐标系的三方换算

CarSim 和 CARLA 的坐标系约定完全不一样。CARLA 基于 Unreal 引擎,X 轴向右,Y 轴朝前,Z 轴向上;CarSim 使用车辆坐标系,X 轴沿车辆前进方向,Y 轴朝驾驶员左侧,Z 轴向上,同时 CarSim 的车辆位置变量 Xo、Yo 是相对世界原点的导出值。两者不仅轴方向不同,姿态角的零度基准和正方向定义也有差异。

我的处理方式是统一在 python_carsim_env 内部维护坐标变换函数,并做单元测试验证。位置变换通常是在 CARLA 地图里选一个参考点,让 CarSim 世界原点对应到 CARLA 出生点,再对轴做映射和符号修正。姿态变换要特别注意 yaw 定义:CARLA 的 yaw 以 X 轴正向为零度逆时针增大,CarSim 的 yaw 是车辆纵轴与坐标轴的夹角,两者之间一般是一个常量偏置加符号翻转。

def convert_to_carla(state): # CarSim 到 CARLA 的坐标映射示例 # 实际符号和偏置必须以单元测试验证为准 x = state["Xo"] + origin_offset[0] y = -state["Yo"] + origin_offset[1] z = state["Zo"] + origin_offset[2] yaw = -math.radians(state["Yaw"]) + yaw_offset return carla.Transform( carla.Location(x, y, z), carla.Rotation(pitch=0, yaw=math.degrees(yaw), roll=0) )

强烈建议写一个专门验证坐标变换的测试:把 CarSim 模型设成已知工况,比如 x=10, y=0, yaw=90 度,然后看 CARLA 里的车辆是否真的朝对应方向运动。这个测试十几分钟就能写完,能省掉后面无数排查时间。

3.3 频率匹配与时间戳设计

状态同步最容易被忽视的是频率不匹配。CarSim 可以输出 1ms 步长的状态轨迹,CARLA 物理步长默认 20ms,控制指令可能是 50Hz,传感器可能只有 10Hz。如果不对齐就记录数据,后面离线分析会非常痛苦。

我用的策略是给所有数据打上统一仿真时间戳。CarSim 状态一旦读出,立刻记录它的仿真时间;CARLA tick 返回后记录 tick 时间;传感器数据在回调里记录接收时间。需要同步到 CARLA 的位置和姿态,按 CARLA tick 频率从 CarSim 最近一次输出中取样,因为同步模式下 CARLA tick 前一定能拿到最新 CarSim 状态。

需要注意,CARLA 的 world.tick(delta_seconds) 参数是仿真时间推进量,不是真实时间。我用仿真时间戳统一两边,不用墙钟。墙钟会因渲染负载波动,仿真时间戳才是 CarSim 和 CARLA 共同的度量。

3.4 延迟补偿与异常降级

即使切了同步模式,状态传输依然有延迟,只是延迟被限制在一个 tick 以内。对绝大多数规控算法测试来说,20ms 的延迟可以接受;但如果你做 500Hz 的底盘反馈控制,就必须做补偿。

我在平台里记录每次同步的时间差 delta_t,即 CarSim 状态时间戳与 CARLA tick 时间戳的差值。如果 delta_t 持续偏大,说明链路带病运行,我会降低控制频率而不是硬跑。位置同步直接覆盖不做预测外推,避免预测偏差导致渲染抖动;数据记录时再做外推补偿,保证标注数据的时间精度。

故障降级重点处理了两类:一类是 CarSim 数值发散,表现为速度异常大或位置突变,检测到后会立即终止当前 episode 并记录日志;另一类是 CARLA 服务器无响应,用超时机制检测,超过设定时间没有收到 tick 回报就重连。

4. 数据采集:从原始传感器流到可用训练集

4.1 数据分类与存储格式选择

联合仿真平台的核心产出之一就是数据。我按用途把采集数据分成三类:车辆动力学真值、传感器观测、控制指令和元数据。车辆动力学真值来自 CarSim,特点是非常干净,适合做规控验证的对照;传感器观测来自 CARLA,包括相机图像、激光雷达点云、毫米波雷达目标列表,天然带有渲染和噪声;控制指令和元数据则记录了每一步的行动和场景配置。

存储格式我建议按用途分开。做 RL 训练用 HDF5 或二进制流,因为训练时需要随机快速读取大量帧,CSV 逐行读会被 I/O 卡死;做规控离线评测用 CSV 加图片序列最简单,方便用 pandas 和 numpy 处理。

我的目录结构大概是这样:

data/ ├── episode_001/ │ ├── metadata.json # 场景配置、天气、控制频率 │ ├── vehicle_state.h5 # CarSim 真值,按时间戳索引 │ ├── controls.csv # 控制指令 │ ├── sensors/ │ │ ├── camera_front/ │ │ │ ├── 00000.png │ │ │ ├── 00001.png │ │ │ └── timestamps.csv │ │ ├── lidar/ │ │ │ └── timestamps.csv │ └── diagrams/ │ ├── velocity_profile.png │ └── trajectory.png

4.2 时间戳对齐:数据质量的命门

数据质量的大敌是时间戳错乱。我实测发现,如果只记录 CARLA tick 时刻而不记录 CarSim 状态时刻,训练数据集里会产生最多一个物理步的对齐误差。对车辆动力学这种毫秒级影响显著的场景,20ms 误差足以让训练出的控制策略抖动。

解决办法是给每个数据样本同时保存三个时间戳:CarSim 求解时间、CARLA tick 时间、接收墙钟时间。离线分析时以 CarSim 时间为准做最近邻对齐,把传感器数据和车辆状态、控制指令合成一个记录。我把它做成了独立工具函数,批量处理一天采集的数据也没问题。

对齐之后一定要画一张时间戳差值分布图。如果差值呈现锯齿状波动,说明同步逻辑里有不稳定等待;如果是一条直线,说明同步节奏稳定。这是我每次数据质量体检的第一步,不要省。

4.3 数据集流水线与可视化抽查

从平台采集到的原始数据还不能直接喂给算法。我实际项目里的数据流水线包括四步:传感器原始数据经过格式标准化,控制指令和控制周期重采样到固定频率,所有数据按 episode 切分并填充场景元数据,最后做可视化抽查。

可视化抽查非常重要。我通常会把轨迹画在 CARLA 地图上,和 CARLA 渲染出的车辆位置叠在一起,肉眼确认没有漂移。这一步能发现时间戳对齐和坐标变换里的隐性错误,数值统计往往没那么直观。

5. 实测踩坑与调优经验

5.1 版本匹配、License 和 DLL 初始化

CarSim 和 CARLA 都是版本敏感的软件。CARLA 升级频繁,Python API 经常变;CarSim 的版本直接决定 VS DLL 的接口导出符号。我最初用 CarSim 2019 加 CARLA 0.9.10,后来迁到 CARLA 0.9.14,发现不少 API 调用都改了。

我的建议是锁定一套能稳定复现的组合,并记录每个组件的版本。CARLA 升级通常会带动地图资产和渲染管线的大改动,CarSim 模型参数页格式也可能变,升级成本远高于表面看到的。

CarSim 的 License 也是坑。它的动态链接库在初始化时会校验 License,服务启动顺序不对会导致 VS_Initialize 返回错误码,而且错误提示很不友好。我后来写了个启动检查脚本,按顺序检查 License 服务、环境变量、DLL 路径,排查效率提高很多。

5.2 车轮箭头动画:五分钟验证同步正确性的土办法

在 CARLA 里用 debug 工具给车轮画箭头,是我验证状态同步是否正确的神技。做法是每一帧从 CarSim 读取四轮外侧点坐标和车轮朝向,用 CARLA 的 debug.draw_arrow 画出来,再叠加在 CARLA 渲染的车轮模型上。

这个方法特别适合抓转向符号反了的问题。CarSim 的 SteeringL1 正值表示左转,而 CARLA 的 steer 参数正值对应方向盘向左打,到底映射到左转还是右转取决于你看的是车轮转角还是方向盘转角。我实际排查过一个 bug:从 CarSim 读出的左前轮转角直接写回 CARLA,方向反了,车辆在原地打转,用数值对比查了半天,换箭头动画五分钟就定位了。

如果你没做过这类验证,强烈建议跑完坐标系单元测试后,再花半小时画一次箭头。这个步骤能直观确认位置、朝向、转向三条链路的正确性,比任何日志都可靠。

5.3 实时性调优:步长、渲染与控制频率的取舍

联合仿真平台跑起来后最常见的抱怨是"卡"。但原因往往不是 CPU 不够,而是三方节奏互相冲突。CarSim 想要 1ms 小步长,CARLA 渲染 20Hz 物理步长要几十毫秒,控制算法希望 50Hz 到 100Hz 拿观测,挤在一起就是卡顿。

我实测下来,CarSim 步长从 1ms 放宽到 5ms,在多数工况下精度损失可以接受,但实时性能明显好转。如果对底盘级高频响应没有强需求,用 5ms 是性价比最高的选择。做 RL 训练时我还会关掉 CARLA 渲染,用无头模式跑,CPU 占用能降一半以上。

还有一个思路:控制频率和 CARLA tick 频率不需要完全一样。CARLA 物理步长较粗时,可以在两个 tick 之间用 CarSim 小步长做亚步进,把高频动力学信息保留下来。主循环用两级循环:外层是控制周期,内层是 CarSim 积分步长,CARLA 只在控制周期结束时 tick 一次。

5.4 坐标抖动与数值发散的处理

平台跑久了会出现两个稳定性问题。坐标抖动表现为车辆在渲染里微微颤动,多半是 CarSim 输出浮点精度不够,或者 CARLA transform 内部处理放大了误差。我的处理是只让 CarSim 状态驱动 CARLA 车辆,不反向修改 CarSim 状态,同时给位置叠加一个只针对高频抖动的低通滤波,动态响应不受影响。

数值发散通常出现在急刹或大转向后的几分钟内,速度值异常跳变。绝大多数情况是 CarSim 初始化状态和 CARLA 场景起点没对齐,比如 CarSim 内初始速度为 0,但 CARLA 里 spawn 的车辆已经被赋予了初速,动力学模型从错误初始条件开始积分。解决方法是 reset 时把 CarSim 的初速、初始位姿、挡位、ABS 状态都初始化和 CARLA 场景一致,而不是只设置位置。

最后说两句大实话。我在搭这套平台之前,也怀疑过 python_carsim_env 是不是过度封装,不如自己直接顺序调用 CarSim DLL 和 CARLA API 更干脆。实际用下来发现,有一个统一的环境接口和一组经过验证的状态同步函数,能让你把所有精力放在算法上,而不是每天和仿真器接线处搏斗。当然这套平台不是银弹:CarSim 的模型参数需要自己标定,CARLA 的传感器仿真也只是逼近真实传感器,联合仿真的结论最终还是要实车验证兜底。如果你正在搭类似的平台,我的建议是从最小闭环开始,先把一辆车跑起来,确认坐标系和同步正确,再逐步加传感器、加数据流水线,千万别一上来就搞大而全的框架。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询