Мой CNC-роутер служил верой и правдой два года, но что-то пошло не так слетела прошивка, а был это woodpecker 0.9.

Сначала я хотел ее просто перезалить, и, с этой целью раздобыл исходные коды Grbl CNC Project. Но любопытство пересилило и я погрузился в изучение этих исходников…

Построены они очень просто и логично, но какой же русский не любит быстрой езды как же можно пройти мимо возможности что-нибудь подковать улучшить! По мотивам того что получилось, эта короткая воскресная заметка.

Собственно идея контроллера для CNC-машины довольна проста и интересна. Есть несколько потоков обработки — один читает данные (gcode) и разбирает их, второй превращает команды в блоки исполнения и третий (stepper) собственно исполняет эти блоки. Вот об этом третьем потоке и пойдет речь.

Степпер имеет дело со списком отдельных команд вида — сделай (X,Y,Z) шагов для всех трёх (как минимум) шаговых двигателей, причем за указанное время и в заданном направлении (ну это так упрощенно). Надо сказать, что шаговый двигатель со своим драйвером довольно простая в управлении штука — задаешь (0 или 1) направление вращения и затем по положительному перепаду входа (0 -> 1) двигатель пытается сделать один шаг (а всего на оборот обычно 200 шагов). Данные уже подготовлены, так что надо просто как-то соотнести 3 целых числа с заданным временем.

В оригинале у автора использован контроллер atmega328p, но практически без изменений все легко переносится на arm (например, stm32). Но вот сам алгоритм не может не вызывать вопросов.

С одной стороны, использован весьма совершенный алгоритм Брезенхэма, а точнее его разновидность Adaptive Multi-Axis Step-Smoothing. Но с другой стороны, как-то это все сложно и главное, плавность хода шагового мотора и точность работы роутера прямо зависят от точности выдачи сигналов управления. В данном случае это обуславливается частотой на которой работает таймер и временем обработки прерываний — а это дает не более 40-50 кГц в лучшем случае, а обычно и того менее — ну то есть точность задания управления 20-50 мксек.

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

Так как я рассматривал переход на cortex-m (ну точнее на stm32h750, который я очень люблю и который очень подешевел), то такая задача может быть решена вовсе без привлечения CPU только лишь с использованием двух каналов DMA и одного 32-битного счетчика.

Идея очень проста. Пусть один канал по переполнению счетчика записывает новые данные на порт, а второй канал записывает новое максимальное значение счетчика (это разумно делать на первом же такте работы счетчика). Тогда для обработки команды из списка надо подготовить для массива из значений изменения для порта и интервалов ожидания между ними.

Получится что-то вроде такого.

Обработка по прерыванию — переключение на новый буфер (двойная буфферизация).

#define MAX_PGM 32
typedef struct _pgm_buffer {
        uint32_t data[MAX_PGM];
        uint32_t delta[MAX_PGM];
} pgm_buffer;
pgm_buffer buf[2];
uint32_t current_buf = 1;
uint32_t flags = 0;
void program_down(DMA_HandleTypeDef *_hdma) {
        TIM2->CR1 &= ~TIM_CR1_CEN;
        if ((flags & BUF_RUNNING) == 0)
                return;
        current_buf ^= 1;
        DMA1_Channel5->CCR &= ~1;
        DMA1_Channel2->CCR &= ~1;
        DMA1_Channel5->CNDTR = MAX_PGM;
        DMA1_Channel2->CNDTR = MAX_PGM;
        DMA1_Channel5->CMAR = (uint32_t) (buf[current_buf].delta);
        DMA1_Channel2->CMAR = (uint32_t) (buf[current_buf].data);
        DMA1_Channel5->CCR |= 1;
        DMA1_Channel2->CCR |= 1;
        TIM2->CNT = 0;
        TIM2->ARR = 8;
        TIM2->EGR |= TIM_EGR_UG;
        TIM2->CR1 |= TIM_CR1_CEN;
}

Инициировать можно так:

       HAL_DMA_RegisterCallback(&hdma_tim2_up, HAL_DMA_XFER_CPLT_CB_ID,
                        program_down);
        HAL_DMA_Start_IT(&hdma_tim2_up, buf, &GPIOA->BSRR, MAX_PGM);
        DMA1_Channel5->CCR &= ~1;
        DMA1_Channel5->CPAR = &TIM2->ARR;
        DMA1_Channel5->CCR |= 1;
        TIM2->CCR1 = 1;
        TIM2->DIER |= TIM_DIER_UDE | TIM_DIER_CC1DE;
        flags |= BUF_RUNNING;

Ну а старт — это:

        program_down(NULL);

Что это дает? Давайте подсчитаем на примере того же stm32h750. Таймер (TIM2) там работает на частоте 200 МГц, минимальное время задержки два такта, но DMA не может переслать данные быстрее 50МГц, то есть между двумя командами на переключение порта можно положить (с учетом возможной занятости шины) 40 нсек (25МГц) — это в 1000 раз лучше исходной реализации!

С другой стороны, ширина порта — 16 бит, так что можно одновременно управлять 8 шаговыми двигателями вместо 3 еще бы знать зачем

При этом заполнение собственно данных не вызывает проблем (с таким-то разрешением!) — простая линейная интерполяция по каждому двигателю отдельно с объединением (для оптимизации) событий ближе 40 нсек.

Собственно выводы.

В мастерской лежит готовый CNC-станок с размером 1.2 метра на 0.8 метра с двигателями и драйверами, но без контроллера. Похоже, надо завершить работу и попробовать на нем, насколько это будет эпично. Если сделаю — обязательно напишу продолжение. А пока я не понимаю, почему это контроллеры делают на atmega и они пищат на всех 3d-принтерах и cnc-роутерах на этих грубых прерываниях…

Ну и конечно, наверно имея мощь Cortex-M7, можно реализовать более плавное управление траекторией движения со всеми ограничениями, но это уже совсем другая статья.

P.S. Видимо, надо дать некоторый гипотетический пример, почему так важно иметь столь малое время.

Допустим, что станку надо переместится на 100 мм по X и на 11 мм по Y и софт разбил это все на участки ускорения и равномерного движения — много участков по 100 шагов на 11 шагов и они проходятся на максимальной скорости, пусть это соответствует 10 кГц. Ну 10 шагов по Y точно уложатся в 100 шагов по X, а вот с 11-тым может приключится беда — он может быть пропущен, так как вызывает удвоение частоты. В итоге перемещение будет на 100 мм по X и от 10 до 11 мм по Y. И это при линейном перемещении, где собственно даже ограничений на допустимые ускорения и скорости просты. А если это выполняется зигзагом? Ну например идет заметание площади 100 на 110 мм в 10 проходов — тогда мы вообще промахнемся очень сильно…

Как раз для устранения этой ошибки и служит предлагаемый алгоритм, а вовсе не для суперфрезеров и т.д…