![]() |
Linux, PC und HardwareProf. Jürgen Plate |
Ein Byte besteht dann aus einer Folge von acht Datenbits, die von Start- und Stoppbit eingerahmt werden. Zwischen zwei aufeinanderfolgenden Bytes können sich auch beliebig lange Pausen befinden, da der Beginn eines Zeichens am Startbit eindeutig erkannt wird. Daher nennt man diese Form der Übertragung "asynchron". Durch die asynchrone Übertragung wird die Übertragungsrate gesenkt, da für z. B. 8 Informationsbits 10 Bits über die Leitung gesendet werden.
Die Datenrate wird in Bit pro Sekunde (bps) bzw. Baud (nach dem französischen Ingenieur und Erfinder Jean Maurice Émile Baudot) angegeben. Dabei werden alle Bits (auch Start- und Stoppbit) gezählt und Lücken zwischen den Bytetransfers ignoriert. Deshalb ist die Baudrate der reziproke Wert der Länge eines Bits. Als Datenraten sind folgende Werte üblich:
150, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600 und 115200
Nach dem Stoppbit kann sofort wieder eine neue Übertragung mit einem Startbit beginnen. Zur Vermeidung von Datenverlusten muss der Empfänger die Datenübertragung anhalten können, wenn keine weiteren Daten mehr verarbeitet werden können. Dieses sogenannte Handshake kann auf zwei Arten realisiert werden:
Wer glaubt, die serielle Kommunikation über Modem sei im Zeitalter von DSL tot, der irrt. Einerseits gibt es auch in den blühenden DSL-Landschaften des Telekom-Einzugsbereichs noch genügend weiße Flecken, nämlich da, wo das Ziehen von Glasfaserleitungen noch richtig ins Geld geht - also ab etwa 10 km von der Stadtgrenze entfernt. In diesen Gegenden erfolgt die Internetanbindung oft noch über ISDN oder Modem.
Aber auch eine relativ neue Form von Modem kommuniziert seriell mit dem Computer, die sogenannten GSM-Modems. GSM ist eine Abkürzung für "Global System for Mobile Communications" (früher "Groupe Spécial Mobile") und ein Standard für volldigitale Mobilfunknetze, der hauptsächlich für Telefonie, aber auch für leitungsvermittelte und paketvermittelte Datenübertragung sowie Kurzmitteilungen (Short Messages, SMS) genutzt wird. In Deutschland ist GSM die technische Grundlage der D- und E-Netze. Der Standard wird heute in 670 GSM-Mobilfunknetzen in weltweit rund 200 Ländern genutzt. Später wurde der Standard um Protokolle wie HSCSD, GPRS und EDGE zur schnelleren Datenübertragung erweitert. GSM ist heute schon ein Standard für die M2M-Kommunikation (Maschine zu Maschine), d.\,h. dass vielfach Überwachungs- und Telemetrieaufgaben per GSM drahtlos über das GSM-Netz erfolgen, wobei sich die GSM-Modems auf die gleiche Art und Weise ansprechen lassen, wie die uralten 2400-BPS-Modems.
Port | Funktion | Bem. |
---|---|---|
Base | Sendedaten (TDR) Empfangsdaten (RDR) | (1) |
Base | Baudrate (niederwertiges Bit) | (2) |
Base+1 | Interrupt Enable Register (IER) | (1) |
Base+1 | Baudrate (höherwertiges Bit) | (2) |
Base+2 | Interrupt ID (IID) | |
Base+3 | Line Control | |
Base+4 | Modem Control | |
Base+5 | Line Status | |
Base+6 | Modem Status |
(1) Bit 7 im Line Control Register = 0}
(2) Bit 7 im Line Control Register = 1}
Im Interruptbetrieb muss für die Schnittstelle auch eine IRQ-Leitung existieren, über die der Interrupt ausgelöst werden kann. Deshalb ist zusätzlich noch eine Programmierung des Interrupt-Controllers 8259 im PC notwendig. Beim Interrupt-Control-Register gilt, dass eine 0 den entsprechenden Interrupt sperrt und eine 1 ihn freigibt. Glücklicherweise wird aber auch dies vom Linux-Schnittstellentreiber erledigt.
00 | Wechsel im Modem-Status (Eingangsleitungen, Priorität 4) |
01 | Sende-Register leer (Sende-Interrupt, Priorität 3) |
10 | Empfang eines neuen Bytes (Priorität 2) |
11 | Overrun-, Framing- oder Parity-Error bzw. Break-Signal empfangen (Priorität 1). |
Die Übertragungsgeschwindigkeit der seriellen Schnittstelle wird mittels der beiden Baudrate-Register eingestellt. Dabei wird der Teilerwert (1 Word) über die ganzzahlige Division
Teiler = 115200/Baudrateermittelt und der LSB-Teil des Teilerwerts ins Baud-Rate-Register LSB (Basisadresse, DLAB = 1) und der MSB-Teil ins Baud-Rate-Register MSB (Basisadresse +1 , DLAB = 1) geschrieben. Für die Initialisierung auf 9600 Baud ergibt beispielsweise die obige Formel 115200 / 9600 = 12 = 0CH. Damit wird das Baud-Rate-Register LSB mit 0CH und das Baud-Rate-Register MSB mit 00H initialisiert.
Den Status der Eingangsleitungen des Bausteins liefern die Statusregister für Modem und Leitungen:
Neben der Masseleitung und den Datenleitungen gibt es noch eine ganze Reihe von Leitungen, die den Verkehr zwischen Rechner und Peripherie steuern. Meist interessieren aber nur einige Leitungen, um den Verkehr zwischen Computer und Peripherie oder zwischen zwei Computern aufrechtzuerhalten. Die anderen Leitungen bleiben unbeschaltet oder werden auf einen festen Pegel gelegt. Die wichtigsten Leitungen sind:
ITU | DIN | US | 25-pol. | 9-pol. | Beschreibung | Richt. |
---|---|---|---|---|---|---|
101 | E 1 | - | 1 | - | Schutzerde | - |
102 | E 2 | GND | 7 | 5 | Signalerde (Ground) | - |
103 | D 1 | TXD | 2 | 3 | Sendedaten (Transmit Data) | ← |
104 | D 2 | RXD | 3 | 2 | Empfangsdaten (Receive Data) | → |
105 | S 2 | RTS | 4 | 7 | Sendeteil einschalten (Request To Send) | ← |
106 | M 2 | CTS | 5 | 8 | Sendebereitschaft (Clear To Send) | → |
107 | M 1 | DSR | 6 | 6 | Betriebsbereitschaft (Data Set Ready) | → |
108.2 | S 1.2 | DTR | 20 | 4 | Terminal ist bereit (Data Terminal Ready) | ← |
109 | M 5 | DCD | 8 | 1 | Empfangspegel (Data Carrier Detect) | → |
125 | M 3 | RI | 22 | 9 | Ankommender Ruf (Ring Indicator) | → |
Die Richtungsangabe bezieht sich auf die Peripherie (→ bedeutet Peripherie nach PC, ← entsprechend PC nach Peripherie). Werden Peripherie/Modem (DÜE = Datenübertragungseinrichtung) und Computer (DEE = Datenendeinrichtung) miteinander verbunden, verwendet man ein Kabel mit einer 1:1-Verbindung der wichtigsten Leitungen.
Sollen hingegen zwei Computer direkt, also ohne Modem miteinander verbunden werden, müssen die Leitungen gekreuzt werden. Werden die Steuerleitungen gleich im Stecker zurückgeführt (RTS auf CTS, DTR auf DSR), kommt man mit einer dreiadrigen Verbindung aus. So eine direkte Verbindung zwischen zwei Computern wird allgemein auch als "Nullmodem" bezeichnet, denn der Datenverkehr kann genauso ablaufen wie bei über Modem verbundenen Computern.
Bei allen Projekten mit seriellen Schnittstellen gilt nach wie vor das Prinzip "Versuch und Irrtum". Das beginnt beim Anschluss des Geräts an den Computer mit der Frage: direkte Verbindung oder gekreuzte Leitungen ("Nullmodemkabel", siehe oben)? Es endet oft damit, dass ein individuelles Kabel gelötet wird. Wenn die Geräte dann rein elektrisch miteinander kommunizieren, beginnt das gleiche Spiel bei der Software: Schnittstellenparameter, Handshake-Format, Protokoll, Zeilenende mit Carriage Return oder Linefeed (oder beidem). Wie lange soll auf ein Zeichen gewartet werden? Und so weiter... Lassen Sie sich also nicht gleich entmutigen! Oft hilft ein Schnittstellentester mit LED-Anzeige, der zwischen Computer und Gerät geschaltet wird.
Bei der Pegelwandlung von TTL nach RS232 können wir praktischerweise auf einen bewährten integrierten Baustein zurückgreifen, den MAX232 von Maxim, um den sich inzwischen eine ganze Familie von Pegelwandlern mit verschiedenen Gehäusebauformen und unterschiedlichen elektrischen Eigenschaften gebildet hat. Der Baustein kann zwei Leitungen von TTL nach RS232 umsetzen und zwei weitere Leitungen von RS232 nach TTL. Somit kann ein Baustein die Sende- und Empfangsleitung (TXD, RXD) und zwei Handshakeleitungen (z. B. CTS, RTS) umsetzen, was in vielen Fällen ausreicht. Zusätzlich sind im Chip noch zwei Ladungspumpen integriert, welche die benötigten Spannungen von +12 V und -12 V erzeugen. Für diese Spannungswandler werden als externe Beschaltung lediglich vier Kondensatoren benötigt (1-uF-Tantal-Elkos). Im folgenden Bild (nach Maxim-Unterlagen) sind die Innenschaltung des Bausteins, die Beschaltung mit den Kondensatoren sowie das Pin-Layout zu sehen.
Die nötigen Grundlagen für die Programmierung liefern die beiden HOWTO-Dokumente, das "Linux Serial HOWTO" von David Lawyer, das "Linux Serial Programming HOWTO" von Peter H. Baumann und das "Text-Terminal-HOWTO" von David Lawyer (Die HOWTOs werden bei den meisten Distributionen schon mit installiert oder lassen sich über das Dokumentationspakete nachinstallieren - sie sind aber auch im Internet oder hier auf der Webseite zu finden). Da es sich bei den seriellen Schnittstellen um normale Gerätedateien handelt, gelten natürlich auch die entsprechenden Regeln der Programmierung für das Ansprechen von Geräten. Dafür können wir auf POSIX-standardisierte Funktionen zurückgreifen. Eine sehr gute Beschreibung finden Sie im GNU C-Library Reference Manual, welches den meisten Distributionen beiliegt (dort Kap. 12). Hier will ich Ihnen eine kurze Zusammenfassung des Terminal-IO bieten, was für die meisten Anwendungen ausreichen sollte.
Um mit einem User-Programm auf die serielle Schnittstelle zugreifen zu können, muss der entsprechende User auch die nötige Schreib- und Leseberechtigung für das Device haben (was normalerweise nicht der Fall ist). In der Regel reicht es, die User, die mit der seriellen Schnittstelle arbeiten, mit in die Gruppe uucp aufzunehmen (ggf. hilft ein Blick auf die Zugriffsrechte der Devices und in die Datei /etc/group). Sinnvoll ist auch das Anlegen eines eigenen (Pseudo-)Users für die Steuerungsprogramme.
Fast alle Veränderungen an den Übertragungsparametern von Terminals oder seriellen Schnittstellen erzielen Sie mit der Struktur termios (Terminal-IO-Settings). Diese Struktur besteht aus fünf Teilen, nämlich vier 32-Bit-Masken für die verschiedenen Flags, der line discipline und dem c_cc-Array, das weitere Parameter, z. B. Wartezeiten nach dem Senden bestimmter Zeichen und die Definition von Steuerzeichen, aufnimmt (adressiert wird es über Präprozessordefinitionen in /usr/include/termbits.h). Sie hat demnach folgendes Aussehen:
struct termios { /* Bitmaske fuer die Eingabe-Flags */ tcflag_t c_iflag; /* Bitmaske fuer die Ausgabe-Flags */ tcflag_t c_oflag; /* Bitmaske fuer die Control-Flags */ tcflag_t c_cflag; /* Bitmaske fuer lokale Einstellungen */ tcflag_t c_lflag; /* line discipline */ char __c_line; /* Array fuer Sonderzeichen/-funktionen */ cc_t c_cc[NCCS]; }Mit dem Shell-Kommando stty -a </dev/ttyS0 können Sie sich jederzeit alle Werte dieser Struktur anzeigen lassen. Mit diesem Kommando kann man auch zahlreiche Parameter setzen. Für den Zugriff auf die termios-Daten gibt es zwei Bibliotheksfunktionen:
int tcgetattr(int filedes, struct termios *termios_p) /* liefert aktuelle Terminal-Settings */Beiden Funktionen wird neben dem Zeiger auf eine Variable vom Typ termios auch der Dateideskriptor des Terminal-Devices übergeben. tcsetattr erwartet zusätzlich den Parameter when, mit dem sich festlegen lässt, wann die neuen Einstellungen übernommen werden sollen. Es gibt drei Möglichkeiten:int tcsetattr(int filedes, int when, const struct termios *termios_p) /* setzt Terminal-Settings */
TCSANOW: Einstellungen sofort ändern TCSADRAIN: Einstellungen ändern, nachdem alle eventuell noch gepufferten Daten gesendet wurden TCSAFLUSH: Einstellungen sofort ändern und Eingabepuffer löschenTCSAFLUSH wäre also neben TCSANOW eine gute Wahl. Theoretisch ließe sich die Übertragungsgeschwindigkeit auch mit der termios-Struktur und tcsetattr() einstellen. Davon wird im GNU-Handbuch jedoch ohne Angabe von Gründen abgeraten. Zum Einstellen der Übertragungsgeschwindigkeit gibt es nämlich eine weitere Bibliotheksfunktion:
Alle genannten Funktionen liefern im Erfolgsfall den Wert 0 zurück und im Fehlerfall -1. Die Komplexität des Terminal-IO entsteht durch zahllose Flags, aus denen die vier Bitmasken zusammengesetzt sind. Die Flags und auch die termios-Struktur sind in der Datei /usr/include/termbits.h definiert und unter anderem im Mini-HOWTO dokumentiert.
Normalerweise werden nicht alle Flags benötigt, um die serielle Schnittstelle zum Laufen zu bringen. Deshalb will ich im Folgenden nur die wichtigsten von ihnen behandeln. Beginnen wir mit den Eingabeflags in c_iflag:
IGNBRK ignoriere Breaks BRKINT beachte Breaks IGNPAR ignoriere Parität INLCR ersetze NL durch CR IGNCR ignoriere CR ICRNL ersetze CR durch NL IUCLC Großbuchstaben in Kleinbuchstaben umwandeln IXON XON/XOFF-Flusssteuerung einschalten IXANY Ausgabe fortsetzen mit einem beliebigen Zeichen IXOFF XON/XOFF-Flusssteuerung ausschalten IMAXBEL akustisches Signal, wenn der Puffer voll ist (Zeilenende)Die Ausgabeflags (c_oflag) sind noch wesentlich zahlreicher als die Eingabeflags, doch brauchen wir in der Regel nur wenige von ihnen. Meist reichen die folgenden:
ONLCR ersetze NL durch CR OCRNL ersetze CR durch NL ONOCR Unterdrücken von CR in Spalte 0 ONLRET eein CR senden OFILL Füllzeichen NUL senden anstelle einer Pause OFDEL Füllzeichen ist DEL statt NULDie dritte Gruppe von Flags, c_cflag, ist für die Übertragungsgeschwindigkeit und das Datenformat zuständig. Zunächst die Flags für die Geschwindigkeit:
B0 hang up B50 50 bps B75 75 bps B110 110 bps B150 150 bps B300 300 bps B600 600 bps B1200 1200 bps B1800 1800 bps B2400 2400 bps B4800 4800 bps B9600 9600 bps B19200 19200 bps B38400 38400 bps B57600 57600 bps B115200 115200 bpsDie anderen Flags dieser Gruppe steuern das Datenformat:
CS5 5 Bit CS6 6 Bit CS7 7 Bit CS8 8 Bit CSTOPB 2 Stoppbits statt einem CREAD Empfangsteil aktivieren PARENB Paritätsbit erzeugen PARODD ungerade Parität statt gerader HUPCL Verbindungsabbruch bei Ende des letzten Prozesses CLOCAL Terminal lokal angeschlossen (ignoriere CD) CRTSCTS Hardware-Handshake einschalten CIGNORE ignoriere ControlflagsAus der letzten Gruppe Flags (c_lflag) brauchen wir nur wenige:
ECHO Einschalten der ECHO-Funktion ICANON Zeilenorientierter Eingabemodus (kanonischer Modus) ISIG bestimmte Sonderzeichen lösen ein Signal aus (z. B. Ctrl-C) XCASE Umwandeln von eingegebenen Groß- in KleinbuchstabenGrundsätzlich unterscheidet man beim Terminal-IO zwei Arten:
Wenn das Programm nicht ewig auf eine Eingabe warten soll, nimmt man also am besten den dritten Fall.
Die folgenden Funktionen zum Öffnen, Lesen, Schreiben und Schließen der Geräteschnittstelle sind im Skript zur C-Programmierung beschrieben.
file_descr = open("/dev/ttyS0", O_RDWR | O_NDELAY | O_NOCTTY); /* Modus: read nicht nicht */ /* write warten controlling entity */Sobald die Gerätedatei erfolgreich geöffnet ist, stellen Sie O_NDELAY sofort wieder ab, da sonst zukünftige read()-Kommandos nicht auf Daten warten, sondern immer sofort zurückkommen und damit ein lastintensives "busy waiting" durchführen (fcntl( filedescriptor, F_SETFL, O_RDWR );). Eine Funktion zum Öffnen eines seriellen Ports könnte also folgendermaßen aussehen:
int open_port(int port) { /* * Oeffnet seriellen Port * Gibt das Filehandle zurueck oder -1 bei Fehler * der Parameter port muss 0, 1, 2 oder 3 sein * * RS232-Parameter * - 19200 baud * - 8 bits/byte * - no parity * - no handshake * - 1 stop bit */ int fd; struct termios options; switch (port) { case 0: fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY); break; case 1: fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY | O_NDELAY); break; case 2: fd = open("/dev/ttyS2", O_RDWR | O_NOCTTY | O_NDELAY); break; case 3: fd = open("/dev/ttyS3", O_RDWR | O_NOCTTY | O_NDELAY); break; default: fd = -1; } if (fd >= 0) { /* get the current options */ fcntl(fd, F_SETFL, 0); if (tcgetattr(fd, &options) != 0) return(-1); bzero(&options, sizeof(options)); /* Structure loeschen, ggf vorher sichern und bei Programmende wieder restaurieren */ cfsetspeed(&options, B19200); /* setze 19200 bps */ /* Alternativ: */ * cfsetispeed(&options, B19200); * * cfsetospeed(&options, B19200); */ /* setze Optionen */ options.c_cflag &= ~PARENB; /* kein Paritybit */ options.c_cflag &= ~CSTOPB; /* 1 Stoppbit */ options.c_cflag &= ~CSIZE; /* 8 Datenbits */ options.c_cflag |= CS8; options.c_cflag |= (CLOCAL | CREAD);/* CD-Signal ignorieren */ /* Kein Echo, keine Steuerzeichen, keine Interrupts */ options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); options.c_oflag &= ~OPOST; /* setze "raw" Input */ options.c_cc[VMIN] = 0; /* warten auf min. 0 Zeichen */ options.c_cc[VTIME] = 10; /* Timeout 1 Sekunde */ tcflush(fd,TCIOFLUSH); if (tcsetattr(fd, TCSAFLUSH, &options) != 0) return(-1); } return(fd); }Benötigt werden meist die folgenden Headerdateien:
#include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <termios.h> #include <strings.h>Bei Bedarf werde noch die drei Headerdateien
#include <signal.h> #include <string.h> #include <errno.h>zusätzlich benötigt.
Nach dem Öffnen können Sie nach Herzenslust mit read() und write() seriell Daten empfangen und senden. Jedoch kann - wie schon angedeutet - immer mal etwas Ungewöhnliches passieren.
Für das Senden wird in der Regel die Funktion write() verwendet, deren erster Parameter der Filedeskriptor ist. Weitere Parameter sind die Adresse des Sendepuffers und die Anzahl der zu sendenden Bytes. Es muss auf jeden Fall überprüft werden, wieviele Bytes gesendet wurden (Rückgabewert von write()) und ob auch alle Bytes gesendet wurden.
int sendbytes(char * Buffer, int Count) /* Sendet Count Bytes aus dem Puffer Buffer */ { int sent; /* return-Wert */ /* Daten senden */ sent = write(fd, Buffer, Count); if (sent < 0) { perror("sendbytes failed - error!"); return -1; } if (sent < Count) { perror("sendbytes failed - truncated!"); } return sent; }
Für das Empfangen wird in der Regel die Funktion read() verwendet, deren erster Parameter der Filedeskriptor ist. Weitere Parameter sind die Adresse des Sendepuffers und die maximale Anzahl der zu empfangenden Bytes. Die Funktion gibt die Anzahl der empfangenen Bytes zurück, wobei dieser Wert auch 0 sein kann. Das Verhalten von read() hängt von den Konfigurationswerten c_cc[VTIME] und c_cc[VMIN] ab. Bei der in open_serial() getroffenen Einstellung kehrt read() auf jeden Fall nach einer Sekunde zurück, ggf. ohne Zeichen empfangen zu haben. Dies ist bei der Programmierung zu berücksichtigen.
Das erste Programmfragment liest bis zu 100 Zeichen in einen Puffer:
char buf[101]; /* Eingabepuffer */ int anz; /* gelesene Zeichen */ ... anz = read(fd, (void*)buf, 100); if (anz < 0) perror("Read failed!"); else if (anz == 0) perror("No data!"); else { buf[anz] = '\0'; /* Stringterminator */ printf("%i Bytes: %s", anz, buf); } ...Das Verfahren eignet sich insbesondere dann, wenn Sie wissen, wieviele Bytes zu erwarten sind. Andernfalls gehen Sie vorsichtiger vor und lesen zeichenweise. Diese Methode eignet sich auch gut, wenn auf ein bestimmtes empfangenes Byte reagiert werden soll (Enter, Newline etc.):
char buf[101]; /* Eingabepuffer für die komplette Eingabe */ int anz; /* gelesene Zeichen */ char c; /* Eingabepuffer fuer 1 Byte */ int i; /* Zeichenposition bzw Index */ ... i = 0; do /* Lesen bis zum Carriage Return, max. 100 Bytes */ { anz = read(fd, (void*)&c, 1); if (anz > 0) { if (c != '\r') buf[i++] = c; } } while (c != '\r' && i < 100 && anz >= 0); if (anz < 0) perror("Read failed!"); else if (i == 0) perror("No data!"); else { buf[i] = '\0'; /* Stringterminator */ printf("%i Bytes: %s", i, buf); } ...Sie sehen schon, das Empfangen wirft mehr Probleme auf, als das Senden. Hier muss immer eine speziell an die Kommunikation angepasste Lösung entwickelt werden. Das folgende Beispiel vermeidet das byteweise Lesen und füllt den Eingabepuffer, bis ein Zeilenende gesendet wurde (danach wartet die andere Station normalerweise, bis sie wieder etwas bekommt).
char buf[1000]; /* Eingabepuffer für die komplette Eingabe */ char *bufptr; /* aktuelle Position in buf */ int nbytes; /* Number of bytes read */ int tries; /* Number of tries so far */ int anz; /* gelesene Zeichen */ char c; /* Eingabepuffer fuer 1 Byte */ int i; /* Zeichenposition bzw Index */ ... /* Bytes in den Puffer einlesen, bis ein CR or NL auftaucht */ bufptr = buf; /* Achtung: Etwas seltsame Pointer-Arithmetik */ while ((anz = read(fd, bufptr, buf + sizeof(buf) - bufptr - 1)) > 0) { if (anz < 0) { perror("Read failed!"); return -1; } bufptr += anz; /* CR oder NL am Ende? */ if ((bufptr[-1] == '\n') || (bufptr[-1] == '\r')) break; /* Schleife verlassen */ } /* Stringterninator anhaengen */ *bufptr = '\0'; printf("%s", buf); ...Noch besser ist es, das Einlesen einer Zeile in eine Funktion zu verlagern. Die folgende Funktion get_line() liest von einer vorhergeöffneten Schnittstelle genau eine Zeile ein (der Unterstrich in get_line() ist notwendig, weil getline() eine Bibliotheksfunktion ist). Als Parameter werden neben dem Filedescriptor das Array und dessen maximale Länge übergeben:
int get_line(int fd, char *buffer, unsigned int len) { /* read a '\n' terminated line from fd into buffer * of size len. The line in the buffer is terminated * with '\0'. It returns -1 in case of error and -2 if the * capacity of the buffer is exceeded. * It returns 0 if EOF is encountered before reading '\n'. */ int numbytes = 0; int ret; char buf; buf = '\0'; while ((numbytes <= len) && (buf != '\n')) { ret = read(fd, &buf, 1); /* read a single byte */ if (ret == 0) break; /* nothing more to read */ if (ret < 0) return -1; /* error or disconnect */ buffer[numbytes] = buf; /* store byte */ numbytes++; } if (buf != '\n') return -2; /* numbytes > len */ buffer[numbytes-1] = '\0'; /* overwrite '\n' */ return numbytes; }Nachteil dieser Lösung ist die Geschwindigkeit bzw. deren Fehlen. Durch die vielen read()-Aufrufe ist die Funktion ziemlich langsam. Besser wäre eine Lösung, bei der ein Datenpaket komplett eingelesen und dann Zeile für Zeile ans aufrufende Programm weitergereicht wird. Genau das macht die folgende Funktion, bei der die Parameter die gleiche Aufgabe haben wie oben. Diese Funktion hat einen internen Puffer, der mittels read() gefüllt wird und dessen Inhalt Stück für Stück bei jedem Aufruf weitergegeben wird. Dazu verwendet die Funktion die statischen Variablen bufptr, count und mybuf, deren Werte erhalten bleiben und bei jedem Aufruf wieder zur Verfügung stehen. Werden mit read() mehrere Zeilen gelesen, bleibt der jeweilige Rest in mybuf erhalten und wird beim nächsten Aufruf der Funktion verarbeitet:
int readline(int fd, char *buffer, unsigned int len) { /* read a '\n' terminated line from fd into buffer * bufptr of size len. The line in the buffer is terminated * with '\0'. It returns -1 in case of error or -2 if the * capacity of the buffer is exceeded. * It returns 0 if EOF is encountered before reading '\n'. * Notice also that this routine reads up to '\n' and overwrites * it with '\0'. Thus if the line is really terminated with * "\r\n", the '\r' will remain unchanged. */ static char *bufptr; static int count = 0; static char mybuf[1500]; char *bufx = buffer; char c; while (--len > 0) /* repeat until end of line */ { /* or end of external buffer */ count--; if (count <= 0) /* internal buffer empty --> read data */ { count = read(fd, mybuf, sizeof(mybuf)); if (count < 0) return -1;/* error or disconnect */ if (count == 0) return 0; /* nothing to read - so reset */ bufptr = mybuf; /* internal buffer pointer */ } c = *bufptr++; /* get c from internal buffer */ if (c == '\n') { *buffer = '\0'; /* terminate string and exit */ return buffer - bufx; } else { *buffer++ = c; /* put c into external buffer */ } } return -2; /* external buffer to short */ }
void alarm_handler(void) { timeout = 1; } signal(SIGALRM, alarm_handler); alarm(60); /* Wartezeit setzen */ ... if (read(file_descr,buffer,1) != 1 && errno == EINTR && timeout) { fprintf(stderr,"TIMEOUT!\n"); break; } ... alarm(0); /* Alarm abschalten */Mit diesen Informationen lassen sich nicht-interaktive Programme für die serielle Schnittstelle schreiben. Was noch fehlt, ist die Möglichkeit, mehrere Schnittstellen gleichzeitig zu überwachen, beispielsweise gleichzeitig Schnittstelle und Tastatur. Je nach Programm würde read() so lange warten, bis etwas an der Schnittstelle eintrifft, auch wenn Sie in der Zwischenzeit wie wild auf die Tastatur hämmern. Eine Möglichkeit wäre, über O_NDELAY zu arbeiten oder VMIN auf 0 zu setzen. Das ist jedoch beides nicht effizient, weil das Programm "busy waiting" mit voller CPU-Leistung angestrengt auf Daten wartet. Glücklicherweise gibt es einen Mechanismus, mit dem man warten kann, bis auf einem File-Deskriptor etwas zum Lesen bereitliegt, nämlich select(). Der Aufrufer übergibt der select()-Funktion eine Liste mit File-Deskriptoren, von denen gelesen oder auf die geschrieben werden soll. Sobald es möglich ist (Daten eingetroffen oder Ausgabewarteschlange frei), kehrt select() mit der entsprechenden Information zurück. Zusätzlich kann man einen Timeout definieren, der angibt, nach welcher Zeit die Funktion in jedem Fall aufgeben soll. Das folgende Beispiel ist dem Serial-Programming-HOWTO entnommen:
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> main() { int fd1, fd2; /* input sources 1 and 2 */ fd_set readfs; /* file descriptor set */ int maxfd; /* maximum file desciptor used */ int loop=1; /* loop while TRUE */ /* open_input_source opens a device, sets the port correctly, and returns a file descriptor */ fd1 = open_input_source("/dev/ttyS0"); if (fd1<0) exit(0); fd2 = open_input_source("/dev/ttyS1"); if (fd2<0) exit(0); maxfd = MAX (fd1, fd2)+1; /* maximum bit entry (fd) to test */ /* loop for input */ while (loop) { FD_SET(fd1, &readfs); /* set testing for source 1 */ FD_SET(fd2, &readfs); /* set testing for source 2 */ /* block until input becomes available */ select(maxfd, &readfs, NULL, NULL, NULL); if (FD_ISSET(fd1)) /* input from source 1 available */ handle_input_from_source1(); if (FD_ISSET(fd2)) /* input from source 2 available */ handle_input_from_source2(); } }Statt der zweiten seriellen Schnittstelle könnten Sie auch mittels FD_SET(STDIN, &readfs); die Tastatur in select() einklinken und dann wahlweise Daten der Tastatur und der RS232 bearbeiten. Das Beispiel blockiert, bis Daten eintreffen. Will man nach einer bestimmten Zeit abbrechen, muss nur der select()-Aufruf geändert werden:
int res; struct timeval Timeout; /* set timeout value within input loop */ Timeout.tv_usec = 0; /* milliseconds */ Timeout.tv_sec = 1; /* seconds */ res = select(maxfd, &readfs, NULL, NULL, &Timeout); if (res == 0) /* number of file descriptors with input = 0, timeout occurred. */Bei diesem Beispiel gibt es nach einer Sekunde einen Timeout. In diesem Fall liefert select() 0 zurück.
Prinzipiell wäre das Lesen und Setzen der Statusinfo über das Modem-Control- und das Modem-Status-Register möglich. Meist ist es jedoch sinnvoller, die Standard-Systemcalls zu verwenden und die Programmierung via Betriebssystem zu erledigen. Dann ist das Programm auch zu anderer Schnittstellen-Hardware kompatibel.
Zum Abfragen der Statusleitungen gibt es den ioctl()-Aufruf mit den Parametern TIOCMGET (Status lesen) und TIOCMSET (Status schreiben). ioctl() liefert einen Integerwert zurück, in dem für jede Leitung ein dem Leitungspegel entsprechendes Bit gesetzt oder gelöscht ist. Die Bits sind über symbolische Konstanten (TIOCM_DTR, TIOCM_RTS usw.) ansprechbar. Ein Beispielprogramm, das alle Leitungen auf einem gegebenen Port überwacht und anzeigt, findet sich im folgenden Listing. Setzen kann ein Programm die Leitungen alle gemeinsam über den Aufruf ioctl(fd,TIOCMSET,&status). Die Bits in der Variablen status haben dieselbe Bedeutung wie bei TIOCMGET.
Pin | Bitmaske | Konstante | I/O | ![]() |
---|---|---|---|---|
DTR | 0002 | TIOCM_DTR | → | |
RTS | 0004 | TIOCM_RTS | → | |
CTS | 0020 | TIOCM_CTS | ← | |
CD | 0040 | TIOCM_CAR | ← | |
RI | 0080 | TIOCM_RNG | ← | |
DSR | 0100 | TIOCM_DSR | ← |
Das Listing enthält Funktionen zum Setzen und Rücksetzen von RTS und DTR, Funktionen für das Abfragen der Einzelwerte von CTS, CD, RI und DSR sowie eine Funktion zum Abfragen des Gesamtstatus (mit allen sechs Werten). Das Hauptprogramm demonstriert deren Anwendung.
#include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> int fd = 0; void SetRTS(int fd) /* Setzt RTS auf ON */ { int currstat; ioctl(fd, TIOCMGET, &currstat); currstat |= TIOCM_RTS; ioctl(fd, TIOCMSET, &currstat); } void ResetRTS(int fd) /* Setzt RTS auf OFF */ { int currstat; ioctl(fd, TIOCMGET, &currstat); currstat &= ~TIOCM_RTS; ioctl(fd, TIOCMSET, &currstat); } void SetDTR(int fd) /* Setzt DTR auf ON */ { int currstat; ioctl(fd, TIOCMGET, &currstat); currstat |= TIOCM_DTR; ioctl(fd, TIOCMSET, &currstat); } void ResetDTR(int fd) /* Setzt DTR auf OFF */ { int currstat; ioctl(fd, TIOCMGET, &currstat); currstat &= ~TIOCM_DTR; ioctl(fd, TIOCMSET, &currstat); } void ClearSerPins(int fd) /* Setzt alle Pins der Schnittstelle auf OFF */ { int currstat = 0; ioctl(fd, TIOCMSET, &currstat); } int GetRI(int fd) /* Gibt 1 zurueck, wenn RI gesetzt ist, sonst 0 */ { int currstat; ioctl(fd, TIOCMGET, &currstat); return((currstat & TIOCM_RNG)?1:0); } int GetCD(int fd) /* Gibt 1 zurueck, wenn CD gesetzt ist, sonst 0 */ { int currstat; ioctl(fd, TIOCMGET, &currstat); return((currstat & TIOCM_CAR)?1:0); } int GetDSR(int fd) /* Gibt 1 zurueck, wenn DSR gesetzt ist, sonst 0 */ { int currstat; ioctl(fd, TIOCMGET, &currstat); return((currstat & TIOCM_DSR)?1:0); } int GetCTS(int fd) /* Gibt 1 zurueck, wenn CTS gesetzt ist, sonst 0 */ { int currstat; ioctl(fd, TIOCMGET, &currstat); return((currstat & TIOCM_CTS)?1:0); } int GetSerStat(int fd) /* Gibt den kompletten seriellen Status zurueck * * ggf. RTS und DTR ausblenden: * * currstat &= (TIOCM_DTR | TOICM_RTS) */ { int currstat; ioctl(fd, TIOCMGET, &currstat); return(currstat); } int main(int argc, char** argv) { int i, stat; /* Parameterprüfung: Das zu verwendende Device muss */ /* als erster Parameter angegeben werden. */ if(argc != 2) { printf("Fehler: Ungültige Parameter-Anzahl.\n"); printf("Aufruf: serpin <device>\n"); return 1; } /* Das Device öffnen */ if((fd = open(argv[1], O_RDWR | O_NDELAY)) < 0) { printf("Fehler: Device \"%s\" kann nicht geöffnet werden.\n", argv[1]); return 2; } /* alles loeschen */ ClearSerPins(fd); /* mit RTS und DTR blinken */ for(i=0; i<5; i++) { usleep(300000); SetRTS(fd); usleep(300000); ResetRTS(fd); usleep(300000); SetDTR(fd); usleep(300000); ResetDTR(fd); } /* Status abfragen, bis RI gesetzt wird */ while(!GetRI(fd)) { stat = GetSerStat(fd); printf("%6X ",stat); printf("+%d+ ",GetCTS(fd)); if (stat & TIOCM_RNG) puts("RI "); if (stat & TIOCM_CAR) puts("CD "); if (stat & TIOCM_DSR) puts("DSR "); if (stat & TIOCM_CTS) puts("CTS "); printf("\n"); sleep(1); } close(fd); return 0; }Quellcode serpin.c
Prinzipiell könnten Sie auch direkt auf den Modem-Port der Schnittstelle schreiben (mittels inb() und outb(), was aber nicht nur wegen der Portabilität unschön ist.
An die Ausgänge lassen sich LEDs direkt anschließen; wenn man Dual-LEDs nimmt, kann man sogar 0 und 1 aktiv anzeigen (z. B. rot-grün - was einen Farbenblinden dann in den Wahnsinn treibt). Einen LED-Strom von 20 mA verkraftet die Schnittstelle normalerweise wunderbar, nur einige Laptops sind manchmal schwach auf der Brust. Der Vorwiderstand wäre dann minimal (12-2)/0.02 = 1000 Ohm. Bei Low-Power-LEDs reicht meist der doppelte Wert.
Schaltet man einen Ausgang auf 1, kann er +12 V für die Eingänge liefern. Die Schnittstelle am PC liefert den Eingangswert 0, wenn der Eingang offen ist. Man kann also vier Taster oder Schalter einseitig mit dem RTS-Ausgang verbinden und die andere Seite dieser Taster/Schalter wieder mit den Eingangspins - und braucht also keinerlei aktive Komponenten.
Diese Tasten bzw. Schalter kann man nun abfragen und im Programm darauf reagieren. Bei der Tastenabfrage muss man die Entprellung per Software durchführen, etwa in der Form:
if (GetRI(fd)) /* Taste RI gedrückt */ { usleep(100*1000); /* 0,1 s Pause */ if (GetRI(fd)) /* Wenn immer noch gedrückt... */ { while(GetRI(fd)); /* ... warten auf's Loslassen */ do_something_awful(); } }Bei Servern, die ohne Tastatur und Bildschirm laufen, kann man mit so einer Tastengruppe einige Funktionen steuern (Herunterfahren, NFS-Mount und -Unmount etc.). Per LED an DTR/RTS ist eine Rückmeldung möglich.
ret = ioctl(fd, funkt, arg);wobei ret der Returncode, fd der Filedescrioptor und funkt eine Konstante ist, die angibt, welche Funktion man aufrufen möchte. Etwaige Argumente werden in arg übergeben. Die Standard-ioctl-Funktionen sind:
TIOCMGET Lesen der Schnittstellen-Statusleitungen TIOCMSET Setzen der Schnittstellen-Steuerleitungen TIOCMBIS Modem-Steuersignale enablen TIOCMBIC Modem-Steuersignale disablen TIOCMIWAIT Warten, bis sich eines der angegebenen Statussignale ändert TIOCGICOUNT Auslesen wie oft sich die angegebenen Statussignale geändert haben TIOCOUTQ Anzahl der noch zu sendenden Bytes TIOCSETD Setzen der line discipline (asynchron, synchron)
Die Funktionen TIOCMGET und TIOCMSET wurden weiter oben schon beschrieben. Die Funktionen TIOCMBIS und TIOCMBIC geben die Steuerleitungen RTS und DTR frei bzw. schalten sie inaktiv. Zum Beispiel:
int rc; int sig = TIOCM_RTS | TIOCM_DTR; /* enable */ rc = ioctl(fd,TIOCMBIS,&sigs); /* disable */ rc = ioctl(fd,TIOCMBIC,&sigs);
Statt im eigenen Programm die Statusleitungen mittels TIOCMGET in einer Schleife zu pollen, bis sich endlich etwas tut, verlagert man die Arbeit besser in den Geräte-Treiber, der das besser kann. Mit der Funktion TIOCMIWAIT kann auf die Änderung (low-high-Flanke oder high-low-Flanke) definierter Statusleitungen warten. Allerdings "hängt" das Programm auch, bis sich etwas tut. Das Auslesen der Statusinfo erfolgt dann mittels TIOCMGET. Zum Beispiel:
int rc; /* Die folgenden Statusbits duerfen beliebig kombiniert werden. */ /* Carier Detect | Ring Indic. | Data Set Ready | Clear To Send */ int sig = TIOCM_CAR | TIOCM_RNG | TIOCM_DSR | TIOCM_CTS; /* Warten ... */ rc = ioctl(fd,TIOCMIWAIT,&sigs);
Am interessantesten ist die Funktion TIOCGICOUNT, die alle möglichen Zähler abfragt. Als Argument wird eine Strukturvariable übergeben. Die zugehörige Struktur ist in der Include-Datei linux/serial.h definiert:
struct serial_icounter_struct { int cts, dsr, rng, dcd; /* Anzahl Pegelwechsel auf den Statusleitungen */ int rx, tx; /* Anzahl emfangener/gesendeter Bytes */ int frame, overrun, parity, brk; /* Anzahl Fehler bzw. Break-Sequenzen */ int buf_overrun; /* Anzahl Empfangspuffer-Ueberlaeufe */ int reserved[9]; };Der Aufruf gleicht den vorhergehenden Beispielen:
int rc; struct serial_icounter_struct zaehler; /* Daten abrufen */ rc = ioctl(fd,TIOCGICOUNT,&zaehler); /* Zaehler auswerten */ printf("Es hat %d mal geklingelt.\n", zaehler.rng);
Die Funktion TIOCOUTQ liefert die Anzahl der noch zu sendenden Bytes. Sie läßt sich einsetzen, um festzusellen, ob alle Daten gesendet wurden:
int rc; int count; /* Warten, bis alles weg ist */ while (rc = ioctl(fd,TIOCOUTQ,&count) != 0);
Die folgende Funktion serwait() wartet (schläft), bis sich eine oder mehrere der vier Steuerleitungen ändern. Sie besitzt fünf Parameter:
int serwait(int fd, int *dcd, int *rng, int *dsr, int *cts)Der Parameter fd ist das Handle einer bereits geöffneten Schnittstelle. Welche Leitungen zu überwachen sind, wird in den Parametern dcd, rng, dsr und cts festgelegt:
int serwait(int fd, int *dcd, int *rng, int *dsr, int *cts) { struct serial_icounter_struct beforec; struct serial_icounter_struct afterc; int icstatus, flags; flags = 0; if(dcd != NULL) { *dcd = 0; flags |= TIOCM_CD; } if(rng != NULL) { *rng = 0; flags |= TIOCM_RNG; } if(dsr != NULL) { *dsr = 0; flags |= TIOCM_DSR; } if(cts != NULL) { *cts = 0; flags |= TIOCM_CTS; } if(flags == 0) return(-1); /* Nothing to do ! */ memset(&beforec, 0, sizeof(struct serial_icounter_struct)); memset(&afterc, 0, sizeof(struct serial_icounter_struct)); icstatus = ioctl(fd, TIOCGICOUNT, &beforec); if(icstatus == -1) { printf("TIOCGICOUNT failed: %s\n", strerror(errno)); return(-1); } /* Wait on the selected flags here */ icstatus = ioctl(fd, TIOCMIWAIT, flags); if(icstatus == -1) { printf("TIOCMIWAIT failed: %s\n", strerror(errno)); return(-1); } icstatus = ioctl(fd, TIOCGICOUNT, &afterc); if(icstatus == -1) { printf("TIOCGICOUNT failed: %s\n", strerror(errno)); return(-1); } if(dcd != NULL) { *dcd = (beforec.dcd != afterc.dcd); } if(cts != NULL) { *cts = (beforec.cts != afterc.cts); } if(rng != NULL) { *rng = (beforec.rng != afterc.rng); } if(dsr != NULL) { *dsr = (beforec.dsr != afterc.dsr); } return(icstatus); }Das folgende Demo-Programm zeigt das Anwendungsprinzip:
int main(void) { int res, fd, dcd, rng, dsr, cts; fd = open_port(1); /* Alle Leitungen zweimal hintereinander abfragen */ res = serwait(fd, &dcd, &rng, &dsr, &cts); printf("Res: %d, DCD: %d, RNG: %d, DSR: %d, CTS: %d\n",res, dcd, rng, dsr, cts); res = serwait(fd, &dcd, &rng, &dsr, &cts); printf("Res: %d, DCD: %d, RNG: %d, DSR: %d, CTS: %d\n",res, dcd, rng, dsr, cts); /* Nur die CTS-Leitung abfragen */ res = serwait(fd, NULL, NULL, NULL, &cts); printf("Res: %d, CTS: %d\n",res, cts); close(fd); return(0); }
use strict; use warnings; $| = 1; # ttyS0 bei Linux entspricht COM1 bei Windows my $port = "/dev/ttyS0"; # Mit dem Linux-Kommando 'stty' die Port-Einstellungen setzen system "stty 2400 ixon -echo < $port"; # Port als Datei zum Lesen und Schreiben öffnen open(COM, "+>$port") or die "Oops $port: $!\n"; # Datei auch auf ungepuffertes Schreiben setzen select(COM); $| = 1; select(STDOUT); # Sendet Q1 an die USV zur Statusabfrage # die USV mag kein normales Newline, sondern # nur ein Carriage-Return print COM "Q1\r"; # 2400 BPS sind recht langsam sleep(1); # Antwort einlesen sysread(COM, my ($line), 50); # Ausgabe auf Console print $line, "\n";; close(COM);Bis auf den system()-Aufruf war das eigentlich genauso wie bei einer Dateiausgabe. Für viele Anwendungen reicht das nicht, da man so weder das Blockieren beim Lesen vermeiden kann, noch lässt sich mit den Steuerleitungen "klappern".
Unter den zahlreichen Modulen auf dem CPAN-Server findet sich auch das passende für die serielle Schnittstelle: Device::SerialPort. Die englischsprachige Dokumentation ist umfassend, und letztendlich spiegeln die Methoden des Moduls die darunterliegenden Funktionen der C-Bibliothek nahezu identisch wider. Der erste Vorteil liegt schon mal darin, dass man auf einen system()-Aufruf verzichten kann. Alle Schnittstellenparameter lassen sich per Methodenaufruf einstellen. Der zweite Vorteil betrifft wieder die Windows-User, denn es gibt ein identisches Modul für Windows, das mit einem einzigen ppm-Kommando wie gewohnt installiert werden kann. Erstaunlicherweise liegt das Modul nicht im Angebot von ActiveState vor, wir müssen es von woanders holen:
ppm install http://www.bribes.org/perl/ppm/Win32-SerialPort.ppdDie beiden Module erlauben sogar das Schreiben von Programmen, die auf beiden Plattformen ohne Änderung laufen, sofern man nur darauf achtet, immer brav Newline als Zeilenende zu verwenden. Das folgende Beispiel skizziert die Vorgehensweise:
#!/usr/bin/perl use strict; use warnings; use vars qw($OS_win); # Vor allem anderen das Betriebssystem checken BEGIN { $OS_win = ($^O eq "MSWin32") ? 1 : 0; print "Perl_Vers.: $], Betriebssystem: $^O\n"; # Debug # das Folgende geht nur innerhalb des BEGIN-Blocks if ($OS_win) { print "Lade Windows-Modul\n"; eval "use Win32::SerialPort"; die "Oops: $@\n" if ($@); } else { print "Lade Unix-Modul\n"; eval "use Device::SerialPort"; die "Oops: $@\n" if ($@); } } # Kommandozeilenparameter holen die "\nUsage: $0 PORT\n" unless (@ARGV); my $port = shift; # Serielle Schnittstelle oeffen (abhaengig vom Betriebssystem) my $serial_port; if ($OS_win) { $serial_port = Win32::SerialPort->new ($port,1); } else { $serial_port = Device::SerialPort->new ($port,1); } die "Kann seriellen Port $port nicht oeffen: $^E\n" unless ($serial_port); # Ab hier ist dann alles identisch my $baud = $serial_port->baudrate(); print "Schnittstelle $port arbeitet mit $baud BPS\n"; # Schnittstellenparameter lesen my $handshake = $serial_port->handshake(); print "Handshake: $handshake\n"; my $databits = $serial_port->databits(); my $parity = $serial_port->parity(); my $stopp = $serial_port->stopbits(); print "$databits Databits, $stopp Stoppbit(s), " . "Paritaet: $parity\n"; # ... usw. # Schnittstelle schliessen $serial_port->close; undef $serial_port;Unter Windows führt ein Aufruf des Programms zu folgender Ausgabe:
D:\Test\Code\Hardware>perl ser1.pl COM1 Perl_Vers.: 5.010001, Betriebssystem: MSWin32 Lade Windows-Modul Schnittstelle COM1 arbeitet mit 1200 BPS Handshake: none 7 Databits, 1 Stoppbit(s), Paritaet: noneDas Modul kann anstelle des seriellen Ports auch mit einem Dateinamen aufgerufen werden. In dieser Datei stehen dann alle Daten einer früher gespeicherten Konfiguration. Das erspart das fest verdrahtete Setzen der Werte im Programm selbst und damit Programmänderungen bei sich ändernden Parametern. Das folgende Beispiel zeigt die Vorgehensweise. Zuerst wird ein serielles Objekt erzeugt, dessen Parameter geändert und in einer Datei gespeichert werden. Danach wird ein neues Objekt mit der Konfiguration aus der Datei versorgt, die natürlich wieder eine reine Textdatei ist: Weil es bei Linux meist einfacher läuft, mache ich jetzt mal wieder die Beispiele unter Windows.
use strict; use warnings; use Win32::SerialPort; # Windows-Variante my $port1 = Win32::SerialPort->new('COM1') or die "Oops!\n"; # Parameter setzen $port1->baudrate(9600) || die "baudrate geht nicht\n"; $port1->parity('even') || die "parity geht nicht\n"; $port1->databits(7) || die "databits geht nicht\n"; $port1->stopbits(2) || die "stopbits geht nicht\n"; $port1->handshake("rts") || die "handshake geht nicht\n"; # defined, weil "0" ein legaler Rueckgabewert ist defined $port1->parity_enable('T') || die "parity_enable geht nicht\n"; # Man kann auch noch Meta-Info hinzufuegen $port1->alias('GSM_Modem'); $port1->devicetype('modem'); # Devicecontrolblock schreiben $port1->write_settings || die "write_settings geht nicht\n"; # Konfigurationsdatei erzeugen $port1->save('mymodem.cfg') || die "save geht nicht\n"; undef $port1; # Konfiguration laden my $port2 = Win32::SerialPort->new('COM1') or die "Oops!\n"; # Alternativ auch: $port1->restart('mymodem.cfg'); # und mal ausgeben ... print "handshake problem\n" unless ("rts" eq $port2->handshake); print "baudrate problem\n" unless (9600 == $port2->baudrate); print "parity problem\n" unless ("even" eq $port2->parity); print "databits problem\n" unless (7 == $port2->databits); print "stopbits problem\n" unless (2 == $port2->stopbits); undef $port2;Das serielle Modul ist leider so umfangreich, dass ich es hier nicht in voller epischer Breite behandeln kann.
Auch die Steuerleitungen lassen sich vom Modul aus direkt ansteuern. Es ist sogar möglich, die Sendeleitung (TXD) auf 0- oder 1-Pegel zu setzen. Es kann also nur die Empfangsleitung nicht direkt angesteuert werden. Es gibt übrigens auch Geräte, die nur über die Steuer- und Statusleitungen mit dem Rechner kommunizieren und die Sende- und Empfangsleitung gar nicht verwenden. Das folgende Listing verwendet die Methoden dtr\_active(), rts\_active() und break\_active(), um die drei Steuerleitungen zu schalten. Zum Lesen der Statusleitungen wird die Methode is\_modemlines() verwendet. Zur Auswahl der einzelnen Statusleitungen dienen einige Konstanten, die vom seriellen Modul per qw(:STAT) exportiert werden.
Zur Vereinfachung habe ich mit zwei Funktionen geschrieben, die jeweils als ersten Parameter das serielle Port-Objekt und als zweiten Parameter den Namen der jeweiligen Steuerleitung übernehmen. Bei SetPin() kommt als dritter Parameter noch ein Wahrheitswert (in der Regel 0 oder 1) hinzu. Mit SetPin() kann einer der drei Ausgänge geschaltet werden, mit GetPin() erhält man den Wert einer Statusleitung. Schließlich kann mittels linestatus() auch der komplette Leitungsstatus ausgegeben werden. Das Programm läuft natürlich auch unter Linux, ich erspare mir aus Platzgründen den Vorspann zur Auswahl des richtigen Moduls:
use strict; use warnings; use Win32::SerialPort qw(:STAT); # Windows-Variante $| = 1; my $port = Win32::SerialPort->new('COM1') or die "Oops!\n"; # Mit DTR, RTS und TXD klappern for my $pin('DTR', 'RTS', 'TXD') { SetPin($port, $pin, 1); sleep 1; SetPin($port, $pin, 0); sleep 1; } # Status einlesen while (1) { linestatus(); sleep 1; } # Port schliessen (hier sorgt Strg-C dafuer) undef $port; sub SetPin # ($port, $pin, 0|1) { my $result; my $port = shift; my $pin = uc(shift); my $value = shift; if ($pin eq 'DTR') { $result = $port->dtr_active($value); } elsif ($pin eq 'RTS') { $result = $port->rts_active($value); } elsif ($pin eq 'TXD') { $result = $port->break_active($value); } else { return undef; } return $result; } sub GetPin # ($port, $pin) { my $port = shift; my $pin = uc(shift); my $status = $port->is_modemlines(); if ($pin eq 'CTS') { return ($status & MS_CTS_ON) ? 1 : 0; } elsif ($pin eq 'DSR') { return ($status & MS_DSR_ON) ? 1 : 0; } elsif ($pin eq 'DCD') { return ($status & MS_RLSD_ON) ? 1 : 0; } elsif ($pin eq 'RI') { return ($status & MS_RING_ON) ? 1 : 0; } else { return undef; } } sub linestatus { my $status = $port->is_modemlines(); printf("Modem status=0x%04X (CTS=%s DSR=%s RNG=%s CD=%s)\n", $status, ($status & MS_CTS_ON) ? "ON " : "off", ($status & MS_DSR_ON) ? "ON " : "off", ($status & MS_RING_ON) ? "ON " : "off", ($status & MS_RLSD_ON) ? "ON " : "off", ); }