Hi,
Pls anyone will share an example which demostrates how to work with a rotary encoder (and groups)?
Thanks,
Kaiyuan
Hi,
Pls anyone will share an example which demostrates how to work with a rotary encoder (and groups)?
Thanks,
Kaiyuan
Hi @dukeduck,
Do you mean something like this?
A C code example would be easier to find, and it should be easy to convert it to Micropython.
@embeddedt / @kisvegabor - could you suggest usage examples (in C) for “rotary encoder (and groups)”?
Thanks @amirgon!
I found this post (ESP32 with Rotary Encoder - Simple example for Newbie) which is related to my question. But I’m pretty new to programming and don’t have a background in C language, by looking at the code in that post I’m a bit confused by the read_encoder function which returns a boolean - how is the turning direction and the pressing of the button data passed to the littlevgl?
Thanks,
Kaiyuan
data->enc_diff = diff - last_diff;;
data->state = btn_state;
Turning direction and value is set on enc_diff
and button state is set on state
.
The return value false
only means that there is no more data to read.
It should be very easy to translate this function to Micropython. Give it a try and let me know if you get stuck.
Hello,
recently worked on this topic. See class below (file “encoder.py”):
"""
code for handling encoders
"""
from machine import Pin
import utime
PERIOD_AQR = 70 # "fast gear" acquire period in ms
TRESH = 4 # "fast gear" speed treshhold
FASTINC = 60 # "fast gear" step-with
class IncRotEc11: # create class "IncRotEc11"
"""
class for handling incremental rotary encoders like e.g. ALPS EC11 series
when turning slow position inc- decrements by 1
when turning fast position inc- decrements by FASTINC
__init__ arguments
IncRotEc11(PinA, PinB, PinSwitch, MaxPos, InitPos)
PinA
pin number channelA
PinB
pin number channelA
PinSwitch
pin number switch
MaxPos
position maximum value
InitPos
position init value
methods
.update()
must be called in main loop or via timer interrupt
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, Pin.PULL_UP) # encoder channel A, PinA, enable internal pull-up resistor
self.ChB = Pin(PinB, Pin.IN, Pin.PULL_UP) # encoder channel A, PinB, enable internal pull-up resistor
self.state = self.ChA.value() + (self.ChB.value() << 1)
self.state_last = self.state
self.switch = Pin(PinSwitch, Pin.IN, Pin.PULL_UP) # encoder pushbutton, PinSwitch, enable internal pull-up resistor
self.pos = (InitPos * 2) + 1
self.position = InitPos
self.position_lastsample = self.position
self.time_lastsample = utime.ticks_ms()
self.table = [[0, 1, -1, 0], [-1, 0, 0, 1], [1, 0, 0, -1], [0, -1, 1, 0]] # incremental rotary encoder states
self.io27Val = 0# DEBUG_ONLY
self.io27 = Pin(27, Pin.OUT, value=0)# DEBUG_ONLY create object, test output, GPIO27
def update(self):
self.state = self.ChA.value() + (self.ChB.value() << 1)
i = (self.table[self.state_last][self.state])
self.state_last = self.state
self.pos += i
self.position = self.pos // 2 # filter out odd and unstable position values that may exist
self.timenow = utime.ticks_ms()
if utime.ticks_diff(self.timenow, self.time_lastsample) > PERIOD_AQR: # fastgear?
self.posdiff = self.position - self.position_lastsample
if abs(self.posdiff) > TRESH:
self.fastgear()
self.position_lastsample = self.position
self.time_lastsample = self.timenow
if self.position > self.MaxPos: # overflow?
self.reset()
elif self.position < 0: # underflow?
self.set()
self.io27Val = not self.io27Val# DEBUG_ONLY toggle test output to enable measuring length of program cycle
self.io27.value(self.io27Val)# DEBUG_ONLY
def reset(self):
self.pos = 0
self.position = 0
self.position_lastsample = self.position
def set(self):
self.pos = (self.MaxPos * 2) + 1
self.position = self.MaxPos
self.position_lastsample = self.position
def fastgear(self):
if self.posdiff > 0:
self.position = self.position + FASTINC
else:
self.position = self.position - FASTINC
self.pos = (self.position * 2) + 1
self.position_lastsample = self.position
Usage:
encoder = IncRotEc11(PINA, PINB, PINSWITCH, MAXPOS, INITPOS) # create new instance of class "IncRotEc11"
encoder.update() # read encoder, must be called in main loop or via timer interrupt
switch = encoder.switch.value() # read encoder switch value
pos = encoder.position # read encoder position
encoder.__init__(PINA, PINB, PINSWITCH, MAXPOS, INITPOS) # set encoder to init-position
encoder.reset() # reset some encoder internals
The code works fine as long as function encoder.update is called with a frequency that fits the speed of the turning encoder. At the moment I have the problem that another functionality takes to much time and can not be interrupted, so that missing steps arise. But that’s another problem I have to treat in a separate posting. Beware to delete the debug-code that may be in the sources!
Good luck!
Peter
@sengp Did you register this as an input device for lvgl? (lv.indev_drv_register
) If you did it could be useful to see that code as well.
@amirgon Posted the project to:
My projects
I have following design problem:
When turning the rotary encoder the alarm setting has to be updated. But a display update via “lv.scr_load(scr)” takes 50…120ms. During that time the main loop of the program pauses and the rotary encoder has missing steps, it still works but it’s not a satisfying behavior. Will it improve when registering it as an input device? I’ll read the docs and claim help if I don’t succed.
best regards,
Peter
Very nice.
I recommend, however, pushing your code to github instead of sending a zip file. It will be more accessible since most people will not risk downloading a zip file from a public forum.
Why does it take so long?
My experience is that full screen update can be done in ~30ms and even less when running ESP32 on 240Mhz, but it depends on what exactly you are rendering.
It is a better design to register an input device to lvgl. But I’m not sure this will solve this problem.
What you should do, is implement an interrupt handler that tracks changes in your input and updates the aggregated change since last lvgl polling.
An interrupt handler will run immediately even when “the main loop of the program pauses”.
Already tried to to implement the reading of the rotary encoder via a callback function. Did it by using a periodical timer interrupt.
tim = Timer(-1)
io27Val = 0
io27 = Pin(27, Pin.OUT, value=0) # create object, test output, GPIO27
def asd():
global io27Val
io27Val = not io27Val # toggle test output
io27.value(io27Val)
tim.init(period=10, mode=Timer.PERIODIC, callback=lambda t:asd()) # timer interrupt
But it did not succeed. During “lv.scr_load(scr)” the interrupt is not executed any more or just in an erratic way. Seems that the SPI transmission uses higher interrupt priority. Do you know a way to change interrupt priority on ESP32?
Or exists a way to just update a small part of the display content (the part containing the Alarm-time)?
Did you really see display updates lasting 30ms?
best regards
Peter
This is not what I meant.
You need an interrupt handler that is triggered by your hardware, not by a timer.
I’ll try someting like the following code, hope it can be done on ESP32:
configure an irq callback
p0.irq(lambda p:print(p))
Will make a reply if it works (or not)
best regards
Peter
That’s how LittlevGL works internally. It only updates the changed objects.
I find it weird that lv.scr_load
takes 30-50 ms. No rendering takes place inside that function; it merely changes the active screen and invalidates all objects on it (code here). My guess is that something else is taking up that time, or your measurement is incorrect.
Thanks @amirgon.
So, enc_diff should be integer? for example, a positive value means “next”, a negative value means “previous”? Then, how to pass “LV_KEY_LEFT” “LV_KEY_RIGHT” command to the lvgl?
Thanks,
Kaiyuan
@sengp Thanks Peter for sharing your project - I like the UI of your alarm, and the seg-7 font is awesome. I was trying to implement this using event callback as described in the docs, I think I’m getting closer.
Kaiyuan
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
Thanks for sharing this, Peter.
I can see a problem with your script: You are not using hard interrupts.
hard
if true a hardware interrupt is used. This reduces the delay between the pin change and the handler being called. Hard interrupt handlers may not allocate memory; see Writing interrupt handlers.
This explains the latency (performance) problem you are seeing.
What is actually happening is that the interrupt handler only schedules micropython to run your handler in the future (which can take some time) instead of immediately.
Hello,
on the micropython ESP32 port the “hard” argument is not available at class Pin, when using irq() method. The feature seems to be available on the ARM based ports but NOT on ESP32.
On the ESP32, MicroPython interrupt handlers are always scheduled for later execution by the interpreter.
So we will not get it working this way…
Found another promising attempt:
pulse count units
But the module mentioned is not included in standard micropython nor the “loboris” port mentioned. I’m already in contact with the author, maybe we can find the sources…
best regards,
Peter
You are right.
I don’t see a reason why this wouldn’t be implemented for ESP32, maybe worth opening an issue on micropython upstream repo.
This really sounds useful, and it will probably be added soon to micropython.
See this forum thread.
Another option is using the espidf module, which provides raw esp-idf micropython API.
I’m using it today to access SPI and I2S directly from micropython. (see for example the micropython ili9341 driver).
I’ve added pcnt support to espidf module (it really only required adding a single line!), so you can simply do something like:
import espidf as esp
esp.pcnt_unit_config({
# ...configure pcnt here...
})
...
cnt_ptr = esp.C_Pointer()
esp.pcnt_get_counter_value(esp.PCNT_UNIT._1, cnt_ptr)
print(cnt_ptr.short_val)
Hello,
thanks for your investigations. Made some big steps.
Had contact with the very nice Prof. at Berkeley University I mentioned in the last post. He gave me the hint where to find the sources of his implementation of the pulse count units on ESP32.
I already included them into lv-micropython V1.12. (machine module) and compiled successfully
Made some tests, the one he already delivered, and one of my own on real hardware.
–> NO MORE MISSING COUNTS DURING UPDATE OF THE DISPLAY!!!
Now i will make a module for (rotary) encoders that makes the whole thing nice to use, the abilities of the pulse count unit are very basic. Think that initial position, over/underflow and “fastgear” should be implemented. Hope to finish tomorrow.
Best regards,
Peter
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:
machine_dec.c \
machine_pin_get_gpio(...) to machine_pin_get_id(...)
extern const mp_obj_type_t machine_dec_type;
{ MP_ROM_QSTR(MP_QSTR_DEC), MP_ROM_PTR(&machine_dec_type) },
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