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:
- 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 \
- Copy file “machine_dec.c” into directory …/lv_micropython/ports/esp32
- In file “machine_dec.c” change function call at line 47 and line 49 from
machine_pin_get_gpio(...) to machine_pin_get_id(...)
- At same directory in file “modmachine.h” after line 18 insert line:
extern const mp_obj_type_t machine_dec_type;
- At same directory in file “modmachine.c” after line 257 insert line:
{ MP_ROM_QSTR(MP_QSTR_DEC), MP_ROM_PTR(&machine_dec_type) },
- 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
Best reagards,
Peter