RK3566部署强化学习四足机器人:从PyTorch到实机控制
2026/9/6 11:51:34 网站建设 项目流程

1. 项目背景:为什么用 RK3566 跑强化学习机器人

Microduck 是一台 25 厘米级别的低成本四足机器人,整个项目在 GitHub 上完全开源,硬件物料成本控制得非常激进。我最初接触这个项目时,核心诉求很明确:不依赖昂贵的 Jetson 系列,也不堆服务器级别的算力,用几百块钱的 RK3566 开发板把强化学习策略跑起来。

先说结论:Microduck 的定位是“学走路”的科研平台,而不是“跑酷玩具”。它的强化学习训练在英伟达 GPU 上完成,训练出来的策略网络被导出成轻量级模型,部署到 RK3566 上做实时推理。整个链路的关键点在于:策略网络在训练时是一个几十 MB 的 PyTorch 模型,但部署到板子上时,必须压缩成几 MB 甚至几百 KB 的推理模型,同时保证控制频率在 100 Hz 以上,也就是每个控制周期不超过 10 毫秒。

RK3566 是一颗四核 Cortex-A55 的 SoC,主频最高 1.8 GHz;集成 0.8 TOPS 算力的 NPU,但实测下来,强化学习策略大多是几十层的小型 MLP,CPU 直接跑完全够用,NPU 在多数场景下反而是杀鸡用牛刀。这个判断直接影响了我后续的模型导出路线选择——不上 RKNN,直接用 ONNX Runtime 的 CPU 后端,这是我踩了不少坑后最终确认的方案,后面会详细展开。

这帖子适合谁看?如果你手上有一块 RK3566 开发板(泰山派、香橙派 5 等),想把强化学习策略部署到实机上做运动控制;或者你已经跑通过 Isaac Gym / MuJoCo 里的仿真训练,但搞不定“训练完怎么上真机”这个临门一脚,那么这篇手记会很有参考价值。整个项目从训练到部署,我在实机上踩了不少坑,包括 USB 设备识别、串口权限、实时性调度、模型格式转换等,下面按完整链路拆开讲。

2. 整体设计与训练阶段:从 GPU 到策略文件

2.1 强化学习框架与算法选型

Microduck 官方仓库默认用的是 legged_gym 这套 Isaac Gym 训练框架,算法是 PPO(近端策略优化)。但这里有个坑:legged_gym 依赖英伟达 Isaac Gym 的旧版本,而且是专为四足机器人设计的,环境接口很固定,如果你是第一次跑,最好严格按 README 的版本来,不要自己升级到最新版,否则 API 全变了,脚本直接跑不起来。

训练目标是让机器人学会站立、踏步、转向。奖励函数里最关键的几项是:

  • 躯干高度保持:维持在 0.2 米左右,偏离越远惩罚越大。
  • 关节力矩惩罚:抑制高频抖动,防止电机过热。
  • 前进速度跟踪:用遥控器或上层命令给定目标线速度,策略学习追踪。
  • 姿态稳定:横滚角、俯仰角保持在零附近,防止摔倒。

如果用 PPO 从头训练,在单张 RTX 4090 上大约需要 2 到 3 小时能收敛。但我更推荐直接用官方仓库提供的预训练权重,因为从头训练对 reward shaping 的调参经验要求比较高,新手很容易出现“仿真里走得挺好,一到实机就抽风”的情况。

2.2 训练环境搭建的版本坑

我用的训练环境是:

  • Ubuntu 20.04
  • Python 3.8
  • PyTorch 1.13(不要用 2.x,legged_gym 旧版本不兼容)
  • Isaac Gym Preview 4
  • CUDA 11.7

这几个版本之间是有依赖关系的:Isaac Gym Preview 4 官方只支持 PyTorch 1.13 和 Python 3.8 到 3.10,装的时候千万别自作主张升级。当时我为了省事用了 Python 3.10,结果一堆 C++ 扩展编译不过去,来回折腾了大半天。

训练完成后,模型保存为.pt文件,里面是一个包含 actor 和 critic 两个网络的 state_dict。部署到实机只需要 actor 网络,也就是策略网络,它的输入是机器人状态(躯干姿态、角速度、关节角度、关节角速度、上次动作等),输出是 12 个关节的目标角度增量。对 Microduck 来说,12 个关节 = 4 条腿 × 3 个自由度(横滚、俯仰、膝关节)。

2.3 策略网络结构分析

Microduck 的 actor 网络是一个三层 MLP:

输入层(约 50 维) -> 全连接(256) -> ReLU -> 全连接(256) -> ReLU -> 全连接(12)

输入维度为何是 50 多?展开来看:躯干姿态四元数(4)、角速度(3)、重力投影(3)、关节角度(12)、关节角速度(12)、上次动作(12)、以及命令速度(3,包括前进、横向、转向),再加一些历史状态,加起来 50 到 60 维。输出是 12 个关节角度增量。

这种结构意味着:推理计算量非常小,单次前向传播大约只有 2 到 3 万次浮点运算,在 RK3566 的 CPU 上跑一次不到 1 毫秒。所以整个控制周期的瓶颈根本不在神经网络推理,而在传感器读取、运动学解算和串口通信。明白了这一点,部署策略就清晰了:把算力留给传感器融合和控制循环,而不是纠结 NPU。

3. 模型转换:从 PyTorch 到 ONNX 再到实机推理

3.1 为什么不直接用 PyTorch 推理

RK3566 的 CPU 是 ARM 架构,PyTorch 虽然提供了 ARM Linux 的预编译包,但实测有两个问题:一是内存占用偏高,一个空模型就吃掉 200 多 MB;二是依赖库体积太大,整个 site-packages 打包出来接近 1 GB。对一块只有 2 到 4 GB 内存的开发板来说,太奢侈了。

所以我的做法是:把 actor 网络导出为 ONNX 格式,然后用 ONNX Runtime 的 CPU 后端在板子上做推理。ONNX 文件只有几百 KB 到 1 MB,ONNX Runtime 的 ARM 版库也就 20 多 MB,整体轻量得多。

3.2 导出 ONNX 的完整过程

先加载训练好的模型,然后只导出 actor:

import torch from legged_gym.envs import LeggedRobot from legged_gym.utils import get_args, task_registry # 加载训练配置和模型 args = get_args() env_cfg, train_cfg = task_registry.get_cfgs(args.task) env_cfg.env.num_envs = 1 env, _ = task_registry.make_env(name=args.task, args=args, env_cfg=env_cfg) from legged_gym.utils.helpers import get_load_path load_path = get_load_path(args, train_cfg, 0) model = torch.load(load_path, map_location='cpu') policy = model['model'].actor # 构造 dummy 输入 dummy_input = torch.randn(1, policy.input_dim) # 导出 ONNX torch.onnx.export( policy, dummy_input, "microduck_policy.onnx", input_names=["obs"], output_names=["action"], dynamic_axes={"obs": {0: "batch"}, "action": {0: "batch"}}, opset_version=11 ) print("导出完成")

这里有个容易被忽略的关键点:model['model']是完整的ActorCritic实例,actor才是策略网络。导出时要把模型切到 eval 模式,并关掉梯度:

policy.eval() with torch.no_grad(): torch.onnx.export(...)

否则导出的图里会包含训练相关的运算节点(比如 dropout、批量归一化的训练分支),推理时行为不一致,实机上表现会很怪。

另一个坑是opset_version。RK3566 的 ONNX Runtime 版本建议用 1.16 以上,对应的 opset 支持到 17 左右。我选 11 是为了保险,兼容性最好。如果你的 ONNX Runtime 版本太新,有些算子被废弃,反而可能加载失败。

3.3 在 RK3566 上安装 ONNX Runtime

这一步看似简单,其实有一个大坑。RK3566 是 ARMv8 架构(64 位),ONNX Runtime 官方提供了manylinux的 ARM 轮子,但默认是 x86 的,不能直接 pip install,必须下 ARM 版:

pip install onnxruntime --platform manylinux2014_aarch64 --only-binary=:all:

另一种方式是从源码编译,但不要这么干,交叉编译 ONNX Runtime 至少需要一两个小时,而且很容易遇到 Eigen、FlatBuffers 等依赖的版本冲突,直接用官方轮子最省心。

装完后立刻验证推理是否正常:

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

如果输出是一组数值而不是报错,说明基础链路已经通了。实际部署时,我还会用ort.SessionOptions()设置线程数为 4,并开启图优化,具体后面在控制循环部分讲。

3.4 关于 NPU 的取舍

RK3566 集成了 0.8 TOPS 的 NPU,很多人会本能地想“是不是该把模型扔到 NPU 上跑”。实测下来,对 Microduck 这个策略网络来说,CPU 单次推理约 0.3 到 0.5 毫秒,NPU 上反而要 1 到 2 毫秒——因为 NPU 需要额外做量化、格式转换和拷贝,小模型的收益几乎为零,还没算上 RKNN 工具链的麻烦。除非你的策略网络是视觉模型(比如端到端做图输入的),否则不建议上 NPU。

4. 实机部署:RK3566 上的控制循环与硬件接线

4.1 硬件清单与系统准备

我用的是一块泰山派 RK3566 开发板,4 GB 内存版本,系统是官方 Debian 11。硬件连接上,Microduck 的 12 个舵机(通常是串行总线舵机)接到板子的 UART 串口,IMU(惯性测量单元)接 I2C 或 SPI,我这里用的是 I2C。

系统层面有四个必须调的地方:

  • 串口设备权限:把当前用户加入dialout组,否则没有权限打开/dev/ttyS0/dev/ttyUSB0
  • 串口别名固定:RK3566 的串口设备可能在两次开机后名称变化,建议写 udev 规则固定。
  • CPU 性能模式:默认的 ondemand 调速器会导致推理延时抖动,建议切到 performance。
  • 减少日志写入:SD 卡的 I/O 延迟会干扰控制循环,最好把日志写到内存盘(/tmp)或只写关键事件。

4.2 UDP 下发指令与舵机协议

Microduck 的舵机一般是串行总线舵机(类似 LX-16A 或 ST3215),协议通常是:帧头 + ID + 指令 + 参数 + 校验。我需要把策略网络输出的 12 个关节角度增量解析成舵机目标角度,再打包成串口帧发出去。

核心控制循环的伪代码如下:

import onnxruntime as ort import serial import numpy as np import time # 初始化 ONNX Runtime sess_options = ort.SessionOptions() sess_options.intra_op_num_threads = 4 sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess = ort.InferenceSession("microduck_policy.onnx", sess_options, providers=["CPUExecutionProvider"]) # 串口初始化 uart = serial.Serial('/dev/ttyS0', 1000000, timeout=0.01) # IMU 初始化(伪代码) imu = init_imu() # 状态缓存 prev_action = np.zeros(12, dtype=np.float32) target_joint_pos = np.zeros(12, dtype=np.float32) cmd_vel = np.array([0.2, 0.0, 0.0], dtype=np.float32) # 目标前进 0.2 m/s # 控制频率 100 Hz control_period = 0.01 next_time = time.time() while True: # 1. 读取 IMU 姿态和角速度 quat, gyro = imu.read() # 从四元数计算重力投影 gravity_proj = quat_to_gravity(quat) # 2. 读取舵机当前实际角度 joint_pos = read_servo_positions(uart) joint_vel = compute_joint_vel(joint_pos) # 差分求速度 # 3. 构造观测向量 obs = np.concatenate([quat, gyro, gravity_proj, joint_pos, joint_vel, prev_action, cmd_vel]) # 4. 策略推理 action = sess.run(None, {"obs": obs.reshape(1, -1)})[0].flatten() # 5. 累积目标角度并下发给舵机 target_joint_pos += action * 0.25 # 缩放因子 send_servo_commands(uart, target_joint_pos) # 6. 保存上次动作,并等待下一个周期 prev_action = action.copy() next_time += control_period sleep_time = next_time - time.time() if sleep_time > 0: time.sleep(sleep_time) else: print("WARNING: 控制周期超时")

这段代码看起来简单,但实机上跑起来有很多细节。

4.3 实机调试的五个关键参数

动作缩放因子。这是我从仿真到实机遇到的第一个大坑。训练时策略输出的动作范围是在仿真环境里标定过的,映射到真机舵机时,直接累加会有两个问题:累加值漂移导致目标角度越界;动作增量过大导致舵机瞬间冲撞机械限位。我最后把缩放因子设成 0.25,同时加了目标角度clip到 [-1.5, 1.5] 弧度的限制。

IMU 数据质量。RK3566 的 I2C 读 IMU 频率如果太低,会导致姿态估计延迟。我实测把 I2C 时钟调到 400 kHz,MPU6050 的输出频率设到 200 Hz,控制循环每次去读最新值,这样延迟可以控制在 5 毫秒左右。注意不要用阻塞式读取,否则 IMU 慢一拍,整个控制循环都会被拖住。

舵机通信超时。串行舵机在 1 Mbps 波特率下,单条指令大约 0.1 毫秒,但 12 个舵机逐个发送如果串行做,就要 1.2 毫秒,加上舵机返回的反馈帧,一个周期内通信开销可能会到 3 到 5 毫秒。我在实机上把舵机反馈关闭了,改成只发指令不读角度,关节位置反馈通过运动学估算,省下的时间足够把控制频率稳在 100 Hz。这个取舍在初期调试阶段尤其重要。

控制频率检测。我在控制循环里加了一个超时打印,如果连续几个周期实际运行时间都超过 10 毫秒,就要优化代码而不是继续加功能。实测发现,Python 层面的列表拼接和 numpy 数组反复转换是最大的性能杀手,建议所有观测向量直接用 numpy 拼接,不要用列表 append。

上电时序。这是硬件上最容易忽略的。舵机供电和板子供电要分开,否则舵机启动瞬间的大电流会把 RK3566 拉复位。我用的是 5V/5A 的稳压模块单独给舵机供电,板子用独立的 5V/2A 供电,共地处理,这样 USB 调试口也不会被干扰。

5. 常见问题与排查技巧实录

5.1 系统识别到 RK3566 但显示为 ADB 设备

这个问题的典型现象是:泰山派通过 USB 连接到电脑,lsusb能看到设备,但设备出现在 ADB 设备列表里,而不是作为一个串口或网络设备。我排查时发现,这是因为板子默认开启了 ADB 调试模式,USB 功能被模拟成了 ADB 接口。

解决办法有两种:

  • 在板子的系统设置里关闭 ADB,切换为“USB 网络共享”或“USB 串口”模式。
  • 如果板子已经进不了系统,无法界面操作,可以通过adb shell登录进去,然后修改/etc/init.d/下的 USB 配置脚本,把g_adb的加载注释掉,重启即可。

另外要留意,如果 USB 设备枚举出来但无法通信,先确认线材是不是支持数据,而不是充电线。这个问题看着低级,但我真的在调试中浪费了半小时。

5.2 舵机抖动与策略震荡

实机上最常见的现象是机器人站在原地疯狂抖动,像帕金森一样。排查下来,原因通常有三个:

  • 动作频率太高:舵机的机械响应速度跟不上策略输出频率。此时要么降低控制频率到 80 Hz,要么加大动作缩放因子,降低单步动作幅度。
  • 关节速度估计噪声大:差分求关节速度时,传感器噪声会被放大,策略网络吸入了噪声,输出抖动。我加了低通滤波,filtered_vel = 0.8 * last_vel + 0.2 * current_vel,抖动明显下降。
  • 舵机死区:低成本舵机有死区问题,微小角度变化无法执行,形成振荡。我设置了最小角度增量阈值,小于 0.005 弧度的增量直接忽略。

5.3 推理延时波动大

如果控制循环偶尔出现一次 20 毫秒的峰值延迟,多半是内存分配或日志写入导致的。ONNX Runtime 在第一次推理时会做内存初始化,后续推理通常稳定,但如果代码里频繁创建 numpy 数组、做类型转换,就会触发内存碎片。

我最终的优化方案:

  • 所有临时数组在一开始就预分配好,循环内只做切片和赋值。
  • 关掉 print 日志,改用按条件的logger.info
  • CPU 调到 performance 模式:cpupower frequency-set -g performance
  • 给控制线程设置高优先级:os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(50))

5.4 实机走不稳,仿真走得很好

这几乎是所有强化学习机器人部署的终极难题。我用 Microduck 也遇到了。核心原因是仿真与实机的“域差异”(sim-to-real gap)。解决思路有两个方向:

  • 系统辨识:测量舵机实际响应延迟、死区、力矩限制,然后把这些参数放进仿真环境里重新训练。Microduck 的仓库里提供了域随机化(domain randomization)的开关,建议把关节摩擦、质量、控制延迟这几个参数加上 ±20% 的随机扰动,训练出来的策略鲁棒性明显提升。
  • 分阶段部署:不要一上来就全姿态控制。先在板子上跑固定的站立姿态,确认舵机、IMU、串口通信都正常;然后只做姿态修正(不上策略网络),用 PID 把躯干稳住;最后再切换到策略输出。这个过程虽然慢,但能帮你把问题逐层剥离,而不是出了故障一头雾水。

5.5 控制频率上不去

如果控制频率一直上不到 100 Hz,先定位瓶颈在哪里,不要盲目优化。我用的排查方法很简单:在每个环节打时间戳,算出 IMU 读取、策略推理、舵机通信各自耗时。

以我的实测数据为例:

环节耗时(毫秒)说明
IMU 读取1.5I2C 频率 400 kHz
关节角度读取2.0关闭反馈后降为 0.5
策略推理(ONNX Runtime)0.4CPU 四线程
舵机指令发送2.012 个舵机串行发送
其他开销1.0数组拷贝、控制逻辑
合计6.9留有余量,可跑 100 Hz

如果舵机通信占了大头,可以考虑用 DMA 方式发送,或者换更高性能的串行舵机。如果 IMU 读取占大头,可以换 SPI 接口的 IMU,速度比 I2C 快一个数量级。

6. 实机效果的进一步优化与扩展

6.1 把控制频率提到 200 Hz

100 Hz 是 Microduck 的典型配置,但如果你发现策略在实机上表现还是不够平滑,可以试试把控制频率拉到 200 Hz。前提是:舵机支持更高的指令频率,IMU 输出频率也要同步提高,否则观测数据跟不上控制频率,反而会引入噪声。

我实测把频率从 100 Hz 提到 200 Hz 后,动作平滑性有明显改善,但舵机温度上升也更快。建议根据舵机负载情况和机器人运动模式来权衡,不要盲目拉高。

6.2 加入遥控器指令

Microduck 支持遥控器输入,把用户的操作指令转化为目标速度。常用的有 PS2 手柄或简单的 2.4G 遥控器。实机上我使用了一款串口透传的 2.4G 接收机,把摇杆输出映射为cmd_vel的三个分量:前进速度、横向速度、偏航角速度。这样调试时可以远程控制机器人的朝向和移动,方便多角度观察步态。

6.3 云端记录与离线分析

实机调试时,我习惯把每次运行的传感器数据、策略输出和延时记录成 CSV 文件,然后上传到电脑端做离线分析。数据记录不要写到 SD 卡,而是写到/tmp内存盘,等跑完再 copy 出来。这样既不影响控制循环稳定性,又能完整回放问题场景。

有一回机器人突然侧翻,我通过回放数据发现是 IMU 的横滚角出现了近 30 度的跳变,原因是一个 I2C 通信错误导致了数据错位。若不是有日志回放,这类偶发问题排查起来会非常耗时。

6.4 从强化学习到传统控制的混用思路

如果你的目标不只是“走起来”,还想做更复杂的任务(比如越障、上斜坡),纯粹依靠一套端到端策略会很吃力。我个人的经验是:用强化学习策略负责底层足端轨迹和姿态稳定,用上层传统控制(比如 MPC 或 PID)负责路径规划和导航。这样可以把强化学习的“涌现能力”和传统控制的“稳定性保证”结合起来,这也是近两年很多机器人团队采用的混合架构。

Microduck 的硬件算力有限,跑上层规划算法时会吃力,建议上层控制放在上位机(树莓派或 x86 工控机),RK3566 只做底层策略推理和舵机驱动,通过串口或以太网与上位机通信。

6.5 训练更鲁棒的策略

最后再聊回训练侧。如果你的 Microduck 实机表现始终不佳,我强烈建议你花时间在域随机化上,而不是反复调实机代码。在 legged_gym 的配置里,找到domain_rand的开关,把以下几个参数加上随机范围:

# 在 cfg 里启用: cfg.domain_rand.randomize_friction = True cfg.domain_rand.friction_range = [0.5, 1.25] cfg.domain_rand.randomize_base_mass = True cfg.domain_rand.added_mass_range = [0.0, 0.2] # kg cfg.domain_rand.randomize_motor_strength = True cfg.domain_rand.motor_strength_range = [0.9, 1.1] cfg.domain_rand.randomize_joint_friction = True cfg.domain_rand.joint_friction_range = [0.0, 0.05]

加上这些随机化参数后重新训练,策略对实机差异的容忍度会明显提高。我记得第一次加上摩擦随机化重训后,Microduck 在实机上从只能走两步变成能连续走十几步,效果立竿见影。

7. 写在最后的一点体会

整个 Microduck 部署流程走下来,我最大的感受是:强化学习机器人的难点不是训练,而是“让训练好的策略走出仿真”。仿真环境里奖励函数给你打分,实机上没人给你打分,只有硬邦邦的地面和抖动的舵机。每一步转化(PyTorch 到 ONNX、ONNX 到实机推理、实机到闭环控制)都是在缩小仿真与现实之间的缝隙。

如果你也在折腾类似的事儿,我只有一个建议:先把数据的流向捋清楚。从 IMU 读到策略输出,再到舵机响应,整个过程里每一步的数据格式、时间戳、延迟都要了如指掌。能在仿真里做好的事,绝不在实机上折腾;能在离线阶段验证的,绝不上电验证。这样踩坑的速度会慢一些,但每一步都会很扎实。

另外,Microduck 这个项目本身还在持续迭代,社区里的 issue 和 PR 都挺活跃,遇到问题先搜一遍 issue,大概率有人踩过同款坑。实在搞不定,把自己排查的过程写清楚再提问,效率会高很多。

希望这篇手记能帮你少走一些弯路,早日看到自己的 Microduck 稳稳当当地走出第一步。

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

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

立即咨询