SW emulace sběrnice I2S pro audio DA převodníky
Při pokusech s audio výstupem na malých MCU existuje několik možností, jak zrealizovat D/A převodník. Velmi často používaný je PWM kanál, který spolu s dolní propustí dává poměrně poslouchatelné výsledky a je velmi levný. Pro kvalitnější výstup lze použít externí D/A převodník. Nabízí se zde sice nejeden převodník s paralelním vstupem, ale zabere příliš mnoho pinů (pro více než 8 bitů). Na relativně výkonných jednočipech AVR naštěstí lze provozovat i klasické audio D/A převodníky se sériovou I2S sběrnicí. Nejznámější typy, ketré nevyžadují MCLK jsou TDA1543 (I2S sběrnice) nebo kvalitnější TDA1545 (dle datasheetu "sběrnice kompatibilní s většinou japonských audio formátů: časový multiplex, TTL, dvojkový doplněk"). Oba lze získat i ze starých CD mechanik nebo přehrávačů. Existuje sice řada dalších typů, ale to jsou téměř výhradně "oversampling" převodníky, kde je potřeba nepřetržitě generovat MCLK (master clock), a hlavně odesílat data synchronně s ním na nějakém zlomku MCLK taktu. To je podle mého názoru zcela mimo možnosti AVR i jiných MCU, kde I2S sběrnice není řešena HW cestou. Naproti tomu dva výše uvedené typy se nechají krmit daty prakticky v libovolných intervalech. Je to skoro jako pracovat s běžným SPI D/A převodníkem. V kombinaci s vyrovnávací pamětí (FIFO) lze pomocí těchto převodníků získat kvalitní audio výstup při relativně malém vytížení AVRka. 1. TDA1543 D/A komunikace
TDA1543 je 16-ti bitový stereo D/A převodník. Výstupy jsou proudové, takže je třeba doplnit ještě převodník proud-napětí s dolní propustí. Maximální vzorkovací kmitočet je 192kHz, kmitočet hodin sběrnice I2S je až 9.2MHz (fungoval i na 10MHz). Převodník je napájen jediným napětím 5V (měl by jet i na 3V3), odběr je poměrně vysoký - asi 50mA. Formát I2S sběrnice je naznačen v grafu. Jak je vidět, 16-ti bitový vzorek ve formátu dvojkového doplňku je odesílán od nejvýznamnějšího bitu (MSB first), data jsou čteny při vzestupné hraně hodin. Signál WS slouží k synchronizaci a multiplexování kanálů. Jeho hodnota se musí změnit vždy PŘED odesláním posledního bitu vzorku, což nepříjemně komplikuje emulaci. Na stejný kanál D/A nelze odeslat více vzorků po sobě. 2. TDA1545 D/A komunikace
TDA1545 je velice podobný předchozímu, ale mnohem kvalitnější (alespoň dle datasheetu :-). Může pracovat jen na 5V napájení a jeho odběr by měl být jen 6mA pro fullscale na obou výstupech. Má nižší zkreslení a nižší šum. Vzorkovací kmitočet může být až 384kHz, kmitočet hodin až 18.4MHz. Vstupní formát zřejmě nemá žádný název. Dle datasheetu "kompatibilní s většinou japonských audio formátů: časový multiplex, TTL, dvojkový doplněk". Z grafu je vidět, že jde vlastně o identický formát, jako I2S, pouze WS signál pro multiplex kanálů je zpozděn o 1 clock - snáze se to realizuje. 3. Vyrovnávací paměť FIFO
Samotná emulace I2S je celkem k ničemu, pokud MCU musí audio data teprve od někud získat nebo
dokonce sám generovat. To zabere poměrně hodně strojového času. Často mnohonásobně víc,
než jedna vzorkovací perioda. V průměru sice audio signál musí být generován dostatečně rychle pro přehrávání na daném
vzorkovacím kmitočtu, ale
některé operace krátkodobě brání odesílání vzorků do D/A, což se podepíše na kvalitě výstupu.
Proto je třeba nějakým způsobem přenechat odesílání vzorků na přerušení procesoru, které může téměř libovolně a
hlavně pravidelně přerušovat vlastní generování signálu v hlavní programové smyčce. Je zřejmé,
že přerušení nemůže odeslat do D/A vzorek, který se právě počítá, případně ještě ani nezačal počítat.
Proto je třeba vytvořit vyrovnávací paměť, do které se v hlavní smyčce ukládají vypočítané vzorky maximální možnou rychlostí
a na druhé straně procesor přes přerušení tyto vzorky v pravidelných intervalech zase vyčítá a posílá do D/A převodníku.
Tímto jednoduchým postupem je zajištěno, že i když rychlost generování signálu kolísá, na D/A převodník data přicházejí pravidelně. Tedy
pokud se vyrovnávací paměť zcela nevyprázdní. Pak dojde k podtečení a opětovnému přehrání obsahu paměťi. Zjednodušeně řečeno je audio FIFO "černou skříňka", do které jednou stranou ukládáme data v náhodných intervalech (pokud je v ní místo) a druhou stranou v pravidelných intervalech vybíráme. Toto platí pro případ D/A, ale samozřejmě to může fungovat i opačně. Třeba z A/D převodníku dostáváme pravidleně data a na výstupu FIFO je zpracováváme jak právě zbývá MCU čas. Zároveň je také zřejmé, že FIFO působí jako zpožďovací linka. Velikost zpoždění je přímo uměrná velikosti FIFO a nepřímo úměrna datovému toku skrz FIFO. To samozřejmě platí pro případ, že stav FIFO je více méně neměnný a téměř maximální. Obvykle toto proměnlivé zpoždění nepředstavuje problém. FIFO buffery se sice vyrábí i jako samostatné obvody, ale pro tyto účely jsou nevhodné. Daleko častější jsou FIFO buffery vytvořené v RAM. Jde o velmi jednoduchý princip - vyhradí se potřebný úsek RAM a vytvoří se 2 pointery. Jeden pro zápis a druhý pro čtení dat. Takto vytvořené FIFO pracuje jako kruhový buffer. Jakmile kterýkoliv z pointerů přesáhne horní hranici vyhrazené paměti, resetuje se zase na její začátek. Banální, ale důležitou operací je zjištění volného prostoru ve FIFO. To lze provést prostým odečtením pointerů. V případě, že jejich pozice je taková, že vyjde záporné číslo, přičte se k němu velikost FIFO. 4. Emulace popsaných sběrnic na AVR
Jak jsem už naznačil v úvodu, AVR jsou dostatečně
výkonné, aby na vzorkovacím kmitočtu 22050Hz pseudo DMA výstup na tyto D/A zabíral jen asi 13% při taktu 16MHz.
Jako MCU jsem pro tuto ukázku zvolil můj oblíbený ATmega32. Nechtěl jsem zapojení nijak zvlášť komplikovat, takže audio není v žádné externí paměti, ale
přijímá se přes virtuální COM port realizovaný pomocí FT232. Navíc je zde doplněno LCD, ale není nutné ho připojovat. Zobrazuje mono/stereo a vzorkovací kmitočet. Navíc je zde rotační enkodér pro změnu vzorkovací frekvence.
K emulaci I2S jsem použil SPI interface taktovaný na maximálních 8MHz, takže odeslání jednoho bytu trvá jen 16 cyklů. Odesílá se vždy jeden stereo
vozrek najednou. Vlastní rutinu jsem vyřešil jako pseudo DMA. Je volaná timerem
2 nastaveným na vzorkovací kmitočet. Vzorky jsou odebírány z vyrovnávací paměti
nastavené na maximum co se vejde (1900B). Podtečení FIFO není nijak kontrolováno - při zastavení toku dat do FIFO se opakuje poslední úsek stále dokola.
Všechny operace jsem se snažil dostat do doby, kdy by MCU pouze čekal na odeslání jednotlivých bytů, takže je tato rutinka poměrně rychlá.
Navíc místo push/pop jsem použil in/out do nevyužitých registrů (mírné zrychlení). Přes preprocesor si lze vybrat variantu pro TDA1545 nebo TDA1543 a
variantu mono/stereo. V případě mono je vždy 1 vzorek odeslán na oba kanály a
ušetří se tak polovina paměti pro FIFO. Pak je zde ještě varianta, která
umožňuje přepínat mono/stereo za běhu programu pomocí proměnné I2Schannels. To
samozřejmě zabere pár cyklů navíc, ale je to ještě únosné.
Jednoduchý ovládací prográmek můžete stáhnout
zde. V ini souboru
je třeba nastavit číslo portu, kde je připojený FT232 převodník. V ovladačích
virtuálního COM portu je ještě vhodné nastavit latency na 2ms (nebo 1ms na strojích od 1GHz - na pomalejších dojde spíš ke zpomalení toku dat). 5. I2S příklad pro AVRGCC
Protože jsem se nedávno konečně dokopal dělat něco na AVR i v C,
rozhodl jsem se, že tohle by mohl být dobrý testovací prográmek, kde si ověřím,
jak moc neefektivní AVRGCC je. Formát je rozpoznán automaticky z RIFF konzervy a pokud není podporován, měla by být vypsána chyba a zbytek toku bude ignorován. Problém s identifikací by mohl nastat u WAV souborů, kde je "data" subchunk v souboru za hranící 0x0078. To je sice vzácná situace, ale stát se to může. Někdy jsou totiž v souboru i tagy a občas bohužel i na začátku. Program je vybaven i timeoutem, takže pokud terminál déle než cca 750ms neodešle data, bude přehrávání ukončeno. Prográmek jsem tentokrát navíc vybavil počitadlem bytů, takže po příjmu posledního bytu vlastních audio dat ("data" subchunk RIFF konzervy) je audio FIFO naplněn nulami. Tím by se mělo přehrávání nehlučně ukončit a zbytek streamu (obvykle "LIST" subchunk), bude ignorován.
Dle očekávání je sice GCC v porovnání s assemblerem (mým assemblerem :-) citelně pomalejší, ale ne zase nějak nepřekonatelně. Zpravidla se
překladači dalo rozumným přeskládáním kódu a změnou zápisu pomoct alespoň k trochu rozumnému výstupu. Pochopitelně samotná
I2S rutina musela zůstat v assembleru jednak kvůli přesnému časování a pak samozřejmě i kvůli efektivitě. Vzhledem k tomu, že
I2S výstup přerušuje příjem dat, tak každý cyklus navíc zvyšuje šanci, že dvou bytový USART FIFO, kterým je AVR na příjimací straně vybaveno, přeteče.
Překvapivě to tentokrát šlo i bez příjmu dat v přerušení. Sice opět musím zkoušet vyčíst druhý byte zkrácenou cestou, ale nepostřehl jsem, že
by to přeteklo, takže to asi bude OK. 5.1. Pár poznatků okolo AVRGCC
Když jsem před pár lety prvně použil Keil C51, tak jsem se ho naučil používat celkem rychle (můj první vyšší jazyk pro jednočipy).
Rozhodně jsem nenarazil na obtíže s optimalizacemi a spol. Stačilo správně používat "volatile".
Předpokládal jsem, že s GCC to nebude nijak výrazně složitější, ale bohužel je.
Z toho důvodu jsem se rozhodl shrnout pár poznatků z práce na tomto mini projektu. Pro toho, kdo s GCC už pracuje jistě žádné
překvapení, ale začátečníkům, jako jsem já, by se to mohlo hodit. ... .def R16H=R17 .def R16B3=R18 .def R19H=R20 .def R19B3=R21 ... .macro addt add @0,@1 adc @0H,@1H adc @0B3,@1B3 .endm addt R16,R19 Bude automaticky rozvinuto do: add R16,R19 adc R17,R20 adc R18,R21 Bohužel chtít něco takto jednoduchého po GNU-as se ukázalo jako téměř neřešitelný problém. Už jsem to skoro chtěl vzdát, ale naštěstí jsem byl na fóru odkázán na nějaký *.h soubor z LibC, kde bylo toto řešeno iterační metodou. No ... málem jsem z té překomplikované hrůzy spadl ze židle, ale pak mi došlo, že to asi jinak nepůjde, takže problém jsem vyřešil takto: .macro addt ra,rb .findreg_ra=-1 .findreg_rb=-1 .findreg_cnt=0 .irp regn,R0,R1,R2,R3,R4,R5,R6,R7,R8,R9,R10,R11 \ R12,R13,R14,R15,R16,R17,R18,R19,R20,R21 \ R22,R23,R24,R25,R26,R27,R28,R29,R30,R31 .ifc \regn,\ra .findreg_ra=.findreg_cnt .endif .ifc \regn,\rb .findreg_rb=.findreg_cnt .endif .findreg_cnt=.findreg_cnt+1 .endr add (.findreg_ra),(.findreg_rb) adc (.findreg_ra)+1,(.findreg_rb)+1 adc (.findreg_ra)+2,(.findreg_rb)+2 .endm
Vypadá to hrozně a o rychlosti překladu při delším zdrojáku raději nepřemýšlím, ale alespoň to funguje.
Smysl onoho kódu je iterační cestou přes ".irp" direktivu identifikovat pořadové číslo registru. AVR verze překladače naštěstí akceptuje
nejen názvy registrů, ale i jejich pořadová čísla, takže dále už to bylo snadné (jednoduché řesení R16+n bohužel použít nelze).
Pochopitelně to bylo potřeba provést se všemi makry, takže jsem k převodu mých starých maker napsal prográmek, který třeba časem zveřejním.
Převedená makra jsou součástí projektu, takže pokud má někdo zájem, lze použít, ale netestoval jsem rozhodně všechny. V C-čkové části programu jsem narazil především na typické obtíže s optimalizacemi. Např. jsem si naivně myslel, že když použiji následující kód: volatile uint8_t *rd; cli(); rd=FIFOrd; sei(); // calc free space int16_t n=rd-FIFOwr; if(n<0) n+=FIFOsize; tak i po optimalizaci dostanu něco jako: lds R22,FIFOwr lds R23,FIFOwr+1 ... cli lds R24,FIFOrd lds R25,FIFOrd+1 sei sub R24,R22 sbc R25,R23 brcc .+4 subi R24,lo8(-FIFOsize) subi R25,hi8(-FIFOsize) Jo, to byla zase naivita. :-) Optimalizátor s klidným svědomím vygeneroval zhruba toto: lds R22,FIFOwr lds R23,FIFOwr+1 ... lds R24,FIFOrd lds R25,FIFOrd+1 sub R24,R22 sbc R25,R23 sbrs R25,7 rjmp .+4 subi R24,lo8(-FIFOsize) subi R25,hi8(-FIFOsize) ... cli sei Ano, tam je mi to zablokované přerušení skutečně hodně platné. Vyřešil jsem teprve až po "konzultaci" s "atomic.h": volatile uint8_t *rd; asm volatile("cli" ::: "memory"); rd=FIFOrd; asm volatile("sei" ::: "memory"); // calc free space int16_t n=rd-FIFOwr; if(n<0) n+=FIFOsize; Parametr "memory" zjevně překladač nutí provést kód přesně tak,
jak je - zakazuje mu "vyoptimalizovat" načtení sdílené proměnné "FIFOrd" někam jinam. Sice jsem měl za to, že to platí jen uvnitř jednoho asm() bloku,
ale asi to funguje i takto. Alternativně (a možna i přednostně) lze
použít makro ATOMIC_BLOCK() z "atomic.h". Vypadá to pak trochu čitelněji a zřejmě je to pak i multiplatformní. (c) 2007-2012, Stanislav Mašláň - Všechna práva vyhrazena.
|