DA převodník (též DAC jako Digital Analog Converter) není na čipech AVR úplnou novinkou, najít jste ho mohli na "automotive" řadě Atmega (např. Atmega32M1). Každopádně pro většinu uživatelů starých AVR to bude něco nového. DAC bývá úzce spjat s vnější elektronikou a já bych teď nerad zabíhal do tak široké problematiky (protože kdejaký analogový obvod napojený na převodník by vydal na celý článek). Budu se držet jen toho co máme na čipu - tedy 8bitového DAC s výstupním bufferem. Pojďme se mu podívat na zoubek a zjistit co dokáže a co už ne.
Náš Atmel má slušnou paletu vnitřních referencí. Máme k dispozici napětí 0.55V, 1.1V, 1.5V, 2.5V a 4.34V a slouží pro účely AD převodníku, DA převodníku a komparátoru. Reference pro DAC je sdružená i pro komparátor, nemůžete tedy volit každému z nich jinou. Výběr provádíte skupinou bitů DAC0REFSEL v registru VREF.CTRLA. K provozu vybrané reference potřebujete napájecí napětí alespoň o půl voltu vyšší (uvádí datasheet). Ale to asi tak nějak tušíte, že s 1.8V napájením nepůjde provozovat 2.5V referenci. V registru VREF.CTRLB můžete aktivovat referenci trvale. Běžně se totiž reference zapíná teprve až spustíte nějakou periferii, která ji vyžaduje. Tento mechanismus slouží ke snižování spotřeby. Rozběh reference ale nějakou dobu trvá. Datasheet uvádí typickou hodnotu 25us. Já se doměřil k informaci, že referenční napětí nabíhá tempem přibližně 0.27V/us směrem nahoru a tempem 0.48V/us směrem dolů. Domnívám se tedy, že reálná doba potřebná k naběhnutí 0.55V reference se pohybuje někde mezi 2-5us. Rozběh na nejvyšší referenční napětí 4.34V pak můžeme očekávat za 20-25us. Pokud neplánujete reference přepínat, nemusí vás to moc zajímat, prostě po jejím zapnutí počkáte uvedených 25us a pak už to můžete pustit z hlavy. Někdy ale budete referenci měnit v průběhu práce a co hůř, budete ji chtít vyměnit rychle. Pak můžete čekací interval zkrátit díky informaci o tempu jakým se reference mění. Například pokud budu měnit referenci z 1.5V na 2.5V (například abych si zvětšil měřicí rozsah AD převodníku) stačí mi počkat (2.5-1.5)/0.27 tedy nějaké 4us. A podobně, pokud budu měnit referenci ze 2.5V na 1.1V, stačí si počkat přibližně 3us.
Zmínit bychom měli také přesnost a stabilitu referencí (od toho se taky odvíjí přesnost a stabilita celého DAC). Datasheet je ve specifikacích přesnosti hodně konzervativní a operuje s rozptylem +-3% a +-5%. Což při teplotním rozsahu -40 až 125°C asi bude pravda. Přece jen značná část aplikací bude žít při pokojových teplotách a tam je realita o poznání růžovější. Na mém kusu jsem se doměřil k hodnotám o víc jak řád lepším. Při pokojových teplotách se všechny hodnoty držely s chybou pod 0.2%. Pro 8bit DAC je to více než dobré. Abych zjistil jak moc referenční napětí plavou s teplotou, položil jsem čip milimetr nad 20W topný rezistor, trochu ho zahřál a sledoval jak se mění výstupní napětí DAC. Nejde zrovna o exaktní měření, ale teplotní výkyv určitě překročil 10°C. Tímto zásahem výstupní napětí DAC (a domnívám se že všech referencí) shodně vzrostlo o 0.7%. To se mi zdá v rozporu s datasheetem, ten totiž uvádí, že se v plném pásmu teplot (-40 až 120°C) reference nezmění víc jak o nějaké 0.4%. A v oblasti našeho pokusu od 20°C do 60°C by měla být ještě stabilnější (grafy v kapitole 35.3 VREF Characteristics v datasheetu). Takže pozor na to !
Ovládání DAC je triviální. V registru DAC0.CTRLA jsou jen tři kontrolní bity.
První test kterému DAC podrobíme bude test "statické přesnosti". Prostě se podíváme jak moc dobře výstupní napětí sedí s vypočteným. Test jsem provedl s 2.5V referencí s nezatíženým výstupem a se zátěží 10kOhm proti GND. Výsledky se zdají vynikající. Graf níže ukazuje odchylku v mV od vypočtené hodnoty pro různé vstupní kódy. Odchylka nepřekročila ani se zátěží cca 2.5mV, tedy drží se pod 0.25LSB. To není špatné.
Jistě jste si všimli, že zatížením výstupu napětí trochu kleslo. Nabízí se tedy otázka jak moc lze výstup DAC zatěžovat ? Datasheet tvrdí, že výstupní odpor by měl být větší než 5kOhm. Doporučuje nám tedy udržet výstupní proud pod 1mA. To je vcelku standardní, ale v rámci testů dáme DAC trochu zabrat a načrtneme si zatěžovací křivku. Pro test jsem zvolil maximální možné výstupní napětí (4.34V) protože to je na zatěžování nejnáchylnější. Na grafu můžete vidět, že datasheet si moc rezervy nenechává (a to je dobře), při zátěži okolo 2kOhm výstupní napětí klesne o 0.5LSB (o půl dílku). Zatěžování odporem menším jak 1kOhm už výstupní napětí mění natolik, že to nelze tolerovat.
Z dynamických vlastností stojí za to vyzkoušet si rychlost přeběhu. Ta vám napoví za jak dlouho můžete na výstupu očekávat zvolenou hodnotu. Provedl jsem tedy jednoduchý test. Nastavil jsem referenci 2.5V, výstup DAC jsem zatížil odporem 10kOhm proti GND a vygeneroval testovací "obrazec". Z oscilogramů můžete vidět, že rychlost přeběhu je něco málo přes 2V/us. V nejhorším případě (tedy s referencí 4.34V) a přechodem z nuly do maxima nebo opačně počítejte s dobou 2us.
V prvním příkladu si zkusíme vygenerovat sinusový průběh s amplitudou 2.5V o různých frekvencích. Využijeme k tomu časovač TCA0 v jehož rutině přerušení budeme nahrávat nová data do DAC. Než se do toho pustíme, vyřešíme ale pár teoretických problémů. Nejprve otázku jak zjistit hodnoty napětí v jednotlivých časech tak aby vznikal právě sinus. První vás asi napadne si je prostě průběžně počítat pomocí čísel s plovoucí desetinnou tečkou (float). Toto řešení je ale pro 8bit MCU značně náročný úkol a výpočty by nejspíš trvaly příliš dlouho. Druhou možností je uložit celý průběh do tabulky. Můžeme si předem vypočítat všechny napěťové hodnoty do konstantního pole (tedy do paměti flash) a odtud už je bez potřeby dalších výpočtů vyčítat. Dokonce se nabízí možnost uložit si tabulku jen s jednou čtvrtinou průběhu a využít toho že sinus je symetrický a ušetřit si tak trochu paměti. V našem příkladu si připravíme celou tabulku, neboť tento přístup umožňuje vytvářet naprosto libovolné průběhy.
Výsledný průběh bude vždy "schodovitý" a je tedy v našem zájmu udržovat amplitudu co nejblíže některé z referencí DAC. Tedy něco málo pod 0.5,1.1,1.5,2.5 nebo 4.3V. Proč ? No zvolíme-li si jako referenci například 4.3V a rozhodneme-li se generovat sinus o amplitudě 4V, máme k dispozici celkem 236 napěťových kroků. Pokud ale budeme se stejnou referencí generovat signál s amplitudou 0.5V, zbude nám jen 30 vertikálních kroků a sinus bude chtě nechtě katastrofálně kostrbatý. Obecně je tedy velmi vhodné generovat průběh o pevné amplitudě (blízké některé z referencí) a případné její snižování či zvyšování provádět až vně čipu (například pomocí operačních zesilovačů, digitálních potenciometrů atd.). Pokud je frekvence signálu neměnná, může být užitečné schody vyhladit malým RC článkem (typu dolní propust).
Dalším úskalím je rychlost generování. Průběh je tím čistší z čím více vzorků je složen. Sami srovnejte z oscilogramů níže, která ze sinusovek je "krásnější". Pro kvalitu signálu je vhodné vytvářet jej z co nejvíce vzorků. V této snaze nás ale krom vertikálního rozlišení (8bit) limitují další dva faktory. V prvé řadě je to paměť. Attiny416 má 4kB paměti, takže tam nevleze záznam ani 4000 vzorků (u větších čipů jako třeba Attiny32xx je situace o poznání lepší). Dalším limitem je výpočetní čas. Z tutoriálu o přerušení víme, že při plném výkonu 20MHz trvá vstup do rutiny přerušení přibližně 1us. Další 1us si necháme pro zpracování a nahrání dat do DAC a další 2us si necháme jako rezervu aby mohl čip dělat i nějakou jinou činnost. Potřebujeme tedy okolo 4us na jeden vzorek. Čím vyšší frekvenci a čím větší počet vzorků chceme, tím méně času na jeden vzorek nám zbývá. Například při 256 vzorcích na periodu, nemůžeme generovat průběh s vyšší frekvencí než přibližně 976Hz. Pro vyšší frekvence musíme buď zkrátit dobu zpracování (tam nějaký malý prostor ještě je) a nebo zmenšit počet vzorků (na úkor čistoty signálu). Ve výjimečných situacích, kdy nemá čip na starosti nic jiného, můžeme generovat průběh přímo v hlavní smyčce a nevyužívat přerušení a dosáhnout tak na vyšší frekvence (za cenu 100% zahlcení čipu).
Jak už bylo řečeno, náš program má za úkol generovat sinusový průběh z tabulky o 256ti hodnotách. Tabulku (konstantní pole) sine[] jsem připravil pomocí skriptu v Octave/Matlab. K časování poslouží přerušení od přetečení timeru TCA0. Stropem TCA0 nastavujeme periodu jednoho vzorku. Nízké frekvence (50Hz a podobné) půjdou nastavit docela přesně. Vysoké frekvence při kterých budeme strop timeru nastavovat na nízké hodnoty, budou mít větší chybu a nepřesnost celkové frekvence bude vyšší (což je teorie spíš timerů než DAC). V našem příkladě bude zapnuté jen přerušení od TCA, pokud ale plánujete příklad upravit a zapnout další přerušení, zvažte zda nedat tomu od timeru vysokou prioritu. Přece jen pro čistý výstupní signál je potřeba přesné časování a není žádoucí aby jiná přerušení mohla zpozdit okamžik DAC převodu. Abychom měli hrubou představu o tom kolik času nám celé přerušení zabírá a do jaké míry tedy vytěžuje MCU, budeme si na PA4 značit aktivitu.
/* tutorial TinyAVR 1-Series * DAC 1 * generujeme sinusový průběh (na PA6) pomocí přerušení * Na PA4 přibližně signalizujeme aktivitu MCU v rutině přerušení */ /* Ve fuses zvolen 20MHz oscilátor (lze volit ještě 16MHz) */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #include <avr/interrupt.h> #define PERIOD 1562 // 20MHz/50/256 void clock_20MHz(void); void init_dac(void); void init_tca(void); // tabulka hodnot pro sinus const uint8_t sine[256]={ 128,131,134,137,140,143,146,149, 152,155,158,162,165,167,170,173, 176,179,182,185,188,190,193,196, 198,201,203,206,208,211,213,215, 218,220,222,224,226,228,230,232, 234,235,237,238,240,241,243,244, 245,246,248,249,250,250,251,252, 253,253,254,254,254,255,255,255, 255,255,255,255,254,254,254,253, 253,252,251,250,250,249,248,246, 245,244,243,241,240,238,237,235, 234,232,230,228,226,224,222,220, 218,215,213,211,208,206,203,201, 198,196,193,190,188,185,182,179, 176,173,170,167,165,162,158,155, 152,149,146,143,140,137,134,131, 128,124,121,118,115,112,109,106, 103,100,97,93,90,88,85,82, 79,76,73,70,67,65,62,59, 57,54,52,49,47,44,42,40, 37,35,33,31,29,27,25,23, 21,20,18,17,15,14,12,11, 10,9,7,6,5,5,4,3, 2,2,1,1,1,0,0,0, 0,0,0,0,1,1,1,2, 2,3,4,5,5,6,7,9, 10,11,12,14,15,17,18,20, 21,23,25,27,29,31,33,35, 37,40,42,44,47,49,52,54, 57,59,62,65,67,70,73,76, 79,82,85,88,90,93,97,100, 103,106,109,112,115,118,121,124, }; int main(void){ clock_20MHz(); // jedeme na plný výkon 20MHz init_dac(); // konfigurace DAC sei(); // povolíme přerušení PORTA.DIRSET = PIN4_bm; // výstup PA4 - poslouží jako pomocná indikace init_tca(); // spustíme časování while (1){ // ... není co dělat } } // periodické přerušení od TCA0 (od přetečení) ISR(TCA0_OVF_vect){ static uint8_t idx=0; // index procházející tabulku PORTA.OUTSET = PIN4_bm; // indikujeme na PA4 vstup do rutiny přerušení DAC0.DATA = sine[idx]; // pošleme na výstup DAC novou hodnotu TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // vyčistíme vlajku v timeru idx++; // posuneme index (příště chceme novou hodnotu z pole) PORTA.OUTCLR = PIN4_bm; // indikujeme na PA4 výstup z rutiny přerušení } void init_dac(void){ VREF.CTRLA = VREF_DAC0REFSEL_2V5_gc; // zapneme DAC 2.5V referenci _delay_us(25); // čas pro stabilizaci reference DAC0.CTRLA = DAC_ENABLE_bm | DAC_OUTEN_bm; // spustíme DAC a připojíme mu jeho výstup (PA6) } void init_tca(void){ TCA0.SINGLE.PER = PERIOD; // perioda timeru (~78us) TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc; // režim normal (tedy žádné PWM, jen pouhé časování) TCA0.SINGLE.INTCTRL = TCA_SINGLE_OVF_bm; // povolíme přerušení od přetečení // spustíme timer s clockem 20MHz TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm; } // Nastaví clock 20MHz (z interního 20MHz bez děličky) void clock_20MHz(void){ // v případě potřeby zde dočasně vypněte přerušení CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLA = CLKCTRL_CLKSEL_OSC20M_gc; // vybírá 20MHz oscilátor CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLB = 0; // vypne prescaler (děličku) }
Abychom tutoriál trochu obohatili, připravíme ještě jeden pokus. Vygenerujeme jednorázový impulz s co největším "saple rate" (tedy s co nejvyšším počtem vzorků za sekundu). Generování nám naprosto zahltí MCU, takže během něj nebudeme moct dělat cokoli jiného. Čas mezi vzorky zkrátíme pod 1us, tedy pokusíme se překročit 1Msps (násobně víc než má DAC papírově zvládat). To bychom si nemohli dovolit, pokud by vytvářený signál obsahoval velké skoky (rychlost přeběhu 2V/us nám neumožňuje provést během 1us například 2V skok). V našem příkladu ale vytvoříme gaussovský impulz ve kterém takové skoky nejsou. Pro ověření si na pinu PA4 změříme jak dlouho se signál generoval. Tvar pulzu opět připravím skriptem v Octave/Matlab a kvůli přehlednosti a snadné úpravě ho do programu vložíme v samostatném souboru table.h. Kvůli maximální rychlosti překládám program s optimalizací -O3.
/* tutorial TinyAVR 1-Series * DAC 2 * jednorázový průběh s co nejvyšším "sample rate" (okolo 2Msps) * průběh je uložen v tabulce table.h a generujeme ho na PA6 * na PA4 si přivádíme informaci o čase potřebném pro vygenerování celého pulzu * program překládám s optimalizací -O3 */ /* Ve fuses zvolen 20MHz oscilátor */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #include "table.h" // obsahuje tabulku s hodnotami pro DAC extern const uint8_t table[]; // tabulka s hodnotami ze souboru table.h void clock_20MHz(void); void init_dac(void); void init_tca(void); uint16_t idx; // index který prochází tabulku s daty int main(void){ clock_20MHz(); // jedeme na plný výkon 20MHz init_dac(); // konfigurace DAC (reference 4.34V) PORTA.DIRSET = PIN4_bm; // informační výstup PA4 while (1){ idx=sizeof(table)-1; // procházíme tabulku od konce PORTA.OUTSET = PIN4_bm; // začínáme generovat while(idx){ // dokud nejsme na konci dat DAC0.DATA = table[idx]; // nakrmíme DAC novou hodnotou idx--; // dekrementujeme index } PORTA.OUTCLR = PIN4_bm; // konec pulzu _delay_ms(100); // po skončení celého průběhu si počkáme 0.1s } } void init_dac(void){ VREF.CTRLA = VREF_DAC0REFSEL_4V34_gc; // zapneme DAC 4.34V referenci _delay_us(25); // čas pro rozběh reference DAC0.CTRLA = DAC_ENABLE_bm | DAC_OUTEN_bm; // spustíme DAC a připojíme mu jeho výstup (PA6) } // Nastaví clock 20MHz (z interního 20MHz bez děličky) void clock_20MHz(void){ // v případě potřeby zde dočasně vypněte přerušení CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLA = CLKCTRL_CLKSEL_OSC20M_gc; // vypouští clock na PB5 (CLKOUT), vybírá 20MHz oscilátor CCP = CCP_IOREG_gc; // odemyká zápis do chráněného registru CLKCTRL.MCLKCTRLB = 0; // vypne prescaler (děličku) }
Na oscilogramech níže můžete vidět, že vygenerovat celou sekvenci 2048 vzorků trvalo přibližně 1012us. To odpovídá 0.5us na vzorek (tedy 10ti strojovým cyklům). Jen pro zajímavost jsem vygeneroval ještě jeden pulz s tabulkou délky 64 bodů (table2.h). Na něm jsou už patrné schody, což je neklamná známka toho, že nás stále nelimituje rychlost přeběhu.
Snad krom silné závislosti referenčního napětí na teplotě (které se budeme detailněji věnovat v díle o ADC) mě všechny vlastnosti DAC pozitivně překvapily. Doufám, že se vám dnešní "testování" DAC líbilo stejně jako mě a že se setkáme u dalších dílů.
Home
| V1.02 26.1.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /