Kurs FPGA Lattice (6). Parametry i ćwiczenia

Kurs FPGA Lattice (6). Parametry i ćwiczenia

W poprzednim odcinku obiecałem, że w tym wydaniu EP poznamy analizator logiczny Reveal. Jednak, żeby go dobrze poznać, najpierw musimy mieć odpowiednio skomplikowany układ, który będziemy mogli badać. Połączenie tych dwóch niełatwych zadań w jednym artykule uniemożliwiłoby poświęcenie im należytej uwagi. Dlatego temat analizatora zostanie mówiony za miesiąc, w kolejnym wydaniu EP.

Zaprojektujemy układ, który będzie wyświetlał liczbę na wyświetlaczu 7-segmentowym, a naciskając przyciski będziemy mogli tę liczbę zwiększać, zmniejszać lub wyzerować. Dowiemy się jak wyeliminować drgania przycisków, jak wykryć zbocze sygnału, jak zrobić licznik liczący w górę lub w dół i jak sterować wyświetlaczem. Nauczymy się także tworzyć bardziej elastyczny kod stosując parametry.

Utworzony teraz kod będzie potrzebny w kolejnej części kursu dotyczącej analizatora. Gotowy projekt jest dostępny w materiałach dodatkowych do artykułu.

Schemat

Analizę zacznijmy od zapoznania się ze schematem, wygenerowanym automatycznie przez Netlist Analyzer, pokazanym na rysunku 1. Sygnały ButtonUp i ButtonDown mają rezystor pull-down wbudowany w strukturę FPGA, choć nie zostało to pokazane na schemacie. Należy je podłączyć do przycisków, zwierających do zasilania o napięciu 3,3 V. Sygnał Reset ma rezystor pull-up podciągający do zasilania. Sygnał ten należy połączyć z przyciskiem zwierającym do masy. Stan wysoki sygnału Reset to prawidłowa praca układu, a stan niski powoduje zerowanie wszystkich komponentów.

Rysunek 1. Schemat wygenerowany na podstawie kodu z dzisiejszego odcinka kursu

Prześledźmy działanie wszystkich modułów. Sygnały ButtonUp i ButtonDown wchodzą do instancji DebounceUp i DebounceDown. Są to dwie identyczne instancje modułu typu Debouncer. Ich zadaniem jest eliminacja efektu drgań styków, które znajdują się w przyciskach. Po wciśnięciu lub zwolnieniu przycisku występuje zjawisko tzw. drżenia styków, co powoduje, że przez pewien krótki czas następują wielokrotne, chaotyczne zmiany pomiędzy stanem niskim i wysokim. Byłoby to odczytane przez układ jako wielokrotne wciśnięcie przycisku i skutkowałoby nieprawidłowym działaniem licznika. Obecność modułów Debouncer eliminuje ten problem i zapewnia odpowiednio filtrowany sygnał.

Kolejnym krokiem są instancje UpDetector i DownDetector modułu typu EdgeDetector. Zadaniem tych modułów jest wykrycie momentu, kiedy przyciski Up i Down zostaną naciśnięte, czyli kiedy stan wejścia Signal zmieni się z 0 na 1. Wtedy, na wyjściach RisingEdge generowany jest stan wysoki, trwający pojedynczy takt sygnału zegarowego. Moduły te mają także możliwość wykrywania zbocza opadającego, tzn. zmiany wejścia Signal z 1 na 0. Spowoduje to wygenerowanie stanu wysokiego na jeden cykl zegarowy na wyjściu FallingEdge. Funkcja ta nie jest używana, a wyjście to pozostało niepodłączone do niczego. Zrobiłem to celowo, aby pokazać, jak wykryć zbocze opadające i jednocześnie zademonstrować, jak syntezator usuwa nieużywany kod. Przekonamy się, że jeżeli ręcznie usuniemy nieużywane fragmenty odpowiedzialne za wykrywanie zbocza opadającego, to uzyskany bitstream będzie używał dokładnie tyle samo zasobów, co przed usunięciem.

Kolejnym modułem jest licznik Counter0. Liczy on w górę lub w dół pod warunkiem, że na wejściu CountUp lub CountDown jest stan wysoki. Jeżeli na obu wejściach jest stan niski, wtedy licznik nie zmienia swojej wartości, obojętnie co dzieje się na wejściu zegarowym. Licznik jest 4-bitowy.

Ostatnim modułem jest instancja Dekoder0 modułu Decoder7seg. Przetwarza on liczbę binarną, dostępną na 4-bitowym wyjściu z licznika, na sygnały sterujące diodami LED w wyświetlaczu 7-segmentowym.

Generator sygnału zegarowego

Moduł OSCH, który już stosowaliśmy w poprzednich odcinkach kursu, użyjemy ponownie ale tym razem będą dwie zmiany. Pierwsza zmiana, bardzo prosta, to zwiększenie częstotliwości zegara. Według instrukcji Reveal User Guide, dostępnej pod adresem [1], częstotliwość taktowania analizatora powinna być większa niż częstotliwość zegara JTAG, tzn. 12 MHz i mniejsza niż 200 MHz. Z moich doświadczeń wynika, że Reveal działa nawet przy częstotliwości 7 MHz, jednak podczas kursu postępujemy zgodnie z instrukcjami – ustawmy więc częstotliwość generatora OSCH z dotychczas stosowanej 2,08 MHz, na 14 MHz.

Druga zmiana polega na nieco innym sposobie poinformowania modułu OSCH o żądanej częstotliwości. Dotychczas ustawialiśmy parametr NOM_FREQ przy pomocy instrukcji defparam w taki sposób:

defparam Generator1.NOM_FREQ = "2.08"

W listingu 1 pokazano sposób definiowania parametrów podczas tworzenia instancji modułu. Po nazwie modułu, ale przed nazwą instancji, wpisujemy znak #, a następnie w nawiasach podajemy wszystkie parametry, jakie chcemy skonfigurować (a dokładniej mówiąc – wszystkie te, którym chcemy zmienić ich wartości domyślne). Następnie zamykamy nawias, podajemy nazwę instancji, otwieramy kolejny nawias i konfigurujemy wszystkie wejścia i wyjścia, tak jak robiliśmy to w poprzednich odcinkach. W tym odcinku kursu zobaczymy jak tworzyć moduły, które oprócz wejść i wyjść przyjmują także parametry tworzone różnymi sposobami.

Listing 1. Kod pliku top.v

// Plik top.v
module top(
input Reset,
input ButtonUp,
input ButtonDown,
output [6:0] Segments
);

// Generator sygnału zegarowego
OSCH #(
.NOM_FREQ(“14.00”)
) OSCH_inst(
.STDBY(1’b0),
.OSC(Clock14MHz),
.SEDSTDBY()
);

// Odszumianie przycisku UP
wire ButtonUpFiltered;
Debouncer #(
.FREQUENCY_MHZ(14),
.PERIOD_US(1000)
) DebounceUp(
.Clock(Clock14MHz),
.Reset(Reset),
.NoisySignal(ButtonUp),
.FilteredSignal(ButtonUpFiltered)
);

// Odszumianie przycisku DOWN
wire ButtonDownFiltered;
Debouncer #(
.FREQUENCY_MHZ(14),
.PERIOD_US(1000)
) DebounceDown(
.Clock(Clock14MHz),
.Reset(Reset),
.NoisySignal(ButtonDown),
.FilteredSignal(ButtonDownFiltered)
);

// Wykrywanie zbocza rosnącego przycisku UP
wire RequestUp;
EdgeDetector UpDetector(
.Clock(Clock14MHz),
.Reset(Reset),
.Signal(ButtonUpFiltered),
.RisingEdge(RequestUp),
.FallingEdge()
);

// Wykrywanie zbocza rosnącego przycisku DOWN
wire RequestDown;
EdgeDetector DownDetector(
.Clock(Clock14MHz),
.Reset(Reset),
.Signal(ButtonDownFiltered),
.RisingEdge(RequestDown),
.FallingEdge()
);

// Licznik
wire [3:0] CountValue;
Counter #(
.WIDTH(4)
) Counter0(
.Clock(Clock14MHz),
.Reset(Reset),
.CountUp(RequestUp),
.CountDown(RequestDown),
.Value(CountValue)
);

// Dekoder wyświetlacza 7-segmentowego
Decoder7seg Dekoder0(
.Data(CountValue),
.Segments(Segments)
);

endmodule

Debouncer

Wadą przycisków mechanicznych jest drganie styków przez krótką chwilę po naciśnięciu lub zwolnieniu przycisku. Na oscyloskopie widoczne są jako krótkie szpilki. Musimy je odfiltrować. Spójrzmy na listing 2. NoisySignal to sygnał wejściowy prosto z przycisku, a FilteredSignal to sygnał po filtrowaniu. Zwróćmy uwagę na to, że jest to zmienna reg, czyli ma możliwość pamiętania stanu. Przechowuje ona ostatnio rozpoznany stabilny stan przycisku.

Listing 2. Kod pliku debounder.v

// Plik debounder.v
module Debouncer(
input Clock, // Zegar
input Reset, // 1 – praca, 0 – reset
input NoisySignal, // Sygnał z przycisku
output reg FilteredSignal // Sygnał odfiltrowany
);

parameter FREQUENCY_MHZ = 10;
parameter PERIOD_US = 100;
localparam DELAY = FREQUENCY_MHZ * PERIOD_US;
localparam WIDTH = $clog2(DELAY + 1);

reg [WIDTH-1:0] Counter = 0; // 1

always @(posedge Clock, negedge Reset) begin
if(!Reset) begin
Counter <= 0;
FilteredSignal <= 0;
end else if(NoisySignal != FilteredSignal && Counter < DELAY) begin
Counter <= Counter + 1’b1; // 2
end else if(Counter == DELAY) begin
Counter <= 0; // 3a
FilteredSignal <= NoisySignal; // 3b
end else begin
Counter <= 0; // 4
end
end

endmodule

Przejdźmy do bloku always. W momencie kiedy zostanie stwierdzone, że stan sygnału NoisySignal jest inny niż FilteredSignal, to zostanie uruchomione zliczanie impulsów zegarowych, przy pomocy licznika Counter (linia 2). Następnie możliwe są dwa scenariusze:

Jeżeli stan sygnału NoisySignal będzie utrzymywał się odpowiednio długo, co znaczy, że drgania styków wygasły, to licznik Counter po pewnym czasie osiągnie wartość równą parametrowi DELAY. Wtedy licznik zostanie wyzerowany (linia 3a) oraz stan sygnału NoisySignal zostanie skopiowany do zmiennej FilteredSignal (linia 3b).

Jeżeli stan sygnału NoisySignal ponownie się zmieni i będzie równy FilteredSignal, oznacza to, że wystąpiło krótkotrwałe drganie styków. Licznik jest zerowany (linia 4) i wszystko zaczyna się od początku.

Moduł ma cztery parametry. Przyjęło się, że ich nazwy pisze się wielkimi literami. Parametry zdefiniowanie przy pomocy instukcji parameter mogą być modyfikowane z zewnątrz. Przykłady konfigurowania takich parametrów widoczne są na listingu 1, gdzie w nawiasach okrągłych poprzedzonych znakiem # definiujemy FREQUENCY_MHZ oraz PERIOD_US odpowiednio na 14 i 1000.

Parametry utworzone instrukcją localparam dostępne są tylko wewnątrz modułu i nie można ich modyfikować z zewnątrz. Można ich użyć jako zmiennych lokalnych. Przeanalizujmy, jaki jest cel parametrów pokazanych na listingu 2. Chcąc odfiltrować drgania styków, licznik Counter musi liczyć tak długo, aż osiągnie wartość DELAY. Im większa ta wartość, tym dłuższy czas zajmie zliczanie impulsów pomiędzy zmianą stanu wejścia NoisySignal i zmianą stanu wyjścia FilteredSignal. Ten czas jest tym dłuższy, im parametr DELAY jest większy. Czas ten musi być dłuższy niż czas drgania styków. Moglibyśmy oczywiście DELAY zdefiniować jako parametr dostępny z zewnątrz.

W ten sposób dałoby się konfigurować czas poprzez ręczne wpisywanie liczby cykli zegarowych. Takie rozwiązanie nie jest zbyt wygodne, ponieważ za każdym razem musielibyśmy obliczyć żądaną liczbę cykli, mnożąc częstotliwość zegara przez żądany czas. Niech ta liczba obliczy się sama – w końcu nie interesuje nas ile cykli trzeba policzyć, tylko ile czasu ma trwać licznie.

