Zéfiros: Estabilização & Ondas

O desenho ideal da ponte giroscópio–batimento: modulação respiratória Ondas, feed-forward síncrono ao golpe, ressonância com bloqueio de fase e modulação aeroelástica do ganho PID — e um mapa honesto do que o firmware atual realmente liga.

Linhagem: OrniFlight e um Ondas simplificado

Ondas não é uma invenção do PteronautOS. A família de modulação de onda — controlo de cadência respiração-pausa, ferocidade de forma de batida, dwell assimétrico e o acoplamento aeroelástico da carga alar à autoridade PID — vive e evolui no OrniFlight projeto, cuja wiki e simulador carregam os algoritmos avançados (incluindo a servo-efficiency table ).

O PteronautOS incorpora uma implementação parcialmente simplificada: a mesma geometria matemática de respiração, escalada para um ESP8285 com um MPU6050 e até nove servos. Onde o OrniFlight simula aeroelástica contínua e explora famílias de envelope completas offline, o PteronautOS tem de decidir em ticks de 4 ms, num núcleo de 80 MHz, com a pilha de rádio ExpressLRS a partilhar o mesmo silício. Cada ganho neste artigo existe em ambos os projetos; as constantes de firmware abaixo são o subconjunto aéreo.

Fluxo do sinal — Zéfiros para Ornithopter

A ponte do giroscópio ao batimento alar é deliberadamente curta — quatro ficheiros, sem alocação dinâmica, sem filas:

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)

Os quatro termos brutos são escritos em Zephyrus::update() imediatamente após o PID de arfagem calcular a sua correção:

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

Os quatro são protegidos por uma armadilha NaN (se qualquer entrada for NaN, os quatro são zerados) para que um frame I²C corrompido nunca chegue à aritmética do mixer. Sete campos em Ornithopter recebem os dados da ponte:

Campo Fonte Zephyrus Consumido por
gyroPitchPTerm pitchPTerm Ondas P → avanço de fase (canal de cadência)
gyroPitchITerm pitchITerm Ondas I → assimetria de batida (canal de equilíbrio)
gyroPitchDTerm pitchDTerm Ondas D → razão de dwell (canal de ferocidade)
gyroPitchErrorRate pitchErrorRate Acumulação de meia-batida SSFF
gyroRudderCorrection rudderCorrection Offset µs do leme de crista (kernels servo e caixa)
gyroAileronCorrection rollCorrection × gearbox gain Caixa: PID de rolagem → superfícies aileron da cauda V
gyroElevatorCorrection pitchCorrection × gain × aeroGainScale Caixa: PID de arfagem → superfícies elevon / profundor

Modulação Ondas — respiração de três canais

Ondas (português: "ondas") é o sistema de respiração de três canais que mapeia os termos brutos P, I e D do PID de arfagem em três dimensões ortogonais da forma de onda de batimento. É um ciclo proprioceptivo: o sentido de arfagem do giroscópio molda diretamente o movimento da asa, e o movimento da asa muda a arfagem que o giroscópio sente.

Cadência — P → avanço de fase

Um erro de arfagem positivo (nariz para cima) avança a fase do oscilador, encurtando a meia-batida atual; um erro negativo atrasa-a. A lei ideal escala a mola do oscilador:

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

O advance() do oscilador consome-o como error = kGain · kGainMod · cadenceTarget − kDamp · cadence. Faixa de afinação 0–100, padrão 20.

Ferocidade — mistura PD no dwell

Os termos P e D são misturados num único sinal de ferocidade que aguça ou suaviza o dwell da 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

Com ferocityPGain = 0 (o padrão) o comportamento é só D — mudanças rápidas de arfagem produzem batidas mais aguçadas enquanto o voo estável permanece sinusoidal.

Equilíbrio — I em assimetria

O termo integral desloca o centro da batida: um viés persistente de nariz para cima fortalece a batida descendente, um viés persistente de nariz para baixo fortalece a ascendente.

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

