欢迎光临
我们一直在努力

Microduck零基础开发教程:模型部署、硬件调试与软件开发

Radxa ZERO 3W 零基础开发教程:模型部署、硬件调试与软件开发

承接《Radxa ZERO 3W 零基础部署调试教程》。
那份教程教你把软件装上,这份教程教你自己造软件:训练一个模型、把它部署上板、
调试舵机和传感器、改一行代码并在一分钟内看到它跑在鸭子上。

全部命令都来自本仓库的真实脚本和文档,
标注了来源文件,可直接跳过去看原文。


怎么读这份教程

这份教程假设你已经走完部署教程(板子能 SSH、robotctl 0.13.0 已装、7 个守护进程在跑)。
如果你的板子还没装好,先回去把部署教程 §7、§8 做完。

它分四块,可以跳着读:

想做什么读哪节
想先搞清楚"鸭子系统到底怎么长在一起的" §1
想训练/部署一个自己的策略或检测模型 §2(全链路)
想调舵机、IMU、ToF、摄像头 §3
想改 Rust 代码并推到板子上跑 §4
想把上面串起来做一个完整小功能 §5(端到端演练)

三块代码、三个仓库,先记在脑子里:

仓库本地路径干什么
microduck e:\\optiDuck\\joyandai\\microduck 上板软件栈:7 个 Rust daemon、robotctl/duckctl、板级脚本。你的开发主要在这里
microduck_rl e:\\optiDuck\\joyandai\\microduck_rl 强化学习训练:MuJoCo + PPO 训走路/站立/翻跟头,导出 ONNX
duck_detector(外部) 在 GitHub/HF 上 检测器训练:YOLO 模型训练 + ONNX→RKNN 转换(不在本地仓库里)

还有一个"仓库"不是代码:Hugging Face Hub。策略(microduck-*)、检测器
(microduck-duck-detector)都以"文件 + 一个 manifest.json"的形式发布在 HF 上,
板子通过 updaterd 从 HF 拉取。模型和策略的更新不经过 daemon 发版——这是全篇最重要的概念之一。


🎯 0. 路线图:从"会部署"到"会开发"

先看整张图,心里有数再逐节拆。三条链路,本文各占一节:

┌────────────────────────────────────────────────┐
模型链路 (§2) │ │
┌────────────┐ ┌──────┴──────┐ ┌───────────┐ ┌───────────────────┐ │
│ 训练 │ │ 导出 ONNX │ │ 转 RKNN │ │ 发布到 HF Hub │ │
│ microduck_ │ →│ scripts/ │ →│ (检测器) │→ │ policies / │ │
│ rl (PPO) │ │ export.py │ │ to_rknn.py│ │ duck-detector │ │
└────────────┘ └─────────────┘ └───────────┘ └─────────┬─────────┘ │
│ updaterd │
硬件链路 (§3) ▼ │
┌────────────────────────┐ ┌────────────────────────────────┐ │
│ robotctl health/monitor│ │ /opt/robot/policies/current │ │
│ 舵机·IMU·ToF·摄像头 │────────│ /opt/robot/detector/current │ │
└────────────────────────┘ └──────────┬─────────────────────┘ │
│ robotd 50 Hz 循环 │
代码链路 (§4) │ │
┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ 改代码 │ →│ 交叉编译 │ →│ dev-push.sh │─│ 打包+签名+apply+健康门 │
└──────────┘ └──────────┘ └──────────────┘ └────────────────────────┘

一句话版本:

  • 模型 = 被动数据(.onnx / .rknn + manifest.json),走 HF Hub → updaterd 通道,
    更新不重启 daemon 之外的任何东西,随时可以回退;
  • 代码 = 主动逻辑(Rust 二进制 + 板级脚本),走 dev-push / release 通道,
    每次替换都要过签名校验 + 健康门 + 自动回滚;
  • 硬件 = 你调试的对象,全部以 robotctl health / monitor 为观测入口。

💡 部署教程讲"装",这里讲"造"。 你在部署教程里见过的 policies/current → seed-v5、
detector/current → seed-duck-v1 这两个符号链接,就是模型链路的终点——读完 §2 你会知道
它们是怎么被填上的、以及如何填上你自己的。


1. 先建立全局观:这个系统是怎么长在一起的

1.1 一张图认识 7 个守护进程

部署教程只让你"装上了",没讲它们各自是什么。这里补上(来自
docs/design/architecture.md):

手柄(padd) 手机(btd) 你/SSH(robotctl) 远端(mediad) GitHub release
│ BLE │ BLE │ ssh │ WebRTC │ https
▼ ▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│padd │ │ btd │ │ robotctl│ │ mediad │ │ updaterd│
└──┬──┘ └──┬──┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ JSON-RPC 2.0 · 一行的 NDJSON · unix socket │
▼ ▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ robotd(50 Hz 控制循环,唯一能碰电机的人) configd(Wi-Fi/名字/配对)│
│ tofd(ToF 8×8 深度矩阵,只发不收) │
└──────────────────────────────────────────────────────────────────────┘
│ 一条 Dynamixel UART(/dev/ttyS2)

15 个舵机 + IMU(id 200,你的鸭子为 HD1910)

三个必须记住的硬规则(后面调试全靠它们):

  • robotd 是唯一能命令电机的人。 其他人只能发"意图"(走多快、看哪里、站起来),安全性由 robotd 裁决。所以舵机总线出问题,先怀疑 robotd。
  • configd / updaterd / btd 不依赖 robotd。 机器人控制循环挂了,你还能改 Wi-Fi、更新、回滚——这是"永不砖"的根基。
  • 服务和数据分两条路: 命令/状态走 unix socket JSON-RPC(控制面),视频/音频帧绝不走 socket(数据面)。所以"摄像头画面卡了"和"电机不动了"永远是两个问题。
  • 1.2 两个必须分清的词:slot 与 skill、policy 与 detector

    词意思例子
    slot(槽位) 机器人默认跑什么:7 个固定槽位 walk/stand/sitstand/ground_pick/kick_left/kick_right/roulade walk 槽装走路步态
    skill(技能) 机器人被要求时跑什么:一次性的动作,跑完自己回去 鞠躬polite-bow、前滚翻 roulade
    policy(策略) 从 RL 训练出的obs[1,61] → actions[1,14] 的 ONNX 模型 walk.onnx
    detector(检测器) 从图像训练出的目标检测模型(YOLO),输入 320×320 帧,输出 2100 个候选框 duck_detect.rknn(NPU)/ .onnx(CPU 兜底)

    robotctl policy list # 看 7 个槽位 + 已装的 skills
    robotctl policy add polite-bow <HF repo> # 加一个技能
    robotctl robot do polite-bow # 让它跑一下

    💡 slot 由配置指向某个模型文件,skill 由名字被调用。 一个策略文件放对地方(槽位/技能),就是"部署"的全部。

    1.3 三条更新通道:什么走哪条,为什么

    更新内容通道门槛失败后果
    策略 / 检测器模型 HF Hub →updaterd(robotctl policy update / robotctl duck-detector update) 只需manifest.json 字段合法 拒绝安装,现有模型不动
    daemon 代码(发版) GitHub release →robotctl update apply daemon 签名 + 健康门 自动回滚到上一个 release
    你的开发代码 dev-push.sh(用开发密钥签名)→ robotctl update apply –from 开发板 + dev key 同上,自动回滚

    为什么模型单独一条通道? 因为"换一个策略"和"换一个 daemon"是两种风险等级:
    策略文件是数据,robotd 每次加载前都会校验形状(obs_len/action_len/model_api/robot.model),
    错了顶多拒绝加载;而 daemon 二进制是代码,错了可能让板子失联,所以必须过签名 + 健康门 + 回滚。
    记住这个分层,后面所有操作都不迷路。

    1.4 你手上的三板斧(先记住,后面反复用)

    robotctl version # 每个 daemon 实际跑的版本 vs 已安装版本(先跑这个,别信记忆)
    robotctl health # 硬件 + 软件一份报告;不健康时退出码非 0(可接脚本)
    robotctl monitor # 实时:指令 vs 实际、IMU 重力、循环频率、电量和温度

    robotctl monitor 是调试主力,先记几个键:q 退出、t 打开 ToF 矩阵、c 打开摄像头、
    d 关掉右侧机器人小图、p 打开手柄输入流。终端不够宽时按 d 关掉机器人图。


    2. 模型部署:从训练到上板(全链路)

    目标:这一节结束,你能把一个自己训出来的模型(或仓库里已有的模型)放到鸭子上跑起来,
    并知道每一步卡住时去哪查。全链路五段:

    ① 训练 (microduck_rl) → ② 导出 ONNX → ③ 彩排 (CPU) → ④ 发布到 HF → ⑤ 上板+验证
    uv run train export.py infer_policy / uv run publish robotctl policy /
    policy-rehearsal duck-detector

    策略和检测器是两条平行的链(训练仓不同、转换不同、上板命令不同),分开讲,最后汇总一张对照表。(uv一个包及命令管理器,可以给你的每个命令单独创造一个环境进行运行,速度快、兼容好)

    2.1 训练一个走路策略(最短路径)

    训练仓是 microduck_rl,基于 mjlab(MuJoCo + Warp)跑 PPO(rsl_rl)。

    mjlab:一个物理机器学习模型,用来分配及观测每次PyTorch的结果。一次训练迭代流程如下

  • mjlab 重置并并行运行数千个仿真环境
    基于 MuJoCo Warp,在 GPU 上批量步进物理世界。

  • 策略网络(PyTorch)接收观测,输出动作
    观测是 PyTorch 张量,策略网络也是 PyTorch nn.Module,两者共享 GPU 内存,零拷贝。

  • mjlab 把动作送入物理引擎,计算新观测和奖励
    奖励、终止条件、课程学习、域随机化都由 mjlab 定义。

  • RSL-RL 等算法层用 PyTorch 计算损失和梯度
    mjlab 收集 rollout 数据,RSL-RL 调用 PyTorch 的 autograd 和 optimizer 更新策略参数。

  • 循环直到策略收敛
    mjlab 负责“跑环境”,PyTorch 负责“学参数”。

  • 你的第一目标是跑通冒烟测试,不是训出好策略——冒烟测试 1–2 分钟,能拦下约 95% 的配置错误
    (这是 microduck_rl/AGENTS.md 的原话:Never launch a long run without one)。

    cd e:\\optiDuck\\joyandai\\microduck_rl

    # 0) 先同步依赖(uv 管理,装好 uv 后)
    uv sync

    # 1) 冒烟测试(必须先跑;64 envs × 5 iterations,1–2 分钟)
    uv run train Mjlab-Velocity-Flat-MicroDuck –env.scene.num-envs 64 –agent.max_iterations 5

    # 2) 正式训练(需要 CUDA GPU;8–12 GB 显存用 1024 envs,≥16 GB 用 4096)
    uv run train Mjlab-Velocity-Flat-MicroDuck \\
    –env.scene.num-envs 1024 \\
    –agent.max_iterations 15000

    # 3) 续训(从某个 checkpoint 接着跑)
    uv run train Mjlab-Velocity-Flat-MicroDuck \\
    –env.scene.num-envs 1024 \\
    –agent.load-checkpoint logs/rsl_rl/velocity/<run>/model_15000.pt \\
    –agent.resume True \\
    –agent.max_iterations 30000

    来源:docs/training_config_velocity_hd1910.md §10 训练命令、microduck_rl/AGENTS.md Commands。

    训练产物:logs/rsl_rl/velocity/<timestamp>/model_<iter>.pt;
    曲线在 wandb 项目 mjlab_microduck(本地跑是离线 run:wandb/offline-run-*)。

    训练前必须懂的三件事(否则训出来的东西上不了板)

    ① 观测是 61 维,全系统共享,别动它。 布局固定:48 维本体感受 + 13 维命令块 [twist(3), head_pose(4), body_pose(6)],顺序固定。新任务只改命令块的含义,不能删槽位
    (AGENTS.md Invariants 第一条)。全零命令 = 站立——这是部署时的空闲状态,必须显式训练过。

    ② 关节布局是 14 个舵机,ctrl idx = joint idx:

    0–4 左腿:hip_yaw, hip_roll, hip_pitch, knee, ankle
    5–8 脖子/头:neck_pitch, head_pitch, head_yaw, head_roll
    9–13 右腿

    MDP 是 Markov Decision Process 的缩写,中文叫 马尔可夫决策过程 。它是强化学习里描述“智能体如何在一系列状态下做决策”的数学框架。

    写 MDP 时永远用 _servo_joint_ids / _servo_joint_pos 辅助函数取关节下标,
    不要硬编码(换带被动关节的模型时下标会错位)。

    ③ 执行器是 BAM 模型(电压控制的作动器,摩擦力由执行器自己算)。电机参数按型号分开存:
    microduck_rl/vendor/bam/bam/params/hd1910/*.json(你的鸭子)/ hls2909(另一款)。
    训练、彩排命令都要带对应 –motor hd1910(§2.3)。dof_frictionloss 在 BAM 下是零——
    给关节摩擦加 DR **(Domain Randomization,域随机化)**要缩放执行器的 friction_scale,否则是静默的无效操作。
    为什么按型号分开?因为换执行器 = 重训策略:官方 XL330 的 ONNX 是给 XL330 的 BAM M6
    执行器模型训的,直接换到 HD1910 上不能 bit-exact 复用(OpenMicroDuck docs/servo.md §1),
    电机型号写在训练/彩排命令里(–motor hd1910),从源头就锁对执行器参数。

    训练纪律(少走几个月的弯路)
    • 奖励符号: microduck_mdp 里自取负的惩罚项(*_penalty / *_l1 返回 ≤ 0)要用正权重,
      否则双重取负变成"奖励违规"。铁律:每个 Episode_Reward/<penalty> 在 wandb 里必须 ≤ 0。
    • 预算参考: 简单一次性动作 ≈ 1000 iterations(4096 envs);走路等复杂步态 4000–6000。
    • 失败先测再改: 别凭感觉调奖励,先 headless 评估 checkpoint,很多"失败"只是 checkpoint 太早。

    💡 当前板子实况: 板子上 policies/current → seed-v5 是官方策略集。你训练的第一步
    不是替换它,而是先在 PC 上跑通"冒烟 → 导出 → 彩排",等 §2.3 的彩排通过再谈上板。

    2.2 导出 ONNX(归一化会被烤进模型)

    训练完的 checkpoint 不能直接用,必须走官方导出器——它会把观测归一化
    (obs_normalization=True)烘焙进 ONNX。手工转换会丢掉归一化,在仿真里玩看不出问题
    (play 时环境会再归一化一次),上了真机才露馅。这是 AGENTS.md 明确警告过的坑。

    # 方式 A:从 wandb run 导出
    uv run scripts/export.py Mjlab-Velocity-Flat-MicroDuck –wandb-run-path <entity/project/run_id>

    # 方式 B:从本地 checkpoint 导出(最常用)
    uv run scripts/export.py Mjlab-Velocity-Flat-MicroDuck \\
    –onnx-file walk_hd1910.onnx –checkpoint <iter>

    例如需要导出以下目录模型:E:\\optiDuck\\microduck_rl\\logs\\rsl_rl\\velocity\\2026-09-15_20-44-58_prebom_resume

    实际命令为:

    # 1) 切到训练仓库根目录(导出器按相对路径找 logs/)
    cd E:\\optiDuck\\microduck_rl

    # 2) 导出 model_7750.pt → walk_hd1910.onnx(归一化自动烤进 ONNX)
    uv run scripts/export.py Mjlab-Velocity-Flat-MicroDuck `
    –checkpoint-file "E:\\optiDuck\\microduck_rl\\logs\\rsl_rl\\velocity\\2026-09-15_20-44-58_prebom_resume\\model_7750.pt" `
    –onnx-file walk_hd1910.onnx

    # 3) 确认产物(应显示 walk_hd1910.onnx,约几十 MB)
    Get-ChildItem walk_hd1910.onnx | Select-Object Name, Length, LastWriteTime

    3D演示这模型的命令如下:

    cd E:\\optiDuck\\microduck_rl

    uv run play Mjlab-Velocity-Flat-MicroDuck `
    –checkpoint-file "E:\\optiDuck\\microduck_rl\\logs\\rsl_rl\\velocity\\2026-09-15_20-44-58_prebom_resume\\model_7750.pt" `
    –num-envs 1

    导出实现(microduck_rl/src/mjlab_microduck/export.py L251-L269)的核心动作:
    runner.export_policy_to_onnx 只导出 actor(critic 不上板),把观测归一化的均值/方差写成
    ONNX 常量,并把 model_api、obs_len、action_len 等写进 metadata。

    产出契约(也是板子校验的契约,见 docs/recurrent-policies.md 与 docs/policy-manifest.md):

    输入输出
    前馈模型 obs [1, 61] actions [1, 14]
    LSTM 模型(model_api: 2) obs [1, 61], h_in, c_in actions [1, 14], h_out, c_out

    所有张量 float32、按名字匹配(与顺序无关)。LSTM 导出必须标 model_api: 2,
    老 daemon 会拒绝安装;前馈是 API 1。

    2.3 CPU 彩排:上真机前的最后一次检查

    两个彩排工具,一个在训练仓(仿真),一个在上板仓(真实推理路径):(注意以下命令是linux环境)

    # ① 训练仓:MuJoCo 里用真实执行器参数走一遍(默认已是 hd1910 电机模型)
    uv run scripts/infer_policy.py –walking walk_hd1910.onnx –motor hd1910

    # ② 上板仓:用生产级 Rust 推理路径测延迟,不碰电机总线
    # (在 PC 上开发、最终在板子上跑——板子的耗时才是真预算)
    export ORT_DYLIB_PATH=/path/to/libonnxruntime.so
    cargo run –release -p duck-control –example policy-rehearsal — policy.onnx > rehearsal.json

    policy-rehearsal 会预热模型、清空 LSTM 状态、测 1000 次推理,输出 p50/p95/p99/max 延迟、
    超过 20 ms 的次数和动作序列(docs/recurrent-policies.md §Offline rehearsal)。

    💡 部署侧提醒: 板子上的 ONNX Runtime 由部署脚本装(setup-board.sh 装 1.28.0)。
    检测器走 NPU 时模型是 .rknn,策略走 CPU 时是 .onnx,两者链路不同,别混。

    2.4 发布到 HF Hub:一个模型变成一个"仓库"

    发布前置:HF 登录(一次性的)。 uv run publish 走 huggingface_hub 上传,
    token 从两个地方找,没有就不让你发(报 no cached HF token. Run hf auth login.):

    # 方式 A(推荐):huggingface_hub CLI 登录,token 缓存到本机
    # 先在 https://huggingface.co/settings/tokens 创建 token,再粘贴进来
    hf auth login

    # 方式 B:临时用环境变量(不写盘,仅当前终端生效)
    $env:HF_TOKEN = "hf_xxx" # PowerShell;Linux 用 export HF_TOKEN=hf_xxx

    token 权限要求:publish 要在你名下发仓库(create_repo)并上传文件
    (upload_folder),必须用 Write 权限的 token(fine-grained token 勾 write;
    只读 token 上传会 401/403)。–repo <user>/microduck-<name> 里的 <user> 就是
    登录账号的 HF 用户名,publish 会自动从 token 推断——所以先登录、用户名填自己的。

    💡 如果你之后用 uv run train … –hf-jobs 在 HF 上跑训练,那是另一个
    带 Jobs 权限的 fine-grained token,与本地发布不是一回事(hf_jobs.py 里
    token 鉴权通过但缺 Jobs 权限会明确报 “token-scope problem, not billing”)。

    策略不是"文件上传"这么简单,板子要求旁边有一个 manifest.json 描述它。
    训练仓的 publish 命令帮你把两者一起发布(microduck_rl/AGENTS.md):

    uv run publish –task <TASK_ID> –wandb-run-path <entity/project/run_id> \\
    –checkpoint N –repo <user>/microduck-<name> \\
    –kind episodic –duration-s 4.0
    # 产出:policy.onnx + schema-2 的 manifest.json + README,推送到 HF Hub
    # 板子侧用 `robotctl policy add` 读取它

    publish 只允许发布常命令的 episodic/perpetual 策略;phase/posture 那种由 daemon 驱动的
    策略(地面捡物、坐↔站)属于官方策略集,不走这条普通发布路。

    manifest.json 长什么样(完整字段见 docs/policy-manifest.md,一个典型单策略仓库):

    {
    "schema_version": 2,
    "model_api": 1,
    "obs_len": 61,
    "action_len": 14,
    "robot": { "model": "microduck", "hw_rev": 1, "servos": "hd1910", "control_hz": 50 },
    "name": "polite-bow",
    "kind": "episodic",
    "duration_s": 4.0,
    "chain": false,
    "entry_pose": "standing",
    "description": "Bows from a two-foot stand and comes back up.",
    "command": { "encoding": "constant", "idle": [0, 0, 0],
    "twist": "unused (zeros)", "head": "unused (zeros)", "body": "unused (zeros)" },
    "training": { "task_id": "Mjlab-PoliteBow-Flat-MicroDuck", "repo": "pollen-robotics/microduck_rl",
    "commit": "0bf9897", "branch": "bow", "dirty": false,
    "run": "pollen-robotics/mjlab_microduck/abc123", "checkpoint": 3000,
    "exported": "2026-09-02T14:05:00Z" }
    }

    💡 robot.model(microduck)才是校验字段,写错直接拒绝安装;robot.servos
    只是展示字段(官方例子写 xl330,你的鸭子写 hd1910),不校验但写对才能溯源。
    官方 policy-manifest.md 里的例子、以及 robotctl policy add 的校验规则都基于 robot.model。

    两个最关键的概念字段(kind 和 command.encoding,板子靠它们决定怎么用你的模型):

    kind谁结束这个策略变成什么
    episodic 自己跑duration_s 秒后回安全姿势 一次性技能(robot do 触发)
    perpetual 一直跑直到被告知停下 槽位步态(policy load walk …)
    scripted 可被中断的一次性 记录用,daemon 自己驱动
    command.encodingdaemon 喂给它的命令谁能用它
    缺省 /constant 固定 twist,回程idle 所有一次性技能
    phase [cos 2πφ, sin 2πφ, 0] 随时间转 地面捡物(daemon 驱动,拒绝 policy add)
    posture_flag 一个槽位携带 sit/stand 坐↔站(同上,拒绝 policy add)

    💡 一个常命令的 episodic 策略,加上 duration_s,就是"按名字就能调用的技能"——
    这是社区发布策略最常用的形态。phase/posture 策略不能做成技能,装进槽位由 daemon 驱动。

    2.5 策略上板:五条命令全场景

    板子上所有策略操作都走 robotctl policy …(只读命令不需要 sudo,改状态的要 sudo):

    robotctl policy list # 看 7 个槽位 + 已装技能(两栏表格)
    robotctl policy check # 官方策略集:已装 vs 最新 vs 仓库还提供什么(只读)
    sudo robotctl policy update # 更新到最新(–version v1 可回退到指定版本)

    试自己的文件(不发布、不改配置、不重启):

    sudo robotctl policy load walk /home/radxa/my_walking.onnx

    走路时会先回 home 姿势、加载、再驱动;坐着时则什么都不动——换的不是正在跑的那个网络。
    sudo robotctl policy reset walk 放回原样。

    装别人的(社区)策略:

    robotctl policy search microduck
    sudo robotctl policy load walk RemiFabre/microduck-flamingo-cycle

    加一个技能并触发:

    sudo robotctl policy add polite-bow fffiloni/microduck-polite-bow-b1d864
    robotctl robot do polite-bow

    robot do 需要机器人正在驱动(手柄 Start 后)。技能长度来自 manifest 的 duration_s。

    把技能绑到手柄按钮:

    robotctl pad bindings # 看当前绑定
    sudo robotctl pad bind x polite-bow

    写入 [pad] x = "polite-bow" 一行,padd 一秒内生效,不重启。

    校验规则(被拒绝的常见原因):policy add 在下载前就检查 obs_len(必须 61)、
    action_len(14)、model_api(≤ daemon 的)、robot.model(microduck),
    以及非 constant 编码——任何一项不符直接拒绝,不会动你现有的策略。

    📖 代码侧实现:policy.fetch 由 updaterd 处理,robot.setSkill / robot.policies /
    robot.do 是 duck-ipc-proto 的现成方法名——spaces/policy-shop/app.py 里的"一键安装"
    就是按这个顺序调这四个调用(详见 §4.5)。

    2.6 检测器模型:另一条链(YOLO → RKNN)

    策略是"自己训 + 导出 + 发布",检测器链路的转换步骤不同——它要变成 NPU 能跑的 .rknn:

    ① 训练 (duck_detector 仓) → ② 转 RKNN (scripts/to_rknn.py) → ③ 发布 HF Hub → ④ 上板
    yolo11n 320×320 INT8 量化 → duck_detector.rknn 同名仓发两个文件 robotctl
    同时留 duck_detector.onnx (CPU 兜底) duck-detector

    为什么两个文件? 板子优先跑 NPU 的 .rknn;NPU 不可用时回退 CPU 的 .onnx
    (deploy/robotd.toml L348-358 写了这个选择逻辑)。

    上板命令(跟策略同构,根不同、文件清单固定):

    robotctl duck-detector check # 已装 vs 仓库最新(只读)
    sudo robotctl duck-detector update # 更新(重启 mediad,画面会断一下)
    # 检测器跑不跑还受配置控制:sudo robotctl configure 里 [duck_detector] enabled

    首次上板机制:scripts/seed-detector.sh 由 release 的 postinstall 钩子跑,
    在空板子上按 pin 安装(DETECTOR_REPO / DETECTOR_VERSION / DETECTOR_FILES,脚本 L32-47),
    装到 /opt/robot/detector/releases/seed-duck-v1/ 后原子切换 current 符号链接
    (先写 current.new 再 rename,脚本 L66-126)。它绝不碰不是自己装的东西。

    板子目录结构(/opt/robot 下三条 current 链,是模型链路的"安装现场"):

    /opt/robot/detector/current → releases/seed-duck-v1/ # 检测器(你的板子现在是这个)
    /opt/robot/policies/current → releases/seed-v5/ # 官方策略集(含 manifest.json)
    /opt/robot/daemon/current → releases/<version>/ # daemon 二进制(§4 的主角)

    2.7 NPU 推理链路:模型在板子上到底怎么跑

    检测器的运行时绑定在 duck-detect crate(docs/project/npu-bringup.md):

    • librknnrt.so 是 dlopen 动态加载的,不是链接的(duck-detect/src/rknn.rs)。
      因为它是 vendor blob、不在任何 Debian 套件里,链接它就没法在 CI 交叉编译;
    • 运行时反量化:INT8 量化模型的输出带 scale 和 zero point,
      rknn_outputs_get 在请求时会转成 float——解码层拿到的直接是 float,不用自己算 scale;
    • 版本约束:librknnrt.so(板子上是 v2.3.2,由 scripts/setup-npu.sh 装)不能低于
      转换模型用的 rknn-toolkit2 版本,否则 rknn_init 失败且无解释(Cargo.toml L34-42
      有这条约束的 pin)。

    验证工具 duck-bench(不在 release 里,是测量工具,走 scp;duck-detect/src/bin/duck-bench.rs):

    cargo board –bins -p duck-detect # 交叉编译(§4.3 会讲 cargo board 是什么)
    scp target/aarch64-unknown-linux-gnu/release/duck-bench radxa@<robot>:/var/tmp/
    scp <the>.rknn radxa@<robot>:/var/tmp/duck.rknn
    scp -r datasets/raw/<a-session> radxa@<robot>:/var/tmp/frames

    /var/tmp/duck-bench –model /var/tmp/duck.rknn –frames /var/tmp/frames

    它按顺序回答三个问题:能跑吗 → 还认鸭子吗 → 花多少钱(延迟分位数 + 本进程 CPU)。

    量化模型的第一个坑——阈值: 量化模型的分数在自己的尺度上,float 模型的 0.5
    不是它的 0.5。一跑检测不到东西,先试 –threshold 0.2,别急着怀疑转换坏了。

    实测数字(Radxa ZERO 3 上,2 Hz 节奏,来自 npu-bringup 文档):

    项目数值
    driver / runtime 0.9.8 / 2.3.2
    延迟 p50 / p95 25.7 ms / 58.4 ms
    每帧 CPU(含 letterbox 缩放) 20.7 ms
    满载结束时 SoC 温度 63 °C

    💡 letterbox(1280×720 → 320×320 缩放)在 CPU 上跑,CPU 数字不是纯 NPU 的代价。
    用 NPU 的意义是不占用 robotd 的 50 Hz 循环,这个目标要在测量后确认。

    2.8 两条模型链路对照表

    策略(policy)检测器(detector)
    训练仓 microduck_rl(PPO/MuJoCo) duck_detector(YOLO,外部仓)
    转换 不需要(CPU 直接跑 ONNX) to_rknn.py INT8 量化(NPU 跑 RKNN)
    板载格式 .onnx .rknn + .onnx(CPU 兜底)
    发布地 <user>/microduck-<name> 或官方策略集 pollen-robotics/microduck-duck-detector
    上板命令 robotctl policy add/load/update robotctl duck-detector update
    加载校验 obs_len/action_len/model_api/robot.model 形状 + 阈值
    谁在跑 robotd(50 Hz 循环) mediad(摄像头侧)
    更新后重启 不重启(槽位重新加载) 重启mediad

    3. 硬件调试:从体检到动起来

    ⚠️ 本机现状(2026-09):你手上还没有舵机、摄像头、ToF HAT。 这一节按"硬件到手后的
    完整流程"写,每步都标注**【现在能跑】还是【等硬件】**。等硬件到齐,直接照做即可;
    现在就能跑的部分(总线体检、日志、排障习惯),建议立刻练手。

    核心思想只有一个:板上的一切硬件都由 7 个 daemon 包了一层接口,你永远从
    robotctl 这一侧观察和操作,不要绕过 daemon 直接摸硬件。 绕过了,下一层就是
    "为什么它不理我"的无底洞。

    3.0 硬件地图:这条鸭子身上有什么

    Radxa ZERO 3W(你的板子)
    ┌──────────────────────────────────────────────────────────────┐
    │ /dev/ttyS2 ←── Dynamixel UART(protocol 2.0,唯一电机总线)│
    │ /dev/i2c-4 ←── HAT 上的 ToF(VL53L5/8CX,8×8 深度矩阵) │
    │ CSI-2 MIPI ←── 摄像头(配 rk3568- 前缀的 overlay) │
    │ I2S / codec ←── 音频(喇叭 + 麦克风) │
    │ /dev/dri/renderD128 ←── NPU(§2.7 已讲) │
    └──────────────────────────────────────────────────────────────┘
    │ 一条总线上的两个"人"

    15 × Dynamixel 协议舵机 + IMU 板(id 200,走 imu_to_dxl)
    左腿 20–24 · 右腿 10–14 · 头/脖子/嘴 30–34(robotd-design.md §1.1 官方布局)
    · 同一条 UART、同一个 protocol 2.0、同一次 sync_read 批量读回
    · 舵机具体型号由机器人配置决定:**你的鸭子是 HD1910**(官方鸭子是 XL330,同协议不同电机)
    · 机器人**没有第二根电机总线**,也没用 DXL 之外的协议(docs/design/robotd-design.md §1.1)

    舵机 ID 布局是官方写死的,不是随便编的号(docs/design/robotd-design.md L35-37):

    ID位置数量
    200 imu_to_dxl(IMU 板,伪装成"舵机") 1
    20–24 左腿(hip_yaw → ankle 五关节) 5
    10–14 右腿(同上) 5
    30–34 头 / 脖子 / 嘴 5

    为什么 IMU 能和舵机挂同一条线?因为 Dynamixel 是地址寻址的菊花链:每帧带目标 id,
    主控广播、目标应答。IMU 板把自己伪装成一个 id=200 的"舵机"(协议层)挂进来,
    robotd 一次 sync_read 就把 15 个舵机 + IMU 的数据全拿回来了(IMU 排在 id 向量第一个,
    先于舵机爆回应)。这条链是机器人唯一的运动感知来源,所以排查顺序永远是:总线 → robotd → 上层。

    主控的接口是怎么分给鸭子的(OpenMicroDuck docs/main_controller.md §4,ZERO 3W 的 40-pin 引出):

    接口40-pin 位置鸭子拿来做什么
    UART2 40-pin 引出 HAT 半双工换向后 → 舵机总线 +imu_to_dxl(id 200)
    I²C3 M0 40-pin 的 3 / 5 HAT 上 codec、ToF(原厂同一控制器 M1 给了 USB-C PD;官方 overlay 改到 M0 后失去 PD 协商,5 V 充电仍可用)
    MIPI CSI 22-pin、0.5 mm 间距、4-lane IMX219 一类模组,mediad 用
    I²S 40-pin HAT 上 TLV320AIC3104 音频 codec
    Wi-Fi / BT 板载 configd / btd,不经过 HAT

    ⚠️ UART2 有个 Armbian 默认坑:它默认在 UART2 开 serial-getty(登录终端),一个
    agetty 占着端口,所有舵机就全看不见了。scripts/setup-board.sh 会 mask 这个 unit
    (部署教程已做过)。以后谁再"突然全不认舵机",先 fuser -v /dev/ttyS2 看是不是 agetty
    又回来了(robotd-design.md §1.1)。

    总线的"运行参数"是启动时写进舵机 EEPROM 的(robotd-design.md §1.1、§2.1)——不是默认值:

    寄存器值为什么
    baud_rate 3(= 1 000 000 bps) 50 Hz 环一拍要读写 16 个设备,波特率低于 1 Mbps 拍预算就爆
    return_delay_time 0 XL330 出厂 250 = 每设备 500 µs 换向,16 个设备约 8 ms = 拍预算的 40%,必须清零
    pwm_slope 255 位置环斜率,防电流尖峰
    shutdown 52 错误锁存掩码:过载 + 过热 + 输入电压故障(谁触发谁锁住,必须 reboot 才清)

    💡 理解换向(turnaround):Dynamixel 是半双工总线,一个设备答完才能轮下一个。
    每个设备默认回包前等 500 µs,15 个舵机 + IMU 串下来一拍就吃掉 8 ms(20 ms 拍的 40%)。
    把 return_delay_time 清零,就是把这 8 ms 还回去——所以"init 慢"和"一拍 50 Hz"的账,
    都在这几个寄存器里。

    3.0.1 舵机选型:你的 HD1910 与官方 XL330(为什么能混着讲)

    本节内容主要来自 OpenMicroDuck 仓库的 docs/servo.md(选型逻辑)、docs/bom.md(采购)
    与 docs/structure.md(结构差异)。先记住一句话:
    “总线说 Dynamixel 话” ≠ “电机是 ROBOTIS 原厂”。

    官方 Microduck 的 BOM 是 ROBOTIS Dynamixel XL330-M288-T(18 g、288.4:1 减速比、
    堵转 0.60 N·m @ 6 V、Dynamixel Protocol 2.0)。你的鸭子换成 飞特 Feetech HD-1910-C001,
    OpenMicroDuck 的 3D 结构就是按这颗舵机优化的(docs/structure.md:HD1910 拆掉副舵盘后是
    突起的,XL330 是凹陷的,头部/躯干/胯/腿/脚都为此改过——不要拿 XL330 的结构件硬装 HD1910)。

    两者协议层兼容:HD-1910 是半双工 8N1 的"飞特数字包",总线上按 XL330 的协议/寄存器工作
    (1 Mbps、baud_rate=3),所以 robotd 不用改一行代码。但电机本身不是同一颗——
    换型号必须重训策略(§2 里所有训练/彩排命令带 –motor hd1910,就是这个原因)。

    HD-1910-C001 关键规格(飞特规格书 A/0,2026-09-07,OpenMicroDuck/hardware_spec/servo/):

    项目HD-1910-C001对照 XL330-M288-T
    重量 / 尺寸 21 ± 2 g / 20 × 34 × 23 mm(含舵盘) 18 g / 20 × 34 × 23 mm
    减速比 320 : 1 288.4 : 1
    工作电压 4–8.4 V(6 V 为标定点) 3.7–6.0 V(推荐 5 V)
    堵转扭矩 12 kg·cm(1.18 N·m)@ 6 V 6.1 kg·cm(0.60 N·m)@ 6 V
    额定扭矩 3 kg·cm @ 6 V 手册未给
    空载转速 92 rpm @ 6 V 123 rpm @ 6 V
    电流 @ 6 V 待机 20 mA / 空载 ≤180 mA / 额定 690 mA / 堵转 1.6 A 17 mA / — / — / 1.74 A
    编码器 12-bit 磁编码 12-bit 绝对式(AS5601)
    电机 / 齿轮 空心杯 / 金属齿 有芯 / 工程塑料
    协议 半双工 8N1,飞特数字包(总线按 XL330 协议跑) Dynamixel Protocol 2.0
    恒力模式 模式 2 恒流;默认模式 4(PD 位置) Mode 0 电流 / Mode 5 电流限制位置
    ID 范围 0–253(出厂 ID 1) 0–252
    针序(AMP2.0-3P) 1=Signal / 2=Vcc / 3=GND JST 3P:1=GND / 2=Vcc / 3=DATA
    零售价(2026-09) 预售¥123 $23.90–$27.49 / ¥95–¥238

    HD-1910 在 4.8 / 6 / 7.4 V 的完整电气点(规格书 §5):空载 73 / 92 / 113 rpm;堵转 9 / 12 / 15 kg·cm;
    额定 2.2 / 3 / 3.7 kg·cm;堵转电流 1.2 / 1.6 / 2 A。Kt = 7.5 kg·cm/A(力矩电流比,BAM 标定用)。

    四个必须记的差异(对照表之外的坑):

  • 针序相反,别直插:HD-1910 是 1=Signal / 3=GND,与 XL330 和飞特 HL-2915
    (1=GND / 3=Signal)相反。做转接线/HAT 前先核对针序,插反轻则不认、重则烧舵机板。
    OpenMicroDuck 桌面调试阶段专门有一根 PH2.0 转 5264 3P 转接线(docs/bom.md),
    就是给"外置 URT2(5264 口)→ HD-1910(AMP2.0-3P 口)"用的。
  • 电压范围宽,但别超 8.4 V:HD-1910 走 4–8.4 V,过压保护阈值 10 V。2S 锂电(7.4 V)
    直供在范围内;供电轨的 MOS / 连接器电流预算按 15 路额定电流(690 mA × 15 ≈ 10 A 峰值预算)算。
  • 扭矩几乎翻倍,结构是适配过的:6 V 堵转 12 kg·cm ≈ XL330 的两倍,额定 3 kg·cm。
    OpenMicroDuck 的结构/装配件(cad/)已按 HD-1910 调好,别混用 XL330 零件。
  • 换型号 = 重训策略:官方 9 个 ONNX 是给 XL330 的 BAM M6 执行器模型训的,不能当
    bit-exact 用到 HD-1910 上(servo.md §1 引言:换执行器之后,官方 9 个 ONNX 不能当即插即用步态)。
    你的训练/彩排命令统一带 –motor hd1910(§2.1 ③、§2.3),就是让 BAM 用 HD-1910 的参数
    (microduck_rl/vendor/bam/bam/params/hd1910/*.json)。
  • 3.0.2 采购清单:硬件到手前先对一遍(OpenMicroDuck BOM)

    OpenMicroDuck 把采购分成两个阶段(docs/bom.md)——别等所有零件齐了再开工:

    • 桌面调试版:主控和舵机驱动板外置,用电源适配器供电、舵机驱动板直接连 PC,
      先把控制和策略跑通(研发期主力,你现在正处在的阶段);
    • 整机集成版:Zero Robot HAT + IMU 转接板 + 电池全部进机体内,装好模型软件成整机。

    共用件(两个版本都要):

    器件型号规格数量备注
    舵机 飞特 HD-1910-C001 恒力空心杯舵机,4–8.4 V,堵转 12 kg·cm 15 换型号必须重训策略
    主控 Radxa ZERO 3W RK3566,2 GB RAM,带 WiFi,无 eMMC,带排针版 1 系统装在 MicroSD 上
    SD 卡 MicroSD / TF 64 GB,A2 1 无 eMMC 时必需
    摄像头 IMX219 MIPI FPC 排线,500 万像素 1 CSI 接口
    麦克风 / 扬声器 料号未定

    紧固五金(M2/M2.5 自攻螺丝各规格共约 131 颗、轴承 6700K × 3 与 ET2216 × 1)和 3D 打印结构件
    (约 40 个零件,推荐 PLA,脚垫可试 TPU-95)详见 bom.md 原文,这里不重复。

    桌面调试版额外(研发期必须):

    器件型号规格数量原因
    IMU 模组 LSM6DSV16X 模块 支持 I2C 和 SPI 1 IMU_to_servo 自制板未就绪前,成品模组直接挂主控 I2C/SPI 读陀螺仪/加速度
    舵机转接线 PH2.0 转 5264 3P 1 根 PH2.0 3P 接 1 根 5264 3P 1 外置飞特 URT2 口是 5264/AMP-3,HD-1910 是 AMP2.0-3P,中间必须转接
    舵机驱动板 飞特 URT2 串口总线舵机驱动板 Type-C 接口 1 桌面调试的总线入口,直接连 PC
    舵机电源适配器 7.5 V 3A,DC 5.5×2.1 插头,3C 认证 1 给外置 URT2 / 舵机供电
    主控电源适配器 Type-C PD(5 V 3A 或 5 V 2A) 1 主控外置时单独供电

    整机集成版额外:Zero Robot HAT 转接板(40-pin 半双工舵机换向 + 电池配电 + I²C / I²S 外设)、
    IMU_to_servo 转接板(板载 LSM6DSV16X,挂在舵机总线上、规划从机 ID 200,对齐官方 50 Hz 契约——
    整机里 IMU 与 15 颗舵机同一趟 sync_read,不再走主控上的裸 I²C 模组)、
    7.4 V 3400 mAh 2S 锂电池组(18650×2,2C 放电,XH2.54 2P 公头)。

    3.1 硬件体检:【现在能跑】先养成这个习惯

    robotctl health # 一次性报告:硬件 + 软件 + 网络 + 电量温度
    robotctl health –json # 机器可读,可接脚本;不健康时退出码非 0

    health 是排障第一站:它把"电机总线识别到没有、IMU 读数正不正常、哪个 daemon 没在跑"
    一次性摆出来。退出码非 0 可以拿来写检查脚本(比如每分钟跑一次,非 0 就报警)。

    robotctl monitor # 实时 TUI,看循环的"真实世界"

    monitor 是调试主力,记住它的版面在说什么:

    版面看什么
    指令 vs 实际 每条腿关节的目标角 vs 传感器实测——偏差持续 >3° 就是机械/电机问题
    IMU 投影重力 机器人"以为"自己在往哪倒(跌倒判断的依据)
    循环频率 robotd 是否稳定 50 Hz;掉到 30 Hz 以下先查 CPU 争抢
    电机总线 读写耗时、失败次数、电压/温度(每秒采样一次)
    电量 / 温度 别让训练好的鸭子饿着肚子干活

    monitor 按键:q 退出、t 打开 ToF 矩阵、c 打开摄像头窗口、d 关右侧机器人小图
    (终端窄时按 d)、p 打开手柄输入流。

    💡 把「先 health,再 monitor」练成本能。 这两个命令不碰任何硬件状态、不产生副作用,
    任何时候都可以跑——这是硬件调试里唯一可以闭眼执行的环节。

    3.2 舵机总线调试:【等硬件】Dynamixel 链

    先认识三个基本操作(robotctl robot …,全部由 robotd 代理执行,因为它独占总线):

    robotctl robot init # 给所有关节上电,约 2 秒位置斜坡到 home 姿势
    # 会动所有关节!放架子上或扶住(main.rs L437-448)
    robotctl robot relax # 切电,机器人瘫倒——拿起/收纳前先 relax
    robotctl robot reboot-motors # 重启全部舵机(或 reboot-motors 10 20 只重启这两颗)
    # 舵机过载/过热进入硬件错误时用它恢复,无需拔电池
    # 注意:重启后 torque off,要再 robot init 或按 Start
    # ID 布局见 §3.0(10–14 右腿 / 20–24 左腿 / 30–34 头)

    三个操作对应三个心智模型: init = 上电并站稳(无策略也能站);enable = 把控制权交给
    策略(等于手柄 Start);relax = 放倒。调试时永远先 init 再操作,结束时 relax。

    总线本身的验证(robotd 没起也能测):

    sudo systemctl status robotd # robotd 必须在跑,总线是它独占的
    sudo journalctl -u robotd -f # 看 50 Hz 循环的实时日志:读回失败会在这里报
    robotctl monitor # 看总线健康栏:读写耗时、失败计数
    fuser -v /dev/ttyS2 # "谁占着总线"?出现 agetty = serial-getty 复活了

    判断"总线活着"的三个信号(按可靠度排序):

  • robotctl health 的电机总线行显示"识别到 15 个舵机 + IMU"——最强信号;
  • robotctl monitor 的实际角会跟着指令角动(哪怕有偏差)——说明读和写都通了;
  • journalctl -u robotd 里没有周期性 read failed / timeout——弱信号,有噪音。
  • 三个常见总线坑:

    • 只报 1 个舵机或一半舵机:菊花链断点。从头到尾找松动的接插口,robotctl robot reboot-motors
      后重看 health(新插上的舵机按顺序重新上链);
    • 某个舵机一直过热/过载(monitor 电压温度栏飘红):机械卡死或减速箱锁死,先 reboot-motors <id>
      排除固件态,还红就拆下人工转——电机阻转和固件错误的处理完全不同;
    • 换过舵机后第一次 init 很慢或报错:这是**"收养"机制在工作**(robotd-design.md §2.1)——
      新舵机出厂是 ID 1 / 57 600 baud,跟总线(ID 布局 / 1 Mbps)都对不上。robotd 启动时
      先 ping 15 个期望 ID;恰好一个没回应时,就去 1 Mbps、再降到 57 600 找 ID 1,
      找到后把缺失 ID 和 1 Mbps 波特率写进去、重启它、再跑寄存器检查。所以"换舵机后第一次
      init 慢"是正常的收养周期,等 journalctl -u robotd 出现稳定循环再继续;注意只换一颗
      ——换两颗以上同时缺位,收养逻辑找不到唯一缺失者,会当作总线故障。

    3.3 IMU:【等硬件】姿势的裁判

    IMU 不是独立传感器,它住在电机总线上(id 200,imu_to_dxl),和舵机一起被
    robotd 每 tick 读一次(robotd-design.md L144-156:50 Hz 循环、每秒读一次电压/温度)。

    • 在哪里看:robotctl monitor 的 IMU 投影重力项——三个值接近 (0,0,-1) 表示板子水平;
      明显偏移且跟随你的摇晃,说明 IMU 活着且在正确工作;
    • 跌倒判断:robotd 用 IMU 重力投影决定"我倒了没",所以校准/安装方向错误会让机器人
      误以为自己在跌倒——换过 IMU 板或拆过 HAT 后,先看 monitor 的重力方向对不对再启用控制。
    • ToF HAT 上的 IMU 是另一只([head_imu],默认关闭,docs/project/tof-on-demand.md):sudo robotctl configure # [head_imu] enabled = true 打开,改完需重启 robotd
      tofd –imu # 单独会话里读一次 IMU,验证线路(–imu-hz 可调采样率)

    💡 两个 IMU 别搞混:总线上那个(id 200)是控制用的、必须开;ToF HAT 上那个是
    可选的头部 IMU、默认关。调错对象是新手最常见的"IMU 坏了"误报来源。

    3.4 ToF:【等硬件】HAT 上的 8×8 深度矩阵

    ToF 由独立的 tofd 守护进程独占(跟 robotd 独占总线一个道理),它只发不收:
    把 8×8 深度矩阵写到 /run/tofd/tof.sock,谁想用谁读(architecture.md L80-89)。

    robotctl monitor # 按 t 打开 ToF 矩阵,实时看 8×8 深度(越近越亮)
    ls -la /run/tofd/ # 确认 tof.sock 在(tofd 活着)
    sudo systemctl status tofd

    验证顺序:tofd 在跑 → /run/tofd/tof.sock 存在 → monitor 按 t 有数据 → 拿手在
    HAT 上方 5–20 cm 晃动看矩阵响应。前三步通过但没响应,多半是 I2C 地址或接线。

    ToF 很费 CPU 的教训(tof-on-demand.md 实测):depth + head IMU 的采样开销在
    Radxa ZERO 3 上不可忽略。不用头部 IMU 就保持默认关闭,省一档 CPU 给 robotd。

    3.5 摄像头:【等硬件】看世界的眼睛

    摄像头是"硬件门槛最高"的一环,两个前置条件缺一不可(docs/project/media-bringup.md):

  • CSI 摄像头必须配设备树 overlay,且 Radxa ZERO 3W 上必须用带 rk3568- 前缀的
    overlay 名(主板 MIPI 复用了 SoC 上的 CSI 口,用错前缀加载了别的型号的 overlay,
    摄像头就是黑的);
  • mediad 必须启用——没接摄像头时它找不到 /dev/media* 会退出、被 systemd 反复拉起、
    每十几秒烧满一核(部署教程 §7.3 排障表有这条,你的板子已经把 mediad 停了)。接好
    摄像头、配好 overlay 后第一件事就是把它开回来:sudo systemctl enable –now mediad

  • 验证命令(接好摄像头后):

    robotctl health # [media] 段显示摄像头型号和分辨率为正常
    robotctl monitor # 按 c 打开摄像头窗口
    robotctl frame # 存一帧到本机:机器人视角的一张快照(–quality 可调 JPEG 质量)
    # 帧还有一个 HTTP 出口:http://<robot>:8080/frame(mediad 提供,遥控页面用它)

    画面质量:robotctl configure 里 [media] 段的 quality/resolution 控制。
    检测器吃的是流不是快照——mediad 先把帧送进 duck-detect 推理,再把带框的结果
    编码推给远端(这就是 §2.7 的 NPU 链路在流水线里的位置)。

    3.6 音频:【等硬件】一句话带过

    音频 codec(I2S)由 mediad 管,麦克风听声音、喇叭发鸭子叫。验证就一句话:
    robotctl monitor 按 q 旁边的声音栏有电平跳动 = 麦克风活着;放个声音文件喇叭响 = 输出活着。
    音频和视频共用 mediad,所以"声音没了"先查 mediad 日志,再查 codec 驱动。

    3.7 常见硬件故障速查表

    现象先查下一步
    电机总线一个舵机都不认 robotctl health 总线行、journalctl -u robotd 查/dev/ttyS2 是否存在(ls /dev/ttyS2);没它就是内核/设备树问题
    突然全部舵机不认(之前好好的) fuser -v /dev/ttyS2 出现agetty = serial-getty 复活占了总线;mask 掉(systemctl mask serial-getty@ttyS2)
    认到但read failed 刷屏 总线电压、接线松动 robotctl robot reboot-motors;还不行换线
    只认一半舵机 菊花链断点位置 从主控往后逐段排查插口;新插上的舵机重新上链
    换上新舵机不认 新舵机出厂 ID 1 / 57.6 kbps 等robotd 启动"收养"周期(§3.2),看 journalctl -u robotd
    某个关节指令 vs 实际偏差大 monitor 该关节偏差 机械卡死/齿轮滑齿,拆下人工转确认
    monitor 重力方向不对 IMU 安装方向 重装 IMU 板或改标定,别硬调控制参数
    摄像头黑屏 overlay 有没有rk3568- 前缀 sudo journalctl -u mediad -n 30 看具体报错
    ToF 无数据但 tofd 活着 /run/tofd/tof.sock、I2C 地址 i2cdetect -y 4 看设备在不在总线上
    负载莫名飙高 systemctl show mediad -p NRestarts 没接摄像头时 mediad 崩溃循环,先 disable –now,接好再 enable(你已踩过)

    4. 软件开发:改代码,推到板子上

    目标:这一节结束,你能改一行代码,一分钟内在板子上看到它生效,并且知道自己推的
    版本是谁、出问题怎么退回来。整节围绕一个命令:scripts/dev-push.sh——它就是"从改一行
    到跑起来"的循环(docs/robot/dev-push.md 全文)。

    4.1 代码结构:一个 workspace,七个 daemon

    microduck 是单个 Rust workspace(README.md L92-99),顶层结构:

    microduck/
    ├── Cargo.toml # workspace;[workspace.metadata.rknpu] pin 了 NPU runtime 版本(§2.7)
    ├── .cargo/config.toml # 定义 cargo board(交叉编译别名,§4.3 用)
    ├── robotctl/ # CLI:你调试板子的唯一入口(§3 全用它)
    ├── duckctl/ # CLI:找板子/SSH/SCP(蓝牙、Wi-Fi 都没网也能找到板子)
    ├── robotd/ # 50 Hz 控制循环,唯一碰电机的人(改它最危险也最直观)
    ├── mediad/ # 摄像头/音频/WebRTC 远端
    ├── updaterd/ # 更新引擎:签名、原子切换、健康门、回滚(§4.3 的 apply、§4.7 的错误表都靠它)
    ├── configd/ # Wi-Fi、名字、配对
    ├── padd/ # 手柄接收
    ├── btd/ # 手机蓝牙连接
    ├── tofd/ # ToF 8×8 深度矩阵(只发不收)
    ├── duck-detect/ # 检测器推理绑定(§2.7 讲过 dlopen rknn)
    ├── duck-ipc-proto/ # 共享协议:JSON-RPC 2.0 NDJSON 方法定义(全部 daemon 通信契约)
    ├── spaces/ # 示例项目(Python):hello / vision-demo / policy-shop(§4.5)
    ├── scripts/ # dev-push.sh、provision-board.sh、seed-*.sh 等板级脚本
    └── deploy/ # systemd unit、robotd.toml 部署配置

    要改什么先想清楚: 控制逻辑在 robotd/src/(main.rs 是主循环,control.rs 是策略驱动,
    intents.rs 是意图裁决);命令契约在 duck-ipc-proto;CLI 在 robotctl/src/。
    “加个命令给鸭子” = 协议加方法 + robotd 实现 + robotctl 接线,三处都改。

    4.2 一次性准备(只在第一台板子做一次)

    dev-push 的门槛有三件(dev-push.md §Once):

    # ① 板子必须是 dev board(允许开发密钥)
    # 你已经配过了:/etc/robot/updater.toml 里 allow_dev_keys=true 且 team.dev.pub 已放好。
    # 验证:sudo grep -c 'DEV BOARD' /var/lib/robot/provision.log # 输出 1 为是

    # ② 本机放开发签名密钥(私钥,板子只留 .pub)
    mkdir -p ~/.duck-keys && cp /path/to/team.dev.key ~/.duck-keys/team.dev.key
    # 密钥在别处就设:export DUCK_DEV_SECRET_KEY=/path/to/team.dev.key

    # ③ 装交叉编译工具链(二选一)
    cargo install cargo-zigbuild –locked # 方式 A:zigbuild + zig
    # 或不用装任何东西,每次推加 –docker # 方式 B:Docker

    💡 为什么必须签名? 板子的 updaterd 对任何代码一视同仁:验证签名 → 校验哈希 →
    检查兼容 → 过健康门。开发密钥签的包,客户板子会拒收(跟拒收任意 –ref 一样),
    反过来你的 dev 板也拒收任何没签名的东西。没有"临时绕过",这是设计不是麻烦。

    4.3 dev-push 闭环:改一行 → 板子上看到它

    cd e:\\optiDuck\\joyandai\\microduck

    # 方式一:按名字推(板子在蓝牙范围内,会自动找到 IP 并缓存)
    scripts/dev-push.sh –name duck-c51b # 你的板子名,duckctl scan 可查
    # 方式二:直接给地址(推荐日常用,绕开蓝牙)
    scripts/dev-push.sh radxa@192.168.31.30
    # 或设一次环境变量,之后直接跑
    export DUCK_BOARD=radxa@192.168.31.30
    scripts/dev-push.sh

    完整输出长这样(每一步都在干什么,一目了然):

    ==> building 0.5.1-dev.local.1763400000.g7fc1444 for the board (zigbuild) # 交叉编译
    ==> packaging # 打成 release 同款包
    ==> signing with /Users/you/.duck-keys/team.dev.key # 签名
    ==> copying to radxa@192.168.31.30:/home/radxa/duck-sideload # 拷上板
    ==> applying on radxa@192.168.31.30 # robotctl update apply –from
    ==> 0.5.1-dev.local.1763400000.g7fc1444 is live on radxa@192.168.31.30
    ==> checking every daemon is running it
    current -> 0.5.1-dev.local.1763400000.g7fc1444
    [ok] robotd [ok] configd [ok] padd [ok] updaterd [ok] btd [ok] mediad [ok] tofd

    版本号 0.5.1-dev.local.<时间戳>.<commit>:每次推送带时间戳,两次推送同一棵 dirty 树不会撞车。
    板子上确认与回退:

    robotctl version # 板上每个 daemon 实际跑的版本
    sudo robotctl update rollback daemon # 手动回退到上一个 release(目的性回退就这个命令)

    三个常用变体:

    scripts/dev-push.sh –dry-run radxa@192.168.31.30
    # 干跑:构建/签名/拷贝/校验全做,只差最后"切换 current"。板子什么都不动。推前必用。

    scripts/dev-push.sh –docker radxa@192.168.31.30
    # 本机没有 zig 时用 Docker 构建(更慢,但零工具链要求)

    scripts/dev-push.sh –bootstrap radxa@192.168.31.30
    # 只用于板子版本 < 0.5.0 的第一次推(老 updaterd 没有 apply –from 的 API)。
    # 会停 robotd 并放弃健康门——正常情况下绝不用。

    只看代码是否能在板子上编译(不推):

    cargo board –bins # = cargo zigbuild,用板子 target + glibc floor(.cargo/config.toml 定义)

    4.4 第一个真实改动:加一条"被摸"日志

    找一处真实代码,把 debug 级别提到 info,推上去就能在 journalctl 里看到。位置:
    robotd/src/main.rs L2171-2182(抚摸检测事件处理):

    pet_detect::PettingEvent::Start => {
    let calm = !safety.fallen() && controller.as_ref().is_none_or(|c| !c.busy());
    if calm {
    tracing::info!("petting started");
    voice.play("coo", false);
    } else {
    tracing::debug!("petting detected (ignored: busy or down)");
    // 👇 你的第一行改动:改成 info,这样不用开 debug 就能在日志里看到"被摸但忙"的事件
    // tracing::info!("petting detected (ignored: busy or down)");
    }
    }

    改完推上去:

    ssh radxa@192.168.31.30 'journalctl -f -u robotd' # 开一个窗口盯日志(推之前开好)
    scripts/dev-push.sh radxa@192.168.31.30 # 另一台终端推
    # 等板上出现 "[ok] robotd",去摸摸摄像头/ToF(pet_detect 用它们感知)
    # journalctl 里出现 "petting detected (ignored: busy or down)" 即你的代码在跑了

    📖 这条改动选它有三个理由:位置真实(不是我编的)、改动最小(一行)、验证直观
    (摸一下就有日志)。第一次 dev-push 用它建立"我改的代码真的在板子上跑"的信心,比任何
    理论都强。

    4.5 示例项目:spaces/ 里有什么

    spaces/ 是独立的小项目(不是 workspace 的一部分),演示"用现成接口拼功能",
    全部是 Python、用 uv 跑。

    ① policy-shop —— 策略商店(spaces/policy-shop/app.py L13-16 定义了它依赖的四个调用):

    policy.fetch {repo, file} → updaterd 下载并读 manifest (你的板子用 robotctl policy add 等价)
    robot.setSkill {name, path…} → robotd 写入技能条目并重读技能
    robot.policies → 重载成功了吗?change_error 字段说"不"及原因
    robot.do {skill} → 跑它

    本地跑:

    cd spaces/policy-shop
    uv venv && uv pip install -r requirements.txt
    export DUCK_HOST=192.168.31.30 # 板子 IP
    export HF_TOKEN=hf_xxx # 读 HF 公共仓只需要一个真 token
    uv run app.py

    界面上选一个策略 → 点 install → 点 run:上面四个调用按顺序各来一次。
    这是"客户端怎么调鸭子"的最短教材——四个调用就是 §2.5 那些 robotctl policy … 命令
    的底层方法名。

    ② vision-demo —— 视觉流:DUCK_RECEIVER 指向机器人摄像头帧的接收端,
    演示"不用板子也能看它看到什么"。跑法同样是 uv run app.py(README.md L81-98)。

    ③ hello —— 最小演示:最小可跑的"连上机器人 + 说句话"样板,看目录 README 即可。

    💡 spaces 和 daemon 的关系: daemon 是板子上常驻的系统软件(Rust),spaces 是
    你 PC/手机上的应用(Python)。鸭子只提供 JSON-RPC 接口,怎么拼由你的应用决定。
    想加"遥控页面"“自动投喂”“语音对话”,都是在 spaces 这个层次做。

    4.6 调试三板斧(按顺序用)

    # ① 盯 daemon 日志(推代码前先开好)
    ssh radxa@192.168.31.30 'journalctl -f -u robotd -u configd -u btd -u padd'
    ssh radxa@192.168.31.30 'journalctl -f -u updaterd' # 推更新时盯这个:每阶段、健康门、重启
    # panic 会带完整 backtrace——发布版二进制不剥离符号,帧名都在

    # ② 要 debug 级别的日志:drop-in 覆盖 RUST_LOG(默认 info)
    sudo mkdir -p /etc/systemd/system/robotd.service.d
    sudo tee /etc/systemd/system/robotd.service.d/log.conf > /dev/null <<'EOF'
    [Service]
    Environment=RUST_LOG=debug
    EOF

    sudo systemctl daemon-reload && sudo systemctl restart robotd
    # drop-in 在 /etc 下,能挺过所有后续推送。调完删掉 + daemon-reload 即复原。

    # ③ 问板子"你是谁、什么版本、健不健康"
    robotctl health && robotctl version

    4.7 dev-push 常见错误速查(全部来自 dev-push.md §When it does not work)

    报错真实含义处理
    no dev signing key at … 板子拒收未签名产物 放好team.dev.key,或设 DUCK_DEV_SECRET_KEY
    signature did not verify against any of N usable trusted key(s) 板子不是 dev board(不是包坏了) grep -c 'DEV BOARD' /var/lib/robot/provision.log = 0 就走 install-dev.md 补配置
    apply failed (exit 2) + API mismatch 板子版本太老,没有 apply –from 用一次–bootstrap,之后恢复正常
    preflight check failed: SideloadDir … not there 包放在/tmp//var/tmp 下 updaterd 有 PrivateTmp=yes,看不到你的拷贝;换 ~/duck-sideload 等路径
    could not reach <name> over Bluetooth 名字解析不到地址 duckctl scan 看板子在不在;直接给地址 radxa@IP 绕开蓝牙
    ssh 重刷后连不上 板子重新生成了 host key ./scripts/provision-board.sh radxa@IP –forget-host-key
    is live but not everything is running it 交换成功、健康门过了,但某 daemon 还在旧版 robotctl health 看 units 块;`journalctl -u updaterd -b
    libudev 链接错误(重刷后) 缓存的 libudev 拷贝过期 rm -rf ~/.cache/duck-cross/aarch64,下次推送重新抓

    5. 端到端演练:给鸭子加一个"被摸会回应"的行为

    把三块串成一个完整功能。这个演练分两个阶段,第一阶段现在就能做(纯软件),
    第二阶段等硬件。每个阶段结束都有明确的"过关"标准——不过关别进下一阶段。

    目标功能:摸鸭子(ToF 近距离手势 / 摄像头识别),它发出一声"咕"并做个小动作。

    阶段 0:摸底(现在,10 分钟)

    robotctl health # 板子健康吗?哪个 daemon 没跑?
    robotctl version # 板上 daemon 版本?
    ssh radxa@192.168.31.30 'ls -l /opt/robot/policies/current /opt/robot/detector/current'

    过关标准:health 全绿(mediad 无摄像头是预期状态,不算病)、版本 ≥ 0.5.0、
    policies/current → seed-v5、detector/current → seed-duck-v1。

    阶段 1:在 PC 上走通"训练 → 导出 → 彩排"(现在,1 小时)

    cd e:\\optiDuck\\joyandai\\microduck_rl

    # 1) 冒烟测试(拦配置错误)
    uv run train Mjlab-Velocity-Flat-MicroDuck –env.scene.num-envs 64 –agent.max_iterations 5

    # 2) 导出(用官方 checkpoint;–checkpoint <iter> 填你冒烟跑出来的实际轮数)
    uv run scripts/export.py Mjlab-Velocity-Flat-MicroDuck –onnx-file my_walk.onnx –checkpoint 5

    # 3) CPU 彩排:真推理路径测延迟 + 动作序列
    cargo run –release -p duck-control –example policy-rehearsal — my_walk.onnx > rehearsal.json
    cat rehearsal.json | head

    过关标准:导出无警告、rehearsal.json 里 p50 延迟 < 20 ms、动作序列有限值。
    不过关就先停在这——调出问题的是配置,不是鸭子。

    阶段 2:发布 + 上板(现在能做的部分)

    # 1) 发布到 HF(需要 HF 账号;repo 名必须是 <user>/microduck-<name>)
    uv run publish –task Mjlab-Velocity-Flat-MicroDuck –checkpoint 5 \\
    –repo <you>/microduck-my-walk –kind perpetual

    # 2) 板子上装进 walk 槽位试跑(装到非当前正在跑的位置,安全)
    ssh radxa@192.168.31.30
    robotctl policy list # 看 7 个槽位
    sudo robotctl policy load walk <you>/microduck-my-walk
    sudo robotctl policy reset walk # 试完放回原样

    过关标准:policy load 返回成功(manifest 校验通过)、policy list 里 walk 槽显示你的策略名。

    阶段 3:等硬件——接上舵机、摄像头后的收尾

    # 1) 把 mediad 开回来(§3.5:现在它因为没摄像头被停着)
    sudo systemctl enable –now mediad

    # 2) 让鸭子站起来
    robotctl robot init # 上电回 home(放架子上!)
    robotctl robot enable # 交给策略(等于手柄 Start)
    robotctl monitor # 观察:能站住吗?指令 vs 实际偏差?

    # 3) 触发"被摸"(阶段 2 的 pet 行为已就绪,取决于 pet_detect 配置的感知来源)
    # 观察 monitor 的事件行 + journalctl 里我们的 petting 日志(§4.4)

    # 4) 收工
    robotctl robot relax # 切电,收纳

    过关标准:init 后不抽搐、enable 后站住超过 10 秒、被摸触发回应、relax 干净放倒。

    🎯 这套演练的路线图意义: 阶段 1 验证"你的工具链通"、阶段 2 验证"你的模型契约对"、
    阶段 3 验证"你的鸭子真身"。每一步都能独立验收,卡在哪一步就去哪一节找答案
    (阶段 1 → §2.1-2.3,阶段 2 → §2.4-2.5,阶段 3 → §3 + §4)。


    6. 常见问题速查

    Q1:改完代码不想等整包编译,能不能只推一个文件?
    不能。daemon 是静态编译的 Rust 二进制,任何改动都要重编译 → dev-push。但增量编译约一分钟,
    –dry-run 先验、再正式推,两分钟一轮。

    Q2:robotctl policy load walk xxx 说拒绝,为什么?
    policy add/load 在下载前就校验:obs_len 必须 61、action_len 14、model_api ≤ 当前 daemon、
    robot.model 必须 microduck、命令编码必须 constant。看报错对应哪条(§2.5 校验规则)。

    Q3:模型怎么才算"部署好了"?
    三条 current 链(§2.6):/opt/robot/{policies,detector,daemon}/current 指向你要的版本,
    且 robotctl policy list / robotctl duck-detector check 显示已加载。模型不重启 daemon,代码要重启。

    Q4:板子没接摄像头,为什么 CPU 负载会飙高?
    mediad 崩溃循环(部署教程 §7.3 排障表 + §3.7):找不到 /dev/media* 退出,systemd 反复拉起。
    sudo systemctl disable –now mediad,接好摄像头再 enable。

    Q5:dev-push 报"板子不是 dev board"怎么办?
    grep -c 'DEV BOARD' /var/lib/robot/provision.log 输出 0 就是。走 docs/robot/install-dev.md:
    放 team.dev.pub、改 /etc/robot/updater.toml 的 allow_dev_keys=true、重启 updaterd。

    Q6:训练导出的 ONNX 在彩排里延迟没问题,上板后还是慢?
    彩排在 PC 上跑(§2.3),板子 CPU 弱很多。上板后用 robotctl health 看 CPU、用 NPU 跑检测器
    (§2.7),策略推理预算以板子实测为准,PC 数字只是下限参考。

    Q7:怎么把鸭子接回我自己的网络?(IP 变了)
    duckctl scan 蓝牙找名字 → duckctl –name <名字> wifi connect <ssid> –psk <密码>。
    或者板子上 robotctl system set-name 改名后走蓝牙(§4.7 错误表)。

    Q8:更新 daemon 后板子失联了,怎么办?
    健康门 + 自动回滚(§4.3):起不来的 release 会被自动回退。如果连 updaterd 都坏了,
    SSH 上板:sudo robotctl update rollback daemon。再不行就上板刷机(部署教程兜底)。


    7. 参考资料索引(按主题)

    示例项目

    项目位置演示
    hello spaces/hello/ 连上机器人 + 说句话
    vision-demo spaces/vision-demo/ 收摄像头帧
    policy-shop spaces/policy-shop/ 策略商店四个调用

    资料(本教程 §3.0.1/§3.0.2 的参考来源)

    想读什么文件
    舵机选型:XL330 vs HD1910 全对照、波特率、协议 docs/servo.md
    采购清单:桌面调试版 / 整机集成版两阶段 BOM docs/bom.md
    主控选型:ZERO 3W 接口分配、RK3566、NPU 现状 docs/main_controller.md
    结构说明:HD1910 与 XL330 的结构差异、3D 打印优化点 docs/structure.md
    硬件规格书(HD-1910 / HL 系列 / ED330) hardware_spec/servo/
    HD1910 结构 CAD(Bambu Studio 直接打印) cad/openmicroduck.3mf

    8. 更新日志

    版本日期内容
    v1.1 2026-09-16 参考 OpenMicroDuck 文档补充硬件细节:§3.0 修正官方舵机 ID 布局(左腿 20–24 / 右腿 10–14 / 头 30–34 / IMU 200),新增主控接口分配表与总线寄存器参数(1 Mbps /baud_rate=3 / return_delay_time=0);新增 §3.0.1 舵机选型(HD1910 vs XL330 对照、针序警告、换型号必须重训);新增 §3.0.2 两阶段采购清单;§3.2 补充舵机"收养"机制与 fuser -v /dev/ttyS2 排查、修正 reboot-motors 示例 ID;§3.7 故障表新增 3 行;§2.1 补"换执行器 = 重训策略";§2.4 补发布前置(hf auth login / HF_TOKEN、Write 权限 token、Jobs 权限区分);§7 增加 OpenMicroDuck 资料索引
    v1.0 2026-09 首次成稿:§0 路线图、§1 全局观、§2 模型部署全链路、§3 硬件调试、§4 软件开发、§5 端到端演练、§6 FAQ、§7 索引

    本教程与《Radxa_ZERO_3W_零基础部署调试教程.md》配套使用:部署教程回答"怎么把它装起来",
    本教程回答"怎么让它干我想让它干的事"。

    赞(0)
    未经允许不得转载:171主机测评 » Microduck零基础开发教程:模型部署、硬件调试与软件开发
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址