ESP32/Interrupts and timing/20. Counting pulses
Your chip
Your language
Interrupts and timing · 20 of 81

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.

/esp32/counting-pulses · arduino · S3

Where each method gives up

500 Hz into a polling loop
100% counted
Pulse rate500 Hz
Counted
100%
CPU spent counting
2%
Reading is
correct
Every pulse counted, cheaply. A polling loop is fine here, and it stays fine right up until you add a print or a slow sensor to the same loop.

Choosing, in one line each

  • Polling in loop() — below about 500 Hz, and only if the loop has nothing slow in it. One Serial.println cuts 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.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

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.

pcnt_counter.ino
#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.

When it does not work

The flow meter reads consistently low

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.

The count creeps up with nothing connected

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.

Wi-Fi becomes unreliable at high pulse rates

The interrupt is eating the CPU. Above roughly 100 kHz an ISR per edge is most of the chip. Move to PCNT.

An encoder counts twice as fast as expected

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.

Where this goes next

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.

Browse ESP32 on the forum