▶ CARLA · Autoware · Native ROS2
为自动驾驶做好最后一步
2026-06-19
▶前言
CARLA 原生 ROS2:从 FastDDS 到多中间件架构
上一篇我们回顾了 CARLA 原生 ROS2 从 FastDDS 到 CycloneDDS 的演进历程。不依赖 ROS2(rclcpp、rclpy)、直接在 UE4 进程内调用 DDS C API 的设计确实巧妙,但真正把这套系统跑起来,踩的坑比预想的多得多。
这篇文章记录四个实战中遇到的问题(车辆控制失效、状态数据缺失、TF 冲突、内存泄漏),以及最终怎么用一个集成脚本为自动驾驶做好最后一步。其中有些是本地自行设计修复的,有些是直接引用上游社区 PR。
来源说明:
| 本地设计 | ||
| 直接引用上游 PR | ||
| 本地设计 | ||
| 直接引用上游 | ||
| 本地设计 |
▶一、ROS2 节点 向 CARLA 发送车辆控制命令,车辆不动:CycloneDDS 订阅器的致命缺陷
现象
CARLA 以 --ros2 --rmw=cyclonedds 启动后,传感器数据一切正常——LiDAR 点云、RGB 图像、GNSS、IMU 都在 ros2 topic echo 中正常输出。Autoware 端也能顺利接收这些数据。
问题出在反向:从 Autoware 向 CARLA 发送车辆控制命令时,车辆纹丝不动。
# 这条命令发出去了,但 CARLA 完全没反应 ros2 topic pub --rate 10 /carla/hero/vehicle_control_cmd \ carla_msgs/msg/CarlaEgoVehicleControl \ "{throttle: 0.5, steer: 0.0, brake: 0.0, reverse: false}"用 ros2 topic info 查看,订阅端确实存在(Subscription count: 1),但节点名是 _CREATED_BY_BARE_DDS_APP_——这说明 CARLA 用的是裸 DDS,不是标准的 rclcpp。
CARLA 服务端日志里反复刷着一条错误:
ERROR: carla_cdr_from_ser: fragmented receive is not supported; data will be invalid根因
CARLA 的 CycloneDDS 后端使用自定义 ddsi_sertype 做原始 CDR 字节透传,完全绕过 CycloneDDS 的标准类型系统。这套自定义类型需要实现 ddsi_serdata_ops 表中的 15 个回调函数(eqkey、get_size、from_ser、from_ser_iov、from_keyhash、from_sample、to_ser、to_ser_ref、to_ser_unref、to_sample、to_untyped、untyped_to_sample、free、print、get_keyhash),其中关键的两个是数据接收路径:
| from_ser_iov | ||
| from_ser |
关键区别:CARLA 自己发布的传感器数据,发布端和订阅端都在同一个 UE4 进程内,走 from_ser_iov 路径,所以传感器数据一直正常。但外部 ROS2 节点发布的控制命令,数据从另一个进程通过 DDS 网络层传过来,CycloneDDS 调用的是 from_ser 路径。
PR #9644(实现 CycloneDDS CDR 中间件的那个 PR)对 from_ser 的处理是直接 log_error 然后返回零填充数据。代码注释写道:"CARLA uses local IPC so this should never be triggered in practice"——这个假设在 CARLA 只做传感器发布时是对的,但一旦需要接收外部控制命令就彻底错了。
解决思路
CycloneDDS 的网络层会将大消息拆成多个 RTPS fragment,通过 nn_rdata 链表传递给 sertype 的 from_ser 回调。每个节点记录了该片段在完整消息中的字节范围。
修复的核心就是实现 from_ser:分配 serdata 缓冲区,遍历 fragchain 链表,用 NN_RMSG_PAYLOAD 和 NN_RDATA_PAYLOAD_OFF 宏获取每个片段的实际数据地址,按 min/maxp1 偏移 memcpy 到目标缓冲区。
涉及的 CycloneDDS 内部 API(NN_RMSG_PAYLOAD、NN_RDATA_PAYLOAD_OFF、nn_rdata 结构)来自 头文件,需要额外引入。
改动不大——实现一个函数、加一行 include——但定位这个问题花了很长时间。因为传感器数据一直正常,最初怀疑的是 Autoware 端的 QoS 配置、消息类型匹配、甚至 CARLA 的 Python 控制脚本,直到看到 fragmented receive 这条日志才锁定方向。
相关链接
| #9644 | ||
| #9294 | ||
| #9500 | ||
| #9762 |
▶二、补齐车辆状态话题发布:直接引用 PR #9787
本节内容直接引用上游 PR #9787(贡献者 JArmandoAnaya),未做任何修改。PR 截至 2026-06-18 尚未合并到上游 ue4-dev,我们通过 cherry-pick 提前引入。
背景
CARLA 原生 ROS2 之前只有传感器发布(CARLA→ROS2)和控制订阅(ROS2→CARLA)。但对于自动驾驶算法来说,还需要知道车辆自身的状态——当前速度、加速度、姿态、控制输入,以及车辆的物理参数(轮距、质量、最大刹车扭矩等)。没有这些数据,Autoware 的状态估计和控制诊断模块就缺了输入。
PR 整合内容
PR #9787(贡献者 JArmandoAnaya)补齐了这块能力,新增了以下 ROS2 话题:
| /carla/ | nav_msgs/Odometry | ||
| /carla/ | carla_msgs/CarlaEgoVehicleStatus | ||
| /carla/ | carla_msgs/CarlaEgoVehicleInfo | ||
| /carla/map | std_msgs/String |
Odometry
每帧发布 nav_msgs/Odometry,包含车辆在 odom 坐标系下的位姿和车体坐标系下的线速度/角速度。关键的坐标转换:UE 是左手系,需要 y 取反转右手系;速度从世界坐标投影到车体坐标(点积 forward/right/up 轴);角度从度转弧度。
Vehicle Status
每帧发布车辆的标量速度、加速度(从前后帧速度差分计算,第一帧为零)、姿态四元数,以及当前控制输入的回显。
Vehicle Info
车辆注册时发布一次,使用 TransientLocal QoS(latched),后加入的订阅者也能收到。包含车辆 ID、类型、角色名、各轮参数(摩擦系数、阻尼率、最大转向角、半径、刹车扭矩、位置)、以及物理参数(最大 RPM、转动惯量、质量、阻力系数、质心位置)。
TF 树
按照 REP-105 规范构建:map → odom(静态 identity,latched)+ odom →
UE4 侧集成思路
PR 在 UE4 侧有两个集成点:
车辆注册时(ActorDispatcher::RegisterActor()):获取车辆物理控制参数(GetPhysicsControl),调用 ROS2::ProcessVehicleInfo() 发布 latched 的 VehicleInfo
每帧 tick 时(CarlaEngine::OnPostTick()):遍历 Actor Registry 中的所有车辆,收集 transform、velocity、angular_velocity、control,调用 ROS2::ProcessDataFromVehicle() 发布 odometry + status + TF
PR 规模较大(58 个文件,3900+ 行新增),因为需要新增 4 个 Publisher 类、5 个 POD 消息类型、坐标转换工具函数、CDR 序列化/反序列化、类型元信息(type_name + type_hash)、以及 UE4 侧的集成代码。
配套修复:CDR 序列长度守卫
PR 还修复了一个潜在的 CDR 序列化 bug:PointCloud2::fields、TFMessage::transforms、CarlaEgoVehicleInfo::wheels 等序列的长度字段直接从 container.size() cast 到 uint32_t,理论上可能溢出产生畸形数据流。新增了 serialize_cdr_sequence_length() 辅助函数,写入前检查是否超过上限(2^20),超过则抛异常,与读取端的守卫对齐。
▶三、TF 发布开关:避免与下游 TF 树冲突
问题
CARLA 原生 ROS2 默认每帧发布 /tf(动态变换)和 /tf_static(静态变换),构建 map→odom→base_link 的完整 TF 链。PR #9787 也沿用了这个设计。
但问题是:Autoware 自己维护 TF 树。Autoware 的 EKFLocalizer 以 50Hz 发布 map→base_link,NDTScanMatcher 发布 map→ndt_base_link——这些都是硬编码在源码中的 sendTransform() 调用,没有参数可以关闭。
如果 CARLA 和 Autoware 同时发布 map→base_link,tf2 的"Latest Authority"策略会让两个源交替覆盖同一个变换。EKF 以 50Hz 发布,CARLA 以仿真帧率发布,频率不同,TF 树会在两个不同值之间剧烈抖动。下游节点查询到的位姿就会跳变,定位、规划、控制全部受影响。
这不是带宽问题,而是同一个 frame pair 被两个发布者写入的冲突问题。在 Autoware 接管定位的场景下,CARLA 不应该发布 TF。
解决思路
在 EpisodeSettings 中新增 ros2_publish_tf 布尔设置项,通过 Python API 暴露,默认关闭。
ROS2 单例内部维护 _publish_tf 标志。所有 TF 发布的入口(RegisterVehicle 中的静态 TF、ProcessDataFromVehicle 中的动态 TF)都检查这个标志。SetPublishTF(false) 时还会清理已有的 TF publisher 和静态 TF publisher,释放资源。
settings = carla.WorldSettings(ros2_publish_tf=True) # 按需开启 world.apply_settings(settings)这个设置是仿真回合级参数的,可以在运行时动态切换。关闭后,TF 树完全由下游(Autoware)维护,避免同一 frame pair 的双重发布冲突。
▶四、内存泄漏
来源:直接 backport 自 ue5-dev #8098(提交 239b73e84),本地做了少量微调#8098 原始场景是 save_to_disk 时内存暴涨,其修复(移除 boost::asio::post 包裹)同时解决了 WriteMessage 中 self 引用不释放的问题。
现象
CARLA 长时间运行(尤其是同步模式下高频 tick)时,CarlaUE4 进程的 RSS 随时间线性增长,最终可能吃掉数 GB 内存。
根因
ServerSession::WriteMessage() 中 boost::asio::post(_strand, [=]() { ... }) 包裹的 lambda 用 [=] 按值捕获了 shared_from_this()(即 self)。post 把这个 lambda 排到 strand 上执行,strand 持有 lambda,lambda 持有 self,形成引用循环——io_context 在 CARLA 运行期间一直存在,所以引用永远不会释放:
ServerSession (shared_ptr) → lambda 捕获 self → io_context 持有 lambda → 不释放此外,CloseNow() 中刚被 PR #9740 加入的 _is_closed.exchange(true) double-close 守卫与修复方案冲突——修复需要让 async handler 能正常退出并释放引用,守卫反而阻止了这条路径。
解决
去掉 boost::asio::post 的 lambda 包裹,改用 boost::asio::bind_executor(_strand, handle_sent) 把 strand 绑定到 handler,让 handler 直接传给 async_write。同时移除 CloseNow() 中的 double-close 守卫——_is_closed 成员变为死代码。本地在 backport 基础上做了少量微调(deadline timer 初始化方式、忙等从 sleep_for 改为 yield)。
▶五、集成落地:一个脚本为自动驾驶做好最后一步
四个问题修完,路通了。但 CARLA 自带的两个示例脚本各管一半:
• manual_control.py——pygame 键盘控制 + HUD 仪表盘,不涉及 ROS2
• ros2_native.py——读 JSON 配置生成传感器、调 enable_for_ros() 激活 ROS2 发布,没有键盘控制,没有画面
集成脚本
通过一个脚本实现自动驾驶仿真的完整前端,包括自动驾驶第三视角监视、手动与自动驾驶双模式控制,原生ROS2启动与数据接收发布等等。
整体上就是把 ros2_native.py 的传感器配置搬进 manual_control.py,然后完善功能:
| --ros2-tf | |
| --ros2 |
不传 --ros2,就是普通的 manual_control.py。加上 --ros2 -f stack.json,就是 Autoware 仿真的完整前端。
▶总结
CARLA 原生 ROS2:从 FastDDS 到多中间件架构
至此,基于Carla来打造一个完全自定义的自动驾驶仿真环境的任务就算完成了。🎉
▶文章时效说明:
本文记录的是截至 2026 年 6 月的实况。上述 PR 和 issue 在 GitHub 上持续演进,部分问题可能已被上游修复或有了更优方案,建议读者以最新代码为准。