RK3566部署强化学习四足机器人:从PyTorch到实机控制 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但实测下来强化学习策略大多是几十层的小型 MLPCPU 直接跑完全够用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.04Python 3.8PyTorch 1.13不要用 2.xlegged_gym 旧版本不兼容Isaac Gym Preview 4CUDA 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 MBONNX Runtime 的 ARM 版库也就 20 多 MB整体轻量得多。3.2 导出 ONNX 的完整过程先加载训练好的模型然后只导出 actorimport 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(nameargs.task, argsargs, env_cfgenv_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_locationcpu) 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_version11 ) 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, timeout0.01) # IMU 初始化伪代码 imu init_imu() # 状态缓存 prev_action np.zeros(12, dtypenp.float32) target_joint_pos np.zeros(12, dtypenp.float32) cmd_vel np.array([0.2, 0.0, 0.0], dtypenp.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 kHzMPU6050 的输出频率设到 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 Runtime0.4CPU 四线程舵机指令发送2.012 个舵机串行发送其他开销1.0数组拷贝、控制逻辑合计6.9留有余量可跑 100 Hz如果舵机通信占了大头可以考虑用 DMA 方式发送或者换更高性能的串行舵机。如果 IMU 读取占大头可以换 SPI 接口的 IMU速度比 I2C 快一个数量级。6. 实机效果的进一步优化与扩展6.1 把控制频率提到 200 Hz100 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 稳稳当当地走出第一步。

相关新闻

最新新闻

台风地区实测!彩石金属瓦抗风能力究竟如何?答案惊人!

台风地区实测!彩石金属瓦抗风能力究竟如何?答案惊人!

最近我一直在琢磨,台风地区的屋顶瓦片可太重要了,要是抗风能力不行,那房子可就遭老罪咯!你像那些老旧房子,用的传统瓦片,一到台风天,那瓦片被吹得“噼里啪啦”往下掉,多危险呐&#…

2026/9/6 12:41:42
AI工程化落地:Agent稳定性与内容质量的新挑战

AI工程化落地:Agent稳定性与内容质量的新挑战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/6 12:41:42
智慧燃气安全建设平台是什么?5 大核心功能与应用价值详解

智慧燃气安全建设平台是什么?5 大核心功能与应用价值详解

2021年底,新余市建成智慧燃气项目并投入使用,全市450公里中低压管网自此有了实时感知能力。从政府文件到地方实践,智慧燃气安全建设平台正从概念走向规模化落地。在湖北十堰“6.13”事故之后,燃气安全隐患治理被提升到前所未有的高…

2026/9/6 12:41:42
解读VIPer22a开关电源电路图:从原理到实战调试

解读VIPer22a开关电源电路图:从原理到实战调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/6 12:41:42
开源770B MoE模型实战:从架构解析到本地部署

开源770B MoE模型实战:从架构解析到本地部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/6 12:41:42
GitHub热榜爆款qzonearchive:把QQ空间数据备份到本地,附下载加速指南

GitHub热榜爆款qzonearchive:把QQ空间数据备份到本地,附下载加速指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/6 12:36:41