Fyzikální kabinet FyzKAB

ESP32: Ovládáme servo v MicroPythonu

22. díl „volného seriálu“ článků

Při výběru pohonu pro různé projekty máme k dispozici širokou škálu možností – od krokových motorů až po různé stejnosměrné motory. Každý typ pohonu má své výhody i nevýhody. Pokud však potřebujeme dosáhnout přesně nastavené polohy, je jasnou volbou servomotor. Tento motor, známý také jako „servo“, se původně prosadil v oblasti RC modelů, kde byl využíván k ovládání řízení aut nebo klapek u letadel. Postupně se ale rozšířil i do dalších oblastí, jako je robotika, automatizace či průmyslové aplikace – například pro regulaci ventilů.

V tomto článku se zaměříme na to, jak servo funguje, jak ho připojit k modulu ESP32 a především (!) jak jej ovládat pomocí programovacího jazyka MicroPython.

servo SG90

Jednou z hlavních výhod servomotorů je jejich vynikající poměr výkonu k hmotnosti. Příklad: populární „modré“ servo SG90 od TowerPro, kterým se dnes budeme zabývat, dosahuje točivého momentu až 1,5 kg/cm při hmotnosti pouhých 9 gramů! Díky své nízké ceně a snadnému ovládání jakýmkoliv mikrokontrolérem je ideálním řešením pro různé pohyblivé konstrukce.

Jak servo funguje

Uvnitř každého běžného hobby servomotoru najdeme pět hlavních součástí, které spolupracují, aby zajistily správný pohyb:

  • Stejnosměrný motor: Tento motor poskytuje rotační sílu a je podobný těm, které najdeme v hračkách. Rozdíl spočívá v tom, jak je řízen. Stejnosměrný motor je spojen s výstupní hřídelí pomocí ozubených kol.
  • Převodovka: Soustava ozubených kol zvyšuje točivý moment motoru, což zlepšuje přesnost pohybů. Kromě toho zpomaluje rychlost motoru na výstupu, tedy na ovládacím rameni.
  • Ovládací rameno: Tento plastový díl (nebo kolo) je připevněn na výstupní hřídel a slouží jako místo pro připojení předmětů, které chceme pohybovat – například robotických ramen nebo kormidel modelu lodě.
  • Potenciometr: Tento proměnný rezistor funguje jako zpětnovazební snímač polohy. Jakmile se otáčí výstupní hřídel, mění se odpor potenciometru, což umožňuje řídicí jednotce přesně zjistit aktuální polohu servomotoru.
  • Řídicí jednotka: Toto je „mozek“ servomotoru. Řídicí jednotka přijímá signály z externího ovladače (například modulu ESP32), sleduje polohu potenciometru a řídí stejnosměrný motor. Když dostane signál k pohybu do určité polohy, přijme příkaz a uvede motor do požadované pozice.
servo SG90 - rez

Potenciometr je fyzicky připojen k výstupní hřídeli servomotoru a pomocí elektrického napětí neustále udává její aktuální polohu. Toto napětí slouží jako zpětná vazba pro řídicí jednotku, která jej neustále porovnává s požadovaným signálem polohy z mikrokontroléru. Pokud dojde k rozdílu mezi požadovanou a aktuální polohou, řídicí jednotka upraví polohu stejnosměrného motoru, aby tento rozdíl vyrovnala. Jakmile servo dosáhne požadované pozice, řídicí jednotka motor zastaví.

Tento proces, nazývaný „uzavřená zpětná vazba“, probíhá velmi rychle a opakovaně. Servo neustále kontroluje svou polohu a v případě potřeby provádí jemné korekce, aby se udrželo přesně tam, kde má být. Díky tomu servo velmi dobře drží svou polohu, což je obzvlášť užitečné pro aplikace, jako jsou robotická ramena, která díky tomu neklesnou pod vlastní vahou nebo v důsledku zatížení.

Princip řízení

Hobby servomotory jsou řízeny technikou zvanou „pulzně šířková modulace“ (PWM). Tato metoda spočívá v tom, že se do serva v pravidelných intervalech vysílá série elektrických impulzů. U většiny hobby serv je frekvence těchto impulzů 50 Hz, což znamená, že nový impulz přichází každých 20 ms. Při řízení polohy serva však není rozhodující, jak často impulzy vysíláme, ale jak dlouho každý impulz trvá. Tento čas se nazývá „šířka impulzu“ PWM signálu.

Úhel natočení serva je závislý právě na šířce impulzu. Například u serva SG90, které budeme používat, impulz o délce 1,5 ms způsobí, že servo se otočí do neutrální polohy, tedy na 90 stupňů. Pokud je impulz kratší než 1,5 ms, servo se otočí proti směru hodinových ručiček, a pokud je impulz delší než 1,5 ms, servo se otočí ve směru hodinových ručiček. Každé servo má specifické minimální a maximální hodnoty šířky impulzu, které určují platné rozsahy poloh, do kterých může servo otočit.

schema funkce serva

V mnoha článcích se uvádí, že modelářské servo SG90 má rozmezí pulzů 1–2 ms, a tato informace je často znovu a znovu opakována. Realita je však taková, že spousta bastlířů vůbec neví, jaká je skutečná šířka těchto pulzů, protože obvykle používají knihovny, které se o vše postarají za ně. Když se ale podíváme na servo SG90, které jsme použili při psaní tohoto článku, zjistíme, že jeho skutečný rozsah (jak ukážeme později) je od 0,5 ms do 2,5 ms. Takže někdy je opravdu těžké se v těchto údajích vyznat!

Ne všechna serva jsou stejná. Je důležité mít na paměti, že rozsah šířky impulzu se může u různých modelů servomotorů mírně lišit. Například některá serva mohou mít rozsah 0,5 ms pro 0 stupňů a 2,5 ms pro 180 stupňů. Proto je vždy dobré se podívat na technický list konkrétního serva, který vám poskytne přesné informace.

Zapojení (piny) serva

Většina hobby serv je vybavena standardním třípinovým konektorem s roztečí 0,1″. Ačkoli se barevné kódování vodičů může mezi jednotlivými značkami lišit, piny jsou obvykle uspořádány v následujícím pořadí:

  • Zem (hnědá/černá) – tento pin slouží jako zemnící kolík.
  • 5 V (červený) – pin pro napájení servomotoru. Většina hobby serv vyžaduje napětí mezi 4,8 V a 6 V. Je důležité zajistit správné napětí – příliš nízké způsobí nedostatečný výkon, zatímco příliš vysoké může servo poškodit.
  • Řízení (oranžová/žlutá/bílá) – tento pin přijímá PWM signály (pulzně šířková modulace) z mikrokontroléru, které určují polohu serva.

Barevné kódování vodičů se může lišit mezi různými značkami, ale napájecí vodič bývá téměř vždy červený, což usnadňuje jeho identifikaci. Zemnící vodič bývá obvykle černý nebo hnědý, a ovládací vodič je často oranžový, žlutý nebo bílý.

Je důležité mít na paměti, že když servo nepracuje, potřebuje jen malý proud – asi 10 mA. Jakmile se ale servo začne pohybovat, odběr proudu výrazně vzroste, a to v rozmezí od 100 mA do 250 mA. To znamená, že pro většinu aplikací je lepší použít externí napájení pro servo. Pro naše jednoduché experimenty, kdy servo bude pohybovat „naprázdno“ (bez zátěže), budeme servo napájet přímo z modulu ESP32 (pin 5V), ale jak už bylo zmíněno: „jen pro tyto naše hrátky“! Jakmile by servo mělo pracovat pod zátěží, nebo bychom použili náročnější typ serva (s odběrem přes 250 mA), měli bychom použít samostatný napájecí zdroj. Dále bychom měli vzít v úvahu, že pokud ESP32 napájíme přes USB, máme určité omezení napájení dané právě tímto portem. A to nemluvíme o zvýšené spotřebě, pokud je na modulu ESP32 zapnutý Wi-Fi modul.

Pokud už máme jasný přehled o principu serva, můžeme se pustit do samotné konstrukce.

V první části tohoto článku připojíme servo přímo k modulu ESP32, v druhé části pak využijeme I²C řadiče PCA9685.

Přímé připojení serva

Abychom si užili trochu vývojářského dobrodružství, začneme ovládat servo „drsným“ způsobem, a to pomocí čistého PWM. Až si tuto metodu osaháme, přejdeme na pohodlnější řešení pomocí knihovny, která nám práci usnadní.

Řízení pomocí PWM výstupu

Trochu si zavzpomínáme na jeden z našich prvních článků, kde jsme se učili, jak v MicroPythonu nastavovat piny a jejich režimy (viz článek: Ovládání PWM výstupů na ESP32 pomocí MicroPythonu). Pro řízení serva potřebujeme na výstupním pinu generovat PWM signál s frekvencí 50 Hz. Samotné řízení polohy serva spočívá v nastavení délky výstupního pulzu tak, aby se vešel do intervalu určeného datovým listem konkrétního serva. U našeho serva znamená výchylka 0° pulz o délce 0,5 ms, a maximální výchylky (180°) dosáhneme při pulzu o délce 2,5 ms (u jiných serv bývá tento rozsah obvykle mezi 1–2 ms).

Jedinou podmínkou je, že servo musí být připojeno k pinu na modulu ESP32, který podporuje PWM. My jsme použili pin GPIO22. Připojení serva k modulu je znázorněno v následujícím obrázku.

servo ESP32 (raw) - schema

Může nás napadnout otázka, zda připojení serva napájeného 5 V k pinu GPIO22 modulu ESP32, který pracuje s logikou 3,3 V, nezpůsobí problém. Může se stát, že vyšší napětí (5 V) se dostane na PWM pin s nižší napěťovou úrovní 3,3 V. To by mohlo ohrozit samotný modul ESP32, který není přímo připraven na takovou situaci. Ačkoliv internet nabízí „zaručená“ zapojení, kde je 5 V logika přímo připojena k modulu ESP32 s tvrzením, že to modulu neublíží, oficiální vyjádření výrobce ESP32 říká, že GPIO piny nejsou na tuto situaci navrženy. V praxi se ale poškození pinů nemusí projevit okamžitě, což může vést k tomu, že se zdá, že to modul „vydrží“ (ale bezpečné to není). 😟

Tento problém jsme podrobně rozebrali v článku Ovládáme servo modulem ESP32, a proto se k tomu nebudeme vracet. Pro tento konkrétní případ je ale klíčové, že díky řešení na straně logiky serva (které jsme popisovali výše) je zapojení bezpečné a modul ESP32 není ohrožen.

Program řízení na úrovni PWM

Následující program ukazuje, jak řídit servo SG90 pomocí čistého PWM výstupu. Program nejprve nastaví servo do polohy 0°, po vteřinové pauze se přesune do polohy 180° a nakonec přejde do pozice určené hodnotou v proměnné uhel (výchozí hodnota proměnné uhel je v ukázce 30°).

Kód programu:

from machine import Pin, PWM
import time

uhel = 30

sg90 = PWM(Pin(22, mode=Pin.OUT))
sg90.freq(50)

while True:
    sg90.duty(26)    # 0.5ms/20ms = 0.025 = 2.5%, 0.025*1023=25.6
    time.sleep(1)
    sg90.duty(128)    # 2.5ms/20ms = 0.125 = 12,5%, 0.125*1023=128
    time.sleep(1)

    pulse_width_ms = 0.5 + (uhel/180)*(2.5-0.5)
    pwm_value = int((pulse_width_ms/20)*1023)
    sg90.duty(pwm_value)
    time.sleep(1)

Pojďme si kód postupně vysvětlit. V první části se importují moduly pro obsluhu pinů, PWM a časování. Zajímavější je ale až deklarace objektu sg90, který slouží k ovládání PWM signálu na pinu GPIO22.

sg90 = PWM(Pin(22, mode=Pin.OUT))
sg90.freq(50)

Deklarace objektu sg90 je oproti tradiční metodě (nejprve deklarace pinu a následně PWM) trochu zjednodušena. Pin GPIO22 je nastaven jako výstup a okamžitě se využije pro deklaraci objektu PWM. Pomocí metody freq() se nastaví výstupní frekvence PWM signálu na požadovaných 50 Hz.

V komentářích metody duty() je naznačen princip výpočtu šířky výstupního PWM pulzu pro dosažení požadované polohy serva.

Pojďme si to vysvětlit podrobněji:

Pokud je frekvence PWM signálu 50 Hz, znamená to, že jeden pulz trvá 1/50 sekundy, tedy 20 ms. Standardní rozlišení PWM výstupu na modulu ESP32 je desetibitové, což znamená, že výstupní hodnota může nabývat 1024 různých hodnot (od 0 do 1023). Hodnota 0 znamená nulovou délku PWM pulzu, zatímco hodnota 1023 odpovídá maximální délce (plné) trvání pulzu, tedy 20 ms.

Pro přehlednost se někdy tento výstup vysvětluje v procentech, přičemž hodnota 0 odpovídá 0 % a hodnota 1023 znamená 100 % trvání pulzu.

Pokud potřebujeme pulz o délce 0,5 ms, musíme na PWM výstupu nastavit hodnotu, která odpovídá stejné proporci z celkové délky pulzu (20 ms) jako 0,5 ms. 0,5 ms je jedna čtyřicetina z 20 ms, což znamená, že i výstupní hodnota musí být jedna čtyřicetina z 1023. Po zaokrouhlení na celé číslo to odpovídá hodnotě 26.

Podobně pro 2,5 ms (což je 12,5 % z 20 ms) zjistíme, že odpovídající výstupní hodnota pro PWM je 128.

V hlavní nekonečné smyčce, kromě nastavení serva do krajních poloh, také probíhá výpočet výstupní PWM hodnoty pro zadaný úhel.

pulse_width_ms = 0.5+(uhel/180)*(2.5-0.5)
pwm_value = int((pulse_width_ms/20)*1023)

Nejdříve se vypočítá délka pulzu v mikrosekundách, která je uložena do proměnné pulse_width_ms. Poměr zadaného úhlu k maximálnímu rozsahu serva (180°) se vynásobí časovým intervalem odpovídajícím délce řídicího pulzu. K této hodnotě se následně přičte 0,5 ms, což posouvá čas pulzu nad hranici minimálního trvání.

Druhým krokem je přepočet doby trvání pulzu na výstupní hodnotu PWM, což jsme si již popsali výše. Výsledná hodnota pro PWM je uložena do proměnné pwm_value.

Pro praktičtější využití by se převod z úhlu na PWM hodnotu mohl realizovat jako funkce, například pojmenovaná uhel2pwm(). Kód předchozího programu by pak mohl vypadat takto:

from machine import Pin, PWM
import time

sg90 = PWM(Pin(22, mode=Pin.OUT))
sg90.freq(50)

def uhel2pwm(uhel):
    pulse_width_ms = 0.5 + (uhel/180)*(2.5-0.5)
    pwm_value = int((pulse_width_ms/20)*1023)
    return pwm_value

while True:
    sg90.duty(uhel2pwm(0))
    time.sleep(1)
    sg90.duty(uhel2pwm(180))
    time.sleep(1)
    sg90.duty(uhel2pwm(30))
    time.sleep(1)

ESP32 + Servo (raw)

Řízení pomocí knihovny (na úrovni PWM)

V předchozím příkladu jsme si vytvořili vlastní funkci, která nám zjednodušila obsluhu PWM výstupu. Možná nás ale napadlo, jestli už neexistuje nějaká knihovna, která by nám ovládání serva ještě víc usnadnila. A skutečně, není to žádné překvapení – takových knihoven existuje hned několik.

Jednou z nejjednodušších je knihovna servo.py, kterou vytvořil Philip van Allen. Tato knihovna nabízí dvě základní metody: write_us(), která umožňuje nastavit délku PWM impulzu v mikrosekundách, a write_angle(), která slouží pro nastavení serva do požadovaného úhlu. Argument této funkce může být buď degrees= pro stupně, nebo radians= pro radiány.

Možná se teď ptáme: Jakou výhodu tedy má použití takto jednoduché knihovny? Odpovědí je, že přináší určitou míru abstrakce, díky které nemusíme v hlavním programu psát a spravovat definice pomocných funkcí. Navíc knihovny jako tato často umožňují nastavit i různé jiné parametry, jako je pracovní frekvence, minimální a maximální délka pulzu (v mikrosekundách) nebo maximální úhel serva. Tím je ovládání serva flexibilnější.

Zajímavou alternativou je knihovna micropython-servo, která je dostupná na serveru pypi.org. Tento zdroj je považován za „oficiálnější“ pro MicroPython a může nabídnout některé další možnosti, které nenajdeme v předchozí knihovně.

Pro následující ukázku se ale zaměříme na knihovnu od Philipa van Allena.

Tuto knihovnu bohužel nenajdeme přímo v balíčkovém správci Thonny IDE, takže ji musíme stáhnout přímo z autorova repozitáře. Soubor servo.py je k dispozici ve složce s příklady. Po stažení souboru jej musíme zkopírovat do složky lib na modulu ESP32. Nejlepším způsobem, jak přistupovat k MicroPython souborům na modulu ESP32, je použití prostředí Thonny IDE. V předchozích článcích jsme si ukazovali jeden z možných postupů, který spočíval v jednoduchém Uložit jako… Dnes si ale ukážeme elegantnější přístup, abychom se opět naučili něco nového.

Vložení knihovny do MicroPython zařízení (metoda č. 2)

  • Připojíme zařízení s MicroPythonem k počítači, spustíme prostředí Thonny IDE.
  • V Thonny IDE v menu Zobrazení zaškrtneme položku Soubory.
  • Na levé straně pracovního prostoru se zobrazí sloupec, který zobrazuje soubory v aktuální složce počítače (nahoře) a strukturu souborů v připojeném zařízení ESP32 s nainstalovaným MicroPythonem (dole).
  • V počítači vyhledáme soubor knihovny, který chceme nakopírovat do zařízení ESP32.
  • Ve struktuře složek ESP32 se „proklikáme“ až do složky lib. Pokud tato složka v kořenové složce neexistuje, vytvoříme ji kliknutím pravým tlačítkem myši a výběrem možnosti Nová složka.
  • Označíme soubor knihovny v horním okně (na počítači) a pravým tlačítkem myši vybereme možnost Nahrát do /lib (viz následující obrázek).
nacteni knihovny do ESP32
  • Soubor se zkopíruje z PC do zařízení s MicroPythonem a tím je proces dokončen.

Zmíněný postup je vlastně obdobný způsobu „Uložit jako…“, jen nyní nemusíme soubor knihovny načítat do okna kódu v prostředí Thonny IDE.

Jakmile máme soubor knihovny nakopírovaný do modulu ESP32, můžeme do prostředí Thonny IDE vložit následující kód obslužného programu, který následně spustíme přímo v tomto prostředí. Podobně jako u předchozího kódu by se funkčnost kódu měla projevit pohybem serva.

Kód programu:

import time
from machine import Pin
from servo import Servo     # https://github.com/pvanallen/esp32-getstarted/blob/master/examples/servo.py

servo_pin = Pin(22)

my_servo = Servo(servo_pin, min_us=500, max_us=2500)

delay = 8
min_u = 0
max_u = 180

while True:
    for i in range(min_u, max_u):
        my_servo.write_angle(i)
        time.sleep_ms(delay)
    for i in range(max_u, min_u, -1):
        my_servo.write_angle(i)
        time.sleep_ms(delay)

Hlavní částí kódu, která stojí za zmínku, je deklarace objektu pro práci se servem a samotné zadávání polohy serva.

my_servo = Servo(servo_pin, min_us=500, max_us=2500)

Díky tomu, že v knihovně servo.py jsou některé parametry objektu Servo již nastaveny jako výchozí hodnoty, deklarujeme pouze ty parametry, které se liší od těchto výchozích nastavení. Konkrétně jde o PWM pin, na kterém je připojeno servo, a také minimální a maximální délku PWM impulzu, které odpovídají maximální a minimální úhlové výchylce serva.

Nastavení serva se v této knihovně provádí metodou write_angle(). Všimněme si, že pokud není vstupní argument specifikován (např. degrees= nebo radians=), je vstupní argument považován za hodnotu ve stupních. To je způsobeno tím, že (Micro)Python automaticky dosazuje první poziční argument do prvního parametru definovanému ve funkci. V tomto případě se tedy hodnota 30 interpretuje jako degrees=30, a hodnota radians=None (zůstává s výchozí hodnotou zavedenou v knihovně). Tímto způsobem můžeme pohodlně ovládat polohu serva pomocí zadaného úhlu v stupních.

A to by asi k řízení serva pomocí „holého“ PWM stačilo!

Jak jsme v minulém článku slíbili, dnes se také musíme podívat na jednu z dalších sběrnic, kterou najdeme na modulu ESP32. A protože jsme to slíbili, je čas se do toho pustit! Využijeme I²C sběrnici pro řízení serva a představíme si modul řadiče, který umožňuje elegantně ovládat hned několik serv a motorů najednou. Když už jsme se dostali až sem, bylo by škoda neprozkoumat tento zajímavý modul a všechny jeho výhody!

Sběrnice I²C

O sběrnici I²C jsme se již krátce zmínili v článku SPI (I²C) OLED displeji SSD1306 v MicroPythonu). Nyní se k tomu trochu vrátíme, zopakujeme si základy a zároveň přidáme pár nových informací, které by vám mohly přijít vhod.

I²C (Inter-Integrated Circuit)

Sběrnice I²C (z anglického Inter-Integrated Circuit) je sériová komunikační sběrnice, kterou v roce 1982 vyvinula firma Philips (nyní NXP Semiconductors). Je navržena pro jednoduchou a efektivní komunikaci mezi integrovanými obvody uvnitř jednoho zařízení nebo na krátkou vzdálenost mezi více čipy. Vyznačuje se především tím, že využívá pouze dva vodiče – datovou linku (SDA) a hodinovou linku (SCL). To umožňuje výrazně snížit počet potřebných vodičů mezi zařízeními.

Sběrnice I²C pracuje v režimu master-slave, kde jeden hlavní řadič (master) řídí komunikaci a jedeno nebo více podřízených zařízení (slave) na ni reagují. Každé zařízení na sběrnici má přidělenu vlastní adresu, díky které může master cíleně oslovovat konkrétní obvody. Tento způsob adresování spolu s jednoduchým hardwarovým zapojením činí z I²C velmi populární volbu pro propojení senzorů, pamětí, převodníků a dalších periférií.

Typické přenosové rychlosti této sběrnice se pohybují od 100 kbit/s (standard mode) až po 3,4 Mbit/s (high-speed mode), což pokrývá široké spektrum potřeb v oblasti řízení a sběru dat. Díky své jednoduchosti, flexibilitě a široké podpoře je I²C jednou z nejrozšířenějších komunikačních sběrnic v oblasti mikrokontrolérů a embedded elektroniky.

Napěťová úroveň sběrnice I²C závisí na konkrétním implementovaném standardu a napájecím napětí použitých zařízení. I²C je navržena tak, aby byla flexibilní a podporovala různé napěťové úrovně, což umožňuje její použití v širokém spektru aplikací. Napěťová komunikace v našem případě bude na úrovni dané napájecím napětím, tedy u modulu ESP32 to je napětí 3,3 V.

Modul PCA9685 – I²C Řadič pro PWM

Modul PCA9685 je I²C řadič, který umožňuje generování několika PWM signálů. Tento modul se skvěle hodí pro jakékoliv aplikace, které využívají PWM, ať už jde o řízení DC motorů, regulaci jasu LED diod nebo, což je asi nejběžnější použití, pro ovládání více servomotorů.

Co je na tomto modulu opravdu skvělé, je to, že k ovládání až 16 nezávislých PWM výstupů stačí jen dva vodiče sběrnice I²C. To výrazně zjednodušuje zapojení a šetří množství kabelů, což oceníte především v komplikovanějších projektech. PCA9685 je navíc snadno programovatelný, flexibilní a poskytuje velmi přesný výstup, což z něj činí ideální volbu pro pokročilé projekty s více servy nebo jinými zařízeními vyžadujícími PWM.

pca9685 servo driver

Každý modul pracující na sběrnici I²C má svou unikátní adresu, a PCA9685 není výjimkou. Tento modul využívá 6bitovou hardwarovou adresu, která je nastavena pomocí SMD rezistorů na desce. Díky tomu můžeme připojit až 64 modulů na jednu sběrnici, což nám umožňuje řídit až 1024 PWM výstupů. To už je solidní kapacita, co říkáte? 😊

Modul nabízí 12bitové rozlišení výstupního signálu, což znamená, že délku PWM pulzu (v rozsahu od 0 % do 100 %) lze nastavit na hodnoty mezi 0 a 4096. To poskytuje velmi jemné možnosti řízení.

Další výhodou PCA9685 je jeho vestavěný krystalový oscilátor s frekvencí 25 MHz, což znamená, že modul je plně autonomní a nevyžaduje externí krystal. Pokud bychom však potřebovali externí oscilátor, modul podporuje i připojení krystalového oscilátoru s frekvencí až 50 MHz.

Pokud jde o výstupní frekvenci PWM signálů, ta se pohybuje v rozsahu od 40 Hz do 1000 Hz, což poskytuje širokou škálu pro různé aplikace. Modul má také široký rozsah napájecího napětí mezi 2,3 V až 5,5 V, což je ideální pro použití s naším 3,3 V modulem ESP32.

Připojení serva a modulu ESP32 k PCA9685

Modul ESP32, který má předem definované piny pro sběrnici I²C (GPIO21 pro SCL a GPIO22 pro SDA), připojíme k modulu PCA9685 pomocí těchto pinů. Servo SG90 připojíme k výstupnímu kanálu 0 na PCA9685 – žlutý pin konektoru slouží jako PWM signál pro řízení serva, zatímco červený a černý pin slouží pro napájení. Pro napájení modulu PCA9685 použijeme napětí 3,3 V přímo z modulu ESP32 (pin 3V3). Ačkoliv je doporučeno napájet servo externím zdrojem připojeným k napájecí svorkovnici, v tomto případě, kdy pracujeme pouze s nezatíženým servem, budeme servo napájet 5 V taktéž přímo z modulu ESP32 (pin 5V). Celé zapojení je znázorněno na následujícím obrázku a v tabulce.

PCA9685 + ESP32 - schema
Modul PCA9685 (I²C) Modul ESP32
GND GND
OE ---
SCL GPIO21
SDA GPIO22
Vcc 3V3
V+ 5V
Modul PCA9685 (PWM) Servo
PWM oranžová
V+ červená
GND hnědá

Po propojení obou modulů můžeme začít s programováním. Zatímco v předchozích zapojeních nám stačily pouze základní knihovny, v tomto případě se bez specializovaných knihoven rozhodně neobejdeme.

Řízení modulu pomocí I²C a Soft-I²C

Stejně jako u sběrnice SPI umožňuje modul ESP32 provozovat sběrnici I²C buď pomocí hardwarového řadiče (I2C), nebo pomocí softwarové emulace (SoftI2C). Pokud využíváme hardwarový řadič, musíme opět dodržet předem definované GPIO piny – pro SDA je to GPIO22 a pro SCL to na modulu ESP32 je GPIO21.

