Automatic curtain opener with a stepper motor on an Arduino Uno
A NEMA 17 on an A4988 homes to an end switch, opens the curtain at dawn and closes it at dusk, with a button to open, close or stop and a speed knob. The whole build, an Arduino Uno and 9 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 40 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $46. On Mokxi they're free.
What you'll learn
- Driving a stepper motor with an A4988 driver
- Homing against an end switch
- Two light thresholds (hysteresis) to avoid flicker
- Converting steps to millimeters
- Reading a button and a knob while a motor moves
- Difficulty: Intermediate
- Time: About 40 minutes
- Board: Arduino Uno R3
- Parts: 12
How it works
On power-up the sketch homes the curtain. It drives the stepper toward the end switch on pin 2 and stops when the switch closes. That spot becomes 0 mm, fully closed. The A4988 driver turns the pulses on pin 9 into motor steps.
Then loop watches the light sensor on A0 twice a second. Five readings in a row above DAY (700) open the curtain 1000 mm. Five readings in a row below NIGHT (300) close it. The gap between the two stops a passing cloud from flipping it back and forth.
The button on pin 3 opens, closes or stops the curtain at any time. The knob on A1 sets the speed of the next move, from 25 to 150 mm/s. The motor speeds up and slows down smoothly.
Parts list
12 parts, plus the jumper wires. They retail for about $46; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Stepper driver, A4988In Claw Machine Kit.
- 1 × Stepper motor, NEMA 17
- 1 × DC jack and adapter
- 1 × Capacitor, 100 µF
- 2 × Power, 5 V
- 1 × Limit switch, lever microswitchIn Claw Machine Kit, Vending Machine Kit.
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Light sensor
- 1 × Resistor, 10k ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × PotentiometerIn Halloween Prop Kit, Starter Projects Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Arduino Uno R3 pin GND | Ground |
| Stepper driver, A4988 pin STEP | Arduino Uno R3 pin 9 |
| Stepper driver, A4988 pin DIR | Arduino Uno R3 pin 8 |
| Stepper driver, A4988 pin EN | Arduino Uno R3 pin 7 |
| Pushbutton pin 1a | Arduino Uno R3 pin 3 |
| Limit switch, lever microswitch pin NO | Arduino Uno R3 pin 2 |
| Light sensor pin 2 | Arduino Uno R3 pin A0 |
| Resistor, 10k Ω pin 1 | Arduino Uno R3 pin A0 |
| Potentiometer pin 2 | Arduino Uno R3 pin A1 |
| Stepper driver, A4988 pin MS1 | Ground |
| Stepper driver, A4988 pin MS3 | Ground |
| Stepper driver, A4988 pin GND | Ground |
| Stepper driver, A4988 pin MS2 | 5 V |
| Stepper driver, A4988 pin VDD | 5 V |
| Stepper driver, A4988 pin SLP | Stepper driver, A4988 pin RST |
| DC jack and adapter pin V+ | Stepper driver, A4988 pin VMOT |
| Capacitor, 100 µF pin 1 | Stepper driver, A4988 pin VMOT |
| Stepper driver, A4988 pin GNDM | Ground |
| DC jack and adapter pin V- | Ground |
| Stepper motor, NEMA 17 pin A1 | Stepper driver, A4988 pin 2B |
| Stepper motor, NEMA 17 pin A2 | Stepper driver, A4988 pin 2A |
| Stepper motor, NEMA 17 pin B1 | Stepper driver, A4988 pin 1A |
| Stepper motor, NEMA 17 pin B2 | Stepper driver, A4988 pin 1B |
| Capacitor, 100 µF pin 2 | Ground |
| Limit switch, lever microswitch pin COM | Ground |
| Pushbutton pin 2b | Ground |
| Light sensor pin 1 | 5 V |
| Potentiometer pin 3 | 5 V |
| Resistor, 10k Ω pin 2 | Ground |
| Potentiometer pin 1 | Ground |
The note on the canvas
Word for word, as it sits beside the circuit in the editor.
Try it
1. Press Run. The curtain closes until it presses the end switch: click the switch to say it got there. The serial monitor reads homed: closed at 0 mm.
2. Drag the LDR's light up to daylight (100 lux or more). After 2.5 s the curtain opens 1000 mm, speeding up and slowing down smoothly.
3. Drag the light down to dusk (under 5 lux) and it closes again.
4. Press the button to open, close, or stop it mid-move. Turn the knob for the speed of the next move, 25 to 150 mm/s.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Automatic curtain opener: a NEMA 17 on an A4988 pulls the curtain along
// its rail on a GT2 belt. It opens at dawn and closes at dusk on its own, a
// button opens, closes or stops it at any time, and a knob sets how fast it
// travels. FastAccelStepper makes the steps from timer 1 in the background,
// with a smooth start and stop, so loop() is free to watch the light and
// the button while the curtain moves.
//
// A4988 STEP -> 9 (timer 1's OC1A: FastAccelStepper needs 9 or 10 on an Uno)
// DIR -> 8 EN -> 7 (low = driving) RST -> SLP
// MS2 -> 5 V, MS1 and MS3 -> GND: quarter steps
// VMOT -> 12 V with 100 uF across it, VDD -> 5 V, both grounds to GND
// end switch (curtain closed) NO -> 2, COM -> GND
// button (open / close / stop) -> 3 and GND
// LDR from 5 V to A0, 10 kohm from A0 to GND: brighter reads higher
// 10 kohm knob: ends on 5 V and GND, wiper -> A1 (travel speed)
//
// The numbers: a 200-step motor in quarter steps is 800 steps a turn, and a
// 20-tooth GT2 pulley moves the belt 20 x 2 mm = 40 mm a turn, so 20 steps a
// millimeter. The curtain travels TRAVEL_MM from closed (the switch) to open.
// Set the A4988's current limit for your motor before it pulls a load.
//
// Things to try. Drag the light on the LDR up to daylight and the curtain
// opens; drag it down to dusk and it closes. Press the button while it
// moves to stop it. Turn the knob to change the speed of the next move.
#include <FastAccelStepper.h>
const int STEP_PIN = 9;
const int DIR_PIN = 8;
const int ENABLE_PIN = 7;
const int HOME_PIN = 2; // end switch, pressed when the curtain is closed
const int BUTTON_PIN = 3;
const int LIGHT_PIN = A0;
const int SPEED_PIN = A1;
const long STEPS_PER_MM = 20; // 800 steps a turn / 40 mm a turn
const long TRAVEL_MM = 1000; // closed to open
const long ACCEL_MM = 200; // mm/s/s: half a second to full speed
const int SPEED_MIN_MM = 25; // mm/s, knob fully anticlockwise
const int SPEED_MAX_MM = 150; // mm/s, knob fully clockwise
const int HOMING_MM = 40; // mm/s while it looks for the switch
// The light, as A0 reads it. Above DAY it is morning (about 70 lux on the
// LDR), below NIGHT it is evening (about 5 lux). The gap between the two
// keeps a cloud from opening and closing it over and over.
const int DAY = 700;
const int NIGHT = 300;
const int LIGHT_CHECKS = 5; // readings in a row, half a second apart
FastAccelStepperEngine engine = FastAccelStepperEngine();
FastAccelStepper *stepper = NULL;
bool daytime = false;
int lightCount = 0;
unsigned long lastLight = 0;
unsigned long lastReport = 0;
bool lastButton = HIGH;
unsigned long buttonChanged = 0;
bool wasMoving = false;
long mm(long steps) { return steps / STEPS_PER_MM; }
int speedMm() {
return map(analogRead(SPEED_PIN), 0, 1023, SPEED_MIN_MM, SPEED_MAX_MM);
}
void moveTo(long targetMm, const char *why) {
int speed = speedMm();
stepper->setSpeedInHz(speed * STEPS_PER_MM);
stepper->moveTo(targetMm * STEPS_PER_MM);
Serial.print(why);
Serial.print(": to ");
Serial.print(targetMm);
Serial.print(" mm at ");
Serial.print(speed);
Serial.println(" mm/s");
}
void home() {
if (digitalRead(HOME_PIN) == HIGH) {
Serial.println("homing: closing until the end switch");
stepper->setSpeedInHz(HOMING_MM * STEPS_PER_MM);
stepper->runBackward();
while (digitalRead(HOME_PIN) == HIGH) {
}
}
stepper->forceStopAndNewPosition(0);
Serial.println("homed: closed at 0 mm");
}
void setup() {
Serial.begin(9600);
pinMode(HOME_PIN, INPUT_PULLUP);
pinMode(BUTTON_PIN, INPUT_PULLUP);
engine.init();
stepper = engine.stepperConnectToPin(STEP_PIN);
if (!stepper) {
Serial.println("STEP must be on pin 9 or 10");
while (true) {
}
}
stepper->setDirectionPin(DIR_PIN); // DIR high opens
stepper->setEnablePin(ENABLE_PIN); // low enables the A4988
stepper->setAutoEnable(true); // the motor rests unpowered
stepper->setAcceleration(ACCEL_MM * STEPS_PER_MM);
home();
Serial.println("curtain opener ready");
}
void loop() {
long pos = stepper->getCurrentPosition();
bool moving = stepper->isRunning();
// Closing past the switch means the curtain slipped on the belt: stop
// there and call it closed.
if (moving && digitalRead(HOME_PIN) == LOW && stepper->targetPos() < pos) {
stepper->forceStopAndNewPosition(0);
Serial.println("end switch: closed at 0 mm");
moving = false;
pos = 0;
}
// The button: open when closed or part open, close when open, stop
// while moving.
bool button = digitalRead(BUTTON_PIN);
if (button != lastButton && millis() - buttonChanged > 30) {
buttonChanged = millis();
lastButton = button;
if (button == LOW) {
if (moving) {
stepper->stopMove();
Serial.println("button: stop");
} else if (mm(pos) >= TRAVEL_MM) {
moveTo(0, "button: close");
} else {
moveTo(TRAVEL_MM, "button: open");
}
}
}
// The light, twice a second. Only a change from night to day or day to
// night moves the curtain, so a button press holds until the next one.
if (millis() - lastLight >= 500) {
lastLight = millis();
int light = analogRead(LIGHT_PIN);
bool changed = daytime ? light < NIGHT : light > DAY;
lightCount = changed ? lightCount + 1 : 0;
if (lightCount >= LIGHT_CHECKS) {
lightCount = 0;
daytime = !daytime;
if (daytime) {
moveTo(TRAVEL_MM, "dawn: open");
} else {
moveTo(0, "dusk: close");
}
}
}
if (moving && millis() - lastReport >= 200) {
lastReport = millis();
Serial.print("at ");
Serial.print(mm(pos));
Serial.println(" mm");
}
if (wasMoving && !moving) {
Serial.print("stopped at ");
Serial.print(mm(pos));
Serial.println(" mm");
}
wasMoving = moving;
}
Code walkthrough
The key functions of the sketch, and what each one does.
home sketch.ino
Finds the closed position. If the end switch is not pressed, it sets a slow speed and runs the motor backward until pin 2 reads LOW. Then forceStopAndNewPosition(0) declares that spot to be 0 mm. Everything after that is measured from here, so the sketch always knows where the curtain is.
void home() {
if (digitalRead(HOME_PIN) == HIGH) {
Serial.println("homing: closing until the end switch");
stepper->setSpeedInHz(HOMING_MM * STEPS_PER_MM);
stepper->runBackward();
while (digitalRead(HOME_PIN) == HIGH) {
}
}
stepper->forceStopAndNewPosition(0);
Serial.println("homed: closed at 0 mm");
}moveTo sketch.ino
Starts a move to a position in millimeters. It reads the knob with speedMm, sets the speed in steps per second, and tells the stepper library to go to the target. It returns right away, because the library keeps stepping in the background. It also prints the reason and speed to the serial monitor.
void moveTo(long targetMm, const char *why) {
int speed = speedMm();
stepper->setSpeedInHz(speed * STEPS_PER_MM);
stepper->moveTo(targetMm * STEPS_PER_MM);
Serial.print(why);
Serial.print(": to ");
Serial.print(targetMm);
Serial.print(" mm at ");
Serial.print(speed);
Serial.println(" mm/s");
}loop sketch.ino
Checks three things over and over without waiting. First the end switch, in case the curtain slips past closed. Then the button, with a 30 ms debounce. Then the light, once every 500 ms. It also prints the position every 200 ms while the curtain moves.
void loop() {
long pos = stepper->getCurrentPosition();
bool moving = stepper->isRunning();
// Closing past the switch means the curtain slipped on the belt: stop
// there and call it closed.
if (moving && digitalRead(HOME_PIN) == LOW && stepper->targetPos() < pos) {
stepper->forceStopAndNewPosition(0);
Serial.println("end switch: closed at 0 mm");
moving = false;
pos = 0;
}
// The button: open when closed or part open, close when open, stop
// while moving.
bool button = digitalRead(BUTTON_PIN);
if (button != lastButton && millis() - buttonChanged > 30) {
buttonChanged = millis();
lastButton = button;
if (button == LOW) {
if (moving) {
stepper->stopMove();
Serial.println("button: stop");
} else if (mm(pos) >= TRAVEL_MM) {
moveTo(0, "button: close");
} else {
moveTo(TRAVEL_MM, "button: open");
}
// ... 34 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The curtain never finishes homing. This project
In the simulator, click the end switch to press it. On real hardware, check the switch wires between pin 2 and GND. - The motor does not move at all. This project
The STEP wire must be on pin 9 or 10 for the stepper library. Also check that the A4988 has motor power and that EN is on pin 7. - The curtain does not react to the light. This project
Light must stay aboveDAYor belowNIGHTfor five checks in a row, about 2.5 seconds. Hold the light level steady. - The motor does nothing and the driver stays dead. Stepper driver
Tie RST to SLP with a jumper, or drive both HIGH. Those two pins float on many boards. Also connect the logic VDD to 5 V and ground, and the motor VMOT to its own supply. - The driver or the chip dies when you power up. Stepper driver
Put a 100 uF capacitor across VMOT and GND, close to the driver. Never connect or unplug the motor while powered, because the voltage spike can kill the chip. - The motor buzzes, shudders, or only vibrates instead of turning. Stepper motor
Coils are swapped. Find the two coil pairs by measuring resistance with a meter (a few ohms between wires of one coil). Connect each pair to one driver coil output, A to A and B to B. - The motor stalls or loses steps when you speed up. Stepper motor
A stepper cannot jump to a high speed. Start with slow step pulses and ramp up gradually. Check the driver current limit (Vref) and give the supply enough voltage, such as 12 V. - Board gets too much voltage and overheats or dies. DC jack and adapter
Check the adapter voltage before plugging in. Boards like the Uno want 7 to 12 V on the jack. A 5 V part wired to a 12 V supply can be destroyed.
Questions and answers
How far does the motor move per millimeter?
A 200-step motor in quarter steps (MS2 high, MS1 and MS3 low) makes 800 steps a turn, and a 20-tooth GT2 pulley moves the belt 40 mm a turn: 20 steps per mm. Change STEPS_PER_MM if your pulley or microstepping differs.
When does it open and close by itself?
It reads the LDR every half second. Five readings in a row above 700 on A0 open it and five below 300 close it, so a passing cloud does not move it. A button press holds until the next change from night to day or day to night.
Why must STEP be on pin 9?
FastAccelStepper makes the steps from timer 1's compare interrupt, whose pins on an Uno are 9 and 10; stepperConnectToPin() returns NULL for any other pin. Timer 1 is then busy, so the Servo library and analogWrite() on 9 and 10 cannot be used.
What power supply does it need?
12 V for the A4988's VMOT with 100 uF across it, and the Uno's 5 V for VDD. Set the A4988's current limit for your motor first. Between moves the outputs are switched off, so the motor stays cool.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.