Datenaustausch mit Interrupt-Routinen
Variablen die sowohl in Interrupt-Routinen (ISR = Interrupt Service Routine(s)), als auch vom übrigen Programmcode geschrieben oder gelesen werden, müssen mit einem volatile deklariert werden. Damit wird dem Compiler mitgeteilt, dass der Inhalt der Variablen vor jedem Lesezugriff aus dem Speicher gelesen und nach jedem Schreibzugriff in den Speicher geschrieben wird. Ansonsten könnte der Compiler den Code so optimieren, dass der Wert der Variablen nur in Prozessorregistern zwischengespeichert wird, die nichts von der Änderung woanders mitbekommen.
Zur Veranschaulichung ein Codefragment für eine Tastenentprellung mit Erkennung einer "lange gedrückten" Taste.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>
//...
// Schwellwerte
// Entprellung:
#define CNTDEBOUNCE 10
// "lange gedrueckt:"
#define CNTREPEAT 200
// hier z. B. Taste an Pin2 PortA "active low" = 0 wenn gedrueckt
#define KEY_PIN PINA
#define KEY_PINNO PA2
// beachte: volatile!
volatile uint8_t gKeyCounter;
// Timer-Compare Interrupt ISR, wird z.B. alle 10ms ausgefuehrt
ISR(TIMER1_COMPA_vect)
{
// hier wird gKeyCounter veraendert. Die übrigen
// Programmteile müssen diese Aenderung "sehen":
// volatile -> aktuellen Wert immer in den Speicher schreiben
if ( !(KEY_PIN & (1<<KEY_PINNO)) ) {
if (gKeyCounter < CNTREPEAT) gKeyCounter++;
}
else {
gKeyCounter = 0;
}
}
//...
int main(void)
{
//...
/* hier: Initialisierung der Ports und des Timer-Interrupts */
//...
// hier wird auf gKeyCounter zugegriffen. Dazu muss der in der
// ISR geschriebene Wert bekannt sein:
// volatile -> aktuellen Wert immer aus dem Speicher lesen
if ( gKeyCounter > CNTDEBOUNCE ) { // Taste mind. 10*10 ms "prellfrei"
if (gKeyCounter == CNTREPEAT) {
/* hier: Code fuer "Taste lange gedrueckt" */
}
else {
/* hier: Code fuer "Taste kurz gedrueckt" */
}
}
//...
}
|
Wird innerhalb einer ISR mehrfach auf eine mit volatile deklarierte Variable zugegriffen, wirkt sich dies ungünstig auf die Verarbeitungsgeschwindigkeit aus, da bei jedem Zugriff mit dem Speicherinhalt abgeglichen wird. Da bei AVR-Controllern innerhalb einer ISR keine Unterbrechungen zu erwarten sind, bietet es sich an, einen Zwischenspeicher in Form einer lokalen Variable zu verwenden, deren Inhalt zu Beginn und am Ende mit dem der volatile Variable synchronisiert wird. Lokale Variable werden bei eingeschalteter Optimierung mit hoher Wahrscheinlichkeit in Prozessorregistern verwaltet und der Zugriff darauf ist daher nur mit wenigen internen Operationen verbunden. Die ISR aus dem vorherigen Beispiel lässt sich so optimieren:
//...
ISR(TIMER1_COMPA_vect)
{
uint8_t tmp_kc;
tmp_kc = gKeyCounter; // Uebernahme in lokale Arbeitsvariable
if ( !(KEY_PIN & (1<<KEY_PINNO)) ) {
if (tmp_kc < CNTREPEAT) {
tmp_kc++;
}
}
else {
tmp_kc = 0;
}
gKeyCounter = tmp_kc; // Zurueckschreiben
}
//...
|
Zum Vergleich die Disassemblies (Ausschnitte der "lss-Dateien", compiliert für ATmega162) im Anschluss. Man erkennt den viermaligen Zugriff auf die Speicheraddresse von gKeyCounter (hier 0x032A) in der ISR ohne "Cache"-Variable und den zweimaligen Zugriff in der Variante mit Zwischenspeicher. Im Beispiel ist der Vorteil gering, bei komplexeren Routinen kann die Zwischenspeicherung in lokalen Variablen jedoch zu deutlicheren Verbesserungen führen.
ISR(TIMER1_COMPA_vect)
{
86a: 1f 92 push r1
86c: 0f 92 push r0
86e: 0f b6 in r0, 0x3f ; 63
870: 0f 92 push r0
872: 11 24 eor r1, r1
874: 8f 93 push r24
if ( !(KEY_PIN & (1<<KEY_PINNO)) ) {
876: ca 99 sbic 0x19, 2 ; 25
878: 0a c0 rjmp .+20 ; 0x88e <__vector_13+0x24>
if (gKeyCounter < CNTREPEAT) gKeyCounter++;
87a: 80 91 2a 03 lds r24, 0x032A
87e: 88 3c cpi r24, 0xC8 ; 200
880: 40 f4 brcc .+16 ; 0x892 <__vector_13+0x28>
882: 80 91 2a 03 lds r24, 0x032A
886: 8f 5f subi r24, 0xFF ; 255
888: 80 93 2a 03 sts 0x032A, r24
88c: 02 c0 rjmp .+4 ; 0x892 <__vector_13+0x28>
}
else {
gKeyCounter = 0;
88e: 10 92 2a 03 sts 0x032A, r1
892: 8f 91 pop r24
894: 0f 90 pop r0
896: 0f be out 0x3f, r0 ; 63
898: 0f 90 pop r0
89a: 1f 90 pop r1
89c: 18 95 reti
ISR(TIMER1_COMPA_vect)
{
86a: 1f 92 push r1
86c: 0f 92 push r0
86e: 0f b6 in r0, 0x3f ; 63
870: 0f 92 push r0
872: 11 24 eor r1, r1
874: 8f 93 push r24
uint8_t tmp_kc;
tmp_kc = gKeyCounter;
876: 80 91 2a 03 lds r24, 0x032A
if ( !(KEY_PIN & (1<<KEY_PINNO)) ) {
87a: ca 9b sbis 0x19, 2 ; 25
87c: 02 c0 rjmp .+4 ; 0x882 <__vector_13+0x18>
87e: 80 e0 ldi r24, 0x00 ; 0
880: 03 c0 rjmp .+6 ; 0x888 <__vector_13+0x1e>
if (tmp_kc < CNTREPEAT) {
882: 88 3c cpi r24, 0xC8 ; 200
884: 08 f4 brcc .+2 ; 0x888 <__vector_13+0x1e>
tmp_kc++;
886: 8f 5f subi r24, 0xFF ; 255
}
}
else {
tmp_kc = 0;
}
gKeyCounter = tmp_kc;
888: 80 93 2a 03 sts 0x032A, r24
88c: 8f 91 pop r24
88e: 0f 90 pop r0
890: 0f be out 0x3f, r0 ; 63
892: 0f 90 pop r0
894: 1f 90 pop r1
896: 18 95 reti
[Bearbeiten] volatile und Pointer
Bei volatile in Verbindung mit Pointern ist zu beachten, ob der Pointer selbst oder die Variable auf die der Pointer zeigt volatile ist.
volatile uint8_t *a; // das Ziel von a ist volatile
uint8_t *volatile a; // a selbst ist volatile
|
Falls der Pointer volatile ist (zweiter Fall im Beispiel), ist zu beachten, dass der Wert des Pointers, also eine Speicheradresse, intern in mehr als einem Byte verwaltet wird. Lese- und Schreibzugriffe im Hauptprogramm (ausserhalb von Interrupt-Routinen) sind daher so zu implementieren, dass alle Teilbytes der Adresse konsistent bleiben, vgl. dazu den folgenden Abschnitt.
[Bearbeiten] Variablen größer 1 Byte
Bei Variablen größer ein Byte, auf die in Interrupt-Routinen und im Hauptprogramm zugegriffen wird, muss darauf geachtet werden, dass die Zugriffe auf die einzelnen Bytes außerhalb der ISR nicht durch einen Interrupt unterbrochen werden. (Allgemeinplatz: AVRs sind 8-bit Controller). Zur Veranschaulichung ein Codefragment:
//...
volatile uint16_t gMyCounter16bit;
//...
ISR(...)
{
//...
gMyCounter16Bit++;
//...
}
int main(void)
{
uint16_t tmpCnt;
//...
// nicht gut: Mglw. hier ein Fehler, wenn ein Byte von MyCounter
// schon in tmpCnt kopiert ist aber vor dem Kopieren des zweiten Bytes
// ein Interrupt auftritt, der den Inhalt von MyCounter verändert.
tmpCnt = gMyCounter16bit;
// besser: Änderungen "außerhalb" verhindern -> alle "Teilbytes"
// bleiben konsistent
cli(); // Interrupts deaktivieren
tmpCnt = gMyCounter16Bit;
sei(); // wieder aktivieren
// oder: vorheriger Status des globalen Interrupt-Flags bleibt erhalten
uint8_t sreg_tmp;
sreg_tmp = SREG; /* Sichern */
cli()
tmpCnt = gMyCounter16Bit;
SREG = sreg_tmp; /* Wiederherstellen */
// oder: mehrfach lesen, bis man konsistente Daten hat
uint16_t count1 = gMyCounter16Bit;
uint16_t count2 = gMyCounter16Bit;
while (count1 != count2) {
count1 = count2;
count2 = gMyCounter16Bit;
}
tmpCnt = count1;
//...
}
|
Die avr-libc bietet ab Version 1.6.0(?) einige Hilfsfunktionen/Makros, mit der im Beispiel oben gezeigten Funktionalität, die zusätzlich auch so genannte memory barriers beinhalten. Diese stehen nach #include <util/atomic.h> zur Verfügung.
//...
#include <util/atomic.h>
//...
// analog zu cli, Zugriff, sei:
ATOMIC_BLOCK(ATOMIC_FORCEON) {
tmpCnt = gMyCounter16Bit;
}
// oder:
// analog zu Sicherung des SREG, cli, Zugriff und Zurückschreiben des SREG:
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
tmpCnt = gMyCounter16Bit;
}
//...
|