Есть много библиотек, которые позволяют управлять шаговыми двигателями c помощью Arduino. В данной статье речь будет идти про биполярный шаговый двигатель с драйвером с интерфейсом step/dir (импульс/направление). Что такое биполярный шаговый двигатель намного лучше расскажут другие статьи на Хабре, для этого достаточно вбить в поисковик (речь про поисковик Хабра) «шаговый». А в этой статье будет рассказано про практическую реализацию сигналов (импульс/направление) на отладочной плате из серии Arduino Nano.

Статья рассчитана на новичков, которые хотят копнуть немного глубже в управление шаговиком, может быть, сделать что-то для себя.

В опытах использован биполярный шаговый двигатель типоразмера NEMA17 с двумя обмотками (фазами) с четырьмя выводами с драйвером управления с интерфейсом step/dir (импульс/направление). Примером такого драйвера управления вполне себе может служить A4988. Драйвер подключен к соответствующему блоку питания. Отладочная плата из серии Arduino Nano (клон) с микроконтроллером Atmega328P. Программирование платы выполнялось в Arduino IDE 1.8.19.

Рис. 1. Схема подключения
Рис. 1. Схема подключения

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

Hidden text

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

Рис. 2. Фото микропереключателей драйвера из экспериментов для статьи в режиме полного шага
Рис. 2. Фото микропереключателей драйвера из экспериментов для статьи в режиме полного шага
Рис. 3. Скриншот части таблицы режимов микрошага для драйвера из экспериментов для статьи
Рис. 3. Скриншот части таблицы режимов микрошага для драйвера из экспериментов для статьи

Для А4988 есть своя таблица.

Рис. 4. Таблица режимов микрошага для любительского драйвера А4988
Рис. 4. Таблица режимов микрошага для любительского драйвера А4988

Есть и многие другие способы конфигурации конкретных драйверов. Всегда надо обращаться к даташиту девайса для получения информации о нем.

В Arduino IDE необходимо открыть стандартный пример Blink по пути Файл->Примеры->01.Basics->Blink и загрузить его в микроконтроллер.

На видео видно как мигает светодиод в соответствие со стандартным примером Blink и вал двигателя делает небольшой поворот. Коротко о том что здесь происходит. Драйверу на вход поступает положительный фронт (переход сигнала на 13 ножке отладочной платы из низкого состояния в высокое), следовательно, вал двигателя делает 1/200 оборота. Всё работает, как и положено.

Для второго эксперимента необходимо изменить частоту мигания светодиода, увеличив ее в два раза. Для этого заменим значение 1000 в параметре функции delay() на 500 (в обеих строках) и посмотрим что будет.

Hidden text
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(500);                       // wait half a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(500);                       // wait half a second
}

Эксперимент даёт понять, что увеличивая частоту сигнала увеличивается скорость вращения вала двигателя. Убедимся в этом и увеличим частоту в 5 раз.

Hidden text
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(100);                       // wait 100 milliseconds
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(100);                       // wait 100 milliseconds
}

Увеличим скорость.

Hidden text
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(10);                       // wait 100 milliseconds
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(10);                       // wait 100 milliseconds
}

Скорость вращения увеличилась, но двигатель работает громко, с вибрацией. На замедленном видео видно как вал двигается неравномерно. Это можно исправить введя такое понятие как микрошаг. Для этого переконфигурируем драйвер управления на работу в режиме 1600 импульсов на оборот.

Hidden text
Рис. 5. Фото микропереключателей драйвера из эксперимента для статьи в режиме микрошага 1/8
Рис. 5. Фото микропереключателей драйвера из эксперимента для статьи в режиме микрошага 1/8
Рис. 6. Скриншот части таблицы режимов микрошага для драйвера из эксперимента для статьи
Рис. 6. Скриншот части таблицы режимов микрошага для драйвера из эксперимента для статьи

Движение стало более равномерным и без вибраций, но медленным. Все дело в том, что частота управляющего сигнала осталась прежней, но на один оборот вала двигателя теперь требуется 1600 импульсов. Остается увеличивать частоту.

Hidden text
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1);                       // wait 100 milliseconds
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(1);                       // wait 100 milliseconds
}

Такое непрерывное вращение шагового двигателя на практике почти не используется. Необходимо вращать вал двигателя на заданное количество шагов. Но перед этим надо избавиться от функции delay(). Ее использование здесь будет только мешать. Чтобы от нее избавиться необходимо ввести понятие таймера. Теперь вся программа будет выглядеть вот так:

unsigned long timer_step = 0; // переменная для таймера управляющего сигнала
const uint8_t pin_step = 13; // переменная для хранения ножки отладочной платы
bool state_pin_step = LOW; // переменная состояния ножки
uint32_t half_period = 1000; // переменная для значения полупериода управляющего сигнала

void setup() {
  pinMode(pin_step, OUTPUT); // инициализация ножки как цифрового выхода
}

void loop() {
  //--данный код будет выполняться один раз в half_period (1 миллисекунда в данном случае)--
  if(micros()-timer_step>=half_period){ 
    timer_step = micros();

    state_pin_step = !state_pin_step; // значение изменяется на противоположное
    digitalWrite(pin_step, state_pin_step); // установка соответствующего значения на цифровом выходе
  }
  //-----------------------------------------------------------------------------------
}

Визуально движение вала двигателя не поменялось. Все потому что управляющий сигнал остался прежним – меандр с частотой 500 Гц. Просто он теперь реализован по-другому. Это небольшое усложнение кода позволяет модернизировать управление шаговым двигателем. Чтобы глубже понять зачем это нужно лучше вбить в любимый поисковик «как избавиться от delay arduino».

Подводя промежуточные итоги:

  1. Для вращения биполярного шагового двигателя на вход подключенного к нему драйвера необходимо подавать импульсы.

  2. Скорость вращения зависит от частоты сигнала подаваемого на вход драйвера управления.

  3. Количество импульсов влияет на положение вала двигателя.

Остановимся на 3 пункте подробнее. Драйвер сейчас сконфигурирован в режим микрошага 1600 импульсов на оборот. Перепишем скетч таким образом, чтобы делать ровно один оборот. Для этого нужно считать импульсы.

unsigned long timer_step = 0; // переменная для таймера управляющего сигнала
uint8_t pin_step = 13; // переменная для хранения ножки отладочной платы
bool state_pin_step = LOW; // переменная состояния ножки
uint32_t half_period = 1000; // переменная для значения полупериода управляющего сигнала
uint32_t number_pulses = 1600; // переменная для хранения количества импульсов на вход драйверу

void setup() {
  pinMode(pin_step, OUTPUT); // инициализация ножки как цифрового выхода

  number_pulses*=2; // импульсы необходимо удвоить, потому что переключение ножки
  //происходит раз в полупериод
}
void loop() {
  //--данный код будет выполнен number_pulses раз (3200 раз в данном случае)--
  while(number_pulses){
    //--данный код будет выполняться один раз в half_period (1 миллисекунда в данном случае)--
    if(micros()-timer_step>=half_period){
      timer_step = micros(); // приравляли переменную таймера для следующего отрезка времени
      number_pulses--; // уменьшили задание на единицу
      state_pin_step = !state_pin_step; // значение изменяется на противоположное
      digitalWrite(pin_step, state_pin_step); // установка соответствующего значения на цифровом выходе
    }
    //----------------------------------------------------------------------------------------
  }
  //---------------------------------------------------------------------------
}

Записывая в переменную number_pulses значение, теперь можно повернуть вал в определенное положение, например четверть оборота. Для этого запишем 400 (1600/4=400); Количество импульсов необходимо умножить на 2, потому что переключение состояния 13 ножки отладочной платы происходит один раз в полупериод.

Кстати это по-прежнему код для мигания светодиодом. В этом можно убедиться, если назначить переменной half_period значение 1000000. Тогда светодиод будет включаться на 1 секунду и на 1 секунду выключаться как в оригинальном Blink, а на четверть оборота уйдет около 13 минут (400*2=800 сек; 800 сек примерно равно 13 минутам).

Вернемся к полупериоду в 1000 микросекунд. Известно, что для увеличения скорости необходимо увеличить частоту. Если поднять частоту в 20 раз до 10 кГц, то вместо быстрого движения вал будет издавать писк, вибрации, но перемещаться на необходимое количество шагов не будет. Здесь необходимо понимать что всё зависит от многих факторов, включая модель самого двигателя (они тоже бывают мощными и более слабыми), напряжения, приложенного к драйверу управления и нагрузки, которая приложена к валу. В Вашем эксперименте двигатель может вращаться и на этой частоте, тогда просто попробуйте ее еще увеличить. А может быть что и на 500 Гц эксперимент будет уже провален. Эти случаи тоже хорошо описаны в других статьях, в том числе и на Хабре. А для преодоления этих явлений необходимо ввести понятие разгона и торможения. Это позволит сдвигать сравнительно большие инерционные нагрузки даже на сравнительно небольших рабочих напряжениях.

Для разгона необходимо плавно увеличивать частоту управляющего сигнала, начиная со старта, а для торможения, соответственно, ее необходимо уменьшать с определенного момента. Для реализации такого управления можно, например, отвести для разгона определенное число импульсов. Разбить это количество на равные части и каждую часть увеличивать частоту (уменьшая период).

В приведенном ниже скетче на разгон отведено 1600 импульсов (один оборот вала). Этот оборот разбит на 200 равных частей. Каждую часть (1/200 оборота) полупериод уменьшается на одну микросекунду.

#define START_HALF_PERIOD 320 // макрос для удобства
#define NUMBER_PULSES 4800 // макрос для удобства

unsigned long timer_step = 0; // переменная для таймера управляющего сигнала
uint8_t pin_step = 13; // переменная для хранения ножки отладочной платы
bool state_pin_step = LOW; // переменная состояния ножки
uint32_t half_period = START_HALF_PERIOD; // переменная для значения полупериода управляющего сигнала
uint32_t number_pulses = NUMBER_PULSES; // переменная для хранения количества импульсов на вход драйверу
uint32_t numPulAcc = 1600; // переменная для хранения количества импульсов для разгона
unsigned char motor_state=0; // переменная для хранения состояния (разгон, рабочий ход, торможение) двигателя

//--функция для формирования импульсов--
void impulse(uint32_t &count){ // на вход по ссылке, чтобы выполнить движение и ждать следующее задание
  count*=2; // импульсы необходимо удвоить, потому что переключение ножки происходит раз в полупериод
  //--данный код будет выполнен number_pulses раз (9600 раз в данном случае)--
  while(count){
    //--данный код будет выполняться один раз в half_period--
    if(micros()-timer_step>=half_period){
      //-------------------------------------
      timer_step = micros(); // приравляли переменную таймера для следующего отрезка времени
      count--; // уменьшили задание на единицу
      state_pin_step = !state_pin_step; // значение изменяется на противоположное
      digitalWrite(pin_step, state_pin_step); // установка соответствующего значения на цифровом выходе
      //--данный код будет выполняться 200 раз за один оборот--
      if(count%16==0){ //оборот разбит на 200 частей (импульсы * 2)
        if(motor_state==0){ // если в режиме разгона
          half_period-=1; // уменьшить полупериод на 1 мкс (тем самым увеличить частоту)
        }
      //---------------------------------------------------------
      }
    }
    //---------------------------------------------
  }
  //---------------------------------------------------------------------------
}
//--функция для расчета количества импульсов--
void calculations(uint32_t &number_pulses){ // на вход по ссылке, чтобы выполнить движение и ждать следующее задание
  uint32_t numPulCru = number_pulses-numPulAcc; // присвоение переменной значения для рабочего хода
  number_pulses=0; // обнуление задания по его выполнению
  if(motor_state==0){ // режим разгона
    impulse(numPulAcc); // формирование сигнала на разгон
    motor_state=1; // переход в режим рабочего хода
  }
  if(motor_state==1){ // режим рабочего хода
    impulse(numPulCru); // формирование сигнала рабочего хода
    motor_state=0; // переход в режим разгона
  }
}
void setup() { 
  pinMode(pin_step, OUTPUT); // инициализация ножки как цифрового выхода
}
void loop() {
  calculations(number_pulses); // расчет и выполнение задания на движение
}

Код мигания светодиодом и одновременно формирования меандра для управления двигателем теперь вынесен в отдельную функцию impulse(uint32_t &count). Для удобства введены два макроса. В переменной numPulAcc записано количество импульсов для разгона. При изменении этой переменной нужно помнить про, то что оборот разбивается на части. Это реализовано в условии if(count%16==0). При выполнении задания на поворот (формирования импульсов) условие срабатывает 200 раз когда остаток от деления равен нулю. Например, 1600 импульсов на разгон / 200 частей = 8. Восьмерку необходимо умножить на 2 и ожидать срабатывания по условию if(count%16==0), потому что переключение выхода отладочной платы происходит раз в полупериод. На разгон необязательно отводить целый оборот, всё зависит от внешних условий: модель двигателя, рабочее напряжение, инерция нагрузки. Например, рассчитаем разгон за пол оборота, тогда 800/200=4, 4*2=8 if(count%8==0). Оборот можно делить и на другие количества частей, а еще полупериод можно уменьшать не на единицу. Всё под конкретную задачу. Также введена еще одна функция для расчетов количества импульсов на разгон и основное движение. Там от заданного количества импульсов вычитаются импульсы на разгон. Переменная обнуляется (это пригодится, когда код будет использован в дальнейшем в прикладных задачах. Без этого обнуления вал будет вращаться постоянно, потому что код выполняется в бесконечном цикле). Далее введено два состояния двигателя (разгон и основное движение) и переходы между ними.

Теперь можно разогнать вал, подавая на вход частоту 10 кГц, до модернизации кода двигатель только пищал и вибрировал, но не двигался. Для этого впишем в макрос START_HALF_PERIOD значение 250 (250-200=50 мкс; 1/(0,00005*2)=10000. Начиная с полупериода 250 будет вычтено 200. Полупериод 50 мкс в соответствие обратно пропорциональной зависимости периода от частоты равняется 10 кГц).

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

#define START_HALF_PERIOD 250 // макрос для удобства
#define NUMBER_PULSES 32000 // макрос для удобства
unsigned long timer_step = 0; // переменная для таймера управляющего сигнала
uint8_t pin_step = 13; // переменная для хранения ножки отладочной платы
bool state_pin_step = LOW; // переменная состояния ножки
uint32_t half_period = START_HALF_PERIOD; // переменная для значения полупериода управляющего сигнала
uint32_t number_pulses = NUMBER_PULSES; // переменная для хранения количества импульсов на вход драйверу
uint32_t numPulAcc = 1600; // переменная для хранения количества импульсов для разгона
uint32_t numPulDec = 1600; // переменная для хранения количества импульсов для торможения
unsigned char motor_state=0; // переменная для хранения состояния (разгон, рабочий ход, торможение) двигателя
//--функция для формирования импульсов--
void impulse(uint32_t &count){ // на вход по ссылке, чтобы выполнить движение и ждать следующее задание
  count*=2; // импульсы необходимо удвоить, потому что переключение ножки происходит раз в полупериод
  //--данный код будет выполнен number_pulses раз (64000 раз в данном случае)--
  while(count){ 
    //--данный код будет выполняться один раз в half_period--
    if(micros()-timer_step>=half_period){
      //-------------------------------------------
      timer_step = micros(); // приравляли переменную таймера для следующего отрезка времени
      count--; // уменьшили задание на единицу
      state_pin_step = !state_pin_step; // значение изменяется на противоположное
      digitalWrite(pin_step, state_pin_step); // установка соответствующего значения на цифровом выходе
      //--данный код будет выполняться 200 раз за один оборот--
      if(count%16==0){ //оборот разбит на 200 частей (импульсы * 2)
        if(motor_state==0){ // если в режиме разгона
          half_period-=1; // уменьшить полупериод на 1 мкс (тем самым увеличить частоту)
        }
        if(motor_state==2){ // если в режиме торможения
          half_period+=1; // увеличить полупериод на 1 мкс (тем самым уменьшить частоту)
        }
      }
      //-------------------------------------------------------
    }
    //-------------------------------------------------------
  }
}
//--функция для расчета количества импульсов--
void calculations(uint32_t &number_pulses){ // на вход по ссылке, чтобы выполнить движение и ждать следующее задание
  uint32_t numPulCru = number_pulses-numPulAcc-numPulDec; // присвоение переменной значения для рабочего хода
  number_pulses=0; // обнуление задания по его выполнению
  if(motor_state==0){ // режим разгона
    impulse(numPulAcc); // формирование сигнала на разгон
    motor_state=1; // переход в режим рабочего хода
  }
  if(motor_state==1){ // режим рабочего хода
    impulse(numPulCru); // формирование сигнала рабочего хода
    motor_state=2; // переход в режим торможения
  }
  if(motor_state==2){ // режим торможения
    impulse(numPulDec); // формарование сигнала на торможение
    motor_state=0; // переход в режим разгона
  }
}
void setup() {
  pinMode(pin_step, OUTPUT); // инициализация ножки как цифрового выхода
}
void loop() {
  calculations(number_pulses); // расчет и выполнение задания на движение
}

Теперь вал постоянно и сравнительно мягко останавливается там где положено. Для этого в функцию impulse(uint32_t &count) в условие if(count%16==0) добавлено еще одно условие для состояния торможения, при этом полупериод необходимо увеличивать. Добавлена переменная numPulDec для задания количества импульсов на торможение. Изменена функция calculations(uint32_t &number_pulses). Во-первых, на основное движение теперь приходится еще меньше импульсов. Во-вторых, добавлен переход из состояния основного движения в состояние торможения. Код можно немного переписать, чтобы задавать различные режимы разгон и торможение, например:

//--данный код будет выполняться 200 раз за один оборот отдельно для разгона--
if(count%16==0){ // оборот разбит на 200 частей (импульсы * 2) отдельно для разгона
  if(motor_state==0){ // если в режиме разгона
    half_period-=1; // уменьши полупериод
  }
  /* //--здесь раньше был режим торможения, но теперь он описан отдельно--
  if(motor_state==2){ // если в режиме торможения
    half_period+=1; // увеличь полупериод
  }
  */ //-------------------------------------------------------------------
}
if(count%16==0){ //оборот разбит на 200 частей (импульсы * 2)
  /* // здесь раньше был режим разгона, но теперь он описан отдельно--
  if(motor_state==0){ // если в режиме разгона
    half_period-=1; // уменьши полупериод
  }
  */ //---------------------------------------------------------------
  if(motor_state==2){ // если в режиме торможения
    half_period+=1; // увеличь полупериод
  }
}
//-------------------------------------------

Осталось продумать еще один случай. На поворот может приходить команда, где импульсов на всё может быть меньше чем отведено на разгон и торможение. Предусмотреть это можно, например, в функции calculations(uint32_t &number_pulses):

//--функция для расчета количества импульсов--
void calculations(uint32_t &number_pulses){ // на вход по ссылке, чтобы выполнить движение и ждать следующее задание
  if(number_pulses>numPulAcc+numPulDec){ // если задание на поворот больше чем требуется импульсов на разгон и торможение
    uint32_t numPulCru = number_pulses-numPulAcc-numPulDec; // присвоение переменной значения для рабочего хода
    number_pulses=0; // обнуление задания по его выполнению
    if(motor_state==0){ // режим разгона
      impulse(numPulAcc); // формирование сигнала на разгон
      motor_state=1; // переход в режим рабочего хода
    }
    if(motor_state==1){ // режим рабочего хода
      impulse(numPulCru); // формирование сигнала рабочего хода
      motor_state=2; // переход в режим торможения
    }
    if(motor_state==2){ // режим торможения
      impulse(numPulDec); // формарование сигнала на торможение
      motor_state=0; // переход в режим разгона
    }
  }else{ // если задание на поворот меньше чем требуется импульсов на разгон и торможение
    numPulAcc = number_pulses/2; // половина задания на разгон
    numPulDec = number_pulses/2; // половина задания на торможение
    number_pulses=0; // обнуление задания по его выполнению
    if(motor_state==0){ // режим разгона
      impulse(numPulAcc); // формирование сигнала на разгон
      motor_state=2; // переход в режим торможения
    }
    if(motor_state==2){ // режим торможения
      impulse(numPulDec); // формарование сигнала на торможение
      motor_state=0; // переход в режим разгона
    }
  }
}
//------------------------------------------------

Подводя итоги. Для управления связки биполярный шаговый двигатель - драйвер управления с интерфейсом step/dir для простых задач вполне себе можно использовать микроконтроллеры AVR на отладочных платах Arduino. Необходимо формировать управляющий сигнал определенной частоты. Для этого необходимо пользоваться таймерами. Чтобы развивать большие скорости и двигать инерционные нагрузки необходимо реализовать разгон-торможение. Команда на движение может поступать меньше чем требуется для разгона-торможения, поэтому необходимо от этого защититься.

Теперь данный код можно использовать для простого управления систем перемещения, которые основаны на биполярных шаговых двигателях. Код очень простой и достаточно гибкий для добавления второго, третьего и т. д. двигателей. Конечно тогда было бы удобнее переписать код в стиле ООП, но это уже на любителя и дело техники. Здесь не было упомянуто о направлении вращения, для этого необходимо лишь выделить еще один вход на отладочной плате под эту задачу и переключать его при необходимости двигаться в одну или другую сторону. Так же необходимо будет реализовать интерфейс взаимодействия, например, UART. Пишем команду, отправляем на микроконтроллер и выполняется движение или настройка параметров. А может быть даже использовать HMI дисплей. Очень удобно. Советую.

Стоит упомянуть, что драйверы управления могут значительно упростить реализацию формирования сигнала или даже сэкономить выходы микроконтроллера. На сегодняшний день даже в самых простых драйверах управления есть еще сигнал Enable, который активирует двигатель. В драйверах подороже реализованы разгон-торможение. А в драйверах еще подороже (например Hiwin E1 или D2, Mitsubishi MR-J4) есть даже внутренние режимы управления, которые позволяют крутить двигатель без воздействия из вне. Но как правило таких режимов недостаточно и как минимум можно пользоваться интерфейсом step/dir.

Примером использования такого исходного кода может служить система перемещения для радиолюбительской антенны из моей статьи https://habr.com/ru/articles/732300/:

Или управление вот таким модулем перемещения:


P.s. Я на работе перемещаю на таком модуле лазерный маркировочный узел. Лазерная маркировка происходит в точке фокуса главного объектива, поэтому при маркировке различных деталей, которые имеют разную толщину необходимо двигать вверх-вниз маркировочный узел. Похожий код на похожем микроконтроллере просто, дешево и надежно справляется с такой задачей без использования сторонних часто тяжеловесных, избыточных и, надо признать, иногда непонятных библиотек.

Еще такой код используется мной для пусконаладки координатного стола 3*1.5 метра станка для лазерной резки весом 3 тонны. Портал весом 200 кг перемещают двигатели Hiwin E1 мощностью 1,5 кВт.

Таким общем этот код можно использовать для управления девайсами с интерфейсом step/dir с уровнем TTL 5 вольт с входными частотами в диапазоне до 20 кГц. А при использовании различных обвязок можно и с другими уровнями. Если надо быстрее, то можно и код ускорить. Но это всё уже совсем другие истории.

Благодарю за внимание.

С. Н.

Комментарии (6)