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.
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.
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.
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"
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 */