Embedded HandbookПриложения
Все разделы › VIII. Электроника вокруг микроконтроллера
STM32F103, STM32G431 · уровень средний · 21 мин чтения
Открыть в интерактивном справочнике

57а. Двигатели: коллекторный, шаговый, бесколлекторный

В разделе 57 двигатель появлялся как нагрузка под ШИМ: индуктивность, ток которой не успевает за напряжением. Здесь — как объект управления. Три типа двигателей, которые чаще всего стоят рядом с микроконтроллером, управляются по-разному: коллекторный — одной скважностью, шаговый — последовательностью токов в двух обмотках, бесколлекторный — переключением трёх фаз по положению ротора. У STM32 для всего этого есть таймер TIM1: дополняющие выходы с защитным интервалом, аварийный вход и вход для датчиков Холла.

Всё, что прошивка считает сама, — код защитного интервала, таблицы шагов, разгон, коммутация по датчикам, преобразования для векторного управления, пуск коллекторного двигателя — собрано в одну программу _verify/motor57a/motor.c и прогнано на компьютере. Формула защитного интервала и работа входов TIM1 сверены со справочным руководством на F1. Сверяет _verify/motor57a_check.py.

Как это называется в документации

Оригинал Перевод
brushed DC motor коллекторный двигатель постоянного тока
stepper motor, full step, half step, microstepping шаговый двигатель, полный шаг, полушаг, микрошаг
brushless DC motor (BLDC) бесколлекторный двигатель
six-step (trapezoidal) commutation шестишаговая коммутация
field-oriented control (FOC) векторное управление
Clarke and Park transforms преобразования Кларк и Парка
back EMF противо-ЭДС
dead-time generator (DTG) генератор защитного интервала
break input BKIN, bits BKE and BKP аварийный вход таймера, его включение и полярность
main output enable (MOE) общее разрешение выходов таймера

57а.1 Три двигателя — три способа управления

Коллекторный Шаговый Бесколлекторный (BLDC)
что переключает ток в обмотках щётки и коллектор, механически контроллер, по шагам контроллер, по положению ротора
чем управляет прошивка скважностью и знаком последовательностью токов в двух обмотках тремя фазами: какая «+», какая «−», какая свободна
силовая часть H-мост (4 ключа) два H-моста или готовый драйвер три полумоста (6 ключей)
положение ротора не нужно известно по числу шагов — пока шаги не пропущены датчики Холла, энкодер или противо-ЭДС
где стоит вентиляторы, насосы, игрушки принтеры, станки, позиционеры дроны, шпиндели, электроинструмент

57а.2 Коллекторный: H-мост и пуск

Коллекторному двигателю достаточно напряжения: щётки сами переключают обмотки. Прошивке остаётся ШИМ (скорость) и направление. Для обоих направлений нужен H-мост — четыре ключа: по диагонали включены два, ток идёт через двигатель в одну сторону; другая диагональ — в другую. Два ключа одной стороны вместе — сквозной ток через мост, поэтому между ними — защитный интервал (57.6 и ниже в 57а.6).

H-мост из четырёх ключей: диагонали задают направление тока через двигатель, стойки требуют защитного интервала

Модель. Электрическая часть: U = R·i + L·di/dt + K·ω, где K·ω — противо-ЭДС. Механическая: J·dω/dt = K·i − b·ω. Возьмём условный двигатель на 12 В с той же обмоткой, что в разделе 57 (R = 2 Ом, L = 1 мГн), K = 0,02 В·с/рад, момент инерции ротора J = 10⁻⁵ кг·м² и трение b = 10⁻⁶ Н·м·с. Программа интегрирует уравнения шагом 1 мкс:

Величина Результат Откуда
ток остановленного двигателя 6 А U / R
пик тока при пуске 5,78 А через 2,34 мс ротор уже начал вращаться, противо-ЭДС растёт
скорость холостого хода 597 рад/с = 5701 об/мин U·K / (R·b + K²)
время до 63 % скорости 49,7 мс механическая постоянная J·R / K² = 50 мс
ток на холостом ходу 0,03 А только на трение

Главный вывод: при пуске двигатель — почти короткое замыкание. Ток пуска почти в 200 раз больше тока холостого хода, и драйвер, блок питания и дорожки считают на него, а не на паспортный рабочий ток. Или ограничивают его — плавным нарастанием скважности.

Ток и скорость коллекторного двигателя: пуск подачей 12 В сразу и с нарастанием скважности за 100 мс

Плавный пуск в той же модели. Скважность растёт от 0 до 100 % за 100 мс (ШИМ усреднён): пик тока — 2,61 А в конце разгона, в 2,2 раза меньше. Пока напряжение растёт с постоянной скоростью, ток идёт только на разгон ротора: J·(dU/dt) / K² = 3 А, и выходит на это значение с механической постоянной 50 мс — к 100 мс как раз 2,6 А. Цена — время: к 250 мс скорость 585 рад/с против 593 при пуске в лоб.

Торможение. Выключить все ключи — двигатель выбегает сам. Замкнуть обмотку двумя нижними ключами — противо-ЭДС гонит ток через замкнутую обмотку, и двигатель тормозится (энергия уходит в тепло обмотки).


57а.3 Шаговый: шаги, полушаги, микрошаги

Биполярный шаговый двигатель — две обмотки, A и B; ротор становится по полю, которое они создают. Меняя токи в определённом порядке, поворачивают поле — и ротор идёт за ним шагами. Типичный двигатель — 1,8° на шаг, 200 шагов на оборот.

Режим Токи (A, B) по порядку Шагов на оборот Что с моментом
полный шаг (+,+) (−,+) (−,−) (+,−) 200 обе обмотки всегда включены: поле в √2 = 1,41 раза сильнее одной
полушаг (+,0) (+,+) (0,+) (−,+) (−,0) (−,−) (0,−) (+,−) 400 через шаг работает одна обмотка — момент «пляшет»
микрошаг 1/16 A = cos, B = sin, 16 ступеней на четверть 3200 поле одной силы при любом угле — ход плавный, тихий

Таблица микрошага 1/16 на четверть поворота поля для 8-битного задания тока (255 — полный ток):

255/0 254/25 250/50 244/74 236/98 225/120 212/142 197/162 180/180
162/197 142/212 120/225 98/236 74/244 50/250 25/254 0/255

Числа таблицы — задание тока обмотки. Держит его драйвер с регулированием тока (раздел 57е): питание берут в разы выше номинального напряжения обмотки, а ключи ограничивают ток на заданном уровне в каждом периоде ШИМ. С простым ШИМ по напряжению ток на скорости не успевает нарастать, момент падает — отсюда пропуски шагов на высоких оборотах.

Сила поля √(A² + B²) на всех 17 точках — от 0,998 до 1,002 полной: округление до 8 бит почти не портит её. В полушаговом режиме она прыгает между 1 и 1,41 — отсюда рывки и шум.

Микрошаг — это не точность позиционирования, а плавность: ротор под нагрузкой отстаёт от поля, и промежуточные положения держатся слабее полных шагов.


57а.4 Разгон шагового без корней

Шаговый нельзя сразу запустить на полной скорости — ротор не успеет за полем и начнёт пропускать шаги. Разгоняют с постоянным ускорением α (шагов/с²). Тогда n-й шаг наступает в момент tₙ = √(2n/α), и интервал между шагами cₙ = tₙ − tₙ₋₁. Считать корень на каждом шаге в прерывании дорого; есть рекуррентная формула:

/* при старте, один раз: интервал в тиках таймера, 8 бит дроби */
uint32_t c = (uint32_t)(F_TIMER * 0.676 * sqrt(2.0 / ALPHA) * 256.0);
TIMx->ARR = ((c + 128u) >> 8) - 1u;

/* в прерывании по шагу n = 1, 2, …: одно деление и одно вычитание */
uint32_t d = 4u * n + 1u;
c -= (2u * c + d / 2u) / d;
TIMx->ARR = ((c + 128u) >> 8) - 1u;

Корень считается один раз — для первого интервала, и тот берут с поправкой 0,676: без неё рекуррентная формула сильно ошибается на первых шагах.

Проверка при α = 2000 шагов/с²:

Шаг Формула, с Точно, с Ошибка
2 0,0128262 0,0130986 2,08 %
10 0,0051292 0,0051317 0,05 %
100 0,0015852 0,0015851 0,003 %
1000 0,0005001 0,0005001 0,003 %

Худшая ошибка — на втором шаге, 2,08 %; к десятому — сотые доли процента. Через 1000 шагов скорость — 1999,4 шага/с при точных 2000. Листинг выше считает то же в целых тиках таймера 1 МГц: на 1000-м шаге 500 тиков при точных 500,1. Восемь бит дроби у c нужны: без них округление копится, и к 1000-му шагу ошибка доходит до 20 %.


57а.5 Бесколлекторный: коммутация по датчикам Холла

Шестишаговая коммутация: коды датчиков Холла, включённые фазы и опережение поля

У BLDC обмотки на статоре, магниты на роторе, и переключать ток должен контроллер. Проще всего — шесть шагов: в каждый момент одна фаза подключена к «+», другая к «−», третья свободна. Какие именно — говорят три датчика Холла, стоящие через 120°: их код — номер сектора.

Все углы здесь и дальше — электрические. У двигателя с p парами полюсов один электрический оборот — 1/p оборота вала, и шесть секторов повторяются p раз за оборот. На двигателе с четырьмя парами полюсов датчики «через 120°» могут стоять через 30° по валу, сдвиг «30°» — это 7,5° поворота вала, а обороты вала в секунду — частота смены секторов, делённая на 6p.

Модель в программе: оси обмоток A, B, C — 0°, 120°, 240°. Ток «в X, из Y» даёт поле в направлении (ось X − ось Y) — шесть векторов через 60°: A−C 30°, B−C 90°, B−A 150°, C−A 210°, C−B 270°, A−B 330°. Для каждого положения ротора (шаг 1°) выбирается вектор на 90° впереди середины сектора. Результат:

Сектор Холл C B A Код «+» «−» Свободна
0 1 0 1 5 B A C
1 0 0 1 1 C A B
2 0 1 1 3 C B A
3 0 1 0 2 A B C
4 1 1 0 6 A C B
5 1 0 0 4 B C A

Что показала модель:

В реальной прошивке таблица «код → ключи» — массив из 8 элементов, индекс — код датчиков. Направление меняется сдвигом таблицы на три сектора.


57а.6 TIM1 для двигателя

Всё перечисленное STM32 делает таймером TIM1 (и TIM8 у старших моделей), а не программой:

Дополняющие выходы и защитный интервал. Каналы CH1…CH3 и CH1N…CH3N управляют верхним и нижним ключом каждого полумоста. Между выключением одного и включением другого таймер сам вставляет паузу — защитный интервал, заданный 8-битным полем DTG регистра BDTR. Кодировка — четыре диапазона:

DTG[7:5] Интервал
0xx DTG × tDTS
10x (64 + DTG[5:0]) × 2tDTS
110 (32 + DTG[4:0]) × 8tDTS
111 (32 + DTG[4:0]) × 16tDTS

Функция из программы выдаёт при tDTS = 125 нс (8 МГц) границы диапазонов 15 875, 16 000…31 750, 32 000…63 000 и 64 000…126 000 нс — ровно пример из справочного руководства. На 72 МГц (tDTS = 13,9 нс):

Нужно Код Получится
100 нс 0x08 111,1 нс
500 нс 0x24 500 нс
1 мкс 0x48 1000 нс
2 мкс 0x88 2000 нс
5 мкс 0xCD 5000 нс

Больше 14 мкс на 72 МГц не получить (код 0xFF); длиннее — через делитель CKD в CR1, который увеличивает tDTS.

Аварийный вход BKIN. Включается битом BKE в BDTR, полярность — BKP. Активный уровень на входе аппаратно и асинхронно сбрасывает MOE, и все выходы уходят в безопасное состояние — без участия процессора. К BKIN подключают компаратор тока или сигнал аварии драйвера.

Датчики Холла. Второй таймер (TIM2…TIM5) с битом TI1S в CR2 принимает три датчика через «исключающее ИЛИ» на вход TI1: любое изменение кода сбрасывает счётчик и защёлкивает время между изменениями — это и есть скорость. Тот же таймер может через TRGO выдать TIM1 событие коммутации COM, и TIM1 переключит выходы на следующий сектор сам.

Энкодер — режим энкодера таймера общего назначения (раздел 13.7).


57а.7 Векторное управление в двух словах

Шесть шагов дают момент с пульсациями: поле прыгает на 60°. Плавный момент даёт векторное управление (FOC): токи всех трёх фаз синусоидальные, поле всегда на 90° впереди ротора. Прошивка на каждом периоде ШИМ:

  1. измеряет токи фаз (достаточно двух — сумма трёх равна нулю);
  2. преобразование Кларк: три тока → два (α, β) в неподвижных осях: α = ia, β = (ia + 2·ib) / √3;
  3. преобразование Парка: поворот на угол ротора θ → d = α·cosθ + β·sinθ, q = −α·sinθ + β·cosθ. В осях ротора ток — постоянные величины: q создаёт момент, d должен быть нулём;
  4. два ПИ-регулятора (раздел 47) держат d = 0 и q = заданному;
  5. обратные преобразования и ШИМ трёх фаз.

Проверка в программе: три синусоидальных тока амплитудой 1 с полем на 90° впереди ротора, угол ротора — через каждые 5°. После Кларк и Парка d не отходит от нуля и q от единицы больше чем на 7·10⁻¹⁶ — точность вычислений double. Так и должно быть: в осях ротора вращающееся поле неподвижно, и регулятору остаётся работа с постоянными величинами.

Для FOC нужен угол ротора — энкодер или оценка по току и напряжению — и быстрый процессор с аппаратной плавающей точкой; у ST для этого есть серия G4 и готовая библиотека управления двигателями.


57а.8 Грабли

Сгорел H-мост при смене направления. Два ключа одной стороны оказались включены одновременно. Защитный интервал и перебор направлений через состояние «всё выключено».

Драйвер уходит в защиту при пуске. Пусковой ток в разы больше рабочего — в примере 5,8 А против 0,03 А на холостом ходу. Плавный пуск скважностью.

Шаговый пропускает шаги на разгоне. Ускорение больше, чем тянет двигатель с нагрузкой. Меньше α или больше ток.

Разгон «дёргается» в начале. Первый интервал без поправки 0,676 — рекуррентная формула на первых шагах сильно врёт.

Шаговый греется, стоя на месте. Ток удержания — полный. Снижать ток при остановке, если нагрузка позволяет.

BLDC дёргается и не набирает обороты. Датчики сдвинуты не так или перепутан порядок фаз: опережение уходит из 60…120°. Проверяют, прокручивая ротор рукой и сверяя коды с таблицей.

Код датчиков 0 или 7. Такого не бывает на исправных датчиках — обрыв или замыкание. Останавливать двигатель.

Защитный интервал «не такой». Код DTG выше 0x7F — уже другой диапазон с другим множителем. Считать по таблице, а не как DTG × tDTS.

Выходы TIM1 молчат после аварии. Сработал BKIN и сбросил MOE. Вернуть его может только программа (или автоматически, если включён AOE).


57а.9 Итоги

Полные исходники

motor.c — расчёты раздела на компьютере: защитный интервал, микрошаг, разгон, коммутация BLDC, Кларк и Парк, пуск коллекторного двигателя.

/* Раздел 57а «Двигатели»: расчёты, которые прошивка делает сама.
 * Собирается обычным gcc на компьютере: gcc -O2 -o motor motor.c -lm && ./motor
 *
 *  1. защитный интервал TIM1: код DTG по времени и обратно (формула справочного руководства F1);
 *  2. шаговый двигатель: полный шаг, полушаг, микрошаг 1/16 — таблица токов;
 *  3. разгон шагового: интервалы между шагами без корня, рекуррентной формулой;
 *  4. BLDC: шестишаговая коммутация по датчикам Холла;
 *  5. преобразования Кларк и Парка;
 *  6. коллекторный двигатель: пуск от 12 В — ток и скорость по уравнениям;
 *  7. тот же пуск с нарастанием скважности за 100 мс — журнал dc_start.csv для рисунка.
 */
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

/* ── 1. DTG ─────────────────────────────────────────────────────────────── */
/* Время защитного интервала для кода DTG при такте tDTS (нс), четыре диапазона. */
static double dtg_to_ns(uint8_t dtg, double tdts)
{
    if ((dtg & 0x80) == 0)
        return dtg * tdts;                              /* 0xx: DTG × tDTS */
    if ((dtg & 0xC0) == 0x80)
        return (64 + (dtg & 0x3F)) * 2 * tdts;          /* 10x: (64 + DTG[5:0]) × 2tDTS */
    if ((dtg & 0xE0) == 0xC0)
        return (32 + (dtg & 0x1F)) * 8 * tdts;          /* 110: (32 + DTG[4:0]) × 8tDTS */
    return (32 + (dtg & 0x1F)) * 16 * tdts;             /* 111: (32 + DTG[4:0]) × 16tDTS */
}

/* Наименьший код, дающий интервал не короче нужного. */
static int ns_to_dtg(double ns, double tdts)
{
    for (int d = 0; d < 256; d++) {
        /* внутри диапазона время растёт с кодом, между диапазонами — тоже */
        if (dtg_to_ns((uint8_t)d, tdts) >= ns - 1e-9)
            return d;
    }
    return -1;
}

/* ── 2. шаговый ─────────────────────────────────────────────────────────── */
static void stepper_tables(void)
{
    /* полный шаг, две обмотки включены: направление тока в A и B */
    static const int full[4][2] = {{+1, +1}, {-1, +1}, {-1, -1}, {+1, -1}};
    /* полушаг: между двумя «двумя включёнными» — одна включена */
    static const int half[8][2] = {{+1, 0}, {+1, +1}, {0, +1}, {-1, +1}, {-1, 0}, {-1, -1}, {0, -1}, {+1, -1}};
    printf("full:");
    for (int i = 0; i < 4; i++)
        printf(" %+d%+d", full[i][0], full[i][1]);
    printf("\nhalf:");
    for (int i = 0; i < 8; i++)
        printf(" %+d%+d", half[i][0], half[i][1]);
    printf("\n");
    /* микрошаг 1/16 на четверть оборота поля: ток A = cos, B = sin, 8 бит (255 — полный ток) */
    double min_mag = 1e9, max_mag = 0;
    printf("micro16:");
    for (int k = 0; k <= 16; k++) {
        double a = k * (M_PI / 2) / 16;
        int ia = (int)lround(255 * cos(a)), ib = (int)lround(255 * sin(a));
        double mag = sqrt((double)ia * ia + (double)ib * ib) / 255;
        if (mag < min_mag) min_mag = mag;
        if (mag > max_mag) max_mag = mag;
        printf(" %d/%d", ia, ib);
    }
    printf("\nmicro16_mag_min=%.4f max=%.4f\n", min_mag, max_mag);
    /* амплитуда тока при полном шаге с двумя обмотками — √2 от тока одной */
    printf("full_two_phase_mag=%.4f\n", sqrt(2.0));
    /* 1,8° на шаг → шагов на оборот; 1/16 — микрошагов */
    printf("steps_per_rev=%d micro16_per_rev=%d\n", (int)lround(360 / 1.8), (int)lround(360 / 1.8) * 16);
}

