从 GPU 到 RK3566:Microduck 强化学习机器人部署实战手记
2026/9/9 8:22:02 网站建设 项目流程

从英伟达 GPU 到 RK3566 实机:Microduck 25 厘米强化学习机器人部署手记

前阵子把一台 25 厘米足的 Microduck 从 Simulink 里的仿真环境搬到了 RK3566 实机上跑,整个过程比我想象中要折腾,但也特别有意思。这个项目本质上是把强化学习训练出来的运动控制策略,部署到一块几百块钱的嵌入式开发板上,让机器人在真实世界里走路、转向,并且保持稳定。训练用的环境是老黄的 RTX 显卡,推理端则完全跑在一颗四核 A55 的 ARM 芯片上,这两个平台之间的鸿沟比想象中大得多。

这篇手记就当作自己踩坑过程的记录,也希望给正在做类似“训练在 GPU、部署在边缘设备”的强化学习机器人项目的朋友一点参考。内容会覆盖整体设计思路、训练环境搭建、模型转换、RK3566 实机部署,以及我遇到的几个典型问题,全程干货,没有废话。

1. 项目整体设计与硬件选型复盘

1.1 为什么选择 25 厘米这个尺寸,以及 Microduck 形态

Microduck 本质上是一只四足机器人,整机高度大约 20 厘米,体长带尾巴大约 25 厘米。这个尺寸级别有几个优点:首先是桌面级测试非常方便,不需要像大型机械狗那样必须去空旷场地;其次是硬件成本可控,整机物料加控制板加起来在 1500 元上下,比动辄几万块的科研级机器人便宜太多;最后是安全风险小,就算强化学习策略在实机上跑飞了,也就是翻个跟头,不会造成什么人身伤害。

四足形态本身也是一个很好的强化学习验证平台。相比双足,四足天然具有更大的静稳定域,控制难度相对低一些,但又要处理接触、摩擦、协调等问题,非常适合用来训练鲁棒步态策略。我之前跑过单足跳跃和双足平衡的实验,最后经验是:四足是性价比最高的入门载体,既有足够的动力学复杂度,又不会让你在调试上耗尽精力。

选型时的另一个考虑是整机的执行器件。25 厘米级别对应的是一般称为“中尺寸”的舵机,比如串行总线舵机,峰值扭矩大概在 5-8 kg·cm 左右。每条腿三个自由度,总共十二个自由度,这种配置既能保证动作的灵活性,又不会让批量力控伺服驱动的成本太高。我们可以利用强化学习在仿真里直接输出十二个关节的目标角度或力矩,然后通过总线协议下发到舵机,整个控制链路非常直接。

1.2 RK3566 才是真正落地的主角

在很多强化学习项目里,大家习惯把目光放在 GPU 训练上,但真实落地的时候你会发现,推理端往往比训练端更考验功力。这次我选的边缘设备是 RK3566 开发板,一颗瑞芯微的 SoC,四核 Cortex-A55 处理器,主频最高 2.0 GHz,集成 Mali-G52 GPU 和 0.8 TOPS 算力的 NPU,支持 INT8/INT16 量化推理。

为啥选它不选树莓派?树莓派的实际算力也不错,但 RK3566 有几个明显优势:首先是 NPU 的存在让 INT8 推理效率比纯 CPU 高不少;其次是编解码能力强,虽然做机器人控制用不上;最关键的是价格——某宝上带 2GB 内存的开发板一百多块就能拿下,整机成本控制真的很重要。

但这里得说清楚一个现实:0.8 TOPS 的 NPU 放在 2025 年看并不算强,跑大模型是完全不行的,但跑我们的强化学习控制策略绰绰有余。如果策略网络是 MLP(多层感知机),输入维度一般在 30~50 之间,网络结构三层各 256 个神经元,单次前向推理的计算量大概在几百万次乘加,这个规模即便在纯 CPU 上也能在 10 毫秒内跑完。所以 NPU 只是加速手段,不是必需品,真正的瓶颈在于整个控制回路的延迟和稳定性。

1.3 强化学习与 PID 控制的关系,不是互斥而是互补

很多刚开始接触机器人控制的朋友会问:既然 PID 就能让机器人站起来走路,为什么还要用强化学习?我的理解是:传统的 PID 控制需要你手动设计控制律,对于四足这种强非线性、变拓扑的动力学系统来说,调参难度非常大,而且很难在复杂地形下保持最优表现。强化学习的思路是让策略网络直接从状态映射到动作,绕过人工建模的过程。

但这不代表 PID 就完全没用了。我们在 Microduck 上采用的是“底层 PID + 上层强化学习策略”的组合:强化学习策略输出的是关节目标角度或目标力矩,底层的舵机控制器再用 PID 去跟踪这个目标。这样做的原因是,舵机的物理特性决定了它不能直接接受“力矩”这种输入,它自身有位置环、速度环的闭环控制逻辑。所以更准确的说法是:强化学习负责决策,PID 负责执行,两者各司其职。

这种分层架构还有一个额外的好处:就算上层策略出了问题,底层的 PID 限幅和舵机保护机制还能兜底,不至于把机器人摔坏。我在实机测试中遇到过策略输出跳变的情况,如果直接对舵机下指令,瞬时电流会很大,但经过底层 PID 的平滑处理后,冲击明显被缓解了。这一点特别重要,后面我会详细讲。

2. 训练侧:从零到能跑的策略

2.1 训练环境的搭建,从 PyTorch 到 Isaac Gym

既然是强化学习项目,训练环境是大头。我用的框架是 NVIDIA Isaac Gym,这是英伟达出的一个基于 GPU 的仿真平台,可以并行跑数千个环境实例,把训练时间从几天压缩到几小时。Isaac Gym 的安装有特定要求,它依赖于特定版本的 PyTorch 和 CUDA,这里我踩了不少坑,值得单独说一下。

我当时的训练机配置是 i9-13900K + RTX 3090 + 64GB 内存,系统是 Ubuntu 22.04。NVIDIA 驱动版本是 535,CUDA 版本 11.8。PyTorch 装的是 1.13.1,因为 Isaac Gym 1.0 这个版本要求 PyTorch 和 CUDA 版本有严格的对应关系,哪怕差一个小版本都可能导致环境创建时报错。具体安装命令大概是先装 CUDA 11.8 的 PyTorch,再装 Isaac Gym 的 pip 包。整个过程如果顺利的话半小时能搞定,但如果你用的是新版本 CUDA 12.x,大概率会遇到兼容性问题。

注意:Isaac Gym 对 Python 版本的容忍度也很低,建议用 Python 3.8 或 3.9,不要用 3.10 以上的版本,否则 import 阶段就可能报错。

训练本身用到的算法是 PPO(近端策略优化)。开源社区里基于 Isaac Gym 的四足机器人训练代码非常多,MIT 的 Cheetah 项目和 ETH 的 legged_robot 项目都是很好的模板。我这次用的是 legged_robot 的改版,因为它支持自定义机器人 URDF,并且内置了地形生成器和奖励函数模板,改起来特别快。

第一个版本我直接把 Microduck 在 SolidWorks 里的模型导出成 URDF,导入 Isaac Gym。这里有一点要注意:URDF 里每个关节的阻尼、摩擦系数、电机扭矩限制,都会直接影响到训练出来的策略能否在真实机器人上迁移。比如把关节阻尼设得太低,仿真里的机器人会像一个散架的骨架,训练出来的动作在实机上会因为摩擦力不够而跑不起来。

2.2 用 IQL 离线强化学习,为什么能省一只机器人

很多做强化学习机器人部署的朋友,第一步想到的就是在仿真里在线训练策略然后直接迁移到实机。但存在一个现实问题:从仿真到实机的 domain gap 很可能导致策略在真实环境中直接失败。要缩小这个差距,要么用 domain randomization 把策略做得很鲁棒,要么用 system identification 把仿真参数调到和实机接近。前者训练调参的工作量很大,后者需要做频繁的实机数据采集。

这次我采用的是一种折中的方案——先训练一个在线策略采集数据集,再用 IQL(离线强化学习)从数据中学习出一个新的鲁棒策略。这个思路在足式机器人领域越来越流行,因为它能降低 domain gap 的影响:离线学习的策略学会的是“在给定状态分布下做出合理动作”,而不是“对仿真状态映射到动作”,因此对仿真参数的变化不那么敏感。

具体流程是:

  1. 先在 Isaac Gym 里用 PPO 训练一个基础策略,让机器人学会平稳行走。
  2. 用这个基础策略在仿真里大规模采样,加入随机扰动,存储 state-action-reward 数据。
  3. 使用 IQL 算法重新在离线数据上训练策略,但这次的训练不对仿真环境做在线交互。
  4. 将 IQL 训练好的策略直接导出到实机,大幅减少实机调试时间。

我采用的 IQL 实现是基于 JAX 的纯离线强化学习框架,训练时在同一个 GPU 上同时跑环境和学习器。实测下来,整个流程跑一遍大约需要 4~6 小时,视网络大小和数据量而定。相比在线训练加 domain randomization 动辄一两天的调参周期,我爱这个方案。

2.3 奖励函数设计与训练参数复盘

奖励函数是整个训练成败的关键。这次我用的奖励函数包含以下几个部分:

  • 前进速度奖励:机器人骨盆的实际前进速度与指令速度的差距,差距越小奖励越高。
  • 姿态稳定奖励:身体姿态角与目标姿态角的差距,这里限制了 body pitch 和 body roll,但 yaw 不限制,因为转向靠 yaw。
  • 关节限位惩罚:如果舵机角度超过设定的安全限位,给出惩罚。
  • 能耗惩罚:所有关节力矩的平方和,防止策略“暴力”控制,也降低实机关节过热风险。
  • 动作平滑惩罚:相邻两步动作差值的惩罚,防止高频抖动。

训练参数的设置我直接用了一套比较保守但可靠的组合:每个训练批次 4096 个并行环境,学习率 3e-4,GAE lambda 0.95,clip range 0.2,训练步数为 5000 万步。在 3090 上大约跑 6~8 小时收敛,观察训练曲线上平均奖励和平均前进速度都稳定后,就可以导出了。

注意:不要只盯着总奖励曲线,要把速度奖励、姿态奖励分开看。有一次总奖励一直在涨,但我一检查发现速度奖励是负的——策略学会了“摔倒然后爬起来”来刷奖励,这种策略在实机上完全不可用。细看每项奖励分量非常重要。

3. 部署链路:从 GPU 到 RK3566 的完整管线

3.1 模型格式转换的第一个坎:PyTorch 到 ONNX

训练好的策略保存在 PyTorch 的 .pt 文件里,要部署到 RK3566,第一步是把模型转成通用的 ONNX 格式。这一步骤本身原理简单,但实际执行时我会踩一些细节坑。

PyTorch 导出 ONNX 的核心是torch.onnx.export函数,需要指定模型输入输出的维度信息。我们的策略网络输入是状态向量,包括关节角度、角速度、身体姿态、线速度、角速度和指令速度等信息,总维度大约 30~50。输出是十二个关节的目标位置或力矩,维度是 12。

转换的代码如下:

import torch import numpy as np # 加载训练好的策略 model = torch.load("microduck_policy.pt", map_location="cpu") model.eval() # 定义输入维度,构造一个随机输入用于追踪网络结构 dummy_input = torch.randn(1, obs_dim, dtype=torch.float32) # 导出 ONNX torch.onnx.export( model, dummy_input, "microduck_policy.onnx", export_params=True, opset_version=11, # 注意 opset 版本,RKNN 对高版本支持不好 do_constant_folding=True, input_names=["obs"], output_names=["action"], dynamic_axes={"obs": {0: "batch_size"}, "action": {0: "batch_size"}}, )

这里有三个容易出错的地方。 第一个是dynamic_axes。我一开始设置了动态 batch,导出的模型在 RK3566 上跑的时候总是报维度错误,后来发现 RK3566 的 RKNN-Toolkit2 对动态维度的支持有限,要么用固定的 1 维 batch,要么转成 RKNN 前用固定 shape 导出 ONNX。最后我的做法是导出一个固定输入 shape 的版本用于 RKNN 转换,另外再导出一个动态 shape 的版本用于调试。 第二个是 opset 版本。RKNN-Toolkit2 目前对 opset 11~13 的支持最好,如果你用了默认的 opset 17 甚至更高,转换时很容易遇到不支持的算子。 第三个是do_constant_folding=True。很多教程会跳过这个参数,但它能帮你把 batch normalization 的均值和方差提前融合进前面的卷积或全连接层的权重里。如果你的策略网络用了 BatchNorm,这个参数不加的话,模型部署后精度会出现明显的漂移。

3.2 RKNN 转换:从 ONNX 到 NPU 能跑的格式

RK3566 的 NPU 不能直接跑 ONNX 格式,需要用瑞芯微官方的 RKNN-Toolkit2 工具链转换。这一点官方文档写得比较零散,流程大致是:

# 在 x86 机器上安装 RKNN-Toolkit2 git clone https://github.com/airockchip/rknn-toolkit2.git cd rknn-toolkit2 pip install -r requirements_cp38-1.5.0.txt pip install rknn_toolkit2-1.5.0-cp38-cp38-linux_x86_64.whl

转换脚本的核心逻辑如下:

from rknn.api import RKNN rknn = RKNN() # 配置量化参数 rknn.config( mean_values=[[0, 0, 0]], std_values=[[1, 1, 1]], target_platform="rk3566", quantized_dtype="w8a8", # 权重量化为 INT8,激活也量化 quantized_algorithm="normal", ) # 载入 ONNX 模型 ret = rknn.load_onnx(model="microduck_policy.onnx") assert ret == 0 # 构建 RKNN 模型 ret = rknn.build(do_quantization=True, dataset="dataset.txt") assert ret == 0 # 导出 RKNN 文件 ret = rknn.export_rknn("microduck_policy.rknn") assert ret == 0

dataset.txt里面是用于校准量化的输入样本文件路径列表,每行一个 npy 文件。这里要特别注意:量化校准数据集必须覆盖到机器人真实运行时可能遇到的状态范围,比如你训练时有各种各样的姿态、速度、地面情况,校准数据也要包含这些。如果只用一组很接近的数据去校准,量化后的模型在极端输入下会出现很大的误差,实机上表现为突然摔倒。

我试过用 500 组随机状态做校准,精度损失大约在 1% 以内,实机表现完全没问题。但如果只用 50 组全是很平稳的状态去做,测试时发现一旦机器人遇到小台阶或者被推了一下,策略的输出就会明显异常。

3.3 在 RK3566 上跑的推理接口

RKNN 模型转换完成后,在 RK3566 上部署就比较舒服了。瑞芯微提供了一个轻量的 Python 推理库rknn-toolkit-lite2,安装后在开发板上直接 import 即可。

from rknnlite.api import RKNNLite # 初始化 rknn_lite = RKNNLite() ret = rknn_lite.load_rknn("microduck_policy.rknn") assert ret == 0 # 初始化 NPU 核心 ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2) assert ret == 0 # 推理 obs = np.array([...], dtype=np.float32).reshape(1, obs_dim) action = rknn_lite.inference(inputs=[obs])

在实际使用时,我发现inference函数会有一定的调用开销,如果控制环路频率设定在 50Hz(每 20 毫秒推理一次),这个开销占整个周期比例很小,对性能影响不大。但如果想跑到 100Hz,建议在 C 层直接调用 rknn 的接口,Python 层的延迟会成为瓶颈。

关于 NPU 核心的分配:我试过只使用一个核心(NPU_CORE_0)和三个核心(NPU_CORE_0_1_2)两种情况。由于我们的网络很小,一个核心的推理时间大约是 1~2 毫秒,三个核心并行约 0.5 毫秒,差别不大。所以如果你没有其他任务要跑,用单核心就行,省得跟别的线程争抢资源。

4. 实机调试与常见问题排查

4.1 步态频率和指令周期的匹配问题

训练完成、模型部署好,到了最激动人心的实机测试环节。第一次下电的时候,我满怀期待地让机器人走起来,结果它抖成一团,像癫痫发作一样,完全站不起来。排查了半天,发现最核心的问题是:仿真里的控制频率和实机的控制频率不一样。

在 Isaac Gym 里,我的控制频率设的是 50Hz(20 毫秒周期),但实机上舵机控制总线的刷新率也是 50Hz。理论上应该匹配,但实际执行时,策略推理结果下发到舵机,舵机执行到实际位置变化,这个反馈链路存在延迟,导致稳定性变差。

解决方法是引入“反馈-前馈”的思路:策略输出的目标角度不直接作为舵机目标,而是通过一个低通滤波器平滑后再下发,同时对关节实际角度与目标角度的误差做比例补偿。这一步本质上就是把底层 PID 控制加回来,但比例系数不能太大,否则会因为反馈延迟引发振荡。

另一个问题是步态频率。仿真里的步频是 2Hz(每 0.5 秒一个完整步态周期),对应的行走速度大约是 0.5m/s。但是实机上因为舵机的响应速度有限,2Hz 的步频会让腿摆不过来,动作看起来特别僵硬。我把策略输出的频率固定住了,但增加了一个“动作频率缩放”参数,实机上把步频降到 1.5Hz,速度降到 0.35m/s,机器人立刻就正常走路了。

4.2 浮动基座坐标不一致导致的动作异常

这是最有意思的一个坑。训练好的策略在仿真里走得很好,但一到实机,机器人的动作就完全不对,不是单纯地抖动,而是整个姿态都扭曲了。我花了整整一天才定位到问题——坐标系的定义不同。

Isaac Gym 里机器人浮动基座(骨盆)的坐标原点在仿真里默认是机器人的初始位置,坐标轴方向是固定的。但实机上,我的代码读取 IMU(惯性测量单元)数据时,把 gravity vector 当成了某些方式,偏向于 body frame 的方向。这就导致策略输入的状态和训练时的分布完全对不上,相当于你用一套斤不是为这套环境准备的策略去控制一个从未见过的机器人,行为自然完全错乱。

排查思路是把策略网络的输入打印出来,和仿真里的输入做对比,观察每个维度的量纲和符号是否一致。我当时打印后发现 body angular velocity 的 z 分量符号反了——因为 IMU 的轴定义和仿真里的坐标轴定义不同。修正后,机器人立刻恢复正常。所以在这里强烈建议:做仿真到实机迁移时,第一步先把状态向量的每个维度逐一验证,而不是直接让机器人走路。

4.3 实测采样频率与调参技巧

经过几轮修改后,我的实机控制回路工作正常了。实测数据大概是这样的:

  • CPU 占用率:RK3566 上运行完整的控制栈(姿态解算 + 策略推理 + 舵机控制),四核 A55 大约占用 15%。
  • NPU 占用率:单次推理 1.2ms,完全不是瓶颈。
  • 控制频率:实测稳定运行在 48~50Hz,偶尔会有调度抖动,但没影响机器人稳定性。
  • 机器人行走速度:最高 0.45m/s,连续行走 10 分钟电池温度在 45℃ 左右,舵机温度在 55℃ 左右。

如果你也遇到策略在实机表现不如仿真,我建议按顺序排查这几项:

  1. 先看传感器预处理有没有问题:IMU 的滤波参数、角速度的单位换算(弧度/度)、关节编码器的零点校准。
  2. 再看控制频率是否匹配:策略输出的频率和控制指令下发频率是否一致,不一致会造成“动作输出被重复执行两次”的问题。
  3. 最后看模型量化是否过度:如果 RKNN 转换时校准数据太少,可以重新生成校准数据再转换一次。

我在实机测试中还发现一个操作层面的技巧:先在“悬空”模式下测试。把机器人的四条腿垫高,让它悬空,然后跑控制程序,观察关节运动是否协调。如果悬空模式下动作都乱套,那大概率是策略输入的问题,而不是地面交互的问题。等悬空模式稳定后,再放到地面上测试,会省很多事。

注意:实机测试时一定给机器人装上一个带提手的背带或挂架。一旦机器人摔倒,一是能防止它继续在失控状态下摩擦地面,二是可以直接提起来断电,避免舵机长时间堵转过热烧毁。别问我怎么知道的,我的第一台 Microduck 舵机就是这么烧掉的。

4.4 常见问题排查速查表

为了方便对照排查,我把这次实机部署过程中遇到的问题整理成了一个速查表:

现象可能原因解决方法
机器人站不起来,腿部没有反应策略输入的状态向量全是 0,或者数据不一致检查 IMU 数据是否正常初始化,关节编码器读数是否变化
机器人站起来但剧烈抖动控制频率过高导致反馈延迟;底层 PID 增益过大降低控制频率到 30-50Hz,减小 PID 比例系数
机器人可以站立但不会前进步态频率与舵机响应速度不匹配;指令速度输入始终为 0调整步态频率缩放,确认指令速度接口有数据输入
策略在仿真好但在实机完全乱走坐标系的定义不一致或传感器数据的轴方向反了逐维度打印策略输入,和仿真数据的分布进行对比
RKNN 量化后策略输出异常校准数据集覆盖不足采集覆盖面更广的校准数据,重新做量化
舵机发热严重策略输出高频抖动;底层 PID 限幅设置过松增加动作平滑惩罚,在实机代码中加入低通滤波器
电池电压掉太快舵机堵转电流过大检查机器人是否被卡住,调整策略的能耗惩罚权重

5. 从仿真到实机,还有一些值得深挖的方向

这次部署算是一次完整的“仿真训练-模型压缩-边缘部署”的流程验证,但坦白说,目前这个方案还有很大的优化空间。比如我这次完全用的是 MLP 策略网络,没有用到 Transformer 或者基于扩散模型的策略,理论上更强的模型能给机器人带来更复杂的运动模式。但受限于 RK3566 的算力,这类大模型估计要等后续带更强 NPU 的 SoC 才能跑起来。

另外,我这次用的是 IQL 离线强化学习来提高鲁棒性,但说实话,真正让策略在实机上站稳的,是整个控制系统的工程细节——频率同步、数据校验、反馈滤波、保护机制。强化学习只是其中的一个环节,甚至在系统和策略层面的努力占比是七三开。做这个领域的人,一定要有全栈思维,不能只盯着训练曲线。在部署过程中,我不断意识到的一个核心观点是:仿真到实机的迁移,起决定作用的往往不是训练技巧,而是工程耐心。

如果你也正在做一个类似的机器人部署项目,建议从小处着手,先让机器人在悬空模式下动起来,再让它在地面上站住,最后再去探索各种行走、转向、爬坡的能力。每一步都有很多细节,但每一步也都有实际的解决方案。希望这篇手记能帮你绕开我踩过的这些坑,让你把更多精力放在机器人的运动能力本身,而不是和传感器数据和模型格式死磕。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询