1. 从训练机到小机器人的最后一公里:为什么一篇部署手记比训练教程更稀缺
先聊点背景。Microduck这个名字在强化学习机器人圈里已经不算陌生,一套25厘米级别的四足机器人硬件方案,配合英伟达GPU上的仿真训练管线,基本是当前从sim-to-real(仿真到现实)路线里性价比最高、最容易复现的入门到进阶项目之一。我在拿到这套方案之后,在GPU上把训练流程跑通、看到reward曲线正常收敛、策略在仿真环境里走得像模像样,一度觉得工作已经完成了大半。但真正把训练好的策略从GPU训练机搬到RK3566实机主控上跑起来,才发现在仿真里一路畅通不代表实机部署就能顺风顺水。
这个项目的麻烦在于,训练端和部署端是完全两套生态。训练端你面对的是英伟达CUDA生态、PyTorch或者Isaac Gym之类的仿真环境,而部署端RK3566是一颗瑞芯微的SoC,CPU架构是四核A55,带NPU但和CUDA完全是两个世界。再加上机器人本体上有IMU、电机驱动板、通信总线这些外设需要实机适配,整个链路从“模型能导出”到“机器人能站起来走路”,中间的坑密集程度远超想象。如果你已经开始用Microduck训练强化学习策略,或者计划把仿真里训练好的策略部署到RK3566这类边缘计算平台上,这篇手记应该能帮你避开不少我在实机上踩过的雷。
先说结论性的一句话:部署的成功率,取决于你在仿真阶段有没有提前为部署留好后路,而不是取决于部署当天你有多努力。这个道理我在反复重刷固件、反复调总线时序之后才彻底想明白。
2. 部署前的硬件/软件认知校核:RK3566主控给Microduck带来的真实约束
2.1 RK3566主控的定位:能跑,但别把它当训练卡用
RK3566在嵌入式平台里算是一个中间档位的芯片。四核Cortex-A55,频率最高到1.8GHz左右,集成0.8 TOPS算力的NPU,支持4K视频解码,内存接口支持到LPDDR4/LPDDR4X。对于Microduck这种体量的强化学习部署任务,CPU算力刚好够用,NPU在理论上可以加速神经网络推理,但实际用下来,NPU只适合跑定点量化模型且对算子支持有限,强化学习策略网络这种结构在NPU上的收益并没有想象中大。
我一开始的思路很直接:既然RK3566带NPU,那就把训练好的策略网络转换到RKNN格式,走NPU推理。这条路理论上最优,实际却是最折腾的。原因在于策略网络里常见的LayerNorm、GELU激活、以及某些自定义的residual结构,在RKNN-Toolkit2的算子支持列表里要么是部分支持,要么需要手工拆解成多个子图。一个在GPU上毫秒级完成的推理,在NPU上来回切子图、搬运数据之后,延迟反而比纯CPU跑ONNX更高。
所以如果你也准备在RK3566上部署Microduck,我的建议是:第一步先评估纯CPU跑ONNX的延迟是否满足控制频率要求。Microduck的控制频率通常在100Hz到200Hz之间,也就是每个控制周期5到10毫秒。RK3566的A55核心上跑一个精简的MLP策略网络(输入几十维,隐层两到三层,输出十几个关节指令),单次推理时间大概在1到3毫秒,完全够用。用CPU跑,省去RKNN量化和算子适配的功夫,部署链路最短,出问题也好排查。
2.2 硬件外设的选型与接线,直接决定部署难度
Microduck这类25厘米级四足机器人,电机一般用的是串行总线舵机或者带FOC驱动的小型无刷电机套件。在RK3566平台上,和电机驱动板的通信方式常见有两种:
| 通信方式 | 典型接口 | 延迟表现 | 部署难度 |
|---|---|---|---|
| UART串行总线 | 板载UART / USB转UART | 较低,可控 | 容易,注意电平匹配 |
| CAN总线 | 板载CAN控制器 + 收发器 | 较低,稳定性好 | 中等,需要配置CAN驱动 |
| USB直连驱动板 | USB Host | 高,不够可控 | 容易但实时性差 |
我实测下来,推荐优先选择UART或者CAN。USB直连在调试阶段方便,但到实际连续运行时,偶尔会出现设备掉线重枚举的问题,对于一个正在走路、需要实时平衡的机器人来说,这是不能接受的隐患。另外,如果电机数量较多(Microduck标准配置是12个自由度),UART波特率要尽量拉高(至少1Mbps以上),并且注意串口缓冲区是否够用,防止高频率发送关节指令时出现丢帧。
IMU的选型也不可忽视。Microduck需要实时的姿态反馈,IMU输出的频率和延迟直接影响到部署时滤波算法的实现难度。我用的是常见的BMI088,SPI接口连接,IMU数据频率设置在1kHz左右。SPI相比I2C优势很明显:稳定、延迟低、CPU占用低,唯一的坑在于RK3566的SPI引脚复用和设备树配置,这个后面专门展开。
2.3 软件环境的准备工作:精简文件系统,不要平台原生态带一堆用不上的东西
系统层面,RK3566可以跑Debian、Ubuntu或者Buildroot。Microduck的部署任务不重,但我强烈建议不要直接拿开发板厂商提供的全功能桌面镜像来跑,因为后台服务太多,CPU会被频繁打断,对实时控制任务影响很大。我自己用的是基于Debian的server版镜像裁剪,把桌面、网络管理服务能删都删,启动后内存占用控制在200MB以内,CPU空闲率保持在95%以上。
编译工具链方面,部署端需要交叉编译或者直接在板子上编译。Microduck的控制程序本身不大,直接在RK3566上编译也可以,但如果你和我一样主力开发机是x86架构,建议提前配好交叉编译环境,这样迭代代码快得多。用交叉编译务必注意一点:所有依赖库的架构必须匹配ARM64,不要出现编译时链接了x86的.a或.so导致运行时崩溃的情况。
3. 仿真到实机的模型通关之路:从PyTorch权重到RK3566上的可执行推理
3.1 模型导出前必须做的两件事:固定输入尺寸和去掉训练专用分支
在GPU训练环境里,策略网络通常和值网络、噪声采样器、经验缓冲等训练结构混在一起。部署时只需要策略网络本体(Actor),而且仿真状态下输入是带batch维度的张量(方便GPU并行采样),实机推理时同一时刻只需要处理一条数据。所以导出前第一件事就是裁剪模型,抽出actor网络,固定batch=1。
第二件事是处理输入/输出的量纲。仿真环境里,状态观测通常是归一化后的值,输出动作也往往会被clip到[-1, 1]区间。实机部署时,不能直接把actor的输出当成最终关节目标,需要在部署代码里把动作映射到真实电机的角度范围或者力矩范围。这个映射关系必须在仿真阶段就记录清楚,不要干到一半再去翻训练代码里的scale和offset。
我在导出ONNX时的核心代码如下,PyTorch版本是2.x,配合torch.onnx.export:
import torch from your_agent import Actor # 假设actor是训练好的策略网络 actor = Actor() state_dict = torch.load("best_policy.pt", map_location="cpu") actor.load_state_dict(state_dict) actor.eval() # 固定一个dummy输入,shape与实机观测维度一致 obs_dim = 48 # 以实际观测维度为准 dummy_input = torch.randn(1, obs_dim) torch.onnx.export( actor, dummy_input, "microduck_actor.onnx", export_params=True, opset_version=12, input_names=["obs"], output_names=["action"], dynamic_axes=None, # 固定shape,避免动态轴 )dynamic_axes=None这一行要划重点。实机部署时我们希望模型推理延迟稳定可预期,动态shape会引入额外判断逻辑,还可能在某些推理引擎里触发多余的预处理。固定成静态shape之后,推理引擎可以做很多编译期优化,延迟更低更稳定。
3.2 ONNX在RK3566上的CPU推理方案:ONNX Runtime的线程与亲和性设置
导出的ONNX模型在RK3566上直接跑,我选择ONNX Runtime作为推理引擎。安装很简单,用pip拉取arm64版本的onnxruntime即可。但版本选择要注意:RK3566是ARMv8架构,要选onnxruntime的aarch64 wheel,不要选x86的。
ONNX Runtime虽然在ARM CPU上开箱即用,但性能优化空间很大。我用到的关键配置如下:
import onnxruntime as ort sess_options = ort.SessionOptions() sess_options.intra_op_num_threads = 2 sess_options.inter_op_num_threads = 1 sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL session = ort.InferenceSession( "microduck_actor.onnx", sess_options=sess_options, providers=["CPUExecutionProvider"], ) obs = get_observation() # 从IMU和关节编码器读取 obs_array = obs.reshape(1, -1).astype(np.float32) action = session.run(None, {"obs": obs_array})[0]线程数设置我专门调过。RK3566是四核A55,intra_op_num_threads设成2、inter_op_num_threads设成1时,推理延迟和CPU占用率取得了一个比较理想的平衡。如果四个线程全用上,单次推理确实能降到1毫秒以内,但此时CPU接近满载,留给控制循环、IMU读取、电机指令发送的时间片就少了,整体控制频率反而跟不上。这里其实涉及一个重要的系统观念:部署不是单把模型推理做到最快,而是要让推理、感知、控制、通信整体在一个时间预算内协同完成。
3.3 把推理延迟压进控制周期:实测数据参考
在同一款RK3566开发板上,我用不同方式实测了模型推理延迟,数据大概如下:
| 推理方式 | 单次推理延迟 | 备注 |
|---|---|---|
| ONNX Runtime(2线程) | 1.8~2.5ms | 综合最优 |
| ONNX Runtime(4线程) | 1.0~1.5ms | CPU占用过高 |
| RKNN-NPU推理 | 2.0~4.0ms | 算子切换开销,不稳定 |
| 手动改写C++单线程计算 | 2.5~3.0ms | 纯CPU浮点计算 |
注意,以上是策略网络较小的情况。如果你的观测维度更大,或者网络层数更深,数据会有所变化,但趋势基本一致:RK3566上做浮点MLP推理,CPU推理已经足够,NPU的收益要等你把模型结构改成完全兼容RKNN的算子集之后才可能显现,前期没必要花那个时间。
3.4 关于量化:能用浮点就别急着int8
不少教程会提到量化到int8来加速NPU推理。但强化学习策略和图像分类模型不一样,策略网络对输入的小扰动极其敏感,量化误差可能导致输出动作产生肉眼可见的抖动,进而让机器人姿态发散。我试过把actor量化到int8后部署,机器人虽然能站住,但站立时能明显看到腿部细微颤颤巍巍的抖动,行走时更是频繁打滑。回头去对比量化前后输出差异,发现某些维度的动作值偏差已经超过了电机死区,后果就被反映到了实机表现上。
所以,除非你对RKNN量化做了充分的校准集测试,否则我建议使用FP32的ONNX直接在CPU上推理。RK3566的A55虽然不算快,但应付Microduck这种小网络足够了。等到后面想把控制频率提到500Hz以上、或者跑更复杂的策略网络时,再回头做量化优化也不晚。
4. RK3566实机上的控制循环设计:从读到算再到发,一个周期内的精细调度
4.1 控制循环的基本时间轴:5毫秒内必须完成的事
Microduck实机运行时,控制频率我设置在200Hz,也就是每个控制周期5毫秒。虽然推理延迟只有不到2.5毫秒,但整个周期的工作不只是推理,完整的时间轴包括:
- 从IMU读取姿态数据(SPI读取,约0.1~0.2ms)
- 从电机驱动板读取关节编码器反馈(UART或CAN,约0.5~1ms)
- 组装观测向量并做必要滤波(约0.1ms)
- ONNX Runtime推理得到动作(约1.8~2.5ms)
- 将动作映射成电机目标并发送(UART或CAN,约0.5~1ms)
- 预留安全检测逻辑(电流、温度、指令超限,约0.1~0.3ms)
这些加起来,在5毫秒的周期内是排得满的,但前提是代码里不能有阻塞调用。如果某一步用了time.sleep或者串口同步等待,控制周期就会被拉长,机器人就会表现出迟滞甚至站立不稳。
4.2 多线程架构:控制线程、IMU线程、通信线程的协作方式
我不建议把所有事情塞进一个死循环里串行做,因为IMU读取和电机通信都是外设操作,它们天然有等待时间,串行会浪费宝贵的CPU时间片。我的做法是拆成三个线程:
- IMU线程:以1kHz频率读取IMU数据,做滑动窗口滤波,把最新姿态写入共享变量。
- 电机通信线程:以200Hz频率发送关节指令,同时接收编码器反馈,把反馈写入共享变量。
- 控制线程:以200Hz频率从共享变量取数据,组装观测,跑推理,算目标动作,递给通信线程发送。
共享变量的读写要注意原子性。Python版本可以用threading.Lock保护,虽然会有一点点开销,但在200Hz频率下完全可接受。C++版本则可以直接用原子变量或者无锁环形缓冲区,效率更高。
4.3 观测向量的细节:从仿真到实机的数据对齐是部署过程中最容易出错的环节
仿真环境里的观测向量是理想化的:位置、速度、角速度精确无噪声,不存在延迟。实机上,IMU数据有噪声和漂移,编码器反馈有量化误差,数据的时间戳也可能对不齐。这些差异如果不处理,即便模型本身训练得很好,实地跑起来也会表现不佳。
我在部署时对观测向量做了以下处理:
- 关节位置:直接使用电机编码器反馈,但要确认单位(弧度)和零点位置与仿真一致。Microduck在仿真里定义零位是所有关节水平伸展,实机装好后要用工具把电机归零到相同位置。
- 关节速度:仿真里直接给的是真实速度,实机编码器如果频率不够高,直接差分会产生很大噪声。我用了一阶低通滤波,截止频率大约20Hz,基本能滤掉大部分抖动。
- IMU角速度与姿态:IMU原始数据要经过低通滤波,并且要注意坐标系对齐。IMU的安装方向如果和仿真定义的坐标系不一致,直接会导致策略输出反向动作,机器人会原地乱踢。
这里我踩过一个具体的坑:IMU的Z轴方向和仿真定义差180度,导致部署后Microduck一上电就往后猛退。排查时先怀疑电机方向,浪费了半天时间,最后把IMU数据打印出来和仿真对比才发现是坐标系翻转问题。这种问题排查起来极其隐蔽,建议部署前先用脚本验证每个观测维度的数值和方向是否符合仿真预期。
4.4 动作映射:策略输出到电机指令的最后一层变换
策略网络的输出是归一化动作值,范围通常在[-1, 1]之间。实机电机需要的是目标角度或者目标力矩。Microduck的默认控制方式是位置控制,每个关节有一个角度范围,比如膝关节是-2.0到2.0弧度。部署时需要一个映射函数:
target_angle = action_clipped * 0.5 * (joint_max - joint_min) + 0.5 * (joint_max + joint_min)这个映射看起来简单,但有一个容易忽略的点:仿真里动作的clip范围可能与实机电机机械限位不完全一致。如果仿真允许的角度范围略大于电机实际能运动到的物理极限,在实机上就要在发送指令前再做一次约束,防止舵机堵转或者齿轮打齿。我的做法是双重保护:代码里clip + 电机驱动板硬件限位,两条防线都保留。
5. 部署中的高频疑难杂症复盘:从设备识别到总线错乱再到步态发散
5.1 泰山派/通用RK3566开发板被识别成ADB设备的问题
很多人刚拿到RK3566开发板时,在电脑上插USB,系统识别到的不是网卡或者串口,而是一个ADB设备。这是开发板厂商预置的Android系统在作怪。泰山派这类RK3566板子在出厂时一般预装Android,通过USB连接电脑时会自动进入ADB模式,导致很多人误以为板子有问题或者驱动没装好。
解决思路其实不复杂。如果你打算跑Linux做实时控制,第一步就是重刷系统为Linux镜像。从官方渠道下载对应的Debian或Buildroot镜像,使用瑞芯微提供的烧录工具(比如RKDevTool)烧写到eMMC或者SD卡。烧录时要注意选择正确的loader文件和分区表,不同板子略有差异,但整体流程都是:进入MaskROM模式或Loader模式,然后加载镜像烧录。
烧录完成后,USB连接电脑通常就会识别为网卡设备(RNDIS或者ECM网卡),或者直接通过串口连接板子调试。这个阶段如果还想用ADB,可以手动安装adb服务,但建议还是彻底转向Linux的开发模式。
5.2 UART通信乱码和丢帧:波特率、电平匹配、缓冲区一个都不能少
Microduck电机驱动板和RK3566主控之间用UART通信时,最典型的故障就是乱码和丢帧。我的排查顺序是:
- 量电平:RK3566的UART引脚电平是3.3V,如果电机驱动板是5V电平,必须先做电平转换,否则通信不稳定不说,长期还可能烧引脚。
- 确认波特率一致性:有些驱动板默认波特率是115200,但Microduck 12个关节的数据量在这个速率下很容易出现周期性丢帧。我直接把波特率调到1.5Mbps或者更高,两边同时改,注意串口线的质量,劣质杜邦线在高速率下非常容易出现误码。
- 检查串口缓冲区:Linux系统下串口默认缓冲区可能不够大,可以通
stty或者代码里设置更大的FIFO。在Python里用pyserial时,要设置timeout避免阻塞,还要及时清空输入缓冲区,防止旧数据堆积导致控制周期延迟。
5.3 上电后机器人腿部抖动或者关节乱窜:排查链路分享
这个现象非常吓人,第一次看到整个机器人像抽搐一样,谁都会慌。我的排查链路是这样的:
- 先确认指令数据的正确性:单独写一个小脚本,向电机发送固定角度值,观察电机是否运动到预期位置。如果单个电机都控制不稳,问题在通信层或者驱动板配置,这跟策略部署无关。
- 再确认观测数据是否异常:上电后打印IMU和编码器数据,看看是否有NaN、跳变、或者方向反转。IMU噪声过大时,策略网络会收到剧烈变化的输入,输出的动作自然也是乱的。
- 然后检查动作映射是否越界:打印策略原始输出和映射后的目标角度,如果大部分时间都在极限位置,说明模型觉得当前状态非常危险,正在输出最大修正,这通常是因为观测向量单位和仿真不一致。
- 最后检查控制频率:如果频率忽高忽低(比如在50Hz到200Hz之间跳),机器人会因为控制周期不稳定而表现得如同抽搐,这多半是代码里出现了阻塞调用。
这套排查流程下来,定位问题的时间从最初的一天缩短到半小时以内。部署工作里,工具链固然重要,系统性的排查思路更值钱。
5.4 步态发散:机器人越走越偏,最终倒下
这个问题非常有代表性。策略在仿真里明明走得很好,实机上一开始也能走几步,但走着走着就偏离方向,越走越歪,最终摔倒。这类问题通常是几个原因叠加的:
- 观测延迟:IMU或者编码器反馈比真实状态滞后了几毫秒,而策略是在仿真中假设完美状态输入下训练的,延迟会导致相位错位,步态逐渐发散。
- PID/滤波器参数差异:如果部署时在观测通路里加了过重的滤波,相当于把状态信息“钝化”了,策略看到的状态变化比实际慢半拍。
- 机械公差:Microduck这类3D打印或者小批量加工的机器人,每条腿的关节间隙和摩擦力不完全一样,而仿真里模型是完全对称的。这种不对称会让步态偏移。
针对步态发散,我的处理经验是:先减少滤波强度,让观测更“锐利”;然后在代码里对偏航角做修正——让期望前进方向始终对准当前偏航的补偿方向,相当于给策略加了一个简单的外环修正;如果还不行,再回头做系统辨识,把实机每条腿的增益差异标定出来,反馈给仿真环境做domain randomization的参考。说到底,sim-to-real的差距要靠“仿真里更狠的随机化”和“实机上更准的标定”两头逼近。
6. 实测数据与调优经验:把部署后的Microduck调到“能稳定走起来”
6.1 整机部署后的性能实测数据
在完成第一版稳定部署之后,我做了一轮系统性的实测,数据如下:
| 指标 | 数值 | 说明 |
|---|---|---|
| 控制频率 | 200Hz | 波动在±2Hz以内 |
| 单周期总耗时 | 4.1~4.5ms | 低于5ms,留有余量 |
| 策略推理耗时 | 1.8~2.5ms | ONNX Runtime CPU推理 |
| IMU数据延迟 | <1ms | SPI读取,1kHz频率 |
| 电机指令发送耗时 | 0.5~0.8ms | 1.5Mbps UART |
| 连续站立时间 | >30分钟 | 无异常抖动,无明显温升 |
| 直行1米偏移量 | 5~10厘米 | 尚可接受,需进一步标定 |
6.2 调优过程中最有效的三个手段
第一,观测向量的低通滤波系数别抄网上的默认值。滤波太强会钝化,滤波太弱会引入噪声,最佳值取决于IMU质量和实际振动环境。我的做法是在板子上录制一段静止状态和一段行走状态的原始数据,离线分析频谱,选出能有效衰减振动噪声的最低截止频率,再换算成代码里的滤波系数。
第二,对电机控制指令做平滑处理。策略输出的动作虽然是连续的,但帧与帧之间的变化在突变场景下可能很大。我加了一阶指数平滑来限制关节角速度的突变率,效果立竿见影,行走姿态明显更自然。平滑系数从0.3开始调,先看站立时候的表现,再测试行走,逐步逼近最优。
第三,关注代码层面的cache友好性。这个有点进阶,但对于控制循环来说很关键。RK3566的A55核心有L1/L2缓存,如果控制循环里频繁访问大数组或者动态分配内存,cache miss率会高,延迟波动就大。我把观测数组、动作数组、IMU历史缓冲等都预先分配固定大小的全局数组,避免在控制线程内做任何动态内存分配。修改后延迟波动从原来的±0.8ms压缩到±0.2ms,稳定性提升非常明显。
6.3 关于仿真随机化的一点反思
部署到最后,我回过头审视仿真端的训练设置,发现真正让部署耗时变短的关键因素,其实是仿真里做了足够强的domain randomization。包括:
- 关节摩擦系数随机化,范围±50%
- 电机力矩限制随机化,范围±20%
- 观测噪声随机化,模拟IMU和编码器噪声
- 重心位置偏移随机化,模拟3D打印件的装配公差
- 控制延迟随机化,模拟通信时序抖动
这些随机化让策略在仿真里见过足够多的“坏情况”,部署到实机时即使某些参数和仿真不一致,策略也能给出稳健的反应。如果你打算从零开始训练一个Microduck的强化学习策略,强烈建议在训练阶段就把这些随机化因素加进去,这比部署时再去调滤波和映射要高效十倍。
7. 部署手记之外还能做什么:把Microduck当做一个强化学习实机验证平台来用
Microduck的部署完成不是终点,而是一个新的起点。25厘米的尺寸意味着它可以在普通办公桌上运行,不需要大型实验场地;RK3566主控意味着整机成本可控,不至于让平台成为实验室专享设备;强化学习策略让它可以不断迭代,今天学会走路,明天就能学会转向,后天可以试试抗扰动恢复。
我个人觉得,接下来最有价值的方向是这几条:
- 更复杂的地形适应性测试:在仿真里加入地毯、斜坡、微小障碍物,训练策略适应不同地形,然后在实机上布置同样条件来验证。这个过程可以系统性检验sim-to-real的泛化能力。
- 把训练数据反馈闭环起来:实机运行中记录状态和动作数据,用这些数据做基于模型的强化学习或者离线强化学习,让策略在真实数据上持续微调。IQL(Implicit Q-Learning)这类离线强化学习算法和这个场景非常契合。
- 多机协同:如果手上有两台以上的Microduck,可以在RK3566上跑简单的协同策略,比如互相保持距离、编队行走。这个方向对计算资源要求不高,但对通信同步和策略鲁棒性要求更高,值得一试。
总的说来,Microduck部署这件事让我最深的感受是:强化学习项目的“最后一公里”——把模型从GPU搬上实机——往往决定了整个项目能否真正发挥价值。仿真里的一切都可以重来,但实机部署每个细节都必须谨慎周全。从模型导出的静态shape,到控制循环的线程调度,再到观测向量的数据对齐,每一步都藏着决定成败的细节。希望这篇手记能给同样在这条路上折腾的人一些参考。