Skip to content

mimxrt: Add Quadrature Encoder and Pulse Counter classes.#12347

Merged
dpgeorge merged 6 commits into
micropython:masterfrom
robert-hh:mimxrt_qecnt
Mar 8, 2026
Merged

mimxrt: Add Quadrature Encoder and Pulse Counter classes.#12347
dpgeorge merged 6 commits into
micropython:masterfrom
robert-hh:mimxrt_qecnt

Conversation

@robert-hh
Copy link
Copy Markdown
Contributor

This PR adds Quadrature Encoder and Pulse Counter classes based on the Encoder hardware of the mimxrt MCUs. The base methods are as simple as possible, with options to make use of the hardware features supporting fast encoder sensors, like index pulses.

Tested with a slow manual encoder and a simulation of fast encoder/counter signals up to a input frequency of 25 MHz.

The PR is a re-submit of PR #7911, which could not be reopened. The conversation at that PR applies here as well (except for the UART part).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Nov 4, 2023

Code size report:

Reference:  rp2/modules/rp2.py: Don't corrupt globals on asm_pio() exception. [c3ca843]
Comparison: tests/extmod_hardware/machine_counter.py: Separate the connection test. [merge of 2322d37]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt: +5892 +1.564% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 4, 2023

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.42%. Comparing base (c3ca843) to head (2322d37).
⚠️ Report is 6 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #12347   +/-   ##
=======================================
  Coverage   98.42%   98.42%           
=======================================
  Files         174      174           
  Lines       22329    22329           
=======================================
  Hits        21978    21978           
  Misses        351      351           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@robert-hh
Copy link
Copy Markdown
Contributor Author

@dpgeorge @projectgus Now that the Encoder/Counter classes are available for ESP32, I have rebased and retested the implementation for MIMXRT. Since the Encoder/Counter hardware differs between ESP32 and MIMXRT, the ESP32 oriented test in extmod_hardware show quite a few deviations. Major reasons:

  • The MIMXRT Encoder always counts every transition. So count numbers are by a factor of 4 higher than in the ESP32. But it does not have the ESP32 glitch to need 3 transitions for the first count.
  • Encoder/Counter input values cannot be read back using the machine.Pin methods, causing the connection test to fail.
  • The MIMXRT Counter always counts the rising edge. The active edge cannot be changed.

@projectgus
Copy link
Copy Markdown
Contributor

Thanks @robert-hh for updating and noting these differences. (I think it's a good example of the potential for tests to find these hardware-specific parts as well.)

The MIMXRT Encoder always counts every transition. So count numbers are by a factor of 4 higher than in the ESP32. But it does not have the ESP32 glitch to need 3 transitions for the first count.

The ESP32 version of this class has a documented port-specific phases parameter, that defaults to 1 but it can also count each transition by setting it 4. So we could change the default value of phases, but another option would be to emulate the phases parameter in software on the mimxrt version. What do you think?

Encoder/Counter input values cannot be read back using the machine.Pin methods, causing the connection test to fail.
The MIMXRT Counter always counts the rising edge. The active edge cannot be changed.

I think the solution for these will probably be @unittest.skipIf() for the respective tests. Possibly in the top block we can set variables like COUNT_FALLING = True and then evaluate those in the skip clauses.

I am a bit surprised that machine.Pin() doesn't work for the input values, though. Is that because there are pins which can never be configured as GPIO, or is that a limitation that once the port muxes them to be counter inputs it doesn't/can't mux them back?

@robert-hh
Copy link
Copy Markdown
Contributor Author

Good day @projectgus, thank you for the swift reply.
Emulating Phase would be possible to a certain extend by dividing the MIMXRT counter by 2 or 4. That would however not cater a possible situation, that ESP32 increases the count with a specific transition. That seems to be the case and may be the reason for the first count in the test needing only 3 transitions. Maybe that could be solved by finding the proper start conditions of the inputs. Another indication of changing the count at a specific transition is the fact, that for ESP32 a single transition down from 0 decreases the counter to -1. A simply division would not cater for that.

Obviously one can skip the test for the FALLING edge of the counter.

I did not look in detail why the Pin value() cannot read back when the Pin was configured for Encoder/Counter. Probably it's because for Encoder/Counter a PIN is configured for a different ALT mode. So maybe the test has to be changed to configure the Pin for machine.Pin mode first, make the connectivity test and then configure it as Encoder/Counter. I wondered why there is the connectivity test at all. The other tests to not have it.

@robert-hh
Copy link
Copy Markdown
Contributor Author

So I added an emulation for phases and adapted the test scripts, taking into account the not-supported options and the fact, that different to ESP32 the MIMXRT expects 4 transitions to count from 0 to 1. For ESP32, setting the initial value of pulses to -1 fixes that.

@robert-hh robert-hh force-pushed the mimxrt_qecnt branch 2 times, most recently from b6eb32c to d115ab6 Compare August 6, 2025 12:02
@projectgus
Copy link
Copy Markdown
Contributor

Sorry I didn't reply last week, @robert-hh - the updated PR looks great to me, thanks for accommodating the limits in the API and tests! I don't think I have any mimxrt board set up to do any local tests on, unfortunately (there might be a Teensy 4 hiding somewhere, will look later).

I wondered why there is the connectivity test at all. The other tests to not have it.

I borrowed this from Ihor's test PR, but it's something I support building into tests - especially hardware-in-loop tests. The idea is to have a quick and user-friendly way to differentiate "the test environment is broken" from "the driver is broken". More than once I've had some unusual pattern of failures and spent too long debugging them before realising a wire is loose, or some equivalent prerequisite is missing (i.e with MicroPython, forgetting to do mpremote rtc --set after hard reset has gotten me more than once).

As you observe, MicroPython's tests don't have much of this kind of thing (yet) - but I think that's to the project's detriment. It costs time even for core developers (like me) who run the test suite a lot, but for developers who aren't already very familiar with the project then it's even more limiting. We get a lot of PRs from new(ish) contributors who are competent developers, but the relevant tests either haven't been run, or they ran and failed and the developer hasn't dug in to figure out why.

(Sorry for dropping in this somewhat tangential rant. cc @hmaerki and @dpgeorge as I know they've been thinking about tests an awful lot, lately.)

In the particular case of the connections test for Counter/Encoder, I think skipping them on mimxrt - as you've done here - is fine for now. Long term, putting them in a separate TestCase at the top of the same file (so they're run without the setUp step that inits the Encoder and muxes the pins) will probably allow them to pass on more platforms, but I can look at that in a follow-up if you like.

@dpgeorge
Copy link
Copy Markdown
Member

More than once I've had some unusual pattern of failures and spent too long debugging them before realising a wire is loose, or some equivalent prerequisite is missing (i.e with MicroPython, forgetting to do mpremote rtc --set after hard reset has gotten me more than once).

That's what #16112 is trying to address, although it's gotten pretty lost among all the other PRs. Might be nice to get that merged, I use it all the time now.

The idea is to have a quick and user-friendly way to differentiate "the test environment is broken" from "the driver is broken

We are definitely missing a simple Pin test that toggles IO and checks it works. That could run before any of these other more sophisticated tests to verify the hardware is wired up correctly.

But also, I think we need to break out all this board wiring configuration into separate files, one for each board, that can be reused in all hardware-related tests. That's something I'm working on now.

@robert-hh
Copy link
Copy Markdown
Contributor Author

putting them in a separate TestCase at the top of the same file (so they're run without the setUp step that inits the Encoder and muxes the pins) will probably allow them to pass on more platforms, , but I can look at that in a follow-up if you like.

I'm not familiar with the mechanics of unittest, so I do not object if you look at that later.

@dpgeorge dpgeorge added this to the release-1.28.0 milestone Mar 4, 2026
Copy link
Copy Markdown
Member

@dpgeorge dpgeorge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robert-hh I've now done my final, full review of this PR. Once the above comments are resolved, this should be good to merge!

Comment thread docs/library/machine.Counter.rst Outdated
Comment thread docs/library/machine.Counter.rst Outdated
Comment thread docs/library/machine.Counter.rst Outdated
Comment thread docs/library/machine.Counter.rst Outdated
Comment thread docs/library/machine.Counter.rst
Comment thread ports/mimxrt/machine_encoder.c Outdated
Comment thread ports/mimxrt/machine_encoder.c Outdated
Comment thread ports/mimxrt/machine_encoder.c
Comment thread ports/mimxrt/machine_encoder.c Outdated
Comment thread ports/mimxrt/machine_encoder.c Outdated
@robert-hh
Copy link
Copy Markdown
Contributor Author

Thank you for the thorough review. I made most of the suggested changes, except one. You suggested a note that the cycles counter is reset when value is set to 0. That is actually not the case.

@robert-hh robert-hh force-pushed the mimxrt_qecnt branch 3 times, most recently from 0ebf0fa to d0af24e Compare March 7, 2026 20:15
Comment thread docs/mimxrt/quickref.rst Outdated
@robert-hh
Copy link
Copy Markdown
Contributor Author

quickref.rst is updated.

Comment thread docs/library/machine.Counter.rst Outdated
Comment thread docs/library/machine.Encoder.rst Outdated
Comment thread docs/mimxrt/quickref.rst Outdated
@robert-hh robert-hh force-pushed the mimxrt_qecnt branch 2 times, most recently from bd1150c to 480e7ba Compare March 8, 2026 10:29
@robert-hh
Copy link
Copy Markdown
Contributor Author

Updated. The documentation is hard to get perfect. Every time one looks at it there are spots to improve. Of course, that holds for code too.

Copy link
Copy Markdown
Member

@dpgeorge dpgeorge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating, this looks good now. Let's get it in so people can use it!

I will squash commits as part of the merge.

@dpgeorge
Copy link
Copy Markdown
Member

dpgeorge commented Mar 8, 2026

The documentation is hard to get perfect.

It doesn't need to be perfect 😄

@robert-hh
Copy link
Copy Markdown
Contributor Author

Thank you.

robert-hh and others added 6 commits March 8, 2026 23:54
These classes are based on the Quadrature Encoder blocks of the i.MXRT
MCUs.  The i.MXRT 102x has two encoders, the other ones four.  The i.MXRT
101x does not support this function.  It is implemented as two classes,
Encoder and Counter.

The number of pins that can be uses as inputs is limited by the MCU
architecture and the board schematics.  The Encoder class supports:
- Defining the module.
- Defining the input pins.
- Defining a pin for an index signal.
- Defining a pin for a reset signal.
- Defining an output pin showing the compare match signal.
- Setting the number of cycles per revolution (min/max).
- Setting the initial value for the position.
- Setting the counting direction.
- Setting a glitch filter.
- Defining callbacks for getting to a specific position, overrun and
  underrun (starting the next revolution).  These callbacks can be hard
  interrupts to ensure short latency.

The encoder counts all phases of a cycle.  The span for the position is
2**32, for the revolution is 2**16.  The highest input frequency is
CPU-Clock/24.  Note that the "phases" argument is emulated at the API
level (the hardware will always count all phases).

The Counter mode counts single pulses on input A of the Encoder.  The
configuration supports:
- Defining the module.
- Defining the input pin.
- Defining the counting direction, either fixed or controlled by the level
  of an input pin.
- Defining a pin for an index signal.
- Defining an output pin showing the compare match signal.
- Setting the counter value.
- Setting the glitch filter.
- Defining a callback which is called at a certain value.
- Settings for MIMXRT1015. The MIMXRT1015 MCU has only one encoder/counter
  unit.

The counting range is 0 - 2**32-1 and a 16 bit overrun counter.  The
highest input frequency is CPU-Clock/12.

The implementation of the `.irq()` method uses the common code from
`shared/runtime/mpirq.c`, including the `irq().flags()` and
`irq().trigger()` methods.

Signed-off-by: robert-hh <[email protected]>
This adds MIMXRT-specific arguments and methods, as a superset of the
original Encoder/Counter documentation.

The mimxrt pinout and quickref docs are updated with relevant information.

Signed-off-by: robert-hh <[email protected]>
For Teensy 4.x.  The connectivity tests and the falling edge of the counter
test are skipped.

Signed-off-by: robert-hh <[email protected]>
Because it requires a different configuration of the pins (in `setUp`).
Eg on mimxrt pins used for an Encoder cannot be read.

Signed-off-by: Damien George <[email protected]>
Because it requires a different configuration of the pins (in `setUp`).
Eg on mimxrt pins used for a `machine.Counter` cannot be read.

Signed-off-by: Damien George <[email protected]>
@dpgeorge dpgeorge merged commit 2322d37 into micropython:master Mar 8, 2026
95 of 96 checks passed
@robert-hh robert-hh deleted the mimxrt_qecnt branch March 8, 2026 16:48
@dpgeorge
Copy link
Copy Markdown
Member

dpgeorge commented Mar 8, 2026

@robert-hh both ADAFRUIT_METRO_M7 and MAKERDIARY_RT1011_NANO_KIT fail to build with this PR, something about XBARA1 undefined. Are you able to fix that?

@robert-hh
Copy link
Copy Markdown
Contributor Author

Will do.

@robert-hh
Copy link
Copy Markdown
Contributor Author

The files mpconfigboard.h for ADAFRUIT_METRO_M7 and MAKERDIARY_RT1011_NANO_KIT are updated. The firmware builds and runs. To be sure, I built the binaries for all MIMXRT boards, and all build fine. The change is published as PR #18908.

For history: Since the MIMXRT1010 does not support Encoder, these boards were not tested with machine.Encoder, and being introduced later that the first versions of the PR12347, they were not any more considered as relevant. So building all boards is a required test step.

@dpgeorge
Copy link
Copy Markdown
Member

dpgeorge commented Mar 9, 2026

For history: Since the MIMXRT1010 does not support Encoder, these boards were not tested with machine.Encoder, and being introduced later that the first versions of the PR12347, they were not any more considered as relevant. So building all boards is a required test step.

OK, thanks for the info. It's hard to get everything right when so many things are supported, and with all the moving pieces. And anyway it's an easy problem to fix.

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.

3 participants