ArduPilot Neural Mixer: a reinforcement-learning walking policy running inside ArduRover (bipeds, quadrupeds, Pixhawk 6C)

ArduPilot Neural Mixer: a reinforcement-learning walking policy running inside ArduRover (bipeds, quadrupeds, Pixhawk 6C)

ArduPilot Neural Mixer

Hi all. Over the last months I have been working on something a bit unusual for ArduPilot: legs. Not a companion computer driving servos through MAVLink, but a neural network that walks a robot from inside the ArduRover firmware, as a normal scheduler task, reading ArduPilot’s own IMU, RC and flight modes and writing ArduPilot’s own servo outputs.

The project is open and on GitHub: virtualrobotix/Ardupilot-Neural-Mixer (tools, simulator, robot catalog, docs), with the firmware in the microduck-ppo branch of virtualrobotix/ardupilot (library AP_NNMixer, two Pixhawk 6C board targets). This post is a tour of what works today, what I learned along the way, and where I would love help from this community.

TL;DR

  • AP_NNMixer is a new ArduRover library. It runs a PPO locomotion policy (an MLP, 512-256-128, ELU) at 50 Hz, in plain C, with int8 weights loaded from the microSD card. No companion computer, no ROS.
  • MicroDuck (the Pollen Robotics open-source biped, 14 servos) stands, walks and turns in SITL with a MuJoCo plant, and the same network runs on a real Pixhawk 6C Mini: 4.9 ms per forward pass, 33 % CPU, 15 s standing hardware-in-the-loop.
  • One firmware, 16 robots in the catalog (bipeds, quadrupeds, a wheeled biped, a hexapod). Topology comes from robot.bin at boot; policies are .nnm files, two int8 slots in RAM, hot switch with a 0.5 s blend.
  • The MuJoCo training environment is the ArduPilot deployment contract (same gravity filter, same action clipping, same PWM quantization) and the actor trains on the int8 grid from the first iteration, so the exported file is the trained network. Firmware vs training parity: 2.7e-7.
  • Beyond walking: gestures by clip imitation (wave, dance), pose teleoperation over MAVLink from a webcam, and a humanoid that gets up on its own after a fall, with a small state machine in the firmware.
  • Missing for a real robot: a servo-bus backend (Dynamixel / Feetech) for joint feedback. That is the next step and the part where I need the most input.

Why inside the autopilot

Legged robots in research run their policy on a Jetson or a laptop. For small, cheap robots (a 350 g humanoid, a 3D-printed quadruped) that is heavier and more expensive than the robot itself. An STM32H7 autopilot already has the IMU, the RC link, the servo outputs, the SD card, the parameters, the logging and a ground station ecosystem. The only missing piece is the controller, and a 200 KB int8 MLP fits.

 GCS / MAVProxy ──MAVLink──▶ ArduRover ─ RC, modes ─▶ AP_NNMixer (PPO, 50 Hz) ─▶ SRV_Channels ─▶ joints
                         AP_InertialSensor ────▶ gravity filter + obs      ◀── joint feedback
                         microSD /APM/nnm/<robot>/robot.bin + policies/*.nnm  → 2 int8 slots in RAM

The observation is built in the trunk FLU frame: gyro, gravity, joint positions and velocities, previous action, commanded twist, plus a few extra channels (gesture clock, pose command). Gravity comes from a small IMU-only complementary filter at loop rate, as in training. The action is a joint offset from the standing pose, clipped, turned into PWM 1500 + q / 0.003 µs. Flight modes map naturally: MANUAL = sticks are the twist, HOLD = twist zero, GUIDED / AUTO / RTL / SMART_RTL = Rover’s desired speed and turn rate saturated to NNM_VX_MAX / NNM_WZ_MAX. Disarmed = servos idle.

MicroDuck: SITL, then a real Pixhawk 6C

The first robot was MicroDuck, using Pollen’s published PPO policy. The MuJoCo plant talks to SITL over the SIM_JSON protocol, and a scripted MAVProxy session arms, stands, walks forward, backward, sideways, turns 90°, goes to HOLD and disarms:

Then the same network on hardware. Pixhawk6C-NNMixer is a dedicated ArduRover target for the STM32H743; MuJoCo simulates only the robot body and exchanges state and SERVO_OUTPUT_RAW over MAVLink USB, while the board’s real IMU is fed in as a disturbance:

Pixhawk 6C Mini, float32 MLP in flash
Forward pass at 50 Hz p50 4.9 ms, p99 5.0 ms
Autopilot CPU load 32.8 % (5.4 % with NNMixer disabled)
Flash 1,947,204 B used, 18.9 KB free (Pixhawk6C-NNMixerSD, SD policies only: 811 KB free)
HIL battery (arm, stand, forward, lateral, turn, HOLD, disarm, parity) 8/8 PASS in SITL
Firmware action vs training network max difference 2.7e-7

The lesson that cost a morning. The first HIL run fell after one second although the observations and the network were bit-exact. The cause was ArduRover’s default INS_GYRO_FILTER of 4 Hz, perfectly sensible for a wheeled vehicle. The gyro reached the policy tens of milliseconds late and at half amplitude. With INS_GYRO_FILTER 0 the duck stands and walks. Any locomotion policy fed from ArduPilot’s gyro will need the same rule.

Training that deploys as is

A policy trained in a generic simulator and copied onto the autopilot sees different numbers than it was trained on. The MuJoCo environment in the repo (tools/robots/nnm_env.py) replicates the firmware path instead: physics at the 200 Hz loop rate, gravity through the same complementary filter, the action clipped and PWM-quantized exactly like AP_NNMixer, the observation laid out from the robot profile, a random 0–1 tick delay on the gyro.

Why it matters: the Microban policy published by its authors, converted to int8 and run in this environment, stands with the simulator’s exact gravity and falls in under a second with the gravity estimated by the firmware filter. It had never seen the filter. The only way to know before flying is to train and evaluate on the contract.

The second piece is int8 from the first iteration (quantization-aware training, nnm_qat.py). The firmware keeps two policies in RAM as int8 (~200 KB each). Quantizing after training introduced errors up to 0.13 rad on Microban actions. With QAT the actor’s forward pass already uses the rounded per-row int8 weights, the gradient passes straight through, and the export copies the integers. tests/test_nnm_pipeline.py checks training network → .nnm file → firmware C forward give the same action.

.venv/bin/python tools/robots/fetch_upstream.py --robot rex             # original repo + MuJoCo scene
.venv/bin/python tools/robots/train_velocity.py --robot rex --name walk # int8 PPO on the deployment contract
.venv/bin/python tools/robots/train_velocity.py --robot rex --eval robots/rex/policies/walk.nnm
.venv/bin/python tools/robots/pack_robot_bin.py --robot rex             # robot.bin for the microSD

Full write-up: docs/robots/training.md (Italian).

Beyond walking: gestures, teleoperation, getting up

Microban (an 18-servo, 350 g open humanoid) became the test bench for everything a biped does besides walking. The network has no memory, so a periodic gesture needs a clock: two extra observation channels carry the sine and cosine of a phase, generated by the firmware from NNM_CLOCK_HZ. Training then imitates a reference clip (the DeepMimic recipe, as in Xue Bin Peng’s MimicKit): the reward is how close the clip’s joints are to the pose of this phase. A wave converged in 300 iterations, a full-body dance in 400.

Microban dance

Pose teleoperation over MAVLink. A ground station runs MediaPipe on a webcam, retargets the person’s arms and knee bend to eight numbers and sends them in a DEBUG_FLOAT_ARRAY named NNM_POSE. The firmware low-passes them, applies a watchdog (NNM_POSE_WD) and copies them into the observation; pose_cmd.nnm executes the pose while keeping balance. All the intelligence stays on the GCS.

Microban pose teleop over MAVLink

Getting up. A biped that cannot stand up after a fall needs a human within arm’s reach. getup.nnm is a one-shot policy: the firmware detects the fall from the trunk tilt, switches to it, runs a 14.2 s clock once (NNM_GETUP_S) and hands control back to the previous policy when the robot is upright. It was trained in two stages: a discovery policy finds a way to stand (simulation only, with a decaying helping force on the trunk), the best rollout is recorded, slowed down and blended into the standing pose, and a second policy imitates that clip in closed loop. Int8, from prone 100 % of the time (4–11 s), from supine 100 % (1.5–3 s), and the walking policy takes over 100 % of the time.

Microban get-up storyboard

That run is one continuous 44 s sequence with the firmware state machine simulated: get up from prone, cross-fade to walk_md.nnm, walk, get pushed, fall, automatic get-up (side chosen from gravity), walk and turn again, stop, then pose_cmd.nnm waving. Details and the dead ends (joint-only imitation never stood up; a 10° upright gate taught the robot to lock its knees) are in gesture_imitation.md.

A quadruped: Freenove Robot Dog

Same firmware, same trainer, four legs. The Freenove Robot Dog (12 PWM servos, so it can plug straight into the autopilot outputs) went through 23 environment versions; walk_v23.nnm trots at about three touchdowns per leg per second with 150 ms flight phases, trunk oscillation 1.2°, 100 % survival.

Freenove trot Freenove push recovery
Full sequence: 2.67 m, straight within ±2°, 180° turn in 4.6 s Four 0.35 m/s impulses (7× the training ones): trunk back within 3° in 0.25 s

The ramp is 7°, up and down with yaw correction. At 10° it stalls halfway: it never saw a slope in training. Honest limits are in every robot page next to the videos.

The robot catalog

Topology is a parameter (NNM_ROBOT), policies are files. All robots share the MicroDuck network; only input and output sizes change. tools/robots/catalog.py is the single source and generates robot.bin, ppo.yaml and the robot pages.

NNM_ROBOT Robot Type Joints Policies today
0 MicroDuck (Pollen Robotics) biped 14 walk — SITL + Pixhawk 6C HIL
1 Microban humanoid 18 walk, walk_md, wave_right, dance_arms, dance, pose_cmd, getup
10 Freenove Robot Dog quadruped, PWM 12 walk_v7 … walk_v23
12 Booster T1 humanoid 12 upstream policy converted to .nnm, verified on the contract
2–9, 11, 13–15 Zeroth-01, Bimo, Legolas, Upkie, Rex/SpotMicro, Yertle, AlbertPro, Open Duck Mini v2, Petoi Bittle, flybody, Yanshee, TienKung bipeds, quadrupeds, wheeled, hexapod 6–20 profiles and PPO configs; scenes ready for some, policies to train

Each page links the original repository, model file, CAD and BOM, lists joints, standing pose, servo outputs and every checkpoint video against the environment version and iteration.

Firmware in practice

Tested board: Pixhawk 6C Mini. Any H7 with a microSD and ~500 KB of free heap should work with a one-line hwdef (include ../<Board>/hwdef.dat + define AP_NNMIXER_ENABLED 1).

/APM/nnm/microduck/robot.bin
/APM/nnm/microduck/policies/walk.nnm
Parameter Value Why
NNM_ENABLE 1 run the task
NNM_ROBOT catalog index topology, read at boot
NNM_POLICY 0 policy file index, alphabetical; changes at runtime (deferred while walking, done in HOLD)
NNM_CLOCK_HZ / NNM_CLOCK_AUTO 0 gesture or gait clock in the two channels after the twist
NNM_GETUP_* fall detection and automatic get-up policy
SERVO1..n_FUNCTION 94.. joint outputs (Scripting1..16)
INS_GYRO_FILTER 0 the policy needs the raw gyro
SCHED_LOOP_RATE 200 gravity filter at 200 Hz, as in training

At boot the GCS shows NNMixer: robot microduck joints=14 obs=61 and NNMixer: loaded .../walk.nnm into slot 0. Telemetry: PPO_FAIL (0 ok, 1 disarmed, 2 no joint feedback, 3 stale), PPO_PGZ ≈ −1 when upright, PPO_SLOT. A policy for another robot is rejected by id and sizes.

What is missing, and where I would like your input

  1. Servo-bus backend. Today joint feedback comes from the MuJoCo bridge. Real MicroDuck and Microban use Feetech / Dynamixel serial servos, so the firmware needs a bus backend that writes targets and reads positions at 50 Hz for 14–18 servos. PWM robots (Freenove, Rex, Yertle, Bittle) need a per-joint calibration (direction, center, rad/µs) in robot.bin. If anyone has done half-duplex Feetech/Dynamixel on ChibiOS UARTs, I would love to compare notes.
  2. More boards. Only the 6C Mini has been exercised. Flash is tight with the float32 fallback MLP in flash; the SD-only target leaves 811 KB.
  3. Rover integration. NNMixer currently rides on ArduRover’s modes and SRV_Channels. Whether a legged vehicle deserves its own frame class, how GUIDED velocity should map to gait commands, and how this could ever be upstreamable are open questions I would rather discuss here than decide alone.
  4. Your robot. Adding a robot is one entry in catalog.py plus a MuJoCo scene. If you have an open biped or quadruped, the pipeline will produce robot.bin, a training config and a doc page for it.

Links

Quickstart in SITL, no hardware needed:

git clone --recurse-submodules https://github.com/virtualrobotix/Ardupilot-Neural-Mixer.git ~/nnmixer
cd ~/nnmixer && uv venv --python 3.12 && uv pip install -r requirements.txt
cd ardupilot && ../.venv/bin/python ./waf configure --board sitl && ../.venv/bin/python ./waf rover && cd ..
.venv/bin/python scripts/demo_mavproxy.py

Comments, criticism and pull requests welcome.

Roberto Navoni — DelphyAI LAB

It’s really great to see this, well done!

Thank you Randy , hope to found support in community for testing different kind of robot frame and help the ardupilot user to learn more about the incredible world of AI apply to robot control :slight_smile:
Suggestion are welcome :slight_smile:
Now i mantain a separate repo but could be great to discuss how could be integrate in master especially the new lib and documents for use it .
I apply the lib to ardurover because is more simple code and small so there is enought space for the implementation . Hope in future to see H7 with more flash and ram for this application . I see also version of micro controller with nlp module inside.