Analogno-digitalni pretvarač (ADC) kod Arduina s ATmega328P, ukratko

U starinskom telefonskom imeniku od 1000 papirnih stranica uvek je moguće naći zadatu osobu sa samo deset otvaranja te debele knjige: prvo se imenik otvori tačno na sredini i utvrdi se da li je – po azbučnom redu – prezime tražene osobe u prvoj ili u drugoj polovini. Tako jedna polovina (500 stranica) može biti zanemarena, a druga polovina opet se otvori tačno na sredini i opet se pogleda u kojoj polovini polovine mora biti traženo prezime. Izbor se time sužava na 250 stranica. Onda se ta perspektivna četvrtina otvori na sredini, pa se na isti način izbor suzi na 125 stranica… i tako dalje. Dakle, potrebno je najviše deset pokušaja-otvaranja da bi se pronašla prava stranica, jer je 2 na 10 = 1024.

Taj algoritam binarne pretrage jeste osnova metoda uzastopnih aproksimacija (successive approximation) koji primenjuju mnogi analogno-digitalni pretvarači (ADC), na primer oni u Arduinu, kojima se po pravilu obraćamo funkcijom analogRead(). A/D pretvarač uzima ulazni napon, pomenutim metodom utvrđuje njegovu vrednost, i izražava je u digitalnom obliku. ADC u mikrokontroleru ATmega328P ima 10-bitnu rezoluciju, što znači da je rezultat A/D pretvaranja (konverzije) izražen desetobitnim brojem.

Donja granica očekivanog ulaznog napona jeste 0 V. Gornja granica određena je vrednošću referentnog napona, koji je podrazumevano jednak naponu napajanja, ali se može podesiti i drukčije.

Kada program zahteva očitavanje ADC (“analognog”) ulaza, tj. vrednosti napona na njemu, hardver A/D pretvarača u mikrokontroleru omogućava da se vrlo mali interni kondenzator (14 pF) napuni do visine trenutnog ulaznog napona i onda odspoji od njega, što se zove ‘uzorkovanje i zadržavanje’, ‘sample-and-hold’.

Zatim kreće postupak uzastopnih aproksimacija: A/D pretvarač sam proizvede napon koji je jednak POLOVINI vrednosti referentnog napona, pa onda uporedi da li je zadržani napon na kondenzatoru veći ili manji od te polovine referentnog. Ako je, na primer, manji, pretvarač u određeni registar upiše “nulu“. Potom pretvarač proizvede napon jednak ČETVRTINI referentnog napona, i opet poredi. Ako je napon na kondenzatoru, na primer, veći od 1/4 referentnog, pretvarač upiše “jedinicu” iza već upisane”nule”, pa proizvede napon koji iznosi TRI OSMINE referentnog napona, pa opet poredi… i tako deset puta uzastopno, sve bliže merenoj vrednosti. Bit po bit, dobije se rezultat: desetobitni ceo broj, decimalno 0…1023. On opisuje vrednost ulaznog napona relativno, u odnosu na referentni napon (koji bi bio predstavljen maksimalnim rezultatom 1023).

Grafički prikaz toka uzastopnih aproksimacija s 4-bitnim rezultatom. Vdac je napon koji A/D pretvarač proizvodi radi poređenja s ulaznim naponom Vin. Referentni napon označen je sa Vref.

Ako pomenuti rezultat konverzije (tj. rezultat funkcije analogRead()) označimo sa adc_res, a referentni napon sa v_ref, u programu izračunavamo vrednost ulaznog napona:

int adc_res;
float v_ref;

float v_in = v_ref * (adc_res + 0.5 ) / 1024;

Zašto dodajemo 0.5: rezolucija je nužno ograničena, tako da svaki rezultat adc_res zapravo predstavlja jedan RASPON napona. Kod 10-bitnog A/D pretvarača postoje 1024 moguća rezultata, tj. 1024 raspona od kojih je svaki širok v_ref/1024, tj. oko 4,88 mV za v_ref = 5.0 V. Na primer: ako je adc_res = 510, to bi upućivalo na raspon od oko 2,490 V do oko 2,495 V. Bez "+ 0.5", za vrednost ulaznog napona uvek bismo uzimali NAJNIŽU vrednost iz dobijenog raspona, tj. uvek bismo zaokruživali na dole, tako da bi greška zaokruživanja mogla biti celih 4,88 mV. Dodavanjem 0.5 na adc_res, za vrednost ulaznog napona uzimamo vrednost iz SREDINE dobijenog raspona, čime je maksimalna greška zaokruživanja smanjena na 2,44 mV.

Valja pomenuti da navedena formula podrazumeva A/D pretvarač koji je savršeno linearan i bez razdešenosti (ofseta). Pretvarači u praksi nikada nisu savršeni, ali pomenuti izvori greške po pravilu su zanemarivi u kontekstu Arduina.

Osim napona napajanja kao reference, Arduino može koristiti i svoj unutrašnji izvor referentnog napona, koji kod ATmega328P daje nominalno 1,1 V. Ta unutrašnja referenca jeste relativno stabilna, ali njena vrednost može biti bilo gde u rasponu 1,0V…1,2 V. Referentni napon trebalo bi da bude što tačniji i stabilniji (u odnosu na vrednost navedenu u programu). Moguće je dovesti i referentni napon spolja, preko pina (A)REF. Vidi funkciju analogReference().

Kod Arduina koji radi na 16 MHz, podrazumevani ADC takt je 125 kHz (glavni takt / 128), a “normalna” konverzija uvek traje 13 ciklusa ADC takta, što daje 104 μs nominalno. (Od toga, punjenje internog kondenzatora traje 1,5 ciklusa.) Izuzetno, prva konverzija posle uključenja A/D pretvarača traje 25 ciklusa.

U jednoj sekundi moguće je napraviti oko 9000 konverzija, ako jedna traje 100+ μs. Da bi Arduino uspešno uzorkovao neki naizmeničan signal, frekvencija tog signala mora biti manja od polovine učestanosti uzorkovanja, tj. manja od 4,5 kHz, u NAJBOLJEM slučaju. Trajanje konverzije moguće je skratiti podizanjem ADC takta (menjajući delitelj) neposredno u odgovarajućem registru mikrokontrolera. Međutim, ubrzavanjem ADC takta preko 200 kHz smanjuje se preciznost, tj. broj upotrebljivih bita u rezultatu.

Napone/signale koji se dovode na analogne ulaze treba prethodno filtrirati, tj. iz njih potisnuti šum i frekventne komponente koje su više od polovine učestanosti uzorkovanja.

Funkcija analogRead() neće raditi kako treba ako je pin-argument prethodno proglašen za izlaz ili ako je na njemu uključen pull-up.

ATmega328P zapravo ima samo jedan analogno-digitalni pretvarač (ADC). Pinovi/ulazi A0…An automatski se povezuju na njega, uvek samo po jedan, po zahtevu iz programa. Potrebno je izvesno (mada vrlo kratko) vreme za te “leteće izmene”. Zato, ako se u programu očitava vrednost na jednom analognom pinu, pa odmah potom na nekom drugom, neki kažu da bi trebalo zanemariti makar prvo očitavanje na drugom pinu, jer može biti netačno. Tehnički list mikrokontrolera ne pominje to kada govori o pojedinačnim konverzijama (‘Single Conversion mode’), kao sa analogRead(). Mada: u vezi s tim valja reći da je za brzu ispravnu smenu ulaza, a i inače, važno da impedansa izvora ulaznog napona nije prevelika. ADC u ATmega328P projektovan je za izvore napona koji imaju unutrašnji otpor manji od 10 kΩ , tj. otpor između idealnog naponskog izvora i ADC ulaza trebalo bi da bude manji od 10 kΩ.

S druge strane, dokumentacija jasno kaže da valja zanemariti prvo sledeće očitavanje posle promene izvora referentnog napona. (Za svaki slučaj – i nekoliko očitavanja, koliko vreme dozvoljava.)

ADC u ATmega328P može generisati prekid (interapt, ‘interrupt’) koji signalizira da je konverzija završena. To je korisno pre svega zato što je A/D pretvaranje relativno sporo. Upotreba ovog prekida omogućava da program ne stoji dok ADC radi svoj posao, već program može istovremeno raditi nešto drugo, a pokupiće rezultat konverzije kada naiđe prekid.

Pinovi A6 i A7 na Arduinu Nano i Pro Mini – NE mogu biti korišćeni kao digitalni ulazi/izlazi, za razliku od pinova A0…A5. Kod Arduina Uno, pinovi A6 i A7 nisu ni izvedeni na konektore. Ta dva pina inače ne postoje kod ATmega328P u starinskom PDIP kućištu.

ATmega328P ima pin AVCC koji omogućava da se napajanje za interni A/D pretvarač posebno filtrira, da bi se zaštitilo od šuma iz digitalinh kola. Međutim, kod Arduina taj je pin već neposredno spojen s pinovima preko kojih se napaja ostatak mikrokontrolera.

ATmega168PA, na kome je zasnovana jedna verzija Arduina Nano, ima praktično isti ADC kao ATmega328P.

Načelno, pod izvesnim uslovima, moguće je softverski podići efektivnu rezoluciju A/D pretvarača koristeći nadodabiranje (‘oversampling’). To je zanimljiva tema, ali taj metod nosi svoje zahteve i teškoće. U kontekstu u kome inače govorimo, mnogo je lakše upotrebiti ili mikrokontroler koji ima ADC odgovarajuće veće rezolucije, ili bolji ADC u obliku namenskog integrisanog kola.

Usput: funkcija analogWrite() nema nikakve veze s A/D pretvaračem.

Usput: ako na analogne ulaze nije povezano ništa, svakakav električni šum, iznutra i spolja, može na njima stvarati nezanemarive, promenljive i uglavnom nepoželjne napone. Iako vrednost tih napona izgleda “slučajna”, nije baš dobra preporuka da se napon očitan s nepovezanog analognog pina koristi kao seme (‘seed’) generatora pseudo-slučajnih brojeva, jer pomenuta vrednost napona može se predugo kretati unutar premalog raspona. Za pomenutu svrhu bolje je kombinovati vrednosti napona s više pinova ili, još bolje, recimo, koristiti samo poslednje bitove iz većeg broja očitavanja.

Još:
https://www.gammon.com.au/adc
https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf

http://ww1.microchip.com/downloads/en/Appnotes/AN2538-ADC-of-megaAVR-in-SingleEnded-Mode-00002538A.pdf



P.S.

Postoji još jedan razlog zašto nije dobro za početnu vrednost generatora pseudoslučajnih brojeva (funkcija randomSeed()) uzimati vrednost s jednom očitanog analognog ulaza.

Vrednost koju daje analogRead() ima vrednost u rasponu od 0 do 1023, uključivo. S druge strane, funkcija random() ima sledeći feature ili bug: ako je početna vrednost (seed) manja od 127774, PRVA vrednost koja se dobije funkcijom random(min, max), biće uvek jednaka

(seed * 7^5) % max            (seed ≠ 0)

Tako, ako je seed manji od 127774, a pozivamo, na primer, random(0,7), PRVI dobijeni pseudoslučajni broj biće uvek 0.

A ako je početna vrednost (seed) veća od 2^32 – 127774, PRVI pseudoslučajni broj koji se dobije funkcijom random(0,7), biće uvek 1.

Dakle, nije dobro ako je poznato da je početna vrednosti generatora pseudoslučajnih brojeva pri samom dnu ili pri samom vrhu raspona unsigned long.

Dobijanje PSEUDOslučajnih brojeva jeste deterministički proces, prosto “muljanje” brojevima koje u principu daje predvidive rezultate (ako je poznata početna vrednost), ali makar se valja potruditi da predviđanje ne bude previše lako.

Moguće rešenje za bolju početnu vrednost:

unsigned long NewSeed(byte ch) {	
const byte SEED_LEN = 32;
const byte CYCLES_TOTAL = 50; //175 ms ukupno. Funkcija se poziva samo jednom, na pocetku programa, tako da trajanje nije kriticno.
int noise_volt;
unsigned long seed = 0;

for (byte cycle=0; cycle < CYCLES_TOTAL; cycle++) {
for(byte bit_pos=0; bit_pos < SEED_LEN; bit_pos++) { //Popunice 32 bita za oko 3,5 ms.
noise_volt = analogRead(ch);
seed ^= ((noise_volt & 1UL) << bit_pos);
//Ili ovako: bitWrite(seed, bit_pos, bitRead(noise_volt,0) ^ bitRead(seed, bit_pos));
}
}
return seed;
}

Funkcija proizvodi 32-bitni broj očitavajući jedan analogni ulaz 1600 puta, koristeći samo nulte bitove (LSB) rezultata. Uz još jednu operaciju deljenja po modulu, funkcija bi se u principu mogla koristiti i umesto random().

Leave a Reply