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).
Модель. Электрическая часть: 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 раз больше тока холостого хода, и драйвер, блок питания и дорожки считают на него, а не на паспортный рабочий ток. Или ограничивают его — плавным нарастанием скважности.
Плавный пуск в той же модели. Скважность растёт от 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 |
Что показала модель:
- поле опережает ротор на 60…120° в любом положении — момент всегда в одну сторону, максимум — при 90°;
- коды 0 и 7 не встречаются ни разу: все датчики в одном состоянии — это обрыв или замыкание проводов датчиков. Прошивка, увидев 0 или 7, должна остановить двигатель;
- датчики должны стоять со сдвигом 30° от осей обмоток. При датчиках прямо на осях граница сектора попадает на середину вектора, и опережение гуляет от 30 до 150° — момент проседает вдвое.
В реальной прошивке таблица «код → ключи» — массив из 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° впереди ротора. Прошивка на каждом периоде ШИМ:
- измеряет токи фаз (достаточно двух — сумма трёх равна нулю);
- преобразование Кларк: три тока → два (α, β) в неподвижных осях:
α = ia,β = (ia + 2·ib) / √3; - преобразование Парка: поворот на угол ротора θ →
d = α·cosθ + β·sinθ,q = −α·sinθ + β·cosθ. В осях ротора ток — постоянные величины:qсоздаёт момент,dдолжен быть нулём; - два ПИ-регулятора (раздел 47) держат
d = 0иq= заданному; - обратные преобразования и ШИМ трёх фаз.
Проверка в программе: три синусоидальных тока амплитудой 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 Итоги
- Коллекторный — H-мост и ШИМ; при пуске почти короткое замыкание: 6 А у двигателя, который на холостом ходу берёт 0,03 А.
- Шаговый — 200 шагов на оборот при 1,8°; микрошаг 1/16 даёт 3200 положений и поле постоянной силы (0,998…1,002).
- Разгон — рекуррентная формула с одним корнем на весь разгон и поправкой 0,676 к первому интервалу; ошибка со второго шага не больше 2,08 %.
- BLDC — шесть секторов по кодам Холла 1…6; поле опережает ротор на 60…120°, если датчики сдвинуты на 30° от осей; коды 0 и 7 — авария.
- TIM1:
DTGв четырёх диапазонах (на 72 МГц до 14 мкс),BKINаппаратно сбрасываетMOE, датчики Холла — черезTI1Sвторого таймера. - FOC: Кларк и Парк превращают вращающиеся токи в постоянные d и q.
Полные исходники
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б «Ток двигателя: шунт, усилитель, выборка в нужный момент ШИМ» — куда ставить шунт, как усилить его сигнал и в какой момент периода ШИМ брать отсчёт.