57г. Klipper: STM32 как исполнитель 3D-принтера — шаги по расписанию от компьютера
В разделе 57а шаговый двигатель разгонялся самим микроконтроллером: интервал между шагами считался в прерывании рекуррентной формулой. Так устроены прошивки принтеров вроде Marlin — всё, от разбора G-кода до кинематики, живёт в одном STM32. В 57в команды двигателю уже приходили по CAN от другого блока.
Klipper делит работу иначе. Всё сложное — разбор G-кода, планирование скорости, кинематику, сглаживание — считает компьютер на Linux (обычно одноплатный). Он знает момент каждого шага с точностью до такта таймера микроконтроллера и присылает готовое расписание. STM32 только исполняет: «через столько-то тактов шагни, потом ещё столько-то раз с таким-то приращением интервала». Здесь — как это расписание сжимается, как устроен протокол, как два устройства договариваются о времени и что меняется, когда плата стоит на печатающей головке и связана по CAN.
Опоры: исходники Klipper (коммит 7bc4d09): МК-сторона —
src/command.c, src/sched.c, src/stepper.c, src/endstop.c,
src/trsync.c, src/generic/armcm_timer.c, src/stm32/stm32f0_timer.c,
src/generic/canserial.c, src/generic/usb_canbus.c, src/stm32/Kconfig;
компьютер — klippy/chelper/stepcompress.c, msgblock.c, serialqueue.c,
itersolve.c, trapq.c, kin_cartesian.c, klippy/clocksync.py,
klippy/stepper.py, klippy/serialhdl.py, klippy/extras/homing.py,
klippy/extras/canbus_ids.py. Код не пересказан на глаз: настоящие
trapq.c, itersolve.c, kin_cartesian.c, stepcompress.c и msgblock.c
посчитали шаги движения и сжали их, настоящий src/command.c с
crc16_ccitt.c разобрал поток байт как микроконтроллер, а вся прошивка
собрана под пять вариантов STM32. Прогон — _verify/klip57g/klip57g.py,
запись — _verify/klip57g/klip57g_host.txt. Лицензия: Klipper — GPL-3.0
(файл COPYING; в заголовках файлов «GNU GPLv3 license», © Kevin O'Connor).
Как это называется в документации
Оригинал Перевод host, klippy компьютер, программа на нём MCU, micro-controller микроконтроллер, исполнитель print time, clock время печати (секунды), такты таймера МК message block, sequence number блок сообщения, номер блока VLQ (variable length quantity) число переменной длины ACK / NAK подтверждение / просьба повторить data dictionary, identify словарь данных, опознание МК при подключении step compression, max error сжатие шагов, допустимая ошибка момента шага queue_step interval count add команда «шагов count, первый через interval, интервал меняется на add» clock synchronization синхронизация часов компьютера и МК endstop, homing, trsync концевик, поиск нуля, синхронизация срабатывания toolhead board, CAN bridge плата на печатающей головке, мост USB — CAN
57г.1 Кто что считает
Прошивка на STM32 не знает ни G-кода, ни миллиметров, ни ускорений. У неё
есть объекты с номерами (oid): шаговый двигатель, концевик, нагреватель,
датчик. Компьютер при подключении настраивает их командами вроде
config_stepper oid=%c step_pin=%c dir_pin=%c …, а дальше присылает
команды с моментами в тактах таймера МК.
На компьютере движение живёт в очереди trapq отрезками с постоянным
ускорением. Для каждого двигателя itersolve.c с кинематикой (декартовой,
CoreXY, дельта…) ищет моменты, когда положение оси пересекает очередную
границу шага, — с плавающей точкой, в секундах. stepcompress.c переводит
их в такты МК и упаковывает в команды queue_step.
На микроконтроллере command.c разбирает блок и вызывает обработчик.
command_queue_step кладёт тройку «interval, count, add» в очередь двигателя
(stepper.c), а сам шаг делает обработчик таймера: поднял ножку STEP,
прибавил интервал, интервал увеличил на add, поставил таймер снова. Все
таймеры — один отсортированный список в sched.c; прерывание берёт первый
из списка и перепланирует его.
Время. Klipper нужен свободно бегущий 32-битный счётчик, а не периодическое прерывание:
| Семейство | Счётчик времени | Прерывание | Частота в сборке |
|---|---|---|---|
| STM32F1, F4 (Cortex-M3/M4) | DWT->CYCCNT — такты ядра | SysTick на разницу до следующего таймера | 72 МГц (F103), 168 МГц (F407) |
| STM32G0 (Cortex-M0+, DWT нет) | TIM2, 32 бита | сравнение канала 1 TIM2 | 64 МГц (G0B1) |
Выбор — в src/stm32/Makefile: для F0 и G0 берётся stm32f0_timer.c,
который использует TIM2, если он есть в заголовке кристалла (в
stm32g0b1xx.h — есть), иначе 16-битный TIM3. Остальные STM32 работают через
armcm_timer.c. Список поддерживаемых кристаллов — src/stm32/Kconfig: от
F031 и F103 до G0B1, G474, H743 и L412.
57г.2 Протокол: блоки, числа переменной длины, CRC
Команды идут блоками. Формат задан в src/command.h:
| Байт | Что |
|---|---|
| 0 | длина всего блока: от 5 (MESSAGE_MIN) до 64 (MESSAGE_MAX) |
| 1 | 0x10 + номер блока 0…15 (MESSAGE_DEST, MESSAGE_SEQ_MASK) |
| 2…n−4 | команды подряд: номер команды и параметры |
| n−3, n−2 | CRC16 старшим байтом вперёд |
| n−1 | 0x7E (MESSAGE_SYNC) |
Номер команды и все целые параметры — числа переменной длины (VLQ): по
7 бит в байте, старший бит байта — «дальше ещё байт». Знак передаётся без
отдельного бита: если в первом байте биты 6 и 5 — 11, число
отрицательное. Кодер из src/command.c:
if (sv < (3L<<5) && sv >= -(1L<<5)) goto f4;
if (sv < (3L<<12) && sv >= -(1L<<12)) goto f3;
if (sv < (3L<<19) && sv >= -(1L<<19)) goto f2;
if (sv < (3L<<26) && sv >= -(1L<<26)) goto f1;
Прогон: компьютер кодирует (msgblock.c), МК разбирает (command_parsef
из command.c):
| Число | Байты | Длина |
|---|---|---|
| 0 | 00 |
1 |
| 95 | 5F |
1 |
| 96 | 80 60 |
2 |
| −32 | 60 |
1 |
| −33 | FF 5F |
2 |
| 1800 | 8E 08 |
2 |
| 12 288 | 80 E0 00 |
3 |
| 1 572 864 | 80 E0 80 00 |
4 |
| 2 147 483 647 | 87 FF FF FF 7F |
5 |
Один байт — от −32 до 95, пять — самые большие 32-битные. Все 15 чисел прогона вернулись без искажений.
CRC16 — crc16_ccitt() с начальным значением 0xFFFF, одна и та же
функция на МК (src/generic/crc16_ccitt.c) и на компьютере
(msgblock_crc16_ccitt). Для строки «123456789» обе дали 0x6F91.
Словарь данных. Номера команд не зашиты в протокол: при сборке
scripts/buildcommands.py собирает из исходников все объявления
DECL_COMMAND, раздаёт номера и кладёт в прошивку сжатый zlib словарь. При
подключении компьютер читает его командой identify и дальше знает, что
queue_step oid=%c interval=%u count=%hu add=%hi — это команда 22, а
частота таймера — константа CLOCK_FREQ. Во всех пяти сборках ниже
queue_step получила номер 22, set_next_step_dir — 21. Словарь
сборки F103 с USB — 116 команд, 25 ответов, 19 констант: 10 860 байт JSON,
в прошивке сжатый — 3746 байт (command_identify_data).
57г.3 Номера блоков, ACK и NAK
На каждый целый блок с верным CRC и ожидаемым номером МК отвечает пустым
блоком из 5 байт с номером следующего ожидаемого блока — это ACK. Если
CRC не сошёлся или номер не тот, МК отвечает таким же блоком со своим
ожидаемым номером — это NAK, компьютер повторяет всё начиная с него.
Прогон через настоящий command_find_and_dispatch():
| Что пришло на МК | Результат | Ответ МК |
|---|---|---|
блок seq 1 0B 11 16 00 8E 08 0A 7D BB 4A 7E — свежий МК ждёт seq 0 |
отброшен | 05 10 9E 81 7E |
| тот же queue_step с seq 0 | выполнен: 10 шагов | 05 11 8F 08 7E |
| seq 1, один байт испорчен | отброшен | 05 11 8F 08 7E |
| seq 2 раньше seq 1 | отброшен | 05 11 8F 08 7E |
| повтор seq 1 | выполнен | 05 12 BD 93 7E |
| повтор seq 2 | выполнен, всего 30 шагов | 05 13 AC 1A 7E |
Первая строка — это и есть подключение: serialqueue.c начинает с номера 1,
а получив ответ на блок, которого не отправлял, принимает номер МК как свой
(update_receive_seq, «must be connection init»). Разбор блока
0B 11 16 00 8E 08 0A 7D BB 4A 7E: длина 11, номер 1, команда 22
(queue_step), oid 0, interval 1800 (8E 08), count 10, add −3 (7D), CRC
0xBB4A, 0x7E. Десять шагов с интервалом 1800 и приращением −3 кончаются на
такте 10 × 1800 − 3 × 45 = 17 865 — столько и насчитал МК.
57г.4 Сжатие шагов: queue_step и max_error
Слать каждый шаг отдельной командой дорого. stepcompress.c ищет отрезки,
которые можно описать тройкой interval, count, add: первый шаг через
interval тактов после предыдущего, дальше интервал каждый раз меняется на
add. Момент шага i (от 1) — interval·i + add·i(i−1)/2: при постоянной
скорости add = 0, при разгоне интервал сокращается.
Точного совпадения не требуется. Для каждого шага допустимо окно
(minmax_point в stepcompress.c):
uint32_t max_error = (point - prevpoint) / 2;
if (max_error > sc->max_error)
max_error = sc->max_error;
return (struct points){ point - max_error, point };
Шаг можно сделать раньше точного момента, но не больше чем на
max_error и не больше половины промежутка до предыдущего шага, и никогда
позже. max_error = MAX_STEPCOMPRESS_ERROR = 25 мкс (klippy/stepper.py),
переведённые в такты МК: 1800 на 72 МГц. Функция compress_bisect_add()
перебирает add делением диапазона, для каждого находит самую длинную
серию, укладывающуюся во все окна, и берёт ту, что дальше всех
продвинулась; check_line() ещё раз проверяет каждую точку.
Прогон. Одно движение по X: 50 мм, скорость 100 мм/с, ускорение
3000 мм/с², 80 шагов на мм, частота МК 72 МГц (F103). Разгон — 0,033333 с
(1,6667 мм), крейсер — 0,466667 с, всего 0,533333 с. Настоящие trapq.c,
itersolve.c и kin_cartesian.c дали 4000 шагов, stepcompress.c
сжал их в 16 команд queue_step и одну set_next_step_dir. Начало
разгона:
| interval | count | add | Байты команды |
|---|---|---|---|
| 7 346 969 | 1 | 0 | 16 00 83 C0 B6 19 01 00 |
| 106 145 | 2 | −30 626 | 16 00 86 BD 21 02 FE 90 5E |
| 58 426 | 4 | −5490 | 16 00 83 C8 3A 04 FF D5 0E |
| 38 184 | 8 | −1577 | 16 00 82 AA 28 08 F3 57 |
| 26 197 | 15 | −533 | 16 00 81 CC 55 0F FB 6B |
| 18 683 | 25 | −206 | 16 00 81 91 7B 19 FE 32 |
Первая команда — один шаг через 7 346 969 тактов (0,102 с) от нулевого момента счёта; дальше серии растут: 2, 4, 8, 15, 25 шагов — чем ровнее разгон, тем длиннее серия. На крейсере интервал 9000 тактов = 125 мкс, ровно 100 мм/с × 80 шаг/мм = 8000 шагов/с.
Сверка. Блоки прошли через command.c, обработчик восстановил моменты
так же, как stepper.c. Все 4000 шагов на месте; раньше точного момента
— не больше 1797,259 такта (допуск 1800), позже — не больше 0,296
такта: это округление точного дробного момента до целого такта, само сжатие
позже не сдвигает.
Сколько байт.
| Команд | Байт команд | Блоков | Байт в линии | |
|---|---|---|---|---|
| без сжатия (count = 1 на каждый шаг) | 4000 | 24 144 | ||
| stepcompress | 17 | 128 | 3 | 143 |
Сжатие — в 188,6 раза. 143 байта по UART на 250 000 бод
(SERIAL_BAUD по умолчанию) идут 5,72 мс, по CAN на 1 Мбит/с — 19 кадров,
2025 бит, 2,02 мс (формула calculate_bittime() из serialqueue.c), а
само движение длится 533,33 мс.
От чего зависит число команд. Тот же отрезок на разных частотах МК и скоростях:
| Частота МК | max_error, тактов | queue_step | Байт в линии | Раньше, до тактов | Позже, до тактов |
|---|---|---|---|---|---|
| 64 МГц (G0B1) | 1600 | 17 | 150 | 1600,073 | 0,303 |
| 72 МГц (F103) | 1800 | 16 | 143 | 1797,259 | 0,296 |
| 168 МГц (F407) | 4200 | 18 | 162 | 4200,481 | 0,441 |
| Скорость, 72 МГц | queue_step | Байт команд | Байт в линии |
|---|---|---|---|
| 50 мм/с | 12 | 100 | 110 |
| 100 мм/с | 16 | 128 | 143 |
| 200 мм/с | 24 | 182 | 202 |
| 300 мм/с | 31 | 232 | 257 |
Быстрее — длиннее участки разгона и торможения, где серии короче. «Раньше» на 64 и 168 МГц чуть больше допуска (1600,073 и 4200,481) — это та же доля такта от округления точного момента: окно считается от целого такта.
57г.5 Часы и концевики
Шаги компьютер назначает во «времени печати» — секундах на своих часах. Чтобы
перевести их в такты МК, нужна связь двух часов. Этим занят klippy/clocksync.py:
- при подключении — 8 запросов
get_clockчерез 50 мс, дальше — раз в 0,9839 с («необычный» период, чтобы не совпадать с другими периодическими событиями); - каждый ответ
clock— 32-битный такт МК; компьютер продлевает его до 64 бит по разнице с прошлым; - пары «время отправки — такт» идут в линейную регрессию с экспоненциальным
забыванием: вес нового замера
DECAY= 1/30; наклон — оценка частоты МК, то есть ухода его кварца; - замер, отклонившийся от прогноза больше чем на 5 сигм и больше чем на 0,5 мс, отбрасывается; отдельно запоминается наименьшая половина времени «туда-обратно».
Оценку частоты и сдвига stepcompress_set_time() получает для каждого
двигателя: момент шага в секундах превращается в такт как
(время − сдвиг) × частота. Если плат несколько (основная и на головке), у
каждой своя регрессия, и одно и то же время печати переводится в такты
каждой платы отдельно.
Если команда опоздала. Команды уходят заранее: компьютер держит
их до BUFFER_TIME_HIGH = 1 с впереди исполнения (klippy/toolhead.py), а
МК кладёт их в очередь двигателя. Если команда пришла, когда её время уже прошло, МК не
«догоняет»: sched_add_timer() останавливает всё с ошибкой «Timer too
close», stepper_load_next() — с «Stepper too far in past» (больше 1 мс
опоздания).
Концевик и поиск нуля. Компьютер присылает endstop_home с моментом
начала, периодом опроса и числом подтверждений. Опрос — тоже таймер в
sched.c: endstop_event() читает ножку, при совпадении переходит к
повторным чтениям через sample_ticks, и только после sample_count
совпадений подряд вызывает trsync_do_trigger(). По умолчанию
ENDSTOP_SAMPLE_TIME = 15 мкс и ENDSTOP_SAMPLE_COUNT = 4
(klippy/extras/homing.py). Срабатывание останавливает двигатели,
подписанные на тот же trsync (stepper_stop() сбрасывает их очереди),
сразу на МК, без ожидания компьютера. Если концевик на одной плате, а
двигатель на другой, их связывает компьютер: каждая плата сообщает, что
жива; не пришло сообщение за TRSYNC_TIMEOUT = 25 мс (250 мс, если плата одна)
— поиск нуля прерывается.
57г.6 CAN, плата на головке и сборка прошивки
CAN. Плату на печатающей головке (например, на G0B1) подключают витой
парой CAN и питанием вместо жгута к каждому двигателю, нагревателю и датчику. Протокол
тот же, байтовый поток режется на кадры по 8 байт (do_write() в
serialqueue.c, стандартные 11-битные идентификаторы). Номера узлов
раздаёт компьютер:
| Что | Идентификатор |
|---|---|
| запрос «кто без номера» и назначение номера | 0x3F0 (CANBUS_ID_ADMIN) |
| ответ платы: 6 байт UUID | 0x3F1 |
| компьютер → плата с номером узла n | n × 2 + 256: для первого n = 4 — 0x108 |
| плата → компьютер | на единицу больше: 0x109 |
UUID платы — 6 байт хеша fasthash64 от заводского номера кристалла
(canserial_set_uuid), его пишут в конфигурацию (canbus_uuid). Первый
номер узла — NODEID_FIRST = 4 (canbus_ids.py). Скорость по умолчанию —
CANBUS_FREQUENCY 1 000 000.
Мост USB — CAN. Основную плату можно собрать режимом
USBCANBUS: для Linux она выглядит как адаптер gs_usb (usb_canbus.c), а
сама остаётся узлом Klipper на этой же шине. Режим есть там, где у
кристалла и USB, и CAN: F042/F072, F207, F405/F407/F429, F446, G0B1, G4, H7.
Кроме STM32F1 — в Kconfig у HAVE_STM32_USBCANBUS стоит
depends on !MACH_STM32F1: у F103 USB и CAN делят одну память буферов
и вместе не работают (198е), так что выбирается одно из двух.
Сборка без сети. Сборка Klipper — make menuconfig и make. Меню не нужно: достаточно
файла .config из двух-трёх строк и make olddefconfig (он в lib/kconfiglib
внутри дерева, Python 3). Для USB на F103:
CONFIG_MACH_STM32=y
CONFIG_MACH_STM32F103=y
Для CAN добавлена строка выбора ножек: CONFIG_STM32_CANBUS_PA11_PA12=y
(F103) или CONFIG_STM32_MMENU_CANBUS_PB0_PB1=y (G0B1). Остальное —
значения по умолчанию из Kconfig: частота, объём памяти, загрузчик.
arm-none-eabi-gcc 12.2 в stm-arm:1:
| Сборка | Частота | Код (text) | Данные | ОЗУ (bss) | klipper.bin |
|---|---|---|---|---|---|
| F103, USB | 72 МГц | 43 682 | 52 | 1164 | 43 736 |
| F103, CAN (bxCAN) | 72 МГц | 43 166 | 52 | 1284 | 43 220 |
| F407, USB | 168 МГц | 46 410 | 64 | 1220 | 46 476 |
| G0B1, USB | 64 МГц | 47 540 | 52 | 1160 | 47 592 |
| G0B1, CAN (FDCAN) | 64 МГц | 47 260 | 52 | 1280 | 47 312 |
По умолчанию F103 собирается под 64 КБ флеш-памяти (FLASH_SIZE 0x10000)
с загрузчиком на 8 КБ (прошивка с 0x8002000) — 43 736 байт влезают в
оставшиеся 56 КБ. F407 — с 0x8008000 (загрузчик 32 КБ). ОЗУ под очереди
команд Klipper выделяет при старте из оставшейся памяти, поэтому bss мал.
Обработчик шага stepper_event_full — 96 байт на F1/F4 и 82 на G0;
прерывание таймера — SysTick 296 байт (F103) и 304 (F407), TIM2 на G0B1 —
284; приём CAN — 360 (bxCAN F103) и 328 (FDCAN G0B1).
Две мелочи сборки на Windows-копии дерева: в образе нет cpp — компоновочный
скрипт готовится через make CPP=arm-none-eabi-cpp; scripts/check-gcc.sh
с переводами строк CRLF не запускается («No such file or directory»). Метка
времени сборки попадает в словарь; для одинаковых размеров от прогона к
прогону она заменена постоянной.
57г.7 Грабли
«Timer too close» или «Stepper too far in past» при печати. МК получил команду, время которой уже прошло: компьютер не успел (перегружен, USB-концентратор, медленная карта памяти) или синхронизация часов сбилась. МК не догоняет, а останавливается — так задумано.
Хвост очереди trapq «вернул» ось. В прогоне без завершающей стоянки
itersolve дошёл до служебного конца очереди с нулевым положением и
насчитал 4000 шагов обратно — stepcompress отказался: «Invalid sequence» с
отрицательным интервалом. В Klipper движение всегда кончается стоянкой, в
своей обвязке её надо добавить самому.
Первый блок после подключения отброшен. Это норма: свежий МК ждёт номер 0, компьютер начинает с 1 и принимает номер из NAK.
Шаг «раньше» больше, чем max_error. 1600,073 такта при допуске 1600 —
доля такта от округления точного момента; окно считается от целого такта.
Номер команды из чужой сборки. Номера раздаются при сборке. Компьютер всегда берёт их из словаря подключённой платы; жёстко вписывать 22 в свою программу нельзя.
F103 и мост USB — CAN. В Kconfig для F1 режима нет: USB и CAN у F103
делят одну память буферов и вместе не работают. Мост — на F042/F072, F207, F405/F407/F429,
F446, G0B1, G4, H7.
Флеш F103 кончается. Сборка по умолчанию считает флеш 64 КБ и загрузчик
8 КБ; остаётся 56 КБ, занято 43 736 байт. Лишние модули отключаются в
src/Kconfig (WANT_ADC, WANT_SPI, WANT_I2C и др.); при флеш меньше
64 КБ HAVE_LIMITED_CODE_SIZE включает -Os.
57г.8 Итоги
- Компьютер считает момент каждого шага, STM32 только исполняет команды по тактам своего таймера: DWT + SysTick на F1/F4, TIM2 на G0.
- Блок: длина,
0x10+ номер 0…15, команды, CRC16,0x7E; 5…64 байта; числа — VLQ от 1 до 5 байт, CRC16 «123456789» = 0x6F91. - ACK — пустой блок из 5 байт с номером следующего; сбой CRC или номера — NAK и повтор.
queue_step interval count addописывает серию шагов с постоянным приращением интервала; шаг не позже точного и не раньше чем на 25 мкс.- Трапеция 50 мм, 100 мм/с, 3000 мм/с², 80 шаг/мм на 72 МГц: 4000 шагов →
16
queue_step, 143 байта в линии вместо 24 144 байт команд — в 188,6 раза. - Часы:
get_clockраз в 0,9839 с, регрессия с весом 1/30. - CAN: номер узла с 4, идентификаторы n × 2 + 256 и на единицу больше, кадры по 8 байт, 1 Мбит/с по умолчанию.
- Прошивка: F103 USB — 43 736 байт, F407 — 46 476, G0B1 CAN — 47 312.
Дальше: раздел 57д «FOC своими руками: SimpleFOC и ODrive — векторное управление бесколлекторным мотором» — от шаговых двигателей по расписанию к бесколлекторным с векторным управлением.