Mixer Reference
From sticks and gyro to µs on the wire: the complete geometry of every mixer PteronautOS can select — wing axes, rudder blends, elevons, trims and clamps.
This page documents the mixer layer of src/lib/Ornithopter/Ornithopter.cpp and the profile system of OrnithopterConfig.h. For the surrounding architecture, start with the
Architecture article
; for pulse timing and trim ranges, see the
Servo Kernel
article.
Servo function tags
Every output of every mixer is a µs value written into _f[], indexed by a ServoFunc tag. Tags are transport — they say what a signal means, never where it is wired:
| Tag | Name | Meaning |
|---|---|---|
0
|
SF_NONE
|
Reserved — no function (first PWM slots stay free for BOOT and I2C) |
1 / 2
|
SF_LEFT_WING / SF_RIGHT_WING
|
Front flapping wings (waveform kernel) |
3
|
SF_RUDDER
|
Head/crest rudder (waveform) or fin rudder (gearbox) — yaw + roll blend |
4
|
SF_MOTOR
|
Brushless ESC throttle (gearbox kernel only) |
5 / 6
|
SF_VTAIL_LEFT / SF_VTAIL_RIGHT
|
V-tail surfaces, elevon-mixed (gearbox kernel) |
7
|
SF_ELEVATOR
|
Traditional separate elevator (gearbox profiles 6/7) |
8 / 9
|
SF_BACK_LEFT_WING / SF_BACK_RIGHT_WING
|
Rear wing pair of a 4-wing airframe (mirror of the front pair, own trim) |
A runtime profile provides the funcMap: an array that maps PWM output index → ServoFunc. The first three slots (PWM 1…3) are always SF_NONE — they belong to the BOOT strap and the I2C bus — so real servo functions begin at PWM index 3 (the firmware comment spells this out: index 3 = CH4, 4 = CH5, 5 = CH6).
The eight mixer profiles
Eight mixer profiles exist. Profiles 0–2 drive the waveform kernel (flapping wings), profiles 3–7 the gearbox kernel (motor + tails):
| # | Profile | Kernel | Servos | funcMap (PWM 3 · 4 · 5) |
|---|---|---|---|---|
0
|
SERVO_2WING
|
waveform | 2 | Left wing · Right wing · — |
1
|
SERVO_2WING_1RUD
|
waveform | 3 | Left wing · Right wing · Rudder |
2
|
SERVO_4WING
|
waveform | 4 (back-right shares the back-left mirror) | Left wing · Right wing · Back-left wing |
3
|
GEARBOX_2VTAIL_1RUD
|
gearbox | 3 | Rudder · V-tail left · V-tail right |
4
|
GEARBOX_1MOT_2VTAIL
|
gearbox | 3 | Motor · V-tail left · V-tail right |
5
|
GEARBOX_1MOT_2VTAIL_1RUD
|
gearbox | 4 (V-tail right shares the left mirror) | Rudder · Motor · V-tail left |
6
|
GEARBOX_1ELE_1RUD
|
gearbox | 2 | Rudder · Elevator · — |
7
|
GEARBOX_1MOT_1ELE_1RUD
|
gearbox | 3 | Rudder · Motor · Elevator |
Selection: build-time flag MIXER_PROFILE (legacy ORNITHOPTER_GEARBOX=1 → profile 5), then runtime-overridable through the WebUI (setOrnithopterProfile). The kernel test is simply activeProfile ≥ GEARBOX_2VTAIL_1RUD.
Wing axis mixing (waveform kernel)
All channel values enter the mixers normalized: _crsfToNorm maps the raw 172…1811 window linearly onto −1…+1. Steering commands are then scaled by the per-profile authority sliders (aileronScale, elevatorScale, 0–100%) times ±60° (ORNI_STEER_MAX_DEG).
Mirror geometry. Wing servos are mirror-mounted, which swaps the meaning of common-mode and differential terms. The code therefore treats aileron (roll) as common — it adds to both wings, which becomes a differential physical deflection — and elevator (pitch) as differential in code, which becomes a common physical deflection. The angle equations, with ORNI_NEUTRAL_ANGLE_DEG = 100° and the 2× multiplier applied to deviation terms only:
angleLeft = NEUTRAL + (aileron + elevator + flapCentre − degL) · 2
angleRight = NEUTRAL + (aileron − elevator − flapCentre + degR) · 2
flapping: degL/R = amplitudeL/R · shapedWave(rawPhase, ferocityL/R, …)
glide: degL = degR = 0 → static glide offset takes over
degL/degR are the instantaneous stroke deflections of the left and right wing, each the product of its own amplitude and a ferocity-shaped wave. Elevator-up strengthens the downstroke; elevator-down the upstroke (asymmetric ferocity). Yaw splits the wings in two complementary ways:
- Ferocity differential (
rudderFerocityRange): left gets+ferocity, right−ferocity— the two wings dwell differently but keep a shared reversal phase (limiarShared), so turns are pure roll torque, never desynchronisation. - Amplitude differential (
rudderAmplitudeDifferential): left amplitude ×(1+Δ), right ×(1−Δ) — symmetric divergence around the throttle-set stroke.
Angles are clamped 0…180° and converted to pulses with the configurable envelope servoMinUs…servoMaxUs (defaults 988…2012): µs = min + angle·(max−min)/180, plus the per-function trim. The glide/flap centre offsets (glideAngleDeg, flappingAngleDeg, ±15°) enter as static terms on their respective branch — so glide and flap centres can be tuned independently per flight profile.
Rudder mixing (both kernels)
The rudder mixer is shared verbatim by both kernels — one equation, two airframes:
mix = rudderNorm · rudderYawWeight + aileronNorm · rudderRollWeight
mix = clamp(mix, −1 … +1)
µs = 1500 + mix · 500 (rudder centre ± 500 µs)
+ gyroRudderCorrection (Zephyrus, when enabled)
+ servoTrimUs[SF_RUDDER]
Defaults: rudderYawWeight = 65%, rudderRollWeight = 35% (runtime-tunable in the WebUI, stored per backup). Roll coupling on the rudder is what lets a wing-heavy pterosaur hold coordinated turns without yaw-string gymnastics; reduce the roll weight if the head rudder feels too aggressive in roll.
Gearbox tail & elevon mixing
The gearbox kernel runs statelessly every tick. Its three sub-mixers:
- Motor —
SF_MOTOR: when armed, throttle 0…1 maps to 1000…2000 µs; disarmed or link-down forces the minimum (motor stopped). - Rudder — identical equation to the section above.
- Tails — two families. Classic profiles (6/7) drive a separate elevator:
µs = 1500 + elevatorNorm·500 + pitchPID + trim. V-tail profiles (3/4/5) elevon-mix both channels onto the two tail servos:
VtailLeft = 1500 + (aileron + elevator)·scale + rollPID + pitchPID + trimL
VtailRight = 1500 + (aileron − elevator)·scale − rollPID + pitchPID + trimR
scale = 500 µs; both aileron/elevator sums clamped to −1…+1 first
On the gearbox kernel, Zephyrus injects both roll and pitch PID corrections into the tail servos (roll anti-phase, pitch common-mode), each hard-clamped to ±250 µs (ZEPHYR_GEARBOX_CLAMP_US). A gyro glitch can therefore waggle, but never stall, a tail servo.
Zephyrus injection, trims & failsafe
Three different injection points exist depending on kernel and axis:
| Axis | Waveform kernel | Gearbox kernel |
|---|---|---|
| Rudder / yaw | gyroRudderCorrection added to the crest rudder µs |
gyroRudderCorrection added to the fin rudder µs |
| Roll (aileron) | — (waveform wings carry no roll PID) | gyroAileronCorrection, anti-phase across V-tail, clamped ±250 µs |
| Pitch (elevator) | — (injected as wave modulation in the Zephyrus layer) | gyroElevatorCorrection, common-mode, clamped ±250 µs |
Trims & limits. Every function has a signed per-servo trim (servoTrimUs[tag], default 0, ±300 µs) added last, and every value passes _clampServo — an absolute 500…2500 µs safety clamp that outranks everything. On failsafe or link-down, all functions return to 1500 µs and a gearbox motor to minimum, so the model always glides to neutral.
Ready for the practical layer? The Waveform Tuning tutorial shows how these numbers feel from the sticks, and the Architecture article shows where each mixer sits in the pipeline.