Lese av ADC på AVR via C

Etter å ha raskt behersket lesing av digitale signaler fra pinner, var jeg gira på å lære hvordan man gjør det samme med de analoge. Mye elektronikk handler tross alt om analoge signaler, det er sjelden man kun har heldigitale kretser.

For at en mikrokontroller skal kunne tolke de analoge signalene den mottar, har den en analog-til-digital omformer. På engelsk blir dette «analog-to-digital converter» (ADC). Den fungerer slik at spenningsintervallet som ADC-en støtter, deles opp og oversettes til en mengde av digitale koder.

Arduino

Som vanlig finnes det publiserte lettforståelige Arduino-prosjekter som demonstrerer dette i praksis, blant annet Analog Read Serial. Alt man trenger er brødbrett, testledninger og et potensiometer (justerbar resistor).

Med vanlig barnevennlig Arduino-kode, er det bokstavelig talt kun en stk. kodelinje for å lese av ADC-en:

int adcVerdi = analogRead(A0); // A0 er valgt pinne

Alt man trenger er en variabel for å oppbevare avlest verdi og navnet på brukt pinne.

Komplett kodeeksempel i Arduino for avlesing av potensiometer:

void setup() {
   Serial.begin(9600);
}

int adcValue = 0;

void loop() {
   int newAdcValue = analogRead(A0);

   if (adcValue == 0) {
      adcValue = newAdcValue;

   } else if (abs(adcValue - newAdcValue) >= 2) {
      adcValue = newAdcValue;
      Serial.println(adcValue);
   }
   delay(1);
}

Så lenge forskjellen fra den gamle ADC-verdien er stor nok, blir den nye verdien skrevet ut via Serial Monitor. ADC-en vil ikke alltid spytte ut den samme verdien, selv om potensiometeret står urørt, så litt slingringsmonn er nødvendig. (Her kommer funksjonen abs() inn i bildet.)

Utfordringen er å gjøre alt dette på egenhånd, ved å selv manipulere registre og sette bits, via embedded C. Å ta full styring over hver lille detalj og dermed ha kontroll over hva som faktisk foregår …

Embedded C

Med "vanlig" C, eller mer korrekt embedded C, blir det mer arbeid. Fordi mikrokontrolleren har en rekke registre med viktige enkeltbits som må konfigureres riktig. Her er man pent nødt til å lese deler av dataarket til mikrokontrolleren, ATmega328P i dette tilfellet, for å skjønne hva som foregår.

Hvis ikke vil man neppe klare å få fungerende kode. Det er veldig mange dårlige eller rotete eksempler på Internett, hvor det ofte dras inn registre og bits som slettes ikke trenger å røres.

Praksis

For å teste dette i praksis valgte jeg lysdiode i stedet for Serial Monitor. Og målet ble å få dioden til å tennes når ADC returnerer en bestemt verdi, så man vet at koden fungerer.

Følgende (fungerende) kode ble resultatet av dette lille prosjektet:

#define F_CPU 16000000UL

#include <avr/io.h>
#include <util/delay.h>

int main(void) {

   // Pin for LED
   DDRD |= 1<<PD3;

   // Deactivate ADC power save mode
   PRR &= ~(1<<PRADC);

   // Disable digital input buffers (5:0) for ADC
   DIDR0 = 0b00111111;

   // Pin and voltage reference setup
   ADMUX = 0x00; // AREF + right adjustment + adc port 0

   // Set a safe clock speed and finally enable ADC
   ADCSRA |= 1<<ADPS2 | 1<<ADPS1 | 1<<ADPS0; // 111: divide MCU clock speed by 128, this gives 16MHz / 128 = 125kHz
   ADCSRA |= 1<<ADEN; // enable ADC

   while (1) {

      // Start conversion
      ADCSRA |= 1<<ADSC;

      // Wait for conversion to finish
      while (ADCSRA & 1<<ADSC)
      {
         ; // do nothing, when waiting for ADSC to become 0
      }

      // Show result
      if (ADC >= 1000) // ADC with 10-bit resolution: 2^10 = 1024 options, so ADC value is 0-1023
         PORTD |= 1<<PD3; // enable pin for connected led
      else
         PORTD &= ~(1<<PD3); // disable pin for connected led
      }

      return 0;
   }
}

Lysdioden tennes når ADC-verdien er 1000 eller mer (tilfeldig valgt grenseverdi). Altså må ADC-en bli gitt spenning veldig nærme det som er referansespenningen.

Det betyr at potensiometeret må skrus nesten helt til bunnen/toppen, så dens resistans er svært liten og derfor knapt skaper et spenningsfall.

Teorien

Dataarket (fra side 246 og utover) forklarer i grunn ganske greit alle detaljene, men det tar likevel litt tid å forstå hvorfor alt trengs. Man bør lese seg opp på ADC-er samtidig, om dette er et nytt tema. Spesielt bør en forstå kvantisering og hva ADC-oppløsning er. Se gjerne eksemplet i «ADC» (et tidligere innlegg her på Bakkenblogg), som forklarer dette.

For å avlese spenning, hvor denne går fra 0V (jord) til AREF (5V), fant jeg ut at man må berøre minst -3- forskjellige registre, også stille inn flere viktige bits (1 eller 0).

Digital Input Disable Register 0  (DIDR0)

Dette registeret er strengt tatt ikke nødvendig å røre. Man vil kunne avlese ADC-en uansett. Registeret gjør det mulig å deaktivere alle digitale inndatabuffer som ADC-en har. Disse brukes ikke når man avleser analoge verdier. Så ved å deaktivere sparer man strøm.

Power Reduction Register (PRR)

Som navnet tilsier er dette et register for strømsparing. I følge dokumentasjonen må man skru av strømsparing for ADC-en, ellers vil den ikke virke. Derfor settes bit-en ved navn PRADC til 0.

Med strøm via USB til Arduino-brettet, virker PRADC å alltid være 0, etter å ha testet dette litt. Men i andre sammenhenger er den kanskje eller antagelig 1. Derfor er det greit å alltid ta den med, for å være sikker på at riktig innstilling er satt.

ADC Multiplexer Selection Register (ADMUX)

Med dette registeret bestemmer man spenningsreferansen (bit REFS1 og REFS0), venstre eller høyre justering av ADC-resultatet (bit ADLAR), også hvilken pinne (bit MUX3 til MUX0) på ADC-en som skal benyttes i dette øyeblikket.

Bare en pinne kan brukes om gangen, men om flere ting allerede er koblet til fysisk, er det fort gjort å bytte mellom pinner for å lese av forskjellige signaler. Faktisk skjer dette så raskt at det virker som det skjer samtidig …

Siden Arduino-prosjektet benyttet 5V, valgte jeg å fortsette med dette. Men for å få dette til å fungere måtte jeg koble 5V til AREF med testledning. Begge bit, REFS1 og REFS0, ble så satt til 0. Valgt analog pinne for ADC ble uendret; 0 = A0 = ADC0, så MUX3 til MUX0 ble også satt til 0, alle sammen.

Og da venstrejustering var fremmed for meg og ikke brukt før, beholdt jeg høyrejustering, så ADLAR ble 0 den også.

ADC Control and Status Register A (ADCSRA)

For å styre ADC-en i drift må man sette verdier (bits) i kontroll- og statusregistrene. ATmega328P har både et A-register og B-register. Sistnevnte (B = ADCSRB) er mer for timere/tellere og håndtering av avbrudd («interrupts»). Dette registeret trenger man ikke røre nå i første omgang. Jeg rørte derfor heller ikke slike bits i ADCSRA-registeret.

Aller først kan man begynne med å sette hvor raskt ADC-en skal avlese verdier, dette gjøres i bitsene ADPS2 til ADPS0. På Arduino Uno (med ATmega328P) er det ekstern oscillator på 16 MHz, dette er altfor raskt for ADC-en. Den trenger ei klokke som er maks 1 MHz, ellers leser den av upresist. (Oppløsningen blir elendig.) Så ved å sette bitsene til 111 brukte jeg høyeste delingsfaktor (128) og fikk klokkefrekvens 16000000/128=125000, altså 125 kHz.

For å skru på ADC-en benyttes bit-en ADEN, som settes til 1. For å starte en ny konvertering (altså avlesning/omgjøring av analog til digital), settes også ADSC til 1. Dette blir ADC-ens "opptatt"-signal i denne sammenhengen.

ADC-en vil nå begynne arbeidet sitt og man må vente til ADSC blir 0 igjen, før man kan avlese ADC-verdien. Første konverteringen etter aktivering vil også ta mer tid og kan være mer unøyaktig enn de som gjøres senere. Men i praksis har dette lite å si.

Data Direction Register D – DDRD

I koden over benyttes pinne PD3 på Port D (makro PORTD), til å skru på en lysdiode. Men dette har ingenting med ADC-en å gjøre. Det er kun for å vise resultatet. (I Arduino-koden øverst ble Serial Monitor brukt i stedet ..)

Konklusjon

Det ble en god del lesing, grubling og knoting, før jeg endelig forstod hvordan alt hang sammen. Og fortsatt er det mye som er uklart, f.eks. avbrudd («interrupts»), men det er fordi jeg ikke har brukt nok tid på det enda.

Alt i alt vil jeg si det er enkelt å bruke en ADC, man trenger bare å studere dataarket godt og gi det tid. Dette er åpenbart et modningsfag, slik som matematikk.

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *