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:
- 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. -
Outputs are tagged, not pinned. The mixers write into
_f[ServoFunc]. Which physical PWM output a tag drives is decided later by the profile'sfuncMap— see the Mixer Reference .Re-pinning a model never touches mixer math. - 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.
- 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:
- Flap gate with hysteresis. Throttle must cross the flap threshold (default raw ≈303, ≈1.08 ms PWM-equivalent) to enter flapping; it leaves only below threshold − 50 hysteresis, so marginal throttle never chatters the wings.
- Frequency. A FlappingOscillator advances with real elapsed time (µs clock, dt capped at 100 ms). The cadence target blends the CH6 flap-frequency channel and throttle via the profile's
throttleFrequencyMix, within 0.5 Hz…flapBaseFreq/10. - Amplitude. Throttle percentage of the servo-speed ceiling
ampMax = degPerSec / (2·f)(withdegPerSec = 60°/servoSpeed_ms), hard-capped at 55° pre-multiplier. Amplitude therefore collapses naturally at high frequency — the wing never demands more than the servo can deliver. - Ferocity. Down-stroke (power) and up-stroke (recovery) ferocities are shaped per stroke, modulated asymmetrically by elevator (pull-up hardens the downstroke, push-over hardens the upstroke) and by throttle coupling, then bounded 0…8 before the shape function.
- Yaw differentials. Rudder adds L/R-opposed ferocity (
rudderFerocityRange) and L/R-opposed amplitude (rudderAmplitudeDifferential) while the stroke-reversal phase stays shared between the wings — the turn authority comes from asymmetric dwell, not from desynchronised wings. - Geometry & output. The shaped −1…+1 wave is scaled into degrees, mixed against aileron/elevator steering (±60° authority × scale), offset by the glide/flap-centre trim, clamped 0…180°, then converted to µs through the configurable
servoMinUs…servoMaxUsenvelope (988…2012 defaults) with per-function trim. - Glide branch. Below the flap threshold the oscillator decays, and the wings hold a static glide angle — an ideal bench-and-launch state.
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:
- maps throttle (armed) to a 1000…2000 µs ESC command (
SF_MOTOR), idle when disarmed; - mixes the tail: V-tail profiles use an elevon blend (aileron+elevator / aileron−elevator), classic profiles use a separate elevator surface;
- lets Zephyrus (when enabled) inject roll and pitch PID corrections into the tail servos, clamped to ±250 µs (
ZEPHYR_GEARBOX_CLAMP_US) so a sensor glitch can never stall a servo; - keeps the same rudder mixer and µs/trim/clamp envelope as the waveform kernel.
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.