Counting with an interrupt
attachInterrupt makes the chip run a short function the instant CLOCK falls, whatever loop() is doing. The function reads DATA and moves the count; loop() only prints it. The same sketch with a 50 ms delay() in loop() now counts every click, where the polling version in the figure counts none.
Let CLOCK interrupt
An interrupt is the chip dropping what it is doing to run a short
function, called a handler, the moment something happens on a pin, then going
back to exactly where it was. attachInterrupt sets one up:
attachInterrupt(digitalPinToInterrupt(CLOCK_PIN), onClockFall, FALLING);FALLING is the event: CLOCK going from HIGH to LOW. onClockFall is the
handler. It does what the first sketch did on that pass of loop(), read
DATA and move the count, except that it does it at the right instant rather
than whenever loop() next gets round to looking.
Run it. loop() reads, prints and then sleeps in delay(50), and every
time it looks, CLOCK happens to be HIGH, so the polling count stays at zero.
The handler runs at each fall regardless, in the middle of the delay(), and
counts all of them.
What changes in the sketch
Three things, and each has a reason.
volatile long count. The count now changes outsideloop()'s view.volatiletells the compiler to read it from memory every time rather than trusting a copy.noInterrupts()round the copy. On an Uno alongis four bytes, read one at a time, and a handler firing in the middle would handloop()half of each. Switching interrupts off for one line prevents it.- Printing moved to
loop(). A handler should be over in a few microseconds. Printing waits for the serial port, and a handler that waits holds everything else up.
The ESP32 cores also want the handler marked IRAM_ATTR, which keeps it in
RAM so it can run while the flash is busy. The sketch defines it as nothing
on the boards that do not have it. Interrupts on a
pin covers the ESP32 side in detail.
Pins that can interrupt
Every GPIO on the ESP32, the ESP32-S3 and the Pico can raise an interrupt. On
an Uno, attachInterrupt works on only two pins, D2 and D3, and that is why
CLOCK and DATA are on them. The next sketch interrupts on both.
The code
The first count, moved into an interrupt handler. onClockFall runs the instant CLOCK falls, reads DATA and moves the count. loop() copies the count, prints it if it changed, and then deliberately wastes 50 ms.
/*
Rotary Encoder - counted by an interrupt TK06 / /p/tk06
Wiring. Count from the square pad on the TinkerBlock board, knob
up, header at the bottom:
GND -> GND
VCC -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
(CLOCK, DATA and BTN all reach whatever VCC is)
NC -> nothing (unconnected on the board)
BTN -> D4 on an Uno, GPIO 27 on an ESP32, GPIO 6 on an
ESP32-S3, GP15 on a Raspberry Pi Pico
CLOCK -> D2, GPIO 25, GPIO 4, GP13 (the same four boards)
DATA -> D3, GPIO 26, GPIO 5, GP14
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.
*/
// CLOCK, DATA and BTN, as GPIO numbers.
// Uno: 2, 3, 4. ESP32: 25, 26, 27. ESP32-S3: 4, 5, 6. Pico: 13, 14, 15.
const int CLOCK_PIN = 4;
const int DATA_PIN = 5;
const int BUTTON_PIN = 6; // not used in this sketch
#ifndef IRAM_ATTR
#define IRAM_ATTR // only the ESP32 cores need it
#endif
volatile long count = 0; // changed inside the handler
// Runs the instant CLOCK falls. Short, and no printing.
void IRAM_ATTR onClockFall() {
if (digitalRead(DATA_PIN) == HIGH) {
count++; // CLOCK fell first
} else {
count--; // DATA fell first
}
}
void setup() {
Serial.begin(115200);
pinMode(CLOCK_PIN, INPUT); // the block has its own pull-ups
pinMode(DATA_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(CLOCK_PIN), onClockFall,
FALLING);
}
void loop() {
static long shown = 0;
noInterrupts(); // copy it in one piece
long now = count;
interrupts();
if (now != shown) {
Serial.println(now);
shown = now;
}
delay(50); // busy elsewhere: the count survives
}IRAM_ATTR keeps the handler 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(50) is there to prove the point: the count survives it.
The same idea in MicroPython. clock.irq runs on_clock_fall when CLOCK falls; it reads DATA and moves the count. The main loop prints the count when it changes and sleeps 50 ms in between.
"""
Rotary Encoder - counted by an interrupt, MicroPython TK06 / /p/tk06
Wiring. Count from the square pad on the TinkerBlock board, knob
up, header at the bottom:
GND -> GND
VCC -> 3V3 (never 5V: CLOCK, DATA and BTN all reach VCC)
NC -> nothing (unconnected on the board)
BTN -> GPIO 27 on an ESP32, GPIO 6 on an ESP32-S3,
GP15 on a Raspberry Pi Pico
CLOCK -> GPIO 25, GPIO 4, GP13 (the same three boards)
DATA -> GPIO 26, GPIO 5, GP14
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
# CLOCK, DATA and BTN, as GPIO numbers.
# ESP32: 25, 26, 27. ESP32-S3: 4, 5, 6. Pico: 13, 14, 15.
CLOCK_PIN = 4
DATA_PIN = 5
clock = Pin(CLOCK_PIN, Pin.IN) # no pull: the block has its own
data = Pin(DATA_PIN, Pin.IN)
count = 0
def on_clock_fall(pin): # short, and no printing
global count
if data.value() == 1:
count += 1 # CLOCK fell first
else:
count -= 1 # DATA fell first
clock.irq(trigger=Pin.IRQ_FALLING, handler=on_clock_fall)
shown = 0
while True:
if count != shown:
shown = count
print(shown)
time.sleep_ms(50) # busy elsewhere: it still countsMicroPython's pin handlers on these ports are usually scheduled to run just after the edge rather than at it. For a hand-turned knob that is soon enough; for a fast one, the state table in the next article copes better with a late read. 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 CLOCK is on D2 in this book. On any other pin digitalPinToInterrupt returns -1 and the handler is never attached.
Because it changes behind loop()'s back. Without volatile the compiler may keep a copy of count in a register and never look at memory again, so loop() prints the same number for ever while the handler counts away.
On an Uno a long is four bytes and the chip reads it one byte at a time. If the handler fires half-way through, loop() gets two bytes of the old count and two of the new. noInterrupts() for the length of one copy prevents that. On the 32-bit boards the copy is a single read anyway, and the two calls do no harm.
Do not. A handler 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. Set the count in the handler and print it from loop(), as this sketch does.
Edit this page — content/books/rotary-encoder/counting-with-an-interrupt.mdx
Questions about this product
See what other owners have asked, and read their solutions.
Rotary Encoder
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.