www.mikrocontroller.net

AVR-Tutorial: UART

Wie viele andere Controller besitzen die meisten AVRs einen UART (Universal Asynchronous Receiver and Transmitter). Das ist eine serielle Schnittstelle, die meistens zur Datenübertragung zwischen Mikrocontroller und PC genutzt wird. Zur Übertragung werden zwei Pins am Controller benötigt: TXD und RXD. Über TXD ("Transmit Data") werden Daten gesendet, RXD ("Receive Data") dient zum Empfang von Daten.

Inhaltsverzeichnis

[bearbeiten] Hardware

Um den UART des Mikrocontrollers zu verwenden, muss der Versuchsaufbau um folgende Bauteile erweitert werden:

Auf dem Board vom Shop sind diese Bauteile bereits enthalten, man muss nur noch die Verbindungen zwischen MAX232 und AVR herstellen wie im Bild zu sehen.

  • Der MAX232 ist ein Pegelwandler, der die -12V/+12V Signale an der seriellen Schnittstelle des PCs zu den 5V/0V des AVRs kompatibel macht.
  • C1 ist ein kleiner Keramikkondensator, wie er immer wieder zur Entkopplung der Versorgungsspannungen an digitalen ICs verwendet wird.
  • Die vier Kondensatoren C2..C5 sind Elektrolytkondensatoren (Elkos). Auf die richtige Polung achten! Minus ist der Strich auf dem Gehäuse. Der exakte Wert ist hier relativ unkritisch, in der Praxis sollte alles von ca. 1µF bis 47µF mit einer Spannungsfestigkeit von 16V und höher funktionieren.
  • X1 ist ein weiblicher 9-poliger SUB-D-Verbinder.
  • Die Verbindung zwischen PC und Mikrocontroller erfolgt über ein 9-poliges Modem-Kabel (nicht Nullmodem-Kabel!), das an den seriellen Port des PCs angeschlossen wird. Bei einem Modem-Kabel sind die Pins 2 und 3 des einen Kabelendes mit den Pins 2 und 3 des anderen Kabelendes durchverbunden. Bei einem Nullmodem Kabel sind die Leitungen gekreuzt, sodass Pin 2 von der einen Seite mit Pin 3 auf der anderen Seite verbunden ist und umgekehrt.
  • Als Faustregel kann man annehmen: Befinden sich an den beiden Enden des Kabels die gleiche Art von Anschlüssen (Männchen = Stecker; Weibchen = Buchse), dann benötigt man ein gekreuztes Kabel, also ein Nullmodem-Kabel. Am PC-Anschluss selbst befindet sich ein Stecker, also ein Männchen, sodaß am Kabel auf dieser Seite eine Buchse (also ein Weibchen) sitzen muss. Da am AVR laut obigem Schaltbild eine Buchse (also ein Weibchen) verbaut wird, muss daher an diesem Ende des Kabels ein Stecker sitzen. Das Kabel hat daher an einem Ende einen Stecker und am anderen Ende eine Buchse und ist daher ein normales Modem-Kabel ( = nicht gekreuzt).

Bild:USART_Kabel.gif

[bearbeiten] Software

[bearbeiten] UART konfigurieren

Als erstes muss die gewünschte Baudrate im Register UBRR festgelegt werden. Der in dieses Register zu schreibende Wert errechnet sich nach der folgenden Formel:

UBRR = \frac {Taktfrequenz} { 16 \cdot Baudrate } - 1

Beim AT90S4433 kann man den Wert direkt in das Register UBRR laden, beim ATmega8 gibt es für UBRR zwei Register: UBRRL (Low-Byte) und UBRRH (High-Byte). Im Normalfall steht in UBRRH 0, da der berechnete Wert kleiner als 256 ist und somit in UBRRL alleine passt. Beachtet werden muss, dass das Register UBRRH vor dem Register UBRRL beschrieben werden muss. Der Schreibzugriff auf UBRRL löst das Neusetzen des internen Taktteilers aus.


WICHTIGER HINWEIS!

