Giter Club home page Giter Club logo

micropython-rotary's Introduction

MicroPython Rotary Encoder Driver

A MicroPython driver to read a rotary encoder. Works with Pyboard, Raspberry Pi Pico, ESP8266, and ESP32 development boards. This is a robust implementation providing effective debouncing of encoder contacts. It uses two GPIO pins configured to trigger interrupts, following Ben Buxton's implementation:

Key Implementation Features

Interrupt based

Whenever encoder pins DT and CLK change value a hardware interrupt is generated. This interrupt causes a python-based interrupt service routine (ISR) to run. The ISR interrupts normal code execution to process state changes in the encoder pins.

Transition state machine

A gray code based transition state table is used to process the DT and CLK changes. The use of the state table leads to accurate encoder counts and effective switch debouncing. Credit: Ben Buxton

File Installation

Two files are needed to use this module

  • platform-independent file rotary.py - a core file for all development boards
  • platform-specific file:
    • rotary_irq_esp.py Platform-specific code for ESP8266 and ESP32 development boards
    • rotary_irq_pyb.py Platform-specific code for Pyboard development boards
    • rotary_irq_rp2.py Platform-specific code for Raspberry Pi Pico development boards

Copying files to development boards

Copy files to the internal MicroPython filesystem using a utility such as ampy or rshell Ampy example below for Pyboards. Note: -d1 option is often needed for ESP8266 boards

ampy -pCOMx put rotary.py
ampy -pCOMx put rotary_irq_pyb.py

mip install

Starting with MicroPython 1.20.0, it can be installed from mip via:

>>> import mip
>>> mip.install("github:miketeachman/micropython-rotary")

Or from mpremote via:

mpremote mip install github:miketeachman/micropython-rotary

Class RotaryIRQ

Constructor

   RotaryIRQ(
       pin_num_clk, 
       pin_num_dt, 
       min_val=0, 
       max_val=10, 
       incr=1,
       reverse=False, 
       range_mode=RotaryIRQ.RANGE_UNBOUNDED,
       pull_up=False,
       half_step=False,
       invert=False)
argument description value
pin_num_clk GPIO pin connected to encoder CLK pin integer
pin_num_dt GPIO pin connected to encoder DT pin integer
min_val minimum value in the encoder range. Also the starting value integer
max_val maximum value in the encoder range (not used when range_mode = RANGE_UNBOUNDED) integer
incr amount count changes with each encoder click integer (default=1)
reverse reverse count direction True or False(default)
range_mode count behavior at min_val and max_val RotaryIRQ.RANGE_UNBOUNDED(default) RotaryIRQ.RANGE_WRAP RotaryIRQ.RANGE_BOUNDED
pull_up enable internal pull up resistors. Use when rotary encoder hardware lacks pull up resistors True or False(default)
half_step half-step mode True or False(default)
invert invert the CLK and DT signals. Use when encoder resting value is CLK, DT = 00 True or False(default)
range_mode description
RotaryIRQ.RANGE_UNBOUNDED encoder has no bounds on the counting range
RotaryIRQ.RANGE_WRAP encoder will count up to max_val then wrap to minimum value (similar behaviour for count down)
RotaryIRQ.RANGE_BOUNDED encoder will count up to max_val then stop. Count down stops at min_val

Methods

value() Return the encoder value


set(value=None, min_val=None, max_val=None, incr=None, reverse=None, range_mode=None) Set encoder value and internal configuration parameters. See constructor for argument descriptions. None indicates no change to the configuration parameter

Examples:

  • set(min_val=0, max_val=59) change encoder bounds - useful to set minutes on a clock display
  • set(value=6) change encoder value to 6. calling value() will now return 6

reset() set encoder value to min_val. Redundant with the addition of the set() method. Retained for backwards compatibility)


add_listener(function) add a callback function that will be called on each change of encoder count


remove_listener(function) remove a previously added callback function


close() deactivate microcontroller pins used to read encoder

Note: None of the arguments are checked for configuration errors.

Example

  • CLK pin attached to GPIO12
  • DT pin attached to GPIO13
  • GND pin attached to GND
  • + pin attached to 3.3V
  • Range mode = RotaryIRQ.RANGE_WRAP
  • Range 0...5
import time
from rotary_irq_esp import RotaryIRQ

r = RotaryIRQ(pin_num_clk=12, 
              pin_num_dt=13, 
              min_val=0, 
              max_val=5, 
              reverse=False, 
              range_mode=RotaryIRQ.RANGE_WRAP)
              
val_old = r.value()
while True:
    val_new = r.value()
    
    if val_old != val_new:
        val_old = val_new
        print('result =', val_new)
        
    time.sleep_ms(50)
  • For clockwise turning the encoder will count 0,1,2,3,4,5,0,1 ...
  • For counter-clockwise turning the encoder will count 0,5,4,3,2,1,0,5,4 ....

Tested With:

Development Boards

  • Pyboard D
  • PYBv1.1
  • TinyPico
  • Lolin D32 (ESP32)
  • Lolin D32 Pro (ESP32 with 4MB PSRAM)
  • Adafruit Feather Huzzah ESP8266
  • Adafruit Feather Huzzah ESP32
  • Raspberry Pi Pico
  • Raspberry Pi Pico W

Rotary Encoders

  • KY-040 rotary encoder
  • Fermion: EC11 Rotary Encoder Module (thanks @sfblackwell)

Wiring for KY-040 encoder

Encoder Pin Connection
+ 3.3V
GND Ground
DT GPIO pin
CLK GPIO pin

Recommended ESP8266 input pins

This Rotary module requires pins that support interrupts. The following ESP8266 GPIO pins are recommended for this rotary encoder module

  • GPIO4
  • GPIO5
  • GPIO12
  • GPIO13
  • GPIO14

The following ESP8266 GPIO pins should be used with caution. There is a risk that the state of the CLK and DT signals can affect the boot sequence. When possible, use other GPIO pins.

  • GPIO0 - used to detect boot-mode. Bootloader runs when pin is low during powerup.
  • GPIO2 - used to detect boot-mode. Attached to pull-up resistor.
  • GPIO15 - used to detect boot-mode. Attached to pull-down resistor.

One pin does not support interrupts.

  • GPIO16 - does not support interrupts.

Recommended ESP32 input pins

This Rotary module requires pins that support interrupts. All ESP32 GPIO pins support interrupts.

The following ESP32 GPIO strapping pins should be used with caution. There is a risk that the state of the CLK and DT signals can affect the boot sequence. When possible, use other GPIO pins.

  • GPIO0 - used to detect boot-mode. Bootloader runs when pin is low during powerup. Internal pull-up resistor.
  • GPIO2 - used to enter serial bootloader. Internal pull-down resistor.
  • GPIO4 - technical reference indicates this is a strapping pin, but usage is not described. Internal pull-down resistor.
  • GPIO5 - used to configure SDIO Slave. Internal pull-up resistor.
  • GPIO12 - used to select flash voltage. Internal pull-down resistor.
  • GPIO15 - used to configure silencing of boot messages. Internal pull-up resistor.

Examples

MicroPython example code is contained in the Examples folder
simple example
uasyncio example
uasyncio with classes example

Oscilloscope Captures

CLK and DT transitions captured on an oscilloscope. CLK = Yellow. DT = Blue

One clockwise step cw

One counter-clockwise step ccw

Board Hall of Fame

Testing with Pyboard D, Pyboard v1.1, TinyPico, and Raspberry Pi Pico development boards pyboard d pyboard b tiny pico raspberry pi pico

Acknowlegements

This MicroPython implementation is an adaptation of Ben Buxton's C++ work:

Other implementation ideas and techniques taken from:

Future Ambitions

  • argument error checking

micropython-rotary's People

Contributors

cameronbunce avatar epmoyer avatar markkamp avatar miketeachman avatar wind-stormger avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

micropython-rotary's Issues

Listener is being skipped sometimes

Nice lib, it was really hard trying to handle all the states. But I encountered a problem, listener is not always called.
Using Raspberry Pico with program:

from rotary_irq_rp2 import RotaryIRQ

rotary = RotaryIRQ(
       pin_num_clk=6, 
       pin_num_dt=11, 
       min_val=0, 
       max_val=10, 
       reverse=False, 
       range_mode=RotaryIRQ.RANGE_UNBOUNDED,
       pull_up=True,
       half_step=False)

lastRotation = rotary.value()
def rotated():
    global lastRotation
    newValue = rotary.value();
    print(newValue)
    if lastRotation < newValue:
        print('clockwise ')
    else:
        print('counterclockwise ')
    lastRotation = newValue
    
rotary.add_listener(rotated)

If I edit ant print error instead of swallowing in rotary.py line 162, it prints schedule queue full
Maybe the problem lies in my rotary encoder taken from an old pc. When I tried writing my own code, it seemed to have a lot of small state changes that changes back before being read.

Counter get stuck on "schedule queue full" error

With a "binary" bounded range (-1 or 1), when the "schedule queue full" RuntimeError is encountered in rotary.py, it blocks all future inputs occuring in the same direction. Thus, the rotary encoder must be rotated in the opposite direction to change self._value.

For my use case, the solution was to reverse the saved value on error instead of silencing it :

        try:
            if old_value != self._value and len(self._listener) != 0:
                micropython.schedule(_trigger, self)
        except RuntimeError:
            self._value = 1 if not self.value else 0

However this will break other range modes such as RotaryIRQ.RANGE_UNBOUNDED.

Below is a sample code derived from the async class example:

import uasyncio as asyncio
from rotary_irq_rp2 import RotaryIRQ


class RotaryEncoder():
    def __init__(self, rotary_encoder, callback):
        self.cb = callback
        self.re = rotary_encoder
        self.re.add_listener(self._callback)
        self.re.reset()
        self.event = asyncio.Event()
        asyncio.create_task(self._activate())

    async def _activate(self):
        while True:
            await self.event.wait()
            value = self.re.value()
            if value:
                self.cb(value)
                self.re.reset()
            self.event.clear()

    def _callback(self):
        self.event.set()


async def main():
    def re_callback(value):
        print("$", value)

    RotaryEncoder(RotaryIRQ(pin_num_clk=16,
                            pin_num_dt=17,
                            min_val=-1,
                            max_val=1,
                            range_mode=RotaryIRQ.RANGE_BOUNDED),
                  re_callback)

    while True:
        await asyncio.sleep_ms(10)

try:
    asyncio.run(main())
except (KeyboardInterrupt, Exception) as e:
    print('Exception {} {}\n'.format(type(e).__name__, e))
finally:
    ret = asyncio.new_event_loop()  # Clear retained uasyncio state

Use multiple rotary encoders with SN74HC165 as input expander

Hi Mike,

I have a project where I want to use five rotary encoders. There are of course GPIO expander ICs such as the MCP23017 but to use that kind of chip would bring in a lot of unnecessary complexity since all we need are inputs. The SN74HC165 is a commonly available 8 bit parallel-load shift register. So I imagine chaining two SN74HC165s together and then at short intervals reading the 16 bit value that contains the 15 bits representing the pins of the five rotary encoders. I could come up with some ideas for how to then link this to the micropython-rotary library, bit perhaps the smarter way to go is to let the author himself lead the way.

Guustaaf

two encodes simultaneous on one esp32, one or other but not both work

Hi, I have a dual encoder (two encoders) connected to micropython on my esp32.
If I connect one or the other it works as expected.
however if I try to setup both at the same time, the first defined encoder works
the second one barly at all, ie it may work if you spin it real fast bot mostly it won't

is there some contention issue because they may be sharing interupts ?
please help.

Starting value separate from min value

Thank you for creating this - it's very nice.
Is it possible to set a starting value that is different from the min value?

Edit: I read the documentation and it's in there! Thanks.

Thanks,
Fred

Thonny issue

Hi I am Thonny 4.1.3 and trying to install 'micropython-umqtt.simple' but each time I get the following error message in the Thonny package manager dialogue box.

install --progress-bar off micropython-umqtt.simple
PROBLEM IN THONNY'S BACK-END: Exception while handling 'install_distributions' (AssertionError).
See Thonny's backend.log for more info.
You may need to press "Stop/Restart" or hard-reset your MicroPython device and try again.

Process ended with exit code 1.

Can anyone please help?

Thanks

RFC: Design concept

For context please see this doc on the design of encoder drivers and this driver.

The following specifics are potential issues:

  1. The runtime of the ISR's is relatively long, and may be extended if there are user callbacks. This runs the risk of reentrancy because the frequency of contact bounce may be arbitrarily high.
  2. The latency incurred by soft ISR's increases the risk of reentrancy or missed bounce transitions.
  3. User callbacks run in an ISR context. This can cause unexpected hazards particularly if the callbacks run complex code.
  4. It is possible to design the ISR's so that they run very fast by replacing the lookup tables with bitwise logic and by delegating to a non-ISR task operations such as rate division, end-stops and running callbacks.
  5. Doing this enables callbacks to run outside of an ISR context removing concurrency hazards.

The driver cited above has the following characteristics. ISR's run in 36μs on a Pyboard 1.1 and 50μs on ESP32 (soft ISR's run fast, but are subject to latency of multiple ms). The ISR design aims to allow for potential latency, detecting missed bounce transitions. Hard ISR's are used if available because latency limits allowable rotational speed.

Rotary push button

Hi Mike - thanks for sharing this. I've gotten my rotary encoder to work beautifully with my esp32. I was wondering whether you also have an implementation to read a button push from a rotary encoder with that feature. Thanks!

Is the transition table wrong?

I like this FSM approach, however I have doubts about the transition table. I checked the original Arduino implementation and found similar issues were also raised. Take the CW sequence as an example, to register a full cycle, CLK/DT should go through 00 -> 10 -> 11 -> 01 -> 00. Start from the initial state _R_START, when CLK/DT==10, the state proceeds to _R_CW_1 as expected. Afterwards, CLK/DT==11, the state should change from _R_CW_1 to _R_CW_2. At this point, the transition in the table is different. The table shows 11 results in _R_START and 00 results in _R_CW_2. Since majority of users reported back correct results, maybe I missed something here?

`_transition_table = [

# |------------- NEXT STATE -------------|            |CURRENT STATE|
# CLK/DT    CLK/DT     CLK/DT    CLK/DT
#   00        01         10        11
[_R_START, _R_CCW_1, _R_CW_1,  _R_START],             # _R_START
[_R_CW_2,  _R_START, _R_CW_1,  _R_START],             # _R_CW_1
[_R_CW_2,  _R_CW_3,  _R_CW_1,  _R_START],             # _R_CW_2
[_R_CW_2,  _R_CW_3,  _R_START, _R_START | _DIR_CW],   # _R_CW_3
[_R_CCW_2, _R_CCW_1, _R_START, _R_START],             # _R_CCW_1
[_R_CCW_2, _R_CCW_1, _R_CCW_3, _R_START],             # _R_CCW_2
[_R_CCW_2, _R_START, _R_CCW_3, _R_START | _DIR_CCW],  # _R_CCW_3
[_R_START, _R_START, _R_START, _R_START]]             # _R_ILLEGAL`

Skipping one step when reversing direction

I am trying out this library and it works great except when reversing direction. I forget the encoder model I am using but its the same across different model encoders. I use a hardware de-bouncer that feeds through a Schmidt Trigger.

If I turn clockwise I can count up, anti-clockwise it counts down but at the point of going from clock2wise to anti-clockwise I get one notch on the encoder that doesn't count.

I was using this library on the Atmega's that had the exact issue: https://github.com/mathertel/RotaryEncoder

I raised an issue: mathertel/RotaryEncoder#19

Seems that this was the issue "This may be caused by the fact that the version < 1.5.0 only reported a new value on state 3 (not on state 0)"

In his code I use FOUR0

enum class LatchMode {
    FOUR3 = 1, // 4 steps, Latch at position 3 only (compatible to older versions)
    FOUR0 = 2, // 4 steps, Latch at position 0 (reverse wirings)
    TWO03 = 3  // 2 steps, Latch at position 0 and 3 
  };

Would be great if I could get this one working for Micropython!

image

Cannot install library into Thonny

Good morning,

Im trying to install the library into thonny 4.1.4,
Windows 10
Python 3.10.11
Tk 8.6.13

And is not possible, I think because that version is not supporting MIP but Pipy, how can I install it?
Even If I put the libraries in the same folder or opening simultaneusly just return me:

ImportError: no module named 'rotary_irq_rp2'

Thank you,

Kind Regards

Using GPIO Extender

Hi Mike,
Firstly, thanks for an excellent MicroPython library for the rotary encoder. So many resources use Circuit Python or Arduino only, so it's great to come across this!

My issue is that I'm using an MCP 23017 to extend the number of GPIO pins. This is because I'm using a Pico to drive an LED matrix and almost all the GPIOs are used for this purpose. I have an adafruit 23017 GPIO extender board working on I2C (using QWIIC connection).

I address the pins like this (where x is the pin number I'm addressing):

mcp = mcp23017.MCP23017(i2c, 0x20)
mcp.pin(x, mode=1, pullup=True)

My tiny brain can't work out a way to use these extended GPIO pins with your Rotary Encoder module. Is this possible?
Interrupts on the extender are through 2 pins (INTA works with the first 8 pins and INTB for the second 8). I assume I need to connect these interrupt pins to a spare GPIO on the Pico itself in order to get interrupts to work?

Many thanks
Dave

Listeners lost when instantiating multiple RotaryIRQs

Hi,

I'm having issues with the following sample code:

aaa = RotaryIRQ(
    pin_num_clk=26,
    pin_num_dt=27,
    min_val=0,
    max_val=10,
    reverse=True,
    range_mode=RotaryIRQ.RANGE_WRAP)

aaa.add_listener(lambda: print('aaa'))

bbb = RotaryIRQ(
    pin_num_clk=26,
    pin_num_dt=27,
    min_val=0,
    reverse=True,
    range_mode=RotaryIRQ.RANGE_UNBOUNDED)

bbb.add_listener(lambda: print('bbb'))

(Note: I'm using the same physical encoder with the same pins, but with otherwise different parameters. Also, I'm using a Raspberry Pi Pico with the latest version of Micropython.)

I would expect to see both aaa and bbb printed every time I move the rotary encoder. However, only bbb gets printed.

In other terms, it looks like instantiating a second RotaryIRQ causes the first one to lose its listeners.

Is that a bug or am I missing something?

Thanks,

Julien

Locks up if a thread is run on core1: Pico Pi W

On Micropython 1.20 if you start a thread on Core1, even if that thread does nothing then when an IRQ is triggered from the rotary encoder on code running on Core0 the whole machine locks up e.g

on core 0

t = start_new_thread(EventManager.Start, (onKeyDown,onKeyUp))
and then some code after that to interact with the encoder using this library, which works perfectly if the thread isn't spawned first.

The thread on core 1 is intended as a class which would handle a touchscreen - I have commented out that code because the failure occurs without it. However the code does work fine on Core1. But then running anything on Core1 even the dummy code above, seems to cause the rotary encoder code to fail. (note: python indentation seems to get lost when I save the issue)

class EventManager:
@staticmethod
def Start(onKeyDown, onKeyUp):
while True:
a = 1

Problem occurs even if the while loop just has a call to time.sleep_ms(1000), it seems like if there is a process running on Core1 then IRQs fail.

tested with EC11 encoder

Hi Mike,

I have been using your lib with Pi Pico and the Fermion: EC11 Rotary Encoder Module

https://www.dfrobot.com/product-1611.html

and all appears ok.

Also tested with two on these encoders on same board at same time.

The push button on the encoder also works and is triggered by pushing the rotary button down. This uses the normal 'Pin(GPx, Pin.IN, Pin.PULL_UP)'.

Regards

Simon

How about including push button handling?

First of all, many thanks for your contribution. I tried quite a few solutions on encoder handling, yours is the most complete and stable one i found.

Talking about complete:
How about including push-button handling?
This should be an easy thing to do - and very handy.

Best regards
Wolf

needs pull-up

Thank you , great work! My (cheap, 2 pin,24 count) encoders need a pull up to work. I did the pull-up before calling the routine and that stays set as long as the class'es Pin creation doesn't change it internally. (It doesn't currently.) Obviously must work without it in many cases so if you don't want to change it in the class, maybe just a note in the example? Thank's again!

uasyncio support

Thank for your work on this. I admire people like yourself and only wish I was smart as those that keep these communities going!

I noticed one of your Future Ambitions was uasyncio support. Has their been any developments on this?

THanks again,

Brian

Works on Raspberry Pi Pico

I tried the code on my Pi Pico and it works (using the rotary_irq_esp32.py import). For my application I had to delete the sleep line in rotary_test.py, otherwise it would skip counts if I turned the switch at a moderate to fast rate. Of course if I wanted to do something else in the main loop, the timing issues may become a problem, but I'm just experimenting at this point.

The code could be tightened for the Pico to have an appropriate list of excluded pins, but I haven't looked them up yet.

Anyway, thanks for the code.

Arguments to callback functions

Hi - is there a way to pass an argument in the callback? When I attempt to do this the list shows a 'None' type.

>>> enc0._listener
[<function callback at 0x20007a90>, None]

I am sending CANbus messages when a rotary encoder is turned and a want to include the rotary encoder id. If I can't pass an argument I would need xx number of callback functions, one for each encoder.

It's counting every 2nd step or skipping steps

Hi,

first of all i like the implementation, great work.
I tested it on a ESP32 and I experienced that I have to turn the encoder 2 steps so that it is counted +1 or -1. Have you experienced this behaviour? But maybe it is just my encoder (30steps per Revolution).

A second issue is, that if i turn the encoder faster it skips steps. Is there a solution to this problem?

TypeError: function takes 8 positional arguments but 7 were given

to whom it may concern

hey trying to use your rotary encoder have had this library working before but that was a while ago when i started coding in python and i tried it again recently for a project and for some reason i am getting a type error for arguments despite inputting all arguments i think i have used it correctly but if not then i would like to know why it is not working here are my copies in case i missed something regarding the files.

please ignore the fact they are pdf github of all websites decides that you can't upload .py files for some reason in the issues section.

rotary copy.pdf

rotary_irq_rp2 copy.pdf

here is my init statement:
encoder = RotaryIRQ(pin_num_clk=Pin(2), pin_num_dt=Pin(3),min_val=0, max_val=menu_length,range_mode=RotaryIRQ.RANGE_WRAP,invert=False,half_step=False,pull_up=True)

regards jesse

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.