Celem ćwiczenia z dzisiejszego odcinka kursu będzie opracowanie generatora DDS, zdolnego do wytworzenia sygnału o dowolnym kształcie, z regulowaną częstotliwością i amplitudą sygnału. Zanim zaczniemy analizować kody w języku Verilog, musimy zapoznać się z teorią funkcjonowania układów DDS. Schemat najprostszego takiego rozwiązania pokazano na rysunku 1.
Składa się on z zaledwie trzech podzespołów: licznika i pamięci ROM – które można umieścić wewnątrz struktury FPGA – oraz przetwornika cyfrowo-analogowego (DAC), umieszczanego najczęściej poza FPGA (układy FPGA, w przeciwieństwie do mikrokontrolerów, na ogół nie mają wbudowanych żadnych peryferiów analogowych).
Przetwornik cyfrowo-analogowy
Zacznijmy od końca, czyli od przetwornika cyfrowo-analogowego. Jest to układ, który przetwarza wielobitowy sygnał cyfrowy na sygnał analogowy, czyli najczęściej napięcie elektryczne. Istnieje wiele różnych metod takiej konwersji, lecz skupimy się tylko na przetworniku R-2R, ponieważ taki znajduje się na płytce User Interface Board (została ona zaprezentowana w EP 09/2023).
Schemat opisanego przetwornika pokazano na rysunku 2.
Jest on bardzo prosty: składa się z drabinki rezystorów „poziomych” i „pionowych”. Elementy poziome podłączone zostają do wyjść poszczególnych bitów sygnału cyfrowego i mają dwa razy większą rezystancję niż rezystory pionowe. Wyjątek stanowi ostatni element pionowy, który ma dwukrotnie większą rezystancję od pozostałych oporników o tej samej „orientacji”. Na płytce User Interface Board zastosowano rezystory 1 kΩ i 2 kΩ. Napięcie wyjściowe z drabinki rezystorowej przechodzi następnie poprzez wzmacniacz operacyjny, pracujący jako wtórnik napięciowy, aby zwiększyć obciążalność prądową przetwornika. Ważne jest, by zastosować wzmacniacz typu rail-to-rail (czyli aby napięcie na jego wejściu i wyjściu mogło zmieniać się w całym zakresie od masy do napięcia zasilającego) – popularny LM358 nie nadaje się do tego zastosowania.
Jak działa taki przetwornik? Jest on sterowany wyjściami cyfrowymi typu push-pull. To znaczy, że wyjście w stanie niskim de facto oznacza połączenie z masą, a stan wysoki – połączenie z szyną zasilającą. Zatem rezystory poziome z jednej strony połączone są z łańcuszkiem innych rezystorów, a z drugiej – połączone są z masą lub zasilaniem. W taki sposób tworzy się dość zagmatwany dzielnik napięcia, jednak obliczenie napięcia wyjściowego pozostaje bardzo proste. Wystarczy zastosować wzór:
gdzie
- VCC – napięcie zasilające,
- INPUT – wartość cyfrowa na wejściu przetwornika,
- BITS – liczba bitów przetwornika.
W naszym przypadku mamy przetwornik 8-bitowy. Maksymalna liczba, jaką jesteśmy w stanie zapisać na 8 bitach, to 255, a mianownik tego ułamka wynosić będzie 256. Przykładowo: jeżeli napięcie zasilania wynosi 3,3 V, wówczas maksymalne napięcie, jakie możemy uzyskać z 8-bitowego przetwornika typu R-2R, wynosić będzie 3,3∙255/256=3,287 V.
Zaletą drabinki R-2R jest prostota, a także czas reakcji. Jedyne opóźnienie wynika tylko z czasu reakcji wzmacniacza operacyjnego oraz pojemności pasożytniczej elementów i ścieżek. Natomiast wadę stanowi jego dokładność – wynika ona z tolerancji rezystorów. Nie ma możliwości, aby zastosować jakieś precyzyjne źródło napięcia odniesienia. Tutaj odniesieniem jest napięcie zasilania, które bywa mocno zaszumione, a to negatywnie wpływa na wynik konwersji.
Pamięć z próbkami
Skoro mamy już przetwornik cyfrowo-analogowy, musimy skądś wziąć dane cyfrowe, które będzie on przetwarzał. W przypadku sygnału sinusoidalnego najczęściej stosowane są dwa rozwiązania:
- CORDIC – algorytm umożliwiający wykonanie różnych funkcji matematycznych, w tym trygonometrycznych. Algorytm ten może być zaimplementowany w sprzęcie, przez co znajduje zastosowanie w układach FPGA, a także w różnych procesorach jako akcelerator obliczeń.
- Lookup table – próbki sygnału zapisane są w pamięci ROM. Wystarczy co pewien czas odczytywać kolejne próbki z pamięci i przekazywać je do przetwornika DAC.
Podczas realizacji zadań z kursu użyjemy metody z pamięcią. Gdybyś chciał poeksperymentować z algorytmem CORDIC, znajdziesz gotowe moduły w bibliotece IP Express (która została omówiona w 5 odcinku kursu, opublikowanym w EP 03/2023).
Bloki pamięci EBR już potrafimy zastosować. Poznaliśmy je w 15 odcinku kursu (EP 01/2024) i opracowaliśmy wówczas moduł, który można konfigurować w bardzo szerokim zakresie. Teraz musimy tylko ustalić, jak ten moduł skonfigurować i skąd wziąć zawartość pamięci.
Załóżmy, że na próbki sygnału chcemy przeznaczyć jeden blok pamięci EBR. Wyjście pamięci powinno być 8-bitowe, ponieważ tyle bitów ma przetwornik R-2R na płytce User Interface Board. Jeden blok EBR z wyjściem 8-bitowym może pomieścić 1024 słowa, czyli w tym przypadku 1024 bajty. Tyle można zaadresować za pomocą wejścia adresowego o szerokości 10 bitów.
Potrzebujemy sposobu na wygenerowanie pliku z wsadem do pamięci, który zostanie zaimportowany za pomocą instrukcji $readmemh(). Plik ma zawierać 1024 8-bitowych wpisów, oddzielonych spacjami lub enterami. Musimy zatem wygenerować tyle wartości funkcji sinus, lecz powinniśmy trochę je poprzekształcać. Interesuje nas fragment funkcji od 0 do 2π, czyli cały jeden okres sinusa. Nasze zadanie polega na przeskalowaniu tego fragmentu na zakres od 0 do 1023, bo takie mamy adresy w pamięci. Ponadto funkcja sinus zwraca wartości od –1 do +1, a nam potrzebne są wartości od 0 do 255, ponieważ wyniki chcemy zapisać w zmiennych 8-bitowych (koniecznie używając do tego celu formatu szesnastkowego).
Plik z wsadem do pamięci możemy wygenerować w dowolny sposób. Ja do tego celu użyłem Excela. Arkusz obliczeniowy oraz gotowy plik z wsadem do pamięci znajdziesz w materiałach dołączonych do niniejszego odcinka.