Eksperymenty z FPGA (10). Podłączamy mikrofon

Eksperymenty z FPGA (10). Podłączamy mikrofon
Pobierz PDF Download icon

W tym odcinku wracamy do przetwornika ADC. Jednak zamiast potencjometru podłączymy do niego mikrofon. Dzięki temu wykonamy eksperymenty z cyfrowego przetwarzania sygnałów. Przed przystąpieniem do pracy jak zwykle przypominam o aktualizacji repozytorium z przykładami (poprzez wywołanie polecenia git pull).

Spis treści

Nasycenie

Wiemy już w jaki sposób usunąć składową stałą. Nadal jednak musimy zmniejszyć liczbę bitów. Dwie najbardziej popularne opcje to obcięcie albo nasycenie. Ich porównanie widzimy na rysunku 14, wygenerowane w programie ModelSim za pomocą komendy:

do truncation_saturation.do

Rysunek 14. Porównanie obcięcia i nasycenia

Kod do generowania pokazuje listing 6. Na górnym wykresie widać zawartość 8-bitowego rejestru data. Linia 21 wskazuje, że zawiera on sinusoidę o amplitudzie 80. Kolejne dwa wykresy zawierają tę samą funkcję, ale przyciętą do długości siedmiu bitów. Możliwa jest prezentacja sygnału z zakresu –64…63. Następuje więc strata części informacji.

Listing 6. Generowanie wykresów z rysunku 15 (11_dc_r/truncation_saturation.sv)
12 module truncation_saturation;
13 parameter N = 8;
14 logic signed [N-1:0]data;
15 logic signed [N-2:0]data_t;
16 logic signed [N-2:0]data_s;
17
18 initial begin
19 for (int i = 0; i < 100; i++) begin
20 #1;
21 data = 80*$sin(2*pi*i/100);
22 data_t = data[N-2:0];
23 if (data > 2**(N-2)-1)
24 data_s = 2**(N-2)-1;
25 else if (data < -(2**(N-2)))
26 data_s = -2**(N-2);
27 else
28 data_s = data[N-2:0];
29 end
30 $stop;
31 end
32 endmodule

Zmienna data_t pokazuje co się stanie, gdy odetniemy najstarszy bit. Kiedy przekraczamy maksymalną wartość, bit znaku, który wcześniej był równy zero, zostaje nadpisany jedynką, pochodzącą z danych, co powoduje zmianę znaku. W przypadku przetwarzania sygnałów dźwięku jest to zjawisko bardzo niekorzystne. Sygnał opisany jako data_s prezentuje sytuację, gdy zastosujemy nasycenie. Uzyskany przebieg jest bliższy wartościom wejściowym. Przed dokonaniem obcięcia sprawdzamy, czy nastąpiło przepełnienie. Jeżeli tak, to zamiast wejściowych danych na wyjście podajemy minimalną albo maksymalną możliwą wartość – w zależności od znaku odebranych danych.

Rysunek 15. Realizacja saturacji w układzie FPGA

Przykładową realizację modułu pokazuje rysunek 15. Mamy w nim dwa bloki kombinacyjne. Pierwszy z nich, nazwany overflow, wykrywa, czy przy obcięciu wystąpi przepełnienie. Na jego podstawie zewnętrzny multiplekser ustala, czy przekazujemy na wyjście wejściowe dane, czy którąś ze stałych. Wyboru stałej dokonuje pierwszy multiplekser na podstawie znaku wejściowych danych. Jest on zwracany przez blok oznaczony jako sign. Na końcu wynik jest zatrzaskiwany w rejestrze. Jak zwykle sygnał valid płynie razem z danymi przez drugi, tym razem resetowany przerzutnik.

Listing 7. Moduł realizujący skalowanie z nasyceniem(11_dc_r/saturation.sv)
10 module saturation #(
11 parameter N_IN = 10,
12 parameter N_OUT = 8
13 ) (
14 StreamBus in,
15 StreamBus out
16 );
17 logic overflow, sign;
18 logic [N_IN-1:N_OUT-1]truncated;
19 logic [N_OUT-1:0]data_out;
20
21 always_comb begin
22 sign = in.data[N_IN-1];
23 truncated = in.data[N_IN-1:N_OUT-1];
24 overflow = &truncated != |truncated;
25 if (overflow)
26 data_out = (sign) ? 2**(N_OUT-1) : 2**(N_OUT-1)-1;
27 else
28 data_out = in.data[N_OUT-1:0];
29 end
30
31 always_ff @(posedge in.clk)
32 out.data <= data_out;
33
34 always_ff @(posedge in.clk or negedge in.rst)
35 if (!in.rst)
36 out.valid <= ‘0;
37 else
38 out.valid <= in.valid;
39
40 endmodule

Przejdźmy teraz do implementacji, którą pokazuje listing 7. Moduł saturation przyjmuje dwa parametry: N_IN i N_OUT, określające szerokość wejścia i wyjścia. Dalej, podobnie jak w module dc_r, mamy zdefiniowane interfejs wchodzący i wychodzący z modułu. Główna część logiki znajduje się w bloku always_comb. W linii 22 wykrywamy znak wejściowych danych. Jest to bardzo prosta operacja: wystarczy wyciągnąć najstarszy bit.

Następnie przygotowujemy wektor zawierający wszystkie bity, które zostaną obcięte i jeden (najstarszy), który zostanie nowym bitem znaku.

Wektor ten został nazwany truncated. W kolejnym wierszu wykrywamy, czy nastąpiło przepełnienie. Korzystamy tutaj z prostej zależności. Jeśli nie nastąpiło przepełnienie, wtedy wszystkie obcięte bity (oraz nowy bit znaku) mają tę samą wartość. Jest to równoważne stwierdzeniu, że logiczna operacja i wykonana na tym wektorze dadzą taki sam wynik jak logiczne OR. Następnie w liniach 25…29 zrealizowane są oba multipleksery z rysunku 16. Pierwszy za pomocą operatora ?:, a drugi z wykorzystaniem instrukcji if/else.

Zostało nam już tylko zaimplementowanie dwóch przerzutników, zatrzaskujących obliczone wyjście. Pierwszy z nich (31…32) obsługuje dane, a drugi (34…38), wyposażony w reset, dba o kontrolny sygnał valid.

Listing 8. Testbench dla modułu stauration (11_dc_r/saturation_tb.sv)
12 module saturation_tb;
13 parameter N_IN = 10;
14 parameter N_OUT = 8;
15
16 logic clk, rst;
17 StreamBus #(.N(N_IN)) in (.clk(clk), .rst(rst));
18 StreamBus #(.N(N_OUT)) out (.clk(clk), .rst(rst));
19
30 initial begin
31 in.valid = 1’b1;
32 for (int i = -2**(N_IN-1); i < 2**(N_IN-1); i++) begin
33 @(posedge clk);
34 in.data = i;
35 end
36 $stop;
37 end
38
39 saturation dut (
40 .in(in),
41 .out(out)
42 );
43 endmodule

Testbench dla naszego modułu znajduje się na listingu 8. Na początku definiujemy dwa parametry określające rozmiar wejścia i wyjścia. Zostały one później wykorzystane przy tworzeniu interfejsów. Generowanie sygnałów zegarowego i resetu jest identyczne jak w poprzednim testbenchu, dlatego zostały pominięte. Zmiana następuje przy generowaniu danych. Za pomocą pętli for (linia 32) generujemy funkcje liniową, która przechodzi przez wszystkie dopuszczalne wartości wejścia. Dla 10 bitów rozpoczniemy więc od –512 i dojdziemy do wartości 511, po czym zakończymy symulację.

Teraz w programie ModelSim możemy wywołać polecenie:

do saturation_sim.do

Rysunek 16. Symulacja modułu saturation

Pojawią się przebiegi podobne do tych z rysunku 16. W pierwszych trzech wierszach znajdują się sygnały: zegarowy, resetu oraz valid. Następnie znajdziemy dane wejściowe przedstawione w formie wykresu. Pod nim są wewnętrzne sygnały modułu. Linia sign (znak) zmienia się z 1 na 0, gdy dane wejściowe staną się nieujemne. Dwa kolejne wiersze to wektor truncated oraz sygnał overflow. Widzimy, że dla bardzo małych i bardzo dużych wejść bity wektora truncated mają różne wartości, co powoduje pojawienie się jedynki logicznej w sygnale overflow. Pod nim znajdziemy dane wyjściowe. Dla mocno ujemnych wymuszeń otrzymujemy na wyjściu stałą wartość ujemną. Następnie, gdy pojawi się sygnał, który mieści się w obsługiwanym przedziale, zobaczymy funkcję liniową, która narasta, aż do osiągnięcia maksymalnej wartości dodatniej, gdzie znowu ulega nasyceniu.

Artykuł ukazał się w
Elektronika Praktyczna
wrzesień 2020
DO POBRANIA
Pobierz PDF Download icon

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik październik 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 październik 2021

Automatyka Podzespoły Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna październik 2021

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich październik 2021

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów