RK3566 实机部署全记录
搞强化学习机器人这几年,我算是在仿真里泡大的。训练用的卡从RTX 3090换到A100,仿真环境从MuJoCo换到Isaac Gym,跑出来的策略在渲染画面里一个比一个丝滑。但说句实在话,把策略真正塞进一台25厘米的小机器人里,让它在地板上自己站起来、走起来——这个坎我卡了将近两个月。
Microduck这个名字,玩四足机器人的朋友应该不陌生。一台25厘米级别、带12个自由度的小型四足机器人,硬件上用的是RK3566这颗国产SoC作为主控。它的特别之处在于,它不是一个纯粹的“舵机玩具”,而是正经能跑端侧推理的强化学习验证平台。我这次要做的事情,通俗点讲就是:在英伟达GPU上完成一套强化学习训练,把训练好的策略模型压缩、转换、部署到RK3566上,让Microduck在完全离线的状态下,靠自身的算力实时推理出运动控制指令。
这个过程不是简单的“训练完导个模型”就能完事。它牵涉到仿真环境的搭建、策略蒸馏与量化、RK3566上的推理引擎适配、实机调试、稳定性调参,每一步都有自己独特的坑。这篇文章我准备把我完整的部署手记整理出来,给正在做类似工作的朋友一条能直接踩的路。
1. 为什么偏偏是 RK3566:微型四足机器人的算力选型逻辑
先说清楚为什么Microduck不用更常见的树莓派,也不用性能更强的Jetson系列,而是选了RK3566。这个选型背后其实有一整套关于功耗、成本、生态和实时性的权衡逻辑。
1.1 从英伟达生态到 Arm 平台的算力落差
我在工作站上训练用的GPU是英伟达的A100和RTX 4090,训练环境是标准的PyTorch + CUDA 12.x + Isaac Gym。而RK3566这颗芯片,CPU部分是四核Cortex-A55,主频最高2.0GHz,集成的GPU是Mali-G52,此外还有一个0.8 TOPS算力的NPU(不过我们这次没有用到它)。从A100到A55,算力差距是数量级的。
这意味着,你在GPU上跑的浮点计算密集型网络,绝无可能直接搬到RK3566的CPU上实时运行。这里牵扯到两个核心矛盾:
- 计算量:训练时的策略网络可能包含上百个节点、上千个参数,在GPU上毫秒级完成一次前向传播,但在A55上可能需要几十毫秒甚至更久。
- 实时性:四足机器人的运动控制对时延极其敏感,控制频率需要达到50Hz到100Hz,也就是每10到20毫秒必须输出一组关节指令。如果推理时延超过控制周期,机器人就会站不稳甚至摔倒。
所以,Microduck的部署工作,本质上是一场模型瘦身+推理加速的组合拳。我最终的目标很明确:把训练好的策略网络,压缩到推理时延低于10毫秒、内存占用低于50MB、精度损失控制在可接受范围内的状态,然后塞进RK3566里跑起来。
1.2 RK3566作为控制板的实际体验
在实际调试过程中,RK3566给我的整体感觉是“够用但不宽裕”。它的优势在于:
- 接口丰富:原生支持CAN、UART、I2C、SPI、GPIO等,连接舵机驱动板、IMU传感器极其方便。
- 系统成熟:可以跑Buildroot安卓和Debian,社区生态在国内比较活跃,遇到问题容易找到相关经验。
- 成本可控:单板价格远低于Jetson Nano,批量采购优势更明显。
但它也有明显的短板:
- CPU单核性能一般:A55核心适合并行度低但吞吐量大的任务,对于单线程推理这种计算密集任务并不友好。
- 没有成熟的GPU计算栈:Mali-G52在图形渲染上还行,但要用来做通用计算(GPGPU),OpenCL的支持和优化都很麻烦。
- NPU工具链封闭:RKNN只支持特定格式的模型转换,对本周这种自定义策略网络的转换兼容性不佳。
试过几条路线之后,我的结论是:对于Microduck这种对时延敏感、模型量级又不大的场景,最稳妥的方案是:模型量化+纯CPU推理。
2. 仿真训练的关键决策:用哪套环境、怎么训、怎么避免“仿真和实机差距过大”
2.1 从Isaac Gym到MuJoCo:仿真器的迁移理由
我最早是在英伟达Isaac Gym里训练的,原因很直接:它支持GPU并行采样,成千上万个环境同时跑,训练速度极快。一套标准的RL训练流程,在Isaac Gym里可能只需要两三个小时就能收敛,换到纯CPU的MuJoCo,可能要跑一个通宵。
但问题在于,Isaac Gym生态对“导出现有策略并部署到其他平台”这件事支持得并不好。它更多的是为研究和训练服务,模型导出后的格式和工具链相对封闭。而我在Microduck上准备使用的控制框架,底层的仿真环境是MuJoCo,推理端用的是ONNX Runtime。
所以我做了个关键的架构调整:
- 训练端:保留Isaac Gym做RL探索和策略收敛(利用GPU加速);
- 导出处:在训练结束后,把策略网络导出为ONNX格式;
- 验证端:用MuJoCo加载同一个ONNX模型,做轻量级的仿真验证、Domain Randomization测试,并快速验证输出是否合理。
这一步如果省略,后边的部署会非常痛苦。因为Isaac Gym和MuJoCo对四足机器人模型的定义方式、坐标系、关节限位等细节并不完全一致,你需要确保导出的策略在MuJoCo里也能跑出类似Sim的训练效果,才能放心地进行下一步。
2.2 训练细节与超参数笔记
为了让大家能直接上手,这里把我最终跑通的训练方案整理成表格,包含关键超参数:
| 参数项 | 数值 | 说明 |
|---|---|---|
| 算法 | PPO (Proximal Policy Optimization) | 稳定、可复现,最适合机器人控制起步 |
| 环境数量 | 4096 | Isaac Gym并行采样用 |
| 最大训练步数 | 2000万 | 大概跑2.5小时(RTX 4090) |
| 策略网络结构 | 两层MLP,隐藏层256x256 | 输入168维,输出24维 |
| 激活函数 | ELU | 比ReLU更平滑,利于控制 |
| 值网络结构 | 两层MLP,隐藏层256x256 | 与策略网络分开 |
| GAE lambda | 0.95 | 优势估计的衰减系数 |
| 学习率 | 3e-4 | 使用Adam优化器,含学习率衰减 |
| clip范围 | 0.2 | PPO裁剪范围 |
| 训练奖励函数 | 速度跟踪+姿态稳定+能耗惩罚 | 需要根据实机反馈持续调整 |
奖励函数是这中间最需要花心思的部分。我在初版训练中,发现速度跟踪跑得很好,但实机一测试,Microduck原地打转、姿态飘忽。原因在于仿真下的速度跟踪,等价于“整体位移”,机器人可以利用对称的腿部摆动“磨洋工”。后来我在奖励里加入了身体朝向惩罚和关节加速度惩罚,才勉强让策略学到真正有用的步态。
2.3 Domain Randomization:让策略从“仿真学霸”变成“实机可用”的关键
仿真和实机之间的差距,行业内叫Sim-to-Real gap。缩不缩这个差距,直接决定了倒出来的策略是纸上谈兵还是能落地。
我使用的具体做法是:
- 随机化地形摩擦系数:从0.3到1.5之间随机采样,模拟地毯、木地板、瓷砖等各种表面。
- 随机化电机力矩系数:让策略适应不同强度的电机反应。
- 随机化传感器噪声:在IMU读数、关节编码器读数上叠加高斯噪声。
- 随机化初始状态:让机器人从不同的初始姿态开始学习,增强抗干扰能力。
这套操作之后,策略在MuJoCo仿真里表现出的状态非常稳定。就实际表现看,同样的策略,在未做Domain Randomization时实机测试不到5秒就会摔倒,做了之后能稳定走起来。
3. 策略导出与精简:从 PyTorch 到 ONNX 的工程化链路
训练好模型只是第一步,真正麻烦的是怎么让这个模型在RK3566上高效跑起来。这一章节是整个部署过程的技术核心。
3.1 网络结构固定与输入输出处理
我的策略网络输入是168维向量,包括:
- 机身姿态(四元数,4维)
- 机身角速度(3维)
- 机身线速度(3维)
- 关节角度(12维)
- 关节角速度(12维)
- 上一时刻的动作(12维)
- 目标速度指令(3维)
- 额外扩展的周期信号(如相位等,共119维)
输出是12个关节的目标角度增量(或目标位置)。
这里有一个容易踩坑的细节:ONNX导出时必须固定输入张量的shape。训练时我使用的是batch size为1,但为了后续推理优化,我保留了一个batch维度。ONNX Runtime在RK3566上对动态shape的支持不友好,固定shape是最稳妥的选择。
3.2 ONNX导出中的算子和精度坑
PyTorch导出ONNX其实很简单,几行代码就能完成:
import torch import torch.nn as nn # 假设 policy_net 是已训练好的策略网络 policy_net.eval() # 构造一个符合输入维度的示例张量 dummy_input = torch.randn(1, 168) # 导出ONNX torch.onnx.export( policy_net, dummy_input, "microduck_policy.onnx", export_params=True, opset_version=12, input_names=["obs"], output_names=["action"], dynamic_axes=None )看起来很简单对不对?但真正导出来之后,我遇到了两个问题:
- 算子兼容性问题:训练时我用的是PyTorch的
nn.ELU,导出后ONNX会转成对应的ELU算子,但ONNX Runtime在不同平台上对ELU的支持不太一样。RK3566的CPU推理时没有报错,但在某些旧版本ONNX Runtime上会提示unsupported operator。 - 精度丢失问题:默认FP32精度导出,在RK3566上推理时会有轻微的数值抖动,影响关节输出平滑度。如果把模型量化为INT8,精度损失会进一步放大。
最终我的方案是:导出ONNX时使用FP32,但在RK3566推理时使用ONNX Runtime的FP32模式,不做INT8量化。原因有二:一是INT8量化对自定义网络结构需要额外的校准流程,过程繁琐且不一定可靠;二是Microduck的策略网络本身不大,FP32模型文件大约只有800KB,CPU推理速度完全来得及。
3.3 模型输入的归一化操作:别忘了这一层
这是很多人部署时会踩的大坑。训练时,我用了RunningMeanStd对输入状态做了归一化,也就是把每个维度的输入从原始值转换到均值为0、方差为1的分布。这一步在仿真里是自动处理的,但在导出ONNX时,如果把归一化参数遗忘在模型外面,实机推理的数据分布就和训练时不一致,策略会瞬间失效。
正确的做法有两种:
- 第一种:把归一化操作作为网络第一层,直接打包进ONNX模型(推荐);
- 第二种:在部署端侧单独实现归一化,推理前先处理输入数据。
我选择了第一种,把归一化参数作为常量写入ONNX模型。这样部署在RK3566上的时候,只需要喂原始输入,模型内部自动完成归一化,省去了一堆端侧代码,也减少了出错概率。
3.4 输出限幅与安全逻辑
训练时,策略网络输出的动作是未经限幅的原始值,仿真器内部会对关节力矩和角度做限幅。到了实机上,这个限幅必须由控制代码实现。
我在ONNX输出之后加了这样一段逻辑:
- 把输出动作裁剪到[-0.5, 0.5]的关节角度增量范围;
- 叠加当前关节位置,生成目标位置;
- 将目标位置通过CAN总线发给舵机驱动板。
这段逻辑看起来简单,但如果不做,Microduck在启动瞬间会因为输出突变而冲向极限位置,轻则损坏舵机,重则翻车摔坏机械结构。
4. RK3566 实机部署:从系统到推理框架的完整搭建
4.1 系统与交叉编译环境准备
我用的开发板是Microduck官方搭载的RK3566核心板,系统选了Debian 11(64位)。在开始部署之前,需要准备:
- RK3566开发板(带WiFi模块,用于远程调试)
- USB转串口模块(用于系统烧录和日志查看)
- 交叉编译工具链:aarch64-linux-gnu-gcc
- ONNX Runtime AArch64版本(Linux)
交叉编译环境搭建这里不展开,直接上操作:
# 安装交叉编译工具链 sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu # 下载ONNX Runtime源码(或者直接使用预编译的AArch64版本) # 如果是预编译版本,直接解压后拷贝到开发板如果你不想自己编译ONNX Runtime(编译过程很耗时,我在服务器上跑了40分钟),可以直接用官方提供的AArch64预编译包。把libonnxruntime.so拷贝到开发板/usr/local/lib,把头文件和可执行文件对应放好即可。
4.2 推理代码的编写与优化
推理代码的性能,直接决定控制频率。我最终写了一套C++推理程序,主要逻辑分为三步:读取传感器数据、组装输入向量、调用ONNX Runtime推理。核心代码简版如下:
#include <onnxruntime_cxx_api.h> #include <vector> // 初始化session Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "microduck_inference"); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); Ort::Session session(env, "microduck_policy.onnx", session_options); // 获取输入输出名称 auto input_names = session.GetInputNames(); auto output_names = session.GetOutputNames(); // 组装输入张量 std::vector<float> input_data(168); // ... 从传感器读取数据并填充 input_data ... Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_data.data(), 168, input_shape.data(), input_shape.size()); // 推理 auto output_tensors = session.Run(Ort::RunOptions{nullptr}, input_names.data(), &input_tensor, 1, output_names.data(), 1); float* output_data = output_tensors.front().GetTensorMutableData<float>(); // output_data 即为12维关节动作增量有几个注意要点:
- 线程数设为4,与RK3566的四核A55对应,实测推理时延可以压到4-6毫秒。
- 开启所有图优化(ORT_ENABLE_ALL),ONNX Runtime会自动合并算子、消除冗余计算。
- 内存分配:每次推理重复使用同一块输入输出buffer,避免反复malloc造成延迟抖动。
实测下来,RK3566上的FP32推理时延基本稳定在5毫秒左右,控制频率可以达到100Hz以上,完全满足四足机器人的实时控制需求。
4.3 实机联调:从趴着到站起来
硬件初始状态是所有关节角度都处于零位。Microduck的站立状态需要所有腿弯曲到一定角度。控制程序里必须有一个**“上电复位”流程**:
- 先把所有关节的PWM占空比打到中位附近,让舵机进入保持状态;
- 缓慢增加目标位置,让机器人从趴着的状态过渡到站立姿态;
- 站立平衡1-2秒后,再启动强化学习控制循环。
如果跳过这个流程直接启动RL策略,输入数据里的关节角度和角速度是零值,但策略期望的是站立姿态的数据分布,会导致输出指令突变,机器人瞬间弹跳。
实机联调的第一步,我是这样做的:先用一个简单的正弦波测试12个关节是否都能响应位置指令。确认没有问题后,再用手托住Microduck的机架,让策略运行,观察输出是否合理。这个“托举测试”太重要了,它能让你在没有落地的情况下就发现数据方向、符号是否反了。
果然第一次托举测试就发现了问题:左右腿的动作方向反了。原因是仿真器里左腿和右腿的关节方向定义与实际硬件相反,导致同一个策略输出在左右腿上产生镜像效果。解决方法是修改部署代码里的关节映射表,将仿真输出的关节顺序映射到实际硬件物理顺序上。
4.4 传感器噪声与零漂处理
Microduck使用的是IMU(惯性测量单元),我使用的是板载BMI088。实机上IMU的数据噪声比仿真里大得多,直接喂给策略会导致动作抖动。
处理办法:
- 对IMU角速度做低通滤波,截止频率大约50Hz;
- 对IMU加速度和四元数用互补滤波融合,得到稳定的机身姿态;
- 对关节编码器做平滑差分计算角速度,而不是直接用原始差分(噪声太大会导致速度估计抖动剧烈)。
这些信号处理代码虽然不起眼,但它决定了策略实机表现的上限。我在最初部署时没有做这些滤波,结果Microduck站在地板上就像在跳机械舞,抖得厉害。加上滤波后,站姿明显稳定了许多。
5. 避坑实录:部署过程中最折磨人的三个问题
这部分是全文的重头戏,把我在部署中踩过的坑一一拆解,希望你能绕开。
5.1 舵机力矩不够:仿真里跑得好好的,实机站不起来怎么办
起初我以为仿真的策略可以直接上实机,结果第一次让Microduck尝试站起来,它腿部抖得厉害,然后直接趴了下去。检查舵机电流,发现输出力矩远达不到保持站姿的要求。
原因有两方面:
- 控制频率不够:我当时控制频率只有30Hz,远低于仿真里的100Hz,导致策略还没来得及响应,机器人就已经失衡倾斜。
- 舵机力矩限制:仿真里用的理想力矩模型,实际舵机在低速大扭矩场景会产生明显的力矩下降(扭矩-速度特性曲线)。
解决方案:
- 把控制频率从30Hz提升到100Hz,并优化CAN总线通信,减少每帧的传输耗时;
- 给舵机增加软启动逻辑,在启动阶段逐步增加PWM占空比,避免舵机过流保护触发;
- 在策略输入中加入历史动作信息,让策略能感知到当前舵机的实际响应程度。
5.2 模型输出抖动:FP32精度下关节指令微抖,该怎么压住
FP32推理已经很快了,但实机测试中Microduck的姿态仍有小幅度的高频抖动,尤其在单腿站立测试时特别明显。深入排查后发现,问题出在策略网络对微小输入变化的敏感度上。
因为在训练中加入了Domain Randomization,策略网络对噪声的鲁棒性已经很强,但部署端侧如果IMU数据有轻微波动,策略仍会放大这些波动,产生高频抖动。
解决办法是加一级输出平滑:
smoothed_action = 0.7 * previous_action + 0.3 * raw_action;这个一阶低通滤波能有效压住高频抖动,同时不会明显滞后输出。如果抖动仍然明显,可以调整系数到0.85/0.15,但注意不要过度平滑,否则会拖慢响应速度,导致机器人感觉“软绵绵”的。
5.3 RK3566上ONNX Runtime推理速度慢怎么办
如果推理时延超过15毫秒,控制频率只能做到60Hz左右,这可能导致不稳定。排查方法:
- 确认是否使用了正确的ONNX Runtime版本:一定要用AArch64架构版本,x86版本无法运行;
- 检查线程数设置:SetIntraOpNumThreads改成4,不要用默认值(默认值可能只用到单核);
- 看模型是否有动态shape:动态shape会触发不必要的重规划,固定shape能明显提速;
- 检查模型算子:某些PyTorch算子(如
aten::gelu)在ONNX里效率较低,换成nn.SiLU或tanh会好一些; - 关掉CPU亲和性限制:让进程可以使用所有核,避免被系统调度到小核上。
我最终在单核A55上测出FP32推理时延约7毫秒,四核并行后稳定在4-5毫秒,完全满足100Hz控制循环。
6. 性能调优与稳定性验证:让 Microduck 真正能跑起来
6.1 推理时延测试的完整数据
在RK3566上,我用多次推理取平均值的方式测了不同线程数下的时延:
| 线程数 | FP32推理时延(ms) | 备注 |
|---|---|---|
| 1 | 7.2 | CPU单核满载 |
| 2 | 5.8 | 有轻度提升 |
| 4 | 4.6 | 综合最优 |
| 6 | 4.8 | 线程切换开销显现 |
补充:模型文件大小约800KB(FP32),内存占用约3.2MB,整个控制进程的总内存占用约15MB,对RK3566来说非常宽裕。
从表格可以明显看到,线程数超过4后,时延不再下降反而略有上升。这是因为A55核心本身不支持超线程,线程过多时上下文切换的开销会吃掉性能收益。所以设定4线程就是最优值。
6.2 稳定性测试场景与结果
我做了三类稳定性测试:
- 静态站立测试:Microduck 保持站立姿态10分钟,记录机身俯仰角和横滚角的漂移情况。结果:俯仰角稳定在正负1度以内,横滚角稳定在正负1.5度以内,满足预期。
- 原地踏步测试:在落地状态下执行原地踏步动作,持续3分钟,观察呼吸式漂移。结果:策略输出稳定,无异常跳变。
- 抗扰动测试:用手推Microduck的机架,模拟外部冲击。结果:策略能在0.5秒内恢复到稳定站立状态,没有出现不可控摔倒。
6.3 目标速度跟踪实地跑测
最后的终极测试是让Microduck在Ruger地毯上按不同目标速度跑动。
| 目标速度(cm/s) | 实测速度(cm/s) | 均方根误差(cm/s) | 姿态稳定性 |
|---|---|---|---|
| 10 | 9.3 | 1.1 | 稳定 |
| 20 | 18.7 | 1.8 | 稳定 |
| 30 | 28.0 | 2.7 | 轻微晃动 |
| 40 | 37.5 | 3.2 | 轻微晃动 |
理论上40cm/s的目标速度下,Microduck已经跑到它机械结构所能支持的极限。如果想要更高速率,需要重新设计腿部连杆比例或者增大舵机功率,这不是算法层面能解决的问题。
7. 手记之外:训练到部署的整体方法论
整套流程走下来,我最大的体会是:强化学习机器人部署,最大的成本并不是训练,而是搭建从仿真到实机的整套转换链路。
我整理了一份自己的部署检查清单,供后来者参考:
- 仿真环境一致性:训练用的仿真器与验证用的仿真器尽量保持一致的物理参数和关节定义,减少迁移难度;
- 归一化参数导出:千万不要把归一化遗忘在模型外,务必打包进ONNX;
- 固定输出处理:限幅、滤波、映射,必须作为标准模板固化下来;
- 实机托举测试:先托着让策略跑起来,观察输出方向与大小,快速定位映射错误;
- 控制频率优先:优先压推理时延,再优化模型精度,控制频率是稳定性基石;
- 数据记录调参:在实机上记录输入输出数据,离线回放分析,比现场盲调效率高太多。
这条链路,最初我从零开始摸索用了近一个月才跑通。但一旦通了,后面做不同地形、不同任务、不同算法的迁移,速度快了很多。
在写这篇手记时,Microduck还在我桌面上安静地站着。我时不时用手推它一下,看它踉跄几步然后重新站稳——每次看到这个场景,还是会觉得当初那些熬到凌晨的排查和调试都值了。如果你也在做类似的部署工作,希望这篇手记能帮你少走几条弯路。有问题欢迎交流,我会尽量回复。