Rychlá Fourierova transformace (FFT) pro AVR
FFT je sice obvykle počítána na rychlých DSP, které jsou k tomu instrukční sadou podstatně lépe přizpůsobeny, nicméně MCU řady AVR, přestože jen 8-bitové, poskytují dostatek výkonu pro celkem svižný výpočet v pevné řádové čárce. Pokud se algoritmus má vejít do interní RAM, tak je jeho rozlišení značně omezeno, resp. je třeba volit kompromis mezi přesností výpočtu a rozlišením transformace. Asi úplné minimum, kdy má smysl se o FFT pokoušet, je 16-bit rozlišení, kde pro 4kB interní RAM může být maximální rozlišení FFT 512 bodů (je třeba počítat s pamětí pro stack MCU). Poměrně zdařilá implementace FFT pro AVR se, jako řada dalších zajímavých věcí, nachází na známém webu www.elm-chan.org. Jedná se o algoritmus DIF radix 2 (decimace ve frekvenci se základem 2). Uvedený kód ale není nijak zvlášť optimalizovaný a než bych ho zkoumal a upravoval, tak jsem se rozhodl to napsat rovnou znovu a jinak. Pro svou implementaci jsem zvolil metodu DIT (decimace v čase), která se mi zdá o fous méně náročná na čas MCU.
1. Něco teorie k Fourierově transformaci
1.1 FFT vs. DFT
Než se pokusím popsat samotný algoritmus FFT, asi by nebylo od věci vysvětlit, co to vlastně je diskrétní Fourierova transformace (DFT). Teorie na toto téma je plný internet (občas i česky), takže jen stručně. Jde o transformaci signálu z časové oblasti do frekvenční oblasti, tj. vstupem do DFT je diskrétní navzorkovaný signál a výstupem diskrétní spektrum tohoto signálu - informace o frekvenčních složkách v něm obsažených. Jak vstup, tak i výstup (obraz) transformace je v komplexním tvaru. Matematicky je DFT definována takto:
kde A a B jsou vstupní a výstupní hodnoty DFT a FTk je zkráceně zapsaná exponenciála z předchozího vztahu (bývá označována jako "twiddle faktor"): 1.2 Inverzní DFT
Jak se asi dá očekávat, tak DFT má, stejně jako spojitá verze, i inverzní variantu, která ze spektra (obrazu) zpětně rekonstruuje vstupní sígnál (předlohu).
Změna proti dopředné transformaci spočívá pouze ve změně směru rotace twiddle faktoru z FTk na FT-k. DIT motýlek pak bude vypadat takto:
1.3 FFT Cooley - Tukey DIT radix 2
Takto je označován zřejmě nejrozšířenější algoritmus pro výpočet rychlé Fourierovy transformace (FFT). Je poskládán kompletně z výše popsaných motýlků. Algoritmus je použitelný pouze pro délku vstupní posloupnosti N=2M (základ 2 neboli "radix 2"). Celý výpočet je dělen na etapy, kterých je M=log2(N) a v každé etapě je vypočteno N/2 motýlků, takže M*N/2 motýlků celkem, tj. 2*M*N násobení a 3*M*N součtů. Příklad tohoto algoritmu pro N=8 (M=3 etap): Pokud má být algoritmus programově efektivně realizovatelný, tak je nejprve třeba vhodně přeskládat prvky vstupní posloupnosti. Této úpravě se říká bitová inverze - obrátí se pořadí bitů v adrese vstupního prvku:
Je to sice poněkud nepříjemná operace (zejména má-li se provádět v rámci jednoho pole), nicméně algoritmus FFT pak může proběhnout na místě, tj. v rámci jednoho pracovního pole, což je na MCU s malou RAM nutnost. Výstup výše uvedeného algoritmu je již naštěstí ve správném pořadí. Drobnou nepříjemností algoritmu je, že se v jeho jednotlivých etapách struktura mění (mění se poloha a vzdálenost vstupů motýlku), nicméně dá se to poměrně obstojně zvládnout pomocí dvou pointerů. Vlastní FFT tohoto typu na již správně přeskládaných datech by v C mohla vypadat přibližně takto: #define N 8 typedef struct{ double re; double im; }TCplx; double tsincos[N]={0.000, 1.000, 0.7071, 0.7071, 1.000, 0.000, 0.7071,-0.7071}; TCplx FFTwarr[N]; void FFTditR2(void) { double dsin,dcos; double *Fnk; TCplx C; TCplx *A,*B; int s,k; int n=1; int angf=N/2; while(n<N) { A=&FFTwarr[0]; B=&FFTwarr[n]; Fnk=&tsincos; for(k=0;k<n;k++) { dsin=*Fnk++; dcos=*Fnk++; for(s=0;s<angf;s++) { C.re=B->im*dsin + B->re*dcos; C.im=B->im*dcos - B->re*dsin; B->re=A->re - C.re; B->im=A->im - C.im; A->re=A->re + C.re; A->im=A->im + C.im; A+=2*n; B+=2*n; } A-=(N-1); B-=(N-1); Fnk+=(angf-1)*2; } n<<=1; angf>>=1; } }
Hodnoty twiddle faktoru pro jednotlivé úhly jsou již předpočteny v poli tsincos[]. Vnitřní smyčka provádí vlastní výpočet motýlku pro každou sekci s etapy. Nadřazená smyčka toto zopakuje pro každý úhel twiddle faktoru resp. pro každé posunutí k motýlku vůči začátku sekce s. Nejvyšší smyčka toto celé opakuje pro každou etapu výpočtu m - v každé další etapě je vzdálenost mezi vstupy motýlku n dvojnásobná a frekvence rotace úhlu twiddle faktoru angf poloviční. Graficky znázorněný význam jednotlivých proměných pro druhou etapu výpočtu: 1.3.1 Možnosti optimalizace
Pokud by bylo třeba jít do extrému, tak lze ušetřit trochu strojového času vypuštěním násobení z výpočtu motýlku v případě, že úhel twiddle faktoru je 0 nebo pi/2, kdy hodnoty sin() a cos() nabývají pouze hodnot 0, -1 a +1. Pro úhel 0 by motýlek vypadal takto: 1.4 Fourierova transformace a okna
Protože vstupem ke spektrální analýze je prakticky vždy neperiodický signál, tj. obecná řada vzorků o libovolné délce, je třeba před vlastní DFT provést jeho periodizaci - výběr úseku ke zpracování. To se provádí pomocí okénkových funkcí. Nejjednodušší funkcí je obdélníkové okno, které realizuje prostý výřez úseku ze zdrojové posloupnosti bez úpravy jeho amplitudy. Při výběru úseku mohou nastat dvě situace. Buďto na sebe začátek a konec vybraného úseku spojitě a hladce navazuje nebo ne. V prvním případě nebude s analýzou pomocí DFT problém, protože periodizovaný signál nebude obsahovat žádné strmé přechody. Tento případ ale v reálu nastane málokdy (s ohledem na zcela náhodný charakter analyzovaného signálu) a periodizovaný signál tak bude často obsahovat nespojitosti: První graf ukazuje výběr (obdélníkové okno) úseku ze sinusového průběhu, vzhledem k délce úseku má signál frekvenci 1,25 (tedy nekoherentní vzorkování). Druhý průběh ukazuje pár period periodizovaného signálu (získáno opakováním vybraného úseku) - to je průběh, který ve skutečnosti "vidí" DFT. Výsledné spektrum pak obsahuje jasně patrný peak signálu o frekvenci 1,25, ale také všechny frekvenční složky odpovídající nespojitosti v analyzovaném signálu. Frekvenční složky s menšími amplitudami by se zcela ztratily. Řešením tohoto problému je použití okna, které kromě výběru navíc vhodně upravuje amplitudu signálu (násobení signálu oknem). Jednou z možností je např. velmi jednoduché Hannovo okno, jehož průběh je v grafech naznačen tečkovaně: Záčátek i konec úseku na sebe hladce navazuje, periodizovaný signál neobsahuje žádné nespojitosti a spektrum je tak podstatně čistší. Bohužel Hannovo okno, stejně jako každé jiné, má svoje vlastní spektrum a to se pochopitelně přičte ke spektru analyzovaného signálu (násobení předloh je ekvivalentní konvoluci spekter). Výsledkem je rozložení/prosáknutí energie analyzovaného signálu do širšího frekvenčního pásma a s tím související změna amplitudy hlavního peaku protože celková energie musí zůstat zachována. Díky tomu se např. mohou "slít" dva blízké peaky do jednoho. Některá běžně používaná okna a jejich vlastnosti jsou uvedeny v následující tabulce:
CG ... koeficient zesílení hlavního peaku při koherentním vzorkování CW ... počet postranních peaků při koherentním vzorkování SLA ... amplituda nejbližšího postranního laloku SLD ... strmost poklesu amplitudy potranních laloků Obecně platí, že čím bohatší je spektrum okna, tím menší amplitudu má jeho nejbližší postranní lalok. Průběhy uvedených oken:
A jejich spektra (1 bin = 0,01):
2. Realizace FFT na AVR (AvrAsm32 assembler)
Nyní už k vlastní realizaci na AVR. Kvůli potřebné RAM jsem zvolil ATmega644, která jí má 4kB (nejvíce v DILku). Protože pokoušet se o ladění FFT s vykreslováním na LCD je poněkud nepraktické, rozhodl jsem se napsat prográmek pro Windows XP, který slouží jako grafické rozhraní. S ním zařízení komunikuje jako obvykle po sériovém portu resp., kvůli rozumné odezvě, přes virtuální USB-COM port. Přenosová rychlost je nastavená na maximální 2MBd @ XTAL=16MHz bez parity. Původně jsem předpokládal, že bude třeba použít řízení toku směrem do PC, protože FT232BM má pouze 384/128B FIFO, ale systém si s tím nějak očividně poradil i při 1024B blocích dat. Doplnit testování RTS linky v rutině "UsTxByte" by ale v případě potřeby neměl být problém. Program pro PC je napsán v prostředí BDS 2006 - Turbo C++ a testován pouze v XP. Co to bude dělat v novějších OS netuším, ale práce s COM portem snad zůstala zpětně kompatibilní. Před použitím programu je třeba nastavit číslo COM portu a baudrate v souboru "config.ini". Program je vybaven timeouty, takže v případě problémů s komunikací by neměl vytuhnout. Případné problémy se sekáním komunikace by mělo řešit nastavení latence USB-COM převodníku přímo v ovladačích.
Prográmek předně obsahuje jednoduchý signálový generátor, parametry lze měnit scrollbarem nebo ručně zadat. Vygenerovaný signál je zobrazen v horním grafu. Dále lze nastavit, co má MCU spočítat. Jednak je zde možnost zobrazit časový průběh po aplikaci okna ("time domain"), dále lineární amplitudové spektrum ("frequency domain"), případně logaritmické amplitudové spektrum a nakonec zase logaritmické, ale s logaritmem vypočteným na PC (pro přesnější výsledky). Výsledek je zobrazen ve spodním grafu. U logaritmického měřítka je y-osa udaná v dB*1000. U obou grafů lze ručně zadat rozsah y-osy (hodnotu potvrdit stiskem "Enter"). Stisk "Enter" v prázdném poli nastaví zpět defaultní hodnotu. Poslední věcí, co prográmek umí, je výpis nastavení FFT na MCU a výpis statistik, tj. především změřené časy. Výpis změřených časů pro různá nastavení FFT ukazuje následující tabulka:
Časy jsou měřeny pro FFTprec=0 a přesnou rutinu odmocniny (viz. popis SW pro MCU), tedy stejnou, jako používá implementace AVRFFT (z webu www.elm-chan.org). FFTopt zapíná optimalizaci pomocí zjednodušených motýlků (opět viz. popis níže). Nárůst výkonu pro FFTopt=1 při FFTN=256 je cca 24%, což je celkem slušný efekt. Pro menší FFTN je nárůst výkonu ještě výraznější. Konkrétně pro FFTN=256 a lineární výstup je moje implementace asi o 7,3% (resp. o 21% pro FFTopt=1) rychlejší oproti AVRFFT. Při zvýšené přesnosti výpočtu (FFTprec=1) bude výkon asi o 12% nížší (a odstup signál/šum asi o 8dB vyšší). Význam prvků tabulky by měl být zřejmý z níže uvedného popisu SW pro MCU. Program ke stažení: AVR_FFT_tester.zip (320kB). 2.1 SW pro MCU
Firmware pro MCU je napsán kompletně v ASM, nicméně ty podstatné funkce realizující FFT nemají parametry, takže přepracování do "*.s" funkcí pro jazyk C není problém (jen bude asi problém s některými direktivami použitého překladače AVRasm32). Kód je pro přehlednost rozdělen do několika modulů, které budou popsány dále. 2.1.1 Modul "FFTditR2.asm" Zde jsou umístěny rutiny vlastní FFT transformace. Všechny pracují s jediným pracovním polem FFTwarr[] word prvků, zapsáno v C: int16 FFTwarr[FFTN*2];
přičemž FFTN je velikost FFT. Toto pole je komplexní (proto *2), přičemž sudé prvky představují realné hodnoty (v textu označované jako re{FFTwarr[]}) a liché imaginární (v textu jako im{FFTwarr[]}).
Všechny rutiny by měly být funkční pro FFTN={32, 64, 128, 256, 512}. V případě doplnění lookup tabulek i pro menší rozsah, ale rozhodně ne pro vyšší. Už pro FFTN=512 je v kódu spousta preprocesorového balastu, protože už prostě nezbylo dost registrů na rozšíření všech potřebných proměnných na 16-bit a práce s proměnnými v RAM je poněkud pomalá. 2.1.2 Modul "FFTditR2tables.asm"
Zde jsou všechny lookup tabulky potřebné pro FFT pro různé velikosti FFTN. Předně je zde tabulka TFFTbrv, která obsahuje předpočítané koeficienty pro bitovou inverzi (pro FFTN≤64 8-bit, jinak 16-bit). 2.1.3 Modul "FFTmacros.asm" Nějaká ta makra různých násobení a také makra obsahující zjednodušený motýlkový algoritmus pro úhly 0 a pi. 2.1.4 Modul "math.asm"
Zde jsou rutiny složitější aritmetiky. Předně 32-bit odmocnina stažená z
www.elm-chan.org ("tech. notes" sekce). Tahle rutinka je přesná, zato ale dost pomalá (cca 550 cyklů). Z toho důvodu jsem se rozhodl zkusit to rychleji přes lookup tabulky s omezenou přesností a celkem se to i povedlo. Funguje to zhruba tak, že se vstupní proměnná x rozdělí na část indexovací (výběr prvku z lookup) a část zlomkovou, která slouží k linární interpolaci mezi prvky lookup. Pro 8-bit vstup je přesnost absolutní (vrací to zaokrouhlenou hodnotu) pro 16-bit a více je chyba pod 0,6%. Kvůli vyšší přesnosti je lookup rozdělena na dvě části s různým rozlišením. 2.1.5 Modul "main.asm" Zde jsou jen rutiny pro použití USARTu a vlastní komunikační protokol s PC. Ten vypadá z pohledu MCU zhruba takto:
Kromě uvedeného jsou zde všechny podstatné přepínače preprocesoru. Jednak klasicky frekvence hodin XTAL v Hz, dále FFTN jakožto zvolená velikost FFT, FFTwindow udávající typ okna, FFTopt, který umožňuje zakázat použití zjednodušených motýlků (ušetří se nějaká ta FLASH, ale bude to pomalejší), FFTprec, který zapne korekci po aritmetické rotaci pro vyšší odstup signál/šum a nakonec FFTfsqrt, kterým se vybírá mezi přesnou/rychlou rutinou odmocniny. 3. Realizace FFT na AVR (AVR GCC)
Poměrně nedávno jsem se přinutil začít experimentovat s AVR GCC a protože jsem si chtěl trochu procvičit
kombinování assembleru se zbytkem projektu v C, rozhodl jsem se přepracovat původní assemblerovskou implementaci FFT pro AVR GCC.
Pokud mohu soudit, tak se to celkem i zdařilo a nepodepsalo se to nijak znatelně ani na velikosti zdrojáku.
Při té příležitosti jsem rovněz opravil pár chybek a nedostatků, především vlastní DIT motýlek který, jak jsem byl upozorněn,
generuje opačné znaménko u imaginární složky. Vzhledem k tomu, že jsem počítal amplitudové spektrum, tak se to pochopitelně
neprojevilo, ale pro korektnost jsem raději algoritmus opravil. 3.1 Testovací program pro PC Komunikace by měla být kompatibilní i s původní verzí programu, ale raději bych použil novou, lehce vylepšenou verzi (ke stažení dále). Bylo opraveno pár chybek, přidáno menu s možností testování algoritmů sqrt32f() a log10f() a rovněž něco informací o signálu a spektru navíc. Nastavení zůstalo stejné, tj. stací zadat číslo COM portu a baudrate. Program ke stažení: AVR_FFT_tester_v2.zip (365kB). 3.2 AVRFFT pro AVR GCC K vlastní implementaci AVR FFT patří pouze následující soubory:
Zbytek je součást ukázkového programu. 3.2.1 Modul "FFT_routines.s" Jde o hlavní modul celé FFT implementace. Jsou zde funkce:
void FFT_input(void); provádějící okéknování a bitovou reverzi adres, dále: void FFT_dit_r2(void); provádějící vlatní FFT transformaci a na konec: void FFT_out_lin(void); void FFT_out_log(void); které vypočtou amplitudové spektrum, případně navíc provedou logaritmování s normalizací na 0dB. Všechny funkce pracují se společným globálním polem komplexních prvků: int16_t FFTwarr[FFTN*2]; Komplexní čísla jsou v poli uložena postupně po složkách, tj. re{0}, im{0}, re{1}, im{1}, ..., re{FFTN-1}, im{FFTN-1}. Použití FFT knihovny je zhruba následující:
Pokud jde o možnosti nastavení algoritmu, tak byl proti původní implementaci přidán jen jedem parametr navíc a sice FFTdupim. Seznam a význam jednotlivých nastavení uvádí následující tabulka:
K tomuto modulu navíc přímo patří lookup tabulky "FFT_lookup.inc", vyšší makra "FFT_macros.inc" a základní makra "macros.inc" (společná pro všechny assemblerovské soubory). 3.2.2 Modul "FFT_tables.inc" Zde jsou umístěny všechny lookup tabulky FFT algoritmu. Jednak je zde tabulka pro bitovou reverzi adres TFFTbrv, dále předpočítaný sin() a cos() (TFFTsico) v rozsahu 0 až 180° a na konec tabulky oken TFFTwdw. U znaménkových oken (např. Flattop) musí být navíc definováno: .equ FFTWS,1 Tabulky jsem předgeneroval jen pro rozsah 32 až 512 bodů, pro větší není implementace stavěná, menší lze vygenerovat pomocí malé utilitky: AVR_FFT_lookup.zip (64kB) 3.2.3 Modul "FFT_math.s" Tento modul obsahuje aritmetiku použitou FFT algoritmem. Předně je zde rutinka 32-bit odmocniny od ChanN FFT_sqrt32c_asm (není přímo volatelná jako C funkce) a její GCC wrapper: uint16_t FFT_sqrt32c(uint32_t x); který již přímo volatelný z C části programu je. Takto odděleně je funkce řešena z toho důvodu, aby dobře "sedla" do vlastního FFT algoritmu bez zbytečné omáčky (záloha registrů), kterou vyžaduje GCC. Další funkcí je rychlá 32-bit odmocnina FFT_sqrt32f_asm a její GCC wrapper: uint16_t FFT_sqrt32f(uint32_t x);
Tato funkce je řešena pomocí dvojice lookup tabulek s interpolací a je velmi rychlá,
nicméně vyžaduje dost FLASH pro tabulky a její přesnost je omezená na asi 0.6%
(většina rozsahu x má chybu mnohem nižší). int16_t FFT_log10f(uint16_t x);
Funkce je řešena pomocí trojice lookup tabulek a lineární interpolace. Obsahuje sadu lookup tabulek pro každé
okno, takže je zajištěno, že fullscale sinu odpovídá vždy výstup y=0dB. Rozsah výstupu je podle okna od
cca -88dB do +6dB. Všechno mimo je ořezáno na tento rozsah. Výstupní formát je ve tvaru
fixed point (fx 8.8). Pokud by bylo třeba generovat lookup pro jiná okna případně jiná rozlišení, pak je k disopzici
utilitka pro jejich generování log10lookup.zip (64kB). 3.2.4 Modul "main.c"
Zde je jednoduchá demonstrační apliakce. Ukázkový prográmek je navržen pro jendočip ATmega644,
který má poměrně dost RAM (4kB), takže lze bez obtíží
implementovat i 512-ti bodové FFT. Komunikace s PC je opět řešena přes virtuální USB-COM port na 2Mbd bez řízení toku, ale lze použít
i klasický COM port na 115200bd, nicméně odezva bude dost pomalá.
Příkaz 'F':
Příkaz 'S':
Příkaz 'L':
Na ostatní příkazy reaguje MCU chybovým znakem 'E'. (c) 2010-2012, Stanislav Mašláň
|