Learning Goals 5 min
By the end of this lesson you will be able to:
- Explain the difference between "while a button is held" (level-triggered) and "the moment a button is pressed" (edge-triggered).
- Remember the button's previous state across loop iterations by storing it in a mutable global variable — your first real use of a plain
intthat changes while the sketch runs. - Use a minimal
ifstatement to detect when the button's state changes and trigger an action exactly once per press — building a press-to-toggle LED.
Warm-Up 10 min
Two lessons ago you wired a button with INPUT_PULLUP. Pressing it lit the onboard LED while you held it down. The moment you released, the LED went dark. The LED followed the button — that's called a level-triggered mirror.
Today we want something different: each press should toggle the LED once, no matter how long you hold the button. That requires noticing the edge — the exact moment the button changed from released to pressed.
Quick-fire puzzle
Aiman writes a counter that adds 1 to a number every time digitalRead returns LOW (pressed, with INPUT_PULLUP). He holds the button down for 2 seconds.
- How many times does the body of
loop()run in those 2 seconds, roughly? (Hint: the Arduino UNO runsloop()thousands of times per second.) - What number does Aiman's counter show after 2 seconds of holding?
- What number should it show if we want it to count presses, not "milliseconds of hold time"?
Reveal the answer
- Tens of thousands. Each
loop()pass takes only a few microseconds on the UNO. - Tens of thousands. Aiman has counted "how many moments the button was pressed" — far more than what he wanted.
- One. One physical press = one count. To get that, the sketch has to notice when the button changed from released to pressed, and react only at that instant.
That "noticing the change" is today's whole lesson. The trick: remember what the button was the last time we checked, and compare.
New Concept 20 min
The big idea — level vs edge
A digital pin's reading can tell you two different things:
- Level: what is the pin right now? HIGH or LOW. This is what
digitalReadgives you, every time you call it. - Edge: did the pin just change since the last time we looked? Rising edge = LOW → HIGH; falling edge = HIGH → LOW.
For an INPUT_PULLUP button: falling edge = press (HIGH → LOW), rising edge = release (LOW → HIGH). Edge tells you about events — moments in time. Level tells you about states — what's happening continuously.
Remembering across loop iterations — mutable globals
To notice an edge, you need to remember what the button was last time you read it. But loop() starts fresh each iteration — local variables disappear at the end of the function. So we use a mutable global variable: declared above setup(), with no const, the value survives from one loop pass to the next.
// Globals — they persist for the whole sketch.
int lastButton = HIGH; // not const — this value will change
int ledState = LOW; // also mutableThis is the first real use of plain int from L01-09. Pin numbers and delays stayed the same forever, so they were const int. lastButton and ledState change as the sketch runs, so they get plain int.
A minimal if statement — just enough for today
To detect a state change, we need a way to do something only when a condition is true. That's the if statement. Today's lesson is the smallest possible introduction — full if/else coverage waits for L01-20 next class.
if (currentButton != lastButton) {
// runs only when the button has changed since last loop pass
}if— the keyword.(...)— the condition, in parentheses. Today we use comparison operators:!=("not equal to") and==("equal to").{ ... }— the body. Runs once if the condition is true, skipped entirely if false.
Note: != is "not equal" and == (two equals signs) is "equal". A single = means assignment — that's a totally different operation. Mixing them up is the most common compile-time bug in the world.
The state-change pattern in four steps
Every state-change sketch in every language has the same four-step rhythm inside loop():
- Read the current state of the input.
- Compare it to the previous state (the mutable global).
- Act only if the comparison reveals an edge.
- Remember the current state — save it back into the mutable global, ready for next time.
The toggle move
When we want a press to flip the LED, we flip its state variable using the ! operator from L01-17:
ledState = !ledState; // flip: HIGH ↔ LOWIf ledState was LOW, it becomes HIGH. If it was HIGH, it becomes LOW. Then we send the new value to the LED with digitalWrite.
Reuse the L01-17 wiring
Today's hardware is unchanged from L01-17: button at cols 11/14 on D7 with INPUT_PULLUP, onboard LED on pin 13. Two wires only — GND to A11, signal from D7 to H11. Keep it set up.
One more thing: the bounce
Real push buttons don't switch cleanly. When the metal contact closes, it vibrates microscopically for a few milliseconds — one physical press can register as several rapid state changes. That's called bounce, and it's the topic of L01-18 "Debouncing a Button" in detail.
For today, a small delay(20) at the end of loop() hides the bounce: it slows the loop down so the noisy "press → noise → settled" sequence finishes during the delay, and the next read sees only the settled state.
Worked Example 20 min
Goal: build a press-to-toggle LED — each physical press of the button flips the onboard LED on or off, exactly once per press.
Step 1 — review the wiring
The L01-17 INPUT_PULLUP wiring is exactly what you need. Two wires: GND to A11 (black), and D7 to H11 (yellow). No external resistor. If your kit is still wired from L01-17, you're ready.
Step 2 — declare the mutable globals
Open a new sketch and call it press-to-toggle. At the top, declare two pin constants and two state variables:
// Pin assignments — same as L01-17.
const int BUTTON_PIN = 7;
const int LED_PIN = 13;
// Mutable globals — these change as the sketch runs.
int lastButton = HIGH;
int ledState = LOW;Step 3 — write setup()
Nothing new here — both pins get a pinMode as usual:
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}Step 4 — write loop() as the four-step pattern
void loop() {
int currentButton = digitalRead(BUTTON_PIN); // 1. Read
if (currentButton != lastButton) { // 2. Compare
if (currentButton == LOW) { // falling edge = press
ledState = !ledState; // 3. Act — toggle
digitalWrite(LED_PIN, ledState);
}
}
lastButton = currentButton; // 4. Remember
delay(20); // hide the bounce (see L01-18)
}This loop() is the longest you've seen — eleven lines of logic — because state change detection genuinely is a 4-step pattern with a nested condition. The structure is worth memorising; you'll write it many more times in Cluster C and D.
Step 5 — upload and test
- Press the button once. Watch the onboard
LLED. It should turn on. - Press again. It turns off.
- Press a third time. On.
- Now hold the button down for 2 seconds. Notice the LED stays in whatever state the press put it into — the hold doesn't matter, only the original press did.
- That last bullet is the whole point: holding has no effect. Only edges trigger action.
Step 6 — trace through three presses
Fill in the table on paper, one row per loop pass, to convince yourself the pattern works.
| Pass | What you did | currentButton | lastButton (before) | Did "compare" fire? | Did "act" fire? | New ledState |
|---|---|---|---|---|---|---|
| 1 | nothing | HIGH | HIGH | no | no | LOW |
| 2 | (still nothing) | HIGH | HIGH | no | no | LOW |
| 3 | press! | LOW | HIGH | yes | yes | HIGH (flipped) |
| 4 | holding | LOW | ____ | ____ | ____ | ____ |
| 5 | still holding | LOW | ____ | ____ | ____ | ____ |
| 6 | release | HIGH | ____ | ____ | ____ | ____ |
The first three rows are filled. Try rows 4–6 yourself. The point: rows 4 and 5 see the same level but no edge, so nothing happens; row 6 sees a rising edge (release) but our nested if only fires for LOW (press), so the LED stays put.
Try It Yourself 20 min
Goal: Make the LED toggle on release (rising edge) instead of press (falling edge). Most physical buttons in user interfaces actually do this — the action fires when you let go, so you can "cancel" by sliding your finger off before releasing.
Change one word in the inner if:
if (currentButton == HIGH) { // was LOW; HIGH = rising edge = release
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}Questions:
- Press and release the button. Does the LED toggle as your finger goes down or as it goes up? ____
- Now try this: press, hold for 2 seconds, then release. Does the toggle happen at the start of the hold or at the end? ____
- If you hold the button and unplug the Arduino without releasing, did a toggle ever happen? ____
Goal: Count the presses and print the running total to the Serial Monitor. (Serial gets its own deep lesson at L01-23; today is a preview.) Open the Serial Monitor at 9600 baud while the sketch runs.
const int BUTTON_PIN = 7;
const int LED_PIN = 13;
int lastButton = HIGH;
int ledState = LOW;
int pressCount = 0; // new — also mutable global
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
int currentButton = digitalRead(BUTTON_PIN);
if (currentButton != lastButton) {
if (currentButton == LOW) {
pressCount = pressCount + 1;
Serial.println(pressCount);
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
lastButton = currentButton;
delay(20);
}Questions:
- Press the button ten times slowly and steadily. Does the Serial Monitor show
1, 2, 3, …, 10? ____ - Press the button quickly ten times in a row. Do you sometimes see numbers skip — like 1, 2, 4, 6? Why might that happen? ____ (Hint: think about bounce, and the topic of L01-18.)
- If you remove the
delay(20)at the end ofloop(), does the problem get better or worse? ____
Goal: A three-state cycle. Instead of toggling between LOW and HIGH, cycle through three values: 0, 1, 2 — and use that to set three different blink rhythms. State 0 = LED off. State 1 = LED on. State 2 = LED blinking quickly.
You'll need a small bit of arithmetic to advance through 0, 1, 2, then wrap back to 0:
// On each press, advance through 0 → 1 → 2 → 0 → 1 → 2 …
mode = mode + 1;
if (mode == 3) {
mode = 0;
}The rest of loop() needs to act on the current mode — turning the LED off, on, or blinking. For now (no else if yet — that's L01-20), use three separate if blocks:
if (mode == 0) {
digitalWrite(LED_PIN, LOW);
}
if (mode == 1) {
digitalWrite(LED_PIN, HIGH);
}
if (mode == 2) {
// fast blink — toggle the LED every loop pass
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}Questions:
- How many presses does it take to get from mode 0 back to mode 0? ____
- The three
ifblocks all run every loop pass. Couldn't we save a few cycles by checking only one of them? (Hint: this is exactly whatelse ifis for. You'll meet it in L01-20.) ____
Mini-Challenge 15 min
The "two-press unlock"
Some real devices need two presses in a row to activate — a safety feature that stops accidental triggers. Examples: a microwave's "Cancel" → "Cancel" double-press to reset, a car alarm's two-press disarm.
Build it: the onboard LED should light up only after the user has pressed the button twice. After that, pressing again resets it to off (and the next two-press cycle starts).
Your task:
- Add a new mutable global integer,
pressesSoFar, that counts up from 0. - On each falling edge (press), add 1 to
pressesSoFar. - When
pressesSoFarequals 2, turn the LED on. - When
pressesSoFarequals 3, turn the LED off and reset the counter to 0. - Keep the
delay(20)debounce at the end ofloop().
It works if:
- One press: LED stays dark.
- Two presses: LED comes on.
- Three presses: LED goes back off.
- Four presses: same as one press above (cycle restarts).
Reveal one valid sketch
// Two-press unlock — LED lights after 2 presses, resets after 3.
const int BUTTON_PIN = 7;
const int LED_PIN = 13;
int lastButton = HIGH;
int pressesSoFar = 0;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int currentButton = digitalRead(BUTTON_PIN);
if (currentButton != lastButton) {
if (currentButton == LOW) {
pressesSoFar = pressesSoFar + 1;
if (pressesSoFar == 2) {
digitalWrite(LED_PIN, HIGH);
}
if (pressesSoFar == 3) {
digitalWrite(LED_PIN, LOW);
pressesSoFar = 0;
}
}
}
lastButton = currentButton;
delay(20);
}Notice three new if blocks nested inside one another — and the absence of else. We'll learn how else if would compact this into one cleaner block in L01-20 next class.
Recap 5 min
The four-step state-change pattern — read, compare, act on edge, remember — turns a continuously-readable digital pin into a stream of one-shot events. A mutable global (int with no const) holds the previous reading so we can compare on the next loop pass. A minimal if fires the action only when the levels differ in the right direction. From here on, every "press a button to make something happen" sketch uses this pattern.
- Level vs edge
- Level = what the pin reads right now. Edge = the brief instant the pin changed. Buttons-as-events are about edges; buttons-as-status-lights are about levels.
- Falling edge / rising edge
- Falling = HIGH → LOW. Rising = LOW → HIGH. For an
INPUT_PULLUPbutton, a press is a falling edge. - Mutable global
- A plain
intdeclared abovesetup()(noconst). The value survives acrossloop()iterations, making it the only place we can remember anything. - The state-change pattern
- Read → compare to remembered previous → act if changed → remember current. Four steps, in this order, every time.
- if statement
- A keyword that runs a block of code only when a condition is true. Today's introduction is intentionally minimal;
else,else ifand chained conditions come at L01-20. - == / !=
- Comparison operators.
==is true when the two sides are equal.!=is true when they differ. Not to be confused with=, which is assignment.
Homework 5 min
The light-pattern selector. Combine today's state-change trick with the multi-LED hardware from L01-10 to build a project where each press changes the LED pattern:
- Mode 0: all three LEDs off.
- Mode 1: red LED on, others off.
- Mode 2: yellow LED on, others off.
- Mode 3: green LED on, others off.
- Mode 4: all three on at once.
- Press again from Mode 4 to go back to Mode 0 — the cycle restarts.
Hardware: re-plug the three LEDs from L01-10 (red on D9, yellow on D10, green on D11). Add the button from L01-17 (on D7 with INPUT_PULLUP). Use the breadboard's − rail as the shared GND bus, same as L01-15.
Sketch: use the state-change pattern from class to advance a mode variable on each press, and write five tiny if blocks to set the LED states for each mode.
Also: a design reflection on paper.
- Your homework sketch will have around 25 lines in
loop()— fiveifblocks, each setting three LEDs. How many lines could it be if you hadelse iffrom L01-20? ____ - If you wanted a SIXTH mode (say, "red and yellow only"), how many places in your sketch would you need to edit, in order? ____
Bring back next class:
- The saved
.inofile (call itled-pattern-selector). - A short phone video (15–20 seconds) cycling through all five modes by pressing the button.
- Your design-reflection answers on a notebook page.
Heads up for next class: L01-20 "if / else" gives the if we used today its full set of partners — else, else if, and chained conditions. After that lesson, today's nested-if homework will compress beautifully.