Learning Goals 5 min
Yesterday's rig was "snap to position" — three discrete angles. Today you'll make the servo glide smoothly through the whole range, then learn the non-blocking version using millis(). By the end you will be able to:
- Write the classic
Sweepsketch using aforloop anddelay()— the example bundled with the Arduino IDE — and explain what changing the loop step or the delay does to the motion. - Rewrite the same sweep using
millis()so the servo motion does not block the rest ofloop()— directly applying the L02-35 "blink without delay" pattern to a different output device. - Identify three real-world reasons you almost never want a 100%-speed servo move in a polished project: noise, current spike, mechanical wear — and pick a reasonable sweep speed in degrees-per-second for your application.
Warm-Up 10 min
Re-attach the horn to your SG90 so today's motion is visible. Plug in the bench-test wiring from L03-02 (signal on D9, red on 5V, brown on GND).
Recall: for loop with a counter as an angle
You used for loops in L01-11 to step through pin numbers (for (int p = 2; p <= 6; p++)). The same idea works for angles: step a counter from 0 to 180, write it to the servo each time, and the servo will physically follow.
One-question warm-up
Without writing any code, predict: if you sweep for (int a = 0; a <= 180; a++) { s.write(a); delay(15); }, roughly how long does the sweep take from end to end?
Reveal
180 iterations × 15 ms each = 2700 ms ≈ 2.7 seconds. Tweak the delay(15) to control sweep speed: shorter delay = faster sweep; longer delay = slower. (The servo can't actually travel a full degree in 15 ms — it physically lags the commands. We'll see and discuss that in §3.)
New Concept · Smooth motion with for and with millis() 25 min
Version 1 — the textbook sweep
The Arduino IDE ships with this exact sketch under File → Examples → Servo → Sweep. We'll meet it now, then improve it.
#include <Servo.h>
Servo myServo;
int pos = 0;
void setup() {
myServo.attach(9);
}
void loop() {
for (pos = 0; pos <= 180; pos++) {
myServo.write(pos);
delay(15);
}
for (pos = 180; pos >= 0; pos--) {
myServo.write(pos);
delay(15);
}
}The delay(15) sets the pace. A delay of 15 ms × 180 steps = 2.7 s per direction, so ~5.4 s for the full there-and-back. Increase the delay → slower, smoother-looking sweep. Decrease it → faster, more jerky.
Why the sweep looks chunky at small delays
An SG90 takes ~0.10 s per 60°, which is about 1.7 ms per degree. If you set delay(15) the servo has plenty of time to settle on each commanded angle — motion looks smooth. If you set delay(2) the commands arrive faster than the servo can move; the servo lags, sometimes skipping intermediate positions. Setting the delay much higher than 15 ms gives obvious stop-go motion (each command, then a long pause, then the next).
The big problem with this sketch
While the servo is sweeping, nothing else can run. The delay(15) calls block the CPU for 15 ms each, 180+ times per direction. You can't read a button, check a sensor, blink an LED — your sketch is asleep most of the time.
This is the same lesson as L02-33 ("Why delay() is a problem") and the fix is the same — replace delay() with millis()-based timing.
Version 2 — non-blocking sweep using millis()
The pattern is identical to blink-without-delay (L02-35), just applied to a servo instead of an LED:
#include <Servo.h>
const int SERVO_PIN = 9;
const unsigned long STEP_INTERVAL_MS = 15;
Servo myServo;
int pos = 0;
int dir = 1; // +1 forward, -1 back
unsigned long lastStep = 0;
void setup() {
myServo.attach(SERVO_PIN);
}
void loop() {
// 1. Servo step (non-blocking)
if (millis() - lastStep >= STEP_INTERVAL_MS) {
lastStep = millis();
pos += dir;
if (pos >= 180) { pos = 180; dir = -1; }
if (pos <= 0) { pos = 0; dir = +1; }
myServo.write(pos);
}
// 2. Anything else can also live here — it runs ~thousands of times
// per second while the servo is sweeping.
}Read it line by line:
lastSteprecords when we last advanced the angle.- If 15 ms have passed since then, advance one step in the current direction.
- At the ends of travel, flip
dirso the next step goes the other way. - Crucially:
loop()returns quickly when there's no work to do — your sketch can read sensors, react to buttons, drive other actuators, all while the servo continues to sweep in the background.
Choosing a step interval that matches your servo
One useful rule: aim for the step interval to be slightly longer than the servo's natural movement time per degree. For an SG90 (~1.7 ms/°), an interval of 5–20 ms gives smooth visible motion. Slower servos (heavy gears, MG996R) want 30–40 ms.
| Step interval | Sweep speed (deg / s) | Full 0→180→0 cycle | Looks like |
|---|---|---|---|
| 5 ms | 200°/s | ~1.8 s | Quick & precise — start of fast motion. Risk of servo lag. |
| 15 ms | 67°/s | ~5.4 s | Default. Smooth and deliberate. |
| 30 ms | 33°/s | ~10.8 s | Slow, theatrical — good for indicator needles. |
| 100 ms | 10°/s | ~36 s | Glacial. Useful for "sunrise" effects, not for robotics. |
Worked Example · Sweep + blink in parallel 20 min
To prove the non-blocking pattern, we'll add an LED that blinks once per second while the servo sweeps. If we'd used Version 1 (with delay), the LED would skip beats. With Version 2, the LED is perfectly steady.
Step 1 — wiring
| Component | UNO pin |
|---|---|
| SG90 signal | D9 |
| SG90 +5 V | 5V (with 100 µF cap to GND) |
| SG90 GND | GND |
| LED + 220 Ω resistor | D6 → resistor → LED → GND |
Step 2 — sketch
// L03-03 · Non-blocking sweep + heartbeat LED
#include <Servo.h>
const int SERVO_PIN = 9;
const int LED_PIN = 6;
const unsigned long STEP_INTERVAL_MS = 15;
const unsigned long BLINK_INTERVAL_MS = 500;
Servo myServo;
int pos = 0;
int dir = 1;
unsigned long lastStep = 0;
unsigned long lastBlink = 0;
bool ledOn = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
myServo.attach(SERVO_PIN);
}
void loop() {
unsigned long now = millis();
// Sweep step
if (now - lastStep >= STEP_INTERVAL_MS) {
lastStep = now;
pos += dir;
if (pos >= 180) { pos = 180; dir = -1; }
if (pos <= 0) { pos = 0; dir = +1; }
myServo.write(pos);
}
// Heartbeat blink
if (now - lastBlink >= BLINK_INTERVAL_MS) {
lastBlink = now;
ledOn = !ledOn;
digitalWrite(LED_PIN, ledOn);
}
}Step 3 — upload and watch the LED
The servo sweeps continuously. The LED ticks on / off exactly twice per second (1 Hz), independent of where the servo is in its sweep. This is the practical payoff of the non-blocking pattern.
Step 4 — break it on purpose (to feel the difference)
Replace the sweep step with the blocking version below and re-upload:
for (int a = 0; a <= 180; a++) {
myServo.write(a);
delay(15);
}
for (int a = 180; a >= 0; a--) {
myServo.write(a);
delay(15);
}The LED now flashes once… then pauses for ~5 seconds while the servo sweeps… then flashes once more. The blocking version literally can't blink the LED on schedule because the CPU is busy waiting in delay.
Revert to Version 2 and the LED returns to a steady 1 Hz. Now you've felt why we care about non-blocking code.
Step 5 — extract a helper
The non-blocking sweep is something you'll want again. Wrap it into a function so other sketches can re-use it as-is:
void stepSweep(Servo &s, int &pos, int &dir, int lo, int hi) {
pos += dir;
if (pos >= hi) { pos = hi; dir = -1; }
if (pos <= lo) { pos = lo; dir = +1; }
s.write(pos);
}The & in the parameter list means "pass by reference" — the helper modifies the caller's pos and dir directly. We'll revisit references in L03-41 when we wrap behaviour into classes.
Try It Yourself 15 min
Goal: Limit the sweep to 30°–150° instead of 0°–180°. Why is this useful for a real mechanism (e.g. a windscreen wiper)?
Hint & discussion
Change the end-stop checks:
if (pos >= 150) { pos = 150; dir = -1; }
if (pos <= 30) { pos = 30; dir = +1; }A wiper, a pan camera or a robot arm rarely uses the servo's full physical travel — going to the end-stops can damage the servo gears and waste time getting back to the "useful" range. Limiting in software is the easy fix.
Goal: Speed up the return sweep (faster going back than forward). Forward sweep at 30 ms / step, return at 5 ms / step. Hint: you'll need a different step interval depending on the current dir.
Hint
unsigned long stepInterval =
(dir > 0) ? FWD_INTERVAL_MS : REV_INTERVAL_MS;
if (millis() - lastStep >= stepInterval) {
// ... unchanged ...
}This is the unevenly-spaced "eye-blink" pattern — quick close, slow open. Watch out for the servo's mechanical limit: 5 ms / step is > 200 °/s, which is faster than the SG90 can physically move, so the visible return will be limited by the motor, not by your timing.
Goal: Make the sweep speed depend on a potentiometer on A0 — turn the pot and the sweep speeds up or slows down live, without restarting the sketch. Map the pot reading to a step interval between 5 ms and 100 ms.
Hint
int raw = analogRead(A0);
unsigned long stepInterval = map(raw, 0, 1023, 5, 100);
if (millis() - lastStep >= stepInterval) { /* ... */ }This is the classic "control speed live with a dial" rig — you'll use it again for stepper motors (L03-13), DC motors (L03-09), and ultimately in PID tuning (L04-26). The pattern: read input, compute parameter, drive output, repeat — all non-blocking.
Mini-Challenge · "Three pendulums" 10 min
Imagine you have three servos (you don't — yet, that's L03-04). On paper, describe the data each one would need to sweep independently with different speeds, ranges and start positions.
- List the variables one servo's sweep needs: pin, current position, direction, lower bound, upper bound, step interval, last-step timestamp. (Seven values.)
- How would you bundle those seven values together so "servo 2" and "servo 3" don't have to be six copies of the same variable names? Sketch the data structure — a
structwould be a perfect tool. (We'll formally meetstructs in L03-40.) - How many simultaneous independent sweeps could one UNO conceivably manage with this pattern, before
loop()starts to take meaningful time per iteration?
Reveal one good answer
- Variables:
Servo s,int pos,int dir,int lo,int hi,unsigned long stepInterval,unsigned long lastStep. - A
struct SweepServo { Servo s; int pos; int dir; int lo; int hi; unsigned long step; unsigned long lastStep; };— then create an arraySweepServo pendulums[3];and loop over it inloop(). - Realistic answer: dozens. Each iteration of
loop()does a fewmillis()compares and at most oneservo.write()per servo. The bottleneck is the number of physical servo timers (the UNO's Servo library supports up to 12 servos simultaneously) and the 5 V current budget (~500 mA per servo while moving — you'll outgrow the on-board regulator long before the software limit).
Recap 5 min
Sweeping a servo is the simplest example of continuous motion. The textbook version uses a blocking for loop with delay(); the production version uses the L02-35 non-blocking pattern with millis() — and the same code shape will reappear for every actuator in Level 3 and Level 4. The key insight: loop() should return quickly so it can also do everything else your sketch needs. Speed is set by step interval, range by lo/hi clamps, direction by a separate sign variable. Tomorrow we use the same servo as a visible indicator — pointing at a value rather than just sweeping for show.
- Sweep
- Servo motion that steps smoothly through a range of angles, typically driven by a counter that increments each timestep.
- Blocking code
- Code that holds up the CPU until it finishes — usually because of
delay(),pulseIn()with a long timeout, or busy-wait loops. The whole sketch effectively pauses. - Non-blocking code
- Code that defers waiting to a timestamp comparison ("is enough time elapsed?") and lets
loop()return between checks. Multiple things can appear to happen at once. - Step interval
- The minimum time between two updates to an animated value. Sets the speed of a sweep, fade, or any other smooth animation.
- Direction variable
- An explicit
+1/-1(ortrue/false) that records which way a sweep is currently going. Cleaner than inferring direction from the current position. - Pass by reference
- A function parameter declared
int &xmeans the function can modify the caller's variable, not just a copy. Useful for helpers that update several values at once. - Sweep speed (deg / s)
- Total angular distance covered per second = 1000 / step interval, in degrees per second. Helps when you need to specify motion in physical units.
Homework 5 min
Three exercises tonight.
- Build the "sweep + heartbeat" rig from §4 and video the LED while the sweep runs. Cut between the blocking and non-blocking versions and watch the LED behaviour change. This is the most concrete demo of non-blocking code you'll ever see — keep the video.
- Calculate: at a step interval of 20 ms, how many full 0°→180°→0° cycles will the servo complete in 1 minute? Show your maths.
- Read ahead to ARD-L03-04 (Indicator Needle). Bring one piece of stiff card / cardstock and a printer-friendly hand-drawn dial face: 0–10, like an analog volume knob.
Bring back next class:
- Your sweep + LED video (or the working physical rig).
- Your full-cycle maths.
- Your dial face + cardboard pointer — we'll glue them to the servo horn for the indicator needle build.