
前言上一篇文章從Kruchten的41視圖視角分析了RL_SAR軟件架構。Kruchten視圖模型已經回答「從哪幾個角度看軟件」但換電機協議會不會影響策略相關代碼這件事圖上不一定看得出。四層回答的是邏輯視圖里控制棧到底按什么切開四層補的是一條領域約束。換 SDK / 仿真后端應只動 HAL。發軟、跟不住應先查實時層的 dt 和 PD而不是先改網絡。換 himloco.pt應只動決策配置與觀測清單。加「按 3 跳舞」應只加 FSM 技能和 YAML。這些是變更影響規則Kruchten 41視圖不會自動寫出來所以有必要補四層模型分析否則 sim2real、換機型、換策略的邊界是模糊的。為什么必須分層可以把整機想成一家餐廳灶臺和鍋電機、IMU、通信協議各家都不一樣。后廚節奏幾毫秒發一次力矩、PD 跟蹤必須穩定客人等不起。主廚拍板看姿態、推策略、決定下一步動作可以慢一點但必須看對信息。前廳點單走路、跳舞、接導航才是用戶真正要的服務。如果不分層換一臺機器人就要把 PD、觀測、策略、按鍵一起改一遍。分層之后換機型主要改 HAL換策略主要改感知決策的配置加一個「跳舞」技能只動業務層的狀態機。四層之間只通過窄接口說話下層上報「現在關節在哪、身體怎么傾斜」上層只下發「目標位置 / 速度 / 剛度」。上層不關心這是真機還是仿真下層不關心當前是在走路還是在跳舞。四層職責對照如下層級典型頻率核心問題在 rl_sar 中的落點業務應用層人機交互級現在要走路、跳舞還是跟導航走fsm_*.hpp技能狀態、Control、/cmd_vel、policy/*/config.yaml感知決策層~50 Hz看見什么、決定下一步動作ComputeObservation()、InferenceRuntime、FSM、MotionLoader實時運動控制層~200 Hz如何按時、安全地把目標變成力矩LoopFunc、RLControl()、Interpolate()、robot_joint_controllerHAL 硬件抽象層總線級怎么把各家硬件讀成同一份結構RobotState / RobotCommand、GetState()/SetCommand()1. HAL 硬件抽象層1.1 什么是 HAL 硬件抽象層硬件抽象層Hardware Abstraction Layer它把各家機器人/電機/總線的差異擋在下面向上提供統一的 RobotState / RobotCommand 以及 GetState() / SetCommand() 這類接口。HAL 的目標只有一句話上層永遠面對同一份狀態和同一份指令不要面對廠商協議。工業上常見的做法是定義統一數據結構關節位置、速度、估計力矩、IMU 四元數。用適配器把廠商 SDK / 仿真器填進這份結構。用一張關節映射表處理「訓練時的關節順序」和「SDK 里的關節順序」不一致。沒有 HAL策略網絡的輸入順序會和電機編號綁死。換一臺 Go2或從仿真遷到真機策略就要重訓或手改下標。1.2 該層項目示例rl_sar項目的 rl_sdk.hpp 里定義了兩份核心結構。讀上來的叫 RobotStateIMU 電機狀態寫下去的叫 RobotCommand每個關節的 q / dq / tau / kp / kd。基類 RL 只聲明兩個純虛函數GetState(RobotState*)把硬件讀成統一狀態SetCommand(const RobotCommand*)把統一指令寫回硬件真正干活的是子類RL_Real真機、RL_SimGazebo、MuJoCo 仿真。上層 RobotControl() 永遠是同一句先 GetState再跑狀態機再 SetCommand。以 Unitree Go2 真機為例。GetState() 從 DDS 話題 rt/lowstate 取出 IMU 四元數、陀螺儀再按 joint_mapping 把電機 q / dq / tau_est 填進 RobotState。SetCommand() 則組裝 LowCmd_寫回 rt/lowcmd并補上 CRC。上層完全看不到 0xFE 0xEF 這種協議頭。仿真側走另一條路Gazebo 里沒有廠商 DDS而是 robot_msgs/MotorCommand 下發、MotorState 回讀真正把「位置目標」變成「仿真力矩」的是 robot_joint_controller 插件。對決策層來說兩邊都是同一份 RobotState。L4W4 更「直接」自己的 UDP SDK 解析 MCU 報文。HAL 的價值在這里特別明顯——策略代碼一行都不用為 UDP 改。1.3 關節映射訓練空間 ≠ 硬件空間Go2 的 base.yaml 里關節按 FR / FL / RR / RL 排列himloco 策略的 joint_mapping 卻是 [3, 4, 5, 0, 1, 2, 9, 10, 11, 6, 7, 8]。也就是說觀測和動作按訓練順序排讀寫電機按 SDK 順序排。 G1 的 whole-body tracking 策略映射更長因為 29 個自由度的訓練順序和 Unitree HG SDK 順序也不一樣。可以把它理解成「插座轉換頭」墻上的孔電機編號各家不同插頭策略輸出必須經過 mapping 才能插上。仿真和真機還有一個容易踩坑的細節角速度坐標系。項目在觀測里用 ang_vel_axis 區分 body 和 world——ROS1 Gazebo 是世界系ROS2 / MuJoCo / 真機是機體坐標系。這也屬于 HAL 向決策層「翻譯物理含義」的一部分。2. 實時運動控制層2.1 實時運動控制層職責實時運動控制層的主責是在固定短周期內把上層給的關節目標變成持續、安全的伺服推理來不及也不能停發力。 它不管「往哪走、跳什么舞」只保證在 5 ms 節拍上給電機下發目標位置、目標速度等。和相鄰層的邊界上層給的是目標本層給的是「每拍都有效的伺服指令」。本層不讀 DDS/UDP 報文格式這些是HAL做的事。這一層有三條鐵律周期確定用獨立線程按固定 dt 跑必要時綁 CPU。控制律簡單關節級 PD阻抗足夠快、足夠好懂。安全兜底力矩限幅、姿態保護、柔順下電必須能在這一層直接生效不能等神經網絡想完再救。強化學習策略通常跑在 50 Hz 量級推理有開銷而電機伺服需要 200 Hz 甚至更高。中間用 decimation抽稀控制環每拍都發 PD 指令策略每隔 N 拍才更新一次目標。兩次推理之間關節仍然跟著上一拍的 q* / dq* 走。2.2 該層項目示例Go2 的 base.yaml 里dt: 0.0055 ms200 Hzdecimation: 4。啟動時拉起三條 LoopFunc 線程線程周期職責loop_controldt 5 msGetState → StateControllerFSM→ SetCommandloop_rldt × decimation 20 ms組觀測、推理、ComputeOutput結果推進并發隊列loop_keyboard50 ms鍵盤人機接口不進實時熱路徑策略線程和伺服線程之間用 tbb::concurrent_queue 解耦推理慢了控制環仍用上一拍目標而不是卡住不發指令。Forward() 里對模型互斥鎖用 try_lock——正在切換策略文件時直接沿用上一拍 action避免 200 Hz 環被加載模型堵住。2.3 該層運控邏輯策略輸出的不是原始電流而是歸一化動作。ComputeOutput() 做三件事actions × action_scale 得到相對默認姿態的增量。普通關節變成位置目標 q* default_dof_pos Δq輪足的輪子關節走速度目標wheel_indices。軟件側預計算力矩τ K p ( q ? ? q ) ? K d q ˙ \tau K_p (q^{*} - q) - K_d \dot{q}τKp?(q??q)?Kd?q˙?再按 torque_limits 限幅。真機上Unitree 電機固件會再跑一遍阻抗τ K p ( q ? ? q ) K d ( q ˙ ? ? q ˙ ) τ f f \tau K_p(q^{*} - q) K_d(\dot{q}^{*} - \dot{q}) \tau_{\mathrm{ff}}τKp?(q??q)Kd?(q˙???q˙?)τff?。仿真里沒有電機固件就由 robot_joint_controller::UpdateFunc() 用同一條公式把 effort 寫進 Gazebo。所以仿真和真機都是這條 PD只是執行地點不同。起身、趴下不走神經網絡而走 Interpolate()在若干個 5 ms 周期里把當前關節角線性插到 default_dof_pos剛度用 fixed_kp / fixed_kd比 RL 的 rl_kp / rl_kd 更「站得住」。這是實時層的經典手法——大行程用插值精細運動才交給策略。被動模式更直接kp 0、kd 8、tau 0相當于關節變阻尼器人可以按倒機器人而不會硬頂。這是實時層的安全態不經過策略。3. 感知決策層3.1 感知決策層職責這一層回答兩件事——「現在是什么情況」和「下一步做什么」在傳統機器人里這里往往是狀態估計 規劃 WBC。強化學習部署里對應關系變成感知把 IMU、關節、指令、歷史動作拼成策略訓練時見過的那些向量observation。決策神經網絡 forward 出 action有限狀態機決定「現在該不該推理、該用哪份策略」。注意這里的「感知」通常不是激光建圖那種環境感知而是本體感知proprioception。視覺、導航可在以后掛到業務層經 cmd_vel 灌進來。觀測必須和訓練嚴格對齊縮放ang_vel_scale、dof_pos_scale、裁剪clip_obs、歷史幀堆疊缺一項部署就會「看起來在動但不是訓練時那樣動」。3.2 該層項目示例ComputeObservation() 按 YAML 里的 observations 列表逐項拼接。Go2 的 himloco 策略菜單是commands, ang_vel, gravity_vec, dof_pos, dof_vel, actions → 一共 45 維。觀測項物理含義人話commands期望 vx, vy, yaw再乘 commands_scale你想讓它往哪走ang_vel機體角速度身體轉得有多快gravity_vec重力在機體坐標下的方向現在是不是快要歪了dof_pos相對默認站姿的關節角腿現在收著還是蹬著dof_vel關節速度腿甩得有多猛actions上一拍網絡輸出讓策略有短期記憶himloco 還開了 6 幀歷史observations_history: [0,1,2,3,4,5]由 ObservationBuffer 環形緩存。網絡一次看到的不是「這一瞬間」而是最近約 0.12 秒的身體 continuity。這對抑制抖動、估計接觸很有幫助。G1 跳舞則換成另一張列表motion_command參考軌跡的關節位置速度 motion_anchor_ori_b軀干相對動作錨點的姿態 本體狀態。MotionLoader 按仿真時間軸推進 BVH/CSV 參考運動。同一套 ComputeObservation()換列表就能從「走路」變成「跟動作」。推理后端被收在 InferenceRuntime按文件后綴自動選 libtorch 或 ONNX Runtime。決策層同樣不綁定某一種推理庫。3.3 FSM決策層的「交通指揮」神經網絡不會自己決定「該不該站起來」。項目用通用 FSM每個狀態實現 Enter / Run / Exit / CheckChange。這里用Go2 四態講一下邏輯進入 RLFSMStateRLLocomotion 的 Enter() 才會 InitRL(“go2/himloco”)讀 config.yaml、加載 himloco.pt、重置觀測。Run() 里調用 RLControl()從隊列取出策略算出的 q* / dq*填進 RobotCommand 的 kp/kd。FSM 在控制環頻率下運行推理在更慢的環決策結果通過隊列灌進實時層。G1 在同一套 FSM 骨架上多掛了 Charleston、Dance102、Gangnam Style。技能結束動作播放到 100%會 RequestStateChange 回 locomotion——這是決策層對「任務做完了」的判斷不是業務層彈窗。4. 業務應用層4.1 業務應用層職責業務層回答產品問題今天這臺機器人提供哪些能力行走、舞蹈、導航跟隨誰來下指令手柄、鍵盤、ROS 導航棧換任務要不要重編底層理想情況只換配置和模型文件它允許非實時、允許和人交互但不能直接寫電機寄存器。所有業務意圖都要翻譯成「期望速度」或「切換 FSM 狀態」再交給下面三層。4.2 該層項目示例G1 的業務菜單交互業務含義決策層加載的配置數字鍵 1 / RB方向上基礎行走g1/robomimic/locomotion數字鍵 2Charleston 舞蹈g1/robomimic/charleston播完回行走數字鍵 3全身跟蹤舞蹈g1/whole_body_tracking/dance_102 動作 CSV數字鍵 4江南 Styleg1/whole_body_tracking/gangnam_style鍵盤 N導航模式開關用 /cmd_vel 覆蓋手柄速度Control 結構里的 x / y / yaw 就是業務層的「速度點單」。手柄搖桿直接寫入開了 navigation_mode 后ROS 的 geometry_msgs/Twist/cmd_vel覆蓋這三項。于是 Nav2、鍵盤遙控、手柄可以共用同一個 locomotion 策略業務層只是換了指令來源。策略文件本身也是業務資產policy/機器人/技能/config.yaml 描述觀測清單、動作縮放、Kp/Kd、模型文件名。換一份 pt YAML不必改 C就能在同一臺 G1 上換技能。這是業務層和決策層之間最干凈的契約。再往上仿真啟動方式也是業務roslaunch rl_sar gazebo.launch rname:go2 或 ./rl_sim_mujoco g1 scene_29dof。用戶選的是「在哪演、演哪臺機器人」。4.3 多機型是業務層的「產品矩陣」README 里的支持列表A1、Go2、Go2W、G1、Lite3、L4W4、D1…看起來像硬件表從分層看其實是每種機器人注冊一個 FSMFactoryREGISTER_FSM_FACTORY啟動時按 robot_name 自動 CreateFSM。業務上「再支持一臺新機器人」的標準路徑是HAL實現該機型的 GetState / SetCommand實時層核對 dt、fixed_kp/kd、力矩限幅決策層補一份與訓練對齊的 config.yaml業務層注冊 FSM 狀態至少 Passive / GetUp / Locomotion下面三層穩定后產品經理口中的「新技能」往往只發生在第 4 步。5. 把四層串成一個控制拍下面用 Go2 真機、已經站起來、正在搖桿前進為例看 20 ms 里數據怎么走。6. 分層帶來的三條工程好處仿真和真機共用決策。 RL 基類里的觀測、推理、FSM、輸出縮放仿真與真機各寫一份 HAL 即可。訓練–仿真–真機之間最容易弄錯的是「觀測定義」和「關節順序」它們被顯式寫在 YAML 里而不是散落在 #ifdef。故障被關在該關的層。 姿態保護、力矩限幅屬于實時層即使策略輸出離譜也可以先趴下或限幅。業務層按錯鍵最多切到 Passive不會直接改 CRC 或 UDP 緩沖區。時間尺度分開。 鍵盤 20 Hz、策略 50 Hz、伺服 200 Hz。若把推理塞進 5 ms 環模型稍一變大就會丟拍若把 PD 降到 50 Hz落地沖擊會明顯變差。四足和人形在這套分層里的差別主要是 HAL 的關節數量、實時層的增益表以及業務層掛了幾個技能狀態。G1 有 29 個自由度、能跳舞Go2 是 12 關節 locomotion——對上面兩層來說都只是「另一份 YAML 和另一張 FSM 表」。分層不是為了把代碼寫得好看而是為了讓「換硬件、換頻率、換策略、換產品功能」四件事不要纏在一起。rl_sar 用 RobotState 這一窄接口、兩條時間環、一份可配置觀測、以及可注冊的 FSM把這件事做得很具體。把這四層看清再去讀 rl_sdk.cpp 和任意一個 rl_real_*.cpp在整體邏輯的把握上會清楚很多。