Skip to content

esp32_pcnt

PCNT: the ESP32’s pulse counter — eight units, each a signed 16-bit counter fed by an edge signal and steered by a control signal.

What it is for: reading a quadrature encoder as a signed position with direction, or counting a flow meter / tachometer. An edge on the signal input steps the counter; the control input’s level decides whether that step is up or down. A per-unit glitch filter rejects pulses shorter than a configured number of APB clock cycles, so contact bounce and line noise do not show up as phantom counts.

The one non-obvious thing: mode is a matrix, not a direction

Section titled “The one non-obvious thing: mode is a matrix, not a direction”

There is no “count up” bit. Each channel has an action for the positive edge and one for the negative edge (hold / increase / decrease), and then the control level can modify that action: keep it, invert it, or hold. So “up when the control pin is high, down when it is low” is expressed as pos = Increase, control-high = Keep, control-low = Inverse. This is exactly esp-idf’s model (pcnt_ll_set_edge_action / pcnt_ll_set_level_action), and getting it wrong reads as a counter that moves the right amount in the wrong direction.

DR_REG_PCNT_BASE = 0x3FF57000 (esp-idf soc/soc.h). Layout from soc/pcnt_struct.h; the field placement in conf0 is the part a wrong map silently corrupts, so it is pinned by the tests below. Each unit owns three config words (conf0/1/2, 12 bytes) at base + unit*0x0C; the counters are a separate block of eight words at base + 0x60; the global control / reset / pause register is at base + 0xB0.

The peripheral is clock-gated off out of reset (DPORT_PCNT_CLK_EN), like every other ESP32 peripheral — [PcntUnit::new] enables it first, or every register below reads back zero with no fault.

struct ChannelMode {
pub pos: EdgeAction,
pub neg: EdgeAction,
pub high: LevelAction,
pub low: LevelAction,
}

One channel’s behaviour: what each edge does, and how the control level bends it. A unit has two channels; a single-signal encoder uses channel 0.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

References: EdgeAction, LevelAction

struct PcntUnit { /* private fields */ }

One configured PCNT unit.

enum EdgeAction {
Hold,
Increase,
Decrease,
}

What a counter does on an edge of the signal input.

Values are the hardware encoding (pcnt_channel_edge_action_t): the field documents 2'd1 as increase and 2'd2 as decrease, everything else a no-op.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

enum Filter {
Off,
Cycles(u16),
}

The glitch filter setting: off, or on with a width in APB clock cycles.

A pulse narrower than thres cycles is discarded before it can reach the counter. thres is clamped to [MAX_FILTER_THRES].

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

enum LevelAction {
Keep,
Inverse,
Hold,
}

How the control input’s level modifies the edge action.

Values are the hardware encoding (pcnt_channel_level_action_t).

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

const MAX_FILTER_THRES: u16;

The widest glitch-filter threshold the hardware accepts (filter_thres is ten bits; esp-idf’s PCNT_LL_MAX_GLITCH_WIDTH).

const NUM_UNITS: u8;

Number of PCNT units on this chip.