Programmieren der seriellen Schnittstelle:

Relativ häufig fragen Leser des Buches, wie man denn die serielle Schnittstelle programmiert. Einen Einstieg will ich mit den folgenden Informationen und Programmbeispielen ermöglichen.

1) Low-Level Sicht:

Kern der seriellen Schnittstelle ist der UART-Baustein. UART steht für Universal Universal Asynchronous Receiver/Transmitter. Der Urvater der UART Bausteine ist der 8250. Die kompatiblen Nachfolger haben die Bezeichnung 16450, 16550, 16650 und 16750. Ab dem 16550 enthalten die UART Bausteine einen internen Buffer, einen FIFO (First In First Out) Speicher. Die Größe dieses Buffers ist 16 Byte beim 16550, 32 Byte beim 16650 und 64 Byte beim 16750. Die UART Bausteine enthalten 8-Bit Register, die für die Konfiguration, also für die Programmierung oder als Sende- und Empfangsregister verwendet werden.

Register Name Adresse = BasisAdresse + Offset Zugriffsmode
Transmitter Holding Buffer 0 Write
Receiver Buffer Read
Divisor Latch Low Byte Read/Write
Interrupt Enable Register 1 Read/Write
Divisor Latch High Byte Read/Write
Interrupt Identification Register 2 Read
FIFO Control Register Write
Line Control Register 3 Read/Write
Modem Control Register 4 Read/Write
Line Status Register 5 Read
Modem Status Register 6 Read
Scratch Register 7 Read/Write

Die Standardeinstellungen eines PCs legen die Port-Adressen, also die Basisadresse des Bausteins für die ersten vier seriellen Schnittstellen auf folgende Adressen:

Schnittstelle Adresse Interrupt
COM1 03F8 4
COM2 02F8 3
COM3 03E8 4
COM4 02E8 3

Das Betriebssystem DOS und auch die Nachfolger Windows 3.1/3.11 bis zu Windows 98 erlauben jedem Programm den direkten Zugriff auf die Port-Adressen und damit auf die Register einer UART. Dadurch ist es möglich, diese Bausteine und damit die serielle Schnittstelle auf "Low-Level" - Art zu programmieren. C-Systeme für DOS stellen Funktionen für den Portzugriff zur Verfügung. Das sind Funktionen um auf eine Portadresse zu schreiben (outport, outportb bei Borland, _outp, _outpw, _outpd bei Microsoft) und Funktionen, mit denen man von einer Portadresse lesen kann (inport, inportb bei Borland, _inp, _inpw, _inpd bei Microsoft). Die zugehörigen Definitionsdateien sind dos.h (Borland) und conio.h (Microsoft). Zusätzlich gibt es die Möglichkeit C-Funktionen als Interrupt-Routinen zu verwenden. Die ersten beiden Programmbeispiele zeigen das Prinzip dieser Programmiertechnik.

2) High Level Sicht:

Windows NT und andere Multitask- und Multiuserbetriebssysteme erlauben es einem Programm nicht, direkt mit Portadressen zu arbeiten. Dies ist Sache der Gerätetreiber, die natürlich auch programmiert werden müssen und deren Programmierer wiederum mit diesen Details konfrontiert sind. Der Anwendungsprogrammierer in C oder C++ verwendet API-Funktionen oder sogar Klassen und kann auf einer relativ hohen Abstraktionsebene mit der Schnittstelle umgehen. Dies zeigt das Beispiel 3.

3) Links und Literaturhinweise:

Update am 1.1.2014: Die ursprünglich (~ 2003) hier angegebenen Links führen noch auf existierende Seiten:

http://www.beyondlogic.org
führt jetzt auf eine Seite mit umfangreichen Informationen zur USB Schnittstelle, die man ja durchaus auch als Nachfolger der seriellen UART Schnittstelle bezeichnen kann.

Den alten Inhalt dieser Quelle findet man nun auf: http://retired.beyondlogic.org, bis zum Abschnitt über die serielle Schnittstelle muss man etwas hinunterscrollen.

http://www.exar.com
exar ist immer noch ein wichtiger Hersteller von UART Bausteinen, das Portfolio der Firma ist jedoch bedeutend größer. D.h. mann muss auch auf dieser Seite etwas suchen.

Lange Zeit war das Buch PC intern von Michael Tischer / Bruno Jennrich , Verlag DATA BECKER ein Klassiker für Informationen zum Thema PC, DOS, Windows 95. Sucht man bei z.B. bei Amazon nach Michael Tischer, so werden als neue Bücher nur noch fremdsprachliche Bücher des Autors angeboten.

Es gibt jede Menge Bücher zum Thema Schnittstellen, Suchbegriffe: "PC Schnittstellen", "Interfacing the PC", "PC Interfacing"

4) Programm-Beispiele:

Um die Beispiele zu testen, muß an die serielle Schnittstelle ein Terminal oder ein zweiter PC angeschlossen werden. Am PC müssen Sie ein Kommunikationsprogramm (Terminalemulation) verwenden. Das Terminal oder der PC muß mit einem sogenannten Nullmodemkabel angeschlossen werden. Das ist eine Kabel mit zwei weiblichen Steckern, das je zwei Leitungen auskreuzt.
Terminaleinstellungen: 9600 Baud, 8 Bit, keine Parität, 1 Stopbit, (kein Protokoll)

Konfigurieren Sie die Schnittstelle zuerst mit dem Kommandozeilen Befehl Mode:

mode COM1 96,n,8,1

und testen Sie die grundsätzliche Funktion der Verbindung mit dem Kommando:

dir > COM1

Damit leiten Sie die Ausgaben des dir-Befehls auf die COM1-Schnittstelle um. Am angeschlossenen Terminal oder PC muß jetzt der Inhalt des aktuellen Verzeichnissen erscheinen.

Beispiel 1: Polling Methode (DOS, auch WIN 95/98, kompiliert mit Visual C++/6.0, LCC-Win32)

Beispiel 2: Interrupt Methode (DOS, auch WIN 95/98, kompiliert mit Turbo C 2.0)

Beispiel 3: Windows API Funktionen (WIN 95/98, Windows NT/2000, kompiliert mit Visual C++/6.0, LCC-Win32)

Die Beispiele 1 und 3 zeigen die Technik des "polling", d.h. der Buffer der Schnittstelle wird periodisch nach anliegenden Zeichen abgefragt. Da moderne UART Bausteine einen internen Buffer von 16 und mehr Byte haben, kann diese einfache Technik oft erfolgreich angewandt werden. Beispiel 1 und 3 implementieren folgenden Algorithmus:

Wiederhole
  Anliegende Zeichen von der serielle Schnittstelle lesen und in Zeichenkette ablegen
  Zustand der Zeichenkette auswerten, Kontrollausgabe
  Tastatur abfragen
Bis auf der Tastatur ESC eingegeben wird.

/*
termpoll.c
----------------------------------------------------------
Comment from K. Zeiner:
I found this good example program 1997 on the website:
www.senet.com.au/~speacock/

The commands _outp (oder outportb) and _inp (inportb) were 
used under DOS to write to and read from port adresses.
These commands were also valid on a WIN95 and WIN98 system, 
On a Windows NT / 2000 system a program with these commands
can not be executed.
------------------------------------------------------------
Written By: Craig Peacock <cpeacock@senet.com.au>
*/

#include <stdio.h>
#include <conio.h>

#define PORT 0x3F8   /* COM1 */

/* Defines Serial Ports Base Address
   COM1 0x3F8
   COM2 0x2F8
   COM3 0x3E8
   COM4 0x2E8
*/

int main(void)
{
  int checkBuffer;
  int c;
  _outp(PORT + 1 , 0);     /* Turn off interrupts */
  /* PORT - Communication Settings        */
  _outp(PORT + 3 , 0x80);  /* SET DLAB ON */
  _outp(PORT + 0 , 0x0C);  /* Set Baud rate - Divisor Latch Low Byte */
  /* Default 0x03 =  38,400 BPS */
  /*         0x01 = 115,200 BPS */
  /*         0x02 =  56,700 BPS */
  /*         0x06 =  19,200 BPS */
  /*         0x0C =   9,600 BPS */
  /*         0x18 =   4,800 BPS */
  /*         0x30 =   2,400 BPS */
  _outp(PORT + 1 , 0x00);  /* Set Baud rate - Divisor Latch High Byte */
  _outp(PORT + 3 , 0x03);  /* 8 Bits, No Parity, 1 Stop Bit           */
  _outp(PORT + 2 , 0xC7);  /* Configure FIFO Control Register         */ 
  _outp(PORT + 4 , 0x0B);  /* Turn on DTR, RTS, and OUT2              */
  printf("\nSample Comm's Program. Press ESC to quit \n");

  do {
    checkBuffer = _inp(PORT + 5); /* Check LSR to see if characters has been received */
    if (checkBuffer & 1) {
      c = _inp(PORT);             /* get the character  */
      printf("%c", c);            /* print character to screen  */
      if (c == 13) printf("\n");
      _outp(PORT, c);             /* write the character to the port */
    }
    if (kbhit()) {
      c = getch();        /* if a key is/was pressed, get character from keyboard */
      _outp(PORT, c);     /* send Char to Serial Port */
    }

 } while (c !=27);       /* Quit when ESC (ASCII 27) is pressed */

  return 0;
}

Die zweite Technik verwendet Interruptroutinen. Ein einlangendes Zeichen informiert das System mit einem Interrupt. Dieser Interrupt wird vom Betriebssystem dadurch behandelt, dass auf eine bestimmte Programmadresse gesprungen wird. Auf diese Adresse zeigt der Interruptvektor. Man kann nun eine sogenannte Interruptroutine schreiben, die statt dieser Standardbehandlung aufgerufen wird. Die Technik ist im wesentlichen:

Das Eintreffen der Zeichen und das Abarbeiten der Zeichen erfolgt nicht im gleichen Takt. Deshalb müssen die Zeichen in einem Buffer zwischengelagert werden. Das Programm verwendet dazu einen Ringbuffer.


/*
Comment: K. Zeiner
You need an old compiler (Turbo C , Microsoft C for DOS <= 7.0) to compile
this code.
You can use the executable only with DOS and with WIN95/WIN98.
*/

/* Name       : Sample Comm's Program - 1024 Byte Buffer - buff1024.c   */
/* Written By : Craig Peacock                                           */
/* Some comments added by Karlheinz Zeiner                              */
/* Copyright 1997 CRAIG PEACOCK                                         */
/* See http://www.beyondlogic.org/serial/serial1.htm                    */
/* for More Information                                                 */


#include <dos.h>
#include <stdio.h>
#include <conio.h>

#define PORT1 0x2E8  /* Port Address Goes Here */
#define INTVECT 0x0B /* Com Port's IRQ here (Must also change PIC setting) */

/* Defines Serial Ports Base Address
   COM1 0x3F8
   COM2 0x2F8
   COM3 0x3E8
   COM4 0x2E8             */

char ch;
char buffer[1025];    /* storage for ring-buffer */
int  bufferin = 0;    /* position for storing the next character */
int  bufferout = 0;   /* position for reading the next character */

void interrupt (*oldport1isr)();

/* Interrupt Service Routine (ISR) for PORT1 
	 This function is called, if the port receives a character */

void interrupt PORT1INT()  
{
  int c;

  do {
    /* get the content of the LSR (line status register) 
       if Bit 0 of LSR is set, than one or more data bytes are available */
    c = inportb(PORT1 + 5);
    if (c & 1) {	/* check Bit 0 */
      buffer[bufferin] = inportb(PORT1);  /* get the character and store it in the buffer */
      bufferin++;
      if (bufferin == 1024) { bufferin = 0; }  /* ring buffer */
    }
  } while (c & 1);	    /* while data ready */

  outportb(0x20,0x20);  /* clear the interrupt */
}

void main(void)
{
 int c;
 outportb(PORT1 + 1 , 0);        /* Turn off interrupts - Port1 */
 oldport1isr = getvect(INTVECT); /* Save old Interrupt Vector of later recovery */

 setvect(INTVECT, PORT1INT);     /* Set Interrupt Vector Entry */
                                 /* COM1 - 0x0C, COM2 - 0x0B, COM3 - 0x0C, COM4 - 0x0B */

 /*         PORT 1 - Communication Settings         */

  outportb(PORT1 + 3 , 0x80);  /* SET DLAB ON */
  outportb(PORT1 + 0 , 0x03);  /* Set Baud rate - Divisor Latch Low Byte */
            /* Default 0x03 =  38,400 BPS */
            /*         0x02 =  56,700 BPS */
            /*         0x06 =  19,200 BPS */
            /*         0x0C =   9,600 BPS */
            /*         0x18 =   4,800 BPS */
            /*         0x30 =   2,400 BPS */
  outportb(PORT1 + 1 , 0x00);  /* Set Baud rate - Divisor Latch High Byte */
  outportb(PORT1 + 3 , 0x03);  /* 8 Bits, No Parity, 1 Stop Bit */
  outportb(PORT1 + 2 , 0xC7);  /* FIFO Control Register */
  outportb(PORT1 + 4 , 0x0B);  /* Turn on DTR, RTS, and OUT2 */
  /* Set Programmable Interrupt Controller */
  /* COM1, COM3 (IRQ4) - 0xEF  */
  /* COM2, COM4 (IRQ3) - 0xF7  */
  outportb(0x21,(inportb(0x21) & 0xF7));
  outportb(PORT1 + 1 , 0x01);  /* Interrupt when data received */

  printf("\nSample Comm's Program. Press ESC to quit \n");

  do {
    if (bufferin != bufferout) {
      ch = buffer[bufferout];
      bufferout++;
      if (bufferout == 1024) {bufferout = 0;
    }
    printf("%c",ch);}
    if (kbhit()) {
      c = getch();
      outportb(PORT1, c);
    }
  } while (c != 27);

  outportb(PORT1 + 1 , 0);                 /* Turn off interrupts - Port1 */
  outportb(0x21, (inportb(0x21) | 0x08));  /* MASK IRQ using PIC */
                                           /* COM1 und COM3 (IRQ4) - 0x10  */
                                           /* COM2 unf COM4 (IRQ3) - 0x08  */

 setvect(INTVECT, oldport1isr);            /* Restore old interrupt vector */

}

Für die Konfiguration des Bausteines über die UART-Register, z.B. die Einstellung der Baudrate, benötigt man relativ viel Detailwissen. Auch C-System für DOS stellen einige weitere Funktionen zur Verfügung, die etwas einfacher zu handhaben sind. Die Turbo-C Bibliothek stellt die Funktion
bioscom(int cmd, char abyte, int port);
zur Verfügung. Mit dieser Funktion kann man die Schnittstelle konfigurieren und kann Zeichen ausgeben und einlesen.

Das dritte Programmbeispiel ist eine Windows32 Konsolanwendung und verwendet API-Funktionen. Dieses Programm läuft auch unter WIN NT/2000.
Ein Makro PERR wertet eventuelle Fehler aus. API Funktionen liefern meistens als Rückgabewert einen Erfolgs-/Fehlerstatus. Nach dem Aufruf einer API-Funktion prüft man in der Regel diesen Status.

Die Datei error.c enthält die Funktion perr, welche die Auswertung der Fehlerinformationen implementiert. Die Funktion stammt aus der Dokumentation zu Visual C++.

/* error.c */


#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>      

/*********************************************************************
* PURPOSE : report API errors. Allocate a new console buffer, display
*           error number and error text, restore previous console
*           buffer
* INPUT   : current source file name, current line number, name of the
*           API that failed, and the error number
* RETURNS : none
*********************************************************************/

/* maximum size of the buffer to be returned from FormatMessage */       
#define MAX_MSG_BUF_SIZE 512

void perr(PCHAR szFileName, int line, PCHAR szApiName, DWORD dwError)
{
  CHAR szTemp[1024];
  DWORD cMsgLen;
  CHAR *msgBuf;       /* buffer for message text from system */
  int iButtonPressed; /* receives button pressed in the error box */

  /* format our error message */
  sprintf(szTemp, "%s: Error %d from %s on line %d:\n", szFileName,
      dwError, szApiName, line);
  /* get the text description for that error number from the system */              
  cMsgLen = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM |
      FORMAT_MESSAGE_ALLOCATE_BUFFER | 40, NULL, dwError,
      MAKELANGID(0, SUBLANG_ENGLISH_US), (LPTSTR) &msgBuf, MAX_MSG_BUF_SIZE,
      NULL);
  if (!cMsgLen)
    sprintf(szTemp + strlen(szTemp), "Unable to obtain error message text! \n"
        "%s: Error %d from %s on line %d", __FILE__,
        GetLastError(), "FormatMessage", __LINE__);
  else
    strcat(szTemp, msgBuf);

  strcat(szTemp, "\n\nContinue execution?");
  MessageBeep(MB_ICONEXCLAMATION);
  iButtonPressed = MessageBox(NULL, szTemp, "Console API Error",
      MB_ICONEXCLAMATION | MB_YESNO | MB_SETFOREGROUND);
  /* free the message buffer returned to us by the system */
  if (cMsgLen)
    LocalFree((HLOCAL) msgBuf);
  if (iButtonPressed == IDNO)
    exit(1);
  return;

}

Die Definitionsdatei dazu:

/* error.h */
#define PERR(bSuccess, api) {if (!(bSuccess)) perr(__FILE__, __LINE__, \
   api, GetLastError());}
void perr(PCHAR szFileName, int line, PCHAR szApiName, DWORD dwError);

Das eigentliche Programm:

/* File:     serialcom.c
   Author:   Karlheinz Zeiner
   Purpose:  Sample C program (MS-Windows console application)
             Read from and write to a serial interface with API-functions.
   Platform: WIN95, WIN98, WIN-NT, ...
*/
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include "error.h"

#define ESC 27
#define EOL 13  /* end of line */        

void main(void)
{
  DCB dcb;      /* device control block */

                 
  HANDLE hCom;
  BOOL fSuccess;

  BOOL bLineEnd;
  char szLine[80];
  char cKb;
  int i;
  DWORD BytesRead, BytesWrite;
  COMMTIMEOUTS timeouts;  
  int portid;
  char *ComPort[] = {"COM1","COM2","COM3","COM4","COM5","COM6"}; 

  /* DCB and COMMTIMEOUTS are system-defined structures,             */
  /* HANDLE, BOOL, DWORD are predefined simple datatypes (typedef's) */
  /* The used constants are also defined in the windows-header files */
 
  printf("Port-Nummer [1..6]: ");
  scanf("%i", &portid);
  portid--;

  hCom = CreateFile(ComPort[portid],
     GENERIC_READ | GENERIC_WRITE,
     0,
     NULL,
     OPEN_EXISTING,
     0,               /* no overlapped I/O             */
     NULL);           /* must be NULL for comm devices */

  PERR(hCom != INVALID_HANDLE_VALUE, "CreateFile");

  fSuccess = GetCommState(hCom, &dcb);
  PERR(fSuccess, "GetCommState");

  /* configure the port */

  dcb.BaudRate = 9600;
  dcb.ByteSize = 8;
  dcb.Parity = NOPARITY;
  dcb.StopBits = ONESTOPBIT;
  dcb.fDtrControl = DTR_CONTROL_DISABLE;
  dcb.fInX = FALSE;

  fSuccess = SetCommState(hCom, &dcb);
  PERR(fSuccess, "SetCommState");

  fSuccess = GetCommTimeouts (hCom, &timeouts);
  PERR(fSuccess, "GetCommTimeouts");

  /* Only to show the content of the COMMTIMEOUTS structur */  
  printf("Timeout-values:\n"
    "ReadIntervalTimeout         = %u\n"
    "ReadTotalTimeoutMultiplier  = %u\n"
    "ReadTotalTimeoutConstant    = %u\n"
    "WriteTotalTimeoutMultiplier = %u\n"
    "WriteTotalTimeoutConstant   = %u\n", 
    timeouts.ReadIntervalTimeout,
    timeouts.ReadTotalTimeoutMultiplier,   
    timeouts.ReadTotalTimeoutConstant,
    timeouts.WriteTotalTimeoutMultiplier,  
    timeouts.WriteTotalTimeoutConstant);

  /*  
  Set  timeout to  0 to  force that:  If a character is in  the buffer, the
  character is read, If no character is in the buffer, the function do  
  not wait  and returns  immediatly
  */ 
  timeouts.ReadIntervalTimeout = MAXDWORD; 
  timeouts.ReadTotalTimeoutMultiplier = 0;
  timeouts.ReadTotalTimeoutConstant = 0; 

  fSuccess = SetCommTimeouts (hCom, &timeouts);
  PERR(fSuccess, "SetCommTimeouts");

  printf(
    "\n\n--------------------------------------------------------------------------\n"
    "Wait for inputs from the serial port\n"
    "Gets maximal 70 characters until a EndOfLine character (RETURN) is detected\n\n");

  i = 0;
  bLineEnd = FALSE;
  do {
    /* look for a character in the input buffer */ 
             
    ReadFile ( hCom, &szLine[i], 1, &BytesRead, NULL);
    if (BytesRead > 0) {  
      /* a character was read, show the character and the ASCII -Code */                 
      printf("%c<%03u>", szLine[i], szLine[i]);
      if (szLine[i] == EOL)  /* check end of line */
        bLineEnd = TRUE;
      i++;
    }

    if (bLineEnd || i > 70) {
      szLine[--i] = '\0';
      printf("\n%-s  (%3i characters\n)", szLine, i);
      /* Write the string back to the serial port */
      i = 0;
      WriteFile( hCom, "\n", 1, &BytesWrite, NULL);
      while (szLine[i]) {
        WriteFile( hCom, &szLine[i++], 1, &BytesWrite, NULL);
      }
      i = 0;
      bLineEnd = FALSE;
    }

    /* give us a chance to end the program with ESC from keyboard */
    if (kbhit())
       cKb = getch();
        
  } while (cKb != ESC); 

  fSuccess = CloseHandle(hCom);
  PERR(fSuccess, "CloseHandle");

} /* end main */