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