Aus RN-Wissen.de
Wechseln zu: Navigation, Suche


Ein Software-UART ist eine Software, welche die Funktionalität eines UART (Universal Asynchronous Receiver and Transmitter) implementiert., oder kurz: eine serielle Schnittstelle in Software nachbildet.

Einführung

Stehen keine oder nicht genügend Hardware-UARTs zur Verfügung, muss man selbst Hand anlegen. Die Herausforderung ist dabei, die festgelegten Zeitrahmen bzw. Übertragungsraten/Bitlängen möglichst genau einzuhalten, um die Kommunikation fehlerfrei zu halten. Dies gilt in besonderem Maße für den Empfänger (Receiver), der nicht entscheiden kann, wann Daten eintreffen.

Dafür benötigt man eine Zeitbasis. Am konfortabelsten geht das mit einem Timer. Ohne Timer muss man die Zeit durch Abzählen von Maschinenzyklen machen, während die restliche Software warten muss bzw. Interrupts deaktiviert sein müssen.

Jeder diese Herangehensweisen hat ihre Vor- und Nachteile. Welcher Weg der bessere ist, kann nur im Gesamtzusammenhang des Programmes entschieden werden.

Wie auch immer; Ziel der Software ist es, UART-Datenframes senden und empfangen zu können. Bei Inaktivität (idle) ist die Signalleitung auf 1 (High). Jeder Frame beginnt mit einem Startbit (S), das 0 ist. Danach folgen die 8 Datenbits und schliesslich ein Stop-Bit (P), das immer 1 ist. Danach ist die Leitung wieder inaktiv oder es folgen weitere Frames.

UART-Frame

Läuft der AVR mit einer Taktrate von F_CPU und die Übertragung mit der Baudrate BAUDRATE, dann dauert ein Bit

[math]Z_\mathrm{bit} = \frac\mathrm{F_{CPU}}\mathrm{BAUDRATE}[/math]

Zyklen. Bei 16MHz und 38400 Baud dauert ein Bit also rund 417 Zyklen, bei 1MHz und 9600 hat man nur rund 104 Zyklen Zeit.

Mit Verwendung von Timer1

Dateikopf

Im Dateikopf wird festgelegt, an welchen Ports die Ein- und Ausgabe stattfindet, mit welcher Baudrate übertragen wird und welche Teile des Soft-UARTs aktiv sind, also übersetzt werden und als Funktionen zur Verfügung stehen bzw. Flash verbrauchen. Damit eintreffende Daten nicht verloren gehen, können die Eingabedaten in einer FIFO zwischengespeichert werden.

Die verwendeten Ports sind aus Effizienzgründen hartcodiert. Der Receiver-Eingang liegt an einem ICP (InputCapture-Eingang). Ebenfalls hartcodiert ist das Sendeformat 8N1 (8 Datenbits, kein Parity, 1 Stopbit).

F_CPU
ist die Taktrate des AVR und muss irgendwo definiert worden sein, entweder über ein #define oder per Kommandozeile mit -DF_CPU=...
#define BAUDRATE ...
Die Baudrate, gleich für Receiver und Transmitter.
#define SUART_TXD
Wenn dieses Define gesetzt ist, wird der Transmitter aktiv bzw. die für ihn notwendigen Code-Sequenzen werden eingeblendet.
#define SUART_RXD
Wenn dieses Define gesetzt ist, wird der Receiver aktiv bzw. die für ihn notwendigen Code-Sequenzen werden eingeblendet.
FIFO aktivieren
Um die FIFO zu aktivieren, wird einfach der Header fifo.h includet und dadurch die entsprechende Codeerzeugung veranlasst.
#define INBUF_SIZE ...
Die Größe des Eingabe-Puffers für die Receiver-FIFO
#include <avr/io.h>
#include <avr/interrupt.h>

#ifndef SIGNAL
#include <avr/signal.h>
#endif // SIGNAL 

#include "uart.h"

// Folgende Zeile einkommentieren, falls FIFO verwendet werden soll 
// #include "fifo.h" 

#define BAUDRATE 38400

#define nop() __asm volatile ("nop")

#ifdef SUART_TXD
    #define SUART_TXD_PORT PORTB
    #define SUART_TXD_DDR  DDRB
    #define SUART_TXD_BIT  PB1
    static volatile uint16_t outframe;
#endif // SUART_TXD 

#ifdef SUART_RXD
    #define SUART_RXD_PORT PORTB
    #define SUART_RXD_PIN  PINB
    #define SUART_RXD_DDR  DDRB
    #define SUART_RXD_BIT  PB0
    static volatile uint16_t inframe;
    static volatile uint8_t inbits, received;

    #ifdef _FIFO_H_
        #define INBUF_SIZE 4
        static uint8_t inbuf[INBUF_SIZE];
        fifo_t infifo;
    #else // _FIFO_H_ 
        static volatile uint8_t indata;
    #endif // _FIFO_H_ 
#endif // SUART_RXD 

Initialisierung

Hier wird der Software-UART initialisiert. Die Initialisierung ist gegen Interrupts gesperrt. Das Modus- und OCR1A-Register von Timer1 werden so initialisiert, daß eine Zeitbasis für den UART entsteht.

// Initialisierung für einen ATmega8 
// Für andere AVR-Derivate sieht dies vermutlich anders aus: 
// Registernamen ändern sich (zB TIMSK0 anstatt TIMSK, etc). 
void uart_init()
{
    uint8_t tifr = 0;
    uint8_t sreg = SREG;
    cli();

    // Mode #4 für Timer1 
    // und volle MCU clock 
    // IC Noise Cancel 
    // IC on Falling Edge 
    TCCR1A = 0;
    TCCR1B = (1 << WGM12) | (1 << CS10) | (0 << ICES1) | (1 << ICNC1);

    // OutputCompare für gewünschte Timer1 Frequenz 
    OCR1A = (uint16_t) ((uint32_t) F_CPU/BAUDRATE);

#ifdef SUART_RXD
    SUART_RXD_DDR  &= ~(1 << SUART_RXD_BIT);
    SUART_RXD_PORT |=  (1 << SUART_RXD_BIT);
    TIMSK |= (1 << TICIE1);
    tifr  |= (1 << ICF1) | (1 << OCF1B);
#else
    TIMSK &= ~(1 << TICIE1);
#endif // SUART_RXD 

#ifdef SUART_TXD
    tifr |= (1 << OCF1A);
    SUART_TXD_PORT |= (1 << SUART_TXD_BIT);
    SUART_TXD_DDR  |= (1 << SUART_TXD_BIT);
    outframe = 0;
#endif // SUART_TXD 

    TIFR = tifr;

    SREG = sreg;

#ifdef _FIFO_H_
   fifo_init (&infifo,   inbuf, INBUF_SIZE);
#endif // _FIFO_H_

}

Transmitter

Das Senden ist am einfachsten: Zuerst wird gewartet, bis eine evtl. laufende Sendung abgeschlossen ist. Dann wird der Ausgabe-Frame zusammengebaut (1 Startbit, 8 Datenbits, 1 Stopbit und 1 Bit als interne Endekennung). Dann wird OutputCompare1A-Interrupt aktiviert das entsprechende Interrupt-Flag zurückgesetzt, damit das erste Bit nicht zu kurz wird (wenn das Flag gesetzt wäre, würde direkt nach dem sei() ein Interrupt auftreten. Ein Transmit ist fertig, wenn outframe gleich 0 ist. Das Auslesen von outframe muss unter Interrupt-Deaktivierung geschehen. uart_putc geht davon aus, daß Interrupts global aktiviert sind (ansonsten hängt es sich auf bzw. es wird nicht ausgegeben).

#ifdef SUART_TXD
void uart_putc (const char c)
{
    do
    {
        sei(); nop(); cli(); // yield(); 
    } while (outframe);

    // frame = *.P.7.6.5.4.3.2.1.0.S   S=Start(0), P=Stop(1), *=Endemarke(1) 
    outframe = (3 << 9) | (((uint8_t) c) << 1);

    TIMSK |= (1 << OCIE1A);
    TIFR   = (1 << OCF1A);

    sei();
}
#endif // SUART_TXD 

Danach kommt es zu OC1A-Interrupts. Für jeden IRQ wird ein Bit ausgegeben. Im letzten Interrupt wird das Senden eingestellt und die ISR deaktiviert ihren eigenen IRQ.

#ifdef SUART_TXD
SIGNAL (SIG_OUTPUT_COMPARE1A)
{
    uint16_t data = outframe;
   
    if (data & 1)      SUART_TXD_PORT |=  (1 << SUART_TXD_BIT);
    else               SUART_TXD_PORT &= ~(1 << SUART_TXD_BIT);
   
    if (1 == data)
    {
        TIMSK &= ~(1 << OCIE1A);
    }   
   
    outframe = data >> 1;
}
#endif // SUART_TXD

Receiver

Eine fallende Flanke löst einen Input-Capture-IRQ aus. Der in ICR1 gesicherte Timer1-Wert wird gelesen, und die halbe Bit-Zeit OCR1A/2 wird draufaddiert, um den Empfangszeitpunkt (sample time) zu erhalten. Die ISR deaktiviert ihren IRQ und aktiviert stattdessen den OutputCompare1B-Interrupt, der die Bits einsammelt.

#ifdef SUART_RXD
SIGNAL (SIG_INPUT_CAPTURE1)
{
    uint16_t icr1  = ICR1;
    uint16_t ocr1a = OCR1A;
   
    // Eine halbe Bitzeit zu ICR1 addieren (modulo OCR1A) und nach OCR1B
    uint16_t ocr1b = icr1 + ocr1a/2;
    if (ocr1b >= ocr1a)
        ocr1b -= ocr1a;
    OCR1B = ocr1b;
   
    TIFR = (1 << OCF1B);
    TIMSK = (TIMSK & ~(1 << TICIE1)) | (1 << OCIE1B);
    inframe = 0;
    inbits = 0;
}
#endif // SUART_RXD

Die OutputCompare1B-ISR sammelt die Bits ein. Wurden 10 Bits übertragen, wird das empfangene Byte gespeichert, falls es nicht ungültig ist. Gespeichert wird in der Modul-globalen Variablen indata (oder bei aktiviertem FIFO-Mechanismus in infifo), und der Empfangsmerker received wird gesetzt.

#ifdef SUART_RXD
SIGNAL (SIG_OUTPUT_COMPARE1B)
{
    uint16_t data = inframe >> 1;
   
    if (SUART_RXD_PIN & (1 << SUART_RXD_BIT))
        data |= (1 << 9);
      
    uint8_t bits = inbits+1;
   
    if (10 == bits)
    {
        if ((data & 1) == 0)
            if (data >= (1 << 9))
            {
#ifdef _FIFO_H_         
                _inline_fifo_put (&infifo, data >> 1);
#else            
                indata = data >> 1;
#endif // _FIFO_H_            
                received = 1;
            }
      
        TIMSK = (TIMSK & ~(1 << OCIE1B)) | (1 << TICIE1);
        TIFR = (1 << ICF1);
    }
    else
    {
        inbits = bits;
        inframe = data;
    }
}
#endif // SUART_RXD

Zwei Funktionen, um empfangene Daten zu lesen. Je nachdem, ob die FIFO verwendet wird, sind diese Funktionen unterschiedlich implementiert. Das Benutzer-Interface bleibt jedoch gleich, und die Verwendung einer FIFO transparent.

#ifdef SUART_RXD
#ifdef _FIFO_H_

int uart_getc_wait()
{
    return (int) fifo_get_wait (&infifo);
}

int uart_getc_nowait()
{
    return fifo_get_nowait (&infifo);
}

#else // _FIFO_H_

int uart_getc_wait()
{
    while (!received)   {}
    received = 0;
   
    return (int) indata;
}

int uart_getc_nowait()
{
    if (received)
    {
        received = 0;
        return (int) indata;
    }
   
    return -1;
}

#endif // _FIFO_H_
#endif // SUART_RXD

Benutzer-Interface und Header

Im Header uart.h werden die Funktionen veröffentlicht und stehen in anderen Modulen zur Verfügung:

extern void uart_init()
Initialisiert den Soft-UART. Vor Verwendung des Soft-UARTs müssen Interrupts mit sei() global aktiviert worden sein (egal, ob vor oder nach dem Aufruf von uart_init).
extern void uart_putc (const char c)
Sendet das Zeichen c. Vorher wird gewartet, bis eine evtl. laufende Sendung beendet ist.
extern int uart_getc_wait()
Wartet bis zum nächsten Empfang bzw. liefert das empfangene Zeichen im Wertebereich 0...255.
extern int uart_getc_nowait()
Schaut nach, ob ein Zeichen empfangen wurde und liefert dieses gegebenenfalls als int zurück (Wertebereich ist 0...255). Wurde nichts empfangen, wird -1 geliefert.

Die Defines SUART_TXD und SUART_RXD zum Aktivieren der Code-Sequenzen für Transmitter resp. Receiver stehen hier im Header; das kann natürlich auch in die Kommandozeile/ins Makefile wandern und mit -DSUART_RXD etc. angeschaltet werden, Gleiches gilt für F_CPU und BAUDRATE.

#ifndef _UART_H_
#define _UART_H_

#define SUART_TXD
#define SUART_RXD

extern void uart_init();

#ifdef SUART_TXD
    extern void uart_putc (const char);
#endif // SUART_RXD

#ifdef SUART_RXD
    extern int uart_getc_wait ();
    extern int uart_getc_nowait();
#endif // SUART_RXD

#endif /* _UART_H_ */

Weblinks

Siehe auch