Maszyna stanów skończonych dla programisty systemów wbudowanych

Maszyna stanów skończonych dla programisty systemów wbudowanych
Pobierz PDF Download icon
Maszyna stanów skończonych jest pojęciem abstrakcyjnym i definiuje zachowanie systemów dynamicznych jako maszyny o skończonej liczbie stanów i skończonej liczbie przejść pomiędzy tymi stanami. Defi nicja ta może wydawać się dość abstrakcyjna dla typowego programisty, jednak jak się przekonamy, maszyny stanów skończonych odgrywają bardzo ważną rolę w programowaniu mikroprocesorów.
89ELEKTRONIKA PRAKTYCZNA 1/2010 Maszyna stanów skończonych dla programisty systemów wbudowanych Dodatkowe informacje: Bibliotekę sm-lib można pobrać bezpłatnie ze strony internetowej http://toan.pl Nie skłamię, mówiąc, że większość pro- gramistów nieświadomie i  bardzo często używa tych maszyn stanów. Aby zachęcić Czytelnika do dalszej lektury, powiem tylko, że ich używanie prowadzi do powstawania niezawodnych programów, których popraw- ność można udowodnić matematycznie. Z tego powodu artykuł jest lekturą obowiąz- kową dla wszystkich tych, dla których nie- zawodność programu jest kluczowa. Poniżej skupimy się na praktycznym wykorzystaniu maszyny stanów przy minimalnej dawce niezbędnej teorii. Jeden przykład wart więcej niż 1000 słów Zacznijmy nasze rozważania od proste- go przykładu. Na rys. 1 umieszczono sche- mat urządzenia do zmiany świateł. Zastoso- wano w nim dwa przyciski, które powodu- ją zmianę świateł na ?w przód? oraz ?w tył?. Na rys.  2 pokazano algorytm, który powi- nien wykonywać program. Wynika z niego, że wciskając wielokrotnie przycisk BT1, po- winno się uzyskać sekwencję przełączania świateł: zielone, żółte, czerwone, zielone, żół- te, czerwone... Przycisk BT2 służy do zmiany sekwencji na ?w tył?, więc wciskając go, po- winno się uzyskać sekwencję: czerwone, żół- te, zielone, czerwone, żółte... Napisanie programu dla tego algorytmu raczej nie sprawi nikomu problemu. Zanim Czytelnik zapozna się ze źródłem mojego programu (list.  1), proponuję napisać na kartce zarys swojej implementacji przedsta- wionego problemu. Wykonanie tego ćwicze- nia pozwoli porównać nasze rozwiązania. Maszyna stanów skończonych dla programisty systemów wbudowanych Maszyna stanów skończonych jest pojęciem abstrakcyjnym i  de?niuje zachowanie systemów dynamicznych jako maszyny o  skończonej liczbie stanów i  skończonej liczbie przejść pomiędzy tymi stanami. De?nicja ta może wydawać się dość abstrakcyjna dla typowego programisty, jednak jak się przekonamy, maszyny stanów skończonych odgrywają bardzo ważną rolę w  programowaniu mikroprocesorów. Efekt takiego porównania może być bardzo ciekawy i pozwoli spojrzeć na problem z in- nej perspektywy. W  swojej implementacji pominąłem wykonanie funkcji pobierz_przy- cisk() oraz swiatlo(), ponieważ są one zależne od użytego procesora, a my skupiamy się na istocie problemu, a nie na konkretnej imple- mentacji. W  naszych rozważaniach użyty sprzęt nie ma żadnego znaczenia, ponieważ można je snuć w  odniesieniu do każdego mikroprocesora, również w dużych kompu- terach. Analizując kod źródłowy z list. 1, moż- na zauważyć, że użyłem zmiennej o nazwie stan do zapisu informacji o  bieżącym, za- świeconym świetle. Taka organizacja kodu jest niczym innym, jak maszyną stanów. W programie mamy trzy możliwe stany, któ- re odpowiadają zaświeconemu światłu: ZIE- LONE, CZERWONE oraz ŻÓŁTE. Porównaj- my teraz kod programu z algorytmem. Chociaż wykonaliśmy implementację algorytmu, to kod programu nie stanowi dokładnego jego opisu. Przydałby się jakiś sposób na implementację algorytmu bezpo- średnio z diagramu. Ma rację ten, kto podej- rzewa, że takie narzędzie zostanie za chwilę przedstawione. Zanim to jednak nastąpi, musimy przyjrzeć się, jak wygodnie repre- zentować algorytmy w pamięci procesora. Modelowanie algorytmu Zastanówmy się, jak zapisać nasz algo- rytm do sterowania światłami. Najpierw wy- różnimy trzy stany: STAN 1: ZIELONE STAN 2: ŻÓŁTE STAN 3: CZERWONE Następnie stwórzmy warunki przejścia do następnego stanu: STAN 1: ZIELONE WARUNEK 1: PRZYCISK BT1 WCIŚNIĘTY ?> PRZEJŚCIE DO STANU 2 ŻÓŁTE WARUNEK 2: PRZYCISK BT2 WCIŚNIĘTY ?> PRZEJŚCIE DO STANU 3 CZERWONE Rys. 1. Rys. 2. NOTATNIK KONSTRUKTORA 90 ELEKTRONIKA PRAKTYCZNA 1/2010 NOTATNIK KONSTRUKTORA Słownik: Automat skończony: równoważne określenie dla maszyny stanów skończonych. Graf: zbiór wierzchołków oraz połączeń między tymi wierzchołkami. Grafy mają duże praktyczne znaczenie dla informatyki i  są uogólnieniem wielu struktur danych, np. drzew binarnych. Są stosowane np. w  systemach GPS, ponieważ pozwalają łatwo rozwiązać problem komiwojażera. UML: obiektowy język modelowania programów. Umożliwia m.in. modelowanie maszyny stanów, które będą implementowane w  postaci obiektów np. w  języku C++ lub Java. Preprocesor języka C: program interpretujący, który przetwarza wstępnie kod języka C. Wszystkie instrukcje, które w  programie zaczynają się od znaku #, są instrukcjami preprocesora. Najbardziej znane instrukcje to #include oraz #de?ne. Sieci Petriego: są uogólnieniem maszyny stanów i  umożliwiają modelowanie współbieżnych zdarzeń. STAN 2: ŻÓŁTE WARUNEK 1: PRZYCISK BT1 WCIŚNIĘTY ?> PRZEJŚCIE DO STANU 3 CZERWONE WARUNEK 2: PRZYCISK BT2 WCIŚNIĘTY ?> PRZEJŚCIE DO STANU 1 ZIELONE STAN 3: CZERWONE WARUNEK 1: PRZYCISK BT1 WCIŚNIĘTY ?> PRZEJŚCIE DO STANU 1 ZIELONE WARUNEK 2: PRZYCISK BT2 WCIŚNIĘTY ?> PRZEJŚCIE DO STANU 2 ŻÓŁTE Udało nam się zapisać algorytm w  for- mie słownika stanów. Zapiszmy słownik sta- nów w bardziej formalnej formie: Taki zapis jest już wystarczający, żeby zaimplementować go właśnie w  programie. Zanim jednak przedstawię bibliotekę oraz sposób imple- mentacji takiego opisu w ko- dzie programu, przejdźmy o krok dalej i zastanówmy się, czy algorytm da się przedsta- wić jako zwykłą tabelę? Macierzowa reprezentacja maszyny stanów Przedstawienie algorytmu w  for- mie macierzy jest jak najbardziej moż- liwe. Każdy, kto kiedykolwiek prze- rabiał teorie grafów, nie będzie miał problemów ze zrozumieniem macie- rzowej formy algorytmu. Algorytm jest w istocie grafem skierowanym i moż- na utworzyć dla niego macierz przejść. Żeby to udowodnić, na rys. 3 przed- stawiłem algorytm w  formie grafu skierowanego. Wierzchołek grafu od- powiada stanowi maszyny, natomiast łuk odpowiada warunkowi przejścia z jednego stanu do drugiego. W tab. 1 pokazano macierz przejść dla nasze- go algorytmu. Wiersze reprezentują stan, z którego ma nastąpić przejście, natomiast kolumny stan docelowy, czyli stan, do którego nastąpi przejście ze stanu pierwot- nego. Dane w tabeli odpowiadają warunkom, jakie powinny zostać spełnione aby przejście z  jednego stanu do drugiego było możliwe. Macierz jest kwadratowa, czyli jej rozmiar to N×N, gdzie N oznacza liczbę wszystkich sta- nów/wierzchołków. Macierz stanów różni się od macierzy sąsiedztwa grafu tym, że ta pierwsza niesie informacje o  warunkach przejścia, nato- miast macierz sąsiedztwa zawiera informa- cje o  liczbie dróg łączących wierzchołki. W przypadku, gdy chcemy przejść ze stanu 1 ZIELONE do stanu 3 CZERWONE, wybie- ramy w tabeli wiersz numer 1 oraz kolumnę numer 3 i sprawdzamy, jaki warunek kryje się w tabeli dla tego wyboru: BT2. Dyspo- nując utworzoną macierzą, możemy spraw- dzić, czy jest ona poprawna. Pierwszym testem jest sprawdzenie, czy w  danym wierszu nie powtarzają się dwa takie same List. 1. #de?ne ZIELONE 1 #de?ne ZOLTE 2 #de?ne CZERWONE 3 #de?ne BT1 1 #de?ne BT2 2 void swiatlo(int s) { ... } char pobierz_przycisk() { ... } int main() { int stan=ZIELONE; // poczatkowy stan char przycisk; while(1) { przycisk=pobierz_przycisk(); if (przycisk==BT1){ switch(stan){ case ZIELONE: stan=ZOLTE; break; case ZOLTE: stan=CZERWONE; break; case CZERWONE: stan=ZIELONE; break; } } if (przycisk==BT2){ switch(stan){ case ZIELONE: stan=CZERWONE; break; case ZOLTE: stan=ZIELONE; break; case CZERWONE: stan=ZOLTE; break; } } swiatlo(stan); } return 0; } List. 2. #include ?sm-lib/sm_seq.h? #de?ne ZIELONE 1 #de?ne ZOLTE 2 #de?ne CZERWONE 3 #de?ne BT1 1 #de?ne BT2 2 void swiatlo(int s) { ... } char pobierz_przycisk() { ... } void sygnalizacja() { int k; k = pobierz_przycisk(); SMS_BEGIN(SWIATLA,ZIELONE); SMS_STATE_BEGIN(SWIATLA, ZIELONE, swiatlo(ZIELONE) ); SMS_STATE_COND(SWIATLA, k==BT1, ZOLTE); SMS_STATE_COND(SWIATLA, k==BT2, CZERWONE); SMS_STATE_END(); SMS_STATE_BEGIN(SWIATLA, ZOLTE, swiatlo(ZOLTE) ); SMS_STATE_COND(SWIATLA, k==BT1, CZERWONE); SMS_STATE_COND(SWIATLA, k==BT2, ZIELONE); SMS_STATE_END(); SMS_STATE_BEGIN(SWIATLA, CZERWONE, swiatlo(CZERWONE) ); SMS_STATE_COND(SWIATLA, k==BT1, ZIELONE); SMS_STATE_COND(SWIATLA, k==BT2, ZOLTE); SMS_STATE_END(); SMS_END(SWIATLA,ZIELONE); } int main() { while(1) { sygnalizacja(); } return 0; } Rys. 3. Stan Warunki 1 BT1->2, BT2->3 2 BT1->3, BT2->1 3 BT1->1, BT2->2 Tab. 1. 91ELEKTRONIKA PRAKTYCZNA 1/2010 Maszyna stanów skończonych dla programisty systemów wbudowanych warunki. Taka sytuacja nie może mieć miej- sca, ponieważ przy spełnionym warunku powstałaby niejednoznaczność, do którego stanu należy przejść. Kolejnym testem może być sprawdzenie przekątnej macierzy. Na przekąt- nej nie powinno być warunków, ponieważ nie ma sensu zmiana stanu na ten sam stan, dlatego że jest to marnowanie mocy proce- sora. Biblioteka dla języka C Przedstawię poniżej bardzo prostą bibliotekę napisaną w pre- procesorze języka C. Użyłem ma- kra preprocesora z tego względu, że biblioteka jest dedykowana dla małych mikroprocesorów, więc rozmiar kodu i wydajność ma klu- czowe znaczenie. Biblioteka po- trzebuje dla de?nicji każdej ma- szyny stanów tylko 1 bajta pamię- ci. Jest wieloplatformowa i  bez żadnych zmian można jej używać na każdym mikroprocesorze. Je- dynym ograniczeniem jest kompilator, który powinien obsługiwać makra preprocesora. Na całą bibliotekę składają się tylko dwa pliki: sm_cond.h oraz sm_table.h. Pierwszy plik zawiera implementację maszyny stanów w  formie słownika stanów. Natomiast dru- gi plik zawiera macierzową implementację maszyny stanów. Biblioteka jest sprawdzo- na w boju, ponieważ użyłem jej już w kilku programach dla różnych urządzeń. Zanim jednak pokażę przykład użycia biblioteki, odpowiemy sobie na pytanie: Po co stosować tę bibliotekę, gdy można samemu zaprogra- mować tę trywialną funkcjonalność? Otóż zastosowanie biblioteki ma duże znaczenie dla stabilności rozwiązania. Biblioteka jest dobrze przetestowana i dzięki temu nie po- trzebujemy testować funkcjonalności maszy- ny stanów, a jedynie sam algorytm. Zapew- ne wielu Czytelników zechce zastosować tę bibliotekę w  swoim programie, więc wyjdą na jaw błędy, których sam nie wykryłem, co sprawi, że biblioteka będzie jeszcze bardziej niezawodna. Spójrzmy teraz na list.  2. Za- wiera on odpowiednik programu z list. 1, ale napisany z  użyciem biblioteki sm_cond.h. Moglibyście spytać, gdzie jest właściwy pro- gram? Odpowiedź jest prosta: Program jest zawarty w  regułach maszyny stanów. Prze- List. 3. void sygnalizacja() { int k; k = pobierz_przycisk(); static unsigned char current_state = 1; switch ( current_state ) { case 1: { swiatlo(1); if( k==1 ) { current_state = 2; break; }; if( k==2 ) { current_state = 3; break; }; break; }; case 2: { swiatlo(2); if( k==1 ) { current_state = 3; break; }; if( k==2 ) { current_state = 1; break; }; break; }; case 3: { swiatlo(3); if( k==1 ) { current_state = 1; break; }; if( k==2 ) { current_state = 2; break; }; break; }; default: current_state = 1; } } int main() { while(1) { sygnalizacja(); } return 0; } List. 4. #include #include ?sm-lib/sm_seq.h? #de?ne ON 1 #de?ne OFF 0 #de?ne TRUE 1 #de?ne FALSE 0 #de?ne GETC() getchar() #de?ne PUTS(X) printf(X) static int echo; void init() { PUTS(?INIT OK\n?); echo=OFF; } void at() { PUTS(?OK\n?); } void ati() { PUTS(?MODEM FIRMWARE v.1.0.0\n?); } void error() { PUTS(?BLAD\n?); } void ate() { echo=!echo; if (echo==ON) printf(?ECHO ON\n?); else printf(?ECHO OFF\n?); } char pobierz_znak() { int k = GETC(); if ( (echo==ON) ) printf(?%c?,k); return k; } void modem() { static char c=0; SMS_BEGIN(MODEM,1); SMS_STATE_BEGIN(MODEM, 1, init() ); SMS_STATE_COND(MODEM, TRUE, 2); SMS_STATE_END(); List. 4. c.d. SMS_STATE_BEGIN(MODEM, 2, c=pobierz_znak() ); SMS_STATE_COND(MODEM, c==?a?, 3); SMS_STATE_COND(MODEM, c==? ?, 2); SMS_STATE_COND(MODEM, c==?\n?, 2); SMS_STATE_COND(MODEM, c!=0, 10); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 3, c=pobierz_znak() ); SMS_STATE_COND(MODEM, c==?t?, 4); SMS_STATE_COND(MODEM, c!=0, 10); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 4, c=pobierz_znak() ); SMS_STATE_COND(MODEM, c==?i?, 5); SMS_STATE_COND(MODEM, c==?e?, 6); SMS_STATE_COND(MODEM, c==?z?, 7); SMS_STATE_COND(MODEM, c==? ?, 4); SMS_STATE_COND(MODEM, c==?\n?,13); SMS_STATE_COND(MODEM, c!=0, 10); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 5, c=pobierz_znak() ); SMS_STATE_COND(MODEM, c==?\n?, 8); SMS_STATE_COND(MODEM, c==? ?, 11); SMS_STATE_COND(MODEM, (c!=0), 10); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 6, c=pobierz_znak() ); SMS_STATE_COND(MODEM, c==?\n?, 9); SMS_STATE_COND(MODEM, c==? ?, 12); SMS_STATE_COND(MODEM, (c!=0), 10); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 7, c=pobierz_znak() ); //ATZ SMS_STATE_COND(MODEM, c==?\n?, 1); SMS_STATE_COND(MODEM, (c!=0), 10); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 8, ati() ); //ATI SMS_STATE_COND(MODEM, TRUE, 2); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 9, ate(); ); //ATE SMS_STATE_COND(MODEM, TRUE, 2); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 10, error() ); // ERROR SMS_STATE_COND(MODEM, TRUE, 2); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 11, ati() ); //ATI SMS_STATE_COND(MODEM, TRUE, 4); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 12, ate() ); //ATE SMS_STATE_COND(MODEM, TRUE, 4); SMS_STATE_END(); SMS_STATE_BEGIN(MODEM, 13, at() ); //AT SMS_STATE_COND(MODEM, TRUE, 2); SMS_STATE_END(); SMS_END(MODEM,1); } int main() { while(1) { modem(); } return 0; 92 ELEKTRONIKA PRAKTYCZNA 1/2010 NOTATNIK KONSTRUKTORA śledźmy teraz instrukcje odpowiedzialne za de?nicję maszyny stanów. Makro SM_BEGIN(SWIATLA, ZIELO- NE) de?niuje nową maszynę stanów o  na- zwie SWIATLA. Nazwa SWIATLA musi być unikalna. Nazwanie maszyny stanów jest konieczne z  tego względu, że możemy zde?niować wiele maszyn stanów w  swo- im programie i mógłby pojawić się kon?ikt nazw. Stała ZIELONE w  wywołaniu tego makra oznacza startowy stan maszyny. Stałe ZIELONE, CZERWONE i ZOLTE muszą być unikalnymi liczbami i  służą do oznaczenia stanu maszyny. Następnie w kodzie znajdują się trzy de?nicje pary: STANLISTA WARUNKÓW. Makro SM_STATE_BEGIN(SWIATLA, ZIELONE, swiatlo(ZIELONE) ) rozpoczyna parę i zawiera trzy argumenty. Pierwszy ar- gument to nazwa maszyny, drugi argument to stan, którego dotyczy para, natomiast trze- ci argument to akcja do wykonania dla tego stanu. Akcją może być wywołanie funkcji tak jak w naszym przykładzie lub można umie- ścić bezpośrednio jakieś instrukcje w  tym miejscu. Makro SM_STATE_COND(SWIA- TLA, k==BT1, ZOLTE) de?niuje warunek dla danego stanu i zawiera również trzy ar- gumenty. Pierwszy argument to nazwa ma- szyny, drugi argument to warunek, który po- winien zostać spełniony, żeby przejść z tego stanu do stanu podanego w argumencie trze- cim. Możemy oczywiście zde?niować sobie dowolną liczbę warunków oraz stanów. Mu- simy jednak uważać, aby całość był spójna i żeby nigdy nie było sytuacji zde?niowania warunku przejścia do stanu, którego de?ni- cja nie istnieje. Jeśli chcemy zobaczyć czysty kod języka C bez preprocesora, to możemy użyć opcji -E kompilatora GCC (inne kompi- latory także powinny posiadać taką opcję). Na list.  3 pokazano kod po przetworzeniu go przez preprocesor. Jak widać na listingu, biblioteka nie tworzy żadnych wywołań do swoich wewnętrznych funkcji, tylko gene- ruje kod i wstawia go w miejscu wywołania makr. Takie podejście zapewnia dużą wy- dajność rozwiązania i łatwą analizę takiego kodu. Praktyczny przykład: analizator komend AT Komendy AT są szeroko stosowane w  modemach. Wybór takiego przykła- du jest nieprzypadkowy. Bardzo często w  praktyce elektronika trzeba utworzyć protokół transmisji danych do komunika- cji z  danym urządzeniem. Ten przykład pokaże, w jaki sposób użyć do tego maszy- ny stanów. W prezentowanym przykładzie została zrobiona analiza 4 podstawowych komend AT: at ? zwraca OK ati ? informacje o modemie ate ? włącza/wyłącza echo atz ? reset urządzenia Komendy można łączyć w jednym ciągu np. dla ciągu znaków ?ati e z? zostaną wy- konane komendy: ati, ate oraz atz. Nasz ana- lizator musi być odporny na błędy oraz na różne sytuacja np. ?ati e z?, czyli wiele spacji rozdzielających w ciągu. Kod programu znajduje się na list.  4. Kod w obecnej postaci można skompilować dowolnym kompilatorem na komputer PC. Jeśli będziemy chcieli skompilować go na mikroprocesor i wykorzystać UART do jego obsługi, to musimy jedynie zmienić de?nicje GETC i PUTS oraz dodać inicjalizację odpo- wiednich urządzeń specy?czną dla danego mikroprocesora. Implementując analizator komend w oparciu o zaprezentowaną biblio- tekę, oszczędzamy pamięć RAM kosztem pamięci programu (np. Flash). Oszczędność pamięci RAM jest dużą zaletą, gdy piszemy program na mały mikroprocesor, ponieważ zazwyczaj mamy do dyspozycji niewiele pa- mięci RAM, a za to dużo pamięci programu typu Flash. Podsumowanie Może się wydawać, że przedstawiona biblioteka jest wręcz idealna do każdego za- stosowania, ale niestety tak nie jest. Nie ma prostej odpowiedzi na pytanie, kiedy należy ją stosować. Są jedynie pewne przesłanki, które pomagają to ustalić. Jeśli piszemy program i  chcemy użyć zmiennej statycznej w  danej funkcji, czyli chcemy pamiętać stan tej zmiennej w kolej- nych krokach, to jest to dla nas sygnał, że należy się zastanowić nad użyciem maszyny stanów. Podobnie ma się sprawa z rekurencją. Często lepszym rozwiązaniem od rekuren- cji będzie użycie maszyny stanów. Jeśli np. chcemy zbudować wielopoziomowe menu w swoim urządzeniu to wręcz musimy zasto- sować maszynę stanów. Przestrzegam także przed tworzeniem maszyn o  bardzo dużej liczbie możliwych stanów. Dużo lepszym po- mysłem jest podzielenie całego programu na logiczne części i zastosowania kilku maszyn stanów zamiast jednej bardzo rozbudowanej. Przy tworzeniu algorytmu sterującego bardzo pomocne mogą okazać się programy kompu- terowe do tworzenia diagramów algorytmu. Do tworzenia takich diagramów wystarczy program OpenOf?ce. Jeśli chcielibyśmy dedy- kowane narzędzie to można użyć programów do modelowania sieci Petriego. Zachęcam wszystkich Czytelników do własnych prób w  tej dziedzinie. Polecam zainteresować się także sieciami Petriego z tego względu, że są uogólnieniem maszyny stanów. Będę również wdzięczny za wszelkie uwagi oraz sugestie dotyczące artykułu oraz prezentowanej bi- blioteki, które można wysłać na mój e-mail. Tomasz Orłowski tomek@toan.pl
Artykuł ukazał się w
Styczeń 2010
DO POBRANIA
Pobierz PDF Download icon

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik wrzesień 2021

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio wrzesień - październik 2021

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka Podzespoły Aplikacje wrzesień 2021

Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna wrzesień 2021

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich wrzesień 2021

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów