Зефир: стабилизация и Ondas

Идеальная схема моста от гироскопа к взмаху крыла: дыхательная модуляция Ondas, синхронный взмаху feed-forward, фазово-замкнутый резонанс и аэроупругая модуляция усиления ПИД — и честная карта того, что реально соединено в текущей прошивке.

Происхождение: OrniFlight и упрощённый Ondas

Ondas — не изобретение PteronautOS. Семейство волновой модуляции — управление каденцией «вдох-пауза», свирепость формы взмаха, асимметричное удержание и аэроупругая связь нагрузки крыла с властью ПИД — живёт и развивается в OrniFlight проекте, чьи вики и симулятор несут продвинутые алгоритмы (включая servo-efficiency table ).

PteronautOS содержит частично упрощённую реализацию: ту же математическую геометрию дыхания, масштабированную на ESP8285 с одним MPU6050 и до девяти серво. Там, где OrniFlight симулирует непрерывную аэроупругость и офлайн исследует целые семейства огибающих, PteronautOS должен решать за 4 мс тики, на ядре 80 МГц, деля кремний со стеком радио ExpressLRS. Каждый коэффициент в этой статье есть в обоих проектах; константы прошивки ниже — воздушное подмножество.

Сигнальный тракт — от Зефира к Ornithopter

Мост от гироскопа к взмаху крыла намеренно короткий — четыре файла, без динамического выделения, без очередей:

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)

Четыре сырых члена записываются в Zephyrus::update() сразу после того, как ПИД тангажа вычислит коррекцию:

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

Все четыре защищены NaN-ловушкой (если любой вход — NaN, все четыре обнуляются), чтобы повреждённый кадр I²C никогда не достиг арифметики микшера. Семь полей Ornithopter принимают данные моста:

Поле Источник Zephyrus Потребляется
gyroPitchPTerm pitchPTerm Ondas P → опережение фазы (канал каденции)
gyroPitchITerm pitchITerm Ondas I → асимметрия взмаха (канал баланса)
gyroPitchDTerm pitchDTerm Ondas D → коэффициент удержания (канал свирепости)
gyroPitchErrorRate pitchErrorRate Накопление полувзмаха SSFF
gyroRudderCorrection rudderCorrection Смещение µs гребневого руля (серво и редукторное ядра)
gyroAileronCorrection rollCorrection × gearbox gain Редуктор: ПИД крена → элеронные поверхности V-хвоста
gyroElevatorCorrection pitchCorrection × gain × aeroGainScale Редуктор: ПИД тангажа → элевон / руль высоты

Модуляция Ondas — трёхканальное дыхание

Ondas (порт. «волны») — трёхканальная система дыхания, отображающая сырые P, I и D члены ПИД тангажа на три ортогональные размерности волны взмаха. Это проприоцептивная петля: ощущение тангажа гироскопом напрямую формирует движение крыла, а движение крыла меняет тангаж, который чувствует гироскоп.

Каденция — P → опережение фазы

Положительная ошибка тангажа (нос вверх) опережает фазу осциллятора, укорачивая текущий полувзмах; отрицательная — задерживает. Идеальный закон масштабирует пружину осциллятора:

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

advance() осциллятора потребляет это как error = kGain · kGainMod · cadenceTarget − kDamp · cadence. Диапазон настройки 0–100, по умолчанию 20.

Свирепость — PD-смесь в удержание

Члены P и D смешиваются в единый сигнал свирепости, который заостряет или смягчает удержание волны:

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

При ferocityPGain = 0 (по умолчанию) поведение чисто D — быстрые изменения тангажа дают более резкие взмахи, а установившийся полёт остаётся синусоидальным.

Баланс — I в асимметрию

Интегральный член смещает центр взмаха: устойчивое смещение носа вверх усиливает нисходящий взмах, устойчивое вниз — восходящий.

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

Баланс — самый дестабилизирующий канал при высоком значении — идеальный гид держит его низким (5–15) и добавляет последним. Применение свирепости, сочетая команду пилота, Ondas и смещения SSFF, сохраняет власть пилота над полным окном [1.0, 8.0]:

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

Формирование волны и смешение ПД

Сама волна следует семейству взмахов OrniFlight: каждая половина использует свою свирепость, общий порог реверса держит левое/правое крыло в фазе, а смесь формы на профиль непрерывно превращает плоский взмах dwell/плато в округлый пирамидальный путь. Интерактивный исследователь ниже рендерит заданную геометрию для любой пары свирепости вниз/вверх — ту же кривую, что микшер запрашивает каждый тик.

Свирепость управляет и характером, и таймингом полувзмаха. Внутренне настройка WebUI 0–100 соответствует 0–8. Более высокая свирепость опускания укорачивает долю цикла опускания; более высокая свирепость возврата укорачивает долю возврата. Оставшееся время передаётся противоположной половине — паузы на нуле нет.

Настройка формы свирепости для профиля выбирает характер высокой свирепости. При 0 % предпочтительна конечная форма плато/квадрата. При 100 % — скруглённый пирамидальный путь. Каждая половина по-прежнему использует свою свирепость, поэтому половина с низкой свирепостью остаётся вытянутой и синусоидальной, а половина с высокой свирепостью приближается к прямому диагональному проходу.

Перекос опускания и подъёма (±100) смещает центр каждого полувзмаха вдоль собственной оси «начало–конец» — то же полупериодное зеркалирование, что и у свирепости. Положительный загружает взмах спереди: крыло достигает пиковой скорости раньше, усиливая тягу. Отрицательный сдвигает его к концу: взмах расходует движение в конце, ослабляя тягу. Искажение монотонно и сохраняет крайние точки, поэтому это ровно асимметричная смесь квадрат↔треугольник, а не скачок положения.

Связь формы тяги с газом объединяет выдержку и передне-задний центр в одном регуляторе. Полный газ квадратит взмах И сдвигает вперёд оба полувзмаха — пиковая скорость наступает раньше и при опускании, и при подъёме; на холостом ходу оба нейтральны. Единственный скаляр агрессии движет выдержку и центр вместе, так что оба крыла и оба полувзмаха идут в унисон — управление тягой, а не креном или тангажом.

Связь aileron→skew фронт-грузит одно крыло и запаздывает другое — момент крена на оси skew, зеркальный близнец skew от throttle. Стик влево прибавляет skew левому крылу и снимает с правого; вправо — наоборот. Используется вместе или вместо дифференциала амплитуды руля.

Slew — это переходный буст или торможение по скорости стика. Прибавление газа ненадолго front-load ОБА полувзмаха, сброс газа ненадолго late-load ОБА; отфильтрованная НЧ скорость (τ = 0,10 с) вносит толчок, который затем затухает, как только стик в покое. Две связи: slew скорости throttle (симметричный, тяга) и slew скорости элерона (дифференциальный, крен).

Slew gain Zephyrus применяет ту же переходную идею к рулю: быстрое изменение ориентации кратко усиливает коррекцию руля (мкс), с НЧ-фильтром (τ = 0,12 с) и жёстким ограничением ±80 мкс. Обостряет отклик на быстрые изменения ориентации без роста статического усиления.

Матрица осей управления

Стик руля направления не имеет власти по рысканию в микшере крыльев: его дифференциальные ручки (амплитуда и свирепость) создают ту же асимметрию лево/право, что и перекос элеронов, — а эта асимметрия есть крен. Истинное рыскание существует только в автономном гребневом руле (Zephyrus, только гироскоп).

Вход управления Параметр волны Эффект полёта
Стик элеронов Перекос (skew) — дифференциальный, лево/право Крен
Стик руля направления амплитуда + свирепость — дифференциальные, лево/право Крен
Рыскание — гироскоп Zephyrus Гребневой руль Рыскание (автономное)

Пилотирование и slew

Полный цикл

Текущая форма волны Точная прямая опора Разворот Левое крыло Правое крыло

Каждая половина растянута до равной ширины

Половина опускания Половина подъёма, направление выровнено Точная диагональ Левое крыло Правое крыло

Чтение экстремальных комбинаций

Комбинация 7 / 1 явно даёт короткий заострённый спуск и длинный синусоидальный возврат. Точные и почти максимальные значения требуют осторожности: текущий закон длительности использует остаток ниже свирепости 8. Следовательно, 8 / 0 выделяет на опускание лишь около 0,125% цикла, а даже 8 / 6 — около 0,5%. Положение остаётся непрерывным, но запрошенные скорость и ускорение могут стать физически недостижимыми.

Якорное демпфирование (k₂)

Демпфирование осциллятора выставляется как настраиваемый якорь. Базовый закон — демпфированная пружина к цели каденции:

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

При anchorGain = 0 (по умолчанию) демпфирование тугое — быстрая сходимость к заданной каденции. Большие значения поднимают k₂ к 110 при усилении 100, сильнее запирая ритм и отбрасывая возмущения. Диапазон 0–100.

Резонанс — фазово-замкнутый насос

Резонанс — это фазозаблокированный lock-in усилитель: он умножает скорость ошибки тангажа на sin(фаза осциллятора) и накапливает — усиливая взмах именно тогда, когда коррекция наиболее эффективна, и гася её, когда она боролась бы с крылом.

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

Накопленное значение вводится в свирепость взмаха и возврата для следующего кадра. При resonanceGain = 0 (по умолчанию) весь путь отключён. Утечка затухания предотвращает неконтролируемое накопление при длительном возмущении.

SSFF — синхронный взмаху feed-forward

Синхронный со взмахом feed-forward философски отличается от любого ПИД-члена: он корректирует следующий полувзмах по тому, что гироскоп измерил во время текущего. При каждом пересечении нуля машущего синуса (реверс крыла) средняя скорость ошибки тангажа завершённого полувзмаха становится смещением свирепости противоположной половины.

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

Смещение вверх применяется к нисходящему взмаху (корректируя ошибку, накопленную на предыдущем восходящем); вниз — к восходящему. Когда ssffGain падает до нуля, смещения явно очищаются, чтобы отключение в WebUI никогда не оставляло устаревшие значения в волне. Аккумулятор 32-битный и сбрасывается на полувзмах — при 250 Гц понадобилось бы ~24 дня непрерывного махания для переполнения, что не является практической проблемой.

Аэроупругая модуляция усиления ПИД

Вся модуляция Ondas и SSFF масштабируется коэффициентом, который знает, машет ли орнитоптер или планирует — аэроупругая модуляция усиления. В серво-(волновом) ядре селектором служит шлюз взмаха с гистерезисом 50 мкс:

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

В редукторном ядре селектор — состояние мотора (armed && throttleNorm > 0.1f). По умолчанию:

Коэффициент По умолчанию Диапазон Когда активен
aeroGlideCoeff 40 0–100 Крылья неподвижны — планирование или мотор редуктора выключен
aeroFlapCoeff 40 0–100 Крылья машут — или мотор редуктора работает

Масштабирование к 0–1 делает слайдер 0–100 властью гироскопа над крылом: при 100 гироскоп полностью владеет модуляцией, при 0 — выключен. Власть по ядрам:

Власть Серво (волновое) ядро Редукторное ядро
Гребневой руль крен + рыскание вместе только рыскание (крен/тангаж идут на ноги)
Волна крыла Полная модуляция дыхания Ondas / SSFF
Ножные элероны (V-хвост) ПИД крена → элеронная сторона элевонной смеси
Ножные элевоны / руль высоты ПИД тангажа × aeroGainScale → сторона руля высоты

ПИД-коррекции редуктора ограничены ±250 мкс (ZEPHYR_GEARBOX_CLAMP_US) до микширования; путь гребневого руля ограничен ±200 мкс. Один кадр задержки присущ: aeroGainScale вычисляется в редукторном микшере, но потребляется следующим zephyrusUpdate() — 4 мс при 250 Гц, ничтожно для механических серво.

Панель WebUI и endpoints конфигурации

Панель орнитоптера показывает две группы стабилизации, когда сборка включает Zephyrus. Слайдеры Ondas и SSFF (0–100): Каденция P→Фаза, Свирепость D→Удержание, Свирепость P→Удержание, Баланс I→Асимметрия, Якорь k₂, Резонанс, Усиление SSFF. Аэроупругая группа: Коэффициент планирования и Коэффициент взмаха. Все значения сохраняются через POST /pteronautos/config и стримятся обратно через 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 вычисляется и только для чтения. POST принимает частичные обновления и на сервере ограничивает каждое поле диапазоном [0, 100].

Руководство по настройке

Идеальная последовательность лётных испытаний — каждый шаг это полёт, а не перетаскивание слайдера:

  1. Обнулите все усиления. Летайте только вручную; планер должен быть стабильным и предсказуемым, прежде чем гироскоп получит голос.
  2. Включите гребневой руль Zephyrus (ПИД крена+рыскания). Проверьте, что руль сопротивляется мягкому покачиванию крыла, прежде чем что-либо коснётся волны.
  3. Каденция (P→Фаза) от 10 вверх. Слишком высоко = дёрганые крылья, охота фазы; слишком низко = ничего. Верно = крылья ощущаются «связанными» с тангажом.
  4. Свирепость D (D→Удержание) от 10. Слишком высоко = волна колеблется между резким и мягким; верно = быстрые изменения тангажа заостряют взмах, установившийся полёт остаётся синусоидальным.
  5. Свирепость P (опционально) от 5 — подмешивает власть P в удержание.
  6. Баланс (I→Асимметрия) последним, низко. Это самый дестабилизирующий канал; только 5–15. Он борется с постоянным дрейфом, но не с переходными толчками.
  7. SSFF от 5, только после того как каденция и свирепость держатся. Слишком высоко = перекоррекция и флаттер полувзмаха; верно = видимое демпфирование амплитуды колебаний тангажа.
  8. Аэроупругие коэффициенты — последними. Сначала настройте планирование (крылья неподвижны), затем взмах. Держите гироскоп авторитетным вторым пилотом, а не нервным пилотом.

Состояние реализации и открытые несоответствия

Честная карта, текущая прошивка (Ornithopter.cpp @ 72c63020). Идеальные законы выше — это проект; код содержит полную поверхность параметров, константы, слайдеры WebUI и мостовую прослойку — но блок лётных лесов сейчас отключает большую часть гироскопа от серво-ядра:

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

Закоммиченный вместе с работой по i18n как «STEP 7» NaN-тест и никогда не откатывавшийся, этот блок означает, что текущее поведение серво-ядра таково:

Это не критика математики — это известное, именованное состояние: модули стабилизации скомпилированы, параметризованы, соединены мостом и ждут, когда леса STEP-7 будут убраны и заменены настоящей NaN-политикой (защиты Validatio уже дают данным MPU чистый путь). До тех пор считайте руководство по настройке Ondas/SSFF/резонанса выше готовой к полёту спецификацией, сверенной с Zephyrus.cpp:729–737 и OrnithopterWaveform.h, но ещё не воздушной.

Авторитетная запись проектирования остаётся STABILIZATION.md ; его родственный проект OrniFlight ведёт непрерывно развиваемое исследование ONDAS.