knock sensor/Reading it/08. Catching it with an interrupt
Reading it · 08 of 10

Catching it with an interrupt

A loop with a delay(100) in it reads the pin ten times a second, and a knock keeps SIGNAL HIGH for only a few milliseconds in all, so most knocks fall between two reads. attachInterrupt with RISING makes the chip run a short function at the start of every pulse instead, whatever loop() is doing. The hold-off moves into that function.

Looking now and then

A loop that reads the pin with digitalRead sees what SIGNAL is at that instant and nothing in between. The first two sketches got away with it because their loops did nothing else and went round every few microseconds. Put a delay(100) in, or a display update, or a network call, and the pin is read once every hundred milliseconds or so.

Polling against an interrupt
How the sketch reads
One pass of loop()100 ms
Knocks
5
Caught
0
HIGH per knock
about 3.5 ms
At 100 ms a pass, the pin is read 10 times a second and is HIGH for only about 3.5 ms per knock, so a knock can come and go between two reads: 0 of 5 caught. A delay(100) in loop() is how the old sketches missed knocks.

Five knocks in a second, on an ESP32-S3. Each keeps SIGNAL HIGH for only a few milliseconds in all, across its separate pulses. Slide the loop's time up from nothing: at a couple of milliseconds a pass every knock is still caught, and by 50 ms none is. At 100 ms a pass, which is what the old sketch for this block did, catching a knock is luck, about one in thirty. Switch to An interrupt and all five count, whatever the loop is doing.

Being told instead

An interrupt is the chip watching the pin in hardware. When SIGNAL goes from LOW to HIGH, it stops whatever it was doing, runs a short function of yours, and goes back to exactly where it was. attachInterrupt sets one up:

attachInterrupt(digitalPinToInterrupt(KNOCK_PIN), onKnock, RISING);

RISING is the change from LOW to HIGH, which on this board is the start of a pulse. It fires on every pulse of the burst, so the hold-off goes inside the function: count the knock if the last counted one is at least 100 ms old, otherwise do nothing. That is the last article's rule, word for word, and it runs a few microseconds after the pulse begins rather than whenever loop() next looks.

Three rules for the function

Keep it short. Compare, count, return. No printing and no waiting: while the function runs, other interrupts can be held off.

Mark shared variables volatile. knocks changes where loop() cannot see it happen, and volatile tells the compiler to read it from memory every time.

Copy multi-byte values with interrupts off. On an Uno an unsigned long is four bytes, read one at a time. noInterrupts() and interrupts() around the copy stop a knock landing half-way through it.

The ESP32 cores also want the function marked IRAM_ATTR, which keeps it in fast internal memory; the #ifndef lines make that word vanish on the Uno and the Pico, so one sketch builds on all four. On an Uno, attachInterrupt works on only two pins, D2 and D3, and that is why SIGNAL is on D2.

Knock a few times while it runs. The count goes up by one per knock, even though loop() spends almost all its time asleep in delay(100).

The code

The hold-off from the last article, moved into an interrupt. onKnock runs the instant SIGNAL goes from LOW to HIGH; it counts the knock if the last one is HOLD_OFF_MS old. loop() copies the count, prints it if it changed, and then deliberately wastes 100 ms.

knock_interrupt.ino
/*
  Knock Sensor - counted by an interrupt                 TK28 / /p/tk28

  Wiring. Count from the square pad on the TinkerBlock board, switch
  at the top, header at the bottom:

    GND    -> GND
    VCC    -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
              (during a knock, SIGNAL gives your pin whatever VCC is)
    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 KNOCK_PIN = 4;
const unsigned long HOLD_OFF_MS = 100;  // longer than the spring rings

#ifndef IRAM_ATTR
#define IRAM_ATTR               // only the ESP32 cores need it
#endif

volatile unsigned long knocks = 0;      // changed inside onKnock
volatile unsigned long lastKnock = 0;

// Runs the instant SIGNAL goes HIGH. Short, and no printing.
void IRAM_ATTR onKnock() {
  unsigned long now = millis();
  if (now - lastKnock >= HOLD_OFF_MS) {  // not the same knock
    knocks++;
    lastKnock = now;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(KNOCK_PIN, INPUT);    // the block has its own pull-down
  attachInterrupt(digitalPinToInterrupt(KNOCK_PIN), onKnock, RISING);
}

void loop() {
  static unsigned long shown = 0;

  noInterrupts();               // copy it in one piece
  unsigned long n = knocks;
  interrupts();

  if (n != shown) {
    shown = n;
    Serial.print("knocks: ");
    Serial.println(n);
  }
  delay(100);                   // busy elsewhere: the count survives
}

IRAM_ATTR keeps the function in RAM on the ESP32 cores; the three lines at the top define it as nothing where it does not exist, so one sketch builds on all four boards. The delay(100) is there to prove the point: the count survives it.

When it does not work

attachInterrupt does nothing on my Uno.

On an Uno only D2 and D3 can raise an external interrupt, which is why SIGNAL is on D2 in this book. On any other pin digitalPinToInterrupt returns -1 and the function is never attached.

It counts nothing, or counts when I let go.

Check the last argument is RISING. A knock is a change from LOW to HIGH on this board, because it has a pull-down. FALLING, copied from a sketch for an active-low sensor, fires at the end of each pulse instead, and CHANGE fires at both ends.

Why is knocks volatile?

Because it changes behind loop()'s back. Without volatile the compiler may keep a copy of knocks in a register and never look at memory again, so loop() prints the same number for ever while the interrupt counts away.

Can I print from inside onKnock?

Do not. An interrupt function should be a few lines that finish at once: printing waits for the serial port, and while it waits, other interrupts can be held off. Count in the function and print from loop(), as this sketch does.

Where this goes next

A lock that opens to a rhythm, whatever the speed.

A secret knock

Edit this page — content/books/knock-sensor/catching-it-with-an-interrupt.mdx

Community

Questions about this product

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

Ask a question ↗

Knock Sensor

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