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 пин (пример) |
|---|---|---|---|
| STEP | STEP | — | GPIO25 |
| DIR | DIR | — | GPIO26 |
| STEP | — | STEP | GPIO14 |
| DIR | — | DIR | GPIO12 |
| ENABLE | EN | EN (общий) | GPIO27 |
| GND (логика) | GND | GND | Общий с 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.
Клеммная колодка на 12 контактов 2,5 мм 3А
Клеммник соединительный на 12 контактовХорошо крепится к стенкам пластмассовых щитов при помощи клее..
21.00грн.
Умный дом #10: Датчик CO2
Умный дом #10: Датчик CO₂ Контроль качества воздуха в помещении — один из ключевых элем..
Термопара K-типа -100...800°C 5х100мм
Термопара типа К ТХА хромель-алюмель используется для измерения экстремально высоких температур до&n..
204.73грн.
Двойная шестерня для зубчатого ремня на 20 зубьев под ось 5 мм
Сдвоенная шестерня для передачи крутящего момента с одного вала на другой.Применяется для разделения..
97.08грн.
Мотор безщеточный A2212 1000KV
Мотор трехфазный безколлекторный для мультикоптеров 1000KVKV: 1000 оборотов на вольтМаксимальна..
360.40грн.





