R 5a-04, RS232.Praktyczne programowanie


Rozdział 5 Programowa obsługa interfejsu RS 232C w Windows

Czytając ten rozdział, zapoznasz się ze sposobami konstrukcji algorytmów, realizujących transmisję szeregową w środowisku Windows, które charakteryzuje się pewnymi cechami nie mającymi odpowiedników w MS DOS. Poznanie i umiejętne wykorzystanie tych cech sprawi, iż problem obsługi interfejsów szeregowych z poziomu Windows — uważany powszechnie za trudny — przestanie być dla nas tajemnicą. Pokażemy, w jaki sposób należy tworzyć aplikacje, służące do programowej obsługi łącza szeregowego RS 232C zarówno w C++Builderze jak i w Delphi. W moim odczuciu wśród programistów istnieje zauważalny podział na osoby programujące głównie w Delphi oraz na preferujące Buildera lub ogólnie C++ do Windows. Jednak zdaniem wielu osób uniwersalność jest jedną z tych zalet, jakie powinny charakteryzować programistę. W rozdziale tym przybliżymy Czytelnikowi podobieństwa i różnice w sposobie konstrukcji algorytmów realizujących transmisję szeregową, pisanych w Delphi oraz Builderze.

W dalszej części książki spotkamy się z typami danych, których poznanie i zrozumienie ma kluczowe znaczenie w projektowaniu aplikacji, obsługujących urządzenia zewnętrzne. Zacznijmy od ich przypomnienia. W tabeli poniżej przedstawiono porównanie podstawowych typów zmiennych wykorzystywanych w kompilatorach, które będą dla nas istotne. Większości z nich można używać zamiennie, pisząc zarówno w Delphi jak i w C++Builderze.

Tabela 5.1.

Typy zmiennych stosowanych w Delphi oraz w C++ Builderze

Delphi Rozmiar / Znak / Typ C++ Builder

w bajtach + / -

ShortInt 1 integer signed char

SmallInt 2 integer short

LongInt 4 integer

Byte 1 bez znaku integer unsigned char

Word 2 bez znaku integer unsigned short

Integer 4 integer int

Cardinal 4 bez znaku integer unsigned int

Boolean 1 true/false bool

ByteBool true/false unsigned char

1 bez znaku integer

WordBool true/false unsigned short

2 bez znaku integer

LongBool true/false

4 bez znaku integer

AnsiChar 1 1 znak ANSI character char

WideChar 2 1 znak Unicode character wchar_t

Char 1 bez znaku character char

AnsiString ≈3GB ANSIChar AnsiString AnsiString

String[n] n = 1..255 ANSIChar string SmallString<n>

bajtów

ShortString 255 bajtów ANSIChar string SmallString<255>

String 255 lub ≈3GB ANSIChar AnsiString AnsiString

Single 4 floating point float

number

(liczba zmiennoprzecinkowa)

Double 8 floating point double

number

Extended 10 floating point long double

number

Real 4 floating point double

number

Pointer 4 generic pointer void *

(wskaźnik ogólny, adresowy)

PChar 4 bez znaku pointer to characters unsigned char *

(daleki wskaźnik na C-łańcuch)

PAnsiChar 4 bez znaku pointer to ANSIChar unsigned char *

Comp 8 floating point number Comp

Konstruując nasze programy, będziemy starali się jak najszerzej wykorzystywać standardowe zasoby Windows, w szczególności tzw. interfejs programisty Win 32 API (ang. Application Programming Interface). Jego umiejętne wykorzystanie umożliwi naszym aplikacjom błyskawiczne skonfigurowanie i uzyskanie dostępu do portu komunikacyjnego. Błędnym jest twierdzenie, że sama, nawet bardzo dobra znajomość języka programowania wystarczy, żeby stworzyć poprawnie działający w Windows program. Otóż musimy zdawać sobie sprawę z faktu, o którym często się zapomina — niemożliwe jest napisanie udanej aplikacji, mającej pracować w pewnym środowisku (czytaj — systemie operacyjnym), bez znajomości danego środowiska. Wiele już zostało powiedziane na temat dobrych i złych stron Windows, należy jednak pamiętać, że daje on nam swoją wizytówkę, ofertę współpracy, czyli API. Już nie wystarczy umiejętność wykorzystywania ulubionego kompilatora. Zasoby Delphi czy Buildera połączymy z zasobami systemu operacyjnego, a spoiwem tym będzie właśnie uniwersalne Win32 API. Istnieje wiele warstw API, używanych w zależności od potrzeb. W tym i dalszych rozdziałach zajmiemy się szeroko rozumianą warstwą komunikacyjną.

Win32 API korzysta ze specjalnego systemu nazewnictwa zmiennych, z tzw. notacji węgierskiej wprowadzonej przez Węgra Karoja Szimoni'ego. Zgodnie z nią do rdzenia nazwy zadeklarowanej zmiennej dodaje się przedrostek (ang. prefix). Chociaż istnieją pod tym względem pewne rozbieżności pomiędzy nazewnictwem Microsoftu i Borlanda, to jednak zapis taki bardzo ułatwia szybkie ustalenie roli zmiennej w programie oraz jej typ. W następnych rozdziałach będziemy starali się — wszędzie gdzie jest to możliwe — zachowywać oryginalne, samokomentujące się nazewnictwo Win32 API (większość nazw API będziemy traktować jako nazwy własne). Z doświadczenia wiem, że stosowanie takiej konwencji bardzo pomaga w studiowaniu plików pomocy. Oczywiście, moglibyśmy silić się na oryginalność, wprowadzając własne zmienne, zrozumiałe tylko dla piszącego dany program — wówczas przykłady musiałyby być zapisane jako wręcz humorystyczna mieszanina języków polskiego i angielskiego. Trzeba też przyznać, że byłby to bardzo skuteczny sposób zaciemnienia obrazu API. Zrozumienie znaczenia nazw tam stosowanych okaże się w przyszłości niezwykle cenne, gdyż API można czytać jak książkę. Aby pomóc Czytelnikom, którzy nie zetknęli się dotąd z tymi pojęciami, poniżej przedstawiono ogólne zasady tworzenia niektórych przedrostków.

Tabela 5.2.

Ogólne zasady tworzenia przedrostków wg notacji węgierskiej.

Przedrostek Skrót angielski Znaczenie

a array tablica

b bool zmienna logiczna TRUE lub FALSE (1 lub 0)

by byte unsigned char znak (bajt)

ch char znak

cb count of bytes liczba bajtów

dw double word podwójne słowo

evt event zdarzenie

f flag znacznik

fdw flag of double word zacznik typu dw

fn function funkcja

h handle identyfikator (uchwyt)

i integer typ całkowity 4-bajtowy

id (ID) identification identyfikacja

in input wejście, dane wejściowe

l long int typ całkowity długi 4-bajtowy

lp long pointer wskaźnik typu long int

lpc long pointer to C- string wskaźnik typu long int do C-łańcucha

lpfdw long pointer to flag of dw wskaźnik typu lp do znacznika typu double
word

lpfn long pointer to function wskaźnik typu lp do funkcji

n short or int typ krótki lub całkowity

np near pointer bliski wskaźnik

(w środowisku 32-bitowym to samo co lp)

out output wyjście, dane wyjściowe (przetworzone)

p pointer wskaźnik

(w środowisku 32-bitowym to samo co lp)

pfn pointer to function wskaźnik do funkcji

que queue kolejka, bufor danych

s (sz) string łańcuch znaków

st struct struktura

t type typ

u unsigned bez znaku

w (word) unsigned int słowo

wc WCHAR znak zgodny z Unicode

PRZYPOMNIJMY

Fizyczny adres pamięci w PC zaopatrzonym w procesor Pentium może składać się z segmentu i offsetu (tzw. przemieszczenia), określając tym samym możliwość istnienia wskaźników bliskich (ang. near pointer) i dalekich (ang. far pointer). Bliski wskaźnik, stanowiąc 32-bitowy adres efektywny, jest offsetem wewnątrz segmentu. Daleki wskaźnik stanowi 48-bitowy adres zawierając 16-bitowy selektor segmentu i 32-bitowy offset. Dalekie wskaźniki używane są w modelu segmentowym pamięci. Stosując metody zarządzania pamięcią, programy nie adresują w sposób bezpośredni pamięci fizycznej, adresują natomiast jej model. Win32 korzysta z płaskiego modelu pamięci (ang. flat memory model). W modelu tym segmenty mogą całkowicie pokrywać się z zakresami pamięci fizycznej lub z tymi jej adresami, które można mapować (odwzorowywać) do pamięci fizycznej. Wszystkie rejestry segmentowe podczas działania programu zawierają tą samą wartość - selektor odpowiedniego segmentu. W związku z tym wskaźniki stanowią 32-bitowe offsety w 4 GB segmencie (232 ≅ 4GB).

Windows oferuje nam ponadto kilka typów danych, z których część tylko nieznacznie różni się sposobem zapisu w implementacjach Delphi i Buildera. Typy te mają najczęściej postać struktury lub klasy i są bardzo często wykorzystywane w warstwie komunikacyjnej programów. Tabela 5.3 przedstawia przykładowe, ogólnie stosowane sposoby ich deklaracji.

Tabela 5.3. Typowe sposoby deklaracji stosowane w programach komunikacyjnych

Delphi Znaczenie C++ Builder

dcb : TDCB; struktura kontroli portu DCB dcb;

hCommDev : THANDLE; 32-bitowy identyfikator HANDLE hCommDev;

portu — jednorazowo

nadana wartość

identyfikująca port

Stat : TCOMSTAT; struktura zawierająca COMSTAT Stat;

dodatkowe informacje

o porcie szeregowym

Overlapped : TOVERLAPPED; struktura zawierająca OVERLAPPED Overlapped;

informacje używane przy

transmisji asynchronicznej

byX : BYTE; bajt BYTE byX;

wKey : WORD; słowo 2-bajtowe WORD wKey;

dwErrors : DWORD; podwójne słowo — 4 bajty DWORD dwErrors;

(double word)

bResult : BOOL; znacznik TRUE-FALSE BOOL bResult;

1 0

uFun : UINT; unsigned int UINT uFun;

Zaopatrzeni w powyższą terminologię pójdźmy dalej i zobaczmy, do czego mogą nam być przydatne poszczególne struktury oraz funkcje interfejsu programisty — Win32 API.

Wykorzystanie elementów Win32 API w C++ Builder. Część I

Poznawanie tajników obsługi portu szeregowego w Windows rozpoczniemy, z czysto praktycznych względów, od pisania programów w C++ Builderze . C++ posiada składnię taką jak API, dlatego prościej nam będzie zapoznać się z budową funkcji oraz struktur oferowanych przez interfejs programisty. Ułatwi to też zrozumienie, w jaki sposób i w jakiej kolejności należy umieszczać je w programie.

Fundamentalne znaczenie ma struktura kontroli urządzeń zewnętrznych DCB (ang. Device Control Block). W Windows struktura DCB w pewnym sensie odpowiada funkcji 00h przerwania 14h BIOS-u. Udostępnia nam jednak nieporównywalnie większe możliwości programowej obsługi łącza szeregowego umożliwia bezpośrednie programowanie rejestrów układu UART. W poniższych tabelach przedstawiono specyfikację bloku kontroli urządzeń zewnętrznych DCB:

Tabela 5.4.

Zmienne struktury DCB reprezentujące dopuszczalne parametry ustawień portu szeregowego

Typ Zmienna Znaczenie Wartość,

Stała symboliczna

DWORD DCBlength rozmiar struktury należy wpisać

DWORD BaudRate określenie prędkości CBR_110 CBR_19200

transmisji (bity/sek) CBR_300 CBR_ 38400

CBR_600 CBR_56000

CBR_1200 CBR_57600

CBR_2400 CBR_115200

CBR_4800 CBR_128000

CBR_9600 CBR_256000

CBR_14400

WORD wReserved nie używane 0

WORD XonLim określenie minimalnej liczby domyślnie: 65535,

bajtów w buforze wejściowym w praktyce XonLim

przed wysłaniem specjalnego ustala się jako ½ rozmiaru

znaku sterującego XON deklarowanego wejściowego

bufora danych

WORD XoffLim określenie maksymalnej liczby domyślnie: 65535, bajtów w buforze wejściowym w praktyce XoffLim

przed wysłaniem specjalnego ustala się jako ¾ rozmiaru

znaku sterującego XOFF deklarowanego bufora

wejściowego

BYTE ByteSize wybór liczby bitów danych 5, 6, 7, 8

BYTE Parity określenie kontroli parzystości EVENPARITY

parzysta

MARKPARITY

bit parzystości stale równy 1

NOPARITY

brak kontroli

ODDPARITY

nieparzysta

BYTE StopBits wybór bitów stopu ONESTOPBIT

1 bit stopu

ONE5STOPBITS

w przypadku słowa 5-

bitowego bit stopu

wydłużony o ½

TWOSTOPBITS

2 bity stopu

char XonChar określenie wartości znaku XON standardowo

dla nadawania i odbioru (char) DC1,

(wysłanie znaku przywraca transmisję) dziesiętnie: 17

char XoffChar określenie wartości znaku XOFF standardowo

dla nadawania i odbioru (wysłanie (char) DC3,

XOFF wstrzymuje transmisję dziesiętnie: 19

do czasu odebrania znaku XON)

char ErrorChar określenie wartości znaku zastępującego opcjonalnie: 0

bajty otrzymane z błędem parzystości lub SUB

char EofChar określenie wartości znaku końca opcjonalnie: 0

otrzymanych danych

char EvtChar określenie wartości znaku służącego do opcjonalnie: 0

sygnalizowania wystąpienia danego

zdarzenia

WORD wReserved1 obecnie nie używane

Tabela 5.5.

Pola bitowe reprezentujące dopuszczalne wartości znaczników sterujących struktury DCB

Typ Pole Właściwości Wartość

bitowe pola Znaczenie

Stała symboliczna

DWORD fBinary tryb binarny (Win 32 API TRUE / 1

podtrzymuje jedynie ten

tryb transmisji danych)

DWORD fParity umożliwia ustawienie sprawdzania TRUE / 1

parzystości — sposobu reakcji na kontrola parzystości włączona

bit parzystości

FALSE / 0

bit parzystości nie jest sprawdzany

DWORD fOutxCtsFlow umożliwia ustawienie sprawdzania TRUE / 1

sygnału na linii CTS w celu kontroli jeżeli sygnał CTS jest

danych wyjściowych nieaktywny, transmisja jest

wstrzymywana do czasu

ponownej aktywacji linii CTS

FALSE / 0

włączenie sygnału na linii

CTS nie jest wymagane do

rozpoczęcia transmisji

DWORD fOutxDsrFlow umożliwia ustawienie sprawdzania TRUE / 1

sygnału na linii DSR w celu kontroli jeżeli sygnał DSR jest

danych wyjściowych nieaktywny, transmisja jest

wstrzymywana do czasu

ponownej aktywacji linii DSR

FALSE / 0

włączenie sygnału na linii

DSR nie jest wymagane do

rozpoczęcia transmisji

DWORD fDtrControl specyfikacja typu kontroli DTR_CONTROL_DISABLE / 0

sygnału DTR sygnał na linii DTR jest nieaktywny

DTR_CONTROL_ENABLE / 1 sygnał na linii DTR jest aktywny

DTR_CONTROL_HANDSHAKE / 2

włączenie potwierdzania przyjęcia

sygnału DTR — potwierdzenie musi

być odebrane na linii DSR.

Używane w trybie półdupleksowym

ewentualne błędy transmisji w tym

trybie są usuwane przez funkcję

EscapeCommFunction().

DWORD fDsrSensitivity specyfikacja wykorzystania TRUE / 1

poziomu sygnału na linii DSR otrzymane bajty są ignorowane,

o ile linia DSR nie jest w stanie

wysokim

FALSE / 0

stan linii DSR jest ignorowany

DWORD fTXContinueOnXoff kontrola przerwania transmisji TRUE / 1

w przypadku przepełnienia wymuszanie kontynuowania

bufora wejściowego i, ewentualnie, transmisji nawet po

wystąpienia znaków XoffChar wystąpieniu znaku XOFF i

oraz XonChar wypełnieniu wejściowego

bufora danych powyżej

XoffLim bajtów

FALSE / 0

transmisja nie jest kontynuowana,

dopóki bufor wejściowy nie

zostanie opróżniony do pułapu

XonLim bajtów i nie

nadejdzie znak XON potwierdzenia

dalszego odbioru

DWORD fOutX programowe ustawienie protokołu TRUE / 1

XON-XOFF w czasie wysyłania transmisja zostaje przerwana

danych po odebraniu znaku XoffChar

i wznowiona po otrzymaniu

znaku XonChar

FALSE / 0

XON-XOFF w czasie wysyłania

nie jest ustawiony

DWORD fInX programowe ustawienie protokołu TRUE / 1

XON-XOFF w czasie odbioru znak XoffChar jest wysyłany,

danych kiedy bufor wejściowy jest

pełny lub znajduje się w nim

XoffLim bajtów;

znak XonChar jest wysyłany,

kiedy bufor wejściowy

pozostaje pusty lub znajduje

się w nim XonLim bajtów

FALSE / 0

XON-XOFF w czasie odbioru

nie jest ustawiony

DWORD fErrorChar umożliwia zastąpienie bajtów TRUE / 1

otrzymanych z błędem parzystości zastąpienie jest wykonywane

znakiem ErrorChar ponadto fParity musi być

ustawione jako TRUE

FALSE / 0

zastąpienie nie jest wykonane

DWORD fNull odrzucenie odebranych nieważnych TRUE / 1

lub uszkodzonych bajtów nieważne bajty zostaną

odrzucone przy odbiorze

FALSE / 0

nieważne bajty nie będą

odrzucane

DWORD fRtsControl specyfikacja kontroli sygnału na RTS_CONTROL_DISABLE / 0

linii RTS sygnał na linii RTS jest nieaktywny

RTS_CONTROL_ENABLE / 1

sygnał na linii RTS jest aktywny

RTS_CONTROL_HANDSHAKE / 2

włączenie potwierdzania przyjęcia

sygnału RTS (potwierdzenie musi

być odebrane na linii CTS).

Używane w trybie półdupleksowym.

Sterownik podwyższa stan linii

RTS, gdy wypełnienie bufora

wejściowego jest mniejsze od ½ .

Stan linii RTS zostaje obniżony,

gdy bufor wypełniony jest w ¾ .

Ewentualne błędy transmisji w tym

trybie usuwane są przez funkcję

EscapeCommFunction().

RTS_CONTROL_TOGGLE / 3

linia RTS jest w stanie wysokim,

jeżeli są bajty do transmisji i jest ona

możliwa; po opróżnieniu bufora

komunikacyjnego linia RTS

pozostaje w stanie niskim

DWORD fAbortOnError ustawienie wstrzymywania TRUE / 1

operacji nadawanie-odbiór wszelkie operacje nadawania i

przy wykryciu błędu transmisji odbioru są wstrzymywane, zaś

dalsza komunikacja nie jest

możliwa, dopóki błąd nie

zostanie usunięty przez wywołanie

funkcji ClearCommError()

FALSE / 0

nawet jeżeli wystąpi błąd,

transmisja jest kontynuowana —

błąd może być usunięty przez

wywołanie funkcji

ClearCommError()

DWORD fDummy2 zarezerwowane, nie używane

Większość pól tej struktury to pola jednobitowe. fDtrControl, fRtsControl są polami dwubitowymi. Aktualnie nie używane pole fDummy2 jest siedemnastobitowe. W perspektywie, wraz z wReserved oraz wReserved1, będzie wykorzystane na potrzeby innych protokołów komunikacyjnych. W Win32 API blok kontroli urządzeń deklarowany jest w sposób następujący:

typedef struct _DCB {

DWORD DCBlength;

...

} DCB;

Deklaracja ta tworzy nowe słowo kluczowe typu DCB (struktura). Zalecane jest, aby przed użyciem tej struktury jako parametru do elementu DCBlength wpisać wartość sizeof(DCB).

PRZYPOMNIJMY

Strukturę tworzy zbiór logicznie powiązanych elementów, np. zmiennych lub(i) pól bitowych. Pole bitowe stanowi zbiór przylegających do siebie bitów, znajdujących się w jednym słowie. Adres struktury pobieramy za pomocą operatora referencji &, co umożliwia nam działania na jej składowych. Do struktury jako całości możemy odwołać się przez jej nazwę, zaś do poszczególnych jej elementów, czyli zmiennych oraz pól bitowych, przez podanie nazwy zmiennej reprezentującej strukturę oraz — po kropce — nazwy konkretnej zmiennej lub pola struktury, np.:dcb.fDtrControl = DTR_CONTROL_DISABLE. Operator składowych struktur ″.″ jest lewostronnie łączny. Grupa związanych ze sobą zmiennych i pól bitowych traktowana jest jako jeden obiekt.

Zanim przejdziemy do praktycznego zastosowania poznanych pól struktury DCB, musimy zapoznać się z czterema podstawowymi funkcjami Win32 API, służącymi do programowej konfiguracji portów szeregowych. W dalszej części książki funkcji takich będzie przybywać, ale te przedstawione poniżej należy traktować jako najbardziej podstawowe.

Pierwszą, z którą się zapoznamy będzie funkcja utworzenia i otwarcia pliku lub urządzenia CreateFile(). Już sama nazwa wskazuje, że może być wykorzystywana nie tylko do obsługi portu szeregowego. Teraz jednak będzie nas interesować tylko to konkretne zastosowanie. Specyfikacja zasobów funkcji CreateFile() najczęściej używanych do operacji plikowych zamieszczona jest w uzupełnieniu 1. Funkcja ta da nam 32-bitowy identyfikator danego portu przechowywany pod właściwością HANDLE, do którego będą adresowane wszystkie komunikaty.

Ogólnie rzecz ujmując, przed rozpoczęciem czytania z portu szeregowego (lub innego urządzenia) należy o powyższym fakcie poinformować system operacyjny. Czynność tę określa się jako otwieranie portu do transmisji. Jednak zanim zaczniemy wykonywać jakiekolwiek operacje na porcie, system operacyjny musi sprawdzić, czy wybrany port komunikacyjny istnieje i czy w danym momencie nie jest przypadkiem w jakiś sposób już wykorzystywany. W przypadku uzyskania dostępu do portu system operacyjny przekazuje do aplikacji jego identyfikator. We wszystkich operacjach wejścia-wyjścia zamiast szczegółowej nazwy portu komunikacyjnego używa się właśnie jego identyfikatora.

? Niekiedy identyfikatory tego typu nazywa się uchwytami. Niestety, dosłowne przetłumaczenie angielskiego słowa handle jako uchwyt, np. handle of drawer — uchwyt, rączka szuflady, nie jest dla nas w pełni precyzyjne. Właściwszym wydaje się utożsamianie handle z identyfikatorem (unikalną wartością zlokalizowaną w danym obszarze pamięci i skojarzoną z konkretnym portem komunikacyjnym, oknem czy plikiem). W potocznej angielszczyźnie handle może również oznaczać „ksywę”, pozwalajacą na szybką identyfikację danej osoby lub rzeczy. Koncepcja identyfikatorów nie jest niczym nowym, stosowano ją już w MS DOS, jednak dopiero w Windows zyskała nową jakość.

Składnia CreateFile() wygląda następująco:

HANDLE CreateFile(LPCTSTR lpFileName,

DWORD dwDesiredAccess,

DWORD ShareMode,

LPSECURITY_ATTRIBUTES lpSecurityAttributes,

DWORD dwCreationDistribution,

DWORD dwFlagsAndAttributes,

HANDLE hTemplateFile);

Na tym etapie naszych rozważań tylko trzy parametry powyższej funkcji są istotne dla kompletnej konfiguracji portu szeregowego. Wyjaśnimy teraz ich znaczenie.

Pierwszy parametr lpFileName jest wskaźnikiem do zadeklarowanego ciągu znaków zakończonego zerem (zerowym ogranicznikiem), tzw. null terminated string lub do C-łańcucha (dokładniej do pierwszego znaku tego łańcucha), w którym przechowywana będzie nazwa (numer) portu. Z poprzednich rozdziałów pamiętamy, że ogólnie przyjęte jest stosowanie nazewnictwa portów szeregowych jako COMn (nazwy COMn znajdują się na liście nazw zastrzeżonych), gdzie n oznacza numer portu. Deklaracja numeru portu szeregowego, np. 2. będzie więc przedstawiać się w sposób bardzo prosty:

LPCTSTR lpFileName = ”COM2”;

lub, co jest równoważne:

unsigned const char *lpFileName = ”COM2”;

Należy jednak pamiętać, że pierwszy z przedstawionych sposobów deklaracji jest najprzydatniejszy w środowisku Windows, gdyż C++Builder i tak samodzielnie dokona konwersji typu unsigned const char * na typ LPCTSTR. Można oczywiście zmienną, pod którą przechowywać będziemy numer portu, zadeklarować w sposób tradycyjny — typowy dla środowiska DOS, używając typu char . Deklaracja taka będzie w pełni poprawna:

char lpFileName[5] = ”COM2”;

Sposób ten jednak przestaje jednak odpowiadać obecnym tendencjom w technikach programowania do Windows. Stosowany natomiast bywa w deklaracjach zmiennych lokalnych, które reprezentują wybrany port szeregowy występujący w obrębie danej funkcji. Ponadto jawne zadeklarowanie wskaźnika do C-łańcucha umożliwi nam pośrednie odwoływanie się do wskazanego obiektu (w naszym przypadku do łańcucha znaków reprezentującego numer portu) bez potrzeby kontrolowania jego rozmiaru, co w przyszłości okaże się bardzo wygodne.

PRZYPOMNIJMY

Win32 API posługuje się łańcuchami o długości większej niż 256 znaków. Aby przełamać to ograniczenie, zrezygnowano z zapamiętywania w pierwszym bajcie liczby określającej długość łańcucha znaków. W C-łańcuchach ostatnim znakiem kończącym ciąg jest 0 (NUL lub heks. 00), którego nie należy mylić ze znakiem zero (48 lub heks. 30). Stąd nazwa null terminated string. C-łańcuchy osiągają długość 65535 znaków plus końcowy, tzw. NUL-bajt. Są one dynamicznie alokowane w pamięci, zaś ilość pamięci zajmowanej przez C-łańcuch jest automatycznie dostosowywana do jego długości, co w pełni odpowiada architekturze Windows.

Parametr dwDesiredAccess typu DWORD umożliwia ustalenie rodzaju dostępu do portu szeregowego. Typ DWORD (DoubleWORD) jest typem 32-bitowym. Z praktycznego punktu widzenia najwygodniej jest ustalić rodzaj dostępu jako GENERIC_READ | GENERIC_WRITE (zapisuj do portu lub odczytuj z portu). Umożliwi nam to płynne wysyłanie i odbieranie komunikatów, co w pełni odpowiada półdupleksowemu wariantowi transmisji. Jeżeli zechcemy korzystać jedynie z trybu simpleksowego, do dwDesiredAccess wystarczy przypisać jeden z wybranych rodzajów dostępu.

Parametrowi dwCreationDistribution należy przypisać właściwość OPEN_EXISTING - otwórz istniejący (port). Pozostałym przyporządkujemy następujące wartości:

DWORD ShareMode = 0 (FALSE);

LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL;

DWORD dwFlagAndAttributes = 0 (FALSE);

HANDLE hTemplateFile = NULL.

Funkcją, która zwróci nam ustawienia portu ostatnio zapamiętane w strukturze DCB będzie:

BOOL GetCommState(HANDLE hCommDev, LPDCB lpdcb),

gdzie:

hCommDev jest identyfikatorem danego portu, CreateFile() zwraca nam ten identyfikator, lpdcb jest wskaźnikiem do struktury DCB, zawierającej informację o aktualnych ustawieniach parametrów łącza szeregowego. Funkcja ta (jak i wszystkie inne typu BOLL) zwraca wartość TRUE w wypadku pomyślnego jej wykonania, ewentualnie wartość FALSE w sytuacji przeciwnej. Wybrany przez nas port szeregowy ostatecznie skonfigurujemy zgodnie ze specyfikacją struktury DCB, za pomocą funkcji SetCommState(), która reinizjalizuje i uaktualnia wszystkie dostępne parametry w ustawieniach łącza szeregowego:

BOOL SetCommState(HANDLE hCommDev, LPDCB lpdcb).

Jednak tutaj parametr, na który wskazuje lpdcb musi już zawierać informacje o nowych, wybranych przez nas parametrach ustawień portu komunikacyjnego. Należy też pamiętać, że funkcja SetCommState() nie zostanie wykonana pomyślnie, jeżeli posługując się strukturą DCB element XonChar ustalimy identycznie z XoffChar. Przed zakończeniem działania aplikacji otwarty port szeregowy należy koniecznie zamknąć i zwolnić obszar pamięci przydzielony na jego identyfikator, korzystając z:

BOOL CloseHandle(HANDLE hCommDev).

We wszystkich przedstawionych powyżej funkcjach hCommDev w pełni identyfikuje dany port szeregowy, zawierając kompletną informację o tym, do którego łącza szeregowego będziemy wysyłać komunikaty. Ponieważ funkcje te mogą obsługiwać komunikaty wysyłane do wielu portów komunikacyjnych (jak również komunikaty odbierane od wiele portów), zatem każdy otwarty i zainicjalizowany port szeregowy będzie identyfikowany właśnie za pomocą swojego własnego hCommDev. Nie należy przydzielać tego samego identyfikatora do dwóch różnych portów komunikacyjnych, tak samo jak nie należy z jednym portem kojarzyć dwóch różnych identyfikatorów.

! Przy pisaniu aplikacji obsługujących łącze szeregowe należy koniecznie zamknąć port przed opuszczeniem programu. Jeżeli zechcesz korzystać z zegara systemowego przy obsłudze RS-a lub techniki programowania wielowątkowego, musisz pamiętać, że samo zamkniecie aplikacji nie powoduje automatycznego zamknięcia portu. Jego identyfikator dalej będzie przechowywany w pamięci. W pewnych przypadkach aplikacja z nie zamkniętym portem szeregowym może stać się programem rezydentnym i uniemożliwi powtórne otwarcie wybranego portu. Dobrym zwyczajem jest w pierwszej kolejności zaprojektowanie funkcji lub procedury obsługi zdarzenia zamykającego otwarty port. System operacyjny powinien być zawsze poinformowany o fakcie zamknięcia portu.

W praktyce zdarzają się jednak sytuacje, w których zamknięcie portu okaże się niemożliwe, np. z powodu jakiegoś błędu w algorytmie lub niewłaściwego sposobu wywołania danej funkcji. Mówimy wówczas, że program się załamał lub zawiesił. Ten problem powtarza się często w trakcie testowania programów komunikacyjnych. Nie należy wówczas od razu używać kombinacji Ctrl Alt Del lub czegoś bardziej drastycznego. W takich przypadkach trzeba rozwinąć z głównego menu opcję Project oraz wybrać Compile Unit, tak jak pokazano to na rys. 5.1. Nazwa działającej aplikacji powinna pojawić się na dolnym pasku zadań.

Rysunek 5.1. Przykładowy sposób wstrzymywania działania aplikacji p_test_1 z otwartym portem szeregowym

0x01 graphic

Po pojawieniu się informacji: Usuwanie sesji w toku. Zakończyć ? należy nacisnąć przycisk

OK .

0x01 graphic

Po kolejnym komunikacie znów należy potwierdzić:

0x01 graphic

Postępując tak, w większości przypadków odzyskasz program oraz odblokujesz łącze szeregowe. Sposób ten okaże się szczególnie przydatny przy kłopotach z aplikacją komunikacyjną korzystającą z komponentu typu TTimer, generującego zdarzenia w równych odstępach czasu.

Testowanie portu szeregowego

Mając na uwadze wszystko, co powiedzieliśmy do tej pory, spróbujemy napisać w C++Builderze prosty program wykorzystujący przedstawione powyżej funkcje Win32 API oraz niektóre zasoby struktury DCB. Zadaniem naszej aplikacji będzie ustawienie wybranych parametrów danego portu szeregowego, otwarcie go oraz odczytanie nowych ustawień. W tym celu stwórzmy nową standardową aplikację (polecenie File — New Application). Niech jej formularz składa się z dwóch przycisków TButton, pięciu komponentów typu TEdit oraz pięciu typu TLabel. Korzystając z inspektora obiektów (Object Inspector) oraz z karty właściwości (Proprties), cechę Name przycisku Button1 zmień na CloseComm, zaś jego cechę Caption na &Zamknij. Podobnie cechę Name przycisku Button2 zmień na OpenComm, zaś Caption na &Otwórz port. Cechy Caption komponentów TLabel zmień odpowiednio na Prędkość transmisji, Liczbę bitów danych, Parzystość, Bity stopu, Linia DTR. Cechy Text komponentów TEdit wyczyść. Formularz naszej aplikacji, wyglądającej podobnie jak na rysunku 5.2, znajduje na dołączonym CD w katalogu \KODY\BUILDER\RS_01\p_RS_01.bpr.

Rysunek 5.2. Formularz główny projektu p_RS_01.bpr

0x01 graphic

Wydruk 5.1. Kod formularza aplikacji testującej wybrane ustawienia portu szeregowego

//-------RS_01.cpp-----

//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------

#include <vcl.h>

#pragma hdrstop

#include "RS_01.h"

#pragma package(smart_init)

#pragma resource "*.dfm"

TForm1 *Form1;

HANDLE hCommDev; // identyfikator portu szeregowego

DCB dcb; // struktura kontroli portu

LPCTSTR lpFileName = "COM2"; // wskaźnik do nazwy portu

//char *lpFileName = "COM2";

//--------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

}

//-----------------funkcja zamyka otwarty port szeregowy------------

int __fastcall Close_Comm(HANDLE hCommDev)

{

if ((hCommDev == 0) || (hCommDev == INVALID_HANDLE_VALUE))

{

return FALSE;

}

else

{

CloseHandle(hCommDev);

return TRUE;

}

}

//----------------zamknięcie portu i aplikacji----------------------

void __fastcall TForm1::CloseCommClick(TObject *Sender)

{

Close_Comm(hCommDev);

Application->Terminate();

}

//---------------otwarcie portu i ustawienie jego parametrów----------

void __fastcall TForm1::OpenCommClick(TObject *Sender)

{

hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,

0, NULL, OPEN_EXISTING, 0, NULL);

if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest

//otwarty prawidłowo

{

dcb.DCBlength = sizeof(dcb); // aktualny rozmiar

// struktury DCB

GetCommState(hCommDev, &dcb); // udostępnienie aktualnych

// parametrów DCB

dcb.BaudRate = CBR_1200; // prędkość transmisji

dcb.fParity = TRUE; // sprawdzanie parzystości

dcb.Parity = NOPARITY; // ustawienie parzystości

dcb.StopBits = TWOSTOPBITS; // bity stopu

dcb.ByteSize = 7; // bity danych

dcb.fDtrControl = 1; // np. kontrola linii DTR

SetCommState(hCommDev, &dcb); // reinicjalizacja DCB

}

else

{

switch ((int)hCommDev)

{

case IE_BADID: // w przypadku błędnej identyfikacji portu

// BADIDentify pokaż komunikat

MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"

" aktywny.", "Błąd",MB_OK);

break;

};

}

//-------------sprawdzenie i wyświetlenie ustawionej prędkości------

switch (dcb.BaudRate)

{

case CBR_9600:

Edit1->Text = IntToStr(dcb.BaudRate);

break;

case CBR_1200:

Edit1->Text = IntToStr(dcb.BaudRate);

break;

case CBR_300:

Edit1->Text = IntToStr(dcb.BaudRate);

break;

case CBR_110:

Edit1->Text = IntToStr(dcb.BaudRate);

break;

}

//-------------sprawdzenie i wyświetlenie ustawionych bitów danych-

switch (dcb.ByteSize)

{

case 8:

Edit2->Text = IntToStr(dcb.ByteSize);

break;

case 7:

Edit2->Text = IntToStr(dcb.ByteSize);

break;

case 6:

Edit2->Text = IntToStr(dcb.ByteSize);

break;

case 5:

Edit2->Text = IntToStr(dcb.ByteSize);

break;

}

//-------------sprawdzenie i wyświetlenie ustawionej parzystości----

switch (dcb.Parity)

{

case NOPARITY:

Edit3->Text = "Brak";

break;

case ODDPARITY:

Edit3->Text = "Nie parzysta";

break;

case EVENPARITY:

Edit3->Text = "Parzysta";

break;

case MARKPARITY:

Edit3->Text = "Znacznik: 1";

break;

}

//-------------sprawdzenie i wyświetlenie ustawionych bitów stopu---

switch (dcb.StopBits)

{

case ONESTOPBIT:

Edit4->Text = "1";

break;

case TWOSTOPBITS:

Edit4->Text = "2";

break;

case ONE5STOPBITS:

Edit4->Text = "1.5";

break;

}

//-------------sprawdzenie i wyświetlenie stanu linii DTR-----------

switch (dcb.fDtrControl)

{

case DTR_CONTROL_DISABLE:

Edit5->Text = "Nieaktywna";

break;

case DTR_CONTROL_ENABLE:

Edit5->Text = "Aktywna";

break;

case DTR_CONTROL_HANDSHAKE:

Edit5->Text = "Handshaking";

break;

}

//--------------------------------------------------------------------

}

Stworzyliśmy zatem bardzo prostą, wręcz „dydaktyczną” aplikację, ale taki właśnie był nasz cel. Możemy zauważyć, że obsługa tego programu sprowadza się wywołania funkcji obsługi zdarzenia OpenCommClick(). Naciśnięcie przycisku Otwórz port powoduje automatyczne skonfigurowanie wybranego wcześniej portu szeregowego oraz odczytanie jego aktualnych wybranych ustawień. Dobrze byłoby, gdyby Czytelnik spróbował samodzielnie skonfigurować port z większą liczbą parametrów a następnie je odczytał. Nabiera się przez to większej wprawy w manipulowaniu znacznikami struktury DCB. Zamknięcie portu i aplikacji nastąpi po wywołaniu funkcji obsługi zdarzenia CloseCommClick(), w której z kolei dokonujemy wywołania funkcji Close_Comm(), zamykającej port szeregowy i aplikację. Przyglądając się kodowi funkcji obsługi zdarzenia OpenCommClick() zauważymy, że tuż po wywołaniu CreateFile() zastosowaliśmy następującą instrukcje warunkową, sprawdzającą, czy funkcja ta zwróciła prawidłowy identyfikator zadeklarowanego portu:

if (hCommDev != INVALID_HANDLE_VALUE)

{

...

}

else

{

switch ((int)hCommDev)

{

case IE_BADID: // W przypadku błędnej identyfikacji portu

// BADIDentify pokaż komunikat

...

break;

};

}

Łatwo można się przekonać, że w przypadku błędnego przydzielenia identyfikatora dla portu COMn, funkcja CreateFile() zwraca wartość INVALID_HANDLE_VALUE (niewłaściwa wartość identyfikatora), zdefiniowaną w Win32 API. Jest to bardzo skuteczna metoda zabezpieczenia się przed próbą otwarcia nie istniejącego lub już otwartego portu (urządzenia). Zauważmy też, że po to by odczytać aktualną wartość hCommDev, musieliśmy wymusić przekształcenie typów, używając operacji rzutowania (int)hCommDev. Każdy już się chyba przekonał, że identyfikator czy — jak kto woli — „uchwyt” typu HANDLE nie jest żadnym numerem bezpośrednio nadanym portowi komunikacyjnemu, lokalizuje jedynie unikalny obszar pamięci, do którego należy się odwołać, by uzyskać dostęp do danego urządzenia.

! Raz otwartego portu komunikacyjnego nie można otworzyć powtórnie, podobnie jak nie uda się otworzyć już otwartego okna. Nie można też powtórnie skorzystać z obszaru pamięci, z którego właśnie korzystamy.

Jeżeli mimo wszystko port nie został otwarty prawidłowo, dobrze by było, gdyby aplikacja powiadomiła nas o tym fakcie. W tym celu można skorzystać z komunikatów Windows typu IE_ (ang. Identify Error — błąd identyfikacji portu — urządzenia lub jego ustawień). W poniższej tabeli przedstawiono najczęściej otrzymywane od Windows tego typu komunikaty:

Tabela 5.6.

Najczęściej używane komunikaty błędnej identyfikacji ustawień portu szeregowego

Identyfikacja komunikatu Wartość Znaczenie

IE_BADID -1 niewłaściwa identyfikacja

urządzenia

IE_BAUDRATE -12 błędnie określona szybkość transmisji

IE_BYTESIZE -11 błędnie określona liczba bitów danych

IE_DEFAULT -5 niewłaściwie określone parametry

domyślne urządzenia

IE_HARDWARE -10 odbiornik jest zablokowany

IE_MEMORY -4 niewłaściwie ustalono rozmiar buforów

IE_NOPEN -3 urządzenie nie jest otwarte do transmisji

IE_OPEN -2 urządzenie pozostaje otwarte

Podczas lektury wydruku programu RS_01.cpp może zadziwić fakt, iż w pierwszej kolejności tuż po otwarciu portu wyszukaliśmy funkcją GetCommState() jego bieżące ustawienia. Następnie wybranym parametrom DCB przypisaliśmy nowe, własne wartości, które ostatecznie wpisaliśmy do struktury kontroli łącza szeregowego funkcją SetCommState(). Technika pisania tego typu aplikacji określana jest mianem przeładowywania lub przedefiniowania danych (ang. override). Użycie funkcji SetCommState() spowoduje prawdopodobnie poprawne przypisanie nowych wartości do DCB jednak pisząc program w ten sposób powodujemy tzw. zachodzenie na siebie lub przeciążania danych (ang. overload), co w pewnych wypadkach nie jest rzeczą pożądaną. Zachęcam Czytelnika piszącego aplikacje dla Windows do stosowania metod przeładowywania danych. Wówczas o wiele rzadziej będą się pojawiać na ekranie irytujące komunikaty w stylu: błąd w module kernel32.dll lub kernel32.exe.

W celu dokładniejszego zapoznania się z możliwościami testowania systemów komunikacyjnych dostępnych w Windows poniżej przedstawiono pewną bardzo użyteczną strukturę oferowaną przez Win32 API. Zawarte w niej informacje mogą być wykorzystywane do pełnego odczytywania wszystkich istotnych parametrów interesującego nas łącza komunikacyjnego oraz usług potencjalnie przez nie oferowanych.

Tabela 5.7. Zasoby struktury COMMPROP

Typ Element struktury Znaczenie Zawartość elementu,

Maska określająca

włączony bit

WORD wPacketLength Określa (w bajtach) rozmiar należy odczytać,

porcji pakietu danych zależy też od typu

sterownika

WORD wPacketVersion wersja struktury nr 2 w Win 9x

DWORD dwServiceMask określenie maski bitowej, SP_SERIALCOMM

wskazującej na typ aktualnie jest zawsze określone

dostępnej usługi komunikacyjnej

DWORD dwReserved1 zarezerwowane, nie używane

DWORD dwMaxTxQueue maksymalny rozmiar wewnętrznego 0 oznacza, że nie ustalono

bufora wyjściowego nadajnika maksymalnej wartości

(w bajtach)

DWORD dwMaxRxQueue maksymalny rozmiar wewnętrznego 0 oznacza, że nie ustalono

bufora wejściowego odbiornika maksymalnej wartości

(w bajtach)

DWORD dwMaxBaud maksymalna dostępna prędkość BAUD_075 75 b/s

transmisji w bitach na sekundę BAUD_110 110

BAUD_134_5 134.5

BAUD_150 150

BAUD_300 300

BAUD_600 600

BAUD_1200 1200

BAUD_1800 1800

BAUD_2400 2400

BAUD_4800 4800

BAUD_7200 7200

BAUD_9600 9600

BAUD_14400 14400

BAUD_19200 19200

BAUD_38400 38400

BAUD_56K 56K

BAUD_57600 57600

BAUD_115200 115200

BAUD_128K 128K

BAUD_USER

programowalne

DWORD dwProvSubType typ usługi komunikacyjnej PST_FAX faks

PST_LAT protokół LAT

(Local — Area Transport)

PST_MODEM modem

PST_NETWORK_BRIDGE

niewyspecyfikowana sieć

PST_PARALLELPORT

port równoległy

PST_RS232 RS-232

PST_RS422 RS-422

PST_RS423 RS-423

PST_RS449 RS-449

PST_SCANNER skaner

PST_TCPIP_TELNET

protokół TCP/IP

PST_UNSPECIFIED

brak specyfikacji

PST_X25 protokół X.251

DWORD dwProvCapabilities określa maskę bitową PCF_16BITMODE

identyfikującą rodzaj funkcji tryb 16-bitowy

udostępnianych przez PCF_DTRDSR

usługę komunikacyjną kontrola DTR-DSR

(dostarczyciela usługi) PCF_INTTIMEOUTS

czas przeterminowania

PCF_PARITY_CHECK

sprawdzanie parzystości

PCF_RLSD

kontrola RLSD

PCF_RTSCTS

kontrola RTS-CTS

PCF_SETXCHAR

możliwość użycia

protokołu XON/XOFF

PCF_SPECIALCHARS

specjalny znak

PCF_TOTALTIMEOUTS

kontrola czasu

przeterminowania transmisji

PCF_XONXOFF

podtrzymanie protokołu

XON-XOFF

DWORD dwSettableParams specyfikacja maski bitowej SP_BAUD prędkość

identyfikującej parametry SP_DATABITS długość

transmisji podlegające ew. słowa danych

zmianom SP_HANDSHAKING

kontrola przepływu danych

SP_PARITY parzystość

SP_PARITY_CHECK

sprawdzanie parzystości

SP_RLSD sygnał RLSD

SP_STOPBITS bity stopu

DWORD dwSettableBaud specyfikacja maski bitowej

umożliwiającej ustawienie tak samo jak w dwMaxBaud

prędkości transmisji

WORD wSettableData specyfikacja maski bitowej DATABITS_5

identyfikującej możliwe do DATABITS_6

użycia długości słowa danych DATABITS_7

DATABITS_8

DATABITS_16

DATABITS_16X

szczególna długość słowa

danych

WORD wSettableStopParity specyfikacja maski bitowej STOPBITS_10 1 bit stopu

identyfikującej możliwe do STOPBITS_15 1,5 bitu

użycia wartości bitów stopu STOPBITS_20 2 bity

i kontroli parzystości PARITY_NONE brak

PARITY_ODD nieparzysta

PARITY_EVEN parzysta

PARITY_MARK 1

PARITY_SPACE 0

DWORD dwCurrentTxQueue aktualny maksymalny rozmiar 0 oznacza, że wartość ta

wewnętrznego bufora wyjściowego nie jest dostępna

nadajnika (w bajtach)

DWORD dwCurrentRxQueue aktualny maksymalny rozmiar 0 oznacza, że wartość ta

wewnętrznego bufora wejściowego nie jest aktualnie

odbiornika (w bajtach) dostępna

DWORD dwProvSpec1 specyfikacja formatu danych w zależnoście od

wymaganych przez daną usługę dwProvSubType

komunikacyjną aplikacje powinny

ignorować ten człon,

chyba że mają

szczegółowe informacje

odnośnie formatu danych

wymaganych przez

dostarczyciela usługi

DWORD dwProvSpec2 jak wyżej

WCHAR wcProvChar[1] jak wyżej

Jeżeli dwProvSubType przypisano

PST_MODEM , musi nastąpić

odwołanie do struktur

MODEMDEVCAPS oraz

MODEMSETTINGS1.

dwProvSpec1 oraz dwProvSpec2

nie są wówczas używane.

W Win32 API COMMPROP deklaruje się następująco:

typedef struct _COMMPROP {

WORD wPacketLength;

...

} COMMPROP;

Powyższa deklaracja tworzy nowe słowo kluczowe typu COMMPROP (struktura).

Zbudujmy teraz aplikację, za pomocą której będziemy mogli selektywnie odczytywać stan poszczególnych masek bitowych udostępnianych przez COMMPROP. Wykorzystamy tu znany nam już proces maskowania z wykorzystaniem operatora iloczynu bitowego & (bitowe i). Program będzie odczytywał wartość wybranego elementu struktury, a następnie poprzez wybranie kolejnych masek będzie selektywnie sprawdzał, czy włączone są konkretne bity odpowiedzialne za pewne parametry transmisji. Omawiany projekt znajduje się na dołączonym CD w katalogu \KODY\BUILDER\RS_02\p_RS_02.bpr. Do testowania wybierzmy elementy: dwSettableParams , reprezentowany na 32 bitach oraz wSettableData i wSettableStopParity , reprezentowane na 16 bitach. Zastosujemy nieco odbiegający od przedstawionego wcześniej projekt formularza. Składać się on będzie z dwóch dobrze nam już znanych przycisków, reprezentujących zdarzenia polegające na otwarciu portu oraz na jego zamknięciu. Zastosowałem ponadto dwa komponenty typu TTrackBar dwa TEdit oraz dwa typu TLabel, tak jak pokazuje to rysunek 5.3. Obsługa zdarzenia TrackBar1Change() polega na wybraniu interesującego nas elementu struktury COMMPROP oraz odczytaniu jego aktualnej wartości. Jeżeli zechcemy sprawdzić, czy włączony jest konkretny bit reprezentujący wybrany atrybut transmisji przechowywany w danym elemencie struktury, wystarczy przesunąć wskaźnik uruchamiający funkcję obsługi zdarzenia TrackBar2Change(). Funkcją, która zwraca aktualne własności portu komunikacyjnego identyfikowanego przez hCommDev będzie:

BOOL GetCommProperties(HANDLE hCommDev, LPCOMMPROP lpCommProp);

lpCommProp jest wskaźnikiem do struktury COMMPROP, której format danych w ogólnym przypadku należy najpierw zainicjalizować:

CommProp.dwProvSpec1 = COMMPROP_INITIALIZED;

Informacje tam zawarte mogą być pomocne przy odwoływaniu się do rodziny funkcji SetCommState(), SetCommTimeouts() lub SetupComm().

Rysunek 5.3. Formularz główny projektu p_RS_02.bpr

0x01 graphic

Wydruk 5.2. Kod formularza aplikacji testującej wybrane zasoby struktury COMMPROP

//-------RS_02.cpp-----

//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------

#include <vcl.h>

#pragma hdrstop

#include "RS_02.h"

#pragma package(smart_init)

#pragma resource "*.dfm"

TForm1 *Form1;

HANDLE hCommDev; // identyfikator portu

COMMPROP CommProp; // właściwości portu

LPCTSTR lpFileName="COM2"; // wskaźnik do nazwy portu szeregowego

//--------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

}

//----------funkcja zamyka otwarty port szeregowy---------------------

int __fastcall Close_Comm(HANDLE hCommDev)

{

if ((hCommDev == 0) || (hCommDev == INVALID_HANDLE_VALUE))

{

return FALSE;

}

else

{

CloseHandle(hCommDev);

return TRUE;

}

}

//----------zamknięcie poru i aplikacji-------------------------------

void __fastcall TForm1::CloseCommClick(TObject *Sender)

{

Close_Comm(hCommDev);

Application->Terminate();

}

//-------otwarcie portu i ustawienie jego parametrów------------------

void __fastcall TForm1::OpenCommClick(TObject *Sender)

{

hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,

0, NULL, OPEN_EXISTING, 0, NULL);

if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest

// otwarty prawidłowo

{

CommProp.dwProvSpec1 = COMMPROP_INITIALIZED;// inicjalizuje

// format danych

// usługi. Port

// szeregowy jest

// zawsze dostępny

GetCommProperties(hCommDev, &CommProp);

}

else

{

switch ((int)hCommDev)

{

case IE_BADID:

ShowMessage("Niewłaściwa nazwa portu lub port jest"

" aktywny.");

break;

};

}

}

//---------wybrane maski bitowe---------------------------------------

void __fastcall TForm1::TrackBar1Change(TObject *Sender)

{

switch (TrackBar1->Position)

{

case 1: {

TrackBar2->Max = 7;

Label1->Caption = "dwSettableParams";

Edit1->Text=IntToStr(CommProp.dwSettableParams);

break;

}

case 2: {

TrackBar2->Max = 6;

Label1->Caption = "wSettableData";

Edit1->Text=IntToStr(CommProp.wSettableData);

break;

}

case 3: {

TrackBar2->Max = 8;

Label1->Caption = "wSettableStopParity";

Edit1->Text=IntToStr(CommProp.wSettableStopParity);

break;

}

} //koniec switch

}

//------------------zawartość maski-----------------------------------

void __fastcall TForm1::TrackBar2Change(TObject *Sender)

{

if (TrackBar1->Position == 1) {

switch (TrackBar2->Position)

{

case 1: {

Label2->Caption = "SP_PARITY";

Edit2->Text=IntToStr(CommProp.dwSettableParams & SP_PARITY);

break;

}

case 2: {

Label2->Caption = "SP_BAUD";

Edit2->Text=IntToStr(CommProp.dwSettableParams & SP_BAUD);

break;

}

case 3: {

Label2->Caption = "SP_DATABITS";

Edit2->Text=IntToStr(CommProp.dwSettableParams &

SP_DATABITS);

break;

}

case 4: {

Label2->Caption = "SP_STOPBITS";

Edit2->Text=IntToStr(CommProp.dwSettableParams &

SP_STOPBITS);

break;

}

case 5: {

Label2->Caption = "SP_HANDSHAKING";

Edit2->Text=IntToStr(CommProp.dwSettableParams &

SP_HANDSHAKING);

break;

}

case 6: {

Label2->Caption = "SP_PARITY_CHECK";

Edit2->Text=IntToStr(CommProp.dwSettableParams &

SP_PARITY_CHECK);

break;

}

case 7: {

Label2->Caption = "SP_RLSD";

Edit2->Text=IntToStr(CommProp.dwSettableParams & SP_RLSD);

break;

}

} //koniec switch

} // koniec if

if (TrackBar1->Position == 2) {

switch (TrackBar2->Position)

{

case 1: {

Label2->Caption = "DATABITS_5";

Edit2->Text=IntToStr(CommProp.wSettableData & DATABITS_5);

break;

}

case 2: {

Label2->Caption = "DATABITS_6";

Edit2->Text=IntToStr(CommProp.wSettableData & DATABITS_6);

break;

}

case 3: {

Label2->Caption = "DATABITS_7";

Edit2->Text=IntToStr(CommProp.wSettableData & DATABITS_7);

break;

}

case 4: {

Label2->Caption = "DATABITS_8";

Edit2->Text=IntToStr(CommProp.wSettableData & DATABITS_8);

break;

}

case 5: {

Label2->Caption = "DATABITS_16";

Edit2->Text=IntToStr(CommProp.wSettableData & DATABITS_16);

break;

}

case 6: {

Label2->Caption = "DATABITS_16X";

Edit2->Text=IntToStr(CommProp.wSettableData & DATABITS_16X);

break;

}

} //koniec switch

} // koniec if

if (TrackBar1->Position == 3) {

switch (TrackBar2->Position)

{

case 1: {

Label2->Caption = "STOPBITS_10";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

STOPBITS_10);

break;

}

case 2: {

Label2->Caption = "STOPBITS_15";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

STOPBITS_15);

break;

}

case 3: {

Label2->Caption = "STOPBITS_20";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

STOPBITS_20);

break;

}

case 4: {

Label2->Caption = "PARITY_NONE";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

PARITY_NONE);

break;

}

case 5: {

Label2->Caption = "PARITY_ODD";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

PARITY_ODD);

break;

}

case 6: {

Label2->Caption = "PARITY_EVEN";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

PARITY_EVEN);

break;

}

case 7: {

Label2->Caption = "PARITY_MARK";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

PARITY_MARK);

break;

}

case 8: {

Label2->Caption = "PARITY_SPACE";

Edit2->Text=IntToStr(CommProp.wSettableStopParity &

PARITY_SPACE);

break;

}

} //koniec switch

} // koniec if

}

//--------------------------------------------------------------------

Dla przykładu rozpatrzmy dwSettableParams typu DWORD, a więc reprezentowany na 32 bitach. Odczytując odpowiednią wartość, przekonaliśmy się, że cała zawarta tam informacja zapisana jest na 7 pierwszych bitach dwSettableParams. Użyliśmy operatora &, aby sprawdzić, czy włączone są poszczególne bity reprezentujące atrybuty związane z konkretnymi parametrami komunikacyjnymi. Patrząc na wartości w postaci binarnej łatwo zorientujemy się, jaki jest aktualny stan logiczny poszczególnych bitów zawartych w tej zmiennej i za co są one odpowiedzialne.

wartość

bit 7

bit 6

bit 5

Bit 4

bit 3

bit 2

bit 1

bit 0

dwSettableParams

127

0

1

1

1

1

1

1

1

maska rezultat maskowania

SP_PARITY

1

0

0

0

0

0

0

0

1

SP_BAUD

2

0

0

0

0

0

0

1

0

SP_DATABITS

4

0

0

0

0

0

1

0

0

SP_STOPBITS

8

0

0

0

0

1

0

0

0

SP_HANDSHAKING

16

0

0

0

1

0

0

0

0

SP_PARITY_CHECK

32

0

0

1

0

0

0

0

0

SP_RLSD

64

0

1

0

0

0

0

0

0

W analogiczny sposób możemy przetestować wszystkie maski bitowe udostępniane przez COMMPROP, odpowiadające właściwym pozycjom konkretnych bitów. Jeżeli jako rezultat iloczynu bitowego wartości elementu struktury z maską określającą włączony bit otrzymamy wartość 0, oznaczać to będzie, że testowany bit jest wyłączony i dany parametr komunikacyjny nie jest aktualnie dostępny. Bieglejsi w temacie Czytelnicy zapewne już zorientowali się, jakie prezenty oferuje nam ta struktura. Manipulowanie bitami jest tu sprawą dobrania odpowiednich operatorów przesuwania, maskowania i dopełniania. Można np. skasować bity SP_PARITY i SP_BAUD w elemencie dwSettableParams:

CommProp.dwSettableParams &= ~(SP_PARITY | SP_BAUD);

Podobnie w jakimś fragmencie aplikacji można zastosować warunek:

if ((CommProp.dwSettableParams & (SP_PARITY | SP_BAUD)) == 0)

{

...

}

który będzie prawdziwy, gdy oba bity będą skasowane. Jednak osobom mniej zaawansowanym w operacjach bitowych odradzałbym jakiekolwiek próby ingerowania w zawartość COMMPROP.

Przykładowy algorytm realizujący zamianę liczb z postaci dziesiętnej na binarną można znaleźć w Uzupełnieniu 2.

Jeżeli mimo wszystko ktoś zechciałby bliżej zainteresować się tym tematem, powinien odwołać się do opisanej poniżej struktury, zawierającej informacje o stanie konfiguracji danego urządzenia komunikacyjnego.

Tabela 5.8. Specyfikacja struktury COMMCONFIG.

Typ Element struktury Znaczenie Zawartość

DWORD dwSize rozmiar struktury w bajtach należy wpisać

WORD wVersion wersja struktury należy odczytać

WORD wReserved zarezerwowane

DCB dcb struktura kontroli portu patrz DCB

szeregowego

DWORD dwProviderSubType identyfikacja typu dostarczanej patrz COMMPROP

usługi komunikacyjnej i, tym

samym, wymaganego formatu

danych

DWORD dwProviderOffset określenie offsetu dla danych 0, jeżeli nie określono

wymaganych przez typu danych

dostarczyciela usługi

komunikacyjnej; offset

(tzw. przesunięcie) określony

jest zwykle w stosunku do

początku struktury

DWORD dwProviderSize rozmiar danych (bajty) zależnie od typu

wymaganych przez usługi

usługę komunikacyjną

(dostarczyciela usługi)

WCHAR wcProviderData[1] dane dostarczane wraz jeżeli ustalono typ usługi:

z usługą (ponieważ PST_RS232 lub

przewidywane jest w PST_PARALLELPORT ,

przyszłości uzupełnienie człon ten jest pomijany;

struktury, aplikacja jeżeli ustalono

powinna używać PST_MODEM ,

dwProviderOffset należy odwołać się do

w celu określenia położenia MODEMSETTINGS

wcProviderData)

Win32 API COMMCONFIG deklaruje następująco:

typedef struct _COMM_CONFIG {

DWORD dwSize;

...

} COMMCONFIG, *LPCOMMCONFIG;

Powyższa deklaracja tworzy dwa nowe słowa kluczowe typu COMMCONFIG (struktura) oraz LPCOMMCONFIG (wskaźnik do struktury).

Aktualną konfigurację łącza komunikacyjnego odczytamy, korzystając z funkcji API:

BOOL GetCommConfig(HANDLE hCommDev, LPCOMMCONFIG lpCC,

LPDWORD lpdwSize);

lpCC wskazuje na strukturę COMMCONFIG, zaś lpdwSize jest wskaźnikiem do 32-bitowej zmiennej, określającej rozmiar struktury. Bieżącą konfigurację portu komunikacyjnego zapiszemy za pomocą:

BOOL SetCommConfig(HANDLE hCommDev, LPBYTE lpCC, DWORD dwSize);

lpCC jest wskaźnikiem do COMMCONFIG, zaś dwSize określa w bajtach rozmiar struktury wskazywanej przez lpCC. Przed przekazaniem tej struktury jako parametru należy do elementu dwSize wpisać wartość równą sizeof(COMMCONFIG).

Nawiązanie połączenia

Teraz, kiedy z poziomu C++Buildera umiemy otwierać i zamykać port szeregowy oraz odczytywać i ustawiać (a nawet modyfikować) jego parametry transmisji, zapoznamy się ze sposobami wysyłania i odbierania konkretnych komunikatów poprzez interfejs szeregowy RS 232C. Jednak zanim przejdziemy do pisania naszej aplikacji, musimy stworzyć jej plan, czyli zastanowić się, jakie niezbędne cele ma ona realizować. Otóż każda poprawnie napisana aplikacja realizująca obsługę łącza szeregowego będzie składać się z czterech podstawowych segmentów:

  1. Segment inicjalizujący wybrany przez nas port szeregowy. Będzie to część konfiguracyjna aplikacji. Z poprzednich rozdziałów pamiętamy, że przeznaczone do wysłania w formie szeregowej dane, pliki, komendy lub zapytania będą transmitowane w postaci ramek danych - najmniejszej porcji informacji, jaką jednorazowo możemy przesłać przez łącze szeregowe. Wszystkie cechy ramki (prędkość transmisji, liczba bitów stopu, liczba bitów danych, sposób kontroli parzystości, różne sposoby kontroli sygnałów sterujących łącza) muszą być uzgodnione pomiędzy nadajnikiem a odbiornikiem jeszcze przed nawiązaniem łączności. Część aplikacji realizującej to zagadnienie będziemy nazywać segmentem konfiguracyjnym. Takie proste programy konfigurujące umiemy już tworzyć, korzystając chociażby ze struktury DCB.

  2. Segment wysyłający (nadający) komunikaty — dane do łącza szeregowego.

  3. Segment odbierający komunikaty — dane przychodzące do łącza szeregowego od urządzeń zewnętrznych (innego komputera lub różnego rodzaju przyrządów pomiarowych, ew. modemu).

  4. Segment zamykający port i aplikację.

W praktyce jednak będziemy musieli rozważyć wariant aplikacji, w której segmenty nadający i odbierający będą ściśle ze sobą współpracować, tworząc niejako „dwa w jednym”. W przyszłości spotkamy się również z koniecznością zapisu danych w odpowiednim formacie oraz ich wizualizacji, np. w postaci różnego rodzaju wykresów. Nauczymy się konstruować tego typu algorytmy.

Projektowanie naszego programu rozpoczniemy od zapoznania się z niezbędnymi funkcjami Win32 API, potrzebnymi do zbudowania poszczególnych jego segmentów.

Segment inicjalizująco-konfiguracyjny

Niestety, poznane wcześniej funkcje CreateFile(), GetCommState(), SetCommState(), które wykorzystaliśmy, pisząc program RS_01.cpp oraz sama znajomość struktury DCB już nie wystarczą nam do pełnej i prawidłowej inicjalizacji portu w trybie do wysyłania i odbierania komunikatów przez łącze szeregowe. Tę część naszej aplikacji będziemy musieli trochę wzbogacić. Pierwszą, z którą się zapoznamy, będzie funkcja:

BOOL SetupComm(HANDLE hCommDev,

DWORD cbInQueue,

DWORD cbOutQueue);

Inicjalizuje ona parametry komunikacyjne danego portu szeregowego. Pierwszy parametr tej funkcji to dobrze nam już znany identyfikator portu COMn. Parametry cbInQueue oraz cbOutQueue określają rozmiary bufora wejściowego i wyjściowego, czyli buforów (obszarów pamięci operacyjnej) przechowujących dane odbierane i wysyłane przez łącze COMn. Poziom ich wypełnienia w czasie transmisji możemy ustalić, odwołując się do elementów dwCurrentTxQueue oraz dwCurrentRxQueue struktury COMMPROP. Zauważmy w tym miejscu, że problem realizacji transmisji szeregowej można rozwiązać niekoniecznie stosując metodę buforowania danych. W niektórych przypadkach większy pożytek daje metoda polegająca na ciągłym odczytywaniu znak po znaku danych wcześniej zapisanych na dysku, a następnie, również znak po znaku, bezpośrednim przekierowywaniu ich do portu szeregowego — pojedyncze znaki nie są buforowane. Tak samo można postąpić odbierając dane. W dalszej części książki zapoznamy się z tym sposobem transmisji. Teraz jednak skoncentrujemy się na buforowaniu danych. Często spotykam się z opinią, że arbitralne ustalenie rozmiaru bufora danych może być czymś zgubnym dla aplikacji. Czy, gdy ustalony bufor będzie nawet dużo większy niż otrzymane dane, aplikacja może się zawiesić? Otóż niekoniecznie musi tak być. Przekonamy się o tym, pisząc kod segmentu odbierającego dane.

Kolejną, bardzo pomocną w monitorowaniu portu szeregowego, będzie funkcja odzyskująca komunikaty pomocne w zaprogramowaniu sposobu realizacji transmisji szeregowej:

BOOL GetCommMask(HANDLE hCommDev, LPDWORD lpfdwEvtMask);

Wybór komunikatów, na które zechcemy „wyczulić” naszą aplikację dokonamy, korzystając z:

BOOL SetCommMask(HANDLE hCommDev, DWORD fdwEvtMask);

Dzięki odpowiedniemu wyborowi parametru fdwEvtMask (Event Mask)aplikacja będzie mogła otrzymać pełną informację o aktualnym stanie transmisji. A oto dopuszczalne stałe symboliczne, reprezentujące najczęściej używane w transmisji szeregowej komunikaty o zdarzeniu, tzw. komunikaty typu EV_ (ang. Event Value — znaczenie zdarzenia lub po prostu Event — zdarzenie), które można przyporządkować parametrowi fdwEvtMask:

EV_BREAK — wykryto przerwanie połączenia. Zdarzenie będzie sygnalizowane, jeżeli wejście

RxD odbiornika będzie pozostawać w niskim stanie logicznym w czasie dłuższym

niż potrzebny na transmisję jednej ramki.

EV_CTS — wykryto zmianę poziomu sygnału CTS (Clear To Send), np. z wysokiego na niski

lub odwrotnie.

EV_DSR — wykryto zmianę poziomu sygnału DSR (Data Send Ready).

EV_ERR — wykryto błąd statusu linii. Możliwe są tutaj trzy przypadki:

CE_FRAME — błąd protokołu ramki. Bit stopu oczekiwany przez urządzenie odbierające nadszedł za późno lub za wcześnie. Będzie to oznaczać, że nie uzgodniono wcześniej pomiędzy nadajnikiem a odbiornikiem identycznej prędkości transmisji lub(i) sposobu reagowania na bit parzystości, lub(i) liczby bitów danych, lub(i) liczby bitów stopu.

CE_OVERRUN — błąd przepełnienia lub przekrywania się danych. Dane przychodzą do portu szybciej niż mogą być fiizycznie pobierane z bufora wejściowego odbiornika. Następuje wówczas przekrywanie się bajtów. Bajt danych znajdujący się w buforze zostanie zamazany przez następny przychodzący bajt.

CE_RXPARITY — błąd niezgodności kontroli parzystości pomiędzy nadajnikiem a odbiornikiem. Otrzymywane dane mogą być niekompletne lub zafałszowane.

EV_RING — wskaźnik wywołania, tzw. Ring Indicator został odebrany.

EV_RLSD — sygnał RLSD (Receive Line Signal Detect) zmienił poziom.

EV_RXCHAR — odebrano znak i umieszczono go w buforze wejściowym.

EV_RXFLAG — odebrano ostatni znak sterujący i umieszczono go w buforze wejściowym.

Znak ten musiał być wcześniej wyspecyfikowany zgodnie ze strukturą DCB.

EV_TXEMPTY — ostatni znak z bufora wyjściowego został wysłany.

Należy w tym miejscu zwrócić uwagę na fakt, że tak naprawdę powyższe stałe symboliczne reprezentowane są przez pewne maski bitowe, a jako takie podlegają one wszelkim bitowym operacjom logicznym, których przykłady przedstawiliśmy nieco wcześniej, przy okazji prezentacji struktury COMMPROP.

maska

Wartość

bit 8

bit 7

bit 6

bit 5

Bit 4

bit 3

bit 2

Bit 1

bit 0

EV_RXCHAR

1

0

0

0

0

0

0

0

0

1

EV_RXFLAG

2

0

0

0

0

0

0

0

1

0

EV_TXEMPTY

4

0

0

0

0

0

0

1

0

0

EV_CTS

8

0

0

0

0

0

1

0

0

0

EV_DSR

16

0

0

0

0

1

0

0

0

0

EV_RLSD

32

0

0

0

1

0

0

0

0

0

EV_BREAK

64

0

0

1

0

0

0

0

0

0

EV_RING

256

1

0

0

0

0

0

0

0

0

EV_ERR

128

0

1

0

0

0

0

0

0

0

CE_OVERRUN

2

0

0

0

0

0

0

0

1

0

CE_RXPARITY

4

0

0

0

0

0

0

1

0

0

CE_FRAME

8

0

0

0

0

0

1

0

0

0

Jeżeli np. chcielibyśmy ustalić sposób reakcji na zdarzenie polegające na tym, że aplikacja w jakiś sposób wykryje fakt wysłania ostatniego znaku z bufora wyjściowego oraz dotyczące zmiany poziomu sygnału na linii CTS, wystarczy napisać:

GetCommMask(hCommDev, &fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY | EV_CTS);

Możliwe są również inne rozsądne kombinacje masek udostępnianych przez fdwEvtMask. Można je stosować w zależności od potrzeb. Zauważmy też, że najlepsze efekty w wykrywaniu ewentualnych błędów statusu linii dają tylko kombinacje masek CE_ z EV_ERR, co zostało zaznaczone w powyższym zestawieniu. Nie będziemy w tym miejscu przedstawiać szczegółowego kodu segmentu konfiguracyjnego aplikacji, gdyż poza ewentualnym wywołaniem przedstawionych wcześniej funkcji nie będzie się on zasadniczo różnił od kodu funkcji obsługi zdarzenia OpenCommClick(), zamieszczonego w wydruku programu RS_01.cpp. Zapewne dodamy tam w przyszłości kilka nowych elementów, które omówione zostaną w kolejnych podrozdziałach.

Win32 API rozróżnia pojęcia event oraz event mask.

Zdarzenie (ang. event) określane jest jako zmiana występująca w aktualnym stanie obiektu, będąca źródłem odpowiednich komunikatów przekazywanych do aplikacji lub bezpośrednio do systemu. Reakcja obiektu na wystąpienie zdarzenia udostępniana jest aplikacji dzięki funkcji (procedurze) obsługi zdarzeń (event function lub event procedure), będącej wydzieloną częścią kodu.

Maska zdarzenia (ang. event mask) utożsamiana jest z typową maską bitową, która może być wykorzystywana do rozpoznawania lub filtrowania jednego lub więcej komunikatów, będących jedną z metod reakcji obiektu na wystąpienie szczególnego zdarzenia.

Segment wysyłający komunikaty

Najwyższy czas, abyśmy zapoznali się z jednym ze sposobów nawiązywnia komunikacji z urządzeniem zewnętrznym. Ponieważ nie wiem, czy Czytelnik podejmie próbę komunikacji z innym komputerem, czy też z jakimś konkretnym przyrządem, przedstawię tu metodę ogólną. Dobrym zwyczajem każdego programisty, a już szczególnie osoby stojącej przed problemem nawiązania komunikacji z jakimś urządzeniem za pośrednictwem interfejsów szeregowych jest to, by zawczasu przygotować się na trudności. Otóż istnieje pewna klasa przyrządów, może już nie najnowszych ale jeszcze wykorzystywanych, które mają pewną ciekawą właściwość. Nie można nawiązać z nimi kontaktu, dopóki komputer nie wyśle do urządzenia sygnału: żądanie nadawania. Innymi słowy trzeba uaktywnić linię RTS. Podobnie może też być w przypadku linii DTR (patrz rozdz. 2). Trudności napotkamy też, gdy będziemy musieli zastosować programową kontrolę przepływu danych z wykorzystaniem protokołu XON-XOFF. Może się również zdarzyć, że z jakiś względów będziemy chcieli nagle przerwać transmisję i zechcemy użyć popularnego przycisku Reset. Problemy te można rozwiązać, uzyskując dostęp do rejestrów układu UART. Jednak w Windows nie musimy tego robić. API daje nam gotową funkcję, którą należy tylko umiejętnie wykorzystać:

BOOL EscapeCommFunction(HANDLE hCommDev, DWORD dwFunc);

gdzie dwFunc możemy przypisać jedną ze stałych symbolicznych:

CLRDTR — linia DTR przechodzi w stan nieaktywny;

CLRRTS — linia RTS przechodzi w stan nieaktywny;

SETDTR — stan aktywny linii DTR;

SETRTS — stan aktywny linii RTS;

SETXOFF — wstrzymanie transmisji po wystąpieniu znaku sterującego XOFF;

SETXON — przywrócenie transmisji po otrzymaniu znaku sterującego XON;

SETBREAK — wstrzymanie transmisji (dane znajdujące się w buforze wyjściowym a

jeszcze nie wysłane nie są tracone);

CLRBREAK — przywrócenie transmisji znaków.

I w tym wypadku predefiniowane stałe reprezentują pewne maski bitowe, których bezpośrednie użycie lub wykorzystanie ich rozsądnych kombinacji może być bardzo pomocne w sterowaniu komunikacją.

maska

wartość

bit 4

bit 3

bit 2

bit 1

bit 0

SETXOFF

1

0

0

0

0

1

SETXON

2

0

0

0

1

0

SETRTS

3

0

0

0

1

1

CLRRTS

4

0

0

1

0

0

SETDTR

5

0

0

1

0

1

CLRDTR

6

0

0

1

1

0

SETBREAK

8

0

1

0

0

0

CLRBREAK

9

0

1

0

0

1

Większość nowoczesnych przyrządów, z którymi komunikujemy się za pośrednictwem interfejsów szeregowych, nie wymaga już sprawdzania linii RTS i DTR czy stosowania specjalnych protokołów transmisyjnych. Urządzenia te są już same w sobie komputerami. Dlatego konieczność wywoływania EscapeCommFunction() jest w większości przypadków powodowana potrzebą wstrzymania na jakiś czas transmisji (bez utraty danych). API dostarcza dwóch prostszych w użyciu funkcji, których efekt działania jest identyczny jak przy zastosowaniu EscapeCommFunction(), wywołanej odpowiednio z SETBREAK lub CLRBREAK:

BOOL SetCommBreak(HANDLE hCommDev);

oraz

BOOL ClearCommBreak(HANDLE hCommDev);

Zasadniczą częścią segmentu wysyłającego komunikaty do portu szeregowego będzie zdefiniowana w Win32 API funkcja:

BOOL WriteFile(HANDLE hCommDev,

LPCVOID lpBuffer,

DWORD nNumberOfBytesToWrite,

LPDWORD lpNumberOfBytesWritten,

LPOVERLAPPED lpOverlapped);

Ogólnie rzecz biorąc, może ona zapisywać dane do dowolnego urządzenia (w tym również pliku) jednoznacznie wskazanego przez identyfikator hCommDev. W przypadku transmisji szeregowej może być z powodzeniem stosowana zarówno do jej wariantu synchronicznego jak i asynchronicznego. Funkcja ta zapisuje dane do obszaru pamięci (bufora danych) identyfikowanego przez wskaźnik lpBuffer. LPCVOID lpBuffer odpowiada klasycznej deklaracji wskaźnika ogólnego (adresowego) stałej, czyli: const void *Buffer. Rozmiar bufora ustala się w zależności od potrzeb, zasobów pamięci komputera oraz pojemności bufora danych urządzenia zewnętrznego. Następny parametr nNumberOfBytesToWrite określa liczbę bajtów do wysłania, zaś wskaźnik lpNumberOfBytesWritten wskazuje liczbę bajtów realnie wysłanych. Aby nie doszło do przekroczenia rozmiaru bufora danych wyjściowych, liczba bajtów faktycznie wysłanych może być mniejsza niż nNumberOfBytesToWrite, dlatego funkcja umieszcza ją w zmiennej lpNumberOfBytesWritten stanowiącej przedostatni parametr. W ten sposób działa mechanizm ochrony dla wysyłanych danych. Ostatni parametr lpOverlapped jest wskaźnikiem struktury OVERLAPPED. Zawiera ona informacje o dodatkowych metodach kontroli transmisji, polegających na sygnalizowaniu aktualnego położenia pozycji wskaźnika transmitowanego pliku. Większość elementów tej struktury jest zarezerwowana przez system operacyjny. Jeżeli jednak chcielibyśmy skorzystać z jej usług należałoby w funkcji CreateFile() parametrowi dwFlagsAndAttributes przypisać znacznik FILE_FLAG_OVERLAPPED (tzw. nakładane wejście-wyjście). Trzeba wówczas samodzielnie (niemalże „ręcznie”) sygnalizować pozycję pliku w pamięci. Na potrzeby naszych rozważań nie ma to większego sensu, dlatego bez jakichkolwiek wyrzutów sumienia wskaźnik lpOverlapped zignorujemy, przypisując mu NULL. Zakładając, że celem naszym jest wysłanie tylko jednego znaku lub ciągu znaków, np. komunikatu czy nawet pliku, rozważania na temat konstrukcji segmentu wysyłającego moglibyśmy już w zasadzie zakończyć. Zastanówmy się jednak, co się stanie, jeżeli będziemy musieli za pomocą naszej aplikacji skierować do portu szeregowego dwa (lub więcej) następujące po sobie pojedyncze znaki w krótkim czasie, ale tak by odbiornik nie potraktował ich jako jednego ciągu. Zgoda odbiornika na przyjęcie kolejnego komunikatu może, ale nie musi być wymagana. Mówiąc krótko, odbiornik będzie musiał rozróżnić wysyłane znaki zarówno pod względem treści jak również czasu przybycia. Można próbować różnych sposobów, takich jak sztuczne wstrzymywanie procesu po wysłaniu pierwszego znaku czy chociażby użycie komponentu TTimer. Jest jednak prostszy sposób — możemy skorzystać z funkcji API:

BOOL WaitCommEvent(HANDLE hCommDev,

LPDWORD lpfdwEvtMask,

LPOVERLAPPED lpOverlapped);

Wartości, na jakie może wskazywać lpfdwEvtMask, są formalnie identyczne z opisanymi wcześniej dla funkcji SetCommMask(), zaś wskaźnikowi do struktury OVERLAPPED należy po prostu przypisać NULL z powodu, który został już omówiony przy okazji prezentacji WriteFile(). Nietrudno zauważyć, że użycie w programie funkcji WaitCommEvent() będzie miało sens o tyle, o ile wcześniej (np. w warstwie konfiguracyjnej) użyjemy SetCommMask(). Aby uzyskać informację np. o tym, że ostatni znak z bufora komunikacyjnego został wysłany, a sygnał na linii CTS zmienił poziom (lub nastąpiło inne zaprogramowane zdarzenie) i można przystąpić do dalszej transmisji, w warstwie konfiguracyjnej aplikacji należy zapisać:

//--otwarcie portu i ustawienie jego parametrów--------

void __fastcall TForm1::OpenCommClick(TObject *Sender)

{ ...

DWORD fdwEvtMask;

...

GetCommMask(hCommDev, &fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY | EV_CTS);

...

}

Zwróćmy w tym miejscu uwagę na bardzo ważną rzecz, mianowicie na sposób, w jaki zainicjowaliśmy funkcję SetCommMask(). Alternatywnym sposobem przypisania parametrowi wejściowemu fdwEvtMask wartości EV_TXEMPTY lub np. EV_CTS będzie następująca konstrukcja:

//--otwarcie portu i ustawienie jego parametrów--------

void __fastcall TForm1::OpenCommClick(TObject *Sender)

{ ...

DWORD &fdwEvtMask = (EV_TXEMPTY | EV_CTS);

SetCommMask(hCommDev, fdwEvtMask);

...

}

Mówimy, że parametry wejściowe typu fdwEvtMask w pewnych wypadkach mogą być traktowane jako zmienne referencyjne. Przekazanie argumentu do funkcji za pomocą zmiennej referencyjnej, krótko — referencji (ang. refernce oznacza również wskazanie, odniesienie) jest w istocie zbliżone do przekazania wskaźnika do jej argumentu. Referencja jest specjalnym typem wskaźnika — umożliwia traktowanie wskaźnika jako regularnego obiektu. Zainicjowanie referencji jest natychmiastowe i następuje w momencie wywołania funkcji. Polega na błyskawicznym przekazaniu jej wybranych przez nas argumentów. Należy jednak zwrócić uwagę, że powyższy sposób inicjowania referencji w pewnych przypadkach może zostać potraktowany przez kompilator jako tymczasowy i otrzymamy informację:

[C++ Warning] RS_03.cpp(140): W8028 Temporary used to initialize

'fdwEvtMask'

Jeżeli jednak korzystamy z funkcji API, nie musimy się tym zbytnio przejmować.

Wywołanie WaitCommEvent() w segmencie wysyłającym można zrealizować w następujący sposób:

//----------------wysyłanie komunikatów---------

{ ...

// wyślij komunikat funkcją WriteFile()

...

if (WaitCommEvent(hCommDev, &fdwEvtMask, NULL) > 0) // sprawdza

//rezultat wykonania funkcji

{ ... // wyświetl komunikat

return TRUE;

}

else

return FALSE;

...}

Analizując powyższe zapisy, rozumiemy już, dlaczego wywoływany w funkcjach SetCommMask(), GetCommMask() oraz WaitCommEvent() parametr fdwEvtMask musi być typu DWORD, chociaż — jak pamiętamy — oryginalnie deklarowany w tej funkcji jest wskaźnik lpfdwEvtMask. W przyszłości fdwEvtMask uczynimy parametrem globalnym i będzie on traktowany jako zmienna referencyjna w całej aplikacji. Ważne jest, że jeżeli fdwEvtMask „potraktujemy” operatorem adresowym &, spowodujemy zainicjowanie referencji podczas wywołania funkcji, ale tylko wówczas, gdy będzie ona w stanie zwrócić nam jakąś wartość. W ten sam sposób możemy oczywiście naszą aplikacje „wyczulić” na wszystkie udostępnione przez fdwEvtMask zdarzenia lub ich kombinacje.

I ZASADA KORZYSTANIA Z API

Jeżeli w oryginalnej postaci funkcji występuje wskaźnik, np. LPDWORD lpXx (DWORD *Xx), LPVOID lpXx (void *Xx), LPCVOID lpXx (const void *Xx), to w przypadku wywołania funkcji w programie parametry, na które będzie on wskazywać, powinny być zainicjowane operatorem &. W nawiasach podano odpowiednie, tradycyjnie stosowane oznaczenia.

Przykład:

Oryginalna deklaracja w Win32 API:

BOOL WriteFile(...,LPDWORD lpNumberOfBytesWritten, ... );

Z zapisu LPDWORD lpNumberOfBytesWritten odczytamy:

lpNumberOfBytesWritten jest wskaźnikiem i będzie wskazywać na dane typu DWORD.

Wywołanie:

DWORD NumberOfBytesWritten;

WriteFile( ..., &NumberOfBytesWritten ,...);

Zmienna lub zmienne przekazywane w ten sposób do funkcji, np. WriteFile() muszą być poprzedzone operatorem &, tak aby mogły być utworzone odpowiednie wskaźniki. Przy wykorzystaniu API w ten sposób dokonuje się wywołanie funkcji z przekazywaniem argumentów przez adres.

Konstrukcja segmentu wysyłającego komunikaty będzie jedną z prostszych w naszej aplikacji. Odzwierciedla jednak ogólną metodę projektowania tego typu programów, właściwą dla Win32 API. W naszej aplikacji segment ten przybierze postać funkcji, do której będziemy mogli się wielokrotnie odwoływać i pobierać z niej tylko naprawdę potrzebne informacje. Jeżeli dokładnie przeanalizujemy rolę wszystkich parametrów występujących w funkcji WriteFile(), musimy dojść do wniosku, że tylko trzy pierwsze (czyli hCommDev — identyfikator portu, lpBuffer — wskaźnik bufora danych, nNumberOfBytesToWrite — liczba bajtów do wysłania) są naprawdę istotne, gdyż o liczbę bajtów faktycznie wysłanych zatroszczą się już mechanizmy ochrony dla danych wysyłanych, zaimplementowane w funkcji WriteFile(). Kod naszej funkcji Write_Comm() zapisującej dane do portu będzie mógł przyjąć następującą uproszczoną postać:

int __fastcall Write_Comm(HANDLE hCommDev, LPCVOID lpBuffer,

DWORD nNumberOfBytesToWrite)

{

DWORD NumberOfBytesWritten;

if (WriteFile(hCommDev, lpBuffer, nNumberOfBytesToWrite,

&NumberOfBytesWritten, NULL) > 0)

{

WaitCommEvent(hCommDev, &fdwEvtMask, NULL);

return TRUE;

}

else

return FALSE;

}

Zwróćmy jeszcze uwagę, że nagłówek tej funkcji mógłby być równie dobrze zapisany tradycyjnie:

int __fastcall Write_Comm(HANDLE hCommDev, const void *Buffer,

DWORD nNumberOfBytesToWrite)

lub nawet jako:

int __fastcall Write_Comm(..., void *Buffer, ...)

// LPVOID lpBuffer

Deklarując wskaźnik bufora reprezentującego pewien obszar pamięci operacyjnej przy wysyłaniu danych, wcale nie musimy wskazywać na jakiś stały obszar tej pamięci, chociaż API właśnie to sugeruje. Pamiętajmy, że operujemy na C-łańcuchach. Jeżeli w buforze wyjściowym znajdzie się NUL bajt (nazywany niekiedy zerowym ogranicznikiem) kończący łańcuch, nadawanie zostanie przerwane. Nieco inaczej będzie to wyglądało w przypadku danych odbieranych, co zostanie omówione za chwilę.

PRZYPOMNIJMY

Dane typu LPVOID (void *), będąc wskazaniami adresowymi, nie wskazują na obiekty. Lokalizują jedynie pewne obszary pamięci operacyjnej.

Pokazany sposób zapisu funkcji wysyłającej komunikaty do łącza szeregowego nie jest oczywiście jedynym z możliwych. Można ją zapisać w sposób maksymalnie uproszczony, jedynie z dwoma parametrami formalnymi, które tak naprawdę są dla nas najistotniejsze. Będą nimi: identyfikator portu oraz liczba bajtów do wysłania:

char Buffer_O[cbOutQueue]; // bufor danych wyjściowych

...

int __fastcall Write_Comm(HANDLE hCommDev,

DWORD nNumberOfBytesToWrite)

{

...

if (WriteFile(hCommDev, &Buffer_O[0],

nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL) > 0)

{

...

return TRUE;

}

else

return FALSE;

}

Jeżeli zdecydujemy się na taki zapis, wówczas jednym z parametrów wywoływanej funkcji API WriteFile() musi być jawnie użyty bufor danych wyjściowych Buffer_O. Standardowo argumenty funkcji w C++ przekazujemy przez wartość. W ten sposób powodujemy utworzenie kopii argumentu w wywoływanej funkcji, zapobiegając tym samym możliwości zmodyfikowania jego początkowej wartości. W przypadku naszej funkcji Write_Comm() będzie to nNumberOfBytesToWrite, czyli liczba bajtów do wysłania będąca jednocześnie parametrem funkcji Win32 API WriteFile(). Jeżeli z kolei funkcja ma zmodyfikować wartości zmiennych będących jej argumentami, parametry powinny być jawnie zadeklarowane jako wskaźniki.

Jest jeszcze wiele rzeczy, których nie uwzględniliśmy. Naprawdę niezawodna aplikacja powinna mieć parę dodatkowych zabezpieczeń. Wszystkie je omówimy po kolei w następnych podrozdziałach. Obecnie ważne jest dla nas to, że użyliśmy wszystkich omówionych do tej pory funkcji Win32 API, poznaliśmy w jakiej kolejności należy je wywoływać. Po skompletowaniu całego programu przekonasz się, że mimo swojej prostoty będzie on funkcjonalny. Teraz, kiedy poznaliśmy, w jaki sposób można coś „powiedzieć” do urządzenia, czas najwyższy, abyśmy nauczyli się „słuchać” i „rozumieć” jego odpowiedzi.

Segment odbierający komunikaty

DRUGIE PRAWO SODDA:

Wcześniej czy później i tak musi nastąpić najgorszy z możliwych splotów okoliczności.

Uzupełnienie:

Każdy system musi być zaprojektowany w taki sposób, aby stawić czoła najgorszemu z możliwych splotów okoliczności.

Każdy, kto kiedykolwiek interesował się komunikacją komputerową wie, że występowanie błędów w tym procesie jest czymś nieuniknionym i poniekąd naturalnym. Ustalenie sposobu reagowania na wystąpienie błędu w czasie transmisji szeregowej jest zawsze bardzo istotnym elementem aplikacji. Przypominamy sobie z poprzednich rozdziałów, że istnieje pewien sposób zabezpieczenia danych przed zafałszowaniem w czasie ich przekazu. Sposobem tym jest kontrola bitu parzystości. Jest on jednak mało efektywny. Korzystając z zasobów struktury DCB, można w pewnym stopniu zabezpieczyć się przed odbieraniem przekłamanych danych. Wystarczy w części konfiguracyjnej aplikacji odwołać się do jednego ze znaczników DCB, a mianowicie do fAbortOnError (patrz tabela 5. 5.), pisząc:

dcb.fAbortOnError = TRUE;

co spowoduje wstrzymanie wykonywania wszelkich operacji wysyłania i odbierania danych przy wykryciu jakiegokolwiek błędu w komunikacji z portem szeregowym. Reinicjalizacja odbioru i nadawania poprzez port komunikacyjny identyfikowany przez hCommDev nastąpi po wywołaniu funkcji:

BOOL ClearCommError(HANDLE hCommDev,

LPDWORD lpErrors,

LPCOMSTAT lpStat);

gdzie:

lpErrors jest wskaźnikiem do 32-bitowej danej typu DWORD, reprezentującej jeden z typów błędów:

CE_BREAK — wykryto przerwanie połączenia.

CE_DNS — urządzenie przyłączone do łącza równoległego nie zostało określone (Win 9x).

CE_FRAME — wystąpił błąd protokołu ramki danych.

CE_IOE — podczas komunikacji nastąpił jakiś błąd wejścia-wyjścia ( Input-Output).

CE_MODE — żądanie nadawania nie jest podtrzymywane lub identyfikator portu

(urządzenia) ma błędną specyfikację.

CE_OOP — urządzenie przyłączone do łącza równoległego sygnalizuje brak papieru, co jest

typowe dla drukarek, faksów, niektórych faksmodemów (Win 9x).

CE_OVERRUN — nastąpiło całkowite wypełnienie wejściowego bufora danych. Następny

znak będzie zignorowany.

CE_PTO — został przekroczony czas oczekiwania na połączenie z portem równoległym, tzw.

przeterminowanie czasu połączenia (Win 9x).

CE_RXOVER — bufor wejściowy został przepełniony. Albo nie ma w nim już fizycznie

miejsca, albo został odebrany jakiś znak następujący po znaku końca

pliku EOF.

CE_RXPARITY — wykryto błąd niezgodności kontroli parzystości.

CE_TXFULL — próba transmisji znaku przy całkowitym wypełnieniu bufora wyjściowego.

Zbiór wartości, na które wskazuje lpErrors, można z powodzeniem traktować jako maski bitowe z możliwością wykonywania na nich odpowiednich działań. Zawarta tam pełna informacja może być aktualnie zapisana na 16 bitach.

maska

wartość

b

15

b

14

b

13

b

12

b

11

b

10

b

9

b

8

b

7

b

6

b

5

b

4

b

3

B

2

b1

b 0

CE_RXOVER

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

1

CE_OVERRUN

2

0

0

0

0

0

0

0

0

0

0

0

0

0

0

1

0

CE_RXPARITY

4

0

0

0

0

0

0

0

0

0

0

0

0

0

1

0

0

CE_FRAME

8

0

0

0

0

0

0

0

0

0

0

0

0

1

0

0

0

CE_BREAK

16

0

0

0

0

0

0

0

0

0

0

0

1

0

0

0

0

CE_TXFULL

256

0

0

0

0

0

0

0

1

0

0

0

0

0

0

0

0

CE_PTO

512

0

0

0

0

0

0

1

0

0

0

0

0

0

0

0

0

CE_IOE

1024

0

0

0

0

0

1

0

0

0

0

0

0

0

0

0

0

CE_DNS

2048

0

0

0

0

1

0

0

0

0

0

0

0

0

0

0

0

CE_OOP

4096

0

0

0

1

0

0

0

0

0

0

0

0

0

0

0

0

CE_MODE

32768

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

Widzimy więc, że funkcja ClearCommError() jest przydatna nie tylko do obsługi portu szeregowego. W programie sterującym łączem szeregowym można ją wywołać jedynie z parametrami domyślnymi. Będzie wówczas usuwała, w miarę swojej możliwości, wszystkie ewentualne błędy pojawiające się w trakcie transmisji.

ClearCommError(hCommDev, &Errors, &Stat);

Wskaźnik lpStat wskazuje na strukturę COMSTAT. Chcąc wyjaśnić jego znaczenie, musimy odwołać się do tej struktury będącej również częścią Win32 API. Zawiera ona informacje o aktualnych zasobach i dodatkowych sposobach kontroli wybranego łącza szeregowego. Tabela 5.9 przedstawia znaczenie zawartej tam informacji.

Tabela 5.9.

Informacje zawarte w elementach struktury COMSTAT

Typ Element struktury Właściwości Wartość zwracana ,

Znaczenie

DWORD fCtsHold Określa, czy transmisja jest TRUE / 1

wstrzymywana do czasu Transmisja jest wstrzymana.

odebrania przez komputer FALSE / 0

sygnału CTS. Transmisja nie jest wstrzymywana.

DWORD fDsrHold Określa, czy transmisja jest TRUE / 1

wstrzymywana do czasu Transmisja jest wstrzymana.

odebrania przez komputer FALSE / 0

sygnału DSR. Transmisja nie jest wstrzymywana.

DWORD fRlsdHold Określa, czy transmisja jest TRUE / 1

wstrzymywana do czasu załączenia Transmisja jest wstrzymana.

stanu aktywnego na linii DCD. FALSE / 0

Linia DCD oznacza też RLSD Transmisja nie jest wstrzymywana.

(Received Line Signal Detect).

DWORD fXoffHold Określa, czy transmisja jest TRUE / 1

wstrzymywana po odebraniu Transmisja jest wstrzymana.

znaku sterującego XOFF. FALSE / 0

Transmisja nie jest wstrzymywana.

DWORD fXoffSent Określa, czy transmisja jest TRUE / 1

wstrzymywana po wysłaniu Transmisja jest wstrzymana.

znaku sterującego XOFF. Następnym wysłanym

znakiem będzie XON, bez

względu na aktualnie

transmitowany znak.

FALSE / 0

Transmisja nie jest wstrzymywana.

DWORD fEof Określa, czy został wykryty TRUE / 1

znacznik końca pliku EOF. Odebrano znak EOF.

--> FALSE / 0[Author:kdh]

DWORD fTxim sterowanie transmisją TRUE / 1

Jeżeli w buforze znajduje się znak

wysyłany za pomocą funkcji

TransmitCommChar(), to ma

on pierwszeństwo przed innymi,

już znajdującymi się w buforze

wyjściowym.

FALSE / 0

Znak wysyłany funkcją

TransmitCommChar() nie będzie

miał pierwszeństwa przed innymi.

DWORD fReserved zarezerwowane, nie używane

DWORD cbInQue liczba bajtów danych otrzymanych należy je odczytać

w wyniku transmisji szeregowej,

ale jeszcze nie przeczytanych

DWORD cbOutQue liczba bajtów danych pozostających do należy je odczytać

wysłania

Większość pól COMSTAT to pola jednobitowe. Wyjątkiem jest dwudziestopięciobitowe, obecnie nie używane pole fReserved. W Win32 API struktura ta deklarowana jest w sposób następujący:

typedef struct _COMSTAT {

DWORD fCtsHold : 1;

...

} COMSTAT, *LPCOMSTAT;

Deklaracja ta tworzy dwa nowe słowa kluczowe typu COMSTAT (struktura) i LPCOMSTAT (wskaźnik do struktury).

Tak naprawdę na tym etapie rozważań będzie nam potrzebna tylko jedna ze zmiennych oferowanych przez COMSTAT. Ale zanim pokażemy, jak ją optymalnie wykorzystać, przypatrzmy się funkcji:

BOOL ReadFile(HANDLE hCommDev,

LPVOID lpBuffer,

DWORD nNumberOfBytesToRead,

LPDWORD lpNumberOfBytesRead,

LPOVERLAPPED lpOverlapped);

Użycie jej w programie zapewni nam odczytanie wszelkich danych przychodzących do łącza szeregowego identyfikowanego przez hCommDev. Można ją stosować zarówno do wariantu transmisji synchronicznej jak i asynchronicznej. lpBuffer jest dobrze nam znanym wskaźnikiem do bufora danych, przez który będziemy odczytywać wszelkie informacje. nNumberOfBytesToRead określa liczbę bajtów do odebrania, zaś lpNumberOfBytesRead będzie wskazywać na liczbę bajtów rzeczywiście odebranych. Aby nie dopuścić do przekroczenia rozmiaru bufora danych wejściowych, liczba bajtów faktycznie odebranych może być mniejsza niż nNumberOfBytesToRead, dlatego funkcja umieszcza ją w zmiennej lpNumberOfBytesRead, stanowiącej przedostatni parametr. W ten sposób działa mechanizm ochrony dla danych odbieranych. Wskaźnik lpOverlapped, tak jak poprzednio w funkcji WriteFile(), zignorujemy (NULL).

Win32 operacje wejścia-wyjścia realizuje za pomocą czytania z plików lub pisania do plików. Wszystkie urządzenia zewnętrzne, łącznie z końcówką użytkownika traktowane są jako pliki wchodzące w skład systemu plików. Komunikacja programu z urządzeniami zewnętrznymi realizowana jest poprzez jednorodny, wspólny aparat systemu plików, którego najbardziej podstawowymi funkcjami są: CreateFile(), ReadFile() oraz WriteFile().

Ktoś mógłby zapytać: no dobrze, ale skąd będę wiedział ile bajtów mam przeczytać? Czy nadający będzie musiał mnie o tym za każdym razem informować? Odpowiedzi poszukajmy, śledząc kod zaprojektowanej przez nas funkcji Read_Comm():

int __ fastcall Read_Comm(HANDLE hCommDev, LPVOID lpBuffer,

LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)

{

COMSTAT Stat;

DWORD Errors;

DWORD nNumberOfBytesToRead;

ClearCommError(hCommDev, &Errors, &Stat);

if (Stat.cbInQue > 0)

{

if (Stat.cbInQue > Buf_Size)

nNumberOfBytesToRead = Buf_Size;

else

nNumberOfBytesToRead = Stat.cbInQue;

ReadFile(hCommDev, lpBuffer, nNumberOfBytesToRead,

lpNumberOfBytesRead, NULL);

}

else

*lpNumberOfBytesRead = 0;

return TRUE;

}

Jej nagłówek równie dobrze może być zapisany w ten sposób:

int __fastcall Read_Comm(HANDLE hCommDev, void *Buffer,

DWORD *NumberOfBytesRead, DWORD Buf_Size)

Ale na pewno nie tak:

int __ fastcall Read_Comm(..., const void *Buffer, ... , ...)

// LPCVOID lpBuffer

Nie należy w deklaracji funkcji odczytującej dane wskazywać na jakiś stały obszar pamięci reprezentowany przez bufor danych. Musi on mieć możliwość elastycznego dostosowywania się do liczby bajtów przychodzących do łącza. Na tym prostym przykładzie widzimy, jak pożyteczna okazała się znajomość struktury COMSTAT. Już nie musimy ciągle monitorować zawartości bufora. Jeśli wykorzystamy własność elementu cbInQue (count bytes input queue — por. tab. 5.2), odczyt danych będzie bardzo prosty. nNumberOfBytesToRead automatycznie dostosuje się do rozmiaru danych w buforze. Maksymalny dopuszczalny rozmiar bufora danych zostanie przekazany funkcji poprzez parametr Buf_Size. W przypadku, kiedy w buforze nie będzie żadnego znaku do odebrania (Stat.cbInQue = 0), należy wskaźnikowi lpNumberOfBytesRead przypisać 0. Jeżeli natomiast nie będziemy chcieli skorzystać z usług COMSTAT, w funkcji ClearCommError() wskaźnikowi lpStat wystarczy przypisać NULL.

Zauważmy, że podobnie jak w przypadku wysyłania komunikatów, również funkcja odczytująca dane pojawiające się w łączu szeregowym może być zapisana w prostszy sposób z trzema lub nawet dwoma parametrami formalnymi, co już w pełni będzie usprawiedliwiało użycie konwencji przekazywania parametrów __fastcall.

Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy

...

int __fastcall Read_Comm(HANDLE hCommDev,

LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)

{

...

{

...

ReadFile(hCommDev, &Buffer_I[0], nNumberOfBytesToRead,

lpNumberOfBytesRead, NULL);

}

else

*lpNumberOfBytesRead = 0;

return TRUE;

}

Korzystając z tego zapisu w wywołaniu funkcji API ReadFile(), należy jawnie odwołać się do bufora danych wejściowych Buffer_I.

Używając w deklaracji funkcji konwencji __fastcall należy się spodziewać, że trzy pierwsze parametry funkcji mogą być umieszczone w rejestrach EAX, EDX oraz ECX (jeżeli oczywiście jest to możliwe). Parametry 8-bitowe typu char (signed oraz unsigned) mogą być umieszczane w AL, DL, CL, parametry 16-bitowe typu short (signed oraz unsigned) w AX, DX, CX, zaś 32-bitowe typu int/long (signed oraz unsigned) w rejestrach EAX, EDX, ECX — zob. rozdział 4.

Rejestry nie będą używane, jeżeli parametrami funkcji będą dane zmiennopozycyjne lub struktury. Parametry tego typu są odkładane na stosie.

Na zakończenie tej części naszych rozważań celowym będzie skomentowanie faktu umieszczenia funkcji ClearCommError() w segmencie odbierającym komunikaty przychodzące do portu szeregowego. Zapewne nie ma wśród nas nikogo, kto nie rozegrałby kiedykolwiek meczu piłkarskiego. Zawsze w lepszej sytuacji jest zawodnik podający piłkę, odbierający musi bardzo uważać, żeby dokładnie ją przyjąć i dalej rozegrać. Dokładnie tak samo jest przy komunikacji komputerowej. Wiem z własnego doświadczenia, że 90% błędów powstaje niestety po stronie odbierającego dane. Bardzo łatwo można się przekonać, że funkcja Read_Comm() bez ClearCommError() w wielu przypadkach po prostu by nie działała! Używaj jej zawsze, nawet jeżeli podstawiłeś dcb.fAbortOnError = FALSE (patrz tabela 5.5).

Przykładowa aplikacja

Zanim przejdziemy do bardziej ambitnych rozważań, zapoznamy się z ogólną metodą konstruowania w C++Builderze algorytmów pomocnych w realizacji transmisji szeregowej. Kompletnym przykładem aplikacji wykorzystującej skonstruowane przez nas funkcje zapisu i odczytu danych będzie projekt \KODY\BUILDER\RS_03\p_RS_03.bpr. Działanie aplikacji będzie polegało na wysłaniu odpowiedniego komunikatu do przyrządu pomiarowego oraz wyświetleniu i zapisaniu na dysku odpowiedzi. Z czysto praktycznych względów zastosujemy tu najprostszą metodę zapisu danych do pliku. Wygląd głównego formularza oraz jego kod RS_03.cpp przedstawione są poniżej.

Rysunek 5.4. Formularz główny projektu p_RS_03.bpr

0x01 graphic

Do jego zaprojektowania wykorzystałem pięć komponentów typu TCheckBox, za pomocą których można wybrać prędkość transmisji oraz numer portu szeregowego. W ten sam sposób można wzbogacić aplikację o możliwość wyboru parzystości, bitów stopu czy rozmiaru bitów danych. Wizualizacja odbieranych komunikatów będzie możliwa dzięki zastosowaniu komponentu typu TEdit. Obsługę zdarzeń polegających na otwarciu portu do transmisji, wysłaniu i odebraniu danych oraz zamknięciu portu zapewniają komponenty TButton. Z przyciskiem Wyślij skojarzona będzie funkcja obsługi zdarzenia SendClick(), w którym wywoływane będą nasze funkcje Write_Comm() oraz Read_Comm(). Pozostałe przyciski pełnią taką samą rolę jak w przypadku programu testującego łącze. Za pomocą przedstawionego niżej programu testowałem transmisję z pewnym przyrządem zwanym kontrolerem temperatury. Wysłałem do miernika zapytanie o jego identyfikację (ID). Każdy nowoczesny przyrząd pomiarowy powinien nam się przedstawić. Większość z nich, niezależnie od przeznaczenia i firmy, w której zostały wyprodukowane, robi to w odpowiedzi na standardową komendę - zapytanie: *IDN?Identification query, podając nazwę producenta, numer fabryczny i kolejny numer modelu. Również zapytanie np. o aktualnie mierzoną temperaturę (lub inną wartość) jest standardowe: CDAT? Jeżeli jednak Czytelnik nie ma takiego urządzenia, program ten można testować, łącząc się z innym komputerem. Wówczas wskaźnik query może wskazywać na dowolny ciąg znaków, nie dłuższy oczywiście niż zadeklarowany obszar pamięci (bufor danych). W tym przykładzie zadeklarowałem nieco przesadnie bufor danych o rozmiarze 64 bajtów zarówno do nadawania jak i odbioru. Ogólnie rzecz biorąc liczba przesyłanych bajtów może być całkowicie dowolna. Najczęściej używanymi są: 1, oznaczające przesłanie jednego znaku (nie buforowane) oraz 2, 8, 16, 32,..., 512, 1024 i 2048, co odpowiada fizycznemu rozmiarowi bloku danych akceptowanemu przez większość nowoczesnych urządzeń zewnętrznych. Ciąg znaków wskazanych przez query zostanie skopiowany do obszaru pamięci, którego pierwszy znak jest wskazany przez bufor danych wyjściowych Buffer_O. Czynność ta zostanie wykonana za pomocą znanej funkcji strcpy(Buffer_O, query). Rezultatem będzie dana typu char *, wskazująca pierwszy znak obszaru pamięci, do którego wykonano kopiowanie.

Wydruk 5.3. Kod formularza aplikacji realizującej transmisję szeregową

//----RS_03.cpp-------------

//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------

#include <vcl.h>

#include <stdio.h>

#pragma hdrstop

#include "RS_03.h"

#pragma package(smart_init)

#pragma resource "*.dfm"

#define cbOutQueue 64 //rozmiar bufora danych wyjściowych

#define cbInQueue 64 //rozmiar bufora danych wejściowych

TForm1 *Form1;

LPCTSTR query = "*IDN?"; // przykładowe zapytanie

//unsigned const char *query = "*IDN?";

char Buffer_O[cbOutQueue]; // bufor danych wyjściowych

char Buffer_I[cbInQueue]; // bufor danych wejściowych

DWORD Number_Bytes_Read; // Number Bytes to Read — liczba bajtów

// do czytania

HANDLE hCommDev; // identyfikator portu

LPCTSTR lpFileName; // wskaźnik do nazwy portu

DCB dcb; // struktura kontroli portu szeregowego

DWORD fdwEvtMask; // informacja o aktualnym stanie transmisji

COMSTAT Stat; // dodatkowa informacja o zasobach portu

DWORD Errors; // reprezentuje typ ewentualnego błędu

//----------------zamyka port-----------------------------------------

int __fastcall Close_Comm(HANDLE hCommDev)

{

CloseHandle(hCommDev);

return TRUE;

}

//----------------wysłanie danych-------------------------------------

int __fastcall Write_Comm(HANDLE hCommDev, LPCVOID lpBuffer,

DWORD nNumberOfBytesToWrite)

{

DWORD NumberOfBytesWritten;

EscapeCommFunction(hCommDev, SETRTS);

if (WriteFile(hCommDev, lpBuffer,

nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL) > 0)

{

WaitCommEvent(hCommDev, &fdwEvtMask, NULL);

EscapeCommFunction(hCommDev, CLRRTS);

return TRUE;

}

else

return FALSE;

}

//-------------------odczyt danych--------------------------------

int __fastcall Read_Comm(HANDLE hCommDev, LPVOID lpBuffer,

LPDWORD lpNumberOfBytesRead, DWORD Buf_Size)

{

DWORD nNumberOfBytesToRead;

ClearCommError(hCommDev, &Errors, &Stat);

if (Stat.cbInQue > 0)

{

if (Stat.cbInQue > Buf_Size)

nNumberOfBytesToRead = Buf_Size;

else

nNumberOfBytesToRead = Stat.cbInQue;

ReadFile(hCommDev, lpBuffer, nNumberOfBytesToRead,

lpNumberOfBytesRead, NULL);

}

else

*lpNumberOfBytesRead = 0;

return TRUE;

}

//--------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

}

//---------zamknięcie portu i aplikacji-------------------------------

void __fastcall TForm1::CloseCommClick(TObject *Sender)

{

Close_Comm(hCommDev);

Application->Terminate();

}

//---------otwarcie portu do transmisji-------------------------------

void __fastcall TForm1::OpenCommClick(TObject *Sender)

{

if (CheckBox1->Checked == TRUE) // wybór portu

lpFileName="COM1";

if (CheckBox2->Checked == TRUE)

lpFileName="COM2";

hCommDev = CreateFile(lpFileName, GENERIC_READ | GENERIC_WRITE,

0, NULL, OPEN_EXISTING, 0, NULL);

if (hCommDev != INVALID_HANDLE_VALUE) // sprawdza, czy port jest

// otwarty prawidłowo

{

SetupComm(hCommDev, cbInQueue, cbOutQueue);

dcb.DCBlength = sizeof(dcb); // aktualny rozmiar

// struktury DCB

GetCommState(hCommDev, &dcb);

if (CheckBox3->Checked == TRUE) // wybór prędkości transmisji

dcb.BaudRate=CBR_300;

if (CheckBox4->Checked == TRUE)

dcb.BaudRate=CBR_1200;

if (CheckBox5->Checked == TRUE)

dcb.BaudRate=CBR_9600;

//--parametry komunikacyjne-------

dcb.Parity = ODDPARITY; // ustawienie parzystości

dcb.StopBits = ONESTOPBIT; // bity stopu

dcb.ByteSize = 7; // bity danych

//--przykładowe ustawienia znaczników sterujących DCB----

dcb.fParity = TRUE; // sprawdzanie parzystości

dcb.fDtrControl = DTR_CONTROL_ENABLE; // sygnał DTR stale

// aktywny

dcb.fRtsControl = RTS_CONTROL_DISABLE;// RTS — stan

// nieaktywny

dcb.fOutxCtsFlow = FALSE;

dcb.fOutxDsrFlow = FALSE;

dcb.fDsrSensitivity = FALSE;

dcb.fAbortOnError = FALSE;

dcb.fOutX = FALSE;

dcb.fInX = FALSE;

dcb.fErrorChar = FALSE;

dcb.fNull = FALSE;

SetCommState(hCommDev, &dcb);

GetCommMask(hCommDev, &fdwEvtMask);

SetCommMask(hCommDev, EV_TXEMPTY);

//DWORD &fdwEvtMask = EV_TXEMPTY | EV_CTS;

//SetCommMask(hCommDev, fdwEvtMask);

}

else

{

switch ((int)hCommDev)

{

case IE_BADID: // W przypadku błędnej identyfikacji portu

// BADIDentify pokaż komunikat

MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"

" aktywny.", "Błąd", MB_OK);

break;

};

}

}

//------------------------------------------------------------------

void __fastcall TForm1::SendClick(TObject *Sender)

{

FILE *pstream; // wskaźnik do pliku

if (hCommDev > 0) // powtórnie sprawdza czy port jest otwarty

{

strcpy(Buffer_O, query);

Write_Comm(hCommDev, Buffer_O, strlen(Buffer_O));

Sleep(1000); // charakterystyczne opóźnienie sprzętowe

FlushFileBuffers(hCommDev);

Read_Comm(hCommDev, &Buffer_I[0], &Number_Bytes_Read,

sizeof(Buffer_I));

if (Number_Bytes_Read > 0) // jeżeli odebrano jakieś bajty

{

pstream = fopen("dane.dat","a"); // otwarcie pliku do zapisu

Edit1->Text = &Buffer_I[0];

//(*Edit1).Text = &Buffer_I[0];

fprintf(pstream, "%s", Edit1->Text);

//fprintf(pstream, "%s", (*Edit1).Text);

fclose(pstream); // zamknięcie pliku

}

}

else

MessageBox(NULL, "Port nie został otwarty do transmisji.",

"Błąd", MB_OK);

}

//--------------------------------------------------------------------

Patrząc na treść funkcji obsługi zdarzenia OpenCommClick(), uważny Czytelnik zapewne dostrzegł, że sygnał DTR uczyniliśmy stale aktywnym w trakcie połączenia. Oczywiście są urządzenia, które tego nie wymagają, ale sygnalizujemy tu pewną ogólną ideę konstrukcji programów komunikacyjnych. Zauważmy też, że sygnał na linii RTS w momencie inicjalizacji portu został ustawiony jako nieaktywny. Zrobiliśmy tak, aby komputer nie sygnalizował urządzeniu od razu zamiaru przekazywania danych. Zamiar ten będziemy każdorazowo sygnalizować wewnątrz funkcji Write_Comm(), jeżeli oczywiście urządzenie tego wymaga. Uczyniliśmy to, wykorzystując właściwości EscapeCommFunction(). Należy jednak pamiętać, że po wysłaniu danego komunikatu sygnał ten trzeba każdorazowo dezaktywować za pomocą tej samej funkcji, tak jak przedstawia to powyższy kod. W ten sam sposób, w zależności od potrzeb, można uaktywniać również inne linie sygnałowe. Można też w odpowiednich miejscach w programie odwoływać się do opisanych wcześniej elementów struktury COMSTAT (fCtsHold, fDsrHold, fRlsdHold). Ale o tym wszystkim musi już zadecydować osoba mająca przed sobą konkretne urządzenie.

Analizując z kolei zapis funkcji obsługi zdarzenia SendClick() dojdziemy do wniosku, że cztery punkty wymagają komentarza.

  1. Użycie funkcji opóźniającej Sleep().

Przy realizacji transmisji pomiędzy dwoma komputerami podtrzymywanie jakiegoś sztucznego opóźnienia pomiędzy wysyłaniem a odbiorem danych nie jest wymagane, rzecz jasna pod warunkiem, że transmitujemy ich stosunkowo niewiele. Jeżeli będziemy przesyłać ciąg znaków reprezentujący większy fragment tekstu, musimy pamiętać, że poszczególne znaki przesyłane będą po kolei (szeregowo) i dopiero w buforze wejściowym zostaną połączone w jedną całość. To z reguły zabiera trochę czasu. Podobnie jest w przypadku urządzeń pomiarowych. Niestety, miernik musi mieć czas na to, by odpowiednio zareagować na komendę, musi też mieć czas na dokonanie pomiaru i przestrojenie się. Charakterystyczny dla niego czas opóźnienia (podawany zazwyczaj w milisekundach) jest zawsze wyspecyfikowany w instrukcji obsługi, którą dostajemy od producenta wraz z przyrządem. Należy przy tym zwrócić baczną uwagę na fakt, że opóźnienie takie bezpośrednio zależy od aktualnej prędkości transmisji. Im mniejsza jest jej prędkość, tym dłużej musimy czekać na odpowiedź urządzenia.

  1. Czyszczenie buforów komunikacyjnych.

Pomiędzy funkcje zapisującymi do portu szeregowego i odczytujące z niego informacje wstawiliśmy funkcję Win32 API, której pełny opis wyglada następująco:

BOOL FlushFileBuffers(HANDLE hCommDev);

Użycie jej w programie spowoduje wyczyszczenie bufora komunikacyjnego. Oznacza to, że wszystkie znajdujące się w nim jeszcze nie wykorzystane dane zostaną przekierowane do portu komunikacyjnego (lub innego urządzenia) jednoznacznie identyfikowanego przez hCommDev, pod warunkiem, że identyfikator ten zostanie zainicjowany z rodzajem dostępu GENERIC_WRITE. Jest to jeden ze sposobów zabezpieczenia się przed odbiorem własnych, wysyłanych komunikatów. Użycie funkcji „przepłukującej” bufor (ang. flush — płukać, przepłukiwać) jest szczególnie pożyteczne w przypadku realizacji transmisji szeregowej pomiędzy dwoma komputerami z wykorzystaniem tego samego bufora komunikacyjnego zarówno do nadawania jak i odbioru danych. Czytelnik może się o tym przekonać, testując program z tylko jednym buforem bez owej funkcji.

Możemy w praktyce spotkać się z sytuacją, w której należy szybko fizycznie usunąć, wykasować jeszcze nie przetransmitowane lub odebrane znaki znajdujące się w buforze wyjściowym lub wejściowym. W tym celu można skorzystać z usług:

BOOL PurgeComm(HANDLE hCommDev, DWORD fdwAction);

gdzie fdwAction można przypisać jedną z własności lub ich kombinację:

PURGE_TXABORT — wszelkie operacje zapisu (transmisji) do portu identyfikowanego przez

hCommDev zostaną natychmiast przerwane, nawet jeżeli nie zostały

zakończone.

PURGE_RXABORT — wszelkie operacje odczytu z portu zostaną natychmiast przerwane, nawet

jeżeli nie zostały zakończone.

PURGE_TXCLEAR — bufor wyjściowy zostanie wyczyszczony; nastąpi skasowanie zawartości.

PURGE_RXCLEAR — bufor wejściowy zostanie wyczyszczony, nastąpi skasowanie zawartości.

I w tym przypadku stałe symboliczne PURGE_ z powodzeniem można potraktować jako swego rodzaju maski bitowe, reprezentujące właściwe pozycje odpowiednich bitów z możliwością wykonywania na nich operacji logicznych.

maska

wartość

bit 4

bit 3

bit 2

bit 1

bit 0

PURGE_TXABORT

1

0

0

0

0

1

PURGE_RXABORT

2

0

0

0

1

0

PURGE_TXCLEAR

4

0

0

1

0

0

PURGE_RXCLEAR

8

0

1

0

0

0

Zauważmy też, że PurgeComm() można używać „profilaktycznie” w różnych częściach aplikacji, zależnie od naszych potrzeb. Należy tylko pamiętać, że wywołanie jej z PURGE_TXCLEAR lub PURGE_TXABORT ma sens jedynie przed odczytem danych, zaś z PURGE_RXCLEAR lub PURGE_RXABORT tylko przed wysłaniem danych.

  1. Wywołania funkcji:

Write_Comm(hCommDev, Buffer_O, strlen(Buffer_O));

Zapisuje ona (wysyła) do portu identyfikowanego przez hCommDev blok pamięci określony przez Buffer_O, o długości strlen(Buffer_O), w którym znajduje się ciąg znaków wskazany przez tę zmienną. Zaś dzięki funkcji:

Read_Comm(hCommDev, &Buffer_I[0], &Number_Bytes_Read,

sizeof(Buffer_I));

odczytujemy do bufora wejściowego Buffer_I blok danych o rozmiarze Number_Bytes_Read, pochodzących z łącza identyfikowanego przez hCommDev. Użycie sizeof(Buffer_I) zapewnia nam jedynie ustalenie górnego ograniczenia rozmiaru bajtów, które spodziewamy się otrzymać w wyniku transmisji szeregowej, jednak stosujemy je ze względów czysto praktycznych. Deklarowany rozmiar bufora zostanie przekazany poprzez Buf_Size do funkcji ReadFile(). Bez tego nie byłoby sensu odwoływać się do elementu cbInQue struktury COMSTAT, gdyż cbInQue nie miałoby punktu odniesienia! Równie dobrze funkcję tę moglibyśmy wywołać następująco:

Read_Comm(hCommDev, Buffer_I, &Number_Bytes_Read,

sizeof(Buffer_I));

Jednak w przypadku jawnego czytania danych z bufora wejściowego do dobrego stylu programowania należy używanie operatora &, dającego adres zmiennej Buffer_I. Zatem pisząc &Buffer_I[0] jawnie odzyskujemy adres początku ciągu znaków znajdujących się w buforze wejściowym. W miarę jak aplikacje zaczną się rozrastać, odrobina asekuranctwa może okazać się niekiedy bardzo pomocna. Zwróćmy też uwagę, że chociaż można mieć co do tego zastrzeżenia, to do pliku dane.dat jawnie zapisaliśmy zawartość komponentu Edit1.

  1. Zastosowane przez nas w powyższym przykładzie konstrukcje instrukcji warunkowych typu:

if (hCommDev > 0)

{

...

if (Number_Bytes_Read > 0)

{

...

można oczywiście znacznie uprościć, pisząc je w postaci:

if (hCommDev)

{

...

if (Number_Bytes_Read)

{

...

Podobną sytuację będziemy mieli, jeżeli skorzystamy z instrukcji warunkowej if przy jawnym sprawdzaniu rezultatu wykonania funkcji API, zwracających wartość TRUE (1) lub FALSE (0). Ogólnie nie jest to wymagane i stosowane przez nas zapisy w większości wypadków można znacznie uprościć, np. zamiast if(WriteFile(...)>0) lub if(WriteFile(...)==TRUE), pisząc po prostu if(WriteFile(...)).W tym oraz dalszych przykładach jawne sprawdzanie podobnych warunków zostało zachowane również w celu utrzymania większej przejrzystości kodu.

Podsumowanie

Czytając niniejszy podrozdział dowiedzieliśmy się już sporo na temat podstawowych sposobów programowej realizacji transmisji szeregowej w środowisku Windows za pomocą C++Buildera. Umiemy już wykorzystywać niektóre struktury oraz funkcje oferowane nam przez Win32 API, obsługujące operacje wejścia-wyjścia portu szeregowego. Dzięki zaznajomieniu się z notacją węgierską znaczenie wskaźników i zmiennych stosowanych w API przestało być dla nas tajemnicą. Poznaliśmy też, w jaki sposób można kontrolować ewentualne błędy występujące w trakcie transmisji oraz jak należy minimalizować ich wpływ na działanie aplikacji. Stworzyliśmy również programy testujące łącze szeregowe oraz inny program realizujący już prawdziwą transmisję. Wszystkie one zostały przedstawione w taki sposób, aby można było je samodzielnie modyfikować i ewentualnie uzupełnieniać. Dzięki temu wiemy, jakie wymagania będą w przyszłości stały przed tego typu aplikacjami.

Ćwiczenia

1. Zmodyfikuj formularz oraz kod programu RS_01.cpp tak, aby można było odczytać kompletną informację o aktualnych ustawieniach wybranego portu szeregowego.

  1. Zmodyfikuj przykładowy program RS_03.cpp w taki sposób, aby można było samodzielnie po jego uruchomieniu określić nazwę pliku przechowującego odebrane dane oraz ścieżkę dostępu do niego. Postaraj się wykorzystać jedynie standardowe funkcje C++ podtrzymywane w Windows oraz komponent typu TEdit.

  2. Na podstawie projektu p_RS_03.bpr stwórz aplikację, w której funkcjom Write_Comm() oraz Read_Comm() będą przyporządkowane oddzielne zdarzenia. Spróbuj samodzielnie zaprojektować właściwe relacje pomiędzy nimi.

Niekiedy spotyka się próby dosłownego tłumaczenia takich nazw i tworzenie konstrukcji podobnych do: wskBlad lub fwsk, co ma symbolizować wskaźnik do danej Blad (czyt. Błąd) czy wskaźnik do file. Moim zdaniem trzymanie się międzynarodowych, ogólnie akceptowanych standardów jest zawsze dobrym zwyczajem.

1 Starsza, ale jeszcze wykorzystywana wersja protokołu stosowanego do projektowania sieci pakietowych i rozległych o przepustowości do 2 Mb/s. Jest stopniowo wypierany przez nowsze technologie Frame Relay, umożliwiające osiągnięcie przepustowości do 45 Mb/s.

1 Miłośnikom modemów specyfikację tych struktur prezentujemy w uzupełnieniu 3.

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

H:\Książki\!Lukasz\RS 232. Praktyczne programowanie\4 po jezykowej\R_5_I.doc

51

Czy nie zapomniano tu podać znaczenia zaznaczonej wartości?



Wyszukiwarka

Podobne podstrony:
Praktyczne programowanie, R 5c-04, Szablon dla tlumaczy
Praktyczne programowanie, R 6-04, Szablon dla tlumaczy
Praktyczne programowanie, R 8-04, Szablon dla tlumaczy
Praktyczne programowanie, R 8-04, Szablon dla tlumaczy
Praktyczne programowanie, R 5b-04, Szablon dla tlumaczy
Praktyczne programowanie, R 1do4-04, Szablon dla tlumaczy
2006 04 GIMP w praktyce [Grafika]
program praktyk dziennik praktyk i plan praktyk, Program praktyk-Technologia zywnosci
04 Nauczanie i Praktykowanie
3TI zadania praktyki, Programowanie
04 instrukcje jezykow programowania
Bluetooth Praktyczne programowanie
3TI info do zadan praktyki, Programowanie
04 Finanse publiczne w programach partii politycznych
RS 232C praktyczne programowanie Od Pascala i C do Delphi i Buildera Wydanie II
2019 04 21 Nowy program rządu dla opiekunów osób niepełnosprawnych Zainteresowanie jest duże Nieza

więcej podobnych podstron