Перейти к содержимому

Итоговая работа уровня «Старт»: тяговый стенд, версия 1 … ↩

Итоговая работа уровня «Старт»: тяговый стенд, версия 1 mod. Четверг, 15 октября 2026 22:55 . См. 10

BeeFC · Урок S10 из 33 · Уровень «Старт»

Итоговая работа объединяет всё, что сделано на уровне «Старт»: планировщик, консоль, выход на двигатель, измерение напряжения и тока. К ним добавляется драйвер тензодатчика, и BeeFC сам снимает характеристику двигателя с воздушным винтом.

Результат итоговой работы — BeeFC версии 0.9 и отчёт о характеристиках двигателя: тяга, потребляемая мощность и удельная тяга при разных значениях газа. Эти данные понадобятся в уроке P7 для модели винтомоторной группы.

Содержание

1. Цели урока

Продолжительность итоговой работы — шесть часов: один час теории и пять часов практики. После урока слушатель:

  • собирает тяговый стенд и подключает тензодатчик к учебной плате;
  • пишет драйвер микросхемы HX711 и проверяет его тестом с имитацией микросхемы;
  • выполняет тарирование и калибровку стенда эталонным грузом;
  • реализует автоматическую развёртку газа с защитами по току, тяге и напряжению;
  • получает характеристику двигателя и рассчитывает удельную тягу.
↑ К оглавлению

2. Термины и сокращения

ТерминОпределение
тензодатчикупругий элемент с наклеенными тензорезисторами, сопротивление которых меняется при деформации
мостовая схемасоединение четырёх тензорезисторов, на выходе которого появляется напряжение, пропорциональное силе
HX71124-битный аналого-цифровой преобразователь с усилителем для мостовых датчиков
тарированиезапоминание показания датчика без нагрузки
калибровка эталонным грузомопределение цены одного кода датчика по грузу известной массы
удельная тягаотношение тяги к потребляемой мощности, г/Вт
развёртка газаавтоматическое ступенчатое изменение длительности импульса с записью показаний
формат CSVтекстовая таблица, столбцы которой разделены запятыми
↑ К оглавлению

3. Конструкция стенда

Балочный тензодатчик закрепляется одним концом в массивном основании вертикально. На свободном конце установлена площадка с двигателем, ось двигателя горизонтальна. Тяга воздушного винта изгибает балку, и тензодатчик измеряет силу напрямую, без рычага.

Тяговый стенд: вид сбоку и схема соединений. Размеры ориентировочные, полный размер рисунка открывается по щелчку.
Тяговый стенд: вид сбоку и схема соединений. Размеры ориентировочные, полный размер рисунка открывается по щелчку.
Размещение стенда: Поток от воздушного винта направляется в сторону, где нет людей. Плоскость вращения винта закрывается защитным экраном. Основание стенда прижимается к столу струбцинами: тяга двигателя класса 2207 с пятидюймовым винтом может превышать один килограмм. Оператор находится сбоку от стенда, а не перед ним и не за ним.
ЭлементРекомендацияПримечание
тензодатчикпредел измерения не меньше удвоенной ожидаемой тягидля трёхдюймового винта — 1 кг, для пятидюймового — 3 кг
HX711модуль с выводом RATE, переключённым на 80 Гцна многих модулях по умолчанию 10 Гц
питаниелабораторный источник с ограничением тока или батарея через предохранительток ограничивается по техническим данным двигателя
датчик токадатчик регулятора, полётного контроллера или отдельный модульурок S9

Указанные значения — учебный ориентир. Предел измерения и ограничение тока выбираются по техническим данным своего двигателя.

↑ К оглавлению

4. Драйвер HX711

Протокол обмена

HX711 соединяется с микроконтроллером двумя линиями: DOUT и SCK. Протокол реализуется программно, без аппаратного интерфейса.

  1. Низкий уровень на DOUT означает, что преобразование завершено.
  2. Микроконтроллер подаёт на SCK 24 импульса и после каждого переднего фронта читает один бит, начиная со старшего.
  3. Дополнительные импульсы выбирают канал и усиление для следующего преобразования: один импульс — канал A с усилением 128, два — канал B с усилением 32, три — канал A с усилением 64.
  4. Результат передаётся в 24-битном дополнительном коде и расширяется до 32 бит с сохранением знака.

Файл drivers/hx711.h

typedef enum {            /* число импульсов после 24 бит данных */
    HX711_A_128 = 1,      /* канал A, усиление 128 */
    HX711_B_32  = 2,      /* канал B, усиление 32 */
    HX711_A_64  = 3       /* канал A, усиление 64 */
} hx711_gain_t;

typedef struct {
    hal_pin_t    dout, sck;
    hx711_gain_t gain;
    int32_t      offset;          /* показание без нагрузки (тарирование) */
    float        grams_per_count; /* масштаб по эталонному грузу */
} hx711_t;

void    hx711_init(hx711_t *h, hal_pin_t dout, hal_pin_t sck, hx711_gain_t gain);
bool    hx711_ready(const hx711_t *h);
int32_t hx711_read_raw(const hx711_t *h);
int32_t hx711_sign_extend(uint32_t raw24);
float   hx711_to_grams(const hx711_t *h, int32_t raw);
Plain text

Файл drivers/hx711.c

bool hx711_ready(const hx711_t *h)
{
    return !hal_gpio_read(h->dout);       /* низкий уровень DOUT: данные готовы */
}

int32_t hx711_sign_extend(uint32_t raw24)
{
    /* 24-битный дополнительный код: если старший бит 23 установлен,
     * верхний байт заполняется единицами. */
    if (raw24 & 0x800000u) raw24 |= 0xFF000000u;
    return (int32_t)raw24;
}

/* Один импульс SCK. Высокий уровень дольше 60 мкс переводит HX711 в режим
 * пониженного энергопотребления, поэтому импульс защищён от прерываний.
 * Защищается только импульс (около 1 мкс), а не всё чтение (около 50 мкс):
 * длинная критическая секция привела бы к потере байтов последовательного порта. */
static bool hx711_clock(const hx711_t *h)
{
    uint32_t irq = hal_irq_disable();
    hal_gpio_write(h->sck, true);
    hal_delay_us(1);
    bool bit = hal_gpio_read(h->dout);
    hal_gpio_write(h->sck, false);
    hal_irq_restore(irq);
    hal_delay_us(1);
    return bit;
}

int32_t hx711_read_raw(const hx711_t *h)
{
    uint32_t value = 0;
    for (int i = 0; i != 24; i++)               /* старший бит первым */
        value = (value * 2u) | (hx711_clock(h) ? 1u : 0u);
    for (int i = 0; i != (int)h->gain; i++)     /* выбор канала и усиления */
        (void)hx711_clock(h);
    return hx711_sign_extend(value);
}

float hx711_to_grams(const hx711_t *h, int32_t raw)
{
    return (float)(raw - h->offset) * h->grams_per_count;
}
Plain text
Почему импульс защищён от прерываний: Если высокий уровень на SCK держится дольше 60 мкс, HX711 переходит в режим пониженного энергопотребления, и чтение срывается. Обработчик прерывания, выполненный в середине импульса, может растянуть его. Поэтому каждый импульс выполняется в короткой критической секции. Защищать всё чтение целиком нельзя: оно длится около 50 мкс, и за это время последовательный порт на скорости 420 000 бод потеряет принятые байты.

Тест с имитацией микросхемы

Тест подключает к поддельной реализации портов два перехватчика. Перехватчик записи считает передние фронты на SCK, перехватчик чтения выдаёт на DOUT очередной бит заданного 24-битного числа. Так проверяются порядок битов, расширение знака, число импульсов усиления и закрытие всех критических секций.

Файл tests/unit/test_level1.c

void test_hx711_reads_negative_value_and_sets_gain(void)
{
    hx711_t h;
    hx_pattern = 0xFFFC18u;                 /* -1000 в 24-битном дополнительном коде */
    hx_bit = 0; hx_sck = false;
    fake_gpio_set_hooks(hx_read, hx_write);
    hx711_init(&h, /*dout*/ 1, /*sck*/ 2, HX711_A_64);

    TEST_ASSERT_EQUAL_INT32(-1000, hx711_read_raw(&h));
    TEST_ASSERT_EQUAL_INT(24 + 3, hx_bit);  /* 24 бита и 3 импульса усиления */
    TEST_ASSERT_FALSE(fake_gpio_get_output(2));   /* SCK оставлен в нуле */
    TEST_ASSERT_EQUAL_INT(0, fake_irq_depth);       /* все критические секции закрыты */
    TEST_ASSERT_EQUAL_INT(1, fake_irq_max_depth);   /* и не вложены друг в друга */
}
Plain text
↑ К оглавлению

5. Тарирование и калибровка

  1. Без нагрузки усредняются шестнадцать показаний, результат записывается в поле offset.
  2. На площадку двигателя подвешивается эталонный груз известной массы, например 200 г, через нить и блок так, чтобы сила действовала по оси двигателя.
  3. Цена кода grams_per_count равна массе груза, делённой на разность усреднённого показания и offset.
  4. Линейность проверяется вторым грузом: расхождение показания с массой груза не должно превышать 1 %.

Команды консоли tare и cal 200 выполняют первые три шага, а результат сохраняется в ОЗУ до перезагрузки. Сохранение параметров во флеш-памяти появится в уроке P12.

↑ К оглавлению

6. Развёртка газа

Задача развёртки — конечный автомат, который выполняется планировщиком с частотой 100 Гц.

СостояниеДействиеПереход
ожиданиеимпульсов на двигатель неткоманда sweep при разблокированных двигателях
ступеньустановить газ ступени, ждать две секундыследующая ступень или спуск
спускплавно уменьшить газ до 1000 мкс за одну секундуостановка
остановкаблокировка двигателей, вывод итоговожидание
авариянемедленная блокировка двигателейожидание

Ступени идут от 1100 мкс с шагом 50 мкс до верхнего предела, заданного командой. Переход в состояние «авария» выполняется из любого состояния по одной из причин:

  • ток превысил заданный предел;
  • тяга превысила 90 % предела измерения тензодатчика;
  • напряжение элемента батареи опустилось до критического порога из урока S9;
  • от тензодатчика нет данных дольше 100 мс;
  • в консоль поступил любой символ.

Синхронный журнал

Строка журнала записывается при каждом новом показании HX711, то есть 80 раз в секунду. В строку попадают последние значения газа, тока и напряжения. Строка занимает около 40 байт, поток составляет около 3200 байт в секунду, что меньше трети пропускной способности порта на скорости 115 200 бод.

time_ms,throttle_us,thrust_g,current_a,voltage_v
12000,1300,54.2,1.12,16.33
12012,1300,54.9,1.13,16.33
Plain text
↑ К оглавлению

7. Обработка на компьютере

Сценарий на языке Python делит журнал на ступени, отбрасывает первые 0,5 секунды каждой ступени, пока двигатель разгоняется, и усредняет остальное. Удельная тяга вычисляется как отношение тяги к электрической мощности \( P = U \cdot I \).

Файл tools/thrust_report.py

def steps(rows):
    """Делит журнал на ступени газа и усредняет установившуюся часть каждой."""
    out, group = [], []
    for r in rows + [None]:
        if group and (r is None or r["throttle_us"] != group[0]["throttle_us"]):
            t0 = group[0]["time_ms"]
            steady = [g for g in group if g["time_ms"] - t0 >= SETTLE_MS]
            if steady:
                n = len(steady)
                thrust = sum(g["thrust_g"] for g in steady) / n
                power = sum(g["current_a"] * g["voltage_v"] for g in steady) / n
                out.append({"throttle_us": group[0]["throttle_us"], "thrust_g": thrust,
                            "power_w": power, "g_per_w": thrust / power if power > 0.5 else 0.0})
            group = []
        if r is not None:
            group.append(r)
    return out
Plain text
python3 tools/thrust_report.py sweep.csv --png thrust.png
| Газ, мкс | Тяга, г | Мощность, Вт | Удельная тяга, г/Вт |
|---|---|---|---|
| 1300 | 54 | 13.7 | 3.92 |
| 1500 | 150 | 45.5 | 3.30 |
Plain text

Значения в примере получены на модельных данных. Удельная тяга обычно наибольшая на малом газе и снижается с ростом мощности: это закономерность, которую слушатель проверяет на своём двигателе.

↑ К оглавлению

8. Итоговая проверка уровня

Уровень «Старт» считается пройденным, когда в репозитории слушателя выполнены все условия.

  1. Все модульные тесты уровня, включая скрытые, пройдены.
  2. Проверка правил архитектуры make arch-check пройдена.
  3. Прошивка собирается без предупреждений, изменения отмечены меткой v0.9.
  4. Журнал развёртки и отчёт docs/s10_report.md с таблицей и графиками добавлены в репозиторий.
  5. Анализатор журнала на сервере подтверждает, что тяга растёт с газом, ступени не короче двух секунд, а аварийных остановок по причине отсутствия данных нет.
↑ К оглавлению

9. Практика

Задание S10.1. Драйвер HX711

