ESP32: незалежне керування двома кроковими двигунами NEMA17

Коротко: нижче — два готових варіанти: (1) швидкий старт на бібліотеці AccelStepper (найпростіший запуск і підходить для більшості завдань) та (2) просунутий варіант на двох апаратних таймерах ESP32 для точної генерації STEP-імпульсів без блокувань. У статті також є схема підключення, формули та практичні поради щодо живлення і струму.


1) Що знадобиться

  • ESP32 DevKit (WROOM або WROVER).
  • Два крокові двигуни NEMA17 (зазвичай 1.5–2.0 А/фаза).
  • Два драйвери: A4988 / DRV8825 / TMC2208/TMC2209 (у режимі STEP/DIR).
  • Блок живлення для двигунів: 12–24 В (по струму з запасом).
  • Живлення 5 В для ESP32 (можна від USB, але «земля» спільна з живленням драйверів).
  • Електроліт 100–470 мкФ на кожен драйвер між VMOT і GND (якнайближче до плати драйвера).
  • Проводи, радіатори/обдув для драйверів при великих токах, кінцевики (за бажанням).

2) Підключення (типова схема)

Важливо: у драйверів VMOT і GND — тільки для двигунів (12–24 В). Логіка STEP/DIR/EN — до ESP32 (3.3 В). Обов’язково з’єднайте GND логіки та силової землі.

Сигнал Мотор A (драйвер №1) Мотор B (драйвер №2) Пін ESP32 (приклад)
STEPSTEPGPIO25
DIRDIRGPIO26
STEPSTEPGPIO14
DIRDIRGPIO12
ENABLEENEN (спільний)GPIO27
GND (логіка)GNDGNDСпільний з GND ESP32
VMOT+12…24 В+12…24 ВВід силового БЖ

Мікрокроки (MS1/MS2/MS3 у A4988 чи M0/M1/M2 у DRV8825/TMC) — налаштовуються перемичками. Наприклад, поставте 1/16 (часто за замовчуванням).


3) Обмеження струму драйвера (Vref)

  • A4988: Imax = Vref / (8 · Rsense) → Vref = Imax · 8 · Rsense.
    Приклад: Rsense=0.05 Ω, Imax=1.0 А → Vref ≈ 0.4 В.
  • DRV8825: Imax = Vref / (5 · Rsense).

Регулюйте підлаштувальним резистором при знеструмлених моторах, вимірюючи Vref відносно GND. Починайте з меншого струму і збільшуйте при пропусках кроків або нестачі моменту.


4) Кінематика і частоти

  • Кроковий кут: зазвичай 1.8° → 200 повних кроків/оберт.
  • Мікрокроки: при 1/16 → 200 × 16 = 3200 кроків/оберт.
  • Частота STEP для заданих RPM: f = (steps_per_rev × RPM) / 60.

5) Варіант А (швидкий старт): бібліотека AccelStepper

Плюси: мінімум коду, плавні розгони/гальмування, легко керувати двома моторами незалежно. Мінуси: при екстремально великих швидкостях і навантаженні може знадобитися «залізний» підхід.

/*
  ESP32 + AccelStepper: незалежне керування двома NEMA17
  - Два мотори (A і B) на драйверах STEP/DIR
  - Хоумінг по кінцевиках (активний НУЛЬ, INPUT_PULLUP)
  - Демонстраційна послідовність незалежних переміщень

  Важливо:
  * GPIO для STEP/DIR можна переназначати, але уникайте "системних" пінів (boot strapping).
  * ENABLE у більшості драйверів активний по рівню LOW (вмикає драйвер).
*/

#include <Arduino.h>
#include <AccelStepper.h>

// ======================== КОНФІГ ПІНІВ ========================
constexpr int STEP_A = 25;
constexpr int DIR_A  = 26;

constexpr int STEP_B = 14;
constexpr int DIR_B  = 12;

constexpr int EN_PIN = 27;   // спільний EN для обох драйверів (LOW = увімкнено)

constexpr int END_A  = 33;   // кінцевик мотора A (мінімум), замикає на GND
constexpr int END_B  = 32;   // кінцевик мотора B (мінімум), замикає на GND

// ===================== НАЛАШТУВАННЯ РУХУ =====================
constexpr float A_MAX_SPEED   = 3000.0f;   // крок/с
constexpr float A_ACCEL       = 2000.0f;   // крок/с^2
constexpr float B_MAX_SPEED   = 2500.0f;   // крок/с
constexpr float B_ACCEL       = 1800.0f;   // крок/с^2

// Хоумінг
constexpr long  HOMING_TRAVEL_STEPS = 1'000'000L; // "далеко до кінцевика"
constexpr float HOMING_SPEED        = 1200.0f;    // модуль швидкості при хоумінгу
constexpr long  BACKOFF_STEPS       = 800;        // від’їзд від кінцевика після спрацювання
constexpr float BACKOFF_SPEED       = 800.0f;     // швидкість від’їзду

// ======================== ОБ’ЄКТИ МОТОРІВ ====================
AccelStepper motorA(AccelStepper::DRIVER, STEP_A, DIR_A);
AccelStepper motorB(AccelStepper::DRIVER, STEP_B, DIR_B);

// ========================= ПРОТОТИПИ =========================
void homeOne(AccelStepper& m, int endPin, bool dirToMin);
void planNextDemoMove();

// ============================ SETUP ==========================
void setup() {
  // Увімкнення драйверів
  pinMode(EN_PIN, OUTPUT);
  digitalWrite(EN_PIN, LOW);  // LOW зазвичай вмикає драйвер

  // Кінцевики (активний НУЛЬ)
  pinMode(END_A, INPUT_PULLUP);
  pinMode(END_B, INPUT_PULLUP);

  // Параметри моторів
  motorA.setMaxSpeed(A_MAX_SPEED);
  motorA.setAcceleration(A_ACCEL);

  motorB.setMaxSpeed(B_MAX_SPEED);
  motorB.setAcceleration(B_ACCEL);

  // --------- Хоумінг у бік "мінімуму" (до кінцевиків) --------
  homeOne(motorA, END_A, /*dirToMin=*/true);
  homeOne(motorB, END_B, /*dirToMin=*/true);

  // Приклад: незалежні стартові цілі
  motorA.moveTo(8000);   // до +8000 кроків
  motorB.moveTo(-5000);  // до -5000 кроків
}

// ============================= LOOP ==========================
void loop() {
  // ВАЖЛИВО: викликати часто — кроки формуються всередині run()
  motorA.run();
  motorB.run();

  // Коли обидва дійшли — задаємо нові незалежні цілі
  if (motorA.distanceToGo() == 0 && motorB.distanceToGo() == 0) {
    planNextDemoMove();
  }
}

// ======================== РЕАЛІЗАЦІЇ =========================

// Хоумінг одного мотора у напрямку "мінімального" кінцевика
// endPin — вхід кінцевика (INPUT_PULLUP), активний LOW
// dirToMin = true — рухатися у бік зменшення координати
void homeOne(AccelStepper& m, int endPin, bool dirToMin) {
  // Швидкість і прискорення для хоумінгу
  m.setMaxSpeed(HOMING_SPEED);
  m.setAcceleration(HOMING_SPEED * 4.0f);

  // Їдемо "далеко" у бік кінцевика, доки він не спрацює
  const long farTarget = dirToMin ? -HOMING_TRAVEL_STEPS : HOMING_TRAVEL_STEPS;
  m.moveTo(farTarget);

  while (digitalRead(endPin) == HIGH) { // HIGH = не натиснутий (через PULLUP)
    m.run();
  }

  // Зупинилися на кінцевику — від’їжджаємо трохи назад, щоб його звільнити
  m.stop(); // акуратно загасить прискорення
  m.setMaxSpeed(BACKOFF_SPEED);
  m.move(dirToMin ? BACKOFF_STEPS : -BACKOFF_STEPS);
  while (m.distanceToGo() != 0) {
    m.run();
  }

  // Нульова координата після хоумінгу
  m.setCurrentPosition(0);

  // Робочі параметри швидкості/прискорення повертаються у setup()
}

// Проста демонстраційна "сценка" з 4 фаз
void planNextDemoMove() {
  static int phase = 0;
  switch (phase++ % 4) {
    case 0:
      motorA.moveTo(0);
      motorB.moveTo(10000);
      break;
    case 1:
      motorA.moveTo(12000);
      motorB.moveTo(2000);
      break;
    case 2:
      motorA.moveTo(-4000);
      motorB.moveTo(-8000);
      break;
    case 3:
      motorA.moveTo(6000);
      motorB.moveTo(0);
      break;
  }
}


6) Варіант B (просунутий): два апаратні таймери ESP32

Тут кожен мотор отримує свій апаратний таймер (Timer Group), що у ISR формує STEP-імпульси. Це дає точну частоту, мінімальні джиттери і повну незалежність. Нижче — базовий приклад із рівномірною швидкістю.

/*
  ESP32: два незалежних крокових (NEMA17) на апаратних таймерах
  - Для кожного мотора використовується власний hw_timer_t та ISR, що формує STEP-імпульси.
  - Частота задається у Гц, напрямок — рівнем DIR.
  - Приклад демонструє незалежні рухи з періодичною переустановкою цілей.

  Піни та рівні:
  * ENABLE більшості драйверів активний по рівню LOW (вмикає драйвер).
  * Кінцевики/датчики у цьому прикладі не використовуються (мінімальний каркас таймерів).

  Безпека:
  * Не змінюйте підключення моторів при ввімкненому живленні.
  * Ставте електроліт 100–470 мкФ на VMOT кожного драйвера.
*/

#include <Arduino.h>

// ===================== КОНФІГУРАЦІЯ ПІНІВ =====================
constexpr int STEP_A = 25;
constexpr int DIR_A  = 26;

constexpr int STEP_B = 14;
constexpr int DIR_B  = 12;

constexpr int EN_PIN = 27;  // спільний EN для драйверів (LOW = увімкнено)

// =============== НАЛАШТУВАННЯ ТАЙМЕРА/ШІМ ДЛЯ STEP ===========
/*
  Таймер ESP32 налаштовуємо на 1 МГц (тік = 1 мкс).
  Ми перемикаємо пін кожен напівперіод (halfPeriodUs), тобто період кроку T = 2 * halfPeriodUs.
  Частота кроку stepHz => halfPeriodUs = 500000 / stepHz.
*/
constexpr uint32_t TIMER_DIVIDER = 80; // 80 МГц / 80 = 1 МГц (1 мкс/тік)

// ======= СТРУКТУРА ДВИГУНА ТА ГЛОБАЛЬНІ ЕКЗЕМПЛЯРИ ===========
struct StepperHW {
  uint8_t stepPin, dirPin;
  volatile bool     stepState   = false;   // поточний стан лінії STEP
  volatile uint32_t stepsDone   = 0;       // кількість сформованих кроків (по фронту)
  volatile uint32_t stepsTarget = 0;       // цільове число кроків
  hw_timer_t*       timer       = nullptr; // апаратний таймер
};

StepperHW M1{STEP_A, DIR_A};
StepperHW M2{STEP_B, DIR_B};

// Для контексту в ISR
StepperHW* gM1 = &M1;
StepperHW* gM2 = &M2;

// Напівперіоди (мкс) для кожного мотора
volatile uint32_t halfPeriodUs_M1 = 500;  // 1 кГц за замовчуванням (T=1000 мкс)
volatile uint32_t halfPeriodUs_M2 = 500;

// ========================= ПРОТОТИПИ ==========================
void IRAM_ATTR step_isr_M1();
void IRAM_ATTR step_isr_M2();
void startMove(StepperHW& M, bool dir, int32_t steps, uint32_t stepHz);
void enableDrivers(bool on);
bool  isIdle(const StepperHW& M);
void  initTimers();
void  planNextDemoMoves();

// ========================== ISR-и =============================
// Формування меандру на STEP і підрахунок кроків по фронту
void IRAM_ATTR step_isr_M1() {
  gM1->stepState = !gM1->stepState;
  digitalWrite(gM1->stepPin, gM1->stepState);
  if (gM1->stepState) { // рахуємо тільки фронти
    if (++gM1->stepsDone >= gM1->stepsTarget) {
      timerAlarmDisable(gM1->timer);
      gM1->stepState = false;
      digitalWrite(gM1->stepPin, LOW);
    }
  }
}

void IRAM_ATTR step_isr_M2() {
  gM2->stepState = !gM2->stepState;
  digitalWrite(gM2->stepPin, gM2->stepState);
  if (gM2->stepState) {
    if (++gM2->stepsDone >= gM2->stepsTarget) {
      timerAlarmDisable(gM2->timer);
      gM2->stepState = false;
      digitalWrite(gM2->stepPin, LOW);
    }
  }
}

// ======================= ХЕЛПЕРИ РУХУ ========================
// Запуск переміщення: напрямок, кількість кроків і частота кроків (Гц)
void startMove(StepperHW& M, bool dir, int32_t steps, uint32_t stepHz) {
  if (steps <= 0 || stepHz == 0) return;

  // Напрямок
  digitalWrite(M.dirPin, dir ? HIGH : LOW);

  // Цілі/лічильники
  M.stepsTarget = static_cast<uint32_t>(steps);
  M.stepsDone   = 0;
  M.stepState   = false;
  digitalWrite(M.stepPin, LOW);

  // Перерахунок напівперіоду
  const uint32_t halfPeriodUs = 500000UL / stepHz;

  // Прив'язка до конкретного таймера/мотора
  if (&M == &M1) {
    halfPeriodUs_M1 = halfPeriodUs;
    timerAlarmWrite(M.timer, halfPeriodUs_M1, true); // авто-перезапуск
  } else {
    halfPeriodUs_M2 = halfPeriodUs;
    timerAlarmWrite(M.timer, halfPeriodUs_M2, true);
  }

  // Пуск
  timerAlarmEnable(M.timer);
}

// Вмикання/вимикання драйверів (спільний EN)
void enableDrivers(bool on) {
  // Для драйверів типу A4988/DRV8825: LOW = увімкнено, HIGH = вимкнено
  digitalWrite(EN_PIN, on ? LOW : HIGH);
}

// Перевірка: мотор вільний (рух завершено)
bool isIdle(const StepperHW& M) {
  return (M.stepsDone >= M.stepsTarget);
}

// Ініціалізація таймерів: 1 МГц, ISR прикріплені
void initTimers() {
  // Мотор 1
  M1.timer = timerBegin(/*num=*/0, /*divider=*/TIMER_DIVIDER, /*countUp=*/true);
  timerAttachInterrupt(M1.timer, &step_isr_M1, /*edge=*/true);

  // Мотор 2
  M2.timer = timerBegin(/*num=*/1, /*divider=*/TIMER_DIVIDER, /*countUp=*/true);
  timerAttachInterrupt(M2.timer, &step_isr_M2, /*edge=*/true);
}

// Демонстраційний план: якщо мотори вільні — дати нові незалежні команди
void planNextDemoMoves() {
  static uint32_t t0 = millis();
  if (millis() - t0 < 2000) return; // оновлюємо цілі раз на ~2 секунди
  t0 = millis();

  if (isIdle(M1)) {
    // кроки 6000..9999, частота 1500..4499 Гц, випадковий напрямок
    startMove(M1, random(0, 2), 6000 + random(0, 4000), 1500 + random(0, 3000));
  }
  if (isIdle(M2)) {
    // кроки 4000..7999, частота 1200..3699 Гц, випадковий напрямок
    startMove(M2, random(0, 2), 4000 + random(0, 4000), 1200 + random(0, 2500));
  }
}

// ============================ SETUP ===========================
void setup() {
  // Піни
  pinMode(EN_PIN, OUTPUT);
  pinMode(M1.stepPin, OUTPUT);
  pinMode(M1.dirPin,  OUTPUT);
  pinMode(M2.stepPin, OUTPUT);
  pinMode(M2.dirPin,  OUTPUT);

  // Вмикаємо драйвери
  enableDrivers(true);

  // Таймери на 1 МГц і ISR
  initTimers();

  // Стартові незалежні рухи
  startMove(M1, /*dir=*/true,  8000, 4000); // 4000 Гц
  startMove(M2, /*dir=*/false, 5000, 2500); // 2500 Гц
}

// ============================= LOOP ==========================
void loop() {
  // Періодично плануємо наступні незалежні профілі руху
  planNextDemoMoves();

  // Для плавних розгонів/гальмувань змінюйте timerAlarmWrite() за розкладом (трапеція/С-крива):
  //  - зменшуйте/збільшуйте halfPeriodUs_* по таймеру/тікерам;
  //  - не забувайте, що ISR перемикає STEP кожні halfPeriodUs мкс.
}


7) Поради з надійності

  • Не підключайте/не змінюйте двигуни при увімкненому живленні — можна спалити драйвер.
  • На кожен драйвер ставте електроліт 100–470 мкФ (VMOT↔GND) + кераміку 100 нФ.
  • Проводи моторів скручуйте попарно (A+/A−, B+/B−).
  • EN тримайте активним тільки коли потрібне утримання моменту — драйвери менше гріються.
  • Для TMC використовуйте режими StealthChop/SpreadCycle за завданням (тихо/момент).

8) Часті запитання

Чому мотори «дзвенять» на малих обертах? Це нормальна особливість крокових двигунів; збільшіть мікрокроки, додайте плавний розгін/гальмування, спробуйте TMC-драйвери.

Чи можна живити ESP32 від того ж БЖ, що і мотори? Так, через DC-DC, але «земля» має бути спільною. Лінію 5 В ESP32 не з’єднуйте напряму з VMOT!

Як порахувати «кроки на міліметр»? Для гвинта з кроком 8 мм і 1/16 мікрокроками: 200×16/8 = 400 кроків/мм.


9) Висновок

Для більшості проєктів підійде AccelStepper: просто, плавно, незалежно для кожного мотора. Якщо потрібна жорстка частотна точність і велике навантаження — використовуйте апаратні таймери (або RMT/LEDC) і керуйте профілями швидкості з FreeRTOS-тасків. В обох випадках ESP32 легко забезпечує незалежне керування двома NEMA17.

<< Проекти << Усі товари >> Статті, уроки >>

Написати відгук

Примітка: HTML размітка не підтримується! Використовуйте звичайтий текст.
    Погано           Добре
BMS плата захисту 3х літій-іонних акумуляторів 10А

BMS плата захисту 3х літій-іонних акумуляторів 10А

Модуль захисту батареї трьох літій-іонних акумуляторівНапруга відсічення при заряді 4,25...4,35ВНапр..

114.46грн.

Джойстик двохосьовий з кнопкою

Джойстик двохосьовий з кнопкою

Двохосьовий джойстик з кнопкою — це зручний модуль керування для проєктів на Arduino, ESP32 та інших..

44.72грн.

ESP32 LED PWM: частоти та роздільна здатність ШІМ (LEDC)

ESP32 LED PWM: частоти та роздільна здатність ШІМ (LEDC)

ESP32 LED PWM Controller: керування яскравістю світлодіодів ESP32 має вбудований модуль LED PWM C..

Arduino SIM800 CBC рівень заряду

Arduino SIM800 CBC рівень заряду

Привіт друзі! SIM800L має аналого-цифровий перетворювач, який призначений для вимірювання напруги жи..

Цифровий датчик температури DS18B20

Цифровий датчик температури DS18B20

Напруга живлення 3 ... 5,5 ВРобоча температура -55 ... +125 °CТочність ±0,5 °C забезп..

25.83грн.