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

Conversation

jonathanhogg
Copy link
Contributor

Provides pretty complete support for the ESP32 PCNT peripheral including control pin, second channel and events.

@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch 2 times, most recently from 794f821 to a4b35c2 Compare July 24, 2021 17:37
@jonathanhogg
Copy link
Contributor Author

jonathanhogg commented Jul 24, 2021

Examples:

from machine import Counter, Pin


# Simple counter:

def callback(counter):
    print("Counter value:", counter.value())


simple = Counter(0, pin=Pin(0, Pin.IN, pull=Pin.PULL_UP), rising=Counter.INCREMENT, threshold1=5, maximum=10, filter=1000)
simple.irq(handler=callback, trigger=Counter.IRQ_THRESHOLD1)
simple.start()


# Rotary encoder, 1X detection:

pin_a = Pin(12, Pin.IN, pull=Pin.PULL_UP)
pin_b = Pin(14, Pin.IN, pull=Pin.PULL_UP)

encoder = Counter(1, pin=pin_a, falling=Counter.INCREMENT, rising=Counter.DECREMENT, mode_pin=pin_b, mode_low=Counter.HOLD)

# 2X detection:

encoder = Counter(1, pin=pin_a, falling=Counter.INCREMENT, rising=Counter.DECREMENT, mode_pin=pin_b, mode_low=Counter.REVERSE)

# 4X detection:

encoder = Counter(1, filter=1000, minimum=-100, maximum=100)
encoder.init(0, pin=pin_a, falling=Counter.INCREMENT, rising=Counter.DECREMENT, mode_pin=pin_b, mode_low=Counter.REVERSE)
encoder.init(1, pin=pin_b, falling=Counter.DECREMENT, rising=Counter.INCREMENT, mode_pin=pin_a, mode_low=Counter.REVERSE)

@IhorNehrutsa
Copy link
Contributor

See #6639

@jonathanhogg
Copy link
Contributor Author

See #6639

I did actually take a look at this implementation (I tried to hunt down all I could find), but I preferred to just expose the whole PCNT API through a single class rather than having a separate class for quadrature decoding – the latter being trivially implemented with the former.

That said – as per the comment in #5216 – I wrote this code for my own use case and opened this PR as an FYI for anyone interested in counters. It may make more sense for MicroPython in the general case to use a separate quadrature decoder class as this might map better to other ports (I see STM has its own way of doing quad decoding with counters). Rotary encoders are such a common use case that a specific API for them makes sense (kind of like the neopixel library).

@dpgeorge
Copy link
Member

Thanks, it looks interesting.

If this were to be accepted (as part of the machine module) it would need documentation and the possibility to eventually implement it for other ports, based on the docs.

@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch 2 times, most recently from de61a90 to 3f93fb0 Compare July 29, 2021 19:26
@jonathanhogg
Copy link
Contributor Author

@dpgeorge: OK, you've sucked me in to writing some documentation for it – even though I said I wasn't going to try and see this through to mainline 😉

Notes:

  • I wasn't clear on the form for documenting a class in the main library that is only (presently) supported on one port. It's hard to be certain what exactly the "common" API should be, so I've gone for a very limited API.
  • I've documented the full ESP32 API in the ESP32 quick reference doc.
  • There seems to be no consistency in the documents over English dialect. I have gone for the American spelling of "initialize" as – despite being a Brit – I've always found it safer to use American spelling for computing/electronics work and it's become a habit now.

I'm conscious of not wanting to step on the toes of the people who have tried to get a pulse counter PR into MicroPython before and I can see the value in @IhorNehrutsa's idea to have a separate class for quadrature decoding – although whether this ought to be in a pure-Python wrapper like the neopixel library is an open question.

That said, I'm committed to supporting this code as it's going into production hardware this year whether it goes mainline or not.

@jonathanhogg jonathanhogg marked this pull request as ready for review July 29, 2021 19:39
@IhorNehrutsa
Copy link
Contributor

Hi, Jonathan.
I think the best name for your class is PCNT as a full raw access point to ESP32 hardware.

@jonathanhogg
Copy link
Contributor Author

Hi, Jonathan.
I think the best name for your class is PCNT as a full raw access point to ESP32 hardware.

The idea is to have a core API that can be implemented in other ports using their counter hardware (or in software with pin interrupts), but that is flexible enough to extend to the full features of the ESP32 PCNT hardware. So I chose the class name to be non-port-specific.

@robert-hh
Copy link
Contributor

