3.4 Windows-API-Programmierung


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


weiter
zurück