Skip to content

esp32/esp32_pcnt: Add PCNT class and Counter/Encoder shims in machine #7582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/esp32/quickref.rst
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,45 @@ supported ADC resolutions:
- ``ADC.WIDTH_12BIT`` = 12


Pulse Counter (pin pulse/edge counting)
---------------------------------------

The ESP32 provides up to 8 pulse counter peripherals depending on the hardware,
with id 0..7. These can be configured to count rising and/or falling edges on
any input pin.

Use the :ref:`esp32.PCNT <esp32.PCNT>` class::

from machine import Pin
from esp32 import PCNT

counter = PCNT(0, pin=Pin(2), rising=PCNT.INCREMENT) # create counter
counter.start() # start counter
count = counter.value() # read count, -32768..32767
counter.value(0) # reset counter
count = counter.value(0) # read and reset

The PCNT hardware supports monitoring multiple pins in a single unit to
implement quadrature decoding or up/down signal counters.

See the :ref:`machine.Counter <machine.Counter>` and
:ref:`machine.Encoder <machine.Encoder>` classes for simpler abstractions of
common pulse counting applications. These classes are implemented as thin Python
shims around the ``PCNT()`` class::

from machine import Pin, Counter

counter = Counter(0, Pin(2)) # create a counter as above and start it
count = counter.value() # read the count as an arbitrary precision signed integer

encoder = Encoder(0, Pin(12), Pin(14)) # create an encoder and begin counting
count = encoder.value() # read the count as an arbitrary precision signed integer

Note that the id of these ``Counter()`` and ``Encoder()`` objects is an
arbitrary number, each uniquely identified object will be allocated a free PCNT
unit.


Software SPI bus
----------------

Expand Down
123 changes: 123 additions & 0 deletions docs/library/esp32.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,129 @@ Constants

Used in `idf_heap_info`.


.. _esp32.PCNT:

PCNT
----

This class provides access to the ESP32 hardware support for pulse counting.
There are 8 pulse counter units, with id 0..7.

.. class:: PCNT([id], *, ...)

Create a new PCNT object with the given unit ``id`` or return the existing
object if it has already been created. If the ``id`` positional argument is
not supplied then a new object is created for the first unallocated PCNT
unit. Keyword arguments are passed to the ``init()`` method as described
below.

.. method:: PCNT.init(*, ...)

(Re-)initialise a pulse counter unit. Supported keyword arguments are:

- ``pin``: the input Pin to monitor for pulses
- ``rising``: an action to take on a rising edge - one of
``PCNT.INCREMENT``, ``PCNT.DECREMENT`` or ``PCNT.IGNORE`` (the default)
- ``falling``: an action to take on a falling edge
- ``mode_pin``: ESP32 pulse counters support monitoring a second pin and
altering the behaviour of the counter based on its level - set this
keyword to any input Pin
- ``mode_low``: set to either ``PCNT.HOLD`` or ``PCNT.REVERSE`` to
either suspend counting or reverse the direction of the counter (i.e.,
``PCNT.INCREMENT`` behaves as ``PCNT.DECREMENT`` and vice versa)
when ``mode_pin`` is low
- ``mode_high``: as ``mode_low`` but for the behaviour when ``mode_pin``
is high
- ``filter``: set to a value 1..1023, in ticks of the 80MHz clock, to
enable the pulse width filter
- ``minimum``: set to the minimum level of the counter value when
decrementing (-32768..-1) or 0 to disable
- ``maximum``: set to the maximum level of the counter value when
incrementing (1..32767) or 0 to disable
- ``threshold0``: sets the counter value for the
``PCNT.IRQ_THRESHOLD0`` event (see ``irq`` method)
- ``threshold1``: sets the counter value for the
``PCNT.IRQ_THRESHOLD1`` event (see ``irq`` method)
- ``value``: reset the counter value (must be 0 if specified)
- ``channel``: see description below

The hardware initialisation is done in stages and so some of the keyword
arguments can be used in groups or in isolation to partially reconfigure a
unit:

- the ``pin`` keyword (optionally combined with ``mode_pin``) can be used
to change just the bound pin(s)
- ``rising`, ``falling``, ``mode_low`` and ``mode_high`` can be used
(singly or together) to change the counting logic - omitted keywords
use their default (``PCNT.IGNORE`` or ``PCNT.NORMAL``)
- ``filter`` can be used to change only the pulse width filter (with 0
disabling it)
- each of ``minimum``, ``maximum``, ``threshold0`` and ``threshold1`` can
be used to change these limit/event values individually; however,
setting any will reset the counter to zero (i.e., they imply
``value=0``)

Each pulse counter unit supports two channels, 0 and 1, each able to
monitor different pins with different counting logic but updating the same
counter value. Use ``channel=1`` with the ``pin``, ``rising``, ``falling``,
``mode_pin``, ``mode_low`` and ``mode_high`` keywords to configure the
second channel.

The second channel can be used to configure 4X quadrature decoding with a
single counter unit::

pin_a = Pin(2, Pin.INPUT, pull=Pin.PULL_UP)
pin_b = Pin(3, Pin.INPUT, pull=Pin.PULL_UP)
rotary = PCNT(0, pin=pin_a, falling=PCNT.INCREMENT, rising=PCNT.DECREMENT, mode_pin=pin_b, mode_low=PCNT.REVERSE)
rotary.init(channel=1, pin=pin_b, falling=PCNT.DECREMENT, rising=PCNT.INCREMENT, mode_pin=pin_a, mode_low=PCNT.REVERSE)
rotary.start()

.. method:: PCNT.value([value])

Call this method with no arguments to return the current counter value or
pass the value 0 to reset the counter and return the value before reset.
ESP32 pulse counters do not support being set to any value other than 0.
Read and reset is not atomic and so it is possible for a pulse to be
missed.

.. method:: PCNT.irq(handler=None, trigger=PCNT.IRQ_ZERO)

ESP32 pulse counters support interrupts on these counter events:

- ``PCNT.IRQ_ZERO``: the counter has reset to zero
- ``PCNT.IRQ_MINIMUM``: the counter has hit the ``minimum`` value
- ``PCNT.IRQ_MAXIMUM``: the counter has hit the ``maximum`` value
- ``PCNT.IRQ_THRESHOLD0``: the counter has hit the ``threshold0`` value
- ``PCNT.IRQ_THRESHOLD1``: the counter has hit the ``threshold1`` value

``trigger`` should be the desired events or'ed together. The ``handler``
function should take a single argument which will be a bit mask indicating
which counter event(s) occurred.

The handler is called with the MicroPython scheduler and so will run at a
point after the interrupt. If another interrupt occurs before the handler
has been called then the events will be coalesced together into a single
call and the bit mask will indicate all events that have occurred.

To avoid race conditions between a handler being called and retrieving the
current counter value, the ``value()`` method will force execution of any
pending events before returning the current counter value (and potentially
resetting the value).

Only one handler can be in place per-unit. Set ``handler`` to ``None`` to
disable the event interrupt (or call ``irq()`` with no arguments).

.. Note::
ESP32 pulse counters reset to *zero* when reaching the minimum or maximum
value. Thus the ``IRQ_ZERO`` event will also trigger when either of these
events occurs.

See the :ref:`machine.Counter <machine.Counter>` and
:ref:`machine.Encoder <machine.Encoder>` classes for simpler abstractions of
common pulse counting applications.


.. _esp32.RMT:

RMT
Expand Down
74 changes: 74 additions & 0 deletions docs/library/machine.Counter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.. currentmodule:: machine
.. _machine.Counter:

class Counter -- pulse counter
==============================

Counter implements pulse counting by monitoring an input signal and counting
rising or falling edges.

Minimal example usage::

from machine import Pin, Counter

counter = Counter(0, Pin(0, Pin.IN)) # create Counter for pin 0 and begin counting
value = counter.value() # retrieve current pulse count

Availability: esp32 port

Constructors
------------

.. class:: Counter(id, ...)

Construct a Counter object with the given id. Values of *id* depend on a
particular port and its hardware. Values 0, 1, etc. are commonly used to
select hardware block #0, #1, etc. Additional arguments are passed to the
``init()`` method described below.

Methods
-------

.. method:: Counter.init(src, *, ...)

Initialise the Counter with the given parameters:

- *src* specifies the input pin as a :ref:`machine.Pin <machine.Pin>` object.
May be omitted on ports that have a predefined pin for a given hardware
block.

Additional keyword-only parameters that may be supported by a port are:

- *edge* specifies the edge to count. Either ``Counter.RISING`` (the default)
or ``Counter.FALLING``.

- *direction* specifies the direction to count. Either ``Counter.UP`` (the
default) or ``Counter.DOWN``.

- *filter_ns* specifies a minimum period of time in nanoseconds that the
source signal needs to be stable for a pulse to be counted. Implementations
should use the longest filter supported by the hardware that is less than
or equal to this value. The default is 0 (no filter).

.. method:: Counter.deinit()

Stops the Counter, disabling any interrupts and releasing hardware resources.
A Soft Reset should deinitialize all Counter objects.

.. method:: Counter.value([value])

Get, and optionally set, the counter value as a signed integer.
Implementations should aim to do the get and set atomically.

Constants
---------

.. data:: Counter.RISING
Counter.FALLING

Select the pulse edge.

.. data:: Counter.UP
Counter.DOWN

Select the counting direction.
64 changes: 64 additions & 0 deletions docs/library/machine.Encoder.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.. currentmodule:: machine
.. _machine.Encoder:

class Encoder -- quadrature decoding
====================================

Encoder implements decoding of quadrature signals as commonly output from
rotary encoders, by counting either up or down depending on the order of two
input pulses.

Minimal example usage::

from machine import Pin, Encoder

counter = Counter(0, Pin(0, Pin.IN), Pin(1, Pin.IN)) # create Encoder for pins 0, 1 and begin counting
value = counter.value() # retrieve current count

Availability: esp32 port

Constructors
------------

.. class:: Encoder(id, ...)

Construct an Encoder object with the given id. Values of *id* depend on a
particular port and its hardware. Values 0, 1, etc. are commonly used to
select hardware block #0, #1, etc. Additional arguments are passed to the
``init()`` method described below.

Methods
-------

.. method:: Encoder.init(phase_a, phase_b, *, ...)

Initialise the Encoder with the given parameters:

- *phase_a* specifies the first input pin as a
:ref:`machine.Pin <machine.Pin>` object.

- *phase_a* specifies the second input pin as a
:ref:`machine.Pin <machine.Pin>` object.

These pins may be omitted on ports that have predefined pins for a given
hardware block.

Additional keyword-only parameters that may be supported by a port are:

- *filter_ns* specifies a minimum period of time in nanoseconds that the
source signal needs to be stable for a pulse to be counted. Implementations
should use the longest filter supported by the hardware that is less than
or equal to this value. The default is 0 (no filter).

- *phases* specifies the number of signal edges to count and thus the
granularity of the decoding. Ports may support either 1, 2 or 4 phases.

.. method:: Encoder.deinit()

Stops the Encoder, disabling any interrupts and releasing hardware resources.
A Soft Reset should deinitialize all Encoder objects.

.. method:: Encoder.value([value])

Get, and optionally set, the encoder value as a signed integer.
Implementations should aim to do the get and set atomically.
2 changes: 2 additions & 0 deletions docs/library/machine.rst
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ Classes
machine.I2S.rst
machine.RTC.rst
machine.Timer.rst
machine.Counter.rst
machine.Encoder.rst
machine.WDT.rst
machine.SD.rst
machine.SDCard.rst
Expand Down
1 change: 1 addition & 0 deletions ports/esp32/boards/ESP32_GENERIC_C3/mpconfigboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

// Enable UART REPL for modules that have an external USB-UART and don't use native USB.
#define MICROPY_HW_ENABLE_UART_REPL (1)
#define MICROPY_PY_ESP32_PCNT (0)
1 change: 1 addition & 0 deletions ports/esp32/esp32_common.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ list(APPEND MICROPY_SOURCE_PORT
modesp.c
esp32_nvs.c
esp32_partition.c
esp32_pcnt.c
esp32_rmt.c
esp32_ulp.c
modesp32.c
Expand Down
Loading
Loading