资讯动态

强化学习四足机器人部署实战:从英伟达GPU到RK3566实机

发布时间:2026/9/6 10:00:50 来源:尧图企业网站定制
把强化学习机器人部署到实机听起来就是训练完导出权重再跑起来真做起来才发现从英伟达 GPU 的仿真环境到 RK3566 主控的实机中间隔着工具链、实时性、Sim-to-Real 一大堆问题。Microduck 是一台 25 厘米的四足机器人运动控制完全由强化学习策略驱动没有手写步态也没有显式姿态 PID。这篇手记记录了我把训练好的策略从仿真搬到 RK3566 实机的完整过程适合正在折腾 Microduck 或者类似低成本 RL 四足平台的开发者参考。全文偏部署实践训练部分只讲能让模型落地的关键细节GPU 上怎么调 reward 这类可以放到以后单独写。1. 为什么是“英伟达 GPU 训练 RK3566 实机”这条路线1.1 微型四足机器人选型时的核心约束Microduck 这类 25 厘米级四足机器人和实验室里那些动辄几万块的科研平台有个本质区别它要在低成本、低功耗、小体积的前提下跑完完整的强化学习部署链路。机身只有这么点空间电池容量有限电机功率有限主控板也不能像工控机那样随便堆散热。强化学习策略在训练阶段确实吃算力。我在英伟达 GPU 上用 MuJoCo 仿真并行采集数据单张 RTX 3090 可以同时跑 2000 个机器人环境一个百万步的离线强化学习任务几个小时就能出结果。这种算力需求嵌入式平台在物理上就不可能满足也没必要满足。训练和推理本来就是两套不同规模的问题。但实机推理就完全是另一回事了。Microduck 的运动控制策略是一个多层感知机参数不到 1MB单次前向传播在四核 A55 处理器上跑只需要 1 到 2 毫秒。这个算力门槛低到随便一块开发板都能胜任。真正的难点反而不在算力而在 I/O 接口是否齐全、能否稳定支撑实时控制循环、系统是不是好调试。1.2 RK3566 对比树莓派 4B为什么最后选了 RK3566我最早考虑过树莓派 4B毕竟社区资料最多出问题随便一搜就有答案。但仔细对比下来RK3566 在几个关键维度上更匹配这种低成本机器人项目。项目RK3566树莓派 4BCPU四核 Cortex-A551.8GHz四核 Cortex-A721.5GHzNPU0.8 TOPS支持 RKNN无内存2GB/4GB LPDDR42GB/4GB/8GB LPDDR4典型价格约 200 元级别约 400 元级别板载 UART/SPI/I2C丰富容易引出也够用但树莓派生态偏桌面5V 供电稳定性耐造机器人供电波动影响小对供电要求更敏感电压跌落容易重启实时性改造空间Linux 普通内核即可PREEMPT_RT 也支持硬件中断和电源管理在强实时场景下略繁琐选择 RK3566 真正的理由有三条。第一是价格200 元级别的板子损坏成本低在机器人上试错压力小第二是接口机器人要接 IMU、串口通信模块、看门狗和电源管理RK3566 的引脚引出方便很多第三是 NPU 的存在给后续扩展留了余地虽然 Microduck 的 MLP 策略在 CPU 上就够跑但以后如果要加视觉传感器RK3566 的 0.8 TOPS NPU 至少能提供一个本地推理的选项。1.3 上下位机分工架构在实机上我没有让 RK3566 直接驱动电机。电机控制是一个强实时任务需要 1kHz 甚至更高频率的电流环/位置环Linux 非实时内核很难稳定保证这种时序。所以整体架构采用上位机加下位机的分工方式RK3566 作为上位机运行 Linux负责加载强化学习策略、读取 IMU 和关节状态、构建观测向量、执行网络推理、输出目标关节角度。STM32 作为下位机运行裸机或 RTOS负责电机位置环、编码器读取、电流采样、通信协议解析。控制频率可以做到 1kHz。上下位机之间用串口 UART 连接波特率 921600STM32 以 500Hz 上报状态RK3566 以 100Hz 下发关节目标角度。这种架构的好处是把实时性要求高的部分放到单片机把算法部署方便的部分放到 Linux 板两边各干各擅长的事。整个链路走通之后后续换更复杂的策略、加传感器都只需要改 RK3566 这边的代码下位机基本不用动。2. 训练阶段Microduck 的强化学习策略是怎么在仿真里“长”出来的2.1 任务定义与观测动作空间部署之前必须先搞清楚一件事策略到底在解决什么问题输入输出是什么频率是多少。这个决策直接影响后面的模型转换和实机控制循环设计。我把 Microduck 的运动控制任务定义为给定前进速度 vx、横向速度 vy、偏航角速度 wyaw 三个指令策略输出 12 个关节的目标位置。这里假设 Microduck 是每条腿 3 个自由度、共 12 个关节的常见四足构型。如果你的 Microduck 是每条腿 2 个自由度只需要把维度改成 8整体思路完全一致。观测向量是 45 维具体为线速度和偏航角速度指令 3 维vx、vy、wyaw基座角速度 3 维从 IMU 陀螺仪读取基座坐标系下的重力向量 3 维从 IMU 加速度计估计并归一化12 个关节角度12 个关节角速度上一时刻的 12 个动作动作空间是 12 个关节位置目标范围限制在正负 0.8 rad。这里最关键的是把“上一个动作”放进观测向量。由于策略网络本身是无记忆的 MLP如果不给这一项策略就不知道自己的历史输出是什么实机上很容易出现高频抖动。仿真里加了这一项之后控制输出平滑程度会明显改善。控制频率在训练和仿真里统一设定为 200Hz每步 5ms。这个频率对位置控制的四足机器人来说够用而且实机上也能通过 STM32 串口稳定支撑。2.2 为什么选择 IQL 离线强化学习而不是纯 PPO训练算法我在 IQL 和 PPO 之间来回试过好几轮最终主力用了 IQL 离线强化学习。PPO 是强化学习机器人运动控制的经典选择但它有一个很现实的问题在线 rollout 的方差很大。每次训练都要不断和环境交互采集新数据训练曲线波动明显一张 GPU 要同时处理采样和梯度更新出问题之后也不容易定位是采样问题还是奖励设计问题。IQL 的思路是先离线收集一批数据再用这些数据学习 Q 函数最后从 Q 函数里提取策略。由于数据是固定的训练过程非常稳定可以反复调试奖励系数和网络结构而不需要重新采样。这对实机部署前的迭代非常有利。数据采集是离线强化学习能否成功的关键。我用仿真里已有的一个传统步态控制器作为数据生成策略在 MuJoCo 里随机跑不同的速度和转向指令采集了约 50 万条转移样本。数据里还刻意加了一些极端动作和扰动让离线数据集覆盖足够广的状态空间。整个采集过程大概用了半天时间之后这一份数据可以反复用于训练实验。IQL 训练的核心超参数我放在了下面超参数数值expectile0.7batch size256学习率3e-4discount0.99训练步数100 万网络结构MLP 256×3激活函数ReLU动作范围正负 0.8 rad在 RTX 3090 上100 万步训练大约 4 小时。训练完成后我会在 MuJoCo 里用固定随机种子做 50 次 rollout 评估统计行走距离、姿态稳定时间和抗推成功率只挑选评估结果最好的 checkpoint 进入部署流程。2.3 奖励函数设计与 Domain RandomizationMicroduck 能在实机上走路训练时的奖励函数和域随机化缺一不可。奖励函数我用了加权组合每项都有明确物理含义奖励项表达式作用速度跟踪exp(-姿态保持重力向量投影尽量接近 [0,0,1]保持机身水平期望高度exp(-(h - h_ref)²)维持指定躯干高度力矩惩罚-w_tau * sum(tau²)降低电机负担和发热动作平滑-w_rate * sum((a_t - a_{t-1})²)抑制抖动权重参数我边训练边调最终确定速度跟踪权重最高力矩惩罚和动作平滑次之。我的经验是如果机器人站不稳优先检查姿态保持项如果站得稳但走不动优先检查速度跟踪项如果走起来抖动厉害重点调动作平滑权重和动作范围。Domain Randomization 是 Sim-to-Real 转移的护城河。我在仿真里对摩擦系数、电机延迟、关节零位偏置、负载质量、传感器噪声都做了随机化。比如摩擦系数范围设为 0.2 到 1.2电机动作延迟随机 0 到 2 个控制步关节零位加入正负 0.05 rad 的随机偏置。这些看似微小的随机化是实机能够站稳的关键前提。没有域随机化的策略在仿真里再漂亮上了实机基本都会躺平或者抽搐。3. 模型从 PyTorch 到 RK3566 的转换ONNX、RKNN 和一场几乎翻车的量化3.1 先导 ONNX再验证数值一致性训练完的策略网络是 PyTorch 格式要上 RK3566 实机第一件事是转换成通用中间格式。我首选 ONNX因为它在 CPU 推理和 RKNN 转换两条路上都走得通。导出脚本的核心部分长这样import torch import numpy as np model torch.jit.load(policy.pt) model.eval() dummy_input torch.randn(1, 45) torch.onnx.export( model, dummy_input, policy.onnx, input_names[obs], output_names[action], dynamic_axes{obs: {0: batch}, action: {0: batch}}, opset_version12, )导出之后我没有直接拿去部署而是在 PC 上先用 ONNX Runtime 和 PyTorch 的原始输出做数值一致性验证。这里很容易踩坑——ONNX 导出看起来成功但某些算子实现上会有细微差异模型权重是好的但推理结果悄悄变了。import onnxruntime as ort import torch sess ort.InferenceSession(policy.onnx, providers[CPUExecutionProvider]) x np.random.randn(1, 45).astype(np.float32) y_onnx sess.run([action], {obs: x})[0] with torch.no_grad(): y_torch model(torch.from_numpy(x)).numpy() print(max abs diff:, np.max(np.abs(y_onnx - y_torch)))这个值我要求小于 1e-5。如果超过这个量级说明 ONNX 模型已经和原始模型有了可感知的偏差实机上很可能会表现为动作抖动。我实际测试中遇到过算子融合导致的 1e-3 级别差异最后是通过固定 opset 版本解决的。3.2 试过 RKNN 量化最后选择 CPU 上的 ONNX Runtime下一步按理说应该转 RKNN用 RK3566 的 NPU 跑推理。我也确实试过而且试了很久。RKNN 转换的基本流程是from rknn.api import RKNN rknn RKNN() rknn.config(target_platformrk3566) rknn.load_onnx(modelpolicy.onnx) rknn.build(do_quantizationFalse) rknn.export_rknn(policy_fp16.rknn)但问题出在两个地方。第一RK3566 的 NPU 对 INT8 量化支持最好我的 RL 策略输入输出都是连续浮点数量化之后关节位置输出出现了约 0.02 rad 的抖动。这个幅度看着不大但在实机上足以让 Microduck 站都站不稳所有关节都在微颤。第二策略网络是一个标准的 MLP对 NPU 来说属于小尺寸网络单次推理虽然只要零点几毫秒但驱动调用、缓存一致性和初始化开销反而比计算本身更不稳定。我一度尝试混合量化保留部分对精度敏感的层为浮点但 RKNN 工具链对这种小网络的量化校准支持并不算友好反复调了几版效果都不理想。后来我做了一个性能对比发现这个决策可以更快做出来。方案平均推理耗时稳定性实机表现RKNN INT8约 0.8ms偶发抖动站立时关节微颤行走时转弯漂移RKNN FP16约 1.5ms相对稳定可用但工具链适配麻烦ONNX Runtime CPU FP32约 1.8ms非常稳定和仿真表现基本一致对于 25 厘米级别的四足机器人100Hz 控制周期意味着每周期只有 10ms 预算CPU 上 1.8ms 的推理耗时完全够用而且省掉了 NPU 推理带来的数值精度问题。所以我最终选择了一个比较务实的方案模型直接用 ONNX Runtime 的 C API 在 RK3566 的 CPU 上跑 FP32 推理。NPU 留给以后加视觉模型时再用。3.3 一个小坑RKNN 更偏向图像输入MLP 的 shape 需要适配这里提一个经验如果你还是想用 RKNN 跑这种策略网络一定要提前处理输入 shape。RKNN 工具链最常用的是 4 维图像输入格式比如 1×3×224×224对纯 MLP 的 2 维输入支持比较别扭。我最早直接把 (1, 45) 的输入丢进去转换时不报错但在板端推理时维度对不上。解决办法是把输入 reshape 成 1×1×1×45 之类的 4 维张量同时在导出 ONNX 之前就固定好输入形状。但这会引入额外的一次内存拷贝虽然延迟不大但代码维护起来比较烦。所以我还是更推荐小策略网络直接走 CPU 推理省心且稳定。4. RK3566 实机部署控制循环、上下位机通信与实时性优化4.1 实机系统架构与传感器接入RK3566 运行的是 Debian 系统启动后自动加载策略模型和控制程序。整个实机部署按模块拆成三层感知层IMU 通过 SPI 连接 RK3566读取 500Hz 的陀螺仪和加速度计数据关节状态由 STM32 编码器采集后通过串口上报。决策层RK3566 上的 C 控制程序从状态缓冲区构建 45 维观测调用 ONNX Runtime 推理得到 12 维动作。执行层RK3566 把动作通过串口发送给 STM32STM32 解析后执行电机位置环控制。IMU 我建议单独接在 RK3566 上而不是经过 STM32 转发。这样做的原因是强化学习策略对观测延迟非常敏感如果 IMU 数据先到 STM32 再串口转发到 RK3566会多出至少一个传输周期的延迟实机站立时会明显给人一种“反应迟钝”的感觉。直接 SPI 读取可以将整个 IMU 数据链路延迟控制在 1ms 以内。4.2 100Hz 控制循环的 C 骨架实机控制频率我没有直接沿用仿真的 200Hz而是先降到 100Hz。原因有两个一是 ONNX Runtime CPU 推理在 RK3566 上约 1.8ms加上传感器读取和串口通信200Hz 时每周期只有 5ms 预算余量太小二是 STM32 的电机位置环本身是 1kHz策略输出的是位置目标而非力矩100Hz 输出位置对电机执行来说完全够用。控制主循环的核心结构如下#include chrono #include thread auto next std::chrono::steady_clock::now(); while (running) { // 1. 读取 IMU 和关节状态 read_imu(imu_data); read_joint_state_from_stm32(joint_state); // 2. 构建观测向量 float obs[45]; build_obs(obs, imu_data, joint_state, cmd, prev_action); // 3. ONNX Runtime 推理 float action[12]; run_policy(obs, action); // 4. 动作滤波与限幅 smooth_action(action, prev_action, cmd); // 5. 发送给 STM32 send_cmd_to_stm32(cmd); prev_action cmd; next std::chrono::milliseconds(10); std::this_thread::sleep_until(next); }关键的时序问题是传感器数据是上一时刻的推理输出要下一时刻才生效所以观测里必须携带上一时刻动作。在代码里我把 prev_action 和当前传感器数据一起构建观测这样网络内部能够把“上一时刻我做了什么动作”和“当前我看到什么状态”在时间上对齐。4.3 实时性优化不是硬实时但不能有大抖动RK3566 跑的是通用 Linux 内核要做到严格的实时控制不太现实但四足机器人运动控制对周期抖动有一个容忍上限。我的经验是100Hz 控制循环单周期最大抖动不能超过 3ms否则策略感知到的状态时间间隔不稳定动作会变得不连贯。实测经验下来以下三个优化手段最有效。第一设置线程调度策略为 SCHED_FIFO优先级 80。这样控制线程可以抢占大部分普通进程避免日志写入或网络服务造成干扰。struct sched_param param { .sched_priority 80 }; pthread_setschedparam(thread, SCHED_FIFO, param);第二设置 CPU 亲和性。把控制线程绑定到独立的 CPU 核心其他系统任务分散到其他核避免线程在核间迁移带来的 cache 失效和调度延迟。cpu_set_t set; CPU_ZERO(set); CPU_SET(3, set); pthread_setaffinity_np(thread, sizeof(set), set);第三在控制循环里统计实际周期时间。我调试时会把每轮实际耗时写到一个共享缓冲区另一个普通线程每秒钟打印一次 P95 和最大周期而不是在控制线程里直接打印日志。这样既能观察实时性又不影响控制时序。实测在 RK3566 上经过这三步优化100Hz 控制循环的 P95 周期抖动稳定在 1.2ms 左右最大周期不超过 2.5ms。这个水平对 Microduck 的运动控制完全够用站姿稳定行走节奏也没有异常。5. Sim-to-Real 实测踩坑从“疯狂抽搐”到稳定行走5.1 关节零位标定引起的“假瘫痪”第一次给 Microduck 上电策略给出的动作明明是站立指令机器人却直接趴在地上四条腿的姿态和仿真里完全对不上。排查了很久才发现是关节零位标定问题。在仿真里关节角度 0 对应标准站姿。但实机的电机编码器零点在装配时是随机的位置没有校准过的 RK3566 根本不知道仿真里的 0 对应实机的哪个角度。策略在仿真里学到的所有状态映射关系到了实机上都因为零位偏移而错位。解决办法是做一个简单的零位标定流程把 Microduck 放在一个水平平面上手动调整 12 个关节到仿真里的标准站姿角度记录每个关节编码器的原始读数作为零位偏移量存储下来。控制程序读取关节状态时先减去这个偏移量再进行归一化。标定之后机器人第一次上电就能站起来虽然还有些晃但状态方向完全正确。5.2 IMU 方向约定不一致导致“仰头暴走”第二个大坑出现在 IMU 数据处理上。仿真里的重力向量是在基座坐标系下表示的也就是说如果机器人水平站立重力向量在基座系下应该是 [0, 0, 1]。但我的 IMU 驱动最初输出的方向约定完全不同重力向量的符号和坐标轴方向都没对齐。这个问题的表现非常诡异机器人站立时四条腿会不断调整姿势整体呈现一种“仰着头想往前冲”的状态偶尔还会突然加速像在追一个看不见的目标。调试过程其实不复杂我在 RK3566 上读取 IMU 原始数据并打印和仿真里的观测值做对比。把机器人手动摆成已知姿态验证 IMU 数据经过旋转矩阵后是否与仿真约定一致。最后在驱动代码里加了一个坐标轴交换和符号翻转重力向量对齐之后整个策略瞬间就“安静”了。这个教训是Sim-to-Real 最容易忽略的不是算法差异而是坐标系的工程约定。5.3 动作滤波与输出死区细节决定站立质感即使零位和 IMU 都处理好了Microduck 在站立时还是能感觉到轻微的抖动手摸上去能感受到高频微震。原因在于策略输出本身带有小的噪声这些噪声经过电机位置环放大虽然幅度不大但持续存在会加速电机发热也会让站立姿态看起来不自然。我在控制循环里加了一阶低通滤波cmd_filtered alpha * action (1.0 - alpha) * prev_cmd;alpha 取 0.6配合 100Hz 控制频率相当于大约 10Hz 截止频率。滤波之后的动作变化明显平滑。同时我又加了一个死区判断如果当前动作和上一时刻动作的差值小于 0.005 rad直接沿用上一时刻动作不更新指令。这两个小改动叠加之后Microduck 的站立状态从“微颤”变成了“安静”行走姿态也更自然。5.4 意外摔倒与自动恢复策略实机测试中不可能总是一次成功机器人走着走着可能被地面异物绊倒或者转弯速度过快侧翻。如果没有一套可靠的急停和恢复机制一块几百块的板子很可能在一次摔倒中损坏。我在控制循环顶部加入状态监测如果 IMU 检测到机身姿态超过安全角度比如横滚角大于 60 度或者串口通信连续超时 100ms立刻停止发送动作指令并向 STM32 发送急停命令。同时把 12 个关节目标位置切换到一条预设的“躺平”轨迹让机器人尽量以低姿态落地减少冲击。这个机制看似简单但非常救命。有一次测试中Microduck 在加速行走时被地毯边缘绊住如果没有急停电机会在堵转状态下持续输出大电流轻则烧驱动重则打齿。加了急停之后最多就是翻个身重新扶起来就能继续跑。5.5 供电跌落问题RK3566 开发板对供电波动比想象中敏感。Microduck 的电机瞬态电流很大站立和急加速时容易拉低电池电压导致 RK3566 重启。最初出现这个问题时我一度以为是策略出错后来查看系统日志才发现是反复重启。解决方法是把 RK3566 的供电和电机供电完全分开用一路独立的 5V 稳压模块给 RK3566 供电电池直接给电机驱动供电。另外在电池端并联一个较大容量的电容吸收瞬态压降。经过这些处理之后再没出现过重启问题。这一条对任何带嵌入式主控的小型机器人项目都适用。6. 部署完成后的性能数据与后续扩展6.1 RK3566 实机实测数据整个链路稳定之后我记录了一些参考数据给后来者一个量级概念。环境是室内平整地面Microduck 重量约 1.2kg2S 锂电池容量 2200mAh。项目实测数据单个策略推理耗时约 1.8msCPU FP32控制循环频率100HzP95 周期抖动约 1.2ms待机功耗约 4.5W站立功耗约 12W行走功耗约 18W单电池续航约 20 到 25 分钟RK3566 核心温度正常工作约 55 到 60 摄氏度这个功耗水平对于 25 厘米级机器人来说算比较合理的。如果后续想提升续航优先优化策略的动作平滑度减少电机的无效做功比单纯加大电池更有意义。6.2 下一步可以做的方向跑通这条部署链路之后我自己的路线图是往两个方向扩展。第一个方向是在 Microduck 上加视觉传感器。25 厘米级机器人虽然小但装一个 120 度广角摄像头绰绰有余。RK3566 的 NPU 正好可以用来跑视觉障碍物检测模型把视觉特征和运动控制策略拼接成一个更大的端到端网络。这也是 RK3566 相对纯 CPU 板卡的最大优势。第二个方向是尝试在仿真里加入更多地形干扰。目前部署的是平地行走策略域随机化虽然让机器人具备了抗小扰动能力但面对门槛、斜坡、碎石地面等复杂地形仍然不够。可以继续用 IQL 离线强化学习在仿真数据里加入更多地形样本训练出适应性更强的策略然后复用现有的部署链路直接迭代。6.3 部署完整个项目之后最想说的如果只总结一条经验我会说强化学习机器人项目的难点从来不止是算法和训练而是仿真到实机之间那一段“看不见的工程距离”。模型转换要验证数值一致性控制循环要计算延迟预算IMU 要统一坐标系关节要标定零位供电要隔离摔倒要有保护。每一步单独拿出来都不难但串在一起就构成了 RL 机器人从 GPU 到实机的完整壁垒。Microduck 现在能稳定走完会议室走廊转弯、停止、抗推都还算自然。回头再看整个过程最有价值的不是最终模型跑得有多好而是我终于知道了一套低成本四足平台从训练到部署的完整链路该怎么搭。希望这篇手记能帮准备入坑的朋友少熬几个夜。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价