Serielle Schnittstelle ansprechen und pollen
Da ich bisher eigentlich nur Fragen gestellt habe, wollte auch mal was für die Allgemeinheit tun…
Da ich viel Erfahrungen mit seriellen Schnittstellen gemacht habe, hier ein kleines, Crashkurs- artiges Tutorial. Gewisse Kenntnisse über die RS232 und Delphi bzw. API Programmierung setze ich voraus.
1. Benutzte GlobaleVariablen
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7:
| var PortTimeout : _COMMTIMEOUTS; PortHandle : Integer; PortDCB : TDCB; PortNr : Integer; PortState : Cardinal; WriteOverlapped,ReadOverlapped,StatusOs: TOverlapped; |
2. Die Schnittstelle öffnen / schließen / testen
Um die Schnittstelle zu öffnen und ein gültiges Schnittstellen- Handle zu bekommen, ruft man die Funktion CreateFile auf. Vereinfachtes Bsp:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
| function OpenCOM(Port: byte): boolean; stdcall; begin PortHandle := CreateFile(PChar('\\.\COM'+IntToStr(Port)),GENERIC_READ or GENERIC_WRITE,0, nil,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,LongInt(0)); if PortHandle > 0 then begin Result := true; InitOverlapped(WriteOverlapped); InitOverlapped(ReadOverlapped); InitOverlapped(StatusOs); end else Result := false; end; |
Die Ini Funktion für die Overlapped Strukturen:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8:
| procedure InitOverlapped(var Overlapped : TOverlapped); begin Overlapped.Offset := 0; Overlapped.OffsetHigh := 0; Overlapped.Internal := 0; Overlapped.InternalHigh := 0; Overlapped.hEvent := CreateEvent(nil,True,False,''); end; |
Um die Schnittstelle wieder zu schließen (und es gibt X mistige Programm die das vergessen, was zur Folge hat, dass danach andere Programme nicht mehr auf die Schnittstelle zugreifen können) geht man wie folgt vor:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7:
| procedure CloseCOM; stdcall; begin PurgeComm(PortHandle, PURGE_RXABORT or PURGE_RXCLEAR or PURGE_TXABORT or PURGE_TXCLEAR); SetCommMask(PortHandle,0); CloseHandle(PortHandle); PortHandle := 0; end; |
Bevor man jedoch die Schnittstelle öffnet, sollte man vielleicht auch testen, ob Sie denn verfügbar ist (oder ob die tolle Com-Konsole von Pipi Lüdi- Soft vergessen hat die Schnittstelle zu schließen). Dies mach ich ziemlich billig (Verbesserungsvorschläge jederzeit willkommen):
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
| function ComAvailable(ComNr: byte): boolean; stdcall; var TestHandle : integer; begin TestHandle := CreateFile(PChar('\\.\COM'+IntToStr(ComNr)),GENERIC_READ or GENERIC_WRITE,0, nil,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,LongInt(0)); if (TestHandle <= 0) then Result := false else begin Result := true; CloseHandle(TestHandle); end; end; |
2. Die Schnittstelle konfigurieren:
Die Konfiguration der Schnittstelle erfolgt über den Device Control Block (PortDCB) siehe Windows.pas – TDCB. Mit dieser Struktur lassen sich Parameter wie Baudrate, Flusssteuerung, StopBits usw. setzen und zwar wie im folgenden Beispiel die Baudrate:
Delphi-Quelltext
1: 2: 3: 4: 5: 6:
| function SetBaudRate(baud: cardinal): boolean; stdcall; begin GetCommState(PortHandle,PortDCB); PortDCB.BaudRate := baud; Result := SetCommState(PortHandle,PortDCB); end; |
Die Hilfe des MS-SDK gibt auch bereitwillig Auskunft zu den verwendeten Funktionen.
3. Abfragen und Setzen der Status Leitungen
Um CTS, DCD, DSR und Ring abzufragen, eignet sich die Funktion GetCommModemStatus.
Bsp:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7:
| var PortState: cardinal;
begin GetCommModemStatus(PortHandle,PortState); if ((Portstate and MS_CTS_ON) <> 0) then Result := true else Result := false; |
Um RTS, DTR, XOFF, XON oder einen Break zu setzen oder zurückzusetzen benutzt man die Funktion EscapeCommFunction:
Bsp. Setzen und zurücksetzen der DTR Leitung
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9:
| procedure SetDTR; stdcall; begin if (PortHandle <> 0) then EscapeCommFunction(PortHandle,5); end;
procedure ClearDTR; stdcall; begin if (PortHandle <> 0) then EscapeCommFunction(PortHandle,CLRDTR); end; |
In diesem Zusammenhang bedeutet XON/XOFF, dass ein Empfangen der entsprechenden XON/XOFF Chars simuliert wird. Ein Break ist, wenn ich die TX – Leitung dauerhaft auf 1 setze. ACHTUNG, ist ein Break gesetzt und ich versuche zu senden (mit WriteFile), wird das Programm eine sauberen Abgang hinlegen. Am Besten man lässt den Quatsch – braucht eh keiner.
4. Lesen und Schreiben
Jetzt wird’s endlich interessant. Zum Lesen und Schreiben verwendet man die Funktionen ReadFile und wie sollte es auch sonst sein, WriteFile. Eigentlich ist die Benutzung dieser Funktionen im Zusammenhang mit der seriellen Schnittstelle ganz einfach – wenn man ein Programmierbeispiel hat:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48:
| function SendString(Str: PChar): boolean; stdcall; var written : cardinal; tmpStr : string; i: LongBool; begin if (PortHandle <> 0) then begin tmpStr := string(Str); Result := not WriteFile(PortHandle,tmpStr[1],Length(tmpStr),written,@WriteOverlapped); end; end;
function ReadString(var Buf: PChar): Cardinal; stdcall; var StrBuf: string; Error,BytesRead: DWORD; Status: TComStat; RdSuccess: boolean; begin BytesRead := 0; if (PortHandle <> -1) then begin ClearCommError(PortHandle,Error,@Status); case Error of CE_BREAK,CE_DNS,CE_FRAME,CE_IOE,CE_MODE,CE_OOP,CE_OVERRUN,CE_PTO,CE_RXOVER, CE_RXPARITY,CE_TXFULL: begin BytesRead := 0; end else begin SetLength(StrBuf,Status.cbInQue); RdSuccess := ReadFile(PortHandle,StrBuf[1],Status.cbInQue,BytesRead,@ReadOverlapped); if not RdSuccess then BytesRead := 0; end; end; end; if (BytesRead > 0) then begin Result := BytesRead; try StrPCopy(Buf,StrBuf); except Result := 0; end; end else Result := 0; end; |
An dieser Stelle eine Anmerkung:
Ich habe mir für den Schnittstellen Zugriff zunächst eine Delphi Unit und dann eine DLL geschrieben. Aus der stammen die ganzen Funktionen. Daher übergebe ich auch PChars. Ich bitte etwaige Unelegantheiten im Bezug auf PChar – string zu entschuldigen – die kommen durch das Hin- und Her. Um das ganze als Delphi Unit zu verwenden, sollte überall die Direktive stdcall entfernt werden.
Und nun *Trommelwirbel* das interessanteste:
5. Schnittstellen Polling
Möchte man benachrichtigt werden, wenn auf die Schnittstelle (von aussen) zugegriffen wird (Daten gesendet oder Statusleitungen gesetzt werden), so musste man in grauer Vorzeit die Schnittstelle manuell zyklisch abfragen (Polling). Dies kann man heute sehr elegant mit einem Thread erledigen. Das ganze funktioniert im Groben so:
Man erstelle einen Thread, man halte in an mit der Funktion WaitCommEvent. Der Thread ist nun angehalten und zwar bis eines der Ereignisse eingetreten ist, die man vorher mit SetCommMask spezifiziert hat. Wird der Thread fortgesetzt ist eines der Ereignisse eingetreten. Will man den Thread von „aussen“ aus dem Wartezustand befreien macht man das so:
Delphi-Quelltext
1:
| SetCommMask(PortHandle,0); |
Hier nun ein Bsp. Für einen Polling- Thread. In diesem Beispiel wird, wenn ein Ereignis eintritt, die Funktion ReleaseEvent aufgerufen, die ich hier weglasse, da sie zu speziell wäre.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37:
| type TSerialThread = class(TThread) protected fComEvent: cardinal; procedure Execute; override; public constructor Create; property ComEvent: cardinal read fComEvent; end;
constructor TSerialThread.Create; begin FreeOnTerminate:= True; inherited Create(False); end;
procedure TSerialThread.Execute; var SerialEvent, WaitResult, BytesRead: Cardinal; begin SetCommMask(PortHandle,EV_CTS or EV_BREAK or EV_DSR or EV_ERR or EV_RING or EV_RLSD or EV_RXCHAR or EV_RXFLAG or EV_TXEMPTY); if not WaitCommEvent(PortHandle,SerialEvent,@StatusOs) then begin if (GetLastError = ERROR_IO_PENDING) then begin WaitResult := WaitForSingleObject(StatusOs.hEvent,INFINITE); case WaitResult of WAIT_OBJECT_0: begin if GetOverlappedResult(PortHandle,StatusOs,BytesRead,false) then ReleaseEvent(SerialEvent); end; end; end; end else ReleaseEvent(SerialEvent); end; |
Dieser Thread stellt lediglich die Mindestanforderung an einen Polling thread dar. Für den Hausgebrauch (und mehr) sollte das reichen.
So, bevor ich mich noch mehr zu dem Thema auslasse, will ich erst mal sehen ob das irgendein Schwein interessiert.
MfG SchelmVomElm