Reklama

Začínáme s mikrokontroléry ATmega – A/D převodník 3. díl (další praktické příklady)

Úvod

Druhý díl seriálu na použití A/D převodníku u mikrokontrolérů ATmega se zaměřuje na následující příklady:
 

  • D) ATmega8A – měření více kanálů
  • E) ATmega8A – měření hodnoty AVCC s využitím interní reference
  • F) ATmega644P – Jednoduché diferenciální měření
D) ATmega8A – měření více kanálů

Tento příklad je celkem jednoduchý a předvádí měření na více kanálech (konkrétně na 5ti – ADC0ADC4) s referencí AVCC. Můžete ho využít v aplikacích, kde obsluha zadává vstupní parametry pomocí potenciometrů (např. ovladač k RC modelu). Jako referenci používá AVCC, která se hodí právě k výše zmíněnému účelu. Pro pin AREF platí totéž co v příkladě A), tedy, že je potřeba jej filtrovat kondenzátorem 10 – 100 nF proti GND. Hodiny převodníku opět volíme do povolených mezi (konkrétně na 125 kHz). Celý převod pak obstarává funkce zmer. Jejím argumentem je ukazatel na pole, kam má všech 5 změřených hodnot uložit. Z těla funkce je patrné, že přepínání kanálů je přímočaré. Stačí zvolit vhodnou kombinaci v registru ADMUX a spustit převod (nastavením ADSC). Poté počkat na dokončení převodu (dokud je ADSC v jedničce) a výsledek z registru ADC uložit.

 

001: // D) Atmega8A – měření více kanálů
002:
003: #include <avr/io.h>
004: #define F_CPU 8000000
005: #include <util/delay.h>
006: #include <stdio.h>// kvůli fci printf_P()
007: #include <avr/pgmspace.h>// kvůli fci printf_P()
008:
009: // odesílání UARTem
010: int usart_putchar(char var, FILE *stream);
011: void setup_uart(void);// inicializace UART rozhraní
012: void zmer(uint16_t pole[]);// změří 5 hodnot a vrátí je v poli
013:
014: static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
015: uint16_t val[5];// pole pro výsledky měření
016:
017: int main(void){
018: setup_uart();// nastavení komunikace
019: ADMUX = (1<<REFS0);// reference AVCC
020: // povolit ADC, clock pro ADC F_CPU/64
021: ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);
022:
023: while(1){
024: zmer(val);// změř všech 5 kanálů a ulož je do pole val
025: printf_P(PSTR("%u, %u, %u, %u, %u\n\r"),val[0],val[1],val[2],val[3],val[4]);
026: _delay_ms(700);
027: }
028: }
029:
030: void zmer(uint16_t pole[]){
031: char i;
032: for(i=0;i<5;i++){
033: ADMUX = (1<<REFS0) | i;// nastavení měřeného kanálu
034: ADCSRA |= (1<<ADSC);// spustit převod
035: while(ADCSRA & (1<<ADSC)){}// čekej dokud je ADSC=1 (probíhá převod)
036: pole[i] = ADC;// ulož výsledek převodu
037: }
038: }
039:
040: void setup_uart(void){
041: stdout = &mystdout;// nastavení standardního výstupu
042: // na naši funkci usart_putchar()
043: UBRRL = 51;// baud rate 9600 s clockem 8MHz
044: UCSRB = (1<<TXEN);// zapnout UART vysílač
045: // formát zprávy “8N1″
046: UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1);
047: }
048:
049: int usart_putchar(char var, FILE *stream) {
050: while (!(UCSRA & (1<<UDRE)));
051: UDR = var;
052: return 0;
053: }
Vzorové zdrojové kódy slouží pouze k jednoduché demonstraci funkce, proto často obsahují globální proměnné, neobsahují většinou ukazatele, dále také neobsahují kontroly proměnných, definování nevyužitých pinů mikrokontroléru a podobné správné programátorské návyky. Proto je nutné tyto kódy brát s patřičnou rezervou.
 
E) ATmega8A – měření hodnoty AVCC s využitím interní reference

