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;
}
}
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.
Розумний дім #11: Комунікаційні мережі
Розумний дім #11: Комунікаційні мережі Жодна система розумного дому неможлива без надій..
Модуль термопари К-типу MAX6675 0...+1024 °C для Arduino
Цей модуль на основі спеціалізованої мікросхеми MAX6675 призначений для точного вимірювання високих ..
168.35грн.
Шарніри під обертальну вісь 8, 10 мм 2шт.
Такі шарніри призначені для кріплення вісі черв'ячної передачі на будь-якій з трьох осей переміщення..
164.87грн.
Твердотільне реле 40А
Реле без механічних контактів на 40 А. Силовим елементом даного реле є симистор.Гальванічна розв'язк..
304.49грн.
Розробка IoT пристроїв під ключ
Розробка IoT пристроїв під ключ Потрібен IoT пристрій під ключ — від ідеї..