Utworzyłem parametry FREQUENCY_MHZ oraz PERIOD_US, które można ustawić podczas tworzenia instancji modułu Debouncer (listing 1). Domyślne wartości to 10 MHz i 100 µs, co daje 1000 cykli zegara do zliczenia. W module top te parametry zostały ustawione na 14 MHz i 1000 µs, czyli w rezultacie licznik liczy do 14000. Czas w mikrosekundach należy dobrać doświadczalnie, eksperymentując z przyciskami dostępnymi na płytce prototypowej.

Pozostaje jeszcze ustalić liczbę bitów licznika Counter. Nie chcemy ustawiać tej liczby na sztywno, ponieważ gdyby liczba bitów była za mała, wówczas doszłoby do przepełnienia licznika i zacząłby liczyć od zera, nigdy nie osiągając żądanej wartości. Ustawienie zbyt dużej liczby bitów niż potrzebna marnowałoby zasoby układu FPGA.

Z pomocą przychodzi funkcja $clog2(). Oblicza ona logarytm przy podstawie 2 z podanego argumentu i zaokrągla wynik w górę. Kilka przykładów obliczeń przy pomocy tej funkcji podano w tabeli 1. Zwróć uwagę, że funkcja $clog2() nie podaje, ile bitów potrzeba, aby zapisać badaną liczbę. Dla liczb takich jak 16 czy 256 potrzebujemy rejestrów o długości 5 i 9 bitów, jednak funkcja zwraca 4 i 8. To po prostu zaokrąglony logarytm przy podstawie 2 i nic więcej. Z tego powodu, definiując parametr WIDTH do argumentu DELAY dodajemy 1. Tak obliczoną szerokość rejestru możemy zastosować w linii 1. Tutaj może być kolejna niespodzianka dla początkujących – od uzyskanego wyniku odejmujemy 1. Jest to spowodowane tym, że w Verilogu, tworząc rejestr przykładowo 8-bitowy, w nawiasach kwadratowych podajemy, nie liczbę bitów, lecz numery pierwszego i ostatniego bitu – od zerowego do siódmego [7:0], czyli łącznie osiem bitów.

Moduł Debouncer dostarcza stan logiczny 1, kiedy przycisk jest wciśnięty oraz 0, kiedy przycisk jest zwolniony. Nas interesuje wykrycie momentu, kiedy przycisk został wciśnięty, aby wtedy zwiększyć lub zmniejszyć stan licznika. Z tego powodu potrzebujemy modułu wykrywającego zbocza sygnałów.

Wykrywanie zbocza

Wykrywanie zmiany stanu z 0 na 1 lub z 1 na 0 jest bardzo proste. Rozwiązanie pokazano na listingu 3.

Listing 3. Kod pliku edge_detector.v

// Plik edge_detector.v
module EdgeDetector(
input Clock, // Zegar
input Reset, // 1 – praca, 0 – reset
input Signal, // Badany sygnał
output RisingEdge, // 1 – wykrycie zbocza rosnącego
output FallingEdge // 1 – wykrycie zbocza malejącego
);

reg Previous = 0; // 1

always @(posedge Clock, negedge Reset)
if(!Reset)
Previous <= 0;
else
Previous <= Signal; // 2

assign RisingEdge = (Previous == 1’b0) && (Signal == 1’b1); // 3
assign FallingEdge = (Previous == 1’b1) && (Signal == 1’b0); // 4

endmodule

Potrzebujemy 1-bitowy rejestr Previous, w którym będziemy przechowywać stan badanego sygnału, jaki był w poprzednim takcie zegara (linia 1). Przy każdym zboczu sygnału zegarowego, przepisujemy stan badanego sygnału Signal do zmiennej Previous (linia 2). Znając stan obecny i poprzedni, możemy wywnioskować, czy stan badanego sygnału się zmienił. W tym celu wystarczą proste operatory logiczne. Sprawdzamy, czy stan poprzedni to 0 i obecny to 1 (linia 3). Jeżeli taka sytuacja wystąpi, wówczas na wyjściu RisingEdge pojawi się stan 1, trwający przez jeden takt sygnału zegarowego. Wykrywanie zbocza opadającego (linia 4) działa analogicznie. Kod z listingu 3 prowadzi do syntezy układu, którego schemat pokazano na rysunku 2. Widać wyraźnie, że operatory logiczne zostały zsyntezowane w postaci bramek AND i NOT, które badają stany przed i za przerzutnikiem.

Rysunek 2. Schemat wykrywacza zboczy powstały po syntezie kodu z listingu 3

Trzeba zaznaczyć jeden szczegół – sygnał doprowadzony na wejście Signal modułu EdgeDetector musi być zsynchronizowany z sygnałem zegarowym. Zwróć uwagę, że gdyby stan Signal zmieniał się szybciej niż sygnał zegarowy, wówczas te zmiany byłoby widać na wyjściach RisingEdge lub FallingEdge.

Licznik

Z licznikami mieliśmy już do czynienia w poprzednich częściach kursu, dlatego licznik omówię bardzo skrótowo. Kod został pokazany na listingu 4.

Listing 4. Kod pliku counter.v

module Counter #(
parameter WIDTH = 4 // Liczba bitów licznika
)(
input Clock, // Zegar
input Reset, // 1 – praca, 0 – reset
input CountUp, // 1 – zezwolenie na liczenie w górę
input CountDown, // 1 – zezwolenie na liczenie w dół
output reg [WIDTH-1:0] Value // Wartość licznika
);

always @(posedge Clock, negedge Reset) begin
if(!Reset)
Value <= 0;
else if(CountUp)
Value <= Value + 1’b1; // 1
else if(CountDown)
Value <= Value - 1’b1; // 2
end

endmodule
Rysunek 3. Schemat licznika wygenerowany na podstawie kodu z listingu 4

Reaguje on na zbocza rosnące sygnału zegarowego, ale tylko jeżeli spełniony jest jeden z dwóch warunków. Jeżeli stan wejścia CountUp jest równy 1, wówczas wartość licznika jest zwiększana (linia 1). Natomiast jeżeli stan wejścia CountDown jest równy 1, wtedy wartość licznika jest zmniejszana (linia 2). Co się stanie, jeżeli CountUp i CountDown będą jednocześnie w stanie wysokim? Wtedy licznik będzie liczył w górę. Wynika to z faktu, że w drzewku decyzyjnym if-else sprawdzanie stanu CountUp jest wykonywane wcześniej.

Przyjrzyjmy się bliżej parametrom. Liczba bitów wyjścia Value (czyli jednocześnie maksymalna wartość licznika) jest określona parametrem WIDTH. Gdybyśmy zdefiniowali parametr WIDTH w takim miejscu, jak w module Debouncer (listing 2) to syntezator zgłosiłby ostrzerzenie, że parametr jest używany wcześniej, niż został utworzony.

Z tego powodu musimy skorzystać z innego sposobu definiowania parametrów. Parametry możemy zdefiniować zaraz po nazwie modułu, wpisując je w nawiasy okrągłe poprzedzone znakiem #. Zwróć uwagę, że parametry i porty umieszczone są w osobnych nawiasach.

Sterownik wyświetlacza 7-segmentowego

Doszliśmy wreszcie do ostatniego modułu. Jego zadaniem jest przetworzenie 4-bitowej liczby binarnej, dostarczanej z licznika, na sygnały sterujące poszczególnymi diodami LED w segmentach wyświetlacza. Wyświetlacz składa się z siedmiu segmentów. Zastosowałem wyświetlacz ze wspólną katodą, co znaczy, że wszystkie katody diod LED są ze sobą zwarte wewnątrz wyświetlacza i należy je połączyć do masy. Segment zaświeci się, jeżeli doprowadzimy do niego sygnał w stanie 1, czyli de facto połączymy z zasilaniem. Pamiętaj, by wstawić rezystory ograniczające prąd pomiędzy piny FPGA i segmenty wyświetlacza. Zobacz rysunek 4.

Rysunek 4. Wyświetlacz 7-segmentowy

Przeanalizujmy kod z listingu 5. Konwersja 4-bitowego binarnego kodu na kod wyświetlacza realizowany jest w postaci instrukcji case. Reaguje ona na zmianę wartości Data i przypisuje rejestrowi Segments taką kombinację, by zaświecić odpowiednie segmenty wyświetlacza. Zaleca się, aby w instrukcji case uwzględnić wszystkie możliwe wartości badanej zmiennej – albo opisać je ręcznie, albo opisać najbardziej istotne, a resztę objąć instrukcją default. Taki kod doprowadzi do wygenerowania multipleksera, który jest układem kombinacyjnym, tzn. nie ma żadnych elementów pamięciowych. Można to sprawdzić przy pomocy narzędzia Netlist Analyzer. Niektórzy zalecają, żeby zawsze w instrukcji case uwzględnić również default, która jest wykonywana, gdy wystąpi jakiś przypadek nie uwzględniony przez nas w kodzie. Kod z listingu 5 uwzględnia wszystkie możliwe wartości, jakie są możliwe do zapisania w zmiennej 4-bitowej. W takiej sytuacji nie ma potrzeby stosować default – dlatego ta instrukcja została zakomentowana.

Listing 5. Kod pliku decoder7seg.v

// Plik decoder7seg.v
module Decoder7seg(
input [3:0] Data, // 4-bitowa liczba
output reg [6:0] Segments // Do wyświetlacza
);

always @(*) begin
case(Data)
// Opis segmentów gfedcba
4’h0: Segments[6:0] = 7’b0111111;
4’h1: Segments[6:0] = 7’b0000110;
4’h2: Segments[6:0] = 7’b1011011;
4’h3: Segments[6:0] = 7’b1001111;
4’h4: Segments[6:0] = 7’b1100110;
4’h5: Segments[6:0] = 7’b1101101;
4’h6: Segments[6:0] = 7’b1111101;
4’h7: Segments[6:0] = 7’b0000111;
4’h8: Segments[6:0] = 7’b1111111;
4’h9: Segments[6:0] = 7’b1101111;
4’hA: Segments[6:0] = 7’b1110111;
4’hB: Segments[6:0] = 7’b1111100;
4’hC: Segments[6:0] = 7’b0111001;
4’hD: Segments[6:0] = 7’b1011110;
4’hE: Segments[6:0] = 7’b1111001;
4’hF: Segments[6:0] = 7’b1110001;
// default: Segments[6:0] = 7’b0000000;
endcase
end

endmodule

Spróbuj wyrzucić kilka dowolnych linijek z case i zobacz, co będzie wynikiem syntezy. Okaże się, że w raporcie Map znajdziemy siedem przerzutników więcej! Skąd one się biorą? Taka konstrukcja jest dla syntezatora poleceniem „dla wybranych wartości Data ustaw takie wartości Segments, jakie podaję w kodzie, a jeżeli wystąpi wartość, jakiej nie zdefiniowałem, to pamiętaj ostatnio ustawioną wartość Segments”. W takiej sytuacji muszą zostać wygenerowane przerzutniki, by pamiętać ostatnio ustawioną wartość.

Przerzutniki wygenerowane instrukcją case na ogół są efektem błędnie napisanego kodu. Jest to pułapka, w jaką łatwo wpaść, kiedy pomija się instrukcję default bez zdefiniowania wszystkich możliwych wartości zmiennej. Zwróć jeszcze uwagę, że w kodzie z listingu 5 stosowane są przypisania blokujące (=), ponieważ jest to układ kombinacyjny. Przypisania nieblokujące (<=) stosujemy w układach sekwencyjnych.

Podsumowanie

Kiedy to wszystko zsyntezujemy i wgramy do FPGA, dwoma przyciskami będziemy mogli zwiększać lub zmniejszać cyfrę na wyświetlaczu. Przećwiczyliśmy tworzenie trochę bardziej skomplikowanego projektu, podzielonego na kilka modułów, które można dodatkowo konfigurować z pliku top przy pomocy parametrów. Zaprezentowany projekt będziemy badać w kolejnej części kursu, gdzie poznamy analizator Reveal. Umieszczenie tego analizatora wewnątrz FPGA pozwoli nam obserwować, jak zmieniają się różne sygnały i co jest zapisane w przerzutnikach.

Dominik Bieczyński
leonow32@gmail.com

Czytaj więcej:

  1. Reveal User Guide – https://bit.ly/3ZIFoiA
Artykuł ukazał się w
Elektronika Praktyczna
kwiecień 2023
DO POBRANIA
Materiały dodatkowe
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik listopad 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio listopad - grudzień 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje październik 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna listopad 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich listopad 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów