Rraptor рисует логотип Rraptor from
1i7 on
Vimeo.
Rraptor научился рисовать двумерные картинки в автоматическом режиме:
- приложение Андроид загружает рисунок из DXF-файла с Яндекс.Диска или с SD-карты,
- конвертирует в набор отрезков - команды
G-кода G0 и G01,
- отправляет по Wif- на плату ChipKIT (
WF32 или Uno32+
Wifi Shield);
- плата выполняет G-коды и рисует картинку;
- шаговые моторы управляются ШИМ, прямоугольный сигнал генерируется при помощи прерываний аппаратного таймера pic32.
Код приложения Android:
github.com/1i7/rraptor/tree/drawing-logo/RraptorPultкод прошивки платы:
github.com/1i7/rraptor/tree/drawing-logo/rraptor_mpide2На самом деле эта возможность теоретически была реализована еще 2 месяца назад
в предыдущей версии прошивки - приложение все также грузило DXF-файл и отправляло координаты линий одну за одной на станок. Но на практике станок не очень хорошо себя вел уже при рисовании первой диагональной линии (дергался и издавал страшные звуки), а при попытке нарисовать вторую линию вообще слетал с катушек почти в буквальном смысле; стабильно работало только
перемещение по одной координате за раз.
История здесь такая...
Шаговые моторы и интерфейс STEP/DIR
Для управления
шаговыми моторами обычно используются специальные аппаратные драйверы, которые поддерживают протокол Step/Dir.
В нашем случае
это модель STB57-1:
Но его можно заменить на любой другой, поддерживающий интерфейс Step/Dir, не меняя кода прошивки.
При работе с драйвером Step/Dir (в переводе Шаг/Направление) используется всего три провода (порта) в режиме вывода:
- En: enable - включить (0)/выключить (1) драйвер (да, именно 0 включает, а 1 выключает),
- Dir: direction - направление вращения (0 - туда, 1 - обратно),
- Step: шаг - сигнал вращения (импульс 1>0, т.е. фронт HIGH>LOW).
Для того, чтобы заставить шаговый мотор крутиться, нужно:
1) Подключить к плате 3 провода: En, Dir, Step
2) Установить порт En в 0 (LOW)
3) Установить порт Dir в 1 или 0 (в зависимости от направления)
4) Начать подавать периодический сигнал на порт Step с перепадами HIGH>LOW
Шаг будет совершаться каждый раз, когда импульс Step будет меняться с HIGH на LOW; ширина верхней части сигнала значения не имеет; скорость вращения определяется частотой импульсов (чем чаще шаги, тем быстрее вращение).
В самом простом варианте такой импульс можно генерировать в главном цикле программы просто вставляя нужные задержки в миллисекундах delay или микросекундах delayMicroseconds между подачами импульса на порт Step digitalWrite HIGH/LOW:
stepper_mpide_stepdir.ino void loop() {
// Линия 1
Serial.println("line1");
// задать направление
digitalWrite(motor1_pin_dir, HIGH);
// шагаем мотором - подаём периодический импульс
// (оптимальная частота импульса подбирается экспериментально
// в зависимости от драйвера и шагового мотора;
// 500 мкс - 2000мкс для драйвера stb57+шаговик ST57-H56 ок.).
// 1000 шагов в секунду:
for(int i = 0; i < 2000; i++) {
digitalWrite(motor1_pin_pulse, HIGH);
delayMicroseconds(500);
digitalWrite(motor1_pin_pulse, LOW);
delayMicroseconds(500);
}
delay(1000);
// Линия 2
Serial.println("line2");
// задать направление
digitalWrite(motor1_pin_dir, LOW);
// шаг раз в 2 секунды:
for(int i = 0; i < 10; i++) {
Serial.println("HIGH");
digitalWrite(motor1_pin_pulse, HIGH);
delayMicroseconds(1000000);
Serial.println("LOW");
digitalWrite(motor1_pin_pulse, LOW);
delayMicroseconds(1000000);
}
delay(1000);
}
Для моей комбинации драйвер+мотор минимальное расстояние между двумя импульсами - 1 миллисекунда (установлено экспериментально; можно пробовать давать более частый сигнал, но мотор при этом начинает работать нестабильно, пропускает шаги при малейшем сопротивлении, издает неприятные высокие звуки и т.п.), т.е. 1000 шагов в секунду - максимальная скорость вращения мотора.
Однопоточное ЧПУ
В общем может показаться, что этих базовых строк кода уже вполне достаточно, чтобы написать прошивку для контроллера ЧПУ (числового программного управления станком), которая будет рисовать моторами станка картинку любой сложности. Действительно, любое перемещение печатающего блока на станке - это просто последовательность шагов координатных моторов, а моторами мы шагать уже умеем. Каждая линия картинки разбивается на лесенки к примеру
по Брезенхаму, программа поочередно запускает моторы на короткие пробежки вложенными циклами по несколько шагов, множество ступенек выстраивается в большую картинку. Т.к. каждая из ступенек - это перемещение одного мотора по одной координате (строго вверх/вниз, вправо/влево или вперед/назад), для рисования лесенки достаточно одного потока - пока один мотор работает, все остальные ожидают.
В теории все выглядит ок, но на практике
реализация показала ряд серьезных проблем:
1) Станок дергается и издает страшные звуки в процессе рисования диагональной линии, т.е. частой лесенки (линия все-таки рисуется, но наблюдать за процессом немного боязливо). Мое предположение - при усложнении кода, генерирующего управляющий прямоугольный импульс для порта Step, (появление вложенных циклов, промежуточных проверок между импульсами и т.п.) на ширину импульса начинают влиять не только предсказуемые задержки delay и delayMicroseconds, но слабопредскаемое время выполнения вспомогательных участков кода, которые при тактовой частоте процессора pic32 80МГц могут быть не слишком значительными, но таких неравномерных и слабопредсказуемых погрешностей хватает для того, чтобы подпортить сигнал. Возможно с этим можно как-то бороться в рамках выбранного пути, но подход уже начинает выглядеть крайне ненадежным
2) Максимальная скорость перемещения по рабочей области сильно уменьшается из-за того, что моторы работают строго последовательно (в случае рисования лесенкой диагонали 45градусов один мотор шагает в первую итерацию цикла, вторую итерацию простаивает; при независимом вращении каждый мотор мог бы пройти свою координату быстрее ровно в 2 раза шагая на каждую итерацию цикла). Для и так не слишком быстрой винтовой передачи этот фактор сильно заметен.
3) Размещение генератора управляющего сигнала в главном цикле программы исключает любую другую полезную деятельность на контроллере в процессе рисования длинных линий до тех пор, пока линия не будет нарисована. Например код прошивки не сможет принять команду о завершении работы с управляющего приложения по программному каналу связи (через Wifi), чтобы прервать рисование линии до завершения цикла, генерирующего сигналы для моторов (преждевременный аварийный останов только аппаратно по кнопке на контроллере или выдергиванием провода питания).
На видео работы станка с предыдущей прошивкой видно, как в режиме ручного управления координата перемещается короткими пробежками именно по этой причине - контроллер проворачивает моторы на определенное количество шагов, потом прерывает вращение, чтобы обратиться к подсистеме Wifi посмотреть, не пришла ли команда на остановку, потом опят крутит моторами следующую серию шагов и т.п.
Rraptor: первые каракули from
1i7 on
Vimeo.
Крутим моторами, не мешая программе
К счастью нашелся способ решить все перечисленные проблемы и заодно сделать более логичной и понятной кодовую базу. Прямоугольный управляющий сигнал (ШИМ) нужно гененрировать не при помощи задержек между операциями в главном цикле, а при помощи процедуры обработки прерываний от аппаратного таймера (на самом деле это первый правильный способ осуществлять ШИМ на микроконтроллере, а задержки с delay - это баловство для первых тестов).
Обработчик прерывания - это процедура, которая вызывается системой в момент определенного события (в нашем случае это очередной такт таймера), основной цикл программы при этом замораживается до момента завершения процедуры обработчика прерывания, после этого продолжается - получается такая своебразная многозадачность.
Таймер и его прерывания
Немного в сторону,
простой пример работы прерывания от таймера, здесь:
1) Запускаем таймер с частотой 50Гц (т.е. 50 срабатываний в секунду, т.е. одно срабатывание каждые 20 миллисекунд).
2) Функция handle_interrupts - обработчик прерывания, будет вызываеться 50 раз в секунду, т.е. каждые 20 миллисекунд.
3) Делаем так, что обработчик прерывания, т.е. функция handle_interrupts, печатает в последователый порт отладочное сообщение "good by from timer" каждую секунду (для этого используем дополнительный счетчик count, чтобы сообщение печаталось только каждое 50е срабатывание).
timer_handler.cpp void init_handler() {
// Настроим и запустим таймер с периодом 20миллисекунд (50 срабатываний в секунду):
// prescaler=1:64, period=0x61A8:
// 80000000/64/0x61A8=50 (срабатывает 50 раз в секунду, т.е. каждые 20мс)
// Обработчик прерывания от таймера - функция handle_interrupts
// (с заданными настройками будет вызываться каждые 20мс).
initTimerISR(TIMER3, TIMER_PRESCALER_1_64, 0x61A8);
}
int count = 50;
int led_val = 0;
/**
* Процедура, вызываемая прерыванием по событию таймера с заданным периодом.
*/
void handle_interrupts(int timer) {
if(count == 0) {
Serial.println("good by from timer");
digitalWrite(13, led_val);
led_val = !led_val;
count = 50;
}
count--;
}
4) Код основной программы - как видим главный цикл loop почти пуст, он только печатает сообщение "Hello from loop!" и делает блокирующую задержку на 5 секунд, т.е. просто печатает сообщение каждые 5 секунд.
chipkit_timer_mpide.ino #include"timer_handler.h"
void setup() {
Serial.begin(9600);
init_handler();
pinMode(13, OUTPUT);
}
void loop() {
Serial.println("Hello from loop!");
delay(5000);
}
Результат работы программы на контроллере можем наблюдать на компьютере в окне просмотра отладочных сообщений, приходящих сплаты через USB-шнурок в mpide (меню Tools/Serial Monitor):
Как видим, всё сходится - главный цикл программы loop печатает свое "Hello from loop!" каждые 5 секунд, процедура обработки прерывания handle_interrupts печатает свое "good by from timer" каждую секунду, при этом блокирующий вызов delay(5000) в главном цикле loop ему совершенно не мешает (в это раз первый goodby даже порвал первый Hello пополам).
Вот какая вот многозадачность не выходя из mpide.
Замечание: Стандартный API Arduino (который в полной мере реализован на платах ChipKIT) не поддерживает работу с аппаратными таймерами и прерываниями, поэтому для включения таймера и подключения обработчика приходится использовать код, специфический для чипов pic32 (на обычной Arduino с AVR есть свои таймеры и прерывания, но их нужно включать своим AVR-специфическим способом), все системные вызовы для примера выше см в файле
timer_setup.cpp.
ШИМ на прерываниях
Теперь, имея в руках такой инструмент, сгенерировать прямоугольный сигнал необходимой ширины с высокой степенью точности не составит труда: нужно просто задать правильную частоту таймера и в обработчике прерываний в первый нужный момент делать для ножки digitalWrite(HIGH), во второй нужный момент - digitalWrite(LOW) и таким образом совершать шаг мотора.
Примерно так:
stepper_timer.cpp void handle_interrupts(int timer) {
bool finished = true;
for(int i = 0; i < stepper_count; i++) {
cstatuses[i].step_timer -= timer_freq_us;
if( (cstatuses[i].non_stop || cstatuses[i].step_counter > 0) &&
... ) {
...
// Шаг происходит по фронту сигнала HIGH>LOW, ширина ступени HIGH при этом не важна.
// Поэтому сформируем ступень HIGH за один цикл таймера до сброса в LOW
if(cstatuses[i].step_timer < timer_freq_us * 2 && cstatuses[i].step_timer >= timer_freq_us) {
// cstatuses[i].step_timer ~ timer_freq_us с учетом погрешности таймера (timer_freq_us) =>
// импульс1 - готовим шаг
digitalWrite(smotors[i]->pin_pulse, HIGH);
} else if(cstatuses[i].step_timer < timer_freq_us) {
// cstatuses[i].step_timer ~ 0 с учетом погрешности таймера (timer_freq_us) =>
// импульс2 (спустя timer_freq_us микросекунд после импульса1) - совершаем шаг
digitalWrite(smotors[i]->pin_pulse, LOW);
...
}
...
}
...
}
...
}
Мотор шагает, главный цикл тоже работает, и всё одновременно (см первое видео в начале поста с 12й минуты - столик по оси Y перемещается без задержек, при этом контроллер непрерывно мониторит интерфейс Wifi в ожидании команды на остановку и останавливается сразу после того, как кнопка "Y вправо" отпущена на смартфоне) - проблема разделения времени между процедурой управления мотором и основным кодом программы решена.
Теперь если разбить частоту таймера на достаточно маленькие кусочки (текущая версия прошивки тестировалась с вариантами периода таймера 5 микросекунд и 10 микросекунд; минимальный период импуса для шагового мотора напомню 1 миллисекунда, т.е. 1000 микросекунд) и назначить на каждый подключенный мотор пару внутренних счетчиков, можно получить возможность делать очень интересные вещи:
1) гибко регулировать скорость вращения мотора, задавая расстояние между двумя сигналами LOW (т.е. шагами) с привязкой к этой условной шкале таймера (одно деление - 5 или 10 микросекунд);
2) делать это для любого количества подключенных моторов фактически без ограничений - все они будут вращаться одновременно каждый со своей скоростью без необходимости ожидать друг друга.
Проблема разделения времени между двумя и более моторами (и как следствие падение максимальной скорости перемещения по рабочей области в однопоточной программе) также решена.
Рисование линий
Дополнительным приятным следствием из возможности запускать несколько моторов одновременно с разной скоростью является упрощение части кода, отвечающего за математику перемещений по координатам. Для того, чтобы нарисовать произвольную прямую линию, достаточно запустить все три мотора вращаться с разной скоростью так, чтобы печатающий блок из исходного положения в заданную точку по каждой из координат прибыл в одно и тоже время.
Задача сводится к расчету задержек таймера для каждой из координат и проверки граничных условий, Брезенхам не пригодился.
С этим подходом
реализованы 2 команды из
стандарта G-кодов для линейного перемещения координат: G0 (перемещение печатающего блока с максимальной скоростью) и G01 (перемещение печатающего блока с заданной скоростью).
подсветка синтаксиса.