MicroPython i CircuitPython: programowanie sprzętu IoT w nowoczesnym wydaniu

0
28
Rate this post

Nawigacja:

Dlaczego Python trafia na mikrokontrolery: kontekst IoT i nowoczesne podejście

Arduino, C/C++ i PlatformIO kontra „Python na płytce”

Programowanie sprzętu przez lata było kojarzone przede wszystkim z C i C++. Arduino, STM32, AVR, PlatformIO – to klasyczny zestaw narzędzi. Deweloper, który chciał zapalić diodę LED czy odczytać czujnik, kompilował kod w C, wgrywał przez bootloader, a debugowanie polegało często na drukowaniu tekstu przez UART. Taki model wciąż dominuje w przemyśle, ale dla wielu programistów Pythona jest barierą wejścia.

MicroPython i CircuitPython przesuwają akcent z „programowania rejestrów” na programowanie zachowań. Zamiast kompilacji, konfiguracji toolchaina i szukania właściwego pliku nagłówkowego, użytkownik dostaje interpreter Pythona działający bezpośrednio na mikrokontrolerze. Skrypt można zmieniać na żywo, a efekt widać niemal natychmiast – szczególnie w środowiskach z REPL (interaktywną konsolą).

Nie oznacza to, że C/C++ odchodzą do lamusa. Raczej pojawia się obok nich druga ścieżka: Python na płytce jako narzędzie do szybkiego prototypowania, edukacji i lekkich aplikacji IoT. Tam, gdzie liczy się skrócenie czasu do pierwszego działającego prototypu, Python ma wyraźną przewagę.

Dlaczego programiści Pythona wchodzą w hardware i IoT

Osoby pracujące zawodowo z Django, Flaskiem, Pandas czy automatami testów coraz częściej stykają się z urządzeniami IoT. Pojawiają się wymagania typu: integracja z czujnikiem, szybkie demo proof-of-concept na targi, lokalny rejestrator danych z Wi‑Fi. Znajomość Pythona kusi, by wykorzystać ten sam język po „drugiej stronie kabla”.

Przejście z backendu do mikrokontrolera tradycyjną ścieżką (C, rejestry, pliki nagłówkowe, toolchainy) bywa frustrujące. MicroPython i CircuitPython obniżają próg wejścia. Składnia, standardowe konstrukcje, a nawet część bibliotek są znajome. Różni się tylko otoczenie – brak systemu operacyjnego w klasycznym rozumieniu i ograniczone zasoby. Dzięki temu osoba znająca Pythona może w ciągu jednego weekendu zbudować sensowny prototyp czujnika z wysyłką danych do chmury.

W praktyce widać dwa typowe scenariusze:

  • programista Pythona wchodzi w IoT, bo firma testuje nowe produkty lub usługi;
  • hobbysta lub inżynier spoza IT szuka prostszego wejścia w mikrokontrolery niż pełne C/C++.

W obu przypadkach Python na mikrokontrolerze daje szybki efekt i pozwala skupić się na logice domenowej, zamiast walczyć z konfiguracją kompilatora.

Zalety wysokopoziomowej warstwy na mikrokontrolerze

Największą przewagą MicroPython i CircuitPython jest szybkość prototypowania. Krótki kod, brak kompilacji, możliwość podmiany skryptu jak pliku tekstowego – to realnie skraca czas od pomysłu do działającej płytki. Przy projektach IoT, gdzie i tak wiele czasu pochłania dobór czujników, obudowy, zasilania czy sieci, każdy taki zysk ma znaczenie.

Druga istotna cecha to czytelność kodu. Python jest językiem wysokopoziomowym: obsługa list, słowników, stringów, struktury danych pod API HTTP czy MQTT można wyrazić w kilku linijkach. Kod, który w C zajmie kilkadziesiąt linii, w MicroPythonie często zamyka się w kilku wyraźnych funkcjach. To ułatwia utrzymanie, wprowadzanie zmian i przekazywanie projektu innym osobom.

Na poziomie organizacji pracy wysokopoziomowa warstwa daje jeszcze jedną przewagę: łatwiejsze dzielenie się odpowiedzialnościami. Inżynierowie od niskopoziomowej warstwy mogą pisać i optymalizować firmware w C, a logika biznesowa, scenariusze IoT, formaty danych czy integracje z chmurą mogą powstawać w Pythonie. Na wielu płytkach MicroPython jest właśnie taką cienką warstwą nad HAL-em (Hardware Abstraction Layer).

Ograniczenia: pamięć, wydajność i czas reakcji

Python na mikrokontrolerze ma jednak konkretne ograniczenia. Mikrokontrolery to nie pełne komputery. Dla wielu płytek typowy RAM to rząd kilkuset kilobajtów lub kilku megabajtów w lepszym wariancie, a pamięć Flash liczona jest w megabajtach, a nie dziesiątkach gigabajtów. Interpreter Pythona, biblioteki i Twój kod muszą się w tym zmieścić.

Wydajność kodu Python na MCU jest niższa niż kodu w C. Interpreter wprowadza narzut, a garbage collector (GC) co jakiś czas zatrzymuje świat na ułamek sekundy. Dla większości aplikacji IoT (pomiar raz na kilka sekund, wysyłka danych, sterowanie przekaźnikiem) to w ogóle nie przeszkadza. W zastosowaniach wymagających twardego czasu rzeczywistego (precyzyjne sterowanie silnikiem, bardzo szybkie próbkowanie sygnałów) MicroPython lub CircuitPython często nie będą optymalnym wyborem.

Uogólniając:

  • Python ma sens przy lekkich czujnikach IoT, wizualizacjach LED, prostych sterownikach, edukacji, prototypowaniu.
  • C/C++ lub specjalizowany firmware będą lepsze przy wymagającej elektronice, wysokich częstotliwościach i twardych wymaganiach czasowych.

Wiele rozwiązań łączy oba światy: krytyczne fragmenty w C, „logika biznesowa” w MicroPythonie lub CircuitPythonie.

MicroPython i CircuitPython – co to jest i czym się różnią

Wspólne fundamenty: Python 3 na dietetycznym firmware

MicroPython i CircuitPython wywodzą się z tego samego pomysłu: implementacja Pythona 3 dostosowana do mikrokontrolerów. Obydwa projekty dostarczają firmware, który wgrywa się na płytkę zamiast lub obok klasycznego programu w C. Firmware zawiera:

  • interpretator języka Python (VM, garbage collector, runtime),
  • podzbiór standardowych modułów znanych z CPython, dostosowany do ograniczeń MCU,
  • moduły sprzętowe: GPIO, I2C, SPI, UART, Timery, PWM, itp.,
  • REPL – interaktywną konsolę, która pozwala wykonywać kod linia po linii.

Kluczowa cecha wspólna: składnia to Python 3. Istnieją pewne różnice w dostępnych modułach i szczegółach implementacyjnych, ale konstrukcje typu funkcje, klasy, listy, kontekst menedżerów czy wyjątki działają w dobrze znany sposób. Dzięki temu przejście od Pythona na PC do Pythona na płytce nie wymaga zmiany mentalnego modelu języka.

Geneza: MicroPython – projekt systemowy, CircuitPython – zwrot w stronę edukacji

MicroPython został stworzony przez Damiena George’a jako projekt mający dostarczyć „poważny” Python na mikrokontrolery. Celem była praktycznie użyteczna platforma do budowy rzeczywistych systemów wbudowanych, z naciskiem na wydajność, możliwość portowania na różne architektury i stosunkowo niską warstwę abstrakcji nad sprzętem.

CircuitPython powstał jako fork MicroPythona rozwijany głównie przez Adafruit. Główną ideą było uproszczenie pracy początkującym: minimalizacja konfiguracji, filozofia plug-and-play, szerokie wsparcie płytek Adafruit i gotowych modułów. CircuitPython kładzie nacisk na spójne nazewnictwo pinów, ujednolicone API i rozbudowany pakiet bibliotek dla popularnych czujników i modułów.

W praktyce można przyjąć uproszczony obraz:

  • MicroPython – bardziej „systemowy”, bliżej sprzętu, większa elastyczność, ale czasem mniej przyjazny start.
  • CircuitPython – bardziej „edukacyjny” i „produktowy”, rewelacyjny dla pierwszych projektów, szczególnie na płytkach Adafruit i Raspberry Pi Pico.

Główne różnice: konfiguracja, obsługa sprzętu, ekosystem

Najmocniej odczuwalna różnica to model pracy z plikami i firmware. W MicroPythonie typowo:

  • wgrywasz firmware jednym narzędziem (np. esptool),
  • później łączysz się przez REPL lub używasz narzędzi do kopiowania plików (ampy, rshell, Thonny),
  • pliki takie jak boot.py i main.py uruchamiają się przy starcie.

W CircuitPythonie mikrokontroler po podłączeniu przez USB widoczny jest jak pendrive (MSC – Mass Storage Class). Wystarczy otworzyć ten „dysk” i edytować plik code.py (lub main.py). Po zapisaniu pliku kod jest automatycznie wykonywany. To radykalnie upraszcza pierwszy kontakt z płytką – szczególnie u osób, które nie chcą dotykać terminala.

Druga różnica leży w ekosystemie bibliotek. CircuitPython ma rozbudowany „Community Bundle” – zestaw oficjalnie utrzymywanych bibliotek dla konkretnych czujników i modułów. Dla popularnych urządzeń I2C/SPI (BME280, MPU6050, wyświetlacze OLED, ekspandery GPIO) istnieją gotowe pakiety. MicroPython również posiada liczne biblioteki, ale często są one rozproszone po GitHubie, forach i repozytoriach społeczności.

Który wybrać: MicroPython czy CircuitPython

Wybór między MicroPythonem a CircuitPythonem zwykle sprowadza się do odpowiedzi na kilka pytań:

  • Jaki to mikrokontroler? – nie wszystkie płytki są wspierane przez oba projekty. ESP32 ma mocne wsparcie w MicroPythonie, ale CircuitPython także posiada porty na wybrane wersje. RP2040 (Raspberry Pi Pico) dobrze radzi sobie z oboma.
  • Czy najważniejszy jest łatwy start? – jeśli tak, CircuitPython z edycją code.py na „pendrivie” jest zwykle najmniej bolesny.
  • Czy chcesz korzystać z gotowych bibliotek Adafruit? – wówczas CircuitPython ma przewagę.
  • Czy potrzebujesz maksymalnej kontroli i wydajności? – wtedy MicroPython bywa lepszy, szczególnie na ESP32.

W praktyce wiele osób zaczyna od CircuitPythona, żeby „poczuć” sprzęt i zdobyć pierwsze doświadczenia, a następnie, przy bardziej wymagających projektach Wi‑Fi lub własnych płytkach, przechodzi na MicroPythona.

Mikrokontroler na ciemnym tle obok precyzyjnego śrubokręta
Źródło: Pexels | Autor: Tanha Tamanna Syed

Jak działa Python na mikrokontrolerze – architektura i ograniczenia techniczne

Firmware: co naprawdę mieszka w pamięci Flash

Na mikrokontrolerze nie ma pełnego systemu operacyjnego znanego z komputerów. Firmware MicroPythona lub CircuitPythona zawiera w jednym obrazie:

  • minimalny kernel języka Python – stos, alokator pamięci, garbage collector, VM,
  • implementacje wbudowanych typów (int, float, list, dict, str, bytes itd.),
  • moduły sprzętowe dopasowane do konkretnego MCU (np. ESP32, RP2040, STM32),
  • interfejsy I/O – UART, I2C, SPI, USB, część peryferiów.

Firmware jest wgrywany do pamięci Flash, a po resecie mikrokontroler uruchamia interpreter, który startuje z plikiem inicjalizującym (boot.py lub odpowiednikiem) i następnie kodem użytkownika. Nie ma tu standardowego procesu „kompilacja – linkowanie – flashowanie” dla każdej zmiany logiki; działasz na poziomie skryptów.

RAM i Flash: konsekwencje dla kodu i danych

RAM w typowym mikrokontrolerze to często 128 KB, 256 KB lub 512 KB, czasem więcej. Interpreter, stos i dane dynamiczne (twoje obiekty Pythona, wczytane biblioteki, bufory) dzielą się tym ograniczonym zasobem. W rezultacie:

  • duże struktury danych (np. słowniki z tysiącami elementów) są zwykle nierealne,
  • trzeba świadomie zarządzać buforami (np. przy odczycie z sieci lub z pliku),
  • nadmierne logowanie tekstów bywa odczuwalne pamięciowo.

Flash przechowuje firmware oraz system plików użytkownika. MicroPython i CircuitPython zwykle dzielą przestrzeń Flash tak, by część przeznaczyć na firmware, część na mały filesystem (mały w skali desktopu, ale wystarczający na kilkadziesiąt – kilkaset kilobajtów kodu i konfiguracji). Zwykle wystarcza to na kilkanaście skryptów Pythona, kilka bibliotek i podstawową konfigurację.

Interpreter, bajtkod i wpływ na wydajność

MicroPython i CircuitPython kompilują skrypty do bajtkodu, który następnie wykonuje VM. To podobny model jak w CPythonie na komputerze, ale znacznie uproszczony. W niektórych portach MicroPythona istnieje możliwość użycia tzw. native code emitter, który generuje kod maszynowy dla wybranych funkcji, zwiększając wydajność, ale w zamian ograniczając część dynamicznych cech języka.

Przy typowych zadaniach IoT (pomiar raz na sekundę, żądanie HTTP, publikacja MQTT) narzut interpretera ma marginalne znaczenie. Zdecydowanie większe opóźnienia wnosi sieć i peryferia. Wąskie gardła pojawiają się dopiero przy bardzo intensywnych obliczeniach, szybkich pętlach sterujących lub przetwarzaniu dużych buforów danych (np. audio). W takich przypadkach warto:

  • wydzielić fragment do C (moduł natywny),
  • zredukować częstotliwość wykonywania zadania,
  • rozważyć mocniejszy MCU lub inne podejście architektoniczne.

Różnice między popularnymi płytkami z punktu widzenia Pythona

Najpopularniejsze układy i płytki dla MicroPython/CircuitPython to:

ESP32, RP2040, STM32 i inne – co daje konkretny układ

Wybór mikrokontrolera przekłada się bezpośrednio na dostępne moduły Pythonowe, wydajność i możliwości komunikacyjne. Najczęściej spotykane platformy to:

  • ESP32 / ESP8266 – układy z wbudowanym Wi‑Fi (ESP32 dodatkowo z Bluetooth). Bardzo popularne w MicroPythonie ze względu na stosunkowo dużą pamięć i sporą moc obliczeniową. Dobry wybór dla typowych projektów IoT z dostępem do sieci, brokerów MQTT czy API HTTP.
  • RP2040 (Raspberry Pi Pico i klony) – dwurdzeniowy Cortex‑M0+ bez wbudowanego Wi‑Fi, ale z dużą ilością RAM (264 KB) i ciekawym blokiem PIO do szybkich interfejsów. Świetny do projektów „przy biurku”, sterowania, akwizycji danych, a w połączeniu z zewnętrznym modułem Wi‑Fi – także do IoT.
  • STM32 – szeroka rodzina mikrokontrolerów; w MicroPythonie szczególnie znane są płytki Pyboard (oficjalne referencyjne platformy) oraz wybrane płytki Nucleo. Zwykle bardzo stabilne, dobre do bardziej „przemysłowych” zastosowań.
  • nRF52 – układy Nordic z BLE, często wykorzystywane w CircuitPythonie (np. płytki Adafruit z Bluetooth). Nadają się do niskoenergetycznych czujników, beaconów i urządzeń noszonych.

Architektura rdzenia (M0/M4, liczba rdzeni), ilość RAM i Flash, sprzętowe peryferia (np. liczba UART, wsparcie dla HS USB) wpływają na to, jak „komfortowo” będzie się pisało kod w Pythonie. Na tylnym planie pozostają te same ograniczenia – brak pełnego systemu operacyjnego, ograniczone zasoby – ale margines błędu jest inny na ESP32 z 520 KB SRAM, a inny na małym Cortex‑M0 z 32 KB.

Zarządzanie pamięcią i garbage collector w praktyce

Interpreter używa garbage collectora (GC), który co jakiś czas przeskanuje stertę i zwolni nieużywane obiekty. Mechanizm ten potrafi wywołać krótkie pauzy, które przy bardzo czułych pętlach sterujących (np. sterowanie silnikiem w czasie rzeczywistym) mogą dać się odczuć.

Najprostsze techniki łagodzenia skutków GC to:

  • ograniczenie liczby alokacji w krytycznych pętlach (np. nie tworzyć za każdym razem nowych list czy obiektów, tylko używać buforów współdzielonych),
  • ręczne wywołanie gc.collect() w dogodnym momencie, np. po wysłaniu partii danych do chmury, gdy drobne opóźnienie jest akceptowalne,
  • unikanie nadmiernej fragmentacji sterty – np. nie trzymać w pamięci wielu dużych, krótkotrwałych obiektów.

W typowych projektach IoT (pomiar co kilka sekund, komunikacja po Wi‑Fi) GC nie jest dużym problemem. Schody zaczynają się przy aplikacjach „twardo czasowych” – tam zwykle potrzebny bywa kod C, DMA, precyzyjne timery, a Python pełni raczej funkcję warstwy koordynującej niż wykonawczej.

Wybór sprzętu: jakie płytki i moduły nadają się do MicroPython i CircuitPython

Kryteria wyboru płytki pod projekt IoT

Zestaw płytek kompatybilnych z MicroPythonem i CircuitPythonem jest szeroki. Zanim pojawi się decyzja „ESP32 czy RP2040”, dobrze ustalić kilka parametrów projektu:

  • Łączność – czy potrzebne jest Wi‑Fi, Bluetooth, Ethernet, a może tylko komunikacja przewodowa (RS‑485, CAN)?
  • Zasilanie – czy urządzenie będzie zasilane z USB, zasilacza, czy z baterii i musi pracować w trybie niskiej mocy?
  • Interfejsy do czujników – ile linii GPIO, ile ADC, czy wymagane jest I2S, CAN, więcej niż jeden UART?
  • Obciążenie obliczeniowe – proste pomiary i wysyłka do chmury, czy np. lokalna agregacja danych, filtracja, prosta analiza sygnałów?

Zestaw tych odpowiedzi zwykle kieruje w stronę jednej z „rodzin” płytek i zawęża wybór do kilku pozycji wspieranych przez wybrane firmware.

Typowe płytki pod MicroPython

W środowisku MicroPythona ukształtowała się grupa „klasyków”, od których wiele osób rozpoczyna pracę:

  • Pyboard – referencyjna płytka twórców MicroPythona z układem STM32; stabilny, dobrze udokumentowany wybór, zwłaszcza jeśli ważna jest powtarzalność i „konserwatywne” podejście do aktualizacji.
  • ESP32 DevKit / WROOM / WROVER – liczne wersje płytek ESP32, często z wbudowanym USB‑UART. Idealne do projektów sieciowych, prostych bramek IoT, lokalnych serwerów HTTP.
  • ESP8266 NodeMCU / Wemos D1 mini – nieco starsza, słabsza platforma; nadal użyteczna w prostych projektach (np. czujniki temperatury z Wi‑Fi), ale z mniejszym zapasem pamięci.
  • Raspberry Pi Pico (RP2040) – dobrze wspierany przez MicroPythona, tani i dostępny; do projektów IoT wymaga zwykle zewnętrznego modułu Wi‑Fi lub pełni rolę lokalnego sterownika z komunikacją np. po UART z bramką.

Wielu producentów oferuje własne warianty tych płytek. Kluczowe jest, aby firmware MicroPythona posiadał dedykowany port lub był przynajmniej dobrze przetestowany na danym układzie.

Płytki preferowane przez CircuitPython

CircuitPython jest bardzo mocno związany z ekosystemem Adafruit, który projektuje płytki z myślą o plug‑and‑play i jednorodnym nazewnictwie pinów. Charakterystyczne płytki to:

  • Adafruit Feather (różne warianty: RP2040, ESP32, nRF52, M4) – format z wbudowaną ładowarką LiPo, często z Wi‑Fi lub BLE; wygodne do prototypów zasilanych z baterii.
  • Adafruit ItsyBitsy / QT Py – mniejsze form‑factories do bardziej kompaktowych projektów.
  • Płytki z sufiksem „Express” – mają wbudowaną pamięć Flash przeznaczoną na system plików CircuitPythona oraz układy ułatwiające obsługę przez USB (MSC + CDC + HID).
  • Raspberry Pi Pico / Pico W – oficjalnie wspierane w CircuitPythonie, przy czym Pico W zapewnia już natywne Wi‑Fi.

W praktyce wybór CircuitPythona jest szczególnie opłacalny, gdy stosuje się także czujniki i moduły Adafruit – ich biblioteki są zwykle dostępne w wersji gotowej do użycia.

Moduły czujników i „haty” dla IoT

W projektach IoT płytka główna to jedynie początek. Do niej dołącza się różne „nakładki” i moduły:

  • czujniki środowiskowe (temperatura, wilgotność, ciśnienie – np. BME280, SHT31),
  • moduły IMU (akcelerometr, żyroskop, magnetometr),
  • wyświetlacze OLED/TFT (I2C, SPI),
  • moduły komunikacyjne (Ethernet, LoRa, GSM/LTE).

Pod MicroPythonem biblioteki do tych urządzeń bywają porozrzucane po repozytoriach, lecz jednocześnie łatwo jest napisać cienką warstwę obsługi w oparciu o machine.I2C lub machine.SPI. W CircuitPythonie wiele z tych modułów ma gotowe biblioteki w oficjalnym pakiecie, co istotnie skraca czas od pierwszego podłączenia do działającego prototypu.

Przygotowanie środowiska pracy: instalacja, firmware, pierwsze połączenie z płytką

Wybór narzędzi na komputerze

Do pracy z MicroPythonem i CircuitPythonem wystarczy zwykle:

  • edytor tekstu lub IDE (np. VS Code, Thonny, Mu Editor),
  • sterowniki dla interfejsu USB‑UART (jeżeli płytka ich wymaga),
  • narzędzie do flashowania firmware (dla MicroPythona – np. esptool.py na ESP32/ESP8266).

Thonny i Mu są szczególnie wygodne dla osób rozpoczynających pracę, ponieważ integrują edytor, terminal REPL i funkcje wgrywania plików. VS Code wymaga nieco więcej konfiguracji, ale ułatwia pracę przy większych projektach dzięki rozszerzeniom i integracji z systemem kontroli wersji.

Wgrywanie firmware MicroPythona

Na przykładzie ESP32 procedura wygląda zazwyczaj następująco:

  1. Pobrać odpowiedni obraz firmware MicroPythona (.bin) z oficjalnej strony projektu, dopasowany do posiadanego układu (ESP32, ESP8266, warianty z PSRAM).
  2. Podłączyć płytkę przez USB, sprawdzić, na jakim porcie szeregowym jest widoczna (np. /dev/ttyUSB0, COM5).
  3. Użyć esptool.py, aby skasować zawartość Flash:
    esptool.py --port /dev/ttyUSB0 erase_flash
  4. Wgrać firmware:
    esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 esp32-idf4-xxxx.bin

Po restarcie płytka powinna zgłosić się przez port szeregowy jako urządzenie z REPL MicroPythona. Można połączyć się np. za pomocą screen, picocom czy terminala w Thonny i zobaczyć znak zachęty >>>.

Start z CircuitPythonem: pendrive i code.py

Uruchomienie CircuitPythona jest zwykle prostsze. Płytki „Express” i część innych modeli przy wejściu w tryb bootloadera (zwykle przez szybkie, podwójne naciśnięcie przycisku RESET) zgłaszają się jako dysk BOOT. Wystarczy skopiować na niego odpowiedni plik .uf2 z firmware CircuitPythona.

Po przeprogramowaniu urządzenie montuje nowy dysk (np. CIRCUITPY). Na tym dysku znajdują się między innymi:

  • code.py – główny plik wykonywany automatycznie po starcie,
  • ewentualne dodatkowe biblioteki w katalogu lib/,
  • pliki konfiguracyjne, dane, logi.

Edytor tekstu otwiera code.py bezpośrednio z tego „pendrive’a”; po zapisaniu zmian płytka z reguły automatycznie restartuje skrypt. Na początku daje to bardzo szybki cykl „edycja – test”, choć przy większej liczbie plików system plików na mikrokontrolerze nieco spowalnia operacje.

Pierwsze połączenie z REPL

Bez względu na wybór firmware, dostęp do REPL (Read‑Eval‑Print Loop) jest podstawowym narzędziem diagnostycznym. Umożliwia:

  • bezpośrednie wykonywanie komend Pythona,
  • sprawdzanie stanu pinów, odczyt z czujników, proste eksperymenty,
  • podgląd błędów i śladów stosu.

Po nawiązaniu połączenia szeregowym terminalem wystarczy wpisać:

>>> import sys
>>> sys.implementation

aby zobaczyć, czy pracuje MicroPython czy CircuitPython oraz w jakiej wersji. Na tym etapie można już sterować GPIO „z palca”, zanim powstanie pierwszy plik main.py czy code.py.

Płytka Arduino z podłączonymi przewodami na stole warsztatowym
Źródło: Pexels | Autor: Tanha Tamanna Syed

Podstawy programowania sprzętu w MicroPython i CircuitPython

GPIO: włączanie i wyłączanie diod, czytanie przycisków

Najbardziej podstawową operacją jest sterowanie liniami GPIO. W MicroPythonie na ESP32 może to wyglądać w ten sposób:

from machine import Pin
led = Pin(2, Pin.OUT)       # wbudowana dioda na wielu płytkach ESP32
led.value(1)                # włącz
led.value(0)                # wyłącz

W CircuitPython podobny kod używa modułu digitalio i obiektu płytki:

import board
import digitalio

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = True   # włącz
led.value = False  # wyłącz

Widać tu jedną z kluczowych filozoficznych różnic: MicroPython częściej operuje bezpośrednio na numerach pinów, CircuitPython preferuje symboliczne nazwy powiązane z konkretną płytką (board.LED, board.D5 itd.).

Interfejsy I2C, SPI, UART – komunikacja z czujnikami

W projektach IoT większość czujników komunikuje się za pomocą I2C lub SPI. Dla MicroPythona na ESP32 inicjalizacja magistrali I2C może wyglądać następująco:

from machine import Pin, I2C

i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
devices = i2c.scan()
print("Znalezione urządzenia:", devices)

Analogicznie w CircuitPythonie:

Inicjalizacja magistrali w CircuitPythonie

import board
import busio

i2c = busio.I2C(board.SCL, board.SDA)  # częste domyślne piny I2C
while not i2c.try_lock():
    pass

print("Adresy I2C:", [hex(x) for x in i2c.scan()])
i2c.unlock()

W CircuitPythonie dobór pinów jest zwykle z góry określony przez projekt płytki. Programista korzysta z aliasów board.SCL i board.SDA, co upraszcza przenoszenie przykładów między różnymi modelami.

Dla SPI i UART schemat jest podobny – w MicroPythonie powszechnie używa się numerów pinów i ID magistrali, a w CircuitPythonie obiektów z modułu board:

# MicroPython, UART na ESP32
from machine import UART, Pin

uart = UART(2, baudrate=9600, tx=Pin(17), rx=Pin(16))
uart.write(b"Hellorn")
print(uart.read(16))

# CircuitPython, UART
import board
import busio

uart = busio.UART(board.TX, board.RX, baudrate=9600)
uart.write(b"Hellorn")
data = uart.read(16)
print(data)

W praktyce konfiguracja interfejsów wymaga zwykle jednorazowego wysiłku na początku projektu. Potem cała logika dotyczy już protokołu urządzenia, a nie sposobu przesłania bajtów.

Timery, PWM i sterowanie mocą

Przy sterowaniu diodami LED, silnikami czy serwomechanizmami kluczowa jest obsługa PWM (Pulse Width Modulation). W MicroPythonie na ESP32 można stworzyć prosty efekt rozjaśniania diody:

from machine import Pin, PWM
import time

led = PWM(Pin(2), freq=1000)  # 1 kHz

for duty in range(0, 1024):
    led.duty(duty)
    time.sleep_ms(5)

led.deinit()

W CircuitPythonie operuje się na wartości od 0 do 65535:

import board
import pwmio
import time

led = pwmio.PWMOut(board.LED, frequency=1000)

for duty in range(0, 65535, 256):
    led.duty_cycle = duty
    time.sleep(0.01)

led.deinit()

Przy sterowaniu serwami w CircuitPythonie wygodny jest moduł simpleio lub dedykowane biblioteki, w MicroPythonie częściej buduje się funkcje samodzielnie, wykorzystując timery i odpowiednie wypełnienie impulsu.

Pliki startowe: main.py, boot.py, code.py

Po fazie eksperymentów w REPL przychodzi moment na zdefiniowanie, co mikrokontroler ma robić po każdym restarcie. W MicroPythonie typowy podział wygląda następująco:

  • boot.py – wykonywany bardzo wcześnie, umożliwia np. przełączenie trybu pracy USB, konfigurację logowania, montowanie systemu plików w trybie tylko do odczytu,
  • main.py – główny program użytkownika, uruchamiany po boot.py.
# przykładowy main.py dla MicroPythona
import time
from machine import Pin

led = Pin(2, Pin.OUT)

while True:
    led.value(not led.value())
    time.sleep(0.5)

W CircuitPythonie całość logiki umieszcza się zazwyczaj w code.py (lub alternatywnie main.py – wtedy code.py ma pierwszeństwo). Skrypt jest uruchamiany po każdym resecie:

# code.py dla CircuitPythona
import time
import board
import digitalio

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

while True:
    led.value = not led.value
    time.sleep(0.5)

W projektach IoT sensowne jest rozdzielenie plików na mniejsze moduły (np. wifi_config.py, mqtt_client.py), aby uprościć utrzymanie i późniejsze aktualizacje.

Obsługa wyjątków i minimalna diagnostyka

Na mikrokontrolerze nie ma luksusu pełnego stosu debuggera, dlatego obsługa błędów musi być nieco bardziej przemyślana. Prosty wzorzec to pętla główna opakowana w try/except z logowaniem błędów do portu szeregowego lub pliku:

# MicroPython
import sys
import uio
import machine
import time

def run():
    # główna logika aplikacji
    while True:
        # ...
        time.sleep(1)

def log_exception(e):
    buf = uio.StringIO()
    sys.print_exception(e, buf)
    s = buf.getvalue()
    print(s)  # do REPL
    try:
        with open("error.log", "a") as f:
            f.write(s + "n")
    except OSError:
        pass

while True:
    try:
        run()
    except Exception as e:
        log_exception(e)
        time.sleep(5)
        machine.reset()

Podobnie w CircuitPythonie można korzystać z traceback.print_exception i zapisu na CIRCUITPY. Taki mechanizm znacząco ułatwia diagnozowanie błędów występujących tylko w warunkach polowych.

Biblioteki, moduły i ekosystem: jak nie wymyślać koła na nowo

Oficjalne i „półoficjalne” moduły MicroPythona

MicroPython ma zestaw wbudowanych modułów, które są częścią firmware (np. machine, uos, utime, ujson). Dodatkowo istnieją moduły „zamrożone” w firmware przez producentów płytek oraz te, które wgrywa się jako zwykłe pliki .py do systemu plików.

Źródła typowych bibliotek:

  • repozytorium główne MicroPythona – katalog drivers/ oraz examples/,
  • projekty na GitHubie tworzone przez społeczność (np. sterowniki do BME280, SHT31, wyświetlaczy OLED),
  • firmware dostarczane przez producentów płytek – często zawiera ekstra moduły specyficzne dla danego hardware.

Rozprowadzanie bibliotek opiera się zazwyczaj na prostym kopiowaniu plików albo użyciu skryptów, które automatyzują zgrywanie ich na płytkę. W większych projektach stosuje się podkatalog lib/, w którym lądują wszystkie zewnętrzne moduły:

/
  boot.py
  main.py
  config.py
  lib/
      bme280.py
      ssd1306.py
      umqtt_simple.py

W MicroPythonie nie ma domyślnego odpowiednika pip. Istnieją narzędzia takie jak mip (nowsze wersje) czy dawne rozwiązania społeczności, ale ich użycie bywa różne w zależności od portu i firmware.

Ekosystem CircuitPythona: Bundle i biblioteki Adafruit

W przypadku CircuitPythona głównym źródłem bibliotek jest tzw. Adafruit CircuitPython Bundle. To archiwum ZIP zawierające dziesiątki, a nawet setki modułów do popularnych czujników, wyświetlaczy i usług sieciowych.

Procedura korzystania jest dość powtarzalna:

  1. Pobrać z serwisu Adafruit wersję Bundle pasującą do używanej wersji CircuitPythona.
  2. Rozpakować archiwum lokalnie na komputerze.
  3. Skopiować wybrane moduły lub katalogi z lib/ do katalogu lib na dysku CIRCUITPY.

Przykładowo, aby użyć czujnika BME280 po I2C:

import board
import busio
import adafruit_bme280.basic as adafruit_bme280

i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

print(bme280.temperature, bme280.humidity, bme280.pressure)

Wielką zaletą takiego podejścia jest spójność API – czujniki o podobnym przeznaczeniu (np. różne modele termometrów) mają zwykle zbliżony interfejs Pythona, co ułatwia zamiany sprzętu w trakcie projektu.

Różnice w nazewnictwie i modułach „u*”

MicroPython stosuje skrócone nazwy dla wielu modułów standardowych Pythona, aby zmniejszyć ślad w pamięci Flash. Przykłady:

  • ujson zamiast json,
  • uos zamiast os,
  • utime zamiast time,
  • ure zamiast re.

CircuitPython dąży do większej kompatybilności nazw z CPythonem i częściej używa pełnych nazw (np. json, os, time), choć zakres funkcji bywa ograniczony. Przy przenoszeniu kodu między MicroPythonem a CircuitPythonem, albo z „dużego” Pythona, trzeba uwzględnić te różnice. W większych projektach pomaga prosty moduł kompatybilnościowy, który mapuje nazwy:

# compat.py
try:
    import ujson as json
except ImportError:
    import json

Minimalizacja kodu: moduły lekkie i techniki oszczędzania pamięci

Na mikrokontrolerze każdy kilobajt RAM ma znaczenie. Kilka typowych zabiegów:

  • unikanie dużych bibliotek ogólnego przeznaczenia, jeśli wystarczy prosty, wyspecjalizowany kod,
  • dzielenie konfiguracji na mniejsze pliki i ładowanie ich tylko wtedy, gdy są faktycznie potrzebne,
  • rezygnacja z nadmiernego logowania w trybie produkcyjnym,
  • stosowanie struktur danych o stałej długości zamiast rozrastających się list.

MicroPython udostępnia także moduł gc do ręcznego wywoływania odśmiecania pamięci:

import gc

gc.collect()
print("Wolne pamięci:", gc.mem_free())

CircuitPython wykonuje odśmiecanie bardziej automatycznie, ale także pozwala na ręczne wywołanie gc.collect(). W projektach działających miesiącami bez restartu jest to istotne, aby uniknąć powolnego „puchnięcia” zużycia pamięci przez obiekty, które nie zostały uwolnione.

Portowanie bibliotek między MicroPythonem a CircuitPythonem

Jeżeli jakaś biblioteka istnieje tylko dla jednego z ekosystemów, zwykle można ją przenieść z umiarkowanym nakładem pracy. Typowe różnice wymagające uwagi:

  • API portów wejścia/wyjścia (np. machine.I2C vs busio.I2C),
  • różnice w obsłudze czasu (utime vs time, obecność monotonic() w CircuitPythonie),
  • brak niektórych wyjątków lub funkcji w danym środowisku,
  • inna organizacja systemu plików i ścieżek.

W praktyce portowanie polega na wydzieleniu warstwy „hardware abstraction” w osobnym pliku i napisaniu dwóch wariantów – jednego dla MicroPythona, drugiego dla CircuitPythona. Reszta logiki pozostaje wspólna.

IoT w praktyce: Wi‑Fi, MQTT, HTTP i integracja z chmurą

Podłączanie do Wi‑Fi w MicroPythonie

Na układach z modułem Wi‑Fi (ESP32, ESP8266, Pico W) pierwszym krokiem jest skonfigurowanie połączenia z siecią. Dla ESP32 typowy kod wygląda tak:

import network
import time

SSID = "TwojaSiec"
PASSWORD = "TwojeHaslo"

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)

for _ in range(20):
    if wlan.isconnected():
        break
    time.sleep(0.5)

if not wlan.isconnected():
    raise RuntimeError("Brak połączenia z Wi-Fi")

print("Połączono, adres IP:", wlan.ifconfig()[0])

Warto rozdzielić dane dostępu do sieci do osobnego pliku wifi_config.py, który nie trafia do repozytorium (np. jest dodany do .gitignore), aby nie publikować haseł.

Wi‑Fi w CircuitPythonie: ESP32‑S2, ESP32‑S3, Pico W

W CircuitPythonie obsługa Wi‑Fi jest silniej ustrukturyzowana. Przykład dla płytki z ESP32‑S2:

import wifi
import socketpool
import time

SSID = "TwojaSiec"
PASSWORD = "TwojeHaslo"

print("Łączenie z Wi-Fi...")
wifi.radio.connect(SSID, PASSWORD)

print("Połączono, IP:", wifi.radio.ipv4_address)

pool = socketpool.SocketPool(wifi.radio)

Jeżeli urządzenie nie ma wbudowanego Wi‑Fi, ale korzysta z zewnętrznego modułu ESP32 jako współprocesora sieciowego, Adafruit dostarcza bibliotekę adafruit_esp32spi, która ukrywa szczegóły protokołu SPI.

Prosty klient HTTP – pobieranie danych z API

Gdy sieć działa, naturalnym krokiem jest wymiana danych z usługą HTTP. W MicroPythonie często używa się minimalistycznej biblioteki urequests (dostępnej w wielu firmware’ach lub jako dodatkowy plik):

import urequests
import ujson

url = "https://api.coindesk.com/v1/bpi/currentprice.json"
r = urequests.get(url)
data = r.json()
r.close()

print(data["bpi"]["USD"]["rate"])

W CircuitPythonie rolę klienta HTTP pełni zazwyczaj adafruit_requests współpracujący z socketpool: