← До всіх проєктів
Автоматизація Середній

Автоматичні дверцята курника: кроковий двигун, A4988 і концевики

Серце нашого проєкту chicken_door: як ESP32 через драйвер A4988 піднімає й опускає дверцята, зупиняється по концевиках і не залишає мотор крутитися, якщо щось пішло не так.

Складність
Середній
Час
2–3 h
Категорія
Автоматизація
Оновлено

Про проєкт

chicken_door — наш проєкт автоматичних дверцят курника: вранці вони відчиняються після сходу сонця, ввечері зачиняються, а керувати ними можна з телефона через веб-сторінку. Всередині — ESP32-C3, драйвер крокового двигуна A4988, двигун NEMA17 з гвинтовою передачею та два концевики: «відчинено» і «зачинено».

У цій статті — найважливіша частина: схема підключення мотора й базова програма, яка безпечно рухає дверцята. Розклад за сонцем, веб-інтерфейс і камеру залишимо за кадром.

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

Компонент Навіщо
ESP32 (DevKit або ESP32-C3 SuperMini) мозок
Драйвер A4988 (модуль Pololu або аналог) перетворює імпульси STEP на обертання
Кроковий двигун NEMA17 + гвинт/трос піднімає дверцята
2 концевики (мікроперемикачі) зупиняють мотор у крайніх положеннях
Блок живлення 12 В ≥ 2 А живлення мотора (VMOT)
Конденсатор 100 мкФ 35 В біля VMOT гасить сплески напруги — без нього A4988 може згоріти
Резистори 10 кОм — 2 шт. тримають STEP у 0 і драйвер вимкненим, поки ESP32 завантажується

Схема

    12 V (+) ──┬──────────── VMOT ┌──────────┐ 1A ─┐ обмотка A
     100 мкФ  ═╪═            GND  │  A4988   │ 1B ─┘
    12 V (−) ──┴──┬───────── GND  │          │ 2A ─┐ обмотка B   NEMA17
                  │  3V3 ─── VDD  │          │ 2B ─┘
   ESP32 GND ─────┘               │          │
   GPIO26 ──┬────────────── STEP  │          │  RESET ─┐ з'єднати
         10k┴ GND                 │          │  SLEEP ─┘ перемичкою
   GPIO27 ───────────────── DIR   │          │
   GPIO25 ──┬────────────── EN    │          │
         10k┴ 3V3                 └──────────┘

   GPIO32 ──── концевик «ВІДЧИНЕНО» ──── GND
   GPIO33 ──── концевик «ЗАЧИНЕНО»  ──── GND

Земля блока живлення 12 В і земля ESP32 обов'язково спільні.

Сигнал ESP32 DevKit (ця стаття) ESP32-C3 SuperMini (плата chicken_door)
STEP GPIO26 GPIO4
DIR GPIO27 GPIO5
EN GPIO25 GPIO6
Концевик «відчинено» GPIO32 GPIO7
Концевик «зачинено» GPIO33 GPIO10

Як це працює

  • STEP — кожен імпульс HIGH→LOW повертає вал на один крок (1/200 оберту в повнокроковому режимі). Швидкість задає пауза між імпульсами: 3 мс — спокійний хід.
  • DIR — напрямок: HIGH відчиняє, LOW зачиняє.
  • EN — активний LOW. Коли дверцята стоять, драйвер вимикаємо: мотор не гріється і не гуде. На модулі A4988 цей вхід підтягнутий до землі, тому без зовнішнього резистора 10 кОм до 3.3 В мотор увімкнеться сам, поки ESP32 завантажується.
  • Концевики замикають пін на GND (INPUT_PULLUP, натиснутий = LOW). Щоб випадкова завада не зупинила дверцята, рух припиняється лише після трьох зчитувань поспіль.
  • Таймаут — головний захист: якщо концевик не спрацював (обрив дроту, дверцята застрягли), мотор вимкнеться через MAX_MOVE_TIME_MS. Ставте його на 20–30 % більше за реальний час руху.

Базова програма

Команди — з кнопок або з монітора порту: open, close, stop.

// Дверцята курника — базове керування мотором (з проєкту chicken_door)
// Драйвер A4988 + два концевики; команди з кнопок або з Serial.

const int STEP_PIN = 26;
const int DIR_PIN = 27;
const int ENABLE_PIN = 25;        // активний LOW, зовнішня підтяжка 10k до 3.3 В
const int OPEN_LIMIT_PIN = 32;    // концевик на GND: натиснутий = LOW
const int CLOSE_LIMIT_PIN = 33;
const int BTN_OPEN = 18;
const int BTN_CLOSE = 19;

const int DIR_OPEN = HIGH;
const int DIR_CLOSE = LOW;
const unsigned long STEP_INTERVAL_US = 3000;   // більше = повільніше
const unsigned long MAX_MOVE_TIME_MS = 15000;  // реальний час руху + 20–30 %
const unsigned long DEBOUNCE_MS = 20;

class LimitSwitch {
  public:
    LimitSwitch(int pin) : pin(pin) {}
    void begin() {
      pinMode(pin, INPUT_PULLUP);
      raw = stable = pressed();
      changedAt = millis();
    }
    // новий рівень приймаємо, лише якщо він тримається DEBOUNCE_MS
    void update() {
      bool now = pressed();
      if (now != raw) { raw = now; changedAt = millis(); }
      else if (raw != stable && millis() - changedAt >= DEBOUNCE_MS) stable = raw;
    }
    bool active() const { return stable; }
  private:
    int pin;
    bool raw = false, stable = false;
    unsigned long changedAt = 0;
    bool pressed() { return digitalRead(pin) == LOW; }
};

