Counting pulses
A water meter, an anemometer and a wheel encoder are the same problem at three speeds, and the right answer changes twice on the way up. Getting it wrong reads as a calibration error, not as lost data.
Where each method gives up
Choosing, in one line each
- Polling in
loop()— below about 500 Hz, and only if the loop has nothing slow in it. OneSerial.printlncuts that ceiling by half. attachInterrupt— from there up to about 100 kHz. Simple, portable, works in MicroPython.- PCNT — above that, or when the CPU has other work. Hardware counting, hardware glitch filtering, zero CPU.
Turning counts into units
A flow meter's datasheet gives you pulses per litre — 450 is common, which is
7.5 pulses per second per litre per minute. An anemometer gives you a wind speed
per hertz. Do that arithmetic in loop() with a float; the counting side stays
integer and stays fast.
The code
The PCNT peripheral counts edges in hardware with a built-in glitch filter, which is the debounce you would otherwise write by hand. Eight units exist on the classic ESP32.
#include "driver/pulse_cnt.h"
pcnt_unit_handle_t unit = nullptr;
void setup() {
Serial.begin(115200);
pcnt_unit_config_t uc = { .low_limit = -32768, .high_limit = 32767 };
pcnt_new_unit(&uc, &unit);
pcnt_glitch_filter_config_t f = { .max_glitch_ns = 1000 };
pcnt_unit_set_glitch_filter(unit, &f);
pcnt_chan_config_t cc = { .edge_gpio_num = 4, .level_gpio_num = -1 };
pcnt_channel_handle_t ch;
pcnt_new_channel(unit, &cc, &ch);
pcnt_channel_set_edge_action(ch, PCNT_CHANNEL_EDGE_ACTION_INCREASE,
PCNT_CHANNEL_EDGE_ACTION_HOLD);
pcnt_unit_enable(unit);
pcnt_unit_start(unit);
}
void loop() {
int count = 0;
pcnt_unit_get_count(unit, &count);
Serial.printf("%d pulses\n", count);
delay(1000);
}The glitch filter is in APB clock cycles, and it is the reason this does not need software debouncing. 1000 cycles at 80 MHz is 12.5 µs - long enough to kill contact noise, short enough for a fast encoder.
MicroPython does not expose PCNT, so an interrupt is the practical route. It is good for a few tens of thousands of pulses a second, which covers flow meters and wind sensors comfortably.
from machine import Pin
import time
pulses = 0
def on_pulse(pin):
global pulses
pulses += 1
Pin(4, Pin.IN, Pin.PULL_UP).irq(trigger=Pin.IRQ_FALLING, handler=on_pulse)
while True:
time.sleep(1)
n, pulses = pulses, 0 # read and reset atomically
print(n, 'pulses/s -> {:.2f} L/min'.format(n / 7.5))Reading and zeroing the counter in one step matters. Doing it in two lines loses any pulse that arrives between them, which is invisible until you compare totals over a week.
When it does not work
You are polling and missing pulses between reads. A loop with a print in it runs a couple of thousand times a second at best - anything faster than that is being dropped.
A floating input picking up noise. Use INPUT_PULLUP, and add a 100 nF capacitor to ground if the sensor is on a long cable.
The interrupt is eating the CPU. Above roughly 100 kHz an ISR per edge is most of the chip. Move to PCNT.
You attached to CHANGE rather than a single edge, or you are reading a quadrature encoder as if it were a single channel. Both are correct behaviour - adjust the divisor.
The last piece of timing hardware, and the one that exists because bit-banging a 400 ns pulse from a loop does not work.
Addressable LEDs with RMT →Edit this page — content/esp32/counting-pulses.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.