Итоговая работа уровня «Полётный контроллер»: карданов подвес … ↩
mod. Пятница, 16 октября 2026 07:36 . См. 8
BeeFC · Урок P13 из 33 · Уровень «Полётный контроллер»
Итоговая работа собирает всё, что написано на уровне, в версию BeeFC 1.0. Прошивка сама выполняет программу испытаний на кардановом подвесе по команде с аппаратуры управления, записывает бортовой журнал полёта, а инструмент на языке Python выставляет по журналу зачёт или незачёт. Слушатель настраивает регуляторы упрощённым способом, как в Betaflight, и защищает работу журналом, отчётом и видеозаписью.
Для итоговой работы на подвес устанавливаются воздушные винты. Опыт проводится только в защитных очках, с ограждением винтов, с аварийным выключателем на аппаратуре управления и с источником питания, ограниченным по току. Руки и предметы держатся вне плоскости вращения винтов.
Содержание
- 1. Цели урока
- 2. Термины и сокращения
- 3. Что входит в BeeFC 1.0
- 4. Программа испытаний
- 5. Оценка по журналу
- 6. Упрощённая настройка
- 7. Опыт на подвесе
- 8. Отчёт и защита
- 9. Тесты
- 10. Практика
- 11. Как это сделано в больших прошивках
- 12. Проверьте себя
- 13. Чек-лист самопроверки
- 14. Источники
- 15. Что дальше
1. Цели урока
Продолжительность урока — пять часов: один час теории и четыре часа практики. После урока слушатель:
- запускает программу испытаний с аппаратуры управления и знает, какие команды остаются за ним;
- оценивает работу регуляторов по бортовому журналу полёта, а не по впечатлению от опыта;
- настраивает регуляторы общим множителем и отдельными множителями составляющих;
- отличает настройку на краю устойчивости по шуму газа двигателей;
- готовит отчёт, по которому другой человек может проверить результат;
- сравнивает упрощённую и автоматическую настройку в Betaflight, INAV, ArduPilot и PX4.
2. Термины и сокращения
| Термин | Определение |
|---|---|
| программа испытаний | заранее заданная последовательность уставок, которую прошивка выполняет сама |
| участок | часть программы испытаний с постоянным режимом и постоянными уставками |
| перерегулирование | превышение уставки после ступенчатого изменения, в долях величины ступени |
| время установления | время, после которого ошибка остаётся в пределах заданной полосы, здесь 5 % ступени |
| упрощённая настройка | настройка регуляторов несколькими множителями вместо отдельных коэффициентов |
| общий множитель | множитель, одновременно изменяющий все коэффициенты регулятора |
| край устойчивости | настройка, при которой регулятор ещё удерживает угол, но начинает раскачивать двигатели |
| запаздывание | время между выдачей команды и началом её исполнения |
| ПИД | пропорционально-интегрально-дифференцирующий регулятор |
| SIL | программа в контуре моделирования |
3. Что входит в BeeFC 1.0
За двенадцать уроков в прошивке появились все звенья полётного контроллера: чтение инерциального датчика, калибровка, фильтры, определение ориентации, приём команд по протоколу CRSF, управление двигателями по протоколу DShot, регуляторы угловой скорости и угла, микшер, разблокировка двигателей, аварийный режим, параметры и бортовой журнал. Итоговая работа добавляет только три небольшие части:
- программу испытаний, которую прошивка выполняет по тумблеру на аппаратуре управления;
- оценку программы по журналу — одинаковую в прошивке, в модели и в инструменте на языке Python;
- упрощённую настройку регуляторов параметрами.
Главное в уроке — не новый код, а доказательство: прошивка должна выполнить одинаковую для всех слушателей программу и уложиться в нормы, а проверка должна быть повторяемой.
↑ К оглавлению4. Программа испытаний
Программа длится четырнадцать секунд и состоит из десяти участков. Уставки задаются от начала программы:
| Участок | Время, с | Режим | Содержание |
|---|---|---|---|
| 0 | 0–2 | угол | удержание горизонта, в модели — внешний момент |
| 1 | 2–4 | угол | ступень крена 15° |
| 2 | 4–6 | угол | горизонт |
| 3 | 6–8 | угол | ступень тангажа 15° |
| 4 | 8–10 | угол | горизонт, в модели — внешний момент |
| 5 | 10–11 | угловая скорость | рыскание 90 °/с |
| 6 | 11–12 | угловая скорость | рыскание в обратную сторону |
| 7 | 12–12,2 | угловая скорость | крен 100 °/с |
| 8 | 12,2–12,4 | угловая скорость | крен в обратную сторону |
| 9 | 12,4–14 | угол | возвращение к горизонту |
Программа описана таблицей, а не кодом: так её легко прочитать, проверить тестом и повторить в инструменте отчёта.
Файл core/gimbal_prog.h
typedef struct {
uint32_t t0_ms, t1_ms; /* от начала программы */
bool rate_mode;
float roll_deg, pitch_deg; /* уставки угла в режиме угла */
float roll_dps, pitch_dps, yaw_dps; /* уставки угловой скорости */
} gprog_seg_t;
#define GPROG_NSEG 10u
extern const gprog_seg_t GPROG[GPROG_NSEG];
#define GPROG_END_MS 14000u
int gprog_segment(uint32_t t_ms); /* номер участка или −1 после окончания */
typedef struct {
float hold_max_deg; /* наибольшее отклонение от горизонта на участках удержания */
float step_over[2]; /* перерегулирование ступеней крена и тангажа, доли */
float step_settle_s[2]; /* время установления в пределах 5 % */
float yaw_err; /* средняя ошибка угловой скорости рыскания, доли уставки */
float rate_over; /* перерегулирование угловой скорости крена в режиме угловой скорости */
float return_s; /* возвращение к горизонту после режима угловой скорости */
float motor_noise; /* шум газа: СКО второй разности газа на участках 2 и 9, при 125 записях в секунду */
bool complete; /* программа выполнена до конца без аварийного режима */
bool passed;
} gprog_metrics_t;
/* Оценка по записям журнала: rec — массив записей по BEELOG_NF полей. */
gprog_metrics_t gprog_eval(const int32_t (*rec)[BEELOG_NF], uint32_t n);Файл core/gimbal_prog.c
const gprog_seg_t GPROG[GPROG_NSEG] = {
{ 0, 2000, false, 0, 0, 0, 0, 0 }, /* 0: удержание горизонта, внешний момент в модели */
{ 2000, 4000, false, 15, 0, 0, 0, 0 }, /* 1: ступень крена 15° */
{ 4000, 6000, false, 0, 0, 0, 0, 0 }, /* 2: горизонт */
{ 6000, 8000, false, 0, 15, 0, 0, 0 }, /* 3: ступень тангажа 15° */
{ 8000, 10000, false, 0, 0, 0, 0, 0 }, /* 4: горизонт, внешний момент в модели */
{ 10000, 11000, true, 0, 0, 0, 0, 90 }, /* 5: рыскание 90 °/с */
{ 11000, 12000, true, 0, 0, 0, 0, -90 }, /* 6: рыскание −90 °/с */
{ 12000, 12200, true, 0, 0, 100, 0, 0 }, /* 7: крен 100 °/с в режиме угловой скорости */
{ 12200, 12400, true, 0, 0, -100, 0, 0 }, /* 8: обратно */
{ 12400, 14000, false, 0, 0, 0, 0, 0 }, /* 9: возвращение к горизонту */
};Запуск с аппаратуры управления
Программа запускается восьмым каналом аппаратуры управления, когда двигатели разблокированы. Газ, разблокировка и аварийный выключатель остаются за слушателем: программа заменяет только ручки крена, тангажа и рыскания. Аварийный режим или блокировка двигателей прерывают программу сразу.
Файл core/flight.c
/* Урок P13: программа испытаний заменяет ручки, пока включён восьмой канал. Газ, разблокировка
* и аварийный выключатель остаются за слушателем. */
if (st == ARM_ARMED && f->cmd.prog && !f->prog_done && !f->prog_on) { f->prog_on = true; f->prog_t0 = in->now_us; }
if (!f->cmd.prog) f->prog_on = f->prog_done = false;
f->prog_seg = f->prog_on ? gprog_segment((in->now_us - f->prog_t0) / 1000u) : -1;
if (f->prog_on && f->prog_seg < 0) { f->prog_on = false; f->prog_done = true; }
if (f->prog_seg >= 0) {
const gprog_seg_t *g = &GPROG[f->prog_seg];
angle_mode = !g->rate_mode;
f->angle_sp[0] = g->roll_deg;
f->angle_sp[1] = g->pitch_deg;
f->rate_sp[2] = g->yaw_dps * DEG;
}После окончания программа не начинается заново, пока тумблер не выключат и не включат снова. Иначе подвес повторял бы программу без остановки, и слушатель не успел бы осмотреть стенд.
В журнал программа записывает свой номер участка. Поле состояния, введённое в уроке P12, теперь содержит состояние автомата разблокировки в двух младших битах, режим угловой скорости в третьем, признак программы в четвёртом и номер участка в старших битах. Отчёт находит участки по этому полю и не зависит от того, когда слушатель щёлкнул тумблером.
↑ К оглавлению5. Оценка по журналу
Нормы итоговой работы:
| Показатель | Где измеряется | Норма |
|---|---|---|
| удержание горизонта | участок 0 после 0,5 с; участки 2, 4 и 9 после первой секунды | отклонение не больше 3° |
| перерегулирование ступеней | участки 1 и 3 | не больше 15 % |
| время установления ступеней | участки 1 и 3 | не больше одной секунды |
| ошибка угловой скорости рыскания | вторая половина участков 5 и 6 | в среднем не больше 15 % |
| перерегулирование угловой скорости крена | участок 7 | не больше 25 % |
| возвращение к горизонту | участок 9, до отклонения не больше 1° | не больше одной секунды |
| шум газа двигателей | участки 2 и 9 после первой секунды | не больше 0,05 |
| выполнение программы | все участки | все пройдены, аварийного режима не было |
Файл core/gimbal_prog.c
gprog_metrics_t gprog_eval(const int32_t (*rec)[BEELOG_NF], uint32_t n)
{
gprog_metrics_t m = { .step_settle_s = { 9.0f, 9.0f }, .return_s = 9.0f };
int32_t seg_t0[GPROG_NSEG];
uint32_t seen = 0, ok_states = 1, yaw_n = 0;
float yaw_sum = 0.0f, nz_sum = 0.0f, mp[2][4] = { { 0 } };
uint32_t nz_n = 0, run = 0; /* run — подряд идущих записей в окне шума */
for (unsigned i = 0; i != GPROG_NSEG; i++) seg_t0[i] = -1;
for (uint32_t k = 0; k != n; k++) {
const int32_t *r = rec[k];
if (!ST_PROG(r[LF_STATE])) continue;
if (ST_ARM(r[LF_STATE]) != 1) ok_states = 0; /* аварийный режим или блокировка */
unsigned s = (unsigned)ST_SEG(r[LF_STATE]);
if (s >= GPROG_NSEG) continue;
if (seg_t0[s] < 0) seg_t0[s] = r[LF_T];
float t = (float)(r[LF_T] - seg_t0[s]) * 1e-6f; /* время от начала участка, с */
float dur = (float)(GPROG[s].t1_ms - GPROG[s].t0_ms) * 1e-3f;
if (t > dur - 0.05f) seen |= 1u << s; /* участок пройден целиком */
float er = (float)(r[LF_ROLL] - r[LF_ROLL_SP]) * 0.01f, ep = (float)(r[LF_PITCH] - r[LF_PITCH_SP]) * 0.01f;
float e = fmaxf(fabsf(er), fabsf(ep));
if ((s == 0 && t >= 0.5f) || ((s == 2 || s == 4 || s == 9) && t >= 1.0f))
if (e > m.hold_max_deg) m.hold_max_deg = e;
if (s == 1 || s == 3) {
int a = s == 1 ? 0 : 1;
float y = (float)(a == 0 ? r[LF_ROLL] : r[LF_PITCH]) * 0.01f;
float target = a == 0 ? GPROG[s].roll_deg : GPROG[s].pitch_deg;
float over = (y - target) / target;
if (over > m.step_over[a]) m.step_over[a] = over;
if (fabsf(y - target) > 0.05f * target) m.step_settle_s[a] = t; /* последний выход из полосы */
}
if ((s == 5 || s == 6) && t >= dur * 0.5f) {
float sp = (float)r[LF_SZ] * 0.1f, g = (float)r[LF_GZ] * 0.1f;
yaw_sum += fabsf(g - sp) / fabsf(sp);
yaw_n++;
}
if (s == 7) {
float over = ((float)r[LF_GX] * 0.1f - 100.0f) / 100.0f;
if (over > m.rate_over) m.rate_over = over;
}
if (s == 9 && e > 1.0f) m.return_s = t;
if ((s == 2 || s == 9) && t >= 1.0f) { /* дрожание газа: признак края устойчивости */
for (int i = 0; i != 4; i++) {
float u = (float)r[LF_M1 + i] * 0.001f;
if (run >= 2) { float d2 = u - 2.0f * mp[0][i] + mp[1][i]; nz_sum += d2 * d2; nz_n++; }
mp[1][i] = mp[0][i];
mp[0][i] = u;
}
run++;
} else run = 0;
}
m.motor_noise = nz_n ? sqrtf(nz_sum / (float)nz_n) : 0.0f;
for (int a = 0; a != 2; a++) if (m.step_settle_s[a] > 8.0f) m.step_settle_s[a] = 0.0f;
if (m.return_s > 8.0f) m.return_s = 0.0f;
m.yaw_err = yaw_n ? yaw_sum / (float)yaw_n : 1.0f;
m.complete = ok_states && seen == (1u << GPROG_NSEG) - 1u;
m.passed = m.complete && m.hold_max_deg <= 3.0f && m.step_over[0] <= 0.15f && m.step_over[1] <= 0.15f
&& m.step_settle_s[0] <= 1.0f && m.step_settle_s[1] <= 1.0f && m.yaw_err <= 0.15f
&& m.rate_over <= 0.25f && m.return_s <= 1.0f && m.motor_noise <= 0.05f;
return m;
}Шум газа — среднеквадратичное значение второй разности газа двигателей на спокойных участках. Он нужен потому, что регулятор на краю устойчивости держит угол даже лучше обычного, а двигатели при этом мечутся от минимума к максимуму. По углам такая настройка выглядит отличной, на настоящем стенде она греет двигатели и регуляторы оборотов. Вторая разность зависит от частоты записи, поэтому норма задана для 125 записей в секунду.
Точность оценки ориентации
Удержание горизонта измеряется по оценке ориентации самой платы: другого датчика угла на подвесе нет. В модели известен истинный угол, и оценка фильтра Махони расходится с ним во время программы не больше чем на 0,22°. Значит, норма 3° проверяется с запасом. На стенде оценку подтверждает видеозапись с угломером на раме: электронный угломер или смартфон с приложением угломера должен показывать горизонт на участках удержания.
log_div: запись выполняется на каждом log_div-м шаге полётного цикла. Для итоговой работы он равен восьми — 125 записей в секунду. Журнал программы занимает около 45 КБ из 56 КБ буфера.6. Упрощённая настройка
Коэффициенты регулятора угловой скорости из урока P10 подбирались по ступеням угловой скорости без внешних возмущений. С моментом 0,1 Н·м в модели они удерживают горизонт с отклонением 4°, и программа получает незачёт. Подбирать заново девять коэффициентов по трём осям долго, поэтому BeeFC, как и Betaflight, предлагает несколько множителей:
\( K_P = K_{P0} \cdot m \cdot m_{PI}, \quad K_I = K_{I0} \cdot m \cdot m_{PI} \cdot m_I, \quad K_D = K_{D0} \cdot m \cdot m_D \)
Здесь \( m \) — общий множитель, \( m_{PI} \) — множитель пропорциональной и интегральной составляющих, \( m_I \) и \( m_D \) — множители интегральной и дифференциальной составляющих, а \( K_{P0} \), \( K_{I0} \) и \( K_{D0} \) — коэффициенты по умолчанию.
Файл core/simple_tune.h
typedef struct {
bool on; /* выключено — коэффициенты задаются параметрами по отдельности */
float master; /* общий множитель всех коэффициентов */
float pi; /* множитель пропорциональной и интегральной составляющих */
float i; /* дополнительный множитель интегральной составляющей */
float d; /* множитель дифференциальной составляющей */
} simple_tune_t;
void simple_tune_default(simple_tune_t *t);
void simple_tune_apply(const simple_tune_t *t, rate3_cfg_t *rc);Файл core/simple_tune.c
/* Как функция calculateNewPidValues в Betaflight: от коэффициентов по умолчанию, а не от текущих,
* поэтому повторное применение не накапливает изменения. */
void simple_tune_apply(const simple_tune_t *t, rate3_cfg_t *rc)
{
if (!t->on) return;
rate3_cfg_t def;
rate3_default_cfg(&def);
for (int a = 0; a != 3; a++) {
rc->axis[a].kp = def.axis[a].kp * t->master * t->pi;
rc->axis[a].ki = def.axis[a].ki * t->master * t->pi * t->i;
rc->axis[a].kd = def.axis[a].kd * t->master * t->d;
}
}Множители применяются к коэффициентам по умолчанию, а не к текущим. Поэтому повторное применение не накапливает изменения, а возвращение всех множителей к единице восстанавливает исходную настройку. Включённая упрощённая настройка заменяет коэффициенты rate_*, заданные параметрами по отдельности. Применяется она при запуске и после каждой команды param set, когда двигатели заблокированы.
| Параметр | Диапазон | По умолчанию | Назначение |
|---|---|---|---|
tune_on | 0–1 | 0 | включает упрощённую настройку |
tune_master | 0,2–2 | 1 | общий множитель |
tune_pi | 0,2–2 | 1 | множитель пропорциональной и интегральной составляющих |
tune_i | 0,2–2 | 1 | множитель интегральной составляющей |
tune_d | 0,2–2 | 1 | множитель дифференциальной составляющей |
log_div | 1–20 | 4 | делитель частоты записи журнала |
Опыт в модели
./build/beesim --gimbal-prog --dist 0.1 --master 2 --log gimbal.bin
python3 tools/gimbal_report.py gimbal.binПрограмма в модели с внешним моментом 0,1 Н·м на участках 0 и 4, газ 0,5. Значения ориентировочные: модель подвеса упрощена.
| Множители | Удержание, ° | Установление ступени, с | Перерегулирование крена в режиме угловой скорости, % | Шум газа | Итог |
|---|---|---|---|---|---|
| общий 0,5 | 9,00 | 0,74 | 6,2 | 0,0014 | незачёт |
| общий 1 | 4,03 | 0,69 | 4,7 | 0,0026 | незачёт |
| общий 1,5 | 2,50 | 0,68 | 3,5 | 0,0039 | зачёт |
| общий 2 | 1,77 | 0,67 | 1,5 | 0,0051 | зачёт |
| общий 4 | 0,84 | 0,66 | 1,3 | 0,0104 | зачёт |
| общий 10 | 0,79 | 0,67 | 7,4 | 0,6210 | незачёт |
| общий 2, дифференциальный 0,3 | 1,96 | 0,67 | 17,3 | 0,0027 | зачёт |
| общий 2, интегральный 0,3 | 2,94 | 0,81 | 0,0 | 0,0051 | зачёт |
Выводы:
- Удержание горизонта под внешним моментом определяется в основном общим множителем: чем он больше, тем меньше отклонение.
- Время установления ступени почти не зависит от множителей регулятора угловой скорости. Его задаёт внешний контур угла с коэффициентом пять из урока P9: постоянная времени около 0,2 с, а пять процентов достигаются примерно за три постоянные времени.
- Уменьшение дифференциального множителя увеличивает перерегулирование угловой скорости, уменьшение интегрального — ухудшает удержание.
- При общем множителе десять модель ещё держит углы, но шум газа вырастает больше чем в сто раз. Это край устойчивости, и норма на шум отсекает такую настройку.
Модель допускает намного бо́льшие множители, чем настоящий стенд: в ней нет вибраций рамы и резонансов, которые на стенде попадают в показания гироскопа. Поэтому на стенде множитель повышается от единицы шагами по 0,25, и после каждого шага журнал проверяется на шум газа. Верхнюю границу задаёт стенд, а не модель.
7. Опыт на подвесе

