C-Programmierung mit AVR-GCC/ Register

Aus Wikibooks

Mikrocontroller haben außer der Recheneinheit (ALU) und dem Speicher noch weitere Funktionsblöcke, wie Timer/Counter, Schnittstellen, AD-Wandler und Ein-/Ausgabeports. Diese werden alle durch sog. Register konfiguriert und angesprochen. Beim AVR handelt es sich um einen 8-bit Controller, deshalb haben die Register eine Breite von 8 Bit. Natürlich müssen manchmal auch größere Werte gespeichert werden (16-bit Timer, AD-Wandler), dazu aber später mehr.

Die AVR-Controller verfügen über eine Vielzahl von Registern. Die meisten davon sind sogenannte Schreib-/Leseregister. Das heißt, das Programm kann die Inhalte der Register sowohl auslesen als auch beschreiben.

Einzelne Register sind bei allen AVRs vorhanden, andere wiederum nur bei bestimmten Typen. So sind beispielsweise die Register, welche für den Zugriff auf den UART notwendig sind, selbstverständlich nur bei denjenigen Modellen vorhanden, welche über einen integrierten Hardware UART bzw. USART verfügen.

Die Namen der Register sind in den Headerdateien zu den entsprechenden AVR-Typen definiert. Dazu musst du den Namen der controllerspezifischen Headerdatei nicht kennen. Es reicht aus die allgemeine Headerdatei avr/io.h einzubinden.

#include <avr/io.h>

Schreiben in Register[Bearbeiten]

Zum Schreiben kannst du Register einfach wie eine Variable setzen.

DDRA = 0xff;
PORTA = 0x03;

Zu dieser oben gezeigten Schreibweise existiert auch noch eine ausführliche Schreibweise. Die ausführliche Schreibweise solltest du bevorzugen, da dadurch die Zuweisungen selbsterklärend sind und somit der Code leichter nachvollzogen werden kann. Atmel verwendet sie auch bei Beispielen in Datenblättern und in den allermeisten Quellcodes zu Application-Notes.

PORTB |= (1<<PB1);
PORTB &= ~(1<<PB0);

Wir erkennen sofort den gewaltigen Vorteil dieser Schreibweise. Die einzelnen Bits können per Namen angesprochen werden.

Der gcc C-Compiler (genauer der Präprozessor) unterstützt ab Version 4.3.0 Konstanten im Binärformat. Du kannst also die Register auch in binärer Form beschreiben.

PORTB = 0b01000000;

Diese Schreibweise ist jedoch nicht standardkonform und du solltest sie daher insbesondere dann nicht verwenden, wenn du Code mit anderen austauschst oder mit anderen Compilern bzw. älteren Versionen des gcc nutzen willst.

Verändern von Registerinhalten[Bearbeiten]

Einzelne Bits setzt und löschst du "Standard-C-konform" mittels logischer (Bit-) Operationen.

 x |= (1 << Bitnummer);  // Hiermit wird ein Bit in x gesetzt
 x &= ~(1 << Bitnummer); // Hiermit wird ein Bit in x geloescht

Es wird jeweils nur der Zustand des angegebenen Bits geändert, der vorherige Zustand der anderen Bits bleibt erhalten.

In diesem Zuge solltest du dir die Bitoperationen noch mal ins Gedächtnis rufen. Bei Bitoperationen hilft es immer mal wieder sich zu überlegen, welches Ergebnis aus dieser Operation entsteht.

PORTB |= (1 << PB1)

PORTB:       0110 0000
(1 << PB1):  0000 0010
======================
             0110 0010

Beim obigen Beispiel setzen wir zusätzlich zu den schon gesetzten Bits das Bit PB1. Das erreichen wir mit einer logischen ODER Operation auf den Port.

PORTB &= ~(1 << PB1)

PORTB:       0110 0010
(1 << PB1):  0000 0010
~(1 << PB1): 1111 1101
======================
             0110 0000

In diesem Beispiel setzen wir das Bit PB1 explizit auf Null, ohne die anderen Bits zu berühren. Das erreichen wir mit einer logischen UND Operation und einer zusätzlichen Negation auf den Port.

Wir halten also fest: Ein Bit setzen funktioniert mit einer ODER-Verknüpfung. Ein Bit löschen kannst du mit einer UND-Verknüpfung und der Negation.

Beispiel:

#include <avr/io.h>
...
PORTA |= (1 << PA2);    /* setzt Bit 2 an PortA auf 1 */
PORTA &= ~(1 << PA2);   /* loescht Bit 2 an PortA */

Mit dieser Methode lassen sich auch mehrere Bits eines Registers gleichzeitig setzen und löschen.

Beispiel:

#include <avr/io.h>
...
DDRA &= ~((1<<PA0) | (1<<PA3));
PORTA |= (1<<PA0) | (1<<PA3);

Du solltest stets nur die Bits setzen, die du auch manipulieren willst um Seiteneffekte zu vermeiden. Stell dir vor, du änderst den kompletten Port für deine Bedürfnisse ab. Ein anderer Programmteil hingegen nutzt auch andere Bits dieses Ports. Das kann zu Fehlfunktionen führen, die nicht so schnell zu finden sind. Du kannst dir dadurch einiges an Ärger ersparen.

Siehe auch:

Lesen aus Registern[Bearbeiten]

Zum Lesen kannst du auf Register einfach wie auf eine Variable zugreifen.

#include <avr/io.h>
#include <stdint.h>

uint8_t foo;

int main(void)
{
    foo = DDRB;
}

Die Zustände von einzelnen Bits erhältst du durch Einlesen des gesamten Registerinhalts und ausblenden der Bits deren Zustand nicht von Interesse ist. Einige Beispiele zum Prüfen ob Bits gesetzt oder gelöscht sind:

#include <stdint.h>
#include <avr/io.h>

uint8_t i;

int main() {

  if(PINB & (1 << PB1)) {
    //PB1 gesetzt
  } else {
    //PB1 nicht gesetzt
  }

}

Analog dazu lässt sich ein Register auch auf mehrere Bits gleichzeitig prüfen. Dabei musst du beachten, dass das Programm in die positive if-Verzweigung eintritt, wenn eines der beiden Bits gesetzt ist.

#include <stdint.h>
#include <avr/io.h>

uint8_t i;

int main() {

  if(PINB & (1 << PB1 | 1 << PB2)) {
    //PB1 | PB2 gesetzt
  } else {
    //PB1 & PB2 nicht gesetzt
  }

}

Die AVR-Bibliothek (avr-libc) stellt auch Funktionen (Makros) zur Abfrage eines einzelnen Bits eines Registers zur Verfügung, diese sind bei anderen Compilern meist nicht verfügbar (können aber dann einfach durch Macros "nachgerüstet" werden).

bit_is_set (<Register>,<Bitnummer>)
Die Funktion bit_is_set prüft, ob ein Bit gesetzt ist. Wenn das Bit gesetzt ist, wird ein Wert ungleich 0 zurückgegeben. Genau genommen ist es die Wertigkeit des abgefragten Bits, also 1 für Bit0, 2 für Bit1, 4 für Bit2 etc.
bit_is_clear (<Register>,<Bitnummer>)
Die Funktion bit_is_clear prüft, ob ein Bit gelöscht ist. Wenn das Bit gelöscht ist, also auf 0 ist, wird ein Wert ungleich 0 zurückgegeben.

Die Funktionen (eigentlich Makros) bit_is_clear bzw. bit_is_set sind nicht erforderlich, man kann und sollte C-Syntax verwenden, die universell verwendbar und portabel ist. Siehe auch Bitmanipulation.

Warten auf einen bestimmten Zustand[Bearbeiten]

Die avr-libc enthält Funktionen zum Warten auf einen bestimmten Bitzustand. Die Verwendung dieser Funktionen entspricht aber nicht dem C-Standard. Deshalb solltest du die Funktionen nicht verwenden. Das folgende Beispiel zeigt, wie du auf einen bestimmten Zustand wartest.

#include <avr/io.h>

int main() {
  while (!(PINB & (1 << PB1))) ;
}

Natürlich kannst du auch auf ein nicht gesetztes Bit warten.

#include <avr/io.h>

int main() {
  while (PINB & (1<<PB0)) ;
}