RL_SAR项目架构解析 前言强化学习策略往往能在 IsaacGym、IsaacSim 等高性能仿真器中训练出出色的运动能力但从仿真走到真实机器人却长期卡在仿真与实物的差异、通信与控制接口碎片化、以及 ROS 版本、推理后端、机型各不相同等工程鸿沟上。作者 Ziqi Fan 正是出于这一痛点开源了 rl_sarsimulation and real用同一套框架把策略先放到 Gazebo、MuJoCo 中做仿真验证再部署到四足、轮足和人形真机上。项目同时兼容 IsaacGym 与 IsaacSim 训练出的策略兼容ROS Noetic 与 ROS2 Foxy/Humble、libtorch 与 onnxruntime。项目覆盖宇树 A1/Go2/G1、云深处 Lite3、智元 D1 等主流平台并提供手柄/键盘/网页遥控支持技能切换、执行器网络训练以及新增机型的扩展接口。项目把观测、推理、状态机和底层通信封装起来让研究者把精力放在算法本身。对机器人研究与应用而言它降低了 Sim-to-Real 的门槛把论文里的策略更快变成可复现、可落地的真机能力也让社区能在统一部署底座上共享、对比和迭代运动控制成果。虽然上一篇博文根据Kruchten 视图模型理论对MIT Cheetah-Software项目架构做了详细解析。RL_SAR的定位是把 IsaacGym / IsaacSim 训好的策略先在 Gazebo 或 MuJoCo 里做仿真验证再部署到四足、轮足、人形真机。“SAR”即 Simulation And Real。它刻意把“算法内核”与“机型 / 仿真器 / ROS / 推理后端”解耦因此特别适合用 41 来拆。本篇文章也将根据Kruchten 视图模型理论对RL_SAR项目做详细的架构解析。Kruchten 的 41 把同一套软件拆成四类干系人视角逻辑视图讲“系统由哪些功能对象组成”进程视图讲“运行时谁和谁并发、怎么通信”开发视图讲“代码怎么分包、怎么编译”物理视图讲“最终跑在哪些机器和网络上”。第五个 “1” 是场景用例用来把四视图串起来本文按你的要求只展开前四个视图。1. 逻辑视图系统由哪些功能对象组成逻辑视图回答的是忽略线程、进程和目录之后领域模型和功能职责如何划分。rl_sar 的逻辑核心可以概括成一句话——统一的机器人状态 / 指令模型 可替换的策略推理 按机型注册的有限状态机 可插拔的仿真 / 真机适配器。1.1 分层把“策略大脑”和“机器人身体”切开从功能上看系统分成五层自上而下依赖、不允许下层反过来认识上层细节人机输入层键盘、手柄、cmd_vel 导航速度、实验性网页遥控。统一写入 Controlx/y/yaw、导航开关、当前按键。技能编排层有限状态机FSM。负责 Passive / 站起 / 趴下 / 行走 / 舞蹈等技能切换并在进入某个 RL 技能时懒加载对应策略。强化学习内核层抽象基类 RL。负责读 YAML、拼观测、调推理、算 PD 输出、做力矩 / 姿态保护。推理运行时层InferenceRuntime::Model屏蔽 .ptLibTorch和 .onnxONNX Runtime差异。执行器适配层GetState() / SetCommand() 的具体实现。Gazebo、MuJoCo、宇树 DDS、A1 UDP、Lite3 SDK 等都只出现在这一层。这五层保证一件事换机型、换仿真器、换模型格式都不改观测拼装和 PD 的核心公式。1.2 核心领域对象状态、指令、观测、控制内核用四组数据结构把“机器人”从具体 SDK 里抽象出来RobotStateIMU四元数、陀螺、加速度 各关节 q/dq/ddq/tau_est/cur。所有仿真器和真机 SDK 最终都要填进这份结构。RobotCommand各关节的 mode/q/dq/tau/kp/kd。这就是下游电机或 Gazebo 关节控制器真正执行的东西。Observations策略看到的世界——线速度、角速度、重力投影、速度指令、关节位置/速度、上一拍动作以及舞蹈任务的 motion tracking 项。Control操作员意图。键盘枚举、手柄枚举、x/y/yaw、导航模式开关。FSM 用按键做状态迁移观测用 x/y/yaw 当 commands。这四组对象构成逻辑视图里的“通用机器人契约”适配器只负责填 RobotState、消费 RobotCommand策略只认 Observations 和 Control。1.3 策略作为数据base.yaml config.yaml 模型文件逻辑上一台机器人的“身体参数”和一套策略的“大脑参数”是分开的policy//base.yaml机型本体。dt、decimation、关节名、默认站立姿态、固定增益、力矩限幅、必须与实物 SDK 关节顺序一致 的 joint_names。policy///config.yaml某一套策略。model_name、观测项列表、历史帧、缩放系数、rl_kp/rl_kd、action_scale以及把训练关节顺序映到实物顺序的 joint_mapping。模型文件.ptTorchScript JIT或 .onnx。InitRL() 时由 ModelFactory::load_model() 按后缀自动选择后端。以 Go2 HimLoco 为例dt0.005、decimation4控制环 200 Hz、推理环 50 Hz观测 45 维历史 6 帧joint_mapping: [3,4,5,0,1,2,9,10,11,6,7,8] 把训练时的 FL/FR 顺序拧回实物的 FR/FL 顺序。这是 Sim-to-Real 在逻辑层最关键的一座桥映射错了策略会把左腿指令打到右腿。配置层路径约定回答的问题机型本体policy/ROBOT/base.yaml这台机器人有几自由度、关节叫什么、站立姿态是什么策略实例policy/ROBOT/CONFIG/config.yaml这套网络看哪些观测、输出如何缩放、关节如何重排网络权重*.pt/*.onnx策略本身动作捕捉可选*.csv/ BVH 导出G1 舞蹈 / whole-body tracking 的参考轨迹1.4 观测 → 推理 → 动作一条固定的功能流水线不管仿真还是真机逻辑流水线相同GetState()适配器实现把 IMU、关节反馈写入 RobotState。StateController() → FSM.Run()当前技能决定电机指令从哪来——阻尼、插值站立还是 RL。ComputeObservation()按 YAML 里的 observations 列表逐项拼接并乘上对应 scale。角速度还要区分 body / world 坐标系ROS1 Gazebo 用世界系ROS2 / MuJoCo / 真机用机体系。Forward()可选地经 ObservationBuffer 堆历史帧再 model-forward()。ComputeOutput()动作乘 action_scale轮子关节走速度、其余走位置再套 PDτkp⋅(a⋅sqdefault−q)−kd⋅q˙ \tau k_p \cdot (a \cdot s q_{\mathrm{default}} - q) - k_d \cdot \dot{q}τkp​⋅(a⋅sqdefault​−q)−kd​⋅q˙​结果被 torque_limits 夹紧。RLControl()FSM 的 RL 状态里从无锁队列取出目标 q/dq填 kp/kdtau 置 0交给底层位置伺服。SetCommand()适配器实现把 RobotCommand 写成 ROS 话题、MuJoCo 控制量或厂商 SDK 报文。流水线里还有两道安全阀TorqueProtect() 和 AttitudeProtect()逻辑上属于内核而不是某一台具体机器人。1.5 有限状态机技能是一等公民FSM 不是“附加 UI”而是逻辑视图的中枢。框架提供FSMStateEnter / Run / Exit / CheckChange。RLFSMState持有 RL提供插值站立 Interpolate() 和从队列取 RL 输出的 RLControl()。FSMFactory FSMManager REGISTER_FSM_FACTORY静态初始化时按机型名go2、g1…注册工厂。运行时 CreateFSM(“go2”, this) 即可。以 Go2 为例状态只有四个职责清晰Passive进入后提示按 0/A 站起运行时纯阻尼kd8。GetUp从 Passive 来会先插值到一组预站立姿态再插值到 default_dof_pos完成后等 Num1 / RB上键进入行走。GetDown插值回程序启动时的姿态然后回到 Passive。RLLocomotionEnter() 里把 config_name 设为 himloco 并调用 InitRL()——策略在进技能的那一刻才加载。失败则退回 Passive。G1 把同一模式扩成多技能robomimic/locomotion、robomimic/charleston、whole_body_tracking/dance_102、gangnam_style。舞蹈状态会读 MotionLoader 的参考轨迹播完自动切回行走。这就是“一台机器人、多套策略”在逻辑上的实现方式状态进入 换脑状态退出 卸脑。1.6 适配器同一套 RL 接口多种“身体”逻辑上所有仿真 / 真机程序都继承 RL只实现三个纯虚函数GetState()传感器 → RobotState。SetCommand()RobotCommand → 执行器。Forward()多数实现是“算观测 调 model-forward()”个别机型可覆写以适配特殊网络输入。当前逻辑角色对照如下逻辑角色代表实现对内核暴露的能力Gazebo 仿真体RL_SimROS 关节状态 / IMU 入MotorCommand/RobotCommand出MuJoCo 仿真体rl_sim_mujoco中的RL_Sim直接读 MuJoCo 状态、写控制量宇树四足/人形rl_real_go2/rl_real_g1DDSLowState/LowCmd宇树 A1rl_real_a1UDP LCM其他厂商Lite3 / D1 / L4W4各自 SDK逻辑视图小结rl_sar 没有把“Go2 控制器”和“G1 控制器”做成两套平行世界而是用 RL FSM 工厂 策略 YAML 把差异压进配置和适配器。功能对象稳定变化点明确。2. 进程视图运行时谁在跑、如何同步进程视图关注 进程边界、线程、频率、队列和 IPC。rl_sar 的运行时设计可以概括为一个控制进程内部双速率线程仿真时再加一个物理引擎进程真机时再加一条厂商通信通道。2.1 可执行体按“场景 × 机型”切进程编译产物不是单一巨进程而是一组场景化可执行文件可执行文件何时存在进程职责rl_simROS1/ROS2 构建Gazebo 侧的 RL 控制进程rl_sim_mujoco./build.sh -mj单进程MuJoCo 物理 渲染 RLrl_real_go2ROS 或纯 CMakeGo2 / Go2W 真机参数wheelrl_real_g1同上G1 29 DoF 真机rl_real_a1同上A1 真机rl_real_lite3/l4w4/d1同上对应厂商真机Gazebo /robot_state_publisher/joy_nodelaunch 启动物理、TF、手柄采集rosbridge_websocketweb_video_server可选手机网页遥控注意Gazebo 仿真是 双进程其实是多进程——launch 先把世界和机器人拉起来必须再单独启动 rl_sim否则机器人没有策略、会摔倒。MuJoCo 则是 单进程物理线程和 UI 线程都在 rl_sim_mujoco 内部。2.2 线程模型LoopFunc 上的双速率控制所有控制程序都用 LoopFunc 起 detach 的周期线程可选绑核A1 的 UDP 线程绑 CPU 3。Go2 默认时间基是 dt5 ms、decimation4线程名周期频率做什么loop_controldt200 HzGetState→ FSM →SetCommand保证电机指令不断loop_rldt × decimation50 Hz拼观测、神经网络推理、ComputeOutput、入队loop_keyboard50 ms20 Hz终端按键ROS spinner事件驱动随话题IMU、关节、Joy、cmd_vel回调A1udpSend/Recv2 ms500 Hz与电机板 UDPMuJoCo Physics / Render仿真步长 / 显示器—物理积分与 GUIloop_plot可选1–2 ms调试PLOT宏打开后的实时曲线双速率不是摆设推理尤其是带历史帧的策略放不进 200 Hz 电机环而电机又不能等网络算完才更新。于是用 TBB concurrent_queue 做生产者–消费者loop_rl 只负责把 output_dof_pos/vel/tau 推进队列。loop_control 里 FSM 的 RLControl() try_pop本周期没有新动作就沿用上一拍指令队列空则不改写电机环不会被推理抖动卡死。InitRL() 与 forward() 有并发model_mutex 用于换模型时做保护。2.3 进程间通信按场景换“总线”同一套逻辑对象进程之间走完全不同的总线Gazebo ROS1按关节发布 //_controller/commandrobot_msgs/MotorCommand订阅对应 state再订 /gazebo/model_states、/joy、/cmd_vel通过 /gazebo/pause_physics、reset_world 等服务控仿真。Gazebo ROS2改为整机话题 /robot_joint_controller/commandRobotCommand和 stateRobotStateIMU 走 /imuparameter_blackboard 节点上放 robot_name。rl_sim 启动后还会 fork controller_manager spawner 把关节控制器拉起来。MuJoCo无 ROS。进程内直接调 MuJoCo C API手柄走本机 /dev/input。Go2 / G1 真机unitree_sdk2 的 DDS。rt/lowstate 进、rt/lowcmd 出、rt/wirelesscontroller 手柄G1 另有 rt/secondary_imu。启动时 MotionSwitcherClient::ReleaseMode() 把官方运动服务让出来。A1 真机unitree_legged_sdk UDP 500 Hz LCM。可选导航头文件里解开 USE_ROS 后真机进程额外订 /cmd_velFSM 的导航模式会屏蔽摇杆速度。网页遥控机器人上跑 rosbridge_websocket 和 web_video_server浏览器作为又一个进程连进来。2.4 并发与安全约定从进程视图还要看到几条“运行时契约”否则读代码会误判卡顿或丢控控制环绝不能做重计算换策略、读 YAML、加载 .pt 都在 FSM Enter()也就是状态切换那一拍而不是 200 Hz 热路径。推理失败要能退InitRL() 抛异常则 RequestStateChange(“RLFSMStatePassive”)进入阻尼避免加载失败后仍按旧增益猛给力矩。仿真复位是跨进程动作手柄 RBY / 键盘 R 调 Gazebo 的 reset_world 服务控制进程自己并不“重置物理”。detach 线程 condition_variableLoopFunc::shutdown() 靠 _running 和 _cv 结束循环析构顺序必须先停环再拆 ROS/SDK否则会打到已释放的 publisher。进程视图小结rl_sar 用“慢思考、快执行”的双速率把神经网络从实时环里拆出去仿真用 ROS 当总线真机用厂商 DDS/UDP 当总线内核线程模型保持不变。3. 开发视图代码如何组织、如何长出新机型开发视图对应实现视图包、目录、依赖、构建脚本、扩展缝。rl_sar 的开发结构服从一条原则——内核一份机型用文件约定叠加ROS1/ROS2/纯 CMake 三套构建吃同一份源码。3.1 仓库的包边界仓库根下真正参与编译的软件包很少职责切得很干净src/rl_sar主包。可执行文件、FSM、核心库、launch、worlds。src/robot_msgsMotorCommand / MotorState / RobotCommand / RobotState / IMU仿真里控制进程和关节控制器的契约。src/robot_joint_controllerGazebo 用的关节控制器插件。ROS1 按关节各一个 controllerROS2 是整机组 controller。src/rl_sar_zoo各机型 URDF/xacro/MJCF独立仓库、构建时下载主仓不囤模型网格。policy/按机型存放策略相关的运行时数据每台机器人一份 base.yaml关节、控制周期、默认姿态每个策略一份 config.yaml 加上对应的 .pt/.onnx 权重G1 舞蹈还会带参考轨迹 CSV。程序运行时按路径去读这些文件目录本身不参与编译。library/inference_runtime、library/mujoco构建脚本按需下载的 LibTorch / ONNX Runtime / MuJoCo不进业务源码树。** 主包内部再按“稳定内核 / 易变机型”切开**src/rl_sar/├── library/core/ # 与机型无关的内核│ ├── rl_sdk/│ ├── fsm/│ ├── inference_runtime/│ ├── loop / observation_buffer / motion_loader / logger / vector_math├── library/thirdparty/ # 厂商 SDK、mujoco_simulate├── fsm_robot/ # 每机型一个 hpp fsm_all.hpp 汇总├── include/ # rl_sim.hpp、rl_real_*.hpp├── src/ # 各可执行文件的 .cpp├── launch/ # gazebo.launch / gazebo.launch.py├── worlds/├── scripts/ # actuator_net.py、convert_policy.py└── test/ # 目前在 CMake 里注释掉的单元测试3.2 三套构建一份源码build.sh 是开发视图的入口。它先拉推理库和机器人 description再按环境选择后端模式触发编译宏工具产物位置ROS1ROS_DISTROnoeticUSE_ROS1catkin builddevel/ROS2foxy/humbleUSE_ROS2colcon buildinstall/纯 CMake真机./build.sh -mUSE_CMAKEcmakecmake_build/binCMake MuJoCo./build.sh -mjUSE_CMAKEUSE_MUJOCOcmake另产出rl_sim_mujoco双 ROS 的技巧是每个包同时有 package.ros1.xml 和 package.ros2.xmlbuild.sh 按发行版 符号链接成 package.xml。源文件里用 #if defined(USE_ROS1) / USE_ROS2 / USE_CMAKE 切话题类型避免维护三份业务逻辑。Jetson 构建时若检测到 /etc/nv_tegra_release会关掉 ONNX只保留 LibTorch——这是开发视图对物理视图ARM 板的让步。3.3 扩展加一台新机器人要动哪些文件README 把扩展点写成了文件名契约开发视图里这就是模块的“生长方向”——不要改 rl_sdk.cpp按名单补文件运动学描述src/rl_sar_zoo/_description/xacro、gazebo 控制 yaml、ROS1/ROS2 的 package.xml。策略数据policy//base.yaml关节顺序必须跟实物一致、/config.yaml、JIT 导出的 .pt 或 .onnx。FSMfsm_robot/fsm_.hpp并在 fsm_all.hpp 里 #include。用 REGISTER_FSM_FACTORY 挂到 FSMManager。真机适配器include/rl_real_.hpp src/rl_real_.cpp实现 GetState/SetCommand/Forward并在 CMakeLists.txt 里 add_executable。若只要仿真、暂无真机可以只做 description FSM 让 rl_sim 通过 rname: 加载rl_sim 已包含 fsm_all.hpp。这套约定让“新机型”主要是 加法而不是去改内核类图。G1 相对 Go2 多出来的不过是更多 FSM 状态和 MotionLoader不是另一套框架。这套约定让“新机型”主要是 加法而不是去改内核类图。G1 相对 Go2 多出来的不过是更多 FSM 状态和 MotionLoader不是另一套框架。3.4 消息与控制器仿真侧的开发契约robot_msgs 故意做得极窄和内核 RobotCommand/RobotState 一一对应MotorCommand.msgq, dq, tau, kp, kdMotorState.msgq, dq, ddq, tau_est, curRobotCommand / RobotState整机数组 IMUGazebo 里真正把这些数变成力矩的是 robot_joint_controller 插件。开发时若只改策略 YAML不必碰控制器若改关节名则 description 的 robot_control.yaml、base.yaml 的 joint_names / joint_controller_names 必须一起改否则 ROS1 会按名字订不到话题。** 开发视图小结 **目录按“内核 / 机型 / 数据 / 第三方”分层构建脚本把 ROS 差异和二进制依赖挡在门外新机器人走文件清单扩展而不是分叉框架。4. 物理视图部署到哪些机器和网段物理视图描述节点、网络、外设和容器。rl_sar 的部署形态虽然多但都是同一套二进制在不同拓扑上的投影。4.1 五种典型拓扑1. 开发机 Gazebo 仿真Ubuntu 20.04/22.04 ROS Noetic 或 Foxy/Humble。两个终端launch 起 Gazebo 和手柄节点再起 rl_sim。需要显示器、可选 USB 手柄。策略文件在本机 policy/。2. 开发机 MuJoCo 仿真不必装 ROS。./build.sh -mj 后一个进程吃满 CPU/GPU 做物理和渲染。macOS 目前只支持这种仿真。适合没有 NVIDIA Isaac 环境、只想先看策略会不会摔的场合。3. 开发机网线连真机PC 与机器人同一网段PC 跑 rl_real_*推理在 PC 上指令经以太网进机载运动板。适合调试策略代价是必须拖网线或可靠无线官方明确警告无线可能丢包失控。4. 机载 Jetson 部署SSH 上 192.168.123.18纯 CMake 编译rl_real_go2 eth0 常驻。可用 systemd rl_sar.service 开机自启。推理在 Orin 上拔掉调试网线后用官方遥控器。这是最接近产品的物理形态。5. Docker镜像 rl_sar:humblenetwork_mode: host特权容器可选 NVIDIA runtime。policy/ 只读挂载进容器X11 和 /dev/input 映射用来显示 MuJoCo/Gazebo、读手柄。适合统一环境、CI 或演示。4.2 网络与地址物理视图里的“接口表”文档里出现的地址是部署时必须对齐的物理接口而不是逻辑话题名节点地址 / 接口用途A1 有线调试 PC192.168.123.162/24UDP/LCM 连机器人Go2/G1 运动板192.168.123.161低层 DDS 对端Go2/G1 调试 PC同网段例如192.168.123.222跑rl_real_go2 网卡名Go2 Jetson192.168.123.18用户unitreeSSH、机载编译运行智元 D1 Wi-Fi机器人192.168.234.1PC192.168.234.2需在机器人/opt/export/config/sdk_config.yaml写target_ipD1 有线备选192.168.168.168以太网Lite3默认192.168.2.1源码可改无线 UDP需改机器人network.tomlL4W4默认机器人192.168.1.101UDP SDKGo2 真机部署的物理链路可以画得更细PC 的 USB 网卡如 enxf8e43b808e06只是 DDS 的 networkInterface 参数机载部署时这个接口变成 eth0进程从“外部电脑”变成“机器人身体里的一块板”。4.3 外设与安全相关的物理约束物理视图不能只画方框还要标出 真实世界会咬人的接口手柄仿真走 ROS joy_node 读 /dev/input真机走官方无线手柄经 DDS 上报。Docker 必须映射 /dev/input否则容器里没有 RB上键。急停与阻尼逻辑上的 Passivekd8在物理上就是电机进入阻尼文档建议把 LBRB 做成急停。真机无线连接被明确警告可能丢包失控。G1 调试姿态上电后需吊起按 L2R2 进调试模式再启动 rl_real_g1。这是物理操作规程不是软件状态。GPUIsaac 训练不在本仓库部署推理默认 CPU LibTorch。Docker 的 NVIDIA runtime 主要用于 Gazebo/MuJoCo 渲染不是为了在容器里训练。X11MuJoCo / Gazebo GUI 依赖 DISPLAY 和 /tmp/.X11-unix无头服务器只能走软件渲染 profileLIBGL_ALWAYS_SOFTWARE1。4.4 物理视图如何反约束另外三视图四视图不是四张独立海报物理部署会回头限制逻辑和进程机载没有 ROS 守护进程的必要 → 开发视图提供 USE_CMAKE进程视图里真机可以是单进程。Jetson 是 ARM → 不能用 x86 的官方 LibTorch 包ONNX 直接关掉。运动板和推理板分离 → 进程视图必须有一条稳定的 lowcmd/lowstate 通道逻辑视图才能假装“GetState/SetCommand 是函数调用”。策略文件是运行时数据 → 物理上要保证 POLICY_DIR 在目标机存在Docker 选择只读挂载避免容器写坏宿主机模型。**物理视图小结**仿真是“一台带 GUI 的工作站”真机是“PC 或 Jetson 运动板 明确网段”容器是把工作站打包。换拓扑几乎不改逻辑类图只换适配器所连接的那根线。5. 四视图如何互相咬合可以用一次 Go2 部署把四张图叠在一起逻辑操作员按 A 站起、按 RB上进入 RLFSMStateRLLocomotion加载 go2/himloco观测 45 维历史 6 帧PD 输出 12 个关节。进程loop_rl 50 Hz 推理loop_control 200 Hz 把 q/dq/kp/kd 写出仿真时对面是 Gazebo 进程真机时对面是 DDS。开发这一跳不改 rl_sdk只依赖已有的 fsm_go2.hpp、rl_real_go2.cpp 和 policy/go2/himloco/config.yaml。物理先在本机 Gazebo 验证再把网卡设到 192.168.123.222 跑 rl_real_go2最后拷到 Jetson 用 systemd 常驻。这也是 41 里那个未单独成章的 “1 场景”同一个用户故事在四张图上各留下一条轨迹。