Auf Grund permanent wiederkehrender Nachfrage sei hier AUSDRÜCKLICH darauf hingewiesen, dass bei Verwendung des UART im asynchronen Modus dringend ein Quarz oder Ouarzoszillator verwendet werden sollte! Der interne RC-Oszillator der AVRs ist recht ungenau! Damit kann es in Ausnahmefällen funktionieren, muss es aber nicht! Auch ist der interne Oszillator temperaturempfindlich. Damit hat man dann den schönen Effekt, dass eine UART-Schaltung die im Winter noch funktionierte, im Sommer den Dienst verweigert.

Außerdem muss bei der Berechnung von UBRR geprüft werden, ob mit der verwendeten Taktfrequenz die gewünschte Baudrate mit einem Fehler von <1% generiert werden kann. Das Datenblatt bietet hier sowohl die Formel als auch Tabellen unter der Überschrift des U(S)ART an.

 Fehler_{Baudrate}[%] = (\frac{UBRR_{gerundet}+1}{UBRR_{genau}+1} -1 ) \cdot 100

Siehe auch Baudratenquarz

Wer es ganz einfach haben will, nimmt die folgenden Macros. Die rechnen sogar den Fehler aus und brechen die Assemblierung ggf. ab. Das ist dann praktisch idiotensicher.

.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))     ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif

Wer dennoch den internen RC-Oszillator verwenden will, muss diesen kalibrieren. Näheres findet man dazu im Datenblatt, Stichwort Register OSCCAL.


Um den Sendekanal des UART zu aktivieren, muss das Bit TXEN im UART Control Register UCSRB auf 1 gesetzt werden.

Danach kann das zu sendende Byte in das Register UDR eingeschrieben werden - vorher muss jedoch sichergestellt werden, dass das Register leer ist, die vorhergehende Übertragung also schon abgeschlossen wurde. Dazu wird getestet, ob das Bit UDRE ("UART Data Register Empty") im Register UCSRA auf 1 ist.

Genaueres über die UART-Register findet man im Datenblatt des Controllers.

Der ATmega8 bietet noch viele weitere Optionen zur Konfiguration des UARTs, aber für die Datenübertragung zum PC sind im Normalfall keine anderen Einstellungen notwendig.

[bearbeiten] Senden von Zeichen

Das Beispielprogramm überträgt die Zeichenkette "Test!" in einer Endlosschleife an den PC. Die folgenden Beispiele sind für den ATmega8 geschrieben.

.include "m8def.inc"
 
.def temp    = r16                              ; Register für kleinere Arbeiten
.def zeichen = r17                              ; in diesem Register wird das Zeichen an die
                                                ; Ausgabefunktion übergeben
 
.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))      ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif
 
    ; Stackpointer initialisieren
 
    ldi     temp, LOW(RAMEND)
    out     SPL, temp
    ldi     temp, HIGH(RAMEND)
    out     SPH, temp
 
    ; Baudrate einstellen
 
    ldi     temp, HIGH(UBRR_VAL)
    out     UBRRH, temp
    ldi     temp, LOW(UBRR_VAL)
    out     UBRRL, temp
 
    ; Frame-Format: 8 Bit
 
    ldi     temp, (1<<URSEL)|(3<<UCSZ0)
    out     UCSRC, temp
 
    sbi     UCSRB,TXEN                  ; TX aktivieren
 
loop:
    ldi     zeichen, 'T'
    rcall   serout                      ; Unterprogramm aufrufen
    ldi     zeichen, 'e'
    rcall   serout                      ; Unterprogramm aufrufen
    ldi     zeichen, 's'
    rcall   serout                      ; ...
    ldi     zeichen, 't'
    rcall   serout
    ldi     zeichen, '!'
    rcall   serout
    ldi     zeichen, 10
    rcall   serout
    ldi     zeichen, 13
    rcall   serout
    rjmp    loop
 
serout:
    sbis    UCSRA,UDRE                  ; Warten bis UDR für das nächste
                                        ; Byte bereit ist
    rjmp    serout
    out     UDR, zeichen
    ret                                 ; zurück zum Hauptprogramm 

Der Befehl rcall serout ruft ein kleines Unterprogramm auf, das zuerst wartet bis das Datenregister UDR von der vorhergehenden Übertragung frei ist, und anschließend das in zeichen (=r17) gespeicherte Byte an UDR ausgibt.

Bevor serout aufgerufen wird, wird zeichen jedesmal mit dem ASCII-Code des zu übertragenden Zeichens geladen (so wie in Teil 4 bei der LCD-Ansteuerung). Der Assembler wandelt Zeichen in einfachen Anführungsstrichen automatisch in den entsprechenden ASCII-Wert um. Nach dem Wort "Test!" werden noch die Codes 10 (New Line) und 13 (Carriage Return) gesendet, um dem Terminalprogramm mitzuteilen, dass eine neue Zeile beginnt.

Eine Übersicht aller ASCII-Codes gibt es auf www.asciitable.com.

Die Berechnung der Baudrate wird übrigens nicht im Controller während der Programmausführung durchgeführt, sondern schon beim Assemblieren, wie man beim Betrachten der Listingdatei feststellen kann.

Zum Empfang muss auf dem PC ein Terminal-Programm wie z.B. HyperTerminal gestartet werden. Der folgende Screenshot zeigt, welche Einstellungen im Programm vorgenommen werden müssen:

hyperterminal.gif

Linux-Benutzer können das entsprechende Device (z.B. /dev/ttyS0) mit stty konfigurieren und mit cat die empfangenen Daten anzeigen oder ein Terminalprogramm wie minicom nutzen.

[bearbeiten] Senden von Zeichenketten

Eine bequemere Methode um längere Zeichenketten (Strings) zu übertragen ist hier zu sehen. Dabei werden die Zeichenketten im Flash gespeichert. Als Abschluss des Strings wird der Wert 0x00 genutzt, so wie auch in der Programmiersprache C.

.include "m8def.inc"
 
.def temp    = r16                              ; Register für kleinere Arbeiten
.def zeichen = r17                              ; in diesem Register wird das Zeichen an die
                                                ; Ausgabefunktion übergeben
 
.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))     ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif
 
; hier geht unser Programm los
 
    ; Stackpointer initialisieren
 
    ldi     temp, LOW(RAMEND)
    out     SPL, temp
    ldi     temp, HIGH(RAMEND)
    out     SPH, temp
 
    ; Baudrate einstellen
 
    ldi     temp, HIGH(UBRR_VAL)
    out     UBRRH, temp
    ldi     temp, LOW(UBRR_VAL)
    out     UBRRL, temp
 
    ; Frame-Format: 8 Bit
 
    ldi     temp, (1<<URSEL)|(3<<UCSZ0)
    out     UCSRC, temp
 
    sbi     UCSRB,TXEN                      ; TX aktivieren
 
loop:
    ldi     zl,low(my_string*2);            ; Z Pointer laden
    ldi     zh,high(my_string*2);
    rcall   serout_string
    rjmp    loop
 
; Ausgabe eines Strings aus dem Flash
 
serout_string:
    lpm                             ; nächstes Byte aus dem Flash laden
    and     r0,r0                   ; = Null? 
    breq    serout_string_ende      ; wenn ja, -> Ende
serout_string_wait:
    sbis    UCSRA,UDRE              ; Warten bis UDR für das nächste
                                    ; Byte bereit ist
    rjmp    serout_string_wait
    out     UDR, r0
    adiw    zl:zh,1                 ; Zeiger erhöhen
    rjmp    serout_string           ; nächstes Zeichen bearbeiten
serout_string_ende:
    ret                             ; zurück zum Hauptprogramm
 
; Hier wird jetzt der String definiert und im Flash gespeichert
 
my_string:  .db "Test!",10,13,0

[bearbeiten] Empfangen von Zeichen per Polling

Der AVR kann nicht nur Daten seriell senden, sondern auch empfangen. Dazu muss man, nachdem die Baudrate wie oben beschrieben eingestellt wurde, das Bit RXEN setzen.

Sobald der UART ein Byte über die serielle Verbindung empfangen hat, wird das Bit RXC im Register UCSRA gesetzt, um anzuzeigen, dass ein Byte im Register UDR zur Weiterverarbeitung bereitsteht. Sobald es aus UDR gelesen wurde, wird RXC automatisch wieder gelöscht, bis das nächste Byte angekommen ist.

