Example for working with a rotary encoder?

Hello,
now implemented it via interrupt handler (see down), which resulted in minor improvements. Seems that the SPI interrupt has higher priority, so the Pin interrupt is delayed/not executed during TFT update. On ESP32 Pin interrupt priority is not configurable. This results in a poor performance of the rotary decoder. It can be used, but maybe micropython is not the favorite language to implement such things…
But LittlevGL can also be used in C :slight_smile: and it was great fun to have some experience with it and micropython too.

Exists no way to update just a small part of the TFT-display, containing only the digits affected by the rotary-decoder, in much less time than described in the previour posts?

Attached see the micropython rotary encoder class, feel free to use it.

best regards,

Peter

"""
code for handling encoders

Author: sengp
Date: 2020-01-24

"""

from machine import Pin
import utime


class IncRotEc11:  # create class IncRotEc11
    """
    class for handling incremental rotary encoders including push momentary switch
        - based on PIN interrupt
        - debounce functionality
        - fastgear functionality
        - no need for timer interrupt or polling
    Code implemented following these sources:
        https://github.com/miketeachman/micropython-rotary
        http://www.buxtronix.net/2011/10/rotary-encoders-done-properly.html
        https://github.com/buxtronix/arduino/tree/master/libraries/Rotary

    Hint: rotation speed limited by micropython's low performance interrupt capabilities

    optimized for decoders like e.g. ALPS EC11 or BOURNS PEC11L series:
        - e.g. 30 detents/revolution, channel A and B each generating 15 pulses/revolution
        - 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 highly recommended to minimize interrupt frequency
            - do not enable internal pullup/pulldown for A/B channel at MCU input
            - at switch input internal pullup at MCU input enabled
    when turning slow:
        position inc- decrements by 1 in range of 0...MaxPos
    when turning fast:
        position inc- decrements by FAST_INC in range of 0...MaxPos

    __init__ arguments
        IncRotEc11(PinA, PinB, PinSwitch, MaxPos, InitPos)
            PinA
                pin number channel A
            PinB
                pin number channel B
            PinSwitch
                pin number switch
            MaxPos
                position maximum value
            InitPos
                position init value

    methods
        .update()
            called via PinA/PinB.IRQ_RISING or PinA/PinB.IRQ_FALLING interrupt
        .set(position)
            position
                0 ... MaxPos

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

    def __init__(self, PinA, PinB, PinSwitch, MaxPos, InitPos):  # method is automatically called when new instance of class is created
        self.MaxPos = MaxPos
        self.ChA = Pin(PinA, Pin.IN, None)  # encoder channel A, PinA, no internal pull-up or pull-down resistor
        self.ChB = Pin(PinB, Pin.IN, None)  # encoder channel A, PinB, no internal pull-up or pull-down resistor
        self.enable_ChA_irq(self.update)
        self.enable_ChB_irq(self.update)
        self.ABval = self.ChA.value() + (self.ChB.value() << 1)
        self.state = self.ABval
        self.switch = Pin(PinSwitch, Pin.IN, Pin.PULL_UP)  # encoder pushbutton, PinSwitch, enable internal pull-up resistor
        self.position = InitPos
        self.time_lastsample = utime.ticks_ms()
        self.inc = 1
        self.io27Val = 0  # DEBUG_ONLY
        self.io27 = Pin(27, Pin.OUT, value=0)  # DEBUG_ONLY create object, test output, GPIO27

    def enable_ChA_irq(self, callback=None):
        self.ChA.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=callback)

    def enable_ChB_irq(self, callback=None):
        self.ChB.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=callback)

    def update(self, pin):
        # state machine output values and masks
        CW = 0x10  # output indicator CW step
        CCW = 0x20  # output indicator CCW step
        STATE = 0xF  # state mask

        # rotary encoder states
        START0 = 0x0  # detent
        CW1 = 0x1  # state clockwise next START0
        CCW2 = 0x2  # state counterclockwise next START0
        START3 = 0x3  # detent
        CW2 = 0x4  # state clockwise next START3
        CCW1 = 0x5  # state counterclockwise next START3
        END3C = START3 | CW  # end state clockwise reached from START0, output CW
        END3CC = START3 | CCW  # end state counterclockwise reached from START0, output CCW
        END0C = START0 | CW  # end state clockwise reached from START3, output CW
        END0CC = START0 | CCW  # end state counterclockwise reached from START3, output CCW

        table = [
            # state transition table, inputs: ABval, state
            #  |------------ ABval -------------|
            #  |    0       1       2       3   |
            #  |------ --- next state ----------| |-- state --|
                [START0,    CW1,   CCW2, START3],   # START0
                [START0,    CW1, START0,  END3C],   # CW1
                [START0, START0,   CCW2, END3CC],   # CCW2
                [START3,   CCW1,    CW2, START3],   # START3
                [END0C,  START3,    CW2, START3],   # CW2
                [END0CC,   CCW1, START3, START3]]   # CCW1

        FAST_PERIOD = 25  # "fast gear" acquire period in ms
        FAST_INC = 10  # "fast gear" step-with

        self.ABval = self.ChA.value() + (self.ChB.value() << 1)
        self.state = (table[self.state][self.ABval])
        if self.state > STATE:  # output indicator set ?
            # inc- or decrement
            self.timenow = utime.ticks_ms()  # get time
            if utime.ticks_diff(self.timenow, self.time_lastsample) < FAST_PERIOD:  # fastgear?
                self.inc = FAST_INC  # fastgear
            else:
                self.inc = 1  # step by step
            self.time_lastsample = self.timenow
            if (self.state & CW) != 0:  # clockwise ?
                self.position += self.inc  # increment
            else:
                self.position -= self.inc  # decrement
        self.state = self.state & STATE  # remove output indicators from state
        if self.position > self.MaxPos:  # overflow?
            self.position = 0
        elif self.position < 0:  # underflow?
            self.position = self.MaxPos
        self.io27Val = not self.io27Val  # DEBUG_ONLY toggle test output to show execution of IRQ
        self.io27.value(self.io27Val)  # DEBUG_ONLY

    def set(self, pos):
        if (pos >= 0) and (pos <= self.MaxPos):
            self.position = pos