Programowanie w środowisku MicroPython (2). Timery, przerwania i dioda WS2812

Programowanie w środowisku MicroPython (2). Timery, przerwania i dioda WS2812

W tym odcinku kursu stworzymy prostą bibliotekę z własnymi funkcjami, a także zapoznamy się z obsługą przerwań i timerów w ESP32-S3 oraz diod ze sterownikiem WS2812. Napiszemy prosty program, który będzie zmieniał kolor świecenia diody, pracując przy tym całkowicie w tle.

Ile mamy pamięci?

Jak już wiemy z poprzedniego odcinka tego kursu, MicroPython korzysta z pamięci Flash mikrokontrolera do przechowywania plików. Możemy zapisywać pliki wykonywalne *.py, pliki uprzednio skompilowane *.mpy, a także pliki wszystkich innych typów. Nic nie stoi na przeszkodzie, by zainstalować dodatkowy dysk w postaci karty MicroSD lub pamięci EEPROM i na nim przechowywać dodatkowe pliki, jeżeli wbudowana pamięć nam nie wystarcza.

Przed wykonaniem skryptu *.py najpierw jest on kompilowany przez mikrokontroler. Tak powstaje bytecode, który zapisywany jest w pamięci RAM, a następnie może być on wykonany przez wirtualną maszynę MicroPythona. Ów bytecode może oczywiście tworzyć różne zmienne, tablice i inne obiekty, które również są przechowywane w pamięci RAM. Proces kompilacji i wykonywania programu jest szczegółowo opisany w dokumentacji MicroPythona, dostępnej pod adresem [2].

W pierwszej części tego odcinka napiszemy prosty moduł, który poda nam informacje o tym, ile wykorzystujemy pamięci Flash i RAM, a także ile zostało nam jeszcze wolnej pamięci. Przeanalizujmy kod zaprezentowany na listingu 1. Zawiera on tylko dwie funkcje.

# Plik mem_used.py
import os # 1
import gc # 2

def print_rom_used(path = „”): # 3
stats = os.statvfs(path) # 4
block_size = stats[0] # 5
total_blocks = stats[2] # 6
free_blocks = stats[3] # 7
total_rom = total_blocks * block_size
used_rom = (total_blocks – free_blocks) * block_size
print(f”ROM: {used_rom} / {total_rom}”) # 8

def print_ram_used(): # 9
gc.collect() # 10
total_ram = gc.mem_alloc() + gc.mem_free() # 11
used_ram = gc.mem_alloc()
print(f”RAM: {used_ram} / {total_ram}”)

if __name__ == „__main__”: # 12
print_rom_used()
print_ram_used()

Listing 1. Kod pliku mem_used.py

W pierwszych dwóch liniach importujemy moduły, które będą nam potrzebne. Moduł os zawiera różne funkcje związane z systemem plików, zaś gc to garbage collector, czyli „odśmiecacz” pamięci RAM. Ten drugi jest domyślnie aktywny, a jego zadaniem jest wyszukiwanie oraz usuwanie nieużywanych zmiennych – po to, by zwolnić zajmowaną przez nie pamięć. Moduł gc zawiera różne funkcje do konfiguracji odśmiecania oraz informujące o zajętej i dostępnej pamięci RAM.

W linii 3 rozpoczynamy funkcję, której zadaniem jest wyświetlenie ilości miejsca w pamięci Flash zajmowanego przez wszystkie pliki, znajdujące się pod ścieżką określoną argumentem path. Domyślną wartością tego argumentu jest "", czyli katalog główny w systemie plików ESP32. Możliwość wskazania ścieżki przyda nam się w kolejnych odcinkach, kiedy będziemy tworzyć dyski na karcie MicroSD lub w pamięci EEPROM.

Funkcja statvfs() z modułu os zwraca krotkę, którą zapisujemy do zmiennej stats (linia 4). Zawiera ona różne informacje na temat systemu plików. Następnie, aby ułatwić sobie analizę tej krotki, przepiszemy kilka jej części składowych do zmiennych o nieco bardziej zrozumiałych nazwach. W linii 5 odczytujemy, jaki jest rozmiar bloku pamięci w bajtach, ile jest wszystkich dostępnych bloków na dysku (linia 6) i ile jest wolnych bloków (linia 7). Potem przeprowadzamy kilka prostych obliczeń, aby uzyskać ilość dostępnej i zajętej pamięci w bajtach. W linii 8 wyświetlamy wynik na konsoli, stosując f-string.

Kolejna funkcja wyświetli nam informację nt. dostępnej i zajętej pamięci RAM (linia 9). W linii 10 wywołujemy funkcję collect() z modułu gc. Zadaniem tej funkcji jest uruchomienie odśmiecacza, aby przeskanował pamięć i usunął wszystkie niepotrzebne obiekty. W dwóch kolejnych liniach funkcja mem_alloc() zwraca ilość zajętej pamięci, a mam_free() informuje nas o tym, ile pozostało wolnej pamięci.

Przechodzimy teraz do linii 12 zawierającej instrukcję if, która może nieco zaskoczyć osoby dopiero uczące się Pythona. Plik *.py da się bowiem załadować na dwa sposoby. Może on zostać zaimportowany przez inny moduł (w takiej sytuacji zmienna __name__ przechowuje nazwę modułu zaimportowanego) lub uruchomiony bezpośrednio, np. klawiszem F5 w Thonny. W takiej sytuacji zmienna __name__ ma wartość „__main__”. Identycznie jest w przypadku plików boot.py oraz main.py, które są uruchamiane automatycznie po starcie systemu. Celem linii 12 jest sprawdzenie, czy plik został zaimportowany, czy też uruchomiony. W tej drugiej sytuacji zostanie wykonany kod pod instrukcją if, odpowiedzialny za wywołanie dwóch omówionych wcześniej funkcji.

Zapisz plik z listingu 1 w ESP32 pod nazwą mem_used.py, a następnie wciśnij F5. Powinieneś zobaczyć na konsoli komunikaty podobne do poniższych:

>>> %Run -c $EDITOR_CONTENT

MPY: soft reboot
ROM: 315392 / 6291456
RAM: 1648 / 8190464

Piny GPIO

Z obsługą pinów pierwszy raz zetknęliśmy się w poprzednim odcinku, lecz teraz przestudiujemy ten temat dokładniej. MicroPython pozwala zarządzać pinami GPIO na kilka różnych sposobów.

Aby uzyskać dostęp do pinu, musimy najpierw stworzyć egzemplarz klasy Pin z modułu machine. W tym celu musimy najpierw tę klasę zaimportować poleceniem:

from machine import Pin

Następnie trzeba utworzyć instancje klasy Pin – po jednej na każdy pin, którego chcemy użyć. Możemy je przechowywać w zmiennych globalnych, a także przekazywać przez argumenty. Dostęp do pinu będziemy mieć tak długo, jak długo będzie istniała zmienna przechowująca instancję pinu.

Każdy pin (przynajmniej w MicroPythonie na ESP32) może pracować jako wejście, wyjście push-pull lub wyjście z otwartym kolektorem. Tworzymy te piny poniższymi instrukcjami:

pin0 = Pin(0, Pin.IN)
pin1 = Pin(1, Pin.OUT)
pin2 = Pin(2, Pin.OPEN_DRAIN)

Nic nie stoi na przeszkodzie, aby utworzyć kilka instancji, sterujących tym samym pinem GPIO. Pierwszy argument konstruktora to fizyczny numer pinu, zgodnie z dokumentacją używanego przez nas mikrokontrolera. Drugi to kierunek przepływu informacji. Istnieje możliwość, aby dodać trzeci argument, jeżeli chcemy uaktywnić wbudowany rezystor pull-up albo pull-down.

pin3 = Pin(3, Pin.IN, Pin.PULL_UP)
pin4 = Pin(4, Pin.IN, Pin.PULL_DOWN)

Jeżeli pin ma być wyjściem, to możemy ustalić jego stan od razu podczas tworzenia obiektu, korzystając z argumentu value.

pin5 = Pin(5, Pin.OUT, value=0)
pin6 = Pin(6, Pin.OUT, value=1)

Wszystkie układy z rodziny ESP32 pozwalają ustawić także ogranicznik prądowy dla pinu pracującego jako wyjście. Mamy do dyspozycji cztery ustawienia, które należy przekazać za pomocą argumentu drive. DRIVE_0 oznacza 5 mA, DRIVE_1 to 10 mA, DRIVE_2 daje 20 mA i jest to wartość domyślna, a DRIVE_3 umożliwia pobranie z pinu nawet 40 mA.

pin7 = Pin(7, Pin.OUT, drive=Pin.DRIVE_0)
pin8 = Pin(8, Pin.OUT, drive=Pin.DRIVE_1)
pin9 = Pin(9, Pin.OUT, drive=Pin.DRIVE_2)
pin10 = Pin(10, Pin.OUT, drive=Pin.DRIVE_3)

Jeżeli zajdzie potrzeba, aby zmienić konfigurację istniejącego pinu, wówczas musimy posłużyć się metodą init.

pin10.init(Pin.IN, Pin.PULL_DOWN)

Istnieją dwa sposoby odczytywania stanu danej linii GPIO. Oba zwracają wartość 1 lub 0, którą można zapisać w jakiejś zmiennej lub przekazać do funkcji jako argument. Drugi sposób jest lepszy, ponieważ wykonuje się odrobinę szybciej.

pin0.value()
pin0()

Aby zmienić stan pinu pracującego jako wyjście, należy skorzystać z metody value, on, off lub podać żądany stan w nawiasie, od razu za nazwą zmiennej. Ostatni wymieniony sposób jest najszybszy.

pin1.value(1)
pin1.value(0)
pin1.on()
pin1.off()
pin1(1)
pin1(0)

Aby pin wywoływał przerwanie, musimy skorzystać z metody irq i przekazać do niej dwa argumenty. Pierwszym jest nazwa funkcji (koniecznie bez nawiasów!), która zostanie wywołana w momencie zgłoszenia przerwania. Drugim jest wybór rodzaju zbocza wywołującego przerwanie (rosnące, opadające lub oba).

pin0.irq(gpio_int, Pin.IRQ_RISING)
pin0.irq(gpio_int, Pin.IRQ_FALLING)
pin0.irq(gpio_int, Pin.IRQ_RISING | IRQ_FALLING)

To już wszystkie najważniejsze informacje na temat klasy Pin. Zawiera ona jeszcze kilka szczegółów, jakich nie omówiłem, ale nie są one szczególnie istotne. Pełną dokumentację klasy znajdziesz pod adresem [3].

Timery

Układy ESP32 mają cztery sprzętowe timery, ponumerowane od 0 do 3. Aby z nich skorzystać, musimy najpierw zaimportować klasę Timer z modułu machine.

from machine import Timer

Kolejnym krokiem, podobnie jak w przypadku pinów GPIO, jest utworzenie instancji tej klasy. W konstruktorze podajemy numer timera, którego będziemy używać.

tim = Timer(0)

Tak utworzoną instancję możemy skonfigurować i uruchomić za pomocą metody init. Zwróć uwagę, że wszystkie argumenty muszą być nazwane. Argument mode decyduje o trybie pracy timera. Możliwe opcje to: Timer.ONE_SHOT, czyli praca jednorazowa (po zgłoszeniu przerwania timer sam się wyłączy) oraz Timer.PERIODIC, w którym timer zgłasza przerwania cyklicznie tak długo, aż zostanie wyłączony ręcznie. Argument period to czas odliczania w milisekundach. Ostatni argument callback to nazwa funkcji, która ma zostać wywołana w momencie zgłoszenia przerwania.

tim.init(mode=Timer.ONE_SHOT, period=1000, callback=timer_int)
tim.init(mode=Timer.PERIODIC, period=1000, callback=timer_int)

Można także utworzyć instancję timera i od razu ją skonfigurować:

tim0 = tim.Timer(0, mode=Timer.ONE_SHOT, period=1000, callback=timer_int)
tim1 = tim.Timer(1, mode=Timer.PERIODIC, period=1000, callback=timer_int)

Aby zatrzymać timer w dowolnym momencie, należy wywołać metodę deinit.

tim.deinit()

Pełna dokumentacja klasy Timer dostępna jest pod adresem [4].

Dioda WS2812 (NeoPixel)

Omówimy teraz działanie diody WS2812. Jest to bardzo popularny element i zapewne większość Czytelników już o nim słyszała. W środku diody znajdują się struktury świecące na czerwono, zielono i niebiesko oraz wbudowany, cyfrowy sterownik, do którego wystarczy wysłać informację o żądanym kolorze świecenia. Możemy łączyć ze sobą bardzo dużo takich diod w łańcuch (zobacz przykładowy schemat na rysunku 1), a każda z nich może świecić innym kolorem. Co bardzo ważne z praktycznego punktu widzenia, do sterowania diodami potrzebny jest tylko jeden pin procesora, niezależnie od liczby diod połączonych w łańcuchu.

Rysunek 1. Przykład połączenia kilku diod WS2812

Na początku musimy zaimportować moduł neopixel. Nazwa ta została wdrożona przez firmę Adafruit dla produkowanych przez nią gadżetów bazujących na diodach WS2812.

import neopixel

Następnie trzeba utworzyć instancję pinu, który steruje łańcuchem diod. Kolejnym krokiem jest utworzenie obiektu NeoPixel, do którego przekazujemy utworzony wcześniej pin oraz liczbę diod w łańcuchu. W poniższym przykładzie pin GPIO 0 steruje dziesięcioma diodami WS2812.

wspin = Pin(0, Pin.OUT)
ws2812 = neopixel.NeoPixel(wspin, 10)

Można to zrobić także w jednej linijce kodu.

ws2812 = neopixel.NeoPixel(Pin(0, Pin.OUT), 10)

Tak powstaje obiekt, wewnątrz którego znajduje się lista zawierająca informacje o składowych RGB wszystkich diod (dokładniej rzecz ujmując, jest to obiekt typu bytearray, ale nie wchodźmy w takie szczegóły). Aby ustawić kolor diody, musimy składowe RGB objąć nawiasami okrągłymi, tworząc krotkę, a następnie zapisać ją na liście, podając numer wybranej diody w nawiasach kwadratowych. Każda ze składowych może świecić w jednym z 256 stopni jasności, gdzie 0 oznacza całkowite wyłączenie, a 255 świecenie z pełną jasnością.

ws2812[0] = ( 0, 0, 0) # dioda nie świeci
ws2812[1] = (255, 0, 0) # kolor czerwony
ws2812[2] = ( 0, 255, 0) # kolor zielony
ws2812[3] = ( 0, 0, 255) # kolor niebieski
ws2812[4] = (255, 255, 255) # kolor biały

Na razie żadna dioda jeszcze nie świeci. Trzeba bowiem przesłać dane do sterowników WS2812, wywołując metodę write.

ws2812.write()

Gdybyśmy potrzebowali odczytać liczbę diod w łańcuchu, możemy to zrobić na dwa sposoby:

ws2812.n
len(ws2812)

A jeżeli z jakiegoś powodu potrzebujemy dostać się do „surowych” danych bufora pamięci, wówczas musimy odczytać lub zapisać zmienną buf. Więcej informacji na temat klasy NeoPixel znajdziesz pod adresem [5].

Ćwiczenia praktyczne

Napiszemy teraz testowy program, którego celem jest zademonstrowanie procesu inicjalizacji i ustawiania koloru diody WS2812. Wykorzystamy diodę znajdującą się na płytce ESP32-S3-DevKit-C (w rzeczywistości jest tam zamontowana dioda typu SK68XXMINI-HS, ale steruje się nią identycznie jak WS2812). Ponadto skonfigurujemy przycisk zainstalowany fabrycznie na płytce w taki sposób, aby jego naciśnięcie wywoływało przerwania. W procedurze przerwania od przycisku będzie losowany kolor diody, po czym zostanie ustawiony timer, który wygeneruje kolejne przerwanie po dwóch sekundach. Dioda zostanie następnie wyłączona w procedurze przerwania od timera.

Przeanalizujmy kod z listingu 2, który prezentuje zawartość pliku interrupt_demo.py. Plik rozpoczynamy od zaimportowania potrzebnych modułów: mem_used (w linii 1), który stworzyliśmy wcześniej oraz neopixel, pozwalający sterować diodą WS2812. W kolejnej linii importujemy moduł random, który służy do generowania liczb losowych. Finalnie z modułu machine importujemy klasy Pin oraz Timer.

# Plik interrupt_demo.py

import mem_used # 1
import neopixel
import random
from machine import Pin, Timer

def timer_int(source): # 2
print(f”Przerwanie od {source}”)

led[0] = (0, 0, 0)
led.write()

def button_int(source): # 3
print(f”Przerwanie od {source}”) # 4

r = random.randint(0, 255) # 5
g = random.randint(0, 255)
b = random.randint(0, 255)

led[0] = (r, g, b) # 6
led.write() # 7

print(f”Wylosowany kolor to: {led[0]}”) # 8

Timer(0, mode=Timer.ONE_SHOT, period=2000, callback=timer_int) # 9

button = Pin(0, Pin.IN, Pin.PULL_UP) # 10
button.irq(button_int, Pin.IRQ_FALLING) # 11
led = neopixel.NeoPixel(Pin(38, Pin.OUT), 1) # 12
mem_used.print_ram_used() # 13

Listing 2. Kod pliku interrupt_demo.py

Plik interrupt_demo.py zawiera tylko dwie funkcje oraz cztery linijki kodu, które nie są ani funkcjami, ani klasami. Wykonują się one przy każdym uruchomieniu tego pliku. Przejdźmy do linii 10. W tym miejscu tworzymy obiekt klasy Pin, zaimportowany z modułu machine i zapisujemy do zmiennej button. Jak nietrudno się domyślić, będzie on służył do obsługi przycisku, znajdującego się na płytce ESP32-S3-DevKit-C. W nawiasach przekazujemy trzy argumenty do konstruktora klasy. Są to:

  1. fizyczny numer pinu w układzie ESP32-S3, do którego podłączony jest przycisk (czyli pin GPIO 0),
  2. kierunek przepływu informacji (pin ma być wejściem),
  3. zezwolenie na podłączenie rezystora pull-up, wbudowanego w strukturę ESP32-S3. W tym przypadku moglibyśmy pominąć ten argument, ponieważ na płytce znajduje się fizyczny rezystor pull-up, zawsze podciągający pin 0 niezależnie od konfiguracji programowej.

W linii 11 wywołujemy metodę irq obiektu button. W momencie wystąpienia przerwania ma zostać wywołana funkcja button_int. Przycisk ma rezystor pull-up, zapewniający stan wysoki w momencie, gdy klawisz nie jest wciśnięty. Zatem przerwanie musi być wywoływane zboczem opadającym i dlatego w drugim argumencie podajemy wartość Pin.IRQ_FALLING.

Przejdźmy do linii 12, w której tworzymy egzemplarz klasy NeoPixel z modułu neopixel, odpowiedzialnego za sterowanie diodami WS2812. Do konstruktora tej klasy przekazujemy dwa argumenty. Pierwszym jest egzemplarz klasy Pin, sterujący pinem GPIO 38 (ponieważ do niego podłączona jest dioda WS2812). Drugi argument określa, ile znajduje się połączonych diod w łańcuchu – na płytce mamy tylko jedną taką diodę.

Tak utworzony obiekt zapisujemy do zmiennej led.

Na koniec, w linii 13, wywołujemy funkcję wyświetlającą ilość pozostałej pamięci RAM. Funkcja ta pochodzi z modułu mem_used, którą znamy już z listingu 1.

Omówmy teraz procedurę obsługi przerwania od przycisku, która zaczyna się z linii 3. Wygląda jak zupełnie normalne funkcja, jednak ma dwa ograniczenia. Pierwsze z nich polega na tym, że funkcja musi przyjmować jeden argument będący obiektem zgłaszającym przerwanie (dzięki temu można zrobić jedną funkcję obsługującą przerwania od wielu źródeł, która sama może rozpoznać, co ją wywołało). W naszym przykładzie jest to argument source, choć jego nazwa może być dowolna. Drugim ograniczeniem jest to, że funkcja nie powinna zwracać żadnej wartości. Umieszczenie instrukcji return co prawda nie spowoduje błędu, ale nie da też żadnego efektu. Poza tym, w przerwaniach nie zaleca się korzystania z operacji tworzących nowe zmienne lub zmieniających rozmiar, np. przez dodanie elementu do listy. Lepiej korzystać ze zmiennych, które już istnieją i tylko zmienić ich wartość.

Linia 4 ma za zadanie wyświetlić komunikat informujący użytkownika o tym, że zostało zgłoszone przerwanie (oraz o tym, skąd ono pochodzi). W tej linii wyświetlimy funkcją print zawartość zmiennej source. Znajduje się tam obiekt button klasy Pin, który utworzyliśmy w linii 10. Uzyskamy taki sam efekt, jak przy użyciu zapisu print(button).

Następnie, w linii 5 i kolejnych losujemy jasność koloru czerwonego, zielonego i niebieskiego. Korzystamy w tym celu z funkcji randint z modułu random. W argumentach funkcji podajemy zakres, wewnątrz którego ma znajdować się wylosowana liczba, tzn. od 0 do 255.

Linia 6 pokazuje, w jaki sposób możemy sterować kolorem poszczególnych diod w łańcuchu. Diody numerowane są od 0. Indeks diody, której kolor chcemy ustawić, podajemy w nawiasach kwadratowych w taki sam sposób, jakby obiekt led był listą. Składowe RGB musimy scalić w krotkę, obejmując je nawiasami okrągłymi.

Kolejnym krokiem w linii 7 jest odświeżenie diod WS2812. W tym celu wywołujemy metodę write() z obiektu led. Dioda zacznie świecić natychmiast po przetransmitowaniu danych, co trwa ułamek sekundy. Następnie wyświetlamy na konsoli informację o tym, jaki kolor został wylosowany (linia 8).

Dioda WS2812 ma świecić się przez dwie sekundy. Aby odmierzyć czas, skorzystamy z timera. W linii 9 tworzymy instancję obiektu Timer z modułu machine. Konstruktor tego obiektu przyjmuje cztery argumenty. Są to:

  1. numer timera – w przykładowym kodzie jest timer 0, ale można użyć dowolnego,
  2. argument mode – tryb pracy. Chcemy aby timer wyłączył się po zgłoszeniu przerwania, zatem podajemy Timer.ONE_SHOT,
  3. argument period – okres timera w milisekundach,
  4. argument callback – nazwa funkcji, która ma zostać wywołana w momencie zgłoszenia przerwania od timera.

Zwróć uwagę, że numer timera to argument pozycyjny i zawsze musi być podany jako pierwszy, a argumenty mode, period i callback przekazywane są z wykorzystaniem słów kluczowych, których nie możemy pominąć (ale za to możemy je wpisać w dowolnej kolejności).

Konstruktor klasy Timer zwraca egzemplarz konstruowanej klasy, którą moglibyśmy zapisać w jakiejś zmiennej, jednak tego nie robimy, ponieważ w naszym przykładowym kodzie nie potrzebujemy mieć dostępu do metod tejże instancji.

Pozostaje już tylko omówić przerwanie od timera, które zaczyna się od linii 2. Jest ono bardzo podobne do przerwania od przycisku. Różni się tylko tym, że – zamiast losować kolory – wszystkie składowe zerujemy, co w rezultacie powoduje, że dioda WS2812 przestaje świecić.

Gotowe! W programie Thonny naciśnij przycisk F5. Program zostanie przesłany i uruchomiony. Na konsoli zobaczysz tylko informację o pamięci RAM, a zaraz pod nią pojawi się wiersz poleceń interpretera, w którym możemy wpisywać dowolne komendy. Naciśnij przycisk BOOT na płytce ESP32-S3-DevKit-C. Dioda zaświeci się na losowo wybrany kolor i po dwóch sekundach zgaśnie. Na konsoli wyświetlą się komunikaty takie, jak pokazane na rysunku 2.

Rysunek 2. Efekt działania programu z niniejszego odcinka kursu

Kody z dzisiejszego odcinka kursu możesz znaleźć w repozytorium autora na GitHubie, dostępnym pod adresem [1].

W następnym odcinku zobaczymy, jak w MicroPythonie obsługuje się interfejs I²C na przykładzie zegara czasu rzeczywistego DS1307 oraz pamięci EEPROM typu AT24C32.

Dominik Bieczyński
leonow32@gmail.com

Zobacz więcej:

Artykuł ukazał się w
Elektronika Praktyczna
czerwiec 2025
DO POBRANIA
Materiały dodatkowe
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik listopad 2025

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio listopad - grudzień 2025

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje listopad - grudzień 2025

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna listopad 2025

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich grudzień 2025

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów