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;
  }
}

Подсказки: Если нужна «только скорость» без позиций — используйте setSpeed() и runSpeed() для каждого мотора независимо. Можно делать «джог» (ручной прогон) по кнопкам.


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; // (1e6/Hz)/2

  // Привязка к конкретному таймеру/мотору
  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 микросекунд.
}

Идеи для расширения: вынесите профили скорости в отдельные FreeRTOS‑таски, которые по таймеру уменьшают/увеличивают halfPeriodUs (т.е. частоту STEP) до целевой — получите гладкие разгоны без просадок от delay().


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 разметка не поддерживается! Используйте обычный текст.
    Плохо           Хорошо
Клеммная колодка на 12 контактов 2,5 мм 3А

Клеммная колодка на 12 контактов 2,5 мм 3А

Клеммник соединительный на 12 контактовХорошо крепится к стенкам пластмассовых щитов при помощи клее..

21.00грн.

Умный дом #10: Датчик CO2

Умный дом #10: Датчик CO2

Умный дом #10: Датчик CO₂ Контроль качества воздуха в помещении — один из ключевых элем..

Термопара K-типа -100...800°C 5х100мм

Термопара K-типа -100...800°C 5х100мм

Термопара типа К ТХА хромель-алюмель используется для измерения экстремально высоких температур до&n..

204.73грн.

Двойная шестерня для зубчатого ремня на 20 зубьев под ось 5 мм

Двойная шестерня для зубчатого ремня на 20 зубьев под ось 5 мм

Сдвоенная шестерня для передачи крутящего момента с одного вала на другой.Применяется для разделения..

97.08грн.

Мотор безщеточный A2212 1000KV

Мотор безщеточный A2212 1000KV

Мотор трехфазный безколлекторный для мультикоптеров 1000KVKV: 1000 оборотов на вольтМаксимальна..

360.40грн.