Skip to content
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

ESP32: Add Quadrature Encoder and Pulse Counter classes. #8766

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

IhorNehrutsa
Copy link
Contributor

@IhorNehrutsa IhorNehrutsa commented Jun 16, 2022

Applied to industrial encoders. Has a 32-bit wide counter!!! Has a 32-bit wide matching value !!! (64 bit wide is possible).
image

Count pulses on Pin(17) with direction on Pin(16) an irq handler on zero value.

from machine import Counter, Pin

def irq_handler(self):
    print('irq_handler()', self.id(), self.status(), self.value())

cnt = Counter(0, src=Pin(17, mode=Pin.IN), direction=Pin(16, mode=Pin.IN))
cnt.irq(irq_handler, Counter.IRQ_ZERO)  # set irq handler
print(cnt.value())  # get current counter value

Please note that this pull request
ESP32: Add Quadrature Encoder and Pulse Counter classes. #8766
and
docs\machine: Add Counter and Encoder classes. #8072
and
mimxrt: Add Quadrature Encoder and Pulse Counter classes. #7911
are mutually agreed upon.

Note: MicroPython-lib PR Add Stepper Motor PWM-Counter driver. requires this PR.

@harbaum
Copy link

harbaum commented Jun 27, 2022

Please consider #7582 as a replacement for this one. The other one is more flexible as it exposes the PCNT API and implements the machine.Counter and machine.Encoder in Pyrhon which is IMHO a much nicer approach as it allows to use PCNT for further counter types like on that uses two dedicated pins to count up and down.

@harbaum
Copy link

harbaum commented Aug 14, 2022

I would still like to understand why this PR is preferable over e.g. #7582

This one is IMHO significantly more limited than #7582 as it hides the underlying PCNT API of the ESP32. #7582 exposes this and implements Encoder and Counter as one of many possibilities on top of that.

Furthermore this PR IMHO does not handle setting the counter correctly as it implements a separate offset to the non-settable hardware counter which in turn won't work with the IRQs (counter passes zero, counter reaches a certain value) as the offset happens on the software side and is not taken into account by the hardware side.

If I am wrong with my assumptions, then please correct me.

@msamblanet
Copy link

Hello - new here and just built the branch and tested it with a 100 PPR CNC rotary wheel and initial test works great!

I do wonder if it might make sense for the encoder at least to have a default non-zero filter_ns value. I needed it due to bounce on my encoder and suspect a lot of users will be confused by behavior caused by not setting filter_ns for rotary encoders. Those who have use-cases where no filter is needed will likely be more advanced users who will know to override the default.

For reference, this was tested on an Unexpected Maker ProS3. If there are any specific tests the team needs that I can help with, let me know - otherwise I'll report in if I see any issues during my development.

@IhorNehrutsa
Copy link
Contributor Author

@msamblanet Can you check the maximum rotation speed of the encoder shaft when measuring the exact pulse count without pulse losses?

@msamblanet
Copy link

msamblanet commented Aug 18, 2022

@IhorNehrutsa - I don’t have a mechanical drive other than my hand. I was stress testing it by spinning around 6 full rotations per second (600 steps or 2400 counts per second) with no loss (way faster than my real world use case). I was using a 10000ns filter (arbitrary choice but a filter was needed with my setup). Given the frequency of the esp32 pcnt unit (25mhz I think) I’m not too worried here :)

I think once during my testing I lost a couple pulses but I am testing in a breadboard with a couple iffy connections. As it was a single occurrence, I’m ignoring it unless it happens repeatable.

Edit - corrected pulses to counts in rate

@msamblanet
Copy link

I just tried to rebase this PR so I could test it combined with another PR which is based on a new MP base. This PR fails to build when rebased at 2 points in machine_encoder.c. Full error output below. The branch without a rebase builds fine for me.

In file included from /project/micropython/py/mpstate.h:33,
                 from /project/micropython/py/runtime.h:29,
                 from /project/micropython/ports/esp32/machine_encoder.c:50:
/project/micropython/ports/esp32/machine_encoder.c: In function 'mp_machine_Counter_init_helper':
/project/micropython/py/misc.h:54:50: error: size of unnamed array is negative
 #define MP_STATIC_ASSERT(cond) ((void)sizeof(char[1 - 2 * !(cond)]))
                                                  ^
/project/micropython/py/misc.h:58:40: note: in expansion of macro 'MP_STATIC_ASSERT'
 #define MP_STATIC_ASSERT_NOT_MSC(cond) MP_STATIC_ASSERT(cond)
                                        ^~~~~~~~~~~~~~~~
/project/micropython/py/obj.h:764:5: note: in expansion of macro 'MP_STATIC_ASSERT_NOT_MSC'
     MP_STATIC_ASSERT_NOT_MSC((t) != &mp_type_int), assert((t) != &mp_type_int),           \
     ^~~~~~~~~~~~~~~~~~~~~~~~
/project/micropython/py/obj.h:769:31: note: in expansion of macro 'mp_type_assert_not_bool_int_str_nonetype'
 #define mp_obj_is_type(o, t) (mp_type_assert_not_bool_int_str_nonetype(t) && mp_obj_is_exact_type(o, t))
                               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/project/micropython/ports/esp32/machine_encoder.c:483:20: note: in expansion of macro 'mp_obj_is_type'
         } else if (mp_obj_is_type(args[ARG_scale].u_obj, &mp_type_int)) {
                    ^~~~~~~~~~~~~~
/project/micropython/ports/esp32/machine_encoder.c: In function 'mp_machine_Encoder_init_helper':
/project/micropython/py/misc.h:54:50: error: size of unnamed array is negative
 #define MP_STATIC_ASSERT(cond) ((void)sizeof(char[1 - 2 * !(cond)]))
                                                  ^
/project/micropython/py/misc.h:58:40: note: in expansion of macro 'MP_STATIC_ASSERT'
 #define MP_STATIC_ASSERT_NOT_MSC(cond) MP_STATIC_ASSERT(cond)
                                        ^~~~~~~~~~~~~~~~
/project/micropython/py/obj.h:764:5: note: in expansion of macro 'MP_STATIC_ASSERT_NOT_MSC'
     MP_STATIC_ASSERT_NOT_MSC((t) != &mp_type_int), assert((t) != &mp_type_int),           \
     ^~~~~~~~~~~~~~~~~~~~~~~~
/project/micropython/py/obj.h:769:31: note: in expansion of macro 'mp_type_assert_not_bool_int_str_nonetype'
 #define mp_obj_is_type(o, t) (mp_type_assert_not_bool_int_str_nonetype(t) && mp_obj_is_exact_type(o, t))
                               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/project/micropython/ports/esp32/machine_encoder.c:716:20: note: in expansion of macro 'mp_obj_is_type'
         } else if (mp_obj_is_type(args[ARG_scale].u_obj, &mp_type_int)) {
                    ^~~~~~~~~~~~~~
[1412/1425] Building C object esp-idf/main/CMakeFiles/__idf_main.dir/__/modsocket.c.obj
ninja: build stopped: subcommand failed.

@IhorNehrutsa
Copy link
Contributor Author

IhorNehrutsa commented Nov 6, 2022

@msamblanet, @AmirHmZz
PR is rebased and fixed

@IhorNehrutsa
Copy link
Contributor Author

Tested with industrial 6250ppr encoder.
lir-137a.pdf
image

# encoders_test.py

from time import sleep
from machine import Pin

from machine import Encoder

# Define an Encoder pins
ENCODER_A = 16
ENCODER_B = 17
ENCODER_R = 18  # one impulse per revolution

PPR = 6250  # pulses per revolution of the encoder shaft
X124 = 4
PPR *= X124

def callback(enc):
    print("\nEncoder value:", enc.value())

try:
    a = Pin(ENCODER_A, mode=Pin.IN)
    b = Pin(ENCODER_B, mode=Pin.IN)  
    r = Pin(ENCODER_R, mode=Pin.IN)  
    print('A', a, a.value())
    print('B', b, b.value())
    print('Z', r, r.value())

    enc = Encoder(0, a, b, x124=X124)
    enc.irq(handler=callback, trigger=Encoder.IRQ_MATCH1, value=50_000)
    enc.irq(handler=callback, trigger=Encoder.IRQ_ZERO)
    enc.irq(handler=callback, trigger=Encoder.IRQ_MATCH2, value=-50_000)
    print(enc)

    _a = None
    _b = None
    _r = None
    _value = None

    while True:
        __a = a.value()
        __b = b.value()
        __r = r.value()
        __value = enc.value()

        if (_a != __a) or (_b != __b) or (_r != __r) or (_value != __value):
            _a = __a
            _b = __b
            _r = __r
            _value = __value

            print("a={}, b={}, r={}, value={:10}".format(_a, _b, _r, _value), end='        \r')

        sleep(0.1)

finally:
    try:
        # use enc.deinit(), otherwise enc.callback() will work after exiting the program
        enc.deinit()
    except:
        pass

%Run -c $EDITOR_CONTENT

A Pin(16) 0
B Pin(17) 1
Z Pin(18) 1
Encoder(0, phase_a=Pin(16), phase_b=Pin(17), x124=4, match1=50000, match2=-50000)
a=0, b=1, r=1, value=         0        
Encoder value: 0
a=1, b=0, r=1, value=     49687        
Encoder value: 50001
a=1, b=0, r=1, value=     50634        
Encoder value: 49999
a=1, b=1, r=1, value=       526        
Encoder value: -1
a=0, b=1, r=1, value=    -42349        
Encoder value: -50007
a=0, b=0, r=1, value=    -50095        
Encoder value: -50000

@IhorNehrutsa
Copy link
Contributor Author

Add _src inverse pulse input.

.. class:: Counter(id, src=None, \*, direction=Counter.UP, _src=None, edge=Counter.RISING, filter_ns=0)

    The Counter starts to count immediately. Filtering is disabled.

      - *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.

      - *src* is the pulse input :ref:`machine.Pin <machine.Pin>` to be monitored.
        *src* is required in the constructor.

      - *direction* specifies the direction to count. Values for this include the constants

        - Counter.UP - (default value) and
        - Counter.DOWN to control the direction by software or
        - :ref:`machine.Pin <machine.Pin>` object to control the direction externally. If ``Pin.value()``:
           - 0 - count down,
           - 1 - count up.

      - *_src* is the inverse pulse input :ref:`machine.Pin <machine.Pin>` to be monitored.
        If the *_src* keyword is present then the *direction* keyword does not matter.
        *src* and *_src* count in opposite directions, one in the UP direction
        and the other in the DOWN direction, i.e. as an incremental/decremental counter.

      - *edge* specifies which edges of the input signal will be counted by Counter:

        - Counter.RISING : raise edges,
        - Counter.FALLING : fall edges,
        - Counter.RISING | Counter.FALLING : both edges.

      - *filter_ns* specifies a ns-value for the minimal time a signal has to be stable
        at the input to be recognized. The largest value is 12787ns (1023 * 1000000000 / APB_CLK_FREQ).
        The default is 0 – no filter.

@IhorNehrutsa IhorNehrutsa force-pushed the ESP32_PCNT_Encoder_Counter branch 2 times, most recently from a58be0b to 3b44fa5 Compare November 10, 2023 08:34
@IhorNehrutsa IhorNehrutsa force-pushed the ESP32_PCNT_Encoder_Counter branch 2 times, most recently from d0a3849 to 4bd7b04 Compare January 30, 2024 09:08
Copy link

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS

Copy link

codecov bot commented Jan 30, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (d5b9681) 98.36% compared to head (c5bfb0d) 98.36%.
Report is 10 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #8766   +/-   ##
=======================================
  Coverage   98.36%   98.36%           
=======================================
  Files         159      159           
  Lines       21088    21093    +5     
=======================================
+ Hits        20743    20748    +5     
  Misses        345      345           

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

@IhorNehrutsa IhorNehrutsa force-pushed the ESP32_PCNT_Encoder_Counter branch 2 times, most recently from 76ffec5 to bcd081d Compare January 31, 2024 08:35
Signed-off-by: IhorNehrutsa <Ihor.Nehrutsa@gmail.com>
@projectgus
Copy link
Contributor

This is an automated heads-up that we've just merged a Pull Request
that removes the STATIC macro from MicroPython's C API.

See #13763

A search suggests this PR might apply the STATIC macro to some C code. If it
does, then next time you rebase the PR (or merge from master) then you should
please replace all the STATIC keywords with static.

Although this is an automated message, feel free to @-reply to me directly if
you have any questions about this.

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.

None yet

5 participants