FAQ

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Ein Verzeichnis von im Forum oft gestellten und immer wieder beantworteten Fragen und den zugehörigen Antworten:

Wie kann ich Zahlen auf LCD/UART ausgeben?

Aber die Bibliothek, die du benutzt, stellt nur eine Funktion zur Verfügung, mit der man einen String ausgeben kann... Was tun?

In den folgenden Beispielen wird eine selbstgeschriebene Funktion zur Stringausgabe auf LCD - die Funktion lcd_string() - aus dem LCD-Teil des AVR-GCC-Tutorials verwendet:

lcd_string( "Hallo Welt" );  // ggf. auch lcd_out() o.ä. in anderen Libraries

Um also eine Zahl (numerische Konstante oder Variableninhalt) auszugeben, muss von dieser Zahl zunächst ihre String-Repräsentation ermittelt werden. Hier geht es aber nur darum, zu zeigen wie man diese String Repräsenation erzeugen kann. Was man dann mit diesem String weiter macht, ob das dann eine LCD-Ausgabe oder eine UART-Übertragung oder das Abspeichern auf SD-Karte oder ... ist, spielt eine untergeordnete Rolle.

Es gibt mehrere Möglichkeiten, sich die Stringrepräsentation zu erzeugen:

itoa() (utoa(), ltoa(), ultoa(), ftoa() )

itoa() ist keine C-Standardfunktion (wohl aber ihre Umkehrung atoi() ). Auf manchen Compilern heisst diese Funktion dann folgerichtig _itoa(), wobei der führende _ eben anzeigt, dass es sich um eine Erweiterung des C-Standards handelt. Bei WinAVR ist itoa() Bestandteil der mitgelieferten Library avr-libc, in der Library stdlib.h.

  #include <stdlib.h>
  char Buffer[20];
  int i = 25;

  itoa( i, Buffer, 10 );
  lcd_string( Buffer ); // ggf. auch lcd_out() o.ä. in anderen Libraries

itoa( i, Buffer, 10 ); - Die Zahl i wird nach ASCII gewandelt und die String Repräsentierung davon wird in Buffer abgelegt. Die Basis, in der diese Wandlung erfolgt, ist das 10-er System. Wird das dritte Argument von 10 in zb. 2 oder auch 16 abgewandelt, erhält man die binäre oder eben eine hexadezimale Repräsentierung des Wertes. Auch wenn 10, 2 und 16 die häufigsten Angaben an dieser Stelle sind, kann itoa aber grundsätzlich in jedes beliebige Zahlensystem wandlen.

Wichtig ist, darauf zu achten, dass das Array Buffer groß genug dimensioniert wird, um alle Zeichen der Textrepräsentation der Zahl aufzunehmen - inklusive der 0, die den String abschließt, sowie ein mögliches Vorzeichen.

Anzumerken bleibt weiter, dass es normalerweise für alle Datentypen entsprechende Umwandlungsfunktionen gibt, wenn es sie für einen Datentyp gibt. Die Namensgebung lehnt sich an das Schema an: Kürzel_für_den_Datentyp to a. Eine Funktion die einen unsigned int wandelt, heißt dann utoa (oder _utoa), Floating Point heißt dann ftoa (oder _ftoa), etc.

sprintf()

  char Buffer[20];
  int i = 25;

  sprintf( Buffer, "%d", i );
  lcd_string( Buffer ); // ggf. auch lcd_out() o.ä. in anderen Libraries

Diese Methode funktioniert auch bei long oder float Werten. Unbedingt beachtet werden muss allerdings, dass die Typkennzeichnungen im sog. Format-String (hier "%d") mit den tatsächlichen Typen der auszugebenden Werten übereinstimmt. Und dass der Buffer, der den Text aufnimmt, auch groß genug dimensioniert wird. Dabei sollte die 0, die den String terminiert, nicht vergessen werden.

Mit sprintf() hat man dieselben Möglichkeiten zur Formatierung wie bei printf() (siehe unten). Insbesondere gibt es natürlich die Möglichkeit die Zahl gleich in einen umgebenden Text einzubetten bzw. Formatierungen anzugeben:

  char Buffer[20];
  int i = 25;

  sprintf( Buffer, "Anzahl: %d Stueck", i );
  lcd_out( Buffer );

Der "Haken" an der mächtigen Funktion sprintf() ist, daß sie auch bei minimalisierter Konfiguration verhältnismäßig viel Programmspeicher (Flash-ROM) belegt und relativ viel Prozesszeit benötigt. Daher sollte man sprintf() nur verwenden, wenn kein Speicher- und Prozesszeitmangel besteht. Sonst sollte itoa() oder eine eigene, auf die Bedürfnisse optimierte Implementierung auf jeden Fall vorgezogen werden. Der große Vorteil von sprintf liegt darin, dass man über sehr mächtige Formatiermöglichkeiten verfügt, mit der man die Ausgabe einfach steuern und an seine Bedürfnisse anpassen kann. Dinge die man mit einer Funktion wie itoa alle selbst 'händisch' erledigen muss.

Formatierungen mit printf

Für jedes auszugebende Argument muss es im Formatstring einen entsprechenden Formatbezeichner geben. Der Aufbau eines Formatbezeichners ist immer

 %[Modifizierer][Feldbreite][.Präzision]Typ

(Die in eckigen Klammern [ ] angegebenen Elemente können auch weggelassen werden, wenn man sie nicht benötigt. Im einfachsten Fall benötigt man also nur das % und den Buchstaben zur Typ-Kennung.)

Typ ist dabei eine Kennung, der mit dem Datentyp des jeweiligen auszugebenden Argumentes übereinstimmen muss. Einige oft benutzte Kennungen, ohne Anspruch auf Vollständigkeit, sind:

 c    char
 d    int
 f    float, double
 ld   long
 u    unsigned int
 lu   unsigned long
 p    pointer
 s    string
 x    ein int wird ausgegeben, die Ausgabe erfolgt
      aber als Hexadezimalzahl
 X    ein int wird ausgegeben, die Ausgabe erfolgt
      aber als Hexadezimalzahl, wobei Grossbuchstaben verwendet werden

DerModifizierer bestimmt, wie und womit nicht benutzte Felder des Ausgabefeldes gefüllt werden sollen, wie die Ausrichtung innerhalb des Feldes erfolgen soll und ob ein Vorzeichen auch dann ausgegeben werden soll wenn die auszugebende Zahl positiv ist. Wird kein Modifizierer angegeben, so werden nicht benutzte Felder mit einem Leerzeichen gefüllt, positive Vorzeichen unterdrückt und die Ausgabe im Feld rechts ausgerichtet.

 +    Vorzeichen wird immer ausgegeben
 -    Die Ausgabe wird im Ausgabefeld linksbündig ausgerichtet
 0    anstelle von Leerzeichen werden führende 0-en ausgegeben

Die Feldbreite gibt die Breite des Ausgabefeldes an, in die die Ausgabe durchgeführt werden soll. Reicht die angegebene Feldbreite nicht aus, so vergrößert printf diese Breite eigenmächtig. Die Feldbreite muß nicht angegeben werden. In diesem Fall bestimmt printf selbst die Feldbreite, so dass die Ausgabe darin Platz findet. Mit der Feldbreite hat man eine simple Möglichkeit dafür zu sorgen, dass der erzeugte String immer eine konstante Länge hat, selbst wenn die auszugebende Zahl diese Länge gar nicht benötigen würde (wichtig zb. bei Tabellen, damit die Einerstellen der Tabelleneinträge auch sauber untereinander stehen, auch dann wenn die Zahlen sich in unterschiedlichen Wertebereichen bewegen).

Die Präzision kommt nur bei float oder double Zahlen zum Einsatz. Sie legt fest, wieviele Positionen der kompletten Feldbreite für die Ausgabe von Nachkommastellen reserviert werden sollen. Auch sie muss wiederrum nicht angegeben werden und printf benutzt in so einem Fall Standardvorgaben.

Beispiele
"%d"
Ausgabe eines Integer
"%5d"
Ausgabe eines Integer in einem Feld mit 5 Zeichen Breite
"%05d"
Ausgabe eines Integer in einem Feld mit 5 Zeichen Breite, wobei das Feld links mit führenden Nullen auf 5 Zeichen aufgefüllt wird
"%-5d"
Ausgabe eines Integer in einem Feld mit 5 Zeichen Breite. Die Zahl wird linksbündig in das Feld gestellt.
"%6.3f"
Ausgabe eines float (oder double). Die Ausgabe erfolgt in einem Feld mit 6 Zeichen Breite, wobei 3 Nachkommastellen ausgegeben werden. Achtung: In der Feldbreite ist auch ein eventuelles Vorzeichen sowie der Dezimalpunkt enthalten. Bei einer Feldbreite von 6 Zeichen und 3 Nachkommastellen, bleiben bei einer positiven Zahl daher nur 2 Positionen für den Vorkommaanteil, bei negativen sogar nur 1 Stelle (6 - 3 Nachkommastellen - 1 Dezimalpunkt - 1 Vorzeichen = 1)

Eigene Umwandlungsfunktionen

Möchte man itoa() nicht benutzen oder hat es gar auf seinem System nicht zur Verfügung, dann ist es auch nicht schwer, sich selbst eine Funktion dafür zu schreiben:

void ItoA( int z, char* Buffer )
{
  int i = 0;
  int j;
  char tmp;
  unsigned u;    // In u bearbeiten wir den Absolutbetrag von z.
  
    // ist die Zahl negativ?
    // gleich mal ein - hinterlassen und die Zahl positiv machen
    if( z < 0 ) {
      Buffer[0] = '-';
      Buffer++;
      // -INT_MIN ist idR. größer als INT_MAX und nicht mehr 
      // als int darstellbar! Man muss daher bei der Bildung 
      // des Absolutbetrages aufpassen.
      u = ( (unsigned)-(z+1) ) + 1; 
    }
    else { 
      u = (unsigned)z;
    }
    // die einzelnen Stellen der Zahl berechnen
    do {
      Buffer[i++] = '0' + u % 10;
      u /= 10;
    } while( u > 0 );

    // den String in sich spiegeln
    for( j = 0; j < i / 2; ++j ) {
      tmp = Buffer[j];
      Buffer[j] = Buffer[i-j-1];
      Buffer[i-j-1] = tmp;
    }
    Buffer[i] = '\0';
}

Das Grundprinzip ist einfach:
Die Ermittlung der einzelnen Stellen erfolgt in der zentralen Schleife

    do {
      Buffer[i++] = '0' + u % 10;
      u /= 10;
    } while( u > 0 );

durch fortgesetzte Division durch 10 und Restbildung.

   8392
   8392 % 10           -> 2
   8392 / 10  -> 839
    839 % 10           -> 9
    839 / 10  -> 83
     83 % 10           -> 3
     83 / 10  -> 8
      8 % 10           -> 8
      8 / 10  -> 0


Nur leider erhält man dadurch die einzelnen Ziffern der Zahl in umgekehrter Reihenfolge im String ('2' '9' '3' '8' anstelle von '8' '3' '9' '2'). Dies ist aber kein Problem, die nachfolgende Schleife

  for( j = 0; j < i / 2; ++j ) {
    tmp = Buffer[j];
    Buffer[j] = Buffer[i-j-1];
    Buffer[i-j-1] = tmp;
  }

spiegelt den String in sich, sodass danach der String eine korrekte Repräsentation der ursprünglichen Zahl darstellt. Der Funktionsteil vor der 'Zerlegeschleife' behandelt den Sonderfall daß die Zahl negativ ist. Negative Zahlen werden behandelt indem im Endergebnis ein '-' vermerkt wird und danach die Zahl positiv gemacht wird.


Siehe auch:

Datentypen in Operationen

Ein häufiges Problem betrifft die Auswertung von Ausdrücken. Konkret die Frage nach den beteiligten Datentypen. zb

double i;
int j, k;

i = j / k;

Die Frage lautet dann: Warum erhalte ich keine Kommastellen, ich weise doch das Ergebnis einem double zu?

Dazu ist zu sagen, dass C nicht so funktioniert. Die Tatsache dass i eine double Variable ist, ist für die Auswahl der Operation, welche die Division durchführt, völlig irrelevant. C orientiert sich ausschliesslich an den Datentypen der beteiligten Operanden, um zu entscheiden ob die Division als Integer- oder als Gleitkommadivision durchzuführen ist. Und da sowohl j als auch k ein Integer sind, wird die Division als Integerdivision durchgeführt unabhängig davon, was mit dem Ergebnis weiter passiert. Erst nach der Division wird das Ergebnis in einen double überführt, um es an i zuweisen zu können. Zu diesem Zeitpunkt gibt es aber keine Kommastellen mehr, eine Integerdivision erzeugt keine. Und damit tauchen klarerweise auch im Ergebnis keine auf.

Aus genau diesem Grund ist zb das Ergebnis von

double i;
i = 5 / 8;

eine glatte 0 und nicht 0.625. Die Division 5 / 8 wird als Integer Division gemacht und liefert als solche keine Nachkommastellen. Wird das Ergebnis der Division in einen double umgewandelt, um es an i zuweisen zu können, ist das Kind schon in den Brunnen gefallen: Die Nachkommastellen sind schon längst weg bzw. waren nie vorhanden.

Will man den Compiler dazu zwingen, die Division als Gleitkommadivision durchzuführen, so muss man daher dafür sorgen, dass mindestens einer der beteiligten Operanden ein double Wert ist. Dann bleibt dem Compiler nichts anderes übrig, als auch den zweiten Operanden ebenfalls zu einem double zu machen und die Operation als Gleitkommaoperation durchzuführen.

double i;
i = 5.0 / 8.0;

i = (double)j / k;

Generell implementiert der Compiler eine Operation immer im 'höchsten' Datentyp, der in dieser Operation vorkommenden Operanden. Operanden in einem 'niedrigeren' Datentyp werden automatisch immer in diesen 'höchsten' Datentyp umgewandelt, zumindest aber int. Die Reihung orientiert sich dabei an der Regel: Ein 'höherer' Datentyp kann alle Werte eines 'niedrigeren' Datentyps aufnehmen.

   int
   unsigned int
   long
   unsigned long
   long long
   unsigned long long
   double
   

float kommt in dieser Tabelle gar nicht vor, da Gleitkommaoperationen grundsätzlich immer als double-Operationen durchgeführt werden. Wohl kann es aber sein, dass double und float dieselbe Anzahl an Bits benutzen. Damit reduziert sich eine double Operation effektiv auf eine float Operation. (Anmerkung: mit dem C99-Standard hat sich dieses geändert. Dort gibt es dann auch echte float Operationen)

Hat man also in einer Operation 2 Operanden der Datentypen int und long, so wird der int implizit zu einem long gemacht und die Operation als long Operation durchgeführt. Das Ergebnis hat dann den Datentyp long.

Welche Datentypen haben Konstanten

Auch Zahlenkonstante besitzen einen Datentyp, der selbstverständlich vom Compiler bei der Auswahl der Operation berücksichtigt wird. Hier gilt die Regel: Benutzt wird der Datentyp, der die Zahl gerade noch aufnehmen kann. Eine Zahlenkonstante 5 hat daher den Datentyp int. Die Zahl 32767 passt gerade noch in einen int, und hat daher den Datentyp int. 32768 ist für einen int bereits zu groß und hat daher den Datentyp long. 5.0 ist hingegem immer eine double-Konstante. Der Dezimalpunkt erzwingt dieses.

Eine kleine Feinheit gibt es noch zu beachten. Konstanten in dezimaler Schreibweise haben immer einen signed Datentyp, während Konstanten in hexadezimaler bzw. oktaler Schreibweise je nach Wert einen signed oder einen unsigned Datentyp haben können.

Möchte man einer Konstanten einen bestimmten Datentyp aufzwingen, so gibt es dazu 2 (ein halb) Möglichkeiten:

  • Entweder man castet die Konstante in den gewünschten Datentyp
   (long)5
  • oder man benutzt die in C dafür vorgesehene Schreibweise, indem man der Konstanten einen Suffix anhängt, der den gewünschten Datentyp beschreibt
    5L

Beides ergibt eine dezimale 5, die vom Datentyp long ist.

  • eine Zahl in mit einem Dezimalkomma
    5.0

ist immer eine double Zahl. Es sei denn sie hat explizit einen Suffix

    5.0F

dann hat man es mit einer float Zahl zu tun.

Die gültigen Suffixe für Zahlen sind U, L, UL und F:

  • U wie unsigned; dabei wird zuerst int angenommen. Es erfolgt eine automatische Ausweitung auf long, wenn die Zahl den Wertebereich eines unsigned int überschreitet.
  • L wie long; die Zahl selbst kann int oder double sein.
  • UL wie unsigned long. Eigentlich eine Zusammensetzung aus U und L
  • F wie float.

Präprozessordefinitionen besitzen keine eigene Datentypen, da sie eine reine Textersetzungen durchführen. Deren Inhalte können aber ggf. zu Werten aufgelöst werden, die datentypbehaftet sind.

Aktivieren der Floating Point Version von sprintf bei WinAVR bzw AVR-Studio

Project → Configuration Options → Libraries → Available Link Objects
Custom Options → Custom Compilation Options → Linker Options

Beim WinAVR/AVR-Studio wird standardmässig eine Version der printf-Bibliothek verwendet, die keine Floatingpoint-Verarbeitung unterstützt. Die meisten Programme benötigen keine Floatingpoint-Unterstützung, so dass dadurch wertvoller Programmspeicherplatz gespart werden kann.

Benutzt man allerdings eine printf-Variante für die Ausgabe von Floatingpoint-Zahlen, so erscheint an Stelle der korrekt formatierten Zahl lediglich ein '?'. Dies ist ein Indiz dafür, dass die Floatingpoint-Verarbeitung im Projekt aktiviert werden muss.

Um die Floatingpoint-Verarbeitung zu aktivieren, geht man im AVR Studio 4 wie folgt vor:

  • Menüpunkt: Project → Configuration Options
  • Im sich öffnenden Dialog wird in der linken Navigationsleiste der Eintrag Libraries ausgewählt.
  • Unter Available Link Objects werden Bibliotheken angeboten. Für die Aktivierung der Floatingpoint-Unterstützung sind 2 interessant[1]:
    • libprintf_flt.a
    • libm.a
  • Beide Bibliotheken werden durch Aktivieren und einen Druck auf Add Library → in die rechte Spalte übernommen.
  • Danach wählt man in der Navigationsleiste den Eintrag Custom Options.
  • Unter Custom Compilation Options wird Linker Options ausgewählt und in das Textfeld rechts/unten folgender Text eingetragen:
        -Wl,-u,vfprintf
  • Ein Druck auf Add befördert die Zeile in das Listenfeld darüber, welches Optionen für den Linker enthält.
  • Mit OK wird die Konfiguration abgeschlossen.

Bei AVR Studio 5 trägt man die Optionen an anderen Stellen ein (Beitrag von Hal Smith):

  • Project Properties → Toolchain → AVR/GNU C-Linker → Libraries
    In Libraries (-WI, -I), libprintf_flt.a libm.a eintragen.
  • Project Properties → Toolchain → AVR/GNU C-Linker → Miscellaneous
    In Other Linker Flags -Wl,-u,vfprintf eintragen.

Wie funktioniert String-Verarbeitung in C?

→ Siehe: String-Verarbeitung in C

struct Strukturen

→ Siehe: Strukturen in C

Funktionszeiger

→ Siehe: Funktionszeiger in C

Header File - wie geht das

Ein Header File ist im Grunde nichts anderes als eine Sammlung aller Informationen, die ein 'Aussenstehender' benötigt, um die in einem C-File gesammlten Funktionen benutzen zu können.

Trotzdem gibt es immer wieder Schwierigkeiten, wie sich die Sache mit Header Files und/oder #include verhält und wie man eine derartige Lösung aufbauen kann.

Gegeben sei ein System bestehend aus 3 Funktionen

  • main()
  • functionA()
  • functionB()

und einigen globalen Variablen VarExtA bzw. VarExtB, die zu den jewiligen Funktionen A bzw. B gehören. Für dieses System soll eine Aufteilung erfolgen, so dass functionA samt seinen zugehörigen globalen Variablen in einer eigenen C-Datei residiert (FileA.c), functionB in einem eigenen File residiert (FileB.c) und main() als Hautpfunktion seine eigens C-File (Main.c) darstellt und jeweils die entsprechenden Header Files existieren.

Kochrezeptartig kann man in vielen Fällen einfach so vorgehen: Man schreibt erst mal den C-Code der Funktion, die man implementieren möchte. Zb fängt man an mit FileA.c

// FileA.c

int VarExtA;
int VarIntA;

#define A_DEFAULT  5

int functionIntA( void )
{
  VarExtA = A_DEFAULT;
  VarIntA = VarExtB;
  functionB();
};

Jetzt geht man das File, so wie es auch der Compiler macht, von oben nach unten durch schaut den Code durch. Damit functionB aufgerufen werden kann, ist ein Prototyp dafür notwendig. Der kommt aus einem Header File, welches noch nicht existiert, aber im Laufe des Prozesses entstehen und FileB.h heissen wird. FileB.h deshalb, weil die Funktion in FileB.c enthalten ist und man zur Vermeidung von Konfusion die Header Files immer gleich benennt wie die C-Files, nur eben mit einer anderen Dateiendung. FileB.h wird noch geschrieben werden, das hindert uns jetzt aber nicht daran, so zu tun als ob es dieses schon geben würde und ein entsprechender #include ergänzt. (Solange nicht compiliert wird ist das ja auch kein Problem. Irgendwo muss man ja schliesslich mal anfangen die Ergänzungen zu machen.)

// FileA.c

#include "FileB.h"

int VarExtA;
int VarIntA;

#define A_DEFAULT  5

int functionA( void )
{
  VarExtA = A_DEFAULT;
  VarIntA = VarExtB;
  functionB();
};

Gut. Die Variable VarExtB wird ebenfalls über den include hereingezogen werden. Damit ist FileA.c erst mal vollständig. Da fehlt jetzt erst mal nichts mehr. Alles was in FileA.c vorkommt sind entweder C-Schlüsselwörter, durch FileA.c selbst definiert oder kommt über den #include herein.

Der nächste Schritt ist die Überlegung: Was von dem Zeugs in FileA.C soll von anderer Stelle (von anderen C-Files aus) benutzt und verwendet werden können. Welche Dinge muss FileA von sich preis geben.

Da sind zu erst mal die beiden Variablen. Was soll mit denen geschehen? VarExtA soll von aussen zugreifbar sein, VarIntA nicht. Und dann natürlich die Funktion, die aufrufbar sein soll. Bei dem Makro A_DEFAULT kann es unterschiedliche Ansichten geben. Handelt es sich um einen Wert, den ein Aufrufer kennen soll, dann wird man das #define ins Header File verschieben (und aus dem C-File rausnehmen). Sieht man dieses #define aber als 'Privatsache' des C-Files, dann bleibt der #define dort wo er jetzt ist.

Also beginnt man ein Header File für FileA.c zu schreiben, in das all das reinkommt, was FileA.c nach aussen sichtbar machen will.

// FileA.h

extern int VarExtA;

int functionA( void );

Die Variable kriegt ein extern davor (und wird damit zu einer Deklaration), bei der Funktion wird einfach die Implementierung weggenommen und die somit zu einer Deklaration veränderte Zeile mit einem ; abgeschlossen. Wenn man möchte kann man den so geschaffenen Prototypen auch mit einem extern einleiten. Notwendig ist es aber nicht.

Erneuter Blick aufs Header File. Kommt da irgendwas vor, was nicht Standard C Schlüsselwort ist? Nein. Nichts. Also ist dieses Header File somit ebenfalls in sich vollständig.

Zur Sicherheit wird in FileA.c noch einen Include auf das eigene Header File hinzugefügt, denn dann kann der Compiler überprüfen ob die Angaben im Header File mit der tatsächlichen Implementierung übereinstimmen. Und da VarIntA von aussen nicht sichtbar sein soll (auch nicht durch Tricks), wird sie static gemacht.

// FileA.c

#include "FileA.h"
#include "FileB.h"

int VarExtA;
static int VarIntA;

#define A_DEFAULT  5

int functionA( void )
{
  VarExtA = A_DEFAULT;
  VarIntA = VarExtB;
  functionB();
}

Dasselbe für FileB.c. Erst mal einfach runterschreiben

// FileB.c

int VarExtB;
int VarIntB;

#define SETTING_B 0x01

int functionB( void )
{
  VarExtA = 1;
  functionA();
  VarIntB = SETTING_B;

  MeinePrivateFunktion();
}

void MeinePrivateFunktion()
{
  VarIntB = 8;
}

damit functionA aufgerufen werden kann, braucht es wieder einen Prototypen. Den kriegt man über von FileA.h, welches daher includiert wird.

// FileB.c

#include "FileA.h"

int VarExtB;
int VarIntB;

#define SETTING_B 0x01

int functionB( void )
{
  VarExtA = 1;
  functionA();
  VarIntB = SETTING_B;
  MeinePrivateFunktion();
}

void MeinePrivateFunktion()
{
  VarIntB = 8;
}

Was noch? VarExtA. Diese Variable wird aber ebenfalls durch den #include als extern Deklaration ins FileB.c hereingezogen und ist somit bei der Übersetzung von FileB.c bekannt. Kommt sonst noch etwas vor? MeinePrivateFunktion. Diese Funktion soll nur in FileB.c benutzt werden und ist für jemanden ausserhalb FileB.c völlig uninteressant. Nichts destotrotz gilt die Regel: compiliert wird von oben nach unten und verwendet werden kann nur etwas, was auch bekannt ist. D.h. bevor der Aufruf der Funktion in functionB gemacht werden kann, muss es einen Protoypen der Funktion geben. Da diese Funktion aber nicht nach 'aussen' exportiert wird, macht man den Protoypen gleich in das C-File mit hinein.

// FileB.c

#include "FileA.h"

int VarExtB;
int VarIntB;

#define SETTING_B 0x01

void MeinePrivateFunktion();

int functionB( void )
{
  VarExtA = 1;
  functionA();
  VarIntB = SETTING_B;
  MeinePrivateFunktion();
}

void MeinePrivateFunktion()
{
  VarIntB = 8;
}

Würde man die Funktion vorziehen, so dass der Funktionskörper vor der ersten Verwendung steht, dann würde man auch keinen Protoypen benötigen. Eine Funktionsdefinition fungiert als ihr eigener Protoyp.

Kommt sonst noch etwas vor? Nix mehr. In FileB.c wird sonst nix mehr verwendet was nicht entweder C Schlüsselwort oder im File selber oder durch einen #include reinkommt.

Dann wieder: das Header File für B schreiben. Dabei einfach nur überlegen: was soll von FileB.c nach aussen getragen werden?

// FileB.h

extern int VarExtB;

int functionB( void );

und zur Sicherheit wieder ins eigene C-File includen und alle Variablen (oder auch Funktionen), die von aussen nicht sichtbar sein sollen, als static markieren.

// FileB.c

#include "FileB.h"
#include "FileA.h"

int VarExtB;
static int VarIntB;

#define SETTING_B 0x01

static void MeinePrivateFunktion();

int functionB( void )
{
  VarExtA = 1;
  functionA();
  VarIntB = SETTING_B;
  MeinePrivateFunktion();
}

void MeinePrivateFunktion()
{
  VarIntB = 8;
}

damit bleibt nur noch main.c. Auch dieses wird erst mal einfach runtergeschrieben.

int c;

int main()
{
  functionA();
  functionB();
  VarExtA = 1;
  VarExtB = c;
}

Damit functionA aufgerufen werden kann, braucht es einen Prototypen. Woher kommt er? Aus FileA.h. Also gleich mal includen

#include "FileA.h"

int c;

int main()
{
  functionA();
  functionB();
  VarExtA = 1;
  VarExtB = c;
}

Damit functionB aufgerufen werden kann, braucht es einen Prototypen. Wo kommt der her? Aus FileB.h. Also noch ein #include

#include "FileA.h"
#include "FileB.h"

int c;

int main()
{
  functionA();
  functionB();
  VarExtA = 1;
  VarExtB = c;
}

Fehlt noch was? VarExtA ist durch den #include von FileA.h bereits abgedeckt und VarExtB ist durch den #include von FileB.h bereits abgedeckt. Also fehlt nix mehr. Auch in main.c sind damit alle Sachen abgedeckt und es ist in sich vollständig.

Das ist der Standardmechanismus:

  • Implementierung der Funktionen im C File schreiben
  • Überlegen, was von dieser Implementierung von aussen sichtbar sein soll.
  • Dasjenige kommt als Deklaration ins Header File, alles andere am besten static machen.
  • Für alles im C-File, das seinerseits woanders herkommt, gibt es einen #include, der das jeweils notwendige Header File einbindet.
  • Werden im Header File Dinge von woanders benutzt, dann enthält das Header File einen entsprechenden #include
  • Jedes File, sowohl Header-File als auch C-File ist in sich vollständig. Werden dort Dinge benutzt, dann müssen diese vor der Verwendung deklariert worden sein. Wie und wo diese Deklaration herkommt, ist dabei zweitrangig. Es kann sein, dass die Deklaration vor der Verwendung steht, es kann aber auch sein, dass die Deklaration über einen weiteren Include mit aufgenommen wird.

Ich hab da mehrere *.c und *.h Dateien. Was mache ich damit?

C-Programmierung: Workflow

Zunächst ist es wichtig, sich zu vergegenwärtigen, wie C-Compiler und Linker zusammenarbeiten. Ein komplettes Programmier-Projekt kann und wird im Normalfall aus mehreren Quelldateien bestehen, die alle zusammengenommen das komplette Programm bilden.

Der Prozess des Erstellens des Programmes geschieht in mehrerern Schritten:

Compilieren
zunächst werden alle Einzelteile (jede *.c Datei) für sich compiliert. Dabei ensteht aus jeder c-Datei eine sogenannte Object-Datei, in der bereits der Maschinencode für die im c-File programmierten Funktionen enthalten ist
Linken
die einzelnen Object-Dateien werden mit zusätzlichen Bibliotheken und dem Startup-Code zum fertigen Programm gelinkt.

Angenommen, das komplette Projekt besteht aus 2 Dateien:

main.c
int twice (int);

int main()
{
  twice (5);
}
func.c
int twice (int number)
{
  return 2 * number;
}

Dann werden main.c und func.c unabhängig voneinander compiliert. Als Ergebnis erhält man die Dateien main.o und func.o, die den besagten Object-Code enthalten. Diese beiden Zwischenergebnisse werden dann zusammen mit Bibliotheken zum fertigen Programm gebunden (gelinkt), das dann ausgeführt werden kann.

Bekommt man also von irgendwo bereits fertige *.c (und zugehörige *.h) Dateien, so genügt es, die *.c Dateien in das Projekt mit aufzunehmen. Dadurch wird das entsprechende C-File compiliert und das Ergebnis davon, das Object-file, wird dann in das fertige Programm mit eingelinkt.

Achtung
Da jede der C-Dateien unabhängig von allen anderen compiliert wird, bedeutet das auch, dass jede der C-Dateien in sich vollständig sein muss!

Wie eine C-Datei in das Projekt mit aufgenommen wird, hängt im wesentlichen von der benutzten Entwicklungsumgebung ab.

Makefile

Die zusätzliche *.c Datei wird in die SRC Zeile im makefile eingetragen.

AVR-Studio

Hier ist es besonders einfach, eine Datei in das Projekt mit aufzunehmen. Dazu wird im Projektbaum der Knoten "Source Files" aktiviert und mit der rechten Maustaste das Kontextmenü geöffnet. Im Menü wird der Punkt "Add existing Source File(s)" ausgewählt, und anschliessend zeigt man AVR-Studio das zusätzliche C-File. AVR-Studio berücksicht dann diese Datei bei der Projekterzeugung, compiliert es und sorgt dafür, dass es zum fertigen Programm dazugelinkt wird.

Globale Variablen über mehrere Dateien

Ein häufiger Problemkreis in der C Programmierung sind auch globale Variablen, die von mehreren *.c Dateien aus benutzt werden sollen. Was hat es damit auf sich?

Zunächst mal muß man bei der Vereinbarung von Variablen zwischen Definition und Deklaration unterscheiden:

Definition
Mit einer Definition wird der Compiler angewiesen, eine Variable tatsächlich zu erzeugen. Damit er das kann, muß ihm selbstverständlich der exakte Datentyp und auch der Name der Variablen zur Verfügung stehen. Eine Definition sorgt also dafür, dass im späteren Programm Speicherplatz für diese Variable reserviert wird
Deklaration
Mit einer Deklaration teilt man dem Compiler lediglich mit, dass eine Variable existiert. An dieser Stelle soll der Compiler also keinen Speicherplatz reservieren, sondern der Compiler soll einfach nur zur Kenntniss nehmen, daß es eine Variable mit einem bestimmten Namen gibt und von welchem Datentyp sie ist.

Aus obigem folgt sofort, dass eine Definition auch immer eine Deklaration ist. Denn dadurch, daß der Compiler angewiesen wird eine Variable auch tatsächlich zu erzeugen, folgt, dass er dazu auch dieselben Informationen benötigt, die auch in einer Deklaration angegeben werden müssen. Der einzige Unterschied: Bei einer Deklaration trägt der Compiler nur in seinen internen Tabellen ein, dass es diese Variable tatsächlich gibt, während er bei einer Definition zusätzlich auch noch dafür sorgt, dass im fertigen Programm auch Speicher für diese Variable bereitgestellt wird.

Warum ist diese Unterscheidung wichtig?

Weil es in C die sog. One Definition Rule (ODR). Sie besagt, dass in einem vollständigen Programm, also über alle *.c Dateien gesehen, es für eine Variable nur eine Definition geben darf. Es darf allerdings beliebig viele Deklarationen geben, solange diese Deklarationen alle im Datentyp übereinstimmen. Kurz gesagt: Man darf den Compiler nur einmal auffordern, eine Variable zu erzeugen (Definition), kann sich aber beliebig oft auf diese eine Variable beziehen (Deklarationen). Aber Vorsicht! Da der Compiler jede einzelne *.c Datei für sich alleine übersetzt und dabei kein Wissen von ausserhalb benutzt, obliegt es der Verantwortung des Programmierers dafür zu sorgen, dass alle Deklarationen im Datentyp übereinstimmen. Der Compiler kann diese Einhaltung prinzipbedingt nicht überwachen!

Woran erkennt man eine Definition bzw. Deklaration?

Eine Definition einer globalen Variable steht immer ausserhalb eines Funktionsblocks. Zb.

int  MyData;         // Globale Variable namens MyData. Sie ist vom Typ int
char Name[30];       // Globales Array
long NrElements = 5; // Globale Variable, die auch noch initialisiert wird

Eine Deklaration unterscheidet sich von einer Definition in 2 Punkten

  • Es wird das Schlüsselwort extern vorangestellt.
  • Es kann keine Initialisierung geben. Sobald eine Initialisierung vorhanden ist, wird das Schlüsselwort extern ignoriert und aus der Deklaration wird eine Definition.

Beispiele für Deklarationen

extern int  MyData;
extern char Name[30];
extern long NrElements;
extern long NrElements = 5;  // Achtung: Dies ist eine Definition!

Besitzt man also 2 *.c Dateien, main.c und helpers.c, und sollen sich diese beiden Dateien eine globale Variable teilen, so muss in eine Datei eine Definition hinein, während in die andere Datei eine Deklaration derselben Variablen erfolgen muß. Traditionell werden die Definitionen in der *.c-Datei gemacht, die als Hauptdatei des Moduls fungiert, zu der diese Variable konzeptionell gehört. Im Zweifel ist das die *.c Datei, in der main() enthalten ist. Das muss nicht so sein, ist aber eine Konvention, die oft Sinn macht. Alternativ wird auch gerne oft eine eigene *.c Datei (zb. globals.c) gemacht, die einzig und alleine die Defintionen der globalen Variablen enthält.

main.c

int  AnzahlElemente;        // Dies ist die Definition. Hier wird die globale
                            // Variable AnzahlElemente tatsächlich erzeugt.

int main()
{
  AnzahlElemente = 8;
  ...
}

helpers.c

extern int AnzahlElemente;   // Dies ist die Deklaration die auf die globale
                             // Variable AnzahlElemente in main.c verweist.
                             // Wichtig: Der Datentyp muss mit dem in main.c
                             // angegebenen übereinstimmen

void foo()
{
   ...
  j = AnzahlElemente;
  AnzahlElemente = 9;
}

Praktische Durchführung

Besteht ein vollständiges Programm aus mehreren *.c Dateien, dann kann man sich vorstellen, daß es mühsam ist, alle Deklarationen immer auf gleich zu halten. Hier bietet sich der Einsatz eines Header Files an, in der die Deklarationen stehen und welches in die jeweiligen *.c Dateien inkludiert wird

Bsp:

Global.h

extern int Anzahl;

main.c

#include "Global.h"

int Anzahl;      // auch wenn Global.h inkludiert wurde, so muss es eine
                 // Definition der Variablen geben. In Global.h sind ja nur
                 // Deklarationen.

int main()
{
  ...
  Anzahl = 5;
}

foo.c

#include "Global.h"

void foo()
{
  ...
  Anzahl = 8;
  ...
}

bar.c

#include "Global.h"

void bar()
{
  ...
  j = Anzahl;
}

Auf diese Art kann man erreichen, dass zumindest alle Deklarationen ein und derselben Variablen in einem Programm übereinstimmen. Die Datei Global.h wird auch in main.c inkludiert, obwohl man das eigentlich nicht müsste, denn dort wird die Variable ja definiert. Durch die Inclusion ermöglicht man aber dem Compiler die Überprüfung ob die Deklaration auch tatsächlich mit der Definition übereinstimmt.

Solange kein Initialisierungen der globalen Variablen notwendig sind, gibt es noch einen weiteren Trick, um sich selbst das Leben und die Verwaltung der globalen Variablen zu erleichtern. Worin besteht das Problem? Das Problem besteht darin, dass man bei Einführung einer neuen globalen Variablen an 2 Stellen erweitern muss: Zum einen in der Header-Datei, die die 'extern'-Deklaration der Variablen enthält, zum anderen muss in einer C-Datei die Definition der Variablen erfolgen. Das kann man sich mit etwas Präprozessorarbeit auch einfacher machen:

Global.h

#ifndef EXTERN
#define EXTERN extern
#endif

EXTERN int Anzahl;

main.c

#define EXTERN
#include "Global.h"

int main()
{
  ...
  Anzahl = 5;
}

foo.c

#include "Global.h"

void foo()
{
  ...
  Anzahl = 8;
  ...
}

Wie funktioniert das Ganze? Im Grunde muss man nur dafür sorgen, dass der Compiler an einer Stelle das Schlüsselwort extern ignoriert (hier in main.c) und bei allen anderen Inclusionen beibehält. Dadurch das ein Präprozessor-ifndef benutzt wird, kann dieses erreicht werden. Wird das Header File includiert und ist zu diesem Zeitpunkt das Makro EXTERN noch nicht definiert, so wird innerhalb des Header Files EXTERN zu extern definiert und damit in weiterer Folge im Quelltext EXTERN durch extern ersetzt. Wenn daher foo.c das Header File inkludiert, wird die Zeile

EXTERN int Anzahl;

vom Präprozessor zu

extern int Anzahl;

umgewandelt.

In main.c hingegen sieht die Include-Sequenz so aus

#define EXTERN
#include "Global.h"

Wenn Global.h bearbeitet wird, existiert bereits ein Makro EXTERN, das auf einen leeren Text expandiert. Dadurch wird verhindert, dass innerhalb von Global.h das Makro EXTERN mit dem Text extern belegt wird und

EXTERN int Anzahl;

wird daher vom Präprozessor zu

int Anzahl;

erweitert, genau wie es benötigt wird.

Was hat es mit volatile auf sich

Immer wieder hört man im Forum die pauschale Aussage "Variablen die in einer ISR verwendet werden, müssen volatile sein". Nun, das ist so nicht ganz richtig. Welches Problem löst denn eigentlich volatile? Was ist denn das eigentliche Problem, das einer Lösung bedarf?

Das Problem findet sich diesmal im Optimierer eines C Compilers. C Compiler übersetzen, wenn sie optimieren dürfen, den C Code nicht direkt so, wie ihn der Programmierer geschrieben hat, sondern sie versuchen Ressourcen einzusparen. Das kann sowohl Programmspeicher als auch Laufzeit, häufig auch beides gemeinsam sein. Zu diesem Zweck untersuchen sie das Programm und versuchen in der funktional gleichwertigen, in Maschinensprache übersetzen Version, Anweisungen einzusparen. Das dürfen sie auch. Der C-Standard erlaubt Optimierungen, solange die 'As-If'-Regel eingehalten wird. Das bedeutet: Der Compiler darf das Programm umstellen und verändern, solange die Programmergebnisse dieselben bleiben. Eben "As-if" die Optimierung nie stattgefunden hätte.

Nehmen wir ein Beispiel

  i = 2;

  if( i == 5 )
    j = 8;
  else
    j = 6;

In diesem Programmausschnitt darf der Compiler seine Kenntnisse ausnutzen. Er weiß an dieser Stelle, dass i den Wert 2 hat. Damit ist aber auch klar, dass die Bedingung niemals wahr sein kann, denn 2 kann niemals gleich 5 sein. Wenn die Bedingung aber niemals wahr sein kann, dann kann auch die Zuweisung von 8 an j niemals ausgeführt werden. Der Compiler kann also diesen Programmtext zu diesem hier kürzen

  i = 2;
  j = 6;

ohne das sich an den Programmergebnissen etwas ändert. Die zweite Version ist aber kürzer und wird, wegen des Wegfalles des Vergleiches, auch schneller ausgeführt.

Eine andere Form der Optimierung betrifft die Verwaltung von µC-Ressourcen und da wieder ganz speziell die Register. Variablen werden ja erst mal im SRAM-Speicher des µC angelegt. Um mit den Werten von Variablen arbeiten zu können, müssen diese Wert aber vom SRAM-Speicher in µC-Register überführt werden (Register kann man sich wie Speicherstellen in der eigentlichen CPU vorstellen). Nur dort können diese Werte mittels Maschinenbefehlen manipuliert werden. Jetzt haben aber µC nicht beliebig viele Register. Das bedeutet aber auch, der Compiler muss darüber Buch führen, welche Werte (welche Variablen) gerade in welchen Registern liegen und wenn alle Register belegt sind, muss ein anderes Register freigeräumt werden, in dem der Wert aus dem Register wieder ins SRAM zurück übertragen wird. Allerdings kostet das auch Zeit. Der Compiler wird daher versuchen, Variablen, die in einem Programmstück oft benötigt werden, für längere Zeit in den Registern zu halten, um das Registerladen bzw. -zurückschreiben einzusparen. Die Grundannahme lautet dabei immer: In Anweisungen, in denen eine Variable nicht vorkommt, kann diese Variable auch nicht verändert werden. Im Programmstück

  while( 1 ) {
    if( i == 5 )
      j = 8;
  }

gibt es für i keine Möglichkeit, verändert zu werden. Der Compiler kann daher entscheiden, dass er diese Variable, *an dieser Stelle*, gar nicht aus dem SRAM laden muss, sondern sich den entsprechenden Wert in einem Register vorhält und dieses Register ausschließlich dafür reserviert. (Er könnte auch entscheiden, dass der Code nie ausgeführt werden kann, aber das ist eine andere Geschichte)

Der springende Punkt ist nun, dass der Compiler hier eine zu kleine Sicht der Dinge hat. Betrachtet man nur dieses Code Stück, dann gibt es tatsächlich für i keine Möglichkeit, seinen Wert zu verändern. Aber die Dinge ändern sich, wenn Interrupts ins Spiel kommen.

uint8_t i;

ISR( irgendein_Interrupt )
{
  i = 5;
}

int main()
{
  ...

  while( 1 ) {
    if( i == 5 )
      j = 8;
  }
}

Jetzt gibt es plötzlich eine Möglichkeit, wie i seinen Wert ändern kann: Wenn der entsprechende Interrupt ausgelöst wird, dann wird i auf den Wert 5 gesetzt. i, das ist aber nichts anderes als ein bestimmter Speicherbereich im SRAM. D.h. im SRAM wird die Variable tatsächlich korrekt auf den Wert 5 gesetzt. Nur: Als der Compiler die while-Schleife übersetzt hat, wusste er nichts davon, dass diese Möglichkeit existiert. Er hat entschieden, dass er an dieser Stelle den Wert der Variablen in einem CPU-Register halten wird, um Zugriffe einzusparen. Nur wird diese Kopie des Wertes im Register natürlich nicht verändert, wenn in der ISR das Original von i im SRAM verändert wird. Fazit: Obwohl die ISR die Variable tatsächlich verändert, kriegt das der Code im while nicht mit, weil der Compiler es mit der Optimierung an dieser Stelle übertrieben hat. In der while-Schleife wird mit einer Kopie des Wertes von i in einem Register gearbeitet und nicht mit dem originalen Wert von i im SRAM.

Und an dieser Stelle kommt jetzt volatile ins Spiel.

volatile teilt dem Compiler mit, dass ausnahmslos alle Zugriffe auf eine Variable auch tatsächlich auszuführen sind und keine Optimierungen gemacht werden dürfen, weil eine Variable auf Wegen benutzt werden kann, die für den Compiler prinzipiell nicht einsichtig sind. Im obigen Beispiel könnte man argumentieren, dass der Compiler ja wohl die ISR bemerken könne und daher feststellen könnte, dass i tatsächlich verändert wird. Aber das stimmt so in der allgemeinen Form nicht. Niemand sagt, dass der Compiler die ISR überhaupt zu Gesicht bekommen muss, die könnte ja auch in einem ganz anderen C File stecken.

volatile uint8_t i;

ISR( irgendein_Interrupt )
{
  i = 5;
}

int main()
{
  ...

  while( 1 ) {
    if( i == 5 )
      j = 8;
  }
}

wird i volatile gemacht, so verbietet man damit dem Compiler explizit, Annahmen über den Datenfluss von i zu treffen. Innerhalb der Schleife muss also tatsächlich jedes Mal wieder erneut i aus dem SRAM geholt werden und mit 5 verglichen werden. Abkürzungen durch Mehrfachverwendung von Registern oder sonstigen Optimierungstricks sind nicht erlaubt. Und damit ist das Problem gelöst. Wird i in der ISR verändert, so bekommt das auch die Abfrage mit, weil ja jetzt auf jeden Fall auf das Original im SRAM zurückgegriffen wird.

Das Gleiche gilt auch ebenso "in die andere Richtung", wenn also i in der Schleife geändert und in der ISR nur gelesen wird. Auch hier könnte die Optimierung negativ zuschlagen und den Schreibzugriff nur auf eine lokale Kopie der Variable in einem Register durchführen (oder gar ganz wegfallen) lassen, weil der Lesezugriff außerhalb des direkten Programmflusses (in der ISR) für den Compiler nicht ersichtlich ist.

Ein anderes Problem (das ebenfalls mittels volatile gelöst wird) sind Variablen, die tatsächlich im Code überhaupt nie aktiv verändert werden, sondern es sich um Zusatzhardware handelt, die so verschaltet ist, dass sie im Programm in Form einer Variablen auftaucht, z.B. ein Uhren-IC (oder auch ganz banal: Portpins). In diesem Fall wird z.B. die Variable für Sekunden vom Programm gar nicht vom Programm selber verändert, ändert aber trotzdem ihren Inhalt. Die Zusatzhardware selbst macht das. Aus Programmsicht handelt es sich um Speicherzellen, die magisch selbsttätig ihren Wert ändern. Und damit dürfen selbstverständlich auch hier keinerlei Annahmen über den Inhalt der Variablen getroffen werden. Eine derart angebundene externe Hardware nennt man übrigens "memory-mapped", weil sie ihre Werte ins Memory (=Hauptspeicher) mapped (=einblendet).

Allerdings kann volatile nur bei den Variablen sinnvoll genutzt werden, die "von außen" auch änderbar sind. Bei lokalen Variablen, auch statischen, einer Funktion kann das nur passieren, wenn ihre Adresse einer ISR z.B. durch einen globalen Pointer bekannt gemacht wird.

uint8_t *v;
ISR( irgendein_Interrupt )
{
  i = 5;
  *v = 42;
}

int main()
{
  uint8_t i; // kann sich nie unerwartet ändern -> volatile nutzlos, behindert nur Optimizer

  volatile uint8_t j; // kann sich unerwartet ändern (über globalen *v)
  v = &j;
  ...
  while( 1 ) {
    if( i == 5 )
      j = 8;
    if( j == 5 )
      i = 3;
  }
}

Einfach immer alle Variablen generell volatile zu machen, ist dann auch über das Ziel hinausgeschossen. Denn damit legt man in letzter Konsequenz eine wichtige Möglichkeit für Optimierungen durch den Compiler lahm.

Konstanten an fester Flash-Adresse

Wie kann man eine Konstante an entsprechender Adresse im Flash ablegen?

Mehmet Kendi hat eine Lösung für AVR Studio & WinAVR in [1] angegeben.

Timer

Was macht ein Timer?

Oft hört man im Forum die Aussage: Timer sind so kompliziert!

Aber eigentlich stimmt das nicht. Ganz im Gegenteil, Timer sind eigentlich eine sehr einfache Sache. Was genau macht eigentlich ein Timer? Die Antwort lautet: er zählt unabhängig vom restlichen Programmfluss vor sich hin. Und? Was macht er noch? Nichts. Das wars schon. Im Kern ist genau das auch schon alles was ein Timer macht.

ein 8-Bit Timer bei der Arbeit

Wie schnell macht er es?

Aber so einfach ist die Sache dann doch wieder nicht. Da erhebt sich zunächst mal die Frage: wie schnell zählt denn eigentlich so ein Timer? Normalerweise ist der Timer mit der Taktfrequenz des Prozessors gekoppelt, so dass zb bei einer Taktfrequnz von 1Mhz der Timer auch genau so schnell zählt. In 1 Sekunde zählt ein Timer also von 0 bis 1000000, also 1 Mio Zählschritte. Nun kann aber ein beispielsweise 8-Bit Timer nicht bis 1000000 zählen, dazu ist er nicht groß genug. Mit 8 Bit kann man bis 255 zählen. Zählt man da dann noch 1 dazu, dann läuft der Timer über und beginnt wieder bei 0. Man kann daher ruhigen Gewissens sagen: In 1 Sekunde zählt dieser Timer 3906 mal den Bereich von 0 bis 255 (und weiter auf 0) durch (das sind 256 Zählschritte) und zuätzlich schafft er es danach noch bis 64 zu zählen. Denn 3906 * 256 + 64 = 1000000 und wir haben wieder die 1 Mio Zählschritte, die der Timer in 1 Sekunde erledigt.

Das ist ganz schön schnell. Und weil das oft zu schnell ist, hat jeder Timer noch die Möglichkeit sogenannte Vorteiler (Prescaler) vor den Zähltakt zu schalten. Zb einen Vorteiler von 8. Anstelle von 1Mhz bekommt der Timer dann eine Frequenz von 1Mhz / 8 (= 125kHz) präsentiert. Und dementsprechend würde er in 1 Sekunde dann nur noch von 0 bis 125000 (= 1.000.000 / 8) zählen. Als 8 Bit Timer bedeutet das, dass er in 1 Sekunde jetzt nur noch 488 komplette Zyklen 0 bis 255 schafft und dann noch bis 72 zählen kann. Denn 488 * 256 + 72 = 125000

Das kann aber nicht alles gewesen sein?

Bis jetzt ist das alles noch unspektakulär und man fragt sich: Was hab ich jetzt davon, wenn der Timer vor sich hinzählt? Nun, die Situation ändert sich, wenn man weiß, dass man bei bestimmten Ereignissen und/oder Zählerständen etwas auslösen lassen kann. So ist zb. dieser Überlauf von 255 auf 0 so ein Ereignis. Mittels eines Interrupts kann man auf dieses Ereignis reagieren lassen und als Folge davon wird eine Funktion vollautomatisch aufgerufen. Und zwar unabhängig davon, was der µC gerade sonst so tut. Und das ist schon recht cool, denn es bedeutet, dass man regelmäßig zu erfolgende Dinge in so eine ISR (Interrupt Service Routine, die Funktion die aufgerufen wird) stecken kann und der Timer sorgt ganz von alleine dafür, dass diese Funktionalität auch tatsächlich regelmäßig ausgeführt wird. Regelmäßig bedeutet in diesem Fall dann auch wirklich regelmäßig. Denn der Timer zählt ja losgelöst von den restlichen Arbeiten, die der µC sonst so erledigt, vor sich hin. Es spielt keine Rolle, ob der µC gerade mitten in einer komplizierten Berechnung steckt oder nicht. Der Timer zählt vor sich hin, und wenn das entsprechende Ereignis eintritt, wird der Interrupt ausgelöst. Und wenn der entsprechende Interrupt mit einer ISR-Funktion gekoppelt ist, dann wird der normale Programmfluss unterbrochen und der µC arbeitet genau diese eine Funktion ab. Und zwar in regelmässigen Zeitabständen, weil ja auch der Interrupt regelmässig auftritt.

ein 8-Bit Timer löst durch seinen Overflow regelmäßige ISR Aufrufe aus

Gut, beispielsweise 488 mal in der Sekunde mag für so manchen Zweck zu oft sein, aber es gibt ja auch noch andere Vorteiler (welche steht im Datenblatt) und dann kann man ja auch innerhalb der ISR in einer lokalen Variablen mitzählen und zb nur bei jedem 2.ten Aufruf eine Aktion machen, die dann nur noch 244 mal in der Sekunde ausgeführt wird. Hier gibt es also mehrere Möglichkeiten, wie man die Aufrufhäufigkeit weiter herunterteilen kann, so dass man sich der Zahl annähert, die man benötigt.

// Für einen Mega16.
// Andere Prozessoren: siehe Datenblatt wie die Timerkonfiguration einzustellen ist

#define F_CPU 1000000UL
#include <avr/io.h>
#include <avr/interrupt.h>

//
// der Timer wird mit 1Mhz getaktet. Vorteiler ist 8
// d.h. der Timer läuft mit 125kHz und würde daher in 1 Sekunde
// von 0 bis 124999 zaehlen.
// Aber nach jeweils 256 Zaehlungen erfolgt ein Overflow.
// Daher werden in 1 Sekunde 125000 / 256 = 488.28125 Overflows erzeugt
// Oder anders ausgedrückt:  1 / 488.2815 = 0.002048
// alle 0.002048 Sekunden erfolgt ein Overflow
//
ISR( TIMER0_OVF_vect )       // Overflow Interrupt Vector
{
  static uint8_t swTeiler = 0;

  swTeiler++;
  if( swTeiler == 200 ) {    // nur bei jedem 200.ten Aufruf. Effektiv teilt dieses die
                             // die Aufruffrequenz des nachfolgenden Codes nochmal um
    swTeiler = 0;            // einen Faktor 200. Der nachfolgende Code wird daher nicht
                             // alle 0.002 Sekunden sondern alle 0.4096 Sekunden ausgeführt.
                             // Das reicht, dass man eine LED am Port schon blinken sieht.
    PORTD = PORTD ^ 0xFF;    // alle Bits am Port umdrehen, einfach damit sich was tut
  }
}

int main()
{
  DDRD = 0xFF;          // irgendein Port, damit wir auch was sehen

  TIMSK |= (1<<TOIE0);  // den Overflow Interrupt des Timers freigeben
  TCCR0 = (1<<CS01);    // Vorteiler 8, jetzt zählt der Timer bereits

  
  sei();                // und Interrupts generell freigeben

  
  while( 1 )
  {                     // hier braucht nichts mehr gemacht werden.
  }                     // der Timer selbst sorgt dafür, dass die ISR Funktion
}                       // regelmäßig aufgerufen wird

CTC Modus

Manchmal reicht das aber nicht. Benötigt man zb nicht 488 sondern möglichst genau 500 ISR Aufrufe in der Sekunde, so wird man weder mit Vorteiler noch durch Softwaremässiges Weiterteilen in der ISR zum Ziel kommen. Man kann natürlich die Taktfrequenz des kompletten Systems soweit umstellen, dass sich das alles ausgeht, aber oft ist das einfach nicht möglich. Was tun?

Die Sache wäre einfacher, wenn man dem Timer vorschreiben könnte, nicht einfach nur von 0 bis 255 zu zählen, sondern wenn man ihm eine Obergrenze vorgeben könnte. Denn dann könnte man sich eine Obergrenze so bestimmen, dass dieser neue Zählbereich in 1 Sekunde ganz genau so oft durchlaufen werden kann, wie man es benötigt. Und hier kommt der sog. CTC Modus ins Spiel. Denn genau darin besteht sein Wesen: Man gibt dem Timer eine Obergrenze vor. Erreicht seine Zählung diesen Wert, so wird der Timer auf 0 zurückgesetzt und beginnt wieder von vorne. Genau das was wir benötigen. Wollen wir exakt 500 ISR Ausfrufe in der Sekunde haben (bei 1 Mhz Systemtakt), dann wählen wir einen Vorteiler von 8 und setzen die Obergrenze auf 250-1 (nicht vergessen: wir brauchen 250 Zählschritte, das bedeutet der Timer muss von 0 bis 249 zählen, denn auch der Überlauf von 249 zurück auf 0 ist ein Zählschritt). Der Timer taktet dann mit 1Mhz / 8 = 125kHz und da nach jeweils 250 Zählschritten die Obergrenze erreicht ist, wird diese Obergrenze in 1 Sekunde 125000 / 250 = 500 mal erreicht. Genau so wie wir das wollten. Wie wird dem Timer nun mitgeteilt, dass er eine spezielle Obergrenze benutzen soll? Nun, jeder Timer hat verschiedene Modi. Welche das bei einem konkreten µC und bei einem konkreten Timer genau sind, findet sich im Datenblatt im Abschnitt über die Timer. Normalerweise ist immer eines der letzteren Abschnitte eines jeden Kapitels im Datenblatt das interessantere: "Register Summary" oder "Register Description" genannt (die Datenblätter sind da nicht ganz einheitlich). So auch hier.

Auszug aus dem Atmel Datenblatt für den Mega16 - Suche im Datenblatt

In jedem Atmel Datenblatt findet sich bei jedem Timer immer auch besagter Abschnitt (im Inhaltsverzeichnis beim Kapitel über den jeweiligen Timer suchen. Im Datenblatt-PDF daher immer das Inhaltsverzeichnis anzeigen lassen!), und in diesem Abschnitt gibt es eine Tabelle, aus der hervorgeht, welche Modi es gibt, welche Bits dazu in den Konfigurationsregistern gesetzt werden müssen, wie sich dann die Obergrenze des Timer-Zählbereichs zusammensetzt und noch ein paar Angaben mehr. Beim Mega16 findet sich diese Tabelle für den Timer 0 zb auf Seite 83 und dort ist es die Tabelle 14.2. Diese Tabelle sieht im Datenblatt so aus

Mode WGM01 WGM00 Timer/Counter TOP Update of TOV0 Flag
(CTC0) (PWM0) Mode of Operation OCR0 Set-on
0 0 0 Normal 0xFF Immediate MAX
1 0 1 PWM, Phase Correct 0xFF TOP BOTTOM
2 1 0 CTC OCR0 Immediate MAX
3 1 1 Fast PWM 0xFF BOTTOM MAX

Dort beginnt man mit der Recherche und sucht sich die Bits für den gewünschten Modus raus. Mit diesen Bits sieht man dann in den Konfigurationsregistern nach, welches Bit zu welchem Register gehört und setzt es ganz einfach. In unserem Fall möchten wir den CTC Modus, also den Modus 2. Dazu muss das Bit WGM01 gesetzt werden und WGM00 muss auf 0 bleiben. Im Datenblatt ein wenig zurückscrollen bringt ans Licht, dass das Bit WGM01 im Konfigurationsregister TCCR0 angesiedelt ist. Weiters entnehmen wir der Tabelle, dass der Timer bis zum Wert in OCR0 zählen wird (die Spalte TOP). Dort hinein müssen also die 250-1 als Obergrenze geschrieben werden.

Wichtig ist auch noch: Dieser Spezialmodus CTC löst keinen Overflow Interrupt aus, sondern einen sog. Compare Match Interrupt. Dies deshalb, weil die gewünschte Obergrenze ja laut Datenblatt in eines der sog. Compare Match Register geschrieben werden muss (OCR0). Das sind Spezialregister, die nach jedem Zählvorgang mit dem Zählregister verglichen werden. Stimmt ihr Inhalt mit dem des Zählregisters überein, so hat man einen Compare-Match und kann daran wieder eine Aktion (ISR) knüpfen. In diesem speziellen Fall des CTC Modus beinhaltet dieser Compare Match dann auch noch das automatische Rücksetzen des Timers auf 0.

Und natürlich können auch mehrere Techniken kombiniert werden. Z. B. CTC Modus und zusätzliches weiteres softwaremässiges Herunterteilen in der ISR sieht dann so aus:

// Für einen Mega16.
// Andere Prozessoren: siehe Datenblatt wie die Timerkonfiguration einzustellen ist
 
#define F_CPU 1000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
 
ISR( TIMER0_COMP_vect )    // Compare Match Interrupt Vector
{
  static uint8_t swTeiler = 0;
 
  swTeiler++;
  if( swTeiler == 200 ) {
    swTeiler = 0;
    PORTD = PORTD ^ 0xFF;
  }
}
 
int main()
{
  DDRD = 0xFF;          // irgendein Port, damit wir auch was sehen
 
  TIMSK = (1<<OCIE0);               // den Output Compare Interrupt des Timers freigeben
  OCR0  = 250 - 1;                    // nach 250 Zaehlschritten -> Interrupt und Timer auf 0
  TCCR0 = (1<<WGM01) | (1<<CS01);    // Vorteiler 8, CTC Modus
 
  
  sei();                // und Interrupts generell freigeben
 
  
  while( 1 )
  {                     // hier braucht nichts mehr gemacht werden.
  }                     // der Timer selbst sorgt dafür, dass die ISR Funktion
}                       // regelmässig aufgerufen wird

Fast PWM

Aber der Timer kann noch mehr. Wenn der Timer so vor sich hinzählt, dann kann man bestimmte Output-Pins des µC an diesen Timer koppeln. Der Timer kann dann diesen Pin bei erreichen von bestimmten Zählerständen ganz von alleine wahlweise auf 0 schalten, auf 1 schalten oder umdrehen.

Was passiert da genau? Im folgenden sei von der einfachsten Form der PWM auf einem Mega16 ausgegangen: Wenn der Timer in seiner Zählerei bei 0 ist, dann schaltet er den Pin auf 1, bei einem bestimmten Zählerstand soll er den Pin wieder auf 0 zurücksetzen und ansonsten soll der Timer wie gewohnt laufend von 0 bis 255 durchzählen. Es ist die Rede vom Timer-Modus 3, siehe die vorhergehende Tabelle aus dem Datenblatt.

Der Tabelle entnehmen wir wieder: Die Bits WGM01 und WGM00 müssen auf 1 gestellt werden. Der TOP Wert (also der Wert, bis zu dem der Timer zählt) ist 0xFF (also 255) und das OCR0 Register steuert, bei welchem Zählerstand die Timerhardware den Pin wieder auf 0 zurück schaltet. Das Einschalten auf 1 ist vorgegeben (auch das kann man ändern, dazu später mehr).

Stellt man also genau diese Konfiguration her und schreibt in das Register OCR0 beispielsweise den Wert 253, dann beginnt der Timer bei 0 zu zählen, wobei der den Ausgangspin auf 1 schaltet. Wird der Zählerstand 253 erreicht, dann schaltet der Timer den Pin wieder auf 0 zurück und zählt weiter bis 255 um dann wieder erneut bei 0 zu beginnen (und den Ausgansg-Pin wieder auf 1 zu schalten). Der Ausgangspin ist in diesem Fall also die meiste Zeit auf 1 und nur ganz kurz (während der Timer von 253 bis 255 zählt) auf 0.

Schreibt am auf der anderen Seite in das Register OCR0 den Wert 3, dann passiert konzeptionell genau dasselbe nur mit anderen Zahlenwerten. Der Timer beginnt bei 0 zu zählen und schaltet den Ausgansgpin auf 1. Aber diesmal ist es bereits beim Zählerstand 3 so weit: Der Zählerstand stimmt mit dem Wert in OCR0 überein und als Folge davon wird der Ausgangspin wieder auf 0 gestellt. Der Timer zählt natürlich wie immer weiter bis 255 ehe dann das ganze Spiel wieder von vorne beginnt. In diesem Fall war also der Ausgangspin nur ganz kurze Zeit auf 1 (nämich in der Zeit, die der Timer benötigt um von 0 bis 3 zu zählen) und dann die meiste Zeit auf 0. Und genau darum geht es bei PWM: Mit dem Register OCR0 lässt sich daher der zeitliche Anteil steuern, in dem der Pin auf 1 liegt.


// Für einen Mega16.
// Andere Prozessoren: siehe Datenblatt wie die Timerkonfiguration einzustellen ist
 
#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>
  
int main()
{
  uint8_t i;

  DDRB |= (1<<PB3);       // PB3 auf Ausgang stellen. Dieser Pin
                          // trägt auch die Bezeichnung OC0 und ist der Pin
                          // an dem der Timer 0 seine PWM ausgibt

  OCR0  = 250;
  TCCR0 = (1<<WGM01) | (1<<WGM00) | (1<<CS01);    // Vorteiler 8, Fast PWM 8 Bit
  TCCR0 = (1<<COM01); 
  
  while( 1 )
  {                     // hier braucht nichts mehr gemacht werden.
                        // der Timer selbst sorgt dafür, dass die PWM läuft
                        // wir wollen aber ein wenig Action haben.
                        // Also setzen wir das OCR0 Register auf verschiedene
                        // Werte, damit eine angeschlossene LED unterschiedliche
                        // Helligkeiten zeigt
    OCR0 = 30;
    _delay_ms( 1000 );
    OCR0 = 200;
    _delay_ms( 1000 );

    for( i = 0; i < 255; ++i ) {
      OCR0 = i;
      _delay_ms( 10 );
    }
  }
}

Bleibt noch das gesetzte Bit COM01. Was hat es damit auf sich? Bisher war immer die Rede davon, das der Ausganspin bei einem Zählerstand von 0 auf 1 geschaltet wird usw. Das regelt genau dieses Bit. Im Datenblatt findet sich die Tabelle 14.4 auf der Seite 83, die genau regelt welche Bedeutung die Bits COM01 bzw COM00 haben, wenn der Timer Modus auf Fast-PWM eingestellt ist. Achtung: Je nach Timer-Modus haben diese Bits andere Bedeutungen! Man muss sich also immer die zum jeweiligen Timer-Modus gehörende Tabelle im Datenblatt suchen.


  1. libc.a sollte nicht zu den Bibliotheken hinzugefügt werden, da sie automatisch eingebunden wird. Etwas Hintergrundinformationen gibt es in diesem Thread.