logo_elektromys.eu

/ ADC1 |

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.

/ Ovládání |

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.

Errata mimo jiné upozorňuje na to že pokud nastavíte SAMPLEN na větší hodnotu než 0 (tedy pokud chcete použít delší vzorkovací čas než ony minimální dvě periody clocku), přestává SAMPDLY a ASDV fungovat správně. Pokud tedy potřebujete použít SAMPDLY nebo ASDV, musíte pracovat s minimálním vzorkovacím časem. Řekněme že obě funkce jsou pokročilé, takže se s tím teď moc nestrachujte :) A taky je možné že až budete číst tento tutoriál budou tyto chyby dávno odstraněny.

/ Příklad A) Jednoduchý převod |

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ýukový modulek
Z části osazený modul. V horní části je vidět reference TL431. Čidlo MCP9701, které potkáme v dalších příkladech je schované pod "topnými" rezistory vpravo dole.

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 ...

/ Příklad B) Měření teploty s MCP9701 |

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

Tabulka 1. Rozlišení teploty a rozsah měření podle zvolené reference
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

Já si vybral 1.5V referenci, protože její rozlišení je lepší jak 0.1°C a pokrývá rozsah "pokojových" teplot. Chytrá aplikace si může referenci volit automaticky, podle aktuální teploty. Pokud jste si datasheet k čidlu prohlédli důkladně, našli jste tam histogramy z nichž je patrné že zmiňovaný offset 400mV je tu a tam 380mV nebo 440mV a koeficient taky není vždy přesně 19.5mV/°C ale někdy 19.6 nebo 19.7. Proto je rozumné zabudovat tyto hodnoty do programu jako makra, která půjdou snadno změnit (MCP_OFFSET a MCP_SCALE). Pro výpočet teploty zvolíme jednotku desetinu stupně Celsia. Koeficient 19.5 musíme v celých číslech vyjádřit jako 195 a onen přidaný řád přidat při výsledném výpočtu i výsledku AD převodu a "offsetu". Další řád musíme přidat za to že chceme výsledek v desetinách °C. Tím vzniká konstanta 100. Kterou násobíme ve funkci get_temperature(). Přičtení výrazu 2*1023 (tedy poloviny dělitele) slouží ke správnému zaokrouhlování. Dělitel 4*1023 obsahuje 4 kvůli čtyřnásobné akumulaci (zvolena v konfiguraci ADC). Celá aritmetika probíhá ve formátu int32_t protože je znaménková a rozsah 16bit čísla by nestačil. Ani odeslání skrze printf() není úplně triviální. Hodnotu chceme zobrazit jako desetinné číslo, musíme tedy zjistit znaménko a pak číslo převést na kladnou hodnotu (fcí abs()). Pokud najdete jednodušší způsob jak vypsat teplotu ve formátu "xx.y°C", dejte vědět, rád ho sem zveřejním.

/* 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)
}

/ Příklad C) Window Comparator |

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í.

Červená stopa jsou eventy a vyznačují okamžiky převodu. Žlutá stopa je sledovaný signál (točím trimrem). Modrá stopa je reakce softwaru (LEDka, rozsvěcí se log.0)
Detail na okamžik překročení horní hladiny. Je patrné že ADC reaguje se spožděním (až ~250ms). To je dáno nízkou frekvencí měření.

| Závěr /

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ů.

| Odkazy /

Home
| V1.01 5.2.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /