Zum Inhalt springen →

BPI-Centi-S3 Rotary Encoder MicroPython Test

Auf dem BPI-Centi-S3 ist bei Lieferung die MicroPython Firmware „https://github.com/BPI-STEAM/BPI-Centi-S3-Doc/tree/main/micropython_st7789s3_firmware“ installiert. Diese bringt schon einige Module mit. Mit help(‚modules‘) kann man sich die Liste der Module ausgeben lassen.

>>> help('modules')
__main__          gc                uasyncio/stream   upysh
_boot             inisetup          ubinascii         urandom
_onewire          math              ubluetooth        ure
_thread           micropython       ucollections      urequests
_uasyncio         mip/__init__      ucryptolib        uselect
_webrepl          neopixel          uctypes           usocket
apa106            network           uerrno            ussl
btree             ntptime           uhashlib          ustruct
builtins          onewire           uheapq            usys
cmath             st7789            uio               utime
dht               uarray            ujson             utimeq
ds18x20           uasyncio/__init__ umachine          uwebsocket
esp               uasyncio/core     umqtt/robust      uzlib
esp32             uasyncio/event    umqtt/simple      webrepl
flashbdev         uasyncio/funcs    uos               webrepl_setup
framebuf          uasyncio/lock     uplatform
Plus any modules on the filesystem

Zusätzlich zu den Modulen befindet sich auf dem Board noch eine Reihe von Dateien.

Darunter befindet sich auch die Datei rotary.py und rotary_irq_esp.py. Offensichtlich 2 Python Dateien die den Code für den Rotary Encoder enthalten. Schön, dass man den nicht selber schreiben muss :-).

# rotary.py
# MIT License (MIT)
# Copyright (c) 2022 Mike Teachman
# https://opensource.org/licenses/MIT

# Platform-independent MicroPython code for the rotary encoder module

# Documentation:
#   https://github.com/MikeTeachman/micropython-rotary

import micropython

_DIR_CW = const(0x10)  # Clockwise step
_DIR_CCW = const(0x20)  # Counter-clockwise step

# Rotary Encoder States
_R_START = const(0x0)
_R_CW_1 = const(0x1)
_R_CW_2 = const(0x2)
_R_CW_3 = const(0x3)
_R_CCW_1 = const(0x4)
_R_CCW_2 = const(0x5)
_R_CCW_3 = const(0x6)
_R_ILLEGAL = const(0x7)

_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

_transition_table_half_step = [
    [_R_CW_3,            _R_CW_2,  _R_CW_1,  _R_START],
    [_R_CW_3 | _DIR_CCW, _R_START, _R_CW_1,  _R_START],
    [_R_CW_3 | _DIR_CW,  _R_CW_2,  _R_START, _R_START],
    [_R_CW_3,            _R_CCW_2, _R_CCW_1, _R_START],
    [_R_CW_3,            _R_CW_2,  _R_CCW_1, _R_START | _DIR_CW],
    [_R_CW_3,            _R_CCW_2, _R_CW_3,  _R_START | _DIR_CCW],
    [_R_START,           _R_START, _R_START, _R_START],
    [_R_START,           _R_START, _R_START, _R_START]]

_STATE_MASK = const(0x07)
_DIR_MASK = const(0x30)


def _wrap(value, incr, lower_bound, upper_bound):
    range = upper_bound - lower_bound + 1
    value = value + incr

    if value < lower_bound:
        value += range * ((lower_bound - value) // range + 1)

    return lower_bound + (value - lower_bound) % range


def _bound(value, incr, lower_bound, upper_bound):
    return min(upper_bound, max(lower_bound, value + incr))


def _trigger(rotary_instance):
    for listener in rotary_instance._listener:
        listener()


class Rotary(object):

    RANGE_UNBOUNDED = const(1)
    RANGE_WRAP = const(2)
    RANGE_BOUNDED = const(3)

    def __init__(self, min_val, max_val, incr, reverse, range_mode, half_step, invert):
        self._min_val = min_val
        self._max_val = max_val
        self._incr = incr
        self._reverse = -1 if reverse else 1
        self._range_mode = range_mode
        self._value = min_val
        self._state = _R_START
        self._half_step = half_step
        self._invert = invert
        self._listener = []

    def set(self, value=None, min_val=None, incr=None,
            max_val=None, reverse=None, range_mode=None):
        # disable DT and CLK pin interrupts
        self._hal_disable_irq()

        if value is not None:
            self._value = value
        if min_val is not None:
            self._min_val = min_val
        if max_val is not None:
            self._max_val = max_val
        if incr is not None:
            self._incr = incr
        if reverse is not None:
            self._reverse = -1 if reverse else 1
        if range_mode is not None:
            self._range_mode = range_mode
        self._state = _R_START

        # enable DT and CLK pin interrupts
        self._hal_enable_irq()

    def value(self):
        return self._value

    def reset(self):
        self._value = 0

    def close(self):
        self._hal_close()

    def add_listener(self, l):
        self._listener.append(l)

    def remove_listener(self, l):
        if l not in self._listener:
            raise ValueError('{} is not an installed listener'.format(l))
        self._listener.remove(l)
        
    def _process_rotary_pins(self, pin):
        old_value = self._value
        clk_dt_pins = (self._hal_get_clk_value() <<
                       1) | self._hal_get_dt_value()
                       
        if self._invert:
            clk_dt_pins = ~clk_dt_pins & 0x03
            
        # Determine next state
        if self._half_step:
            self._state = _transition_table_half_step[self._state &
                                                      _STATE_MASK][clk_dt_pins]
        else:
            self._state = _transition_table[self._state &
                                            _STATE_MASK][clk_dt_pins]
        direction = self._state & _DIR_MASK

        incr = 0
        if direction == _DIR_CW:
            incr = self._incr
        elif direction == _DIR_CCW:
            incr = -self._incr

        incr *= self._reverse

        if self._range_mode == self.RANGE_WRAP:
            self._value = _wrap(
                self._value,
                incr,
                self._min_val,
                self._max_val)
        elif self._range_mode == self.RANGE_BOUNDED:
            self._value = _bound(
                self._value,
                incr,
                self._min_val,
                self._max_val)
        else:
            self._value = self._value + incr

        try:
            if old_value != self._value and len(self._listener) != 0:
                _trigger(self)
        except:
            pass
# rotary_irq_esp.py
# MIT License (MIT)
# Copyright (c) 2020 Mike Teachman
# https://opensource.org/licenses/MIT

# Platform-specific MicroPython code for the rotary encoder module
# ESP8266/ESP32 implementation

# Documentation:
#   https://github.com/MikeTeachman/micropython-rotary

from machine import Pin
from rotary import Rotary
from sys import platform

_esp8266_deny_pins = [16]


class RotaryIRQ(Rotary):

    def __init__(self, pin_num_clk, pin_num_dt, min_val=0, max_val=10, incr=1,
                 reverse=False, range_mode=Rotary.RANGE_UNBOUNDED, pull_up=False, half_step=False, invert=False):

        if platform == 'esp8266':
            if pin_num_clk in _esp8266_deny_pins:
                raise ValueError(
                    '%s: Pin %d not allowed. Not Available for Interrupt: %s' %
                    (platform, pin_num_clk, _esp8266_deny_pins))
            if pin_num_dt in _esp8266_deny_pins:
                raise ValueError(
                    '%s: Pin %d not allowed. Not Available for Interrupt: %s' %
                    (platform, pin_num_dt, _esp8266_deny_pins))

        super().__init__(min_val, max_val, incr, reverse, range_mode, half_step, invert)

        if pull_up == True:
            self._pin_clk = Pin(pin_num_clk, Pin.IN, Pin.PULL_UP)
            self._pin_dt = Pin(pin_num_dt, Pin.IN, Pin.PULL_UP)
        else:
            self._pin_clk = Pin(pin_num_clk, Pin.IN)
            self._pin_dt = Pin(pin_num_dt, Pin.IN)

        self._enable_clk_irq(self._process_rotary_pins)
        self._enable_dt_irq(self._process_rotary_pins)

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

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

    def _disable_clk_irq(self):
        self._pin_clk.irq(handler=None)

    def _disable_dt_irq(self):
        self._pin_dt.irq(handler=None)

    def _hal_get_clk_value(self):
        return self._pin_clk.value()

    def _hal_get_dt_value(self):
        return self._pin_dt.value()

    def _hal_enable_irq(self):
        self._enable_clk_irq(self._process_rotary_pins)
        self._enable_dt_irq(self._process_rotary_pins)

    def _hal_disable_irq(self):
        self._disable_clk_irq()
        self._disable_dt_irq()

    def _hal_close(self):
        self._hal_disable_irq()

Also dann mal sehen, was man damit so anstellen kann. Schnell einen kleinen Test geschrieben. Das folgende kleine Micrypython Skript initialisiert eine Instanz der RotaryIRQ Klasse. und registriert einen Listener, der automatisch aufgerufen wird, wenn sich der Wert des Rotary Encoders geändert hat. Der Konstruktor der Methode wird mit folgenden Parametern aufgerufen.

argumentdescriptionvalue
pin_num_clkGPIO pin connected to encoder CLK pininteger
pin_num_dtGPIO pin connected to encoder DT pininteger
min_valminimum value in the encoder range. Also the starting valueinteger
max_valmaximum value in the encoder range (not used when range_mode = RANGE_UNBOUNDED)integer
incramount count changes with each encoder clickinteger (default=1)
reversereverse count directionTrue or False(default)
range_modecount behavior at min_val and max_valRotaryIRQ.RANGE_UNBOUNDED(default) RotaryIRQ.RANGE_WRAP RotaryIRQ.RANGE_BOUNDED
pull_upenable internal pull up resistors. Use when rotary encoder hardware lacks pull up resistorsTrue or False(default)
half_stephalf-step modeTrue or False(default)
invertinvert the CLK and DT signals. Use when encoder resting value is CLK, DT = 00True or False(default)

Als init. Value wird die 0 eingetragen „encoder.set(value=0)“ und zum Schluss wird die callback Funktion einmal aufgerufen, damit schon mal eine Zahl auf dem Display angezeigt wird. In der callback Funktion wird dann einfach der Wert des Rotary Encoders auf dem Display angezeigt.

# BPI-Centi-S3 170x320 rotary encoder test 
import st7789
import tft_config
import vga1_bold_16x32 as font
from machine import Pin, PWM
from rotary_irq_esp import RotaryIRQ

tft = tft_config.config(rotation=1, options=0)
tft.init()
tft.fill(st7789.WHITE)
tft.show()

encoder = RotaryIRQ(
       37, 
       47, 
       min_val=-10, 
       max_val=10, 
       incr=1,
       reverse=False, 
       range_mode=RotaryIRQ.RANGE_UNBOUNDED,
       pull_up=False,
       half_step=False,
       invert=False)

def callback():
    value = encoder.value()
    tft.fill(st7789.WHITE)
    tft.text(font, "  " + str(value) + "  ", 120, 20, st7789.color565(0,102,204), st7789.WHITE, 255)
    tft.show()

encoder.add_listener(callback)
encoder.set(value=0)
callback()

Das ganze sieht dann wie folgt aus:

Banana PI BPI Centi S3 MicroPython Rotary Encoder Test

Funktioniert doch gut.

Veröffentlicht in Allgemein