
1. 拆解 Apollo 6.0 Planning 模塊的 Protobuf 定義從消息結(jié)構(gòu)到工程實踐Apollo 6.0 的 Planning 模塊是整個自動駕駛系統(tǒng)里最“燒腦”的部分之一它負(fù)責(zé)把上游感知、定位、地圖、預(yù)測的結(jié)果揉在一起算出一條安全、平滑、可執(zhí)行的軌跡。而 Protobuf 定義就是這套復(fù)雜邏輯的“骨架”和“接口契約”。很多剛接觸 Apollo 的朋友一上來就扎進(jìn) EM Planner、Lattice Planner 的算法細(xì)節(jié)結(jié)果被各種proto文件里的字段繞得暈頭轉(zhuǎn)向。其實把 Planning 模塊的 Protobuf 定義吃透你就能像看地圖一樣清楚知道數(shù)據(jù)從哪來、到哪去、中間經(jīng)過了哪些關(guān)鍵節(jié)點。這篇文章我就結(jié)合自己實際調(diào)試和二次開發(fā)的經(jīng)驗把 Apollo 6.0 Planning 模塊的 Protobuf 定義從頭到尾捋一遍。不管你是剛?cè)胄械淖詣玉{駛軟件工程師還是想基于 Apollo 做二次開發(fā)的愛好者只要你想搞清楚 Planning 模塊到底怎么“思考”、怎么“決策”這些內(nèi)容都能直接拿來參考。1.1 為什么 Protobuf 在 Planning 模塊里如此關(guān)鍵Apollo 是一個典型的分布式、模塊化系統(tǒng)。感知、預(yù)測、規(guī)劃、控制這些模塊可能跑在不同的進(jìn)程甚至不同的計算單元上。它們之間要交換數(shù)據(jù)就得有一套高效、跨語言、結(jié)構(gòu)化的序列化方案。Protobuf 正好滿足這些需求二進(jìn)制傳輸效率高、字段兼容性好、代碼生成機制成熟。在 Planning 模塊里Protobuf 不僅僅是“數(shù)據(jù)容器”它更是模塊間通信的“合同”。比如預(yù)測模塊輸出一個PredictionObstacles規(guī)劃模塊拿到后會把它轉(zhuǎn)換成內(nèi)部需要的障礙物信息再結(jié)合地圖和定位生成最終的ADCTrajectory。如果你不理解這些 proto 里每個字段的含義調(diào)試時看到一堆二進(jìn)制或者 JSON 輸出根本無從下手。我剛開始調(diào) Planning 的時候遇到過一個典型問題車輛在路口莫名其妙急剎。查了半天最后發(fā)現(xiàn)是PredictionObstacles里某個障礙物的priority字段被錯誤地設(shè)成了高優(yōu)先級導(dǎo)致規(guī)劃器把它當(dāng)成了必須避讓的目標(biāo)。如果當(dāng)時我能快速定位到 proto 定義看看這個字段的取值邏輯可能幾分鐘就解決了。所以熟悉 Protobuf 定義不只是“看懂代碼”更是提升調(diào)試效率、避免踩坑的基本功。1.2 Apollo 6.0 Planning 模塊的 Proto 文件組織方式Apollo 的代碼倉庫里modules/planning/proto目錄下存放著 Planning 模塊所有的 Protobuf 定義。這些文件大致可以分成幾類第一類是規(guī)劃器配置類比如planning_config.proto、planner_config.proto它們定義了各種規(guī)劃算法的參數(shù)和開關(guān)第二類是決策與場景類比如decision.proto、scenario.proto描述了規(guī)劃器在不同場景下的決策邏輯第三類是軌跡與調(diào)試類比如ADCTrajectory.proto、planning_internal.proto它們定義了最終輸出的軌跡和內(nèi)部調(diào)試信息。這些 proto 文件之間互相引用形成了一個有層次的依賴關(guān)系。比如PlanningConfig里會包含PlannerConfig而ADCTrajectory里又會引用Decision和PlanningDebug。理解這種組織方式你就能在需要修改或擴展功能時快速找到對應(yīng)的文件而不是像無頭蒼蠅一樣亂翻。2. 核心 Proto 消息逐個拆解從配置到軌跡2.1 PlanningConfig規(guī)劃模塊的“總控面板”PlanningConfig是 Planning 模塊的頂層配置它決定了規(guī)劃器用哪種算法、以什么頻率運行、在哪些場景下啟用哪些功能。這個 proto 里最關(guān)鍵的字段是planner_type它指定了當(dāng)前使用的規(guī)劃器類型比如EM、LATTICE、NAVI等。Apollo 6.0 默認(rèn)使用的是PUBLIC_ROAD模式內(nèi)部會根據(jù)場景動態(tài)切換規(guī)劃器。另一個重要字段是default_task_config它包含了任務(wù)級別的配置比如DP_POLY_PATH_OPTIMIZER、QP_SPEED_OPTIMIZER等。這些任務(wù)配置直接影響了軌跡生成的平滑度和計算耗時。在實際項目中我經(jīng)常需要調(diào)整PlanningConfig里的planning_cycle_time和emergency_planning_cycle_time。前者是正常規(guī)劃周期默認(rèn) 100ms后者是緊急情況下的規(guī)劃周期默認(rèn) 50ms。如果你發(fā)現(xiàn)車輛在緊急避障時反應(yīng)不夠快可以適當(dāng)降低這個值但要注意計算負(fù)載。另外PlanningConfig里還有一個enable_scenario_based_planning開關(guān)打開后會啟用基于場景的規(guī)劃框架這是 Apollo 6.0 的一個重要特性。我建議在調(diào)試新場景時先把這個開關(guān)打開這樣能更清晰地看到每個場景的決策過程。2.2 ADCTrajectory規(guī)劃模塊的“最終答卷”ADCTrajectory是 Planning 模塊輸出的核心消息它包含了車輛未來一段時間內(nèi)的完整軌跡。這個 proto 里最重要的字段是trajectory_point它是一個數(shù)組每個點包含了path_point和v、a、relative_time等信息。path_point里又有x、y、z、theta、kappa、s等字段分別表示位置、航向角、曲率、縱向位移。這些數(shù)據(jù)直接送給控制模塊控制模塊再根據(jù)它們計算油門、剎車和轉(zhuǎn)向。我踩過的一個坑是ADCTrajectory里的trajectory_point數(shù)組長度不是固定的。它取決于規(guī)劃的時間窗口和采樣間隔。如果你在代碼里硬編碼了數(shù)組長度一旦配置改變就會越界。正確的做法是動態(tài)遍歷或者用trajectory_point_size()獲取實際長度。另外ADCTrajectory里還有一個estop字段當(dāng)規(guī)劃器無法生成安全軌跡時會置位這個字段控制模塊收到后會執(zhí)行緊急停車。調(diào)試時如果看到車輛突然停下先檢查這個字段往往能快速定位問題。2.3 Decision規(guī)劃器的“思考過程”Decision這個 proto 記錄了規(guī)劃器在每個規(guī)劃周期做出的決策比如跟車、換道、避讓、停車等。它里面最核心的是main_decision包含了stop、cruise、follow、overtake等子決策。每個子決策又有自己的參數(shù)比如stop里有stop_point和reasonfollow里有target_obstacle_id和distance。這些信息對于調(diào)試非常有用因為它們能告訴你規(guī)劃器“為什么”這么做。舉個例子有一次車輛在跟車時距離前車過近我查看Decision里的follow決策發(fā)現(xiàn)target_obstacle_id指向的障礙物并不是正前方的車輛而是一個相鄰車道被誤判的物體。順著這個線索我檢查了預(yù)測模塊的輸出發(fā)現(xiàn)那個障礙物的位置估計有偏差。如果沒有Decision這個 proto我可能得花大量時間在日志里大海撈針。所以我強烈建議在調(diào)試時把Decision信息打印出來或者用 Apollo 的可視化工具查看它能極大提升排查效率。2.4 PlanningInternal調(diào)試信息的“百寶箱”PlanningInternal是 Planning 模塊內(nèi)部使用的調(diào)試信息 proto它包含了大量的中間計算結(jié)果比如DP_StGraph、SpeedPlan、PathPlan等。這些信息在正常運行時不會輸出但當(dāng)你需要深入分析規(guī)劃器的行為時它們就是無價之寶。比如DP_StGraph里的total_s和total_t能告訴你動態(tài)規(guī)劃在時空圖上的搜索范圍SpeedPlan里的speed_point能展示速度曲線的細(xì)節(jié)。我通常會在planning.conf里把enable_planning_debug設(shè)為true這樣PlanningInternal就會被填充并發(fā)布到/apollo/planning_internal話題上。然后用rosbag錄下來再用 Python 腳本解析。這個過程雖然有點繁瑣但當(dāng)你需要優(yōu)化軌跡平滑度或者分析規(guī)劃器為什么選擇某條路徑時這些數(shù)據(jù)能提供最直接的證據(jù)。需要注意的是打開調(diào)試信息會增加計算和通信開銷所以只在調(diào)試階段使用上線時要關(guān)掉。3. 實操如何基于 Proto 定義進(jìn)行二次開發(fā)與調(diào)試3.1 環(huán)境準(zhǔn)備與 Proto 編譯在開始二次開發(fā)之前你需要確保 Apollo 的編譯環(huán)境已經(jīng)就緒。Apollo 6.0 使用 Bazel 作為構(gòu)建工具Proto 文件的編譯也是通過 Bazel 自動完成的。你不需要手動運行protoc只要在BUILD文件里正確聲明依賴Bazel 就會生成對應(yīng)的 C 和 Python 代碼。不過如果你需要單獨查看某個 proto 生成的代碼可以在bazel-bin目錄下找到。比如modules/planning/proto/ADCTrajectory.pb.h就是編譯后生成的 C 頭文件。我建議在修改任何 proto 之前先運行一次完整的編譯確?;€是干凈的。命令很簡單./apollo.sh build。如果編譯失敗先解決環(huán)境問題再動代碼。另外Apollo 的 Proto 文件里有很多optional和repeated字段修改時要注意兼容性。比如如果你把一個optional字段改成repeated可能會導(dǎo)致舊版本的代碼解析失敗。所以在團隊協(xié)作中修改 proto 一定要謹(jǐn)慎最好遵循“只增不改”的原則。3.2 自定義規(guī)劃器配置的實操步驟假設(shè)你想調(diào)整 EM Planner 的路徑優(yōu)化權(quán)重讓軌跡更貼近車道中心線。這個參數(shù)在EMPlannerConfig里具體是path_optimizer_config下的lateral_weight。你可以直接修改modules/planning/proto/em_planner_config.proto里的默認(rèn)值但更推薦的做法是在planning.conf里覆蓋。Apollo 的配置系統(tǒng)支持從文件加載配置你可以在modules/planning/conf/planning.conf里添加一行--lateral_weight1.5。這樣就不需要重新編譯改完重啟 Planning 模塊即可生效。不過這里有個細(xì)節(jié)planning.conf里的參數(shù)名必須和 proto 里的字段名完全一致包括大小寫。我見過有人寫成--lateralWeight1.5結(jié)果配置沒生效查了半天才發(fā)現(xiàn)是命名風(fēng)格不匹配。另外修改配置后最好用cyber_monitor看一下/apollo/planning話題的輸出確認(rèn)軌跡確實發(fā)生了變化。如果沒變化可能是配置沒加載成功或者被其他配置覆蓋了。3.3 利用 PlanningInternal 進(jìn)行軌跡分析的完整流程前面提到PlanningInternal是調(diào)試?yán)鬟@里我詳細(xì)說一下怎么用。首先確保planning.conf里enable_planning_debug為true。然后啟動 Apollo讓車輛或模擬器跑一段。接著用rosbag record /apollo/planning_internal錄制數(shù)據(jù)。錄完后你可以用 Apollo 自帶的planning_trajectory_analyzer工具或者自己寫 Python 腳本解析。解析時重點關(guān)注DP_StGraph里的total_s和total_t它們能告訴你規(guī)劃器在時空圖上的搜索范圍。如果total_s太小可能意味著規(guī)劃器沒有充分考慮遠(yuǎn)距離的障礙物如果total_t太大可能會增加計算負(fù)擔(dān)。我通常會把SpeedPlan里的speed_point畫成速度-時間曲線直觀地看加速、減速是否平滑。有一次我發(fā)現(xiàn)曲線在某個點突然抖動檢查后發(fā)現(xiàn)是QP_SPEED_OPTIMIZER的max_acceleration設(shè)得太大導(dǎo)致求解器在邊界附近震蕩。把值調(diào)小后曲線就平滑了。這個經(jīng)驗告訴我調(diào)試規(guī)劃器不能只看最終軌跡中間過程的數(shù)據(jù)往往更能說明問題。4. 常見問題與排查技巧實錄4.1 Proto 字段解析失敗或默認(rèn)值異常在實際調(diào)試中最常見的問題之一是 Proto 字段解析失敗或者讀出來的值是默認(rèn)值。這通常有幾個原因一是 proto 文件版本不匹配比如你用的代碼是舊版但數(shù)據(jù)是新版序列化的二是字段被optional修飾但發(fā)送方?jīng)]有設(shè)置接收方就會拿到默認(rèn)值三是網(wǎng)絡(luò)傳輸過程中數(shù)據(jù)被截斷。排查時可以先檢查cyber_monitor里話題的發(fā)布頻率和消息大小如果消息大小異常小可能是序列化出了問題。另外Apollo 的日志里通常會有protobuf相關(guān)的錯誤信息比如Failed to parse看到這類日志就要警惕。我遇到過一次ADCTrajectory里的trajectory_point數(shù)量總是 0。查了半天發(fā)現(xiàn)是發(fā)送方在填充 proto 時忘記調(diào)用add_trajectory_point()而是直接用了mutable_trajectory_point()但沒有設(shè)置大小。這種錯誤在 C 里很隱蔽因為編譯不會報錯。所以在寫填充 proto 的代碼時一定要仔細(xì)檢查add_和mutable_的使用場景。4.2 規(guī)劃器決策與預(yù)期不符的排查思路當(dāng)規(guī)劃器的決策和你的預(yù)期不符時比如該換道時不換道該停車時不停車第一步應(yīng)該是查看Decisionproto 里的main_decision。如果main_decision是cruise但你認(rèn)為應(yīng)該follow那就要檢查PredictionObstacles里是否有障礙物被正確識別。有時候障礙物的priority或is_static字段設(shè)置不當(dāng)會導(dǎo)致規(guī)劃器忽略它。第二步是檢查PlanningInternal里的PathPlan和SpeedPlan看看路徑和速度曲線是否合理。如果路徑規(guī)劃正常但速度規(guī)劃異常那問題可能出在QP_SPEED_OPTIMIZER的約束上。我總結(jié)了一個簡單的排查順序先看Decision再看PlanningInternal最后看ADCTrajectory。這個順序能幫你快速縮小問題范圍。另外Apollo 提供了一個planning_visualizer工具可以把這些 proto 信息可視化出來比看日志直觀得多。如果條件允許我強烈建議用可視化工具輔助排查。4.3 性能問題與 Proto 大小的權(quán)衡Planning 模塊對實時性要求很高通常要在 100ms 內(nèi)完成一次規(guī)劃。如果 Proto 消息太大序列化和反序列化的開銷就會增加進(jìn)而影響整體性能。PlanningInternal雖然有用但它包含大量中間數(shù)據(jù)如果一直開啟會顯著增加 CPU 和內(nèi)存負(fù)擔(dān)。所以我的經(jīng)驗是只在調(diào)試階段開啟enable_planning_debug并且盡量縮小錄制范圍只錄關(guān)鍵話題。另外ADCTrajectory里的trajectory_point數(shù)量也要控制一般 50-100 個點就足夠控制模塊使用了太多點不僅浪費帶寬還可能引入不必要的抖動。還有一個容易被忽視的點Proto 里的string字段。比如Decision里的reason字段如果填充了很長的字符串也會增加消息大小。在性能敏感的場合可以考慮用枚舉代替字符串或者限制字符串長度。這些細(xì)節(jié)在開發(fā)初期可能不明顯但到了系統(tǒng)集成階段往往會成為性能瓶頸。4.4 常見問題速查表問題現(xiàn)象可能原因排查方法解決建議軌跡點數(shù)量為 0發(fā)送方未正確填充trajectory_point檢查add_trajectory_point()調(diào)用確保每次規(guī)劃都添加至少一個點規(guī)劃器忽略障礙物PredictionObstacles里priority或is_static設(shè)置錯誤查看Decision里的follow或stop決策調(diào)整預(yù)測模塊的優(yōu)先級邏輯速度曲線抖動QP_SPEED_OPTIMIZER的max_acceleration過大分析PlanningInternal里的SpeedPlan適當(dāng)降低加速度上限配置不生效planning.conf里參數(shù)名與 proto 字段名不匹配對比 proto 文件和配置文件確保命名完全一致包括大小寫調(diào)試信息缺失enable_planning_debug未開啟檢查planning.conf調(diào)試時設(shè)為true上線時關(guān)閉這個表是我在實際項目中慢慢積累的每次遇到新問題我都會補充進(jìn)去。現(xiàn)在它已經(jīng)成了我排查 Planning 問題的第一手資料。5. 從 Proto 定義看 Planning 模塊的擴展與優(yōu)化5.1 如何安全地擴展 Proto 字段Apollo 的 Planning 模塊還在不斷演進(jìn)你很可能需要添加新的字段來支持新功能。比如你想在ADCTrajectory里增加一個confidence字段表示軌跡的置信度。這時候一定要遵循 Protobuf 的兼容性規(guī)則新增字段必須用新的字段編號不能復(fù)用已刪除字段的編號。另外新字段最好是optional或repeated避免用required因為required在跨版本通信時容易出問題。修改完 proto 后記得重新編譯并更新所有依賴該 proto 的代碼。如果團隊里有多個分支最好提前溝通避免沖突。我個人的習(xí)慣是在修改 proto 時會在文件頭部加一行注釋說明修改日期和原因。這樣以后回溯時能快速知道這個字段是為什么加的。雖然看起來有點繁瑣但在長期維護(hù)的項目里這個習(xí)慣能省下很多溝通成本。5.2 基于 Proto 的規(guī)劃器性能調(diào)優(yōu)經(jīng)驗Planning 模塊的性能調(diào)優(yōu)很大程度上是在 Proto 配置里調(diào)參數(shù)。比如DP_POLY_PATH_OPTIMIZER里的sample_points數(shù)量直接影響路徑搜索的精度和耗時。如果sample_points太少路徑可能不夠平滑太多計算時間會線性增長。我通常的做法是先在模擬環(huán)境里用默認(rèn)值跑一遍記錄耗時和軌跡質(zhì)量然后逐步增加sample_points觀察變化找到一個平衡點后再在實車上驗證。這個過程需要耐心但效果通常很顯著。另一個關(guān)鍵參數(shù)是planning_cycle_time。Apollo 默認(rèn)是 100ms但在某些復(fù)雜場景下規(guī)劃器可能算不過來。這時候你可以考慮降低規(guī)劃頻率比如改成 150ms給規(guī)劃器更多時間。但要注意降低頻率會影響控制的實時性所以必須配合控制模塊的調(diào)參。我試過在低速園區(qū)場景下把planning_cycle_time調(diào)到 200ms車輛依然運行平穩(wěn)但高速場景下就不行了。所以這個參數(shù)一定要結(jié)合具體場景來定。5.3 利用 Proto 進(jìn)行跨模塊協(xié)同調(diào)試Planning 模塊不是孤立的它和感知、預(yù)測、控制都有緊密的交互。通過 Proto 定義你可以清晰地看到模塊間的數(shù)據(jù)流。比如PredictionObstacles從預(yù)測模塊傳到規(guī)劃模塊規(guī)劃模塊處理后生成ADCTrajectory再傳給控制模塊。如果車輛行為異常你可以沿著這條數(shù)據(jù)流逐個模塊檢查 Proto 消息。我常用的方法是在cyber_monitor里同時打開/apollo/prediction、/apollo/planning和/apollo/control三個話題對比它們的輸出。如果預(yù)測模塊輸出的障礙物位置和規(guī)劃模塊使用的障礙物位置不一致那問題就出在數(shù)據(jù)傳遞或轉(zhuǎn)換環(huán)節(jié)。這種跨模塊調(diào)試方法能幫你快速定位問題是在哪個模塊產(chǎn)生的而不是盲目地改代碼。我見過很多新手一遇到問題就懷疑規(guī)劃算法結(jié)果查了半天發(fā)現(xiàn)是感知模塊的誤差。所以熟悉 Proto 定義不僅能讓你看懂 Planning 模塊還能讓你對整個自動駕駛系統(tǒng)的數(shù)據(jù)流有更全面的認(rèn)識。5.4 一個真實的調(diào)試案例換道決策異常最后分享一個我實際遇到的案例。有一次車輛在高速上應(yīng)該換道超車但規(guī)劃器一直不執(zhí)行。我按照前面的排查順序先看Decision發(fā)現(xiàn)main_decision是cruise沒有overtake子決策。然后看PlanningInternal里的PathPlan發(fā)現(xiàn)路徑規(guī)劃器確實生成了一條換道路徑但速度規(guī)劃器沒有跟上。再查QP_SPEED_OPTIMIZER的配置發(fā)現(xiàn)max_lateral_acceleration設(shè)得太小導(dǎo)致速度規(guī)劃器認(rèn)為換道時橫向加速度會超限于是拒絕了換道。把max_lateral_acceleration從 1.0 調(diào)到 1.5 后換道就正常了。這個案例說明Proto 里的參數(shù)不是孤立的它們之間會相互影響。調(diào)試時不能只看一個字段要結(jié)合多個 Proto 消息一起分析。另外這個案例也提醒我配置參數(shù)的默認(rèn)值不一定適合所有場景必須根據(jù)實際道路條件和車輛特性進(jìn)行調(diào)整。每次調(diào)整后最好用rosbag錄下來反復(fù)回放分析確保改動確實解決了問題而不是引入了新的問題。5.5 關(guān)于 Proto 定義維護(hù)的個人體會在長期維護(hù) Planning 模塊的過程中我最大的體會是Proto 定義不僅僅是技術(shù)文檔它更是團隊協(xié)作的“共同語言”。當(dāng)算法工程師、軟件工程師和測試工程師都基于同一套 Proto 定義來溝通時很多歧義和誤解就會自然消失。所以我建議在項目初期就建立 Proto 定義的評審機制任何修改都要經(jīng)過團隊討論確保每個人都理解字段的含義和影響。另外定期更新 Proto 定義的注釋把每個字段的用途、取值范圍、默認(rèn)值都寫清楚這對后來者非常友好。還有一點不要害怕修改 Proto。雖然修改 Proto 可能會帶來一些兼容性問題但只要遵循規(guī)則這些問題都是可控的。相反如果因為害怕修改而把一些臨時邏輯硬編碼在代碼里后期維護(hù)會更痛苦。我在早期項目中就犯過這個錯誤把一些配置寫死在 C 代碼里結(jié)果每次調(diào)整都要重新編譯效率極低。后來改成 Proto 配置后調(diào)試效率提升了好幾倍。所以該用 Proto 的地方就用 Proto這是 Apollo 設(shè)計哲學(xué)的一部分。