Obsługa portu com z poziomu Delphi

aZgon
Ten artykuł wymaga dopracowania!

Jeżeli możesz popraw ten artykuł według zaleceń, które możesz znaleźć na stronie [[Artykuły do poprawy]]. Po dopracowaniu tego tekstu można usunąć ten komunikat.

Otwieranie i zamykanie portu

Jak zaznaczyłem poprzednio, obsługa komunikacji szeregowej w Win32 ( Windows 95,98,NT ) - pomimo wielu podobnych funkcji - opiera się na całkiem innych mechanizmach. Przy tym ich zakres i możliwości są dużo bardziej rozbudowane. O ile więc dla Win16 udało się z grubsza omówić wszystkie praktyczne aspekty - tutaj trzeba będzie ograniczyć się do wybranych zagadnień. Cel jest taki sam : realizacja łączności warsztatowej z poziomu Windows 95 z układami i przyrządami mikroprocesorowymi.

Różnice pojawiają się już w chwili otwierania portu. Nie przewidziano do tego odrębnej funkcji - używamy "wielozadaniowej" funkcji CreateFile ( w Delphi funkcje API są opisane w odrębnym pliku pomocy, natomiast zawarte są w module Windows - generalnie używana jest składnia C ).

HANDLE CreateFile (
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile );

Dla portu szeregowego ( urządzenie komunikacyjne ) parametry są następujące :

  • lpFileName - wskaźnik na null-terminated string opisujący nazwę portu (PChar): w Delphi 2/3 domyślnie używany Ansi String jest przy odczycie kompatybilny z PChar i należy tylko wykonać rzutowanie typu, czyli np. PChar('COM1') ;
  • dwDesiredAccess - określenie rodzaju dostępu : GENERIC_READ or GENERIC_WRITE (zapis i odczyt);
  • dwShareMode - określenie współdzielenia zasobu : tutaj 0 ( wielodostępność zablokowana );
  • lpSecurityAttributes - wskaźnik na strukturę opisującą zabezpieczenia : tutaj nil ;
  • dwCreationDistribution - określa rodzaj akcji wykonywanej na istniejącym lub nie istniejącym pliku - tutaj mamy do czynienia z istniejącym urządzeniem i musimy użyć OPEN_EXISTING ;
  • dwFlagsAndAttributes - określa atrybuty pliku ( tutaj całkowicie pomijane ) oraz sposób realizacji dostępu do pliku / urządzenia : i tu pojawia się kwestia kluczowa dla dalszej organizacji obsługi portu ( dostęp synchroniczny czy asynchroniczny ? ) - będzie ona oddzielnie omówiona ;
  • hTemplateFile - uchwyt dodatkowego pliku z rozszerzonym zestawem atrybutów - tutaj 0.

Typem Pascalowym zwracanym przez funkcję jest THandle.
Zanim przejdziemy dalej, należy koniecznie zdecydować się, jak będziemy obsługiwać nasz port :synchronicznie czy asynchronicznie. Zależy bowiem od tego nie tylko sposób realizacji funkcji komunikacyjnych ( wyprzedzająco : ReadFile, WriteFile, WaitCommEvent ) ale także cała koncepcja umieszczenia obsługi portu w naszej aplikacji.
W trybie synchronicznym każda funkcja komunikacyjna zwraca rezultat dopiero po zakończeniu realizacji zadanej operacji. Merytorycznie - pomijając cały sztafaż Windows - zachowuje się więc jak np. konsolowy ReadLn : wątek wywołujący tę funkcję zatrzymuje się i czeka na skompletowanie procesu. O ile jest to całkiem do przyjęcia przy operacjach szybkich i przewidywalnych ( np. obsługa plików dyskowych ) , to np. przy oczekiwaniu na odbiór transmisji może prowadzić do wielu niemiłych efektów.
Natomiast w trybie asynchronicznym funkcja od razu zwraca sterowanie, zaś do zgłoszenia wykonania swojej operacji używa odrębnych mechanizmów ( nie komunikatów ! ). Większa elastyczność i skuteczność jest niestety opłacona znaczną komplikacją programu. Z powyższego wynika jednakże wniosek, że w rzeczywistych aplikacjach, wykraczających poza modelowe czy szkoleniowe przykłady, należy dla uzyskania właściwych efektów stosować tryb asynchroniczny. Dalsze opisy będą więc dotyczyć rozwiązań opartych na tym trybie.
Dla trybu asynchronicznego należy użyć w dwFlagsAndAttributes flagi FILE_FLAG_OVERLAPPED. Trzeba podkreślić, że trybów nie powinniśmy łączyć, więc użycie tej flagi narzuca późniejsze konsekwentne wywołania asynchroniczne.
Przykładowe otworzenie portu szeregowego może więc wyglądać np. tak :

var
ComHandle :THandle;
....
function OpenPort ( PortNumber : String ) : THandle;
{ tu uwaga : zakładam, że używany jest domyślnie typ Ansi String - jeśli było to zmieniane trzeba wcześniej przywrócić dyrektywą {$H+} }
var
OpeningHandle : THandle ;
begin
Result:=0;
OpeningHandle := CreateFile (
PChar ( PortNumber ),
GENERIC_READ or GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
0 );
{ funkcja zwraca w przypadku powodzenia uchwyt portu, natomiast jeśli coś nie wyjdzie - wartość INVALID_HANDLE_VALUE ( -1) }
if ( OpeningHandle = INVALID_HANDLE_VALUE ) then Exit;
{ tu można też wywołać funkcję GetLastError żeby sprawdzić, co było przyczyną - w uproszczonej praktyce takie sprawdzanie z powodzeniem pomijamy }
{ dodatkowo możemy sprawdzić, czy uzyskany uchwyt jest właściwy dla portu - urządzenia znakowego}
if ( GetFileType ( OpeningHandle ) <> FILE_TYPE_CHAR ) then Exit;
{ wszystko poprawne - więc : }
Result := OpeningHandle;
end;

a później na przykład :

ComHandle := OpenPort ( 'COM1' );
if ( ComHandle = 0 ) then [ informacja o błędzie otwarcia ]

Uzyskany uchwyt jest później używany w wywołaniach funkcji konfiguracyjnych i komunikacyjnych ( jest odpowiednikiem identyfikatora portu z Win16 ).
Do zamknięcia otwartego w ten sposób portu służy funkcja ( też uniwersalna ):
CloseHandle ( AHandle : THandle );
jako parametr podajemy właśnie nasz uchwyt ComHandle.


Konfigurowanie otwartego portu

(2)Etap poprzedni :
po otwarciu portu uzyskaliśmy jego
uchwyt : ComHandle.
Konfigurowanie portu - parametry (struktury danych).
Konfiguracja jest zapisywana w strukturach : TDcb ( taka sama nazwa ale zmieniony układ ) oraz TCommTimeouts ( nowość - pozwala dowolnie ustawiać timeouty transmisji ). Rekord TDcb może być użyty indywidualnie albo jako składnik nadrzędnej struktury TCommConfig ( posługiwanie się TCommConfig udostępnia nam gotowe systemowe okienko dialogowe nastaw portu ! ). Występują także inne specjalizowane struktury ( CommProp, ModemSettings, ModemDevCaps ), które przy naszych zastosowaniach można pominąć.

Podstawowe ustawienia portu są zawarte w rekordzie TDcb :

TDCB = 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;

DCBlength - rozmiar rekordu ( czyli SizeOf (TDcb) ) - nie musimy go jawnie ustawiać;
BaudRate - szybkość transmisji, podobnie jak poprzednio może być podana bezpośrednio albo jako predefiniowany indeks ( CBR_ ... : 110, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 56000, 57600, 115200, 128000, 256000 ) np. CBR_9600 ;
Flags - flagi i pola bitowe :


bit 0 - fBinary
tryb binarny transmisji. W odróżnieniu od Win16, Win32 zawsze używa trybu binarnego i flaga ta jest ( i powinna pozostać ) ustawiona ;
bit 1 - fParity
przy ustawieniu prowadzone jest sprawdzanie parzystości i zgłaszane są błędy ;
bit 2 - fOutxCtsFlow
sterowanie wyjścia linią Cts - przy ustawionej fladze wyłączenie CTS wstrzymuje nadawanie ;
bit 3 - fOutxDsrFlow
to samo tylko dla linii DSR ;
bit 4,5 - fDtrControl
pole bitowe o predefiniowanych wartościach :
DTR_CONTROL_DISABLE - linia DTR wyłączona ;
DTR_CONTROL_ENABLE - linia DTR włączona ;
( w powyższych przypadkach możemy zmieniać stan funkcją EscapeCommFunction );
DTR_CONTROL_HANDSHAKE - linia DTR używana do handshakingu;
( próba ustawienia funkcją EscapeCommFunction powoduje błąd );
Tu uwaga : wartości odnoszą się do 2-bitowego pola. Ponieważ Pascal nie umożliwia operacji na polu bitowym potrzebna jest dodatkowa manipulacja przesuwająca
( np. MyDtrState := DTR_CONTROL_ENABLE * 16 ; Flags := Flags OR MyDtrState ).
bit 6 - fDsrSensivity
sterowanie odbiornika linią Dsr - przy ustawionej fladze port ignoruje znaki przysyłane przy niskim stanie linii DSR ;
bit 7 - fTXContinueOnXoff
określa co w trybie Xon/Xoff port robi z własnym nadawaniem w chwili gdy nie nadążył z odbiorem i wysłał do partnera znak XoffChar ( żądanie wstrzymania nadawania ). Przy ustawionej fladze własne nadawanie jest nadal kontynuowane, przy zgaszonej - jest wstrzymane aż do chwili wysłania XonChar ( inaczej mówiąc port steruje swoim nadajnikiem tak samo jak tego żąda od partnera );
bit 8 - fOutX
określa użycie trybu Xon/Xoff przy nadawaniu. Ustawienie flagi powoduje wstrzymanie nadawania po odebraniu znaku XoffChar i wznowienie po odebraniu XonChar;
bit 9 - fInX
określa użycie trybu Xon/Xoff przy odbiorze. Przy ustawionej fladze port wysyła żądanie wstrzymania transmisji ( XoffChar ) jeśli liczba znaków w kolejce odbiornika przekroczy XoffLim, natomiast po zredukowaniu kolejki poniżej XonLim - wysyła żądanie wznowienia ( XonChar );
bit 10 - fErrorChar
jeśli mamy włączone sprawdzanie parzystości ( fParity ) to ustawienie flagi powoduje zastępowanie znaków odebranych z błędem parzystości znakiem ErrorChar;
bit 11 - fNull
przy ustawieniu tej flagi odbierane bajty o wartości 0 są ignorowane;
bit 12,13 - fRtsControl
pole bitowe o predefiniowanych wartościach :
RTS_CONTROL_DISABLE - linia RTS wyłączona ;
RTS_CONTROL_ENABLE - linia RTS włączona;
( stan można zmieniać funkcją EscapeCommFunction );
RTS_CONTROL_HANDSHAKE - RTS używane do handshakingu;
( użycie EscapeCommFunction powoduje błąd );
Handshaking z użyciem linii sprzętowych jest nieco inny niż Xon/Xoff. Nie korzysta z XonLim i XoffLim. Linia jest gaszona gdy kolejka odbiornika zostaje zapełniona powyżej 3/4 pojemności zaś ustawiana ponownie gdy kolejka opróżni się poniżej połowy pojemności.
RTS_CONTROL_TOGGLE - RTS jest ustawiane gdy w kolejce nadajnika są znaki do wysłania, zaś gaszone po opróżnieniu kolejki nadajnika; ( tu znów należy pamiętać o Pascalowym ustawianiu pola bitowego );
bit 14 - fAbortOnError
w odróżnieniu od Win16 mamy do wyboru czy port zatrzymuje się po wystąpieniu błędu czy kontynuuje pracę. Przy ustawionej fladze port wstrzymuje wszystkie operacje zapisu i odczytu oraz ignoruje wszelkie dalsze operacje komunikacyjne zanim aplikacja nie "skwituje" błędu poprzez wywołanie ClearCommError;
bity 15 - 31
rezerwa, nie używać.
*
wReserved - musi być ustawione na 0;
XonLim - próg wysłania znaku XonChar ( jak opisano wyżej ) ;
XoffLim - próg wysłania znaku XoffChar ( jw. ) ;
ByteSize - ilość bitów danych w przesyłanym znaku ( 4 - 8 ) ;
Parity - rodzaj parzystości ; predefiniowane stałe to : EVENPARITY, MARKPARITY, NOPARITY, ODDPARITY, SPACEPARITY ;
StopBits - liczba bitów stopu ; predefiniowane : ONESTOPBIT, ONE5STOPBITS, TWOSTOPBITS ;
XonChar - znak żądania wznowienia transmisji ;
XoffChar - znak żądania wstrzymania transmisji ; ( takie same dla odbioru i nadawania ) ;
ErrorChar - znak zastępczy dla znaków odebranych z błędem parzystości ;
EofChar - znak określający koniec danych ( z opisu wynika, że i tak nie wykorzystuje się go z racji niedostępności trybu znakowego - nie sprawdzałem );
EvtChar - znak, którego odebranie powoduje zgłoszenie zdarzenia portu;
wReserved 1 - nie używać - zarezerwowane.

Uwaga : dla starszych kostek UART ( 8250 ) zabronione są ustawienia :

  • ByteSize = 4,
  • ByteSize=5 + StopBits=TWOSTOPBITS,
  • ByteSize=6,7,8 + StopBits=ONE5STOPBITS.

Rekord TDcb może być traktowany jako pole struktury nadrzędnej :

TCommConfig = record
dwSize: DWORD;
wVersion: Word;
wReserved: Word;
dcb: TDCB;
dwProviderSubType: DWORD;
dwProviderOffset: DWORD;
dwProviderSize: DWORD;
wcProviderData: array[0..0] of WCHAR;
end;

gdzie :
dwSize - rozmiar rekordu ;
wVersion - numer wersji struktury ( dla Windows 95 = 1 ) ;
wReserved - zarezerwowane - nie używać ;
dcb - to właśnie nasz rekord opisujący urządzenie szeregowe RS 232 ;
dwProviderSubType - określa typ urządzenia realizującego komunikację, w naszym przypadku jest to PST_RS232 ;


pozostałe pola określają dodatkową strukturę danych urządzenia komunikacyjnego, dla PST_RS232 nieistotne.

Następne informacje istotne dla pracy portu szeregowego są zawarte w rekordzie opisującym timeouty :

TCommTimeouts = record
ReadIntervalTimeout: DWORD;
ReadTotalTimeoutMultiplier: DWORD;
ReadTotalTimeoutConstant: DWORD;
WriteTotalTimeoutMultiplier: DWORD;
WriteTotalTimeoutConstant: DWORD;
end;

ReadIntervalTimeout - to czas timeoutu ( analogiczny do stosowanego w Win16 ) pomiędzy odbiorem kolejnych 2 znaków - w milisekundach. Podczas realizacji funkcji ReadFile zliczanie timeoutu rozpoczyna się po odebraniu pierwszego znaku. Jeśli czas pomiędzy kolejnymi znakami przekroczy zadaną wartość - operacja odczytu zostaje skompletowana. ( Przez skompletowanie operacji należy rozumieć zwrot wartości funkcji przy wywołaniu synchronicznym oraz ustawienie skojarzonego zdarzenia w stan "signaled" przy trybie asynchronicznym - o czym później ).
Timeout możemy wyłączyć ustawiając wartość pola na 0.
ReadTotalTimeoutMultiplier itd. :
Został dodatkowo wprowadzony timeout całkowity ( oddzielnie ustawiany dla odbioru i nadawania ). Składa się on z części stałej ( xxTimeoutConstant ) oraz mnożnika ( xxTimeoutMultiplier ) * ilość znaków do realizacji ( podana jako jeden z argumentów funkcji ReadFile , WriteFile ). Pola podajemy także w milisekundach, wartość 0 wyłącza pole. Reakcja funkcji jest taka sama z tym, że timeout jest zliczany od chwili wywołania.
Ustawienia timeoutu odbioru mają ponadto kilka specyficznych kombinacji :

  • ReadIntervalTimeout = MAXDWORD + ReadTotalxx = 0 :
    funkcja ReadFile zwraca wartość natychmiast nawet jeśli żaden znak nie został odebrany ( natomiast są odczytane z kolejki ewentualnie znajdujące się tam znaki odebrane wcześniej );
  • dla Windows 95 : jeśli :
    ReadIntervalTimeout = MAXDWORD +
    ReadTotalTimeoutMultiplier = MAXDWORD +
    0 < ReadTotalTimeoutConstant < MAXDWORD to :
  • jeśli kolejka odbiornika jest pusta to ReadFile czeka na pierwszy odebrany znak, gdy przyjdzie on przed czasem ReadTotalTimeoutConstant - funkcja kończy natychmiast z odczytem tego znaku, gdy czas minie bez znaku - funkcja kończy ze zgłoszeniem timeoutu,
  • jeśli w kolejce są wcześniej odebrane znaki - ReadFile kończy natychmiast, odczytując te znaki.
    Ustawienia timeoutu nadawania są istotne praktycznie tylko gdy korzystamy z handshakingu ( możemy też zauważyć, że nie ma flag opisujących timeouty handshakingu - aplikacja musi timeout handshakingu zlokalizować w obsłudze funkcji WriteFile ).

Konfigurowanie portu - funkcje.

(3)Etap poprzedni :
mamy port szeregowy identyfikowany przez
uchwyt ComHandle, otwarty do obsługi asynchronicznej,
poznaliśmy struktury danych opisujące konfigurację portu.
Konfigurowanie portu - funkcje.
Do operowania na w/w strukturach danych służą funkcje konfiguracyjne :
BuildCommDCB ;
BuildCommDCBAndTimeouts ;
CommConfigDialog ;
GetCommConfig ;
GetCommState ;
GetCommTimeouts ;
SetCommConfig ;
SetCommState ;
SetCommTimeouts ;
SetupComm ;
Poza tym do uaktywniania poszczególnych typów zdarzeń, na które ma reagować port, używamy jak poprzednio :
SetCommMask ;

Procedura konfiguracji jest taka jak poprzednio :

  1. Deklarujemy w aplikacji potrzebne struktury.
  2. Ładujemy do nich wartości portu funkcjami Getxx.
  3. Wprowadzamy wymagane przez nas ustawienia.
  4. Wpisujemy zmienione struktury do portu funkcjami Setxxx.

Ad.1 .
Używamy struktur wg potrzeb. Np. rekord Ustawienia_Portu : TDcb może być niezależną zmienną albo polem TCommConfig. Rekord TCommTimeouts może wystąpić lub nie - o ile zadowalają nas wartości domyślne ; itd.
Ad. 2.
GetCommState ( AComHandle : THandle ; var ADcb : TDCB ) : Boolean ;
gdzie AComHandle - uchwyt obsługiwanego portu ( u nas ComHandle ),
ADcb - rekord ustawień portu ( przekazany przez wskaźnik ).
Funkcja zwraca True po wykonaniu pomyślnym. Wypełnia rekord bieżącymi nastawami podanego portu.

GetCommTimeouts ( AComHandle : THandle ; var ACommTimeouts : TCommTimeouts ) : Boolean ;
Przy pomyślnym wykonaniu ( zwrot True ) ładuje do rekordu ACommTimeouts bieżące wartości timeoutów dla podanego portu.

GetCommConfig ( AComHandle : THandle ; var ACommConfig : TCommConfig ; var ASize : Integer ) : Boolean ;
ACommConfig - rekord konfiguracji portu,
ASize - rozmiar tego rekordu. Tu uwaga : ponieważ rozmiar jest przekazany przez wskaźnik należy go zdefiniować jako oddzielną zmienną Integer :
...
var
ConfRecord : TCommConfig ;
ConfRecordSize : Integer ;
...
ConfRecordSize := SizeOf ( TCommConfig ) ;
GetCommConfig ( ComHandle, ConfRecord, ConfRecordSize );
...
Ad. 3.
Ustawianie parametrów wg naszych potrzeb możemy prowadzić :

  • z poziomu programu,
  • używając własnych okien dialogowych,
  • używając systemowego okienka dialogowego,
  • używając uproszczonych funkcji konfigurujących.
    Nic też nie stoi na przeszkodzie, aby te metody połączyć.
    Dwa pierwsze sposoby są najbardziej elastyczne i osobiście najchętniej je stosuję. Jednak przy uproszczonej konfiguracji "gotowce" mogą zaoszczędzić sporo pracy.
    Funkcja :
    CommConfigDialog ( AName : PChar ; AHandle : HWnd ; var ACommConfig : TCommConfig ) : Boolean ;
    pokazuje znane z różnych programów komunikacyjnych dla Windows okienko dialogowe pozwalające zmienić podstawowe parametry pracy portu ( dodatkową interesującą opcją jest ustawianie stopnia wykorzystania wewnętrznych buforów FIFO nowszych kostek UART ). Jak widać, wśród parametrów nie ma uchwytu portu - dialogu możemy używać bez otwartego portu, określając go przez nazwę ( AName - np. PChar ('COM1' ) ).
    AHandle określa okno - "właściciela" dialogu - może być 0.
    ACommConfig - to np. ConfRecord z przykładu.
    Funkcja zwraca True przy potwierdzeniu zmian przyciskiem OK. Należy jedynie pamiętać, że nawet przy otwartym porcie wprowadzenie danych ogranicza się do ich zapisania w rekordzie - nie przekazuje ich do portu. ( Podobnie przy otwieraniu - pokazuje dane z rekordu ACommConfig - czy są to rzeczywiste dane portu, zależy od poprzedniego użycia GetCommConfig ).
    Tak samo jest z funkcjami BuildCommxx . Natomiast nie wymagają one wcześniejszego załadowania bieżących danych portu - i tak wszystkie parametry zostają ustawione od nowa ( część wg naszej komendy a reszta domyślnie ).
    BuildCommDCB( AOpis : PChar ; var ADcb : TDCB ) : Boolean ;
    ładuje rekord ADcb wartościami podanymi w opisie ustawień AOpis. Zwraca True jeśli zakończona powodzeniem.
    AOpis jest DOS-owym stringiem konfigurującym ( z komendy MODE ) :
    np. '96,n,8,1' ( 9600 baud, bez parzystości, 8 bitów danych, 1 bit stopu ).
    Jeśli podamy nazwę portu - to chociaż jest ona pomijana, musi jednak opisywać prawidłowy istniejący port, np. 'COM1: baud=2400 parity=N data=8 stop=1'. Przy tych wywołaniach domyślnie wyłączony jest cały handshaking ( XOn/XOff i sprzętowy ) , czyli :
  • fInX, fOutX, fOutXDsrFlow, fOutXCtsFlow = False ;
  • fDtrControl = DTR_CONTROL_ENABLE ;
  • fRtsControl = RTS_CONTROL_ENABLE .
    Handshaking typu XOn/XOff możemy włączyć dodając na końcu stringu 'x':
    np. '96,n,8,1,x' , wtedy :
  • fInX, fOutX = True ;
  • fOutXDsrFlow, fOutXCtsFlow = False ;
  • fDtrControl = DTR_CONTROL_ENABLE ;
  • fRtsControl = RTS_CONTROL_ENABLE .
    Natomiast handshaking sprzętowy włączamy literą 'p' na końcu, np.
    '96,n,8,1,p'. Wtedy :
  • fInX, fOutX = False ;
  • fOutXDsrFlow, fOutXCtsFlow = True ;
  • fDtrControl = DTR_CONTROL_HANDSHAKE ;
  • fRtsControl = RTS_CONTROL_HANDSHAKE .

BuildCommDCBAndTimeouts ( AOpis :PChar ; var ADcb : TDcb ; var ACommTimeouts : TCommTimeouts ) : Boolean;
Zwraca True przy powodzeniu. Ponieważ nie sprawdzałem tej funkcji ( wymaga rozszerzonego stringu AOpis, uwzględniającego nastawy timeoutów ) - nie opisuję jej dokładnie.
Ad.4.
Jak zaznaczyłem - ustawione w odpowiednich strukturach parametry należy wpisać do odpowiedniego portu. Poszczególne funkcje są odpowiednikami funkcji Getxx :
SetCommState ( AComHandle : THandle ; var ADcb : TDCB ) : Boolean ;
gdzie
AComHandle - uchwyt obsługiwanego portu ( u nas ComHandle ),
ADcb - rekord ustawień portu ( przekazany przez wskaźnik ).
Funkcja zwraca True po wykonaniu pomyślnym.

SetCommTimeouts ( AComHandle : THandle ; var ACommTimeouts : TCommTimeouts ) : Boolean ;
Przy pomyślnym wykonaniu zwraca True.

SetCommConfig ( AComHandle : THandle ; var ACommConfig : TCommConfig ; var ASize : Integer ) : Boolean ;
AComHandle - uchwyt portu,
ACommConfig - rekord konfiguracji portu,
ASize - rozmiar tego rekordu. Zwrot True przy sukcesie.
Dodatkowo możemy ustawić długość kolejek odbiornika i nadajnika przy pomocy funkcji :
SetupComm ( AComHandle : THandle ; InQueue, OutQueue : Integer ) : Boolean ;
AComHandle - jw.
InQueue - rozmiar w bajtach kolejki odbiornika,
OutQueue - rozmiar w bajtach kolejki nadajnika.
Rozmiary kolejek ustawiamy wg przewidywanego protokołu transmisji.
Funkcja zwraca True przy sukcesie.

No i jeszcze bardzo ważna rzecz : ustawienie maski zdarzeń, na które ma reagować nasz port. Realizuje to ( podobnie jak w Win 16 ) funkcja :
SetCommMask ( AComHandle : THandle ; EvtMask : Integer ) : Boolean ;
AComHandle - jw.
EvtMask - poszczególne bity ( flagi ) odpowiadają zdarzeniom portu.
Ustawienie flagi powoduje uaktywnienie danego zdarzenia :
EV_RXCHAR = 1 - odebrany i umieszczony w kolejce odbiornika dowolny znak ;
EV_RXFLAG = 2 - odebrany i umieszczony w kolejce odbiornika znak oznaczony w strukturze Dcb jako flagowy ;
EV_TXEMPTY = 4 - wysłano ostatni znak z kolejki nadajnika ;
EV_CTS = 8 - uległ zmianie stan linii Cts ;
EV_DSR = $10 - uległ zmianie stan linii Dsr ;
EV_RLSD = $20 - uległ zmianie stan linii Rlsd ;
EV_BREAK = $40 - wykryto na wejściu sygnał Break ;
EV_ERR = $80 - został wykryty błąd linii ( ramka, przepełnienie albo parzystość - CE_FRAME , CE_OVERRUN , CE_RXPARITY ) ;
EV_RING = $100 - wykryty sygnał dzwonka ;
EV_RX80FULL = $200 - kolejka odbiornika zastała wypełniona w 80 % .
Przy ustawianiu maski należy zawsze uwzględnić zdarzenia traktowane jako błąd komunikacji ( uwzględniane w słowie stanu błędu ) - należą do nich : EV_ERR i EV_BREAK.

Jako uzupełnienie można wspomnieć o funkcji GetCommProperties , która pozwala na uzyskanie bardzo szczegółowych informacji o urządzeniu komunikacyjnym ( zakresy dostępnych parametrów itp. ). W praktyce amatorskiej nie jest to zbyt potrzebne, a opis jest obszerny, więc go nie przytaczam.


Asynchroniczna obsługa skonfigurowanego portu

(4)Etap poprzedni :
mamy port szeregowy identyfikowany przez uchwyt ComHandle,
otwarty do obsługi asynchronicznej, skonfigurowany wg naszych potrzeb.
Obsługa portu - funkcje podstawowe.
Do odczytu znaków z portu szeregowego służy uniwersalna funkcja :
ReadFile ( AComHandle : THandle ; ReadBuffer : PChar ; BytesToRead : Integer ; var ReadBytesNumber : Integer ; AReadOverlapped : POverlapped ) : Boolean ;

Do zapisu :
WriteFile ( AComHandle : THandle ; WriteBuffer : PChar ; BytesToWrite : Integer ; var WrittenBytesNumber : Integer ; AWriteOverlapped :POverlapped ) : Boolean ;

Do kontroli zdarzeń portu :
WaitCommEvent ( AComHandle : THandle ; var AEvtMask : Integer ; AEventOverlapped : POverlapped ) : Boolean ;

We wszystkich funkcjach AComHandle oznacza uchwyt portu ( zwrócony przez funkcję CreateFile ).
WriteBuffer i ReadBuffer to wskaźniki na bufory znakowe. Do ReadBuffer są przy odbiorze przepisywane znaki z kolejki odbiornika, natomiast przy wysyłaniu przepisujemy do kolejki nadajnika znaki z WriteBuffer.
BytesToRead oraz BytesToWrite określają ile znaków chcemy odczytać / zapisać.
ReadBytesNumber oraz WrittenBytesNumber są wskaźnikami na zmienne typu Integer, w których funkcje powinny umieścić liczbę znaków odczytanych / wysłanych . ( W praktyce okazuje się, że nie bardzo chce to działać w przypadku wystąpienia timeoutu i należy dodatkowo posługiwać się funkcją GetOverlappedResult ).
AEvtMask jest wskaźnikiem na słowo stanu zdarzeń portu. Funkcja lokuje w tej zmiennej flagi zachodzących zdarzeń ( tych, które odblokowaliśmy konfigurując maskę zdarzeń funkcją SetCommMask ).
Natomiast wszystkie wskaźniki POverlapped ( AReadOverlapped, AWriteOverlapped, AEventOverlapped ) wskazują na struktury typu TOverlapped, które muszą być zdefiniowane dla powyższych funkcji przy ich wywołaniach asynchronicznych ( czyli takich, jakie sobie zadeklarowaliśmy flagą FILE_FLAG_OVERLAPPED w wywołaniu CreateFile ).
Budowa struktury typu TOverlapped jest następująca :

TOverlapped = record
Internal : DWord;
InternalHigh : DWord;
Offset : DWord ;
OffsetHigh : DWord ;
Event : THandle ;
end ;

Pola Internal oraz InternalHigh są zarezerwowane dla systemu operacyjnego. Pola Offset oraz OffsetHigh łącznie określają pozycję w pliku, od której rozpoczyna się proces odczytu / zapisu. Dla urządzenia komunikacyjnego ich zawartość jest pomijana i nie ma znaczenia. Najważniejszym dla nas polem jest Event - uchwyt zdarzenia wskazującego na zakończenie działania funkcji. Zdarzenie przechodzi w stan signaled gdy czynność rozpoczęta wywołaniem funkcji zostaje zakończona - czy to z powodu skompletowania operacji czy też wystąpienia timeoutu. Inaczej mówiąc - zdarzenie jest powiązane z daną funkcją za pomocą struktury TOverlapped skojarzonej z tą funkcją.
Zdarzenie jest samodzielnym obiektem Windows, który musimy przed użyciem utworzyć, a kończąc program - zwolnić. Delphi wspomaga nas klasą TEvent. Obiekt utworzymy przy pomocy konstruktora klasy :
TEvent.Create ( EventAttributes : PSecurityAttributes ; ManualReset, InitialState : Boolean ; const Name : String ) ;
W naszym przypadku atrybuty bezpieczeństwa są zbędne,
ManualReset powinien być ustawiony ( wtedy zdarzenie trzeba wyłączać samodzielnie : albo metodą ResetEvent ; albo ponownie wywołując funkcję komunikacyjną - która robi to automatycznie ),
InitialState - zgaszony ( False - zdarzenie jest utworzone w stanie non signaled ).
Parametr Name jest używany gdy chcemy udostępnić obiekt w całym systemie, tutaj nie jest to potrzebne. Czyli przygotowanie funkcji komunikacyjnych do pracy może wyglądać np. tak :

//[...]
var
ReadOverlapped : TOverlapped ;
WriteOverlapped : TOverlapped ;
EventOverlapped : TOverlapped ;
ReadEvent, WriteEvent, EvtEvent : TEvent ;
//[...]
ReadEvent := TEvent.Create ( nil, True, False, '' ) ;
WriteEvent := TEvent.Create ( nil, True, False, '' ) ; 
EvtEvent := TEvent.Create ( nil, True, False, '' ) ;

ReadOverlapped. Event := ReadEvent.Handle ;
WriteOverlapped. Event := WriteEvent.Handle ;
EventOverlapped. Event := EvtEvent.Handle ;
//[...]

( Przy zwalnianiu obiektów zdarzeń także korzystamy z metod klasy, czyli np. ReadEvent. Free ; zamiast funkcji API ).
Jeśli teraz wywołamy np.
Result := ReadFile ( ComHandle, ReadBuffer, 16, ReadBytesNumber, @ReadOverlapped ) ;
przy założeniu, że kolejka odbiornika jest pusta i że nie mamy ustawionych timeoutów, to od razu otrzymamy zwrot wartości funkcji - Result = False ;
( Tu uwaga : funkcja zwróci False również wtedy, gdy wystąpił jakiś błąd wykonania. Jeśli chcemy się upewnić, czy funkcja wystartowała prawidłowo, sprawdźmy wtedy przy pomocy GetLastError czy wartością błędu jest ERROR_IO_PENDING, która oznacza oczekiwanie na kompletację operacji wejścia / wyjścia. Natomiast jeśli funkcja zwraca True to znaczy, że żądana operacja została wykonana od razu - np. w wypadku gdy były zgromadzone nie odczytane znaki w kolejce odbiornika ) .
Teraz czekamy na odebranie przez port 16 znaków. Gdy te znaki nadejdą, skojarzone z funkcją ( poprzez strukturę ReadOverlapped ) zdarzenie ReadEvent przejdzie w stan signaled . No i niestety nic się samoczynnie nie zdarzy - nie jest w tym momencie wysyłany żaden komunikat, który wystarczy przechwycić i obsłużyć. Musimy sami zadbać o wykrycie uaktywnienia obiektu zdarzenia. Służą do tego funkcje oczekujące ; my użyjemy WaitForMultipleObjects gdyż pozwala ona na obsłużenie większej liczby obiektów. Można więc przy jej pomocy prowadzić np. odbiór, nadawanie i kontrolę wydarzeń portu jednocześnie. Pełna forma funkcji :
WaitForMultipleObjects ( cObjects : Integer ; lpHandles : PWOHandleArray ; WaitAll : Boolean ; Timeout : Integer ) : Integer ;
cObjects - liczba zdarzeń, na które ma reagować funkcja ;
lpHandles - wskaźnik na tablicę uchwytów zdarzeń ( w ogólności nie muszą to być zdarzenia, ale w tym przypadku tylko z nich korzystamy ) - tablica numerowana od indeksu 0 ;
Np. dla naszego powyższego przykładu tablicę deklarujemy :
var
EventsArray : array [0..2] of THandle;
...
EventsArray[0]:=ReadEvent.Handle;
EventsArray[1]:=WriteEvent.Handle;
EventsArray[2]:=EvtEvent.Handle;
...
WaitAll - gdy ustawione ( True ), funkcja odpowiada tylko na jednoczesne uaktywnienie wszystkich obsługiwanych zdarzeń, u nas ( False ) będzie odpowiadać na każde oddzielnie ;
Timeout - określa w [ ms ] czas oczekiwania funkcji, my używamy wartości INFINITE - funkcja ma czekać do skutku ( wyprzedzająco dodam, że jednym ze zdarzeń wyzwalających będzie żądanie zamknięcia wątku obsługującego port - zawsze więc jakieś zdarzenie będzie miało miejsce ).
W razie błędu funkcja zwraca WAIT_FAILED. Jeśli zajdzie jakieś z oczekiwanych zdarzeń wartość funkcji pozwoli określić, które :
WAIT_OBJECT_0 ... WAIT_OBJECT_0 + cObjects - 1 .
W naszym przykładzie : WAIT_OBJECT_0 - dotyczy odbioru, WAIT_OBJECT_0+1 - nadawania, natomiast WAIT_OBJECT_0+2 - zdarzenia portu.
( Result - WAIT_OBJECT_0 = indeks uchwytu w tablicy zdarzeń ) .
Wtedy odpowiednio obsługujemy sygnalizowane zdarzenie i ponownie uruchamiamy mechanizm oczekiwania .

I tu powracamy do problemu wywołanego przy wyborze trybu synchronicznego lub asynchronicznego : na czas oczekiwania program zostaje zawieszony i niczego innego nie może realizować. Poprzednia argumentacja za trybem asynchronicznym była z założenia powierzchowna. Prawdziwą zaletą trybu asynchronicznego jest bowiem właśnie możliwość jednoczesnej obsługi wielu zdarzeń ( dodajmy dla ścisłości - w tym samym wątku ). Natomiast rozwiązaniem sprawy jałowego oczekiwania na zakończenie operacji jest wprowadzenie wielowątkowej struktury naszego programu. O tym w następnym odcinku.


wielowątkowa obsługa skonfigurowanego portu

(5)Etap poprzedni :
uruchomiliśmy asynchroniczny mechanizm obsługi portu
składający się z funkcji komunikacyjnych, zdarzeń
sygnalizacyjnych oraz funkcji oczekującej - teraz chcemy
uniknąć w naszej aplikacji "przestojów" spowodowanych
działaniem tej funkcji.
Dodatkowe wątki w obsłudze portu szeregowego.
Systemy operacyjne Win 32 są systemami wielowątkowymi z wywłaszczaniem. W skrócie oznacza to, że czas procesora jest dzielony na odcinki przeznaczone dla realizacji kolejnych zadań : procesów i wątków. Procesy to np. uruchomione aplikacje, którym system operacyjny przydziela rozgraniczone obszary pamięci i które dzięki temu nie mogą się wzajemnie zakłócać. Natomiast wątek to dodatkowy podproces uruchamiany w ramach procesu - korzystający z tego samego obszaru pamięci oraz tych samych zmiennych globalnych. Jeśli chcemy w ramach programu - procesu wykonywać równolegle jakieś czasochłonne czynności - dla ich realizacji uruchamiamy dodatkowy wątek, który pracuje w tle w sposób praktycznie dla nas niezauważalny i powiadamia wątek główny w chwili zakończenia swojego zadania.
Tak właśnie możemy sobie poradzić z oczekiwaniem na skompletowanie operacji wejścia / wyjścia portu szeregowego bez zakłócenia pracy podstawowej, "widzialnej" części naszej aplikacji. Tworzenie nowych wątków jest rzecz jasna oparte na funkcjach API, natomiast i tutaj Delphi oferuje łatwe w użyciu opakowanie w postaci klasy TThread, która została użyta w dalszych opisach.
Na wstępie pojawia się pytanie, jak podzielić zadania - tzn. ile dodatkowych wątków przeznaczyć do obsługi portu ? Generalna zasada jest taka, żeby wątków nie mnożyć bez wyraźnej potrzeby - mechanizm przełączania jest wprawdzie bardzo wydajny ale nieco czasu jednak zabiera. Można więc np. nadawanie i odbiór umieścić w oddzielnych wątkach przy intensywnym wykorzystaniu portu - natomiast dla przewidywanych w naszym przypadku celów warsztatowych jest to całkowicie zbędne : podawane przykłady dotyczą użycia przez obsługę portu tylko jednego dodatkowego wątku.

Schemat organizacji obsługi komunikacji jest nastepujący :
1 - Inicjalizacja portu ( otwarcie, ustawienie parametrów ).
2 - Utworzenie, skonfigurowanie i uruchomienie wątku obsługi portu.
3 - Praca z użyciem portu szeregowego.
4 - Zatrzymanie i zwolnienie wątku obsługi portu.
5 - Zamknięcie portu.
Ad.1.
Port otwieramy i ustawiamy wg potrzeb tak jak to zostało przedstawione w dotychczasowych odcinkach.
Ad.2.
Do utworzenia wątku obsługi portu użyjemy klasy TCommThread wywodzącej się z abstrakcyjnej klasy TThread . W naszej klasie musimy przede wszystkim nadpisać abstrakcyjną metodę Execute, która decyduje o funkcjonalności wątku. Metoda ta ( czyli cały wątek ) wykonuje się od razu albo na życzenie - zależy to od wartości parametru Suspended konstruktora. W naszym przypadku dogodnie jest utworzyć wątek jako wstrzymany i przed uruchomieniem ustawić sobie jego własności :

CommThread := TCommThread.Create ( True );
CommThread. FreeOnTerminate := True;
{ powoduje samoczynne zwolnienie wątku z chwilą zakończenia - w przeciwnym razie musimy to zrobić sami w programie }
CommThread.Priority := tpHigher;
{ Priorytet wątku określa ile czasu procesora przydziela wątkowi system operacyjny - im wyższy priorytet tym szybciej wykona się wątek, ale oczywiście kosztem innych wątków działających w systemie. Należy tu zachować rozsądny kompromis pomiędzy priorytetem, szybkością procesora oraz przyjętą maksymalną szybkością transmisji szeregowej - dla naszych warsztatowych potrzeb często wystarczy domyślny priorytet tpNormal }
CommThread.Resume;
{ Uruchomienie wątku - start metody Execute }.

Na początku metody Execute tworzymy niezbędne zdarzenia, tablicę zdarzeń oraz struktury TOverlapped. W stosunku do poprzednich przykładów dopiszemy dodatkowe zdarzenia :

  • SendEvent, które posłuży do rozpoczęcia nadawania,
  • StopEvent, które będzie zatrzymywać wątek.
    Następnie wywołamy funkcje odbioru a potem wchodzimy w pętlę oczekiwania. Będzie to wyglądać np. tak :
//[...]
var
ReadEvent,WriteEvent,EvtEvent :TEvent;
SendEvent,StopEvent :TEvent;
EventsArray:array [0..4] of THandle;

ReadOverlapped:TOverlapped;
WriteOverlapped:TOverlapped;
EventOverlapped:TOverlapped;

TrxBuf,RcvBuf:array[0..2047] of Char;

WaitResult:Integer;
BytesToRead:Integer;
BytesToWrite:Integer;
EvtMask:Integer;
ReadBytesNumber:Integer;
WriteBytesNumber:Integer;
ErrorCode:Integer;
ReadDataLength:Integer;

begin
{ utworzenie zdarzeń : }
ReadEvent:=TEvent.Create(nil,True,False,'');
WriteEvent:=TEvent.Create(nil,True,False,'');
EvtEvent:=TEvent.Create(nil,True,False,'');
SendEvent:=TEvent.Create(nil,True,False,'');
StopEvent:=TEvent.Create(nil,True,False,'');

{załadowanie tablicy zdarzeń : }
EventsArray[0]:=ReadEvent.Handle;
EventsArray[1]:=WriteEvent.Handle;
EventsArray[2]:=EvtEvent.Handle;
EventsArray[3]:=SendEvent.Handle;
EventsArray[4]:=StopEvent.Handle;

{wpisanie uchwytów zdarzeń do rekordów TOverlapped : 
ReadOverlapped.hEvent := ReadEvent.Handle;
WriteOverlapped.hEvent := WriteEvent.Handle;
EventOverlapped.hEvent := EvtEvent.Handle;

{ Pierwszy start triggerów dla funkcji WaitForMultipleObjects : }
{zdarzenia portu}
WaitCommEvent(ComHandle,EvtMask,@EventOverlapped);
{operacja odczytu}
ReadFile(ComHandle,RcvBuf,BytesToRead,ReadBytesNumber, @ReadOverlapped);
//[...]

Te wywołania można uzupełnić sprawdzeniem, czy funkcje od razu przy uruchomieniu nie wykryły zdarzeń ( tj. czy zwróciły True ). W zastosowaniach warsztatowych pominąłem to, natomiast przed pierwszym wywołaniem na wszelki wypadek wyzerowałem port funkcją PurgeComm ( o której później ) - nie powoduje to w praktyce negatywnych konsekwencji.
Teraz metoda Execute wchodzi w pętlę oczekiwania, która realizuje ciągłą obsługę komunikacji szeregowej.
Ad. 3 .
Przykładowa struktura pętli obsługi jest następująca :

while True do
begin
WaitResult:=WaitForMultipleObjects(5,@EventsArray,False,INFINITE);
case WaitResult of
WAIT_OBJECT_0 : [...]

Zgłosiło się zdarzenie odbioru. Wpisujemy tu swoją procedurę obsługującą odebrany pakiet znaków. Należy przy tym zwrócić uwagę na następujące szczegóły :

  • Najczęściej przy odbiorze używamy timeoutu. Oznacza to ( jak było wspomniane wcześniej ), że odbiornik zgłasza się zarówno po otrzymaniu zadanej w wywołaniu ReadFile liczby znaków jak i po upłynięciu timeoutu. Jeśli jest to nam potrzebne - właśnie w tym miejscu możemy sprawdzić przyczynę. Posłuży do tego funkcja :
    GetOverlappedResult (AComHandle:THandle ; const lpOverlapped : TOverlapped ; var lpNumberOfBytesTransferred : Integer ; bWait : Boolean) : Boolean ;
    gdzie :
    AComHandle - uchwyt portu;
    lpOverlapped - wskaźnik na strukturę Overlapped powiązaną z badanym procesem;
    lpNumberOfBytesTransferred - wskaźnik na zmienną typu Integer, w której funkcja lokuje odczytany wynik transmisji;
    bWait - flaga określająca czy czekać na zakończenie operacji I/O ( True ), czy wykonać się od razu ( False ).
    Funkcja zwraca True w przypadku sukcesu lub False w przypadku błędu. Jeśli operacja I/O jest jeszcze nie zakończona otrzymujemy False z wartością błędu ( którą możemy odczytać funkcją GetLastError ) równą ERROR_IO_INCOMPLETE. U nas to nie zajdzie bo wywołanie :
    GetOverlappedResult(ComHandle,ReadOverlapped,ReadDataLength,False);
    następuje w odpowiedzi na zakończenie operacji ( natomiast można sprawdzić wynik aby wyeliminować inne błędy ). Jeśli teraz porównamy BytesToRead i ReadDataLength to zlokalizujemy timeout odbioru.

  • Należy ponownie wywołać funkcję ReadFile kontynuując w ten sposób odbiór. Takie wywołanie samoczynnie przestawia zdarzenie ReadEvent w stan non signaled i nie musimy tego robić jawnie.
    WAIT_OBJECT_0 + 1 :
    begin
    WriteEvent.ResetEvent;
    [...]
    Proces wysyłania znaków został zakończony. Tu możemy np. poinformować wątek główny o zwolnieniu nadajnika i mozliwości dalszego nadawania. Zwróćmy uwagę, że ponieważ nie wywołujemy od razu funkcji WriteFile musimy programowo ustawić zdarzenie w stan non signaled przy pomocy metody ResetEvent ( w przeciwnym przypadku będzie ono wciąż zgłaszane ).
    WAIT_OBJECT_0 + 2 : [...]
    W porcie wystąpiła jakaś sytuacja uwzględniona przez nas przy konfiguracji maski zdarzeń portu ( nie pomylmy zdarzenia portu ze zdarzeniem - obiektem Windows ) - np. zmiana stanu linii DSR. Tutaj identyfikujemy przyczynę aktywowania EvtEvent i podejmujemy odpowiednią akcję. Do odczytania słowa zdarzeń używamy funkcji :
    GetCommMask (AComHandle:THandle ; var lpEvtMask:Integer ) : Boolean;
    która lokuje je w zmiennej lpEvtMask, opisanej identycznie jak dla funkcji SetCommmask.
    Na koniec obsługi wywołujemy ponownie WaitCommEvent w celu wznowienia kontroli nad portem ( przy okazji EvtEvent zostaje samoczynnie przestawione w stan non signaled ).
    Powyższe przypadki dotyczyły zakończenia funkcji oczekującej wskutek działania portu szeregowego. Następne są przewidziane dla komunikacji programu ( wątku głównego ) z naszym wątkiem pomocniczym.

WAIT_OBJECT_0 + 3 : [...]
W wątku głównym przygotowujemy dane ( string ) do wysłania w buforze nadajnika ( może być on zmienną globalną i będzie wtedy widziany przez podległy wątek ). Polecenie rozpoczęcia nadawania przez wątek obsługi portu przekazujemy poprzez programowe ustawienie zdarzenia SendEvent w stan signaled : SendEvent.SetEvent. Skutkiem tego jest zakończenie funkcji oczekującej. W obsłudze rozpoczynamy wysyłanie wywołując WriteFile . Musimy też zresetować zdarzenie SendEvent ( SendEvent.ResetEvent ).
Ad.4.
Zakończenie pracy wątku następuje wskutek zakończenia wykonania metody Execute. Zwykłym sposobem przewidzianym w klasie TThread jest użycie metody Terminate. Powoduje ona ustawienie własności Terminated wątku na True. W metodzie Execute powinniśmy cyklicznie sprawdzać wartość tej własności i w razie wykrycia True od razu zakończyć wykonywanie. W naszym przypadku ten sposób nie zadziała, gdyż po wywołaniu funkcji oczekującej wątek znajduje się w stanie zawieszenia i nie ma jak wykryć ewentualnego ustawienia Terminated . W zamian zastosujemy więc jeszcze jedno zdarzenie, którego jedynym celem bedzie przekazanie do wątku pomocniczego polecenia zakończenia pracy.

WAIT_OBJECT_0 + 4 :
begin
StopEvent.ResetEvent;
Break;
end;
Break powoduje wyjście z pętli while. 
end; {case}
end; {while}

Po wyjściu z pętli oczekiwania kończymy metodę Execute zwolnieniem powołanych przez nas zdarzeń :

ReadEvent.Free;
WriteEvent.Free;
EvtEvent.Free;
SendEvent.Free;
StopEvent.Free;

Natomiast wszelkie zasoby używane przez wątek są zwalniane samoczynnie gdyż ustawiliśmy własność FreeOnTerminate.
Ad.5.
Zatrzymanie i zwolnienie wątku obsługi na ogół wiąże się z zamknięciem portu szeregowego. Wcześniej dobrze jest zatrzymać rozpoczęte operacje portu wspomnianą już funkcją PurgeComm. Port zamykamy opisaną wcześniej funkcją CloseHandle.

Na koniec uwagi ogólne :

  1. Jeśli w obsłudze portu szeregowego ( czyli z poziomu wątku pomocniczego ) chcemy odwołać się do komponentów Delphi ( które są tworzone i obsługiwane przez wątek główny ) używajmy metody Synchronize dla uniknięcia niespodzianek ( metoda ta powoduje wykonywanie procedur wątku podrzędnego w kontekście wątku głównego, co pozwala na uniknięcie potencjalnych konfliktów - problem ten jest dokładniej opisany w Pomocy oraz w obfitej literaturze dotyczącej Delphi ).
  2. Nasuwa się pytanie : skoro i tak powołujemy dodatkowe wątki to po co nam te wszystkie skomplikowane mechanizmy - czy wątek nie może sprawdzać w pętli stanu portu ( polling ) ? Jasne, że może - tylko wtedy musi pracować ciągle, angażując czas procesora. Natomiast przy wywołaniach asynchronicznych wątek zostaje zawieszony na czas wykonywania operacji co jest wykrywane przez mechanizm przełączania wątków ( zawieszonemu wątkowi procesor nie jest przydzielany ) dając w efekcie znaczne oszczędności czasowe.
  3. Powyższe propozycje to tylko przykład - można sprawę rozwiązać na wiele innych sposobów. Niemniej w praktyce warsztatowej działają prawidłowo - napisany wg opisanego schematu komponent znakomicie ułatwia życie.

Dodatkowe funkcje obsługi portu

(6)Etapy poprzednie :
Mamy uruchomiony kompletny asynchroniczny
i wielowątkowy mechanizm obsługi portu szeregowego,
który potrzebuje jeszcze tylko kilku dodatkowych funkcji
kontrolnych.
Dodatkowe funkcje obsługi portu szeregowego.
Funkcje te dają nam rozszerzone możliwości kontrolowania pracy portu. Czasem też ( jak wspominana PurgeComm ) usprawniają działanie podstawowych mechanizmów zapisu oraz odczytu danych. Należą do nich ( alfabetycznie ) :
1.
ClearCommBreak (AComHandle : THandle) : Boolean ;
gdzie AComHandle to uchwyt portu ( u nas ComHandle ).Zwracana wartość to True przy wykonaniu poprawnym i False przy niepowodzeniu. Funkcja wyłącza tryb zawieszenia portu.
2.
ClearCommError (AComHandle : THandle ; var lpErrors : Integer ; lpComStat : PComStat ) : Boolean ;
AComHandle - to nasz uchwyt portu ComHandle,
lpErrors - zwracany przez funkcję łączny kod błędu,
PComStat - wskaźnik na dodatkową strukturę informacyjną - może być nil jeśli tych informacji nie potrzebujemy.
Funkcja zwraca True przy poprawnym wykonaniu.
Jest to jedna z najważniejszych funkcji - pozwala monitorować i reagować na wszelkie błędy transmisji. Należy też koniecznie pamiętać, że jeśli w konfiguracji portu ustawiliśmy flagę fAbortOnError to po każdym błędzie komunikacji port przerywa operacje odczytu i zapisu, żadna zaś nowa operacja nie jest akceptowana zanim nie użyjemy ClearCommError. Zawsze należy więc to zrobić w wypadku stwierdzenia w masce zdarzeń portu ustawionych flag EV_ERR lub EV_BREAK. Zwracany kod błędu jest 32 - bitową maską, w której poszczególne flagi (bity) oznaczają różne przyczyny wystąpienia błędu :
CE_BREAK - została wykryta przerwa w transmisji,
CE_FRAME - wystapił błąd ramki,
CE_IOE - podczas komunikacji z urządzeniem stwierdzono błąd we / wy,
CE_MODE - nieprawidłowy parametr AComHandle lub wybrana konfiguracja jest niemożliwa dla danego urządzenia (dostępne zakresy pracy można uzyskać przy pomocy wspominanej wcześniej funkcji GetCommProperties - jednak w praktyce amatorskiej jest to zazwyczaj zbędne),
CE_OVERRUN - przepełnienie bufora odbiornika (UART nie zdążył przepisać odebranego znaku przed nadejściem następnego - następny odbierany znak jest tracony),
CE_RXOVER - przepełniona kolejka odbiornika - brak miejsca na lokację następnych odbieranych znaków,
CE_RXPARITY - został wykryty błąd parzystości,
CR_TXFULL - program spróbował ulokować następny znak do wysłania w pełnej kolejce nadajnika.
Użycie lpErrors zależy oczywiście od naszych potrzeb - często wystarcza samo "potwierdzenie" błędu ( czyli wywołanie funkcji ) zaś analizę jego przyczyn pomijamy.
Z kolei jeśli potrzebujemy informacji bardziej szczegółowych - sięgamy do struktury PComStat^, wypełnianej w trakcie działania funkcji (można też w ten sposób monitorować stan portu niezależnie od występowania błędu ). Dla potrzeb Delphi struktura jest zdefiniowana jako :
TComStateFlag = (fCtsHold, fDsrHold, fRlsHold, fXoffHold, fXOffSent, fEof,fTxim);
TComStateFlags = set of TComStateFlag;
TComStat = record
Flags: TComStateFlags;
Reserved: array[0..2] of Byte;
cbInQue: DWORD;
cbOutQue: DWORD;
end;
PComStat = ^TComStat;
Poszczególne flagi oraz pola oznaczają :
fCtsHold - wysyłanie jest wstrzymane w oczekiwaniu na ustawienie sygnału CTS,
fDsrHold - jak wyżej dla sygnału DSR,
fRlsdHold - jak wyżej dla sygnału RLSD,

  • powyższe dotyczą pracy z aktywnym handshakingiem sprzętowym.
    fXoffHold - wysyłanie jest wstrzymane po odebraniu znaku XOFF,
    fXoffSent - wysyłanie jest wstrzymane po wysłaniu znaku XOFF,
  • powyższe dotyczą handshakingu programowego Xon/Xoff.
    fEof - wskazuje na odebranie znaku końca danych ( EOF ),
    fTxim - wskazuje na oczekujący na wysłanie znak ulokowany w kolejce nadajnika funkcją TransmitCommChar.
    Reserved - nie używamy - rezerwa,
    cbInQue - określa liczbę nie odczytanych znaków w kolejce odbiornika,
    cbOutQue - określa liczbę nie wysłanych znaków w kolejce nadajnika.

EscapeCommFunction (AComHandle:THandle ; dwFunc:Integer) : Boolean;
AComHandle - jak poprzednio,
dwFunc - kod funkcji do wykonania - może przyjąć jedną z następujących wartości :
CLRDTR - wyłączenie linii DTR,
CLRRTS - wyłączenie linii RTS,
SETDTR - ustawienie linii DTR,
SETRTS - ustawienie linii RTS,

  • warto przypomnieć, że takie działania są niemożliwe jeśli w konfiguracji ustawiliśmy tryb handshakingu sprzętowego.
    SETXOFF - symuluje odebranie znaku XOFF,
    SETXON - symuluje odebranie znaku XON,
    SETBREAK - równoznaczne z funkcją SetCommBreak,
    CLRBREAK - równoznaczne z funkcją ClearCommBreak.
    Funkcja zwraca True przy poprawnym wykonaniu.

GetCommModemStatus ( AComHandle:THandle ; var lpModemStat ) : Boolean;
lpModemStat może być kombinacją następujących flag odpowiadających stanowi linii wejściowych : MS_CTS_ON, MS_DSR_ON, MS_RING_ON, MS_RLSD_ON.
Ta funkcja pozwala zarówno sprawdzić stan linii po jego zmianie sygnalizowanej zdarzeniem portu jak i całkiem niezależnie ( np. po włączeniu portu ).
Funkcja zwraca True przy poprawnym wykonaniu.
5.
PurgeComm ( AComHandle:THandle ; fdwAction : Integer ) : Boolean;
fdwAction określa kod operacji do wykonania i może być kombinacją następujących wartości:
PURGE_TXABORT - natychmiast przerywa nie dokończone operacje wysyłania,
PURGE_RXABORT - natychmiast przerywa nie dokończone operacje odbioru,
PURGE_TXCLEAR - czyści kolejkę nadajnika bez wysłania pozostałych znaków ( jesli chcemy opróżnić kolejkę z wysłaniem jej zawartości możemy użyć synchronicznej funkcji FlushFileBuffers ),
PURGE_RXCLEAR - czyści kolejkę odbiornika.
6.
SetCommBreak (AComHandle : THandle) : Boolean;
Zawiesza transmisję i ustawia linię w stanie wstrzymania aż do chwili wywołania ClearCommBreak.
7.
TransmitCommChar (AComHandle : THandle; chTransmit :Char ) :Boolean;
Lokuje znak chTransmit na początku kolejki nadajnika - może być użyta np. do przesłania znaku kontrolnego. Podlega wszelkim mechanizmom kontrolującym nadawanie jak np. handshaking. Może być wywołana jedynie synchronicznie i służy tylko do nadania pojedynczych znaków ( to oznacza, że jeśli poprzedni wstawiony w ten sposób znak nie został jeszcze wysłany - funkcja zwróci błąd : False ).


Na tym kończę przedstawienie wybranych elementów programowania transmisji szeregowej w środowisku Win32. Wiele rzeczy można wykonać inaczej, wielu szczegółów nie wyjaśniam a wielu sam nie sprawdzałem i nie jestem ich pewny . Jeśli ktoś znajdzie jakieś nieścisłości lub błędne informacje - niech da znać ! W każdym razie opisane sposoby sprawują się bez zastrzeżeń w zakładanych na wstępie uproszczonych i ograniczonych zastosowaniach.
Przy opracowaniu powyższego materiału korzystałem z :

  1. kodów źródłowych (freeware) komponentów Delphi;
  2. plików pomocy i źródeł pakietu Delphi 3 Professional;
  3. literatury ( w zakresie API i mechanizmów systemowych - bo o transmisji to tam nie ma prawie nic ! ) głównie związanej z Delphi.

Autorem artykułu jest Jurek Szczesiul

8 komentarzy

Wlasnie psyda sie gotowiec :P

Przestrzeń adresowa procesorów 80x86 dzieli się na obszar pamięci oraz obszar wejścia/wyjścia. Rejestry kontrolera UART (zawiera m.in. port rs232) są dostępne jako część przęstrzeni adresowej wej/wyj (w menadżerze urządzeń możesz sprawdzic zakres wejścia/wyjścia, oznacza to pod jakim adresem jest widziany od strony procesora port szeregowy).
Ponieważ WIN NT/XP miał być pomyślany jako system bezpieczny (jakkolwiek to rozumieć) przestrzeń wejścia/wyjścia nie jest dostępna dla procesów nie będących częścią jądra tzn. standartowych aplikacji. Oznacza to jako użytkownicy nie możemy korzystać z rozkazów assemblera IN oraz OUT. Z tego powodu programy dosowe korzystające z RS232 nie będą działać pod WIN NT. (jaki jest dokładny mechanizm działanie niestety nie wiem, jeśli ktoś ma jakiś sensowny opis proszę podesłać).

Co do używania komponentów: każdy komponent (dokładniej cała biblioteka VCL) opiera się na funkcjach WINAPI. Korzystając z komponentu chcąc nie chcąc wywołujemy funkcje WINDOWSA. Czyli nie musimy wykorzystywać komponentu aby korzystać z portu, wystarczy bezpośrednio wywoływać funkcje WINDOWS.

Przestrzeń adresowa procesorów 80x86 dzieli się na obszar pamięci oraz obszar wejścia/wyjścia. Rejestry kontrolera UART (zawiera m.in. port rs232) są dostępne jako część przęstrzeni adresowej wej/wyj (w menadżerze urządzeń możesz sprawdzic zakres wejścia/wyjścia, oznacza to pod jakim adresem jest widziany od strony procesora port szeregowy).
Ponieważ WIN NT/XP miał być pomyślany jako system bezpieczny (jakkolwiek to rozumieć) przestrzeń wejścia/wyjścia nie jest dostępna dla procesów nie będących częścią jądra tzn. standartowych aplikacji. Oznacza to jako użytkownicy nie możemy korzystać z rozkazów assemblera IN oraz OUT. Z tego powodu programy dosowe korzystające z RS232 nie będą działać pod WIN NT. (jaki jest dokładny mechanizm działanie niestety nie wiem, jeśli ktoś ma jakiś sensowny opis proszę podesłać).

Co do używania komponentów: każdy komponent (dokładniej cała biblioteka VCL) opiera się na funkcjach WINAPI. Korzystając z komponentu chcąc nie chcąc wywołujemy funkcje WINDOWSA. Czyli nie musimy wykorzystywać komponentu aby korzystać z portu, wystarczy bezpośrednio wywoływać funkcje WINDOWS.

Działa na XP. Owszem, trzeba trochę pokombinować, ale nie mówicie że się nie da!

To nie zadziala na windowsach serii 2k (NT,2k,XP - brak dostepu)
W ww. Windowsach trzeba uzyc komponentow..

Poza tym azgon..wiesz co to sa znaczniki < delphi > ??? :)

ja nic z tego nie zrozumialem :-) heheh, jakos moze mam downa :-) jutro jak nie bede tak zmeczony to przeczytam sobie spokojnie :P nara

no fajnie by było ale mi jakoś niekce działć na XP :P

spoko tego mi bylo trzeba... mozesz podac dokladne zrodlo lub kontakt z autorem??