VanHeden Old VanHeden

Datablad

Robot-plattformar Digitala/analoga komponenter AVR/Raspberry Kommunikation Kringutrustning Displayer Sensorer Knappar & LEDs Gott & Blandat Alla PDF:er

Övrig information

Handledningar Karta över ISY Tips o Trix Kuriosa FAQ

C-trix för AVR

Avbrotthantering och I/O i C

Det finns (åtminstone) två saker som är mycket viktiga att känna till när man vill hantera avbrott och I/O i C. Det första är att känna till hur man använder nyckelordet volatile i C. Det andra är att känna till vilka operationer som är atomiska och vilka som inte är det.

Om volatile

Säg att vi har följande kod i en subrutin:

char interrupt_flag;
void wait_for_it(void)

   while(!interrupt_flag){
     ctr++;
   }
   // Do something...
}

Tanken är alltså att vi väntar i wait_for_it() tills vår avbrottshanterare ändrar på interrupt_flag. När vi kompilerar denna kod spottar dock kompilatorn ut sig något i stil med detta:

wait_for_it:
        lds r24,interrupt_flag
        tst r24
        brne .L4
.L5:
        rjmp .L5
.L4:
       // Och så vidare...

While-loopen har här blivit en evig loop utan villkor (rjmp .L5). Kompilatorn har alltså varit såpass smart att den inser att vi aldrig kommer att ändra på interrupt_flag inuti while-loopen. Alltså kan vi aldrig komma ur while-loopen om vi kommit in i den. Notera också att ctr faktiskt aldrig räknas upp inne i while-loopen. Detta eftersom kompilatorn inser att vi aldrig kommer att komma ur while-loopen, alltså spelar det ingen roll huruvida vi räknar upp counter eller inte.

För att råda bot på detta måste vi använda nyckelordet volatile. Enkelt förklarat så säger volatile till kompilatorn att en viss variabel kan ändra sig när som helst utan att något direktiv i C-programmet faktiskt ändrar på variabeln.

För att programmet ovan ska fungera måste vi alltså deklarera interrupt_flag på följande sätt:

volatile char interrupt_flag;

Då kommer wait_for_it att kompileras till denna assemblerkod istället:

wait_for_it:
        lds r24,interrupt_flag
        tst r24
        brne .L4
        lds r18,ctr
        lds r19,(ctr)+1
.L3:
        subi r18,lo8(-(1))
        sbci r19,hi8(-(1))
        lds r24,interrupt_flag
        tst r24
        breq .L3
        sts (ctr)+1,r19
        sts ctr,r18
.L4:

Notera att det också är viktigt att använda nyckelordet volatile i de fall man vill kommunicera med minnesmappad I/O. Detta är dock redan löst i avr-gcc då de makron som definierar exempelvis PINB, PORTA och DDRC redan använder volatile.

Mer om volatile hos AVR Freaks

Om atomiska operationer

Även om du använder volatile är det inte säkert att du har löst alla problem som kan uppstå med din avbrottshanterare. Ett annat problem är att inte alla operationer är atomiska. Vi kan se detta om vi undersöker hur följande kod kompileras av AVR-GCC:

volatile int test_var;
char test(void)
{
  if(test_var < 597){
    return 0;
  }
  return 1;
}

Detta kompileras till följande kod:

// Returvärdet ligger i r18
test:
        ldi r18,0
        lds r24,test_var
        lds r25,(test_var)+1
        subi r24,lo8(597)
        sbci r25,hi8(597)
        brlt .L8
        ldi r18,1
.L8:
        mov r24,r18
        ret

Notera att det krävs två load-instruktioner för att läsa in test_var. Du kan fundera lite på vad som kan hända om den avbrottsrutin som ändrar på test_var aktiveras efter att den första load-instruktionen körs men innan den andra load-instruktionen körs. I detta läge kan det hända att vi läser in ett helt vansinnigt värde ifrån test_var. (Vi kan få samma problem om vi ifrån ett huvudprogram modifierar en variabel som en avbrottshanterare läser.)

Problemet är alltså att trots att test_var är volatile så innebär detta inte att alla operationer där test_var är inblandade automatiskt blir atomiska. Ett sätt att lösa detta är att se till att alla variabler som sätts ifrån avbrottsrutiner är max 8 bitar breda (eftersom en sådan variabel kan läsas och skrivas atomiskt med hjälp av en enda load eller store-instruktion). (Men det kan fortfarande tänkas att vi får problem om det är så att avbrottsrutinen använder flera olika 8 bitar breda variabler för att kommunicera med huvudprogrammet.)

Ett sätt att lösa detta problem är att helt enkelt inte tillåta avbrott att slå igenom i samband med att vi uppdaterar eller läser av viktiga variabler ifrån huvudprogrammet. Detta kan göras genom att lägga kritiska programstycken mellan cli() och sei(). Försök dock att hålla dessa kritiska programstycken korta för att minska risken för missade avbrott.

Om att slå av omptimering

Vissa av de problem som beskrivs i detta dokument kommer att försvinna om du slår av optimering i din C-kompilator. (-O0 i avr-gcc.) (Utan optimering kommer i princip alla variabler att hanteras som om de vore satta till volatile.) Men även utan optimering kommer problemen runt atomiska operationer (eller rättare sagt avsaknade av atomiska operationer) att kvarstå. Du kan alltså inte bara räkna med att alla avbrottsproblem kommer att lösa sig om du slår av optimeringen.

Vidare läsning

Notera att detta dokument enbart behandlar det som är viktigast i samband med kurserna Konstruktion med Mikrodator och Elektronikprojekt Y. Om du behöver använda avbrott och I/O på andra system, speciellt om du också använder DMA, räcker inte volatile till för att lösa alla problem. Du kan också behöva tömma eller invalidera cache:ar och försäkra dig om att I/O-skrivningar utförs i rätt ordning. Exakt hur detta går till varierar mellan olika processorer, men den intresserade kan läsa hur detta hanteras i Linuxdrivrutiner i kapitel 15 i Linux Device Drivers, 3rd Edition) http://lwn.net/Kernel/LDD3/

Mera information om optimering i GCC och volatile finns hos AVR Freaks

Tabeller i programminnet

Följande kod exemplifierar hur man kan lägga upp och läsa ifrån tabeller i programminnet, vilket kan vara nyttigt då RAM-minnet i en AVR är begränsat.

#include<avr/pgmspace.h>
uint8_t index;
uint8_t data;
 
const uint8_t table1[] PROGMEM = {7, 35, 11, 16, 4, 23,
                                  9, 22, 61, 31, 7, 11};
 
data = pgm_read_byte(&table1[index]);

W3.CSS

Friday