O equilíbrio é o canal mais desestabilizador quando alto — o guia de afinação ideal mantém-no baixo (5–15) e adiciona-o por último. A aplicação de ferocidade, combinando comando do piloto, Ondas e viés SSFF, mantém a autoridade do piloto sobre a janela completa [1.0, 8.0]:

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

Modelagem da onda e mistura PD

A forma de onda em si segue a família de batidas OrniFlight: cada metade usa a sua própria ferocidade, um limiar de reversão partilhado mantém as asas esquerda/direita em fase, e uma mistura de forma por perfil transmuta continuamente uma batida plana de dwell/plateau num caminho piramidal arredondado. O explorador interativo abaixo renderiza a geometria comandada para qualquer par de ferocidade descida/subida — a mesma curva que o mixer pede a cada tick.

A ferocidade controla tanto o carácter como o tempo de meio-batimento. Internamente, o valor 0–100 da WebUI mapeia para 0–8. Maior ferocidade de descida encurta a fracção do ciclo da descida; maior ferocidade de retorno encurta a fracção do retorno. O tempo restante é transferido para a metade oposta — não há pausa em zero.

O controlo Forma da Ferocidade por perfil selecciona o carácter de alta ferocidade. A 0% favorece uma forma finita de patamar/quadrada. A 100% favorece um percurso piramidal arredondado. Cada metade continua a usar a sua própria ferocidade, pelo que uma metade de baixa ferocidade permanece alongada e sinusoidal, enquanto uma metade de alta ferocidade se aproxima de uma travessia diagonal directa.

A inclinação de descida e de subida (±100) desloca o centro de cada meio-batimento ao longo do seu próprio eixo início–fim — o mesmo espelhamento por meio-batimento da ferocidade. Positivo antecipa o batimento: a asa atinge a velocidade máxima mais cedo, aumentando o impulso. Negativo desloca-o para o fim: o batimento gasta o seu movimento no final, diminuindo o impulso. A deformação é monótona e preserva os extremos, sendo exatamente uma mistura assimétrica quadrado↔triângulo, nunca um salto de posição.

O acoplamento de forma de empuxo do throttle mistura permanência e centro ântero-posterior num só controle. Aceleração total quadra o batimento E adianta os dois meios-batimentos — a velocidade de pico chega antes tanto na descida quanto na subida; em marcha lenta ambos ficam neutros. O único escalar de agressão move permanência e centro juntos, de modo que as duas asas e os dois meios-batimentos andam em uníssono — autoridade de empuxo, não de rolagem ou arfagem.

O acoplamento aileron→skew front-loada uma asa enquanto atrasa a outra — torque de roll no eixo do skew, o gémeo espelho do skew de throttle. Stick à esquerda soma skew na asa esquerda e remove-o na direita; stick à direita faz o oposto. Usar junto ou em vez do diferencial de amplitude do leme.

Slew é um boost ou travão transitório na taxa de um stick. Dar gás front-loada brevemente AMBOS os meios-batimentos, cortar gás late-loada brevemente AMBOS; a taxa filtrada por passa-baixo (τ = 0,10 s) injeta o impulso, que depois decai assim que o stick repousa. Dois acoplamentos: slew de taxa de throttle (simétrico, empuxo) e slew de taxa de aileron (diferencial, roll).

O slew gain do Zephyrus aplica a mesma ideia transitória ao leme: uma mudança rápida de atitude amplia brevemente a correção do leme (µs), passa-baixo (τ = 0,12 s) e limitada a ±80 µs. Aguça a resposta a mudanças rápidas de atitude sem aumentar o ganho estático.

Matriz de eixos de controlo

O stick de leme não tem autoridade de guinada no misturador de asas: os seus botões diferenciais (amplitude e ferocidade) constroem a mesma assimetria esquerda/direita que a inclinação do aileron — e essa assimetria é rolamento. A guinada verdadeira existe apenas no leme de crista autónomo (Zephyrus, apenas giroscópio).

Entrada de controlo Parâmetro da onda Efeito de voo
Stick de aileron skew — diferencial, esquerda/direita Rolamento
Stick de leme amplitude + ferocidade — diferencial, esquerda/direita Rolamento
Guinada — giroscópio Zephyrus Leme de crista Guinada (autónoma)

Direção de voo & slew

Ciclo completo

Forma de onda actual Referência directa exacta Inversão Asa esquerda Asa direita

Cada metade esticada para largura igual

Metade de descida Metade de subida, direcção alinhada Diagonal exacta Asa esquerda Asa direita

Ler combinações extremas

Uma combinação 7 / 1 produz claramente uma descida curta e pontiaguda e um retorno sinusoidal longo. Valores exactos e quase máximos exigem cuidado: a lei de duração actual usa a quantidade restante abaixo da ferocidade 8. Consequentemente, 8 / 0 atribui apenas cerca de 0,125% do ciclo à descida, enquanto até 8 / 6 atribui cerca de 0,5%. A posição permanece contínua, mas a velocidade e a aceleração pedidas podem tornar-se fisicamente inatingíveis.

Amortecimento de âncora (k₂)

O amortecimento do oscilador é exposto como uma âncora afinável. A lei base é uma mola amortecida em direção ao alvo de cadência:

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

Com anchorGain = 0 (padrão) o amortecimento é apertado — convergência rápida para a cadência comandada. Valores maiores elevam k₂ até 110 no ganho 100, travando o ritmo com mais força e rejeitando perturbações. Faixa de afinação 0–100.

Ressonância — bomba com bloqueio de fase

A ressonância é um amplificador lock-in travado em fase: multiplica a taxa de erro de arfagem por sin(fase do oscilador) e acumula — reforçando a batida exatamente quando uma correção é mais eficaz, amortecendo-a quando lutaria contra a asa.

_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);

O valor acumulado é injetado na ferocidade de batida e de retorno para o próximo frame. Com resonanceGain = 0 (padrão) todo o caminho está desativado. O decaimento com fuga impede acumulação descontrolada durante perturbação sustentada.

SSFF — feed-forward síncrono ao golpe

O feed-forward síncrono de batida é filosoficamente diferente de todos os termos PID: corrige a próxima meia-batida a partir do que o giroscópio mediu durante a atual. Em cada cruzamento de zero do seno de batimento (reversão da asa), a taxa média de erro de arfagem da meia-batida concluída torna-se um viés na ferocidade da metade oposta.

// 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)

O viés de subida é aplicado à batida descendente (corrigindo o erro acumulado na ascendente anterior); o viés de descida à ascendente. Quando ssffGain cai para zero, os viés são explicitamente limpos para que uma desativação na WebUI nunca deixe valores obsoletos na forma de onda. O acumulador é de 32 bits e reiniciado por meia-batida — a 250 Hz precisaria de ~24 dias de batimento contínuo para transbordar, o que não é uma preocupação prática.

Modulação aeroelástica do ganho PID

Toda a modulação Ondas e SSFF é escalada por um coeficiente que sabe se o ornitóptero está batendo ou planando — modulação de ganho aeroelástica. No kernel servo (forma de onda) o seletor é a porta de batimento com a sua histerese de 50 µs:

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

No kernel de caixa o seletor é o estado do motor (armed && throttleNorm > 0.1f). Padrões:

Coeficiente Padrão Faixa Quando ativo
aeroGlideCoeff 40 0–100 Asas paradas — planagem, ou motor de caixa desligado
aeroFlapCoeff 40 0–100 Asas batendo — ou motor de caixa a funcionar

Escalar para 0–1 torna o slider 0–100 a autoridade do giroscópio sobre a asa: a 100 o giroscópio detém totalmente a modulação, a 0 está desligado. Autoridade por kernel:

Autoridade Kernel servo (forma de onda) Kernel de caixa
Leme de crista rolagem + guinada combinadas só guinada (rolagem/arfagem vão para as pernas)
Forma de onda da asa Modulação de respiração completa Ondas / SSFF
Ailerons de perna (cauda V) PID de rolagem → lado aileron da mistura elevon
Elevons de perna / profundor PID de arfagem × aeroGainScale → lado profundor

As correções PID de caixa são limitadas a ±250 µs (ZEPHYR_GEARBOX_CLAMP_US) antes da mistura; o caminho do leme de crista é limitado a ±200 µs. Um frame de atraso é inerente: aeroGainScale é calculado no mixer de caixa mas consumido pela próxima zephyrusUpdate() — 4 ms a 250 Hz, negligenciável para servos mecânicos.

Painel WebUI e endpoints de configuração

O painel do ornitóptero mostra dois grupos de estabilização quando a build inclui Zephyrus. Sliders Ondas & SSFF (0–100): Cadência P→Fase, Ferocidade D→Dwell, Ferocidade P→Dwell, Equilíbrio I→Assimetria, Âncora k₂, Ressonância, Ganho SSFF. Grupo aeroelástico: Coeficiente de Planagem e Coeficiente de Batimento. Todos os valores persistem através de POST /pteronautos/config e voltam 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 é calculado e somente leitura. O POST aceita atualizações parciais e limita cada campo a [0, 100] no servidor.

Guia de ajuste

A sequência ideal de teste em voo — cada passo é um voo, não um arrasto de slider:

  1. Zere todos os ganhos. Voe só manual; a aeronave tem de estar estável e previsível antes de o giroscópio ganhar um voto.
  2. Ative o leme de crista do Zephyrus (PID rolagem+guinada). Verifique que o leme resiste a um balanço suave da asa antes de qualquer coisa tocar na forma de onda.
  3. Cadência (P→Fase) a partir de 10. Demasiado alto = asas aos solavancos, caça de fase; demasiado baixo = nada. Certo = asas sentem-se "ligadas" à arfagem.
  4. Ferocidade D (D→Dwell) a partir de 10. Demasiado alto = a forma de onda oscila entre aguçado e suave; certo = mudanças rápidas de arfagem aguçam a batida, voo estável permanece sinusoidal.
  5. Ferocidade P (opcional) a partir de 5 — mistura a autoridade P no dwell.
  6. Equilíbrio (I→Assimetria) por último, baixo. Este é o canal mais desestabilizador; só 5–15. Contraria deriva persistente, nunca solavancos transitórios.
  7. SSFF a partir de 5, só depois de cadência e ferocidade assentarem. Demasiado alto = sobrecorreção e flutter de meia-batida; certo = amortecimento visível da amplitude de oscilação de arfagem.
  8. Coeficientes aeroelásticos por último. Afine a planagem primeiro (asas paradas), depois o batimento. Mantenha o giroscópio um copiloto autoritário, não um piloto nervoso.

Estado da implementação e inconsistências em aberto

Mapa honesto, firmware atual (Ornithopter.cpp @ 72c63020). As leis ideais acima são o design; o código contém a superfície completa de parâmetros, as constantes, os sliders da WebUI e o shim de ponte — mas um bloco de andaime de teste em voo desliga atualmente a maior parte do giroscópio do 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

Cometido com o trabalho de i18n como um teste de guarda NaN "STEP 7" e nunca revertido, este bloco significa que o comportamento atual do kernel servo é:

Isto não é uma crítica à matemática — é um estado conhecido e nomeado: os módulos de estabilização estão compilados, parametrizados, em ponte, e à espera de que o andaime STEP-7 seja removido e substituído por uma política NaN real (as guardas Validatio já dão aos dados do MPU um caminho limpo). Até lá, trate o guia de afinação Ondas/SSFF/ressonância acima como a especificação pronta para voo, verificada contra Zephyrus.cpp:729–737 e OrnithopterWaveform.h, mas ainda não aérea.

O registo de design autoritativo permanece STABILIZATION.md ; o seu projeto irmão OrniFlight mantém a investigação ONDAS continuamente avançada.