Kurs FPGA Lattice (14). Enkoder obrotowy

Kurs FPGA Lattice (14). Enkoder obrotowy

Enkodery obrotowe są dobrym pomysłem na ciekawy interfejs użytkownika. Obracanie pokrętłem przypomina stare analogowe rozwiązania, jest intuicyjne i wygodne. W dzisiejszym odcinku kursu zobaczymy, jak zastosować enkodery dostępne na płytce testowej User Interface Board współpracującej z MachXO2 Mega.

Enkoder obrotowy składa się z pokrętła i opcjonalnie w tym pokrętle może być także przycisk. Na płytce User Interface Board zastosowano enkodery typu EC12E20-24P24C-SW firmy SR Passives. Zobacz schemat pokazany na rysunku 1.

Rysunek 1. Schemat jednego z dwóch enkoderów na płytce User Interface Board

Enkoder tego typu ma dwa wyjścia, dzięki którym możemy odczytać informacje o obrocie pokrętła. Są to wyjścia A i B, dostępne na pinach 1 i 3. Wyjście S, dostępne na pinie 4, informuje o wciśnięciu pokrętła. Wewnątrz enkodera znajduje się mechanizm zwierający wyjścia A, B i S z pinami 2 i 5, które najwygodniej jest podłączyć prosto do masy.

Wyjście S działa jak zwyczajny przycisk. Kiedy pokrętło enkodera jest wciśnięte, wówczas pin numer 4 jest połączony z masą, co powoduje, że sygnał Enc1-S przechodzi w stan niski. Jednak kiedy pokrętło nie jest wciśnięte, to stan wysoki jest wymuszany przez rezystor pull-up R42.

Wyjścia A i B również mają możliwość zwierania do masy i należy zastosować rezystory pull-up R40 i R41. Wyjścia te są naprzemiennie zwierane do masy przez mechanizm enkodera, w zależności od kierunku obrotu – o tym będzie kilka akapitów niżej.

Pary rezystorów i kondensatorów R43-C40, R44-C41 oraz R45-C42 tworzą filtr dolnoprzepustowy, którego zadaniem jest odfiltrowanie drgań styków. W ten sposób powstaje sygnał wolnozmienny, który powoli narasta lub opada, w miarę ładowania i rozładowywania kondensatora C40, C41 lub C42. Pamiętać należy, że takie sygnały nie powinny być doprowadzone wprost do zwykłego wejścia cyfrowego, ponieważ mogą powodować wielokrotne zmiany stanu, czyli mielibyśmy wciąż ten sam problem, co drgania styków. Rozwiązaniem jest zastosowanie wejść z histerezą. Tak się składa, że wszystkie piny w FPGA MachXO2 są wyposażone w przerzutnik Schmitta z histerezą – wystarczy ją tylko uaktywnić w narzędziu Spreadsheet.

Jeżeli jednak zależy nam, by ciąć koszty elementów, a mamy wolne zasoby w FPGA, to w takiej sytuacji można by ze schematu 1 usunąć wszystkie rezystory i kondensatory. Wtedy drgania styków trzeba odfiltrować w FPGA, na przykład implementując moduł Debouncer, który omawialiśmy w 6 odcinku kursu.

Przyjrzyjmy się bliżej sygnałom A i B. Kiedy nic się nie dzieje, oba sygnały pozostają w stanie wysokim. Enkoder typu EC12E20-24P24C-SW podczas obrotu wytwarza kliknięcia, które użytkownik wyczuwa palcami jako potwierdzenie, że enkoder zarejestrował obrót. Kliknięcie występuje po obrocie pokrętła co każde 15°. Pełny obrót to 24 kliknięcia.

Podczas każdego kliknięcia następują cztery zmiany sygnałów A oraz B. Zobacz rysunek 2. Jeżeli kręcimy w prawo, to najpierw sygnał A przechodzi w stan niski, natomiast jeżeli kręcimy w lewo, to najpierw zbocze opadające zaobserwujemy na sygnale B. W ten sposób możemy określić kierunek obrotu.

Rysunek 2. Przebiegi na wyjściach A i B podczas kręcenia w prawo lub lewo

