speed sensor/Using it/08. A tachometer
Using it · 08 of 10

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.

Polling against an interrupt
The sketch
Pulses
0
Counted
0
Looks
1
A disc gives 200 pulses a second: 600 rpm on a 20-slot disc, not fast. Press Play and watch 60 ms of SIGNAL.

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 rpm

Spin 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.

ir_speed_tachometer.ino
/*
  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.5

When it does not work

It prints 0 rpm with the disc spinning.

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.

The rpm is a steady multiple of what it should be.

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.

It reads a little high when the disc turns very slowly.

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.

Does the MicroPython version keep up?

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.

Where this goes next

The limit is the light sensor, not the sketch.

How fast it counts →

Edit this page — content/books/ir-speed-sensor/a-tachometer.mdx

Community

Questions about this product

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

Ask a question ↗

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.

Browse Modules and blocks on the forum →