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.
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 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.
The same in MicroPython. knock.irq runs on_knock when SIGNAL rises; it counts the knock if the last one is HOLD_OFF_MS old. The main loop prints the count when it changes and sleeps 100 ms in between.
"""
Knock Sensor - counted by an interrupt, MicroPython TK28 / /p/tk28
Wiring. Count from the square pad on the TinkerBlock board, switch
at the top, header at the bottom:
GND -> GND
VCC -> 3V3 (never 5V: during a knock, SIGNAL gives your pin VCC)
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.
KNOCK_PIN = 4
HOLD_OFF_MS = 100 # longer than the spring rings
knock = Pin(KNOCK_PIN, Pin.IN) # no pull: the block has its own
knocks = 0
last_knock = time.ticks_add(time.ticks_ms(), -HOLD_OFF_MS)
def on_knock(pin): # short, and no printing
global knocks, last_knock
now = time.ticks_ms()
if time.ticks_diff(now, last_knock) >= HOLD_OFF_MS:
knocks += 1 # not the same knock
last_knock = now
knock.irq(trigger=Pin.IRQ_RISING, handler=on_knock)
shown = 0
while True:
if knocks != shown:
shown = knocks
print("knocks:", shown)
time.sleep_ms(100) # busy elsewhere: it still countsMicroPython's pin handlers on these ports are usually scheduled to run just after the edge rather than at it, so ticks_ms() may read a millisecond or so late. Against a 100 ms hold-off that does not matter. Stop it with Ctrl-C.
When it does not work
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.
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.
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.
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.
Edit this page — content/books/knock-sensor/catching-it-with-an-interrupt.mdx
Questions about this product
See what other owners have asked, and read their solutions.
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.