Unter DOS kann man noch direkt auf die Hardware des PCs zugreifen und die einzelnen Register des UART programmieren. Ein DOS-Programm unter Turbo Pascal kann daher z.B. COM1 benutzen, auch wenn dort die Maus angeschlossen ist. Es sind also Fehler möglich, die dazu führen, dass die Maussteuerung ausgeschaltet wird.
Windows dagegen überwacht alle Zugriffe auf spezielle Hardware und verhindert die doppelte Verwendung eines Geräts. Bereits das 16-Bit-Betriebssystem Windows 3.1 schränkte direkte Zugriffe auf die Hardware ein, um zu verhindern, dass es zu Konflikten kommt. Nun war es erforderlich, einen Kommunikationskanal zu einem Gerät erst durch einen ordentlichen Treiberzugriff zu öffnen und damit unter Windows anzumelden. Ein später gestartetes Programm erhielt dann keinen Zugriff auf die selbe Hardware.
Mit dem verbesserten Multitasking unter der heute üblichen 32-Bit-Umgebung ab Windows 95/98 ist der geordnete Zugriff auf Geräte und Schnittstellen noch wichtiger geworden. Es wurde daher ein neues Konzept für alle Gerätetreiber geschaffen. Für jedes Gerät wie die serielle Schnittstelle, die Druckerschnittstelle, Laufwerke oder die Soundkarte gibt es nun allgemeine Treiber, über die jeglicher Datenverkehr abgewickelt werden muss.
Für direkte Aufrufe von Windows-Funktionen wurde die Win32-API-Schnittstelle definiert (API = Application Programming Interface). Windows stellt eine Reihe von DLLs zur Verfügung, deren Funktionen von jedem Programm aufgerufen werden können. Die wichtigste ist KERNEL32.DLL. Grundsätzliche Informationen über die einzelnen Aufrufe liefert die Datei Win32.hlp. Hier werden alle wichtigen Aufrufe in Delphi gezeigt. Das Ergebnis ist die Unit RSCOM.PAS auf der CD, die in eigenen Delphi-Programmen eingesetzt werden kann. Zusätzlich wir die Unit weiter unten in die Funktionsbibliothek RSCOM.DLL umgesetzt, die sowohl in Delphi als auch in Visual Basic eingesetzt wird.
Alle Zugriffe auf die serielle Schnittstelle erfolgen über einige wenige API-Aufrufe, die sich im Wesentlichen an der Dateiverwaltung orientieren. Geräte wie die Druckerschnittstelle, die serielle Schnittstelle oder die USB-Schnittstelle werden ebenfalls wie Dateien behandelt:
CreateFile() Öffnen einer Datei oder eines Geräts
CloseHandle() Schließen einer Datei oder eines Geräts
ReadFile() Lesen bzw. Empfangen von Daten
WriteFile() Schreiben bzw. Senden von Daten
DeviceIoControl() Ausführen spezielle Treiberfunktionen
Zur Verdeutlichung des grundsätzlichen Zugriffs auf die serielle Schnittstelle soll hier ein sehr einfaches Beispiel gezeigt werden. Die Schnittstelle COM2 wird mit CreateFile geöffnet, um dann ein Byte mit WriteFile zu senden. CloseHandle schließt den Kommunikationskanal wieder.
Beim Öffnen wird ein Handle verwendet, also eine Zahl vom Typ Integer, die zur eindeutigen Zuordnung des verwendeten IO-Kanals dient. Das Handle wird vom System entsprechend der Reihenfolge von CreateFile-Aufrufen vergeben. Da WriteFile für sehr unterschiedliche Aktionen und Geräte verwendet werden kann, unterscheidet das Betriebssystem diese mit Hilfe des Handles. Entsprechend gibt CloseHandle die entsprechende Schnittstelle wieder frei.
procedure TForm1.Button2Click(Sender: TObject);
var Handle: THandle; {type THandle = Integer;}
Byt: Byte;
Count: DWORD;
begin
Handle:=CreateFile(PChar('COM2'),GENERIC_WRITE,0,NIL,
OPEN_EXISTING,0,0);
Byt := 85;
WriteFile(Handle,Byt,1,Count,NIL);
CloseHandle(Handle);
end;
Listing 3.6 Senden eines Bytes
Dieses erste Beispiel zeigt zwar den grundsätzlichen Zugriff auf die Schnittstelle und sendet Daten, wie man mit einem Oszilloskop an der Leitung TXD leicht feststellen kann. Es hat jedoch noch keinen praktischen Nutzen, weil die Schnittstellenparameter wie die Baudrate oder die Anzahl der Datenbits nicht explizit festgelegt wurden. Es werden einfach die zuletzt verwendeten Parameter verwendet.
Im Folgenden werden alle relevanten Windows-Aufrufe zur seriellen Schnittstelle erläutert und in Delphi-Funktionen verwendet. Die vorgestellten Prozeduren und Funktionen befinden sich in der Unit RSCOM.PAS, die von der CD geladen werden kann.
CreateFile und CloseFile
Die Funktion CreateFile hat zahlreiche Parameter, die hier im einzelnen erläutert werden sollen. Weitere Informationen finden sich in der Hilfe-Datei WIN32.HLP, die auch auf der CD vorhanden ist. Die Deklaration erfolgt hier in C-Konventionen. In Delphi 4 ist sie in der Unit WINDOWS.PAS entsprechend umgesetzt.
HANDLE CreateFile(
LPCTSTR lpFileName, // pointer to name of the file
DWORD dwDesiredAccess, // access (read-write) mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // pointer to
// security attributes
DWORD dwCreationDistribution, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to file with
//attributes to copy
);
lpFileName |
Zeiger auf einen nullterminierten String mit dem Namen des Geräts oder der Datei. |
dwDesiredAccess |
Konstante zur Beschreibung des Schreib- oder Lesezugriffs 0: kein Zugriff GENERIC_READ= $80000000:Lesezugriff GENERIC_WRITE=$40000000:Schreibzugriff |
dwShareMode |
Gibt an, ob eine Datei geteilt werden kann, für COM-Schnittstellen immer 0, da sie nicht von mehreren Anwendungen geteilt werden können. |
lpSecurityAttributes |
Zeiger auf eine Struktur von Sicherheitsattributen, für COM-Schnittstellen nicht unterstützt, daher immer NIL |
dwCreationDistribution |
Gibt für Dateien an, ob sie neu angelegt werden sollen oder nur geöffnet werden sollen, wenn sie bereits existieren. Für die COM-Schnittstelle ist die einzige mögliche Einstellung OPEN_EXISTING = 3. |
dwFlagsAndAttributes |
Übergibt Attribute für Dateien, für Schnittstellen meist 0 oder FILE_FLAG_OVERLAPPED |
hTemplateFile |
Übergibt ein Handle auf eine Vorlagedatei mit erweiterten Attributen. Muss für Schnittstellen immer 0 sein. |
CreateFile gibt bei Erfolg ein gültiges Handle zurück. Falls das Öffnen nicht möglich war, weil die Schnittstelle nicht existiert oder bereits belegt war, ist der Rückgabewert die Konstante:
INVALID_HANDLE_VALUE =-1;
In Delphi muss der Zeiger lpFileName auf einen String mit dem Datei- bzw. Schnittstellennamen mit dem Typ PChar übergeben werden. Die Funktion PChar wandelt einen Pascal-String in einen Zeiger auf einen nullterminierten String entsprechend der C-Konvention um. Bei der Übergabe des Schreib/Lesezugriffs wird im Normalfall GENERIC_READ und GENERIC_WRITE gemeinsam übergeben, indem beide Konstanten mit OR oder mit einer Addition verknüpft werden.
PortHandle:=CreateFile(PChar('COM2'),GENERIC_READ or
GENERIC_WRITE,0,NIL,OPEN_EXISTING,0,0);
Statt der Konstantennamen könnte man auch direkte Konstanten übergeben, was allerdings als schlechter Programmierstil angesehen wird, weil man ihnen die Funktion nicht mehr ansehen kann. Hier soll diese Art des Aufrufs einmal gezeigt werden, um zu verdeutlichen, dass letztlich nur bestimmte Zahlenwerte an das Betriebssystem übergeben werden.
Handle:=CreateFile(PChar('COM2'),$C0000000,0,NIL,3,0,0);
Ein grundlegender Unterschied besteht in der Verwendung der Schnittstelle im Overlapped-Modus (auch als asynchron bezeichnet) oder im Nonoverlapped Modus (auch: synchron). Im Normalfall verwendet man Nonoverlapped, d.h. ein Prozess wird angehalten, bis eine Schreib- oder Leseaktion der seriellen Schnittstelle abgeschlossen ist. Eine Prozedur, die einen längeren Text über COM2 sendet, wird also erst dann wieder verlassen, wenn alle Zeichen vollständig übertragen sind. Andere Programme laufen aber ungestört weiter, da das Multitasking nicht beeinträchtigt wird. Nur der eigene Thread (Prozess) wird angehalten. Insbesondere beim Empfang von Zeichen kann ein Programm "hängen", wenn weniger als die angeforderten Byte tatsächlich eintreffen. Dagegen hilft die Festlegung einer Timeout-Einstellung, die später noch genauer behandelt wird.
Im Overlapped-Modus läuft ein Thread weiter, auch während eine Übertragung über die serielle Schnittstelle noch läuft. Übertragungszeiten können gleichzeitig für andere Vorgänge benutzt werden. Dazu ist aber eine Synchronisierung in Form von Nachrichten über den Stand einer Schnittstellenaktion erforderlich. Die Funktion GetOverlappedResult kann verwendet werden, um das erfolgreiche Ende einer Übertragung zu erfahren. Der Overlapped-Modus ist insgesamt sehr viel komplizierter als der Nonoverlapped-Modus und soll in diesem Buch nicht verwendet werden. Er kann auch nur sinnvoll angewandt werden, wenn größere Datenmengen übertragen werden sollen. Dagegen geht es Mess- Steuer und Regelungstechnik meist um kurze Datenblöcke, deren Übertragungszeit kaum sinnvoll genutzt werden kann.
Jede geöffnete Schnittstelle muss auch wieder geschlossen werden, um sie für andere Anwendungen wieder freizugeben. Dazu dient die Funktion CloseHandle.
BOOL CloseHandle(
HANDLE hObject // handle to object to close
);
In Delphi ist die Verwendung einfach. Als Rückgabewert erhält man bei Bedarf den Wert TRUE für Erfolg oder FALSE, wenn das Schließen nicht möglich war.
procedure CLOSECOM();
begin
CloseHandle(PortHandle);
PortHandle:= 0;
end;
WriteFile und ReadFile
Der eigentliche Schreibzugriff auf die serielle Schnittstelle erfolgt unter Win32 grundsätzlich mit WriteFile. Der Zugriff entspricht dem Schreiben in eine Diskettendatei.
BOOL WriteFile(
HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten,// pointer to number of bytes
// written
LPOVERLAPPED lpOverlapped // pointer to structure needed
//for overlapped I/O
);
hFile |
Handle auf das geöffnete File oder Gerät |
lpBuffer |
Zeiger auf einen Datenpuffer |
nNumberOfBytesToWrite |
Anzahl der zu sendenden Bytes |
lpNumberOfBytesWritten |
Zeiger auf eine Variable mit der Anzahl der tatsächlich geschriebenen Bytes |
lpOverlapped |
Zeiger auf eine Overlapped-Struktur oder NIL für Nonoverlapped |
Textpuffer sind unter Windows immer nullterminierte Strings, also Byte-Arrays, die mit einer Null abgeschlossen sind. Dieser Typ ist nicht kompatibel zum Delphi-Typ STRING. Es gibt aber den Typ PChar als Zeiger auf einen nullterminierten String. Sowohl bei der Übergabe von Textparametern an API-Funktionen als auch bei der Übergabe an eigene Funktionen sollte der Typ PChar verwendet werden. Damit ergibt sich auch eine Kompatibilität zum String-Typ in Visual Basic.
In Delphi 4 wird WriteFile in der Datei WINDOWS.PAS deklariert, wobei die verwendeten Datentypen etwas von denen der ursprünglichen C-Deklaration des Systems abweichen:
function WriteFile(hFile: THandle; const Buffer;
nNumberOfBytesToWrite: DWORD;
var lpNumberOfBytesWritten: DWORD;
lpOverlapped: POverlapped): BOOL; stdcall;
Der Datenpuffer wird hier als Konstante deklariert, d.h. es wird ein Zeiger auf den Datenbereich übergeben. Die Umwandlung in einen Zeiger erfolgt automatisch. Prinzipiell darf ein konstanter Text übergeben werden, wobei in nNumberOfBytes die Anzahl der Zeichen angegeben werden muss. Nach der Rückkehr aus der Funktion WriteFile enthält lpNumberOfBytesWritten im Normalfall die selbe Anzahl. Nur wenn die Übertragung mit einem Fehler abgeschlossen wurde, kann es hier eine Differenz geben. Die Funktion gibt in diesem Fall FASLSE zurück.
Success := WriteFile(Handle, 'ABCDEFG',7,Count,NIL);
Wenn Text in einer Variablen übergeben werden soll, muss es sich um einen nullterminierten String handeln. Da der Typ PChar allerdings einen Zeiger darstellt, Writefile jedoch eine explizite Variable erwartet, muss der Text bei der Übergabe mit "^" dereferenziert werden.
procedure SENDSTRING (Buffer: String);
var BytesWritten: DWord;
begin
WriteFile(PortHandle,(Pchar(Buffer))^,Length(Buffer),
BytesWritten,NIL);
END;
procedure TForm1.Button1Click(Sender: TObject);
var TextString : String;
begin
TextString := Edit1.Text;
SendString (TextString+ #13);
end;
Einfacher ist es, wenn man nur einzelne Zeichen oder Bytes senden will. Statt eines Textpuffers kann nun einfach eine einzelne Byte- oder Integervariable übergeben werden.
procedure SENDBYTE (Dat: Integer);
var BytesWritten: DWord;
begin
WriteFile(PortHandle,Dat,1,BytesWritten,NIL);
END;
Für die umgekehrte Datenrichtung lautet die Funktion ReadFile. In der C-Konvention ist die Funktion so deklariert:
BOOL ReadFile(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // address of buffer that receives data
DWORD nNumberOfBytesToRead, // number of bytes to read
LPDWORD lpNumberOfBytesRead, // address of number of bytes read
LPOVERLAPPED lpOverlapped // address of structure for data
);
hFile |
Handle auf das geöffnete File oder Gerät |
lpBuffer |
Zeiger auf einen Datenpuffer für den Empfang |
nNumberOfBytesToRead |
Anzahl der zu empfangenden Bytes |
lpNumberOfBytesRead |
Zeiger auf eine Variable mit der Anzahl der tatsächlich empfangenen Bytes |
lpOverlapped |
Zeiger auf eine Overlapped-Struktur oder NIL für Nonoverlapped |
Die Funktion ReadFile wird über die Datei WINDOWS.PAS in Delphi 4 importiert.
function ReadFile(hFile: THandle; var Buffer; nNumberOfBytesToRead:
DWORD; var lpNumberOfBytesRead: DWORD; lpOverlapped: POverlapped):
BOOL; stdcall;
Der Anwender muss wissen, wie viele Zeichen er von der Schnittstelle abholen will, und er muss einen Puffer passender Größe zur Verfügung stellen. Besonders einfach liegt der Fall, wenn nur ein einzelnes Zeichen zu einer Zeit empfangen werden soll.
function READBYTE(): Integer;
var Dat: Byte;
BytesRead: DWORD;
begin
ReadFile(PortHandle,Dat,1,BytesRead,NIL);
if BytesRead = 1 then Result:=Dat else Result := -1;
end;
Die Funktion Readbyte liefert bei Erfolg ein Zeichen zurück. Falls sich kein Zeichen im Empfangspuffer befindet, wartet ReadFile auf ein eintreffendes Zeichen oder bricht den Empfangsversuch nach einer Timeout-Zeit ab. BytesRead ist in diesem Fall Null. Readbyte gibt den Wert -1 zurück.
Der Empfang ganzer Zeichenketten ist komplizierter, weil ReadFile die Übergabe der Textlänge erwartet. Oft ist die Länge eines Empfangstextes aber vorher nicht bekannt. Der Empfangstext kann oft besser aus einzelnen Zeichen zusammengesetzt werden. Das Ende des Textes wird z.B. an einem ausbleibenden Empfangsbyte erkannt. Oft werden Texte aber auch durch das CR-Zeichen #13 abgeschlossen. Für den zeilenweisen Empfang eignet sich daher die Funktion ReadString.
function READSTRING(): STRING;
var Dat: Integer;
Data: STRING;
begin
Dat := 0;
while ((Dat > -1) and (Dat <> 13)) do begin
Dat := ReadByte();
if ((Dat > -1) and (Dat <> 13)) then Data := Data + Chr(Dat);
end;
READSTRING := Data;
end;
Das Senden und der Empfang serieller Daten ist auf vorherige Einstellung der Schnittstellenparameter angewiesen, die im Folgenden erläutert werden. Außerdem spielen in vielen Fällen auch Timeout-Zeiten und die Größe des Empfangs- und Sendepuffers eine Rolle.
Schnittstellenparameter in DCB
Speziell für die Eigenschaften der seriellen Schnittstelle existiert in Windows die DCB-Struktur mit einer Länge von insgesamt 22 Bytes. Für jede serielle Schnittstelle existiert ein eigener Steuerblock, der beim Öffnen der Schnittstelle bereits mit Voreinstellungen gefüllt ist. Für eine gezielte Verwendung der Schnittstelle ist es aber erforderlich, alle Einstellungen explizit vorzunehmen. In der C-Schreibweise hat die Struktur den folgenden Aufbau:
typedef struct _DCB { // dcb
DWORD DCBlength; // sizeof(DCB)
DWORD BaudRate; // current baud rate
DWORD fBinary: 1; // binary mode, no EOF check
DWORD fParity: 1; // enable parity checking
DWORD fOutxCtsFlow:1; // CTS output flow control
DWORD fOutxDsrFlow:1; // DSR output flow control
DWORD fDtrControl:2; // DTR flow control type
DWORD fDsrSensitivity:1; // DSR sensitivity
DWORD fTXContinueOnXoff:1; // XOFF continues Tx
DWORD fOutX: 1; // XON/XOFF out flow control
DWORD fInX: 1; // XON/XOFF in flow control
DWORD fErrorChar: 1; // enable error replacement
DWORD fNull: 1; // enable null stripping
DWORD fRtsControl:2; // RTS flow control
DWORD fAbortOnError:1; // abort reads/writes on error
DWORD fDummy2:17; // reserved
WORD wReserved; // not currently used
WORD XonLim; // transmit XON threshold
WORD XoffLim; // transmit XOFF threshold
BYTE ByteSize; // number of bits/byte, 4-8
BYTE Parity; // 0-4=no,odd,even,mark,space
BYTE StopBits; // 0,1,2 = 1, 1.5, 2
char XonChar; // Tx and Rx XON character
char XoffChar; // Tx and Rx XOFF character
char ErrorChar; // error replacement character
char EofChar; // end of input character
char EvtChar; // received event character
WORD wReserved1; // reserved; do not use
} DCB;
Die Elemente fBinary bis fDummy sind als einzelne Flags mit jeweils einem oder zwei Bits abgelegt und belegen zusammen 32 Bit. Bei der Umsetzung in Delphi ist es nicht ohne weiteres möglich, einzelne Flags in der selben Weise zu übergeben wie in C-Programmen. Deshalb steht an dieser Stelle eine einzelne 32-Bit-Variable vom Typ Longint.
Die einzelnen Elemente von DCB werden im Folgenden aufgelistet:
DCBlength |
Länge der DCB-Struktur in Bytes |
BaudRate |
Baudrate: 1200, 2400, 4800, 9600, 19200 usw. |
fBinary |
Flag: Binärmodus |
fParity |
Flag: Paritätsprüfung |
fOutxCtsFlow |
Flag: CTS-Handshake |
fOutxDsrFlow |
Flag: DSR-Handshake |
fDtrControl |
2 Flags: Typ des DTR-Handshales: 0: DTR=0, 1:DTR=1, 2:DTR-Handshake |
fDsrSensitivity |
Flag: Empfang nur bei DSR=1 |
fTXContinueOnXoff |
Flag: Senden auch bei vollen Empfangspuffer |
fOutX |
Flag: Xon/Xoff-Software-Handshake beim Senden |
fInX |
Flag: Xon/Xoff-Software-Handshake beim Empfang |
fErrorChar |
Flag: Bei Paritätsfehler empfangenes Byte durch ErrorChar ersetzen |
fNull |
Flag: Nullbytes beim Empfang übergehen |
fRtsControl |
2 Flags: Typ des RTS-Handshales: 0: RTS=0, 1:RTS=1 ,2:RTS-Handshake, 3:RTS-Toggle |
fAbortOnError |
Flag: Senden und Empfangen werden bei Fehler abgebrochen |
fDummy2 |
17 weitere nicht benutzte Bits |
wReserved |
nicht benutzt |
XonLim |
Anzahl von Bytes im Empfangspuffer, bei der XON gesendet wird |
XoffLim |
Anzahl von Bytes im Empfangspuffer, bei der XOFF gesendet wird |
ByteSize |
RS232-Zeichenlänge in Bit |
Parity |
Paritätsbit: NOPARITY = 0; ODDPARITY = 1; EVENPARITY = 2; MARKPARITY = 3; SPACEPARITY = 4; |
StopBits |
Anzahl der Stopbits: 1, 1,5 oder 2, ONESTOPBIT = 0; ONE5STOPBITS = 1; TWOSTOPBITS = 2; |
XonChar |
verwendetes XON-Zeichen |
XoffChar |
verwendetes XOFF-Zeichen |
ErrorChar |
Ersatszeichen nach Error |
EofChar |
EndOfFile-Zeichen |
EvtChar |
Event-Zeichen |
wReserved1 |
nicht benutzt |
Für Delphi wird die Struktur DCB in WINDOWS.PAS so umgesetzt:
type
{$EXTERNALSYM _DCB}
_DCB = packed record
DCBlength: DWORD;
BaudRate: DWORD;
Flags: Longint;
wReserved: Word;
XonLim: Word;
XoffLim: Word;
ByteSize: Byte;
Parity: Byte;
StopBits: Byte;
XonChar: CHAR;
XoffChar: CHAR;
ErrorChar: CHAR;
EofChar: CHAR;
EvtChar: CHAR;
wReserved1: Word;
end;
TDCB = _DCB;
Die einzelnen Parameter lassen sich durch explizite Zuweisung an Mitglieder des DCB-Struktur einstellen. Man erhält DCB durch GetCommState zunächst mit den aktuellen Vorgaben. Einstellungen wie BaudRate, ByteSize usw. entsprechen den Voreinstellungen, die im Systemmanager unter den Geräteeigenschaften zur Schnittstelle eingetragen wurden, außer sie wurden bereits durch ein vorher gestartetes Programm verändert. Dann werden alle relevanten Elemente neu gefüllt. Der veränderte DCB-Block wird dann mit SetCommState wirksam.
PortHandle:=CreateFile(PChar(COM2),GENERIC_READ or
GENERIC_WRITE,0,NIL,OPEN_EXISTING,0,0);
SetupComm(PortHandle,100,100);
GetCommState(PortHandle,DCB);
DCB.Flags := 1;
DCB.BaudRate:=1200;
DCB.ByteSize:=8;
DCB.StopBits:=2;
DCB.Parity:=0;
SetCommState(PortHandle,DCB)
Im Beispiel holt GetCommState zunächst eine gültige DCB-Struktur. Einzelne Elemente werden dann neu zugewiesen. Mit GetComState werden die neuen Einstellungen wirksam.
Eine andere Möglichkeit bietet die Funktion BuildCommDCB, mit der die wichtigsten Parameter aus einem Openstring der Form (COM2:1200,N,8,1) gelesen und in DCB eingetragen werden.
function OPENCOM (OpenString: pchar): Integer;
var PortStr, Parameter :String;
DCB: TDCB;
begin
Result := 0;
if PortHandle > 0 then CloseHandle(PortHandle);
Parameter := OpenString;
PortStr := copy (Parameter,1,4);
PortHandle:=CreateFile(PChar(PortStr),GENERIC_READ or
GENERIC_WRITE,0,NIL,OPEN_EXISTING,0,0);
GetCommState(PortHandle,DCB);
BuildCommDCB(PChar(Parameter),dcb);
DCB.Flags := 1;
if SetCommState(PortHandle,DCB)then Result := 1;
TimeOuts (10);
end;
In der Funktion OpenCOM wird zunächst nur der erste Teil (z.B. "COM2") aus dem übergebenen OpenString abgetrennt, um mit CreateFile ein Handle zu holen. BuildCommDCB erhält dann den kompletten String und überträgt die Einstellungen in die DCB-Struktur. Nur DCB.Flags muss weiterhin explizit zugewiesen werden, um die Übertragung beliebiger Binärdaten zuzulassen.
Bei der Übergabe von Zeichenketten muss beachtet werden, dass der Delphi-übliche Typ "String" nicht mit dem Format übereinstimmt, das die Win-API erwartet. Dort wird ein nullterminierter String benutzt, der dem Delphi-Typ PChar entspricht. Hier wird der Typ PChar auch für die Übergabe des Openstrings an die OpenCOM-Funktion verwendet. Damit lässt sich die Funktion auch in einer DLL verwenden, die z.B. von Visual Basic benutzt wird.
In der Open-Prozedur wird bereits als Voreinstellung ein Timeout von 10 ms vereinbart. Dies ist wichtig, da die Schnittstelle selbst zunächst ohne Timeout arbeitet. Dies kann zu Problemen führen, wenn ein Programm nicht explizit ein Timeoutintervall einstellt.
Timeouts und Puffergrößen
Bei jedem Empfangsversuch muss man damit rechnen, dass die erwarteten Zeichen nicht eintreffen, z.B. weil ein Gerät nicht angeschlossen wurde oder eine andere Störung vorliegt. Ein empfangendes Programm darf aber in dieser Situation nicht endlos warten, sondern es muss eine definierte Abbruchbedingung geben. Für diesen Zweck stellt Windows Sende- und Empfangs-Timeouts bereit. Die Struktur COMMTimeOuts enthält fünf einzelne Einstellungen.
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;
DWORD ReadTotalTimeoutMultiplier;
DWORD ReadTotalTimeoutConstant;
DWORD WriteTotalTimeoutMultiplier;
DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
ReadIntervalTimeout stellt die maximale Wartezeit zwischen zwei Empfangsbytes ein. Ein Nullwert bedeutet, dass keine TimeOuts verwendet werden.
ReadTotalTimeoutMultiplier ist ein Multiplikator für die gesamte Timeoutzeit, wobei der eingestellte Wert mit der Anzahl der angeforderten Bytes multipliziert wird.
ReadTotalTimeoutConstant ist eine konstante Wartezeit, die zum Produkt aus ReadTotalTimeoutMultiplier und Zeichenmenge hinzu addiert wird. Für den Fall des Empfangs von Einzelzeichen ist dies die einzige relevante Einstellung.
WriteTotalTimeoutMultiplier ist ein Multiplikator für die gesamte Timeoutzeit, wobei der eingestellte Wert mit der Anzahl der zu sendenden Bytes multipliziert wird.
WriteTotalTimeoutConstant ist eine Konstante, die zum Produkt aus WriteTotalTimeoutMultiplier und der Anzahl der mit einer WriteFile-Operation zu sendenden Bytes hinzu addiert wird.
Da hier meist nur einzelne Bytes empfangen werden und auch ReadString einzelne Bytes empfängt und zusammensetzt, braucht nur eine konstante Timeout-Zeit eingestellt zu werden. Man hat dann flexible Möglichkeiten, auf langsam antwortende Geräte und auch auf sporadisch eintreffende Daten zu reagieren.
procedure TIMEOUTS (TOut: Integer);
var TimeOut:TCOMMTIMEOUTS;
begin
TimeOut.ReadIntervalTimeout:=1;
TimeOut.ReadTotalTimeoutMultiplier:=1;
TimeOut.ReadTotalTimeoutConstant:=TOut;
TimeOut.WriteTotalTimeoutMultiplier:=10;
TimeOut.WriteTotalTimeoutConstant:=TOut;
SetCommTimeouts(PortHandle,TimeOut);
end;
In der Senderichtung kann ein Timeout nur dann wirksam werden, wenn eine längere Zeichenkette abgesandt werden soll, wobei der Sendepuffer bereits gefüllt ist. Damit in einem solchen Fall auch langsame Zeichen unter 300 Baud übertragen werden können, wird ein WriteTotalTimeoutMultiplier von 10 ms eingestellt. Zusätzlich wird eine gemeinsame maximale Wartezeit für die Empfangs- und Senderichtung übergeben.
Das Betriebssystem Windows verwendet einen Empfangspuffer und einen Sendepuffer für jede geöffnete serielle Schnittstelle. Jedes Empfangsbyte löst über den UART des PCs einen Interrupt aus und wird dann in den Empfangspuffer übertragen. Der Anwender kann Zeichen daher verspätet abholen. Genauso werden Sendebytes zuerst in den Ausgangspuffer geschrieben und in der Geschwindigkeit übertragen, wie es die Hardware erlaubt. Beim Senden von Zeichenketten dauert daher die Übertragung mit WriteFile nur sehr kurz, während die eigentliche Aussendung im Hintergrund durch Interrupts gesteuert wird.
Die Größen des Sende- und Empfangspuffers können in weiten Grenzen eingestellt werden. Dabei ist es nicht in jedem Fall von Vorteil, einen möglichst großen Puffer einzustellen. Wenn z.B. immer nur Einzelbytes empfangen und ausgewertet werden sollen, kann es ein Nachteil sein, wenn sich noch Zeichen von vorhergegangenen Aktionen im Puffer befinden. Die Einstellung der Puffergrößen erfolgt mit SetupComm für beide Datenrichtungen.
Procedure BUFFERSIZE (Size: Integer);
begin
SetupComm(PortHandle,Size,Size);
end;
function INBUFFER (): DWORD;
var Comstat: _Comstat;
Errors: DWORD;
begin
if ClearCommError (PortHandle, Errors, @Comstat) then
INBUFFER := Comstat.cbInQue else INBUFFER := 0;
end;
function OUTBUFFER (): DWORD;
var Comstat: _Comstat;
Errors: DWORD;
begin
if ClearCommError (PortHandle, Errors, @Comstat) then
OUTBUFFER := Comstat.cbOutQue else OUTBUFFER := 0;
end;
procedure CLEARBUFFER ();
begin
PurgeComm(PortHandle,PURGE_TXCLEAR);
PurgeComm(PortHandle,PURGE_RXCLEAR);
end;
Die tatsächliche Zeichenanzahl in einem Puffer kann mit der Windowsfunktion ClearCommError abgefragt werden. In der Unit RSCOM.PAS werden getrennte Funktionen InBuffer und OutBuffer für beide Datenrichtungen gebildet.
Bei Bedarf können die Puffer mit PurgeComm geleert werden. Die Delphi-Prozedur ClearBuffer leert beide Puffer zugleich. Diese Prozedur kann eingesetzt werden, um sicherzustellen, dass nicht noch alte Daten im Puffer stehen.
Alle hier vorgestellten Funktionen und Prozeduren sind in der Delphi-Unit RSCOM.PAS enthalten, die durchgängig in diesem Buch verwendet wird. Weiter unten wir noch eine RSCOM.DLL vorgestellt, die sich in gleicher Weise verwenden lässt.
Das folgende Beispielprogramm demonstriert den Gebrauch des Puffers und der Textübertragung. Alle Übertragungsparameter und die Puffergröße sind frei einstellbar. Für den Test sollte man eine direkte Verbindung zwischen der Sendeleitung TXD und der Empfangsleitung RXD herstellen. Gesendete Daten werden wieder empfangen und im Puffer abgelegt. Man sieht die Anzahl der im Puffer vorhandenen Zeichen und kann Zeichen zeilenweise abholen.
unit BufferF;
interface
uses
RSCOM, Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Edit2: TEdit;
ButtonOpen: TButton;
Label1: TLabel;
Label2: TLabel;
ButtonBuffer: TButton;
Label3: TLabel;
Edit3: TEdit;
ButtonSenden: TButton;
Label4: TLabel;
ButtonEmpfangen: TButton;
Edit4: TEdit;
Edit5: TEdit;
ButtonClose: TButton;
ButtonLeeren: TButton;
Timer1: TTimer;
procedure ButtonOpenClick(Sender: TObject);
procedure ButtonCloseClick(Sender: TObject);
procedure ButtonBufferClick(Sender: TObject);
procedure ButtonSendenClick(Sender: TObject);
procedure ButtonLeerenClick(Sender: TObject);
procedure ButtonEmpfangenClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
{ Private-Deklarationen}
public
{ Public-Deklarationen}
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ButtonOpenClick(Sender: TObject);
var TextString : String;
begin
TextString := Edit1.Text;
OpenCOM (pchar(TextString));
end;
procedure TForm1.ButtonCloseClick(Sender: TObject);
begin
CloseCOM
end;
procedure TForm1.ButtonBufferClick(Sender: TObject);
var N, Code: Integer;
begin
Val(Edit2.Text, N, Code);
BufferSize (N);
end;
procedure TForm1.ButtonSendenClick(Sender: TObject);
begin
SendString (PChar(Edit3.Text + #13));
end;
procedure TForm1.ButtonLeerenClick(Sender: TObject);
begin
ClearBuffer;
end;
procedure TForm1.ButtonEmpfangenClick(Sender: TObject);
begin
Edit5.Text := ReadString();
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
Edit4.Text := IntToStr (InBuffer);
end;
end.
Listing 3.7 Anwendung des Zeichenpuffers (Buffer.dpr)
Abb. 3.2 Senden und Empfangen von Text ((Buffer.gif))
Abb. 3.2 zeigt das laufende Programm mit einer eingestellten Puffergröße von zehn Zeichen. Der abgesendete Text wird hier auf die Größe des Puffers beschnitten. Überzählige Zeichen gehen verloren. Mit einem größeren Puffer können mehrere Textzeilen hintereinander gesammelt und dann zeilenweise ausgelesen werden.
Zugriff auf Handshakeleitungen
Alle direkten Zugriffe auf die Schnittstellenleitungen erfolgen hier nicht wie unter DOS über UART-Register, sondern über API-Funktionen. Die Ausgangsleitungen DTR, RTS und TXD lassen sich über die Funktion EscapeCommFunction setzen und zurücksetzten. Ihr werden vorbereitete Konstanten für einzelne Schaltaktionen übergeben. SETDTR und CLRDTR beeinflussen die DTR-Leitung, SETRTS und CLRRTS entsprechend die RTS-Leitung. SETBREAK setzt die TXD in den Break-Zustand (vgl. Kap 3.1), schaltet sie also ein. In diesem Zustand können keine seriellen Daten verschickt werden. Mit CLRBREAK wird TXD wieder in den Ruhezustand versetzt und für die Übertragung von Bytes freigegeben.
Zum Lesen der Eingangszustände an CTS, DSR, RI und DCD dient die API-Funktion GetCommModemStatus. Die Funktion liefert alle Informationen des Modem-Statusregisters aus dem UART. Über vordefinierte Bitmasken kann der Zustand jeder einzelnen Leitung ausgelesen werden. Zusätzlich wird aber hier auch die Funktion INPUTS gebildet, mit der man gleich alle vier Leitungen zusammen lesen kann. Das ermöglicht eine erhebliche Zeitersparnis, wenn mehreren Eingangsleitungen abgefragt werden müssen.
procedure DTR(State:integer);
begin
if (State=0) then EscapeCommFunction(PortHandle,CLRDTR)
else EscapeCommFunction(PortHandle,SETDTR);
end;
procedure RTS(State:integer);
begin
if (State=0) then EscapeCommFunction(PortHandle,CLRRTS)
else EscapeCommFunction(PortHandle,SETRTS);
end;
procedure TXD(State:integer);
begin
if (State=0) then EscapeCommFunction(PortHandle,CLRBREAK)
else EscapeCommFunction(PortHandle,SETBREAK);
end;
function CTS():Integer;
Var mask:Dword;
begin
GetCommModemStatus(PortHandle,mask);
if (mask and MS_CTS_ON)=0 then result:=0 else result:=1;
end;
function DSR():Integer;
Var mask:Dword;
begin
GetCommModemStatus(PortHandle,mask);
if (mask and MS_DSR_ON)=0 then result:=0 else result:=1;
end;
function RI():Integer;
Var mask:Dword;
begin
GetCommModemStatus(PortHandle,mask);
if (mask and MS_RING_ON)=0 then result:=0 else result:=1;
end;
function DCD():Integer;
Var mask:Dword;
begin
GetCommModemStatus(PortHandle,mask);
if (mask and MS_RLSD_ON)=0 then result:=0 else result:=1;
end;
function INPUTS():Integer;
Var mask:Dword;
begin
GetCommModemStatus(PortHandle,mask);
INPUTS := (mask div 16) and 15;
end;
Der Zugriff auf die Handshakeleitungen soll mit einem kleinen Programm demonstriert werden. Der Anwender kann alle drei Ausgänge umschalten und alle vier Eingangszustände ablesen.
Abb. 3.3 Zugriff auf die Handshakeleitungen ((RSio.gif))
Das Programm kann für COM1 und COM2 verwendet werden. Es startet zunächst mit COM2 und erkennt selbständig, wenn die Schnittstelle belegt ist. In diesem Fall wird automatisch COM1 geöffnet. Durch Anklicken der Umschaltknöpfe kann auch bei laufendem Programm umgeschaltet werden. Der Versuch, eine nicht freie Schnittstelle zu öffnen, führt zu einer Fehlermeldung.
Für die gegebene Aufgabe sind die Übertragungsparameter der Schnittstelle belanglos. Deshalb kann hier ein verkürzter Openstring "COM2", oder "COM1" verwendet werden. Die OpenCOM-Funktion gibt den Wert Eins zurück, wenn die Schnittstelle geöffnet werden konnte, im Fehlerfall dagegen den Wert Null.
Die Ausgangsprodeduren DTR, RTS und TXD erhalten Integerzahlen 0/1 als Übergabeparameter. Sie werden hier durch Integer-Typumwandlung aus den boolschen Zuständen True/False der CheckBoxes generiert. In umgekehrter Richtung werden alle Eingangsfunktionen CTS bis DCD in einer Timerfunktion aufgerufen und nach Umwandlung in Boolsche Variablen an die entsprechenden CheckBoxes zugewiesen.
unit RSioF;
interface
uses
RSCOM, Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TForm1 = class(TForm)
GroupBox1: TGroupBox;
CheckDTR: TCheckBox;
CheckRTS: TCheckBox;
CheckTXD: TCheckBox;
GroupBox2: TGroupBox;
CheckCTS: TCheckBox;
CheckDSR: TCheckBox;
CheckRI: TCheckBox;
CheckDCD: TCheckBox;
Timer1: TTimer;
ButtonCOM1: TRadioButton;
ButtonCOM2: TRadioButton;
procedure FormCreate(Sender: TObject);
procedure CheckDTRClick(Sender: TObject);
procedure CheckRTSClick(Sender: TObject);
procedure CheckTXDClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure ButtonCOM2Click(Sender: TObject);
procedure ButtonCOM1Click(Sender: TObject);
private
{ Private-Deklarationen}
public
{ Public-Deklarationen}
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.FormCreate(Sender: TObject);
begin
if (OpenCom ('COM2') = 0) then
ButtonCOM1.Checked := True;
end;
procedure TForm1.CheckDTRClick(Sender: TObject);
begin
DTR (Integer(CheckDTR.Checked));
end;
procedure TForm1.CheckRTSClick(Sender: TObject);
begin
RTS (Integer(CheckRTS.Checked));
end;
procedure TForm1.CheckTXDClick(Sender: TObject);
begin
TXD (Integer(CheckTXD.Checked));
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
CheckCTS.Checked := Boolean(CTS);
CheckDSR.Checked := Boolean(DSR);
CheckRI.Checked := Boolean(RI);
CheckDCD.Checked := Boolean(DCD);
end;
procedure TForm1.ButtonCOM2Click(Sender: TObject);
begin
CloseCOM;
if OpenCom ('COM2') = 0 then
ShowMessage ('COM Error');
end;
procedure TForm1.ButtonCOM1Click(Sender: TObject);
begin
CloseCOM;
if OpenCom ('COM1') = 0 then
ShowMessage ('COM Error');
end;
end.
Listing 3.8 Zugriff auf alle
Ein- und Ausgänge (RSIO.dpr)
Download: Delphi-Beispiele