← All projects
Automation Intermediate

Automatic Chicken Coop Door: Stepper Motor, A4988 and Limit Switches

The heart of our chicken_door project: how the ESP32 raises and lowers the door through an A4988 driver, stops at the limit switches and never leaves the motor running when something goes wrong.

Difficulty
Intermediate
Time
2–3 h
Category
Automation
Updated

About the project

chicken_door is our automatic chicken coop door: it opens after sunrise, closes in the evening, and can be controlled from a phone through a web page. Inside: an ESP32-C3, an A4988 stepper driver, a NEMA17 motor with a lead screw and two limit switches — "open" and "closed".

This article covers the most important part: wiring the motor and a basic program that moves the door safely. The sun schedule, web UI and camera stay out of scope.

What you need

Part Why
ESP32 (DevKit or ESP32-C3 SuperMini) the brain
A4988 driver (Pololu board or a clone) turns STEP pulses into rotation
NEMA17 stepper + screw/cord lifts the door
2 limit switches (micro switches) stop the motor at both ends
12 V ≥ 2 A power supply motor supply (VMOT)
100 µF 35 V capacitor at VMOT absorbs voltage spikes — without it the A4988 can burn
2 × 10 kΩ resistors keep STEP low and the driver off while the ESP32 boots

Wiring

    12 V (+) ──┬──────────── VMOT ┌──────────┐ 1A ─┐ coil A
     100 µF   ═╪═            GND  │  A4988   │ 1B ─┘
    12 V (−) ──┴──┬───────── GND  │          │ 2A ─┐ coil B   NEMA17
                  │  3V3 ─── VDD  │          │ 2B ─┘
   ESP32 GND ─────┘               │          │
   GPIO26 ──┬────────────── STEP  │          │  RESET ─┐ join with
         10k┴ GND                 │          │  SLEEP ─┘ a jumper
   GPIO27 ───────────────── DIR   │          │
   GPIO25 ──┬────────────── EN    │          │
         10k┴ 3V3                 └──────────┘

   GPIO32 ──── "OPEN" limit switch   ──── GND
   GPIO33 ──── "CLOSED" limit switch ──── GND

The 12 V supply ground and the ESP32 ground must be shared.

Signal ESP32 DevKit (this article) ESP32-C3 SuperMini (chicken_door board)
STEP GPIO26 GPIO4
DIR GPIO27 GPIO5
EN GPIO25 GPIO6
"Open" limit switch GPIO32 GPIO7
"Closed" limit switch GPIO33 GPIO10

How it works

  • STEP — each HIGH→LOW pulse turns the shaft by one step (1/200 of a turn in full-step mode). The pause between pulses sets the speed: 3 ms gives a calm travel.
  • DIR — direction: HIGH opens, LOW closes.
  • EN — active LOW. While the door is idle the driver is switched off, so the motor neither heats up nor hums. The A4988 board pulls this input to ground, so without an external 10 kΩ pull-up to 3.3 V the motor switches on by itself while the ESP32 boots.
  • Limit switches connect the pin to GND (INPUT_PULLUP, pressed = LOW). So that a noise spike can't stop the door, the motion stops only after three readings in a row.
  • Timeout — the main safety net: if a switch never triggers (broken wire, jammed door), the motor stops after MAX_MOVE_TIME_MS. Set it 20–30 % above the real travel time.

Basic program

Commands come from buttons or the Serial Monitor: open, close, stop.

// Chicken coop door — basic motor control (from the chicken_door project)
// A4988 stepper driver + two limit switches; commands from buttons or Serial.

const int STEP_PIN = 26;
const int DIR_PIN = 27;
const int ENABLE_PIN = 25;        // active LOW, external 10k pull-up to 3.3 V
const int OPEN_LIMIT_PIN = 32;    // switch to GND: pressed = 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;   // bigger = slower
const unsigned long MAX_MOVE_TIME_MS = 15000;  // real travel time + 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();
    }
    // accept a new level only once it has held for 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("Stop: %s\n", reason);
  motion = STOPPED;
  setDriver(false);   // an unpowered motor neither heats up nor hums
}

void startMoving(Motion dir) {
  if (motion == dir) return;
  stopMotor("new command");
  if (openLimit.active() && closeLimit.active()) {
    Serial.println("Both limit switches are pressed - check the wiring");
    return;
  }
  LimitSwitch &target = dir == OPENING ? openLimit : closeLimit;
  if (target.active()) {
    Serial.println(dir == OPENING ? "Already open" : "Already closed");
    return;
  }
  digitalWrite(DIR_PIN, dir == OPENING ? DIR_OPEN : DIR_CLOSE);
  setDriver(true);
  delayMicroseconds(20);   // DIR must settle before the first STEP
  limitReads = 0;
  moveStartedAt = millis();
  lastStepUs = micros();
  motion = dir;
  Serial.println(dir == OPENING ? "Opening..." : "Closing...");
}

// One step when it's time; stop once the target switch reads pressed 3 times in a row
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 ? "OPEN limit" : "CLOSED limit");
      return;
    }
  } else {
    limitReads = 0;
  }
  digitalWrite(STEP_PIN, HIGH);
  delayMicroseconds(5);    // the A4988 needs at least 1 us
  digitalWrite(STEP_PIN, LOW);
}

void setup() {
  Serial.begin(115200);
  digitalWrite(ENABLE_PIN, HIGH);   // driver off before the pin even becomes an output
  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("Commands: open, close, stop");
}

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

  if (motion != STOPPED && millis() - moveStartedAt >= MAX_MOVE_TIME_MS) {
    stopMotor("TIMEOUT - check the door for obstructions");
  }

  // buttons: react to the moment of pressing
  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("command");
  }
}

Tuning

  • A4988 current: set it with the trimmer before the first power-up: Vref = I × 8 × Rs. For most clones with R100 (0.1 Ω) sense resistors that is Vref ≈ 0.8 × I, for genuine Pololu boards with 0.068 Ω it is Vref ≈ 0.54 × I. Check the resistor marking on your board.
  • NC (normally closed) switches are more reliable: a broken wire looks like "pressed", so the motor stops instead of driving on. Compare with HIGH in the code then.
  • Holding: if the door slides down while the motor is off, don't disable the driver in stopMotor() — but then the motor and driver stay warm all the time.
  • Speed: a smaller STEP_INTERVAL_US is faster, but a loaded motor skips pulses that come too often.

What the full project adds

Here steps are made in loop(). The chicken_door firmware generates them from a hardware timer interrupt and checks the limit switch in the same interrupt: however long a web request or a camera frame takes, the motor never stutters or overruns its end position. On top of that come the sunrise/sunset schedule, the web UI, a watchdog and a restart if Wi-Fi stays down for too long.