Na rozdíl od sběrnice SPI, kde jsme měli určité komplikace při použití s OLED displejem, zde není důvod upřednostňovat jeden způsob nad druhým. Rozhodneme se tedy pro hardwarový řadič, což už jsme zohlednili při propojení modulu PCA9685 s ESP32 (viz předchozí schéma a tabulka). Do našeho programu tedy importujeme modul I2C.

Kéž by všechno šlo tak hladce jako import knihovny pro sběrnici I²C!

Knihovna pro modul PCA9685

Aby to všechno nebylo až příliš jednoduché (a abychom měli stále o čem psát 😊), nalezení „rozumné“ knihovny pro modul PCA9685 fungující v MicroPythonu na ESP32 se ukazuje jako pořádně dobrodružný úkol. Návodů na připojení modulu PCA9685 k zařízením jako je Raspberry Pi nebo Raspberry Pi Pico je k dispozici spousta. Několik repozitářů pro ESP32 také existuje, ale většina z nich byla uzavřena autory, a to většinou s tím, že knihovna již není aktuální a byla nahrazena novější verzí – Adafruit_CircuitPython_PCA9685. Na první pohled to nemusí vypadat jako problém! Navíc firma Adafruit je známým výrobcem mnoha modulů, takže bychom mohli očekávat, že tato knihovna bude přímo od zdroje. Co víc by si bastlíř mohl přát?

A zde přichází ta zrada celého řešení od Adafruit – je to komplexnost celého jejich prostředí. Když se pokusíme použít knihovnu pro PCA9685, zjistíme, že tato knihovna vyžaduje další závislosti, a to na:

  • Adafruit CircuitPython
  • Bus Device
  • Register

A to je právě ten problém. Adafruit CircuitPython je speciální pythonovský firmware, který je podporaván jen na některých zažízeních. No, a běžný modul ESP32 mezi ně nepatří! (podporovány jsou pouze typy ESP32-S2 a ESP32-S3)

Ať se na nás vývojáři z Adafruit třeba zlobí, ale pro nás je jejich Adafruit CircuitPython i s celou knihovnou Adafruit_CircuitPython_PCA9685 prostě na… na nic. Tečka! 😟

Abychom si tedy mohli „jen tak“ zahýbat servem s malým a jednoduchým kódem a zároveň použít relativně lehkou knihovnu, nakonec jsme museli sáhnout po některé z již starších a neudržovaných knihoven. A aby toho nebylo málo, nakonec jsme tuto knihovnu museli ještě trochu upravit, aby vše fungovalo jak má. Tak to už někdy v domácím vývoji elektroniky chodí…

Následující kód představuje upravenou verzi knihovny, kterou budeme používat i v našem ukázkovém programu. Kód si zkopírujeme do prostředí Thonny IDE a uložíme jej jak na PC (pro případné budoucí použití), tak do zařízení ESP32 pro běh programu. K tomu můžeme využít buď metodu Uložit jako…, nebo dnešní metodu Nahrát do…. V obou případech soubor uložíme pod názvem pca9685.py.

Kód knihovny pca9685.py: (s ohledem na rozsah kódu jej uvádíme bez obarvení syntaxe)

from machine import Pin
import time
import struct

class PCA9685Driver:
    """PCA9685 driver class"""
    OSCILLATION_FREQ = 25000000
    PWM_RESOLUTION = 4096
    def __init__(self, i2c, i2c_periph_addr = 0x40):
        self.i2c = i2c # initialize I2C bus & pins
        self.periph_addr = i2c_periph_addr # set I2C Peripheral address
        self.pwm_frequency = 50 #initialize PWM frequency to 50Hz. This can be changed via setPWMFrequency function
        self.prescaler = round(PCA9685Driver.OSCILLATION_FREQ/(PCA9685Driver.PWM_RESOLUTION*self.pwm_frequency))-1 # calculate the prescaler
        self._write_reg(PCA9685Registers.PRESCALE, self.prescaler) # write prescaler value to PRESCALE register
        # needs to be done before turning on the internal oscillator
        #Turn on internal oscillator
        mode1_val = self._read_reg(PCA9685Registers.MODE1)
        #print(mode1_val)
        mode1_val = self._clear_bit(mode1_val, MODE1RegBits.SLEEP)
        self._write_reg(PCA9685Registers.MODE1, mode1_val)
        #print(mode1_val)
        time.sleep_us(500)

    def _read_reg(self, register):
        """Helper function to read register contents. Converts byte data to an integer value """
        return int.from_bytes(self.i2c.readfrom_mem(self.periph_addr, register, 1), 'big')

    def _write_reg(self, register, value):
        """Helper function to write bytes into a register. Converts 'value' from int type to byte type"""
        self.i2c.writeto_mem(self.periph_addr, register, struct.pack('B', value))

    def _clear_bit(self, value, bit):
        return value & ~(1 << bit)

    def _set_bit(self, value, bit):
        return value | (1 << bit)

    def set_pwm_frequency(self,pwm_freq):
        """Sets the PWM Frequency. Need to make sure internal oscillator is of before adjusting prescaler value"""
        if( pwm_freq >= 24 and pwm_freq <= 1526): # PCA9685 can generate frequencies in this range (more or less)
            mode1_val = self._read_reg(PCA9685Registers.MODE1)
            if(((mode1_val >> MODE1RegBits.SLEEP) & 0x01) == 0 ): # if internal oscillator is already ON...
                mode1_val = self._set_bit(mode1_val, MODE1RegBits.SLEEP)
                self._write_reg(PCA9685Registers.MODE1, mode1_val) # turn it OFF before adjusting prescaler value
                time.sleep_us(500)

            self.pwm_frequency = pwm_freq
            self.prescaler = round(PCA9685Driver.OSCILLATION_FREQ/(PCA9685Driver.PWM_RESOLUTION*self.pwm_frequency))-1 # Now calculate the new prescaler value for new frequency
            self._write_reg(PCA9685Registers.PRESCALE, self.prescaler) # write it to PRESCALER register

            mode1_val = self._read_reg(PCA9685Registers.MODE1) #RMW op
            if(((mode1_val >> MODE1RegBits.SLEEP) & 0x01) != 0 ):
                mode1_val = self._clear_bit(mode1_val, MODE1RegBits.SLEEP)
                self._write_reg(PCA9685Registers.MODE1, mode1_val) # Turn internal oscillator back ON again
                time.sleep_us(500)
            else:
                print("PWM Frequency needs to be between 24 and 1526 Hz")

    def set_pwm_dc_percent(self,chan, dc_percent):
        """Sets duty cycle as a percentage between 0 & 100%. Can use decimal percentage values i.e. 88.5"""
        if( chan >= 0 and chan <= 15): # Validate channel number
            if(dc_percent >= 0 and dc_percent <= 100): # validate duty cycle value
                off_val = round(dc_percent*4095/100)
                self._write_reg(chan*4+6, 0) # write start ON and OFF times into the 4 registers associated with channel
                self._write_reg(chan*4+7, 0)
                self._write_reg(chan*4+8, (off_val & 0xFF))
                self._write_reg(chan*4+9, ((off_val >> 8) & 0x0F))
            else:
                print("Duty cycle value needs to be between 0 and 100")
        else:
            print("Channel value needs to be between 0 and 15")

    def set_pwm_dc_ontime(self,chan, on_time_ms):
        """Sets duty cycle by ON time. Rising edge is assumed to start at 0 """
        if( chan >= 0 and chan <= 15):# Validate channel number
            period_ms = (1.0 / self.pwm_frequency)*1000 #calculate period
            if(on_time_ms >= 0.0 and on_time_ms <= period_ms): # ON time has to be less than the period
                off_val = round(on_time_ms*4095/period_ms) #Calculate ON time duration
                self._write_reg(chan*4+6, 0) #write start ON and OFF times into the 4 registers associated with channel
                self._write_reg(chan*4+7, 0)
                self._write_reg(chan*4+8, (off_val & 0xFF))
                self._write_reg(chan*4+9, ((off_val >> 8) & 0x0F))
            else:
                print("ON time value needs to be between 0.0 msec and {} msec".format(period_ms))
        else:
            print("Channel value needs to be between 0 and 15")

    def set_pwm_dc(self, chan, falling_edge_cnt ,rising_edge_cnt=0):
        """Most flexible. Sets duty cycle by ON time via two parameters; rising edge start time and falling edge start time"""
        if( chan >= 0 and chan <= 15): # Validate channel number
            if(rising_edge_cnt >= 0 and rising_edge_cnt <= 4095): #Rising edge time/cnt must be between 0 and 4095
                if(falling_edge_cnt > rising_edge_cnt and falling_edge_cnt <= 4095):# Whereas falling edge cnt must be larger than rising edge cnt
                    self._write_reg(chan*4+6, rising_edge_cnt & 0xFF) #write start ON and OFF times into the 4 registers associated with channel
                    self._write_reg(chan*4+7, (rising_edge_cnt >> 8) & 0x0F)
                    self._write_reg(chan*4+8, falling_edge_cnt & 0xFF)
                    self._write_reg(chan*4+9, (falling_edge_cnt >> 8) & 0x0F)
                else:
                    print("falling_edge_cnt value needs to be between rising_edge_cnt: {} and 4095".format(rising_edge_cnt))
            else:
                print("rising_edge_cnt value needs to be between 0 and 4095")
        else:
            print("Channel value needs to be between 0 and 15")

    def servo_set_angle(self, chan, angle):
        """Abstracted Servo function. Assumes angle 0 => 1ms ON pulse and angle 180 => 2ms ON pulse"""
        if self.pwm_frequency != 50:
            self.set_pwm_frequency(50)

        period_ms = (1.0/ self.pwm_frequency) * 1000

        if angle >= 0.0 and angle <= 180.0 :
            on_time_ms = 1.0 + (angle/180.0)*1.0
            self.set_pwm_dc_ontime(chan, on_time_ms)
        else:
            print("Angle value needs to be between 0 and 180")

    def servo_set_angle_custom(self, chan, angle, minpulsewidth_ms, maxpulsewidth_ms):
        """Abstracted Servo function. lets user set ON pulse for angle 0 => 'minpulsewidth_ms' angle 180 => 'maxpulsewidth_ms'"""
        if self.pwm_frequency != 50:
            self.set_pwm_frequency(50)

        period_ms = (1.0/ self.pwm_frequency) * 1000

        if angle >= 0.0 and angle <= 180.0 :
            if ( maxpulsewidth_ms >= minpulsewidth_ms):
                on_time_ms = minpulsewidth_ms + (angle/180.0)*(maxpulsewidth_ms - minpulsewidth_ms)
                self.set_pwm_dc_ontime(chan, on_time_ms)
            else:
                print("maxpulsewidth_ms needs to be larger than or equal to minpulsewidth_ms")
        else:
            print("Angle value needs to be between 0 and 180")

    def disable_clk(self):
        """Disable internal oscillator"""
        mode1_val = self._read_reg(PCA9685Registers.MODE1)
        if(((mode1_val >> MODE1RegBits.SLEEP) & 0x01) != 1 ):
            mode1_val = self._set_bit(mode1_val,MODE1RegBits.SLEEP)
            self._write_reg(PCA9685Registers.MODE1, mode1_val)
            time.sleep_us(500)

    def enable_clk(self):
        """Enable internal oscillator"""
        mode1_val = self._read_reg(PCA9685Registers.MODE1)
        if(((mode1_val >> MODE1RegBits.SLEEP) & 0x01) != 0 ):
            mode1_val = self._clear_bit(mode1_val,MODE1RegBits.SLEEP)
            self._write_reg(PCA9685Registers.MODE1, mode1_val)
            time.sleep_us(500)

    def restart_clk(self):
        """Restart internal oscillator from where it left of. See section 7.3.1.1 in datasheet"""
        mode1_val = self._read_reg(PCA9685Registers.MODE1)
        if(((mode1_val >> MODE1RegBits.RESTART) & 0x01) == 1 ):
            mode1_val = self._clear_bit(mode1_val,MODE1RegBits.SLEEP)
            self._write_reg(PCA9685Registers.MODE1, mode1_val)
            time.sleep_us(500)
            mode1_val = self._read_reg(PCA9685Registers.MODE1)
            mode1_val = self._set_bit(mode1_val, MODE1RegBits.RESTART)
            self._write_reg(PCA9685Registers.MODE1, mode1_val)
            time.sleep_us(500)

    def switch_to_ext_clk(self):
        """Switch to using external clock. See section 7.3.1 p14 in datasheet"""
        mode1_val = self._read_reg(PCA9685Registers.MODE1)
        mode1_val = self._set_bit(mode1_val, MODE1RegBits.SLEEP)
        self._write_reg(PCA9685Registers.MODE1, mode1_val)
        time.sleep_us(500)
        mode1_val = self._read_reg(PCA9685Registers.MODE1)
        mode1_val |= ((1 << MODE1RegBits.SLEEP) | (1 << MODE1RegBits.EXTCLK))
        self._write_reg(PCA9685Registers.MODE1, mode1_val)

    def sw_reset(self):
        """Software reset see section 7.6 in Datasheet"""
        self.i2c.writeto(0x00, struct.pack('B', 0x06))

class MODE1RegBits:
    """MODE1 register bits"""
    SLEEP = 4
    AI = 5
    EXTCLK = 6
    RESTART = 7

class PCA9685Registers:
    """List of PCA9685 internal registers"""
    MODE1 = 0x00
    MODE2 = 0x01
    SUBADR1 = 0x02
    SUDADR2 = 0x03
    SUBADR3 = 0x04
    ALLCALLADR = 0x05
    LED0_ON_L = 0x06
    LED0_ON_H = 0x07
    LED0_OFF_L = 0x08
    LED0_OFF_H = 0x09
    LED1_ON_L = 0x0A
    LED1_ON_H = 0x0B
    LED1_OFF_L = 0x0C
    LED1_OFF_H = 0x0D
    LED2_ON_L = 0x0E
    LED2_ON_H = 0x0F
    LED2_OFF_L = 0x10
    LED2_OFF_H = 0x11
    LED3_ON_L = 0x12
    LED3_ON_H = 0x13
    LED3_OFF_L = 0x14
    LED3_OFF_H = 0x15
    LED4_ON_L = 0x16
    LED4_ON_H = 0x17
    LED4_OFF_L = 0x18
    LED4_OFF_H = 0x19
    LED5_ON_L = 0x1A
    LED5_ON_H = 0x1B
    LED5_OFF_L = 0x1C
    LED5_OFF_H = 0x1D
    LED6_ON_L = 0x1E
    LED6_ON_H = 0x1F
    LED6_OFF_L = 0x20
    LED6_OFF_H = 0x21
    LED7_ON_L = 0x22
    LED7_ON_H = 0x23
    LED7_OFF_L = 0x24
    LED7_OFF_H = 0x25
    LED8_ON_L = 0x26
    LED8_ON_H = 0x27
    LED8_OFF_L = 0x28
    LED8_OFF_H = 0x29
    LED9_ON_L = 0x2A
    LED9_ON_H = 0x2B
    LED9_OFF_L = 0x2C
    LED9_OFF_H = 0x2D
    LED10_ON_L = 0x2E
    LED10_ON_H = 0x2F
    LED10_OFF_L = 0x30
    LED10_OFF_H = 0x31
    LED11_ON_L = 0x32
    LED11_ON_H = 0x33
    LED11_OFF_L = 0x34
    LED11_OFF_H = 0x35
    LED12_ON_L = 0x36
    LED12_ON_H = 0x37
    LED12_OFF_L = 0x38
    LED12_OFF_H = 0x39
    LED13_ON_L = 0x3A
    LED13_ON_H = 0x3B
    LED13_OFF_L = 0x3C
    LED13_OFF_H = 0x3D
    LED14_ON_L = 0x3E
    LED14_ON_H = 0x3F
    LED14_OFF_L = 0x40
    LED14_OFF_H = 0x41
    LED15_ON_L = 0x42
    LED15_ON_H = 0x43
    LED15_OFF_L = 0x44
    LED15_OFF_H = 0x45
    ALL_LED_ON_L = 0xFA
    ALL_LED_ON_H = 0xFB
    ALL_LED_OFF_L = 0xFC
    ALL_LED_OFF_H = 0xFD
    PRESCALE = 0xFE
    TEST_MODE = 0xFF

Pozn:
Samozřejmě nesmíme zapomenout zmínit, že výše uvedená knihovna je odvozena z původního kódu knihovny pca9685, jejímž autorem je Hussam Al-Hertan.

Zatímco samotná knihovna je poměrně rozsáhlá, následující program, který využívá její metody a funkce, bude poměrně stručný. Cílem tohoto programu je ovládání serva připojeného k nultému PWM kanálu modulu PCA9685. Servo se bude pohybovat mezi dvěma krajními polohami, přičemž v každém kroku bude na výstupu prostředí Thonny IDE vypsán úhel, do kterého je servo natočeno.

Nejprve uvedeme celý kód programu a následně se podíváme na jeho klíčové části.

Kód programu:

from machine import Pin, I2C
from pca9685 import PCA9685Driver # https://github.com/halherta/pca9685 (upraveno!)
from time import sleep_ms

# nastaveni I2C sbernice
i2c1 = I2C(1, scl=Pin(21), sda=Pin(22), freq=100000)

# Vypis I2C adres pripojenych zarizeni
print('Skenuji i2c sbernici...')
devices = i2c1.scan()
if len(devices) == 0:
    print('Zadne i2c zarizeni !')
else:
    print('Pocet i2c zarizeni:', len(devices))
    for device in devices:
        print("Hex adresa: ", hex(device), " | Dec adresa: ", device)
print("")
sleep_ms(2000)

# Initialize PCA9685
pwm = PCA9685Driver(i2c=i2c1, i2c_periph_addr = 0x40)
# Kromě běžného adresování, které začíná na 0x40,
# existuje adresa „All Call“ na 0x70 pro adresování
# všech zařízení PCA9685 současně.

# nastaveni PWM frekvence pro servo
pwm.set_pwm_frequency(50)

# nastaveni kanalu se servem
kanal = 0

while True:
    for uhel in range(0, 181, 1):
        pwm.servo_set_angle_custom(kanal, uhel, 0.5, 2.5)
# Obecně platí, že impulsy o délce 1 ms odpovídají poloze 0 stupňů,
# 1,5 ms 90 stupňům a 2 ms 180 stupňům. Pak lze použít:
# pwm.servo_set_angle(0, angle)
# Ale minimální a maximální doba trvání impulsů se může u různých značek někdy
# lišit a může být 0,5 ms pro polohu 0 stupňů a 2,5 ms pro polohu 180 stupňů.
        print(f"\ruhel: {uhel}°   ", end="") # Prázdné znaky pro vymazání případných starých hodnot
        sleep_ms(2)
    sleep_ms(250)
    for uhel in range(180, -1, -1):
        pwm.servo_set_angle_custom(kanal, uhel, 0.5, 2.5)
        # pwm.servo_set_angle(0, angle)
        print(f"\ruhel: {uhel}°   ", end="")
        sleep_ms(2)
    sleep_ms(250)

Popis programu:

První část kódu se zabývá importem potřebných knihoven. Původní verze knihovny pca9685 (autor Hussam Al-Hertan) automaticky importovala a konfigurovala modul SoftI2C, což bylo vhodné pro některé aplikace. V našem případě však chceme využít hardwarové řízení sběrnice I²C, a také si přejeme mít větší flexibilitu při výběru a nastavení I²C sběrnice. Proto jsme v naší upravené verzi knihovny tyto kroky rozdělili a nyní je třeba importovat modul pro I²C (ať už I2C nebo SoftI2C) samostatně.

Vytváříme objekt, nazvaný i2c1, který konfiguruje sběrnici I²C. Tento objekt nastaví komunikační parametry, přičemž používá hardwarový I²C řadič č. 1 modulu ESP32 a nastavuje frekvenci sběrnice na 100 kb/s. I když je zde definice pinů pro SDA a SCL v tomto případě zbytečná, zahrnujeme ji pro případ, že bychom v budoucnu upravili kód pro použití SoftI2C, kdy by byla potřeba. Při použití hardwarového řadiče I²C tato nadbytečná definice pinů neovlivní funkčnost.

i2c1 = I2C(1, scl=Pin(21), sda=Pin(22), freq=100000)

Tento objekt i2c1 následně předáme knihovně pro modul PCA9685, aby mohla využít hardwarovou sběrnici pro komunikaci s modulem.

Metoda scan() pro zjištění připojených zařízení na I²C sběrnici

Jakmile máme nadefinovaný objekt pro práci se sběrnicí I²C, můžeme využít metodu scan(), která je velmi užitečná při práci s I²C sběrnicí. Tato metoda slouží jako skener periférií, které jsou připojené na sběrnici I²C. Metoda scan() vrátí seznam (pole) všech adres zařízení, která odpoví na I²C dotaz. To je obzvlášť užitečné, pokud si nejsme jisti, jakou adresu má naše připojené zařízení, nebo pokud chceme ověřit, zda je zařízení správně připojeno a komunikace s ním funguje.

V následujícím kódu se využije metoda scan() pro zjištění připojených zařízení a jejich adresy jsou vypsány jak v hexadecimálním, tak v desetinném formátu. Tento krok nám pomůže zjistit adresy zařízení, která jsou připojena k sběrnici.

# Vypis I2C adres pripojenych zarizeni
print('Skenuji i2c sbernici...')
devices = i2c1.scan()
if len(devices) == 0:
    print("Zadne i2c zarizeni !")
else:
    print('Pocet i2c zarizeni:', len(devices))
    for device in devices:
        print("Hex adresa: ", hex(device), " | Dec adresa: ", device)
print("")
sleep_ms(2000)

Následující obrázek ukazuje výstup, který dostaneme při připojení modulu PCA9685.

I2C scan - vystup - Thonny
Proč nám modul ESP32 našel dvě zařízení na I²C sběrnici?

Možná nás překvapí, že modul ESP32 na sběrnici I²C detekoval dvě zařízení. Tento výsledek může v první chvíli vyvolat zmatek, ale není to nic neobvyklého. Rychlé vysvětlení nám poskytne datový list modulu PCA9685, podle kterého je náš I²C PWM řadič pojmenován. Tento modul totiž využívá dvě adresy.

První adresa, 0x40, slouží pro zasílání příkazů konkrétním PWM kanálům. Druhá adresa, 0x70, je určena pro globální příkazy, které ovlivňují všechny kanály najednou. Například při poslání příkazu na adresu 0x70 se všech 16 připojených serv natočí do nějaké výchozí pozice. Pro naše jediné servo ale v podstatě není rozdíl, kterou adresu použijeme – nicméně zůstáváme u 0x40 pro běžnou komunikaci.

Po vyřešení těchto I²C záležitostí se konečně dostaneme k modulu PCA9685. Nejprve deklarujeme objekt, který nám umožní komunikovat s tímto řadičem. Jako parametry použijeme objekt I²C sběrnice a adresu modulu. Poté nastavíme požadovanou frekvenci PWM signálu na 50 Hz pomocí metody set_pwm_frequency(50). A nakonec definujeme kanál, na kterém máme připojené servo.

pwm = PCA9685Driver(i2c=i2c1, i2c_periph_addr = 0x40)
pwm.set_pwm_frequency(50)
kanal = 0

V kódu následují dva cykly, které otáčejí servo mezi úhly 0° a 180° a zpět. Pro nastavení pozice serva se běžně používá metoda servo_set_angle(), kde první parametr udává číslo kanálu, na kterém je servo připojeno, a druhý parametr určuje požadovaný úhel ve stupních. Příklad standardního použití vypadá takto:

pwm.servo_set_angle(kanal, angle)

V našem případě však používáme metodu servo_set_angle_custom(), protože metoda servo_set_angle() je navržena pro serva, která přijímají pulzy v rozsahu 1 až 2 ms. Naše servo však vyžaduje pulzy v rozsahu 0,5 až 2,5 ms.

pwm.servo_set_angle_custom(kanal, uhel, 0.5, 2.5)

Dva poslední parametry definují minimální a maximální délku pulzů – 0,5 ms pro minimální úhel a 2,5 ms pro maximální úhel. Tímto způsobem přizpůsobíme ovládání serva podle jeho požadavků.

A tím máme nastavení serva v programu kompletní!

PCA + ESP32

Na závěr bychom rádi zmínili jednu zajímavou „pythnominu“, která se objevuje v našem programu. Jde o následující zápis pro výpis hodnoty úhlu:

print(f"\ruhel: {uhel}° ", end="")

Zápis f-řetězce (f"něco: {promenna}") a parametr end="" bychom již měli znát, ale co znamená to zpětné lomítko \r? Tento znak je označován jako carriage return (CR), což je instrukce, která posune kurzor zpět na začátek aktuálního řádku. Možná si na znak \r vzpomeneme z webových požadavků (článek o tvorbě webového klienta a serveru), kde jsme se v tzv. requestech se znaky \r a \n setkávali.

V našem případě je využití \r ve spojení s parametrem end="" praktické: místo toho, aby se každý výpis úhlu objevil na nové řádce, se hodnota úhlu neustále přepisuje na stejném místě na obrazovce. Tímto způsobem se vyhneme tomu, že by se hodnoty (například všech 180 úhlů při cestě tam a zpět) vypsaly pod sebe. Výsledek v okně výstupu Thonny IDE bude vypadat mnohem přehledněji.

I2C servo - vystup Thonny

Závěr

Na první pohled by se asi řeklo, že o řízení serva pomocí PWM modulace, či pomocí běžně prodávaného I²C modulu není příliš co psát. No, výše uvedené řádky nás v tomto tvrzení asi moc neutvrdily.

V tomto článku jsme si prošli základními technikami ovládání servomotoru pomocí modulu ESP32 a programovacího jazyka MicroPython. Začali jsme s nejzákladnější metodou řízení přes čistý PWM, kde jsme se naučili spočítat délku pulzu a správně nastavit pozici serva. Následně jsme ukázali výhody využití knihovny, která tento výpočet zjednodušuje a umožňuje uživateli jednoduše zadávat požadovaný úhel.

Ve třetí části jsme se ponořili do problematiky ovládání serva přes I²C sběrnici s využitím řadiče PCA9685. Tento přístup nám ukázal nejen možnosti sběrnice I²C, ale také praktické problémy, které mohou nastat při práci s některými knihovnami. I když původní knihovna nevyhovovala našim potřebám, díky drobným úpravám jsme se dostali k funkčnímu řešení a umožnili tak efektivní ovládání více serv pomocí jediného řadiče.

A co nás čeká dál? No, přiznám se, že ještě úplně nevíme. Ale rozhodně máme v plánu se podívat na práci s dalšími čidly na sběrnici I²C. Možná se podíváme na barometrické čidlo BMP180, které by nám mohlo poskytnout informace o atmosférickém tlaku a teplotě – to zní skvěle, ne? Mohlo by nám to otevřít spoustu možností, třeba pro měření nadmořské výšky, predikci počasí nebo další „IoT kouzla“.

Ale… ještě to není úplně jisté. Možná zvolíme jiný směr, možná nás napadne něco úplně jiného. Kdo ví? Každopádně se na to těšíme a doufáme, že i Vás to bude bavit. Tak držte nám palce a buďte s námi, protože naše dobrodružství s ESP32 a MicroPythonem rozhodně nekončí – máme toho ještě spoustu v plánu! A snad Vám naše postřehy a hrátky pomohou rozjet vlastní projekty.

Pokračování: Další článek se připravuje…
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!