Fyzikální kabinet FyzKAB
TechHobby ESP32 + MicroPython ESP32: Čidla a senzory v MicroPythonu ESP32: Rotační enkodér a MicroPython

ESP32: Rotační enkodér a MicroPython

Dalším „čidlem“, které se dnes pokusíme k modulu ESP32 připojit a načítat (v našem, doufám, že již oblíbeném!) MicroPythonu je tzv. rotační enkodér. Volba na enkodér dnes padla hned ze dvou důvodů.

  1. Zaprvé jsme se mu zatím nevěnovali ani v článcích věnovaným programování modulu ESP32 v prostředí Arduino IDE. A nechceme, zde v pythonovských článcích jen kopírovat to, co zde již bylo (i když trochu jinak) uvedeno. Takže sem třeba přilákáme i ty, kteří se zatím nad MicroPythonem na modulu ESP32 trochu ofrňovali.
  2. Zadruhé si myslíme, že příběh kolem rotačního enkodéru by mohl být zase trochu zajímavý, neřku-li až poučný. Ono by se řeklo: „Takové zapojení tří spínačů, co by na tom mohlo být tak zajímavé, že?“ ale uvidíme…

Co je to rotační enkodér?

Rotační enkodér je typ snímače polohy, který převádí úhlovou polohu (otáčení) své osy na výstupní signál, který se pak použije k určení směru otáčení, případně rychlost otáčení nebo z počtu jeho „kroků“ lze určit úhel otočení. Rotační enkodér tak může fungovat třeba jako určitý zpětnovazebný prvek při natáčení motorů, ale klidně i jako takový moderní digitální ekvivalent potenciometru. V případě použití enkodéru jako lepšího potenciometru, jak jej budeme využívat v tomto článku, je jeho předností kupříkladu určitá robustnost plynoucí z toho, že se může plně otáčet bez koncových dorazů, zatímco potenciometr se může otáčet pouze asi o ¾ kruhu (ten, kdo občas pracoval s dětmi a potenciometry jistě ví, kdo tento souboj vždy opouští jako vítěz! 😒

Ale abychom vše nehodnotili jen podle toho, co děti dokáží, či nedokáží zdemolovat, zkusíme to shrnout. Potenciometry jsou nejlepší v situacích, kdy potřebujete znát přesnou polohu knoflíku a získaný výstup je v analogové podobě. Rotační enkodéry zase vítězí v situacích, kdy potřebujete znát změnu polohy místo přesné polohy a výstup získáváme v podobě digitálních stavů (pulzů).

Ještě než se pustíme do dalšího textu, ještě uvedeme, že existují dva typy rotačních enkodérů, a to tzv. absolutní a přírůstkové. Absolutní enkodér nám udává přesnou polohu své osy (knoflíku), zatímco přírůstkový (též tzv. inkrementální) enkodér hlásí, o kolik přírůstků se hřídel pootočila.

V našem článku budeme používat ten nejlevnější rotační enkodér, který je enkodérem přírůstkového typu.

Jak rotační enkodér funguje?

Rotační enkodér, který nadále budeme používat, je vytvořen ze dvou spínačů, které jsou spínány otáčejícím se diskem, na který je připojen společný vývod (zpravidla připojen na zemnění). Tím, jak se disk otáčí, postupně spíná první a druhý spínač ve směru jednom), nebo druhý a pak první (směr opačný). Naprosto hezky a ilustrativně to znázorňuje animace na obrázku č. 1, kde je též znázorněn průběh výstupních signálů.

rotary encoder working - animation
zdroj obrázku: https://lastminuteengineers.com/rotary-encoder-arduino-tutorial/
Obr. 1 – animace vysvětlující funkci enkodéru a ukazující průběh výstupních signálů

Jak jsme již naznačovali, když otočíte knoflíkem, vývody A a B se dostanou do kontaktu se společným uzemňovacím kotoučem v určitém pořadí podle směru, kterým knoflíkem otáčíte. Například při otáčení ve směru hodinových ručiček, nejdříve najede na kotouč vývod A a až za chvíli B. Budeme-li předpokládat, že vývody A a B budou pull-up rezistorem drženy na úrovni HIGH a uzemněním přejdou do stavu LOW, bude průběh signálů vývodů A a B (v tomto směru) následující – viz tabulka č. 1:

  krok 0 krok 1 krok 2 krok 3 krok 4
A (CLK) HIGH LOW LOW HIGH HIGH
B (DT) HIGH HIGH LOW LOW HIGH

Tab. č. 1 – úrovně výstupů A a B enkodéru při otáčení ve směru hodinových ručiček

Pokud začneme otáček osou enkodéru na druhou stranu (a opět předpokládáme výchozí polohu na konfiguraci HIGH, HIGH) je průběh signálů znázorněn v tabulce č. 2.

  krok 0 krok 1 krok 2 krok 3 krok 4
A (CLK) HIGH HIGH LOW LOW HIGH
B (DT) HIGH LOW LOW HIGH HIGH

Tab. č. 2 – úrovně výstupů A a B enkodéru při otáčení proti směru hodinových ručiček

Jak vidíme buď v animaci na obrázku č. 1, nebo v tabulkách č. 1a 2, signály A a B jsou navzájem posunuty, protože jeden kontakt vždy přichází do kontaktu s uzemněným kotoučem před druhým. Toto se nazývá tzv. kvadraturní kódování.

Rotační enkodér lze zakoupit buď ve zcela holé podobě, kdy jde jen o samotný rotační přepínač, nebo v podobě modulu, kdy je tento přepínač dodán na destičce plošného spoje s vývody a připájenou trojicí pull-up rezistorů na spodní straně – viz obrázek č. 2.

enkoder - holy a modul
Obr. 2 – srovnání „holého“ enkodéru a destičky modulu enkodéru

My pro naše hrátky s rotačním enkodérem a MicroPythonem na modulu ESP32 použijeme celý modul enkodéru, tedy rotační enkodér doplněný o trojici pull-up rezistorů. Ale pokud právě v ruce držíte samotný rotační enkodér, nezoufejte! Jak jistě víme, modul ESP32 je na svých digitálních vstupech osazen pull-up rezistory, takže v případě potřeby si je můžeme připnout softwarově přímo v modulu ESP32. A k čemu že jsou ty rezistory dobré? Na to nám asi nejlépe odpoví obrázek č. 3, na kterém je elektronické schéma modulu rotačního enkodéru.

Encoder schema funkce
Obr. 3 – elektronické schéma modulu rotačního enkodéru s pull-up rezistory

Jak vidíme na obrázku č. 3 pull-up rezistory drží výstupy modulu na úrovni napájecího napětí (HIGH) a spínání jednotlivých spínačů stahuje tyto piny na úroveň zemnění (LOW). Tím se generuje požadovaný výstupní signál v podobě stavů HIGH/LOW.

Psali jsme, že námi použitý enkodér pro určení směru a velikosti otočení používá dva spínače. Jak vidíme na schématu, jeden je označen jako CLK (click) a druhý DT (data). Pozorný čtenář si ale jistě všimne, že na obrázku je vyznačen ještě jeden spínač vyvedený na pin SW (switch). Tento spínač slouží na ose rotačního enkodéru jako tlačítko (spíná se zatlačením). Pomocí tohoto enkodéru tedy můžeme jeho otáčením nastavit nějakou hodnotu a stisknutím osy tuto hodnotu kupříkladu potvrdit. V našem obslužném programu se tedy pokusíme načítat i tuto odezvu enkodéru.

Popis pinů enkodéru:

  • CLK (výstup A) je primární výstupní impuls pro určení velikosti otáčení. Pokaždé, když se knoflík otočí o jednu polohu (krok) v libovolném směru, výstup CLK projde jednou změnou HIGH/LOW.
  • DT (výstup B) je stejný jako výstup CLK, ale zpožďuje se o 90° fázový posun. Tento výstup je používán k určení směru otáčení.
  • SW je výstup spínače tlačítka. Když stisknete knoflík, napětí na vývodu klesne na hodnotu LOW.
  • GND je zemnění (udává napěťovou úroveň LOW)
  • VCC je kladné napájecí napětí, výstupní signály HIGH odpovídají této hodnotě (v našem případě bude 3,3 V)

Výše uvedený popis pinů je uveden pro verzi enkodéru, který je zapájen v modulu s pull-up rezistory. Rotační enkodér, jak jsme již zmínili, lze zakoupit i v „holém stavu“, pak musíme doplnit rezistory dle schématu z obr. 3, nebo je nastavit softwarově na vstupních pinech modulu ESP32.

Připojení rotačního enkodéru k modulu ESP32

Jestliže jsme si řekli, že z rotačního enkodéru bude vystupovat trojice digitálních signálů – CLK a DT pro určení směru a počtu kroků a výstup SW pro vyhodnocení tlačítka – můžeme rotační enkodér připojit k libovolným GPIO pinům, které mohou sloužit jako digitální vstup. Jedno z množných zapojení zachycuje obrázek č. 4 (včetně tabulky propojení jednotlivých pinů enkodéru a modulu ESP32). Pokud bychom chtěli použít holý enkodér, připojíme jeho vývody ke stejným pinům (jen vynecháme pin 3V3, který bychom neměli stejně k čemu na enkodéru připojit) a na začátku programu použité GPIO piny nastavíme do režimu s interním pull-up rezistorem.

propojeni rotacni enkoder - ESP32
Obr. 4 – připojení rotačního enkodéru k modulu ESP32

Máme-li rotační enkóder připojený k modulu ESP32, začneme řešit načítání jeho výstupních hodnot.

A) načítání GPIO pinů hlavní smyčkou.

Začneme klasickým přístupem, kdy budeme postupně načítat pin CLK a DT (a SW) enkodéru v hlavní smyčce programu. Změnu otočení osy enkodéru vyhodnotíme pomocí změny hodnoty vstupu CLK. Nastane-li změna CLK, následně načteme hodnoty DT. Nyní se podívejme na obrázek č. 5, případně do tabulek č. 1 a č. 2. Budeme vzájemně porovnávat hodnoty CLK a DT.

prubeh signalu
Obr. 5 – průběh signálů na vývodech CLK a DT rotačního enkodéru

Z obrázku č. 5 vidíme, že v okamžiku změnu signálu CLK je hodnota signálu DT díky jeho fázovému posunu různá podle směru otáčení. Jestliže je hodnota DT rozdílná od hodnoty CLK, jedná se o otáčení ve směru hodinových ručiček, v opačném případě přesně naopak. Vyhodnocení směru otáčení tedy bude otázkou podmínky rovnosti/rozdílnosti vstupů CLK a DT. Pokud při každé změně výstupu CLK dle zjištěného stavu DT určíme nejen směr otáčení, ale zvýšíme/snížíme nějakou proměnnou, dokážeme tak určit i počet kroků, čili velikost otočení enkodéru.

Následující kód programu by měl dělat přesně to, co jsme zde nyní naznačili.

from machine import Pin
import time

# Nastavení pinů
pinCLK = Pin(22, Pin.IN) # Pin CLK na GPIO22
pinDT = Pin(23, Pin.IN)   # Pin DT na GPIO23
pinSW = Pin(21, Pin.IN, Pin.PULL_UP) # Pin SW na GPIO21 (s pull-up odporem)

# Proměnné pro uložení pozice a stavů
poziceEnkod = 0
stavPred = pinCLK.value() # Načtení počátečního stavu CLK
stavCLK = 0
stavSW = 0

# Hlavní smyčka
while True:
    # Načtení aktuálního stavu pinu CLK
    stavCLK = pinCLK.value()

    # Pokud je stav CLK odlišný od předchozího stavu, víme, že osa byla otočena
    if stavCLK != stavPred:
        # Pokud stav pinu DT neodpovídá stavu pinu CLK, rotace byla po směru hodin
        if pinDT.value() != stavCLK:
            print('Rotace vpravo => | ', end='')
            poziceEnkod += 1
        # Pokud stav pinu DT odpovídá stavu pinu CLK, rotace byla proti směru hodin
        else:
            print('Rotace vlevo <= | ', end='')
            poziceEnkod -= 1

        # Vytisknutí aktuální pozice enkodéru
        print(f"Pozice enkoderu: {poziceEnkod}")

    # Uložení posledního stavu pinu CLK pro porovnání v dalším cyklu
    stavPred = stavCLK

    # Načtení stavu pinu SW (tlačítko)
    stavSW = pinSW.value()

    # Pokud je tlačítko stisknuto (stav je LOW), vytiskni zprávu
    if stavSW == 0:
        print('Stisknuto tlacitko enkoderu!')
        time.sleep(0.5) # Zpoždění, aby se předešlo vícečetným registracím stisku

Popis kódu:

V první části importujeme potřebné moduly, jako je objekt Pin pro práci s piny modulu ESP32 a time potřebný pro potřeby vytvoření zpoždění. Dále nastavíme použité GPIO piny (21, 22, 23) jako digitální vstupy pro načítání stavu enkodéru.

Před samotnou hlavní nekonečnou smyčkou programu jsou deklarovány pomocné proměnné.

poziceEnkod = 0

stavPred = pinCLK.value()  # Načtení počátečního stavu CLK
stavCLK = 0
stavSW = 0

Proměnná poziceEnkod slouží jako proměnná pro pozici enkodéru. Při otáčení na jednu stranu budeme její hodnotu zvyšovat, při otáčení na druhou stranu naopak snižovat. Proměnné stavCLK a stavSW slouží pro načtení stavu výstupů CLK (otáčení osy) a SW (stisknutí tlačítka). Proměnná stavPred je určena pro uchování předešlého stavu CLK. Jak jsme říkali, budeme vyhodnocovat změny CLK, které nám budou naznačovat, že bylo osou enkodéru otočeno. Je tedy třeba vědět, jaká byla předešlá hodnota výstupu CLK.

V hlavní nekonečné smyčce je načten stav CLK:

# Načtení aktuálního stavu pinu CLK
stavCLK = pinCLK.value()

následně je testován s předešlým stavem:

if stavCLK != stavPred:

a pokud se liší, nastupuje načtení DT a jeho porovnání se stavem CLK:

if pinDT.value() != stavCLK:   # DT není CLK - rotace po směru
    print("Rotace vpravo => | ", end='')
    poziceEnkod += 1
else:    # DT odpovídá CLK - rotace proti směru
    print("Rotace vlevo <= | ", end='')
    poziceEnkod -= 1

Dle nastalé situace se kód dělí na rotaci po a proti směru hodinových ručiček.

Následuje vypsání nejen směru otáčení, ale i hodnoty proměnné poziceEnkod. Do proměnné stavPred se uloží aktuální hodnota CLK… A může se jít na to znova!

Nesmíme opomenout registraci stavu tlačítka SW, která pro nás ale v tuto chvíli není zase tak zajímavá.

Podívejme se, jak bude vypadat získaný výstup z načítání rotačního enkodéru ve výstupním okně prostředí Thonny IDE (viz obrázek č. 6).

vystup reseni-raw
Obr. 6 – výpis stavu rotačního enkodéru ve výstupu prostředí Thonny IDE

Protože s případnými domácími úkoly jsme asi již tak trochu otravní, zkusíme následující úkol pro čtenáře prezentovat spíše jako „výzvu“ (nebo pro mladší ročníky: „challenge“). Ve výše uvedeném programu je jedna drobnost, která by si zasloužila drobnou úpravu. Jak vidíme, kód běží tak, jak má – nehledejme tedy o funkční problém, ale spíše o efektivitu kódu. Konkrétně se zaměřte na to, kdy se zapisuje předchozí stav CLK.

Původně tato nepřesnost vznikla při překopírování kódu do HTML, ale nakonec jsme si řekli, že ji tu necháme právě pro účely zamyšlení se nad optimalizací kódu. Problém by měla opravit dvojice přidaných mezer – Python je v tom strukturování programu pomocí syntaxe skutečně někdy dost „pythomej“! 😉

B) načítání enkodéru pomocí přerušení.

Předešlý příklad načítání rotačního enkodéru má jednu nevýhodu. Představme si, že bychom v hlavní smyčce chtěli kromě načítání enkodéru také něco dělat jiného. Tato činnost může hlavní smyčku docela zdržet, když pak v tuto chvíli budeme enkodérem otáčet, není jisté, že bude dobře načten. Například pokud v předešlém kódu stisknete tlačítko, program se na půl sekundy zastaví. Na tu chvíli jakákoliv otočení nejsou registrována.

Pro následující řešení tedy využijeme techniky přerušení. O přerušení v MicroPythonu jsme si podrobně říkali v článku: MicroPython: Přerušení na ESP32.

Tak si připomeneme jen to základní:
Přerušení je v programech mikrokontrolérů užitečné tím, že na základě nějaké události umožňuje automatické provádění určitých činností. Tím může vyřešit různé problémy s časováním nebo reakcí na události. Když dojde k přerušení, procesor zastaví provádění hlavního programu, provede úlohu přerušení (speciální část kódu) a poté se vrátí k hlavnímu programu, který pokračuje dále, jako by se nic nestalo. Jedním ze způsobů vyvolání přerušení je vnější (externí) podnět na pinu modulu ESP32. V našem případě budeme využívat změnu logické úrovně na GPIO pinu odpovídajícímu výstupu CLK rotačního enkodéru.

Náš nový kód z hlediska principu vyjde z předchozího kódu. Budeme tedy stejným způsobem testovat výstup enkodéru CLK a na základě jeho změny se dotážeme na DT. Podle srovnání těchto hodnot vyhodnotíme směr a změníme hodnotu proměnné poziceEnkod.

from machine import Pin
import time

# Nastavení pinů
pinCLK = Pin(22, Pin.IN) # Pin CLK na GPIO22
pinDT = Pin(23, Pin.IN)   # Pin DT na GPIO23
pinSW = Pin(21, Pin.IN, Pin.PULL_UP) # Pin SW na GPIO21 (s pull-up odporem)

# Proměnné pro uložení pozice a stavů
poziceEnkod = 0
stavCLK = pinCLK.value() # Načtení počátečního stavu CLK

# Funkce pro zpracování změny stavu CLK
def zpracuj_zmenu_clk(pin):
    global poziceEnkod, stavCLK

    # Načtení aktuálního stavu pinu CLK
    stavCLK = pinCLK.value()

    # Pokud stav pinu DT neodpovídá stavu pinu CLK, rotace byla po směru hodin
    if pinDT.value() != stavCLK:
        print('Rotace vpravo => | ', end='')
        poziceEnkod += 1
    # Pokud stav pinu DT odpovídá stavu pinu CLK, rotace byla proti směru hodin
    else:
        print('Rotace vlevo <= | ', end='')
        poziceEnkod -= 1

    # Vytisknutí aktuální pozice enkodéru
    print(f"Pozice enkoderu: {poziceEnkod}")

# Nastavení přerušení na pin CLK
pinCLK.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=zpracuj_zmenu_clk)

# Hlavní smyčka pro sledování tlačítka
while True:
    # Načtení stavu pinu SW (tlačítko)
    stavSW = pinSW.value()

    # Pokud je tlačítko stisknuto (stav je LOW), vytiskni zprávu
    if stavSW == 0:
        print('Stisknuto tlacitko enkoderu!')

To, co je zde nyní důležité, je nastavení přerušení a volání jeho obslužné rutiny. Kód pro vyhodnocení otáčení enkodéru je nyní v proceduře zpracuj_zmenu_clk(). Tuto proceduru chceme volat v okamžiku změnu úrovně CLK. To provádíme následujícím způsobem:

pinCLK.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=zpracuj_zmenu_clk)

Objektu pinCLK přiřazujeme přerušení, které nastává při události vzestupné hrany (Pin.IRQ_RISING) a i sestupné hrany (Pin.IRQ_FALLING) – spojeno pomocí logického sloučení „|“ (disjukce). Dále je nastaveno, že při těchto událostech dojde k volání funkce zpracuj_zmenu_clk(). Funkce zpracuj_zmenu_clk() je identická s kódem, který jsme si vysvětlili v předešlém programu, takže se mu nyní již nebudeme příliš věnovat.

Abychom mohli říkat, že naše přerušení přerušuje nějaký „hlavní“ program, je zde v hlavní smyčce ponecháno testování tlačítka SW enkodéru. Pochopitelně by asi v reálném případě i toto tlačítko bylo řešeno pomocí příslušného přerušení. My se však zkusíme zaměřit na něco zcela jiného. Spustíme program a podíváme se, jak funguje. Výstup vidíme na obrázku č. 7.

vystup reseni-IRQ
Obr. 7 – stavu enkodéru testovaný přerušením (prostředí Thonny IDE)

Možná to z obrázku 7 není úplně patrné, ale zkuste si vyzkoušet chování programu v reálné situaci. Velice rychle zjistíte, že jeden skok enkodéru rozhodně není registrován jako jeden krok. Ve výpisu se objeví dvě a někdy i tři hodnoty, jako bychom enkodérem otočili o více kroků.

Příčinou je bohužel to natolik vychvalované přerušení!

Již tu bylo naznačeno, že s mechanickými spínači to někdy bývá pěkná divočina. Problém nastává díky tzv. zakmitání (tzv. bouncing) mechanických kontaktů. Při sepnutí kontaktu mechanického spínače může dojít k odskoku kontaktů, tedy k opakovanému vodivému spojení a rozpojení. To se pochopitelně projeví na elektrickém signálu. Velice pěkně to znázorňuje obrázek č. 8.

bouncing princip
zdroj obrázku: https://yungger.medium.com/so-easy-micropython-switch-vs-debounce-73865bd8e978
Obr. 8 – stavu enkodéru testovaný přerušením (prostředí Thonny IDE)

Jak vidíme na obrázku č. 8 místo jedné náběžné hrany, která by měla reprezentovat stisknutí tlačítka, je najednou těch náběžných hran několik. A nyní si vzpomeňme, že v našem programu spouštíme rutinu přerušení dokonce i při hranách úběžných!

Tomuto nežádoucímu efektu se lze vyhnout pomocí hardwarového řešení, kdy enkodér doplníme například RC členem (lze zakoupit i moduly enkodérů již vybavené RC členy), nebo připojením logického obvodu (ideální je kombinace RC členu a klopného obvodu).

Hardwarové řešení zde řešit nebudeme, situaci zkusíme vyřešit softwarově. Musíme se tedy trochu podívat na techniku tzv. debouncingu.

Debouncing

Debouncing je technika, která se používá k odstranění nežádoucích zákmitů, které mohou vzniknout při mechanickém spínání kontaktů (třeba nyní na kontaktu rotačního enkodéru). Zákmity vznikají, protože mechanické spínače mohou po stisku nebo uvolnění kontaktu krátkodobě zakmitat a tak vytvářet několik přechodů mezi HIGH a LOW.

Jednou z technik je časová prodleva. Pokud detekujete změnu stavu na pinu (např. při přechodu z HIGH na LOW), počkáme několik milisekund, než zkontrolujeme stav znovu. Tím bychom mohli překlenout období nežádoucích zákmitů.

Tuto techniku si ukážeme při načítání stisknutí tlačítka SW, pro které také vytvoříme přerušovací rutinu. Možná by nás mohlo napadnout, že bychom mohli použít obyčejnou čekací smyčku:

if stavSW == 0:
        print("Stisknuto tlacitko enkoderu!")
        time.sleep(0.5)  # Zpoždění, aby se předešlo vícečetným registracím stisku

Ale prozor (!), jak jsme si říkali v článku o přerušení, použití jakékoliv čekací smyčky v rutině přerušení je „prasárna“! Ono už i to použití příkazu print v rutině přerušení v předešlém kódu nebylo zcela „košer“.

V rutině přerušení button_pressed() to zkusíme třeba takto:

def button_pressed(pin):
    global last_button_time

    # Získání aktuálního času
    current_time = time.ticks_ms()

    if time.ticks_diff(current_time, last_button_time) > 300:  # Čekáme 300 ms mezi stisky
        print("Tlačítko stisknuto!")
        last_button_time = current_time  # Aktualizace času posledního stisku

Na začátku do proměnné current_time uložíme pomocí funkce time.ticks_ms() aktuální čas běhu programu (v milisekundách). V globální proměnné last_button_time máme uloženo, kdy došlo k poslednímu stisku tlačítka (je třeba nastavit na začátku programu výchozí hodnotu). Funkce time.ticks_diff(
current_time, last_button_time)
určí časový rozdíl (v milisekundách) mezi aktuálním časem current_time a čase posledního stisknutí last_button_time. Je-li tento rozdíl větší než 300 milisekud, nejedná se již o zákmit kontaktů, ale o další legitimní stisknutí tlačítka. Vypíšeme tedy zprávu o stisknutí tlačítka (O. K. asi by se to mělo vypsat lépe, než je použití funkce print v rutině přerušení, ale tady se to teprve učíme… A žádný učený přece… 😉)

Pochopitelně nesmíme zapomenout tuto funkci registrovat jako přerušovací rutinu pinu GPIO 21 (načtení výstupu SW rotačního enkodéru). V tomto případě chceme tuto funkci spouštět pouze při stisknutí, tedy při úběžné hraně (Pin.IRQ_FALLING):

# Nastavení přerušení pro tlačítko SW
SW.irq(trigger=Pin.IRQ_FALLING, handler=button_pressed)

Přesunutím vyhodnocení tlačítka SW do rutiny přerušení se nám rázem hlavní nekonečná smyčka programu vyprázdnila.

Asi čekáte, že zde bude uveden ukázkový kód s vyřešeným debouncingem.

Tak to máte smůlu! 😜

Protože jsme zde společně absolvovali již nejedno pythonovské dobrodružství, mám nyní pocit, že toho již docela dost dokážete. Nastal čas, kdy byste mohli zkusit výše popsanou techniku debouncingu samostatně zapracovat do předešlého programu a ošetřit tak načtení signálu CLK a DT. Nejdříve zkuste ošetřit jen CLK, zda to náhodou nebude stačit. 😊

Berte to třeba jako další dnešní „výzvu“! Ono jen čtení již hotových kódů nikdy nikoho nic neučilo. Takže byste se již mohli trochu „pustit máminy sukně“ a zkusit něco samostatně. Co se může stát? Nic, maximálně to prostě hned nebude fungovat. No a co!


vystup - reseni IRQ deboucing
Obr. 9 – výstup po vyřešení deboucingu (otáčení o jeden krok sem a tam)

C) načítání enkodéru pomocí knihovny.

Ať už jste si zkusili vyřešit zapeklitou situaci kolem deboucingu a zvítězili jste, nebo stále prohráváte, jistě Vás napadne: „Sakra to jsem první na planetě, kdo se s tím musí potýkat?“ A pochopitelně nás asi napadne, že ne, a tedy to někde již bude vyřešeno. Třeba v nějakém předem připraveném modulu (knihovně)!

Modul „micropython-rotary“

Pro asynchronní čtení přírůstků rotačního kodéru doporučuji využít knihovnu micropython-rotary. Tato knihovna se skládá ze dvou souborů, které je třeba přesunout na vaši desku, jako každou jinou knihovnu MicroPythonu nakopírovat do složky lib na modulu ESP32. Tento proces lze jednoduše provést v prostředí Thonny IDE. Podrobný postup, jak ručně nainstalovat knihovnu do modulu ESP32, jsme si ukazovali na příkladu knihovny Microdot v článku: ESP32: server na knihovně vyšší úrovně.

V tomto případě si musíme stáhnout následující soubory:

Po nakopírování souborů modulu (knihoven) podíváme na kód programu, který si následně rozebereme. (Mimochodem, v kódu programu najdete řešení dnešní první „výzvy“)

from machine import Pin
import time
from rotary_irq_esp import RotaryIRQ # Import knihovny pro ESP32

# Nastavení pinů pro rotační enkodér
pinCLK = 22 # Pin CLK na GPIO22
pinDT = 23   # Pin DT na GPIO23
pinSW = 21   # Pin SW (tlačítko) na GPIO21

# Inicializace objektu RotaryIRQ pro ESP32 (použití přerušení pro čtení enkodéru)
rotary = RotaryIRQ(
    pin_num_clk=pinCLK,
    pin_num_dt=pinDT,
    min_val=0,
    reverse=True
)

# Proměnné pro uložení pozice a stavu předchozí pozice
poziceEnkod = 0
pozicePred = rotary.value() # Načteme počáteční hodnotu pozice enkodéru

# Funkce pro zpracování stisknutí tlačítka SW
last_button_time = 0 # Globální proměnná pro čas posledního stisku tlačítka

def button_pressed(pin):
    global last_button_time

    # Získání aktuálního času
    current_time = time.ticks_ms()

    # Ošetření bouncingu: Pokud byl stisk tlačítka proveden nedávno, ignoruj ho
    if time.ticks_diff(current_time, last_button_time) > 300: # Čekáme 300 ms mezi stisky
        print('Tlačítko stisknuto!')
        last_button_time = current_time # Aktualizace času posledního stisku

# Nastavení přerušení pro tlačítko SW
pinSW = Pin(pinSW, Pin.IN, Pin.PULL_UP) # Pin pro tlačítko se pull-up odporem
pinSW.irq(trigger=Pin.IRQ_FALLING, handler=button_pressed) # Přerušení při stisknutí tlačítka

# Hlavní smyčka pro sledování enkodéru
while True:
    # Získání aktuální hodnoty pozice enkodéru
    poziceEnkod = rotary.value()

    # Pokud se pozice změnila, zjistíme směr rotace
    if poziceEnkod != pozicePred:
        if poziceEnkod > pozicePred:
            print('Rotace vpravo => | ', end='') # Směr rotace vpravo
        else:
            print('Rotace vlevo <= | ', end='') # Směr rotace vlevo

        # Vytisknutí aktuální hodnoty pozice enkodéru
        print(f"Pozice enkodéru: {poziceEnkod}")

        # Uložení aktuální pozice pro příští cyklus
        pozicePred = poziceEnkod

    # Pauza mezi výpisy pro zamezení zahlcení konzole
    time.sleep(0.1)

V první části kromě importu modulů Pin a time importujeme i modul pro práci s objekty knihovny rotary_irq_esp. Následuje nastavení vstupních pinů modulu ESP32, v tom se současný program od předešlých neliší.

Pro nás zajímavým momentem je až deklarace objektu pro práci s enkodérem. Podívejme se obecně na konstruktor tohoto objektu včetně popisu jeho argumnetů (viz Tab. č. 3):

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 Popis Poznámka
pin_num_clk vývod GPIO připojený k vývodu CLK enkodéru celé číslo
pin_num_dt vývod GPIO připojený k vývodu DT enkodéru celé číslo
min_val minimální hodnota v rozsahu snímače; též počáteční hodnota celé číslo (výchozí=0)
max_val maximální hodnota v rozsahu snímače
(je ignorováno, pokud je range_mode = RANGE_UNBOUNDED)
celé číslo (výchozí=10)
incr hodnota změny s každým kliknutím snímače celé číslo (výchozí=1)
reverse opačný směr počítání kroků True nebo False (výchozí False)
range_mode chování počtu při min_val a max_val RotaryIRQ.RANGE_UNBOUNDED (výchozí) – enkodér nemá žádné omezení rozsahu počítání
RotaryIRQ.RANGE_WRAP – enkodér bude počítat až do max_val a pak se vrátí k minimální hodnotě (podobné chování pro odpočítávání)
RotaryIRQ.RANGE_BOUNDED – enkodér bude počítat až do max_val a poté se zastaví. Odpočítávání se zastaví na min_val
pull_up povolit interní pull-up rezistory modulu, používá se v případě, že hardware rotačního snímače nemá pull-up rezistory True nebo False (výchozí False)
half_step režim polovičního kroku True nebo False (výchozí False)
invert invertuje signály CLK a DT.
Použití, když je klidová hodnota enkodéru CLK, DT = 00.
True nebo False (výchozí False)

Tab. č. 3 – popis argumentů objektu RotaryIRQ

Objekt RotaryIRQ má následující metody:

.value()
Vrací hodnotu enkodéru
.set(value=None, min_val=None, max_val=None, incr=None, reverse=None, range_mode=None)
Nastaví hodnotu enkodéru a vnitřní konfigurační parametry. Popis argumentů viz konstruktor. None znamená, že nedošlo ke změně konfiguračního parametru
.add_listener(function)
přidá funkci zpětného volání, která bude volána při každé změně počtu kodérů
.remove_listener(function)
odstraní dříve přidanou funkci zpětného volání
.close()
deaktivuje piny mikrokontroléru používané pro čtení enkodéru

Knihovna micropython-rotary pochopitelně využívá přerušení, ale naštěstí v případě určení směru (a dokonce i ošetření bouncingu) je již vyřízeno automaticky. Jednou věc, kterou modul neřeší je tlačítko SW, tam je naším úkolem deklarovat rutinu přerušení a její registrace k dané události. To ale již umíme z předešlé ukázky ošetření debouncingu.

Načítání enkodéru nám běží na pozadí a jediným výstupem pro nás je hodnota proměnné poziceEnkod. V hlavní smyčce tedy netestujeme hodnotu vstupů CLK a DT, ale jen právě tuto proměnnou. Pokud by v hlavní smyčce běžel nějaký náročnější proces, kvůli kterému by řešení přímého testování vstupů CLK a DT mohlo selhávat, tak nyní se to stát nemůže. Testování vstupu CLK probíhá na pozadí, kde se o to stará přerušení, které by hlavní proces klidně přerušilo. Zde v hlavní smyčce máme jen výpis aktuální hodnoty čítače enkodéru a kvůli výpisu i vyhodnocení směru otáčení pomocí testu předchozí hodnoty.

vystup - reseni s lib
Obr. 10 – výstup načtení rotačního enkodéru modulem

Na obrázku č. 10 vidíme výstup hotového řešení s využitím modulu micropython-rotary. Jak vidíme, i jednokrokové potočení vpravo a vlevo mění hodnotu čítače enkodéru o jedničku.

Použití knihovny rotary_irq_esp.py se ukazuje asi jako nejlepší volba, a to z následujících důvodů:

  1. Efektivita a výkon: Knihovny jako rotary_irq_esp.py používají přerušení, což je vysoce efektivní způsob, jak reagovat na změny stavu enkodéru, bez zbytečné zátěže procesoru. To znamená, že ESP32 může zůstat „volný“ pro jiné úkoly, dokud není potřeba reagovat na změnu.
  2. Jednoduchost implementace: Knihovny poskytují jednoduché API, které vám umožňuje snadno začít bez potřeby řešit detaily přerušení a debouncingu, což by vyžadovalo větší úsilí, pokud byste psali kód sami.
  3. Bezpečnost a robustnost: Knihovny jsou obvykle testované a optimalizované, což znamená, že problémům s „race conditions“ a jiným chybám se budete vyhýbat, pokud budete správně používat dokumentované funkce.

Shrnutí

Když porovnáme výše uvedené tři různé přístupy k načítání a zpracování hodnot z rotačního enkodéru v MicroPythonu pro ESP32, musíme konstatovat, že každý z nich má své výhody i nevýhody. Pojďme je trochu shrnout:

1. Čisté načítání pinů

Tento přístup spočívá v neustálém kontrolování stavu pinů (CLK, DT) v hlavní smyčce programu.

  • Výhody:
    • Jednoduchost: Tento přístup je velmi jednoduchý na implementaci. Stačí použít základní funkce Pin.value() pro čtení stavů pinů.
  • Nevýhody:
    • Vysoká zátěž procesoru: Tento způsob vyžaduje neustálé kontrolování stavů pinů, což může vést k vysoké zátěži procesoru, zejména pokud potřebujete reagovat na rychlé změny. To může být problematické v aplikacích, které mají další úkoly (například komunikace nebo složitější výpočty).
    • Nízká efektivita: Tento přístup nevyužívá žádné optimalizace, jako je například čekání na událost (interrupt), což vede k tomu, že procesor neustále provádí čtení pinů, i když se nic nemění.

2. Použití přerušení

Tento přístup využívá přerušení (interrupts) k detekci změny stavu na pinu CLK, což znamená, že procesor bude reagovat pouze na změny, nikoli na každé kontrolování pinu.

  • Výhody:
    • Efektivita: Použití přerušení je mnohem efektivnější, protože místo neustálého kontrolování pinů program čeká na konkrétní událost (změnu stavu pinu). Tím se snižuje zátěž procesoru.
    • Nízká latence: Přerušení umožňuje velmi rychlou reakci na změny, což je užitečné pro aplikace, které potřebují okamžitě reagovat na pohyby enkodéru.
  • Nevýhody:
    • Komplexnost: Práce s přerušeními může být pro začátečníky složitější, zejména v případě, že potřebujete řešit více přerušení.
    • Potenciální problémy s „race conditions“: Pokud není správně řízena synchronizace mezi přerušením a hlavní smyčkou, mohou nastat problémy s kolizemi nebo ztrátou dat.
    • Problém se zákmity spínačů: díky mechanickému zakmitání spínačů enkodéru dochází k několikanásobnému zpuštění přerušení. V případě ošetření tohoto nežádoucího stavu, se celý kód poměrně zesložiťuje.

3. Použití knihovny (např. rotary_irq_esp.py)

Tento přístup využívá hotové knihovny, které implementují efektivní práci s rotačními enkodéry a využívají přerušení pro detekci změn stavů pinů.

  • Výhody:
    • Optimalizace: Knihovny jako rotary_irq_esp.py jsou optimalizované pro práci s rotačními enkodéry a používají přerušení, což zaručuje efektivitu a nízkou zátěž procesoru.
    • Jednoduchost a údržba: Použití hotové knihovny výrazně zjednodušuje implementaci, protože všechny složitosti týkající se přerušení, debouncingu a jiných technických detailů jsou již vyřešeny v knihovně.
    • Flexibilita: Mnoho knihoven poskytuje různé možnosti konfigurace, což umožňuje snadnou úpravu pro různé typy enkodérů a aplikace.
  • Nevýhody:
    • Závislost na externí knihovně: Použití knihovny znamená, že se stáváme závislí na její funkčnosti a aktualizacích. Pokud knihovna nebude podporovat nové verze MicroPythonu na ESP32, může to být problém.
    • Možná složitost v případě chyb: Pokud nastanou problémy, může být obtížné sledovat, co přesně se děje v knihovně, protože veškerá logika je obvykle skrytá uvnitř knihovny.

Jaký je tedy verdikt?

Pokud chceme efektivně využívat hardware a minimalizovat složitost, použití knihovny je ideální volbou. Na druhou stranu, pokud máte specifické požadavky a potřebujete větší kontrolu nad celým procesem, může být použití přerušení přímo vhodnější (ale s větší složitostí).

Závěr

Na to, že je rotační enkodér vlastně jen trojicí spínačů, se nám ty dnešní hrátky s touto komponentou docela protáhly. Kromě výše uvedených tří možných řešení načítání enkodéru, bychom si měli z celé situace odnést dvě docela důležité informace. Prvně bychom nikdy neměli podceňovat žádný modul nebo čidlo, protože ten technický ďábel se opravdu skrývá v detailu. A prostě někdy se ty věci tak nějak zkomplikují. Tak už to bývá nejen ve světě mechatroniky. Druhé sdělení je trochu optimističtější. Podívejte se, co vše jsme dnes na základě předešlých informací již zvládli vyřešit! Použít přerušení, vyřešit ošetření zákmitu mechanického spínače, využít externí knihovnu… Kolik „začátečníků“ s MicroPythonem tohle zvládne?

Teď, když zvládáme připojení externích knihoven ke svému projektu, otevírá se nám možnost používání v MicroPythonu mnohem sofistikovanějších čidel a modulů.

A co to tedy bude příště? Asi by bylo dobré se podívat i na používání nějakých sběrnic (I²C, SPI apod.), kterými modul ESP32 disponuje. Ale kdo ví!

UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!