rotary encoder/Counting every click/08. Counting with an interrupt
Counting every click · 08 of 10

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.

Counting while loop() is busy
Cycles turned
0
Polled count
0
Interrupt count
0
loop() reads CLOCK once, prints, and sleeps for 50 ms. The encoder does not wait for it. Run it.

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 outside loop()'s view. volatile tells the compiler to read it from memory every time rather than trusting a copy.
  • noInterrupts() round the copy. On an Uno a long is four bytes, read one at a time, and a handler firing in the middle would hand loop() 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_interrupt.ino
/*
  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.

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 CLOCK is on D2 in this book. On any other pin digitalPinToInterrupt returns -1 and the handler is never attached.

Why is count volatile?

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.

Why switch interrupts off to copy the count?

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.

Can I print from inside the handler?

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.

Where this goes next

Score every change of either pin, and let bounce cancel itself.

A state table

Edit this page — content/books/rotary-encoder/counting-with-an-interrupt.mdx

Community

Questions about this product

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

Ask a question ↗

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.

Browse Modules and blocks on the forum