Zéphyros : stabilisation et Ondas

La conception idéale du pont gyroscope–battement : modulation respiratoire Ondas, feed-forward synchrone au coup, résonance à verrouillage de phase et modulation aéroélastique du gain PID — et une carte honnête de ce que le firmware actuel câble réellement.

Filiation : OrniFlight et un Ondas simplifié

Ondas n'est pas une invention de PteronautOS. La famille de modulation d'onde — contrôle de cadence respiration-pause, férocité de forme de course, dwell asymétrique et le couplage aéroélastique de la charge alaire à l'autorité PID — vit et évolue dans le OrniFlight projet, dont le wiki et le simulateur portent les algorithmes avancés (y compris le servo-efficiency table ).

PteronautOS embarque une implémentation partiellement simplifiée : la même géométrie mathématique de respiration, mise à l'échelle d'un ESP8285 avec un MPU6050 et jusqu'à neuf servos. Là où OrniFlight simule l'aéroélasticité continue et explore des familles d'enveloppes entières hors ligne, PteronautOS doit décider en ticks de 4 ms, sur un cœur à 80 MHz, la pile radio ExpressLRS partageant le même silicium. Chaque gain de cet article existe dans les deux projets ; les constantes de firmware ci-dessous sont le sous-ensemble embarqué.

Chaîne du signal — de Zéphyros à Ornithopter

Le pont entre le gyro et le battement d'aile est volontairement court — quatre fichiers, aucune allocation dynamique, aucune file :

MPU6050 raw (I2C, 1 kHz register reads)
  ▼
Zephyrus::update()            Mahony AHRS → roll°, pitch°, yawRate °/s
  ▼                            dual PID: roll→0°, pitch→0°, yawRate→0°/s
4 raw pitch terms exposed      pitchPTerm · pitchITerm · pitchDTerm · pitchErrorRate
  ▼
ZephyrusFilter.h (250 Hz)      copies terms onto ornithopter.gyroPitch*
  ▼
Ornithopter::_computeServoMixer() / _computeGearboxMixer()
  ▼   Ondas P → phase · PD → dwell · I → asymmetry · SSFF → next stroke
_f[]  →  funcMap → wing / crest-rudder / tail servos (µs)

Les quatre termes bruts sont écrits dans Zephyrus::update() immédiatement après que le PID de tangage a calculé sa correction :

pitchPTerm     = ZEPHYR_PID_PITCH_KP * pitchErr;                 // raw proportional
pitchITerm     = _pidPitch.integrator;                           // accumulated error
pitchDTerm     = ZEPHYR_PID_PITCH_KD * _pidPitch.lastDerivative; // low-pass filtered D
pitchErrorRate = _pidPitch.lastDerivative;                       // °/s, for SSFF

Les quatre sont protégés par un piège NaN (si une entrée est NaN, les quatre sont mis à zéro) pour qu'une trame I²C corrompue n'atteigne jamais l'arithmétique du mixeur. Sept champs d'Ornithopter reçoivent les données du pont :

Champ Source Zephyrus Consommé par
gyroPitchPTerm pitchPTerm Ondas P → avance de phase (canal de cadence)
gyroPitchITerm pitchITerm Ondas I → asymétrie de course (canal de balance)
gyroPitchDTerm pitchDTerm Ondas D → rapport de dwell (canal de férocité)
gyroPitchErrorRate pitchErrorRate Accumulation de demi-course SSFF
gyroRudderCorrection rudderCorrection Décalage µs du gouvernail de crête (noyaux servo & boîte)
gyroAileronCorrection rollCorrection × gearbox gain Boîte : PID de roulis → surfaces d'aileron d'empennage V
gyroElevatorCorrection pitchCorrection × gain × aeroGainScale Boîte : PID de tangage → surfaces élevon / profondeur

Modulation Ondas — respiration à trois canaux

Ondas (portugais « ondes ») est le système de respiration à trois canaux qui mappe les termes bruts P, I et D du PID de tangage sur trois dimensions orthogonales de la forme d'onde de battement. C'est une boucle proprioceptive : la perception du tangage par le gyro façonne directement le mouvement de l'aile, et le mouvement de l'aile change le tangage ressenti par le gyro.

Cadence — P → avance de phase

Une erreur de tangage positive (nez vers le haut) avance la phase de l'oscillateur, raccourcissant la demi-course en cours ; une erreur négative la retarde. La loi idéale met à l'échelle le ressort de l'oscillateur :

_osc.kGainMod = 1.0f + gyroPitchPTerm * aeroGainScale * cadenceGain * 0.00005f;
// clamped to [0.5, 2.0] — phase can at most double or halve

L'advance() de l'oscillateur le consomme comme error = kGain · kGainMod · cadenceTarget − kDamp · cadence. Plage de réglage 0–100, défaut 20.

Férocité — mélange PD dans le dwell

Les termes P et D sont mélangés en un unique signal de férocité qui durcit ou adoucit le dwell de la forme d'onde :

ferocitySignal = (gyroPitchPTerm * ferocityPGain * 0.00015f
                 + gyroPitchDTerm * ferocityDGain * 0.0003f) * aeroGainScale;
// clamped to [-0.5, 0.5], added to both stroke and return ferocity

Avec ferocityPGain = 0 (le défaut), le comportement est D-seul — des changements rapides de tangage produisent des courses plus nettes tandis que le vol stable reste sinusoïdal.

Balance — I en asymétrie

Le terme intégral décale le centre de course : un biais persistant de nez vers le haut renforce la course descendante, un biais persistant de nez vers le bas renforce la montante.

float iBias = gyroPitchITerm * aeroGainScale * balanceGain * 0.0001f;
// clamped to [-3.0, 3.0]; added to strokeFer, subtracted from returnFer

Le balance est le canal le plus déstabilisant lorsqu'il est élevé — le guide de réglage idéal le garde bas (5–15) et l'ajoute en dernier. L'application de férocité, combinant commande pilote, Ondas et biais SSFF, garde l'autorité du pilote sur toute la fenêtre [1.0, 8.0] :

strokeFer = pilot(stroke) + ferocitySignal + iBias + _ssffFerocityUpBias
returnFer = pilot(return) + ferocitySignal − iBias + _ssffFerocityDownBias

Mise en forme de l'onde et mélange PD

La forme d'onde elle-même suit la famille de courses OrniFlight : chaque moitié utilise sa propre férocité, un seuil d'inversion partagé maintient les ailes gauche/droite en phase, et un mélange de forme par profil transmute continuellement une course plate de dwell/plateau en un chemin pyramidal arrondi. L'explorateur interactif ci-dessous rend la géométrie commandée pour toute paire de férocité bas/haut — la même courbe que le mixeur demande à chaque tick.

La férocité contrôle à la fois le caractère et le timing d'une demi-course. En interne, le réglage 0–100 de la WebUI correspond à 0–8. Une férocité de descente plus élevée raccourcit la part du cycle de la descente ; une férocité de retour plus élevée raccourcit la part du retour. Le temps restant est transféré à la moitié opposée — il n'y a pas de pause à zéro.

Le réglage Forme de Férocité par profil sélectionne le caractère à haute férocité. À 0 %, il favorise une forme finie en plateau/carrée. À 100 %, il favorise un trajet pyramidal arrondi. Chaque moitié utilise toujours sa propre férocité, si bien qu'une moitié à faible férocité reste allongée et sinusoïdale, tandis qu'une moitié à forte férocité s'approche d'un parcours diagonal direct.

L'inclinaison de descente et de montée (±100) décale le centre de chaque demi-battement le long de son propre axe début–fin — le même miroir par demi-battement que la férocité. Une valeur positive charge le battement vers l'avant : l'aile atteint la vitesse maximale plus tôt, augmentant la poussée. Une valeur négative le décale vers la fin : le battement dépense son mouvement en fin de course, réduisant la poussée. La déformation est monotone et préserve les extrémités, donc exactement un mélange asymétrique carré↔triangle, jamais un saut de position.

