RK3566四足机器人强化学习部署:从训练到实机行走
2026/9/7 1:35:16 网站建设 项目流程

这篇内容打算聊一次完整的部署经历,从在英伟达 GPU 工作站上训练强化学习步态策略,到把策略搬进一台 25 厘米高的 Microduck 四足机器人,用一块 RK3566 驱动整机站起来、走出去。整件事最花时间的不是训练,也不是调参,而是“部署”这两个字背后的工程细节。

1. 为什么选 RK3566:25 厘米小机器人的算力与功耗账

1.1 先说说 Microduck 整机的情况

Microduck 是一台 25 厘米左右高度的四足机器人,和常见的 Mini Cheetah 那种研究平台相比,它工艺上更像消费级工程样机:12 个关节,每条腿 3 个自由度,机体内部预留给主控板的空间不大。唤醒赛道的无人机和机器狗都在往紧凑方向走,25 厘米级机体要在有限空间里塞进电池、电机驱动板、IMU 和主控制板,所以主控选型相当关键。

在做部署之前得先想明白一个问题:这台机器人要承担多少计算?强化学习推理的量级其实没有大家想象中大,策略网络往往是多层 MLP,输入维度在几十到一百上下,输出 12 个关节动作,参数量几千到几十万级别。真正消耗算力的是高频控制回路,电机力矩指令的生成频率最低也要 200Hz 以上,控制器要在几毫秒内完成“传感器读取、状态估计、策略推理、力矩输出”一整条链路,这个才考验主控的实时性。

1.2 RK3566 和树莓派 / Jetson Nano 的取舍

我之前在别的项目上用过树莓派 4B 和 Jetson Nano,拿来推过强化学习模型。树莓派的 CPU 是四核 Cortex-A72,单核性能尚可,但 GPIO、I2C、SPI 和 CAN 的调度不够硬实时,在线控制循环里很容易出现周期抖动。Jetson Nano 的 GPU 算力强,跑 CUDA 肯定没有压力,但它功耗高、启动慢、体积大,塞进 25 厘米的小机身有点杀鸡用牛刀,而且整个部署链路的镜像尺寸也不太友好。

RK3566 是瑞芯微推出的一颗四核 Cortex-A55 SoC,集成 Mali-G52 GPU 和 0.8 TOPS 级别的 NPU,整体功耗可以压在 3W 到 6W 的范围内。A55 的性能跑 MLP 推理完全够用,NPU 虽然算力不高,但用来推这类小网络反而有优势。再加上 RK3566 原生支持 CAN、SPI、UART、I2C,电机控制板走 CAN 总线还是 SPI,都留了余地。存储可以用 eMMC 加 TF 卡,系统启动到完全运行状态大概 10 秒内,开机即用,非常适合这种嵌入型机器人。

1.3 算力需求和功耗预算怎么算

我按实际需要做了个粗略预算。控制主频跑在 1.8GHz 时,RK3566 CPU 的持续算力大约是 10000 Dhrystone MIPS 的几分之一,这个数字看起来不亮眼,但策略网络单次推理也就几百万次浮点运算。按 400Hz 控制频率,每秒需要 1.6 亿次浮点运算量,A55 是可以轻松承受的。真正不能忽略的是 DDR 带宽和缓存命中率,推理时如果频繁做动态内存分配,带宽会拖后腿。

功耗方面,整机电池如果是 2S 锂电池,容量大约 1200mAh,主控板加电机驱动板的总电流预算大概在 1.5A 到 2A 之间。RK3566 与电机驱动、IMU 加在一起,能够控制在 5W 附近,剩余的功耗全部要留给关节电机。综合来看,RK3566 在 25 厘米这个尺寸等级的机器人上是一个比较合理的“甜点”选择。

2. 训练侧准备:别等训练完了才考虑部署

2.1 策略网络结构与输入输出

很多人在训练阶段完全不考虑部署,训练完了做导出才发现模型里塞满了自定义算子,或者输出层带了额外处理逻辑,跑到嵌入式设备上不是性能差就是直接不支持。我这次的策略网络结构比较常规,动作空间和观测空间设计如下表:

模块维度说明
观测输入大约 70 维关节角度、关节角速度、IMU 四元数、角速度、上一时刻动作
隐藏层[256, 256]MLP,激活函数用 ReLU(导出友好)
动作输出12 维每条腿 3 个关节位置增量(或目标位置)
控制频率250Hz 到 400Hz训练时固定为 250Hz,实机根据电机驱动能力调整

训练用的框架是 Isaac Gym 这类 GPU 并行环境,单张 RTX 4090 跑了大概三个小时,大概 12000 个仿真环境并行采样,拿到了一个能稳定走路的策略。训练阶段零零散散也会用 MuJoCo 导出验证,但最终真机部署用的还是 Isaac Gym 产出的 ONNX。

有一点必须强调:策略网络的输出一定要保持“纯网络输出”,不要在后处理里加滤波或者限幅。这些逻辑放到嵌入式端代码里做,一旦写进网络,导出后处理算子会非常痛苦,一些轻量推理引擎根本不支持。所以我训练时网络输出就是 12 个动作的原始值,所有动作缩放、限幅、平滑全放到部署侧代码实现。

2.2 域随机化其实是给部署减负

单独说说域随机化。很多做强化学习的朋友在训练时喜欢把摩擦、质量、电机力矩上限这些参数全部固定,仿真里跑得很好,一上真机就摔。Microduck 这类小尺寸机器人在实机上受电池电压波动、地面摩擦差异、IMU 噪声的影响比大型平台更大,所以训练时必须做域随机化。

我在 Isaac Gym 里随机化的参数包括:地面摩擦系数(0.4 到 1.2)、电机力矩常数(±15%)、机身质量(±10%)、IMU 角速度噪声(±0.2 rad/s)、动作延迟(1 到 2 个控制步)。这些随机化参数直接决定了 sim-to-real 迁移是否顺利,本质上是在训练侧就把部署端可能遇到的误差提前纳入考虑。

训练结束后的关键指标有两个。一是仿真内平均奖励和成功率,二是策略对不同随机种子的鲁棒性。我会在训练环境里换几组极端摩擦参数跑测试,如果策略在这些条件下还能维持一定速度前进,才敢往真机走。只要训练侧把域随机化做好,RK3566 部署端就能少做很多补偿。

2.3 训练到什么程度算“能导出”

“能导出”这件事需要提前 10 分钟思考,而不是最后才做。我的要求包括:

  1. 网络必须写成继承torch.nn.Module的标准模块,尽量不用torch.jit.script,也不在 forward 里用 Python 控制流。
  2. 只保留 policy 部分,也就是纯 actor 网络,critic 在导出时直接丢弃。
  3. 所有输入输出必须是连续张量,不要用字典、tuple 或者自定义类。
  4. 训练时需要保存跑得最好的 checkpoint,而不是最后一步的 checkpoint。强化学习训练到后期可能 policy 会退化,保存的 checkpoint 要确保在评测里能连续走完几十秒。

我用 PyTorch 训练,最终导出时调用torch.onnx.export。导出之前先写好一个 wrapper 类,把输入张量的维度和名字固定下来。例如输入命名obs,输出命名action,这个命名在 RK3566 上初始化 session 时会直接用。提前把导出脚本写好,每次训练结束后跑一次,确认 ONNX 能正常输出。

还有一点,训练时控制频率和实机控制频率如果有差异,最好在导出前通过动作时间常数匹配。比如训练用 250Hz,实机跑 400Hz,那么动作平滑参数要按时间常数重新算,否则机器人的步态频率和训练时的频率感完全不同,关节会发飘。

3. ONNX 导出与算子梳理:部署路上最容易堵车的路段

3.1 固定动态轴:把 ONNX 输出形状钉死

接触过 ONNX 的人都知道,PyTorch 默认导出的模型经常带dynamic_axes参数,能导出一个 batch 维度可变的图。灵活是好事,但在嵌入式推理引擎上,动态 shape 反而容易触发额外的内存分配和算子优化失效。我这边直接固定 batch size 为 1。

torch.onnx.export( actor, (obs_tensor,), "microduck_policy.onnx", input_names=["obs"], output_names=["action"], opset_version=11, do_constant_folding=True, dynamic_axes=None, )

dynamic_axes=None这个参数很关键,意味着所有输入输出张量都是静态 shape。RK3566 上用 ONNXRuntime 初始化 session 的时候,静态图能够直接做算子融合和内存规划,推理延迟比动态图稳定得多。固定完 shape 之后用onnx.checker校验一遍图结构,确认所有节点输入输出匹配。

3.2 算子检查和 FP16 转换

RK3566 CPU 默认跑 FP32,如果转成 RKNN 用 NPU,则要考虑量化。但在最初的 CPU 版本里,FP32 就够了。检查算子的方法有两种,一是用onnxruntime直接加载跑一次推理,二是用onnx.shape_inference做完整推断。只要能跑通的算子,后面转 RKNN 时才有参考。

有一个小坑是opset_version。我一开始用了 opset 17,里面有些算子比如ReduceSum的 axes 行为变了,ONNXRuntime 老版本解析会有问题。后来统一改用 opset 11,兼容性最好,RKNN 转换工具对 opset 11 的支持也最成熟。如果训练代码里用到了较新的层,尽可能在导出时先简化到 opset 11 能表达的范围内。

FP16 转换的做法是:先用onnxconverter_common里的float16工具,把模型转成 FP16,再用onnxruntime在 x86 CPU 上比较原模型和 FP16 模型的输出差异。但实际测试下来 RK3566 的 CPU 跑 FP16 并没有性能优势,所以最终实机用的是 FP32,FP16 没有采用。不过在做 RKNN 量化时,这个 FP16 版本能作为调试参考。

3.3 用 ONNXRuntime 在本地先验证

本地验证这一步一定不能省。我通常会在训练机上直接跑一段模拟在线控制的循环,用 ONNXRuntime 加载导出的模型,输入随机的观测张量,检查输出维度、数值范围是否和 PyTorch 原模型一致。更严格的做法是把一段真实 rollout 的观测数据全部回放,对比每一步的动作输出误差。

import onnxruntime as ort import numpy as np sess = ort.InferenceSession( "microduck_policy.onnx", providers=["CPUExecutionProvider"], ) obs = np.random.randn(1, 70).astype(np.float32) action = sess.run(["action"], {"obs": obs})[0] print(action.shape, action)

如果这一步数值与 PyTorch 输出误差在 1e-4 以内,就可以进入 RK3566 实机阶段。很多人在这一步漏了验证,结果到板子上发现输出全是 NaN,回溯了很久才发现是 ONNX 导出时某个输入没有归一化。所以请在进入硬件之前先把软件环境排查干净。

4. RK3566 实机部署:先跑 CPU 再考虑 NPU

4.1 开发环境与交叉编译

RK3566 的板子我用的是一块标准的 Radxa 或者类似量产板,系统是 Debian 系的 ARM64 镜像。开发机上先安装好交叉编译工具链,也可以用板子直接编译,但板子上编译一次很费时间,我建议代码量不大就直接在开发机交叉编译,代码量大就先在板子上把环境写好,再同步代码。

推理引擎优先选 ONNXRuntime 的 ARM64 版本。RK3566 是 ARMv8 架构,ONNXRuntime 官方仓库里没有直接提供这么具体的 release,但可以自己在板子上跑pip install onnxruntime,或者用 RKNN-Toolkit2 附带的部分依赖。实测下来直接安装 ARM64 Linux 版本的 ONNXRuntime 是可行路径。

控制主循环我用 C++ 写,方便做高优先级实时线程。Python 绑定的 ONNXRuntime 用来做功能验证,实机控制不用 Python,因为 Python 的 GC 和 GIL 会在一个控制周期里造成随机性,这个不确定性对步态控制是致命的。

4.2 控制循环的主线程优先级

RK3566 虽然是四核 A55,但是 Linux 默认调度器并不严格保证某线程的周期性。为了跑 400Hz 的控制循环,我用了pthread_setschedparam把控制线程设置成SCHED_FIFO,优先级设到 80 左右。同时把控制线程绑定到 CPU2 或 CPU3,避免和其他系统进程争抢。这里不要绑 CPU0,因为内核很多进程和中断都跑在 CPU0 上,绑上去会发生周期抖动。

控制循环的整体结构大概是:

  1. 从电机驱动板读取 12 个关节的角度和角速度,读取 IMU 数据。
  2. 拼接观测向量(状态估计结果、关节信息、IMU 数据、上一帧动作)。
  3. 调用 ONNXRuntime session 做一次推理。
  4. 对输出动作做缩放与限幅,生成目标位置或目标力矩。
  5. 通过 CAN 或 SPI 总线把力矩指令发给电机驱动板。

有一个细节值得注意:控制线程里绝对不能做任何动态内存分配。ONNXRuntime 的 session 在初始化时已经分配好了所有中间张量缓存,运行时推理只要保证输入输出张量的内存地址和大小不变,就不会触发新的分配。我在初始化阶段创建一个固定的输入缓冲区std::vector<float> obs(70),每次推理直接填充这个缓冲区,再把指向该缓冲区的指针传给 ONNXRuntime。

4.3 RKNN 的诱惑与陷阱

RK3566 的 NPU 算力大约是 0.8 TOPS,对一个小 MLP 来说理论上可以跑得很快。但直接用 RKNN 需要把 ONNX 转成 RKNN 格式,量化成 INT8 或者 FP16。这个过程中最大的麻烦不是转换本身,而是量化误差。

强化学习策略对动作的精度其实很敏感,尤其是关节位置增量这种连续量。INT8 量化后,动作输出可能会从原来的连续值变成几个离散档位,机器人跑步时关节会明显抖动。解决方法是做混合量化,把敏感层保留 FP16 或者 FP32,但这样一来 NPU 的加速收益就打了折扣。

我最终的方案是:RK3566 CPU 跑 ONNXRuntime FP32。实测推理延迟约 2 到 3 毫秒,加上状态读取和电机指令下发,整个控制循环能稳定在 1ms 到 2ms 内完成,跑到 500Hz 也没有压力。RKNN 留给后续有时间再做优化。

如果你确实考虑用 NPU,建议多准备一组带标签的观测数据作为量化校准集,量化后必须在实机上对比步态稳定性,不要只看模型输出的数值误差。另外需要留意的是 RKNN-Toolkit2 版本和板子固件的匹配问题,不同版本转出来的模型在驱动上兼容性不一致,建议直接使用开发板供应商推荐的版本组合。

5. 实机联调:从关节回中到站立的演进过程

5.1 关节回中和编码器校准

第一次上电千万不要直接把策略跑起来。先做电机零位校准。Microduck 的每个关节电机通常带磁编码器或者 AB 相编码器,断电后再上电,编码器读数会丢失绝对位置。需要把每个关节手动拨到机械零位附近,然后让驱动板记录当前编码器值作为零位偏移。

我写了个简单的测试程序,逐关节发送零位力矩,观察电机是否缓慢回到机械位置,然后手动把目标角度设为 0,记录编码器读数。这个步骤虽然枯燥,但直接影响后续所有控制。零位不准,后续的 PD 控制目标角度和实际机械角度会存在恒定偏差,机器人站起来的时候会像喝醉了一样往一边倒。

校准完成后,把 12 个零位偏移值存成配置文件,每次开机读取。注意不同批次的电机在安装时机械零位可能不同,不要把这些偏移硬编码进程序里。

5.2 单腿 PD 测试,别急着全机跑

整机站立之前先做单腿测试。把机器人侧躺或者固定在支架上,单独对其中一条腿的三个关节发送位置指令,验证电机方向、速度反馈和 PD 控制是否正常。这一步能暴露很多方向性问题,比如电机正反方向接反、编码器读数正负相反、CAN 通信偶发丢包等。

我实际踩过一个坑:个别关节的编码器读数的正方向与其他关节相反,这会导致强化学习的观测值直接是反的,机器人会觉得某个关节已经到位,实际电机已经转过了。解决方法是逐关节测试位置跟随,给一个正弦波目标,观察实际反馈是否同相位。

单腿测试通过的标志是:给定一个固定角度,电机能在 300ms 内到达并且不震荡;给定正弦波轨迹,实际角度和指令角度的相位差不超过一两个控制周期。这一步全部通过后再解锁其他三条腿。

5.3 站立、试探、行走——第一次跑起来的经验

当 12 个关节的 PD 都稳定后,先不要加载神经网络。我会有另一个环节:直接用一组固定的关节角度让机器人站起来,确认它能维持平衡而不倒。这个时候 PD 增益需要够大,但也不能太大,否则会产生高频振荡。

Microduck 这种小机器人在桌面或者地板上站立时,机身的轻微抖动是很正常的。如果发生明显的身体抖动,先降低 PD 增益里的微分项,或者降低动作转换时间常数,而不是上来就改动策略网络。

第一次加载策略时,我会把策略输出乘以一个缩放系数 0.3,让机器人的动作幅度非常小,观察它是否试图做出步态动作而不是直接倒下。确认没有剧烈异常后逐渐放大缩放系数到 1.0。这个过程类似于电机调试时的安全边际,能在有问题时减小物理损伤概率。

真正跑起来之后的经验是:训练时的动作延迟如果和实机不一致,步态会很难看。比如训练时动作延迟 1 个控制周期,实机如果不做延迟匹配,关节目标位置和实际位置会有一个周期偏移,等效于增加了相位滞后。为了消除这个差异,我在部署代码里手动加入了动作延迟缓冲,缓存两帧之前的动作作为当前实际执行的动作,这样和训练的时序对应起来,走路的稳定性和姿态会明显改善。

6. 实测数据复盘:步态频率、延迟和能耗

6.1 推理延迟和控制频率

实机联调结束后我做了一轮性能测试,把数据整理成表格方便后续定位问题。测试环境是 RK3566 四核 A55 跑 1.8GHz,系统大概占用一个核,控制线程绑定在 CPU2,ONNXRuntime FP32 单次推理,关掉所有打印输出。

测量项数据说明
控制循环平均周期约 2.1ms对应约 476Hz,实际跑 400Hz 留有余量
控制循环最大周期约 3.5ms出现在系统中断占用 CPU 时
ONNXRuntime 单次推理延迟约 2.3ms输入 70 维,输出 12 维,256x256 网络
整机功耗约 7W主控加电机空载,不包含高强度行走
电池续航约 18 到 22 分钟2S 1200mAh 电池,混合步态实测

推理延迟 2.3ms 是这组网络规模下的正常水平。如果你把网络压缩到 [128, 128],延迟可以降到 1ms 左右,但步态质量可能会下降。控制周期稳定在 2.5ms 以内,对 400Hz 控制频率来说没问题。如果哪天看到最大周期超过 4ms,要先检查是不是有别的线程频繁抢占 CPU、DDR 带宽是否被占满、或者板子上 TF 卡读写拖了后腿。

6.2 稳定性优化的几个细节

跑动过程中出现过几次不稳定现象,排查下来原因各不相同,这里分享三个比较典型的经验。

第一个是 CAN 总线丢包。RK3566 与电机驱动板之间如果用 SPI 转 CAN 模块,在高负载时可能出现偶发丢帧,表现为某条腿瞬间失去力矩指令然后又开始动作。我最后在代码里加了一个通信超时保护:如果连续 20ms 没有收到电机反馈,就让所有电机进入安全模式,输出零力矩并停止步态。同时把 CAN 波特率从 500k 调整到 1M,丢包率明显下降。

第二个是 IMU 数据的预处理。RK3566 的 I2C 读取 IMU 数据如果直接在控制线程里做阻塞读取,偶尔会拖慢整个循环。我的办法是开一个独立的 IMU 读取线程,使用 I2C 轮询加 FIFO 缓存,控制线程拿到的始终是最近一次完整 IMU 数据,不会因为在控制循环里等 I2C 而卡住。实测下来,这能让控制周期抖动降低一半。

第三个是打印日志对实时性的影响。开发阶段大家喜欢在控制循环里加printf,在 RK3566 上这会让周期变得非常不稳定,串口打印一次可能消耗几百微秒到几毫秒。我的做法是所有日志通过环形缓冲区记录到内存,再通过另一个低优先级线程定期输出,控制线程保持干干净净零打印。

还有一个容易被忽略的点是 CPU 频率调节。Debian 系统的默认 CPU 调度可能是ondemand或者schedutil,在负载突增时频率切换会产生延迟尖峰。建议把 RK3566 的 CPU governor 固定到performance,用cpufreq-set -g performance,确保控制线程所在核心始终跑在最高频率。

最后说一点关于后续优化方向:RK3566 上 NPU 跑 INT8 量化策略是可以尝试的,但一定要用校准集做精度验证,并且在实际步态中评估;如果你的目标只是稳定运行,ONNXRuntime FP32 + 400Hz 控制循环已经是一个足够好的组合。还有一个很实用的扩展方向是把策略换成稀疏化版本,去掉网络中权重接近 0 的神经元,A55 上的推理延迟可以进一步压缩,不过这属于锦上添花,先把当前版本跑稳定比什么都重要。

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

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

立即咨询