Timere og avbrudd på AVR

Etter å ha lest om timere og avbrudd («interrupts») en stund nå, begynner endelig ting å falle på plass. Til å begynne med var denne delen av AVR veldig uoversiktlig, fordi det finnes et hav av bits i forskjellige registre som man må kjenne til og forstå.

Men så begynte jeg å innså at man ikke trenger å fullt ut forstå alle mulige konfigurasjoner, bare for å ta i bruk en timer og ha litt nytte av den .. Selv med grunnleggende forståelse kan man benytte en timer til mye forskjellig.

Og for å gjøre meg selv en tjeneste skal jeg fra nå av konsekvent kalle timere for tellere, kanskje dette fordrer mer intuitiv tenking. (Når jeg tenker på timere, så tenker jeg fort på timere i JavaScript – noe som bare forvirrer i denne sammenhengen ..)

Introduksjon

En teller har som oppgave å telle fra 0 og opp til og med en satt maksverdi. Også begynner den vanligvis på nytt igjen. Underveis kan telleren sammenligne tellerverdien med en gitt verdi (som du bestemmer).

Både å få treff ved sammenligning og å nå sluttverdien man skal telle til, gjør at det sendes et avbruddssignal man kan benytte til å kjøre kode man skriver selv.

Riktignok er det ikke bare tellere som sender avbruddssignaler. Det finnes andre kilder også, for eksempel PC6 (på ATmega328P i hvertfall) som ved 0 V skaper reset.

Praksis

Å ta i bruk tellere krever at man setter bits i minst et par registre. Skal man benytte et avbruddssignal til å kjøre kode, krever dette ett register til. Til flere endringer fra det som er standard (altså default), til flere registre og bits blir det ..

Avhengig av situasjonen kan det være nødvendig å deaktivere avbruddssignaler fordi man skal gjøre noe viktig. Også aktiverer man igjen etterpå. Dette gjøres med cli() og sei():

// Disable interrupts globally
cli();

...

// Enable interrupts globally
sei();

Dette vil da foregå i main() eller en funksjon som kjøres derifra.

Tidtaking er ganske grunnleggende å kunne kontrollere, nesten uansett hva man skal programmere. Derfor ble dette et greit sted å begynne her. Til dette valgte jeg å styre ei lysdiode med en enkel 8-bit teller (Timer2). Og med kode som kjører ved såkalt overflyt («overflow) – dvs. når telleren når maksverdien den skal telle til og begynner helt på nytt igjen:

volatile long int counter = 0;

ISR (TIMER2_OVF_vect)
{
   counter++;

   if (counter != 0 && counter == 250)
   {
      counter = 0;
      PORTB ^= 1<<PB4;
   }
}

int main(void)
{
   // 1. Prescaler
   TCCR2B |= 1<<CS22 | 1<<CS21; // 256

   // 2. Fast PWM mode
   TCCR2A |= 1<<WGM21 | 1<<WGM20;
   TCCR2B |= 1<<WGM22;

   // 3. Resolution, value to count to
   OCR2A = 0xFA; // Decimal value: 250

   // 4. Interrupt types
   TIMSK2 |= 1<<TOIE2;

   ////////////////////

   // Digital pin 12 = PB4
   DDRB |= 1<<PB4;
}

Denne kodebiten kan skru ei lysdiode på/av hvert sekund, takket være if-betingelsen som gjør at man venter lenge nok. Uten if-betingelsen ville dioden blitt tent/slukket flere hundre ganger i sekundet. (Se forklaring under EKSEMPEL lenger ned, for matematikken bak.)

Dioden er tilkoblet pinne 4 (PB4) på port D. Den skrus av/på med «bit shifting». For å sende ut må pinnen også være satt til dette i DDRB.

Fortsett å lese Timere og avbrudd på AVR

Kombinere registre på AVR

Hva gjør man hvis man skal oppbevare store verdier i et register og de tilgjengelige (ubrukte) arbeidsregistrene til mikrokontrolleren kun er 8-bit? Da er jo maks antall muligheter bare 2^{8} = 256. Det betyr at verdien bare kan være fra og med 0 til og med 255 … Og dette er altfor lite. Går man over oppstår overflyt («overflow»), da nullstilles registeret; 255 + 1 = 0 og 0 – 1 = 255.

Dette er når nummeret er usignert, altså er negative tall ikke støttet. Ved støtte av negative tall, kan lagret verdi være fra og med -128 til og med 127. (Forklaring: 128, 127, pluss tallet 0, gir 256 muligheter.) Overflyt gir her samme resultat, registeret nullstilles igjen; 127 + 1 = -128 og -128 – 1 = 127.

Om man derimot kombinerer 2 stk. 8-bit registre får man 16-bit og 2^{16} = 65 535 muligheter, det er litt bedre. Med 24-bit blir det 2^{24} = 16 777 216 muligheter, da bør man være rimelig sikret. (Dessuten har jeg ikke funnet flere registre jeg tør å bruke ..)

Her må man i grunn ned på bitnivå for å forstå hvordan det fungerer: Med 12 345 678 som tilfeldig valgt desimaltall, får man heksadesimaltallet BC 61 4E og binærtallet 1011 1100 0110 0001 0100 1110. Delt i tre blir dette BC = 1011 1100, 61 = 0110 0001 og 4E = 0100 1110. Hver del kan nå lagres i et 8-bit register. Ved uthenting må registrene avleses og hver del limes sammen igjen, i riktig rekkefølge. Sjekk selv med kalkulator som gjør om til forskjellige tallsystemer.

Ved å kombinere 3 stk. 8-bit registre (GPIOR0, GPIOR1 og GPIOR2) i ATmega328P, fikk jeg følgende kode som gjør jobben helt greit:

void SetRegisterValue(int isSigned, long int number)
{
   if (isSigned) number += 8388608;

   GPIOR2 = (number & 0xFF0000)>>16;
   GPIOR1 = (number & 0xFF00)>>8;
   GPIOR0 = number & 0x0000FF;
}

long int GetRegisterValue(int isSigned)
{
   long int msb = GPIOR2;
   msb = msb<<16;

   long int cb = GPIOR1;
   cb = cb<<8;

   long int lsb = GPIOR0;

   long int number = msb | cb | lsb;

   if (isSigned) number -= 8388608;

   return number;
}

Navnet GPIOR er forkortelse for «general purpose input output register». Dette er en type register man kan bruke til hva som helst. Og ved testing ser det ut til å fungere helt fint. Ved prøvekjøring klarer programmet å telle gjennom hele intervallet, både når man velger usignert og signert.

Dataene forblir i registeret (så lenge strømmen er tilkoblet), uten at mikrokontrolleren forandrer noe på egenhånd. Og ved overflyt begynner den automatisk på nytt, uten å klage på noe.

Egentlig er ikke dette så komplisert: Ved hjelp av masker (0xFF0000 og 0xFF00) klippes tallet i tre deler. Og for å gjøre 24-bit og 16-bit til 8-bit, benyttes «bit shifting» (fra koden over: >>16, >>8, <<16 og <<8). Dermed kan en tallverdi trygt oppbevares fordelt på flere registre, også forvandles den tilbake etter avlesing.

PWM på AVR med C

For å fake analoge signaler fra en mikrokontroller, benyttes en teknikk kalt pulsbreddemodulasjon. På engelsk er dette «Pulse-Width Modulation» (PWM).

Her sender man ut korte pulser (gjerne på 5V), også ingenting mellom hver puls. Tidsrommet mellom hver puls bestemmer hvor høy gjennomsnittsspenningen blir per periode. Sagt på en annen måte; bredden på pulsen (opp til 100%), styrer hvor høy gjennomsnittsspenningen blir per periode .. (Hver periode er veldig kort, den kan som regel måles i millisekunder.)

En eksempelillustrasjon som viser slike pulser grafisk:

Øverst har man en pulsbredde på 50%, da blir gjennomsnittsspenningen 2.5V. Og nedenfor gir 33% bredde ca 1.67V. Videre ville man fått 0.5V med 10% bredde, 1.25V med 25% bredde, osv.

Altså kan man fake hvilken som helst spenning man ønsker, så lenge man klarer å sette riktig størrelse/bredde på hver puls.

Arduino

Det finnes sikkert mange dokumenterte Arduino-prosjekter på Internett, som viser PWM i praksis. Men når arduino.cc har en side hvor analogWrite() forklares og det er eksempelkode rett nedenfor som viser riktig bruk. Da trengs det ikke noe mer.

Som da jeg lærte om ADC, er det også her kun en nødvendig kodelinje i Arduino:

analogWrite(3, pwmVerdi / 4);
// pwmVerdi er 0-1023, mens analogWrite() trenger 0-255. Tallet 3 er digital pinne nr. 3, denne støtter PWM

Ved å koble til en lysdiode kan man bruke denne for å teste PWM i praksis:

Et komplett kodeeksempel, hvor PWM benyttes for å sakte tenne/slukke en lysdiode:

/*
 * Settings (feel free to change and experiment)
 */
float secPerPeriod = 2; // # of seconds for each completed sine wave period
float frequency = 1.0/secPerPeriod; // Hz, the # of completed sine wave periods in 1 sec
float numberOfIncrements = 1000; // many increments makes for smoother LED lighting

/*
 * Sine wave constants (don't touch!)
 */
const float CIRCLE_RADS = 2*3.145; // 2 times PI constant
const float INCREMENT = CIRCLE_RADS / numberOfIncrements; // how much more to feed into sin() each time
const float TIME_PER_INCREMENT_MS = ((1.0/frequency) * 1000) / numberOfIncrements;

/*
 * Sine wave controls when program runs (don't touch!)
 */
float circleDegreesInRads = 0; // input for sin(), added to during loop
unsigned long timeLastRun = 0; // controls next execution time, updated during loop

void loop() {

   if (timeLastRun == 0 || timeLastRun + TIME_PER_INCREMENT_MS <= millis()) {
      timeLastRun = millis();

      // Update sine wave
      float sinVal = sin(circleDegreesInRads);
      circleDegreesInRads += INCREMENT;
      if (circleDegreesInRads >= CIRCLE_RADS) circleDegreesInRads = 0;

      // Write value to pin connected to LED
      float pwmVal = ((sinVal + 1) / 2) * 1024;
      analogWrite(3, pwmVal / 4);
   }

   delay(1);
}

void setup() {} // unused here

For morroskyld tok jeg i bruk sin()-funksjonen for å styre lysdioda, da det ville blitt for kjedelig å la være. Hastigheten kan enkelt styres ved å angi antall sekunder den periodiske funksjonen skal bruke. Dette gjøres med secPerPeriod øverst.

Embedded C

I Arduino er PWM tydeligvis enkelt, men hvordan arter det seg når man skal gjøre samme jobben via embedded C?

Fortsett å lese PWM på AVR med C

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.

Fortsett å lese Lese av ADC på AVR via C

LCD på AVR med C

Å koble til en LCD på Arduino er forholdsvis enkelt. Man trenger kun LCD, brødbrett, testledninger, 10k \Omega potensiometer, 220\Omega resistor, loddebolt, loddetinn og Arduino-brett med mikrokontroller.

Kretsen

Det første man bør gjøre er å lodde fast ei pinnerekke under LCD-kretskortet, for å få best mulig kontakt når displayet er tilkoblet:

Lar man være å gjøre dette, vil ikke skjermen virke. Det vil kun dukke opp firkantede svarte bokser, fordi noen pinner har for dårlig tilkobling.

Etterpå følger man så prosjektet «Liquid Crystal Displays (LCD) with Arduino» og får ca noe som ligner på følgende oppsett:

Alle disse koblingene kan virke komplekse og uoversiktlige, men det er de ikke. De gule ledningene er for data, de røde er for 5V og svart er minus/jord. Blå blir et slags unntak, da den er ingen av delene; den gir finjustert spenning inn på LCD for å regulere skjermkontrast. Høres kanskje innviklet ut, men det er det ikke.

Fortsett å lese LCD på AVR med C

Kom i gang med C for AVR

Etter å ha prokrastinert altfor lenge, var det idag endelig på tide å komme i gang med Embedded C for ATmega328p. Dette er valgt databrikke på Arduino UNO og derfor et trygt valg når man skal lære seg mikrokontrollerprogrammering.

ATmega328p er del av AVR-familien av mikrokontrollere som Atmel (kjøpt opp av Microchip i 2016) begynte å utvikle i 1996. Hvem som helst kan skaffe seg den. Enten ved å kjøpe løst eller i stedet skaffe seg f.eks. Arduino-startpakke. Jeg valgte Arduino-startpakke siden jeg også trengte brødbrett, testledninger, enkle komponenter, osv.

Arduino-brettet med mikrokontroller er enkelt å programmere via Arduino IDE. Dette er et neddummet programmeringsverktøy som gjør programmeringen lett. Og siden det finnes mange dokumenterte Arduino-prosjekter, med ulike vanskelighetsgrader, kan hvem som helst prøve seg. Derfor er det også et smart valg når man skal lære seg mikrokontrollerprogrammering.

Den reelle utfordringen blir å skrive ren embedded C-kode som kan erstatte jallakode skrevet i Arduino IDE. Dette krever forståelse av C, registermanipulering, osv.

Fortsett å lese Kom i gang med C for AVR

Pekere i C

Et kompakt eksempel som viser bruk av pekere i C:


#include <stdio.h>

int main() {

   // Variabler for dette kodeeksemplet
   int tall = 10; // int: størrelse lik 4 bytes på PC
   int tabell[3] = {11, 12, 13};

   // --------------------------------------------------------------------
   // EKSEMPLER PÅ BRUK AV PEKERE:
   // --------------------------------------------------------------------

   // DEKLARERING:
   int *peker1;
   int *peker2, *peker3; // to samtidig

   // INITIALISERING:
   peker1 = &tall; // peker til variabelen 'tall' (med verdi 10)
   peker2 = tabell; // peker til første elementet i 'tabell'
   peker2 = &tabell[0]; // samme resultat som over (alternativ måte)
   peker3 = &tabell[1]; // peker til andre elementet i 'tabell'
   peker3 = tabell+1; // samme resultat som over (alternativ måte)
   peker3 = tabell+2; // peker til tredje og siste elementet i 'tabell'

   // DEKLARERING OG INITIALISERING SAMTIDIG:
   int *peker4 = &tall; // peker til variabelen 'tall' (med verdi 10)
   int *peker5 = tabell; // peker til første elementet i 'tabell'

   // INKREMENTERING OG DEKREMENTERING:
   peker5++; // peker til andre elementet i 'tabell'
   peker5--; // peker til første elementet i 'tabell' igjen
   peker5 += 1; // samme resultat som øverst (alternativ måte)
   peker5 -= 1; // samme resultat som nestøverst (alternativ måte)

   // DIFFERANSE:
   int differanse =
   peker3 - peker5; // 2, fordi peker3 = siste element og peker5 = første element
   // printf("%d\n", differanse); // kontroll

   // SAMMENLIGNING:
   tabell == peker2; // 1 (sann), begge inneholder adressen til første element
   tabell == peker5; // 1 (sann), begge inneholder adressen til første element
   peker3 == peker5; // 0 (usann), adressen til element 3 er ikke lik element 1
   tabell == &tabell[0]; // 1 (sann), begge har samme adresse
   &peker1 == &tall; // 0 (usann), pekerens adresse er ikke lik adressen til 'tall' 
   // printf("%d %d %d %d %d\n", tabell == peker2, tabell == peker5, peker3 == peker5, tabell == &tabell[0], &peker1 == &tall); // kontroll

   // INDIREKSJON (AVLESING):
   *peker1; // 10
   *peker2; // 11
   *peker3; // 13
   *peker4; // 10
   *peker5; // 11
   *(tabell+1); // 12
   *(tabell+2); // 13
   // printf("%d %d %d %d %d %d %d\n", *peker1, *peker2, *peker3, *peker4,  *peker5, *(tabell+1), *(tabell+2)); // kontroll

   return 0;
}

TBC

Spesialtegn i tekst for C

C har en rekke spesialtegn man kan benytte seg av, når man skal skrive (og formatere) tekst som skal vises i terminalvinduet:

c-escape-sequences

Av alle disse er nok \n mest brukt, siden denne gir linjeskift. Jeg kan knapt huske å ha benyttet de andre noe særlig. Eksempel: printf("Dette er en test!\n");

Benytter man \a vil dette antagelig bare irritere brukeren, hvis den i det hele tatt er støttet på maskinen til vedkommende.

Operatørprioritering

Jeg begynte nylig å repetere og forfriske kunnskapen min i C. Da kom jeg over følgende tabell som viser operatørprioritet («operator precedence» på engelsk):

c-operators-precedence

Dette er den type kunnskap jeg aldri brydde meg noe særlig om. Fordi jeg alltid brukte mye paranteser og jeg pleide å bryte opp komplekse logikkuttrykk. Men, man bør vite litt om hvordan ulike operatører prioriteres likevel ..

AWS Lambda

Etter å ha knotet med AWS Lambda i over en måned har jeg lært mye nyttig som kanskje kan hjelpe andre som skal bruke denne superkjekke tjenesten.

(Det er lite kodeeksempler i AWS’ API-dokumentasjon så hver gang man skal ta i bruk ny funksjonalitet blir det gjerne noe prøving og feiling kombinert med googling.)

Første problemet man som regel opplever er at ens egen Lambda-funksjon avslutter før kall til andre AWS-tjenester returnerer et resultat (eller error). Dette er simpelthen fordi kallet er asynkront og kanskje trenger flere hundre millisekunder før det fullfører.

For å vente på asynkrone kall har man heldigvis flere muligheter.

Promise

En mye brukt, men fortsatt ganske ny løsning, er promise:

const AWS = require('aws-sdk');

exports.handler = async () => {

   var dynamodb = new AWS.DynamoDB(...);
   await dynamodb.putItem(...).promise().then(function() {
      // Kode for suksess her
   }).catch(function(error) {
      // Kode for feil her
   });

   // Synkron kode fortsetter her
};

Her er det await og promise som styrer showet. Og på promise lenkes then også catch. Etterpå kan man fortsette med annen synkron kode. Dette er en veldig ryddig måte å gjøre det på. Dessverre er det ikke alle AWS API-er som har promise.

Om det er flere asynkrone kall som skal gjennomføres bør man fjerne await over og vente på kallene samtidig med Promise.all:

...
var dynamodb = new AWS.DynamoDB(...);
var promise1 = dynamodb.putItem(...).promise();
var promise2 = <annet asynkront API-kall her>.promise();
// o.s.v.
Promise.all([promise1, promise2, ...])

Også henger man på then og catch her i stedet for å ha det på hver promise over. Men dette er selvsagt bare mulig så lenge kallene kan kjøres uavhengig av hverandre.

Hendelser

Man har også anledning til å bruke hendelser:

const AWS = require('aws-sdk');

exports.handler = () => {

   var dynamodb = new AWS.DynamoDB(...);
   var kall = dynamodb.putItem(...);
   kall.on('success', function(respons) {
      // Kode for suksess her
   });
   kall.on('error', function(error) {
      // Kode for feil her
   });
   kall.send();

   // Synkron kode
   // fortsetter her
};

Tilbakekall

Alternativt har man bruk av tilbakekall (altså «callback» på engelsk), men hvor man dessverre havner i "callback helvete" hvis man skal gjøre mange kall:

const AWS = require('aws-sdk');

exports.handler = () => {

   var dynamodb = new AWS.DynamoDB(...);
   dynamodb.putItem(..., function(error, data) {
      if (error) {
         // Kode for feil her
      } else {
         // Kode for suksess her
      }
   });
};

Problemet med alle eksemplene over er at det blir unødig venting. Og siden man betaler for hver påbynte bolk med 100 millisekunder er det viktig å begrense ventingen så mye som mulig.

Eksemplet nedenfor viser den raskeste måten jeg har funnet til nå:

const AWS = require('aws-sdk');

exports.handler = (hendelse, kontekst, tilbakekall) => {
   kontekst.callbackWaitsForEmptyEventLoop = false;

   var dynamodb = new AWS.DynamoDB(...);
   dynamodb.putItem(..., function(error) {
      if (error) {
         // Kode for feil her
      } else {
         // Kode for suksess her
      }

      tilbakekall(null, ...);
   });
};

Gjentatte tester viste at tidsforbruket minst ble halvert.

Å eksperimentere med AWS Lambda er noe man aldri blir ferdig med ..

TBC