Architecture: Kernels & Data Flow

How a CRSF datastream becomes wingbeat and tail: module boundaries, the two mixer kernels, and the state machine that keeps the pterosaur alive when the link dies.

Signal pipeline

PteronautOS is an ExpressLRS 4.x fork that replaced the "PWM passthrough" tail of the receiver with a dedicated flight-control layer. The data path is deliberately short:

CRSF radio link
  │  2.4 GHz SX1280
  ▼
rx_main / CRSF decoder        → ChannelData[]  (raw 172…1811 µs-window values)
  ▼
Ornithopter::update()          → _readChannels() → voice* fields
  ▼  kernel select: activeProfile ≥ GEARBOX_2VTAIL_1RUD ?
_computeServoMixer()   OR   _computeGearboxMixer()
  ▼
_f[]  (indexed by ServoFunc tag, µs)
  ▼
PWM output stage               → funcMap channels → wing / rudder / motor servos

Four structural ideas keep the beast stable and the code small:

  1. Channels are normalized early. Every raw value in the 172–1811 CRSF window is mapped to −1…+1 (_crsfToNorm) or to a percentage once, and only that unit flows through the mixers — no repeated magic constants.
  2. Outputs are tagged, not pinned. The mixers write into _f[ServoFunc]. Which physical PWM output a tag drives is decided later by the profile's funcMap — see the Mixer Reference .Re-pinning a model never touches mixer math.
  3. The kernel is a compile-time family, a runtime choice. Both the waveform flapper and the gearbox controller live in the binary; the active profile picks the kernel every update tick.
  4. Failsafe is not an event, it is a state. Link-down, bench mode and stick-override are explicit gates in update(), so servos can never run away while the radio is silent.

Two kernels: waveform & gearbox

A kernel is the block that turns normalized channels into servo-function µs. PteronautOS has two families, discriminated by the mixer-profile number:

Family Profiles Actuators Model archetype
Waveform (servo) kernel 0 SERVO_2WING · 1 SERVO_2WING_1RUD · 2 SERVO_4WING Flapping wing servos (2–4), crest rudder Direct-drive servo ornithopter — the classic PteronautOS
Gearbox kernel 3 GEARBOX_2VTAIL_1RUD · 4 GEARBOX_1MOT_2VTAIL · 5 GEARBOX_1MOT_2VTAIL_1RUD · 6 GEARBOX_1ELE_1RUD · 7 GEARBOX_1MOT_1ELE_1RUD Motor ESC, V-tail or elevator + rudder Geared / motorised flapper with conventional tail surfaces

Selection happens in update() with the single test PROFILE_IS_GEARBOX (activeProfile ≥ 3). The profile itself is a runtime value: it starts from the MIXER_PROFILE build flag (legacy ORNITHOPTER_GEARBOX=1 still maps to profile 5) but can be switched live from the WebUI via setOrnithopterProfile() — the firmware re-targets a new airframe without a reflash. There are eight profiles in total; their exact funcMap layouts are tabulated in the Mixer Reference.

Waveform kernel (servo flapper)

The waveform kernel is what most people mean by "PteronautOS": two wing servos run a phase-continuous oscillation whose shape is sculpted every cycle. Its anatomy:

Gearbox kernel (motor + tails)

The gearbox kernel trades pure wingbeat for a motorised drivetrain plus conventional tail surfaces. Because a motor turns continuously, this kernel is stateless per tick — no oscillator — and instead:

One ornithopter rig can even carry both personalities: flapping launch with the waveform kernel, cruise on the gearbox — the runtime profile switch is that single line in update().

Link, bench & failsafe states

update() is called on a fixed ~10 ms cadence from the WiFi/servo loop and gates on three flags before any mixer runs:

State Condition Behaviour
Normal flight linkUp + armed channel live Full kernel output follows the sticks
Bench / panel mode benchMode (WebUI panel, no RC link) Neutral sticks are injected, throttle parked below the flap threshold — glide + trim edits are visible live
Stick override stickOverride (virtual channels) Firmware-injected channel values replace CRSF entirely (servo sweeps, tests)
Failsafe link lost (onLinkDown / enterFailsafe) All functions centre to 1500 µs, oscillator reset, motor → min; CRSF packet loss also centres at RX level

The centre-on-loss behaviour is duplicated in two layers on purpose: the CRSF stack failsafes the channel data, and the Ornithopter layer failsafes the geometry (including the oscillation state). A dead radio therefore leaves a gliding, controllable airframe — never a flailing one.

Source map

Module Path Responsibility
Ornithopter layer src/lib/Ornithopter/ Kernel select, mixer math, waveform oscillator, profiles, trim
Flight profiles & constants src/lib/Ornithopter/OrnithopterConfig.h MixerProfile enum, ServoFunc tags, funcMap, all numeric defaults
Oscillator / shape src/lib/Ornithopter/OrnithopterWaveform.h Phase integration, ferocity shaping, reversal logic
Zephyrus (optional) src/lib/Zephyrus/ MPU6050 + Mahony AHRS, dual PID, crest/tail correction
PWM output stage src/lib/ServoOutput/devServoOutput.cpp Writes µs synchronised to the CRSF tick
WebUI config src/lib/WIFI/devWIFI.cpp Runtime profile + tuning persistence (/pteronautos)

The next layer down — exactly which waveform shape the numbers produce, and how every profile wires functions to PWM outputs — lives in the Mixer Reference article, and the servo-timing envelope is documented in the Servo Kernel article.