Céfiros: Estabilización y Ondas
El diseño ideal del puente giroscopio–aleteo: modulación respiratoria Ondas, feed-forward sincronizado con el golpe, resonancia con bloqueo de fase y modulación aeroelástica de la ganancia PID — y un mapa honesto de lo que el firmware actual realmente conecta.
Linaje: OrniFlight y un Ondas simplificado
Ondas no es una invención de PteronautOS. La familia de modulación de onda — control de cadencia respiración-pausa, ferocidad de forma de batida, dwell asimétrico y el acoplamiento aeroelástico de la carga alar a la autoridad PID — vive y evoluciona en el OrniFlight proyecto, cuya wiki y simulador contienen los algoritmos avanzados (incluida la servo-efficiency table ).
PteronautOS integra una implementación parcialmente simplificada: la misma geometría matemática de respiración, escalada a un ESP8285 con un MPU6050 y hasta nueve servos. Donde OrniFlight simula aeroelástica continua y explora familias de envolvente completas sin conexión, PteronautOS debe decidir en ticks de 4 ms, en un núcleo de 80 MHz, con la pila de radio ExpressLRS compartiendo el mismo silicio. Cada ganancia de este artículo existe en ambos proyectos; las constantes de firmware de abajo son el subconjunto aéreo.
Flujo de señal — de Céfiros a Ornithopter
El puente del giroscopio al aleteo es deliberadamente corto — cuatro archivos, sin asignación dinámica, sin colas:
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)
Los cuatro términos crudos se escriben en Zephyrus::update() inmediatamente después de que el PID de cabeceo calcule su corrección:
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
Los cuatro están protegidos por una trampa NaN (si cualquier entrada es NaN, los cuatro se ponen a cero) para que un frame I²C corrupto jamás llegue a la aritmética del mezclador. Siete campos de Ornithopter reciben los datos del puente:
| Campo | Fuente Zephyrus | Consumido por |
|---|---|---|
gyroPitchPTerm
|
pitchPTerm
|
Ondas P → avance de fase (canal de cadencia) |
gyroPitchITerm
|
pitchITerm
|
Ondas I → asimetría de batida (canal de equilibrio) |
gyroPitchDTerm
|
pitchDTerm
|
Ondas D → relación de dwell (canal de ferocidad) |
gyroPitchErrorRate
|
pitchErrorRate
|
Acumulación de media batida SSFF |
gyroRudderCorrection
|
rudderCorrection
|
Offset µs del timón de cresta (kernels servo y caja) |
gyroAileronCorrection
|
rollCorrection
× gearbox gain
|
Caja: PID de alabeo → superficies de alerón de cola V |
gyroElevatorCorrection
|
pitchCorrection
× gain ×
aeroGainScale
|
Caja: PID de cabeceo → superficies de elevón / elevador |
Modulación Ondas — respiración de tres canales
Ondas (portugués: «olas») es el sistema de respiración de tres canales que mapea los términos crudos P, I y D del PID de cabeceo sobre tres dimensiones ortogonales de la forma de onda de aleteo. Es un bucle propioceptivo: la sensación de cabeceo del giroscopio moldea directamente el movimiento del ala, y el movimiento del ala cambia el cabeceo que siente el giroscopio.
Cadencia — P → avance de fase
Un error de cabeceo positivo (nariz arriba) avanza la fase del oscilador, acortando la media batida actual; un error negativo la retrasa. La ley ideal escala el muelle del oscilador:
_osc.kGainMod = 1.0f + gyroPitchPTerm * aeroGainScale * cadenceGain * 0.00005f;
// clamped to [0.5, 2.0] — phase can at most double or halve
El advance() del oscilador lo consume como error = kGain · kGainMod · cadenceTarget − kDamp · cadence. Rango de ajuste 0–100, por defecto 20.
Ferocidad — mezcla PD en el dwell
Los términos P y D se mezclan en una única señal de ferocidad que agudiza o suaviza el dwell de la forma de onda:
ferocitySignal = (gyroPitchPTerm * ferocityPGain * 0.00015f
+ gyroPitchDTerm * ferocityDGain * 0.0003f) * aeroGainScale;
// clamped to [-0.5, 0.5], added to both stroke and return ferocity
Con ferocityPGain = 0 (el valor por defecto) el comportamiento es sólo D — los cambios rápidos de cabeceo producen batidas más agudas mientras el vuelo estable se mantiene sinusoidal.
Equilibrio — I en asimetría
El término integral desplaza el centro de la batida: un sesgo persistente de nariz arriba refuerza la batida descendente, uno de nariz abajo la ascendente.
float iBias = gyroPitchITerm * aeroGainScale * balanceGain * 0.0001f;
// clamped to [-3.0, 3.0]; added to strokeFer, subtracted from returnFer
El equilibrio es el canal más desestabilizador cuando está alto — la guía ideal lo mantiene bajo (5–15) y lo añade al final. La aplicación de ferocidad, combinando comando del piloto, Ondas y sesgos SSFF, mantiene la autoridad del piloto sobre la ventana completa [1.0, 8.0]:
strokeFer = pilot(stroke) + ferocitySignal + iBias + _ssffFerocityUpBias
returnFer = pilot(return) + ferocitySignal − iBias + _ssffFerocityDownBias
Modelado de onda y mezcla PD
La forma de onda en sí sigue la familia de batidas OrniFlight: cada mitad usa su propia ferocidad, un umbral de reversión compartido mantiene las alas izquierda/derecha en fase, y una mezcla de forma por perfil transmuta continuamente una batida plana de dwell/meseta en un camino piramidal redondeado. El explorador interactivo de abajo renderiza la geometría comandada para cualquier par de ferocidad abajo/arriba — la misma curva que el mezclador pide cada tick.
La ferocidad controla tanto el carácter como el tiempo de medio golpe. Internamente, el ajuste 0–100 de la WebUI se mapea a 0–8. Una mayor ferocidad de descenso acorta la parte del ciclo del descenso; una mayor ferocidad de retorno acorta la parte del retorno. El tiempo restante se transfiere a la mitad opuesta: no hay pausa en cero.
El control Forma de Ferocidad por perfil selecciona el carácter de alta ferocidad. Al 0% favorece una forma finita de meseta/cuadrada. Al 100% favorece una trayectoria piramidal redondeada. Cada mitad sigue usando su propia ferocidad, de modo que una mitad de baja ferocidad permanece alargada y sinusoidal, mientras que una de alta ferocidad se aproxima a un recorrido diagonal directo.
El sesgo de descenso y de ascenso (±100) desplaza el centro de cada medio batido a lo largo de su propio eje inicio–fin — el mismo espejado por medio batido que la ferocidad. Positivo adelanta el batido: el ala alcanza la velocidad máxima antes, aumentando el empuje. Negativo lo retrasa: el batido gasta su movimiento al final, reduciendo el empuje. La deformación es monótona y preserva los extremos, de modo que es exactamente una mezcla asimétrica cuadrado↔triángulo, nunca un salto de posición.
El acoplamiento de forma de empuje del throttle combina permanencia y centro delantero-trasero en un mando. A fondo cuadra el golpe Y adelanta ambos medios golpes — la velocidad pico llega antes tanto en el descenso como en el ascenso; en ralentí ambos quedan neutros. El único escalar de agresión empuja permanencia y centro juntos, de modo que ambas alas y ambos medios golpes se mueven al unísono — autoridad de empuje, no de alabeo ni cabeceo.
El acoplamiento aileron→skew front-loada un ala mientras retrasa la otra — par de alabeo sobre el eje del skew, gemelo espejo del skew de throttle. Stick a la izquierda suma skew al ala izquierda y lo resta de la derecha; a la derecha hace lo contrario. Úsalo junto con, o en lugar de, el diferencial de amplitud del timón.
Slew es un impulso o freno transitorio sobre la tasa de un stick. Dar gas front-loada brevemente AMBOS semiciclos, cortar gas late-loada brevemente AMBOS; la tasa filtrada por paso-bajo (τ = 0,10 s) inyecta la sacudida, que luego decae al reposar el stick. Dos acoplamientos: slew de tasa de throttle (simétrico, empuje) y slew de tasa de alerón (diferencial, roll).
El slew gain del Zephyrus aplica la misma idea transitoria al timón: un cambio rápido de actitud amplía brevemente la corrección del timón (µs), filtrada paso-bajo (τ = 0,12 s) y limitada a ±80 µs. Agudiza la respuesta a cambios rápidos de actitud sin elevar la ganancia estática.
Matriz de ejes de control
El stick de timón no tiene autoridad de guiñada en el mezclador de alas: sus mandos diferenciales (amplitud y ferocidad) construyen la misma asimetría izquierda/derecha que el sesgo del alerón — y esa asimetría es alabeo. La guiñada verdadera existe solo en el timón de cresta autónomo (Zephyrus, solo giroscopio).
| Entrada de control | Parámetro de onda | Efecto de vuelo |
|---|---|---|
| Stick de alerón | Sesgo (skew) — diferencial, izquierda/derecha |
Alabeo |
| Stick de timón | amplitud + ferocidad — diferencial, izquierda/derecha |
Alabeo |
| Guiñada — giroscopio Zephyrus | Timón de cresta | Guiñada (autónoma) |
Dirección de vuelo y slew
Ciclo completo
Cada mitad estirada a igual ancho
Leer combinaciones extremas
Una combinación 7 / 1 produce claramente un descenso corto y puntiagudo y un retorno sinusoidal largo. Los valores exactos y casi máximos requieren cuidado: la ley de duración actual usa la cantidad restante por debajo de ferocidad 8. En consecuencia, 8 / 0 asigna solo un 0,125% del ciclo al descenso, mientras que incluso 8 / 6 asigna alrededor del 0,5%. La posición sigue siendo continua, pero la velocidad y la aceleración solicitadas pueden volverse físicamente inalcanzables.
Amortiguación de anclaje (k₂)
El amortiguamiento del oscilador se expone como un ancla ajustable. La ley base es un muelle amortiguado hacia el objetivo de cadencia:
kDamp = 10.0f + anchorGain; // anchorGain 0 → k₂=10 (tight)
error = kGain * kGainMod * cadenceTarget
- kDamp * cadence;
Con anchorGain = 0 (por defecto) el amortiguamiento es firme — convergencia rápida a la cadencia comandada. Valores mayores elevan k₂ hacia 110 con ganancia 100, bloqueando más el ritmo y rechazando perturbaciones. Rango 0–100.
Resonancia — bomba con bloqueo de fase
La resonancia es un amplificador lock-in bloqueado en fase: multiplica la tasa de error de cabeceo por sin(fase del oscilador) y acumula — reforzando la batida exactamente cuando una corrección es más eficaz, amortiguándola cuando lucharía contra el ala.
_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);
El valor acumulado se inyecta en la ferocidad de batida y retorno para el siguiente frame. Con resonanceGain = 0 (por defecto) toda la ruta está desactivada. El decaimiento con fuga evita la acumulación desbocada durante perturbaciones sostenidas.
SSFF — feed-forward sincronizado con el golpe
El feed-forward síncrono de batida es filosóficamente distinto de todo término PID: corrige la siguiente media batida a partir de lo que el giroscopio midió durante la actual. En cada cruce por cero del seno de aleteo (reversión del ala), la tasa media de error de cabeceo de la media batida terminada se convierte en un sesgo sobre la ferocidad de la mitad opuesta.
// 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)
El sesgo hacia arriba se aplica a la batida descendente (corrigiendo el error acumulado en la ascendente anterior); el sesgo hacia abajo a la ascendente. Cuando ssffGain cae a cero, los sesgos se limpian explícitamente para que una desactivación en la WebUI nunca deje valores obsoletos en la forma de onda. El acumulador es de 32 bits y se reinicia por media batida — a 250 Hz necesitaría ~24 días de aleteo continuo para desbordarse, lo que no es una preocupación práctica.
Modulación aeroelástica de la ganancia PID
Toda la modulación Ondas y SSFF se escala por un coeficiente que sabe si el ornitóptero está aleteando o planeando — modulación de ganancia aeroelástica. En el kernel servo (forma de onda) el selector es la puerta de aleteo con su histéresis de 50 µs:
aeroGainScale = isFlapping ? (aeroFlapCoeff * 0.01f)
: (aeroGlideCoeff * 0.01f);
En el kernel de caja el selector es el estado del motor (armed && throttleNorm > 0.1f). Por defecto:
| Coeficiente | Por defecto | Rango | Cuando activo |
|---|---|---|---|
aeroGlideCoeff
|
40 | 0–100 | Alas quietas — planeo, o motor de caja apagado |
aeroFlapCoeff
|
40 | 0–100 | Alas aleteando — o motor de caja en marcha |
Escalar a 0–1 hace del deslizador 0–100 la autoridad del giroscopio sobre el ala: a 100 el giroscopio posee por completo la modulación, a 0 está apagado. Autoridad por kernel:
| Autoridad | Kernel servo (forma de onda) | Kernel de caja |
|---|---|---|
| Timón de cresta | alabeo + guiñada combinados | sólo guiñada (alabeo/cabeceo van a las patas) |
| Forma de onda del ala | Modulación de respiración completa Ondas / SSFF | — |
| Alerones de pata (cola V) | — | PID de alabeo → lado alerón de la mezcla elevón |
| Elevones de pata / elevador | — | PID de cabeceo × aeroGainScale → lado elevador |
Las correcciones PID de caja se limitan a ±250 µs (ZEPHYR_GEARBOX_CLAMP_US) antes de mezclar; la ruta del timón de cresta se limita a ±200 µs. Un frame de retardo es inherente: aeroGainScale se calcula en el mezclador de caja pero lo consume el siguiente zephyrusUpdate() — 4 ms a 250 Hz, despreciable para servos mecánicos.
Panel WebUI y endpoints de configuración
El panel del ornitóptero muestra dos grupos de estabilización cuando la build incluye Zephyrus. Deslizadores Ondas y SSFF (0–100): Cadencia P→Fase, Ferocidad D→Dwell, Ferocidad P→Dwell, Equilibrio I→Asimetría, Ancla k₂, Resonancia, Ganancia SSFF. Grupo aeroelástico: Coeficiente de Planeo y Coeficiente de Aleteo. Todos los valores persisten vía POST /pteronautos/config y vuelven por 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 se calcula y es de sólo lectura. POST acepta actualizaciones parciales y limita cada campo a [0, 100] en el servidor.
Guía de ajuste
La secuencia ideal de prueba en vuelo — cada paso es un vuelo, no un arrastre de deslizador:
- Ponga a cero cada ganancia. Vuele sólo manual; la aeronave debe ser estable y predecible antes de que el giroscopio gane un voto.
- Active el timón de cresta de Zephyrus (PID alabeo+guiñada). Verifique que el timón resiste un balanceo suave del ala antes de tocar la forma de onda.
- Cadencia (P→Fase) desde 10 hacia arriba. Demasiado alto = alas a tirones, caza de fase; demasiado bajo = nada. Correcto = las alas se sienten «conectadas» al cabeceo.
- Ferocidad D (D→Dwell) desde 10. Demasiado alto = la forma de onda oscila entre agudo y suave; correcto = cambios rápidos de cabeceo agudizan la batida, el vuelo estable sigue sinusoidal.
- Ferocidad P (opcional) desde 5 — mezcla la autoridad P en el dwell.
- Equilibrio (I→Asimetría) al final, bajo. Es el canal más desestabilizador; sólo 5–15. Contrarresta la deriva persistente, nunca golpes transitorios.
- SSFF desde 5, sólo después de que cadencia y ferocidad se mantengan. Demasiado alto = sobrecorrección y aleteo de media batida; correcto = amortiguamiento visible de la amplitud de oscilación de cabeceo.
- Coeficientes aeroelásticos al final. Ajuste primero el planeo (alas quietas), luego el aleteo. Mantenga al giroscopio como un copiloto autoritativo, no un piloto nervioso.
Estado de la implementación e inconsistencias abiertas
Mapa honesto, firmware actual (Ornithopter.cpp @ 72c63020). Las leyes ideales de arriba son el diseño; el código contiene la superficie completa de parámetros, las constantes, los deslizadores de la WebUI y el shim del puente — pero un bloque de andamiaje de prueba en vuelo desconecta actualmente la mayor parte del giroscopio del kernel 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
Confirmado con el trabajo de i18n como prueba de guarda NaN "STEP 7" y nunca revertido, este bloque significa que el comportamiento actual del kernel servo es:
- Cadencia, Ferocidad-PD, Equilibrio, Resonancia, SSFF — todos presentes como parámetros y en el puente, pero sus puntos de consumo (
kGainMod,ferocitySignal,iBias,resonanceBias) están fijados a cero o se reinician cada frame (Ornithopter.cpp:282–284, 311–313, 354). - La estabilización del timón de cresta se pone a cero dentro de la rama de aleteo, de modo que la corrección del timón actúa actualmente sólo en la rama de planeo — lo contrario de lo que el vuelo necesita (la mayoría de perturbaciones llegan a mitad de aleteo).
- El kernel de caja es la excepción viva. Lee
gyroAileronCorrection/gyroElevatorCorrection/gyroRudderCorrectiony los aplica con límites (líneas 436–451) — el bloque de puesta a cero STEP-7 vive sólo en el kernel servo.
Esto no es una crítica de las matemáticas — es un estado conocido y nombrado: los módulos de estabilización están compilados, parametrizados, puenteados y esperando a que se retire el andamiaje STEP-7 y se sustituya por una política NaN real (las guardas Validatio ya dan a los datos del MPU un camino limpio). Hasta entonces, trate la guía de ajuste Ondas/SSFF/resonancia de arriba como la especificación lista para vuelo, verificada contra Zephyrus.cpp:729–737 y OrnithopterWaveform.h, pero aún no aerotransportada.
El registro de diseño autoritativo sigue siendo STABILIZATION.md ; su proyecto hermano OrniFlight mantiene la investigación ONDAS continuamente avanzada.