Do eksperymentów zastosujemy popularny mikrofon elektretowy. Niestety zmiany napięcia na jego wyjściu są zbyt małe, aby można je było zmierzyć za pomocą naszego przetwornika ADC. Musimy więc zbudować prosty przedwzmacniacz. Użyjemy do tego celu wzmacniacza operacyjnego LM358 i kilku elementów pasywnych. Schemat układu został pokazany na rysunku 1.
Rezystory R3 i R4 tworzą dzielnik napięcia ustalający „sztuczną masę” dla naszego przedwzmacniacza. Kondensator C1 usuwa składową stałą z wejścia wzmacniacza operacyjnego. Zmieniając wartość R5, możemy regulować wzmocnienie. Na wyjściu znajduje się prosty filtr RC złożony z R6 i C2. Jego częstotliwość graniczna to:
Sygnał podłączamy do analogowego wejścia A0 płytki Rysino. Musimy jeszcze zatroszczyć się o niewykorzystaną połowę LM358. Wyjście drugiego wzmacniacza zostało połączone z jego wejściem odwracającym, natomiast wejście nieodwracające zwarto do masy. Realizacja na płytce stykowej jest pokazana na fotografii 1.
Działanie układu najłatwiej sprawdzić za pomocą oscyloskopu. Na rysunku 2 widoczna jest rejestracja mowy. Widać na nim, że sztuczna masa naszego układu odpowiada napięciu około 1,3 V.
Natomiast rejestrowane sygnały znajdują się w przedziale napięciowym 1,15 V…1,45 V. Aby sprawdzić, przy jakich wartościach następuje nasycenie, na rysunku 3 pokazano zapis klaśnięcia. Wynika z niego, że roboczy zakres napięciowy zawiera się pomiędzy wartościami około 0,05 V a 2 V.
Rejestracja dźwięku
Kiedy mamy już gotowy układ doświadczalny, możemy zabrać się do przygotowania wsadu dla układu FPGA. Naszym pierwszym celem będzie wysłanie pomiarów z przetwornika ADC przez port szeregowy. Uproszczony schemat tego modułu pokazuje rysunek 4.
Aby nie komplikować projektu, będziemy przesyłać jedynie 8 bitów danych. Musimy także zdecydować się na częstotliwość próbkowania. Przyjmuje się, że w mowie występują częstotliwości do około 3 kHz. Oznacza to, że wystarczyłoby próbkować z częstotliwością około 6 kHz. Jednak nie możemy wybrać dowolnej wartości. Ich lista jest dostępna w dokumentacji [1].
Kiedy wybierzemy 50 kHz, możemy łatwo otrzymać próbkowanie z częstotliwością 10 kHz poprzez zwykłe odrzucanie czterech i przepuszczanie co piątej wartości. Uzyskamy to przez wygaszanie sygnału valid. Czynność ta spowoduje, że dane mimo że przejdą przez magistralę, nie zostaną wysłane. Wykorzystamy tu dobrze nam znany licznik modulo. Do przesłania dziesięciu tysięcy 8-bitowych pomiarów, po doliczeniu bitów startu i stopu, uzyskamy minimalną prędkość transmisji równą 100 kbd. Możemy więc skonfigurować UART do standardowej prędkości 115,200 kbd, tak jak w poprzednich eksperymentach, i nadal będziemy mieli pewien zapas.
Budowę rozpoczniemy od wygenerowania bloku IP, obsługującego przetwornik analogowo-cyfrowy. Konfigurację pokazuje rysunek 5. Ustawiamy tam wybraną przez nas częstotliwość próbkowania. Zegar taktujący ADC ustawiamy na 2 MHz (będziemy musieli także odpowiednio skonfigurować pętlę PLL). Tym razem włączamy tylko kanał numer 1.
30 adc adc_inst (
31 .clock_clk(clk),
32 .reset_sink_reset_n(rst),
33 .adc_pll_clock_clk(clk_adc),
34 .adc_pll_locked_export(clk_adc_locked),
35 .command_valid(1’b1),
36 .command_channel(1’b1),
37 .command_startofpacket(1’b0),
38 .command_endofpacket(1’b0),
39 .command_ready(),
40 .response_valid(adc_valid_out),
41 .response_channel(),
42 .response_data(adc_data_out),
43 .response_startofpacket(),
44 .response_endofpacket());
50 assign bus.data = adc_data_out[11-:8];
51
52 counter #(.N(5)) every5 (
53 .clk(clk),
54 .rst(rst),
55 .ce(adc_valid_out),
56 .q(),
57 .ov(ce_5));
58
59 assign bus.valid = ce_5 & adc_valid_out;
60
61 uart_tx #(
62 .F(F),
63 .BAUD(115200)
64 ) uart (
65 .bus(bus),
66 .tx(tx));
Główny moduł naszego projektu pokazuje listing 1. W liniach 30…44 mamy instancję modułu ADC. W tym przypadku konfiguracja magistrali wejściowej jest bardzo prosta, ponieważ chcemy czytać tylko jeden kanał z maksymalną częstotliwością. Wpisujemy numer kanału w sygnale command_channel i ustawiamy command_valid na 1. W linii 50 ustawiamy wysyłanie tylko ośmiu najstarszych bitów. Dalej znajduje się moduł licznika, który zlicza kolejne pomiary z przetwornika, a dla co piątej próbki następuje ustawienie sygnału ce_5. Linia 59 przygotowuje sygnał wyzwalający transmisję. Musimy zastosować bramkę AND, ponieważ sygnał przepełnienia licznika pozostanie aktywny, aż zakończy się następny pomiar. Mogłoby to spowodować kilkukrotne wysłanie pojedynczej próbki. Teoretycznie nie powinno tak się zdarzyć, ponieważ wysłanie jednego bajtu zajmuje więcej czasu niż wynosi odstęp pomiędzy pomiarami przetwornika. Na samym końcu umieszczony został moduł nadajnika portu szeregowego.
Kiedy mamy już gotowy kod, możemy otworzyć w środowisku Quartus projekt 10_record/10_rec.qpf i rozpocząć jego budowę. Po wgraniu bitstreamu do układu FPGA przechodzimy do odczytywania danych. W tym celu można wykorzystać znany nam już program RealTerm. Aby wyświetlane dane były bardziej czytelne, możemy w zakładce Display wybrać opcję uint8 (unsigned int 8 – 8-bitowa liczba całkowita bez znaku), co pokazuje rysunek 6.
Kiedy powiemy coś do mikrofonu, dane powinny się zmieniać, ale pojawiają się zbyt szybko, by zaobserwować to wyraźnie na ekranie konsoli. Dlatego zapiszemy odbierane informacje w pliku tekstowym do późniejszej analizy. W tym celu otwieramy zakładkę Capture (przechwytywanie), co zaprezentowano na rysunku 7.
Następnie wybieramy plik docelowy i zaznaczamy opcję Capture as Hex (przechwytuj w formacie szesnastkowym). Rozpoczynamy naciśnięciem Start: Overwrite (zacznij: nadpisz), mówimy do mikrofonu i klikamy przycisk Stop Capture (zatrzymaj przechwytywanie).
Wykonane przeze mnie przykładowe nagranie znajduje się w pliku 10_record/parse/voice.txt. Kiedy go otworzymy, zobaczymy jedną, długą linię bez żadnych przerw:
656565656565
Każde dwa kolejne znaki odpowiadają kolejnym odebranym bajtom. Są one zapisane w formacie szesnastkowym. Aby je zdekodować oraz zmienić na format dźwiękowy, przygotujemy krótki skrypt w języku Python. Do jego uruchomienia wykorzystam notatnik Jupyter. Można go pobrać razem z pakietem Anaconda [2]. Domyślnie notatnik jako swój główny katalog uznaje folder użytkownika. Jeżeli przechowujemy pliki projektu w innym miejscu, można uzyskać do nich dostęp poprzez stworzenie dowiązania do wybranego katalogu, na przykład wywołaniem w wierszu poleceń komendy:
mklink /D c c:\
Powyższe działanie spowoduje stworzenie dowiązania c, które wskazuje na dysk C:\. Parametr /D informuje, że mamy do czynienia z katalogiem. Teraz możemy załadować notatnik 10_record/parse/Parse.ipynb. Zawarty w nim kod pokazuje listing 2.
01 %matplotlib inline
02
03 import matplotlib
04 import numpy as np.
05 import matplotlib.pyplot as plt
06 from scipy.io.wavfile import write
07
08 def pairwise(iterable):
09 "s -> (s0, s1), (s2, s3), (s4, s5), …"
10 a = iter(iterable)
11 return zip(a, a)
12
13 file="voice.txt"
14 with open(file) as f:
15 data = f.read()
16 x = []
17 for a,b in pairwise(data):
18 s = a + b
19 x.append(int(s, 16))
20 xn = np.uint8(x)
21
22 plt.plot(xn)
23 plt.xlabel("czas, próbki")
24 plt.ylabel("amplituda, j.w.")
25
26 write(‘voice.wav’, 10000, xn)
W pierwszej linii konfigurujemy wyświetlanie wykresów. Następnie ładujemy niezbędne biblioteki. Matplotlib umożliwi nam tworzenie wykresów, a Numpy pozwala na wykonywanie obliczeń numerycznych. Scipy jest rozbudowaną biblioteką ułatwiającą obliczenia naukowe. Wykorzystamy z niej jedynie moduł, pozwalający zapisywać pliki w dźwiękowym formacie wav. W liniach 8…11 znajdziemy funkcję pairwise, która ułatwi nam wczytywanie dwóch kolejnych znaków.
Wiersz 13 zawiera nazwę pliku z zapisanymi danymi. Otwieramy go i wczytujemy zawartość do zmiennej data. Następnie w pętli wczytujemy po dwa znaki i konwertujemy je na liczbę. W linii 20 konwertujemy otrzymaną tablicę na wektor liczb 8-bitowych bez znaku. Na końcu wyrysowujemy wykres z zebranych danych i zapisujemy plik dźwiękowy voice.wav.
Uzyskany wykres pokazuje rysunek 8. Sygnał oscyluje wokół liczby 100. Jego wartości mieszczą się w przedziale 80…115. Oznacza to, że większość z dostępnego nam zakresu (0…255) jest niewykorzystana. Używamy jedynie przedziału o szerokości poniżej 35. Odpowiada on nieco ponad pięciu bitom danych, przy czym przesyłamy ich aż osiem. W dalszej części spróbujemy poprawić ten wynik.
Sposób zbierania danych oraz konwersji ich do pliku tekstowego został także przedstawiony na filmie [3].