去年在搞一个仿生机器人项目时,我发现一个挺尴尬的现状:想做微型双足机器人的人很多,但市面上的方案要么是几百块的玩具舵机人,压根没有跑强化学习策略的条件;要么是实验室级别的大型双足平台,价格和体积都劝退普通人。轮式和四足的开源方案已经非常成熟,双足微型化却处于一个“文档少、坑多、复现难”的阶段。这个微小型双足鸭形机器人项目,就是想把“强化学习驱动的开源架构”这条路走通——机器人总高不到30厘米,总重控制在650克左右,从仿真训练到真机部署全链路开源。本文我会把整个系统的设计决策、硬件选型、强化学习训练过程、sim-to-real迁移踩的坑,以及软件架构的拆分逻辑一次讲清楚。适合想把强化学习策略真正跑到小硬件上的朋友,也适合刚入门RL、想找一个低成本的实物载体来验证算法的人参考。
1. 这个鸭形机器人到底在解决什么问题:尺寸、成本和端到端策略的平衡
1.1 为什么是“鸭形”而不是通用人形
一开始我也想做个缩小版的人形双足,但深入一算就放弃了。人形双足的踝关节通常需要3个自由度,加上髋关节和膝关节,单条腿至少要5到6个关节,两条腿就是10到12个舵机/电机。在微型尺度下,每个关节还要保证足够的扭矩和精度,光物料成本就翻好几倍,控制难度更是呈指数上升。微小型双足机器人最怕的就是“自由度多但扭矩不足”,站都站不稳,更别提跑强化学习。
鸭形结构解决这个问题的方式很聪明。鸭子的腿短、脚掌宽、重心低,天生就是“稳定优先”的行走方案。我的设计里双腿各3个自由度,总共6自由度,没有腰部和踝部的额外自由度。牺牲的灵活性换来了三个实实在在的好处:一是结构容错率高,宽脚掌让支撑多边形变大,强化学习策略更容易发现“怎么站”和“怎么走”的可行解;二是重心低,电池等较重的部件可以放到腹部偏下,ZMP(零力矩点)落在脚掌范围内的概率大大增加;三是外观上就是一个鸭子,做仿生展示时特别直观,给非技术人员演示强化学习效果时,一个会走路的鸭子比一个抽象的两根杆子有感染力得多。
另外,鸭子的形态有一个隐藏优势:未来想扩展到水陆两栖或者装上一对蹼来拨水,结构上都不用大改。这个项目在最初设计时把“腿可以换装”作为了结构接口的一个硬性要求,后面如果你有兴趣复刻,可以直接沿用这个思路。
1.2 要解决的三个核心问题
这个项目本质上是在三个约束条件之间找平衡:尺寸、成本、端到端可行性。
尺寸和重量决定了你能用什么样的执行器和电池。我的目标是总高度小于30厘米、总重量尽量控制在700克以内。这个重量级别下,6自由度是性价比最高的配置,再多关节就得上更贵的电机,少又走不出双足步态。成本方面,整机物料成本需要控制在千元级以内,不仅自己用得起,开源出去别人也愿意复刻。如果整套硬件要上万块,那“开源架构”就只剩下代码开源,别人根本跑不起来。
第三个问题是端到端策略的可行性。微小型平台上,你的算力非常有限,没有GPU,甚至没有像样的Linux主控。策略网络必须在几百毫秒内完成一次推理,并且占用内存在几MB以内。我们最终把强化学习策略压缩成一个两层的MLP(256×256),配合固定步数控制循环,跑在ESP32-S3上能够稳定达到30Hz的推理频率。整个项目的软件栈就是围绕“训练端到端策略 -> 导出轻量模型 -> 嵌入式推理”这个闭环来设计的。
提示:如果你也想做类似项目,我强烈建议在硬件方案定型前先跑一遍终端推理的算力评估,别先把机器人搭出来才发现主控根本跑不动你的策略网络。
2. 硬件上怎么把“鸭形”做成微小型:关节扭矩、重心和结构件的取舍
2.1 硬件核心清单与选型理由
这套硬件清单是我经过三轮迭代后确定下来的,核心选型逻辑就两条:扭矩够用、价格可控。
| 组件 | 型号 | 数量 | 选型理由 |
|---|---|---|---|
| 主控 | ESP32-S3(双核240MHz) | 1 | 支持轻量矩阵运算,自带Wi-Fi/蓝牙,方便调试可视化 |
| 舵机 | LD-1501MG金属齿舵机 | 6 | 单舵机约1.5kg·cm扭矩,重量约20g,性价比最高的入门位置 |
| IMU | BMI088六轴 | 1 | 噪声小、温度漂移低,非常适合动态姿态估计 |
| 电池 | 2S 500mAh锂电池 | 1 | 电压7.4V,能量密度够,重量约65g |
| BEC降压模块 | 5V/3A BEC | 1 | 为舵机和主控分别供电,避免舵机堵转拉垮主控 |
| 结构件 | PLA/TPU 3D打印 | 若干 | 主体PLA,脚掌和关节缓冲TPU |
第一版我贪便宜用了MG90S塑料齿舵机,觉得微型机器人重心低、负载小,应该扛得住。实际测试中,单腿支撑瞬间的冲击扭矩远超静态计算值,塑料齿扫过一轮齿之后直接被磨平。后来换成LD-1501MG金属齿,同样的步态策略测试下,连续跑了几百次都没有失效。所以第一个经验是:微型双足领域,千万别用塑料齿舵机,省下的几十块钱会在调试阶段用好几倍的精力还回去。
2.2 扭矩计算和重心布置的实操推导
选舵机不能只靠感觉,要算一下踝关节到底需要多少扭矩。我用一个简化模型:单腿支撑时,机器人整个身体可以看作一个绕踝关节旋转的刚体。关键参数有两个,一个是总重量,一个是重心到支撑点的水平距离。
假设机器人总重量650克,站立时重心等效到踝关节前方4厘米处。此时踝关节需要克服的静态扭矩大约是:
T = m × g × d = 0.65kg × 9.8m/s² × 0.04m ≈ 0.25kg·m
换算成舵机常用的kg·cm单位,大约是2.5kg·cm。这还没算上动态行走时的惯性冲击。因此单舵机1.5kg·cm的扭矩就很悬了,更不用说早期用的MG90S只有1.8kg·cm。最终方案是踝关节采用双舵机并联驱动,每个踝关节使用两个LD-1501MG,并联输出扭矩接近3kg·cm,实测在行走时没有出现扭矩不足导致的舵机堵转。
重心位置的重排也很关键。鸭子形态的腹部空间比较大,我把电池、降压模块尽量往下放,让机器人整体重心高度控制在脚掌以上约8厘米的位置。重心低的好处是,哪怕强化学习策略暂时没有学到高级平衡技巧,机器人靠结构本身的被动稳定性也能多撑一会儿,这给训练初期的“存活时间”提供了很大的宽松空间。
2.3 结构件开源与打印注意事项
所有结构件的STL文件和STEP图纸都在仓库里,板材参数我也做了备注。打印时有一点特别重要:关节连接件这类受力件,壁厚不要低于3层,填充率建议40%以上;脚掌部分我直接用TPU打印,配合底面贴一层防滑EVA材料,实测下来摩擦系数与仿真中设定的1.0很接近。
第一次打印出来的鸭子有一个位置特别容易断——大腿连杆和髋关节连接处的销孔附近。微型零件尺寸小,孔壁薄,加上舵机转动时这里有周期性交变载荷,疲劳断裂几乎是必然的。解决办法是对称加了一个金属轴套,把受力从塑料壁转移到金属轴上。如果你复刻这个项目,我建议把这部分直接换成M3金属轴套,别省这个钱。
3. 仿真训练:MuJoCo环境搭建、PPO调参与奖励函数的设计逻辑
3.1 仿真器选型:为什么是MuJoCo而不是Isaac Gym或Gazebo
仿真器选型上我做过对比,简要结论如下表:
| 仿真器 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| MuJoCo | 轻量、物理引擎快速、MJCF模型可读性好、CPU单机2~4小时收敛 | 渲染较弱 | 小机器人RL训练 |
| Isaac Gym | GPU并行资源、数千环境同时训练 | 对显卡要求高、环境配置重 | 大规模并行训练、复杂策略 |
| Gazebo | 物理模型精细、与ROS集成成熟 | 训练速度慢 | 部署验证、与ROS生态联调 |
这个项目的策略网络很小,用CPU单机训练完全够用,没必要上Isaac Gym那么重的方案。MuJoCo的优势在于模型文件是文本化的MJCF,改一个关节阻尼、摩擦系数、舵机力矩上限,改完立即生效,不需要重新编译仿真环境。Gazebo留在后期做ROS集成验证时才用。这个取舍很现实:你需要的工具只应该匹配你的问题规模。
配套的RL训练环境,我用gymnasium写了一个自定义DuckEnv环境。状态空间包含6个关节角度、6个关节角速度、IMU的横滚角和俯仰角、躯干高度,一共14维。动作空间是6个关节的目标角度增量。训练库没有直接用Stable-Baselines3,而是用了CleanRL,因为它代码非常直白,想要调试或修改PPO实现细节时不用挖一层抽象封装。
3.2 MJCF模型里最容易搞错的参数
MJCF建模时,有四个参数对sim-to-real的成功率影响最大:接触参数、摩擦系数、关节阻尼和电机力矩限制。
接触点设置上,双脚需要各加两个contact geom,位置分别在前脚掌和后脚跟。一开始我只设置了一个平面接触,仿真里机器人经常绕前脚掌转圈摔倒,因为后脚跟没有接触反馈,模型无法产生真实的支撑效果。摩擦系数与真机匹配方面,我在仿真里设定脚掌和地面的滑动摩擦系数为1.0、扭转摩擦系数为0.1,对应的就是真机TPU脚垫加防滑EVA在瓷砖地面上的实测数据。
关节阻尼是个容易让人困惑的参数。舵机本身有内部减速机构和摩擦,它实际上会抵抗外部的高频运动。仿真中如果阻尼设太小,策略会发现一个作弊方式:高频抖动关节来制造“额外的稳定性”,但这种动作在真机上根本做不出来,因为舵机的机械阻尼会把高频分量吃掉。我的做法是训练前先做一个“舵机扫频响应实验”,记录舵机跟随不同频率正弦信号的实际延迟和幅度衰减,然后把等效的阻尼和延迟参数写进MJCF。
下面的代码片段是本项目MJCF模型的核心参数片段,展示的是踝关节和脚掌接触的定义:
<joint name="ankle_yaw" type="hinge" axis="0 0 1" limited="true" range="-20 20" damping="0.35"/> <joint name="ankle_pitch" type="hinge" axis="0 1 0" limited="true" range="-30 30" damping="0.35"/> <geom name="foot_ground" type="box" size="0.03 0.035 0.005" pos="0.02 0 -0.015" friction="1.0 0.1" material="foot_plastic"/>3.3 奖励函数设计的思路:先能站,再走,最后跑得自然
奖励函数是强化学习训练里最重要的“魔法”,设计逻辑必须一步一步来。我采用了分阶段叠加的奖励结构,整个策略的训练过程分成了三个阶段:
第一阶段只想让机器人站起来。核心奖励是躯干高度保持项:躯干高度接近目标高度时给一个稳定的正奖励,低于或高于目标范围都会扣分。
第二阶段引导向前移动。加入前进速度奖励和方向一致性奖励:躯干前进速度每增加0.01m/s就奖励一定的分数,同时惩罚横向漂移和转动偏差,防止机器人走着走着变成“螃蟹步”。
第三阶段加入自然性约束。能量惩罚项对动作的平方进行扣分,平滑奖励项惩罚相邻两步动作的突变,用来抑制高频抖动。
| 奖励项 | 权重 | 设计意图 |
|---|---|---|
| 生存奖励 | 0.1 | 只要不摔倒就持续给正奖励,让策略优先学习“维持站立” |
| 躯干高度保持 | 0.5 | 鼓励稳定站立姿态 |
| 前进速度 | 1.0 | 核心目标奖励 |
| 方向一致性 | 0.8 | 抑制横向移动和旋转漂移 |
| 能量惩罚 | -0.01 × sum(a²) | 减少不必要的关节动作 |
| 平滑惩罚 | -0.05 × sum((a_t - a_{t-1})²) | 抑制高频抖动 |
超参数方面,我用的PPO设置是:学习率3e-4,clip系数0.2,每轮收集2048步样本,mini-batch大小256,更新4轮。策略网络就是一个256×256的MLP,用tanh激活函数。在单台CPU上,大概训练150万步后能稳定走起来,耗时约2~4小时,具体取决于CPU性能。
3.4 训练过程的“坏情况”和判断标准
训练初期最常见的问题是“站不起来”。奖励曲线缓慢爬升但策略就是学不会保持平衡,此时优先检查站立奖励是否为稀疏问题——如果摔倒前还能拿到几步生存奖励,学习信号是不够的。解决办法是增加“存活加分”的密度。
中期有个特别有意思的坏情况:奖励曲线已经很高了,但看渲染视频发现机器人不是在走,而是在原地下蹲做全身抖动,靠一种诡异的动态平衡蹭出一丁点前进速度。这种情况下把平滑惩罚权重调大就行,往往立竿见影。我在日志里标记这种状态为“伪前进”,看到之后第一反应不是改网络结构,而是先检查是不是动作过于高频。
后期有个经典问题叫“策略退化”:训练超过某个步数后,奖励曲线反而掉下来。这通常是熵系数太小、策略过早收敛到一个确定性策略,缺少探索导致的。我最后的熵系数设置为0.015,并且设了一个很实用的训练停止规则:近10万步的奖励均值不再上升且渲染视频步态自然,就停止训练,不追求奖励最大值。
4. Sim-to-Real的第一课:为什么仿真里健步如飞的鸭子上真机就原地抽搐
4.1 现象描述:真机“跳霹雳舞”
第一次把策略部署到真机上的场景我至今印象很深刻。仿真里那只鸭子走得稳稳妥妥,真机上去之后,前两步看似正常,第三步开始剧烈抖动,两条腿交替乱蹬,整个身体顺着一个方向原地打转,偶尔还会猛地向后面倒过去。家人拍了一段视频,说这哪是机器人走路,这是在跳霹雳舞。
为什么会这样?先说结论:这是sim-to-real gap中最经典的三类问题叠加导致的——舵机死区、通信延迟、控制频率不匹配。
4.2 排查链路:一步一步隔离问题
我排查问题的顺序是先置信度高的环节,再怀疑策略本身。
第一步,检查固件最基本的运动功能。直接把六路舵机用开环方式控制,按照固定顺序发位置指令,观察鸭子能不能完成简单的蹲起动作。这一步排除了舵机本身损坏、接线错误、供电不足等低级问题。
第二步,记录真机数据并与仿真对比。在机器人身上贴了多个标记点,用手机慢动作拍摄腿部动作,再用仿真渲染同角度的视频做逐帧对比。对比后发现,真机舵机的实际角度变化比仿真中的关节角度变化慢了约120到150毫秒。这个延迟来自舵机内置PID控制器的响应时间,而不是通信延迟。
第三步,检查IMU数据。通过串口把IMU数据以200Hz采样率存下来,发现传感器信号本身有约20毫秒的滤波滞后,加上主控处理和其他任务调度的影响,闭环反馈总延迟在50毫秒左右。
第四步,检查策略输出频率。我最初让策略以100Hz推理,但真机上主控还同时在做日志记录、Wi-Fi通信,推理频率根本稳不住100Hz,掉到了30Hz上下。仿真还是按100Hz的控制周期训练,这意味着部署时策略拿到的状态更新频率只有训练时的一半,表现出来就是动作急促、不连贯。
4.3 针对性修复:死区补偿、延迟补偿与控制周期对齐
修复工作分三条线同时推进。
第一条线,舵机死区补偿。LD-1501MG这类舵机在增量角小于约0.5度时,响应非常微弱甚至不动,这会让策略输出的小调整完全失效。我的做法是在固件层加了一个“死区补偿器”:当策略输出的角度增量绝对值小于1度时,把增量映射到2度并配合一个短暂的方向脉冲,确保舵机每次指令都有实际动作。
第二条线,延迟补偿。既然无法消除硬件延迟,就把它纳入训练环境里。我在训练环境的动作接口里加了一个动作缓冲队列,模拟120毫秒的舵机延迟;状态接口里加了一阶低通滤波模拟IMU滞后。让策略在“实际会被延迟控制的系统”上训练,而不是在理想无延迟的仿真上训练,这是最有效也最容易被忽视的sim-to-real手段。
第三条线,控制周期对齐。最终把策略推理频率固定在30Hz,仿真训练环境也以30Hz的控制周期运行。物理仿真步长仍然是5毫秒,但每次策略推理之间会执行6个物理步。这相当于让策略在“低速率决策、快速率物理推进”的真实模式下训练,真机部署后不再出现因控制频率不同导致的异常。
修复之前鸭子站5秒内摔倒的概率接近100%,修复之后连续行走30秒不摔倒的成功率提升到了85%。这一步做完,我才真正意识到:工程问题很多时候不靠更炫的算法,而靠“让仿真环境用与真机一致的方式跟策略交互”。
5. 开源架构拆解:从强化学习训练到真机部署的软件栈怎么组织
5.1 软件栈的分层逻辑
这个项目的仓库结构在设计时参考了四层架构:仿真训练层、策略导出层、嵌入式部署层和硬件抽象层。这样分层的核心逻辑是让每一层都能独立替换或测试。
仿真训练层负责定义环境、运行PPO训练流程、输出训练曲线和策略权重。策略导出层把训练好的PyTorch模型转成ONNX并量化成INT8格式,再把权重表固化成C头文件。嵌入式部署层运行在主控上,负责传感器采集、状态构建、策略推理和控制输出。硬件抽象层把串口、I2C、舵机控制这些接口统一封装,方便后续换主控或者升级传感器。
目录结构大致如下:
duckbot/ ├── sim_env/ # MuJoCo模型 + gymnasium环境 │ ├── assets/duckbot.xml │ ├── duck_env.py │ └── train_ppo.py ├── model_export/ # ONNX导出 + 量化脚本 │ ├── export_onnx.py │ └── quantize.py ├── firmware/ # ESP32-S3固件 │ ├── main/strategy_inference.c │ ├── main/bittle_hw.c │ └── main/deadband_comp.c ├── tools/ # 调试/可视化工具 │ ├── serial_plot.py │ └── web_ui/ └── docs/HARDWARE.md # BOM表和3D打印参数5.2 策略模型的轻量化与嵌入式推理
训练好的MLP只有256×256两层,在PyTorch里参数量约15万。用它做ONNX导出后,再把权重量化成INT8,一个完整模型大概只有40KB左右,ESP32-S3的320KB SRAM放得下。
嵌入式推理没有引入任何推理框架。因为模型就是个两层MLP,一个LLM推理框架反而会增加内存和代码复杂度。我用C语言手写了矩阵乘法,配合查表法实现tanh激活函数,单次推理耗时实测约2.1毫秒,在30Hz控制周期下CPU负载很低。
主控的主循环可以概括成四个阶段:读取传感器并组装14维状态向量、调用策略网络推理得到6维动作、执行死区补偿和角度映射、把目标角度发送给舵机控制模块。
void control_loop_30hz(void) { collect_imu_and_joint_state(&state); mlp_forward_int8(&state, &action); apply_deadband_compensation(&action); apply_smoothing_filter(&action); servo_command_all(action, 6); }5.3 实时调试与可视化工具
开源架构里的调试工具非常影响开发效率。我做了两个工具:一个是串口绘图器,以20Hz频率输出关节角度、IMU姿态和策略动作到PC端,实时绘制曲线;另一个是浏览器端3D可视化界面,主控通过Wi-Fi以WebSocket发送状态数据,浏览器里用Three.js渲染一个简化的鸭子模型,实时转动和翻滚动作都能直观看到。
做这两个工具让我在调试sim-to-real问题时省了大量时间。之前每次改动参数都要重新烧录固件、重启机器人、等好几秒观察行为。有了实时可视化之后,几乎能做到“改参数立刻看到效果”,判断哪个参数影响什么行为的速度快一个数量级。这个经验我也很推荐给所有做硬件RL项目的人——花两周时间做一套调试可视化工具,会在后续调试中为你省下两个月。
6. 实测指标、复现注意事项和一个大胆的下一步方向
6.1 实测指标体系
经过几轮迭代后,我整理了一套可复现的测试指标。所有测试都在室内瓷砖地面完成,环境温度25摄氏度左右。
| 指标 | 测量结果 | 备注 |
|---|---|---|
| 最大行走速度 | 0.12 m/s | 稳定直行10米测均值 |
| 最大转弯角速度 | 0.8 rad/s | 原地转向测试 |
| 连续行走时间 | 32秒(成功率85%) | 触发安全摔倒保护为止 |
| 摔倒后恢复时间 | 约2秒 | 使用摔倒恢复状态机 |
| 满电续航 | 约20分钟 | 伺机空载电流约1.2A |
| 策略推理单次耗时 | 2.1 ms | ESP32-S3 INT8推理 |
把这个和四足机器人比,速度不算快,稳定性也不算顶尖。但在650克级别的6自由度微型双足上,通过强化学习端到端学出走路的完整闭环来说,这个数据我认为已经证明了路线的可行性。
6.2 复现时的常见问题清单
项目开源之后陆续有人复现,我从反馈里整理了几个高频问题,提前写在这里。
舵机型号和扭矩不推荐随意替换。有朋友说自己用的“同规格”舵机,结果动态响应差别很大,鸭子走路像喝醉酒。舵机内部的PID参数差异被策略放大,建议严格按BOM表购买,或者耐心做一轮参数测量后重新训练策略。脚掌材质尽量使用防滑EVA,不要用纯硬塑料。仿真里的摩擦系数是按EVA在瓷砖上测量出来的,如果你换成长毛地毯,摩擦系数完全不同,策略容易摔倒。打印件公差会直接导致关节间隙不同。关节间隙过大会产生额外的自由度,策略无法感知。复刻时条件允许的话,给关节轴加金属轴套,能显著提高结构刚性。
6.3 下一步方向:深度强化学习与因果强化学习的真正结合
这套系统训练出来的鸭子目前使用的是纯PPO强化学习,它能学会走路,却很难回答“为什么会摔”“摔倒是哪条腿先乱了步态”这类因果问题。我下一步准备把因果强化学习(CRL)的思路引进来:通过因果推断工具先学习状态与动作之间的因果关系,再在策略前端嵌入一个“因果优先级判定层”,让机器人摔倒时能快速归因到是高位中心偏移还是脚掌打滑,从而更快触发正确的恢复策略。这个方向目前还在实验环境里跑,短期目标是让“摔倒恢复时间”从2秒缩到0.8秒以内。如果做成了,整个方案的通用性也会更强——未来扩展到大腿更长或者重心更高的小型双足平台上,不需要从头训一遍奖励函数。这也是开源架构最有价值的地方:每一步迭代都能被后来者直接承接,而不是每次都要从零开始。