Продолжительность — 60 минут, выполняется на компьютере.

Реализовать драйвер drivers/hx711 с функциями чтения, расширения знака и пересчёта в граммы.

Критерий автоматической проверки: тесты test_hx711_* пройдены; скрытые тесты проверяют наибольшее и наименьшее значения, все три варианта усиления и отсутствие вложенных критических секций.

Задание S10.2. Сборка и калибровка стенда

Продолжительность — 90 минут.

Собрать стенд, подключить HX711 к двум свободным выводам платы, реализовать команды tare и cal. Проверить линейность вторым грузом и записать результаты в отчёт.

Задание S10.3. Развёртка газа с защитами

Продолжительность — 90 минут, частично на компьютере.

Реализовать модуль core/thrust_sweep как конечный автомат по таблице из раздела «Развёртка газа» и журнал в формате CSV.

Критерий автоматической проверки: скрытые тесты на модельном времени проверяют каждую причину аварийной остановки, продолжительность ступеней и то, что после остановки на выходе нет импульсов.

Задание S10.4. Характеристика двигателя

Продолжительность — 60 минут, выполняется под наблюдением преподавателя или по видеосвязи.

Выполнить развёртку до газа, при котором ток достигает 80 % предела по техническим данным двигателя. Построить графики сценарием thrust_report.py и описать в отчёте, при каком газе удельная тяга наибольшая.

Критерий автоматической проверки: анализатор журнала подтверждает условия пятого пункта итоговой проверки.

↑ К оглавлению

10. Как это сделано в больших прошивках

ПрошивкаВерсияФайл в исходном коде
Betaflight2026.6.2src/main/msp/msp.c
INAV9.1.0src/main/flight/mixer.c
ArduPilotCopter-4.7.2ArduCopter/motor_test.cpp
PX4v1.17.0src/systemcmds/actuator_test
  • В Betaflight вкладка двигателей конфигуратора отправляет сообщение MSP_SET_MOTOR. Обработчик в файле msp.c записывает значения в массив motor_disarmed[], то есть они действуют только при заблокированных двигателях: проверка двигателей и полёт не могут выполняться одновременно.
  • В INAV обработчик MSP_SET_MOTOR в файле fc_msp.c также записывает значения в массив motor_disarmed[], а модуль mixer.c подаёт их на двигатели только в заблокированном состоянии.
  • В ArduCopter перед проверкой двигателей функция mavlink_motor_control_check в файле motor_test.cpp выполняет до шести проверок: плата инициализирована, проверки двигателей пройдены, аппаратура управления откалибрована (если это требуется), аппарат на земле, защитный выключатель нажат, аварийная остановка не активна. Длительность проверки ограничена 600 секундами.
  • В PX4 команда actuator_test set задаёт значение выхода на время, указанное параметром -t. Без этого параметра команда работает в интерактивном режиме и снимает значение после нажатия клавиши Enter — тот же принцип, что и остановка развёртки BeeFC любым символом.
↑ К оглавлению

11. Проверьте себя

  1. Как HX711 сообщает о готовности данных и как выбирается усиление?
  2. Почему в драйвере HX711 от прерываний защищается каждый импульс, а не всё чтение?
  3. Как рассчитывается цена кода тензодатчика?
  4. Почему первые 0,5 секунды каждой ступени не учитываются при обработке?
  5. Почему в Betaflight значения из вкладки двигателей действуют только при заблокированных двигателях?
↑ К оглавлению

12. Чек-лист самопроверки

Результат: Все пункты выполнены. Уровень «Старт» пройден, BeeFC версии 0.9 готов. Следующий уровень — «Полётный контроллер».
↑ К оглавлению

13. Источники

  1. Техническое описание HX711.
  2. Проверка двигателей в ArduCopter.
  3. Обработчик сообщений MSP в Betaflight.
  4. Документация ArduPilot: подключение регуляторов и двигателей.
  5. Открытый тяговый стенд на Arduino с тензодатчиком и HX711 (Printables).
  6. Промышленные тяговые стенды Tyto Robotics.
↑ К оглавлению

14. Что дальше

  • Следующий уровень: «Полётный контроллер», урок P1. Интерфейс SPI и драйвер инерциального датчика ICM-42688-P.
  • Полный перечень уроков приведён в статье «Курс BeeFC: содержание уровня „Старт“».

Проверено: 13.10.2026

↑ К оглавлению
  • мод. : Четверг, 15 октября 2026