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

57е. Ток в обмотке шагового двигателя: ШИМ-чоппер и режимы спада

В 57а.3 микрошаг свёлся к таблице: на каждом шаге обмотке A нужен ток cos, обмотке B — sin. Но таблица говорит, какой ток нужен, а не как его получить. Обмотка — индуктивность с сопротивлением (57.5), и при вращении в ней ещё наводится противо-ЭДС. Драйвер держит ток так: включает мост, ждёт, пока ток дорастёт до порога, выключает на паузу, снова включает. Это и есть чоппер. Самое интересное — что делать с током в паузе: дать ему медленно стечь по закороченной обмотке, быстро сбросить обратным напряжением или смешать одно с другим.

Здесь: три состояния моста и два способа держать ток (постоянная пауза и постоянная частота); почему медленный спад не держит малые токи и почему при вращении он портит синус; свой чоппер на TIM1/TIM8 с внешним компаратором, ЦАП для порога и АЦП для подстройки спада; что из этого видно в готовых драйверах.

Опоры: RM0090 Rev 19 (TIM1/TIM8 у F4: гашение OCxREF по ETR — 17.3.13, регистр TIMx_SMCR с режимом сброса и фильтром ETF, режимы OCxM; АЦП — запуск инжектированной группы, таблица 69, ADC_JSQR), RM0008 Rev 17 (F1: JEXTSEL, бит ADC2_ETRGINJ_REMAP, фильтр ETF), DS8626 Rev 9 (выводы F407), DocID14611 Rev 12 и DocID13587 Rev 17 (выводы и состав F103xE и F103x8/xB), RM0440 Rev 6 (G4: источник ETR — компаратор). Модель обмотки — своя, её параметры — пример, а не паспорт конкретного двигателя: R = 1,5 Ом, L = 3 мГн, шина 24 В, амплитуда противо-ЭДС фазы 0,25 В на рад/с вала, 50 электрических оборотов на механический (1,8° на шаг), полный ток 1,5 А. Прогон — _verify/chop57e/sim57e.c, независимо — ref57e.py, запись — chop57e_host.txt. Про драйверы TMC — исходники Klipper (github.com/Klipper3d/klipper, коммит 7bc4d09), лицензия GPL-3.0 (файл COPYING, © Kevin O'Connor).

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

Оригинал Перевод
chopper, PWM current regulation чоппер, регулирование тока ключами
fixed (constant) off-time постоянная пауза
fixed frequency, peak current mode постоянная частота, регулирование по пику
slow / fast / mixed decay медленный / быстрый / смешанный спад
blanking time время гашения (компаратор не слушают после включения)
recirculation, synchronous rectification ток в паузе, проводящие ключи вместо диодов
sense resistor шунт
back-EMF противо-ЭДС
reset mode (SMS = 100), ETRF режим сброса счётчика, фильтрованный вход ETR
OCxREF clear, OCxCE гашение канала входом ETR
TOFF, TBL, HSTRT, HEND (TMC) пауза, гашение, начало и конец гистерезиса
spreadCycle, stealthChop режимы чоппера драйверов TMC

57е.1 Три состояния моста и два способа держать ток

Обмотка стоит в H-мосте: плечо 1 и плечо 2, в каждом верхний и нижний ключ. Пусть ток течёт из плеча 1 в плечо 2. Тогда у моста три полезных состояния:

Состояние Ключи Напряжение на обмотке Ток через шунт общего провода
разгон верх плеча 1, низ плеча 2 +24 В весь ток обмотки
медленный спад оба нижних 0 (закоротка) ноль: ток крутится через два нижних ключа
быстрый спад низ плеча 1, верх плеча 2 −24 В ток обмотки, но обратного знака

Компаратор сравнивает напряжение шунта с порогом: превысил — выключить разгон. Числа модели при токе 1,5 А:

Сколько держать паузу, решают по-разному. Постоянная пауза: после срабатывания компаратора мост ровно 20 мкс в спаде, затем снова разгон; частота «плавает». Постоянная частота: каждые 50 мкс (20 кГц) разгон начинается заново, срабатывание лишь обрывает его до конца периода.

После включения на шунте выброс от перезаряда ёмкостей ключей, поэтому компаратор первое время не слушают — гашение. В нашем чоппере его роль играет фильтр входа ETR таймера: 8 одинаковых выборок через 32 такта на 168 МГц — 1,524 мкс (57е.5). Пока идёт гашение, ток растёт ещё на 11,05 мА сверх порога.

Ток обмотки 1,5 Ом / 3 мГн под чоппером с постоянной паузой 20 мкс: сверху — стоим на 1,5 А, за 100 мкс медленный спад даёт пилу 15,2 мА на 45,3 кГц, смешанный 55,0 мА на 36,3 кГц, быстрый 174,6 мА на 22,8 кГц; снизу — вращение 2 об/с, средний ток на каждом из 64 шагов микрошага: медленный спад уводит ток до 2,34 А там, где задан ноль, смешанный и подстройка по АЦП идут за заданным


57е.2 Стоим: пульсации, частота и средний ток

Двигатель стоит, на обмотке шаг 0 таблицы (полный ток, порог ЦАП 1862 — 1,5001 А) или шаг 15 (код 25 из 255 — 0,1474 А). Чоппер с постоянной паузой 20 мкс и с постоянной частотой 20 кГц:

Режим 1,5 А: средний ошибка размах частота 0,147 А: средний ошибка размах
пауза, медленный 1,5030 А +2,84 мА 15,17 мА 45,30 кГц 1,1327 А +985,28 мА 11,33 мА
пауза, смешанный ¼ 1,4722 А −27,99 мА 55,00 мА 36,32 кГц 0,1263 А −21,15 мА 41,51 мА
пауза, быстрый 1,4228 А −77,39 мА 174,55 мА 22,78 кГц 0,0780 А −69,48 мА 160,77 мА
пауза, подстройка по АЦП 1,4941 А −6,09 мА 25,13 мА 42,66 кГц 0,1366 А −10,86 мА 26,53 мА
20 кГц, медленный 1,4933 А −6,81 мА 34,01 мА 20,00 кГц 0,4724 А +324,94 мА 11,62 мА
20 кГц, быстрый 1,3127 А −187,49 мА 380,06 мА 20,00 кГц 0,0588 А −88,68 мА 205,02 мА

Что видно.


57е.3 «Пол» медленного спада

Гашение — это наименьшее время разгона: даже если ток уже выше порога, компаратор сработает не раньше, чем через 1,524 мкс. Значит, мост не может дать скважность меньше t / (t + пауза). Средний ток при медленном спаде не опускается ниже

Iпол = Vпит · t / (R · (t + пауза)) — при 24 В, 1,5 Ом, t = 1,524 мкс:

Пауза Модель Формула
20 мкс 1,1327 А 1,1327 А
40 мкс 0,5872 А 0,5872 А
80 мкс 0,2991 А 0,2991 А

Индуктивность в формулу не входит. При паузе 20 мкс медленный спад держит шаги таблицы 0…7 (последний — 1,1585 А задано, 1,1633 А в модели), а с шага 8 (1,0586 А) и ниже ток стоит на 1,1327 А. Поэтому в чистом медленном спаде работают только с высоковольтной шиной и большим током — или удлиняют паузу (частота падает в слышимую область), или добавляют быстрый спад. Смешанный ¼ при 0,147 А ошибается на −21,15 мА, подстройка — на −10,86 мА.


57е.4 Вращение: противо-ЭДС портит синус

Ротор вращается равномерно вместе с полем, нагрузки нет; противо-ЭДС фазы A — −E·sin θ, E = 0,785 В на 0,5 об/с, 1,571 В на 1, 3,142 В на 2 и 6,283 В на 4 об/с. Микрошаг 1/16: 64 шага на электрический оборот, 50 оборотов на механический. Меряем средний ток на каждом шаге против заданного, СКО ошибки за оборот:

Режим 0,5 об/с 1 об/с 2 об/с 4 об/с Размах при 2 об/с
пауза, медленный 555,9 мА 723,7 мА 952,3 мА 1297,7 мА 21,7 мА
пауза, быстрый 82,8 мА 81,0 мА 84,7 мА 129,4 мА 181,1 мА
пауза, смешанный ¼ 40,6 мА 46,5 мА 117,6 мА 524,4 мА 60,7 мА
пауза, подстройка 35,0 мА 39,5 мА 65,6 мА 122,5 мА 61,9 мА
20 кГц, медленный 285,6 мА 464,6 мА 726,1 мА 1091,0 мА 63,0 мА

Почему медленный спад проваливается. В первой четверти оборота заданный ток положителен и убывает, а противо-ЭДС −E·sin θ отрицательна. В медленном спаде на обмотке L·di/dt = −R·i − e = −R·i + E·sin θ: ЭДС толкает ток вверх. Закороченная обмотка работает генератором, и ток растёт там, где должен падать: на шаге 16 (задан ноль) при 2 об/с течёт 2,342 А (рисунок). Первая гармоника тока — 2,0780 А вместо 1,5 и отстаёт на 32,96°. Во второй четверти, где ток растёт, ЭДС помогает — там медленный спад хорош.

Быстрый спад ток сбрасывает всегда, но платит пилой 181,1 мА и средним ниже заданного. Смешанный ¼ хорош до 1 об/с, на 4 об/с четверти паузы мало (524,4 мА). Подстройка сама увеличивает быструю часть, когда нужно: в среднем 14,0 % паузы на 0,5 об/с, 17,8 % на 1, 29,0 % на 2 и 65,1 % на 4 об/с; первая гармоника на 2 об/с — 1,4776 А с отставанием 4,16°.


57е.5 Чоппер на TIM1: компаратор сбрасывает счётчик

Постоянную паузу таймер STM32 делает сам. Выход компаратора идёт на вход ETR. В TIMx_SMCR выбран режим сброса (SMS = 100: «фронт TRGI переинициализирует счётчик и обновляет регистры»), источник TS = 111 — ETRF, фильтр ETF = 1111 (fDTS/32, N = 8). Каждое срабатывание обнуляет счётчик, а каналы 1 и 2 в режимах ШИМ раскладывают счёт на участки паузы и разгона:

Счётчик Плечо «к» (ШИМ 2, CCR = n_off) Плечо «от» (ШИМ 1, CCR = n_fast) Состояние
CNT < n_fast низ верх быстрый спад
n_fast ≤ CNT < n_off низ низ медленный спад
CNT ≥ n_off верх низ разгон

ШИМ 1 при счёте вверх активен, пока CNT < CCR, ШИМ 2 — наоборот; активный уровень канала — верхний ключ плеча, дополняющий выход CHxN с защитным интервалом — нижний. n_fast = 0 — чистый медленный спад, n_fast = n_off — чистый быстрый. Знак тока меняется перестановкой режимов каналов. Для F407 с таймерами на 168 МГц n_off = 3360 (20 мкс), смешанный ¼ — n_fast = 840. Код, который считает режимы и CCR, общий для прошивки и модели:

void chop_legs(chop_out_t *o, int8_t sign, uint16_t n_off, uint16_t n_fast)
{
    if (n_fast > n_off)
        n_fast = n_off;
    o->sign = sign;
    if (sign > 0) {                    /* ток из плеча 1 в плечо 2 */
        o->ocm1 = OCM_PWM2; o->ccr1 = n_off;
        o->ocm2 = OCM_PWM1; o->ccr2 = n_fast;
    } else if (sign < 0) {             /* ток из плеча 2 в плечо 1 */
        o->ocm1 = OCM_PWM1; o->ccr1 = n_fast;
        o->ocm2 = OCM_PWM2; o->ccr2 = n_off;
    } else {                           /* нулевой ток: оба нижних ключа, обмотка закорочена */
        o->ocm1 = OCM_FORCE_LOW; o->ccr1 = 0;
        o->ocm2 = OCM_FORCE_LOW; o->ccr2 = 0;
    }
}

Мост одной обмотки и запись результата в таймер:

static void tim_apply(tim_t *t, const chop_out_t *o, int restart)
{
    t->CCR1  = o->ccr1;
    t->CCR2  = o->ccr2;
    t->CCMR1 = ((uint32_t)o->ocm1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE
             | ((uint32_t)o->ocm2 << TIM_CCMR1_OC2M_Pos) | TIM_CCMR1_OC2PE;
    if (restart)
        t->EGR = TIM_EGR_UG;
}

/* Мост одной обмотки: счётчик вверх, сброс по фронту ETR, канал 4 — запуск АЦП. */
static void tim_bridge_init(tim_t *t)
{
    t->CR1   = 0;
    t->PSC   = 0;
    t->ARR   = 0xFFFFu;                   /* не сработал компаратор — пауза через 65536 тактов */
    t->SMCR  = (ETF_CODE << TIM_SMCR_ETF_Pos) | TIM_SMCR_TS_ETRF | TIM_SMCR_SMS_RESET;
    t->CCMR1 = (OCM_FORCE_LOW << TIM_CCMR1_OC1M_Pos) | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCMR2 = (OCM_PWM2 << TIM_CCMR2_OC4M_Pos) | TIM_CCMR2_OC4PE;   /* OC4REF встаёт при CNT = CCR4 */
    t->CCR4  = N_OFF + N_ADC;
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE
             | TIM_CCER_CC4E;                      /* канал 4 на вывод не выведен */
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

Две тонкости из RM0090. Первая: ШИМ без предзагрузки CCR (OCxPE = 0) документ разрешает только в одноимпульсном режиме — поэтому OCxPE включён, и новые CCR вступают при следующем сбросе счётчика (он же событие обновления). Вторая: в режимах ШИМ уровень OCxREF меняется только когда меняется результат сравнения. Переход из «принудительно неактивен» в ШИМ при смене знака тока сам по себе выход не включит — поэтому tim_apply с restart даёт UG: счётчик в ноль, новая пауза с начала. Модель делает так же. ARR = 65535 — запасной выход: если компаратор не сработал за 390 мкс, счётчик переполнится и начнёт паузу сам.

Постоянная частота делается другой функцией TIM1 — гашением канала (OCxCE): высокий уровень ETRF прижимает OCxREF к нулю до следующего события обновления; при CCR > ARR (ШИМ 100 %) канал снова включится на переполнении:

void tim_fixed_slow_init(tim_t *t)
{
    t->CR1   = 0;
    t->ARR   = TIM_HZ / 20000u - 1u;
    t->SMCR  = ETF_CODE << TIM_SMCR_ETF_Pos;      /* ETR только для гашения, режима сброса нет */
    t->CCR1  = TIM_HZ / 20000u;
    t->CCMR1 = (OCM_PWM1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE | TIM_CCMR1_OC1CE
             | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE;
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

Здесь только медленный спад: плечо 2 всё время на нижнем ключе. Смешанный спад так не сделать — гашение держится до конца периода, отмерить в нём «первые 5 мкс» нечем.

Выводы (обмотка A — TIM1, обмотка B — TIM8), по DS8626 и DocID14611 одни и те же у F407 и F103xE:

Сигнал TIM1 TIM8 F407, альтернативная функция
CH1 / CH2 PA8 / PA9 PC6 / PC7 AF1 / AF3
CH1N / CH2N PB13 / PB14 PA7 / PB0 AF1 / AF3
ETR (выход компаратора) PA12 PA0 AF1 / AF3
шунт → АЦП, порог ← ЦАП PA1 (ADC1_IN1), PA4 (DAC1) PA2 (ADC2_IN2), PA5 (DAC2) аналоговый вход

У F103 таймеры на 72 МГц: n_off = 1440, фильтр ETF = 1100 (fDTS/16, N = 8 — 128 тактов, 1,78 мкс), защитный интервал 36 тактов. Второй таймер с дополняющими выходами и ЦАП есть только у F103xE (high-density): у F103x8/xB по DocID13587 таймер такого рода один, ЦАП нет — там нужна другая схема порога и второй обмотки.

У G4 внешний компаратор не нужен: по RM0440 источник ETR таймера выбирают полем ETRSEL в TIMx_AF1, и tim_etr1 у TIM1 — выход COMP1; порог даёт внутренний ЦАП, гашение — сам компаратор от канала таймера (как в 57б.6 и 44.7). Этот вариант описан, но не собирался.


57е.6 Шаги, ЦАП и подстройка спада по АЦП

Таблица и порог — общий файл chop57e.c. Шаг idx (0…63) даёт модуль и знак тока обмотки A; обмотка B — тот же вызов с idx + 48. Сила поля √(A² + B²) по 64 шагам — от 0,998 до 1,002 полной, как в 57а.3.

int8_t ustep_level(uint8_t idx, uint8_t *level)
{
    idx &= 63u;
    uint8_t q = idx >> 4, r = idx & 15u;
    uint8_t v = (q == 0u || q == 2u) ? ustep_cos[r] : ustep_cos[16u - r];
    *level = v;
    if (v == 0u)
        return 0;
    return (q == 0u || q == 3u) ? 1 : -1;
}
void chop_set(chop_out_t *o, const chop_cfg_t *c, uint8_t idx)
{
    uint8_t level;
    int8_t sign = ustep_level(idx, &level);
    uint16_t n_fast = c->mode == DECAY_SLOW ? 0u : c->mode == DECAY_FAST ? c->n_off : c->n_fast;
    o->dac = (uint16_t)(((uint32_t)level * c->dac_full + 127u) / 255u);
    chop_legs(o, sign, c->n_off, n_fast);
}

Усилитель шунта даёт 1 В на ампер (например, 0,1 Ом и усиление 10), ЦАП и АЦП на 3,3 В — одна шкала, 0,806 мА на код; полный ток — код 1862. Что пишется в таймер при n_off = 3360, n_fast = 840:

Шаг Код из 255 Знак ЦАП OC1M / CCR1 OC2M / CCR2
0 255 + 1862 7 (ШИМ 2) / 3360 6 (ШИМ 1) / 840
8 180 + 1314 7 / 3360 6 / 840
15 25 + 183 7 / 3360 6 / 840
16 0 0 0 4 (неактивен) / 0 4 / 0
17 25 − 183 6 / 840 7 / 3360
32 255 − 1862 6 / 840 7 / 3360

Подстройка. Канал 4 (CCR4 = n_off + 168, через 1 мкс разгона) запускает инжектированное преобразование: ADC1 — по TIM1_CH4 (JEXTSEL 0000), ADC2 — по TIM8_CH4 (1110). В этот момент шунт видит ток, с которым обмотка вышла из паузы. Если он не ниже порога — спад не успел, быстрая часть растёт на 1/8 паузы; если ниже — убывает на 1/32. Восемь срабатываний подряд дают 420, 840, … 3360 (весь быстрый), потом 3255, 3150, 3045.

uint16_t chop_adapt(uint16_t n_fast, uint16_t valley, uint16_t thr, uint16_t n_off)
{
    uint32_t up = n_off / 8u, down = n_off / 32u, n = n_fast;
    if (valley >= thr)
        n = n + up > n_off ? n_off : n + up;
    else
        n = n > down ? n - down : 0u;
    return (uint16_t)n;
}

Выборка должна кончиться раньше, чем сработает фильтр ETR (1,33…1,52 мкс после начала разгона, в зависимости от фазы выборок фильтра), иначе мост уйдёт в паузу посреди выборки и шунт покажет ноль. Поэтому время выборки — наименьшее: 3 такта АЦП на 21 МГц у F407.

static void adc_init(void)
{
    RCC_APB2ENR |= RCC_ADC1EN | RCC_ADC2EN;
    ADC_CCR = 1u << 16;                                    /* ADCPRE = 01: 84 МГц / 4 = 21 МГц */
    ADC1->SMPR2 = 0;                       /* 3 такта АЦП: выборка кончится раньше, чем сработает фильтр ETR */
    ADC2->SMPR2 = 0;
    ADC1->JSQR = 1u << ADC_JSQR_JSQ4_Pos;                  /* JL = 0: один канал, PA1 */
    ADC2->JSQR = 2u << ADC_JSQR_JSQ4_Pos;                  /* PA2 */
    ADC1->CR1 = ADC_CR1_JEOCIE;
    ADC2->CR1 = ADC_CR1_JEOCIE;
    ADC1->CR2 = (1u << 20) | (0u << 16) | ADC_CR2_ADON;    /* по фронту, JEXTSEL 0000 — TIM1_CH4 */
    ADC2->CR2 = (1u << 20) | (14u << 16) | ADC_CR2_ADON;   /* по фронту, JEXTSEL 1110 — TIM8_CH4 */
}

При JL = 0 преобразуется один канал, и берётся он из JSQ4, а не из JSQ1 — так в RM0090 и RM0008. У F103 то же через JEXTTRIG/JEXTSEL в битах 15:12: ADC1 — 001 (TIM1_CC4), ADC2 — 110 плюс бит ADC2_ETRGINJ_REMAP в AFIO_MAPR, иначе 110 — это EXTI15; и перед работой калибровка CAL.

Шаг, прерывание АЦП и запуск:

void stepper_step(int8_t dir)
{
    int8_t s0 = out[0].sign, s1 = out[1].sign;
    step_idx = (uint8_t)((step_idx + dir) & 63u);
    chop_set(&out[0], &cfg[0], step_idx);
    chop_set(&out[1], &cfg[1], (uint8_t)(step_idx + 48u));
    DAC->DHR12R1 = out[0].dac;
    DAC->DHR12R2 = out[1].dac;
    tim_apply(TIM1, &out[0], out[0].sign != s0);
    tim_apply(TIM8, &out[1], out[1].sign != s1);
}

static void adapt(unsigned w, tim_t *t, uint16_t valley)
{
    chop_out_t *o = &out[w];
    if (cfg[w].mode != DECAY_AUTO || o->sign == 0)
        return;
    cfg[w].n_fast = chop_adapt(cfg[w].n_fast, valley, o->dac, cfg[w].n_off);
    chop_legs(o, o->sign, cfg[w].n_off, cfg[w].n_fast);
    tim_apply(t, o, 0);
}
#if defined(CHIP_F407)
void ADC_IRQHandler(void)
#else
void ADC1_2_IRQHandler(void)
#endif
{
    if (ADC1->SR & ADC_SR_JEOC) {
        ADC1->SR = ~ADC_SR_JEOC;
        adapt(0, TIM1, (uint16_t)ADC1->JDR1);
    }
    if (ADC2->SR & ADC_SR_JEOC) {
        ADC2->SR = ~ADC_SR_JEOC;
        adapt(1, TIM8, (uint16_t)ADC2->JDR1);
    }
}

void chopper_init(void)
{
    pins_init();
    RCC_APB1ENR |= RCC_DACEN;
    RCC_APB2ENR |= RCC_TIM1EN | RCC_TIM8EN;
    DAC->CR = DAC_CR_EN1 | DAC_CR_EN2;
    adc_init();
    step_idx = 0;
    stepper_step(0);                                       /* шаг 0: A — полный ток, B — ноль */
    tim_bridge_init(TIM1);
    tim_bridge_init(TIM8);
    tim_apply(TIM1, &out[0], 1);
    tim_apply(TIM8, &out[1], 1);
    NVIC_ISER0 = 1u << ADC_IRQ_NUM;
}

Номер прерывания АЦП у обоих 18 (ADC_IRQn у F407, ADC1_2_IRQn у F103). Он срабатывает на каждом цикле чоппера — на месте при 1,5 А это 45,3 тысячи раз в секунду на обмотку; сколько это стоит процессору, не мерилось.

Всё собрано arm-none-eabi-gcc 12.2.1 с -Os -Wall -Wextra -Werror: fw57e.c — 928 байт кода для F407 (Cortex-M4) и 940 для F103 (Cortex-M3), chop57e.c — 241 байт у обоих; из них chopper_init 544/556, stepper_step 124, adapt 100, обработчик АЦП 76.

Готовые драйверы. В микросхемах вроде A4988 и DRV8825 те же части — компаратор с порогом от входа опорного напряжения, пауза, гашение и выбор спада — сделаны внутри; выводы и формулы тока берут из даташита конкретной микросхемы, с ними раздел не сверялся. У TMC2208/TMC2209 это поля регистра CHOPCONF, их раскладку и умолчания видно в Klipper (tmc2208.py, tmc2209.py): toff — биты 3:0, hstrt 6:4, hend 10:7, tbl 16:15, vsense — бит 17; Klipper пишет toff = 3, hstrt = 5, hend = 0, tbl = 2 (у TMC2130 — 4, 0, 7, 1). Пауза и гашение — те же две величины, что в нашей модели; в микросекунды их переводит даташит Trinamic. Ток Klipper считает в tmc2130.py так: I = (CS + 1)·Vfs / (32·(Rш + 0,020)·√2) (действующее значение), Vfs = 0,32 В или 0,18 В при vsense = 1; шунт по умолчанию 0,110 Ом. Для 0,8 А это vsense = 1, IRUN = 25 и 0,7955 А; для 1,2 А чувствительной шкалы не хватает (её предел 0,9791 А), выходит vsense = 0, IRUN = 21, 1,1966 А. По умолчанию Klipper ставит режим spreadCycle; stealthChop включают stealthchop_threshold. Документация Klipper пишет, что spreadCycle даёт больше момента и точности, stealthChop заметно тише, но в опытах отставал по положению примерно на 75 % полного шага.


57е.7 Грабли

Малый ток не держится — двигатель греется на удержании. «Пол» медленного спада: Vпит·t/(R·(t + пауза)). При 24 В, 1,5 Ом, гашении 1,524 мкс и паузе 20 мкс — 1,1327 А вместо заданных 0,147 А. Нужен смешанный спад или длиннее пауза.

При разгоне двигатель теряет момент и шумит, хотя на месте всё хорошо. Медленный спад на убывающей четверти не сбрасывает ток против противо-ЭДС: на 2 об/с ток 2,342 А там, где задан ноль, СКО 952,3 мА. Смешанный или подстраиваемый спад.

Постоянная частота плюс быстрый спад на открытых ключах — ток без предела. Ток перешёл через ноль в паузе, шунт видит его как ток разгона, компаратор держит гашение — и мост гонит ток в обратную сторону. В модели при 0,5 об/с до 15,986 А, при 2 об/с до 13,627 А. Нужен детектор нуля или спад через диоды, а в первую очередь — аварийный вход (57а.6).

Подстройка не работает — АЦП показывает ноль. Выборка длиннее, чем до срабатывания фильтра ETR: мост ушёл в паузу посреди выборки. Запуск через 1 мкс, самое раннее срабатывание — через 1,33 мкс: на выборку остаётся меньше трети микросекунды.

Сменили знак тока — обмотка молчит до 390 мкс. Канал перевели из «принудительно неактивен» в ШИМ без UG: уровень OCxREF ждёт смены результата сравнения, то есть переполнения счётчика.

Вторая обмотка «срабатывает» сама по себе. ADC2 на F103 с JEXTSEL = 110 без ADC2_ETRGINJ_REMAP запускается от EXTI15, а не от TIM8.

Чоппер пищит. Постоянная пауза — переменная частота: при вращении с быстрым спадом в модели до 10,36 кГц. Либо постоянная частота, либо короче пауза.


57е.8 Итоги

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

chop57e.c — логика чоппера: таблица микрошага, режимы моста, порог ЦАП, подстройка спада.

/* Раздел 57е: логика чоппера — таблица микрошага, режимы каналов моста, порог ЦАП, подстройка спада. */
#include "chop57e.h"

/* Четверть косинуса для микрошага 1/16: 255·cos(k·π/32), k = 0…16 — та же таблица, что в 57а.3. */
const uint8_t ustep_cos[17] = {
    255, 254, 250, 244, 236, 225, 212, 197, 180, 162, 142, 120, 98, 74, 50, 25, 0
};

/* Ток обмотки A на шаге idx (0…63 — электрический оборот): модуль 0…255 и знак.
 * Обмотка B — тот же вызов с idx + 48 (синус = косинус, сдвинутый на четверть оборота). */
int8_t ustep_level(uint8_t idx, uint8_t *level)
{
    idx &= 63u;
    uint8_t q = idx >> 4, r = idx & 15u;
    uint8_t v = (q == 0u || q == 2u) ? ustep_cos[r] : ustep_cos[16u - r];
    *level = v;
    if (v == 0u)
        return 0;
    return (q == 0u || q == 3u) ? 1 : -1;
}

/* Плечи моста по знаку тока. Счётчик сбрасывается в 0 срабатыванием компаратора:
 *   CNT < n_fast           — быстрый спад: плечо «от» на верхнем ключе, плечо «к» на нижнем (−Vпит);
 *   n_fast <= CNT < n_off  — медленный спад: оба нижних ключа (обмотка закорочена);
 *   CNT >= n_off           — разгон: плечо «к» на верхнем ключе, плечо «от» на нижнем (+Vпит).
 * ШИМ 2 активен при CNT >= CCR, ШИМ 1 — при CNT < CCR; активный выход = верхний ключ плеча. */
void chop_legs(chop_out_t *o, int8_t sign, uint16_t n_off, uint16_t n_fast)
{
    if (n_fast > n_off)
        n_fast = n_off;
    o->sign = sign;
    if (sign > 0) {                    /* ток из плеча 1 в плечо 2 */
        o->ocm1 = OCM_PWM2; o->ccr1 = n_off;
        o->ocm2 = OCM_PWM1; o->ccr2 = n_fast;
    } else if (sign < 0) {             /* ток из плеча 2 в плечо 1 */
        o->ocm1 = OCM_PWM1; o->ccr1 = n_fast;
        o->ocm2 = OCM_PWM2; o->ccr2 = n_off;
    } else {                           /* нулевой ток: оба нижних ключа, обмотка закорочена */
        o->ocm1 = OCM_FORCE_LOW; o->ccr1 = 0;
        o->ocm2 = OCM_FORCE_LOW; o->ccr2 = 0;
    }
}

/* Всё для шага idx: порог ЦАП по таблице и режимы каналов по знаку. */
void chop_set(chop_out_t *o, const chop_cfg_t *c, uint8_t idx)
{
    uint8_t level;
    int8_t sign = ustep_level(idx, &level);
    uint16_t n_fast = c->mode == DECAY_SLOW ? 0u : c->mode == DECAY_FAST ? c->n_off : c->n_fast;
    o->dac = (uint16_t)(((uint32_t)level * c->dac_full + 127u) / 255u);
    chop_legs(o, sign, c->n_off, n_fast);
}

/* Подстройка спада по отсчёту АЦП в начале разгона (valley) против порога (thr), коды одной шкалы.
 * Ток к концу паузы не опустился ниже порога — спад не успевает: быстрая часть растёт на 1/8 паузы.
 * Опустился — быстрая часть убывает на 1/32 паузы: медленный спад даёт меньше пульсаций. */
uint16_t chop_adapt(uint16_t n_fast, uint16_t valley, uint16_t thr, uint16_t n_off)
{
    uint32_t up = n_off / 8u, down = n_off / 32u, n = n_fast;
    if (valley >= thr)
        n = n + up > n_off ? n_off : n + up;
    else
        n = n > down ? n - down : 0u;
    return (uint16_t)n;
}

fw57e.c — чоппер тока двух обмоток на TIM1 и TIM8.

/* Раздел 57е: чоппер тока двух обмоток шагового двигателя на STM32F407 (или STM32F103xE).
 * Обмотка A — мост на TIM1 (CH1/CH1N — плечо 1, CH2/CH2N — плечо 2), обмотка B — тот же мост на TIM8.
 * Внешний компаратор на обмотку: «+» — усиленный шунт общего провода моста (1 В/А), «−» — выход ЦАП;
 * выход компаратора — на вход ETR своего таймера. Срабатывание сбрасывает счётчик (режим сброса):
 * начинается пауза N_OFF тактов, в её начале N_FAST тактов быстрого спада, дальше медленный.
 * Через 1 мкс разгона канал 4 запускает АЦП: ток в начале разгона подстраивает долю быстрого спада.
 * Такты настроены заранее (раздел 6): таймеры — 168 МГц у F407, 72 МГц у F103. */
#include "regs57e.h"
#include "chop57e.h"

#if defined(CHIP_F407)
#define TIM_HZ   168000000u
#define ETF_CODE 15u                      /* 1111: fDTS/32, N = 8 — 256 тактов, 1,52 мкс */
#elif defined(CHIP_F103)
#define TIM_HZ   72000000u
#define ETF_CODE 12u                      /* 1100: fDTS/16, N = 8 — 128 тактов, 1,78 мкс */
#endif
#define N_OFF    (TIM_HZ / 50000u)        /* пауза 20 мкс */
#define N_ADC    (TIM_HZ / 1000000u)      /* выборка АЦП через 1 мкс разгона */
#define N_DT     (TIM_HZ / 2000000u)      /* защитный интервал 0,5 мкс */
#define DAC_FULL 1862u                    /* 1,5 А: 1,5 В при 3,3 В на 4096 кодов */

static chop_cfg_t cfg[2] = {
    { N_OFF, 0, DAC_FULL, DECAY_AUTO },
    { N_OFF, 0, DAC_FULL, DECAY_AUTO },
};
static chop_out_t out[2];
static uint8_t step_idx;

/* CCR с предзагрузкой: новые значения вступают при следующем сбросе счётчика (он же событие
 * обновления). Режим канала (OCxM) меняется сразу, но уровень ШИМ пересчитывается только при смене
 * результата сравнения — поэтому при смене знака тока счётчик сбрасывается вручную (UG). */
static void tim_apply(tim_t *t, const chop_out_t *o, int restart)
{
    t->CCR1  = o->ccr1;
    t->CCR2  = o->ccr2;
    t->CCMR1 = ((uint32_t)o->ocm1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE
             | ((uint32_t)o->ocm2 << TIM_CCMR1_OC2M_Pos) | TIM_CCMR1_OC2PE;
    if (restart)
        t->EGR = TIM_EGR_UG;
}

/* Мост одной обмотки: счётчик вверх, сброс по фронту ETR, канал 4 — запуск АЦП. */
static void tim_bridge_init(tim_t *t)
{
    t->CR1   = 0;
    t->PSC   = 0;
    t->ARR   = 0xFFFFu;                   /* не сработал компаратор — пауза через 65536 тактов */
    t->SMCR  = (ETF_CODE << TIM_SMCR_ETF_Pos) | TIM_SMCR_TS_ETRF | TIM_SMCR_SMS_RESET;
    t->CCMR1 = (OCM_FORCE_LOW << TIM_CCMR1_OC1M_Pos) | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCMR2 = (OCM_PWM2 << TIM_CCMR2_OC4M_Pos) | TIM_CCMR2_OC4PE;   /* OC4REF встаёт при CNT = CCR4 */
    t->CCR4  = N_OFF + N_ADC;
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE
             | TIM_CCER_CC4E;                      /* канал 4 на вывод не выведен */
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

/* Для сравнения: постоянная частота 20 кГц, медленный спад. Канал 1 — ШИМ 100 % (CCR1 > ARR),
 * высокий уровень ETR гасит его до следующего переполнения; плечо 2 всё время на нижнем ключе. */
void tim_fixed_slow_init(tim_t *t)
{
    t->CR1   = 0;
    t->ARR   = TIM_HZ / 20000u - 1u;
    t->SMCR  = ETF_CODE << TIM_SMCR_ETF_Pos;      /* ETR только для гашения, режима сброса нет */
    t->CCR1  = TIM_HZ / 20000u;
    t->CCMR1 = (OCM_PWM1 << TIM_CCMR1_OC1M_Pos) | TIM_CCMR1_OC1PE | TIM_CCMR1_OC1CE
             | (OCM_FORCE_LOW << TIM_CCMR1_OC2M_Pos);
    t->CCER  = TIM_CCER_CC1E | TIM_CCER_CC1NE | TIM_CCER_CC2E | TIM_CCER_CC2NE;
    t->BDTR  = N_DT | TIM_BDTR_MOE;
    t->EGR   = TIM_EGR_UG;
    t->CR1   = TIM_CR1_CEN;
}

#if defined(CHIP_F407)
static void pin_af(gpio_t *g, unsigned pin, unsigned af)
{
    g->MODER = (g->MODER & ~(3u << 2 * pin)) | (2u << 2 * pin);
    g->AFR[pin >> 3] = (g->AFR[pin >> 3] & ~(15u << 4 * (pin & 7u))) | (af << 4 * (pin & 7u));
}

static void pins_init(void)
{
    RCC_AHB1ENR |= 7u;                                     /* GPIOA, GPIOB, GPIOC */
    pin_af(GPIOA, 8, 1);  pin_af(GPIOA, 9, 1);             /* TIM1_CH1, TIM1_CH2 */
    pin_af(GPIOB, 13, 1); pin_af(GPIOB, 14, 1);            /* TIM1_CH1N, TIM1_CH2N */
    pin_af(GPIOA, 12, 1);                                  /* TIM1_ETR — компаратор обмотки A */
    pin_af(GPIOC, 6, 3);  pin_af(GPIOC, 7, 3);             /* TIM8_CH1, TIM8_CH2 */
    pin_af(GPIOA, 7, 3);  pin_af(GPIOB, 0, 3);             /* TIM8_CH1N, TIM8_CH2N */
    pin_af(GPIOA, 0, 3);                                   /* TIM8_ETR — компаратор обмотки B */
    GPIOA->MODER |= (3u << 2 * 1) | (3u << 2 * 2) | (3u << 2 * 4) | (3u << 2 * 5); /* PA1, PA2 — АЦП; PA4, PA5 — ЦАП */
}

static void adc_init(void)
{
    RCC_APB2ENR |= RCC_ADC1EN | RCC_ADC2EN;
    ADC_CCR = 1u << 16;                                    /* ADCPRE = 01: 84 МГц / 4 = 21 МГц */
    ADC1->SMPR2 = 0;                       /* 3 такта АЦП: выборка кончится раньше, чем сработает фильтр ETR */
    ADC2->SMPR2 = 0;
    ADC1->JSQR = 1u << ADC_JSQR_JSQ4_Pos;                  /* JL = 0: один канал, PA1 */
    ADC2->JSQR = 2u << ADC_JSQR_JSQ4_Pos;                  /* PA2 */
    ADC1->CR1 = ADC_CR1_JEOCIE;
    ADC2->CR1 = ADC_CR1_JEOCIE;
    ADC1->CR2 = (1u << 20) | (0u << 16) | ADC_CR2_ADON;    /* по фронту, JEXTSEL 0000 — TIM1_CH4 */
    ADC2->CR2 = (1u << 20) | (14u << 16) | ADC_CR2_ADON;   /* по фронту, JEXTSEL 1110 — TIM8_CH4 */
}
#elif defined(CHIP_F103)
static void pin_mode(gpio_t *g, unsigned pin, unsigned mode)
{
    volatile uint32_t *cr = pin < 8u ? &g->CRL : &g->CRH;
    unsigned sh = 4u * (pin & 7u);
    *cr = (*cr & ~(15u << sh)) | (mode << sh);
}

static void pins_init(void)
{
    RCC_APB2ENR |= RCC_AFIOEN | RCC_IOPAEN | RCC_IOPBEN | RCC_IOPCEN;
    pin_mode(GPIOA, 8, 0xB);  pin_mode(GPIOA, 9, 0xB);     /* TIM1_CH1, CH2: AF, 50 МГц */
    pin_mode(GPIOB, 13, 0xB); pin_mode(GPIOB, 14, 0xB);    /* TIM1_CH1N, CH2N */
    pin_mode(GPIOC, 6, 0xB);  pin_mode(GPIOC, 7, 0xB);     /* TIM8_CH1, CH2 */
    pin_mode(GPIOA, 7, 0xB);  pin_mode(GPIOB, 0, 0xB);     /* TIM8_CH1N, CH2N */
    pin_mode(GPIOA, 12, 0x4); pin_mode(GPIOA, 0, 0x4);     /* TIM1_ETR, TIM8_ETR: вход */
    pin_mode(GPIOA, 1, 0x0);  pin_mode(GPIOA, 2, 0x0);     /* АЦП */
    pin_mode(GPIOA, 4, 0x0);  pin_mode(GPIOA, 5, 0x0);     /* ЦАП */
}

static void adc_init(void)
{
    RCC_CFGR = (RCC_CFGR & ~(3u << 14)) | (2u << 14);      /* ADCPRE = 10: 72 МГц / 6 = 12 МГц */
    RCC_APB2ENR |= RCC_ADC1EN | RCC_ADC2EN;
    AFIO_MAPR |= AFIO_ADC2_ETRGINJ_REMAP;
    adc_t *a[2] = { ADC1, ADC2 };
    for (unsigned k = 0; k < 2u; k++) {
        a[k]->CR2 = ADC_CR2_ADON;                          /* включить */
        for (volatile unsigned d = 0; d < 1000u; d++) { }  /* больше 1 мкс на пробуждение */
        a[k]->CR2 = ADC_CR2_ADON | ADC_CR2_CAL;
        while (a[k]->CR2 & ADC_CR2_CAL) { }
        a[k]->SMPR2 = 0;                                   /* 1,5 такта: выборка до срабатывания фильтра */
        a[k]->JSQR = (k + 1u) << ADC_JSQR_JSQ4_Pos;
        a[k]->CR1 = ADC_CR1_JEOCIE;
    }
    ADC1->CR2 = (1u << 15) | (1u << 12) | ADC_CR2_ADON;    /* JEXTTRIG, JEXTSEL 001 — TIM1_CC4 */
    ADC2->CR2 = (1u << 15) | (6u << 12) | ADC_CR2_ADON;    /* JEXTSEL 110 + переназначение — TIM8_CC4 */
}
#endif

/* Шаг микрошага: dir = +1 или −1. Вызывать с выключенным прерыванием АЦП или из прерывания
 * того же приоритета — обработчик АЦП тоже пишет out[] и CCR. */
void stepper_step(int8_t dir)
{
    int8_t s0 = out[0].sign, s1 = out[1].sign;
    step_idx = (uint8_t)((step_idx + dir) & 63u);
    chop_set(&out[0], &cfg[0], step_idx);
    chop_set(&out[1], &cfg[1], (uint8_t)(step_idx + 48u));
    DAC->DHR12R1 = out[0].dac;
    DAC->DHR12R2 = out[1].dac;
    tim_apply(TIM1, &out[0], out[0].sign != s0);
    tim_apply(TIM8, &out[1], out[1].sign != s1);
}

static void adapt(unsigned w, tim_t *t, uint16_t valley)
{
    chop_out_t *o = &out[w];
    if (cfg[w].mode != DECAY_AUTO || o->sign == 0)
        return;
    cfg[w].n_fast = chop_adapt(cfg[w].n_fast, valley, o->dac, cfg[w].n_off);
    chop_legs(o, o->sign, cfg[w].n_off, cfg[w].n_fast);
    tim_apply(t, o, 0);
}

#if defined(CHIP_F407)
void ADC_IRQHandler(void)
#else
void ADC1_2_IRQHandler(void)
#endif
{
    if (ADC1->SR & ADC_SR_JEOC) {
        ADC1->SR = ~ADC_SR_JEOC;
        adapt(0, TIM1, (uint16_t)ADC1->JDR1);
    }
    if (ADC2->SR & ADC_SR_JEOC) {
        ADC2->SR = ~ADC_SR_JEOC;
        adapt(1, TIM8, (uint16_t)ADC2->JDR1);
    }
}

void chopper_init(void)
{
    pins_init();
    RCC_APB1ENR |= RCC_DACEN;
    RCC_APB2ENR |= RCC_TIM1EN | RCC_TIM8EN;
    DAC->CR = DAC_CR_EN1 | DAC_CR_EN2;
    adc_init();
    step_idx = 0;
    stepper_step(0);                                       /* шаг 0: A — полный ток, B — ноль */
    tim_bridge_init(TIM1);
    tim_bridge_init(TIM8);
    tim_apply(TIM1, &out[0], 1);
    tim_apply(TIM8, &out[1], 1);
    NVIC_ISER0 = 1u << ADC_IRQ_NUM;
}

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

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