enum Motion { STOPPED, OPENING, CLOSING };

LimitSwitch openLimit(OPEN_LIMIT_PIN);
LimitSwitch closeLimit(CLOSE_LIMIT_PIN);
Motion motion = STOPPED;
unsigned long moveStartedAt = 0;
unsigned long lastStepUs = 0;
int limitReads = 0;

void setDriver(bool on) { digitalWrite(ENABLE_PIN, on ? LOW : HIGH); }

void stopMotor(const char* reason) {
  if (motion != STOPPED) Serial.printf("Стоп: %s\n", reason);
  motion = STOPPED;
  setDriver(false);   // знеструмлений мотор не гріється й не гуде
}

void startMoving(Motion dir) {
  if (motion == dir) return;
  stopMotor("нова команда");
  if (openLimit.active() && closeLimit.active()) {
    Serial.println("Натиснуті обидва концевики — перевірте проводку");
    return;
  }
  LimitSwitch &target = dir == OPENING ? openLimit : closeLimit;
  if (target.active()) {
    Serial.println(dir == OPENING ? "Вже відчинено" : "Вже зачинено");
    return;
  }
  digitalWrite(DIR_PIN, dir == OPENING ? DIR_OPEN : DIR_CLOSE);
  setDriver(true);
  delayMicroseconds(20);   // DIR має встановитися до першого STEP
  limitReads = 0;
  moveStartedAt = millis();
  lastStepUs = micros();
  motion = dir;
  Serial.println(dir == OPENING ? "Відчиняю..." : "Зачиняю...");
}

// Один крок, якщо настав час; зупинка, коли цільовий концевик натиснутий 3 рази поспіль
void stepMotor() {
  if (motion == STOPPED || micros() - lastStepUs < STEP_INTERVAL_US) return;
  lastStepUs += STEP_INTERVAL_US;
  int limitPin = motion == OPENING ? OPEN_LIMIT_PIN : CLOSE_LIMIT_PIN;
  if (digitalRead(limitPin) == LOW) {
    if (++limitReads >= 3) {
      stopMotor(motion == OPENING ? "концевик ВІДЧИНЕНО" : "концевик ЗАЧИНЕНО");
      return;
    }
  } else {
    limitReads = 0;
  }
  digitalWrite(STEP_PIN, HIGH);
  delayMicroseconds(5);    // A4988 потрібно щонайменше 1 мкс
  digitalWrite(STEP_PIN, LOW);
}

void setup() {
  Serial.begin(115200);
  digitalWrite(ENABLE_PIN, HIGH);   // драйвер вимкнений ще до того, як пін стане виходом
  pinMode(ENABLE_PIN, OUTPUT);
  pinMode(STEP_PIN, OUTPUT);
  pinMode(DIR_PIN, OUTPUT);
  pinMode(BTN_OPEN, INPUT_PULLUP);
  pinMode(BTN_CLOSE, INPUT_PULLUP);
  openLimit.begin();
  closeLimit.begin();
  Serial.println("Команди: open, close, stop");
}

void loop() {
  openLimit.update();
  closeLimit.update();
  stepMotor();

  if (motion != STOPPED && millis() - moveStartedAt >= MAX_MOVE_TIME_MS) {
    stopMotor("ТАЙМАУТ — перевірте, чи не застрягли дверцята");
  }

  // кнопки: реагуємо на момент натискання
  static int lastOpen = HIGH, lastClose = HIGH;
  int o = digitalRead(BTN_OPEN), c = digitalRead(BTN_CLOSE);
  if (o == LOW && lastOpen == HIGH) startMoving(OPENING);
  if (c == LOW && lastClose == HIGH) startMoving(CLOSING);
  lastOpen = o;
  lastClose = c;

  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    cmd.trim();
    if (cmd == "open") startMoving(OPENING);
    else if (cmd == "close") startMoving(CLOSING);
    else if (cmd == "stop") stopMotor("команда");
  }
}

Налаштування

  • Струм A4988 виставте підстроювальним резистором до першого ввімкнення: Vref = I × 8 × Rs. Для більшості клонів з резисторами R100 (0.1 Ом) це Vref ≈ 0.8 × I, для оригінальних Pololu з 0.068 Ом — Vref ≈ 0.54 × I. Перевірте маркування резисторів на своєму модулі.
  • Концевики NC (нормально замкнені) надійніші: обрив дроту виглядає як «натиснутий», і мотор зупиняється, а не їде далі. Тоді в коді порівнюйте з HIGH.
  • Утримання: якщо дверцята сповзають униз при вимкненому моторі, не вимикайте драйвер у stopMotor() — але мотор і драйвер тоді постійно грітимуться.
  • Швидкість: менший STEP_INTERVAL_US — швидше, але надто часті імпульси навантажений мотор пропускає.

Що в повному проєкті

Тут кроки робляться в loop(). У прошивці chicken_door їх генерує апаратний таймер у перериванні, і там же перевіряється концевик: хоч би як довго оброблявся веб-запит чи кадр камери, мотор не смикається й не проїжджає кінцеве положення. Поверх цього — розклад за сходом і заходом сонця, веб-інтерфейс, сторожовий таймер (watchdog) і перезавантаження, якщо Wi-Fi довго недоступний.