Das erste einfache Testprogramm soll das empfangene Byte auf den an Port D angeschlossenen LEDs ausgeben. Dabei sollte man daran denken, dass PD0 (RXD) bereits für die Datenübertragung zuständig ist, so dass das entsprechende Bit im Register PORTD keine Funktion hat und damit auch nicht für die Datenanzeige verwendet werden kann.

Nachdem der UART konfiguriert ist, wartet das Programm einfach in der Hauptschleife darauf, dass ein Byte über den UART ankommt (z.B. indem man im Terminalprogramm ein Zeichen eingibt), also RXC gesetzt wird. Sobald das passiert, wird das Register UDR, in dem die empfangenen Daten stehen, nach temp eingelesen und an den Port D ausgegeben.

 
.include "m8def.inc"
 
.def temp = R16
 
.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))     ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif
 
    ; Stackpointer initialisieren
 
    ldi     temp, LOW(RAMEND)
    out     SPL, temp
    ldi     temp, HIGH(RAMEND)
    out     SPH, temp
 
    ; Port D = Ausgang
 
    ldi     temp, 0xFF
    out     DDRD, temp
 
    ; Baudrate einstellen
 
    ldi     temp, HIGH(UBRR_VAL)
    out     UBRRH, temp
    ldi     temp, LOW(UBRR_VAL)
    out     UBRRL, temp
 
    ; Frame-Format: 8 Bit
 
    ldi     temp, (1<<URSEL)|(3<<UCSZ0)
    out     UCSRC, temp
 
    sbi     UCSRB, RXEN                     ; RX (Empfang) aktivieren
 
receive_loop:
   sbis     UCSRA, RXC                      ; warten bis ein Byte angekommen ist
   rjmp     receive_loop
   in       temp, UDR                       ; empfangenes Byte nach temp kopieren
   out      PORTD, temp                     ; und an Port D ausgeben.
   rjmp     receive_loop                    ; zurück zum Hauptprogramm 

[bearbeiten] Empfangen von Zeichen per Interrupt

Dieses Programm lässt sich allerdings noch verfeinern. Statt in der Hauptschleife auf die Daten zu warten, kann man auch veranlassen dass ein Interrupt ausgelöst wird, sobald ein Byte angekommen ist. Das sieht in der einfachsten Form so aus:

 
.include "m8def.inc"
 
.def temp = R16
 
.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))     ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif
 
.org 0x00
        rjmp main
 
.org URXCaddr                                   ; Interruptvektor für UART-Empfang
        rjmp int_rxc
 
; Hauptprogramm
 
main:
 
    ; Stackpointer initialisieren
 
    ldi     temp, LOW(RAMEND)
    out     SPL, temp
    ldi     temp, HIGH(RAMEND)
    out     SPH, temp
 
    ; Port D = Ausgang
 
    ldi     temp, 0xFF
    out     DDRD, temp
 
    ; Baudrate einstellen
 
    ldi     temp, HIGH(UBRR_VAL)
    out     UBRRH, temp
    ldi     temp, LOW(UBRR_VAL)
    out     UBRRL, temp
 
    ; Frame-Format: 8 Bit
 
    ldi     temp, (1<<URSEL)|(3<<UCSZ0)
    out     UCSRC, temp
 
    sbi     UCSRB, RXCIE                    ; Interrupt bei Empfang
    sbi     UCSRB, RXEN                     ; RX (Empfang) aktivieren
    
    sei                                     ; Interrupts global aktivieren
    
loop:
    rjmp loop                               ; Endlosschleife
 
; Interruptroutine: wird ausgeführt sobald ein Byte über das UART empfangen wurde
 
int_rxc:
    push    temp                            ; temp auf dem Stack sichern
    in      temp, UDR                       ; epfangendes Byte lesen,
                                            ; dadurch wird auch der Interrupt gelöscht
    out     PORTD, temp                     ; Daten ausgeben
    pop     temp                            ; temp wiederherstellen
    reti                                    ; Interrupt beenden 

Diese Methode hat den großen Vorteil, dass das Hauptprogramm (hier nur eine leere Endlosschleife) andere Dinge erledigen kann, während der Controller Daten empfängt. Auf diese Weise kann man mehrere Aktionen quasi gleichzeitig ausführen, da das Hauptprogramm nur kurz unterbrochen wird, um die empfangenen Daten zu verarbeiten.

Probleme können allerdings auftreten, wenn in der Interruptroutine die gleichen Register verwendet werden wie im Hauptprogramm, da dieses ja an beliebigen Stellen durch den Interrupt unterbrochen werden kann. Damit sich aus der Sicht der Hauptschleife durch den Interruptaufruf nichts ändert, müssen alle in der Interruptroutine geänderten Register am Anfang der Routine gesichert und am Ende wiederhergestellt werden. Das gilt vor allem für das CPU-Statusregister (SREG)! Sobald ein einziger Befehl im Interrupt ein einziges Bit im SREG beeinflusst, muss das SREG gesichert werden. Das ist praktisch fast immer der Fall, nur in dem ganz einfachen Beispiel oben ist es überflüssig, weil die verwendeten Befehle das SREG nicht beeinflussen. In diesem Zusammenhang wird der Stack wieder interessant. Um die Register zu sichern, kann man sie mit push oben auf den Stapel legen und am Ende wieder in der umgekehrten Reihenfolge(!) mit pop vom Stapel herunternehmen.

Im folgenden Beispielprogramm werden die empfangenen Daten nun nicht mehr komplett angezeigt. Stattdessen kann man durch Eingabe einer 1 oder einer 0 im Terminalprogramm eine LED (an PB0) an- oder ausschalten. Dazu wird das empfangene Byte in der Interruptroutine mit den entsprechenden ASCII-Codes der Zeichen 1 und 0 (siehe www.asciitable.com) verglichen.

Für den Vergleich eines Registers mit einer Konstanten gibt es den Befehl cpi register, konstante. Das Ergebnis dieses Vergleichs kann man mit den Befehlen breq label (springe zu label, wenn gleich) und brne label (springe zu label, wenn ungleich) auswerten.

 
.include "m8def.inc"
 
.def temp = R16
 
.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))     ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif
 
.org 0x00
        rjmp main
 
.org URXCaddr
        rjmp int_rxc
 
; Hauptprogramm
main:
    
    ; Stackpointer initialisieren
 
    ldi     temp, LOW(RAMEND)
    out     SPL, temp
    ldi     temp, HIGH(RAMEND)
    out     SPH, temp
 
    ; Port B = Ausgang
 
    ldi     temp, 0xFF
    out     DDRB, temp
 
    ; Baudrate einstellen
 
    ldi     temp, HIGH(UBRR_VAL)
    out     UBRRH, temp
    ldi     temp, LOW(UBRR_VAL)
    out     UBRRL, temp
 
    ; Frame-Format: 8 Bit
 
    ldi     temp, (1<<URSEL)|(3<<UCSZ0)
    out     UCSRC, temp
 
    sbi     UCSRB, RXCIE                ; Interrupt bei Empfang
    sbi     UCSRB, RXEN                 ; RX (Empfang) aktivieren
    
    sei                                 ; Interrupts global aktivieren
    
loop:
    rjmp loop                           ; Endlosschleife
 
; Interruptroutine: wird ausgeführt sobald ein Byte über das UART empfangen wurde
 
int_rxc:
    push    temp                        ; temp auf dem Stack sichern
    in      temp, sreg                  ; SREG sichern
    push    temp
    
    in      temp, UDR                   ; UART Daten lesen
    cpi     temp, '1'                   ; empfangenes Byte mit '1' vergleichen
    brne    int_rxc_1                   ; wenn nicht gleich, dann zu int_rxc_1
    cbi     PORTB, 0                    ; LED einschalten, low aktiv
    rjmp    int_rxc_2                   ; Zu int_rxc_2 springen
int_rxc_1:
    cpi     temp, '0'                   ; empfangenes Byte mit '0' vergleichen
    brne    int_rxc_2                   ; wenn nicht gleich, dann zu int_rxc_2
    sbi     PORTB, 0                    ; LED ausschalten, low aktiv
int_rxc_2:
 
    pop     temp
    out     sreg, temp                  ; SREG wiederherstellen
    pop     temp                        ; temp wiederherstellen
    reti
webmaster@mikrocontroller.netImpressumWerbung auf Mikrocontroller.net