На настоящем подвесе нельзя приложить внешний момент по программе. Вместо этого перед опытом на конце одного луча закрепляется груз массой 50 г. Он создаёт постоянный момент около 0,04 Н·м, который должна убрать интегральная составляющая. Порядок опыта:
- Установить параметры
log_div 8иtune_on 1, сохранить командойparam save. - Проверить аварийный выключатель без воздушных винтов, затем установить винты и ограждение.
- Разблокировать двигатели, поднять газ до половины, включить восьмой канал.
- Дождаться окончания программы — подвес вернётся к горизонту и останется в нём.
- Заблокировать двигатели, выгрузить журнал командой
log dumpи построить отчёт. - Если отчёт показывает незачёт, изменить множители и повторить опыт.
8. Отчёт и защита
Инструмент gimbal_report.py повторяет функцию gprog_eval на языке Python, выводит показатели с нормами и возвращает код ноль только при зачёте. Для защиты он также печатает контрольную сумму SHA-256 файла журнала и подпись параметров из заголовка журнала: по ним проверяющий убеждается, что отчёт построен по тому самому журналу и тем самым параметрам.
Файл tools/gimbal_report.py
def evaluate(recs):
m = {"hold_max_deg": 0.0, "step_over": [0.0, 0.0], "step_settle_s": [9.0, 9.0],
"yaw_err": 1.0, "rate_over": 0.0, "return_s": 9.0}
seg_t0, seen, ok_states, yaw = {}, set(), True, []
nz, hist = [], [] # вторые разности газа; последние записи подряд в окне шума
for r in recs:
st = r[F["state"]]
if not (st >> 3) & 1:
continue
if st & 3 != 1:
ok_states = False
s = (st >> 4) & 15
if s >= len(PROG):
continue
seg_t0.setdefault(s, r[F["t"]])
t = (r[F["t"]] - seg_t0[s]) * 1e-6
dur = (PROG[s][1] - PROG[s][0]) * 1e-3
if t > dur - 0.05:
seen.add(s)
er = (r[F["roll"]] - r[F["roll_sp"]]) * 0.01
ep = (r[F["pitch"]] - r[F["pitch_sp"]]) * 0.01
e = max(abs(er), abs(ep))
if (s == 0 and t >= 0.5) or (s in (2, 4, 9) and t >= 1.0):
m["hold_max_deg"] = max(m["hold_max_deg"], e)
if s in (1, 3):
a = 0 if s == 1 else 1
y = r[F["roll"] if a == 0 else F["pitch"]] * 0.01
target = PROG[s][3] if a == 0 else PROG[s][4]
m["step_over"][a] = max(m["step_over"][a], (y - target) / target)
if abs(y - target) > 0.05 * target:
m["step_settle_s"][a] = t
if s in (5, 6) and t >= dur * 0.5:
sp, g = r[F["sz"]] * 0.1, r[F["gz"]] * 0.1
yaw.append(abs(g - sp) / abs(sp))
if s == 7:
m["rate_over"] = max(m["rate_over"], (r[F["gx"]] * 0.1 - 100.0) / 100.0)
if s == 9 and e > 1.0:
m["return_s"] = t
if s in (2, 9) and t >= 1.0:
u = [r[F[k]] * 0.001 for k in ("m1", "m2", "m3", "m4")]
if len(hist) >= 2:
nz += [(u[i] - 2 * hist[-1][i] + hist[-2][i]) ** 2 for i in range(4)]
hist.append(u)
else:
hist = []
m["step_settle_s"] = [0.0 if v > 8 else v for v in m["step_settle_s"]]
m["return_s"] = 0.0 if m["return_s"] > 8 else m["return_s"]
if yaw:
m["yaw_err"] = sum(yaw) / len(yaw)
m["motor_noise"] = (sum(nz) / len(nz)) ** 0.5 if nz else 0.0
m["complete"] = ok_states and seen == set(range(len(PROG)))
return mОтчёт по журналу из модели при общем множителе два:
Отчёт итоговой работы BeeFC: карданов подвес
записей 2125, частота 125 Гц, подпись параметров a26aa9b5
[+] программа выполнена до конца без аварийного режима: да
[+] удержание горизонта, наибольшее отклонение: 1.77° (норма до 3.0°)
[+] ступень крена 15°: перерегулирование: 0.1 %
[+] ступень тангажа 15°: перерегулирование: 0.1 %
[+] ступень крена: установление в пределах 5 %: 0.67 с
[+] ступень тангажа: установление в пределах 5 %: 0.67 с
[+] рыскание 90 °/с: средняя ошибка: 0.6 %
[+] крен 100 °/с в режиме угловой скорости: перерегулирование: 1.5 %
[+] возвращение к горизонту после режима угловой скорости: 0.02 с
[+] шум газа двигателей на участках удержания: 0.0051 (норма до 0.05)
[+] журнал без повреждений: да
[+] частота записи 125 Гц (параметр log_div 8): 125 Гц
ИТОГ: ЗАЧЁТНа защиту слушатель сдаёт:
- журнал программы на подвесе и отчёт по нему с итогом «ЗАЧЁТ»;
- вывод команды
paramс подписью параметров, совпадающей с подписью в отчёте; - видеозапись опыта, на которой видны подвес, угломер и аппаратура управления;
- ответы на вопросы проверяющего по коду своей прошивки.
9. Тесты
Файл tests/unit/test_level2.c
void test_simple_tune_master_one_keeps_defaults_and_does_not_accumulate(void)
{
rate3_cfg_t def, rc;
rate3_default_cfg(&def);
simple_tune_t t;
simple_tune_default(&t);
rc = def;
rc.axis[0].kp = 9.0f;
simple_tune_apply(&t, &rc); /* выключено: ничего не меняется */
TEST_ASSERT_EQUAL_FLOAT(9.0f, rc.axis[0].kp);
t.on = true;
simple_tune_apply(&t, &rc); /* все множители 1: коэффициенты по умолчанию */
for (int a = 0; a != 3; a++) {
TEST_ASSERT_EQUAL_FLOAT(def.axis[a].kp, rc.axis[a].kp);
TEST_ASSERT_EQUAL_FLOAT(def.axis[a].ki, rc.axis[a].ki);
TEST_ASSERT_EQUAL_FLOAT(def.axis[a].kd, rc.axis[a].kd);
}
t.master = 1.5f; t.i = 0.5f; t.d = 2.0f;
simple_tune_apply(&t, &rc);
simple_tune_apply(&t, &rc); /* второй раз — тот же результат */
TEST_ASSERT_FLOAT_WITHIN(1e-6f, def.axis[0].kp * 1.5f, rc.axis[0].kp);
TEST_ASSERT_FLOAT_WITHIN(1e-6f, def.axis[0].ki * 0.75f, rc.axis[0].ki);
TEST_ASSERT_FLOAT_WITHIN(1e-7f, def.axis[0].kd * 3.0f, rc.axis[0].kd);
}Файл tests/unit/test_level2.c
/* Программа выполняется один раз и не начинается заново, пока тумблер не выключат. */
void test_gprog_runs_once_until_switch_cycled(void)
{
flight_cfg_t c;
flight_default_cfg(&c);
flight_t f;
flight_init(&f, &c);
flight_in_t in = { .gyro_ok = true, .link_ok = true };
for (int i = 0; i != 16; i++) in.rc_us[i] = 1000;
in.rc_us[0] = in.rc_us[1] = in.rc_us[3] = 1500; /* ручки в центре */
uint32_t started = 0, ms = 0;
for (; ms != 40000; ms++) {
in.now_us = ms * 1000u;
in.rc_us[c.rc.arm_ch] = ms >= 6000 ? 2000 : 1000;
in.rc_us[2] = ms >= 7000 ? 1500 : 1000;
in.rc_us[c.rc.prog_ch] = (ms >= 8000 && ms < 30000) || ms >= 31000 ? 2000 : 1000;
bool was = f.prog_on;
flight_step(&f, &in);
if (!was && f.prog_on) {
started++;
TEST_ASSERT_TRUE(ms == 8000 || ms == 31000);
}
if (ms == 8000 + 2500) TEST_ASSERT_EQUAL_FLOAT(15.0f, f.angle_sp[0]); /* ступень крена */
if (ms == 8000 + GPROG_END_MS + 100) { TEST_ASSERT_FALSE(f.prog_on); TEST_ASSERT_TRUE(f.prog_done); }
if (ms == 29000) TEST_ASSERT_EQUAL_FLOAT(0.0f, f.angle_sp[0]); /* снова ручки */
}
TEST_ASSERT_EQUAL_UINT32(2, started);
TEST_ASSERT_EQUAL_INT(ARM_ARMED, f.arm.state);
}Тест в контуре моделирования выполняет программу с внешним моментом и оценивает её по журналу из того же буфера, что и в прошивке:
Файл sim/sil_flight.c
void sil_gprog_script(uint32_t t, sil_rc_t *rc, void *ctx)
{
const gprog_script_t *s = ctx;
if (t >= 4000) rc->rc[4] = 2000; /* разблокировка */
if (t >= 5000) rc->rc[2] = 1500; /* газ 0,5 */
if (t >= SIL_GPROG_START_MS) rc->rc[7] = 2000; /* программа испытаний */
uint32_t p = t - SIL_GPROG_START_MS;
if (t >= SIL_GPROG_START_MS && p >= 800 && p < 1300) rc->dist[0] = s->dist_nm; /* участок 0 */
if (t >= SIL_GPROG_START_MS && p >= 8800 && p < 9300) rc->dist[1] = -s->dist_nm; /* участок 4 */
}Файл tests/unit/test_level2.c
void test_gprog_defaults_fail_hold_tuned_pass(void)
{
flight_cfg_t c;
flight_default_cfg(&c);
c.log_div = 8.0f; /* 125 записей в секунду */
gprog_metrics_t m = run_gprog(&c, 0.1f);
TEST_ASSERT_TRUE(m.complete);
TEST_ASSERT_TRUE(m.hold_max_deg > 3.0f); /* коэффициенты урока P10 не держат возмущение */
TEST_ASSERT_FALSE(m.passed);
c.tune.on = true;
c.tune.master = 2.0f; /* упрощённая настройка */
m = run_gprog(&c, 0.1f);
TEST_ASSERT_TRUE(m.complete);
TEST_ASSERT_TRUE(m.hold_max_deg < 2.5f);
TEST_ASSERT_TRUE(m.passed);
c.tune.master = 10.0f; /* край устойчивости: углы в норме, газ дрожит */
m = run_gprog(&c, 0.1f);
TEST_ASSERT_TRUE(m.hold_max_deg < 3.0f);
TEST_ASSERT_TRUE(m.motor_noise > 0.05f);
TEST_ASSERT_FALSE(m.passed);
}mtest урока P10 и проверку аварийного выключателя урока P11. Ограждение винтов обязательно; опыт без ограждения не засчитывается.10. Практика
Задание P13.1. Программа и оценка
Продолжительность — 60 минут, выполняется на компьютере.
Реализовать функции gprog_segment и gprog_eval и запуск программы в flight_step.
Критерий автоматической проверки: тесты test_gprog_table_is_contiguous и test_gprog_runs_once_until_switch_cycled пройдены; скрытый тест проверяет прерывание программы аварийным режимом и оценку журнала с пропущенным участком.
Задание P13.2. Упрощённая настройка в модели
Продолжительность — 60 минут, выполняется на компьютере.
Реализовать функцию simple_tune_apply и параметры упрощённой настройки, найти в модели наименьший общий множитель, при котором программа получает зачёт, и множитель, при котором появляется шум газа.
Критерий автоматической проверки: тесты test_simple_tune_master_one_keeps_defaults_and_does_not_accumulate и test_gprog_defaults_fail_hold_tuned_pass пройдены; отчёт gimbal_report.py по журналу модели совпадает с выводом beesim --gimbal-prog.
Задание P13.3. Настройка на подвесе
Продолжительность — 90 минут, выполняется на кардановом подвесе.
Выполнить программу на подвесе с грузом на луче, повышая общий множитель от единицы шагами по 0,25, пока отчёт не покажет зачёт с запасом по шуму газа.
Критерий автоматической проверки: нет; слушатель сдаёт журналы всех опытов и таблицу множителей с показателями из отчётов.
Задание P13.4. Защита
Продолжительность — 30 минут, выполняется вместе с проверяющим.
Представить журнал, отчёт, вывод команды param и видеозапись, ответить на вопросы по коду.
Критерий автоматической проверки: инструмент gimbal_report.py возвращает код ноль, подписи параметров в отчёте и в выводе param совпадают.
11. Как это сделано в больших прошивках
| Прошивка | Версия | Файл в исходном коде |
|---|---|---|
| Betaflight | 2026.6.2 | src/main/config/simplified_tuning.c |
| INAV | 9.1.0 | src/main/flight/ez_tune.c |
| ArduPilot | Copter-4.7.2 | libraries/AC_AutoTune/AC_AutoTune_Multi.cpp |
| PX4 | v1.17.0 | src/modules/mc_autotune_attitude_control |
- В Betaflight функция
calculateNewPidValuesвычисляет коэффициенты от значений по умолчанию теми же произведениями, что и BeeFC: общий множитель, множитель пропорциональной и интегральной составляющих, множитель интегральной составляющей, множитель дифференциальной составляющей и множитель упреждения. Для тангажа добавляются множители отношения к крену. Все множители хранятся в процентах, по умолчанию 100, наибольшее значение 200. Отдельные функции так же масштабируют частоты среза фильтров гироскопа и дифференциальной составляющей. - В INAV функция
ezTuneUpdateзадаёт коэффициенты от собственных значений по умолчанию: ползунок отклика меняет пропорциональную составляющую, ползунок устойчивости — интегральную, ползунок демпфирования — дифференциальную, ползунок агрессивности — упреждение. Один параметр частоты фильтра задаёт сразу фильтр гироскопа, фильтр дифференциальной составляющей и задержку предиктора Смита. - В ArduPilot класс
AC_AutoTune_Multiнастраивает регуляторы автоматически серией рывков в полёте: цель по крену и тангажу 180 °/с, по рысканию 90 °/с. Дифференциальный коэффициент подбирается по отскоку после рывка, допустимую величину отскока задаёт параметрAUTOTUNE_AGGR, по умолчанию 0,075. Найденные коэффициенты уменьшаются на долюAUTOTUNE_GMBK, по умолчанию 0,25, чтобы оставить запас устойчивости. - В PX4 модуль
mc_autotune_attitude_controlдобавляет к уставке угловой скорости последовательность ступеней с убывающей длительностью, по заданному моменту и измеренной угловой скорости оценивает модель объекта рекуррентным методом наименьших квадратов и рассчитывает коэффициенты по желаемому времени нарастанияMC_AT_RISE_TIME, по умолчанию 0,14 с, с коэффициентом демпфирования 0,7. ПараметрMC_AT_APPLYопределяет, применяются ли новые коэффициенты после посадки или сразу в воздухе; второй вариант помечен в описании как опасный.
Упрощённая настройка в Betaflight и INAV оставляет решение человеку и меняет всего несколько чисел. Автоматическая настройка в ArduPilot и PX4 требует полёта и сама решает, что такое хорошо. BeeFC делает промежуточный шаг: решение принимает слушатель, но оценка выполняется по журналу автоматически и одинаково для всех.
↑ К оглавлению12. Проверьте себя
- Почему программа испытаний не управляет газом?
- Почему множители применяются к коэффициентам по умолчанию, а не к текущим?
- Почему время установления ступени угла почти не меняется при росте общего множителя?
- Зачем норма на шум газа, если угловые показатели в норме?
- Почему модель допускает бо́льшие множители, чем настоящий подвес?
- Зачем в отчёте контрольная сумма журнала и подпись параметров?
13. Чек-лист самопроверки
14. Источники
- Упрощённая настройка Betaflight.
- Модуль EzTune в INAV.
- Автоматическая настройка ArduPilot.
- Автоматическая настройка PX4.
- Параметры автоматической настройки PX4.
- OpenGimbal: открытый карданов подвес Делфтского технического университета.
- Руководство по сборке испытательных подвесов для малых БВС (Circuit Digest).
- Изображение OpenGimbal: подвес с БВС Crazyflie на штативе.
15. Что дальше
- Следующий уровень: «Автопилот», первый урок — A1. Протокол MAVLink: подключение BeeFC к QGroundControl.
- Полный перечень уроков приведён в статье «Курс BeeFC: содержание уровня „Полётный контроллер“».
Проверено: 13.10.2026
↑ К оглавлению- мод. : Пятница, 16 октября 2026