Автоматичні дверцята курника: кроковий двигун, A4988 і концевики
Серце нашого проєкту chicken_door: як ESP32 через драйвер A4988 піднімає й опускає дверцята, зупиняється по концевиках і не залишає мотор крутитися, якщо щось пішло не так.
Про проєкт
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 довго недоступний.