Tento příklad, ačkoliv má trochu nesrozumitelný název skýtá celkem velký potenciál. Řekněme, že vás napadla otázka. Může si mikrokontrolér sám sobě změřit napájecí napětí (třeba když je připojený přímo na akumulátor a chce zjistit stav jeho nabití)? Vzhledem k tomu, že měřené napětí musí být vždy rovno nebo menší referenčnímu a to musí být menší nebo rovno napájecímu, mohlo by se na první pohled zdát, že ne. Naštěstí to samozřejmě jde. První možností je připravit si dělič z napájecího napětí a měřit tedy jen jeho zlomek (třeba polovinu). Takový má dostatečně nízkou hodnotu, abyste mohli využít interní nebo externí referenci a změřit ho. Abyste však mohli hodnotu napájecího napětí spočítat, tak musíte znát přesně parametry děliče. Někteří z vás se ale budou chtít použití dalších dvou vnějších součástek vyhnout. A pro ně je tu druhá možnost :)

Podíváte-li se do datasheetu uvidíte, že kromě osmi vstupních kanálů (ADC0ADC7) může multiplexer připojit na vstup A/D převodníku ještě GND a Bandgap referenci (VBG). Pro Bandgap referenci platí to co pro interní reference převodníku, tedy že je celkem stabilní, ale její hodnota není předem přesně známa. Představte si, že bychom její hodnotu ale přesně znali (a zanedlouho si ukážeme jak ji zjistit). V takovém případě můžeme změřit převodníkem známou hodnotu (VBG má v tomto případě přibližně 1.3 V). Když tedy zvolíme převodníku jako referenci neznámé AVCC (napájecí napětí mikrokontroléru) a změříme tuto interní referenci, můžeme pomocí trojčlenky dopočítat hodnotu AVCC! Stačí z nám známého vztahu vyjádřit Vref.

U = ADC_hodnota x Vref / 1024

Vref = U x 1024 / ADC_hodnota

A protože měřené napětí U známe (je to 1.3 V reference) a výsledek převodu (ADC_hodnota) přirozeně také, jsme schopni stanovit hodnotu napájecího napětí (Vref)! Kromě toho také můžeme dělat i jiná kouzla. Jistě si vzpomenete, že není možné přímo měřit napětí větší než referenční. Takže použitím interní reference 2.56 V se vzdáváme možnosti měřit signály o větším napětí než oněch 2.56 V. Kdežto, použijeme-li AVCC jako referenční napětí tak můžeme měřit s větším rozsahem (až do napájecího napětí, takže typicky 3.3 V nebo 5 V). A díky tomu, že jsme schopni hodnotu napájecího napětí (které se může dosti měnit) stanovit, můžeme teď měřit i relativně přesně. Abychom však nejásali zbytečně moc, poznamenejme, že tento postup dost redukuje přesnost. Jestliže je napájecí napětí 5 V a Bandgap reference 1.3 V stanovujeme hodnotu napájecího napětí s chybou přibližně 5 / 1.3 = 3.8 LSB z AVCC, tedy asi 3.8 x 4.8m V = +/-20 mV. Takže to celkově zase taková sláva není.
Nyní bychom si měli říct, jak stanovit pokud možno přesně hodnotu interní Bandgap reference. Připojíme mikrokontrolér ideálně na stabilní napájecí napětí, a změříme si jeho hodnotu multimetrem. Dejme tomu, že mě vyšla 5.01 V. Nahrajeme do mikrokontroléru jednoduchý program, který v ADMUX vybere na vstup A/D převodníku kanál 14 (tedy interní Bandgap referenci), jako referenci převodníku zvolíme AVCC (REFS0 = 1) a necháme si výsledek převodu vypsat do PC. Dejme tomu, že mě vyšel výsledek převodu 258. Protože přesně známe referenční napětí převodníku (5.01 V) mohu spočítat přesně hodnotu Bandgap reference (tak jako u všech předchozích převodů)

U = ADC_hodnota x Vref / 1024.

U = 258 x 5.01 / 1024 = 1.26 V

To je tedy přesné napětí Bandgap reference. Jak ho jednou znáte, tak můžete počítat s tím, že už se příliš měnit nebude (viz graf “Bandgap Voltage vs. VCC” v datasheet).

Ještě malý komentář si zaslouží postup jak pomocí Bandgap reference měřit obecně jakákoli napětí na vstupech ADC0ADC7. Někdo z vás možná namítne, proč o tom psát, vždyť je to přece jasné. Nejprve změříme hodnotu AVCC s pomocí známé VBG a spočteme ji pomocí vztahu Vref = VBG x 1024 / ADC_hodnota. Pak už jen změříme příslušný kanál (např. ADC1) a ze známého vztahu U = ADC_hodnota x Vref / 1024 dopočítáme napětí U (Vref je v tomto případě AVCC). Takhle ale děláte přesně to, co matematika s celými čísly nemá ráda. Rozdělujete výpočet na dva a v obou z nich dělíte. Vhodnější je sloučit výpočet do jednoho vztahu a vyvarovat se tak jednoho dělení. Výsledný vztah pro napětí na vnějším kanálu (ADC1) pak bude vypadat následovně:

U = VBG x ADC_kanálu / ADC_BGref,

kde VBG je stanovené napětí Bandgap reference v mV, ADC_kanálu je výsledek převodu kanálu ADC1 a ADC_BGref je výsledek převodu interní Bandgap reference. Přesně tento postup provádíme v příkladu. Nejprve inicializujeme převodník, zvolíme jako referenci AVCC, počkáme 70 µs než se spolehlivě nastartuje interní Bandgap reference. Poté zvolíme v multiplexeru jako vstup kanál 14 (interní Bandgap referenci) a výsledek převodu uložíme do proměnné ref. Poté přepneme multiplexer na kanál ADC1 a výsledek jeho měření uložíme do val. Nakonec nás čeká trocha matematiky, kterou jsme už ale probrali a v proměnných “avcc” a “u” nám zůstane napájecí napětí mikrokontroléru a napětí na kanále ADC1. Opět výpočty provádím v mV. Je potřeba si uvědomit, že napájecí napětí se může měnit s vnějšími okolnostmi (teplota, odběr okolních obvodů atd.), proto je dobré proces měření hodnoty AVCC nebo přesněji řečeno proměnné “ref” provádět před měřením vnějšího kanálu (např. ADC1) a doufat že se během tohoto procesu napájecí napětí nijak výrazně nezmění. Chápu, že příklad je poněkud komplexnější a možná vás děsí, ale nikdo vás nenutí tento postup používat ;)

 

001: // E) Atmega8A – měření AVCC pomocí známé interní reference
002: #include <avr/io.h>
003: #define F_CPU 8000000
004: #include <util/delay.h>
005: #include <stdio.h>// kvůli fci printf_P()
006: #include <avr/pgmspace.h>// kvůli fci printf_P()
007:
008: #define VBG 1260// experimentálně stanovená hodnota bandgap reference v mV
009:
010: // odesílání UARTem
011: int usart_putchar(char var, FILE *stream);
012: void setup_uart(void);
013:
014: static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
015: uint16_t ref, val, u, avcc;
016:
017: int main(void){
018: setup_uart();// nastavení komunikace
019: ADMUX = (1<<REFS0) | 0b1110;// reference AVCC, měření na kanálu 14 (interní
020: // reference 1.20V)
021: // povolit ADC, clock pro ADC F_CPU/64
022: ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);
023: _delay_us(70);// počkej než se nastartuje bangap reference
024:
025: while(1){
026: ADMUX &= ~(0b1111);// mažeme nastavení multiplexeru, neovliňujeme
027: // nastavení reference
028: ADMUX |= 0b1110;// nastavujeme k měření kanál 14 (interní reference ~1.20V)
029: ADCSRA |= (1<<ADSC);// spustit převod
030: while(ADCSRA & (1<<ADSC)){}// čekej dokud je ADSC=1 (probíhá převod)
031: ref = ADC;// výsledek měření bandgap reference
032: ADMUX &= ~(0b1111);// mažeme nastavení multiplexeru,
033: // neovliňujeme nastavení reference
034: ADMUX |= 1;// nastavujeme k měření kanál ADC1
035: ADCSRA |= (1<<ADSC);// spustit převod
036: while(ADCSRA & (1<<ADSC)){}// čekej dokud je ADSC=1 (probíhá převod)
037: val=ADC;// výsledek měření na ADC1
038: // AVCC[mV] = 1024*BG[mV]/ref
039: avcc = (uint16_t)(((uint32_t)1024*VBG)/(uint32_t)ref);
040: // U = BG*val/ref
041: u = (uint16_t)((VBG*(uint32_t)val)/1024);
042: // vypsat na PC
043: printf_P(PSTR("AVCC = %u mV, U = %u mV\n\r"),avcc,u);
044: _delay_ms(700);
045: }
046: }
047:
048: void setup_uart(void){
049: stdout = &mystdout;// nastavení standardního výstupu na
050: // naši funkci usart_putchar()
051: UBRRL = 51;// baud rate 9600 s clockem 8MHz
052: UCSRB = (1<<TXEN);// zapnout UART vysílač
053: // formát zprávy “8N1″
054: UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1);
055: }
056:
057: int usart_putchar(char var, FILE *stream) {
058: while (!(UCSRA & (1<<UDRE)));
059: UDR = var;
060: return 0;
061: }
Vzorové zdrojové kódy slouží pouze k jednoduché demonstraci funkce, proto často obsahují globální proměnné, neobsahují většinou ukazatele, dále také neobsahují kontroly proměnných, definování nevyužitých pinů mikrokontroléru a podobné správné programátorské návyky. Proto je nutné tyto kódy brát s patřičnou rezervou.
 
