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

57к. Шаговик 28BYJ-48 и ULN2003: полношаговый и полушаговый режимы, разгон по таймеру

В 57а шаговый двигатель был биполярным: две обмотки, ток в каждую — в обе стороны, значит, на обмотку нужен мост. В 57е этот ток держал чоппер, в 57ж крошечный стрелочный двигатель обошёлся вообще без драйвера. Здесь — самый дешёвый шаговик из наборов: мотор-редуктор 28BYJ-48 на 5 В с платой на ULN2003. Он униполярный: у каждой обмотки есть средний вывод, и ток в половину обмотки всегда течёт в одну сторону. Мост не нужен — хватает четырёх ключей на землю.

Что внутри: как устроены четыре фазы и общий провод; почему у редуктора передаточное число 63,68, а не 64, и куда уходит ошибка, если считать 4096 полушагов на оборот; три таблицы коммутации и почему все четыре вывода надо менять одной записью в BSRR; что такое ULN2003 и сколько на ней теряется; до какой частоты мотор трогается с места без разгона и как выглядит пропуск шага; трапеция разгона по прерыванию TIM2 с таблицей интервалов; когда держать ток, а когда снимать, и что делать с люфтом.

Опоры: RM0008 (DocID13902 Rev 17): атомарная установка и сброс выводов, 9.1.2, с. 160; регистр GPIOx_BSRR, с. 172. DocID13587 Rev 17 (STM32F103x8/xB): PB12–PB15 с допуском 5 В, с. 30; ток вывода 25 мА и 150 мА на все выводы, таблица 7, с. 37. AN3208 Rev 1 (Doc ID 17411), с. 5: униполярная обмотка — две катушки со средним выводом, у биполярной момент больше, потому что ток идёт по всей катушке. Харрис и Харрис «Цифровая схемотехника и архитектура компьютера», с. 330–332: униполярный двигатель управляется простыми ключами, волновой, двухфазный и полушаговый режимы, ток при питании напрямую не успевает нарасти на большой скорости. Драйвер Zephyr 4.4.2 drivers/stepper/gpio_stepper/h_bridge_stepper_ctrl.c — та же таблица полушага на 8 тактов, полный шаг — через такт; в описании привязки — «сборки транзисторов Дарлингтона». Документов на 28BYJ-48 и на ULN2003 в проекте нет: числа зубьев редуктора, 50 Ом и 5 В — типовые, так их приводят продавцы и разборы, не проверено. Все числа ниже — прогон драйвера _verify/step57k/byj57k.c, собранного arm-none-eabi-gcc 12.2 под Cortex-M3 и запущенного в QEMU 7.2, и модель мотора на Python 3.11, которая крутит ротор этой же последовательностью фаз. Канал ULN2003 — в ngspice-39. Параметры ротора и обмотки в модели условные; важны не сами частоты, а соотношения между режимами.

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

Оригинал Перевод
unipolar, bipolar stepper униполярный (половины обмотки со средним выводом), биполярный шаговый
centre tap, common wire средний вывод, общий провод
gear ratio, gearbox, backlash передаточное число, редуктор, люфт
stride angle угол шага выходного вала
wave drive, two-phase on (full step), half step волновой режим, полный шаг (две фазы), полушаг
Darlington transistor array сборка составных транзисторов Дарлингтона
clamp (freewheeling) diode, COM диод для тока обмотки после выключения, общий катод диодов
saturation voltage VCE(sat) напряжение на открытом ключе
atomic bit set/reset, GPIOx_BSRR атомарная установка и сброс выводов
start-stop (pull-in) rate частота, с которой мотор трогается без разгона
pull-out rate частота, на которой разогнанный мотор срывается
trapezoidal velocity profile трапеция скорости: разгон, ход, торможение
holding torque момент удержания
missed (lost) step пропуск шага

57к.1 Мотор, плата и редуктор: 63,68, а не 64

В корпусе 28BYJ-48 две обмотки, у каждой средний вывод. Средние выводы соединены и выведены общим проводом на +5 В. Остальные четыре конца — это четыре фазы A, B, C, D, по одной на ключ ULN2003: включить ключ — пустить ток через половину обмотки на землю. A и C — половины одной обмотки, их поля направлены навстречу; B и D — другой. Включить A и C вместе бессмысленно: поля гасят друг друга.

Слева — подключение: выводы PB12…PB15 STM32F103 идут на входы IN1…IN4 ULN2003, выходы OUT1…OUT4 — на полуобмотки A, B, C, D, общий провод мотора и COM ULN2003 (катоды диодов) — на +5 В; вход канала от 3,3 В берёт 0,68 мА, открытый ключ — 0,79 В при 84 мА по SPICE. Справа — 8 тактов полушага: A, AB, B, BC, C, CD, D, DA; чётные такты — волновой режим, нечётные — полный шаг, A и C — половины одной обмотки. Внизу слева — редуктор 32/9 · 22/11 · 26/9 · 31/10 = 63,684, 4075,77 полушага на оборот выхода, при 4096 — плюс 1,787° за оборот и 178,7° за 100 оборотов. Внизу справа — трапеция разгона: с 300 до 1000 полушагов/с за 228 шагов, интервалы из таблицы 3333…1001 мкс, разгон 0,35 с, торможение — та же таблица с конца

Ротор — постоянный магнит. Полный оборот поля — четыре полных шага, а на оборот ротора их 32: в модели это восемь пар полюсов, 64 полушага на оборот ротора. Дальше — четыре ступени редуктора. Часто пишут «64:1» и «4096 полушагов на оборот выхода», но по числам зубьев, которые приводят разборы этого редуктора, выходит другое:

Ступень 1 2 3 4 Всего
зубья 32 / 9 22 / 11 26 / 9 31 / 10 283712 / 4455
передаточное 3,556 2 2,889 3,1 63,684

Точное число — 63,68395. На оборот выходного вала — 4075,7728 полушага, или 2037,8864 полного шага; один полушаг поворачивает выход на 0,08833°. Целого числа шагов на оборот нет вовсе.

Куда уходит ошибка. Если считать 4096 полушагов на оборот, каждый «оборот» на деле длиннее на 1,787°. Ошибка копится:

Оборотов Полушагов «по 4096» Ошибка Точная цель Ошибка
1 4096 1,787° 4076 0,020°
10 40960 17,866° 40758 0,024°
100 409600 178,660° 407577 −0,025°

За сотню оборотов стрелка, барабан или задвижка уходят на полоборота. Округлить до 4076 и прибавлять его каждый оборот — лучше, но тоже копит: +0,0201° за оборот, 2,01° за сотню. Правильно — считать цель от абсолютного угла, рациональным числом, и только потом округлять:

/* Угол выходного вала в сотых долях градуса → полушаги, с округлением, без накопления ошибки:
 * 4075,77 полушага на оборот = 64 · 283712 / 4455. */
int32_t byj_cdeg_to_half(int32_t cdeg)
{
    int64_t num = (int64_t)cdeg * (int64_t)(BYJ_HALF_ROTOR * BYJ_GEAR_NUM);
    int64_t den = (int64_t)BYJ_GEAR_DEN * 36000;
    return (int32_t)(num >= 0 ? (num + den / 2) / den : -((-num + den / 2) / den));
}

1° — 11 полушагов, 90° — 1019, оборот — 4076, сто оборотов — 407577; ошибка каждой цели меньше половины полушага, сколько бы оборотов ни было до неё. Произведение 18 157 568 на угол в сотых градуса в 32 бита не влезает — отсюда int64_t. Если у вашего экземпляра другие шестерни, меняются две константы BYJ_GEAR_NUM и BYJ_GEAR_DEN; проверить просто — сто оборотов и метка на валу.


57к.2 Три таблицы тактов и одна запись в BSRR

Порядок тактов задаёт направление поля. В модели поле каждой фазы повёрнуто на 90° электрических: A — 0°, B — 90°, C — 180°, D — 270°. Таблица полушага — восемь тактов, поле на каждом поворачивается на 45°:

Такт 0 1 2 3 4 5 6 7
фазы A AB B BC C CD D DA
маска IN4…IN1 0001 0011 0010 0110 0100 1100 1000 1001
режим волновой полный волновой полный волновой полный волновой полный

Чётные такты — волновой режим: одна фаза, четыре такта на оборот поля. Нечётные — полный шаг: две соседние фазы, поле посередине между ними и в √2 раза сильнее. Все восемь подряд — полушаг: шаг вдвое мельче, но момент гуляет между одной фазой и двумя. Так же устроен драйвер Zephyr: одна таблица на 8 тактов, полный шаг — через такт (lut-step-gap = 2). Порядок фаз A, B, C, D у него тот же.

/* Полушаг, 8 тактов; бит 0 — IN1 (A), бит 1 — IN2 (B), бит 2 — IN3 (C), бит 3 — IN4 (D).
 * Чётные такты — одна фаза (волновой режим), нечётные — две (полный шаг). */
static const uint8_t half_tab[8] = {0x1u, 0x3u, 0x2u, 0x6u, 0x4u, 0xCu, 0x8u, 0x9u};

Положение драйвер держит в полушагах в любом режиме: в полушаговом шаг — ±1, в остальных — ±2. Поэтому режим можно менять между ходами, не теряя счёта.

Одна запись на такт. На полушаге от такта к такту меняется один вывод, на полном шаге и в волновом — два. Если писать выводы по одному, как делает цикл по gpio_pin_set_dt в Zephyr или четыре вызова записи вывода, между записями порт проходит промежуточные состояния. Стенд записал такты по одному выводу в порядке IN1…IN4:

Режим Переходов С промежуточным состоянием Что в промежутке
полушаг 8 0 —
полный шаг 4 4 в двух из четырёх — обе половины одной обмотки (A + C + D, A + B + D)
волновой 4 4 в трёх — все фазы выключены, в одном — D и A

Обе половины одной обмотки разом — их поля гасят друг друга, и ток в этот миг идёт впустую. Длится это недолго, но такого состояния вообще не должно быть. Регистр BSRR решает это одной записью: младшие 16 бит ставят выводы, старшие — сбрасывают, и всё это — одно атомарное обращение по шине (RM0008, 9.1.2, с. 160), без запрета прерываний. Если в одном слове оказались и установка, и сброс одного вывода, побеждает установка (с. 172).

/* Одно слово BSRR: включённые фазы — в BS, выключенные — в BR. Все четыре вывода меняются одной записью. */
uint32_t byj_bsrr(uint8_t mask)
{
    uint32_t on = (uint32_t)(mask & 0xFu), off = (uint32_t)(~mask & 0xFu);
    return on << BYJ_PIN0 | off << (BYJ_PIN0 + 16u);
}

Для PB12…PB15 слова получаются такие: такт A — 0xE0001000, AB — 0xC0003000, всё выключено — 0xF0000000. Вторая польза атомарности: светодиод или другой вывод того же порта, который трогает основной цикл, прерывание шага не собьёт; с записью в ODR через «прочитать — изменить — записать» такое бывает.


57к.3 ULN2003: ключи Дарлингтона, диоды на COM, ток и тепло

ULN2003 — сборка составных транзисторов Дарлингтона (170а.1) с открытым коллектором: несколько каналов в одном корпусе, у каждого — резистор на входе и диод с выхода на общий вывод COM, эмиттеры — на землю (170а.8 советует такие сборки как раз для индуктивной нагрузки). Число каналов и цоколёвка — по документации производителя. Вход канала открывается от логической единицы, ключ тянет свою полуобмотку к земле. Внутреннюю схему канала модель берёт типовую: 2,7 кОм на входе, 7,2 кОм от базы первого транзистора на землю, 3 кОм — второго; номиналы по документации производителя, на диске её нет. Транзисторы — модель NPN из раздела 170.

Канал от вывода 3,3 В, полуобмотка 50 Ом от 5 В, по SPICE:

Что Значение
ток полуобмотки 84,2 мА
напряжение на открытом ключе 0,792 В
ток входа от вывода МК 0,683 мА
тепло на одном ключе 0,0667 Вт
то же, две фазы 0,133 Вт

Ключ не насыщается до нуля: составной транзистор держит на себе напряжение база-эмиттер второго транзистора (170а.1). При 50, 200, 300 и 500 мА модель даёт 0,769, 0,850, 0,894 и 0,980 В. Из 5 В обмотке достаётся 4,2 В — отсюда 84 мА вместо 100.

Вход ULN2003 вывод почти не грузит: 0,683 мА против 25 мА на вывод и 150 мА на все (DocID13587, таблица 7, с. 37). Выводы PB12…PB15 к тому же с допуском 5 В (с. 30), но здесь это не нужно — ULN2003 открывается от 3,3 В.

Диоды и COM. Полуобмотка — индуктивность. Когда ключ закрывается, ток не может оборваться сразу: он уходит через диод канала на COM. Если COM соединён с +5 В, выход поднимается чуть выше питания — в модели до 5,83 В, — и ток спадает до 10 % за 0,871 мс; постоянная времени полуобмотки L/R в модели — 0,60 мс. Без COM пути у тока нет, и напряжение на ключе растёт до пробоя (51.2 — гасящий диод у любой катушки): каждое выключение отдаёт в ключ ½·L·I² = 0,106 мДж, на 1000 полушагов/с — это 500 выключений в секунду. На готовой плате проверьте, куда идёт COM; на макетке его забывают.

Ток и тепло. Две фазы под током — 168,3 мА от источника 5 В: 0,708 Вт в обмотках и 0,133 Вт в ULN2003, всего 0,841 Вт. Одна фаза — вдвое меньше, 0,421 Вт. И это не только на ходу: стоящий мотор с включёнными фазами греется так же (57к.6).


57к.4 Частота шагов: где мотор срывается без разгона

Обмотка при питании прямо от 5 В — цепь L/R: ток нарастает не сразу, и на большой скорости не успевает дорасти до полного (Харрис и Харрис, с. 332). Ротору с редуктором ещё нужно время, чтобы догнать поле. Если с места сразу шагать часто, ротор не успевает, поле уходит вперёд больше чем на полоборота электрических, и ротор тянет назад. Мотор гудит и стоит.

Модель: полуобмотка 50 Ом и 30 мГн (индуктивность условная), ротор с постоянным магнитом, восемь пар полюсов, инерция, сухое и вязкое трение — условные; ключ — 0,792 В из SPICE, после выключения ток идёт через диод на COM. Драйвер шагает с места с постоянной частотой; после хода фазы держат ещё 100 мс, и положение ротора сравнивается с командой. Отставание меньше 2 полушагов — это трение, которое держит ротор чуть в стороне от такта, а не пропуск.

Режим Трогается Срывается Предельная скорость выхода
волновой 225 шагов/с 250 шагов/с 6,62 об/мин
полный шаг 300 шагов/с 350 шагов/с 8,83 об/мин
полушаг 550 полушагов/с 600 полушагов/с 8,10 об/мин
полушаг, 4,5 В 500 полушагов/с 550 полушагов/с 7,36 об/мин

Волновой режим слабее всех: одна фаза, момент в √2 раза меньше. Полный шаг и полушаг трогаются с места почти на одной скорости ротора, но у полушага шаг вдвое мельче. При 4,5 В вместо 5 — так бывает при питании с USB через диод платы — порог ниже: ток меньше, и момент тоже.

Как выглядит пропуск. Сорвавшийся с места мотор не проходит часть хода — он почти весь ход стоит. Полушаг на 600/с — из 256 полушагов не сделано 216. И ещё одно: во всех 21 срыве прогона отставание кратно 8 полушагам. Ротор проскальзывает не на шаг, а на целый оборот поля — четыре полных шага, — и встаёт в такой же такт. Поэтому прошивка о пропуске не узнает: такт тот же, счёт идёт дальше. Узнать можно только датчиком на валу — например, AS5600 из 57и.


57к.5 Трапеция разгона по прерыванию TIM2

Чтобы идти быстрее, чем мотор трогается с места, скорость наращивают постепенно — трапецией: разгон с постоянным ускорением, ход, торможение. Для постоянного ускорения a скорость на i-м шаге от начала разгона — v² = v0² + 2·a·i, интервал до следующего шага — 1/v. В 57а.4 интервалы считала рекуррентная формула с поправкой первого шага. Здесь — проще: таблица интервалов один раз при старте, а в прерывании — только чтение.

/* Трапеция: на i-м шаге разгона v² = v0² + 2·a·i (шагов/с, шагов/с²), интервал = 1 / v в мкс.
 * Корень считается здесь, один раз на шаг таблицы, — в прерывании только чтение таблицы. */
uint32_t byj_ramp_init(byj_ramp_t *r, uint32_t v0, uint32_t vmax, uint32_t acc)
{
    uint32_t i = 0;
    while (i < BYJ_RAMP_MAX) {
        uint64_t v2 = (uint64_t)v0 * v0 + 2u * (uint64_t)acc * i;
        uint32_t vq = isqrt64(v2 << 16);                  /* скорость × 256 */
        if (vq >= vmax * 256u)
            break;
        r->ramp[i++] = (uint16_t)((256000000u + vq / 2u) / vq);
    }
    r->n_acc = (uint16_t)i;
    r->c_run = (uint16_t)((1000000u + vmax / 2u) / vmax);
    if (i == BYJ_RAMP_MAX)
        r->c_run = r->ramp[i - 1u];                      /* таблица кончилась раньше vmax: дальше — её скорость */
    return i;
}

Прошивка: с 300 до 1000 полушагов/с, a = 2000 полушагов/с². Таблица — 228 интервалов, 456 байт: от 3333 до 1001 мкс, крейсерский — 1000 мкс. Модель сверила каждый интервал с точной формулой: расхождение не больше 0,502 мкс — это округление до целой микросекунды. Разгон по таблице — 0,3517 с, по формуле (vmax − v0)/a — 0,35 с. Последняя строка byj_ramp_init — страховка: если таблица кончилась раньше крейсерской скорости, ход продолжится со скоростью её конца, а не прыгнет к vmax.

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

/* Интервал перед (j+1)-м шагом: k — шагов до ближнего края хода; разгон и торможение — одна таблица. */
static uint32_t interval(const byj_t *m, uint32_t j)
{
    uint32_t k = m->total - 1u - j < j ? m->total - 1u - j : j;
    return k < m->r->n_acc ? m->r->ramp[k] : m->r->c_run;
}
/* Прерывание таймера: следующий такт, одна запись в порт, интервал до следующего шага (0 — ход окончен). */
uint32_t byj_isr(byj_t *m)
{
    if (!m->left)
        return 0u;
    uint8_t d = m->mode == BYJ_HALF ? 1u : 2u;
    m->idx = (uint8_t)((m->idx + (m->dir > 0 ? d : 8u - d)) & 7u);
    m->pos += m->dir * (int32_t)d;
    byj_port_write(byj_bsrr(byj_mask(m->idx)));
    m->done++;
    if (--m->left == 0u)
        return 0u;
    return interval(m, m->done);
}

TIM2 тикает раз в 1 мкс (PSC = 71 при 72 МГц), прерывание по обновлению делает шаг и пишет новый ARR. Предзагрузка ARR выключена: счётчик только что начал с нуля, новый ARR заведомо больше CNT и действует сразу — ловушки с переходом через 65 535 из 13.4 здесь нет. Интервалы влезают в 16 бит: самый длинный в прошивке — 3333 мкс.

void TIM2_IRQHandler(void)
{
    TIM2->SR = (uint16_t)~TIM_SR_UIF;
    uint32_t c = byj_isr(&mot);
    if (c) {
        TIM2->ARR = c - 1u;
    } else {
        TIM2->CR1 &= (uint16_t)~TIM_CR1_CEN;
        busy = 0;
    }
}

Оборот выхода в прошивке — 4076 полушагов за 4,3233 с, из них 3620 — на крейсерской скорости; журнал стенда совпал с таблицей знак в знак, ход симметричен. Что даёт разгон в модели (ход — 2048 шагов):

Режим, ускорение Дошёл без пропуска Сорвался Скорость выхода на пределе
полушаг, 2000 полушагов/с² 1300 полушагов/с 1400 полушагов/с 19,14 об/мин
полный шаг, 1000 шагов/с² 700 шагов/с 800 шагов/с 20,61 об/мин
полушаг, 2000 полушагов/с², 4,5 В 1000 полушагов/с 1200 полушагов/с 14,72 об/мин

Без разгона полушаг трогался с 550/с, с разгоном доходит до 1300/с — в 2,4 раза быстрее. На скорости полный шаг сильнее полушага: две фазы всегда, момент ровнее. Сорвавшийся на ходу мотор опять просто встаёт: на 1400/с не сделано 1464 полушага из 2048.

Ускорение тоже не бесконечно. До 1000 полушагов/с с 300:

Ускорение, полушагов/с² 2000 5000 10 000 20 000 50 000
ход 2048 полушагов 2,295 с 2,148 с 2,099 с 2,075 с срыв

С 2000 до 20 000 ход короче всего на 0,22 с: почти всё время — крейсерский участок. Брать ускорение с запасом от предела дёшево.


57к.6 Удержание, отключение фаз и люфт

Держать или снимать ток. После хода ротор держат фазы последнего такта. Две фазы — 0,841 Вт и в модели 30,3 мН·м на выходе, одна — 0,421 Вт и 21,4 мН·м (момент модели при идеальном редукторе — условный). Час удержания — 0,84 Вт·ч тепла в мотор и ULN2003, хотя они ничего не делают. Без тока выход держит только трение редуктора (в модели — сухое трение на роторе; сколько держит настоящий мотор, не проверено). Если нагрузка лёгкая, делают так: после хода полсекунды держать, чтобы ротор успокоился, и снять ток:

/* Снять ток со всех фаз: держит только трение редуктора. Такт idx запомнен — byj_move подаст его же. */
void byj_release(byj_t *m)
{
    byj_port_write(byj_bsrr(0u));
    m->on = 0;
}

Важно, с какого такта начать следующий ход. byj_move сначала подаёт ток в тот же такт, в котором мотор остановился, и только потом шагает: стенд проверил, что первая запись обратного хода совпала с последней записью прямого. Если подать другой такт — ротор прыгнет к нему. После сброса МК такт не известен; модель: ротор стоял в такте 1…7, прошивка подаёт такт 0:

Стоял в такте 1 2 3 4 5 6 7
прыжок, полушагов −1,48 −2,34 −2,84 0 2,84 2,34 1,48
на выходе −0,130° −0,207° −0,251° 0 0,251° 0,207° 0,130°

Такт 4 — противоположный: поле тянет ротор ровно назад, и он стоит в неустойчивом равновесии; в модели остался на месте, на деле уйдёт на 4 полушага в любую сторону — 0,353°. Редуктор делит прыжок в 63,7 раза, поэтому на выходе это доли градуса; но после сброса ноль всё равно ищут заново — концевиком или датчиком.

Люфт. Между шестернями есть зазор: при смене направления ротор сначала выбирает его, а выход стоит. В модели люфт — 1,0° на выходе (условно; у своего экземпляра его меряют). Ход на 90° вперёд кончился на −0,457° от цели, ход назад в ноль — на +0,456°: одна и та же точка со двух сторон расходится на 0,913°. Лекарство — подходить к цели всегда с одной стороны. Если цель ниже, сначала уйти ниже неё на запас больше люфта, затем подойти снизу:

/* Люфт: к цели всегда подходим в одну сторону (вверх). Если цель ниже — сначала на backlash ниже неё.
 * Возвращает число ходов (1 или 2), *via — точка первого хода. */
int32_t byj_plan(int32_t pos, int32_t target, int32_t backlash, int32_t *via)
{
    if (target >= pos || backlash <= 0) {
        *via = target;
        return 1;
    }
    *via = target - backlash;
    return 2;
}

С запасом 24 полушага (2,12°) возврат в ноль кончился на −0,448° — так же, как и ход вперёд (−0,457°): постоянный сдвиг снимается установкой нуля, а разброс между направлениями — 0,009° вместо 0,913°.


57к.7 Грабли

Мотор дрожит на месте или идёт рывками. Фазы подключены не в том порядке — на плате разъём с ключом, но при своей разводке или в коде (PB12, PB14, PB13, PB15) их путают. В модели при полном шаге 150 шагов/с и переставленных IN1 и IN2 ротор стоит и дрожит (размах 0,1 полушага), с переставленными IN2 и IN3 — сдвинулся на 15,2 полушага из 256. На полушаге та же ошибка ведёт себя иначе: IN1 ↔ IN2 — мотор честно идёт в обратную сторону (−256,4), IN2 ↔ IN3 — уходит назад на 239,9 полушага. Проверять порядок медленно, по одному такту, и смотреть на вал.

С USB мотор слабый и срывается. Питание с USB, да ещё через диод платы, бывает заметно ниже 5 В. В модели при 4,5 В порог старта падает с 550 до 500 полушагов/с, а предел с разгоном — с 1300 до 1000. Да и порт USB — не для моторов: 168 мА двух фаз плюс помехи от выключения обмоток на общей земле с МК. Мотор — от отдельного источника 5 В, земля общая.

COM не подключён к +5 В. Ток полуобмотки после выключения рвётся, и энергия 0,106 мДж на каждое выключение уходит в пробой ключа. Мотор ещё крутится, но ULN2003 греется и ломается. COM — к тому же +5 В, что и общий провод мотора.

Греется, хотя стоит. Фазы под током после хода: 0,841 Вт на двух фазах. Снимать ток через полсекунды после хода (byj_release) или держать одну фазу.

Через сто оборотов позиция уехала на полоборота. Счёт по 4096 полушагов на оборот: +1,787° за оборот, 178,66° за сотню. Цель — от абсолютного угла через 283712/4455 (57к.1).

Мотор гудит и не трогается. Частота с места выше порога: в модели полушаг срывается с 600/с. Начинать с меньшей частоты и разгоняться по таблице.

После сброса вал дёрнулся. Прошивка подала не тот такт, в котором ротор стоял. Внутри работы — подавать тот же такт перед ходом (так делает byj_move); после сброса ноль ищут заново.

Две обмотки сразу при смене такта. Выводы пишут по одному: на полном шаге два перехода из четырёх проходят через обе половины одной обмотки. Одна запись в BSRR.

К цели приходит то с одной, то с другой стороны. Люфт: 0,913° между направлениями в модели. Подход всегда снизу (byj_plan).


57к.8 Итоги

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

byj57k.h — выводы, режимы, редуктор, типы и прототипы драйвера.

/* byj57k.h — раздел 57к: шаговый мотор-редуктор 28BYJ-48 через ULN2003, без регистров микроконтроллера.
 * Четыре фазы IN1…IN4 — подряд на выводах одного порта, начиная с BYJ_PIN0; выдача — словом BSRR через
 * порт byj_port_write: в прошивке fw57k.c это GPIOB->BSRR, на стенде qemu57k.c — запись в журнал.
 * Код один для прошивки и стенда. */
#ifndef BYJ57K_H
#define BYJ57K_H
#include <stdint.h>

#define BYJ_PIN0        12u         /* IN1 — PB12, IN2 — PB13, IN3 — PB14, IN4 — PB15 */

#define BYJ_WAVE        0u          /* одна фаза: A, B, C, D */
#define BYJ_FULL        1u          /* две фазы: AB, BC, CD, DA */
#define BYJ_HALF        2u          /* полушаг: 8 тактов */

/* Редуктор: (32/9)·(22/11)·(26/9)·(31/10) = 283712 / 4455 ≈ 63,684 — не 64.
 * Ротор — 64 полушага на оборот; выходной вал — 64 · 283712 / 4455 ≈ 4075,77 полушага. */
#define BYJ_GEAR_NUM    283712u
#define BYJ_GEAR_DEN    4455u
#define BYJ_HALF_ROTOR  64u

#define BYJ_RAMP_MAX    1024u       /* интервалов в таблице разгона: 2 КБ ОЗУ */

void byj_port_write(uint32_t bsrr);

typedef struct {
    uint16_t ramp[BYJ_RAMP_MAX];    /* интервал i-го шага разгона, мкс */
    uint16_t n_acc;                 /* шагов до крейсерской скорости */
    uint16_t c_run;                 /* крейсерский интервал, мкс */
} byj_ramp_t;

typedef struct {
    const byj_ramp_t *r;
    int32_t pos;                    /* положение в полушагах */
    uint32_t left, done, total;     /* шагов осталось, сделано, всего */
    uint8_t mode, idx;              /* режим, такт 0…7 таблицы полушага */
    int8_t dir;                     /* +1 или −1 */
    uint8_t on;                     /* фазы под током */
} byj_t;

uint8_t byj_mask(uint8_t idx);
uint32_t byj_bsrr(uint8_t mask);
void byj_init(byj_t *m, uint8_t mode);
uint32_t byj_ramp_init(byj_ramp_t *r, uint32_t v0, uint32_t vmax, uint32_t acc);
uint32_t byj_move(byj_t *m, const byj_ramp_t *r, int32_t steps);
uint32_t byj_isr(byj_t *m);
void byj_release(byj_t *m);
int32_t byj_cdeg_to_half(int32_t cdeg);
int32_t byj_plan(int32_t pos, int32_t target, int32_t backlash, int32_t *via);

#endif

byj57k.c — таблица тактов, слово BSRR, таблица разгона, шаг в прерывании, редуктор и люфт — общий для прошивки и стенда.

/* byj57k.c — раздел 57к: таблицы фаз, слово BSRR, трапеция разгона, шаг в прерывании, редуктор и люфт. */
#include "byj57k.h"

/* Полушаг, 8 тактов; бит 0 — IN1 (A), бит 1 — IN2 (B), бит 2 — IN3 (C), бит 3 — IN4 (D).
 * Чётные такты — одна фаза (волновой режим), нечётные — две (полный шаг). */
static const uint8_t half_tab[8] = {0x1u, 0x3u, 0x2u, 0x6u, 0x4u, 0xCu, 0x8u, 0x9u};

uint8_t byj_mask(uint8_t idx)
{
    return half_tab[idx & 7u];
}

/* Одно слово BSRR: включённые фазы — в BS, выключенные — в BR. Все четыре вывода меняются одной записью. */
uint32_t byj_bsrr(uint8_t mask)
{
    uint32_t on = (uint32_t)(mask & 0xFu), off = (uint32_t)(~mask & 0xFu);
    return on << BYJ_PIN0 | off << (BYJ_PIN0 + 16u);
}

void byj_init(byj_t *m, uint8_t mode)
{
    m->r = 0;
    m->pos = 0;
    m->left = m->done = m->total = 0;
    m->mode = mode;
    m->idx = mode == BYJ_FULL ? 1u : 0u;
    m->dir = 1;
    m->on = 0;
}

static uint32_t isqrt64(uint64_t x)
{
    uint64_t r = 0, b = (uint64_t)1 << 62;
    while (b > x)
        b >>= 2;
    while (b) {
        if (x >= r + b) {
            x -= r + b;
            r = (r >> 1) + b;
        } else {
            r >>= 1;
        }
        b >>= 2;
    }
    return (uint32_t)r;
}

/* Трапеция: на i-м шаге разгона v² = v0² + 2·a·i (шагов/с, шагов/с²), интервал = 1 / v в мкс.
 * Корень считается здесь, один раз на шаг таблицы, — в прерывании только чтение таблицы. */
uint32_t byj_ramp_init(byj_ramp_t *r, uint32_t v0, uint32_t vmax, uint32_t acc)
{
    uint32_t i = 0;
    while (i < BYJ_RAMP_MAX) {
        uint64_t v2 = (uint64_t)v0 * v0 + 2u * (uint64_t)acc * i;
        uint32_t vq = isqrt64(v2 << 16);                  /* скорость × 256 */
        if (vq >= vmax * 256u)
            break;
        r->ramp[i++] = (uint16_t)((256000000u + vq / 2u) / vq);
    }
    r->n_acc = (uint16_t)i;
    r->c_run = (uint16_t)((1000000u + vmax / 2u) / vmax);
    if (i == BYJ_RAMP_MAX)
        r->c_run = r->ramp[i - 1u];                      /* таблица кончилась раньше vmax: дальше — её скорость */
    return i;
}

/* Интервал перед (j+1)-м шагом: k — шагов до ближнего края хода; разгон и торможение — одна таблица. */
static uint32_t interval(const byj_t *m, uint32_t j)
{
    uint32_t k = m->total - 1u - j < j ? m->total - 1u - j : j;
    return k < m->r->n_acc ? m->r->ramp[k] : m->r->c_run;
}

/* Начать ход на steps шагов режима (знак — направление). Сразу подаёт ток в текущий такт — без шага,
 * чтобы после отключения фаз ротор не дёрнуло; возвращает интервал до первого шага (0 — шагать некуда). */
uint32_t byj_move(byj_t *m, const byj_ramp_t *r, int32_t steps)
{
    m->r = r;
    m->dir = steps < 0 ? -1 : 1;
    m->total = m->left = (uint32_t)(steps < 0 ? -steps : steps);
    m->done = 0;
    byj_port_write(byj_bsrr(byj_mask(m->idx)));
    m->on = 1;
    return m->total ? interval(m, 0) : 0u;
}

/* Прерывание таймера: следующий такт, одна запись в порт, интервал до следующего шага (0 — ход окончен). */
uint32_t byj_isr(byj_t *m)
{
    if (!m->left)
        return 0u;
    uint8_t d = m->mode == BYJ_HALF ? 1u : 2u;
    m->idx = (uint8_t)((m->idx + (m->dir > 0 ? d : 8u - d)) & 7u);
    m->pos += m->dir * (int32_t)d;
    byj_port_write(byj_bsrr(byj_mask(m->idx)));
    m->done++;
    if (--m->left == 0u)
        return 0u;
    return interval(m, m->done);
}

/* Снять ток со всех фаз: держит только трение редуктора. Такт idx запомнен — byj_move подаст его же. */
void byj_release(byj_t *m)
{
    byj_port_write(byj_bsrr(0u));
    m->on = 0;
}