Należy podkreślić, że istnieją enkodery, które nie klikają podczas obracania i mają inną liczbę impulsów na obrót. Ponadto w enkoderach bez klikania nie da się wyróżnić stanu spoczynkowego, bo sekwencja zmian sygnałów A i B może być zatrzymana w dowolnym momencie.

Jednak zanim zaczniemy analizować zmiany sygnałów A, B i S, to musimy z nimi coś zrobić, aby przypadkiem nie wpaść w pułapkę metastabilności. Co takiego? Sygnały z enkodera doprowadzone do pinów FPGA to sygnały całkowicie asynchroniczne, zmieniające się w sposób niezależny od sygnału zegarowego, więc mogłyby spowodować błąd setup time lub hold time. Musimy je zsynchronizować z sygnałem zegarowym, który taktuje pozostałe elementy wewnątrz FPGA. Jeżeli nie wiesz dlaczego – koniecznie przeczytaj 11 odcinek kursu.

W tym celu najpierw musimy zbudować moduł synchronizujący.

Moduł Synchronizer

Sygnał wejściowy może się zmieniać w dowolnej chwili, przez co istnieje ryzyko, że nie zostanie spełniony warunek setup time lub hold time. W rezultacie może to prowadzić do powstania stanu metastabilnego, który z kolei doprowadzi do błędnego działania reszty układu.

Rozwiązaniem problemu jest synchronizacja sygnału z domeną zegarową. Polega na przepuszczeniu go przez dwa przerzutniki D w taki sposób, jak to pokazano na rysunku 3.

Rysunek 3. Schemat prostego synchronizatora

Nawet jeżeli przydarzy się nam jakiś stan metastabilny za przerzutnikiem Buffer0, to wygasi się on przed przejściem poprzez Buffer1.

Prześledźmy kod pokazany na listingu 1.

Listing 1. Kod pliku synchronizer.v

// Plik synchronizer.v

`default_nettype none
module Synchronizer #(
parameter WIDTH = 1 // 1
)(
input wire Clock,
input wire Reset,
input wire [WIDTH-1:0] Async_i, // 2
output wire [WIDTH-1:0] Sync_o // 3
);

// Rejestry
reg [WIDTH-1:0] Buffer0; // 4
reg [WIDTH-1:0] Buffer1;

// Przesuwanie bitów między rejestrami
always @(posedge Clock, negedge Reset) begin
if(!Reset) begin // 5
Buffer0 <= 0;
Buffer1 <= 0;
end else begin
Buffer0 <= Async_i; // 6
Buffer1 <= Buffer0; // 7
end
end

// Przypisanie wyjścia
assign Sync_o = Buffer1; // 8

endmodule
`default_nettype wire

Jest to implementacja rozwiązania z rysunku 3, ale z tą różnicą, że może być używana dla sygnałów wielobitowych. Rozwiązanie jest bardzo proste – dla każdego bitu tworzona jest osobna para przerzutników.

W linii 1 definiujemy parametr WIDTH określający liczbę bitów sygnału wejściowego, a jednocześnie także sygnału wyjściowego. Parametru tego używamy w liniach 2 oraz 3, gdzie znajdują się porty wejściowe i wyjściowe. Następnie tworzymy dwie zmienne Buffer0 i Buffer1 typu reg (linia 4). Liczba bitów tych zmiennych musi być równa szerokości wejścia i wyjścia.

Dalej mamy prosty blok always, reagujący (tak jak zawsze) na zbocze rosnące sygnału zegarowego i zbocze opadające sygnału resetującego. Jeżeli sygnał Reset jest w stanie niskim, to zerujemy bufory (linia 5). A jeżeli nie jest, to wtedy kopiujemy stan wejścia Async_i do Buffer0 i jednocześnie kopiujemy obecną zawartość Buffer0 do Buffer1 (linia 6 i 7). Przypominam, że te dwie linie wykonują się jednocześnie w tej samej chwili, a nie jedna po drugiej, jak by to miało miejsce w standardowym języku programowania.

Pozostaje już tylko przypisać zmienną Buffer1 do wyjścia Sync_o (linia 8).

Moduł Encoder

Przejdźmy teraz do modułu odpowiedzialnego za obsługę enkodera obrotowego, który pokazano na listingu 2. Zacznijmy od omówienia wejść i wyjść.

Listing 2. Kod pliku encoder.v

// Plik encoder.v

`default_nettype none
module Encoder #(
parameter CLOCK_HZ = 10_000_000
)(
input wire Clock,
input wire Reset,
input wire AsyncA_i,
input wire AsyncB_i,
input wire AsyncS_i,
output reg Increment_o,
output reg Decrement_o,
output wire ButtonPress_o,
output wire ButtonRelease_o,
output wire ButtonState_o
);

// Synchronizacja wejść z domeną zegarową
wire A; // 1
wire B;
wire S;
Synchronizer #(
.WIDTH(3) // 2
) SynchronizerA(
.Clock(Clock),
.Reset(Reset),
.Async_i({AsyncA_i, AsyncB_i, !AsyncS_i}), // 3
.Sync_o({A, B, S}) // 4
);

// Definicje maszyny stanów
reg [1:0] State; // 5
localparam IDLE = 0;
localparam WAIT_FOR_LOW_A = 1;
localparam WAIT_FOR_LOW_B = 2;
localparam WAIT_FOR_IDLE = 3;

// Analiza obrotu pokrętła
always @(posedge Clock, negedge Reset) begin
if(!Reset) begin
Increment_o <= 0;
Decrement_o <= 0;
State <= IDLE;
end else begin
case(State)

IDLE: begin // 6
if(A & !B) begin
State <= WAIT_FOR_LOW_A;
end else if(!A & B) begin
State <= WAIT_FOR_LOW_B;
end else if(!A & !B) begin
State <= WAIT_FOR_IDLE;
end
end

WAIT_FOR_LOW_A: begin // 7
if(!A & !B) begin
Decrement_o <= 1’b1;
State <= WAIT_FOR_IDLE;
end else if(A & B) begin
State <= IDLE;
end
end

WAIT_FOR_LOW_B: begin // 8
if(!A & !B) begin
Increment_o <= 1’b1;
State <= WAIT_FOR_IDLE;
end else if(A & B) begin
State <= IDLE;
end
end

WAIT_FOR_IDLE: begin // 9
Increment_o <= 1’b0;
Decrement_o <= 1’b0;
if(A & B) begin
State <= IDLE;
end
end

endcase
end
end

// Analiza wciśnięcia przycisku
EdgeDetector EdgeDetector_inst( // 10
.Clock(Clock),
.Reset(Reset),
.Signal_i(S), // 11
.RisingEdge_o(ButtonPress_o), // 12
.FallingEdge_o(ButtonRelease_o) // 13
);

assign ButtonState_o = S; // 14

endmodule
`default_nettype wire

Wejścia AsyncA_i, AsyncB_i oraz AsyncS_i należy wyprowadzić poprzez porty modułu top na piny układu FPGA i połączyć je z wyjściami A, B i S enkodera w taki sposób, jak to omawialiśmy we wstępie tego odcinka kursu. Następne cztery wyjścia informują o wykryciu jakiegoś zdarzenia, jak opisano poniżej:

  • Increment_o – inkrementacja, obrót w prawo,
  • Decrement_o – dekrementacja, obrót w lewo,
  • ButtonPress_o – wciśnięcie przycisku,
  • ButtonRelease_o – zwolnienie przycisku.

Jeżeli jakieś z tych zdarzeń zostanie wykryte, to na odpowiadającym mu wyjściu pojawi się stan wysoki przez jeden takt zegarowy.

Wyjście ButtonState_o odzwierciedla aktualny stan przycisku. Stan 0 oznacza przycisk zwolniony, 1 to przycisk wciśnięty.

Moduł zaczynamy od stworzenia synchronizatora dla sygnałów pochodzących z enkodera. Ponieważ mamy do zsynchronizowania trzy sygnały, parametr WIDTH ustawiamy na 3 (linia 2). Sygnały wejściowe sklejamy za pomocą operatora konkatenacji {} – z trzech sygnałów 1-bitowych robimy w ten sposób jeden sygnał 3-bitowy (linia 3). Zwróć uwagę, że AsyncS_i poprzedzony jest wykrzyknikiem. Jest to negacja z tego powodu, że przycisk enkodera działa w logice odwrotnej, czyli w stanie bezczynności przycisk daje 1, a kiedy jest wciśnięty, to otrzymujemy 0. Dla wygody odwrócimy to.

Wyjście z synchronizatora zbudujemy w analogiczny sposób (linia 4). Łączymy je z trzema sygnałami wire, które zostały zadeklarowane w linii 1 i kolejnych. Te sygnały będziemy analizować w dalszej części kodu.

W linii 5 tworzymy rejestr maszyny stanów i poniżej definiujemy możliwe stany. Maszyna stanów opisana jest w następującym bloku always. Opiszemy teraz, co dzieje się w poszczególnych stanach:

  • IDLE (linia 6) – Jest to sytuacja, kiedy pokrętło enkodera nie obraca się lub znajduje się pomiędzy kliknięciami. Wtedy linie A i B są w stanie wysokim. Oczekujemy tak długo, aż jedna z nich przejdzie w stan niski. Na podstawie tego określimy kierunek obrotu. Jeżeli zostanie wykryte zbocze opadające sygnału A, to przechodzimy do stanu WAIT_FOR_LOW_B, a jeżeli zbocze opadające na B, to przechodzimy do WAIT_FOR_LOW_A. Nie jest prawidłowa sytuacja, kiedy oba sygnały A i B zmienią się jednocześnie na 0. Wtedy wchodzimy w stan WAIT_FOR_IDLE, gdzie oczekiwać będziemy, aż oba sygnału znowu będą równe 1.
  • WAIT_FOR_LOW_A (linia 7) i WAIT_FOR_LOW_B (linia 8) – Podczas tych stanów jeden z sygnałów już jest równy 0 i oczekujemy, aż drugi z nich również będzie zerem. Wtedy ustawiamy zmienną Increment_o lub Decrement_o i przechodzimy do WAIT_FOR_IDLE. Natomiast jeżeli zdarzy się sytuacja, że sygnał będący w stanie niskim wróci do stanu wysokiego, to znaczy, że pokrętło enkodera cofnęło się, a więc i my musimy cofnąć się do stanu IDLE.
  • WAIT_FOR_IDLE (linia 9) – Tutaj już tylko przypisujemy zmiennym Increment_o oraz Decrement_o stan wysoki (niezależnie od tego, jak te zmienne były ustawione w poprzednim takcie zegarowym) i czekamy, aż A i B będą równie 1. Wtedy wracamy do stanu IDLE.

Pozostaje już tylko zająć się przyciskiem enkodera. Zastosowujemy omawiany wcześniej w odcinku 6 moduł EdgeDetector (linia 10). Do wejścia badanego sygnału doprowadzamy sygnał S, wychodzący z synchronizatora (linia 11). Wyjścia modułu, odpowiedzialne za wykrywanie zbocza rosnącego i opadającego, łączymy prosto do wyjść modułu Encoder (linie 12 i 13). Wyjście ButtonState_o, informujące o aktualnym stanie przycisku, łączymy z sygnałem S bez żadnych innych operacji (14).

Testbench modułu Encoder

Zanim napiszemy moduł top, musimy przetestować w symulatorze, czy nasze kody działają poprawnie. W tym celu opracujemy testbench pokazany na listingu 3.

Listing 3. Kod pliku encoder_tb.v

// Plik encoder_tb.v

`timescale 1ns/1ns
`default_nettype none
module Encoder_tb();

parameter CLOCK_HZ = 10_000_000;
parameter HALF_PERIOD_NS = 1_000_000_000 / (2 * CLOCK_HZ);

// Generator sygnału zegarowego
reg Clock = 1’b1;
always begin
#HALF_PERIOD_NS;
Clock = !Clock;
end

// Zmienne testowe
reg Reset = 1’b0;
reg AsyncA = 1’b1;
reg AsyncB = 1’b1;
reg AsyncS = 1’b1;

// Instancja testowanego modułu
Encoder DUT( // 1
.Clock(Clock),
.Reset(Reset),
.AsyncA_i(AsyncA),
.AsyncB_i(AsyncB),
.AsyncS_i(AsyncS),
.Increment_o(),
.Decrement_o(),
.ButtonPress_o(),
.ButtonRelease_o(),
.ButtonState_o()
);

// Eksport wyników symulacji do pliku
initial begin
$dumpfile(“encoder.vcd”);
$dumpvars(0, Encoder_tb);
end

// Sekwencja testowa
initial begin
$timeformat(-6, 3, “us”, 12);
$display(“===== START =====”);

@(posedge Clock);
Reset = 1’b1;

// Opóźnienie
#2025; // 2

// Dwa wciśnięcia przycisku
repeat(2) begin // 3
#1000 AsyncS = 1’b0;
#1000 AsyncS = 1’b1;
end

#2000;

// Dwa obroty w prawo (inkrementacja)
repeat(2) begin // 4
#500 AsyncA = 1’b0;
#500 AsyncB = 1’b0;
#500 AsyncA = 1’b1;
#500 AsyncB = 1’b1;
#1000;
end

#2000;

// Dwa obroty w lewo (dekrementacja)
repeat(2) begin // 5
#500 AsyncB = 1’b0;
#500 AsyncA = 1’b0;
#500 AsyncB = 1’b1;
#500 AsyncA = 1’b1;
#1000;
end

#2000;

// Niewłaściwa operacja – zmiana tylko A
repeat(2) begin // 6
#500 AsyncA = 1’b0;
#500 AsyncA = 1’b1;
#1000;
end

// Niewłaściwa operacja – zmiana tylko B
repeat(2) begin // 7
#500 AsyncB = 1’b0;
#500 AsyncB = 1’b1;
#1000;
end

// Niewłaściwa operacja – zmiana razem A i B
repeat(2) begin // 8
#500 AsyncA = 1’b0; AsyncB = 1’b0;
#500 AsyncA = 1’b1; AsyncB = 1’b1;
#1000;
end

$display(“====== END ======”); // 9
$finish;
end

endmodule
`default_nettype wire

Testbench zaczynamy w podobny sposób, jak wszystkie inne testbenche opisywane w tym kursie – tworzymy generator sygnału zegarowego, tworzymy zmienne, którymi będziemy manipulować i dalej tworzymy instancję testowanego modułu (linia 1). Nie ma tu nic nadzwyczajnego, więc przejdźmy od razu do sekwencji testowej.

Symulujemy sygnały pochodzące z enkodera, które są niezsynchronizowane z sygnałem zegarowym. Z tego powodu będziemy wprowadzać dużo opóźnień instrukcją #, jak na przykład w linii 2.

Zacznijmy od dwukrotnego wciśnięcia przycisku enkodera, stosując pętlę repeat(), co pokazano w linii 3 i kolejnych. Efekt symulacji tego kodu widzimy na rysunku 4.

Rysunek 4. Przebiegi uzyskane podczas symulacji dwukrotnego wciśnięcia przycisku

Następnie zasymulujemy obrót o dwa kliknięcia w prawo (linia 4) i w lewo (linia 5). Zmieniamy stan zmiennych AsyncA i AsyncB tak, jak to pokazano na rysunku 2. Efekt symulacji przedstawiono na rysunku 5.

Rysunek 5. Symulacja obrotu enkodera

Dalej w linii 6, 7 i 8 symulujemy kilka sytuacji nieprawidłowych, po czym kończymy symulację. Przebiegi sygnałów zarejestrowane od początku do końca symulacji pokazano na rysunku 6.

Rysunek 6. Całość symulacji

Aby wykonać symulację w symulatorze Icarus Verilog, należy utworzyć plik encoder.bat, którego treść przedstawiono na listingu 4.

Listing 4. Kod pliku encoder.bat

@echo off
iverilog -o encoder.o encoder.v encoder_tb.v synchronizer.v edge_detector.v
vvp encoder.o
del encoder.o

Ten plik należy później uruchomić. W rezultacie powstanie plik wynikowy VCD, który otwieramy w GTKWave. Było to dokładniej opisane w 12 odcinku kursu.

Moduł Top

Skoro wiemy już, że nasz moduł enkodera działa poprawnie w symulatorze, czas przetestować go na prawdziwym FPGA w płytce MachXO2 umieszczonej w płytce rozszerzeń User Interface Board. Opracujemy prostą aplikację, w której będziemy korzystać z dwóch enkoderów do sterowania dwoma licznikami 16-bitowymi. Stan tych liczników będziemy prezentować w formacie szesnastkowym na 8-cyfrowym wyświetlaczu LED za pomocą modułu DisplayMultiplex, który opracowaliśmy w 9 odcinku kursu. Pierwsze cztery cyfry po prawej stronie (świecące się żółtym kolorem, jak widać na fotografii tytułowej) będą odpowiedzialne za jeden z tych liczników, a cztery cyfry po lewej (niebieskie) – za drugi z nich. Czyli obracając pokrętła enkoderów, będziemy zwiększać lub zmniejszać jedną z dwóch liczb pokazanych na wyświetlaczu. Ponadto wykorzystamy sygnał wciśnięcia przycisku enkodera, aby wyświetlić liczbę 1234 (szesnastkowo), a sygnał zwolnienia jego przycisku wyświetli 5678 (też szesnastkowo). Mając wciśniętą gałkę enkodera, będzie można ją także obracać w tym samym czasie, zmieniając liczby na wyświetlaczu. Dodatkowo, jeżeli któryś z enkoderów będzie wciśnięty, to będzie świecić się odpowiadająca mu dioda LED.

Zatem w module top musimy powołać dwie instancje enkoderów i jeden sterownik wyświetlacza. Każda z tych instancji zawierać będzie instancje modułów podrzędnych – aby lepiej zrozumieć, co w czym jest zawarte, przyda się widok hierarchii, który pokazano na rysunku 7.

Rysunek 7. Hierarchia modułów

Zobaczmy teraz kod modułu top, który widać na listingu 5.

Listing 5. Kod pliku top.v

// Plik top.v

`default_nettype none
module top(
input wire Reset, // Pin 17

input wire Encoder1A_i, // Pin 68
input wire Encoder1B_i, // Pin 67
input wire Encoder1S_i, // Pin 66
input wire Encoder2A_i, // Pin 71
input wire Encoder2B_i, // Pin 70
input wire Encoder2S_i, // Pin 69

output wire LED1, // Pin 12
output wire LED2, // Pin 13
output wire [7:0] Cathodes_o,
output wire [7:0] Segments_o
);

// Generator sygnału zegarowego
parameter CLOCK_HZ = 14_000_000;
wire Clock;
OSCH #(
.NOM_FREQ("14.00")
) OSCH_inst(
.STDBY(1’b0),
.OSC(Clock),
.SEDSTDBY()
);

// Encoder 1
wire Increment1; // 1
wire Decrement1;
wire ButtonPress1;
wire ButtonRelease1;

Encoder Encoder1( // 2
.Clock(Clock),
.Reset(Reset),
.AsyncA_i(Encoder1A_i),
.AsyncB_i(Encoder1B_i),
.AsyncS_i(Encoder1S_i),
.Increment_o(Increment1),
.Decrement_o(Decrement1),
.ButtonPress_o(ButtonPress1),
.ButtonRelease_o(ButtonRelease1),
.ButtonState_o(LED1)
);

// Encoder 2
wire Increment2; // 3
wire Decrement2;
wire ButtonPress2;
wire ButtonRelease2;

Encoder Encoder2( // 4
.Clock(Clock),
.Reset(Reset),
.AsyncA_i(Encoder2A_i),
.AsyncB_i(Encoder2B_i),
.AsyncS_i(Encoder2S_i),
.Increment_o(Increment2),
.Decrement_o(Decrement2),
.ButtonPress_o(ButtonPress2),
.ButtonRelease_o(ButtonRelease2),
.ButtonState_o(LED2)
);

// Counter 1
reg [15:0] Counter1; // 5
always @(posedge Clock, negedge Reset) begin
if(!Reset)
Counter1 <= 0;
else if(ButtonPress1) // 6
Counter1 <= 16’h1234;
else if(ButtonRelease1) // 7
Counter1 <= 16’h5678;
else if(Increment1) // 8
Counter1 <= Counter1 + 1’b1;
else if(Decrement1) // 9
Counter1 <= Counter1 – 1’b1;
end

// Counter 2
reg [15:0] Counter2;
always @(posedge Clock, negedge Reset) begin
if(!Reset)
Counter2 <= 0;
else if(ButtonPress2)
Counter2 <= 16’h1234;
else if(ButtonRelease2)
Counter2 <= 16’h5678;
else if(Increment2)
Counter2 <= Counter2 + 1’b1;
else if(Decrement2)
Counter2 <= Counter2 – 1’b1;
end

// Display instance
DisplayMultiplex #( // 10
.CLOCK_HZ(CLOCK_HZ),
.SWITCH_PERIOD_US(1000),
.DIGITS(8)
) DisplayMultiplex_inst(
.Clock(Clock),
.Reset(Reset),
.Data_i({Counter2, Counter1}), // 11
.DecimalPoints_i(8’b00010000), // 12
.Cathodes_o(Cathodes_o),
.Segments_o(Segments_o),
.SwitchCathode_o()
);

endmodule

`default_nettype wire

Na liście portów mamy wejścia A, B i S pochodzące z dwóch enkoderów, które oznaczać będziemy cyframi 1 i 2. Analogicznie, wszystkie zmienne dotyczące tych enkoderów również będą miały cyfrę 1 lub 2. W linii 1 i kilku kolejnych tworzymy kilka zmiennych typu wire, które będą przekazywać sygnały z wyjść modułu Encoder1, którego instancję tworzymy w linii 2. Sygnały te będą sterowały pracą licznika Counter1, który tworzymy w linii 5. W następującym bloku always sprawdzamy stan wszystkich zmiennych z linii 1. Jeżeli ButtonPress1 jest w stanie wysokim, to wpisujemy 1234 do Counter1 (linia 6), a jeżeli w stanie wysokim jest ButtonRelease1, to wpisujemy 5678. Jeżeli żaden z tych warunków nie jest spełniony, to sprawdzamy, czy moduł enkodera wykrył inkrementację, o czym świadczy sygnał Increment1 i jeżeli tak, to stan licznika zwiększamy o 1 (linia 8). W ostateczności sprawdzamy zmienną Decrement1, czy wykryto dekrementrację i w tym przypadku pomniejszamy stan licznika o 1.

W linii 3 i kolejnych tworzymy analogiczny zestaw sygnałów dla drugiego enkodera, którego moduł tworzymy w linii 4. Blok always, odpowiedzialny na operowanie jego licznikiem Counter2, wygląda identycznie jak w przypadku pierwszego, więc nie będziemy ich omawiać drugi raz.

W linii 10 tworzymy instancję modułu DisplayMultiplex, który steruje 8-cyfrowym wyświetlaczem. Do jego wejścia podajemy oba liczniki Counter1 i Counter2 sklejone operatorem konkatenacji {} (linia 11). Dla zwiększenia czytelności zapalimy kropkę pomiędzy tymi dwoma licznikami (linia 12).

Syntezujemy nasz projekt, a następnie otwieramy narzędzie Spreadsheet, gdzie trzeba skonfigurować piny wejściowe i wyjściowe w taki sposób, jak pokazano na rysunku 8.

Rysunek 8. Konfiguracja pinów FPGA w narzędziu Spreadsheet

Pamiętaj, że wejścia enkoderów powinny mieć aktywną dużą histerezę!

Pozostaje już tylko wygenerować bitstream, wgrać go do FPGA i kręcić enkoderami!

W następnym odcinku poznamy bardzo ważne peryferium, jakim jest blok pamięci EBR. Może pracować jako ROM, RAM, FIFO czy dwuportowa pamięć RAM. Ale o tym za miesiąc…

Dominik Bieczyński
leonow32@gmail.com

Repozytorium modułów wykorzystywanych w kursie do pobrania ze strony: https://github.com/leonow32/verilog-fpga
Artykuł ukazał się w
Elektronika Praktyczna
grudzień 2023
DO POBRANIA
Materiały dodatkowe

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik kwiecień 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio maj - czerwiec 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje kwiecień 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna kwiecień 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich maj 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów