简介面向Apollo PnC规划与控制开发者的基础项目工程包适合希望深入理解自动驾驶路径规划、轨迹生成与车辆控制模块的工程师也适合初学者搭建首个PnC开发环境。压缩包仅7KB包含12个文件以json、xml、conf、env等配置文件为主涵盖工程构建、编译环境与代码格式化等配置同时提供版本管理、构建脚本等工程文件目录结构精简便于围绕项目代码进行源代码阅读与定制。已有513人学习资源附带PnC核心概念梳理涵盖基于图搜索、多项式插值、车辆动力学响应控制等关键知识点并明确PnC与感知、定位、高精地图等模块的接口关系帮助理解整套模块协作机制。在此基础上还能获得配置文件管理、测试用例编写与模拟环境验证等实践要点帮助开发者从理论到工程完整掌握Apollo PnC的开发链路高效推进自动驾驶功能落地。1. Apollo PnC 基础项目到底在解决什么问题从入门到能改代码“Apollo PnC 开发人员的基础项目”听上去像一套教程真正有价值的其实是一个能跑起来的闭环工程。PnC 就是 Planning规划与 Control控制的合称一个决定“车往哪条路走、怎么避障”另一个决定“方向盘、油门怎么动才能跟上”。这个基础项目最大的价值是把零散模块串成一条可验证的链路设置起终点、看到规划轨迹、观察控制跟不跟得上、改动一处代码立刻看到行为变化。它不是算法论文集合而是让开发人员在最小成本内建立对 PnC 工程的心智模型。适合准备做规划或控制的工程师也适合刚接手自动驾驶相关工作、需要快速理解上下游接口的同事。我按实操顺序拆这条路先建模块地图再跑闭环然后挂代码最后把坑集中列一遍。2. 看懂 Apollo PnC 的模块地图规划与控制之间那条“看不见的交易”2.1 PnC 管的是哪一段从感知、决策到执行的完整链路先画一张信息流定位与高精地图提供车辆位置和道路结构感知输出障碍物列表预测模块给出障碍物未来轨迹路由模块给出从起点到终点的全局路径。把这些都送进 PnC 的入口之后规划模块的任务是在全局路径约束下生成一条带速度分配的局部轨迹控制模块再把这条轨迹转化成转向、油门、刹车的具体指令。这里有个新手容易忽略的点规划和控制的工作频率与数据形态差别很大。规划普遍工作在十赫兹量级它关心的是语义层面的决策——要不要变道、要不要让行、这段路限速多少控制则要跑到几十赫兹它只关心当前车辆状态和规划轨迹之间的误差如何被快速消除。把两者拆开的工程原因也在这里规划器可以容忍几百毫秒级的延迟控制器不行反过来控制器里的标定参数也不该耦合进交规决策逻辑。所以这两个模块之间真正传递的不是什么复杂结构而是一串轨迹点。每个轨迹点包含位置、朝向、曲率、速度和加速度。控制模块拿到它之后要做的就是让车辆状态尽量贴合这条曲线。理解了这条“看不见的交易”你就明白为什么规划轨迹上一处明显的曲率突变会让控制器在下一个时刻剧烈打方向——控制侧没有“意图”的概念它只有误差。2.2 从代码目录看懂 PnC 的分层planning 与 control 各自管什么打开 Apollo 的代码仓库PnC 相关代码主要集中在 modules/planning 和 modules/control 两个目录下。不同版本目录名可能略有调整整体结构如下apollo ├── modules │ ├── planning │ │ ├── planner # 规划器的组织入口 │ │ ├── tasks # 可插拔的规划任务 │ │ ├── common # 规划相关的数据结构与工具 │ │ └── conf # 规划参数配置 │ ├── control │ │ ├── controller # 具体控制器实现 │ │ ├── common # 控制与车辆状态相关 │ │ └── conf # 控制器参数配置 │ └── dreamview # 可视化与仿真交互这两层并非孤立。planning 下的配置决定车辆行驶风格比如巡航速度、跟车距离、变道时机control 下的配置决定车辆动态表现比如方向盘增益、油门响应、制动舒适度。你可以把 planning 理解为“决定做什么”control 理解为“决定做多快、做多准”。中间通过一条轨迹接口连接在代码里对应一个公共的轨迹数据结构两边都依赖它但互不调用对方的内部实现。这个解耦设计让开发者可以只改规划侧的某个 task或者只调控制器里的某组参数而不会牵连整个系统。对新手来说常动的文件其实很少planning/conf 下的限速与场景开关planning/tasks 里的某个 task 实现control/conf 下的控制器参数表。第一次接触这个仓库不要急着看懂每个目录先把 conf 文件里的常用参数和代码里的数据结构对应起来后面改代码才有抓手。目录职责新手常改内容modules/planning/tasks规划动作的最小单元新增或修改 task 逻辑modules/planning/conf场景、限速、参数开关调整速度与场景行为modules/control/controller控制器算法切换或调整控制策略modules/control/conf控制增益与限制调超调、稳态误差2.3 把环境跑起来的最小动作容器、编译与 DreamView在动手打开代码之前得先让整套环境跑起来。常见做法是使用官方提供的容器化开发方式避免在一台机器上堆依赖。各版本启动脚本略有差异最稳妥的第一步是先把仓库克隆回来再按文档要求初始化容器环境下面以较新版本常见的命令风格为例# 进入开发容器不同版本入口命令略有差异以官方文档为准 aem start # 首次编译整个工程时间会比较长 aem build这里不建议跳过编译直接去跑别人打包好的镜像。PnC 开发人员的价值就在于能修改、能重建、能验证如果只看运行结果你只是在调一个黑匣子。编译完成后再启动可视化与仿真工具浏览器里打开 DreamView 界面这里你能看到地图、车辆模型、传感器状态以及后续的规划轨迹。编译环节有两个硬性条件磁盘空间足够内存足够。全量编译对内存的需求容易超过日常开发机的心理预期后文避坑章节会展开说。如果机器配置一般可以先只编译 planning 和 control 两个模块跑通功能后再做全量编译能省下大量等待时间。提示首次编译时间很长中间不要同时跑大量其他应用。把编译当作优先级最高的任务能显著减少内存被挤爆导致的反复重编。这一章做完你已经有了一个能启动的 PnC 开发环境下一步是用它跑通第一个闭环。3. 十分钟跑通 PnC 闭环Sim Control 的最小复现路径3.1 为什么新人不建议用真实 bag 起步Sim Control 的选型逻辑第一次接触 PnC 闭环时不少新人的第一反应是找个真实路测的数据包来跑。真实数据确实最有说服力但它对调试场景并不友好数据包里的传感器、定位、地图必须完全匹配差一个时间戳都会让车辆位置漂到地图之外而且真实路测数据里包含了大量感知噪声一旦出问题你很难分清是规划不对、控制不对还是上游数据本身不对。Sim Control 模式解决了这个问题它用仿真器代替真实传感器把一条完整的车辆位置、速度、转向状态按固定频率发布给规划和控制模块。对 PnC 代码来说它并不知道上游是真实车辆还是仿真器因为它看到的主题结构完全一样。这套设计的好处是场景可重复、状态可预期、出问题时可定位。我一般会建议新人先跑 Sim Control而不是直接回放实际路测数据。原因很简单当目标车速是 20km/h 且道路平直时规划轨迹应该接近一条平滑直线如果此时控制还出现明显超调问题大概率在控制参数或车辆模型而不是数据噪声。用可控输入定位问题是调试的基本功。3.2 跑通第一个闭环起终点、轨迹与速度跟踪在 DreamView 里切换到 Sim Control 模式后剩下操作基本都是图形界面选择一个地图区域设置车辆的初始位置和朝向再在地图上点两个点作为起点和终点。确认后路由模块先找一条全局路径规划模块紧接着在全局路径上生成局部轨迹车辆模型开始运动控制模块开始跟踪。如果你想看规划和控制模块内部到底在做什么不要只看界面直接在容器里挂上 cyber 的监控工具按主题名订阅对应的输出消息# 在当前容器环境内查看各模块通信情况 cyber_monitor这个命令会拉起一个交互式监控面板里面列出当前所有活跃主题及其发布频率。重点看规划输出的轨迹主题和控制输出的指令主题规划轨迹应该持续刷新控制指令应该跟随车辆状态实时变化。如果规划轨迹不刷新说明起终点路由没成功如果控制指令长时间为零说明控制模块没收到有效轨迹或车辆模型没有进入就绪状态。跑这个最小闭环时还有一个参数要留心车辆模型的选择。Sim Control 默认的车辆动力学模型大部分时候够用但不同模型的转向延迟、加速度上限差异很大直接决定控制参数能不能收敛。出现“轨迹看着对方向盘乱打”的情况时先回去确认车辆模型和控制器标定是否一致。3.3 怎么判断 PnC 真在工作三张图与两行日志很多新手跑完一圈看到画面里车动了就以为 PnC 跑通了。实际上判断系统是否正常工作至少要看三个东西规划轨迹线是否连续平滑车辆中心位置是否贴合规划轨迹速度曲线是否符合设置的目标速度。第一眼的问题大多出在前两项轨迹线断开说明规划器在某个位置找不到合法参考线车辆与轨迹偏离大说明控制误差没压住或轨迹本身存在曲率跳变。判断的第二层是日志。界面展示的是处理后的结果日志才是原因。在容器里用关键字过滤规划模块日志是每个 PnC 开发者的基本功# 查看 planning 模块最近日志中的决策关键字 grep -i decision\|change_lane\|follow /apollo/data/log/*.INFO | tail -n 50这里 -i 参数让匹配忽略大小写tail 限制只取最后 50 行方便快速定位最新状态。这些日志会告诉你车辆当前处于哪个场景、执行了哪些决策跟车状态下轨迹速度会收敛到前车速度变道状态下参考线会切换。日志与界面状态对上后你才算真正掌握了一个可验证的闭环。此后每次改动代码都用同样方法确认行为变化这也是后文回归测试的基础。4. 从“跑通”到“改得动”挂一个自定义 Task 的最小闭环4.1 先认清最小执行单位Scenario、Stage 与 Task 的关系跑通闭环只是第一步PnC 开发人员真正的日常是改行为。改行为之前得先看懂行为在代码里是怎么被组织出来的。规划模块普遍采用场景-阶段-任务三层结构场景是道路环境的抽象比如跟车、变道、停车阶段是场景内部的状态递进比如变道前准备、变道执行、变道结束任务是阶段里真正干活的单元比如计算安全距离、调整参考线速度。这三个层级里任务是最适合下手的修改点。原因有二第一任务边界清晰输入输出都围绕参考线和障碍物展开不太需要理解整个场景的状态机第二任务之间通常是串行执行一个任务出错不会立刻拖垮整条规划链路回滚也容易。我第一次在 PnC 里动手就是先在一个跟车任务里加了一段速度补偿逻辑效果比想象中可控得多。改代码前明确一点你不需要改规划器主框架也不需要动场景决策模型。只要找到当前场景对应阶段的任务列表在合适位置插入或替换一个任务即可。这种插件式结构是规划架构里刻意保留的扩展口。4.2 加一个 Task 的最小代码改法Init 与 Execute 的职责边界一个最小的任务在代码上只关心两件事初始化时读取配置执行时修改参考线信息或轨迹。下面是一个自定义任务骨架常见实现里都围绕这两个方法展开// 自定义Task骨架基于参考线限制速度 class DemoSlowDownTask final : public Task { public: bool Init(const TaskConfig config) override { // 在这里读取限速阈值等参数 // 例如从 config 里读一个 soft_limit_speed return true; } bool Execute(Frame* frame, ReferenceLineInfo* reference_line_info) override { // 从 reference_line_info 拿到参考线与障碍物状态 // 对超出限速的轨迹点做调整 // 修改的是轨迹速度不是车辆状态 return true; } };不要小看这两个方法的职责边界Init 只负责把外部配置转成内部成员变量不要在 Init 里做任何与当前帧相关的判断Execute 只处理当前帧数据不要在里面保存跨帧状态。很多新手在这个环节翻车是在 Execute 里缓存了上一帧的障碍物信息车辆已经越过障碍物逻辑却还按缓存数据做决策。写完代码后要把这个任务注册进对应阶段的任务列表里并在规划配置中打开开关。常见做法是在配置文件中把新任务类型加进指定任务序列同时确认开关不是默认关闭。不同版本差异不小最直接的办法是找一个已有任务做参照复制它的注册方式、改名、替换实现。4.3 调试闭环编译、跑场景、看决策日志代码加好后进入验证循环。先只编译改动的模块而不是全量编译# 只编译 planning 模块速度比全量编译快很多 aem build planning编译通过后回到 Sim Control 跑一遍刚才的场景。此时不要急着看车辆跑得好不好先看两件事新任务有没有被加载有没有出现在决策日志里。如果日志里根本找不到新任务输出多半是注册没生效或配置开关没打开这时候检查注册文件比检查算法逻辑更有用。调试循环里容易犯的错是每次改完配置就重启整个系统。规划模块在大多数情况下可以单独重启只需要确保上下游主题重新连接即可。具体方式依版本而异但原则一致能只重启一个模块就不要动其他进程。这样每次迭代间隔可以从几分钟压缩到几十秒长时间下来省出的时间很可观。注意调试时保持场景不变只改一处参数观察行为差异。如果同时改了任务逻辑和限速参数出了问题很难判断是哪个改动导致。这个环节的血泪经验是先让系统“错得明确”再谈“改得正确”。我见过太多人一上来就写一大段新逻辑编译过了就以为完成结果行为异常时又怀疑到一大堆无关模块上去。把改动缩小、输出打全、场景锁死是 PnC 调试唯一的捷径。5. Apollo PnC 开发避坑指南编译、数据与坐标系的五个翻车现场5.1 编译环节的两个经典坑坑一编译中途进程消失机器卡死日志没有明确报错。现象终端先卡住接着某些编译进程被系统终止界面上看不到具体错误。原因几乎都是内存不足全量编译时多线程并行会在短时间内同时吃进大量内存物理内存不够时进程直接被系统杀掉。解决方法是限制并行任务数# 限制并行任务避免编译进程被内存挤出 aem build --jobs 2--jobs 2 的含义是让编译器最多同时跑两个编译任务降低内存峰值。如果机器确实吃紧可以只编译 planning 或 control 模块按需构建。这类问题不会出现在官方示例的“顺利路径”里但几乎是每个本地开发者的必经关卡。坑二编译明明成功启动时提示找不到某个动态库。我第一次遇到时第一反应是重新编译结果反复编译也没用。实际原因通常是缓存信息没刷新或者编译产物被部分清理过。解决方法是重建并刷新动态库缓存而不是手动删除库文件。手动删库文件是这条路上最危险的“后悔药”你根本不知道它被哪个模块依赖。5.2 数据回放里的两个坑坑三回放数据包后车辆位置漂到地图外或者整台车出现在另一个区域。现象上看起来像定位或地图问题多数时候真正原因只有一个数据包里的定位主题格式与当前地图或车辆坐标系不匹配又或者包内时间戳沿时间轴出现跳变。处理方式很简单新手阶段优先用 Sim Control 替代真实数据起步。如果必须用数据包先检查定位主题的时间戳是否连续再确认地图和数据包录制场景一致。坑四回放速度比预期快或慢控制曲线明显抖动。这个坑容易被误判成控制器问题。实际上回放工具普遍提供一个速率参数如果不保持实时速率回放规划与控制模块收到的数据流节奏不稳定控制器的滤波和积分参数是按实时节奏标定的时序一变抖动自然出现。解决方式是把回放速率固定在 1.0并保证 CPU 不过载。回放不是加速看视频它也是系统测试的一部分。5.3 参考线与 Frenet 坐标系的三个易错点坑五参考线方向反了规划目标点始终跑在车道反向侧。这个坑看起来像玄学实际是坐标系变换时混用了车辆坐标系与全局坐标系。规划内部普遍使用以参考线为基准的 Frenet 坐标纵向 s 表示沿参考线方向走了多远横向 l 表示偏离参考线侧向多少。s 和 l 的语义完全依赖参考线起点与方向一旦参考线方向取反整个决策结果都会错位。验证方向有一个简单办法取参考线上相邻两个点计算从起点指向终点的朝向角再和车辆在全局坐标系下的行驶方向做叉乘比对符号不一致就说明参考线方向有问题。这类问题在仿真里出现得不多但自己做数据集或导入外部地图时非常常见。遇到“规划轨迹与车道方向相反”的现象先查参考线不要直接怀疑决策算法。6. 更进一步把 PnC 改动变成可验证的工程习惯6.1 把 Planning 的决策输出当“黑匣子”来读调试效率高的开发者几乎不看界面里的漂亮曲线而是先读决策输出。开启规划模块的调试日志后每个任务的决策内容会按帧输出。把决策当成黑匣子的输入输出输入是参考线和障碍物输出是决策标签和轨迹。改代码前后各存一份行为差异一目了然。6.2 给控制器做一次阶跃响应测试控制器调参不要全凭感觉给目标速度一个阶跃记录跟踪曲线。比如让目标速度从 0 跳到 30km/h把车速输出按时间存成 CSV用一段简单脚本看超调和稳定时间import pandas as pd df pd.read_csv(speed_step.csv) overshoot (df[speed].max() - 30.0) / 30.0 * 100 # 超调百分比 print(fovershoot: {overshoot:.1f}%)有了数字调参才不是玄学。每次调完把数据文件名和参数一起存档隔周回来还能对上。6.3 让回归测试替你守门最后一次改动可能影响十公里外的场景纯靠眼睛看不出问题。把常用场景的仿真结果保存下来用脚本比对改动前后的轨迹差异差异超过阈值就报警。养成这个习惯后我改代码不再心慌因为你永远知道上一次正常行为长什么样。希望帮到你。本文还有配套的精品资源点击获取