/* ── 3. разгон ──────────────────────────────────────────────────────────── */
/* Равноускоренное движение: n-й шаг случается в момент t_n = √(2n/α) (α — шагов/с²).
 * Интервал c_n = t_n − t_(n−1). Без корня: c_n = c_(n−1) − 2c_(n−1)/(4n+1), а первый
 * интервал берут с поправкой 0,676, чтобы ряд совпал с точным уже со второго шага. */
static void accel_profile(void)
{
    const double alpha = 2000.0;                        /* шагов в секунду за секунду */
    double c0 = 0.676 * sqrt(2 / alpha);
    double c = c0, t = 0, worst = 0;
    int worst_n = 0;
    printf("accel c0_exact=%.6f c0=%.6f\n", sqrt(2 / alpha), c0);
    for (int n = 1; n <= 1000; n++) {
        if (n > 1)
            c = c - 2 * c / (4 * (n - 1) + 1);
        t += c;
        double exact = sqrt(2.0 * n / alpha) - sqrt(2.0 * (n - 1) / alpha);
        double err = fabs(c - exact) / exact;
        if (n >= 2 && err > worst) { worst = err; worst_n = n; }
        if (n == 2 || n == 10 || n == 100 || n == 1000)
            printf("accel n=%d c=%.7f exact=%.7f err=%.3f%%\n", n, c, exact, 100 * err);
    }
    printf("accel worst_err_from2=%.2f%% at n=%d; after 1000 steps speed=%.1f steps/s (exact %.1f)\n",
           100 * worst, worst_n, 1 / c, alpha * sqrt(2 * 1000 / alpha));
}

/* То же в целых числах — так считает прерывание по шагу. Тики таймера 1 МГц, у интервала 8 бит
 * дроби: без них ошибка округления копится и к 1000-му шагу доходит до 20 %. */
static struct { uint32_t ARR; } tim_step, *TIMx = &tim_step;

static void accel_int(void)
{
    const double ALPHA = 2000.0;                                     /* шагов в секунду за секунду */
    const uint32_t F_TIMER = 1000000u;                               /* такт таймера шага */
    double worst = 0;
    uint32_t worst_n = 0;
/* при старте, один раз: интервал в тиках таймера, 8 бит дроби */
uint32_t c = (uint32_t)(F_TIMER * 0.676 * sqrt(2.0 / ALPHA) * 256.0);
TIMx->ARR = ((c + 128u) >> 8) - 1u;
    for (uint32_t n = 1; n < 1000; n++) {
/* в прерывании по шагу n = 1, 2, …: одно деление и одно вычитание */
uint32_t d = 4u * n + 1u;
c -= (2u * c + d / 2u) / d;
TIMx->ARR = ((c + 128u) >> 8) - 1u;
        double exact = (sqrt(2.0 * (n + 1) / ALPHA) - sqrt(2.0 * n / ALPHA)) * F_TIMER;
        double err = fabs(TIMx->ARR + 1.0 - exact) / exact;
        if (err > worst) { worst = err; worst_n = n + 1; }
        if (n + 1 == 2 || n + 1 == 10 || n + 1 == 100 || n + 1 == 1000)
            printf("accel_int n=%u ticks=%u exact=%.1f err=%.3f%%\n", (unsigned)(n + 1), (unsigned)(TIMx->ARR + 1u), exact, 100 * err);
    }
    printf("accel_int worst=%.2f%% at n=%u\n", 100 * worst, (unsigned)worst_n);
}

/* ── 4. BLDC ────────────────────────────────────────────────────────────── */
/* Датчики Холла через 120°: код HA|HB<<1|HC<<2. Для каждого из шести секторов — какая фаза
 * подключена к «+», какая к «−», какая свободна. Проверка: поле статора опережает ротор
 * на 60…120° в любом секторе — момент всегда в одну сторону. */
#define HALL_OFFSET 30.0

static void bldc_six_step(void)
{
    const char *ph = "ABC";
    int seen[8] = {0};
    double worst_lo = 1e9, worst_hi = -1e9;
    for (int deg = 0; deg < 360; deg++) {
        double th = deg + 0.5;                                   /* угол ротора, электрические градусы */
        /* датчики стоят через 120°, но сдвинуты на 30° от осей обмоток: иначе граница сектора
         * пришлась бы на середину вектора поля и опережение гуляло бы 30…150° */
        double r = fmod(th - HALL_OFFSET + 360, 360);
        int ha = r < 180;
        int hb = fmod(r - 120 + 360, 360) < 180;
        int hc = fmod(r - 240 + 360, 360) < 180;
        int code = ha | hb << 1 | hc << 2;
        seen[code]++;
        int sector = (int)(r / 60);                              /* 0…5 */
        /* поле статора — на 90° впереди середины сектора */
        double field = HALL_OFFSET + sector * 60 + 30 + 90;
        /* из шести векторов «+X −Y» выбираем ближайший к нужному полю. Оси обмоток A, B, C —
         * 0°, 120°, 240°; ток «в X, из Y» даёт поле по направлению (ось X − ось Y):
         * A−C 30°, B−C 90°, B−A 150°, C−A 210°, C−B 270°, A−B 330° */
        static const struct { int hi, lo; double ang; } vec[6] = {
            {0, 2, 30}, {1, 2, 90}, {1, 0, 150}, {2, 0, 210}, {2, 1, 270}, {0, 1, 330}};
        int best = 0;
        double bd = 1e9;
        for (int v = 0; v < 6; v++) {
            double d = fabs(fmod(vec[v].ang - field + 540, 360) - 180);
            if (d < bd) { bd = d; best = v; }
        }
        double lead = fmod(vec[best].ang - th + 720, 360);
        if (lead < worst_lo) worst_lo = lead;
        if (lead > worst_hi) worst_hi = lead;
        if (fmod(r, 60) < 1)
            printf("bldc sector=%d hall=%d%d%d code=%d +%c -%c float=%c\n", sector, hc, hb, ha, code,
                   ph[vec[best].hi], ph[vec[best].lo], ph[3 - vec[best].hi - vec[best].lo]);
    }
    printf("bldc codes_seen:");
    for (int c = 0; c < 8; c++)
        printf(" %d:%d", c, seen[c]);
    printf("\nbldc lead_min=%.1f lead_max=%.1f\n", worst_lo, worst_hi);
}

/* ── 5. Кларк и Парк ────────────────────────────────────────────────────── */
static void clarke_park(void)
{
    double worst_d = 0, worst_q = 0;
    for (int deg = 0; deg < 360; deg += 5) {
        double th = deg * M_PI / 180;
        /* три фазных тока амплитудой 1, ротор под углом th, ток «по q» — на 90° впереди */
        double ia = cos(th + M_PI / 2), ib = cos(th + M_PI / 2 - 2 * M_PI / 3), ic = cos(th + M_PI / 2 + 2 * M_PI / 3);
        double alpha = ia;                                         /* Кларк, амплитудно-инвариантный */
        double beta = (ia + 2 * ib) / sqrt(3.0);
        double d = alpha * cos(th) + beta * sin(th);               /* Парк */
        double q = -alpha * sin(th) + beta * cos(th);
        if (fabs(d) > worst_d) worst_d = fabs(d);
        if (fabs(q - 1) > worst_q) worst_q = fabs(q - 1);
        (void)ic;
    }
    printf("park max|d|=%.2e max|q-1|=%.2e\n", worst_d, worst_q);
}

/* ── 6. коллекторный: пуск ──────────────────────────────────────────────── */
static void dc_motor(void)
{
    const double V = 12, R = 2, L = 1e-3, K = 0.02, J = 1e-5, B = 1e-6;
    double i = 0, w = 0, dt = 1e-6, t = 0, ipk = 0, t_ipk = 0, t63 = -1;
    double w_ss = V * K / (R * B + K * K);                        /* установившаяся скорость без нагрузки */
    while (t < 0.5) {
        double di = (V - R * i - K * w) / L;
        double dw = (K * i - B * w) / J;
        i += di * dt;
        w += dw * dt;
        t += dt;
        if (i > ipk) { ipk = i; t_ipk = t; }
        if (t63 < 0 && w >= 0.632 * w_ss) t63 = t;
    }
    printf("dc stall=%.2f A ipk=%.2f A at %.2f ms; w_ss=%.1f rad/s = %.0f rpm; t63=%.1f ms; tau_m=%.1f ms; i_end=%.3f A\n",
           V / R, ipk, t_ipk * 1e3, w_ss, w_ss * 60 / (2 * M_PI), t63 * 1e3, J * R / (K * K) * 1e3, i);
}

/* ── 7. пуск «в лоб» и плавный: скважность от 0 до 100 % за 100 мс (ШИМ усреднён) ──
 * Журнал dc_start.csv — для рисунка: время, мс; ток и скорость при пуске в лоб; то же при плавном. */
static void dc_soft_start(void)
{
    const double V = 12, R = 2, L = 1e-3, K = 0.02, J = 1e-5, B = 1e-6, RAMP = 0.1;
    double i1 = 0, w1 = 0, i2 = 0, w2 = 0, dt = 1e-6, ipk2 = 0, t_ipk2 = 0;
    FILE *f = fopen("dc_start.csv", "w");
    for (long n = 0; n <= 250000; n++) {
        double t = n * dt;
        if (f && n % 500 == 0)
            fprintf(f, "%.1f,%.4f,%.2f,%.4f,%.2f\n", t * 1e3, i1, w1, i2, w2);
        double u2 = V * (t < RAMP ? t / RAMP : 1.0);
        double di1 = (V - R * i1 - K * w1) / L, dw1 = (K * i1 - B * w1) / J;
        double di2 = (u2 - R * i2 - K * w2) / L, dw2 = (K * i2 - B * w2) / J;
        i1 += di1 * dt;
        w1 += dw1 * dt;
        i2 += di2 * dt;
        w2 += dw2 * dt;
        if (i2 > ipk2) { ipk2 = i2; t_ipk2 = t + dt; }
    }
    if (f)
        fclose(f);
    printf("dc_soft ramp=100 ms ipk=%.2f A at %.1f ms; w_250ms=%.1f rad/s (direct %.1f)\n", ipk2, t_ipk2 * 1e3, w2, w1);
}

int main(void)
{
    const double tdts72 = 1e3 / 72.0;                              /* нс: такт 72 МГц, CKD = 00 */
    const double tdts8 = 125.0;
    printf("dtg8 max: %.0f %.0f %.0f %.0f %.0f %.0f %.0f\n", dtg_to_ns(0x7F, tdts8), dtg_to_ns(0x80, tdts8),
           dtg_to_ns(0xBF, tdts8), dtg_to_ns(0xC0, tdts8), dtg_to_ns(0xDF, tdts8), dtg_to_ns(0xE0, tdts8), dtg_to_ns(0xFF, tdts8));
    int ns_list[] = {100, 500, 1000, 2000, 5000};
    for (unsigned k = 0; k < sizeof ns_list / sizeof ns_list[0]; k++) {
        int d = ns_to_dtg(ns_list[k], tdts72);
        printf("dtg72 want=%d ns -> DTG=0x%02X = %.1f ns\n", ns_list[k], d, dtg_to_ns((uint8_t)d, tdts72));
    }
    printf("dtg72 max=%.0f ns\n", dtg_to_ns(0xFF, tdts72));
    stepper_tables();
    accel_profile();
    accel_int();
    bldc_six_step();
    clarke_park();
    dc_motor();
    dc_soft_start();
    return 0;
}

Дальше: раздел 57б «Ток двигателя: шунт, усилитель, выборка в нужный момент ШИМ» — куда ставить шунт, как усилить его сигнал и в какой момент периода ШИМ брать отсчёт.

Открыть в интерактивном справочнике