Die asynchrone serielle Schnittstelle - UART
Die Abkürzung UART steht für Universal Asynchronous Receiver Transceiver (siehe wikipedia Artikel).
Der Quellcode und die Angaben dieser Seite wurden mit einem AtMega328p Microcontroller auf einem Arduino Board getestet. Alle Angaben zu Seitenzahlen und Sektionsnummern beziehen sich auf das zugehörige Datenblatt [M328p].
Mit Hilfe des UART können Daten zwischen dem Arduino und dem Computer über die serielle Schnittstelle übertragen werden.
Der Arduino verfügt über eine Funktionseinheit USART die in 3 verschiedenen Modi betrieben werden kann. Dem Titel folgend wird sich dieser Artikel ausschließlich auf den Asynchronous USART-Modus beziehen. Beide Bits UMSEL01 und UMSEL00 im UCSR0C Register müssen auf den Wert 0 gesetzt sein, damit der Arduino in diesem Modus arbeitet.[1]
Betriebsart
[Bearbeiten]Die Daten werden mit einer vorgegebenen Geschwindigkeit und in einem vorgegebenen Format übertragen. Sender und Empfänger müssen sich über die Geschwindigkeit und das Format einig sein. Die Geschwindigkeit wird in der Einheit Baud (bit pro Sekunde) angegben. Aus diesem Grund wird die Geschwindigkeit auch als Baudrate bezeichnet.
Baudrate
[Bearbeiten]Die Baudrate kann als Bruchteil von 1/16 bzw. 1/8 der CPU Taktfrequenz eingestellt werden. [2] Der Bruchteil kann mit einem 12-bit Wert angegeben werden. Formeln für die Umrechnung zwischen den 12-bit Werten im UBRR0 (usart bit rate) Register sowie dem U2X0 Flag im UCSR0A (usart control & status) Register und der resultierenden Baudrate können im Handbuch nachgelesen werden:[3]
U2X0 | Baudrate | UBBR0 |
---|---|---|
0 | ||
1 |
Eine Übersicht, wie genau sich einige typische Baudraten bei gegebener Taktfrequenz erreichen lassen, findet sich ebenfalls im Handbuch. [4] Eine Berechnung für beliebige Konfigurationen ist online z. B. mit dem WormFood's AVR Baud Rate Calculator möglich.
Der folgende Code-Schnipsel kann verwendet werden, um die Werte für das UBRR0 Register und das U2X0 Flag im UCSR0A Register zu berechnen.
uint8_t u2x0 = 1;
uint32_t ubrr0 = (F_CPU / 4 / baudrate - 1) / 2;
if (ubrr0 > 4095) {
u2x0 = 0;
ubrr0 = (F_CPU / 8 / baudrate - 1) / 2;
}
uint8_t ubrr0_h = (uint8_t) (ubrr0 >> 8);
uint8_t ubrr0_l = (uint8_t) (ubrr0 & 0xFF);
Format
[Bearbeiten]Angaben zu unterstützten Formaten finden sich im Datenblatt.[5]
Daten werden als Folge von Zeichen fester Größe übertragen. Die Zeichengröße kann dabei als 5, 6, 7, 8 oder 9 Bit gewählt werden. Die Zeichengröße kann man den Bits UCSZ01 und UCSZ00 im UCSR0C Register und dem UCSZ02 Bit im UCSR0B festlegen.[6]
Jede übertragene Einheit wird mit einem oder zwei Stoppbits abgeschlossen. Die Anzahl der Stoppbits kann mit dem Bit USBS0 im Register UCSR0C vorgegeben werden.[7]
Optional kann jede übertragene Einheit mit einer Prüfsumme Parity versehen werden. Die Einstellungen für die Parität können mit den Bits UPM01 UPM00 im Register UCSR0C vorgenommen werden.[8]
Initialisierung
[Bearbeiten]Bevor Baudrate oder Format geändert werden sollte laut Handbuch sichergestellt werden, dass keine Übertragung mehr im Gange ist.[9] Die Flags TXC0 (tx complete) und RXC0 (rx complete) im UCSR0A Register können zu diesem Zweck überprüft werden.
Damit der Arduino mit der serielle Schnittstelle arbeiten kann, muss das Power Reduction Bit PRUSART0 im PRR Register auf dem Wert 0 stehen.[10]
Sender und Empfänger können mit den Flags RXEN0 und TXEN0 im Register UCSR0B aktiviert bzw. deaktiviert werden.[11]
Arbeitsweise
[Bearbeiten]Für den Transfer von Daten von und zur seriellen Schnittstelle steht das Datenregister UDR0 zur Verfügung. Daten, die über die serielle Schnittstelle empfangen wurden, können aus diesem Register ausgelesen werden. Daten, die über die serielle Schnittstelle versendet werden sollen, können in dieses Register geschrieben werden.[12]
Für das Senden und das Empfangen von Daten verwendet der Arduino intern zwei Schieberegister.
Empfangene Datenpakete transferiert der Arduino vom Receive Schieberegister in einen internen Empfangspuffer, der über das Datenregister UDR0 ausgelesen werden kann. Der interne Empfangspuffer kann maximal 2 Zeichen aufnehmen.[13]
Treffen Datenpakete schneller ein, als sie abgeholt werden, so wird das DOR0 (data overrun) Flag im UCSR0A Register gesetzt, sobald bei gefülltem Empfangspuffer und Schieberegister ein neues Startbit empfangen wird.[14]
Zu versendende Datenpakete transferiert der Arduino vom Datenregister in das Transmit Schieberegister. Das Bit UDRE0 (data register empty) im Register UCSR0A gibt Auskunft darüber, ob Daten in das Datenregister geschrieben können, oder ob es noch in Benutzung ist.[15]
Außer internem Empfangspuffer sowie Receive und Transmit-Schieberegistern verfügt der Arduino über keinen Hardware-Puffer für die Kommunikation mit der seriellen Schnittstelle. Ohne Software Lösung können Daten somit nur Zeichen für Zeichen übertragen werden.
I/O ohne Interrupts
[Bearbeiten]Wenn für den Betrieb der seriellen Schnittstelle keine Interrupts verwendet werden, kann es bei dem Versuch Daten zu senden bzw. zu empfangen vorkommen, dass die Hardware noch nicht bereit ist, die gewünschte Operation direkt auszuführen.
Bei dem Versuch, zu sendende Daten zu schreiben, kann es vorkommen, dass das Datenregister noch belegt ist und nicht beschrieben werden kann. Bei dem Versuch empfangende Daten zu lesen, kann es vorkommen, dass noch keine neuen Daten verfügbar sind. In beiden Fällen muss entschieden werden, wie mit der Situation weiter zu verfahren ist.
Zwei typische Ansätze mit dem Problem umzugehen bestehen darin, den Zugriff auf die Hardware blockierend bzw. nicht-blockierend durchzuführen. Bei einer Implementierung, die den Zugriff blockierend durchführt, wird der Programmfluss so lange angehalten, bis die gewünschte Operation ausgeführt werden kann. Bei einer Implementierung, die den Zugriff nicht-blockierend durchführt, wird die gewünschte Operation verworfen und diese Tatsache im zurückgelieferten Statuscode vermerkt.
Lesen
[Bearbeiten]Das Bit RXC0 (rx complete) im UCSR0A Register gibt an, ob über die serielle Schnittstelle eingetroffene Daten verfügbar sind. Wenn Daten zur Verfügung stehen, kann der empfangene Wert im Register UDR0 ausgelesen werden.
Ist ein Format mit Paritätsprüfung im Einsatz, so gibt das Flag UPE0 (parity error) im UCSR0A Register Auskunft darüber ob ein Fehler bei der Paritätsprüfung aufgetreten ist.
blockierend
[Bearbeiten]int serial_blocking_read_byte() {
loop_until_bit_is_set(UCSR0A, RXC0);
if (! bit_is_clear(UCSR0A, UPE0))
return EOF;
uint8_data = UDR0;
return data;
}
nicht-blockierend
[Bearbeiten]int serial_read_byte() {
if (! bit_is_set(UCSR0A, RXC0))
return EOF;
if (! bit_is_clear(UCSR0A, UPE0))
return EOF;
uint8_data = UDR0;
return data;
}
Schreiben
[Bearbeiten]Das Bit UDRE0 (data register empty) im UCSR0A Register gibt an, ob der Datenpuffer frei oder belegt ist. Wenn der Datenpuffer frei ist, kann der gewünschte Wert in das Register UDR0 geschrieben werden.
blockierend
[Bearbeiten]int serial_blocking_write_byte(uint8_t data) {
loop_until_bit_is_set(UCSR0A, UDRE0);
UDRO = data;
return data;
}
nicht-blockierend
[Bearbeiten]int serial_write_byte(uint8_t data) {
if (! bit_is_set(UCSR0A, UDRE0))
return EOF;
UDRO = data;
return data;
}
Beispiel Projekt
[Bearbeiten]Zum Ausprobieren der bis hier erarbeiteten Operationen kann das folgende Projekt verwendet werden.
Es kann sowohl mit den im Kapiel TODO: link eintragen vorgestellten Werkzeugen als pures C Programm von der Kommandozeile aus übersetzt und in den Arduino geladen werden, als auch mit der Arduino IDE übersetzt werden.
Das Preprozessor Flag ARDUINO, das im #ifdef Wrapper um die main Funktion abgefragt wird, wird von der Arduino IDE beim Compileraufruf gesetzt wodurch sie für die Arduino IDE, die ihre eigene main Funktion in das Binary linkt unsichtbar bleibt.
Die nur bei Übersetzung von der Kommondozeile für den Compiler sichtbare main Funktion empfindet die Arbeitsweise der Arduino IDE wie folgt nach: Nach Start ruft sie einmalig die Funktion setup() und hiernach periodisch die loop() Funktion auf.
/* ANTI-COPYRIGHT NOTICE
* ---------------------
* This work is free. You can redistribute it and/or modify it under the
* terms of the Do What The Fuck You Want To Public License, Version 2,
* as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
*/
#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>
// Forward Declarations
void serial_init(unsigned long baudrate, uint8_t frame_format);
int serial_read_byte(void);
int serial_blocking_read_byte(void);
int serial_write_byte(uint8_t data);
int serial_blocking_write_byte(uint8_t data);
void setup(void);
void loop(void);
/*
UCSR0C mode parity stop size* polarity
+-------------------+---------------+-------+-----------------+---------+
| UMSEL01 : UMSEL00 | UPM01 : UPM00 | USBS0 | UCSZ01 : UCSZ00 | UPOL0 |
+-------------------+---------------+-------+-----------------+---------+
UPM0[1:0] none(00) even(10) odd(01)
USBS0 1bit(0) 2bit(1)
UCSZ0[2:0] 5bit(000) 6bit(001) 7bit(010) 8bit(011) 9bit(111)
*/
// size 8 / parity none / 1 stop bit
#define SERIAL_FORMAT_8N1 0b00000110
#define BAUDRATE 57600L
#define BLINK_DELAY_MS 1000
#define sbi(port, bit) (port) |= (uint8_t) (1 << (bit))
#define cbi(port, bit) (port) &= (uint8_t) ~(1 << (bit))
void serial_init(uint32_t baudrate, uint8_t frame_format){
// calculate baud rate settings
uint8_t u2x0 = 1;
uint32_t ubbr0 = (F_CPU / 4 / baudrate - 1) / 2;
if (ubbr0 > 4095) {
u2x0 = 0;
ubbr0 = (F_CPU / 8 / baudrate -1) / 2;
}
// set baud rate
UBRR0H = (uint8_t) (ubbr0 >> 8);
UBRR0L = (uint8_t) (ubbr0 & 0xFF);
if (u2x0) sbi(UCSR0A, U2X0);
else cbi(UCSR0A, U2X0);
// enable receive / transmit
UCSR0B |= _BV(RXEN0) | _BV(TXEN0);
// set frame format
UCSR0C = frame_format;
}
// READ
int serial_read_byte() {
if (! bit_is_set(UCSR0A, RXC0))
return EOF;
uint8_t data = UDR0;
return data;
}
int serial_blocking_read_byte() {
loop_until_bit_is_set(UCSR0A, RXC0);
uint8_t data = UDR0;
return data;
}
// WRITE
int serial_write_byte(uint8_t data) {
if (! bit_is_set(UCSR0A, UDRE0))
return EOF;
UDR0 = data;
return data;
}
int serial_blocking_write_byte(uint8_t data) {
loop_until_bit_is_set(UCSR0A, UDRE0);
UDR0 = data;
return data;
}
void setup() {
sbi(DDRB, DDB5); // LED: PORTB5 out
serial_init(BAUDRATE, SERIAL_FORMAT_8N1);
}
void loop() {
int c;
c = serial_blocking_read_byte();
//c = serial_read_byte();
if (c != EOF)
serial_blocking_write_byte((uint8_t) c);
sbi(PINB, PINB5); // toggle LED
_delay_ms(BLINK_DELAY_MS); // delay
}
#ifndef ARDUINO
int main(void) {
setup();
while(1)
loop();
}
#endif
In Betrieb nehmen
[Bearbeiten]Boards wie das des Arduino UNO verfügen bereits über einen fest auf der Platine verdrahteten RS232-Konverter. In diesem Fall ist für die Verbindung zum PC lediglich nötig, beide Geräte mit einem USB-Kabel zu verbinden.
Andernfalls kann ein separater USB-RS232-Konverter verwendet werden. Für das Experiment habe ich einen Konverterkabel mit FT232 Serial (UART) IC verwendet, es sollte aber auch mit jedem anderen Konverter gelingen. Des weiteren wird eine Leuchtdiode an PORTB5 benötigt, mit deren Hilfe der Fortschritt des Programms beobachtet werden kann. Bei Verwendung eines Arduino Boards sollte dort bereits eine LED verdrahtet sein.
Das Beispiel stellt ein einfaches echo-Programm zur Verfügung. Über die serielle Schnittstelle eintreffende Daten werden mit serial_blocking_read_byte() entgegengenommen und mit serial_blocking_write() wieder über die serielle Schnittstelle ausgegeben.
Der Port B5, an dem die Leuchtdiode verdrahtet ist, wird durch setzen des Bits DDB5 im DDRB (data direction) Register mit sbi(DDRB, DDB5) als Ausgabeport konfiguriert. Setzen des PINB5 Bits im Register PINB mit sbi(PINB, PINB5) sorgt dafür, dass der Schaltzustand von Port B5 getoggelt wird und die Leuchtdiode somit ihren Zuständen von an auf aus, bzw. von aus auf an wechselt.
Für das Senden und Empfangen von Daten über die serielle Schnittstelle am PC habe ich das Programm gtkterm verwendet. Das Beispielprogramm verwendet eine Baudrate von 57600 und ein Format mit 8-bit Zeichenlänge, ohne Parität mit einem Stoppbit. Bevor die Kommunikation mit dem Arduino stattfinden kann, müssen Geschwindigkeit und Format der seriellen Schnittstelle des PC an diese Werte angepasst werden.
Nutzer, die die Arduino-IDE verwenden, können alternativ auch auf den mitgelieferten seriellen Monitor zurückgreifen. Er findet sich unter Werkzeuge->Serieller Monitor. Bei Benutzung des seriellen Monitors der Arduino-IDE ist zu beachten, dass eingegebene Zeichen erst nach Betätigung des Buttons send am Stück verschickt werden und nicht direkt nach der Eingabe.
Auf der Tastatur eingegebene Zeichen sollten nun über die serielle Schnittstelle an den Arduino verschickt und von dort wieder zurück geschickt werden, um schließlich im Terminal zu erscheinen. Nach jedem eingegebenen Zeichen sollte der Schaltzustand der Leuchtdiode von an aus aus, bzw. von aus auf an wechseln.
Beobachtungen
[Bearbeiten]Beobachtungen, die mit dem Programm gemacht werden können:
- Der Schaltzustand der Leuchtdiode wechselt nur nach Empfang eines Zeichens. (blockierendes Lesen wartet auf Empfang des nächsten Zeichens - wenn es sein muss für immer!)
- Bei schneller Eingabe werden einige Zeichen 'verschluckt' und nicht ausgegeben.
Durch Auskommentieren von Zeile 105 und Entfernen der Kommentarzeichen am Anfang von Zeile 106 kann die Wirkung der nicht-blockierenden Variante Empfangsfunktion ausprobiert werden.
Beobachtungen, die mit dieser Version gemacht werden können:
- Der Schaltzustand der Leuchtdiode wechselt nun periodisch unabhängig vom Empfang von Zeichen. (nicht-blockierendes Lesen wartet nicht auf Empfang des nächsten Zeichens)
- Schnell eingegebene Zeichen werden noch immer 'verschluckt' und nicht ausgegeben.
Phänomen zwei resultiert aus der begrenzten Größe des Empfangspuffers und tritt in beiden Varianten auf. Eine typische Ausgabe zeigt nebenstehender Screenshot, aufgenommen nach zweimaligem schnellen Streifen über die Ziffernreihe der Tastatur. [16]
Gut sichtbar ist die Wirkung des internen Empfangspuffers. Jeweils die ersten zwei Zeichen 1 und 2 hat der Arduino direkt nach Empfang vom Schieberegister in den Empfangspuffer verschoben. Zeichen die danach versandt wurden landeten zunächst im Schieberegister, wurden dort aber jeweils durch das nächste eintreffende Zeichen ersetzt. Das jeweils zuletzt versandte Zeichen: im ersten Durchgang die 5, im zweiten Durchgang die 0 steckte noch im Schieberegister. [17]
Ohne Nutzung von Interrupts kann eine Verbesserung nur erreicht werden, wenn häufiger gepollt, d.h. versucht wird eventuell bereits eingetroffene Daten abzufragen. In diesem Fall steigt aber auch die Rechenzeit, die darauf verwendet wird zu prüfen, ob eventuell bereits neue Daten anstehen. Selbst wenn gar keine neuen Daten eingetroffen sind.
Ein alternativer Ansatz besteht darin, die Daten genau dann in Empfang zu nehmen, wenn sie auch tatsächlich beim Arduino eintreffen. Dies ist durch die Nutzung von Interrupts möglich, um die es im nächsten Abschnitt gehen wird.
I/O mit Interrupts
[Bearbeiten]Mit Interrupts ist es möglich direkt auf das Eintreffen von empfangenen Daten zu reagieren und zu versendende Daten auch ohne aktives Warten genau dann zu schreiben, wenn das Datenregister bereit ist. Für die Serielle Schnittstelle stehen drei Interrupts zur Verfügung, die einzeln aktiviert und deaktiviert werden können.
Das Bit RXCIE0 (rx complete interrupt enable) im USCR0B Register legt fest, ob beim Empfang neuer Daten ein Interrupt ausgelöst wird. Das Bit TXCIE0 (tx complete interrupt enable) im USCR0B Register legt fest, ob nach dem Datenversand ein Interrupt ausgelöst wird. Das Bit UDRIE0 (data register empty interrupt enable) im UCSR0B Register legt fest, ob ein Interrupt ausgelöst wird, wenn der Datenpuffer bereit für die Aufnahme neuer Daten ist.
Wie bei allen Interrupts muss auch hier zusätzlich zu den gesetzten Bits die Zustellung von Interrupts global aktiviert werden. Zu diesem Zweck kann die Funktion sei() verwendet werden.
Die Interrupt Routinen können mit dem ISR() Makro im Header avr/interrupt.h definiert werden. Die Indices für die genannten Interrupts können mit den Makros USART_RX_vect, USART_RX_vect und USART_UDRE_vect angegeben werden.
Lesen
[Bearbeiten]Um mit einem Interrupt darüber informiert zu werden, dass an der seriellen Schnittstelle neue Daten eingetroffen sind, muss das Bit RXCIE0 im USCR0B Register gesetzt sein. Zudem muss die Zustellung von Interrupts global aktiviert werden. Dies kann mit dem folgenden Code Schnipsel umgesetzt werden.
sbi(UCSR0B, RXCIE0); // rx complete interrupt enable
sei(); // enable interrupts
Für die zugehörige Interrupt Service Routine kann folgendes Skelett verwendet werden.
ISR(USART_RX_vect) {
// RX COMPLETE: do something here !
}
Für die Implementierung des Rumpfs stehen zwei Ansätze zur Verfügung.
Zum einen können die eintreffende Daten direkt verarbeitet werden, zum anderen können können sie zunächst nur zwischengespeichert werden um sie zu einem späteren Zeitpunkt zu verarbeiten. Im ersten Fall spricht man auch davon, dass die Daten ungepuffert verarbeitet werden, im zweiten Fall, davon dass sie gepuffert' verarbeitet werden.
Ungepuffert
[Bearbeiten]Das echo Programm aus dem vorangegangenen Abschnitt liefert ein Beispiel für ein Programm, das ohne Pufferung auskommt.
Empfang und Versand von Daten sollen nun von der Interrupt Routine übernommen werden. Sämtliche Zugriffe auf die Serielle Schnittstelle können deshalb aus der Funktion loop() entfernt werden.
void loop() {
sbi(PINB, PINB5); // toggle LED
_delay_ms(BLINK_DELAY_MS); // delay
}
Um über das Eintreffen neuer Daten mit einem Interrupt informiert zu werden, müssen das Bit RXCIE0 im USCR0B Register gesetzt und die Zustellung von Interrupts global aktiviert werden. Eine gute Stelle das zu tun ist die setup() Funktion, die wie folgt erweitert werden kann:
void setup() {
sbi(DDRB, DDB5); // LED: PORTB5 out
serial_init(BAUDRATE, SERIAL_FORMAT_8N1);
sbi(UCSR0B, RXCIE0); // rx complete interrupt enable
sei(); // enable interrupts
}
Nächster Schritt ist die Erstellung der zugehörigen Interrupt Service Routine. Bei ihrer Implementierung kann auf die bereits erstellten Funktionen zurückgegriffen werden.
ISR(USART_RX_vect) {
int c = serial_read_byte();
serial_blocking_write_byte((uint8_t) c);
}
Es fehlt ein letztes include des Headers avr/interrupt.h und schon sollte das neue echo Programm fertig programmiert sein.
#include <avr/interrupt.h>
Datenpufferung
[Bearbeiten]Variablen, auf die sowohl vom Hauptprogramm als auch von einer Interrupt Service Routine zugegriffen werden, müssen vor konkurrierendem Zugriff geschützt werden. Im allgemeinen Fall genügt es dafür nicht, die betreffenden Variablen volatile zu deklarieren.
Stattdessen muss im Hauptprogramm für die Zeit des Zugriffs die Ausführung von Interrupt-Routinen global deaktiviert werden und dafür Sorge getragen werden, dass der Compiler den betreffenden Zugriff im Zuge der Optimierung beim Code Re-ordering nicht aus dem gewünschten Bereich verschiebt. Zu diesem Zweck können die Makros des Headers util/atomic.h der avr-libc verwendet werden.
Nur im Spezialfall, dass schreibende Zugriffe auf die Variable entweder ausschließlich im Hauptprogramm oder ausschließlich in Interrupt-Routinen stattfinden und es sich um Variablen der Länge 8-bit handelt, genügt es, die betreffende Variable volatile zu deklarieren, um sie vor konkurrierendem Zugriff zu schützen.
Warteschlange
[Bearbeiten]Für die Pufferung von Daten kann eine Warteschlange - je nach persönlicher Vorliebe manchmal auch als Queue oder FIFO bezeichnet verwendet werden. Zugriff auf die von ihr verwalteten Daten stellt sie über die Operationen push_tail() und pop_head() zur Verfügung, mit denen Werte am Ende der Schlange eingfügt, bzw. am Kopf der Schlange entnommen werden können.
Datenstruktur
[Bearbeiten]Für den Puffer selbst wird ein Array fester Länge, dessen Elemente vom Typ char sind verwendet. Die Speicherposition des Arrays soll im Folgenden in der Variable buffer festgehalten sein. Buchführung darüber, an welcher Stelle des Arrays sich Anfang (head) und Ende (tail) der Schlange befinden erfolgt mit Hilfe zweier weiterer Variablen.
Die Variable head wird dabei immer den Index des Array Elements am Anfang der Schlange angeben. Die Variable tail hingegen soll stets den Index der nächsten freien Speicherposition am Ende der Schlange angeben.
Einen Sonderfall stellt eine leere Schlange dar. Für sie sollen die Werte von head und tail identisch sein. Dies ist zugleich der Zustand, auf den die Schlange zu Beginn initialisiert wird.
Operationen
[Bearbeiten]Neue Daten können mit push_tail() in die Schlange eingereiht werden, indem ihr Wert an die durch tail angegebene Position des Puffers geschrieben wird und der Wert von tail um 1 erhöht wird. Erreicht der Wert von tail einen Wert, der einem Index außerhalb des Puffers entspricht, so wird tail auf 0 zurückgesetzt, um die Speicherung von Daten am Anfang des Puffers fortzusetzen.
Bevor auf die beschriebene Art und Weise neue Daten in die Schlange eingereiht werden, muss allerdings zunächst geprüft werden, ob der Puffer groß genug ist, um weitere Daten aufzunehmen. Das ist nur dann der Fall, wenn der berechnete neue Wert für tail nicht identisch zu head ist.
Analog können mit pop_head() in der Schlange aufgereihte Daten vom Kopf der Schlange entnommen werden, indem sie von der durch head angegebenen Position des Puffers gelesen und der Wert von head anschließend um 1 erhöht und gegebenenfalls auf 0 zurückgesetzt wird.
Auch hier ist zunächst zu prüfen, ob die gewünschte Operation überhaupt zulässig ist, d.h. ob die Schlange überhaupt Daten enthält. Eine leere Schlange liegt nur im oben genannten Sonderfall, dass head und tail identisch sind, vor.
Code Beispiel
[Bearbeiten]Eine Möglichkeit die Warteschlange wie beschrieben zu implementieren findet sich im folgenden Code Beispiel.
Achtung! Direkt verwendet werden kann der Code Schnipsel so noch nicht! Der Zugriff auf die Schlange ist nicht vor konkurrierendem Zugriff aus Hauptprogramm und Interrupt Routinen geschützt!
#define BUFFER_SIZE 64
#define NIL -1
char buffer[BUFFER_SIZE];
uint8_t head = 0;
uint8_t tail = 0;
int push_tail(char value) {
uint8_t next_tail = (tail + 1) % BUFFER_SIZE;
if (next_tail == head)
return NIL;
buffer[tail] = value;
tail = next_tail;
return 0;
}
int pop_head() {
if (head == tail)
return NIL;
uint8_t data = buffer[head];
head = (head + 1) % BUFFER_SIZE;
return data;
}
Implementierung
[Bearbeiten]Um mit einem Interrupt darüber informiert zu werden, dass an der seriellen Schnittstelle neue Daten eingetroffen sind, muss das Bit RXCIE0 im USCR0B Register gesetzt sein. Zudem muss die Zustellung von Interrupts global aktiviert werden. Dies kann mit dem folgenden Code Schnipsel umgesetzt werden.
// enable rx interrupt
sbi(UCSR0B, RXCIE0);
// global interrupts enable
sei();
Die eintreffenden Daten sollen in einer Warteschlange gesammelt werden. Um konkurierenden Zugriff zu vermeiden werden sowohl head als auch tail volatile deklariert. Zudem muss darauf geachtet werden, dass jeweils nur Hauptprogramm, bzw. Interrupt Routinen schreibend auf eine der Variablen zugreift.
Das Hinzufügen neuer Daten am Ende der Schlange wird ausschließlich in der Interrupt Routine stattfinden. Hierfür muss nur auf die Variable tail schreibend zugegriffen werden.
Des Entfernen von Daten am Kopf der Schlange hingegen wird ausschließlich im Hauptprogramm stattfinden. Hier muss die Variable tail nur gelesen werden. Schreibend wird ausschließlich auf die Variable head zugegriffen.
// BUFFERED READ
#define RX_BUFFER_SIZE 64
static uint8_t rx_buffer[RX_BUFFER_SIZE];
static volatile uint8_t rx_head = 0;
static volatile uint8_t rx_tail = 0;
int serial_rx_get() {
if (rx_head == rx_tail)
return EOF;
uint8_t c = rx_buffer[rx_head];
rx_head = (uint8_t) (rx_head + 1) % RX_BUFFER_SIZE;
return c;
}
ISR(USART_RX_vect) {
if (bit_is_set(UCSR0A, UPE0)) {
UDR0;
return;
}
uint8_t next_tail = (uint8_t) (rx_tail + 1) % RX_BUFFER_SIZE;
uint8_t c = UDR0;
if (next_tail == rx_head)
return;
rx_buffer[rx_tail] = c;
rx_tail = next_tail;
}
Register Übersicht
[Bearbeiten]Register | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|---|
Baud Rate Registers | ||||||||
UBRR0L | baud rate low | |||||||
UBBR0_7 | UBRR0_6 | UBBR0_5 | UBBR0_4 | UBBR0_3 | UBBR0_2 | UBBR0_1 | UBBR0_0 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
UBRR0H | reserved | baud rate high | ||||||
x | x | x | x | UBBR0_11 | UBBR0_10 | UBBR0_9 | UBBR0_8 | |
0 | 0 | 0 | 0 | |||||
Control and Status Registers | ||||||||
UCSR0A | rx complete | tx complete | data register empty | frame error | data overrun | parity error | double speed | mpc mode |
RXC0 | TXC0 | UDRE0 | FE0 | DOR0 | UPE0 | U2X0 | MPCM0 | |
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | |
UCSR0B | rx complete interrupt | tx complete interrupt | data register empty interrupt | rx enable | tx enable | size* | rx 9-th bit | tx 9-th bit |
RXCIE0 | TXCIE0 | UDRIE0 | RXEN0 | TXEN0 | UCSZ02 | RXB80 | TXB80 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
UCSR0C | mode | parity | stop bits | size* | polarity | |||
UMSEL01 | UMSEL00 | UPM01 | UPM00 | USBS0 | UCSZ01 | UCSZ00 | UPOL0 | |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | |
Data Register | ||||||||
UDR0 | tx / rx data | |||||||
UDR0_7 | UDR0_6 | UDR0_5 | UDR0_4 | UDR0_3 | UDR0_2 | UDR0_1 | UDR0_0 | |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Fußnoten
[Bearbeiten]- ↑ (siehe [M328p]: Table 24-8 USART Mode Selection S. 249)
- ↑ Das Bit U2X0 des UCSR0A Registers gibt Auskunft darüber mit welchem Bruchteil gerechnet werden soll. Ist es gesetzt wird 1/8 genommen ansonsten 1/16
- ↑ (siehe [M328p]: Table 24-1. Equations for Calculating Baud Rate Register Setting, S. 227)
- ↑ (siehe [M328p]: 24.11. Examples of Baud Rate Setting, S. 240)
- ↑ (siehe [M328p]: Sektion 24.5. Frame Formats)
- ↑ (siehe [M328p]: Table 24-11. Character Size Settings, S. 250)
- ↑ (siehe [M328p]: Table 24-10. Stop Bit Settings, S. 250)
- ↑ (siehe [M328p]: Table 24-9. USART Mode Selection, S. 249) - Bei dem Titel USART Mode Selection scheint es sich um einen Druckfehler zu handeln. Bereits Table Table 24-8. trägt diese Bezeichnung. Der korrekte Titel der Tabelle wäre wohl Parity Mode Selection
- ↑ (siehe [M328p]: 24.6. USART Initialization, S. 230)
- ↑ (siehe [M328p]: 14.12.3. Power Reduction Register, S. 71)
- ↑ (siehe [M328p]: 24.8. Data Reception – The USART Receiver, S. 233 / 24.7. Data Transmission – The USART Transmitter, S. 231)
- ↑ Wird ein 9-bit Format verwendet, so werden die Bits TXB80 und RXB80 im Register UCSR0B zur Angabe des höchstwertigen Bits von empfangenen bzw. von zu versendenden Daten verwendet. Es ist in diesem Fall laut Handbuch wichtig die höchstwertigen Bits des Datenpakets (TXB80, bzw. RXB80 im Register UCSR0B) vor dem Inhalt des Datenregisters (UDR0) zu verarbeiten.
- ↑ siehe [M328p]: 24.8.4. Receiver Error Flags, S. 235, Absatz 3 ...occurs when the receive buffer is full (two characters)... das ist meines Wissens die einzige Stelle, an der die Größe des Empfangspuffers zumindest indirekt angegeben ist
- ↑ (siehe [M328p]: 24.8.4. Receiver Error Flags, S. 235)
- ↑ (siehe [M328p]: 24.7.3. Transmitter Flags and Interrupts)
- ↑ Bei Verwendung des Seriellen Monitors der Arduino IDE lässt sich der beschriebene Effekt noch einfacher beobachten. Hier werden eingegebene Zeichen erst nach Betätigen des send Buttons versandt. Man braucht also keine ganz so schnellen Finger.
- ↑ Je nachdem ob die blockierende Variante oder die nicht-blockierende Variante gewählt wird und je nach Glücklichkeit des Timings können statt der ersten zwei hier auch die ersten drei eingetippten Zeichen und das zuletzt getippte Zeichen sichtbar sein.
</nocinclude>