@jonathanhogg I have the impression that the way you have chosen the API is very port-specific for ESP32. As you said, it exposes the RTOS API to Python.
And especially for the Quadrature Encoder I consider it's better to define a separate class, even if it uses the same hardware, and the implementation uses the same functions. The way it seems to be squeezed into the counter class is not obvious. Of course one could use Python subclassing for it, at the cost of performance. And what confuses me are the chosen names for keywords.

  • the various mode_xxxx options
  • target for the irq trigger. Why not go for "trigger", like done in other places.
  • instead of "rising" and "falling" something like "slope" for the active slope and "direction" for up/down counting seems more clear to me.

@jonathanhogg
Copy link
Contributor Author

jonathanhogg commented Oct 18, 2021

@jonathanhogg I have the impression that the way you have chosen the API is very port-specific for ESP32. As you said, it exposes the RTOS API to Python. And especially for the Quadrature Encoder I consider it's better to define a separate class, even if it uses the same hardware, and the implementation uses the same functions. The way it seems to be squeezed into the counter class is not obvious. Of course one could use Python subclassing for it, at the cost of performance. And what confuses me are the chosen names for keywords.

  • the various mode_xxxx options
  • target for the irq trigger. Why not go for "trigger", like done in other places.
  • instead of "rising" and "falling" something like "slope" for the active slope and "direction" for up/down counting seems more clear to me.

Hiya,

Ta for the feedback. In rough order:

  • Yes, I agree that quadrature decoding is probably better done in a separate class – I made that very point in the discussion; however, exposing the complete PCNT functionality (this isn't "squeezed in", it's exactly how the ESP32 counter hardware works) enables a quadrature decoder class to be implemented in (very simple) pure Python. It seemed to make sense to me that ports expose their actual functionality and then another layer on top adapts this to something easier to use. I'm up for further discussion on this, though.
  • The "MODE_" options are ESP32 specific, which is why I didn't document them in the core API; I'm happy to accept alternative names for them, though – the actual ESP32 names are "KEEP", "REVERSE" and "DISABLE", but these made no more sense to me (other than "reverse").
  • Good catch on target – this is actually trigger in the code and I've made a mistake in the docs!
  • As noted in my comment on ESP32 Pulse Counter Driver #5216, I specifically chose not to use slope/direction arguments as this immediately limits you to being able act on only one edge. The ESP32 PCNT hardware supports taking different actions on both rising and falling edges. The more general form of the API is applicable to ESP32 or more limited hardware counters.

Honestly, it's great to see people engaging in this topic as I'd prefer to see a consensus-driven version hit mainline and not have to maintain a slew of my own patches.

Jonathan

@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch from ca8a6bb to 2171786 Compare October 18, 2021 16:32
@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch from 2171786 to aea74be Compare December 2, 2021 11:59
@jonathanhogg jonathanhogg marked this pull request as draft December 8, 2021 14:25
@jonathanhogg
Copy link
Contributor Author

Leaving this PR around for reference and because I'm using this code in production devices. However, I'm not going to work on it any more.

I'm leaving @IhorNehrutsa and @robert-hh to come up with a common API for counters and encoders (#6639 and #7911 respectively) in the hope that one day we see it hitting mainline and I can throw this one in the bin.

@robert-hh
Copy link
Contributor

That's a little bit sad. A combination of your driver and the one of @IhorNehrutsa would be more complete. I just saw you made a great work on ADC, which improves it a lot. I hope it gets merged. It may take a while until the Encoder/Counter support will be in the main line, if ever. It does not seem to be a desperately sought feature.

@jonathanhogg
Copy link
Contributor Author

That's a little bit sad. A combination of your driver and the one of @IhorNehrutsa would be more complete.

Yeah, I thought about this, but I think the main (only?) purpose of the modal functionality in the ESP32 PCNT API is to support encoders and this is going to be more understandable to a user wrapped in an Encoder class – and certainly the only way we'll see a cross-port API. The event callbacks are neat, but are going to be hard to merge in with the wide-counter support in @IhorNehrutsa's work and I feel like that's probably more useful to an end user.

I just saw you made a great work on ADC, which improves it a lot. I hope it gets merged.

Yes, I'm hoping so too. Support for ADC block 2 has been outstanding for ages and the ESP32 calibrated voltage API is really great.

It may take a while until the Encoder/Counter support will be in the main line, if ever. It does not seem to be a desperately sought feature.

I actually had someone email me recently in desperate need of counter support on ESP32 and asking for me to cut them a custom build of my branch!

Having good PRs covering common API documentation, and MIMXRT and ESP32 implementations will be a great start. I feel like one more port would tip it over the line. STM32 seemed like an obvious choice, but hardware timer support is bound up in the pyb module and that looks like a can of worms…

@IhorNehrutsa
Copy link
Contributor

@jonathanhogg
Would I add an IRQ support handler to #6639
for PCNT_EVT_THRES_0, PCNT_EVT_THRES_1 ?

@harbaum
Copy link

harbaum commented Feb 18, 2023

PCNT is very ESP specific.

Encoder and Counter on the other hand are independent from that and don't even have to be frozen into MP as they can be added in pure python on top of PCNT later.

I don't understand why the integration of PCNT needs to be delayed until Encoder and Counter are stable.

@cederom
Copy link
Contributor

cederom commented Feb 19, 2023

From user perspective, I found working Encoder and CAN code with examples running on NuttX RTOS that builds and flashes under a minute so I will switch to that platform, big delays of useful verified stuff into a release is a big disadvantage of MicroPython project, please reconsider..

@IhorNehrutsa
Copy link
Contributor

Unfortunately, this is not the first and not the only case of delays in solving problems in Micropython. Unfortunately for all... :(

@X-Ryl669
Copy link

X-Ryl669 commented May 2, 2023

Just a remark here, PCNT is also useful for Flow meters, where neither Counter or Encoder are required (we need to assert the pulse per time period, not the count of pulses, and ideally, we don't want to run an IRQ per pulse). So the argument about waiting for Encoder or Counter to be merged first seems like it's counter intuitive to me.

@X-Ryl669
Copy link

X-Ryl669 commented May 2, 2023

Would it be possible to implement this as a native module or is it relying on some symbols that's not in default micropython firmware on ESP32 ?

@IhorNehrutsa
Copy link
Contributor

@X-Ryl669

Just a remark here, PCNT is also useful for Flow meters....

May you post some link of Flow meters timing chart?
Or Flow meters models.
Thanks.

@X-Ryl669
Copy link

X-Ryl669 commented May 2, 2023

flow meters are very slow, compared to an rotary/axial encoder. The typical frequency will be in the hundred of Hz, not kHz. Their duty cycle is completely random either, since they are based on a magnet + hall effect sensor + Schmidt Trigger.
You can have very short pulse, but with a large (random) period between them, or the opposite.

However, they are observing a physical system with friction and resistance so their answer is very non linear:

  • When the flow is very low, the turbine or blade will not move due to the fluid going around the obstacle instead of pushing it,
  • When the flow is very high, the turbine will dampen the flow itself (the back of the blade will hit the flow and give a resistance).

So one must apply different calibration table based on what regime they are in. This means that the number of pulse isn't very important, but the number of pulse per second is, since it allows to find out what regime they are in, and then you can interpret the pulse count. So basically, you'll have to sort the pulses in different buckets, one bucket for each (calibrated) regime.

So an implementation that's based on a Pin input IRQ and a getTickUs() will not work well, since you always have the issue of not knowing when the IRQ triggered (and the time between the handler is executing the get time function). Worse: you'll usually zero your counter in the handler and store the "last" time in a variable too only if some period elapsed.
This delay is the issue since any pulse getting in while the IRQ is processing will be lost or its timing unsure.
You can't sleep while doing this either, so this is also quite power inefficient.

With a PCNT, and a Timer, you'll have an interrupt for the timer at a very precise interval, where you can read the pulse counter and deduce the regime you're in and the accumulated flow/pulses, while still having the PCNT running and counting new pulses. Also, you can sleep while the HW is counting the pulses.

@jonathanhogg
Copy link
Contributor Author

@X-Ryl669:

Would it be possible to implement this as a native module or is it relying on some symbols that's not in default micropython firmware on ESP32 ?

It relies on symbols that are not available from a native module. The IDF PCNT driver isn't even compiled in without being explicitly linked to.

There is actually surprisingly little that you can do from a native module as they require an explicit MicroPython lookup table for linking to anything at all. On the ESP32 you can't even do floating point division in a native module. Believe me, I've bounced up against these limitations often.

Just a remark here, PCNT is also useful for Flow meters, where neither Counter or Encoder are required (we need to assert the pulse per time period, not the count of pulses, and ideally, we don't want to run an IRQ per pulse). So the argument about waiting for Encoder or Counter to be merged first seems like it's counter intuitive to me.

Honestly, I don't think that anything other that a lack of interest / core-developer time is holding up mainlining a PCNT implementation. There have been numerous runs at it over the last couple of years and if a particular API was or wasn't the problem that that would have been indicated and resolved by now.

As noted before, I'm still committed to maintaining this PR. I've been working on higher-level stuff for the last few months, but I'll rebase this to 1.20 in a moment.

@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch from 0cef532 to 7ece16c Compare May 3, 2023 10:46
@harbaum
Copy link

harbaum commented May 3, 2023

For the records: I have built the latest version and I am using it on a daily basis and everything still works as expected.

Let me know if there's anything I can do to help getting this merged.

@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch from 7ece16c to 4ed5510 Compare June 15, 2023 16:11
@IhorNehrutsa
Copy link
Contributor

@jonathanhogg @harbaum
Have you tried changing threshold1 like here?

from machine import Counter, Pin

def callback(counter):
    print("Counter value:", counter.value())

simple = Counter(0, pin=Pin(0, Pin.IN, pull=Pin.PULL_UP), rising=Counter.INCREMENT, threshold1=5, maximum=10, filter=1000)
simple.irq(handler=callback, trigger=Counter.IRQ_THRESHOLD1)
simple.start()

...

simple.irq(handler=callback, trigger=Counter.IRQ_THRESHOLD1, threshold1=50)

...

It seems that it is impossible... :(

@jonathanhogg
Copy link
Contributor Author

@jonathanhogg @harbaum Have you tried changing threshold1 like here?
...
It seems that it is impossible... :(

No, you can only change threshold1 in the init() method and doing so always clears the counter value back to 0. The latter appears to be a hard limitation of the hardware or driver as new threshold values only apply on a counter reset.

I think that being able to change the thresholds in the irq() method is confusing as it's clearly a configuration change of the unit – particularly with the implicit counter reset. However, you don't need to reset the IRQ when changing the threshold anyway, so you should be able to just swap that last line for:

simple.init(threshold1=50)

I don't have any hardware in front of me to test with, but that's how the code was designed.

Obviously you're going to need to change that maximum limit if you want to have the threshold1 actually trigger. You can safely do that in the same init() call if you need to change it on the fly for some reason.

@IhorNehrutsa
Copy link
Contributor

MicroPython v1.20.0-223-g4ed55102f on 2023-08-15; ESP32 module with ESP32

from machine import Counter, Pin

counter = Counter(0, Pin(0, Pin.IN, pull=Pin.PULL_UP))
counter.start()

Traceback (most recent call last):
File "", line 4, in
AttributeError: 'Counter' object has no attribute 'start'

@jonathanhogg
Copy link
Contributor Author

@IhorNehrutsa Oh, sorry! I just noticed that you're using Counter() instead of PCNT(). None of what you're trying to do will work with Counter().

@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch from 4ed5510 to ad7de60 Compare August 15, 2023 14:08
@jonathanhogg
Copy link
Contributor Author

So I've just rebased this and tweaked the code slightly so that it will build against ESP IDF v5.0.2, which is the release that MicroPython production builds are now being made with – and therefore what 1.21 will be using.

In exciting news, ESP IDF v5.0 has a completely new PCNT driver driver/pulse_cnt.h! Thankfully the old API is still supported, although now deprecated. So I'm going to have to re-write this code at some point soon. I'm not planning on doing this before 1.21 comes out, though, as it'll obviously break builds against older versions.

Also, I'm not in any hurry as I'm not actually doing any projects at the moment that are using MicroPython and I have a significant amount of other work I should be doing…

@jonathanhogg
Copy link
Contributor Author

Superseded by #12346.

@jonathanhogg jonathanhogg reopened this Jul 18, 2024
@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch from ad7de60 to e892d1a Compare July 18, 2024 16:43
Add a new `PCNT` class to the `esp32` module that provides complete,
low-level support to the ESP32 PCNT pulse counting hardware units.
Document the new `esp32.PCNT` class for hardware pulse counting.
Add thin Python shims around `esp32.PCNT` to implement the
`machine.Counter` and `machine.Encoder` classes (as per micropython#8072 API).
Add documentation for the core Counter/Encoder API as currently
implemented by the esp32 port. This documentation is a simplification of
that in PR micropython#8072.
@jonathanhogg jonathanhogg force-pushed the esp32_machine_counter branch from e892d1a to 0c3aa6e Compare October 29, 2024 14:54
@vahithosan
Copy link

Thank you. Which esp-idf version should I use when compiling firmware?

@jonathanhogg
Copy link
Contributor Author

It should be compatible with all of the 4 and 5 versions. Think last time I compiled it I did so against 5.2, but I'm away from my laptop so I can't check. You may get a warning about use of the deprecated PCNT API, but it still works fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants