Rychlá Hartleyova transformace (FHT) pro AVR
Po úpěšné implementaci Cooley-Tukey FFT algoritmu na AVR, která se ukázala být ještě o něco výkonnější, než AVRFFT z webu www.elm-chan.org, kterou jsem se inspiroval, jsem si řekl, že by nebylo od věci zkusit nějakou pokročilejší optimalizační metodu výpočtu spektra. Vzhledem k mým dřívějším experimentům s transformacemi v rámci jednoho VŠ předmětu o zpracování dat jsem se rozhodl pro Hartleyovu transformaci. Ta se mi zdála na první pohled celkem jednoduchá a výhodná pro jednočipy, protože nepracuje s komplexními čísly, ale pouze s reálným pracovním polem. Tak lze ušetřit polovinu RAM a navíc zvýšit rychlost výpočtu. Výstup transformace se však dá velmi jednoduše přepočítat na komplexní spektrum odpovídající DFT, pokdu by bylo třeba. 1. Něco teorie k Hartleyově transformaci
1.1 DHT vs DFT
Diskrétní Hartleyova transformace (DHT) je definována velmi jednoduchým vztahem: 1.2 FHT - rychlá DHT
DHT sama o sobě je pro výpočet spektra efektivnější, než DFT, nicméně podobně jako DFT má jisté příznivé vlastnosti, které umožňují
její zefektivnění. Celkové odvození je pěkně popsáno např. v [1] nebo [2].
Optimalizace je založena na stejném principu, jako u FFT, tj. na půlení zdrojové řady a dopočítávání druhé poloviny spektra.
Prvním krokem je rozdělit vstupní posloupnost na sudé a liché prvky:
kde pro koeficienty SNk a CNk platí: Díky optimalizacím jsou první etapy složené jen z DFT motýlků bez rotace (násobení twiddle faktorem) a dále odpadají násobení pro S,CN0 a S,CNn. V zájmu redukce velikosti lookup tabulek pro sin() a cos() je zde využito jejich periodicity, takže index k pro k>n/2 je nahrazen indexem n-k. Proto se také vyskytuje u spodního cosinu záporné znaménko. Celkový počet operací takto zjednodušeného algoritmu je celkem obtížně na vyjádřitelný vztahem, takže uvádím pouze tabulku s příklady pro konkrétní N, pro FHT a obdobně optimalizované FFT:
Počet násobení klesl na polovinu a součtů cca na 60% pro N=256. Co do počtu operací, bude FHT téměř dvakrát rychlejší, než FFT, ale v reálu
něco výkonu navíc sebere složitější indexování dat. 1.3 FHT - příklad algoritmu v C Zdroj [1] mimo odvození uvádí také konkrétní implementaci v jazyce Ratfor (Rational Fortran), která se zdá být celkem efektivní, jen nevyužívá pointery, takže jsem ji trochu upravil pro své potřeby. Možný příklad implementace v C v přesnosti double ukazuje následující kód: #define N 16 // sin(k/N*pi), cos(k/N*pi), ... (k=0..N/2-1) double tsincos[N]={0.000, 1.000, 0.3827, 0.9239, ...}; double FHTwarr[N]; void FHT(void) { int n=1; int angf=N/2; while(n<N) { double *ang=&tsincos[0]; double *A=&FHTwarr[0]; double *B=&FHTwarr[n]; double *C=&FHTwarr[n-1]; double *D=&FHTwarr[n*2-1]; // ang=0 for(k=0;k<angf;k++) { double tmp=*B; *B=*A-tmp; *A=*A+tmp; A+=n<<1; B+=n<<1; } A-=(N-1); B-=(N-1); // full butterflies while(n>2 && ang<n) { ang+=angf<<1; dsin=ang[0]; dcos=ang[1]; for(k=0;k<angf;k++) { double E=*B*dcos + *D*dsin; double F=*B*dsin - *D*dcos; *B=*A-E; *A=*A+E; *D=*C-F; *C=*C+F; A+=n<<1; B+=n<<1; C+=n<<1; D+=n<<1; } A-=(N-1); B-=(N-1); C-=(N+1); D-=(N+1); } // ang=pi/2 for(k=0;n>1 && k<angf;k++) { double tmp=*D; *D=*C-tmp; *C=*C+tmp; C+=n<<1; D+=n<<1; } n<<=1; angf>>=1; } }
Algoritmus pracuje s již přeskládanými daty (je třeba nejdříve provést bitovou inverzi adres)!
Sinus a cosinus je opět předpočten a to pouze v rozashu 0 až 90° (polovina proti FFT). Vlastní výpočet je
dělen na tři části. První odpovídá úhlu 0°, takže byla zjednodušena na prosté součty/rozdíly. Stejné zjednodušení je provedeno
pro úhel 90° (uplatňuje se až od druhé etapy). Plný dvojitý motýlek se provádí až od třetí etapy a to jen pro úhly mimo 0 a 90°. 1.4 FHT - měřítkování V popisu k FFT jsem se nijak nezmínil o jedné docela podstatné operaci, kterou je třeba provádět při výpočtu ve fixed point (integer). Pokud mají mezivýsledky v jednotlivých etapách zůstat v mezích integeru, je třeba provádět průběžně měřítkování. Tímto tématem se detailně zabývá nejeden článek, nicméně většinou se použije prosté dělení dvěma. Nejlépe lze operaci pochopit na příkladu, takže uvádím příklad 16-bit implementace: #define N 16 // sin(k/N*pi), cos(k/N*pi), ... (k=0..N/2-1) [fx 1.15] int16 tsincos[N]={0, 32767, 12540, 30273, ...}; int16 FHTwarr[N]; void FHT(void) { int16 n=1; int16 angf=N/2; while(n<N) { int16 *ang=&tsincos[0]; int16 *A=&FHTwarr[0]; int16 *B=&FHTwarr[n]; int16 *C=&FHTwarr[n-1]; int16 *D=&FHTwarr[n*2-1]; // ang=0 for(k=0;k<angf;k++) { int16 b=*B/2; int16 a=*A/2; *A=a+b; *B=a-b; A+=n<<1; B+=n<<1; } A-=(N-1); B-=(N-1); // full butterflies while(n>2 && ang<n) { ang+=angf<<1; dsin=*ang[0]; dcos=*ang[1]; for(k=0;k<angf;k++) { E=(int16)((int32)*B*dcos + (int32)*D*dsin)/65536; F=(int16)((int32)*B*dsin - (int32)*D*dcos)/65536; int16 a=*A/2; int16 C=*C/2; *A=a+E; *B=a-E; *C=c+F; *D=c-F; A+=n<<1; B+=n<<1; C+=n<<1; D+=n<<1; } A-=(N-1); B-=(N-1); C-=(N+1); D-=(N+1); } // ang=pi/2 for(k=0;n>1 && k<angf;k++) { int16 c=*C/2; int16 d=*D/2; *C=c+d; *D=c-d; C+=n<<1; D+=n<<1; } n<<=1; angf>>=1; } } Pro zvýšení odstupu signál/šum je navíc možné doplnit zaokrouhlování, ale to se na úrovni C děla špatně, takže není součástí kódu. Alternativně by také bylo možné dočasně počítat ve vyšší přesnosti, protože při operaci (a/2+b/2) oproti (a+b)/2 dojde k chybám. Pro mírný nárůst výkonu lze nahradit dělení rotacemi vpravo, ale samozřejmě se to podepíše na přesnosti, protože aritmetická rotace vpravo není symetrická kolem nuly. Takto vniklé chyby porostou s každou další etapou algoritmu. Chytrý překldač však převede dělení dvěma na rotaci s korekcí, takže nároky nebudou nijak drastické, ale např. zrovna AVRGCC se s tím nezatěžoval a práskl mi tam 16/16-bit dělení. :-) Holt nic není dokonalé. 1.5 Zdroje Zde je pár docela dobrých zdrojů nejen ohledně Hartleyovy transformace.
2. Realizace FHT na AVR (AVR GCC) Zapojení je identické se zapojením pro testování FFT implementace. Opět se jedná o ATmega644 na 16MHz, komunikace přes USART na 2Mbd, bez parity a řízení toku. Pokud nevadí dlouhá odezva, pak lze zpomalit na takty vhodné pro COM port. Po odladění jsem provedl nějaká ta měření výkonu. Dle očekávání šlape algoritmus podstatně rychleji. Výsledky jsou uvedeny v následujících tabulkách: Pro FHT už více času zabere výpočet amplitudového spektra, než vlastní FHT algoritmus. Výkon by se dal dost zvýšit použitím interpolované odmocniny, ale pro objektivní srovnání s FFT implementací jsem použil přesnou 32-bit odmocninu. Pro srovnání s FFT jsem zvolil nastavení FFTopt=1, FFTprec=1 a lineární spektrum, což asi nejvíce odpovídá použitým optimalizacím u FHT: Samotná transformace je i přes komplikace s nedostatkem pracovních registrů a složitějším přstupem k RAM téměř dvakrát rychlejší, ale celkový výkon dost sráží zmněný výpočet amplitudového spektra (32-bit odmocnina):
Výkon v tomto případě vzrostl proti původní FFT asi na 140% pro N=256.
Pro spektrum v logaritmickém měřítku se zpomalení výpočtem spektra až tak neprojeví, protože jsem ho vyřešil přes lookup
tabulky s interpolací (cca 4x rychleší, než 32-bit odmocnina). Zde je nárůst výkonu asi na 160% pro N=256. 2.1 Testovací program pro PC Komunikace by měla být kompatibilní s programem pro testování FFT implementace. V souboru "config.ini" stačí nastavit číslo portu a baudrate a mělo by to chodit. Program ke stažení: AVR_FFT_tester_v3.zip (365kB). 2.2 AVRFHT pro AVR GCC K vlastní implementaci AVR FFT patří pouze následující soubory:
Zbytek je součást ukázkového programu. 2.2.1 Modul "FHT_routines.s" Jde o hlavní modul celé FHT implementace. Jsou zde funkce: void FHT_input(int16_t *src); provádějící okéknování a bitovou reverzi adres ze zdrojových dat src. Na rozdíl od původní FFT implementace musí být zdrojová data jinde, než v globálním pracovním poli FHTwarr[]! Pokud jsou vzorky pro zpracování získávány průběžně např. z ADC, tak lze adaptovat a použít C funkci z modulu "FHT.c" provádějící totéž. Dále je zde funkce: void FHT_do(void); provádějící vlastní FHT transformaci a na konec: void FHT_out_lin(void); void FHT_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: int16_t FHTwarr[FHTN]; Použití FHT knihovny je zhruba následující:
Pokud jde o možnosti nastavení algoritmu, tak jsou značně menší, něž pro FFT:
K tomuto modulu navíc přímo patří lookup tabulky "FHT_lookup.inc", vyšší makra "FHT_macros.inc" a základní makra "macros.inc" (společná pro všechny assemblerovské soubory). 2.2.2 Modul "FHT_tables.inc" Zde jsou umístěny všechny lookup tabulky FHT algoritmu. Jednak je zde tabulka pro bitovou reverzi adres TFHTbrv, kde jsou cílové relativní adresy udané v bytech. Dále je zde předpočítaný sin() a cos() (TFHTsico) v rozsahu 0 až 90° normalizovaný na amplitudu 32768 místo 32767 protože zde funkce nabývá jen kladních hodnot. Na konec jsou zde tabulky oken TFHTwdw. U znaménkových oken (např. Flattop) musí být navíc definováno: .equ FHTWS,1 Tabulky jsem předgeneroval jen pro rozsah 32 až 512 bodů, pro větší není implementace stavěná, menší lze vygenerovat pomocí utilitky: AVR_FFT_lookup.zip (67kB) 2.2.3 Modul "FHT_math.s" Tento modul obsahuje aritmetiku použitelnou FFT/FHT algoritmem. Předně je zde rutinka 32-bit odmocniny od ChanN FFT_sqrt32c_asm (není přímo volatelná jako GCC 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 FHT algoritmu bez zbytečné omáčky (zálohy 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_s32log10f(uint32_t x);
Tato funkce slučuje výpočet 32-bit odmocniny s logaritmováním do jedné funkce, což je změna
proti verzi použité u FFT. Provádí následující výpočet:
Pokud by bylo třeba generovat lookup pro jiná okna (jiné xm), pak je k disopzici
utilitka pro jejich generování: sqrt32log10f_lookup.zip (64kB). 2.2.4 Modul "main.c" Zde je jednoduchá demonstrační aplikace zcela kompatibilní s FFT implementací. Jsou realizovány celkem tři příkazy. Jednak samotný výpočet FFT s měřením počtu cyklů (příkaz 'F'), dále testovací smyčka pro algoritmus odmocniny sqrt32f() (příkaz 'S') a nakonec testovací smyčka pro oveřování algoritmu pro logaritmování amplitudového spektra log10f() (příkaz 'L').
Příkaz 'F':
Příkaz 'S':
Příkaz 'L':
Na ostatní příkazy reaguje MCU chybovým znakem 'E'. (c) 2012, Stanislav Mašláň
|