latching button/Reading it/06. A level, not an event
Reading it · 06 of 9

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

A level, not an event
Real changes
0
Level sketch printed
1
Change sketch printed
0
The switch will be turned on at 1.5 s and off at 4.5 s. Watch what each sketch has to say about it.

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_changes.ino
/*
  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.

When it does not work

It prints Turned ON twice for one press.

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.

It says Starts ON when I plug it in. Is that a change?

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.

Could I use an interrupt instead?

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.

Why unsigned long for changedAt?

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.

Where this goes next

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

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

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.

Browse Modules and blocks on the forum