四足/人形机器人整机分层架构解析
上一篇文章从Kruchten的4+1视图视角分析了RL_SAR软件架构。Kruchten视图模型已经回答「从哪几个角度看软件」,但换电机协议会不会影响策略相关代码这件事,图上不一定看得出。四层回答的是逻辑视图里控制栈到底按什么切开,四层补的是一条领域约束。
- 换 SDK / 仿真后端,应只动 HAL。
- 发软、跟不住,应先查实时层的 dt 和 PD,而不是先改网络。
- 换 himloco.pt,应只动决策配置与观测清单。
- 加「按 3 跳舞」,应只加 FSM 技能和 YAML。
这些是变更影响规则,Kruchten 4+1视图不会自动写出来,所以,有必要补四层模型分析,否则 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_controller |
| HAL 硬件抽象层 | 总线级 | 怎么把各家硬件读成同一份结构 | 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 里定义了两份核心结构。读上来的叫 RobotState(IMU + 电机状态),写下去的叫 RobotCommand(每个关节的 q / dq / tau / kp / kd)。基类 RL 只声明两个纯虚函数:
- GetState(RobotState*):把硬件读成统一状态
- SetCommand(const RobotCommand*):把统一指令写回硬件
真正干活的是子类:RL_Real(真机)、RL_Sim(Gazebo)、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.005(5 ms,200 Hz),decimation: 4。启动时拉起三条 LoopFunc 线程:
| 线程 | 周期 | 职责 |
|---|---|---|
| loop_control | dt = 5 ms | GetState → StateController(FSM)→ SetCommand |
| loop_rl | dt × decimation = 20 ms | 组观测、推理、ComputeOutput,结果推进并发队列 |
| loop_keyboard | 50 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)−Kdq˙,再按 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 |
| 数字键 2 | Charleston 舞蹈 | g1/robomimic/charleston(播完回行走) |
| 数字键 3 | 全身跟踪舞蹈 | g1/whole_body_tracking/dance_102 + 动作 CSV |
| 数字键 4 | 江南 Style | g1/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…)看起来像硬件表,从分层看其实是:每种机器人注册一个 FSMFactory(REGISTER_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,在整体逻辑的把握上,会清楚很多。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)