FAQ
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:
<c> lcd_string( "Hallo Welt" ); // ggf. auch lcd_out() o.ä. in anderen Libraries </c>
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 Libary stdlib.h.
<c>
#include <stdlib.h> char Buffer[20]; int i = 25;
itoa( i, Buffer, 10 ); lcd_string( Buffer ); // ggf. auch lcd_out() o.ä. in anderen Libraries
</c>
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()
<c>
char Buffer[20]; int i = 25;
sprintf( Buffer, "%d", i ); lcd_string( Buffer ); // ggf. auch lcd_out() o.ä. in anderen Libraries
</c>
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:
<c>
char Buffer[20]; int i = 25;
sprintf( Buffer, "Anzahl: %d Stueck", i ); lcd_out( Buffer );
</c>
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.
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
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).
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:
<c> 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';
} </c>
Das Grundprinzip ist einfach:
Die Ermittlung der einzelnen Stellen erfolgt in der zentralen Schleife
<c>
do {
Buffer[i++] = '0' + u % 10;
u /= 10;
} while( u > 0 );
</c>
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
<c>
for( j = 0; j < i / 2; ++j ) {
tmp = Buffer[j];
Buffer[j] = Buffer[i-j-1];
Buffer[i-j-1] = tmp;
}
</c>
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:
- Forenbeitrag Integer-Zahl in String mit bestimmter Zeichenlänge
- Forenbeitrag (Resourcenschonend) Wert einer Variable am LCD ausgeben von Niels Hüsken
- Festkommaarithmetik
Datentypen in Operationen
Ein häufiges Problem betrifft die Auswertung von Ausdrücken. Konkret die Frage nach den beteiligten Datentypen. zb <c> double i; int j, k;
i = j / k; </c> 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 <c> double i; i = 5 / 8; </c> 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.
<c> double i; i = 5.0 / 8.0;
i = (double)j / k; </c>
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 Konstante?
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 Konstante in hexadezimaler bzw. oktaler Schreibweise immer einen unsigned Datentyp haben.
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
<c>
(long)5
</c>
- oder man benutzt die in C dafür vorgesehene Schreibweise, indem man der Konstanten einen Suffix anhängt, der den gewünschten Datentyp beschreibt
<c>
5L
</c> Beides ergibt eine dezimale 5, die vom Datentyp long ist.
- eine Zahl in mit einem Dezimalkomma
<c>
5.0
</c> ist immer eine double Zahl. Es sei denn sie hat explizit eien Suffix <c>
5.0F
</c> 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.
Aktivieren der Floating Point Version von sprintf beim WinAVR mit AVR-Studio
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
Funktionszeiger
- → Siehe: Funktionszeiger in C
Ich hab da mehrere *.c und *.h Dateien. Was mache ich damit?
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 werdenmit zusätzlichen Bibliotheken und dem Startup-Code zum fertigen Programm gelinkt.
Angenommen, das komplette Projekt besteht aus 2 Dateien:
- main.c
<c> int twice (int);
int main() {
twice (5);
} </c>
- func.c
<c> int twice (int number) {
return 2 * number;
} </c>
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, daß 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. <C> 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 </C>
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 <C> extern int MyData; extern char Name[30]; extern long NrElements; extern long NrElements = 5; // Achtung: Dies ist eine Definition! </C>
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 <C> int AnzahlElemente; // Dies ist die Definition. Hier wird die globale
// Variable AnzahlElemente tatsächlich erzeugt.
int main() {
AnzahlElemente = 8; ...
} </C>
helpers.c <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;
} </C>
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 <C> extern int Anzahl; </C>
main.c <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;
} </C>
foo.c <C>
- include "Global.h"
void foo() {
... Anzahl = 8; ...
} </C>
bar.c <C>
- include "Global.h"
void bar() {
... j = Anzahl;
} </C>
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 <C>
- ifndef EXTERN
- define EXTERN extern
- endif
EXTERN int Anzahl; </C>
main.c <C>
- define EXTERN
- include "Global.h"
int main() {
... Anzahl = 5;
} </C>
foo.c <C>
- include "Global.h"
void foo() {
... Anzahl = 8; ...
} </C>
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 <C> EXTERN int Anzahl; </C> vom Präprozessor zu <C> extern int Anzahl; </C> umgewandelt.
In main.c hingegen sieht die Include-Sequenz so aus <C>
- define EXTERN
- include "Global.h"
</C> 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 <C> EXTERN int Anzahl; </C> wird daher vom Präprozessor zu <C> int Anzahl; </C> erweitert, genau wie es benötigt wird.
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.
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 999999, also 1 Mio - 1 Zählschritte. Nun kann aber ein beispielsweise 8-Bit Timer nicht bis 999999 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.
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 Teiler dann eine Frequenz von 1Mhz / 8 (= 125kHz) präsentiert. Und dementsprechend würde er in 1 Sekunde dann nur noch von 0 bis 124999 (= 1000000 / 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ässig 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ässig ausgeführt wird. 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.
<C> // 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
TCCR0 = (1<<CS01); // Vorteiler 8, jetzt zählt der Timer bereits TIMSK |= (1<<TOIE0); // den Overflow Interrupt des Timers freigeben
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 </C>
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 der letzte Abschnitt eines Kapitels im Datenblatt das interessantere: Register Summary. So auch hier. In jedem Atmel Datenblatt findet sich bei jedem Timer immer auch 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. 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 in eines der sog. Compare Match Register geschrieben werden muss. Das sind Spezialregister, die nach jedem Zählvorgang mit dem Zählregister verglichen werden. Stimmt ihr Inhalt mit dem des Zählrgisters ü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.
<c> // 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 </c>
- ↑ libc.a sollte nicht zu den Bibliotheken hinzugefügt werden, da sie automatisch eingebunden wird. Etwas Hintergrundinformationen gibt es in diesem Thread.
