A level, not an event
The switch reports where it is, not what just happened. To act once when it is turned on or off, a sketch keeps the last level it believed and compares: a difference is the event.
The same pin, two questions
Both sketches read the same pin as often as each other. The first asks where is the switch? and prints the answer every time, so it says ON over and over for one press. The second asks is it somewhere different from last time? and prints only when the answer is yes.
The switch itself only ever answers the first question. It has a position, and the pin reports the position. There is no such thing as a press on the wire, only a level before and a different level after. An event is something the sketch works out.
Remember, then compare
The change sketch keeps one variable, settled, holding the level it last
believed. Each time round loop() it reads the pin, and if the reading differs
from settled, that is a change: it prints Turned ON or Turned OFF and updates
settled.
It starts by reading the switch in setup(). That first reading is not a
change, because nobody did anything; the switch was already where it is. So the
sketch prints Starts ON or Starts OFF and waits for a difference.
Why there are two variables
A contact does not change cleanly. For a moment after each press it touches
and parts a few times, and a loop running thousands of times a second sees
every one. So the sketch also keeps lastRead, the raw pin, and notes the time
whenever it moves. A new level becomes settled only once it has held for
20 ms. Bounce at each
change shows what
happens without it.
The loop never waits for any of this. It checks the clock, the way blinking
without stopping does on
the XL LED, so anything else in loop() keeps running.
The code
Prints once when the switch is turned on and once when it is turned off, and says where it started. It never waits, so the rest of loop() is free. Change SWITCH_PIN to the pin you wired SIGNAL to.
/*
Latching button - acting on a change TK05 / /p/tk05
Wiring. Count from the square pad on the TinkerBlock board, switch
side up, header at the bottom:
GND -> GND
VCC -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
(never 5V on a 3.3 V board: SIGNAL is VCC when on)
NC -> nothing (unconnected on the board)
SIGNAL -> D2 on an Uno, GPIO 25 on an ESP32, GPIO 4 on an
ESP32-S3, GP15 on a Raspberry Pi Pico
Arduino IDE
Tools > Board your board, e.g. ESP32S3 Dev Module
Tools > Port the one that appears when you plug in
Tools > USB CDC On Boot Enabled (ESP32-S3 only)
No library needed.
*/
// The GPIO number SIGNAL is wired to.
// Uno: 2. ESP32: 25. ESP32-S3: 4. Pico: 15.
const int SWITCH_PIN = 4;
const unsigned long SETTLE_MS = 20; // a new level must hold this long
int settled; // the level the sketch believes
int lastRead; // the pin at the last read, bounces and all
unsigned long changedAt = 0; // when lastRead last changed
void setup() {
Serial.begin(115200);
pinMode(SWITCH_PIN, INPUT); // the board's pull-down holds it LOW
settled = digitalRead(SWITCH_PIN);
lastRead = settled;
Serial.println(settled == HIGH ? "Starts ON" : "Starts OFF");
}
void loop() {
int now = digitalRead(SWITCH_PIN);
if (now != lastRead) { // it moved, or it bounced: restart clock
lastRead = now;
changedAt = millis();
}
// A change is a new level that has held for SETTLE_MS.
if (now != settled && millis() - changedAt >= SETTLE_MS) {
settled = now;
Serial.println(settled == HIGH ? "Turned ON" : "Turned OFF");
}
// Anything else goes here. Nothing above waits.
}Two remembered levels: lastRead is the raw pin, which bounces; settled is the level the sketch believes. A change counts only once the pin has held a new level for SETTLE_MS. Bounce at each change, later in this book, is why.
The same in MicroPython: ticks_ms is the clock and ticks_diff subtracts two readings of it, so the loop never sleeps and reports each change once.
"""
Latching button - acting on a change, MicroPython TK05 / /p/tk05
Wiring. Count from the square pad on the TinkerBlock board, switch
side up, header at the bottom:
GND -> GND
VCC -> 3V3 (never 5V: SIGNAL is VCC when the switch is on,
and these are 3.3 V pins)
NC -> nothing (unconnected on the board)
SIGNAL -> GPIO 25 on an ESP32, GPIO 4 on an ESP32-S3,
GP15 on a Raspberry Pi Pico
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine and time are built in.
"""
from machine import Pin
import time
# The GPIO number SIGNAL is wired to. ESP32: 25. ESP32-S3: 4. Pico: 15.
SWITCH_PIN = 4
SETTLE_MS = 20 # a new level must hold this long
switch = Pin(SWITCH_PIN, Pin.IN) # the board's pull-down holds it LOW
settled = switch.value() # the level the sketch believes
last_read = settled # the pin at the last read
changed_at = time.ticks_ms() # when last_read last changed
print("Starts ON" if settled else "Starts OFF")
while True:
now = switch.value()
if now != last_read: # it moved, or it bounced
last_read = now
changed_at = time.ticks_ms()
# A change is a new level that has held for SETTLE_MS.
held = time.ticks_diff(time.ticks_ms(), changed_at)
if now != settled and held >= SETTLE_MS:
settled = now
print("Turned ON" if settled else "Turned OFF")
# Anything else goes here. Nothing above waits.Use ticks_diff, never a plain subtraction: ticks_ms wraps round much sooner than Arduino's millis, and ticks_diff is right across the wrap. last_read bounces; settled is the level the sketch believes.
When it does not work
Something in the sketch believes the level before it has settled. The contact bounces for a moment at every change, and a loop that compares every raw read sees each bounce as a change. Keep the settle time: a new level counts only after it has held for 20 ms.
No, and the sketch does not treat it as one. It reads the switch once in setup() and believes that, so the first thing it prints is the position it found. Only a later difference from that is a change. That is deliberate: the switch was already on, nobody turned it on.
You could attach one to the pin's change, and it would fire on every bounce too, so it still needs the settle time, now inside an interrupt where timing is awkward. A switch pressed by hand changes a few times a minute at most. Reading it every time round loop() is plenty.
It holds a millis() reading, and millis() outgrows an int in about half a minute on an Uno. The subtraction millis() - changedAt stays right even when millis() wraps round after about 49 days. The XL LED book's blinking-without-stopping page covers the same rule.
The position is still there after the board restarts. A sketch can use that.
Surviving a reset →Edit this page — content/books/latching-button/a-level-not-an-event.mdx
Questions about this product
See what other owners have asked, and read their solutions.
Latching Button
Loading discussions…
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.