Le couplage de forme de poussée du throttle fusionne maintien et centre avant-arrière en un curseur. Pleins gaz carre le battement ET avance les deux demi-battements — la vitesse de pointe arrive plus tôt tant à la descente qu'à la montée ; au ralenti les deux restent neutres. L'unique scalaire d'agression pousse maintien et centre ensemble, si bien que les deux ailes et les deux demi-battements se déplacent à l'unisson — autorité de poussée, pas de roulis ni de tangage.

Le couplage aileron→skew front-loade une aile tout en retardant l'autre — couple de roulis sur l'axe du skew, jumeau miroir du skew de throttle. Manche à gauche ajoute du skew à l'aile gauche et l'ôte à droite ; manche à droite fait l'inverse. À utiliser avec, ou à la place du, différentiel d'amplitude du gouvernail.

Le slew est un boost ou frein transitoire sur la vitesse d’un stick. Donner des gaz front-load brièvement les DEUX demi-battements, couper les gaz late-load brièvement les DEUX ; la vitesse filtrée passe-bas (τ = 0,10 s) injecte l’à-coup, qui décroît ensuite dès que le stick est au repos. Deux couplages : slew de vitesse de throttle (symétrique, poussée) et slew de vitesse d’aileron (différentiel, roulis).

Le slew gain du Zephyrus applique la même idée transitoire au gouvernail : un changement rapide d'assiette amplifie brièvement la correction du gouvernail (µs), filtrée passe-bas (τ = 0,12 s) et bornée à ±80 µs. Il aiguise la réponse aux changements rapides d'assiette sans hausser le gain statique.

Matrice des axes de commande

Le manche de direction n'a aucune autorité en lacet dans le mélangeur d'ailes : ses boutons différentiels (amplitude et férocité) construisent la même asymétrie gauche/droite que l'inclinaison d'aileron — et cette asymétrie est du roulis. Le vrai lacet n'existe que dans le gouvernail de crête autonome (Zephyrus, gyroscope uniquement).

Entrée de commande Paramètre d'onde Effet de vol
Manche d'aileron Inclinaison (skew) — différentielle, gauche/droite Roulis
Manche de direction amplitude + férocité — différentielle, gauche/droite Roulis
Lacet — gyroscope Zephyrus Gouvernail de crête Lacet (autonome)

Pilotage & slew

Cycle complet

Forme d'onde actuelle Référence directe exacte Inversion Aile gauche Aile droite

Chaque moitié étirée à largeur égale

Moitié de descente Moitié de montée, direction alignée Diagonale exacte Aile gauche Aile droite

Lire les combinaisons extrêmes

Une combinaison 7 / 1 produit clairement une descente courte et pointue et un retour sinusoïdal long. Les valeurs exactes et quasi maximales demandent du soin : la loi de durée actuelle utilise la quantité restante en dessous de férocité 8. Par conséquent, 8 / 0 n'alloue qu'environ 0,125 % du cycle à la descente, tandis que même 8 / 6 alloue environ 0,5 %. La position reste continue, mais la vitesse et l'accélération demandées peuvent devenir physiquement inatteignables.

Amortissement d'ancrage (k₂)

L'amortissement de l'oscillateur est exposé comme une ancre réglable. La loi de base est un ressort amorti vers la cible de cadence :

kDamp = 10.0f + anchorGain;                  // anchorGain 0 → k₂=10 (tight)
error = kGain * kGainMod * cadenceTarget
      - kDamp * cadence;

À anchorGain = 0 (défaut), l'amortissement est serré — convergence rapide vers la cadence commandée. Des valeurs plus élevées montent k₂ vers 110 au gain 100, verrouillant plus fort le rythme et rejetant les perturbations. Plage 0–100.

Résonance — pompe à verrouillage de phase

La résonance est un amplificateur lock-in verrouillé en phase : il multiplie le taux d'erreur de tangage par sin(phase de l'oscillateur) et accumule — renforçant la course exactement quand une correction est la plus efficace, l'amortissant quand elle combattrait l'aile.

_resonanceAccum += gyroPitchErrorRate * sinf(_osc.phase)
                 * resonanceGain * 0.01f * dt;
_resonanceAccum *= expf(-dt / 0.15f);        // leaky integrator, τ = 0.15 s
clamp(_resonanceAccum, -2.0f, 2.0f);

La valeur accumulée est injectée dans la férocité de course et de retour pour la trame suivante. À resonanceGain = 0 (défaut), tout le chemin est désactivé. La décroissance fuyante empêche l'accumulation emballée pendant une perturbation soutenue.

SSFF — feed-forward synchrone au coup

Le feed-forward synchrone de course est philosophiquement différent de tout terme PID : il corrige la prochaine demi-course à partir de ce que le gyro a mesuré pendant la courante. À chaque passage par zéro du sinus de battement (inversion d'aile), le taux moyen d'erreur de tangage de la demi-course terminée devient un biais sur la férocité de la moitié opposée.

// zero-cross detected: finished one half-stroke, starting the next
meanError = _ssffAccumError / count;
bias      = meanError * ssffGain * 0.00001f;   // clamped ±2.0
// upstroke just ended        → bias feeds the downstroke (power stroke)
// downstroke just ended      → bias feeds the upstroke (recovery)

Le biais haut est appliqué à la course descendante (corrigeant l'erreur accumulée sur la montante précédente) ; le biais bas à la montante. Quand ssffGain tombe à zéro, les biais sont explicitement effacés pour qu'une désactivation WebUI ne laisse jamais de valeurs périmées dans la forme d'onde. L'accumulateur est 32 bits et réinitialisé par demi-course — à 250 Hz, il faudrait ~24 jours de battement continu pour déborder, ce qui n'est pas une préoccupation pratique.

Modulation aéroélastique du gain PID

Toute la modulation Ondas et SSFF est mise à l'échelle par un coefficient qui sait si l'ornithoptère bat ou plane — modulation de gain aéroélastique. Dans le noyau servo (forme d'onde), le sélecteur est la porte de battement avec son hystérésis de 50 µs :

aeroGainScale = isFlapping ? (aeroFlapCoeff  * 0.01f)
                           : (aeroGlideCoeff * 0.01f);

Dans le noyau boîte, le sélecteur est l'état du moteur (armed && throttleNorm > 0.1f). Défauts :

Coefficient Défaut Plage Quand actif
aeroGlideCoeff 40 0–100 Ailes immobiles — plané, ou moteur de boîte coupé
aeroFlapCoeff 40 0–100 Ailes battantes — ou moteur de boîte en marche

La mise à l'échelle 0–1 fait du curseur 0–100 l'autorité du gyro sur l'aile : à 100 le gyro possède entièrement la modulation, à 0 il est coupé. Autorité par noyau :

Autorité Noyau servo (forme d'onde) Noyau boîte
Gouvernail de crête roulis + lacet combinés lacet seul (roulis/tangage vont aux pattes)
Forme d'onde d'aile Modulation de respiration complète Ondas / SSFF
Ailerons de patte (empennage V) PID de roulis → côté aileron du mélange élevon
Élevons de patte / profondeur PID de tangage × aeroGainScale → côté profondeur

Les corrections PID de boîte sont bornées à ±250 µs (ZEPHYR_GEARBOX_CLAMP_US) avant mélange ; le chemin de gouverne de crête est borné à ±200 µs. Un frame de retard est inhérent : aeroGainScale est calculé dans le mixeur de boîte mais consommé par le prochain zephyrusUpdate() — 4 ms à 250 Hz, négligeable pour des servos mécaniques.

Panneau WebUI et endpoints de configuration

Le panneau de l'ornithoptère affiche deux groupes de stabilisation quand le build inclut Zephyrus. Curseurs Ondas & SSFF (0–100) : Cadence P→Phase, Férocité D→Dwell, Férocité P→Dwell, Balance I→Asymétrie, Ancre k₂, Résonance, Gain SSFF. Groupe aéroélastique : Coefficient de plané et Coefficient de battement. Toutes les valeurs persistent via POST /pteronautos/config et reviennent via GET /pteronautos/state :

{
  "ornithopter": {
    "cadence_gain": 20.0, "ferocity_d_gain": 20.0, "balance_gain": 10.0,
    "ferocity_p_gain": 0.0, "anchor_gain": 0.0, "resonance_gain": 0.0,
    "ssff_gain": 0.0,
    "aero_glide_coeff": 40.0, "aero_flap_coeff": 40.0,
    "aero_gain_scale": 0.4
  }
}

aero_gain_scale est calculé et en lecture seule. POST accepte des mises à jour partielles et borne chaque champ à [0, 100] côté serveur.

Guide de réglage

La séquence idéale d'essai en vol — chaque étape est un vol, pas un glissement de curseur :

  1. Mettez chaque gain à zéro. Volez en manuel seul ; la cellule doit être stable et prévisible avant que le gyro gagne un vote.
  2. Activez la gouverne de crête de Zephyrus (PID roulis+lacet). Vérifiez que la gouverne résiste à un léger balancement d'aile avant de toucher la forme d'onde.
  3. Cadence (P→Phase) à partir de 10. Trop haut = ailes saccadées, chasse de phase ; trop bas = rien. Juste = les ailes semblent « connectées » au tangage.
  4. Férocité D (D→Dwell) à partir de 10. Trop haut = la forme d'onde oscille entre net et doux ; juste = des changements rapides de tangage durcissent la course, le vol stable reste sinusoïdal.
  5. Férocité P (optionnel) à partir de 5 — mélange l'autorité P dans le dwell.
  6. Balance (I→Asymétrie) en dernier, bas. C'est le canal le plus déstabilisant ; 5–15 seulement. Il contrecarre la dérive persistante, jamais les à-coups transitoires.
  7. SSFF à partir de 5, seulement après que cadence et férocité tiennent. Trop haut = sur-correction et flottement de demi-course ; juste = amortissement visible de l'amplitude d'oscillation de tangage.
  8. Coefficients aéroélastiques en dernier. Réglez d'abord le plané (ailes immobiles), puis le battement. Gardez le gyro en copilote autoritaire, pas en pilote nerveux.

État de l'implémentation et incohérences ouvertes

Carte honnête, firmware actuel (Ornithopter.cpp @ 72c63020). Les lois idéales ci-dessus sont la conception ; le code contient la surface de paramètres complète, les constantes, les curseurs WebUI et le shim de pont — mais un bloc d'échafaudage de test en vol déconnecte actuellement l'essentiel du gyro du noyau servo :

// Ornithopter.cpp:269-275  (inside the flapping branch)
#ifdef ZEPHYRUS_ENABLED
    // STEP 7: ALL gyro values hard-zeroed (NaN guard test)
    _osc.kGainMod = 1.0f;              // cadence channel OFF
    gyroRudderCorrection  = 0.0f;      // crest rudder OFF while flapping
    gyroAileronCorrection = 0.0f;
    gyroElevatorCorrection= 0.0f;
#endif

Committé avec le travail d'i18n comme test de garde NaN « STEP 7 » et jamais annulé, ce bloc signifie que le comportement actuel du noyau servo est :

Ce n'est pas une critique des mathématiques — c'est un état connu et nommé : les modules de stabilisation sont compilés, paramétrés, pontés, et attendent que l'échafaudage STEP-7 soit retiré et remplacé par une vraie politique NaN (les gardes Validatio donnent déjà aux données MPU un chemin propre). Jusque-là, traitez le guide de réglage Ondas/SSFF/résonance ci-dessus comme la spécification prête au vol, vérifiée contre Zephyrus.cpp:729–737 et OrnithopterWaveform.h, mais pas encore embarquée.

Le dossier de conception de référence reste STABILIZATION.md ; son projet frère OrniFlight détient la recherche ONDAS continuellement avancée.