/* Угол выходного вала в сотых долях градуса → полушаги, с округлением, без накопления ошибки:
 * 4075,77 полушага на оборот = 64 · 283712 / 4455. */
int32_t byj_cdeg_to_half(int32_t cdeg)
{
    int64_t num = (int64_t)cdeg * (int64_t)(BYJ_HALF_ROTOR * BYJ_GEAR_NUM);
    int64_t den = (int64_t)BYJ_GEAR_DEN * 36000;
    return (int32_t)(num >= 0 ? (num + den / 2) / den : -((-num + den / 2) / den));
}

/* Люфт: к цели всегда подходим в одну сторону (вверх). Если цель ниже — сначала на backlash ниже неё.
 * Возвращает число ходов (1 или 2), *via — точка первого хода. */
int32_t byj_plan(int32_t pos, int32_t target, int32_t backlash, int32_t *via)
{
    if (target >= pos || backlash <= 0) {
        *via = target;
        return 1;
    }
    *via = target - backlash;
    return 2;
}

fw57k.c — прошивка STM32F103: PB12…PB15 — IN1…IN4 платы ULN2003, TIM2 — шаги по таблице разгона.

/* fw57k.c — раздел 57к: 28BYJ-48 через плату ULN2003 на STM32F103 (BluePill), 72 МГц, TIM2 — 72 МГц.
 * IN1…IN4 платы — PB12…PB15 (двухтактные выходы); «+» платы — от отдельного источника 5 В, земля общая.
 * TIM2: PSC = 71 — тик 1 мкс; прерывание по обновлению — шаг, затем ARR = интервал − 1 (без предзагрузки:
 * счётчик только что с нуля, новый ARR больше CNT и действует сразу, раздел 13.4).
 * Цикл: оборот вперёд, удержание 0,5 с, фазы снять; назад в ноль с подходом снизу (люфт), снова 0,5 с.
 * Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "byj57k.h"

#define CPU_HZ          72000000u
#define TICK_HZ         1000000u
#define V_START         300u            /* полушагов/с: трогается без разгона (57к.5) */
#define V_RUN           1000u           /* крейсерская: 14,7 об/мин на выходе */
#define ACC             2000u           /* полушагов/с² */
#define HOLD_MS         500u            /* после хода держать ток, затем снять */
#define BACKLASH_HALF   24              /* подход снизу: 2,1° — больше люфта редуктора с запасом */

static byj_t mot;
static byj_ramp_t ramp;
static volatile uint8_t busy;
static volatile uint32_t ms;

void byj_port_write(uint32_t bsrr)
{
    GPIOB->BSRR = bsrr;                 /* все четыре фазы — одной записью (RM0008, с. 172) */
}

void TIM2_IRQHandler(void)
{
    TIM2->SR = (uint16_t)~TIM_SR_UIF;
    uint32_t c = byj_isr(&mot);
    if (c) {
        TIM2->ARR = c - 1u;
    } else {
        TIM2->CR1 &= (uint16_t)~TIM_CR1_CEN;
        busy = 0;
    }
}

void SysTick_Handler(void)
{
    ms++;
}

static void move(int32_t steps)
{
    uint32_t c = byj_move(&mot, &ramp, steps);
    if (!c)
        return;
    busy = 1;
    TIM2->CNT = 0;
    TIM2->ARR = c - 1u;
    TIM2->CR1 |= TIM_CR1_CEN;
    while (busy)
        __WFI();
}

static void go_to(int32_t target)
{
    int32_t via;
    if (byj_plan(mot.pos, target, BACKLASH_HALF, &via) == 2)
        move(via - mot.pos);
    move(target - mot.pos);
}

static void wait_ms(uint32_t n)
{
    uint32_t t0 = ms;
    while (ms - t0 < n)
        __WFI();
}

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    GPIOB->BSRR = byj_bsrr(0u);                             /* фазы выключены до включения выходов */
    GPIOB->CRH = (GPIOB->CRH & 0x0000FFFFu) | 0x22220000u;  /* PB12…PB15: выход 2 МГц, двухтактный */
    TIM2->PSC = CPU_HZ / TICK_HZ - 1u;
    TIM2->ARR = 0xFFFFu;
    TIM2->EGR = TIM_EGR_UG;                                 /* PSC — в действующий регистр (13.4) */
    TIM2->SR = (uint16_t)~TIM_SR_UIF;
    TIM2->DIER = TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM2_IRQn);
    SysTick_Config(CPU_HZ / 1000u);

    byj_init(&mot, BYJ_HALF);
    byj_ramp_init(&ramp, V_START, V_RUN, ACC);
    for (;;) {
        go_to(byj_cdeg_to_half(36000));                     /* 4076 полушагов, а не 4096 */
        wait_ms(HOLD_MS);
        byj_release(&mot);
        wait_ms(1000u);
        go_to(0);
        wait_ms(HOLD_MS);
        byj_release(&mot);
        wait_ms(1000u);
    }
}

Дальше: раздел 58 «Импульсные преобразователи».

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