Denne artikkelen tar for seg tema som kan være litt innfløkte, fordi vi hopper over en del av de innebygde Arduino-funksjonene og jobber direkte med registrene i mikrokontrolleren. Foreløpig tar vi for oss AtMega328 som er kontrolleren som sitter både i Arduino Uno og Arduino Nano, med flere.

Hvorfor i all verden skal vi gjøre det, spør du kanskje? Jo, i en del tilfeller blir koden både enklere, raskere og mer effektiv hvis vi manipulerer registrene direkte.

Først et eksempel som viser kode både med og uten Arduino-funksjonene:

Jeg har bygd opp en krets med Arduino Nano med 6 lysdioder og tilhørende seriemotstander. I tillegg har vi 3 trykknapper som vi skal bruke senere. Skjemaet er tegnet i Proteus, som angir pinnenummere litt annerledes. IO0, IO1, IO3 osv. tilsvarer D0, D1, D2, og de analoge pinnene angis med AD0, AD1 osv i stedet for A0, A1. For å gjøre oppkoblingen så enkel som mulig, bruker vi de interne pullup-motstandene på knappene slik at knappene kortslutter pinnen direkte mot jord når de aktiveres.

(Høyreklikk og velg "Åpne bilde i ny fane" for større bilde)

I den første testen skal vi bare blinke alle lysdiodene i et mønster, og ta tiden på de to forskjellige metodene for å sammenligne. For at det skal ha noen hensikt, må vi unngå å bruke delay-funksjonen. Det vil gjøre at blinkingen går så raskt at den ikke blir synlig for oss. Signalene kan eventuelt ses på et oscilloskop hvis du ønsker det.

Jeg vil bruke millis-funksjonen for å ta tiden, så helt uten Arduino-funksjoner blir det ikke.

Program med Arduino-funksjoner:

void setup() {
//Setter opp de 6 pinnene som utganger
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
pinMode(11, OUTPUT);
pinMode(12, OUTPUT);
pinMode(13, OUTPUT);
//Starter serieporten
Serial.begin(115200);
}

void loop() {
unsigned long start=millis(); //lagrer start-tiden
for(int i=0;i<10000;i++){ //Kjører testen 10.000 ganger
//Skrur hver annen lysdiode på og av
digitalWrite(8, HIGH);
digitalWrite(9, LOW);
digitalWrite(10, HIGH);
digitalWrite(11, LOW);
digitalWrite(12, HIGH);
digitalWrite(13, LOW);
//Skrur hver annen lysdiode av og på
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
digitalWrite(10, LOW);
digitalWrite(11, HIGH);
digitalWrite(12, LOW);
digitalWrite(13, HIGH);
}
//Skriver ut resultatet
Serial.print("Testen tok: ");
Serial.print(millis()-start);
Serial.println(" millisekunder");
while(1); //Stopper programmet her (uendelig løkke)
}

Program (nesten) uten Arduino-funksjoner:

void setup() {
//Setter opp de 6 pinnene som utganger
DDRB=0b00111111; //DDRB styrer port B på mikrokontrolleren
//Starter serieporten
Serial.begin(115200);
}

void loop() {
unsigned long start=millis(); //lagrer start-tiden
for(int i=0;i<10000;i++){ //Kjører testen 10.000 ganger
//Skrur hver annen lysdiode på og av
PORTB=0b00010101; //Skriver direkte til porten, alle pinnene i en gang.
//Skrur hver annen lysdiode av og på
PORTB=0b00101010;
}
//Skriver ut resultatet
Serial.print("Testen tok: ");
Serial.print(millis()-start);
Serial.println(" millisekunder");
while(1); //Stopper programmet her (uendelig løkke)
}

 

AtMega328 er en 8-bits mikrokontroller, og derfor også 8-bits porter. På denne er Port B, C og D tilgjengelig, men slik Arduinoen er bygd opp er det ikke praktisk mulig å bruke alle 8 bits i alle portene. For eksempel er bit 6 og 7 på port B koblet til krystallet som lager klokkefrekvensen på 16MHz. På port D er alle 8 tilgjengelig fra D0-D7, men D0 og D1 brukes samtidig til kommunikasjon mot PC'en, det er ikke alt som kan kobles til der uten at det blir problemer med programmering av kretsen.

For å gjøre det enkelt har jeg derfor valgt å bruke bare 6 pinner fra port B i dette eksempelet. Om disse skal være inn- eller utganger styres av et register som heter DDRB. DDR står for Data Direction Registry og B'en står dag selvsagt for port B. Tilsvarende har vi da også med DDRC og DDRC på denne kontrolleren. Disse registerene er også 8 bit, og en 1'er i registreret betyr at pinnen skal være en utgang, en 0 betyr inngang.

I setup-funksjonen setter vi opp porten med linjen DDRB=0b00111111;  Når vi starter et tall med 0b betyr det at vi angir tallet binært. Her ser vi at vi starter med to 0'er, som angir at PB7 og PB6 er innganger. De 6 enerene deretter gir oss utganger på pinnene PB5-PB0. Denne metoden gjør at vi kan sette opp inn- og utganger på 8 pinner samtidig, så fremt de tilhører samme port. 

Oversikt over pinnene på Arduino Nano kan du for eksempel finne her: https://www.teachmemicro.com/arduino-nano-pinout-diagram/  
Tilsvarende for Arduino Uno: https://www.circuito.io/blog/arduino-uno-pinout/ 

Det samme gjelder når vi skal skrive digitale verdier til pinnene, vi skriver 8 bits med en kodelinje: PORTB=0b00010101; Denne linja setter høyt signal (1) på pinnene PB0, PB2 og PB6, mens resten av pinnene blir lave. Som vi ser i den første versjonen av programmet, måtte vi bruke 6 digitalWrite for å få til det samme med standard Arduino-funksjoner.

Begge programmene skriver de samme bitmønstrene til pinnene, vekselvis 0 og 1, 10.000 ganger, mens tiden måles med millis-funksjonen.
Resultatet på min Nano:
  Program 1:  482 millisekunder
  Program 2:     3 millisekunder!

Hadde det ikke vært for Arduino-funksjonene, som for eksempel millis som står og går i bakgrunnen, hadde program 2 gått enda fortere. I teorien skulle det tatt 1,25 ms, men en forbedring på 160 ganger syns jeg uansett er bra!

I neste artikkel tar jeg for meg bit-shifting og lesing av registere direkte som erstatning for digitalRead.