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.
BMS плата захисту 3х літій-іонних акумуляторів 10А
Модуль захисту батареї трьох літій-іонних акумуляторівНапруга відсічення при заряді 4,25...4,35ВНапр..
114.46грн.
Джойстик двохосьовий з кнопкою
Двохосьовий джойстик з кнопкою — це зручний модуль керування для проєктів на Arduino, ESP32 та інших..
44.72грн.
ESP32 LED PWM: частоти та роздільна здатність ШІМ (LEDC)
ESP32 LED PWM Controller: керування яскравістю світлодіодів ESP32 має вбудований модуль LED PWM C..
Arduino SIM800 CBC рівень заряду
Привіт друзі! SIM800L має аналого-цифровий перетворювач, який призначений для вимірювання напруги жи..
Цифровий датчик температури DS18B20
Напруга живлення 3 ... 5,5 ВРобоча температура -55 ... +125 °CТочність ±0,5 °C забезп..
25.83грн.





