A tachometer
A tachometer that counts every bar on an interrupt and prints revolutions a minute once a second. The handler does one thing, add one, and ignores a second edge within 500 µs, which keeps a wobbling edge from counting twice. The arithmetic happens in loop(), with the count copied while the handler is paused.
Looking is not counting
The first read looks at SIGNAL every 10 ms. That is a sample, and pulses that begin and end between two samples are never seen.
Press Play with each sketch. At 200 pulses a second, 600 rpm on a 20-slot disc, each pulse lasts 5 ms, so a look every 10 ms lands at the same point of every second pulse and sees the same level each time. It counts nothing, and a slightly different speed gives a different wrong answer. The trace is drawn to show the effect, not measured.
An interrupt turns that round. The pin itself watches for a rising edge and
stops the processor to run a short function the moment one arrives, whatever
loop() is doing. Nothing is missed while the loop waits or prints.
How the sketch counts
onEdge is the whole handler: read micros(), and if at least LOCKOUT_US
has passed since the last counted edge, add one to pulses. Both variables
are volatile, because the handler changes them behind loop()'s back.
The lockout is there because the comparator has no hysteresis: an edge that wobbles at the deciding point can flip SIGNAL twice in a few microseconds. 500 µs hides that, and it is shorter than the gap between bars at any speed the block can honestly follow.
Once a second, loop() turns interrupts off, copies pulses, sets it back
to 0 and turns them on again. On an Uno a count is four bytes and the
processor reads one at a time, so without the pause an edge landing mid-copy
could leave it half old and half new. Then it divides by the time that
really passed and works out rpm with the slots and
rpm arithmetic.
What you should see
At 115200, once a second:
0 pulses 0 rpm
63 pulses 189 rpm
64 pulses 192 rpmSpin a 20-slot disc through the slot by hand and the count rises and falls with it. On a motor, the reading settles and steps by 3 rpm, the resolution of a one-second count on 20 openings.
The code
attachInterrupt calls onEdge on every rising edge, each bar entering the beam. onEdge adds one unless the last edge was under LOCKOUT_US ago. Once a second loop() takes the count, sets it back to 0 and prints pulses and rpm.
/*
Infrared Speed Sensor - tachometer TK61 / /p/tk61
Wiring, the same as the first read. Count from the square pad on
the TinkerBlock board, parts up, header at the bottom:
GND -> GND
VCC -> 3V3 on an ESP32, ESP32-S3 or Pico; 5V on an Uno
NC -> nothing (unconnected on the board)
SIGNAL -> GPIO 4 on an ESP32-S3, GPIO 25 on an ESP32,
D2 on an Uno, GP15 on a Raspberry Pi Pico
Mounting: a slotted disc on the shaft, its rim through the slot
so each bar breaks the beam once as it passes.
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 pin SIGNAL is wired to.
// Uno: 2. ESP32: 25. ESP32-S3: 4. Pico: 15.
const int SENSOR_PIN = 4;
// Openings in your disc: one pulse each, every turn.
const int SLOTS = 20;
// Count for this long, then print.
const unsigned long WINDOW_MS = 1000;
// Ignore a second edge this soon after the last one.
const unsigned long LOCKOUT_US = 500;
// ESP32 cores want interrupt handlers in IRAM; others do not care.
#ifndef IRAM_ATTR
#define IRAM_ATTR
#endif
volatile unsigned long pulses = 0;
volatile unsigned long lastEdgeUs = 0;
// Runs on every rising edge: a bar has just entered the beam.
void IRAM_ATTR onEdge() {
unsigned long at = micros();
if (at - lastEdgeUs >= LOCKOUT_US) {
pulses++;
lastEdgeUs = at;
}
}
unsigned long windowStart;
void setup() {
Serial.begin(115200);
pinMode(SENSOR_PIN, INPUT); // the board drives SIGNAL itself
attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onEdge, RISING);
windowStart = millis();
}
void loop() {
if (millis() - windowStart < WINDOW_MS) return;
// Take the count and start the next one, with the handler paused
// so it cannot change pulses half way through the copy.
noInterrupts();
unsigned long n = pulses;
pulses = 0;
interrupts();
unsigned long elapsed = millis() - windowStart;
windowStart += elapsed;
float perSecond = n * 1000.0 / elapsed;
float rpm = perSecond * 60.0 / SLOTS;
Serial.print(n);
Serial.print(" pulses ");
Serial.print(rpm, 0);
Serial.println(" rpm");
}Set SLOTS to the openings on your disc. IRAM_ATTR keeps the handler in fast memory on the ESP32 family and means nothing elsewhere. The sketch compiles for an ESP32-S3 and an Uno; on an Uno, only D2 and D3 take an interrupt.
View on GitHub · blocks/tk61-ir-speed-sensor/arduino/ir_speed_tachometer/ir_speed_tachometer.ino @ v1.5The same in MicroPython: Pin.irq calls on_edge on every rising edge, and the main loop takes the count once a second with interrupts briefly off.
"""
Infrared Speed Sensor - tachometer, MicroPython TK61 / /p/tk61
Wiring, the same as the first read:
GND -> GND
VCC -> 3V3 (the block pulls SIGNAL up to VCC with 10k)
NC -> nothing (unconnected on the board)
SIGNAL -> GPIO 25 on an ESP32, GPIO 4 on an ESP32-S3,
GP15 on a Raspberry Pi Pico
Mounting: a slotted disc on the shaft, its rim through the slot
so each bar breaks the beam once as it passes.
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Stop it with Ctrl-C.
"""
from machine import Pin, disable_irq, enable_irq
import time
# The GPIO number SIGNAL is wired to. ESP32: 25. ESP32-S3: 4. Pico: 15.
SENSOR_PIN = 4
SLOTS = 20 # openings in your disc: one pulse each, every turn
WINDOW_MS = 1000 # count for this long, then print
LOCKOUT_US = 500 # ignore a second edge this soon after the last
pulses = 0
last_edge = time.ticks_us()
def on_edge(pin):
# Runs on every rising edge: a bar has just entered the beam.
global pulses, last_edge
at = time.ticks_us()
if time.ticks_diff(at, last_edge) >= LOCKOUT_US:
pulses += 1
last_edge = at
sensor = Pin(SENSOR_PIN, Pin.IN) # the board drives SIGNAL itself
sensor.irq(trigger=Pin.IRQ_RISING, handler=on_edge)
start = time.ticks_ms()
while True:
time.sleep_ms(20)
elapsed = time.ticks_diff(time.ticks_ms(), start)
if elapsed < WINDOW_MS:
continue
state = disable_irq() # take the count in one piece
n = pulses
pulses = 0
enable_irq(state)
start = time.ticks_add(start, elapsed)
rpm = n * 1000 / elapsed * 60 / SLOTS
print(n, "pulses ", round(rpm), "rpm")Set SLOTS to your disc. ticks_us and ticks_diff handle the microsecond counter wrapping round. Stop it with Ctrl-C.
View on GitHub · blocks/tk61-ir-speed-sensor/micropython/ir_speed_tachometer.py @ v1.5When it does not work
Watch the red LED first: if it is not flickering, the disc is not reaching the beam. If it flickers and the count stays at 0, SIGNAL is not on SENSOR_PIN, or on an Uno it is on a pin with no interrupt: use D2 or D3.
SLOTS does not match the disc. Count the openings on yours and set SLOTS to that. A reading exactly twice too high can also mean the sketch was changed to count CHANGE instead of RISING.
A slow edge can flip the comparator several times over more than 500 µs, and the lockout only hides the first half millisecond. Raise LOCKOUT_US for a slow shaft; it must stay under the time between two bars at your top speed.
At the speeds a hobby motor reaches, usually yes. Its handler runs slower than the C one, but a few thousand edges a second is within reach on an ESP32 or a Pico. Counts that fall at high speed while the Arduino sketch holds steady are the hint.
Edit this page — content/books/ir-speed-sensor/a-tachometer.mdx
Questions about this product
See what other owners have asked, and read their solutions.
Infrared Speed 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.