F) ATmega644P – Jednoduché diferenciální měření

A/D převodník některých Atmelů je vybaven možností měřit diferenciálně. To znamená, že umí přímo převádět rozdíl napětí mezi dvěma kanály. Na první pohled se vám to může zdát zbytečné, vždyť přece není problém změřit obě napětí samostatně a výsledky měření od sebe odečíst. Tento prvoplánový (a hloupý) postup má ale svoje úskalí. U signálů s vyšším napětím vás nutí používat větší referenci i přes to, že rozdíl signálů, který chcete měřit je malý. Tím vás okrádá o rozlišovací schopnost. Vysvětlím na příkladě. Máte dvojici signálů, dejme tomu U1 = 2.9V a U2 = 3.1V. Abyste každý z nich mohli měřit, tak potřebujete referenční napětí větší jak 3.1 V. Dejme tomu, že zvolíte Uref = 4.096 V. Při něm je rozlišovací schopnost převodu 4 mV. Po změření a odečtení zjistíte, že rozdíl je 200 mV +/ -4 mV. Pokud ale využijete diferenciálního převodu, můžete použít referenci například 2.56 V s rozlišením přibližně 2 mV. A máte tak šanci dosáhnout výsledku s lepší přesností. Další důležitou výhodou diferenciálního měření je fakt, že nedochází k žádnému časovému rozposunutí mezi vzorky z obou kanálů (na rozdíl od výše uvedené hloupé metody) – vzorkování probíhá v jeden relativně krátký a dobře definovaný okamžik. Tím největším bonusem diferenciálního režimu je ale zesilovač, umožňující měřit 10x, 20x nebo 200x zesílený difereniální signál. O těchto možnostech se dozvíte až v dalších příkladech. My zůstaneme u diferenciálního měření bez zesilovače. Pro každý Atmel je potřeba prohlédnout datasheet a zjistit zda je vůbec možné provádět diferenciální převod a mezi kterými kanály. Já jsem pro tento příklad použil ATmega644, protože z těch Atmelů co znám je jediný, který má tak vybavený A/D převodník i v DIP pouzdře. A očekávám, že někteří ze čtenářů mohou patřit ke starší generaci, která se SMD technologii bude vyhýbat. Stejné funkce v DIP pouzdře by měl ale mít i AtTmega164P a 324P a v SMD provedení pak i takový evergreen jako je ATmega16/32.

V rámci příkladu si ukážeme diferenciální měření mezi kanály ADC0 a ADC1. Využijeme interní referenci 2.56 V, kterou je dobré si zkalibrovat – viz příklad C). Na oba kanály si přiveďte napětí například pomocí potenciometrů a pokud chcete ověřit funkci, připojte si i voltmetr. Program je nekomplikovaný a přímočarý. A komentář si zaslouží jen několik kroků. Všimněte si, že u ATmega644 je vhodné vypnout vstupní buffery na pinech, na kterých měříme (PA0 a PA1). Pro snažší úpravu výstupních dat si zapínám bitem ADLAR zarovnání dat doleva. Výsledek diferenciálního měření může být kladný i záporný a je tedy ve formátu znamenkového čísla. Zarovnáním vlevo si zajistíme možnost pracovat s výsledkem jako s int16_t. Po přečtení hodnoty z registru ADC použiji rotaci doprava. A tady je potřeba zpozornět! Při rotaci vpravo byste mohli očekávat, že zleva se číslo bude plnit nulami. Ale takto rotace vpravo na typu int16_t v překladači implementována není. Namísto toho, aby šlo o logický posun se provádí aritmetický posun (tedy dělení mocninou dvou). Konvenční rotace, která přidává zleva nuly by znaménkový typ znehodnotila. Doplněním nuly zleva do záporného čísla ať už bylo jakékoli by vzniklo okamžitě kladné (viz reprezentace čísla ve dvojkovém doplňku). Díky vhodné interpretaci rotace pro int16_t získáme z převodu správnou hodnotu v typu int16_t bez jakékoli složité aritmetiky. Pak už nás čeká jen kousek matematiky, kde platí, tak jako vždy trojčlenka. Jen s tím rozdílem, že celých 1024 kroků se dělí na kladný a záporný interval a každý z nich je tak pokryt pouze 512 kroky. Ještě malé upozornění závěrem. Jestli budete příklad zkoušet, dejte si pozor na to, že mikrokontrolér je v tomto příkladu taktován 16 MHz. Tato konfigurace typicky vyžaduje krystal. Vzhledem k tomu, že ATmega644 nepatří k nejlevnějším mikrokontrolérům, proto ho používám většinou jen na “náročnější” aplikace a tam většinou vyžaduji vyšší takt. Takže pokud ji máte k dispozici a pracujete bez krystalu, tak si musíte program upravit pro 8 MHz. Což spočívá jen ve dvou krocích. Snižte hodnotu v UBRR na 53 a prescaler pro A/D převodník nastavte na dělení 64.

 

001: // F) Atmega644P – Jednoduché diferenciální měření
002:
003: #include <avr/io.h>
004: #define F_CPU 16000000
005: #include <util/delay.h>
006: #include <stdio.h>// kvůli fci printf_P()
007: #include <avr/pgmspace.h>// kvůli fci printf_P()
008:
009: #define VREF 2598// experimentálně stanovená hodnota vnitřní
010: // reference ("2.56") v mV (měření na AREF)
011: // odesílání UARTem
012: int usart_putchar(char var, FILE *stream);
013: void setup_uart(void);
014:
015: static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
016: int16_t hodnota,u;
017:
018: int main(void){
019: setup_uart();// ke komunikaci použijeme UART0
020: // reference interních 2.56V, diferenciální
021: // měření ADC0-ADC1
022: ADMUX = (1<<REFS1) | (1<<REFS0) | (1<<ADLAR) | 0b10000;
023: // ADC_clock = F_CPU/128;
024: ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);
025: // vypníná digital input buffer na PA0,PA1
026: DIDR0 = (1<<ADC0D) | (1<<ADC1D);
027: _delay_us(70);// čas na stabilizaci interní reference
028:
029: while (1) {
030: _delay_ms(700);
031: ADCSRA |= (1<<ADSC);// spustit převod
032: while(ADCSRA & (1<<ADSC)){}// čekej dokud je ADSC=1 (probíhá převod)
033: hodnota = ADC;// vyčti výsledek převodu (zarovnaný vlevo)
034: hodnota = hodnota>>6;// pozor, posun pro signed int ! interpretace
035: // záleží na překladači !
036: // přepočíst na napětí
037: u = (int16_t)((int32_t)hodnota*VREF/512);
038: // poslat do PC
039: printf_P(PSTR("ADC = %i, U = %i mV\n\r"),hodnota, u);
040: }
041: }
042:
043: void setup_uart(void){
044: stdout = &mystdout;// nastavení standardního výstupu
045: // na naši funkci usart_putchar()
046: UBRR0L = 103;// baudrate 9600 s clockem 16MHz
047: UCSR0B = (1<<TXEN0);// zapnout UART vysílač
048: // formát zprávy “8N1″
049: UCSR0C = (1<<UCSZ00) | (1<<UCSZ01);
050: }
051:
052: int usart_putchar(char var, FILE *stream) {
053: while (!(UCSR0A & (1<<UDRE0)));
054: UDR0 = var;
055: return 0;
056: }

 
Autor: Michal Dudka
 

Následující a předchozí příspěvek v kategorii:

 
Předchozí: Začínáme s mikrokontroléry ATmega – A/D převodník 2. díl (první praktické příklady)

 
Tajned facebook
 

Za případné chyby v textu, ve zdrojovém kódě, nebo ve schématickém zapojení se omlouváme.
AUTOŘI NEBEROU ŽÁDNOU ODPOVĚDNOST ZA PŘÍPADNÉ ÚJMY NA ZDRAVÍ ČI MAJETKU.