Example for working with a rotary encoder?

Hello,

now I will sum up my results about handling rotary encoders using micropython and LittlevGL on ESP32.

Software (polling) and pin interrupt based approaches to implement a rotary encoder interface on the ESP32, all failed. Display update by LittlevGL (via “lv.scr_load(scr)”) takes about 50…120ms. During this time the encoder interface is almost blind and can not count pulses, leading to missing pulses and so unreliable behavior. The same effect will occur when not using LittlevGL, but if some other high priority functionality would need some processing time…

But there is a solution for ESP32 - named pulse counter module (PULSE_CNT):
https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/pcnt.html
In short: on ESP32 eight (8) hardware counter based interfaces for rotary (incremental) encoders can run simultaneously when the hardware is configured in a suitable way.

Fortunately someone already documented the behavior and implemented the code.

Much thanks to Mr. Boser for code, documentation and support !

At
https://people.eecs.berkeley.edu/~boser/courses/49_sp_2019/N_gpio.html#_quadrature_decoder
exists a description how these counting units can be used.

The code for implementing the feature can be found at:
https://github.com/bboser/MicroPython_ESP32_psRAM_LoBo/blob/quad_decoder/MicroPython_BUILD/components/micropython/esp32/machine_dec.c

The C-code “machine_dec.c” can be integrated into lv_micropython V1.12 or standard micropython.
Commit for “loboris” micropython port see at:
https://github.com/bboser/MicroPython_ESP32_psRAM_LoBo/commit/15806f44ad565e77967da57d67ad068001f44e28

To implement “DEC” functionality into lv-micropython 1.12 execute following steps:

  1. At directory …/lv_micropython/ports/esp32 add following line to “Makefile” at section “# List of MicroPython source and object files” after line “machine_pwm.c \”:
    machine_dec.c \
  2. Copy file “machine_dec.c” into directory …/lv_micropython/ports/esp32
  3. In file “machine_dec.c” change function call at line 47 and line 49 from
    machine_pin_get_gpio(...) to machine_pin_get_id(...)
  4. At same directory in file “modmachine.h” after line 18 insert line:
    extern const mp_obj_type_t machine_dec_type;
  5. At same directory in file “modmachine.c” after line 257 insert line:
    { MP_ROM_QSTR(MP_QSTR_DEC), MP_ROM_PTR(&machine_dec_type) },
  6. Re-compile lv-micropython

Afterwards module “machine” on ESP32 will include quadrature decoder peripheral “DEC”. Deviant from documentation the feature “dec.filter(value)” is not implemented in the sources mentioned above. But working without filter was OK and lead to good results.

Hint: counting sequence of 16-Bit pulse count unit on ESP32, may be not be as expected and over/under-run behaviour of 16-Bit signed hardware counters must be considered when developing encoder algorithms.
Up/down: 0, 1, 2, 3, 2, 1, 0, -1, -2, -3, -2, -1, 0, 1, 2, 3, …
Up: 0, 1, 2, …, 32765, 32766, 0, 1, 2, 3, …
Down: 0, -1, -2, …, -32765, -32766, 0, -1, -2, -3, …

To simplify the use of rotary encoders module “encdec.py” including class “IncRotEc11” and based on “DEC” was coded, see remarks at module/class description below:

"""
code for handling encoders attched to ESP32 16-Bit pulse counting units

Counting sequence and over/under-run behaviour of hardware counters must be considered when using encoder algorithms
Up/down:    0, 1, 2, 3, 2, 1, 0, -1, -2, -3, -2, -1, 0, 1, 2, 3, 4, ...
Up:	        0, 1, 2, 3, ..., 32765, 32766, 0, 1, 2, 3, ...
Down:       0, -1, -2, -3, ..., -32765, -32766, 0, -1, -2, -3, ...

Author: sengp
Date: 2020-02-03

"""

from machine import Pin
from machine import DEC


class IncRotEc11:  # create class IncRotEc11
    """
    class for handling incremental rotary encoders including push momentary switch
        - hardware counter based on ESP32 pulse counting unit, 16-Bit signed
        - quadrature decoder based on ESP32 pulse counting unit
        - debounce functionality based on ESP32 pulse counting unit not implemented (filter missing at class DEC)
        - no need for timer interrupt or polling
    Code implemented and based on these sources:
        https://people.eecs.berkeley.edu/~boser/courses/49_sp_2019/N_gpio.html#_quadrature_decoder
        https://github.com/bboser/MicroPython_ESP32_psRAM_LoBo/blob/quad_decoder/MicroPython_BUILD/components/micropython/esp32/machine_dec.c

    ATTENTION: Prevent position error by avoiding over/under-run of hardware counter.
               Call of method "setpos" resets hardware counter value to 0. Call of method "setpos" at appropriate location in user program is a
               suitable practice to prevent hardware-counter overrun (use actual position as argument). Use method "getcnt" to obtain
               information about counter values span from becomming critical (over/under-run), or to implement fastgear functionality

    optimized for encoders like e.g. ALPS EC11 or BOURNS PEC11L series:
        - e.g. 30 detents/revolution and channel A and B each generating 15 pulses/revolution leads to 60 impulses/revolution
          Hint: on a decoder described above, impulses must be divided by 2 to result in one impulse/detent
        - channel A -> bit0, channel B -> bit1
        - cyclic AB values are (CW: left to right, CCW: right to left): ...,0,1,3,2,0,1,3,2,0,1,3,2,0,1,...
        - mechanical detents at AB value 0 and 3
        - suggested circuit to connect rotary encoder including switch:
            - A/B and switch common connected to GND
            - A/B connected via 10K pullup to VCC
            - A/B channel output low pass filtering via 10K/10nF, see BOURNS PEC11L datasheet
              Hint: low pass filtering is recommended to prevent glitches at counter input
            - do not enable internal pullup for A/B channel at MCU input when low pass filter is assembled
            - at switch input internal pullup at MCU input enabled
        - when turning right position increments by 1 in range of 0...MaxPos
        - when turning left position decrements by 1 in range of 0...MaxPos

    __init__ arguments
        IncRotEc11(Unit, PinA, PinB, PinSwitch, MaxPos, InitPos, Filter)
            Unit
                number of pulse count unit
                    0 ... 7
            PinA
                pin number channel A
            PinB
                pin number channel B
            PinSwitch
                pin number switch
            MaxPos
                position maximum value
            InitPos
                position init value
                    0 ... MaxPos

    methods
        .getpos()
            RetVal
                0 ... MaxPos
        .setpos(position)
            position
                0 ... MaxPos
        .getcnt()
            hardware counter value
                -32765 ... 32765

    variables
        .switch.value()
            0 = pressed
            1 = not pressed
        .position
            0 ... MaxPos
    """

    def __init__(self, Unit, PinA, PinB, PinSwitch, MaxPos, InitPos):  # method is automatically called when new instance of class is created
        self.dec = DEC(Unit, Pin(PinA), Pin(PinB))
        self.switch = Pin(PinSwitch, Pin.IN, Pin.PULL_UP)  # encoder pushbutton, PinSwitch, enable internal pull-up resistor
        self.MaxPos = MaxPos
        self.offset = InitPos
        self.count = 0
        self.position = self.calcpos()

    def calcpos(self,):
        # arguments in position calculation can be positive or negative. Following line of code is valid when using Python,
        # cause Python uses the "mathematical variant" of the modulo operator. This code will fail when it is ported to e.g.
        # C, C++ or Java, cause these languages implement the "symmetrical variant" of the modulo operator, leading to
        # wrong results (in this algorithm) on negative arguments.
        return ((self.count + self.offset) % (self.MaxPos + 1))

    def getpos(self,):
        self.count = (self.dec.count() >> 1)  # read counter and divide counter value by 2 to get one impulse/detent
        self.position = self.calcpos()
        return self.position

    def setpos(self, pos):
        if (pos >= 0) and (pos <= self.MaxPos):
            self.dec.clear()
            self.offset = pos
            self.count = 0
            self.position = self.calcpos()

    def getcnt(self,):
        return self.count

Sample program using module “encdec.py”:

from encdec import IncRotEc11
import utime

"""
Quadrature Decoder Demo

Rotary encoder ChA/ChB connected to pins 13/14, switch connected to pin 12

"""


UNIT = 0
CHA = 13
CHB = 14
SWITCH = 12
MAXPOS = 59
INITPOS = 30

val = 0
oldval = val


encoder = IncRotEc11(UNIT, CHA, CHB, SWITCH, MAXPOS, INITPOS)  # create new instance of class "IncRotEc11"


while True:
    val = encoder.getpos()
    if val != oldval:
        print("Position: {:5d}      Switch: {:1d}".format(val, encoder.switch.value()))
        oldval = val
    utime.sleep_ms(300)

have fun using rotary encoders :slight_smile:

Best reagards,
Peter

2 Likes