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 OCxREFclear,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 А:
- разгон: ток растёт на 7,250 А/мс (
(24 − 1,5·1,5)/3 мГн); - медленный спад за 20 мкс: падение 14,93 мА — обмотка стекает с постоянной L/R = 2000 мкс;
- быстрый спад за 20 мкс: 174,13 мА, в 11,7 раза больше;
- смешанный (5 мкс быстрый, 15 мкс медленный): 54,58 мА.
Сколько держать паузу, решают по-разному. Постоянная пауза: после срабатывания компаратора мост ровно 20 мкс в спаде, затем снова разгон; частота «плавает». Постоянная частота: каждые 50 мкс (20 кГц) разгон начинается заново, срабатывание лишь обрывает его до конца периода.
После включения на шунте выброс от перезаряда ёмкостей ключей, поэтому компаратор первое время не слушают — гашение. В нашем чоппере его роль играет фильтр входа ETR таймера: 8 одинаковых выборок через 32 такта на 168 МГц — 1,524 мкс (57е.5). Пока идёт гашение, ток растёт ещё на 11,05 мА сверх порога.
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 мА |
Что видно.
- Чоппер держит пик, а не среднее. Средний ток ниже порога примерно на половину размаха: у быстрого спада −77,39 мА при размахе 174,55 мА. Медленный спад даже чуть выше порога (+2,84 мА): перелёт за время гашения (11 мА) больше половины его маленькой пилы.
- Чем быстрее спад, тем реже переключения: 45,30 → 36,32 → 22,78 кГц. При постоянной паузе частота зависит от тока и скорости — при вращении 2 об/с с быстрым спадом циклы в модели от 10,36 до 46,46 кГц, нижняя граница уже слышна. Постоянная частота от этого свободна, но при быстром спаде ей нечем ограничить его длину: размах 380,06 мА.
- Быстрый спад на малом токе перескакивает ноль: при 0,147 А за паузу ток уходит до −2,8 мА, при 20 кГц — до −47,3 мА.
- Медленный спад малый ток не держит вовсе: задано 0,147 А, а течёт 1,1327 А. Это не поломка модели — следующий подраздел.
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 Итоги
- Мост держит ток тремя состояниями: разгон (+24 В), медленный спад (закоротка, 14,93 мА за 20 мкс при 1,5 А), быстрый (−24 В, 174,13 мА).
- Чоппер держит пик: средний ток ниже порога на половину пилы (−77,39 мА у быстрого спада), медленный — +2,84 мА.
- Гашение ставит «пол» медленному спаду:
Vпит·t/(R·(t + пауза))= 1,1327 А при паузе 20 мкс; 0,5872 А при 40 мкс. - При вращении медленный спад проигрывает противо-ЭДС: СКО от 555,9 мА на 0,5 об/с до 1297,7 мА на 4 об/с; смешанный ¼ — 40,6…524,4 мА; подстройка по АЦП — 35,0…122,5 мА, доля быстрого спада от 14,0 % до 65,1 %.
- Постоянную паузу делает TIM1 в режиме сброса по ETR (
SMS= 100,TS= 111,ETF= 1111 — 1,524 мкс на 168 МГц), постоянную частоту — гашение каналаOCxCE; при смене знака —UG. - Прошивка двух обмоток на TIM1/TIM8 + ЦАП + ADC1/ADC2: 928 байт для F407 и 940 для F103xE плюс 241 байт общей логики.
Полные исходники
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ж «Микрошаг без драйвера: стрелочный шаговый двигатель прямо от таймера».