先说结论:这条路能走通,而且一旦打通,调试效率比在Linux容器里高很多。我最近刚在一台Windows机器上完成了一套仿真链路,思路很简单——把Apollo控制模块的算法核心从整个框架里拆出来,在Windows下用CMake独立编译成一个可执行程序,再通过UDP与CarSim做闭环联合仿真。整个过程踩了不少坑,尤其是依赖库、编译选项和通信时序这三块,所以把完整过程和背后逻辑整理出来,给想省时间的同行参考。
先说清楚这篇文章适合谁。如果你目前的工作是对Apollo控制模块里的LQR、MPC这类控制器做算法验证和参数调优,手里只有Windows环境,又不想每次改一行代码就rebuild一遍Docker镜像,那这套"独立编译+CarSim联合仿真"的方案会非常对路。如果你想把CarSim换成别的车辆动力学软件,桥接思路同样适用。
1. 为什么要折腾"独立编译":仿真场景下的取舍与定位
1.1 控制模块在自动驾驶链路里的位置
自动驾驶系统通常拆成定位、感知、预测、规划、控制这几个模块。控制模块处于链路末端,输入是规划层给出的目标轨迹,再加车辆当前状态,输出是油门、刹车、转向指令。
说白了,前面感知、预测做得再好,最终都要靠控制模块把指令发到执行器。做控制算法的人最常面对的场景是:规划模块给了一条参考轨迹,我关心的是"车辆能不能稳定跟住这条轨迹"。这时候如果为了验证一个PID参数,要启动整个自动驾驶系统,等于杀鸡用牛刀,而且环境搭建成本极高。
所以在控制方向的仿真验证里,核心需求其实只有两个:一是有个能跑的控制算法实体,二是有个像样的车辆模型。其他模块都可以先放一边。
1.2 为什么不用官方推荐的Docker方案
Apollo官方环境基本是Linux + Docker,这本身没有错,但在我这种Windows日常开发的情况下,问题不少。
首先是性能损耗。Windows上的Docker通过虚拟机跑Linux,磁盘文件共享、网络通信都有性能折损。仿真控制要求固定周期、低延迟,每多一层虚拟化都让人不放心。其次是迭代效率,Docker里改一行代码,要重新编译镜像、重新启动容器,整个周期几分钟起步,一天改几十次参数得疯掉。
最现实的问题是CarSim。CarSim在Windows上原生跑得最顺,仿真工程文件都在Windows文件系统里。把CarSim拉进Docker的Linux网络环境,还要处理host网络、端口映射这些配置,每次重新创建容器都可能忘掉哪一步配置。
所以我的判断是:与其勉强迁就,不如直接把Apollo控制模块抽出来,编译成Windows原生程序。这样控制算法和CarSim都跑在同一个操作系统上,通信走本机UDP,简单直接。
1.3 "独立编译"到底编译什么
需要明确一件事:这里说的独立编译,不是把整个Apollo系统搬到Windows,而是只针对modules/control下的控制模块核心代码。
我刚接触这套方案时长了个教训,以为要维护一份Apollo全量代码树在Windows上。实际不用。控制模块真正依赖的东西并不多,主要是几个控制器实现、滤波算法、消息定义以及一些工具库。我把这些摘出来,单独建立一个CMake工程,不依赖Cyber RT通信框架,自己写一个简单的主循环完成"收状态-算控制-发指令"的闭环。
这样做最大的好处在于,你可以拿任何一段参考轨迹扔给控制器,然后在CarSim里观察车辆响应。控制器内部逻辑、参数、甚至算法都可以按自己的想法替换,而不是被整个Apollo框架绑定住。
2. 拆解联合仿真:谁负责车,谁负责大脑
2.1 CarSim在链路中扮演的角色
CarSim本质上是高精度车辆动力学仿真器,它把轮胎力学、悬架特性、转向系统、空气阻力这些细节都建模进去了。相比简单运动学模型,CarSim能真实反映车辆在高速过弯、紧急变道时的动态响应,这对控制算法验证非常重要。
在联合仿真链路里,CarSim负责执行控制指令,并且把车辆状态反馈回来。你可以把CarSim理解成一个"虚拟测试车",Apollo控制模块是"驾驶员",驾驶员给出转向、油门、刹车,车按照动力学特性产生运动。
CarSim端通常有两种使用方式:一种是独立的CarSim程序直接仿真并输出动画,另一种是通过Simulink中的S-Function接入外部信号。做联合仿真经常用第二种,因为S-Function允许自定义输入输出端口,方便和其他工具通信。
2.2 Apollo控制模块需要哪些输入,CarSim需要哪些指令
联合仿真设计的第一步是把数据接口定义清楚,这里涉及两边的输入输出。
从Apollo控制模块的角度看,它需要的是所在坐标系下的车辆位置和姿态信息。具体来说包括车辆全局坐标X、Y,航向角,纵向速度,横摆角速度,以及纵向加速度。这些信息在Apollo内部对应的是定位数据,但联合仿真时我们直接由CarSim提供。从CarSim的角度看,它需要的是控制指令:前轮转角(或方向盘转角)、油门位置、制动压力。
我把接口整理成一张表,方便对照:
| 数据方向 | 内容 | 单位 | 说明 |
|---|---|---|---|
| CarSim -> Apollo | 车辆X坐标 | m | 全局坐标系 |
| CarSim -> Apollo | 车辆Y坐标 | m | 全局坐标系 |
| CarSim -> Apollo | 航向角Yaw | rad | 注意CarSim默认可能是deg |
| CarSim -> Apollo | 纵向速度Vx | m/s | 注意CarSim默认可能是km/h |
| CarSim -> Apollo | 横摆角速度AVz | rad/s | 注意CarSim默认可能是deg/s |
| CarSim -> Apollo | 纵向加速度Ax | m/s² | 用于纵向控制补偿 |
| Apollo -> CarSim | 前轮转角 | deg | CarSim执行器输入 |
| Apollo -> CarSim | 油门指令 | 0~1 | 相对值 |
| Apollo -> CarSim | 制动指令 | 0~1 | 相对值 |
这个表是整个通信协议的基础,后面Simulink模型里配置S-Function的导入导出变量,以及Apollo侧UDP报文解析,都是按这张表来的。
2.3 通信桥梁选型:UDP是最省心的选择
确定两边的数据后,要考虑怎么传。我评估过三种方案。
第一种是Simulink里直接用S-Function调外部DLL。理论上可以实现Apollo算法作为DLL直接在Simulink里跑,省掉网络通信这一层,但问题是要把Apollo的算法代码封装成C接口DLL,调试起来非常别扭,而且必须跟Simulink的编译体系绑定,每次生成C代码都要重新编译。
第二种是文件交换方式,即Apollo程序把控制指令写入文本文件,CarSim侧定时读取。这个方案简单但延迟太大,实时性没法保证,仿真频率稍高就会丢步,只适合离线离线标定,不适合闭环。
第三种是UDP本机通信。Apollo控制程序作为独立的UDP服务端,Simulink侧通过UDP接收模块接收状态量、UDP发送模块发送控制指令。本机UDP延迟微秒级,完全满足控制周期要求,而且通信的调试诊断工具非常丰富(例如抓包看报文),独立编译的程序和Simulink模型之间边界清晰,哪边出问题一眼就能看出来。
我最终选UDP。实际测试下来,哪怕控制周期压到10ms,UDP通信也没有成为瓶颈。
3. Windows下控制模块源码的抽取与编译:核心难点在这里
3.1 依赖库的安装
控制模块源码依赖了不少第三方库,好在大部分都能在vcpkg里直接装。我的环境是Visual Studio 2022加上vcpkg,一键安装下面这些库:
vcpkg install eigen3 gflags glog protobuf osqp-eigen --triplet x64-windows逐个解释一下为什么需要它们。eigen3是Apollo控制器里大量使用的线性代数库,做矩阵运算、二次规划求解都离不开。gflags和glog是Google的命令行参数和日志库,Apollo的代码风格依赖它们。protobuf是消息序列化库,Apollo的proto定义文件需要用protoc工具生成对应的C++类。osqp-eigen是OSQP求解器的Eigen封装,Apollo的MPC控制器里用它做优化求解。
这里有个细节值得注意:protobuf的版本要相对克制,不要一上来就装最新版。Apollo的proto文件是基于特定版本语法写的,太新的protobuf可能因为语法弃用导致生成代码失败。我用的是3.20.x这个系列,兼容性很好。
3.2 源文件究竟要抽哪些
从Apollo源码里抽代码时,我的做法是建立一个干净目录结构,而不是复制整个仓库:
apollo-lite/ ├── CMakeLists.txt ├── proto/ # 从modules/control/proto复制 │ ├── control_cmd.proto │ ├── lateral_control.proto # 按需保留 │ └── ... ├── common/ # 从modules/common复制需要的部分 │ ├── math/ │ └── ... └── control/ ├── common/ # 控制模块内部工具 ├── controller/ │ ├── lateral_controller/ │ ├── longitudinal_controller/ │ └── mpc_controller/ # 按需选择 └── ...真正需要动脑子的是去掉对Cyber RT的依赖。Apollo整个框架的消息传递和调度机制基于Cyber,但控制算法本身并不依赖Cyber的特性。我做了三个层面的替换:
第一,把cyber::Time::Now()这类时间调用替换成std::chrono::system_clock::now(),保证控制周期计量准确。第二,把AINFO、ADEBUG、AERROR这些日志宏映射到glog的LOG(INFO)、LOG(ERROR),用宏定义做兼容桥接,这样源码里的日志调用基本不用改。第三,把异步消息触发改成同步函数调用,Apollo原本通过Cyber订阅定位和规划消息,在独立编译版本里,直接用CarSim发过来的状态数据作为控制器输入。
搞清楚原理后,抽取工作并不复杂,真正花时间的是处理编译错误,后面专门讲。
3.3 CMake工程组织与控制循环
我把CMakeLists.txt写得尽量简洁,核心思路是让所有控制算法源文件编译进一个可执行程序,同时把控制器实现编写成相对独立的接口,方便替换。
cmake_minimum_required(VERSION 3.16) project(apollo_control_lite) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Eigen3 REQUIRED) find_package(gflags REQUIRED) find_package(glog REQUIRED) find_package(protobuf REQUIRED) find_package(osqp-eigen REQUIRED) # 用protoc生成proto对应的C++类 protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS proto/control_cmd.proto proto/localization.proto proto/planning_trajectory.proto ) add_executable(apollo_control_lite main.cc ${PROTO_SRCS} control/controller/lateral_controller.cc control/controller/longitudinal_controller.cc control/controller/mpc_controller.cc control/common/control_math.cc # ... 根据实际抽取的源文件补充 ) target_include_directories(apollo_control_lite PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${EIGEN3_INCLUDE_DIR} ) target_link_libraries(apollo_control_lite PRIVATE gflags glog protobuf::libprotobuf osqp-eigen )这个工程本身不复杂,但第一次编译时我遇到了一堆因为"Apollo假设自己在Linux下跑"而产生的兼容问题,下面详细说说最典型的几个。
3.4 编译期经典报错与处理方案
这里把我实际遇到的报错整理成了一张表,每个问题都花了不少时间才定位。
| 报错现象 | 根本原因 | 解决办法 |
|---|---|---|
找不到cyber/time/time.h | 代码直接引用了Cyber头文件 | 全局搜索#include "cyber/,替换成标准库头文件 |
AINFO、ADEBUG未定义 | Apollo日志宏依赖Cyber日志体系 | 在不改动原代码的前提下,在公共头文件里做宏映射到glog |
找不到modules/common/proto/*.pb.h | proto文件没有生成 | 确认CMake的protobuf_generate_cpp步骤执行成功,且包含生成目录 |
| 对齐错误导致编译失败 | Eigen在MSVC下的内存对齐要求更严格 | 为相关容器添加Eigen::aligned_allocator,或统一使用Eigen::MatrixXd这类动态矩阵 |
链接时unresolved external symbol | 某些源文件依赖的符号没参与编译 | 用nm(Linux下)或dumpbin(Windows下)检查缺失符号,补充对应的.cc源文件 |
编译器不认识__attribute__((unused)) | GCC专有语法 | 定义宏#define __attribute__(x)为空,或用[[maybe_unused]]替代(C++17) |
几个问题里最坑的是第一个。我原本以为只需要复制控制模块的源文件就能编译,结果发现连control_math.cc这种工具文件里都直接#include了Cyber头文件。这说明Apollo内部模块间耦合比想象中紧密,抽取时必须把每一个cyber引用都排查干净。
Eigen对齐问题是MSVC平台的老朋友了。在Linux的GCC下,Eigen的固定尺寸类型(如Vector4d、Matrix2d)会自动做对齐,但在MSVC下如果你直接把它们放进std::vector或作为结构体成员,可能触发断言。我的做法是尽量控制器的状态矩阵用Eigen::MatrixXd这类动态尺寸类型,从根源上避免对齐问题。
__attribute__问题主要出现在一些移植自Linux的工具代码中。Apollo源码里偶有GCC专有语法,MSVC不识别,用空宏定义跨平台桥接是最快的处理办法。
编译期问题解决了,接下来才是真正有意思的部分——联调。
4. 搭建联合仿真闭环:从数据对接到跑通
4.1 CarSim侧的配置
CarSim环境的搭建有几个关键点。进入CarSim的主界面后,先建一个车辆模型。车型参数直接用CarSim自带的"B级车"或"C级车"模板即可,控制算法验证阶段不需要过于精细的整车参数,但要注意把转向系统、悬架这些保持默认值,减少不确定因素。
然后是仿真场景设置。我用的是CarSim内置的测试场,里面有一段直线加弯道的组合路线。在实际控制测试中,这样的场景足够考验横向控制器能不能稳住。
导出变量配置是重头戏。CarSim支持配置S-Function的导入导出信号,这一步需要和前面的数据接口表一一对应。我记得CarSim的变量名称有特定格式,比如Vx是纵向速度、Yaw是航向角,但因为单位可能是km/h或deg,需要在配置时看清楚单位定义,或导出原始量后自己换算。
4.2 Simulink桥接模型结构
CarSim配置完成后,在Simulink里新建模型,拖入CarSim S-Function模块。这个模块本质上是CarSim生成的动态链接库封装,它会按照配置在仿真中计算车辆状态。
我的模型结构是这样的:
[CarSim S-Function] 输出车辆状态 -> 打包成UDP报文 -> [UDP Send] [UDP Receive] 解析控制指令 -> 接入CarSim S-Function的控制输入端口Simulink推荐用定步长求解器,步长设为1ms。这个步长在UDP通信中很关键,如果设成变步长,每次仿真步进时间不确定,Apollo侧收到的数据时间戳会跳变,控制效果会时好时坏。定步长虽然计算量大一点,但稳定可靠。
UDP Send和UDP Receive模块可以使用Simulink Real-Time或者DSP System Toolbox里的实现。如果工具箱不全,也可以写自己的S-Function完成UDP收发,但通常自带模块够用了。
打包报文时我建议用最简单的double数组二进制格式:一条报文里固定放6个double值,依次是X、Y、Yaw、Vx、AVz、Ax。Apollo侧收到后按相同顺序解析。控制指令回传也类似,放3个double:前轮转角、油门、制动。这样做的好处是格式简单,两台程序之间完全可控,出了问题直接用抓包工具看报文二进制内容就能定位。
4.3 Apollo侧程序的主循环设计
独立编译出来的Apollo控制程序,核心是一个固定周期的循环:
int main(int argc, char** argv) { google::ParseCommandLineFlags(&argc, &argv, true); google::InitGoogleLogging(argv[0]); // 创建UDP服务端,固定端口接收CarSim/Simulink发来的车辆状态 UdpServer server(9001); // 创建UDP客户端,发送控制指令到Simulink的UDP Receive端口 UdpClient client(9002); // 初始化控制器:LQR/MPC参数加载、参考轨迹设置 auto lateral_controller = std::make_unique<LateralController>(); auto longitudinal_controller = std::make_unique<LongitudinalController>(); lateral_controller->Init(); longitudinal_controller->Init(); // 主控制循环:控制周期20ms constexpr int kControlPeriodMs = 20; while (running) { auto cycle_start = std::chrono::steady_clock::now(); // 1. 读取当前车辆状态 VehicleState state; if (server.Recv(state)) { LOG(INFO) << "收到车辆状态: x=" << state.x << " y=" << state.y << " yaw=" << state.yaw; } else { LOG(WARNING) << "未收到车辆状态消息,等待下个周期"; } // 2. 计算参考轨迹上的目标点(从规划模块模拟而来) TrajectoryPoint target_point = plan_reference_trajectory(state); // 3. 执行横向和纵向控制 ControlCommand cmd; cmd.steer = lateral_controller->ComputeControlCmd(state, target_point); cmd.throttle = longitudinal_controller->ComputeThrottle(state, target_point); cmd.brake = longitudinal_controller->ComputeBrake(state, target_point); // 4. 发送控制指令 client.Send(cmd); // 5. 精确定时 auto cycle_end = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( cycle_end - cycle_start); std::this_thread::sleep_for(std::chrono::milliseconds(kControlPeriodMs) - elapsed); } }这段代码里有一个非常重要的设计:参考轨迹是从哪来的。在联合仿真里,我通常预置一组轨迹点(比如直线或正弦曲线),控制器的任务就是跟踪这条预置轨迹。实际项目中,如果需要验证更复杂的场景,可以用一个轻量级的规划算法在Apollo侧实时生成轨迹,也可以用录制好的轨迹文件作为输入。
4.4 闭环调参与验证方法
链接起来后的第一件事不是看控制效果,而是验证数据链路是否正常。
先做开环测试:Apollo侧发送一个固定的前轮转角,比如右转5度,观察CarSim输出的车辆是否画出预期的圆弧轨迹。如果轨迹明显反向或完全不转,先检查前轮转角正负号约定是否一致。这个步骤能排除绝大多数通信和坐标系问题。
然后做闭环测试。第一轮我用的是简单直线参考轨迹,观察横向偏差。如果方向对了但来回振荡,通常是控制增益偏大或执行器模型缺失,把增益降下来再试。第二轮跑弯道,重点关注转向响应是否跟得上参考轨迹曲率变化,以及出弯是否有横摆振荡。
判断标准也很直接:直线路段横向偏差稳定在0.2m以内,弯道路段横向偏差不超过0.5m,速度跟踪误差控制在1km/h以内,这些指标都达到后,这套联合仿真链路算是基本跑通了。
5. 踩坑实录:那些能让仿真直接崩掉的细节
5.1 坐标系和单位:最阴险的错误
联合仿真最容易出错的地方不是算法,而是坐标系和单位不一致,而且这种错误表现极其诡异——控制器会觉得车辆实际状态和期望状态之间总是有固定偏差,于是不停输出修正指令,最终导致车辆画圈或冲出道路。
CarSim输出数据的默认单位要看具体配置,很多情况下速度是km/h,横摆角速度是deg/s,而我做独立编译时把Apollo侧的数据单位预设为m/s和rad/s。这两个单位不转换,低速还能看,一旦速度超过50km/h,误差会被控制器放大,整个仿真直接发散。
我的做法是在Apollo侧程序入口处做统一的单位换算层,而不去动CarSim端的配置。所有从UDP包里解析出来的数据先行转换为SI单位,再进入控制算法。这样即使CarSim那边导出单位改了,只需调整换算系数,不会破坏控制逻辑。
5.2 仿真步长与控制周期:两个时钟必须各管各
这个坑可以说是联合仿真最容易忽视的技术细节。
Simulink每个步长计算1ms,CarSim会把这1ms内的车辆状态算出来。但Apollo控制器有自己的控制周期,比如20ms。也就是说,控制器每20ms才决策一次,但车辆在20ms内已经运转了20个仿真步。如果不做处理,会出现两种错误:一是控制器收到数据的速度被Simulink发送速度推着走,控制周期失去意义;二是两条线程各自独立工作,控制命令到达Simulink时可能已经过时。
我的处理思路是:在Simulink侧,每个仿真步都发送状态报文,但Apollo侧只保留"最新状态"作为决策依据,不维护历史序列。在Apollo侧,控制周期严格用steady_clock控制,每个20ms周期开始读取缓冲区里的最新状态。Simulink侧的控制指令接收端也一样,每个仿真步都读取最新的UDP缓冲区数据,而不是等一条"新报文"。这样的设计能保证控制器在一个固定节奏上工作,而Simulink高速率仿真不会打断它。
我把这个逻辑画成对应关系更好理解:Simulink是一个高速跑表,每个毫秒都更新车辆状态;Apollo是一个低速决策者,每20毫秒看一眼表盘读数,做一次决定。只要保证"看表"的时候读到的是最新读数,两个时钟互不干扰,仿真就能稳定。
5.3 指令限幅与执行器延迟模型
刚开始闭环测试时,我直接让控制器算出来的指令原封不动传给CarSim。然后发现转向指令在弯道里疯狂抖动,数值一会儿正20度一会儿负15度,车辆跟着S型走位。
原因有两层。第一层是控制器输出本身没有限幅。Apollo的控制器内部逻辑虽然考虑了执行机构约束,但独立编译后我简化了部分配置,导致输出饱和没有生效。解决办法是在UDP发送前统一对三个指令做限幅:前轮转角限制在[-30deg, 30deg],油门限制在[0, 1],制动限制在[0, 1]。
第二层是执行器延迟。CarSim的转向系统带有执行器动态模型,转向指令加上去之后,实际前轮转角需要几十毫秒才能跟上。控制器的状态反馈里已经体现了这个延迟,所以控制参数如果按无延迟假设调,必然过冲。这也解释了为什么控制器在直线终点前就开始打反方向。处理办法有两种,要么在CarSim里把转向执行器时间常数调小,适合仿真早期;要么在Apollo侧控制算法里显式加入执行器延迟模型,适合做更接近实车的控制研究。
5.4 UDP通信的可靠性问题
UDP在本机通信基本不会丢包,但不代表没有坑。
最典型的是缓冲区和端口绑定问题。Windows下UDP接收缓冲区默认大小不大,如果Simulink以1ms频率发送,Apollo侧来不及读取,消息会在系统缓冲区堆积,然后被后续消息覆盖,表现出来就是状态数据跳变。解决办法是开大接收缓冲区:
int socket_buf_size = 1 << 20; // 1MB setsockopt(sock, SOL_SOCKET, SO_RCVBUF, reinterpret_cast<char*>(&socket_buf_size), sizeof(socket_buf_size));另一个问题更隐蔽。我在Simulink模型里把UDP Receive的本地端口和Apollo发送端口弄反了,导致Apollo发到9002端口,Simulink却在9002端口收Apollo的数据,但Apollo自身监听9001端口收状态。两边各说各话,但程序都不报错,仿真就是不动。
排查这类问题我的方法是脚本抓包,用Python写一段简单的socket监听脚本挂在可疑端口上,打印收到的报文内容和来源地址,很快就能定位哪条链路断了。
5.5 protobuf版本导致的运行时崩溃
最后说一个比较隐蔽的crash:protobuf版本不一致导致的运行时错误。
我一开始在vcpkg里装了最新版protobuf,编译没问题,但运行时一执行控制程序就抛出CHECK failed,提示一个问题。这是因为vcpkg里同时装了多个protobuf相关库,不同库链接到了不同版本的protobuf运行时,导致消息解析时内部结构不匹配。
解决办法是锁定protobuf版本并统一链接。我只在CMake里显式链接protobuf::libprotobuf,同时确保所有第三方库(比如glog)使用的是同一条protobuf依赖链。如果还出问题,最稳妥的路径是手动编译指定版本的protobuf,然后用vcpkg的overlay机制指向本地的protobuf安装目录。
6. 这套方案怎么继续扩展
独立编译方案跑通之后,很多工作都可以顺势展开。目前Apollo侧控制器是被我硬编码编译进去的,如果改了控制器代码,需要重新编译整个工程,耗时大概几十秒,相比Docker整体rebuild已经快不少。但如果你希望运行时就能切控制器参数而不重编译,可以引入一个配置文件解析器,把PID增益、MPC权重全部外部化,启动时从yaml或json读配置,这样调参完全不用碰代码。
另一个很自然的扩展方向是把视角转向"策略验证"。当前链路中CarSim和Apollo跑在同一台Windows机器上,网络环境比较简单。如果后面需要做多机协同仿真,比如一台机器跑Apollo大脑、另一台机器跑CarSim,UDP通信的IP地址换一下就行,协议不用变,这个桥接设计天然支持远程部署。
还有一点值得说,独立编译之后的控制模块由于只在算法层面保留Apollo的骨架,完全可以当作一个纯粹的控制算法沙盒。我后来把LQR控制器替换成自己写的滑模控制器,编译和验证的流程同样畅通。这意味着你可以在这套环境里测试任何Apollo没有的控制算法,而不必关心Apollo整体框架怎么支持它。
我个人实际测试下来的体会是,这套方案的价值不在于"绕开官方环境",而在于它让控制算法验证变成了一件轻量的事情。控制工程师最需要的是快速试错——参数调坏了大不了重新跑一遍仿真,编译失败了大不了花几十秒重新编译,不会被沉重的框架拖住。如果你做的也是控制方向的算法验证,不妨把Apollo的控制模块当做一个算法库来用,而不是把自己绑定在整套仿真环境上。