Зефир: стабилизация и 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].
Руководство по настройке
Идеальная последовательность лётных испытаний — каждый шаг это полёт, а не перетаскивание слайдера:
- Обнулите все усиления. Летайте только вручную; планер должен быть стабильным и предсказуемым, прежде чем гироскоп получит голос.
- Включите гребневой руль Zephyrus (ПИД крена+рыскания). Проверьте, что руль сопротивляется мягкому покачиванию крыла, прежде чем что-либо коснётся волны.
- Каденция (P→Фаза) от 10 вверх. Слишком высоко = дёрганые крылья, охота фазы; слишком низко = ничего. Верно = крылья ощущаются «связанными» с тангажом.
- Свирепость D (D→Удержание) от 10. Слишком высоко = волна колеблется между резким и мягким; верно = быстрые изменения тангажа заостряют взмах, установившийся полёт остаётся синусоидальным.
- Свирепость P (опционально) от 5 — подмешивает власть P в удержание.
- Баланс (I→Асимметрия) последним, низко. Это самый дестабилизирующий канал; только 5–15. Он борется с постоянным дрейфом, но не с переходными толчками.
- SSFF от 5, только после того как каденция и свирепость держатся. Слишком высоко = перекоррекция и флаттер полувзмаха; верно = видимое демпфирование амплитуды колебаний тангажа.
- Аэроупругие коэффициенты — последними. Сначала настройте планирование (крылья неподвижны), затем взмах. Держите гироскоп авторитетным вторым пилотом, а не нервным пилотом.
Состояние реализации и открытые несоответствия
Честная карта, текущая прошивка (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-тест и никогда не откатывавшийся, этот блок означает, что текущее поведение серво-ядра таково:
- Каденция, Свирепость-PD, Баланс, Резонанс, SSFF — все присутствуют как параметры и в мосту, но их точки потребления (
kGainMod,ferocitySignal,iBias,resonanceBias) прижаты к нулю или сбрасываются каждый кадр (Ornithopter.cpp:282–284, 311–313, 354). - Стабилизация гребневого руля обнуляется внутри ветви взмаха, поэтому коррекция руля сейчас действует только в ветви планирования — противоположно тому, что нужно полёту (большинство возмущений приходит в середине взмаха).
- Редукторное ядро — живое исключение. Оно читает
gyroAileronCorrection/gyroElevatorCorrection/gyroRudderCorrectionи применяет их с ограничениями (строки 436–451) — блок обнуления STEP-7 живёт только в серво-ядре.
Это не критика математики — это известное, именованное состояние: модули стабилизации скомпилированы, параметризованы, соединены мостом и ждут, когда леса STEP-7 будут убраны и заменены настоящей NaN-политикой (защиты Validatio уже дают данным MPU чистый путь). До тех пор считайте руководство по настройке Ondas/SSFF/резонанса выше готовой к полёту спецификацией, сверенной с Zephyrus.cpp:729–737 и OrnithopterWaveform.h, но ещё не воздушной.
Авторитетная запись проектирования остаётся STABILIZATION.md ; его родственный проект OrniFlight ведёт непрерывно развиваемое исследование ONDAS.