Przyszedł czas aby zacząć temat o nowym projekcie, tj. filtr cyfrowy typu FIR (filtr o skończonej odpowiedzi impulsowej, z ang. Finite Impulse Response) zaimplementowany w FPGA :)
W sumie nie wiem od czego tu zacząć, ponieważ matematyka stojąca za cyfrowym filtrowaniem i ditheringiem nie będzie rozwijana, ale jednak trzeba coś wspomnieć aby zrozumieć działanie filtrowania częstotliwości w domenie cyfrowej. Zacznijmy jednak od tego, że zadaniem takiego filtru jest dokładnie takie same jak zwrotnicy w kolumnie głośnikowej, tj. filtrowanie danych częstotliwości i przepuszczanie innych
Różnica jest jednak taka, że taki filtr pracuje w domenie cyfrowej oraz ma zazwyczaj wysoki rząd, tzw. dużą ilość tapsów bądź współczynników. Taki filtr może być każdego typu, tj. dolnoprzepustowy, górnoprzepustowy, środkowyprzepustowy czy jakikolwiek inny typ, tj. zależy to tylko i wyłącznie od użytych współczynników. W tym wypadku będę skupiać się na filtrze dolnoprzepustowym, ponieważ takim typu filtrem jest ten projekt.
Pozwolę sobie zapożyczyć grafikę filtru typu FIR aby wyjasnić mniej więcej na jakiej zasadzie działa taki filtr:

Mamy wejście filtru, które w naszym wypadku jest wejściem strumienia danych audio. Człon Z -1 to po prostu próbka audio opóźniona o jeden, tj. każda poprzednia próbka przed tą nowszą (od najnowszej do najstarszej). Człony Bx to współczynniki filtru, które mnożone są poprzez wcześniejsze próbki ze strumienia audio. Wszystkie wyniki tych mnożeń są potem dodawane do siebie i tak o to otrzymujemy próbkę na wyjściu układu
Najprostszym przykładem filtru FIR jest po prostu liczenie średniej wartości ze wszystkich poprzednich próbek. Zobaczmy na powyższy obrazek i załóżmy, że wszystkie trzy człony Bx mają taką samą wartość, tj. 1/3. Zakładając, że na wejściu układu mamy próbki o wartościach 1, 2 i 3 to matematyka stojąca za filtrem wygląda następująco: 1/3 * 1 + 1/3 * 2 + 1/3 * 3 = 2
Inaczej pisząc w przypadku takich współczynników liczymy tylko średnią na bazie N próbek wejściowych. Inną nazwą takiego filtru to średnia ruchoma, tj. z ang. moving-average (https://pl.wikipedia.org/wiki/Średnia_ruchoma). Ciekawe jest to, że taki filtr już kiedyś stworzyłem, ale nie sam filtr i nie cyfrowo, ale wbudowany w przetwornik D/A, tj. temat z "NOS" DAC:
https://diyaudio.pl/showthread.php/2...-dobrze-zrobić
Przykładowy FIR oraz powyższy przetwornik D/A działają dokładnie tak samo, ale jeden działa w domenie cyfrowej a drugi w analogowej
Taki filtr jest tzw. ekstrapolatorem rzędu pierwszego. Niestety jego zdolność filtrowania i rekonstrukcji sygnału pozostawia wiele do życzenia, więc jest to generalnie tylko ciekawostka, ale ma też swoje plusy (szczególnie przy strumieniu DSD). Myślę, że warto tutaj wspomnieć też o tym, że ekstrapolatorem rzędu zerowego jest zwyczajny przetwornik R-2R w formie NOS
Poniższy obrazek przedstawia różnicę między ekstrapolatorem rzędu zerowego (NOS) a ekstrapolatorem rzędu pierwszego (liniowa interpolacja) oraz ich odpowiedzi impulsowe wraz z charakterystyką częstotliwościową:

Naprawdę nie ma co się rozpisywać, ponieważ pisanie o filtrach FIR może i być ciekawe, ale zbędne i czasochłonne. Temat dotyczy samego projektu takiego filtru a nie tego jaka matematyka za tym wszystkim stoi, więc postaram się jeszcze po szybkości wspomnieć o interpolacji, filtrowaniu sin(x) / x i ditheringu oraz przejść do samego projektu filtru na FPGA.
W nazwie temat wspomniałem, że zaprojektowany filtr cyfrowy jest typu interpolacyjnego, tj. takiego, który zwiększa oryginalną częstotliwość próbkowania, przykładowo 44.1 kHz, do zadanej częstotliwości, np. w moim wypadku jest to 705.6 kHz. Z prostej matematyki można wywnioskować, że filtr interpoluje 16-krotnie (44.1 kHz * 16 = 705.6 kHz). Zacznijmy jednak od tego co to jest interpolacja. Interpolacja jest to proces wyznaczenia pewnej funkcji, która przyjmuje takie same wartości w zadanej przedziale w którym są już wyznaczone pewne punkty. Sygnał audio zapisany jest jako wartość amplitudy (punkt na osi Y) w pewnych odstępach czasowych (oś X, np. 44.1 kHz). Inaczej pisząc zadaniem interpolacji jest po prostu wyznaczenie dodatkowych punktów pomiędzy istniejącymi już punktami. Taki zabieg najlepiej przedstawić na obrazku, więc zobaczmy poniżej:

Czerwony to sygnał taki jaki mamy zapisany w pliku i zrekonstruowany na przetworniku R-2R w formie NOS (ekstrapolator rzędu zerowego). Czarny to ten sam sygnał, ale interpolowany do nieskończenie wyższej częstotliwości w celu odtworzenia większej ilości punktów oryginalnego zapisu. Inaczej pisząc taki zabieg w praktyce to po prostu rekonstrukcja sygnału wraz z filtrowaniem dolnoprzepustowym. Interpolacja w formie cyfrowej to dwa zabiegi, tj. wrzucenie próbek o zerowej amplitudzie do sygnału tak aby zwiększyć częstotliwość oraz filtrowanie dolnoprzepustowe w celu pozbycia się odbić sygnału, które powstały z powodu zwiększenia próbkowania. Na poniższych obrazkach można zobaczyć jak to w praktyce wygląda:


W pierwszym obrazku na samej górze posiadamy sygnał oryginalny, dyskretny (czarne punkty) oraz ciągły (szara linia). Sygnału w formie ciągłej nie da się zapisać, ponieważ wymagałoby to nieskończonej ilości miejsca, więc w praktyce zapisujemy tylko sygnał w formie dyskretnej (próbkowany w równych odstępach czasu, np. 44.1 kHz). W celu odtworzenia oryginalnego sygnału musimy zwiększyć ilość próbek, które pozwolą nam zamienić sygnały dyskretny na sygnał ciągły (analogowy). W tym celu wrzucamy próbki o wartości 0 pomiędzy już istniejące próbki (drugi wykres). Taki zabieg pozwolił nam faktycznie zwiększyć próbkowania, ale niestety wprowadził też odbicia sygnału. W praktyce podczas próby odtworzenia takiego sygnału otrzymujemy sygnał powielony względem "lustra", które tworzy się na wielokrotności oryginalnej częstotliwości próbkowania (44.1 kHz). Ten problem można zobaczyć na drugim obrazku po prawej stronie gdzie widnieje wykres w domenie częstotliwościowej. W celu eliminacji tych odbić stosuje się filtrowanie dolnoprzepustowe, które w praktyce przesuwa odbicia na wyższą częstotliwość gdzie można już spokojnie filtrować filtrem analogowym z niskim rzędem. Inaczej pisząc czym większa interpolacja tym dalej przesuwane są odbicia oryginalnego sygnału.
No i w sumie to tyle wystarczy na temat samej interpolacji, więc na szybkości przejdźmy jeszcze do filtrowania dolnoprzepustowego funkcją sin(x) / x. W przetwarzaniu sygnału używa się tylko funkcji sin(x) / x do filtrowania dolnoprzepustowego, ponieważ z matematycznego punktu widzenia jest ona idealnym filtrem dolnoprzepustowym (z ang. brick-wall). Wynika to z faktu, że odpowiedź częstotliwościowa funkcji sin(x) / x jest funkcją prostokątną:

Oznacza to w praktyce tyle, że przepuszcza idealnie zadane częstotliwości a wszystkie poza wyznaczoną odpowiedzią częstotliwościową po prostu usuwa. Normalnie idealny filtr, ale nie ma tak łatwo
Niestety funkcja sin (x) / x jest nieskończona, więc nie da się jej przedstawić w formie praktycznego filtru cyfrowego. W tym celu używa się metody tzw. okna czasowego (z ang. windowing), który pozwala nam "wygasić" funkcję po pewnym okresie, która w praktyce jest nieskończona. Przykładowo taką funkcją okna czasowego może być okno Kaisera (ja takiego użyłem do wyznaczenia współczynników mojego filtru sin (x) / x). Okno Kaisera wygląda jak na poniższym obrazku:

Teraz zobaczmy sobie co takie okno czasowe może zdziałać z nieskończona funkcją sin (x) / x:

Czerwony wykres - okno czasowe.
Zielony wykres - nieskończona funkcja sin(x) / x.
Niebieski wykres - funkcja sin(x) / x po zastosowaniu okna czasowego.
Taki zabieg w praktyce pozwala zastosować funkcję sin(x) / x do filtrowania
Oczywiście wprowadza to pewne błędy filtrowania i taki filtr nigdy nie będzie idealny jak sama funkcja sin(x) / x, ale zwiększając ilość współczynników (dyskretnie zapisanego sygnału sin(x) / x po oknie czasowym) można dążyć do tej idealnej odpowiedzi częstotliwościowej. Niestety trzeba też pamiętać o tym, że w praktyce mamy ograniczoną ilość pamięci w której możemy zapisać współczynniki, więc wprowadza to dodatkowe błędy kwantyzacji (cyt. "nieodwracalne nieliniowe odwzorowanie statyczne zmniejszające dokładność danych przez ograniczenie ich zbioru wartości").
No i można zacząć filtrować! No prawie, ponieważ jako, że pracujemy na dyskretnych sygnałach a nie na ciągłych i do tego jeszcze mamy do czynienia z błędami kwantyzacji to jeszcze dosyć ważną rzeczą przy takim projekcie jest tzw. dithering podczas redukcji bitów wyjściowego słowa. Dlaczego redukcji? W praktyce nasz przetwornik D/A operuje przykładowo na słowach audio o długości 18 bitów. Zakładając, że nasze słowo wejściowe to 24 bitowy sygnał audio oraz sam fakt, że te słowo przechodzi filtrowanie w którym występuje mnożenie, powoduje, że zależnie od ilości bitów współczynników filtru, które mogą przykładowo mieć 32 bity, otrzymujemy wynik na poziomie 56 bitów. No dobra, ale mnożymy 24 bitową liczbę przez 32 bitową liczbę, więc jakim cudem mamy wynik na 56 bitach? Taka po prostu jest matematyka
Mnożąc dwie liczby X oraz Y o długości bitów odpowiednio 24 oraz 32 wynik tego zabiegu wymaga 56 bitów do zapisu. Zresztą, nawet jakbyśmy nie musieli nic mnożyć to i tak musimy zredukować 24 bity do 18 bitów, które przyjmuje przetwornik. Taki zabieg jest wymagany, ponieważ błędy kwantyzacji sygnału wprowadzane przez redukcję bitów (np. ucięcie ostatnich bitów) są wprowadzane w sam sygnał audio. Dither to najprostsza metoda przeniesienia energii tych błędów kwantyzacji na losowy szum w sygnale audio. Inaczej pisząc specjalnie dodajemy szum do sygnału audio
Najlepiej będzie to pokazać na przykładzie:
Bez ditheringu:

Z ditheringiem:

Na przykładzie tego filtra zostało pokazane jak dither może pomóc przy odwzorowaniu sygnału -96 dBFS na 18 bitowym przetworniku :) Na górze też jest średnia z 10 pomiarów, więc koniec końców różnica w szumie jest żadna :)
Dithering wymaga generatora liczb pseudolosowych, ponieważ chcemy aby nasz szum był po prostu losowy. No prawie, ponieważ nie potrzebujemy losowości w sensie nieprzewidywalności samych generowanych liczb, ale ciągu generowanych wartości, które mają rozkład jednostajnie ciągły, tj. taki dla którego wystąpienie danej liczby z danego przedziału jest tak samo prawdopodobne jak dla każdej innej z tego samego przedziału. Pewną własnością rozkładu jednostajnie ciągłego jest to, że dodanie dwóch takich losowych liczb zamienia rozkład na trójkątny, czyli taki, którego gęstość prawdopodobieństwa rozkłada się w formę trójkątną gdzie jego wierzchołek to liczby, które mają największą szansę na trafienie. Na poniższym obrazku X oraz Y to rozkład jednostajnie ciągły a Z to trójkątny:

Rozkład trójkątny jest świetny do ditheringu audio, ponieważ zazwyczaj mając jedynkę chcemy aby dalej to była jedynka, tj. z dużym prawdopodobieństwem, ale aby nie było to równomierne. Inaczej pisząc ditheringiem można nazwać operację posiadania liczby X oraz losowania liczby Y chcąc trafić tak, aby X ~ Y, ale jednak nie chcemy znanej liczby X, ale nowej liczby Y, która ma duże prawdopodobieństwo trafić blisko X
Źródłem takiej pseudo losowości do wygenerowania rozkładu trójkątnego w FPGA może być rejestr przesuwający z liniowym sprzężeniem zwrotnym, który także został zaimplementowany w tym projekcie. Zaimplementowany rejestr ma okres 2^47 i pracuje z zegarem MCLK, który może tykać z częstotliwością do 49.152 MHz. Z prostej matematyki wynika, że taki rejestr zanim zacznie się powtarzać z liczbami to minie ponad miesiąc ciągłej pracy, więc tym bardziej jest wystarczający do generowania pseudo losowych wartości
Zresztą, po pomiarach z ditherem i bez można jasno wywnioskować, że losowość spełnia pewne wymogi i energia błędu kwantyzacji jest zamieniana na energię szum.
No dobra, może koniec już samej teorii. Myślałem, że skrócę to wszystko dosyć znacząco omijając pewne rzeczy, ale teraz jak na to patrzę to widzę, że nawet mocno skrócony opis potrafi być rozległy w takiej tematyce.
_____________________________________
W każdym wypadku przejdźmy do samego projektu filtru, który został zaimplementowany w FPGA (bezpośrednio programowalna macierz bramek) w formie RTL (opisu sprzętu w formie logiki w VHDL'u). FPGA użyte w tym projekcie to Spartan-6 XC6SLX9 w obudowie TQFP-144. W innych projektach zazwyczaj używam Spartan-3 XC3S50AN, ponieważ posiadam ich jeszcze trochę i sobie cenię ich prostotę oraz wbudowany FLASH do konfiguracji. Niestety do tego projektu taki Spartan-3 jest po prostu za słaby i nie posiada odpowiednich jednostek do implementacji sensownego filtru cyfrowego. Pewnie, jakiś filtr można tam zmieścić, ale po co tworzyć następne dziadostwo, które w sumie nie wiele więcej wprowadza niż gotowe układy dostępne na rynku od lat 90. Zaprojektowany filtr implementuje zarówno interpolację jak i decymację. Matematycznie wykonuje on 16-krotną interpolację dla każdej wejściowej częstotliwości (tj. od 44.1 kHz do 768 kHz). W praktyce wygląda to tak, że sygnał 768 kHz też jest interpolowany 16-krotnie (do 12.288 MHz), ale koniec końców sygnał jest decymowany do zadanej wartości (odrzucanie próbek). Operacja interpolacji i decymacji na tych samych współczynnikach skraca się, więc w praktyce to wygląda tak, że filtr nie liczy próbek, które i tak zostaną odrzucone
Na rynku mamy wiele układów filtrów cyfrowych takich jak SAA7220, DF1706, SM5847, itp. Wszystkie te układy służą do tego samego, tj. interpolacji sygnału audio. W praktyce niektóre są lepsze a niektóre gorsze, więc każdy będzie grał inaczej. Przykładowo SAA7220 jest chyba najlepszym przykładem kiepskiego filtru zrobionego po taniości
Współczynniki 12-bitowe, słowo wejściowe maksymalnie do 16 bitów, bardzo mały akumulator (dużo błędów zaokrąglenia wyników mnożenia) oraz brak ditheringu. Ewidentnie widać, że to co stracimy w kiepskim filtrze cyfrowym na pewno nie uda nam się nadrobić przetwornikiem D/A. Filtry cyfrowe takie jak DF1706 czy SM5847 są już znacząco lepsze i to widać, ale ich kaskadowana interpolacja na różnych współczynnikach raczej do mnie nie przemawia. Filtr taki jak SM5847 interpoluje 8-krotnie w 3 fazach, tj. pierwsza faza posiada 169 współczynników (fs do 2fs), druga faza już tylko 29 współczynników (2fs do 4fs) oraz ostatnia faza z 17 współczynnikami (4fs do 8fs). Tak po prostu łatwiej było go zaprojektować, ale niekoniecznie lepiej sonicznie. Filtr cyfrowy przedstawiony w tym temacie interpoluje i decymuje na tych samych współczynnikach, więc nie ważne jaka jest częstotliwość wejściowa to zawsze te same współczynniki używane są do stworzenia wyjściowego strumienia
Mój filtr ma stałą wartość polifazy oraz interpolacji, która wynosi 16. Oznacza to w sumie tyle, że każda nowa próbka audio tworzona jest na bazie 512 poprzednich próbek (8192 / 16 = 512). Sygnał powinien być perfekcyjnie zrekonstruowany na bazie poprzednich próbek.
W danej chwili współczynniki filtru wraz z użytym oknem oraz pasmem przepustowym wyglądają następująco:

Trzeba pamiętać, że filtr jest interpolacyjny, więc pasmo przepustowe liczy się na bazie docelowej częstotliwości.
W praktyce nie ma co się do tych współczynników przywiązywać, ponieważ zależnie od sonicznych doświadczeń można w chwilę załadować nowe i znowu odsłuchiwać
Poniżej parę zdjęć, które już wrzucałem w budowanie na ekranie, ale warto też pokazać je w tym temacie:
Rekonstrukcja sygnału 20 kHz przy próbkowaniu 48 kHz:

Inaczej mówiąc idealny sinus, czyli taki jaki powinien być.
Warto pokazać jak wygląda sinus 10 kHz przy ekstrapolatorze rzędu zerowego (NOS) oraz przy ekstrapolatorze rzędu pierwszego (liniowa interpolacja), więc odpowiednio z mojego tematu o NOS DAC:
10 kHz i ekstrapolator rzędu zerowego (R-2R, taki typowy NOS):

10 kHz i ekstrapolator rzędu pierwszego (liniowa interpolacja):

Jak widać takie przetworniki nie radzą sobie z filtrowaniem i odwzorowaniem sygnału przy 10 kHz a gdzie tam do 20 kHz. Oczywiście jest to całkowicie normalne, ponieważ taki był ich zarys działania, ale warto o tym wspomnieć.
Tak samo odpowiedź impulsowa filtru cyfrowego z tego tematu:

Tutaj widać lekkie "dzwonienie" od filtru cyfrowego i jest to całkowicie normalne, ponieważ pokazuje to, że filtr ma ograniczone pasmo do rekonstrukcji sygnału. Gdyby filtr był nieskończony tak jak funkcja sin(x) / x oraz jego pasmo byłoby nieskończone to bylibyśmy w stanie idealnie odwzorować prostokąt, ale po prostu nie ma takiej fizycznej możliwości a nawet jakby była to skończylibyśmy na nieprawidłowo odwzorowanym sygnale audio :)
Filtr wygląda jak poniżej:

Zworka ROM decyduje o tym jaki filtr załadować do głównej pamięci RAM. Dostępne są dwa filtry, tj. o liniowej fazie oraz o minimalnej fazie. Oba są rzędu 8192 i fizycznie nie da się więcej zmieścić :)
Dodatkowo DAC na AD1865 do tego projektu:

Przy okazji zaprojektowałem też specjalną wersję filtru pod TDA1541 oraz TDA1540:

W tym projekcie interpolacja jest 8-krotna oraz taktowanie przetwornika wygląda trochę inaczej. Dodatkowo sam zegar CLK jest synchroniczny względem MCLK.
Podsumowując główne cechy mojego filtru są następujące:
- Interpoluje do 705.6 kHz bądź 768 kHz. Zawsze. Niezależnie od częstotliwości wejściowej, który może wahać się od 44.1 kHz do 768 kHz.
- 8192 współczynników w dwóch różnych filtrach do wyboru (liniowa faza oraz minimalna faza).
- Asynchroniczny zegar taktujący dane do przetwornika. Taki zabieg pozwala taktować PCM56, AD1865 i podobne aż do 768 kHz :)
- Wewnętrzne tłumienie o 1 dB.
- Jednostki mnożące to 32x35 z akumulatorem 67-bitowym, więc słowo audio jest akceptowane w pełni do 32 bitów rozdzielczości a współczynniki mają rozdzielczość 35 bitów. Błędy kwantyzacja na tym poziomie są już naprawdę minimalne.
- Główny rdzeń pracuje przy częstotliwości 225 MHz.
- Dithering TDPF.
- Wyjście strumienia jest osobne dla kanału L oraz R. Posiada tez odwrócone wyjścia, więc można podłączyć przetworniki w formie różnicowej.
- Wybór wyjściowego słowa od 16 do 24 bitów.
Rdzeń pracuje przy takiej częstotliwości, że wymaga to dobrej znajomości budowy FPGA oraz idei przetwarzania potokowego (z ang. pipeliningu) a routowanie przy próbie spełnienia wymagań czasowych wygląda następująco:
[code]
PAR will use up to 4 processors
Starting Multi-threaded Router
Phase 1 : 11122 unrouted; REAL time: 3 secs
Phase 2 : 8282 unrouted; REAL time: 4 secs
Phase 3 : 2791 unrouted; REAL time: 6 secs
Phase 4 : 2921 unrouted; (Setup:10769, Hold:1222, Component Switching Limit:0) REAL time: 7 secs
Updating file: core.ncd with current fully routed design.
Phase 5 : 0 unrouted; (Setup:21621, Hold:581, Component Switching Limit:0) REAL time: 13 secs
Phase 6 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 15 secs
Phase 7 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 20 secs
Phase 8 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 20 secs
Phase 9 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 20 secs
Phase 10 : 0 unrouted; (Setup:20232, Hold:0, Component Switching Limit:0) REAL time: 20 secs
Phase 11 : 0 unrouted; (Setup:0, Hold:0, Component Switching Limit:0) REAL time: 20 secs
Total REAL time to Router completion: 20 secs
Total CPU time to Router completion (all processors): 33 secs [/code]
Inaczej mówiąc trzeba się modlić, że wartości Setup oraz Hold zostaną sprowadzone do zera
Projekt jest jeszcze w fazie rozwoju, więc pewne rzeczy mogą się jeszcze zmienić. Na razie tyle, ale temat będzie kontynuowany
W sumie nie wiem od czego tu zacząć, ponieważ matematyka stojąca za cyfrowym filtrowaniem i ditheringiem nie będzie rozwijana, ale jednak trzeba coś wspomnieć aby zrozumieć działanie filtrowania częstotliwości w domenie cyfrowej. Zacznijmy jednak od tego, że zadaniem takiego filtru jest dokładnie takie same jak zwrotnicy w kolumnie głośnikowej, tj. filtrowanie danych częstotliwości i przepuszczanie innych

Pozwolę sobie zapożyczyć grafikę filtru typu FIR aby wyjasnić mniej więcej na jakiej zasadzie działa taki filtr:

Mamy wejście filtru, które w naszym wypadku jest wejściem strumienia danych audio. Człon Z -1 to po prostu próbka audio opóźniona o jeden, tj. każda poprzednia próbka przed tą nowszą (od najnowszej do najstarszej). Człony Bx to współczynniki filtru, które mnożone są poprzez wcześniejsze próbki ze strumienia audio. Wszystkie wyniki tych mnożeń są potem dodawane do siebie i tak o to otrzymujemy próbkę na wyjściu układu

Najprostszym przykładem filtru FIR jest po prostu liczenie średniej wartości ze wszystkich poprzednich próbek. Zobaczmy na powyższy obrazek i załóżmy, że wszystkie trzy człony Bx mają taką samą wartość, tj. 1/3. Zakładając, że na wejściu układu mamy próbki o wartościach 1, 2 i 3 to matematyka stojąca za filtrem wygląda następująco: 1/3 * 1 + 1/3 * 2 + 1/3 * 3 = 2

https://diyaudio.pl/showthread.php/2...-dobrze-zrobić
Przykładowy FIR oraz powyższy przetwornik D/A działają dokładnie tak samo, ale jeden działa w domenie cyfrowej a drugi w analogowej



Naprawdę nie ma co się rozpisywać, ponieważ pisanie o filtrach FIR może i być ciekawe, ale zbędne i czasochłonne. Temat dotyczy samego projektu takiego filtru a nie tego jaka matematyka za tym wszystkim stoi, więc postaram się jeszcze po szybkości wspomnieć o interpolacji, filtrowaniu sin(x) / x i ditheringu oraz przejść do samego projektu filtru na FPGA.
W nazwie temat wspomniałem, że zaprojektowany filtr cyfrowy jest typu interpolacyjnego, tj. takiego, który zwiększa oryginalną częstotliwość próbkowania, przykładowo 44.1 kHz, do zadanej częstotliwości, np. w moim wypadku jest to 705.6 kHz. Z prostej matematyki można wywnioskować, że filtr interpoluje 16-krotnie (44.1 kHz * 16 = 705.6 kHz). Zacznijmy jednak od tego co to jest interpolacja. Interpolacja jest to proces wyznaczenia pewnej funkcji, która przyjmuje takie same wartości w zadanej przedziale w którym są już wyznaczone pewne punkty. Sygnał audio zapisany jest jako wartość amplitudy (punkt na osi Y) w pewnych odstępach czasowych (oś X, np. 44.1 kHz). Inaczej pisząc zadaniem interpolacji jest po prostu wyznaczenie dodatkowych punktów pomiędzy istniejącymi już punktami. Taki zabieg najlepiej przedstawić na obrazku, więc zobaczmy poniżej:

Czerwony to sygnał taki jaki mamy zapisany w pliku i zrekonstruowany na przetworniku R-2R w formie NOS (ekstrapolator rzędu zerowego). Czarny to ten sam sygnał, ale interpolowany do nieskończenie wyższej częstotliwości w celu odtworzenia większej ilości punktów oryginalnego zapisu. Inaczej pisząc taki zabieg w praktyce to po prostu rekonstrukcja sygnału wraz z filtrowaniem dolnoprzepustowym. Interpolacja w formie cyfrowej to dwa zabiegi, tj. wrzucenie próbek o zerowej amplitudzie do sygnału tak aby zwiększyć częstotliwość oraz filtrowanie dolnoprzepustowe w celu pozbycia się odbić sygnału, które powstały z powodu zwiększenia próbkowania. Na poniższych obrazkach można zobaczyć jak to w praktyce wygląda:


W pierwszym obrazku na samej górze posiadamy sygnał oryginalny, dyskretny (czarne punkty) oraz ciągły (szara linia). Sygnału w formie ciągłej nie da się zapisać, ponieważ wymagałoby to nieskończonej ilości miejsca, więc w praktyce zapisujemy tylko sygnał w formie dyskretnej (próbkowany w równych odstępach czasu, np. 44.1 kHz). W celu odtworzenia oryginalnego sygnału musimy zwiększyć ilość próbek, które pozwolą nam zamienić sygnały dyskretny na sygnał ciągły (analogowy). W tym celu wrzucamy próbki o wartości 0 pomiędzy już istniejące próbki (drugi wykres). Taki zabieg pozwolił nam faktycznie zwiększyć próbkowania, ale niestety wprowadził też odbicia sygnału. W praktyce podczas próby odtworzenia takiego sygnału otrzymujemy sygnał powielony względem "lustra", które tworzy się na wielokrotności oryginalnej częstotliwości próbkowania (44.1 kHz). Ten problem można zobaczyć na drugim obrazku po prawej stronie gdzie widnieje wykres w domenie częstotliwościowej. W celu eliminacji tych odbić stosuje się filtrowanie dolnoprzepustowe, które w praktyce przesuwa odbicia na wyższą częstotliwość gdzie można już spokojnie filtrować filtrem analogowym z niskim rzędem. Inaczej pisząc czym większa interpolacja tym dalej przesuwane są odbicia oryginalnego sygnału.
No i w sumie to tyle wystarczy na temat samej interpolacji, więc na szybkości przejdźmy jeszcze do filtrowania dolnoprzepustowego funkcją sin(x) / x. W przetwarzaniu sygnału używa się tylko funkcji sin(x) / x do filtrowania dolnoprzepustowego, ponieważ z matematycznego punktu widzenia jest ona idealnym filtrem dolnoprzepustowym (z ang. brick-wall). Wynika to z faktu, że odpowiedź częstotliwościowa funkcji sin(x) / x jest funkcją prostokątną:

Oznacza to w praktyce tyle, że przepuszcza idealnie zadane częstotliwości a wszystkie poza wyznaczoną odpowiedzią częstotliwościową po prostu usuwa. Normalnie idealny filtr, ale nie ma tak łatwo


Teraz zobaczmy sobie co takie okno czasowe może zdziałać z nieskończona funkcją sin (x) / x:

Czerwony wykres - okno czasowe.
Zielony wykres - nieskończona funkcja sin(x) / x.
Niebieski wykres - funkcja sin(x) / x po zastosowaniu okna czasowego.
Taki zabieg w praktyce pozwala zastosować funkcję sin(x) / x do filtrowania

No i można zacząć filtrować! No prawie, ponieważ jako, że pracujemy na dyskretnych sygnałach a nie na ciągłych i do tego jeszcze mamy do czynienia z błędami kwantyzacji to jeszcze dosyć ważną rzeczą przy takim projekcie jest tzw. dithering podczas redukcji bitów wyjściowego słowa. Dlaczego redukcji? W praktyce nasz przetwornik D/A operuje przykładowo na słowach audio o długości 18 bitów. Zakładając, że nasze słowo wejściowe to 24 bitowy sygnał audio oraz sam fakt, że te słowo przechodzi filtrowanie w którym występuje mnożenie, powoduje, że zależnie od ilości bitów współczynników filtru, które mogą przykładowo mieć 32 bity, otrzymujemy wynik na poziomie 56 bitów. No dobra, ale mnożymy 24 bitową liczbę przez 32 bitową liczbę, więc jakim cudem mamy wynik na 56 bitach? Taka po prostu jest matematyka


Bez ditheringu:

Z ditheringiem:

Na przykładzie tego filtra zostało pokazane jak dither może pomóc przy odwzorowaniu sygnału -96 dBFS na 18 bitowym przetworniku :) Na górze też jest średnia z 10 pomiarów, więc koniec końców różnica w szumie jest żadna :)
Dithering wymaga generatora liczb pseudolosowych, ponieważ chcemy aby nasz szum był po prostu losowy. No prawie, ponieważ nie potrzebujemy losowości w sensie nieprzewidywalności samych generowanych liczb, ale ciągu generowanych wartości, które mają rozkład jednostajnie ciągły, tj. taki dla którego wystąpienie danej liczby z danego przedziału jest tak samo prawdopodobne jak dla każdej innej z tego samego przedziału. Pewną własnością rozkładu jednostajnie ciągłego jest to, że dodanie dwóch takich losowych liczb zamienia rozkład na trójkątny, czyli taki, którego gęstość prawdopodobieństwa rozkłada się w formę trójkątną gdzie jego wierzchołek to liczby, które mają największą szansę na trafienie. Na poniższym obrazku X oraz Y to rozkład jednostajnie ciągły a Z to trójkątny:

Rozkład trójkątny jest świetny do ditheringu audio, ponieważ zazwyczaj mając jedynkę chcemy aby dalej to była jedynka, tj. z dużym prawdopodobieństwem, ale aby nie było to równomierne. Inaczej pisząc ditheringiem można nazwać operację posiadania liczby X oraz losowania liczby Y chcąc trafić tak, aby X ~ Y, ale jednak nie chcemy znanej liczby X, ale nowej liczby Y, która ma duże prawdopodobieństwo trafić blisko X

Źródłem takiej pseudo losowości do wygenerowania rozkładu trójkątnego w FPGA może być rejestr przesuwający z liniowym sprzężeniem zwrotnym, który także został zaimplementowany w tym projekcie. Zaimplementowany rejestr ma okres 2^47 i pracuje z zegarem MCLK, który może tykać z częstotliwością do 49.152 MHz. Z prostej matematyki wynika, że taki rejestr zanim zacznie się powtarzać z liczbami to minie ponad miesiąc ciągłej pracy, więc tym bardziej jest wystarczający do generowania pseudo losowych wartości

No dobra, może koniec już samej teorii. Myślałem, że skrócę to wszystko dosyć znacząco omijając pewne rzeczy, ale teraz jak na to patrzę to widzę, że nawet mocno skrócony opis potrafi być rozległy w takiej tematyce.
_____________________________________
W każdym wypadku przejdźmy do samego projektu filtru, który został zaimplementowany w FPGA (bezpośrednio programowalna macierz bramek) w formie RTL (opisu sprzętu w formie logiki w VHDL'u). FPGA użyte w tym projekcie to Spartan-6 XC6SLX9 w obudowie TQFP-144. W innych projektach zazwyczaj używam Spartan-3 XC3S50AN, ponieważ posiadam ich jeszcze trochę i sobie cenię ich prostotę oraz wbudowany FLASH do konfiguracji. Niestety do tego projektu taki Spartan-3 jest po prostu za słaby i nie posiada odpowiednich jednostek do implementacji sensownego filtru cyfrowego. Pewnie, jakiś filtr można tam zmieścić, ale po co tworzyć następne dziadostwo, które w sumie nie wiele więcej wprowadza niż gotowe układy dostępne na rynku od lat 90. Zaprojektowany filtr implementuje zarówno interpolację jak i decymację. Matematycznie wykonuje on 16-krotną interpolację dla każdej wejściowej częstotliwości (tj. od 44.1 kHz do 768 kHz). W praktyce wygląda to tak, że sygnał 768 kHz też jest interpolowany 16-krotnie (do 12.288 MHz), ale koniec końców sygnał jest decymowany do zadanej wartości (odrzucanie próbek). Operacja interpolacji i decymacji na tych samych współczynnikach skraca się, więc w praktyce to wygląda tak, że filtr nie liczy próbek, które i tak zostaną odrzucone

Na rynku mamy wiele układów filtrów cyfrowych takich jak SAA7220, DF1706, SM5847, itp. Wszystkie te układy służą do tego samego, tj. interpolacji sygnału audio. W praktyce niektóre są lepsze a niektóre gorsze, więc każdy będzie grał inaczej. Przykładowo SAA7220 jest chyba najlepszym przykładem kiepskiego filtru zrobionego po taniości


W danej chwili współczynniki filtru wraz z użytym oknem oraz pasmem przepustowym wyglądają następująco:

Trzeba pamiętać, że filtr jest interpolacyjny, więc pasmo przepustowe liczy się na bazie docelowej częstotliwości.
W praktyce nie ma co się do tych współczynników przywiązywać, ponieważ zależnie od sonicznych doświadczeń można w chwilę załadować nowe i znowu odsłuchiwać

Poniżej parę zdjęć, które już wrzucałem w budowanie na ekranie, ale warto też pokazać je w tym temacie:
Rekonstrukcja sygnału 20 kHz przy próbkowaniu 48 kHz:

Inaczej mówiąc idealny sinus, czyli taki jaki powinien być.
Warto pokazać jak wygląda sinus 10 kHz przy ekstrapolatorze rzędu zerowego (NOS) oraz przy ekstrapolatorze rzędu pierwszego (liniowa interpolacja), więc odpowiednio z mojego tematu o NOS DAC:
10 kHz i ekstrapolator rzędu zerowego (R-2R, taki typowy NOS):

10 kHz i ekstrapolator rzędu pierwszego (liniowa interpolacja):

Jak widać takie przetworniki nie radzą sobie z filtrowaniem i odwzorowaniem sygnału przy 10 kHz a gdzie tam do 20 kHz. Oczywiście jest to całkowicie normalne, ponieważ taki był ich zarys działania, ale warto o tym wspomnieć.
Tak samo odpowiedź impulsowa filtru cyfrowego z tego tematu:

Tutaj widać lekkie "dzwonienie" od filtru cyfrowego i jest to całkowicie normalne, ponieważ pokazuje to, że filtr ma ograniczone pasmo do rekonstrukcji sygnału. Gdyby filtr był nieskończony tak jak funkcja sin(x) / x oraz jego pasmo byłoby nieskończone to bylibyśmy w stanie idealnie odwzorować prostokąt, ale po prostu nie ma takiej fizycznej możliwości a nawet jakby była to skończylibyśmy na nieprawidłowo odwzorowanym sygnale audio :)
Filtr wygląda jak poniżej:

Zworka ROM decyduje o tym jaki filtr załadować do głównej pamięci RAM. Dostępne są dwa filtry, tj. o liniowej fazie oraz o minimalnej fazie. Oba są rzędu 8192 i fizycznie nie da się więcej zmieścić :)
Dodatkowo DAC na AD1865 do tego projektu:

Przy okazji zaprojektowałem też specjalną wersję filtru pod TDA1541 oraz TDA1540:

W tym projekcie interpolacja jest 8-krotna oraz taktowanie przetwornika wygląda trochę inaczej. Dodatkowo sam zegar CLK jest synchroniczny względem MCLK.
Podsumowując główne cechy mojego filtru są następujące:
- Interpoluje do 705.6 kHz bądź 768 kHz. Zawsze. Niezależnie od częstotliwości wejściowej, który może wahać się od 44.1 kHz do 768 kHz.
- 8192 współczynników w dwóch różnych filtrach do wyboru (liniowa faza oraz minimalna faza).
- Asynchroniczny zegar taktujący dane do przetwornika. Taki zabieg pozwala taktować PCM56, AD1865 i podobne aż do 768 kHz :)
- Wewnętrzne tłumienie o 1 dB.
- Jednostki mnożące to 32x35 z akumulatorem 67-bitowym, więc słowo audio jest akceptowane w pełni do 32 bitów rozdzielczości a współczynniki mają rozdzielczość 35 bitów. Błędy kwantyzacja na tym poziomie są już naprawdę minimalne.
- Główny rdzeń pracuje przy częstotliwości 225 MHz.
- Dithering TDPF.
- Wyjście strumienia jest osobne dla kanału L oraz R. Posiada tez odwrócone wyjścia, więc można podłączyć przetworniki w formie różnicowej.
- Wybór wyjściowego słowa od 16 do 24 bitów.
Rdzeń pracuje przy takiej częstotliwości, że wymaga to dobrej znajomości budowy FPGA oraz idei przetwarzania potokowego (z ang. pipeliningu) a routowanie przy próbie spełnienia wymagań czasowych wygląda następująco:
[code]
PAR will use up to 4 processors
Starting Multi-threaded Router
Phase 1 : 11122 unrouted; REAL time: 3 secs
Phase 2 : 8282 unrouted; REAL time: 4 secs
Phase 3 : 2791 unrouted; REAL time: 6 secs
Phase 4 : 2921 unrouted; (Setup:10769, Hold:1222, Component Switching Limit:0) REAL time: 7 secs
Updating file: core.ncd with current fully routed design.
Phase 5 : 0 unrouted; (Setup:21621, Hold:581, Component Switching Limit:0) REAL time: 13 secs
Phase 6 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 15 secs
Phase 7 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 20 secs
Phase 8 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 20 secs
Phase 9 : 0 unrouted; (Setup:20232, Hold:581, Component Switching Limit:0) REAL time: 20 secs
Phase 10 : 0 unrouted; (Setup:20232, Hold:0, Component Switching Limit:0) REAL time: 20 secs
Phase 11 : 0 unrouted; (Setup:0, Hold:0, Component Switching Limit:0) REAL time: 20 secs
Total REAL time to Router completion: 20 secs
Total CPU time to Router completion (all processors): 33 secs [/code]
Inaczej mówiąc trzeba się modlić, że wartości Setup oraz Hold zostaną sprowadzone do zera

Projekt jest jeszcze w fazie rozwoju, więc pewne rzeczy mogą się jeszcze zmienić. Na razie tyle, ale temat będzie kontynuowany

Skomentuj