AVR-GCC-Tutorial/Der UART
Über den UART kann ein AVR leicht mit einer RS-232-Schnittstelle eines PC oder sonstiger Geräte mit "serieller Schnittstelle" verbunden werden.
Allgemeines zum UART
Mögliche Anwendungen des UART:
- Debug-Schnittstelle
- z. B. zur Anzeige von Zwischenergebnissen ("printf-debugging" - hier besser "Logging" oder "UART-debugging") über RS-232 auf einem PC. Auf dem Rechner reicht dazu ein Terminalprogramm (MS-Windows: Hyperterm oder besser [1], HTerm; Unix/Linux z. B. minicom). Ein direkter Anschluss ist aufgrund unterschiedlicher Pegel nicht möglich, jedoch sind entsprechende Schnittstellen-ICs wie z. B. ein MAX232 günstig und leicht zu integrieren. Rechner ohne serielle Schnittstelle können über fertige USB-seriell-Adapter angeschlossen werden.
- Mensch-Maschine Schnittstelle
- z. B. Konfiguration und Statusabfrage über eine "Kommandozeile" oder Menüs (siehe z. B. Forumsbeitrag Auswertung RS232-Befehle und Artikel Tinykon)
- Übertragen von gespeicherten Werten
- z. B. bei einem Datenlogger
- Anschluss von Geräten
- mit serieller Schnittstelle (z. B. (Funk-)Modems, Mobiltelefone, Drucker, Sensoren, "intelligente" LC-Displays, GPS-Empfänger).
- "Feldbusse"
- auf RS485/RS422-Basis mittels entsprechenden Bustreiberbausteinen (z. B. MAX485)
- DMX, Midi
- etc.
- LIN-Bus
- Local Interconnect Network: Preiswerte Sensoren/Aktoren in der Automobiltechnik und darüber hinaus
Einige AVR-Controller haben ein bis zwei vollduplexfähige UART (Universal Asynchronous Receiver and Transmitter) schon eingebaut ("Hardware-UART"). Übrigens: Vollduplex heißt nichts anderes, als dass der Baustein gleichzeitig senden und empfangen kann.
Neuere AVRs (ATmega, ATtiny) verfügen über einen bis vier USART(s), dieser unterscheidet sich vom UART hauptsächlich durch interne FIFO-Puffer für Ein- und Ausgabe und erweiterte Konfigurationsmöglichkeiten. Die Puffergröße ist allerdings nur 1 Byte.
Die Hardware
Der UART basiert auf normalem TTL-Pegel mit 0V (logisch 0) und 5V (logisch 1). Die Schnittstellenspezifikation für RS-232 definiert jedoch -12V ... -3V (logisch 1) und +3 ... +12V (logisch 0). Daher muss der Signalaustausch zwischen AVR und Partnergerät invertiert werden. Für die Anpassung der Pegel und das Invertieren der Signale gibt es fertige Schnittstellenbausteine. Der bekannteste davon ist wohl der MAX232.
Streikt die Kommunikation per UART, so ist oft eine fehlerhafte Einstellung der Baudrate die Ursache. Die Konfiguration auf eine bestimmte Baudrate ist abhängig von der Taktfrequenz des Controllers. Gerade bei neu aufgebauten Schaltungen (bzw. neu gekauften Controllern) sollte man sich daher noch einmal vergewissern, dass der Controller auch tatsächlich mit der vermuteten Taktrate arbeitet und nicht z. B. den bei einigen Modellen werksseitig eingestellten internen Oszillator statt eines externen Quarzes nutzt. Die Werte der verschiedenen fuse-bits im Fehlerfall also beispielsweise mit AVRDUDE kontrollieren und falls nötig anpassen. Grundsätzlich empfiehlt sich auch immer ein Blick in die AVR_Checkliste.
Controller mit nur einem Quarzanschluss (z.B Atmega328), die den Timer2 im Asynchron Modus benutzen, müssen intern mit dem RC-Oszillator getaktet werden. Dieser sollte dann kalibriert werden: Kalibrieren des internen Oszillators mit Timer2 als Zeitbasis
Die UART-Register
Die UART wird über vier separate Register angesprochen. Die USARTs der ATMEGAs verfügen über mehrere zusätzliche Konfigurationsregister. Das Datenblatt gibt darüber Auskunft. Die folgende Tabelle gibt nur die Register für die UARTs der ATmega8/16/32 u.ä. wieder.
UCSRA | UART Control and Status Register A. Hier teilt uns der UART mit, was er gerade so macht.
RXC (UART Receive Complete)
TXC (UART Transmit Complete)
UDRE (UART Data Register Empty)
FE (Framing Error)
DOR (Data Over Run)
PE (Parity Error)
U2X (Double the transmission speed)
MPCM (Multi Prozessor Communication Mode)
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UCSRB | UART Control and Status Register B. In diesem Register stellen wir ein, wie wir den UART verwenden möchten.
RXCIE (RX Complete Interrupt Enable)
TXCIE (TX Complete Interrupt Enable)
UDRIE (UART Data Register Empty Interrupt Enable)
RXEN (Receiver Enable)
TXEN (Transmitter Enable)
UCSZ2 (Characters Size)
RXB8 (Receive Data Bit 8)
TXB8 (Transmit Data Bit 8)
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UCSRC | UART Control and Status Register C.
URSEL (Register Select)
UMSEL (USART Mode Select)
UPM1:0 (Parity Mode)
USBS (USART Stop Bit Select)
UCSZ1:0 (Character Size)
UCPOL (Clock Polarity)
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UDR | UART Data Register. Hier werden Daten zwischen UART und CPU übertragen. Da der UART im Vollduplexbetrieb gleichzeitig empfangen und senden kann, handelt es sich hier physikalisch um 2 Register, die aber über die gleiche I/O-Adresse angesprochen werden. Je nachdem, ob ein Lese- oder ein Schreibzugriff auf den UART erfolgt wird automatisch das richtige UDR angesprochen. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
UBRR | UART Baud Rate Register. In diesem Register müssen wir dem UART mitteilen, wie schnell wir gerne kommunizieren möchten. Der Wert, der in dieses Register geschrieben werden muss, errechnet sich nach folgender Formel (wenn U2X Bit 0 gesetzt ist):
Es sind Baudraten bis über 115200 Baud möglich, je nach Controller und CPU-Frequenz. Siehe Datenblatt. |
UART initialisieren
Wir wollen nun Daten mit dem UART auf die serielle Schnittstelle ausgeben. Dazu müssen wir den UART zuerst mal initialisieren. Dazu setzen wir je nach gewünschter Funktionsweise die benötigten Bits im UART Control Register.
Da wir vorerst nur senden möchten und noch keine Interrupts auswerten wollen, gestaltet sich die Initialisierung wirklich sehr einfach, da wir lediglich das Transmitter Enable Bit setzen müssen:
UCSRB |= (1<<TXEN);
Neuere AVRs mit USART haben mehrere Konfigurationsregister und erfordern eine etwas andere Konfiguration. Für einen ATmega16 z. B.:
UCSRB |= (1<<TXEN); // UART TX einschalten
UCSRC = (1<<URSEL)|(1 << UCSZ1)|(1 << UCSZ0); // Asynchron 8N1
Nun ist noch das Baudratenregister UBRR der verwendeten UARTs einzustellen, bzw. bei neueren AVRs die beiden Register UBRRL und UBRRH. . Die Berechnung wird während des Compilerlaufs ausgeführt, beansprucht also in der gezeigten Form weder Speicher noch Rechenzeit des Controllers. Das Ergebniss wird jedoch als ganzzahliger Wert eingesetzt, d.h. Nachkommastellen werden einfach abgeschnitten und es erfolgt keine Rundung. Aus diesem Grund kann man sich eines kleinen Tricks bedienen, indem vor der eigentlichen Division bei der Zuweisung die Hälfte des Wertes dazu addiert wird. Allgemein formuliert bedeutet das: int i = ( a + b/2 ) / b;
. Dies wird in der unten angegebenen Berechnung von UBRR_VAL ausgenutzt um den Fehler zu minimieren. (Eine ausführliche Erklärung zum cleveren Runden findet sich in einer Forumsdiskussion.)
/*
UART-Init:
Berechnung des Wertes für das Baudratenregister
aus Taktrate und gewünschter Baudrate
*/
#ifndef F_CPU
/* In neueren Version der WinAVR/Mfile Makefile-Vorlage kann
F_CPU im Makefile definiert werden, eine nochmalige Definition
hier wuerde zu einer Compilerwarnung fuehren. Daher "Schutz" durch
#ifndef/#endif
Dieser "Schutz" kann zu Debugsessions führen, wenn AVRStudio
verwendet wird und dort eine andere, nicht zur Hardware passende
Taktrate eingestellt ist: Dann wird die folgende Definition
nicht verwendet, sondern stattdessen der Defaultwert (8 MHz?)
von AVRStudio - daher Ausgabe einer Warnung falls F_CPU
noch nicht definiert: */
#warning "F_CPU war noch nicht definiert, wird nun nachgeholt mit 4000000"
#define F_CPU 4000000UL // Systemtakt in Hz - Definition als unsigned long beachten
// Ohne ergeben sich unten Fehler in der Berechnung
#endif
#define BAUD 9600UL // Baudrate
// Berechnungen
#define UBRR_VAL ((F_CPU+BAUD*8)/(BAUD*16)-1) // clever runden
#define BAUD_REAL (F_CPU/(16*(UBRR_VAL+1))) // Reale Baudrate
#define BAUD_ERROR ((BAUD_REAL*1000)/BAUD) // Fehler in Promille, 1000 = kein Fehler.
#if ((BAUD_ERROR<990) || (BAUD_ERROR>1010))
#error Systematischer Fehler der Baudrate grösser 1% und damit zu hoch!
#endif
Die Makros sind sehr praktisch, da damit sowohl automatisch der Wert für UBRR, als auch die Abweichung in der generierten (möglichen) von der gewünschten Baudrate berechnet wird. Im Falle einer zu hohen Abweichung (+/-1%) wird eine Fehlermeldung ausgegeben und der Compilerablauf abgebrochen. Damit können viele Probleme mit "UART sendet komische Zeichen" vermieden werden. Ausserdem kann man mühelos die Einstellung an eine neue Taktfrequenz bzw. Baudrate anpassen, ohne selber rechnen oder in Tabellen nachschlagen zu müssen.
Die eigentliche Initialisierung der UART Register kann im Hauptprogramm main() vorgenommen werden. Öfters wird jedoch eine Funktion z. B. uart_init() dafür geschrieben, die in der eigenen Codesammlung in mehreren Projekten verwendet werden kann.
Für einige AVR (z. B. ATmega169, ATmega48/88/168, AT90CAN jedoch nicht für z. B. ATmega16/32, ATmega128, ATtiny2313) wird durch die Registerdefinitionen der avr-libc (io*.h) auch für Controller mit zwei UBRR-Registern (UBRRL/UBRRH) ein UBRR bzw. UBRR0 als "16-bit-Register" definiert und man kann den Wert direkt per UBRR = UBRR_VAL zuweisen. Intern werden dann zwei Zuweisungen für UBRRH und UBRRL generiert.
/* UART-Init Bsp. ATmega48 */
void uart_init(void)
{
UBRR0 = UBRR_VAL;
UCSR0B |= (1<<TXEN0);
// Frame Format: Asynchron 8N1
UCSR0C = (1<<UCSZ01)|(1<<UCSZ00);
}
Die einzelne Anweisung ist nicht bei allen Controllern möglich, da die beiden Register nicht bei allen aufeinanderfolgende Addressen aufweisen. Die getrennte Zuweisung an UBRRH und UBRRL wie im nächsten Beispiel gezeigt, ist universeller und portabler und daher vorzuziehen. Wichtig ist, dass UBRRH vor UBRRL geschrieben wird:
/* UART-Init Bsp. ATmega16 */
void uart_init(void)
{
UBRRH = UBRR_VAL >> 8;
UBRRL = UBRR_VAL & 0xFF;
UCSRB |= (1<<TXEN); // UART TX einschalten
UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); // Asynchron 8N1
}
Inzwischen gibt es in der avr-libc Makros für obige Berechnung der UBRR Registerwerte aus Taktrate F_CPU und Baudrate BAUD. Dazu wird die Includedatei <util/setbaud.h> eingebunden, nachdem F_CPU und die gewünschte Baudrate definiert wurden. Einige Beispiele zur Anwendung finden sich in der Dokumentation der avr-libc. Im Quellcode kann dann analog zur oben gezeigten Vorgehensweise einfach das Makro UBRR_VALUE (bzw. UBRRH_VALUE und UBRRL_VALUE) an der entsprechenden Stelle eingesetzt werden. Es wird auch automatisch ermittelt, ob der U2X-Modus (vgl. Datenblatt) zu geringeren Abweichungen führt und dann dem Makro USE_U2X ein Wert ungleich null zugewiesen. Ein Beispiel (angelehnt an die avr-libc-Dokumentation):
#include <avr/io.h>
#define F_CPU 1000000 /* evtl. bereits via Compilerparameter definiert */
#define BAUD 9600
#include <util/setbaud.h>
void uart_init(void)
{
UBRRH = UBRRH_VALUE;
UBRRL = UBRRL_VALUE;
/* evtl. verkuerzt falls Register aufeinanderfolgen (vgl. Datenblatt)
UBRR = UBRR_VALUE;
*/
#if USE_2X
/* U2X-Modus erforderlich */
UCSRA |= (1 << U2X);
#else
/* U2X-Modus nicht erforderlich */
UCSRA &= ~(1 << U2X);
#endif
// hier weitere Initialisierungen (TX und/oder RX aktivieren, Modus setzen
}
Siehe auch:
- Dokumentation der avr-libc zu <util/setbaud.h>
- WormFood's AVR Baud Rate Calculator online.
- AVR Baudraten-Rechner in JavaScript
Senden mit dem UART
Senden einzelner Zeichen
Um nun ein Zeichen auf die Schnittstelle auszugeben, müssen wir dasselbe lediglich in das UART Data Register schreiben. Vorher ist zu prüfen, ob das UART-Modul bereit ist, das zu sendende Zeichen entgegenzunehmen. Die Bezeichnungen des/der Statusregisters mit dem Bit UDRE ist abhängig vom Controllertypen (vgl. Datenblatt).
// bei neueren AVRs steht der Status in UCSRA/UCSR0A/UCSR1A, hier z.B. fuer ATmega16:
while (!(UCSRA & (1<<UDRE))) /* warten bis Senden moeglich */
{
}
UDR = 'x'; /* schreibt das Zeichen x auf die Schnittstelle */
// das entpricht dem "C Code Example USART_Transmit" in der ATMEL-Doku ...
// besser ist es aber, die CPU nicht in einer Warteschleife zu blockieren:
if(UCSRA & (1<<UDRE)) /* Senden, wenn UDR frei ist */
{ UDR = 'x'; /* schreibt das Zeichen x auf die Schnittstelle */
}
Zum "Blockieren" vgl. AVR-GCC-Tutorial
Senden einer Zeichenkette (String)
Die Aufgabe "String senden" wird durch zwei Funktionen abgearbeitet. Die universelle/controllerunabhängige Funktion uart_puts übergibt jeweils ein Zeichen der Zeichenkette an eine Funktion uart_putc, die abhängig von der vorhandenen Hardware implementiert werden muss. In der Funktion zum Senden eines Zeichens ist darauf zu achten, dass vor dem Senden geprüft wird, ob der UART bereit ist den "Sendeauftrag" entgegenzunehmen.
/* ATmega16 */
int uart_putc(unsigned char c)
{
while (!(UCSRA & (1<<UDRE))) /* warten bis Senden moeglich */
{
}
UDR = c; /* sende Zeichen */
return 0;
}
/* puts ist unabhaengig vom Controllertyp */
void uart_puts (char *s)
{
while (*s)
{ /* so lange *s != '\0' also ungleich dem "String-Endezeichen(Terminator)" */
uart_putc(*s);
s++;
}
}
Die in uart_putc verwendeten Schleifen, in denen gewartet wird bis die UART-Hardware zum senden bereit ist, sind insofern etwas kritisch, da während des Sendens eines Strings nicht mehr auf andere Ereignisse reagiert werden kann. Universeller ist die Nutzung von FIFO(first-in first-out)-Puffern, in denen die zu sendenden bzw. empfangenen Zeichen/Bytes zwischengespeichert und in Interruptroutinen an die U(S)ART-Hardware weitergegeben bzw. von ihr ausgelesen werden. Dazu existieren fertige Komponenten (Bibliotheken, Libraries), die man recht einfach in eigene Entwicklungen integrieren kann. Es empfiehlt sich, diese Komponenten zu nutzen und das Rad nicht neu zu erfinden.
Senden von Variableninhalten
Sollen Inhalte von Variablen (Ganzzahlen, Gleitkomma) in "menschenlesbarer" Form gesendet werden, ist vor dem Transfer eine Umwandlung in Zeichen ("ASCII") erforderlich. Bei nur einer Ziffer ist diese Umwandlung relativ einfach: man addiert den ASCII-Wert von Null zur Ziffer und kann diesen Wert direkt senden.
#include <avr/io.h>
#include <stdlib.h>
//...
// hier uart_putc (s.o.)
int main (void)
{
// Ausgabe von 0123456789
char c;
uart_init();
for (uint8_t i=0; i<=9; ++i) {
c = i + '0';
uart_putc( c );
// verkuerzt: uart_putc( i + '0' );
}
while (1) {
}
return 0; // never reached
}
Soll mehr als eine Ziffer ausgegeben werden, bedient man sich zweckmäßigerweise vorhandener Funktionen zur Umwandlung von Zahlen in Zeichenketten/Strings. Die Funktion der avr-libc zur Umwandlung von vorzeichenbehafteten 16bit-Ganzzahlen (int16_t) in Zeichenketten heißt itoa (Integer to ASCII). Man muss der Funktion einen Speicherbereich zur Verarbeitung (buffer) mit Platz für alle Ziffern, das String-Endezeichen ('\0') und evtl. das Vorzeichen bereitstellen.
#include <avr/io.h>
#include <stdlib.h>
//...
// hier uart_init, uart_putc, uart_puts (s.o.)
int main(void)
{
char s[7];
int16_t i = -12345;
uart_init();
itoa( i, s, 10 ); // 10 fuer radix -> Dezimalsystem
uart_puts( s );
// da itoa einen Zeiger auf den Beginn von s zurueckgibt verkuerzt auch:
uart_puts( itoa( i, s, 10 ) );
while (1) {
;
}
return 0; // never reached
}
Für vorzeichenlose 16bit-Ganzzahlen (uint16_t) exisitert utoa. Die Funktionen für 32bit-Ganzzahlen (int32_t und uint32_t) heißen ltoa bzw. ultoa. Da 32bit-Ganzzahlen mehr Stellen aufweisen können, ist ein entsprechend größerer Pufferspeicher vorzusehen.
Auch Gleitkommazahlen (float/double) können mit bereits vorhandenen Funktionen in Zeichenfolgen umgewandelt werden, dazu existieren die Funktionen dtostre und dtostrf. dtostre nutzt Exponentialschreibweise ("engineering"-Format). (Hinweis: z.Zt. existiert im avr-gcc kein "echtes" double, intern wird immer mit "einfacher Genauigkeit", entsprechend float, gerechnet.)
dtostrf und dtostre benötigen die libm.a der avr-libc. Bei Nutzung von Makefiles ist der Parameter -lm in in LDFLAGS anzugeben (Standard in den WinAVR/mfile-Makefilevorlagen). Nutzt man AVRStudio als IDE für den GNU-Compiler (gcc-Plugin) ist die libm.a unter Libaries auszuwählen: Project -> Configurations Options -> Libaries -> libm.a mit dem Pfeil nach rechts einbinden. Siehe auch die FAQ
#include <avr/io.h>
#include <stdlib.h>
//...
// hier uart_init, uart_putc, uart_puts (s.o.)
/* lt. avr-libc Dokumentation:
char* dtostrf(
double __val,
char __width,
char __prec,
char * __s
)
*/
int main(void)
{
// Pufferspeicher ausreichend groß
// evtl. Vorzeichen + width + Endezeichen:
char s[8];
float f = -12.345;
uart_init();
dtostrf( f, 6, 3, s );
uart_puts( s );
// verkürzt: uart_puts( dtostrf( f, 7, 3, s ) );
while (1) {
;
}
return 0; // never reached
}
Empfangen
Einzelne Zeichen empfangen
Zum Empfang von Zeichen muss der Empfangsteil des UART bei der Initialisierung aktiviert werden, indem das RXEN-Bit im jeweiligen Konfigurationsregister (UCSRB bzw UCSR0B/UCSR1B) gesetzt wird. Im einfachsten Fall wird solange gewartet, bis ein Zeichen empfangen wurde, dieses steht dann im UART-Datenregister (UDR bzw. UDR0 und UDR1 bei AVRs mit 2 UARTS) zur Verfügung (sogen. "Polling-Betrieb"). Ein Beispiel für den ATmega16:
#include <inttypes.h>
#include <avr/io.h>
/* Siehe auch obere Baudrateneinstellung */
/* USART-Init beim ATmega16 */
void uart_init(void)
{
UBRRH = UBRR_VAL >> 8;
UBRRL = UBRR_VAL & 0xFF;
UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); // Asynchron 8N1
UCSRB |= (1<<RXEN); // UART RX einschalten
}
/* Zeichen empfangen */
uint8_t uart_getc(void)
{
while (!(UCSRA & (1<<RXC))) // warten bis Zeichen verfuegbar
;
return UDR; // Zeichen aus UDR an Aufrufer zurueckgeben
}
Und die Anwendung in einem Beispiel:
#include <avr/io.h>
#include <stdlib.h>
// hier Makro für die Baudratenberechnung
// hier uart_init, uart_getc (s.o.)
int main(void)
{
uart_init();
while (1)
{
uint8_t c;
c = uart_getc();
// hier etwas mit c machen z.B. auf PORT ausgeben
DDRC = 0xFF; // PORTC Ausgang
PORTC = c;
}
return 0; // never reached
}
Die Funktion uart_getc() blockiert allerdings den Programmablauf, denn es wird gewartet, bis ein Zeichen empfangen wird! Möchte man das Warten vermeiden, kann das RXC-Bit in einer Programmschleife abgefragt werden und dann nur bei gesetztem RXC-Bit UDR ausgelesen werden.
#include <avr/io.h>
#include <stdlib.h>
// hier Makro für die Baudratenberechnung
// hier uart_init, uart_getc (s.o.)
int main(void)
{
uart_init();
while (1)
{
if ( (UCSRA & (1<<RXC)) )
{
// Zeichen wurde empfangen, jetzt abholen
uint8_t c;
c = uart_getc();
// hier etwas mit c machen z.B. auf PORT ausgeben
DDRC = 0xFF; // PORTC Ausgang
PORTC = c;
}
else
{
// Kein Zeichen empfangen, Restprogramm ausführen...
}
}
return 0; // never reached
}
Eleganter und in den meisten Anwendungsfällen "stabiler" ist die Vorgehensweise, die empfangenen Zeichen in einer Interrupt-Routine einzulesen und zur späteren Verarbeitung in einem Eingangsbuffer (FIFO-Buffer) zwischenzuspeichern. Dazu existieren fertige und gut getestete Bibliotheken und Quellcodekomponenten (z. B. UART-Library von P. Fleury, procyon-avrlib und einige in der "Academy" von avrfreaks.net).
Siehe auch:
- Dokumenation der avr-libc/stdlib.h
- Die Nutzung von sprintf und printf
- Peter Fleurys UART-Bibiliothek fuer avr-gcc/avr-libc
TODO: 9bit
Empfang von Zeichenketten (Strings)
Beim Empfang von Zeichenketten, muß man sich zunächst darüber im klaren sein, daß es ein Kriterium geben muß, an dem der µC erkennen kann, wann ein Text zu Ende ist. Sehr oft wird dazu das Zeichen 'Return' benutzt, um das Ende eines Textes zu markieren. Dies ist vom Benutzer einfach eingebbar und er ist auch daran gewöhnt, daß er eine Eingabezeile mit einem Druck auf die Return Taste abgeschlossen wird.
Prinzipiell gibt es jedoch keine Einschränkung bezüglich dieses speziellen Zeichens. Es muß nur sichergestellt werden, daß dieses spezielle 'Ende eines Strings' - Zeichen nicht mit einem im Text vorkommenden Zeichen verwechselt werden kann. Wenn also im zu übertragenden Text beispielsweise kein ';' vorkommt, dann spricht nichts dagegen, den Benutzer die Eingabe eines Textes mit einem ';' abschließen zu lassen.
Im Folgenden wird die durchaus übliche Annahme getroffen, daß eine Stringübertragung identisch ist mit der Übertragung einer Textzeile und daher mit einem Return ('\n') abgeschlossen wird.
Das Problem der Übertragung eines Strings reduziert sich damit auf die Aufgabenstellung: Empfange und sammle Zeichen in einem char Array, bis entweder das Array voll ist oder das Text Ende Zeichen' empfangen wurde. Danach wird der empfangene Text noch mit einem '\0' Zeichen abgeschlossen um einen Standard C-String daraus zu machen, mit dem dann weiter gearbeitet werden kann.
/* Zeichen empfangen */
uint8_t uart_getc(void)
{
while (!(UCSRA & (1<<RXC))) // warten bis Zeichen verfuegbar
;
return UDR; // Zeichen aus UDR an Aufrufer zurueckgeben
}
void uart_gets( char* Buffer, uint8_t MaxLen )
{
uint8_t NextChar;
uint8_t StringLen = 0;
NextChar = uart_getc(); // Warte auf und empfange das nächste Zeichen
// Sammle solange Zeichen, bis:
// * entweder das String Ende Zeichen kam
// * oder das aufnehmende Array voll ist
while( NextChar != '\n' && StringLen < MaxLen - 1 ) {
*Buffer++ = NextChar;
StringLen++;
NextChar = uart_getc();
}
// Noch ein '\0' anhängen um einen Standard
// C-String daraus zu machen
*Buffer = '\0';
}
Beim Aufruf ist darauf zu achten, dass das empfangende Array auch mit einer vernünftigen Größe definiert wird.
char Line[40]; // String mit maximal 39 zeichen
uart_gets( Line, sizeof( Line ) );
Bei der Benutzung von sizeof() ist allerdings zu beachten, dass sizeof() nicht die Anzahl der Elemente des Arrays liefert, sondern die Länge in Byte. Da ein char nur ein Byte lang ist, passt der Aufruf 'uart_gets(Line, sizeof( Line ) );' in diesem Fall. Falls man - aus welchen Gründen auch immer - andere Datentypen benutzen möchte, sollte man zur korrekten Angabe der Array-Länge folgende Vorgehensweise bevorzugen:
int Line[40]; // Array vom Typ int
uart_gets( Line, sizeof( Line ) / sizeof( Line[0] ) );
Interruptbetrieb
Hier wird das Grundwissen des Artikels Interrupt und des Abschnitts AVR-GCC-Tutorial: Programmieren_mit_Interrupts vorausgesetzt.
Empfangen (RX)
Beim ATmega8 muss das RXCIE Bit im Register UCSRB gesetzt werden, damit ein Interrupt beim Empfang eines Zeichens ausgelöst werden kann.
/* siehe auch obere Baudrateneinstellung */
/* USART-Init beim ATmega16 */
void uart_init(void)
{
UBRRH = UBRR_VAL >> 8;
UBRRL = UBRR_VAL & 0xFF;
UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); // Asynchron 8N1
UCSRB |= (1<<RXEN)|(1<<TXEN)|(1<<RXCIE); // UART RX, TX und RX Interrupt einschalten
}
Natürlich muss "Global Interrupt Enable" mittels des Befehls sei() aktiviert sein. Interrupt-spezifische Definitionen werden über die Includedatei eingebunden:
#include <avr/interrupt.h>
Der Interrupt wird immer ausgelöst, wenn ein Zeichen erfolgreich empfangen wurde. Zusätzlich braucht man die Interruptserviceroutine (ISR).
In diesem Beispiel enthält die ISR einen FIFO-Puffer (First in, First out). Dafür werden ein paar globale Variablen und Makros benötigt:
#define UART_MAXSTRLEN 10
volatile uint8_t uart_str_complete = 0; // 1 .. String komplett empfangen
volatile uint8_t uart_str_count = 0;
volatile char uart_string[UART_MAXSTRLEN + 1] = "";
ISR(USART_RXC_vect)
{
unsigned char nextChar;
// Daten aus dem Puffer lesen
nextChar = UDR;
if( uart_str_complete == 0 ) { // wenn uart_string gerade in Verwendung, neues Zeichen verwerfen
// Daten werden erst in uart_string geschrieben, wenn nicht String-Ende/max Zeichenlänge erreicht ist/string gerade verarbeitet wird
if( nextChar != '\n' &&
nextChar != '\r' &&
uart_str_count < UART_MAXSTRLEN ) {
uart_string[uart_str_count] = nextChar;
uart_str_count++;
}
else {
uart_string[uart_str_count] = '\0';
uart_str_count = 0;
uart_str_complete = 1;
}
}
}
Zur Funktion: Wurde eine komplette Zeichenkette empfangen, also das Ende (\n oder \r) erkannt oder die maximale Länge UART_MAXSTRLEN erreicht, wird die globale Variable uart_str_complete auf '1' gesetzt. Damit wird dem Hauptprogramm, welches auf diese Variable pollt, mitgeteilt, dass die Zeichenkette uart_string zur Verarbeitung bereit steht. Nach der Verarbeitung der Zeichenkette in der entsprechenden main-Routine, muss die Variable uart_str_complete wieder auf '0' zurück gesetzt werden. Dadurch werden alle neu empfangenen Zeichen wieder in den globalen Puffer geschrieben.
(Baustelle)
- Empfangen (Receive) (Anm.: z.T. erledigt)
- ggf. Fallstricke (UDR in der ISR lesen!)
- Komplettes, einfaches Beispiel (Echo (noch buggy beim Datenzugriff, siehe Lit. 2+3!)), ggf. LED zur ISR-Empfangsanzeige oder Overflow-Anzeige
- Senden (Transmit)
- Variante "UART Data Register Empty" (UDRE) [2]
- Variante "UART Transmit Complete" (TXC)
- FIFO-Puffer [3], Ringpuffer (Byte Buffering (circular))
- UART-Bibliotheken
- UART-Library von Peter Fleury (UART (interrupt driven), Byte Buffering (circular))
- Updated AVR UART Library (modernisierte und erweiterte Version der Bibliothek von Peter Fleury)
- Procyon AVRlib von Pascal Stang (UART (interrupt driven), Byte Buffering (circular), VT100 Terminal Output)
- uart_driver.c Beispieltreiber in C mit minimalstem Code fuer ATMega8. Sendet und empfaengt Strings ueber Interrupts (von Markus Zimmermann)
- uartavr BSD-3-Clause Library von Christian Rapp für ATmega328. Interrupt driven und Byte Buffering (circular), API Dokumentation
- Literatur
- avrfreaks.net Tutorial inkl. Diskussion (engl.)
- avr-libc FAQ: Why do some 16-bit timer registers sometimes get trashed?
Software-UART
Falls die Zahl der vorhandenen Hardware-UARTs nicht ausreicht, können weitere Schnittstellen über sogennante Software-UARTs ergänzt werden. Es gibt dazu (mindestens) zwei Ansätze:
- Der bei AVRs üblichste Ansatz basiert auf dem Prinzip, dass ein externer Interrupt-Pin für den Empfang ("RX") genutzt wird. Das Startbit löst den Interrupt aus, in der Interrupt-Routine (ISR) wird der externe Interrupt deaktiviert und ein Timer aktiviert. In der Interrupt-Routine des Timers wird der Zustand des Empfangs-Pins entsprechend der Baudrate abgetastet. Nach Empfang des Stop-Bits wird der externe Interrupt wieder aktiviert. Senden kann über einen beliebigen Pin ("TX") erfolgen, der entsprechend der Baudrate und dem zu sendenden Zeichen auf 0 oder 1 gesetzt wird. Die Implementierung ist nicht ganz einfach, es existieren dazu aber fertige Bibliotheken (z. B. bei avrfreaks oder in der Procyon avrlib).
- Ein weiterer Ansatz erfordert keinen Pin mit "Interrupt-Funktion" aber benötigt mehr Rechenzeit. Jeder Input-Pin kann als Empfangspin (RX) dienen. Über einen Timer wird der Zustand des RX-Pins mit einem vielfachen der Baudrate abgetastet (dreifach scheint üblich) und High- bzw. Lowbits anhand einer Mindestanzahl identifiziert. (Beispiel: "Generic Software Uart" Application-Note von IAR)
Neuere AVRs (z. B. ATtiny26 oder ATmega48,88,168,169) verfügen über ein Universal Serial Interface (USI), das teilweise UART-Funktion übernehmen kann. Atmel stellt eine Application-Note bereit, in der die Nutzung des USI als UART erläutert wird (im Prinzip "Hardware-unterstützter Software-UART").
Handshaking
Wenn der Sender ständig sendet, wird irgendwann der Fall eintreten, daß der Empfänger nicht bereit ist, neue Zeichen zu empfangen. In diesem Fall muß durch ein Handshake-Verfahren die Situation bereinigt werden. Handshake bedeutet nichts anderes, als daß der Empfänger dem Sender mitteilt, daß er zur Zeit keine Daten annehmen kann und der Sender die Übertragung der nächsten Zeichen solange einstellen soll, bis der Empfänger signalisiert, daß er wieder Zeichen aufnehmen kann.
Hardwarehandshake (RTS/CTS)
Beim Hardwarehandshake werden zusätzlich zu den beiden Daten-Übertragungsleitungen noch 2 weitere Leitungen benötigt: RTS (Request To Send) und CTS (Clear To Send). Jeder der beiden Kommunikationspartner ist verpflichtet, bevor ein Zeichen gesendet wird, den Zustand der RTS Leitung zu überprüfen. Nur wenn die Gegenstelle darauf Empfangsbereitschaft signalisiert, darf das Zeichen gesendet werden. Um der Gegenstelle zu signalisieren, daß sie zur Zeit keine Zeichen schicken soll, wird die Leitung CTS benutzt.
Softwarehandshake (XON/XOFF)
Beim Softwarehandshake sind keine speziellen Leitungen notwendig. Stattdessen werden besondere ASCII-Zeichen benutzt, die der Gegenstelle signalisieren, das Senden einzustellen bzw. wieder aufzunehmen.
- XOFF Aufforderung das Senden einzustellen
- XON Gegenstelle darf wieder senden
Nachteilig bei einem Softwarehandshake ist, dass dadurch keine direkte binäre Datenübertragung mehr möglich ist, denn von den möglichen 256 Bytewerten werden ja nun zwei (nämlich für XON und für XOFF) für besondere Zwecke benutzt und fallen daher aus. In einem quasi-zufälligen Bytestrom, den eine Binärdatei darstellt, kämen diese beiden reservierten früher oder später vor und würden dann den Strom ungewollt stoppen und starten.
Galvanische Trennung
Für eine hohe Überspannungsfestigkeit empfielt es sich, die Datenkanäle über Optokoppler zu führen. Es bietet sich z.b. der 6N138 an, ein "normaler" CNY-17 ist für hohe Baudraten nicht brauchbar.
Fehlersuche
Erstaunlich oft wird im Forum der Hilferuf laut: "Meine UART funktioniert nicht, was mache ich falsch". In der überwiegenden Mehrzahl der Fälle stellt sich dann heraus, daß es sich um ein Hardwareproblem handelt, wobei da wiederrum der Löwenanteil auf das Konto einer nicht korrekt eingestellten Taktrate geht: Der µC benutzt nicht einen angeschlossenen Quarz, so wie er auch im Programm eingetragen ist, sondern läuft immer noch mit dem internen RC-Takt. Daraus resultiert aber auch, daß der Baudraten Konfigurationswert falsch berechnet wird.
Hilfreich zum Aufspüren solcher Fehler ist auch die AVR-Checkliste.
Links
Tipps zur Verarbeitung von Strings sind in den FAQ.