Protože analogově-digitální převodník (ADC) na moderních AVR má nespočet funkcí, nebudu se pokoušet probrat je všechny najednou. V úvodu jen zmíním ty nejzajímavější z nich a k detailům se propracujeme skrze příklady v rámci několika tutoriálů. Převodník má rozlišení 10bitů a měl by zvládat až 115ksps (tisíc převodů za sekundu) s plným rozlišením. S redukovaným rozlišením na 8bitů je frekvence převodů vyšší a později zjistíme jak moc jde "přetaktovat". Měřit lze až 12 vstupů (podle počtu vývodů vašeho čipu), všechny jako "single-ended" tedy proti GND. Krom vstupů z vnějšku lze měřit napětí z vnitřního teplotního čidla, vnitřní referenci nebo výstup DAC. ADC umí sám výsledky akumulovat (sčítat) a provádět tak vlastně "oversampling" nebo průměrování". Převod můžete spouštět softwarově, eventem nebo může běžet trvale (free running). ADC umí sám porovnávat výsledky převodu se dvěma volitelnými hodnotami a informovat vás o vybrané události (například překročení některé z nich atd.). Přirozeně též umí volat přerušení a je schopen běžet v režimech spánku. K dispozici máte širokou paletu vnitřních napěťových referencí, které znáte z DAC (0.55V, 1.1V, 1.5V, 2.5V, 4.34V). Krom nich může být referencí i napájecí napětí (užitečné pro proporční měření například potenciometrů atd). U větších čipů jako je Attiny1616 a pod. můžete referenci přivést i z vnějšku. U attiny416 to možné není.
Než začnete číst, seznámím vás v krátkosti s obsahem. Nejprve si stručně projdeme ovládání ADC, poté si vyzkoušíme jednoduchý převod a využijeme "akumulaci" ke snížení šumu. Pak příklad upravíme a akumulaci využijeme ke zvýšení rozlišení. Následně si vyzkoušíme měřit teplotu čidlem MCP9701 a v závěru ověříme funkčnost window coparatoru.
Protože ovládání je vcelku přímočaré, okomentuji ovládací registry a budu doufat, že vše pochytíte v demonstračních příkladech.
V prvním příkladu si předvedeme jednoduché měření s pomocí vnitřní reference. Výsledek přepočítáme na napětí a pošleme UARTem do PC. Protože se AD převody a nepájivé pole nemají moc rády (kontaktní pole není příliš spolehlivé) budu příklady provádět na "studentském" přípravku. Čímž chci jen říct, že bude mít zapojení slušně vyřešené kontakty. Za pomoci reference TL431 si vytvořím stabilní napětí 2.495V a k němu připojím trimr 10kOhm, jehož jezdec přivedu na PA5 (AIN5) našeho jednočipu. Výstup trimru ještě "zafiltruji" kapacitou 100nF a poslouží jako "neznámé" napětí. TL431 je kvalitnější reference než ty vnitřní co jsou v Atmelu a kdybych mohl, rád bych ji použil i jako referenci pro AD převodník, ale to na Tiny416 nejde (větší Ttiny to umožňují). K měření tedy použiji vnitřní 2.5V referenci. Tu je potřeba vybrat v periferii VREF podobně jako jsme ji vybírali v tutoriálu o DAC. Platí pro ni stejné podmínky co se týče času stabilizace a podobně. Před měřením musíme ošetřit příslušný vstup (PA5). Je vhodné odpojit vstupní buffer (skrze registr PIN5CTRL a skupinu bitů ISC). Ten by mohl v extrémním případě ovlivňovat malým proudem měřený signál. Jako clock pro ADC zvolíme 20MHz/64 = 312kHz. Na převod nijak nespěchám, takže klidně můžu volit o něco menší nebo větší frekvenci. Ta by se podle datasheetu měla pohybovat mezi 200kHz až 1.5MHz pro reference větší jak 1.1V (což je náš případ). Od zvolené frekvence se odvíjí i vzorkovací čas, který je minimálně dvě periody. V našem případě tedy může být 2/312=6.4us. Já ale pro jistotu vzorkovací čas trošku natáhnu na (2+3)/312 = 16us. I když to v tomto případě asi nemá význam, neboť 100nF kondenzátor se bude jevit jako dostatečně tvrdý zdroj. Když už jsem se k tomu dostal, vyberu si i vzorkovací kondenzátor 5pF (datasheet doporučuje volit 10pF pro malá referenční napětí). Ve stejném registru (CTRLC) vyberu i referenci pro AD převodník jako vnitřní (která z vnitřních referencí to bude jsem vybral už ve VREF). Tím konfigurace končí a pak už jen bitem ENABLE ADC spustím. V tom okamžiku se rozběhne i reference a je potřeba počkat ~25us na její stabilizaci.
Převod se provádí podobně jak na starých AVR. Nastavíte bit STCONV a počkáte dokud je nastaven. Jak přejde do log.0, je hotovo a můžete si z ADC0.RES přečíst výsledek. Protože jsme nastavili akumulaci na 4x, máme ve výsledku 4 násobně větší hodnotu. Jednoduchou bitovou rotací vpravo o dva bity ji podělíme 4-mi. Použití akumulace nám v tomto případě snižuje šum. Výsledek ještě přepočítáme na napětí. Datasheet uvádí jako převodní vztah:
V=ADC*Vref/1023
Kde ADC je výsledek převodu a Vref je referenční napětí. Abych nemusel používat "floaty" budu pracovat s napětím v mV. Při počítání dbám na to aby se nejprve násobilo. Dělení je ztrátová operace a proto ji nechávám až na konec. Dalo by se říct, že čím větší dělenec a čím menší dělitel tím je chyba menší. Protože výsledné napětí v mV má "větší" rozlišení než ADC (napětí 0 až 2500mV obsahuje 2500 kroků, ADC má ale 1024 kroků), skáče nám poslední cifra v čísle o dva a někdy o tři. Je tedy nepřesná a navíc v obsluze na PC vzbuzuje dojem, že naše aplikace opravdu měří milivolty. Určitě to znáte z různých arduino návodů, kde výpis do terminálu obsahuje nesmysly typu "teplota: 19.57685°C". My si tedy dáme záležet abychom vypsali jen to co považujeme za relevantní. Náš výsledek by měl být správný na desetiny voltu. Zdá se tedy, že stačí když napětí v milivoltech podělíme deseti a je to. Ale to není úplně správné. Celočíselné dělení zaokrouhluje dolů. Tak například 7/4=1 a my bychom určitě chtěli aby 7/4=1.75=2. Proto před dělením přičteme k číslu pětku (polovinu dělitele), to zaručí že po dělení dostaneme správně zaokrouhlený výsledek. Ten pomocí printf() a běžných matematických úprav pošleme do terminálu jako "desetinné" číslo. Pokud bychom se chtěli co nejvíc vyvarovat chyb, měli bychom výpočet upravit tak aby v něm bylo pouze jedno dělení (teď dělíme 1023 a chvíli potom 10ti). To by vám ale teď mohlo zamotat hlavu a krom toho se vám často bude hodit onen mezivýsledek v milivoltech, takže jsme zvolil tuto cestu.
Ovládání UARTu jsem si půjčil z předchozího tutoriálu. Pojďme se tedy podívat na zdrojový kód. Kvůli UARTu je trochu rozsáhlejší, ale věřím že se v něm zorientujete.
/* tutorial TinyAVR 1-Series * ADC1 - A) jednoduchý převod * Na PA5 je výstup odporového děliče (10k trimr + 100nF filtrace) z TL431 (2.495V) * Změří napětí na PA5 (AIN5) proti vnitřní 2.5V referenci * a výsledek pošle na UART (skrze mEDBG do terminálu PC) */ /* Ve fuses zvolen 20MHz oscilátor */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #include <stdio.h> // kvůli Printf_P (formátovací řetězec je ve Flash) #include <avr/pgmspace.h> // kvůli makru PSTR (v printf_P) #define BAUDVAL 8334 // 9600 baud při 20MHz #define V_REF 2507 // napětí vnitřní 2.5V reference (empiricky určeno) //#define V_REF 1105 // napětí vnitřní 1.1V reference (empiricky určeno) //#define V_REF 555 // napětí vnitřní 0.55V reference (empiricky určeno) //#define V_REF 4354 // napětí vnitřní 4.34V reference (empiricky určeno) //#define V_REF 1505 // napětí vnitřní 1.5V reference (empiricky určeno) void clock_20MHz(void); void init_adc(void); uint16_t adc_get(void); void usart_init(void); int usart_putchar(char var, FILE *stream); // kouzelná formule, pro přesměrování printf na UART static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE); uint16_t adc_val, volt; // výsledek převodu a přepočítaná hodnota napětí int main(void){ clock_20MHz(); // taktujeme na plný výkon PORTA.PIN5CTRL = PORT_ISC_INPUT_DISABLE_gc; // vypnout vstupní buffer na PA5 (AIN5) init_adc(); // rozběhnout referenci a ADC usart_init(); // konfigurace UARTu stdout = &mystdout; // přesměrujeme Printf na UART while (1){ adc_val = adc_get(); // změřit napětí volt = (uint16_t)(((uint32_t)adc_val*V_REF)/1023); // převést výsledek na mV volt = (volt + 5)/10; // převod z mV na setiny voltu se správným zaokrouhlením (lze zakomponovat do předchozího výpočtu) // vypíšeme napětí i výsledek převodu do PC printf_P(PSTR("ADC = %u (%u.%02u V)\n\r"),adc_val,volt/100,volt%100); _delay_ms(1000); // ... každou sekundu } } uint16_t adc_get(void){ uint16_t tmp; ADC0.COMMAND = ADC_STCONV_bm; // spustit převod while(ADC0.COMMAND & ADC_STCONV_bm){} // počkat na dokončení převodu tmp = ADC0.RES; // vyčíst výsledek převodu tmp = tmp>>2; // "průměrujeme" akumulaci 4 výsledků return tmp; } void init_adc(void){ VREF.CTRLA = VREF_ADC0REFSEL_2V5_gc; // vybrat referenci pro ADC ADC0.CTRLA = ADC_RESSEL_10BIT_gc; // zvolit rozlišení 10bit ADC0.CTRLB = ADC_SAMPNUM_ACC4_gc; // akumulujeme výsledek 4 převodů // vzorkovací kondenzátor 5pF, prescaler 20M/64 = 312kHz, použít vnitřní referenci (2.5V) ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_PRESC_DIV64_gc | ADC_REFSEL_INTREF_gc; ADC0.MUXPOS = ADC_MUXPOS_AIN5_gc; // zvolit vstup pro ADC (PA5) ADC0.SAMPCTRL = 3; // 2+3 = 5 period (16us) vzorkovací čas ADC0.CTRLA |= ADC_ENABLE_bm; // spustit ADC _delay_us(25); // počkat na stabilizaci reference } // 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) } // odešle jeden znak (formát pro "printf" implementaci) int usart_putchar(char var, FILE *stream){ while(!(USART0.STATUS & USART_DREIF_bm)){} // počkat než bude místo v odesílacím bufferu USART0.TXDATAL = var; // naložit data k odeslání return 0 ; } void usart_init(void){ // využijeme toho že po startu/restartu je UART nastaven ... // ... jako 8bit, 1stop bit, žádná parita, asynchronní režim // část konfigurace si tedy můžu odpustit PORTA.OUTSET = PIN1_bm; // log.1 PA1 (Tx) - neutrální hodnota na UARTu PORTA.DIRSET = PIN1_bm; // PA1 (Tx) - je výstup PORTMUX.CTRLB = PORTMUX_USART0_bm; // Remapujeme Tx na PA1 USART0.BAUD = BAUDVAL; // nastavit baudrate USART0.CTRLB |= USART_TXEN_bm; // povolit vysílač (od teď přebere UART kontrolu nad PA1) }
V ukázce jsme použili akumulaci k průměrování a odstranění drobného šumu. Stačí příklad jen trochu upravit a můžeme akumulaci použít ke zvýšení rozlišení. Změníme akumulaci na maximum (64 vzorků), čímž se z našeho 10bit AD převodníku stane mizerný 16bit (výsledkem převodu budou čísla 0 až 65535). Šum po oversamplingu bude ale tak velký, že ani náhodou nepůjde o použitelný 16bit výsledek. Na druhou stranu to ale bude lepší jak pouhých 10bit. Zkusíme tedy zvýšit rozlišení přibližně 2.5krát a měřit napětí přímo v milivoltech. Rozlišení pak bude přibližně 11.3bitu. Úpravou musí projít též matematický převod na napětí. Tentokrát ho udělám korektně, tedy nejprve násobím referenční napětí s výsledkem akumulovaného převodu, pak přičtu polovinu dělitele a nakonec podělím. Hodnota 64 v děliteli odpovídá míře oversamplingu. Test ukazuje že výsledek má opravdu rozlišení na jednotky mV a nešumí. Z letmého porovnání tří hodnot (1.885V, 1.220V a 0.834V) s multimetrem je patrné že odchylka není větší jak 1mV ! Takže můžeme jásat ? Ne tak úplně. Rozlišení je opravdu vyšší. Ale přesnost bohužel ne. Ta se váže na kvalitu reference. Stačí aby byla reference jen o 0.1% jinde než čekáme a ve výsledku to udělá chybu 2.5mV. Částečně to můžete zachránit tím, že si referenci přesně dopočítáte (jako jsem to udělal já). Navíc se najdou i aplikace kde nejde o přesnost, ale vyšší rozlišení se hodí - třeba když chcete detekovat o kolik se veličina změnila a příliš vás nezajímá kolik přesně je. Tam vám oversampling může pomoct. Jen pro představu jsem zahřál čip fénem (na takových 45°C - tedy změna o cca 20°C) a výsledek uletěl o 8mV. Dovolím si uveřejnit jen upravenou část zdrojového kódu.
// ... úsek zdrojového kódu ... while (1){ adc_val = adc_get(); // změřit napětí // trocha matematiky která převede akumulovaný výsledek na mV se správným zaokrouhlováním volt = (uint16_t)(((uint32_t)adc_val*V_REF+32736UL)/(64UL*1023UL)); // pošleme výsledek do PC printf_P(PSTR("ADC = %u (%u mV)\n\r"),adc_val,volt); _delay_ms(1000); // ... každou sekundu } } uint16_t adc_get(void){ uint16_t tmp; ADC0.COMMAND = ADC_STCONV_bm; // spustit převod while(ADC0.COMMAND & ADC_STCONV_bm){} // počkat na dokončení převodu tmp = ADC0.RES; // vyčíst výsledek převodu // vracíme akumulovaný výsledek (něco jako 16bit ADC) return tmp; } void init_adc(void){ VREF.CTRLA = VREF_ADC0REFSEL_2V5_gc; // vybrat referenci pro ADC ADC0.CTRLA = ADC_RESSEL_10BIT_gc; // zvolit rozlišení 10bit ADC0.CTRLB = ADC_SAMPNUM_ACC64_gc; // akumulujeme výsledek na maximum (64 převodů = "16bit") // vzorkovací kondenzátor 5pF, prescaler 20M/64 = 312kHz, použít vnitřní referenci (2.5V) ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_PRESC_DIV64_gc | ADC_REFSEL_INTREF_gc; ADC0.MUXPOS = ADC_MUXPOS_AIN5_gc; // zvolit vstup pro ADC (PA5) ADC0.SAMPCTRL = 3; // 2+3 = 5 period (16us) vzorkovací čas ADC0.CTRLA |= ADC_ENABLE_bm; // spustit ADC _delay_us(25); // počkat na stabilizaci reference } // ... úsek zdrojového kódu ...
Na tomto místě jsem měl v plánu předvést jak lze měřit přibližnou teplotu čipu pomocí vnitřního teplotního čidla. To rozhodně nebude sloužit jako teploměr, ale s trochou snahy ho lze použít na kompenzaci výkyvů frekvence oscilátoru nebo vnitřních referencí. Protože čidlo má hodně volné tolerance, máte v paměti v sekci SIGROW uloženy kalibrační konstanty TEMPSENSE0 a TEMPSENSE1. Já je tam bohužel nemám ... přesněji řečeno mám tam hodnoty 0xFF a 0xFF. Asi mám čip některé z prvních výrobních serií (revize B, NVM 0). Nechci zveřejňovat nevyzkoušený návod, takže pokud máte chuť, najděte si v datasheetu sekci 30.3.2.6 Temperature Measurement. Ta perfektně dokumentuje postup měření i výpočtu teploty, včetně zdrojáku (!). Dovolím si místo toho navrhnout náhradní řešení a měřit teplotu okolí čidlem MCP9701.
MCP9701
V krátkosti vám čidlo představím. Jde o levné (6.50 kč na TME) lineární teplotní čidlo, které je k dostání v klasickém "tranzistorovém" pouzdře TO-92 nebo SOT-23. Přesností zrovna neoplývá a jako domácí teploměr bych ho nejspíš nepoužil. Nicméně pro orientační měření, nebo nějakou běžnou termoregulaci asi postačí. Jeho výhodou krom ceny je schopnost měřit i teploty "pod nulou". Ani po stránce odběru není jeho výkon špatný a odběr se typicky pohybuje okolo 6uA. Kvůli měření záporných teplot je výstup "posunut" na hodnotu 400mV při 0°C. S každým stupněm teploty výstup roste nebo klesá s koeficientem 19.5mV/°C. Výstupní napětí se tak pohybuje v rozsahu 200mV až 2.9V pro teploty -10°C až 125°C. Dá se předpokládat, že reálně bude čidlo měřit až k -20°C, ale vzhledem k mírným klimatickým podmínkám mé domoviny, nemám moc příležitostí to ověřit. Pro náročnější uživatele, kteří vyžadují širší teplotní rozsah je k dostání i varianta MCP9700, která pracuje od -40°C do 125°C (s offsetem 500mV a koeficientem 10mV/°C).
Na vývojovém kitu mám čidlo připojeno pod dvojicí topných rezistorů. Smyslem je připravit prostředí pro "domácí úlohu" regulace teploty. Výstup čidla je přiveden na PA7 naší Attiny a snadnou úpravou lze modifikovat první příklad k měření teploty. Ona modifikace spočívá jen v prohození kanálu (ADC0.MUXPOS), vypnutí bufferu na PA7 (PORTA.PIN7CTRL) a v drobné úpravě matematiky. Komentář si zaslouží volba reference. Čím menší referenci zvolím, tím lepší větší bude mít mé měření citlivost. Naopak čím větší referenci zvolím, tím pokryji větší teplotní rozsah. Možnosti rozsahu shrnu do krátké tabulky
Reference | Maximální teplota | Rozlišení teploty |
---|---|---|
0.55V | 7°C | 0.03°C |
1.1V | 35°C | 0.06°C |
1.5V | 56°C | 0.08°C |
2.5V | 107°C | 0.13°C |
/* tutorial TinyAVR 1-Series * ADC1 - B) měření teploty pomocí MCP9701 * Výstup z MCP9701 na PA7 * výsledek pošle na UART (skrze mEDBG do terminálu PC) */ /* Ve fuses zvolen 20MHz oscilátor */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #include <stdio.h> // kvůli Printf_P (formátovací řetězec je ve Flash) #include <stdlib.h> // kvůi fci abs() #include <avr/pgmspace.h> // kvůli makru PSTR (v printf_P) #define BAUDVAL 8334 // 9600 baud při 20MHz //#define V_REF 2507 // napětí vnitřní 2.5V reference (empiricky určeno) //#define V_REF 1105 // napětí vnitřní 1.1V reference (empiricky určeno) //#define V_REF 555 // napětí vnitřní 0.55V reference (empiricky určeno) //#define V_REF 4354 // napětí vnitřní 4.34V reference (empiricky určeno) #define V_REF 1505 // napětí vnitřní 1.5V reference (empiricky určeno) // parametry teplotního čidla MCP9701 (lze změnit k přesnější kalibraci) #define MCP_OFFSET 400 // mV #define MCP_SCALE 195 // jednotkou je 0.1*mV/°C void clock_20MHz(void); void init_adc(void); int16_t get_temperature(void); void usart_init(void); int usart_putchar(char var, FILE *stream); // kouzelná formule, pro přesměrování printf na UART static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE); uint16_t adc_val, volt; // výsledek převodu a přepočítaná hodnota napětí int16_t temp; int main(void){ clock_20MHz(); // taktujeme na plný výkon PORTA.PIN7CTRL = PORT_ISC_INPUT_DISABLE_gc; // vypnout vstupní buffer na PA7 (AIN7) init_adc(); // rozběhnout referenci a ADC usart_init(); // konfigurace UARTu stdout = &mystdout; // přesměrujeme Printf na UART while (1){ _delay_ms(1000); // každou sekundu temp=get_temperature(); // přečteme teplotu (v desetinnách °C) // pošleme část zprávy včetně znaménka teploty na terminál if(temp<0){printf_P(PSTR("t = -"));}else{printf_P(PSTR("t = +"));} // a dále pracujeme už jen s kladnou hodnotou teploty temp=abs(temp); // pošleme "desetinné" vyjádření teploty printf_P(PSTR("%i.%i °C\n\r"),temp/10,temp%10); } } // vrací teplotu v desetinách °C int16_t get_temperature(void){ int32_t tmp; ADC0.COMMAND = ADC_STCONV_bm; // spustit převod while(ADC0.COMMAND & ADC_STCONV_bm){} // počkat na dokončení převodu tmp = ADC0.RES; // vyčíst výsledek převodu // "hodně" matematiky v jednom řádku - viz text tutoriálu tmp = (100*((tmp*V_REF+2*1023)/(4*1023) - MCP_OFFSET))/MCP_SCALE; return tmp; } void init_adc(void){ VREF.CTRLA = VREF_ADC0REFSEL_1V5_gc; // vybrat referenci pro ADC ADC0.CTRLA = ADC_RESSEL_10BIT_gc; // zvolit rozlišení 10bit ADC0.CTRLB = ADC_SAMPNUM_ACC4_gc; // akumulujeme výsledek 4 převodů // vzorkovací kondenzátor 5pF, prescaler 20M/64 = 312kHz, použít vnitřní referenci (1.5V) ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_PRESC_DIV64_gc | ADC_REFSEL_INTREF_gc; ADC0.MUXPOS = ADC_MUXPOS_AIN7_gc; // zvolit vstup pro ADC (PA7) ADC0.SAMPCTRL = 3; // 2+3 = 5 period (16us) vzorkovací čas ADC0.CTRLA |= ADC_ENABLE_bm; // spustit ADC _delay_us(25); // počkat na stabilizaci reference } // 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) } // odešle jeden znak (formát pro "printf" implementaci) int usart_putchar(char var, FILE *stream){ while(!(USART0.STATUS & USART_DREIF_bm)){} // počkat než bude místo v odesílacím bufferu USART0.TXDATAL = var; // naložit data k odeslání return 0 ; } void usart_init(void){ // využijeme toho že po startu/restartu je UART nastaven ... // ... jako 8bit, 1stop bit, žádná parita, asynchronní režim // část konfigurace si tedy můžu odpustit PORTA.OUTSET = PIN1_bm; // log.1 PA1 (Tx) - neutrální hodnota na UARTu PORTA.DIRSET = PIN1_bm; // PA1 (Tx) - je výstup PORTMUX.CTRLB = PORTMUX_USART0_bm; // Remapujeme Tx na PA1 USART0.BAUD = BAUDVAL; // nastavit baudrate USART0.CTRLB |= USART_TXEN_bm; // povolit vysílač (od teď přebere UART kontrolu nad PA1) }
V posledním příkladu tohoto tutoriálu se podíváme na funkci "okénkového komparátoru" (Window comparator). Jak už jsem nastínil v úvodu, může ADC samo porovnávat výsledek převodu se dvěma hodnotami a ve vybrané situaci volat přerušení. K čemu to je ? Nastíním dva případy kdy by se vám to mohlo hodit. Dejme tomu, že potřebujete hlídat podpětí nebo přepětí nějakého signálu. Chcete reagovat v co nejkratším čase a obejít se bez externích součástek. Připojit k signálu vnitřní komparátor nemůžete, protože ten ohlídá jen podpětí nebo jen přepětí, ne obě možnosti zároveň. Krom toho má také méně vstupních kanálů, takže má limitované možnosti ve výběru signálů. Pravda, má suverénně nejrychlejší reakci, ale jak už jsme řekli, nemůžeme ho z uvedených důvodů použít. Můžeme ale nastavit ADC ve free-running módu s vysokou frekvencí (například 150ksps) a zapnout window comparator. ADC neustále měří napětí a jakmile hodnota vyjde z povolených mezí, vyvolá přerušení. Reakční čas může být do 10us. A u toho všeho se může jádro věnovat další činnosti. Druhý, o poznání pravděpodobnější, scénář se rýsuje v low-power aplikacích. Dejme tomu, že budete chtít hlídat napětí akumulátoru. Chcete sledovat zda není na baterii podpětí abyste následně mohli vypnout zátěž. Nebo naopak přepětí aby jste vypnuli nabíjení. To vše chcete provádět na pozadí a nechat jádro uspané aby zbytečně "nežralo".
Náš příklad bude mít blíže ke druhé variantě. Naprogramujeme AD převodník tak aby přibližně 4krát za sekundu zkontroloval napětí na PA5 (náš trimr) a volal přerušení pokud bude mimo nastavené hranice. Na důkaz že k došlo k přepětí rozsvítí LED (PB5) a při podpětí LED zhasne. Jak to zrealizujeme ? Konečně k něčemu opravdu využijeme event systém. Víme, že AD převod je možné spouštět eventem. Nabízí se tedy využít PIT ke generování pravidelných eventů a ty využít ke spouštění AD převodu. Jádro bude mít díky tomu volné ruce a pokud bychom to uměli, mohli bychom ho i uspat. PIT už známe, takže jen ve stručnosti zmíním jeho konfiguraci. Jako clock mu vybereme 1kHz z ULP. Spustíme ho bez periodických přerušení. Výstupy PITu je možné připojit na asynchronní event kanál 3 (ASYNCCH3). My si zvolíme výstup PIT_DIV256, tedy clock PITu/256 což jsou přibližně 4Hz. Tyto eventy jsem v rámci demonstrace vyvedl ještě na výstup EVOUT2 (PC2), ale ve zdrojovém kódu to nenajdete. ADC0 je z hlediska event systému "uživatel 1" (Asyncuser1) a tam tedy přivedeme eventy z kanálu ASYNCCH3. Tím je vyřešena otázka spouštění ADC (trigger).
Nastavování ADC bude probíhat podobně jako v předchozích příkladech. Zvolíme referenci (2.5V), rozlišení (10bit), akumulaci (4x), clock (20MHz/64), vstup (AIN5/PA5), vzorkovací kondenzátor (5pF) a vzorkovací čas (16us). Nastavování window comparatoru je přímočaré. Nejprve zapíšeme hladiny (registry WINHT a WINLT). Protože máme akumulaci 4x a ADC porovnává s hladinami výsledek až po akumulaci (což je žádoucí a logické), musíme i hladiny zapisovat v adekvátním formátu (čtyřnásobek hodnoty převodu). K definici hladin slouží makra LOW_LIMIT_VOLTAGE a HIGH_LIMIT_VOLTAGE, do nichž zapisujete napětí v milivoltech. Jednoduchý přepočet s trojčlenkou z nich do maker LOW_LIMIT a HIGH_LIMIT napočítá odpovídající výsledky ADC. Aby to bylo srozumitelné, uvedu to konkrétně. Chci aby horní hladina byla 1.5V. Výsledek jednoho převodu napětí 1.5V bude s referencí 2.507V roven 1.5/2.507*1023=612. S akumulací 4x to bude hodnota 4x612=2448. Do WINHT bych měl tedy zapsat 2448. Krom hladin je potřeba zvolit sledovanou událost. My chceme sledovat situaci kdy signál klesne pod dolní mez nebo překročí horní mez. Tu specifikujeme v registru CTRLE hodnotou ADC_WINCM_OUTSIDE_gc. Přerušení povolíme bitem WCMP v registru INTCTRL. Následně už jen povolíme spouštění převodu eventy (STARTEI v EVCTRL) a spustíme ADC.
ADC dostane 4x za sekundu pokyn k převodu a pokud výsledek vyjde mimo nastavené pásmo vyvolá se rutina přerušení. V ní si program přečte výsledek, čímž smaže vlajku a podle jeho hodnoty rozsvítí nebo zhasne LEDku. Může se stát, že nebudete potřebovat číst výsledek převodu (pokud třeba hlídáte jen přepětí nebo jen podpětí), pak můžete vlajku mazat klasicky zápisem log.1 do bitu WCMP. Jednou z těchto možností ji ale smazat musíte, jinak se bude přerušení volat neustále (známá věc). Teď mrkněte na zdroják.
/* tutorial TinyAVR 1-Series * ADC1 - C) Analog watchdog * PIT skrze event systém pravidelně spouští ADC převod * ADC v režimu okénkového komparátoru hlídá napětí na PA5 * pokud napětí překročí horní hranici, nebo podkročí dolní hranici * volá se přerušení a aplikace při překročení horní hranice rozsvítí LED * při podkročení dolní hranice zhasíná LED * hlídání napětí probíhá na pozadí, program reaguje jen při překročení/podkročení */ /* Ve fuses zvolen 20MHz oscilátor */ #define F_CPU 20000000UL #include <util/delay.h> #include <avr/io.h> #include <avr/interrupt.h> #define V_REF 2507 // napětí vnitřní 2.5V reference (empiricky určeno) //#define V_REF 1105 // napětí vnitřní 1.1V reference (empiricky určeno) //#define V_REF 555 // napětí vnitřní 0.55V reference (empiricky určeno) //#define V_REF 4354 // napětí vnitřní 4.34V reference (empiricky určeno) //#define V_REF 1505 // napětí vnitřní 1.5V reference (empiricky určeno) #define LOW_LIMIT_VOLTAGE 1000UL //mV #define HIGH_LIMIT_VOLTAGE 1500UL // mV #define AKUMULACE 4 // počet vzorků které ADC akumuluje během převodu // vypočtené komparační hladiny pro výsledky převodu (i s akumulací) #define LOW_LIMIT (LOW_LIMIT_VOLTAGE*1023*AKUMULACE)/V_REF #define HIGH_LIMIT (HIGH_LIMIT_VOLTAGE*1023*AKUMULACE)/V_REF // indikační LED (PB5 zapojená proti VCC) #define LED_ON PORTB.OUTCLR = PIN5_bm; #define LED_OFF PORTB.OUTSET = PIN5_bm; void clock_20MHz(void); void init_adc(void); void init_pit(void); // přerušení od Window comparatoru ADC ISR(ADC0_WCOMP_vect){ uint16_t tmp; tmp = ADC0.RES; // čtení výsledku převodu zároveň maže vlajku perušení // (vlajku lze mazat i klasicky zápisem ADC0.INTFLAGS = ADC_WCMP_bm;) // pokud jsme přes horní hranici, rozsvítit LED, jinak jsme pod dolní hranicí - zhasnout LED if(tmp>=HIGH_LIMIT){LED_ON;}else{LED_OFF;} } int main(void){ clock_20MHz(); // taktujeme na plný výkon LED_OFF; // LED ze začátku zhasnutá PORTB.DIRSET = PIN5_bm; // LED (PB5) výstup PORTA.PIN5CTRL = PORT_ISC_INPUT_DISABLE_gc; // vypnout vstupní buffer na PA5 (AIN5) init_adc(); // rozběhnout referenci a ADC init_pit(); // spustit PIT (generuje pravidelné eventy pro ADC) // do event kanálu ASYNCCH3 zavést výstup PITu (1k/256 = ~ 4Hz) EVSYS.ASYNCCH3 = EVSYS_ASYNCCH3_PIT_DIV256_gc; // Asyncuser1 = ADC0, bere eventy z ASYNCCH3 (tedy z PITu) EVSYS.ASYNCUSER1 = EVSYS_ASYNCUSER1_ASYNCCH3_gc; sei(); // povolit globálně přerušení (ADC bude volat) while (1){ // ... není co dělat } } void init_pit(void){ RTC.CLKSEL = RTC_CLKSEL_INT1K_gc; // nastavím zdroj clocku pro PIT - vnitřní 1kHz z ULP oscilátoru while(RTC.PITSTATUS & RTC_CTRLABUSY_bm){} // čekej dokud probíhá předchozí zápis do PITCTRLA RTC.PITCTRLA = RTC_PERIOD_OFF_gc | RTC_PITEN_bm; // spustí "naprázdno" PIT (bez povoleného přerušení) } void init_adc(void){ VREF.CTRLA = VREF_ADC0REFSEL_2V5_gc; // vybrat referenci pro ADC ADC0.CTRLA = ADC_RESSEL_10BIT_gc; // zvolit rozlišení 10bit ADC0.CTRLB = ADC_SAMPNUM_ACC4_gc; // akumulujeme výsledek 4 převodů ADC0.WINLT = LOW_LIMIT; // nastavíme dolní hladinu okénkového komparátoru ADC0.WINHT = HIGH_LIMIT; // nastavíme horní hladinu okénkového komparátoru ADC0.CTRLE = ADC_WINCM_OUTSIDE_gc; // okénkový komparátor hlídá vystoupení ze stanoveného pásma ADC0.INTCTRL = ADC_WCMP_bm; // povolíme přerušení od okénkového komparátoru ADC0.EVCTRL = ADC_STARTEI_bm; // převod spouštěn Eventem // vzorkovací kondenzátor 5pF, prescaler 20M/64 = 312kHz, použít vnitřní referenci (2.5V) ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_PRESC_DIV64_gc | ADC_REFSEL_INTREF_gc; ADC0.MUXPOS = ADC_MUXPOS_AIN5_gc; // zvolit vstup pro ADC (PA5) ADC0.SAMPCTRL = 3; // 2+3 = 5 period (16us) vzorkovací čas ADC0.CTRLA |= ADC_ENABLE_bm; // spustit ADC _delay_us(25); // počkat na stabilizaci reference } // 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) }
Výsledné chování si můžete prohlédnout na oscilogramech. Červená stopa je signál v event kanálu. S každou vzestupnou hranou ADC měří napětí na žluté stopě (výsledek mého točení trimrem). Modrá stopa je signál z LEDky. Protože je LED na kitu připojena proti VCC, rozsvěcí se log.0. Jako první vás asi zarazí, že hladiny příliš nesedí. Že se LEDka zhasne při poklesu pod 980mV vás asi moc neznervózní (přesnost měření není v tomto případě nijak závratná), ale že se rozsvítí při překročení až 1.62V (namísto 1.5V) to už zarážející být může. Je to tím že napětí kontrolujeme jen 4 za sekundu. Když se podíváte na druhý oscilogram, uvidíte že k překročení došlo přibližně v čase vyznačeném kurzorem. Nejbližší převod proběhne ale až 130ms po tom a za tu dobu stihne napětí vzrůst na oněch 1.62V. Frekvence převodů je tedy pro tak rychlé změny příliš nízká. Napětí akumulátoru se bude měnit ale o několik řádů pomaleji, takže u něj by stačilo měřit i s mnohem nižší frekvencí.
První várka příkladů je za námi a můžeme hodnotit. Trochu mě zklamala absence koeficientů pro vnitřní teplotní čidlo. Mile naopak překvapil nízký šum a schopnost oversamplingu reálně navýšit rozlišení. Hodnotu vnitřní reference jsem zjišťoval jednoduchou trojčlenkou. Změřil jsem známé napětí, zjistil výsledek převodu a dopočítal jaká reference by takové situaci odpovídala. Moje 2.5V reference tedy měla 2.507V. S touto informací jsem zběžně otestoval přesnost našeho ADC a výsledky jsou na 10bit převodník pozitivní. Využil jsem příkladu s oversamplingem, měřil napětí s rozlišením na mV a porovnával ho s měřením na přesném stolním multimetru HM8112-3. Odchylka nebyla nikde větší jak 1mV ! Doufám že to stálo za tu práci a že si na základě tohoto tutoriálu uděláte obrázek o použití ADC na moderních AVR. A doufám že se brzy setkáme u dalších dílů.
Home
| V1.01 5.2.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /