Reklama

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

Úvod

V dnešním díle si ukážeme první tři příklady ze seriálu o A/D převodníku a to:
 

  • A) ATmega8A – jednoduchý převod s VREF = AVCC
  • B) ATmega8A – měření s externí referenci (TL431)
  • C) ATmega8A – měření s interní referencí 2.56 V a s průměrováním

Všechny zdrojové kódy lze stáhnout na konci článku.

A) ATmega8A – jednoduchý převod s VREF = AVCC

Tento příklad demonstruje použití převodníku s referencí AVCC. Na AVCC je napájecí napětí mikrokontroléru. To běžně nebývá přesně známé a může měnit svoji hodnotu s teplotou nebo odběrem (3.3 V může klidně kolísat v rozsahu 3.2 – 3.4 V). Takže typicky jeho hodnotu nebudete moc dobře znát. S takovou referencí pak není možné přesně měřit napětí. Proč tedy něco takového používat jako referenci? Existují hned dva důvody. Za prvé, ne vždy potřebujete měřit přímo napětí! Může vás zajímat třeba natočení potenciometru (například v joysticku). V takovém případě připojíte potenciometr mezi VCC a GND a měříte napětí na jezdci potenciometru. V konečném důsledku vás ale nezajímá číselná hodnota napětí na jezdci, ale její poměr k VCC, protože ten určuje míru otočení potenciometru. Poměr napětí na jezdci proti VCC je ale přesně to, co vám poskytuje převodník s referencí rovnou VCC! Takže výsledná hodnota v rozsahu 0 – 1024 vyčtená z převodníku přesně odpovídá natočení potenciometru a jakýkoli přepočet na napětí není potřebný. Kromě toho se tato hodnota nemění s kolísajícím napájecím napětím. Jestliže napájecí napětí klesne, klesne i napětí na jezdci potenciometru a jejich poměr zůstane stejný (to je smysl potenciometru). To je asi nejtypičtější využití. Druhý případ, kdy využijete toto nastavení reference jsou situace kdy potřebujete přímo měřit napětí v rozsahu 0 – VCC a nechcete je dělit odporovým děličem (ať už vás k tomu vede lenost, nedostatek místa na DPS nebo nějaké ušlechtilejší důvody). Vaše měření pak bude trpět výše zmíněnými nedostatky, tedy nepřesně známou referencí a tudíž i nepřesným měřením. Což se naštěstí dá částečně léčit (viz příklad G, který bude uveden ve čtvrtém dílu seriálu).

V tomto případě je potřeba připojit na AREF kondenzátor (typicky 100 nF) proti GND (viz Obr. 1). Po zvolení reference byste měli na AREF naměřit stejné napětí jako na AVCC. V tomto příkladě (tak jako ve většině ostatních) budeme výsledek převodu posílat UARTem do PC a číst si ho v terminálu. Kdo se s UARTem ještě nesetkal, tak si přečtěte návod Zde. K odesílání spotřebujeme pin TX (PD1) a měřené napětí budeme přivádět na pin ADC1 (PC1), na který si připojte jezdec potenciometru (jeho konce připojte k VCC a GND). Protože přesnou hodnotu AVCC neznáme, budeme výsledek uvádět proporcionálně v procentech (měříme míru natočení potenciometru). Protože je ale převodník 10bitový, tedy jeho rozsah čítá 1024 hodnot a procentních bodů je jen 100, budeme interně měřit polohu potenciometru v promile a procentní výsledek vypíšeme i s jedním desetinným místem. Matematika schovaná za výpočtem promile je pouhá trojčlenka a věřím, že ji zvládnete :) Nezapomeňte, že při aritmetice s integery je potřeba dávat pozor na přetečení, proto ve výpočtu přetypovávám hodnotu na 32bit integer (aby se do něj výsledek násobení vešel).

 
Obr. 1: Použití AVCC jako reference vyžaduje připojení 100 nF kondenzátoru na AREF

Obr. 1: Použití AVCC jako reference vyžaduje připojení 100 nF kondenzátoru na AREF

 

001: // A) Atmega8A – jednoduchý převod, REF=AVCC
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: // odesílání UARTem
009: int usart_putchar(char var, FILE *stream);
010: void setup_uart(void);// nastavení parametrů UARTu
011:
012: static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
013: uint16_t hodnota, promile;
014:
015: int main(void){
016: setup_uart();// nastavení komunikace
017: ADMUX = (1<<REFS0) | 0b0001;// reference AVCC, měření na kanálu ADC1
018: // povolit ADC, clock pro ADC F_CPU/64
019: ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);
020: while(1){
021: ADCSRA |= (1<<ADSC);// spustit převod
022: while(ADCSRA & (1<<ADSC)){}// čekej dokud je ADSC=1 (probíhá převod)
023: hodnota=ADC;// vyčti výsledek převodu
024: // trojčlenka
025: promile = (uint16_t)((uint32_t)hodnota*1000/1023);
026: printf_P(PSTR("ADC = %u, %u.%u%%\n\r"),hodnota,promile/10,promile%10);
027: _delay_ms(700);// ať máme dost času si výsledek v terminálu přečíst
028: }
029: }
030:
031: void setup_uart(void){
032: stdout = &mystdout;// nastavení standardního výstupu na naši funkci usart_putchar()
033: UBRRL = 51;// baud rate 9600 s clockem 8MHz
034: UCSRB = (1<<TXEN);// zapnout UART vysílač
035: // formát zprávy “8N1″
036: UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1);
037: }
038:
039: int usart_putchar(char var, FILE *stream) {
040: while (!(UCSRA & (1<<UDRE)));// čekej než se odešle poslední znak
041: UDR = var;// odešli další znak
042: return 0;
043: }
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.
 
B) ATmega8A – měření s externí referenci (TL431)

Tento příklad slouží jako ukázka použití externího referenčního napětí. Tato, na externí součástky náročnější, varianta slouží typicky k přesnějším měřením. Má hned dvě hlavní výhody. Díky tomu, že si referenční napětí volíte sami, volíte si i rozsah měření. Jestliže o měřeném signálu víte, že se bude měnit v rozsahu 0 – 2 V můžete použít externí referenci o hodnotě 2 V a měřený signál se tak bude pohybovat v celém měřícím rozsahu převodníku. Získáte tím nejvyšší možné rozlišení. Naopak pokud bude signál v rozsahu například 0 – 4 V, tak vám nic nebrání připojit si externí referenci o hodnotě 4 V čímž opět přizpůsobíte rozsah převodníku měřenému signálu. K čemu to je? Představte si, že měříte signál v rozsahu 0 – 1 V převodníkem s referencí 0 – 5 V. Výsledek převodu může v takovém případě nabývat hodnot pouze v rozsahu 0 – 205. Měřený signál tedy rozdělíte pouze na 205 úrovní a měření bude hrubé. Referenci se proto snažte volit co nejmenší (ale tak aby ji měřený signál nepřekročil). Druhá výhoda externí reference je v přesnosti. Interní reference mají dost nepřesnou hodnotu a je potřeba ji ručně měřit a korigovat v programu. Jako externí referenci můžete použít jeden z mnoha integrovaných obvodů (TL431, LM336, LM4040, MCP1541, REF191 atd.), jejichž výstupní napětí je jasně definované a předem známé. My jako externí referenci použijeme obvod TL431. Jeho výstupní napětí jde pomocí děliče upravovat. Na Obr. 2 vidíte způsob zapojení. Rezistory R3 a R4 slouží k hrubému nastavení referenčního napětí někde okolo 4 V, trimr R2 pak slouží k jemnému doladění hodnoty. Já ji naladil přesně na 4.096 V (je to pěkně dělitelné 1024). Až ji budete ladit, připojte voltmetr mezi AREF a GND a otáčejte trimrem R2 tak dlouho než dosáhnete vámi požadované hodnoty. Rozsah přeladění by měl být přibližně 3.95 – 4.5 V. Měřený signál přivádějte na ADC1.

 
Obr. 2: Připojení externí reference k mikrokontroléru. R1,R2,R3,R4 a VR1 tvoří obvod reference. Jehož výstup je přiveden na AREF.

Obr. 2: Připojení externí reference k mikrokontroléru. R1,R2,R3,R4 a VR1 tvoří obvod reference. Jehož výstup je přiveden na AREF.

 

Program je celkem přímočarý, externí referenci nastavujete nulováním bitů REFS0 a REFS1. Já je explicitně nenuluji, protože po restartu mikrokontroléru jsou vynulované. V multiplexeru (ADMUX) zvolíme kanál ADC1. Nastavením bitu ADEN spustíme převodník, hodiny převodníku volíme v povoleném rozsahu, z 8 MHz děličkou 64 dostáváme 125 kHz. Převod spouštíme nastavením bitu ADSC a čekáme, dokud je tento bit nastaven. Jakmile je jeho hodnota log. 0 víme, že převod skončil a můžeme vyzvednout výsledek z “registru” ADC. Pak už zbývá jen trojčlenka a přepočet na napětí. Hodnotu referenčního napětí můžete specifikovat v makru VREF. Protože počítáme s celými čísly, která ale reprezentují čísla desetinná, musíme si zvolit pevně pozici desetinné čárky. Jinak řečeno celou aritmetiku jsem se rozhodl provádět v jednotkách mV. Volba je přirozeně na vás. Nikdo vám nebrání vyjadřovat napětí v 10 mV, ale přišli byste tak o část informace, protože rozlišení převodníku při referenci 4.096 V je 4 mV. Výpočty s celými čísly je potřeba řešit tak, aby se “ztrátové” operace prováděly buď co nejméně, nebo alespoň co “nejpozději”. To je přirozeně téma na delší povídání, tak jen lehce nastíním o jaký problém jde. Sledujte následující příklad:

  • Chceme vypočíst x = a/b*c
  • a=4; b=3; c=22;
  • Teoreticky správně: x = (4/3)*22 = 1.333*22 = 29.333
  • Celočíselně špatně: x = (a/b)*c = 1*c = 22;
  • Celočíselně dobře: x = (a*c)/b = 88/3 = 29;

Zvláště pokud je výpočet komplexnější tak se nevyplatí rozdělovat ho na mezivýsledky. Lepší je s tužkou na papíře spočítat obecný vztah a pak ho uspořádat tak aby se eliminovaly “zaokrouhlovací” chyby. Vyjma toho je příklad nezáludný. K odeslání výsledků do PC je využit USART s jehož použitím se můžete seznámit Zde.

 

001: // B) Atmega8A – měření s externí referenci 4.096V
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: #define VREF 4096// napětí externí reference v mV
010:
011: // odesílání UARTem
012: int usart_putchar(char var, FILE *stream);
013: void setup_uart(void);// inicializace UART rozhraní
014:
015: static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
016: uint16_t val,u;
017:
018: int main(void){
019: setup_uart();// nastavení komunikace
020: ADMUX = 1;// externí reference na AREF (REFS1=0,REFS0=0), kanál ADC1
021: // povolit ADC, clock pro ADC F_CPU/64
022: ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);
023:
024: while(1){
025: ADCSRA |= (1<<ADSC);// spustit převod
026: while(ADCSRA & (1<<ADSC)){}// čekej dokud je ADSC=1 (probíhá převod)
027: val = ADC;// výsledek měření na ADC1
028: // přepočíst na napětí
029: u = (uint16_t)((uint32_t)val*VREF/1023);
030: // odeslat do PC
031: printf_P(PSTR("val = %u, u= %u mV\n\r"),val,u);
032: _delay_ms(700);
033: }
034: }
035:
036: void setup_uart(void){
037: stdout = &mystdout;// nastavení standardního výstupu na naši funkci usart_putchar()
038: UBRRL = 51;// baud rate 9600 s clockem 8MHz
039: UCSRB = (1<<TXEN);// zapnout UART vysílač
040: // formát zprávy “8N1″
041: UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1);
042: }
043:
044: int usart_putchar(char var, FILE *stream) {
045: while (!(UCSRA & (1<<UDRE)));
046: UDR = var;
047: return 0;
048: }
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.
 
C) ATmega8A – měření s interní referencí 2.56 V a s průměrováním

Na tomto příkladě si předvedeme jak používat interní referenci mikrokontroléru. Jestliže se nechcete obtěžovat s připojováním externí reference, případně na ni nemáte na DPS místo, můžete využít referenci interní, kterou je vybaven nejspíš každý mikrokontrolér. Její použití vás do jisté míry limituje. V prvé řadě vám omezuje rozsah měření, protože jak jsem již nastínil v úvodu, měřené napětí musí být nižší jak referenční. Mnohdy vás to může donutit snižovat měřená napětí odporovým děličem (který se špatně kalibruje) a obecně tak snižovat přesnost měření. Kromě toho je interní reference dosti nepřesně určená. Typicky ji datasheet uvádí s rozptylem +/- 0.1 V. Naštěstí její hodnota příliš nekolísá s teplotou ani s provozním napětím mikrokontroléru, takže když ji jednou změříte, už se s ní dále dá zacházet jako s relativně přesnou referencí. Problém je v tom, že se liší kus od kusu, takže ji musíte měřit u každého konkrétního mikrokontroléru. I přes to je ale její použití široce rozšířené (ne vždy potřebujete měřit přesnou hodnotu napětí). Ošetření pinu AREF je stejné jako v příkladě A), tedy kondenzátorem 10 – 100 nF mezi AREF a GND. Jakmile referenci zapnete, tak si můžete její hodnotu na pinu AREF změřit. Výsledek měření je pak vhodné využít v programu pro výpočet napětí. V mém případě měla interní reference hodnotu 2.53 V.

Aby program nebyl tak nudný, tak si v něm ukážeme mimo jiné i ukázku toho, jak drobně zvýšit přesnost měření průměrováním. Průměrování má smysl tam, kde je měřená hodnota zarušená (obsahuje náhodný šum). Rozhodně to není samospásný prostředek ke zvýšení přesnosti. Klidný a nezašumněný signál běžně průměrováním nezlepšíte (i když by se vám na první pohled mohlo zdát, že ano). Teď ale přejděme ke zdrojovému kódu. Nastavením bitů REFS0 a REFS1 vybereme interní referenci jako referenci pro převodník. Po nastartování A/D převodníku (bitem ADEN) se spustí i interní reference a potřebuje 70 µs na svoje ustálení. Hodiny pro převodník volíme pomocí bitů ADPSn na 125 kHz. Celý proces měření i s průměrováním řeší funkce zmer. Prvním argumentem je kanál, na němž chceme měřit. U ATmega8 máme k dispozici de facto osm vnějších kanálů (ADC0 až ADC7). V našem příkladě volíme kanál ADC1. Druhým argumentem je počet vzorků, z nichž má funkce průměrovat. Typicky stačí osm nebo šestnáct vzorků. My volíme třicet dva. Maximální množství vzorků je pro tuto funkci šedesát čtyři. To je dáno rozsahem uint16_t. Pokud bychom zvolili pro proměnnou tmp větší formát, tak bychom mohli průměrovat větší množstvím vzorků, ale nemělo by to asi valný význam. Funkce nejprve ošetří vstupy, pak zápisem do registru ADMUX zvolí kanál. Číslo kanálu je také pro jistotu ošetřeno (maskou 0x0f), aby uživatel zadáním nevhodných hodnot nezpůsobil zbytečný problém. Dále se ve “forcyklu” do proměnné tmp přičítají výsledky převodu. Po načtení požadovaného množství vzorků se spočte aritmetický průměr a funkce výsledek vrátí. Ten je v hlavní smyčce zpracován a odeslán do PC pomocí USART (více o USARTu Zde).

 

001: // C) Atmega8A – měření s interní referencí 2.56V
002: // a s průměrováním
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: #define VREF 2530// změřená hodnota interní reference v mV
010:
011: // odesílání UARTem
012: int usart_putchar(char var, FILE *stream);
013: void setup_uart(void);// inicializace USART
014: // měří a průměruje
015: uint16_t zmer(char kanal, char vzorku);
016:
017: static FILE mystdout = FDEV_SETUP_STREAM(usart_putchar, NULL, _FDEV_SETUP_WRITE);
018: uint16_t val,u;
019:
020: int main(void){
021: setup_uart();// nastavení komunikace
022: // vybírá referenci interní 2.56V
023: ADMUX = (1<<REFS0) | (1<<REFS1);
024: // povolit ADC, clock pro ADC F_CPU/64
025: ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);
026: _delay_us(70);// počkej než se nastartuje reference
027:
028: while(1){
029: val=zmer(1,32);// změř napětí na kanálu 1 a průměruj ze 32 měření
030: // vypočti hodnotu napětí
031: u = (uint16_t)((uint32_t)val*VREF/1023);
032: // odešli ji do PC
033: printf_P(PSTR("val = %u, u= %u mV\n\r"),val,u);
034: _delay_ms(700);
035: }
036: }
037:
038: // kanal – vybírá kanál ADC (0 až 7)
039: // vzorku – počet měření z nichž se
040: // průměrovat (max 64)
041: uint16_t zmer(char kanal, char vzorku){
042: char i;
043: uint16_t tmp=0;
044:
045: if(vzorku>64){vzorku=64;}// ošetření vstupních dat
046: // vybírá kanál, reference interní 2.56V
047: ADMUX = (1<<REFS0) | (1<<REFS1) | (kanal & 0x0f);
048: for(i=0;i<vzorku;i++){
049: ADCSRA |= (1<<ADSC);// spustit převod
050: while(ADCSRA & (1<<ADSC)){}// čekej dokud je ADSC=1 (probíhá převod)
051: tmp = tmp + ADC;// sčítá výsledky převodu
052: }
053: return (tmp/vzorku);// vypočítá průměr a vrátí ho
054: }
055:
056: void setup_uart(void){
057: stdout = &mystdout;// nastavení standardního výstupu na
058: // naši funkci usart_putchar()
059: UBRRL = 51;// baud rate 9600 s clockem 8MHz
060: UCSRB = (1<<TXEN);// zapnout UART vysílač
061: // formát zprávy “8N1″
062: UCSRC = (1<<URSEL) | (1<<UCSZ0) | (1<<UCSZ1);
063: }
064:
065: int usart_putchar(char var, FILE *stream) {
066: while (!(UCSRA & (1<<UDRE)));
067: UDR = var;
068: return 0;
069: }
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.
 

 
Autor: Michal Dudka
 

Jiné příspěvky v kategorii:

 
Začínáme s mikrokontroléry ATmega – A/D převodník 1. díl (teoretický základ)

 
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.