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