Special Edition




RS 232C

Definicja interfejsu
Będziemy posługiwać się pewnymi pojęciami, których zdefiniowanie pomoże uniknąć w przyszłości niejasności dotyczących użytych sformułowań. Na wstępie zdefiniujmy, co będziemy rozumieli poprzez urządzenia zewnętrzne (peryferyjne) komputera. Będą nimi np.: modemy, skanery, drukarki lub wszelkiego rodzaju przyrządy pomiarowe, dla których komputer jako całość pełnić będzie rolę kontrolera, sterując ich pracą. Dalej będziemy je nazywali po prostu urządzeniami. Bardzo często w praktyce trzeba połączyć z komputerem jakiś konkretny przyrząd, np.: woltomierz, amperomierz, czujnik temperatury, zasilacz wysokiego napięcia, wagę elektroniczną, kasę fiskalną czy nawet inny komputer. Z reguły (nie dotyczy to do komputerów) wymagane przez to urządzenie poziomy prądów i napięć są różne od dostarczanych przez PC. Również szybkość przestrajania się i działania takich przyrządów bywa różna i nie zawsze jest zgodna z szybkością działania PC. Z tego względu niezbędnym jest stosowanie specjalnych układów, zapewniających odpowiednie wzajemne dopasowanie urządzeń i komputera, tzw. interfejsów. Być może niektórzy Czytelnicy znają definicję interfejsu, a ściślej systemu interfejsu, w wersji określonej polską normą PN-83/T-0653. Z jej treści możemy wywnioskować że system interfejsu jest to zbiór niezależnych od urządzeń elementów mechanicznych, elektrycznych i funkcjonalnych, koniecznych w procesie wymiany informacji pomiędzy urządzeniami. Na potrzeby niniejszej książki definicję tę, stworzoną w początkach ery komputeryzacji w Polsce, nieco rozszerzymy i uogólnimy. W tłumaczeniu z angielskiego interface oznacza obszar wzajemnego oddziaływania (niekiedy wyraz ten tłumaczy się dosłownie jako sprzęg). Dla nas pierwsze tłumaczenie będzie intuicyjnie jasne i zadowalające. Przez interfejs, a dokładniej system interfejsu będziemy rozumieli pewien fizyczny układ, organizujący komunikację komputera (urządzenia) z innym urządzeniem zewnętrznym. Integralną częścią tego układu musi być system procedur i funkcji, programowo realizujących komunikację, czyli specjalistyczne oprogramowanie. Wymiana danych pomiędzy komputerem a urządzeniami (lub pomiędzy dwoma urządzeniami) realizowana jest dzięki wcześniejszemu ustaleniu protokołu transmisji, czyli specyficznego zbioru reguł, procedur lub różnego rodzaju konwencji dotyczących formatu i czasu trwania przesyłania danych. Przesyłane dane mogą być buforowane lub nie buforowane. Bufor (ang. buffer) definiowany jest jako obszar pamięci użytej do skompensowania różnic w szybkości przesyłania danych lub w czasie występowania znaków sterujących (lub innych zdarzeń) podczas transmisji pomiędzy urządzeniami. Wymiana danych realizowana jest przez tzw. kanał transmisyjny, którego właściwości zależeć będą od jego fizycznej formy. Wszelkie dane mogą być przesyłane za pośrednictwem różnego rodzaju przewodów, kabli światłowodowych czy fal radiowych. Przedstawione definicje są zgodne z normami przyjętymi w 2000 roku przez: Intel, Microsoft, Compaq, Lucent Technologies, Philips, NEC oraz Hawlett Packard. Definicje te zostały opublikowane w Universal Serial Bus Specification Revision 2.0. Wraz z rozwojem szeroko rozumianej techniki komputerowej powstało wiele standardów interfejsów, zarówno analogowych, cyfrowych szeregowych, cyfrowych równoległych, jak i cyfrowych szeregowo-równoległych. Standaryzacji poddano wszystkie rozwiązania konstrukcyjne w zakresie budowy gniazd przyłączeniowych, rozmieszczenia w nich sygnałów elektrycznych o określonych parametrach. Zunifikowano metody transmisji danych oraz ich protokoły. W przypadku interfejsów analogowych mamy do czynienia z transmisją, gdzie informacja przekazywana jest w postaci zmian amplitudy prądu lub napięcia. Jej przebieg czasowy może być określany czasem trwania impulsu (transmisja z podziałem czasowym) lub częstotliwością sygnału (transmisja z podziałem częstotliwościowym). Pierwszy z wymienionych sposobów umożliwia przesyłanie informacji w ściśle określonych sekwencjach czasowych. Wymaga to jednak stosowania skomplikowanej synchronizacji, zarówno od strony nadającego jak i odbierającego sygnał. W drugim przypadku niezbędne jest używanie specjalnych modulatorów i demodulatorów sygnału. Ze względu na znaczny stopień komplikacji protokołu transmisji interfejsy te wychodzą już powoli z użycia. Interfejsy cyfrowe wykorzystują kodowane sygnały binarne do przesyłu danych. W przypadku interfejsów szeregowych dane przesyłane są szeregowo bit po bicie. Do najważniejszych standardów, realizujących transmisję szeregową należy zaliczyć: RS 232C (w Europie zwany V.24) i jego rozwinięcia: RS 422, RS 423, RS 449, RS 485, interfejs IEEE 1394 (Firewire), stosowany głównie w urządzeniach przetwarzających dźwięk i obraz oraz najnowszy produkt USB (ang. Universal Serial Bus). Standard RS w swej podstawowej wersji wymaga jedynie trzech przewodów do realizacji transmisji - dwóch sygnałowych plus przewód masy. W USB wszystkie dane przesyłane są kolejno za pośrednictwem czterożyłowego kabla. Cyfrowe interfejsy równoległe transmitują poszczególne bajty danych równolegle. Wiąże się to z koniecznością stosowania dosyć rozbudowanego okablowania takich urządzeń. Do najważniejszych tego typu standardów należy zaliczyć ten stosowany powszechnie w drukarkach Centronics pracujący z napięciami o poziomach TTL. W roku 1992 opracowano 8-bitowy standard portu do transmisji równoległej EPP (ang. Enhanced Parallel Port), pozwalający realizować dwukierunkową transmisję z szybkością do 2 MB/s przy długości kabla 2 m. Pewną odmianą EPP jest opracowany w tym samym roku interfejs ECP (ang. Extended Capabilities Port). W porównaniu do swojego poprzednika protokół ten przewiduje możliwość zmian prędkości transmisji w zależności od aktualnych potrzeb. Spośród bardziej zaawansowanych interfejsów równoległych należy wymienić wprowadzony w latach 80. VME oraz najnowszy VXI (VMEbus Extension for Instrumentation). Bez wątpienia najlepszym z nich zarówno pod względem szybkości przesyłu danych jak i parametrów elektrycznych, okazał się ten ostatni. Jednak jest on bardzo kosztowny i obecnie bywa stosowany jedynie w wysoko specjalizowanych układach kontrolno-pomiarowych. Wśród cyfrowych interfejsów szeregowo-równoległych największą popularność zdobył IEEE 488 (międzynarodowy standard znany też jako IEC 626). Jest szeroko wykorzystywany w przemyśle i laboratoriach naukowych. W tym przypadku transmisja odbywa się bitowo-równolegle oraz bajt po bajcie szeregowo. Do niedawna interfejsów szeregowych używano w prostszych układach pomiarowych i komunikacyjnych, traktowane były jako uzupełnienie opcjonalnie stosowanych IEEE 488. Bardzo szybki rozwój inteligentnych-programowalnych urządzeń spowodował, że znowu dostrzeżono dobre strony RS. Zawsze podkreślano, że wadą urządzeń opartych na transmisji szeregowej jest jej mała prędkość. Jednak zarzut ten stracił już mocno na znaczeniu wraz z pojawieniem się nowej generacji pewnej rodziny układów scalonych, tzw. UART (ang. Universal Asynchronous Receiver - Transmitter), będących integralną częścią płyt głównych komputerów i przetwarzających strumień danych z postaci szeregowej na równoległą i odwrotnie Niezaprzeczalną zaletą interfejsów szeregowych RS jest ich niski koszt, dzięki czemu urządzenia w nie zaopatrzone stały się ogólnie dostępne. Postęp cywilizacyjny, jaki dokonał się w ostatnim dwudziestoleciu, pokazał oprócz wielu innych i taką ciekawą prawidłowość, że nie może stać się standardem żadne urządzenie, jeżeli nie będzie dostępne przeciętnemu podatnikowi. Trzeba zdawać sobie sprawę, iż dzisiaj to stwierdzenie odnosi się raczej do zamożniejszych społeczeństw, ale i u nas też zaczyna już powoli obowiązywać. Nie tak dawno miałem możliwość uczestniczyć w projektowaniu pewnego dużego sytemu, zbierającego dane właśnie z wykorzystaniem transmisji szeregowej za pomocą łączy RS 232C. Na pewno wielkie nadzieje należy wiązać z USB. Twórcy tego złącza : Intel, Microsoft, Compaq, Hawlett Packard, Lucent, Philips, IBM i NEC postawili sobie za cel skonstruowanie interfejsu o znakomitych parametrach eksploatacyjnych, umożliwiającego komputerowi swobodną komunikację z wieloma najprzeróżniejszymi urządzeniami. Można je swobodnie dołączać do PC bez potrzeby ustawiania adresów portów, kanałów DMA czy poziomów przerwań - system operacyjny natychmiast powinien je rozpoznać. W standardzie tym urządzenia zewnętrzne przyłączane są do komputera poprzez tzw. koncentratory (ang. Hub). Są już dostępne na rynku, pracujące w tym standardzie klawiatury, monitory (które mogą również pełnić rolę koncentratorów, o ile posiadają kilka gniazd USB), myszy, czytniki, nagrywarki CD i DVD, dyski twarde, różnego rodzaju skanery, drukarki i głośniki. Pojawiają się również - na razie nieliczne - prototypy przyrządów pomiarowych opartych na USB. Miałem okazję uczestniczyć w prezentacji jednego z takich urządzeń. Muszę jednak zauważyć, i nie jest to tylko moje odczucie, że pełne wdrożenie USB do szeroko rozumianych systemów pomiarowo-komunikacyjnych będzie wymagało jeszcze bardzo wiele wysiłku, szczególnie ze strony projektantów konkretnych przyrządów. Niektórzy z nich zdają się zapominać, że przeznaczenie np. wagi cyfrowej czy zasilacza wysokiego napięcia jest inne od przeznaczenia odtwarzacza DVD. Nie wszystkie urządzenia działające na zasadzie "włącz i zapomnij" są efektywne. Poważnym mankamentem tego nowego standardu jest wymóg ciągłej aktualizacji sterowników komunikacyjnych oraz rozróżnialność urządzeń ze względu na pobór mocy - maksymalny prąd zasilający jedno urządzenie nie powinien obecnie przekraczać 500 mA. Nie istnieje też w chwili obecnej kompletna, zunifikowana specyfikacja złącza USB. Niestety, producenci płyt głównych jeszcze nie doszli pod tym względem do całkowitego porozumienia. Jak na razie różne typy złączy należy stosować do różnych urządzeń. Zauważmy jednak, że obserwowane ciągłe udoskonalanie standardów transmisji szeregowej, ciągłe zwiększanie szybkości i niezawodności działania RS 232, który jeszcze długo pozostanie używanym standardem oraz prace nad USB wskazują, że przyszłość może należeć do tego typu transmisji.
Nowoczesna transmisja asynchroniczna oraz standard RS 232C
Podstawową wersję RS 232 (ang. Recommended Standard) wprowadzono w 1962 roku w USA. Początkowo standard ten miał służyć jedynie do obsługi modemów. Od tego czasu był poddawany kilkakrotnej aktualizacji celem bardziej optymalnego dostosowania na potrzeby szeregowej transmisji danych. Największą popularność zdobyła wersja RS 232C wprowadzona w 1969 roku, zaś oficjalnie do rangi standardu została podniesiona w roku 1986. RS 232C jest powszechnie stosowanym i akceptowanym standardem dla szeregowej wymiany danych cyfrowych pomiędzy urządzeniem DTE (ang. Data Terminal Equipment), obecnie utożsamianym z komputerem, a DCE (ang. Data Communication Equipment) - urządzeniem zewnętrznym (w oryginale modemem). W sposób jednoznaczny definiuje on parametry elektryczne, mechaniczne i logiczne łącza szeregowego. Oficjalna jego nazwa brzmi: Interface Between Data Terminal and Data Circuit Termination Equipment Employing Serial Binary Data Interchange. RS 232C stosowany bywa wszędzie tam, gdzie mniej istotną rolę odgrywa przepustowość łącza, natomiast ważna jest niezawodność i prostota obsługi protokołu komunikacyjnego. Komputery osobiste wyposażone są w łącza szeregowe przystosowane do transmisji asynchronicznej, tzn. komputer i urządzenie muszą pracować z jednakową, wcześniej uzgodnioną prędkością oraz taką samą strukturą znaków. Transmisja taka może być realizowana w trybie bez potwierdzenia odbioru lub z potwierdzeniem odbioru. Drugi sposób zapewnia nam możliwość kontrolowania poprawności wysyłanych-odbieranych danych. Dane przesyłane są w postaci tzw. ramki (ang. frame), która jest najmniejszą porcją możliwej do przesłania informacji. Bity przesyłane są kolejno. Do kodowania znaków stosuje się najczęściej kod ASCII (American Standard Code of Information Interchange). Początkowo stosowano 128 znaków zapamiętywanych na 7 bitach. W tym przypadku pierwszy bit danych - bit nr 0 poprzedzony był znacznikiem początku ramki - bitem startu. Ósmy bit (bit nr 7) służył do kontroli parzystości. Następnie przesyłany był znacznik końca ramki - jeden lub dwa bity stopu. Wraz z pojawieniem się strony kodowej ASCII o 256 znakach, pierwsze 32 znaki z przedziału 0-31 oraz znak 127 zaczęto rezerwować na potrzeby transmisji danych lub jako znaki sterujące dla urządzeń zewnętrznych. Obecnie zbiór ASCII jest podzestawem Unicode zawierającego 65536 znaków, który używany jest do reprezentowania znaków większości języków świata. Obecnie ramka może zawierać od 5 do 8 bitów danych (jednak większość spotykanych urządzeń posługuje się słowem 7 lub 8 bitowym) poprzedzonych bitem startu oraz zakończonych bitem parzystości i jednym lub więcej bitami stopu. Przed rozpoczęciem transmisji bit startu przyjmuje zawsze wartość 0, zaznaczając wyraźnie moment początkowy. Odwrotność czasu trwania transmisji jednego bitu określa szybkość przesyłu w bitach na sekundę. Korzystając z funkcji BIOS-u, możemy uzyskać transmisję w granicach od 110 do 9600 b/s. Przekonamy się, że w Windows może być ona znacznie, znaczne większa. Powodem tych rozbieżności są pewne różnice w sposobie inicjalizacji procedur obsługujących łącze szeregowe stosowane w BIOS i Windows. Możliwa do uzyskania szybkość transmisji zależy przede wszystkim od typu układu scalonego UART, w jaki zaopatrzona jest nasza płyta główna. Niezależnie od tego, że przetwarza on dane z postaci szeregowej na równoległą i odwrotnie, to obsługuje również sygnały sterujące interfejsu RS 232C. Bit kontroli parzystości przesyłany za ostatnim bitem danych jest jedną z metod monitorowania poprawności transmitowanych danych. Z reguły przyjmuje dwie wartości: 0 lub 1. Ilości jedynek w polu danych może być uzupełniana do liczby parzystej (evenparity) lub nieparzystej (oddparity). Bit parzystości może być stale równy 1 (markparity), stale równy 0 (space) lub może być nie ustawiony (noparity). Bity stopu zawsze oznaczają koniec ramki. Może wówczas nastąpić transmisja kolejnej "paczki" danych. RS 232C jest interfejsem cyfrowym, zatem jego poziomom logicznym (0-1) należy przypisać określone przedziały napięć zarówno ujemnych jak i dodatnich, które są z reguły nieco wyższe od stosowanych w komputerze. Pozwala to w dużym stopniu uniezależnić sygnał na wejściu interfejsu od przypadkowych zakłóceń. Dla sygnałów sterujących i sygnałów współpracy logicznej 1 odpowiada przedział od +3 do +25V, tzw. stan aktywny, wysoki, włączony lub "ON". Logicznemu 0 odpowiada przedział od -3 do -25 V, jest to stan nieaktywny, niski, wyłączony lub "OFF". Dla linii przesyłania danych logicznej 1 (tzw. "Mark") opowiada przedział napięć od -3 do -25V, zaś logicznemu zeru (tzw. "Space") przedział od +3V do +25V. Widzimy więc, że sygnały sterowania i współpracy są aktywne w stanie wysokim, zaś sygnały danych w stanie niskim (Mark). Łącze w trakcie ciszy utrzymywane jest stanie logicznej 1. Transmisja rozpoczyna się od bitu startu, który zawsze przyjmuje wartość logicznego 0. Po nim następuje transmisja ośmiu bitów reprezentujących znak. Póżniej jest bit parzystości, potem dwa bity stopu zamykające ramkę. Bitowi stopu odpowiada stan niski. Po nim łącze wraca do stanu ciszy. Jeden lub dwa bity stopu stosowane są po to, by odbiornik i nadajnik mogły dokonać wzajemnej synchronizacji przed transmisją kolejnej ramki danych. W praktyce układy nadajników zasilane są napięciem ±12V, dając amplitudę sygnałów ±8V. W tej sytuacji bitom 0 oraz 1 transmitowanego bajta odpowiadają napięcia odpowiednio +12V oraz -12V . Standardową linię interfejsu RS 232C stanowi 25-żyłowy przewód, przy czym większość z tych linii wykorzystuje się dla potrzeb transmisji synchronicznej. W standardzie IBM wykorzystuje się jedynie 9 sygnałów, które są wystarczające do zrealizowania transmisji asynchronicznej. W komputerach PC używano początkowo dwóch rodzajów złączy szeregowych: 9- oraz 25-końcówkowych typu DB-9 i odpowiednio DB-25. W komputerach zaopatrzonych w nowsze płyty główne spotyka się jedynie złącza DB-9. Podobnie wersji DB-25 nie spotyka się już w nowoczesnych urządzeniach pomiarowych Wykaz sygnałów wykorzystywanych obecnie w interfejsie RS 232C z uwzględnieniem przedstawionych typów złączy podano niżej. Linia 23 DSRD (Data Signal Rate Detector) w złączu DB-25 nie została uwzględniona w poniższym zestawieniu, gdyż obecnie praktycznie nie jest wykorzystywana. : Rysunek 1 Linie TxD oraz RxD są przeznaczone do obustronnego przesyłania danych. Nazywamy je liniami danych. Pozostałe zaś są liniami sterującymi lub kontrolnymi (oczywiście za wyjątkiem linii masy). Ogólnie sygnały przekazywane łączem RS 232C można podzielić na trzy grupy:
1) sygnały danych: RxD, TxD,
2) sygnały sterujące urządzeniem zewnętrznym: RTS, DTR,
3) sygnały odbierane od urządzenia (kontrolne): CTS, DSR, RI, RLSD (DCD).
RTS - CTS handshaking
tak naprawdę istotnym będzie tryb półdupleksowy z potwierdzeniem odbioru (transmisja dwukierunkowa naprzemienna), tzw. handshaking. W tym trybie komputer i urządzenie mogą naprzemiennie nadawać i odbierać, wykorzystując jeden logiczny kanał danych. Jest to metoda pytanie - odpowiedź. Należy przyznać, że sposób ten jest najprostszym i najskuteczniejszym środkiem wyegzekwowania interesującej nas informacji. Aby zrealizować taką prawdziwą konwersację pomiędzy komputerem a urządzeniem wystarczy wykorzystać dwie linie danych RxD i TxD oraz dwie linie sterujące RTS i CTS z magistrali RS 232C. Sygnał RTS musi być stale aktywny lub przełączany do tego stanu przed rozpoczęciem nadawania. Podobnie nadawanie znaków może nastąpić tylko wówczas, gdy sygnał CTS będzie włączony. Taki sposób sprzętowej kontroli transmisji nazywany jest Hardware flow control lub RTS - CTS handshaking lub jako Out-of-Band flow control - sygnały sterujące są generowane i sprawdzane niezależnie od sygnałów danych. Dostępny jest on w większości współczesnych systemów komunikacyjnych. Linia wejściowa TxD (3) komputera połączona jest z linią wejściową RxD (2) urządzenia. Linie te służą obustronnej wymianie danych. Za pomocą sygnałów RTS (7) - CTS (8) dokonywany jest wybór aktualnego kierunku transmisji. Po załączeniu łącza szeregowego linia DTR (4) zostaje włączona. W odpowiedzi urządzenie aktywuje linię DSR (6), sygnalizując gotowość do współpracy. Komputer, chcąc przesłać dane do urządzenia, aktywuje swój sygnał RTS (7), czekając na potwierdzenie od urządzenia na linii CTS (8). Jeżeli została ona uaktywniona, komputer wysyła dane linią TxD. Po zakończeniu transmisji linia RTS jest wyłączana (OFF), na co urządzenie odpowiada również przełączeniem linii CTS do stanu nieaktywnego (OFF). Ogólnie rzecz biorąc dane przesyłane do komputera linią RxD będą odbierane wówczas, gdy linie DSR i RLSD (DCD) (1) będą włączone. jest to typowy układ połączeń stosowany przy współpracy pomiędzy dwoma urządzeniami DTE. Jak to się ma do klasycznego połączenia DTE - DCE ? Należy przyznać, że z owym klasycznym sposobem łączenia ma to niewiele wspólnego. Przyczyna jest prosta - nowoczesne urządzenia pomiarowe zaopatrzone są już w programowalne jednostki arytmetyczno-logiczne i tak na dobrą sprawę są już same w sobie komputerami. Innych produktów nie spotyka się obecnie zbyt często na rynku. Rysunek 2 przedstawia pełną sekwencję stanów linii interfejsu RS 232C. Należy jednak zwrócić uwagę, że w większości spotykanych obecnie przypadków DSR i DTR pozostają zwarte, gdyż nowoczesne urządzenia pomiarowe w ogóle nie posługują się linią DSR, zaś linia DCD wykorzystywana jest przeważnie przez modemy. Jeżeli jednak chcielibyśmy z niej zrobić użytek, należy wejście DCD komputera połączyć z linią wyjściową DTR urządzenia. Analogicznie można połączyć ze sobą dwa komputery. W tym przypadku układ połączeń można jeszcze uprościć, zwierając linie RTS - CTS oraz DSR - DTR. W nowoczesnych przyrządach pomiarowych coraz częściej spotyka się złącze modułowe RJ-11, charakteryzujące się niewielkimi rozmiarami i prostotą montażu przewodów we wtyczkach. Bardzo często wykorzystuje się tu jedynie dwa sygnały dla potrzeb transmisji asynchronicznej. Na Rysunek 3 pokazano specyfikację najczęściej wykorzystywanych przez nie sygnałów. Należy jednak zauważyć, że niekiedy występują tu pewne rozbieżności. Niektórzy producenci nieco odmiennie definiują linie sygnałowe w RJ-11, ale zawsze jest to wyraźnie zaznaczone w instrukcji obsługi przyrządu.
Konwertery interfejsu RS 232C
Istniej wiele rodzajów konwerterów sygnałów interfejsu RS 232C na inne standardy RS. Do najczęściej stosowanych należą układy służące do łączenia urządzeń wyposażonych w interfejs RS 232C z urządzeniami wyposażonymi w interfejs RS 485 lub RS 422. Transmisja szeregowa w standardach 485 lub 422 jest dużo szybsza i bardziej odporna na zakłócenia, zapewnia ponadto większy zasięg transmitowanych sygnałów. W standardzie RS 485 szeregowa transmisja danych cyfrowych odbywa się przez dwuprzewodową symetryczną linię transmisyjną, do której można dołączyć nawet 32 nadajniki i odbiorniki. Stosując odpowiednie powielacze sygnału, liczbę takich urządzeń można znacznie zwiększyć. Interfejs ten umożliwia realizację wielopunktowej transmisji w trybie półdupleksowym. Standard elektryczny RS 422 nie różni się w istocie od RS 485. Różnica polega na możliwości dołączenia do jednej pary przewodów jednego nadajnika nawet do 10 odbiorników. W układzie RS 422 możliwa jest transmisja w trybie pełnego dupleksu, czyli w modzie jednoczesnego nadawania i odbioru danych. Konwertery interfejsu RS 232C konstruowane są w postaci niewielkich pudełek, zawierających z jednej strony złącze DB-25 lub DB-9 do podłączenia do łącza RS 232C w komputerze, zaś z drugiej strony inne złącze, np. PHOENIX do podłączenia linii i napięcia zasilającego. Układy te zapewniając izolację galwaniczną łączonych urządzeń i linii transmisyjnej, z reguły zasilane są oddzielnym zasilaczem stabilizowanym. Stosując tego rodzaju konwertery sygnałów możliwe jest uzyskanie szybkości transmisji w granicach 2,5 Mb/s przy maksymalnej długości linii około 1200 m, co wydaje się rozwiązaniem w pełni zadawalającym nawet w warunkach przemysłowych. Zestaw dwóch konwerterów może być z powodzeniem stosowany do realizacji połączenia pomiędzy dwoma urządzeniami zaopatrzonymi w interfejs RS 232C.
Protokół XON-XOFF
Wiele urządzeń wymaga stosowania programowej kontroli przepływu danych z wykorzystaniem tzw. protokołu XON-XOFF. Przykładem praktycznego wykorzystania niektórych znaków jest właśnie ten protokół, czasami nazywany DC1-DC3 lub ^Q-^S. Jeżeli dane przychodzą zbyt szybko do odbiornika i urządzenie odbierające nie może ich tak szybko pobierać z bufora wejściowego, program sterujący może wysłać znak XOFF (DC3 lub dziesiętnie 19 albo Control - S). Urządzenie nadające zatrzymuje dalszą transmisję (jeżeli oczywiście wie, co to jest XOFF), dopóki od strony odbiornika nie nadejdzie znak XON (DC1 lub dziesiętnie 17 albo Control - Q). Jeżeli jednak XOFF zostanie wysłany zbyt późno, może nastąpić przepełnienie bufora wejściowego. Podobnie opóźnienie wysłania XON z reguły powoduje zablokowanie portu komunikacyjnego. We współczesnych, inteligentnych urządzeniach o wysokim stopniu wzajemnej synchronizacji protokół ten przewiduje wysłanie XOFF, jeżeli bufor wejściowy jest wypełniony powyżej 3 / 4 deklarowanego rozmiaru. Program sterujący urządzeniem wysyła znak XON, jeżeli bufor jest wypełniony mniej niż w 1 / 2. W tym przypadku transmisja musi przebiegać w pełnym trybie dupleksowym. Dane przekazywane są jednocześnie w obu kierunkach niezależnie od siebie, po oddzielnych liniach transmisyjnych . Wykorzystujemy tu dwie pary linii RxD i TxD (RTS i CTS nie mają znaczenia), linia DTR może (ale nie musi) być wykorzystywana do włączania i wyłączania urządzenia. Windows podtrzymuje ten protokół
Protokół ENQ-ACK
Jest to jedna z obecnie rzadziej stosowanych metod kontroli przepływu danych w urządzeniach pomiarowych. Urządzenie transmitujące wysyła regularnie zapytanie ENQ po każdej, wcześniej ustalonej porcji transmitowanych danych. Kiedy odbierający jest gotowy do przyjęcia kolejnego bloku informacji, wysyła do nadającego potwierdzenie ACK, sygnalizując tym samym gotowość przyjęcia następnej porcji danych. W metodzie tej z reguły nie kontroluje się stopnia wypełnienie bufora pamięci. Jeżeli nadający po wysłaniu kolejnej porcji informacji nie otrzyma potwierdzenia jej odbioru, zaczyna wysyłać przez pewien, ściśle ustalony czas znak, np. LF dając, odbiornikowi możliwość ewentualnego przetworzenia wcześniej otrzymanych danych. Jeżeli po tym czasie nie nadejdzie potwierdzenie ACK, nadajnik wstrzymuje dalszą transmisję do czasu jego otrzymania.
Rola oprogramowania a podstawowe funkcje interfejsu
Aby zrealizować prawidłową wymianę informacji pomiędzy komputerem i urządzeniami zewnętrznymi, dla których pełnić on będzie rolę kontrolera, należy w pierwszej kolejności określić funkcje szeroko rozumianego interfejsu w tym systemie. W dobrze zaprojektowanym układzie interfejs powinien spełniać następujące wymagania:
1. Zapewnienie właściwego sposobu inicjalizacji połączenia. Aby uzyskać dostęp do urządzenia przyłączonego do portu komunikacyjnego, port ten należy fizycznie uaktywnić - otworzyć do transmisji. W standardowym PC mamy z reguły do dyspozycji tylko dwa szeregowe porty komunikacyjne. Często zachodzi jednak konieczność obsłużenia wielu urządzeń. Stosuje się wówczas specjalne karty lub konwertery, umożliwiające uzyskanie dostępu do większej ich liczby. Przyrządy podłączone do portów komunikacyjnych muszą być rozróżnialne, jeżeli chcemy nimi naprawdę sterować. Będą takimi, jeżeli aplikacja będzie w stanie rozróżnić poszczególne porty szeregowe i odpowiednio do nich kierować komunikaty oraz odbierać dane. Właściwa inicjalizacja portu polega na nadaniu mu unikalnego identyfikatora, którym można swobodnie operować w trakcie działania programu.
2. Zapewnienie właściwej synchronizacji transmitowanych danych pomiędzy komputerem i urządzeniami zewnętrznymi oraz udostępnienie metod natychmiastowej i automatycznej korekty różnego rodzaju błędów, pojawiających się w czasie transmisji. Większość standardowych interfejsów ma wbudowane funkcje synchronizacji, które w pewnym stopniu mogą minimalizować pojawiające się opóźnienia w kanale transmisyjnym, jedną z głównych przyczyn powstawania błędów. Rolą oprogramowania będzie ich umiejętne wyzwalanie. Aplikacja zarządzająca transmisją musi być skonstruowana w sposób zapewniający bezbłędne funkcjonowanie systemu pomiarowego lub komunikacyjnego. Właściwa reakcja na pojawiające się w czasie transmisji błędy oraz możliwość ich ewentualnej korekcji są zawsze istotnymi elementami programu komunikacyjnego.
3. Zapewnienie właściwej kontroli transmisji oraz wyboru jej kierunku. Kontrolowanie aktualnego kierunku transmisji może być realizowane sprzętowo lub programowo. Na pewno bardziej przydaną jest umiejętność programowej kontroli przepływu danych. Użytkownik danego systemu wie najlepiej, jakie dane i w jakim czasie chce otrzymać od urządzenia. Aplikacja obsługująca dany interfejs musi być tak zaprojektowana, aby możliwym był "płynny" wybór kierunku nadawanie-odbiór. W tym miejscu należy zwrócić szczególną uwagę na to, by nie tracić danych w momencie zmieniania kierunku transmisji. Stosując metodę buforowania danych, program musi być wyczulony na możliwość odbierania swoich własnych komunikatów przy nagłej zamianie ról z nadajnika na odbiornik
4. Udostępnienie możliwości zatrzymania transmisji w dowolnym momencie, bez ryzyka utraty danych. Tę właściwość same interfejsy posiadają tylko w ograniczonym stopniu. Program kontrolujący transmisję jest naprawdę funkcjonalny wówczas, gdy zawiera opcje umożliwiające czasowe wstrzymanie operacji odbioru-nadawania bez ryzyka utraty informacji. Jest to szczególnie ważne w przypadku aplikacji wielowątkowych lub generujących własne przerwania systemowe. Właściwość tę musi uwzględniać oprogramowanie sterujące jednocześnie portem szeregowym oraz różnego rodzaju kartami przetwornikowymi zaopatrzonymi w przetworniki analogowo-cyfrowe. W obecnych komputerach procesor programuje zaledwie kilka rejestrów sterujących urządzenia wysyłając rozkaz wykonania pewnej operacji (np. odebranie znaku przez port szeregowy). Istnieją przynajmniej dwa sposoby poinformowania procesora o tym, że dana operacja właśnie się zakończyła. Po pierwsze można zastosować tzw. polling, gdzie procesor wysyła regularne zapytania do urządzenia. Częstotliwość tych zapytań jest kontrolowana przez aktualnie działającą aplikację. Jednak w praktyce bardzo trudno jest określić optymalną częstotliwość takich odpytywania i z tego względu sposób ten jest bardzo niewygodny. Drugi sposób polega na tym, że samo urządzenie zgłasza wykonanie danego zadania. W stosownym momencie procesor przerywa wykonywanie aktualnego programu, pamiętając stan swoich rejestrów uaktywnia funkcje reagujące na zgłoszenie danego urządzenia. Mówimy wówczas, że nastąpiło przerwanie sprzętowe interrupt pochodzące od urządzenia. Ten sposób sterowania przepływem danych w PC jest stosowany wszędzie tam, gdzie mamy do czynienia z intensywnym i nieregularnym przepływem danych pomiędzy urządzeniami a pamięcią operacyjną. W praktyce mamy możliwości programowej kontroli aktualnie występujących przerwań. Jednak w rzeczywistości jest to zadanie uciążliwe i lepiej jest wykorzystać zalety programowania obiektowo-zdarzeniowego.
5. Zapewnienie możliwości odpowiedniego odbierania, przechowywania i wysyłania danych. Standardowe interfejsy mają możliwość buforowania danych. Jest to zaleta, którą doceniamy wtedy, gdy nie jesteśmy w stanie w sposób ciągły odbierać przychodzących danych lub nie możemy ich wysłać w ściśle określonym momencie. Oprogramowanie sterujące przepływem danych pełni w takich przypadkach rolę wspomagającą. Aplikacja powinna umieć odczytać aktualny stan bufora wejściowego i zdecydować o pobraniu z niego interesujących nas danych. Odbierając informacje w sposób ciągły, należy nieustannie kontrolować bufor danych, nawet w sensie fizycznej ingerencji. Dobrze działający program nie może dopuścić do jego przepełnienia, gdyż grozi to całkowitą utratą informacji. Rolą oprogramowania będzie również odpowiednie czyszczenie bufora danych w trakcie transmisji. Jest to zawsze punkt newralgiczny systemu komunikacyjnego.
W celu ujednolicenia i uproszczenia sposobów projektowania oprogramowania wykorzystywanego w różnych systemach pomiarowych stworzono standard opisujący zestaw uniwersalnych instrukcji programujących urządzenia pomiarowe, tzw. język SCPI (ang. Standard Commands for Programmable Instruments). Zdefiniowane są tam wszystkie ujednolicone przez producentów urządzeń pomiarowych instrukcje (rozkazy), umożliwiające zaprogramowanie nowoczesnego przyrządu w zależności od wykonywanego przezeń zadania. Programiści dostali więc do dyspozycji uniwersalny język zapytań i odpowiedzi, należy tylko umiejętnie go wykorzystać. Niestety, to niewątpliwe udogodnienie nie zwalnia nas od konieczności samodzielnego stworzenia (lub kupna) aplikacji, potrafiącej wykorzystać zalety SCPI.
Jak testować programy do transmisji szeregowej ?
Mirror w MS DOS
Mirror jest typowym programem umożliwiającym testowanie różnego rodzaju łączy komunikacyjnych, w tym standardu RS 232C. Program ten z reguły jest dostępny wszędzie tam, gdzie w użyciu były (lub jeszcze są) interfejsy CAMAC . Jeżeli spotkasz komputer przyłączony do CAMAC-a, Mirror najczęściej będzie znajdował się w katalogu \ASM. Po jego uruchomieniu i przeczytaniu informacji o producencie należy nacisnąć dowolny klawisz, by przejść do wyboru opcji programu Ponieważ interesuje nas tylko port szeregowy, należy więc w dolnym pasku komend wpisać 5 (lub inną liczbę odpowiadającą opcji COMM) i potwierdzić klawiszem Enter. Po uzyskaniu informacji, że dostępne łącza zostały zdiagnozowane, należy nacisnąć Home. Po tej operacji zobaczymy główne menu programu Komendy wpisujemy w dolnej linii po zapytaniu Command? W celu opuszczenia programu wystarczy wpisać qu (quit). Obszar Communications parameters służy do wyboru ustawień parametrów transmisji danego portu szeregowego. Numer łącza wybierzemy wpisując PO Enter Następnie wybieramy konkretny numer portu, np. 2 i znowu potwierdzamy. W sposób analogiczny dokonamy ustawień wszystkich interesujących nas parametrów transmisji. Powiedzmy, że chcemy ustalić jej prędkość - wystarczy po Command? wpisać SP Enter i dokonać odpowiedniego wyboru. Jeżeli zostały ustalone wszystkie niezbędne parametry komunikacyjne, czyli prędkość (SPeed), długość słowa danych (DAta), numer portu (POrt), parzystość (PArity) oraz ilość bitów stopu (STop) wystarczy nacisnąć Home, by przejść do okna, w którym możemy już wpisywać z klawiatury informacje przeznaczone do wysłania. Powrotu do poprzedniej planszy programu dokonujemy naciskając Home. Widzimy, że obsługa tego programu nie powinna sprawić nikomu trudności. Udostępnia on jeszcze szereg opcji, z którymi można się samodzielnie zapoznać. Nas jednak nie będą one interesowały, gdyż nie będziemy z nich korzystać
Terminal dla Windows 3.x oraz 9x
Aplikacja ta służy programowej realizacji bezpośredniego połączenia dwóch komputerów lub komputera i modemu poprzez interfejs RS 232C. Jej wygląd oraz zasada działania nie różnią się w obu wersjach Windows. W wersji Windows 3.x Terminal.exe znajduje się w Akcesoriach pod charakterystyczną ikoną przedstawiającą komputer i telefon. Program ten działa równie dobrze w środowisku 32-bitowym. Po jego uruchomieniu wybieramy opcję Ustawienia - Mamy tu szereg ciekawych możliwości, jednak dla nas najbardziej interesującą będzie Transmisja. Po rozwinięciu tej planszy można wybrać sposób przesyłania tekstu (standardowy, znak po znaku lub co wiersz). Potem należy w pierwszej kolejności określić numer portu komunikacyjnego. Po jego wyborze wszystkie pozostałe opcje staną się dostępne. Naciskając OK właściwie jesteśmy już w stanie nadawać z klawiatury lub odbierać informacje z sąsiedniego komputera (na którym w przyszłości będzie uruchomiona samodzielnie napisana przez nas aplikacja). Rozwijając menu Przesyłanie bez problemu określimy dany plik tekstowy, który możemy przesłać lub odebrać. Trzeba przyznać, że poważną wadą tego programu jest ograniczenie się do możliwości operowania tylko na plikach tekstowych lub binarnych. Nas będą głównie interesowały tzw. pliki beztypowe, powszechnie używane obecnie przy sterowaniu różnego rodzaju urządzeniami zewnętrznymi. Kolejną wadą omawianego programu jest również to, że zupełnie nie nadaje się do sterowania czymkolwiek. Co najwyżej, korzystając z niego jesteśmy w stanie stwierdzić, czy z danym przyrządem można w ogóle nawiązać jakąkolwiek komunikację. Jednak mimo tych niedogodności program znakomicie będzie nadawał się do testowania naszych algorytmów. Zważywszy na fakt, że już niedługo będziemy musieli zająć się również programową obsługą plików, przypomnijmy w tym miejscu pewne istotne informacje na ich temat. Ogólnie rozróżniamy dwa główne rodzaje plików: tekstowe oraz binarne. Plik tekstowy stanowi pewien zbiór, składający się z ciągu znaków ASCII. Poszczególne informacje w nim zawarte pogrupowane są w kolejnych wierszach, z których każdy zakończony jest parą znaków CR LF. W tego rodzaju plikach dane zapisywane są sekwencyjnie, co ma oczywiście swoje dobre i złe strony. Główną ich wadą jest utrudnienie wyszukiwania określonego fragmentu danych zawartych w takim pliku. Niemniej jednak z reguły dąży się do tego, by pracować z danymi uporządkowanymi sekwencyjnie - wówczas nie musimy wykonywać częstych przeszukiwań tekstu. Pliki binarne, zawierając informacje binarne zrozumiałe jedynie dla określonych programów, nie są przeznaczone do bezpośredniego oglądania. W większości wypadków pliki takie są po prostu skompilowanymi wykonywalnymi programami lub ich bibliotekami. Pewną odmianę plików binarnych stanowią tzw. pliki zdefiniowane (ang. typed files). Struktura informacji zawartych w takiego rodzaju zbiorach jest najczęściej typu rekordowego o zmiennej lub stałej długości, zawierającego różnego rodzaju pola. Terminal umożliwia przesyłanie plików binarnych, przy czym posługuje się dwiema metodami:
1. XModem/CRC, gdzie wykorzystuje się algorytmy liczenia sum kontrolnych CRC (ang. Cyclic Redundancy Code) dla bloków danych. W odróżnieniu od tradycyjnej sumy danych, posługującej się sumowaniem wszystkich bajtów lub słów w bloku danych, algorytm CRC może wykorzystywać różne odmiany tzw. wielomianów generacyjnych. Rozmiar sumy kontrolnej zależy od stopnia używanego wielomianu.
2. Kermit jest specjalnym protokołem transmisji danych, opracowanym w 1981 roku na Uniwersytecie Columbia w Nowym Jorku. Jego nazwa pochodzi od imienia bardzo znanej i komunikatywnej postaci Kermita z równie znanego serialu Muppet Show. Protokół ten umożliwia swobodny transfer zarówno danych w postaci tekstowej jak i binarnej. Znalazł on zastosowanie między innymi w usługach oferowanych przez TCP-IP, X.25 oraz LAN.
Przedmiotem naszego zainteresowania będą głównie tzw. pliki beztypowe lub amorficzne (ang. untyped files). Tego rodzaju zbiory umożliwiają swobodny dostęp do danych, bez potrzeby wnikania w ich budowę. Elementy plików beztypowych należy traktować jako ciągi pojedynczych bajtów o strukturze, która dla użytkownika jest nieistotna. Jeszcze inną ich zaletą jest fakt, że pozostają niesprzeczne, czyli kompatybilne z innymi zbiorami danych i doskonale nadają się do programowania wszelkich operacji wejścia-wyjścia. Dane w postaci tego rodzaju plików są obecnie powszechnie wykorzystywane w programach sterujących urządzeniami pomiarowymi.
RS 232C w MS DOS
Zapoznamy się z niektórymi sposobami realizacji transmisji szeregowej w środowisku MS DOS. Wbrew pozorom jest to bardzo ciekawe i kształcące zajęcie. Poznając zasady tworzenia takich algorytmów w DOS i Windows można się przekonać, jak wielki postęp został dokonany w tej dziedzinie programowania. Wiadomości tu przedstawione pomogą zrozumieć, że idea programowania w Windows nie powstała od razu. Powstała dzięki odpowiedniemu ulepszaniu i adaptacji najlepszych elementów środowisk programistycznych DOS-a.
Borland C+
Zaprezentujemy dwie bardzo użyteczne funkcje zdefiniowane w standardzie ANSI C, umożliwiające bardzo szybkie skonfigurowanie w trybie do transmisji asynchronicznej wybranego łącza szeregowego. Ich prototypy znajdują się w pliku bios.h. Będą nimi:
int bioscom(int cmd, char abyte, int port);
oraz
unsigned _bios_serialcom(int cmd, char abyte, int port);
W obu przypadkach parametr port określa dany port komunikacyjny, dla 0 = COM1, dla 1 = COM2, lub ogólnie (n-1) = COMn. Należy pamiętać, że nazwy COMn znajdują się na liście nazw zastrzeżonych, nie można ich zatem nadawać żadnemu plikowi. cmd określa rodzaj wykonywanej przez port szeregowy operacji. Poniżej przedstawiono wartości, jakie można przypisać parametrowi cmd : Rysunek 4 .Sposób ustalenia wszystkich cech charakterystycznych ramki danych określa parametr abyte: Rysunek 5 .Przykładowo, jeżeli chcemy ustalić: prędkość transmisji na 9600 b/s; sprawdzanie parzystości polegające na tym, że liczba jedynek w polu danych będzie uzupełniana do liczby nieparzystej; jeden bit stopu oraz długość słowa danych jako 8 bitów, to argumentowi abyte należy przypisać: abyte = (0xE0 | 0x08 | 0x00 | 0x03) w funkcji bioscom() lub abyte = (_COM_9600 | _COM_ODDPARITY | _COM_STOP1 | _COM_CHR8) jeżeli zechcemy użyć _bios_serialcom(). Z zapisów tych wynika, że przypisanie konkretnej wartości reprezentującej strukturę ramki danych odbywa się poprzez wykonanie operacji maskowania z zastosowaniem operatora bitowej alternatywy |. Powyższe ustawienie parametrów ramki równie dobrze można odczytać jako: Rysunek 6 . Zarówno bioscom(), jak i _bios_serialcom() zwracają wartość 16-bitową. Bardziej znaczący bajt tej liczby zawiera bity opisujące aktualny stan transmisji.
Bardziej znaczący bajt:
bit 7 (Time out)
1 = błąd przekroczenia czasu nawiązania połączenia, tzw. błąd przeterminowania operacji
bit 6 (Transmit shift register empty) 1 = rejestr przesuwny nadajnika jest pusty
bit 5 (Transmit holding register empty) 1 = rejestr wyjściowy nadajnika jest pusty
bit 4 (Break detect) 1 = połączenie zostało przerwane
bit 3 (Framing error) 1 = błąd protokołu ramki
bit 2 (Parity error) 1 = błąd parzystości
bit 1 (Overrun error) 1 = błąd przepełnienia bufora odbiornika
bit 0 (Data ready) - gotowe dane
1 = w buforze odbiornika znajduje się bajt danych
0 = bufor odbiornika jest pusty
W przypadku gdy parametr cmd został ustawiony jako: _COM_INIT - (0), _COM_SEND - (1) lub _COM_STATUS - (3), poszczególne bity mniej znaczącego bajta reprezentują sobą informacje obrazujące stan linii sterujących łącza RS 232C:
Mniej znaczący bajt:
bit 0 (Change in clear to send) 1 = sygnał na linii CTS zmienił poziom
bit 1 (Change in data set ready) 1 = sygnał na linii DSR zmienił poziom
bit 2 (Trailing edge ring detector) 1 = sygnał na linii RI zmienił poziom
bit 3 (Change in receive line signal detector) 1 = sygnał na linii RLSD zmienił poziom
bit 4 (Clear to send)
1 = sygnał na linii CTS jest aktywny
0 = nieaktywny
bit 5 (Data set ready)
1 = sygnał na linii DSR jest aktywny
0 = nieaktywny
bit 6 (Ring indicator)
1 = sygnał na linii RI jest aktywny
0 = nieaktywny
bit 7 (Receive line signal detect)
1 = sygnał na linii RLSD jest aktywny
0 = nieaktywny.
Jeżeli w miejsce cmd wpisalibyśmy _COM_RECEIVE - (2), wówczas mniej znaczący bajt wartości zwracanej przez bioscom() oraz _bios_serialcom() będzie zawierać informację (bajt) odebraną przez port szeregowy. Zobaczmy, jak w praktyce można wykorzystać funkcję bioscom(). Przedstawiony wydruk algorytm realizuje naprzemienną transmisję szeregową, emulując prosty terminal. Przy jego projektowaniu w dużym stopniu wykorzystałem właściwości preprocesora. Działanie programu można testować, łącząc się z innym komputerem, na którym uruchomiony jest ten sam program lub inny, opisany w poprzednim rozdziale. Ekran podzielony jest na dwie części, aby ułatwić nam śledzenie zarówno danych odbieranych jak i wysyłanych. Wiadomości wpisujemy z klawiatury.
//-----------dos_rs_1.cpp------------------------------------
#include
#include
#include
#include
#include
#define WINDOW_IN() window (1,3,80,12) // okno do odbioru
#define WINDOW_OUT() window (1,14,80,25) // okno do nadawania

// Prędkość transmisji
#define SPEED_110 0x00
#define SPEED_150 0x20
#define SPEED_300 0x40
#define SPEED_600 0x60
#define SPEED_1200 0x80
#define SPEED_2400 0xA0
#define SPEED_4800 0xC0
#define SPEED_9600 0xE0
// Parzystość
#define NOPARITY 0x00
#define ODDPARITY 0x08
#define EVENPARITY 0x18

// Bity stopu
#define STOP_1 0x00
#define STOP_2 0x04
// Długość słowa danych
#define ByteSize_7 0x02
#define ByteSize_8 0x03
// Numer portu
#define COM_1 0
#define COM_2 1
#define rs_status() bioscom(STATUS, 0, COM)
#define rs_init() bioscom(INIT, SPEED | PARITY | STOP | \ ByteSize, COM)
#define rs_send(output) bioscom(SEND, output, COM)
#define rs_receive() bioscom(RECEIVE, 0, COM)
int STATUS = 3;
int INIT = 0;
int SEND = 1;
int RECEIVE = 2;
// parametry transmisji
int COM = COM_2, SPEED =SPEED_9600, PARITY = ODDPARITY, STOP = STOP_1, ByteSize = ByteSize_7;
main()
{ int i, j, m, k, n; // zmienne sterujące pozycją kursora
int in, out; // dane odbierane-nadawane v window(1,1,80,24);
clrscr();
cout<<" [Esc] - koniec programu";
WINDOW_IN();
cout<< " -- Nadawanie -- ";
WINDOW_OUT();
cout<< " -- Odbiór -- ";
rs_init(); // inicjalizacja portu
j = 2; m = 2; k = 1; n = 2;
while (i != 0)
{
rs_status();
if (kbhit())
{
WINDOW_IN ();
gotoxy(j, m);
out = getch();
j ++;
if (out == '\x0D')
{
m ++;
j = 1;
cprintf("\n");
}
gotoxy(j, m);
if (out == '\x08')
j = j - 2;
putch(out);
if (j == 80)
{
j = 1;
m ++;
cprintf("\n");
};
if (m >= 12)
m = 12;
if (out == '\x1B')
i = 0;
else
rs_send(out); // wysyłanie znaków
}// koniec if(kbhit())
if (rs_status() & 0x100)
if((in = rs_receive() & 0x7F) != 0) // odbiór
{
WINDOW_OUT ();
if (in == '\x08') k = k - 1;
if (k == 80)
{
k = 1;
n ++;
cprintf("\n");
}
if (n >= 12)
n = 12;
if (in == '\x0D')
{
n ++;
k = 1;
cprintf("\n");
}
if(char(in) > '\x1F')
{
k ++;
gotoxy(k, n);
putch(in);
}
}
}// koniec while
window(1,1,80,25);
clrscr();
return 0;
}

Najważniejszym fragmentem przedstawionego programu są dwie, na pozór niewinnie wyglądające instrukcje warunkowe:
if (rs_status() & 0x100)
if((in = rs_receive() & 0x7F) != 0)
{...
// odbierz znak
}
Rozpatrzmy najpierw pierwszą z nich. Użyliśmy operatora &, aby sprawdzić rezultat iloczynu bitowego makrowywołania rs_status() z wartością 256 (heksadecymalnie 0x100). Ktoś mógłby zapytać, w jakim celu? Otóż zastanówmy się, co reprezentuje sobą wartość 256. Aby ją otrzymać: wykonamy proste działanie: do 255 (binarnie 255 = 1 1 1 1 1 1 1 1 ) dodamy 1, w wyniku otrzymując 256, czyli 1 0 0 0 0 0 0 0 0. Dla naszego PC oznaczać to będzie, że jeżeli powyższy warunek ma być spełniony, to w ośmiobitowym rejestrze przechowującym mniej znaczący bajt musi pojawić się 0 0 0 0 0 0 0 0. Jedynka natomiast zostanie przeniesiona (carry) do dziewiątego, czyli zerowego bitu bardziej znaczącego bajta. Będzie to sygnałem, że w rejestrze buforowym odbiornika pojawił się jakiś bajt. W jednej linijce kodu wykonaliśmy więc dwie bardzo ważne operacje: wstępnie sprawdziliśmy, czy jest wyzerowany bajt, który w przyszłości będzie zawierał odebrany znak oraz wysłaliśmy do rejestru stanu transmisji sygnał o możliwości odbioru danych. Makrowywołanie rs_receive() jest niczym innym jak swego rodzaju wywołaniem funkcji bioscom() z cmd o wartości 2. Jeżeli rezultat iloczynu bitowego rs_receive() z maską 0x7F (0x7F dziesiętnie: 127 = 0 1 1 1 1 1 1 1 ) będzie różny od zera oznacza to, że mniej znaczący bajt wartości zwracanej przez bioscom() zawiera odebrany przez port szeregowy znak. Teraz wystarczy go już tylko wyświetlić na ekranie. Należy zwrócić uwagę, że używając warunku:
if((in = rs_receive() & 0xFF) != 0)
też otrzymamy prawidłową transmisję.
PRZYPOMNIJMY
Instrukcja w postaci:
#define identyfikator_1(identyfikator, ...) ciąg znaków
jest makrodefinicją. Użycie w programie pierwszego identyfikatora:
identyfikator_1(identyfikator,...)
jest makrowywołaniem. Makrowywołanie zastępowane jest ciągiem znaków podanym w definicji identyfikatora_1.
Oczywiście w analogiczny sposób ktoś mógłby zrealizować również transmisję plików, na przykład według przedstawionego schematu:
{
... int out;
FILE *pstream;
pstream = fopen("plik.txt" , "r");
rs_init();
...
while((out = fgetc(pstream)) != EOF)
rs_send(out);
fclose(pstream);
...
}
Borland Pascal
Zapoznamy się teraz ze sposobem realizacji transmisji szeregowej właściwym dla Pascala. Niestety, w środowisku MS DOS kompilator ten nie jest tak przyjazy jak C++. Będziemy musieli trochę uwagi poświęcić przerwaniom i rejestrom. Opiszemy trzy podstawowe funkcje przerwania 14h BIOS-u, za pomocą których można w prosty sposób zrealizować obsługę wybranego portu szeregowego. Będziemy musieli odwołać się też do rejestrów. Spośród szesnastu rejestrów, które mogą być wykorzystywane przez programistów, dla nas najważniejsze będą dwa pierwsze rejestry ogólnego przeznaczenia, którymi posługują się interesujące nas funkcje BIOS-u. Czytelnikom, którzy nie interesowali się dotąd takimi zagadnieniami wyjaśnijmy, że rejestry ogólnego przeznaczenia procesora Pentium są 32-bitowe. Będą to: EAX, EDX, ECX, EBX, EBP, ESI, EDI oraz ESP. Nazwy ich pochodzą od nazw rejestrów procesora 8086: AX, BX itd. Rejestry te przechowują argumenty operacji (tzw. operandy) dla operacji arytmetycznych i logicznych. Mogą też zawierać operandy dla obliczania adresów. Rejestr DX jest wykorzystywany w różnego rodzaju operacjach mnożenia i dzielenia, jest również jedynym, w którym można podać adres portu w operacjach wejścia-wyjścia. AX (tzw. akumulator) służy przede wszystkim do wykonywania przesłań oraz operacji arytmetyczno-logicznych. Ze względu na to, że operacje wykonywane na rejestrze AX są optymalizowane, wykonanie ich przebiega znacznie szybciej niż na pozostałych rejestrach.
Funkcja 00h
Funkcja 00h przerwania 14h umożliwia odpowiednie skonfigurowanie i inicjalizację wybranego portu szeregowego ustalając jego parametry transmisji. Dane wejściowe tej funkcji przedstawiają się następująco:
AH - 00h,
DX - przechowuje numer portu: 0 = COM1, 1 = COM2,
AL - określa parametry transmisji
Funkcja ta w rejestrze AH zwraca status łącza, zaś w AL status linii modemu.
Funkcja 01h
Wysyła znak do wybranego portu szeregowego. Danymi wejściowymi są:
AH - 01h,
DX - numer łącza,
AL - wysyłany znak.
W bajcie przechowywanym w rejestrze AH zwraca ona status łącza. Funkcja 02h
Służy do odbioru znaku z określonego portu szeregowego. Danymi wejściowymi są:
AH - 02h,
DX - numer łącza szeregowego.
W AL zwracany jest znak odebrany w wyniku transmisji, zaś w AH zwracany jest status portu
Funkcja 03h
Określa status danego portu szeregowego. Danymi wejściowymi są:
AH - 03h, DX - numer łącza. W rejestrze AX zwracany jest status portu: bardziej znaczący bajt (rejestr AH) zwraca bajt statusu łącza, mniej znaczący bajt (rejestr AL) zwraca bajt statusu modemu
Poniżej został przedstawiony prosty program, wykorzystujący funkcje 01h oraz 02h przerwania 14h BIOS-u. Skorzystaliśmy tutaj z procedury Intr(), wykonującej określone przerwania programowe. Wygląda ona następująco:
procedure Intr(IntNo: Byte; var regs: Registers);
gdzie IntNo jest numerem przerwania, zaś regs jest jednym z rekordów typu Registers zdefiniowany w module Dos:
type
Registers = record
case Integer of
0: (AX, BX, CX, DX, BP, SI, DI, DS, ES, Flags: Word);
1: (AL, AH, BL, BH, CL, CH, DL, DH: Byte);
end;
Kod źródłowy programu dos_rs_2.pas napisanego w Pascalu, realizującego transmisję szeregową
Program dos_rs_2;
uses
Crt, Dos;
var
regs : Registers;
mode: Char; {tryb nadawanie-odbiór}
out : Char; {wysyłane znaki}
Begin
ClrScr;
regs.AH := 0; {01h}
regs.DX := 1; {COM2}
regs.AL := 234; {9600 b/s, ODDPARITY, 7 bitów danych, 1 bit stopu}
Intr($14, regs);
WriteLN(' 1 - Nadawanie 2 - Odbiór [Esc] - koniec nadawania i odbioru');
mode := ReadKey;
if(mode = '1') then
begin
Repeat {nadawanie}
out := ReadKey;
regs.AL := ord(out);
regs.AH := 1;
regs.DX := 1;
Intr($14, regs);
Write(char(regs.AL));
Until(out = #27);
end;
if(mode = '2') then
begin
Repeat {odbiór}
regs.AH := 2;
regs.DX := 1;
Intr($14, regs);
Write(char(regs.AL));
Until(regs.AL = 27);
end;
if(mode = #27) then exit;
Repeat Until KeyPressed;
End.

Programowa obsługa interfejsu RS 232C w Windows
zapoznasz się teraz 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 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. Typy zmiennych stosowanych w Delphi oraz w C++ Builderze
Delphi
Rozmiar w bajtach
Znak +/-
Typ
C++ Builder
ShortInt
1
integer
signed char
SmallInt
2
integer
short
LongInt
4
integer
Byte
1
bez znaku
integer
unsigned char
Byte
2
bez znaku
integer
unsigned short
Integer
4
integer
int
Cardinal
4
bez znaku
integer
unsigned int
Boolean
1
true/false
bool
ByteBool

1
true/false
bez znaku

integer
unsigned char
WordBool

2
true/false
bez znaku

integer
unsigned short
LongBool

4
true/false
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
≈ 3 GB
ANSIChar
AnsiString
AnsiString
String[n]
n=1.255 bajtów
ANSIChar
string
SmallString[n]
ShortString
255 bajtów
ANSIChar
string
SmallString<255>
String
255 lub ≈ 3 GB
ANSIChar
AnsiString
AnsiString
Single
4
floating point number [liczba zmiennoprzecinkowa]
float
Double
8
floating point number
double
Extended
10
floating point number
long double
Real
4
floating point number
double
Pointer
4
generic pointer [wskaźnik ogólny, adresowy]
void*
PChar
4
bez znaku
pointer to characters [daleki wskaźnik na C-łańcuch]
unsigned char*
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ę. Poniżej przedstawiono ogólne zasady tworzenia niektórych przedrostków.
Ogólne zasady tworzenia przedrostków wg notacji węgierskiej.


Przedrostek
Skrót angielski
Znaczenia
a
array
tablica
b
bool
tablica zmienna logiczna TRUE lub FALSE (1 lub 0)
by
byte unsigned char
znak (bajt))
ch
char
znak
cb
count of byte
liczba bajtów
dw
double word
podwójne słowo
evt
event
zdarzenie
f
flag
znacznik
fdw
flag of double word
znacznik typu dw
fn
function
funkcja
h
handle
identyfikato (uchwyt)
i
integer
typ całkowity długi 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 toflag of dw
wskaźnik typu lp do znacznika typu double word
lpfn
long pointer to function
wskaźnk 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 Rysunek 7 przedstawia przykładowe, ogólnie stosowane sposoby ich deklaracji

Wykorzystanie elementów Win32 API w C++ Builder
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. Poniżej przedstawiono specyfikację bloku kontroli urządzeń zewnętrznych DCB:



Typ
Zmienna
Znaczenie
Wartość, stała symboliczna
DWORD
DBClength
rozmiar struktury
nalezy wpisać
DWORD
BaudRate
określenie prędkości transmisji (bity/sek)
CBR_110 CBR_19200
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
wRewserved
nieużywane
0
WORD
XonLim
określenie minimalnej liczby bajtów w buforze wejściowym przed wysłaniem specjalnego znaku sterującego XON
domyślnie 65535, w praktyce XonLim ustala się jako 1 / 2 rozmiaru deklarowanego wejściowego bufora danych
WORD
XoffLim
określenie maksymalnej liczby bajtów w buforze wejściowym przed wysłaniem specjalnego znaku sterującego XOFF
domyślnie 65535, w praktyce XoffLim ustala się jako 3 / 4 rozmiaru deklarowanego wejściowego bufora
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 1 / 2
TWOSTOPBITS 2 bity stopu
char
XonChar
określenie wartości znaku XON dla nadawania i odbioru(wysyłanie znaku przywraca transmisję)
standardowo (char) DC1 , dziesiętnie 17
char
XoffChar
określenie wartości znaku XOFF dla nadawania i odbioru (wysłanie XOFF wstrzymuje transmisję do czzsu odebrania znaku XON)
standardowo (char) DC3 , dziesiętnie 19
char
ErrorChar
określenie wartości znaku zastępującego bajty otrzymane z błędem parzystości
opcjonalnie 0 lub SUB
char
EofChar
określenie wartości znaku końca otrzymanych danych
opcjonalnie: 0
char
EvtChar
określenie wartości znaku służącego do sygnalizowania wystąpienia danego zdarzenia
opcjonalnie: 0
WORD
wReserved1
Obcenie nie używane
tablica


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

Typ
Pole bitowe
Właściwości pola
Wartość,znaczenie, stała symboliczna
DWORD
fBinary
tryb binarny (Win 32 API podtrzymuje jedynie ten tryb transmisji danych)
TRUE/1
DWORD
fParity
Znaczeniaumożliwia ustawienie sprawdzania parzystości - sposobu reakcji na bit parzystości
TRUE/1 - kontrola parzystosci włączona
FLASE/0 - bit parzystości nie jest włączony
DWORD
fOutxCtsFlow
umożliwia ustawienie sprawdzania sygnału na linii CTS w celu kontroli danych wyjściowych
TRUE / 1 jeżeli sygnał CTS jest 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 sygnału na linii DSR w celu kontroli danych wyjściowych
TRUE / 1 jeżeli sygnał DSR jest 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 sygnału DTR
DTR_CONTROL_DISABLE / 0 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
fTXContinueOnXoff
kontrola przerwania transmisji w przypadku przepełnienia bufora wejściowego i, ewentualnie wystąpienia znaków XoffChar oraz XonChar
TRUE / 1 wymuszanie kontynuowania transmisji nawet po wystąpieniu znaku XOFF i 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 XON-XOFF w czasie wysyłania danych
TRUE / 1 transmisja zostaje przerwana 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 XON-XOFF w czasie odbioru danych
TRUE / 1 znak XoffChar jest wysyłany, 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 otrzymanych z błędem parzystości znakiem ErrorChar
TRUE / 1 zastąpienie jest wykonywane ponadto fParity musi być ustawione jako TRUE
FALSE / 0 zastąpienie nie jest wykonane
DWORD
fNull
odrzucenie odebranych nieważnych lub uszkodzonych bajtów
TRUE / 1 nieważne bajty zostaną odrzucone przy odbiorze
FALSE / 0 nieważne bajty nie będą odrzucane
DWORD
fRtsControl
specyfikacja kontroli sygnału na lini RTS
RTS_CONTROL_DISABLE / 0 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 operacji nadawanie-odbiór przy wykryciu błędu transmisji
TRUE / 1 wszelkie operacje nadawania i 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
zerowane, 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,. Nazwa działającej aplikacji powinna pojawić się na dolnym pasku zadań. Po pojawieniu się informacji: Usuwanie sesji w toku. Zakończyć ? należy nacisnąć przycisk OK .Po kolejnym komunikacie znów należy potwierdzić.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ść.
Kod formularza aplikacji testującej wybrane ustawienia portu szeregowego
//-------RS_01.cpp-----
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
#include
#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:

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ą. 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.
Zasoby struktury COMMPROP

Typ
Element struktury
Znaczenie
Zawartość elementu,Maska określająca włączony bit
WORD
wPacketLength
Określa (w bajtach) rozmiar porcji pakietu danych
należy odczytać, zależy od typu sterownika
WORD
wPacketVersion
wesja struktury
nr 2 w Win 9x
DWORD
dwServiceMask
określenie maski bitowej ,wskazującej na typ aktualnie dostępnej usługi komunikacyjnej
SP_SERIALCOMM jest zawsze określone
DWORD
dwReserved1
zarezerwowane, nie używane
DWORD
dwMaxTxQueue
maksymalny rozmiar wewnętrznego bufora wyjściowego nadajnika (w bajtach)
0 oznacza, że nie ustalono maksymalnej wartości
DWORD
dwMaxRxQueue
maksymalny rozmiar wewnętrznego bufora wejściowego odbiornika (w bajtach)
0 oznacza, że nie ustalono maksymalnej wartości
DWORD
dwMaxBaud
maksymalna dostępna prędkość transmisji w bitach na sekundę
BAUD_075 75 b/s
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.25
DWORD
dwProvCapabilities
określa maskę bitową identyfikującą rodzaj funkcji udostępnianych przez usługę komunikacyjną (dostarczyciela usługi)
PCF_16BITMODE tryb 16-bitowy
PCF_DTRDSR kontrola DTR-DSR
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 identyfikującej parametry transmisji podlegające ew. zmianom
SP_BAUD prędkość
SP_DATABITS długość słowa danych
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 prędkości transmisji
tak samo jak w dwMaxBaud
WORD
wSettableData
specyfikacja maski bitowej identyfikującej możliwe do użycia długości słowa danych
DATABITS_5
DATABITS_6
DATABITS_7
DATABITS_8
DATABITS_16
DATABITS_16X
szczególna długość słowa danych
WORD
wSettableStopParity
specyfikacja maski bitowej identyfikującej możliwe do użycia wartości bitów stopu i kontroli parzystości
STOPBITS_10 1 bit stopu
STOPBITS_15 1,5 bitu
STOPBITS_20 2 bity
PARITY_NONE brak
PARITY_ODD nieparzysta
PARITY_EVEN parzysta
PARITY_MARK 1
PARITY_SPACE 0
DWORD
dwCurrentTxQueue
aktualny maksymalny rozmiar wewnętrznego bufora wyjściowego nadajnika (w bajtach)
0 oznacza, że wartość ta nie jest aktualnie dostępna
DWORD
dwCurrentRxQueue
aktualny maksymalny rozmiar wewnętrznego bufora wejściowego odbiornika (w bajtach)
0 oznacza, że wartość ta nie jest aktualnie dostępna
DWORD
dwProvSpec1
specyfikacja formatu danych wymaganych przez daną usługę komunikacyjną
w zależnoście od dwProvSubType 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żejk/small>
WORD
wcProvChar[1]
Znaczenie
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. 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().
. Kod formularza aplikacji testującej wybrane zasoby struktury COMMPROP
//-------RS_02.cpp-----
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------

#include
#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.
Rysunek 8 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.
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
Zawartość elementu,MAska określająca włączony bit
DCB
dcb
struktura kontroli portu szeregowego
patrz DCB
DWORD
dwProviderSubType
identyfikacja typu dostarczanej usługi komunikacyjnej i, tym samym, wymaganego formatu danych
patrz COMMPROP
DWORD
dwProviderOffset
określenie offsetu dla danych wymaganych przez dostarczyciela usługi komunikacyjnej; offset (tzw. przesunięcie) określony jest zwykle w stosunku do początku struktury
0, jeżeli nie określono typu danych
DWORD
dwProviderSize
rozmiar danych (bajty) wymaganych przez usługę komunikacyjną (dostarczyciela usługi)
zależnie od typu usługi
WCHAR
wcProviderData[1]
dane dostarczane wraz z usługą (ponieważ przewidywane jest w przyszłości uzupełnienie struktury, aplikacja powinna używać dwProviderOffset w celu określenia położenia wcProviderData)
jeżeli ustalono typ usługi: PST_RS232 lub PST_PARALLELPORT , człon ten jest pomijany; jeżeli ustalono PST_MODEM , należy odwołać się do położenia MODEMSETTINGS

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: Rysunek 9
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. 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ą :Rysunek 10
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: v 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
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 , 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 :Rysunek 11 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.
Informacje zawarte w elementach struktury COMSTAT

Typ
Element struktury
Właściwość
Wartość zwracana, Znaczenie
DWORD
fCtsHold
Określa, czy transmisja jest wstrzymywana do czasu odebrania przez komputer sygnału CTS
TRUE / 1 Transmisja jest wstrzymana.
FALSE / 0 . Transmisja nie jest wstrzymywana.
DWROD/small>
fDsrHold
Określa, czy transmisja jest wstrzymywana do czasu odebrania przez komputer sygnału DSR
TRUE / 1 Transmisja jest wstrzymana.
FALSE / 0 Transmisja nie jest wstrzymywana.
DWORD
fRlsdHold
Określa, czy transmisja jest wstrzymywana do czasu załączenia stanu aktywnego na linii DCD Linia DCD oznacza też RLSD (Received Line Signal Detect).
TRUE / 1 Transmisja jest wstrzymana.
FALSE / 0 Transmisja nie jest wstrzymywana
DWORD
fXoffHold
Określa, czy transmisja jest wstrzymywana po odebraniu znaku sterującego XOFF
TRUE / 1 Transmisja jest wstrzymana.
FALSE / 0 Transmisja nie jest wstrzymywana
DWORD
fXoffSent
Określa, czy transmisja jest wstrzymywana po wysłaniu znaku sterującego XOFF
TRUE / 1 Transmisja jest wstrzymana. 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 znacznik końca pliku EOF
TRUE/1 - odebrano znak EOF, FALSE / 0
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 w wyniku transmisji szeregowej, ale jeszcze nie przeczytanych
należy je odczytać
DWORD
cbOutQue
liczba bajtów danych pozostających do wysliczba bajtów danych pozostających do wysłania
należy je odczytać

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
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.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. 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.
Kod formularza aplikacji realizującej transmisję szeregową
//----RS_03.cpp-------------
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
#include
#include
#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.
2. 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 :Rysunek 12
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.
3. 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.
4. 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.

Wysyłamy znak po znaku
Bardzo często musimy szybko wysłać do urządzenia tylko jeden znak. Może to być jakiś wyjątkowy znak sterujący, np. rozkaz natychmiastowego wyłączenia się przyrządu. Postępując w sposób opisany w poprzednim podrozdziale, czyli wykorzystując buforowanie danych, napotkamy pewną trudność. Rozkaz wysyłany przez nas będzie musiał poczekać w buforze na swoją kolej. Może zdarzyć się i taka sytuacja, że zechcemy wysłać jakiś plik, nawet dosyć duży, i niekoniecznie w tym celu zechcemy zastanawiać się nad ustalaniem rozmiaru bufora komunikacyjnego. W takich przypadkach przychodzi nam z pomocą Win32 API, oferując funkcję:
BOOL -TransmitCommChar(HANDLE hCommDev, char chTransmit);
Transmituje ona znak określony przez chTransmit do łącza identyfikowanego przez hCommDev. Bardzo ważną właściwością tej funkcji jest, że znak przez nią wysyłany będzie miał pierwszeństwo przed innymi wysyłanymi z bufora. Stosując ją należy jednak pamiętać, że nie można w ten sposób wysłać dwóch znaków jednocześnie, lecz co najwyżej "prawie" jednocześnie. Z reguły minimalne wymagane opóźnienie pomiędzy kolejnymi wywołaniami TransmitCommChar() wynosi 1 milisekundę. Chociaż jest ona rekomendowana przez API głównie do transmisji synchronicznej, to można ją też z powodzeniem stosować na potrzeby przesyłania asynchronicznego. Poniżej została zaprezentowana przykładowa aplikacja wykorzystująca własności opisanej funkcji. Przy jej pomocy możemy wysłać dowolny znak lub plik wcześniej zapisany na dysku. Kolejne przyciski typu TSpeedButton, za pomocą których wysyłamy kolejne litery alfabetu, zgrupowane są w obszarze określonym przez komponent TBevel. Z przyciskiem Wyślij plik skojarzona jest funkcja obsługi zdarzenia SendFileClick().
Kod formularza aplikacji realizującej nie buforowaną transmisję szeregową
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//----RS_04.cpp-------------
#include
#include
#pragma hdrstop
#include "RS_04.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName; // przechowuje nazwę portu
DCB dcb; // struktura kontroli portu szeregowego
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Close_Comm(hCommDev);
Application->Terminate();
}
//--------------------------------------------------------------------
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
{
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE)
dcb.BaudRate=CBR_19200;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE; // sprawdzanie parzystości
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
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);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest"
" aktywny.", "Błąd !", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendFileClick(TObject *Sender)
{
FILE *pstream;
char chTransmit;
if (hCommDev > 0)
{
if ((pstream = fopen("tekst.txt", "rt")) > 0)
{
while ((chTransmit = fgetc(pstream))!= EOF)
{
TransmitCommChar(hCommDev, chTransmit);
Sleep(1);
}
}
else
MessageBox(NULL, "Błąd otwarcia pliku.", "Błąd pliku!", MB_OK);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'A');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton2Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'B'); else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton3Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'C');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton4Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'D');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
if (hCommDev > 0)
TransmitCommChar(hCommDev, 'E');
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd !", MB_OK);
}
//--------------------------------------------------------------------
Patrząc na teść przedstawionej aplikacji, można by odnieść wrażenie, że problem jest tak banalny, że nie wart głębszego zastanawiania się nad nim. Sceptykom opowiem pewną historię. Będąc na początku lat dziewięćdziesiątych w Amsterdamie, jeszcze za czasów studenckich, zaszedłem do sklepu RTV-AGD, trafiając na moment, w którym sprzedawca demonstrował jednemu z klientów prosty regulator oświetlenia (w naszych sklepach czegoś takiego wtedy nie widziałem). Jego głównymi, widocznymi elementami były dwa pokrętła: dwustopniowe włącz-wyłącz oraz trójstopniowe, dające trzy możliwe poziomy natężenia światła. W przeliczeniu kosztował on około 20$. Do tego oferowany był prosty program działający w Windows 3.11 w cenie 15$. W porównaniu do kursu ówczesnej złotówki koszt zestawu nie był mały, przynajmniej dla mnie. Obsługa programu sprowadzała się do wyboru jednego z pięciu przycisków. Zainteresowała mnie zasada jego działania. Instrukcja obsługi napisana była również po angielsku na jednej, małej kartce papieru. Przeczytałem, że naciśnięcie przycisku oznaczonego symbolem I powoduje wysłanie do urządzenia komendy "I", zaś naciśnięcie O wysłanie komendy "O" itd. Podobną zasadą sterowania posługuje się bardzo wiele niekoniecznie prostszych urządzeń. Na zakończenie tego fragmentu naszych rozważań chciałbym poruszyć jeszcze jeden temat. Ze względu na to, że powyższy algorytm jest jednym z prostszych i krótszych, jakie przedstawiamy w tej książce, pragnę w tym miejscu wytłumaczyć się ze sposobu, w jaki umieszczane są w programie pisane przez nas funkcje. Postępując zgodnie z kanonami programowania zorientowanego obiektowo w C++Builderze, nagłówek przykładowej funkcji:
int __fastcall Close_Comm(HANDLE hCommDev)
należałoby zapisać następująco:
int __fastcall TForm1::Close_Comm(HANDLE hCommDev)
przez co jawnie stałaby się obiektem klasy TForm1, która dziedziczy własności TForm, czyli bazowej klasy formularza. Postępując konsekwentnie (zresztą nie mamy innego wyboru), należy powyższą deklarację umieścić w pliku nagłówkowym RS_04.h, np. w sekcji private lub public, tak jak przedstawia to poniższy wydruk.
Zawartość pliku RS_04.h
//--------------------------------------------------------------------
#ifndef RS_04H
#define RS_04H
//--------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
#include
//--------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TButton *CloseComm;
TButton *OpenComm;
TButton *SendFile;
TCheckBox *CheckBox1;
TCheckBox *CheckBox2;
TCheckBox *CheckBox3;
TBevel *Bevel1;
TLabel *Label1;
TSpeedButton *SpeedButton1;
TSpeedButton *SpeedButton2;
TSpeedButton *SpeedButton3;
TSpeedButton *SpeedButton4;
TSpeedButton *SpeedButton5;
void __fastcall CloseCommClick(TObject *Sender);
void __fastcall OpenCommClick(TObject *Sender);
void __fastcall SendFileClick(TObject *Sender);
void __fastcall SpeedButton1Click(TObject *Sender);
void __fastcall SpeedButton2Click(TObject *Sender);
void __fastcall SpeedButton3Click(TObject *Sender);
void __fastcall SpeedButton4Click(TObject *Sender);
void __fastcall SpeedButton5Click(TObject *Sender);
private: // User declarations
int __fastcall TForm1::Close_Comm(HANDLE hCommDev);
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
//--------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//--------------------------------------------------------------------
#endif
Użycie __fastcall TForm1:: niewątpliwie przyspiesza wywołanie napisanych przez nas funkcji takich jak Close_Comm(), Read_Comm(), Write_Comm(). Ponadto w ich treści można w prosty sposób z dużą oszczędnością kodu odwoływać się do innych obiektów formularza zadeklarowanych w sekcji __published, zwanych tutaj metodami. Jest to na pewno bardzo wygodne. Pamiętajmy jednak, że zajmujemy się specyficznymi aplikacjami komunikującymi się ze światem zewnętrznym. Nawet niewielka pomyłka programisty, być może mało istotna w innego typu programach (absolutnie nic im nie ujmując), dla nas może okazać się zgubna. Przekonasz się, jak dużo czasu zabiera testowanie takich algorytmów oraz ich konserwacja, która tak naprawdę nigdy się nie kończy. Używając __fastcall TForm1:: jest się niestety skazanym na pracę z dwoma (lub więcej) plikami jednocześnie. Dla Czytelników testujących tego typu programy może to być trochę niewygodne. Wiele osób ceni sobie możliwość szybkiego usunięcia funkcji z programu w momencie, kiedy nie jest już potrzebna jak również możliwość szybkiego zmodyfikowania jej nagłówka lub listy parametrów. Dlatego nowe funkcje będę deklarował "tradycyjnie", używając jedynie konwencji __fastcall, również dla przejrzystości kodu.
Wysyłamy pliki
Nauczyliśmy się wysyłać pojedyncze znaki oraz ich ciągi w postaci jawnie deklarowanych C-łańcuchów. Poznaliśmy też prosty sposób przesłania pliku znak po znaku. Czas abyśmy zobaczyli, jak można je przesyłać i odbierać, wykorzystując bufor transmisyjny oraz inne funkcje Win32 API. Ale zanim przejdziemy do zasadniczego tematu tego fragmentu naszej książki, pragnę zaprezentować pewną strukturę wraz z jej pokrewnymi funkcjami. Jest nią COMMTIMEOUTS. Dodajmy w tym miejscu, że jej znajomość nie jest wcale wymagana do zrealizowania asynchronicznej transmisji szeregowej z poziomu Windows, niemniej jednak zdecydowałem się zamieścić jej opis w tym miejscu, aby zachować ciągłość rozważań na temat interesującej nas warstwy komunikacyjnej Win32 API. Zasoby tej struktury przedstawione są w tabeli 5.10. Udostępniają nam one informacje o tzw. czasach przeterminowania transmisji w trakcie przesyłania danych (ang. time - out of transmission). Jest to ważny termin, z którym niektórzy na pewno już się zetknęli. Pragnę jednak dodać, że o ile rozwiązując problemy związane ze sterowaniem komputerowym, niezbyt często korzysta się z jej usług, o tyle ta znajomość może być przydatna w przypadku realizowania różnego rodzaju przesyłania danych pomiędzy dyskami. W pewnych przypadkach COMMTIMEOUTS determinuje zachowanie się takich funkcji jak ReadFile() czy WriteFile().
Informacje zawarte w strukturze COMMTIMEOUTS

Typ
Element struktury
Właściwości
DWORD
ReadIntervalTimeout
Określa maksymalny czas (milisekundy), pomiędzy pojawieniem się na linii komunikacyjnej dwu znaków. W trakcie wykonywania ReadFile() czas jest liczony od momentu pojawienia się pierwszego znaku. Jeżeli przedział czasu pomiędzy nadejściem dwu znaków przekracza tą wartość, oznacza to, że operacja ReadFile() jest zakończona. Wartość 0 oznacza, że nie ustalono wymaganego okresu pomiędzy nadejściem dwu kolejnych znaków. Przypisanie wartości MAXDWORD powoduje, że czytany znak jest pobierany z bufora natychmiast po pojawieniu się tam. i
DWORD
ReadTotalTimeoutMultiplier
Określa mnożnik (milisekundy) użyty do obliczenia całkowitego przedziału czasu (przeterminowanie) dla operacji czytania (odbioru). Dla wszystkich takich operacji wartość ta jest mnożona przez liczbę bajtów przewidzianą do odebrania z dysku lub łącza komunikacyjnego.
DWORD
ReadTotalTimeoutConstant
Określa stałą (milisekundy) użytą do obliczania czasu przeterminowania operacji czytania. Dla wszystkich takich operacji wartość ta jest dodawana do ReadTotalTimeoutMultiplier i do oczekiwanej liczby nadchodzących bajtów
DWORD
WriteTotalTimeoutMultiplier
Określa mnożnik (milisekundy) użyty do obliczenia całkowitego przedziału czasu (przeterminowanie) dla operacji zapisywania (wysyłania). Dla wszystkich takich operacji wartość ta jest mnożona przez liczbę bajtów przewidzianą do wysłania (zapisywania). 0 oznacza, że nie ustalono czasu przeterminowania dla operacji zapisu na dysku lub do łącza komunikacyjnego.
DWORD
WriteTotalTimeoutConstant
Określa stałą (milisekundy) użytą do obliczania czasu przeterminowania operacji wysyłania. Dla wszystkich takich operacji wartość ta jest dodawana do WriteTotalTimeoutMultiplier oraz do oczekiwanej liczby wysyłanych bajtów. 0 oznacza, że nie ustalono czasu przeterminowania dla operacji zapisu (wysyłania).

Win32 API strukturę tę definiuje jako:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;
...
} COMMTIMEOUTS, *LPCOMMTIMEOUTS;
Definicja ta tworzy dwa nowe słowa kluczowe: COMMTIMEOUTS (struktura) oraz LPCOMMTIMEOUTS (wskaźnik do struktury). Łatwo się domyślić, że aktualne parametry przeterminowania operacji zapisu i odczytu np. z portu komunikacyjnego odczytamy za pomocą funkcji:
BOOL GetCommTimeouts(HANDLE hCommDev, LPCOMMTIMEOUTS lpCommTimeouts);
Własne ustawienia wpiszemy, korzystając z:
BOOL SetCommTimeouts(HANDLE hCommDev, LPCOMMTIMEOUTS lpCommTimeouts);
W obu przypadkach lpCommTimeouts jest wskaźnikiem struktury opisanej w tabeli 5. 10. Najprostszym sposobem użycia przedstawionych wyżej instrukcji jest poniższy fragment kodu:
{
...
COMMTIMEOUTS CommTimeouts;
...
GetCommTimeouts(hCommDev, &CommTimeouts);
CommTimeouts.ReadTotalTimeoutConstant = 1;
CommTimeouts.ReadIntervalTimeout = 1;
CommTimeouts.ReadTotalTimeoutMultiplier = 1;
SetCommTimeouts(hCommDev, &CommTimeouts);
...
}
co oznaczać będzie, że poszczególne znaki powinny być pobierane z bufora komunikacyjnego w odstępach 1 milisekundy. Trzeba jednak w tym miejscu zauważyć, że użyteczność zasobów struktury COMMTIMEOUTS w odniesieniu do aplikacji sterujących nowoczesnym urządzeniem zewnętrznym w pewnych wypadkach może okazać się wątpliwa. Głównym zadaniem takiej aplikacji będzie prawidłowe odczytanie zawartości bufora wejściowego, w którym powinna się znajdować już kompletna informacja pochodząca od przyrządu. W jakim czasie ona się tam znajdzie, decyduje już samo urządzenie. Nasze funkcje Read_Comm() oraz Write_Comm() powinny poradzić sobie z tym problemem bez potrzeby szukania pomocy z zewnątrz, może za wyjątkiem mojej ulubionej WaitCommEvent(). Przejdźmy teraz do zasadniczego tematu obecnych rozważań, czyli do operacji plikowych. Zanim pokażemy przykład ich realizacji, tradycyjnie już powinniśmy zapoznać się z pewnymi bardzo wygodnymi w użyciu funkcjami API. Chociaż nie są one produktem Win32 API, jednak zostały zachowane w 32-bitowym środowisku w celu zapewnienia jego kompatybilności z aplikacjami 16-bitowymi. Zastosowana przez nas metoda przesyłania pliku będzie bardzo prosta: najpierw otworzymy w trybie do czytania wyprany zbiór, potem przeczytamy, umieszczając jednocześnie jego zawartość w buforze komunikacyjnym, zamkniemy go, a następnie przetransmitujemy jego zawartość do wybranego urządzenia. Zacznijmy od otwarcia pliku w trybie do czytania. Czynność tę ułatwi nam funkcja:
HFILE _lopen(LPCSTR lpPathName, int iReadWrite);
Otwiera ona istniejący plik, umieszczając wskaźnik pliku na jego początku. lpPathName jest wskaźnikiem do C-łańcucha, reprezentującego nazwę pliku wraz z pełną ścieżką dostępu. iReadWrite określa tryb, w jakim plik chcemy otworzyć. Mamy następujące możliwości:
OF_READ - otwarcie pliku w trybie do czytania,
OF_READWRITE - otwarcie w trybie do czytania i zapisywania,
OF_WRITE - otwarcie tylko w trybie do zapisywania.
Możliwe jest też otwarcie pliku w tzw. trybie współdzielenia lub akcji:
OF_SHARE_COMPAT - możliwość wielokrotnego otwierania pliku w trybie kompatybilnym z innymi trwającymi obecnie procesami.
OF_SHARE_DENY_NONE - otwarcie pliku bez naruszania innych trwających operacji czytania lub zapisu do pliku.
OF_SHARE_DENY_READ - otwarcie pliku z jednoczesnym usunięciem z niego innych operacji czytania.
OF_SHARE_DENY_WRITE - otwarcie pliku z jednoczesnym usunięciem innych operacji zapisu do niego.
OF_SHARE_EXCLUSIVE - otwarcie pliku w trybie zastrzeżonym z jednoczesnym usunięciem operacji zapisu i odczytu.
Każdą z podanych wyżej wartości bazowych argumentu iReadWrite można, korzystając z działań logicznych, zsumować z odpowiednimi wartościami stałych symbolicznych OF_SHARE_x. Funkcja _lopen() wywołana prawidłowo zwraca identyfikator pliku przechowywany we właściwości HFILE (Handle of File). W przeciwnym wypadku zwraca HFILE_ERROR, reprezentującą błędną wartość przydzielonego do lpPathName identyfikatora. Dane z otwartego już pliku przeczytamy, korzystając z:
UINT _lread(HFILE hFile, LPVOID lpBuffer, UINT uBytes);
Parametr hFile jest identyfikatorem pliku, lpBuffer jest wskaźnikiem do bufora, w którym przechowujemy dane gotowe do wysłania, zaś uBytes jest rozmiarem tego bufora. Wartością zwracaną przez tę funkcję jest liczba bajtów aktualnie przeczytanych, jeżeli jest ona mniejsza od uBytes. Czytanie musi być powtarzane do momentu wykrycia znacznika końca pliku EOF (End of File). W przypadku błędnego wywołania należy spodziewać się wartości HFILE_ERROR. Po otwarciu i przeczytaniu danych każdy plik należy zamknąć. Zrobimy to za pomocą:
HFILE _lclose(HFILE hFile);
W momencie pomyślnego jej wykonania funkcja ta zwróci nam wartość 0, w przeciwnym razie otrzymamy HFILE_ERROR. Patrząc na budowę przedstawionych funkcji, musimy dojść do wniosku, że coś nam one przypominają. To prawda, ReadFile()i CloseFile() są po prostu ich rozwinięciami. Brakuje nam jeszcze odniesienia do WriteFile(). Zaprezentujmy je zatem:
UINT _lwrite(HFILE hFile, LPCSTR lpBuffer, UINT uBytes);
Idąc tym tokiem rozumowania ktoś mógłby pomyśleć, że w takim razie odpowiednikiem CreateFile() będzie np. _lcreat(). W pewnym sensie to prawda, ale nie w przypadku transmisji korzystającej z portów komunikacyjnych. Jeżeli już szukalibyśmy analogii, to znajdziemy ją pod postacią funkcji OpenComm(), nie podtrzymywanej obecnie przez Win32 API. Ogólnie rzecz biorąc, zamiast używanych dotychczas przez nas ReadFile() oraz WriteFile() moglibyśmy równie dobrze posługiwać się dużo prostszymi _lread()lub _lwrite() , w sposób jaki mogą ilustrować to zamieszczone poniżej przykłady zmodyfikowanych funkcji Write_Comm() oraz Read_Comm()wraz z ich możliwymi wywołaniami:
//----------------wysłanie danych-------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev, LPCSTR lpBuffer, UINT uBytes)
{ if (_lwrite((int)hCommDev, lpBuffer, uBytes) != HFILE_ERROR)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
} //-------------------odczyt danych--------------------------------
int __fastcall Read_Comm(HANDLE hCommDev, LPVOID lpBuffer, DWORD Buf_Size)
{
UINT uBytes;
ClearCommError(hCommDev, &Errors, &Stat);
if (Stat.cbInQue > 0)
{ if (Stat.cbInQue > Buf_Size)
uBytes = Buf_Size;
else
uBytes = Stat.cbInQue;
_lread((int)hCommDev, lpBuffer, uBytes);
}
else
uBytes = 0;
return TRUE;
}
//--------------------------------------------------------------------
{
...
Write_Comm(hCommDev, Buffer_O, strlen(Buffer_O));
...
Read_Comm(hCommDev, &Buffer_I[0], sizeof(Buffer_I));
...
}
Identyczną sytuację będziemy mieli, odczytując lub zapisując plik dyskowy. Z powodzeniem można w tym celu wykorzystać uniwersalne ReadFile() oraz WriteFile(). Po tym być może nieco przydługim wstępie nadszedł czas, abyśmy napisali aplikację przenoszącą pliki beztypowe pomiędzy komputerem a innym urządzeniem zewnętrznym. Będziemy ją testować, łącząc się z innym komputerem, na którym uruchomiony jest jeden z terminali. Budowa tej aplikacji siłą rzeczy musi być już bardziej skomplikowana od tych przedstawionych wcześniej. Pliki beztypowe umożliwiają bezpośredni dostęp do ich zawartości, bez potrzeby wnikania w strukturę. Są one kompatybilne, czyli niesprzeczne z innymi plikami, co sprawia, że szczególnie dobrze nadają się do realizacji wszelkich operacji wejścia-wyjścia. Pliki takie, których elementy traktowane są jako ciągi bajtów niezidentyfikowanej struktury, są obecnie powszechnie stosowane do sterowania różnego rodzaju urządzeniami. Projektując aplikację, wykorzystałem trzy komponenty TMemo. W pierwszym z nich wyświetlany będzie początek pliku aktualnie przeczytanego z dysku, zaś jego całość zostanie umieszczona w buforze wyjściowym. Zawartość Memo2 pokazywać będzie początek pliku aktualnie transmitowanego, zaś Memo3 odpowiedź drugiego komputera, również w formie jakiegoś pliku. Często postępujemy w ten sposób, że przed wysłaniem jeszcze raz oglądamy początek zbioru, aby upewnić się, czy rzeczywiście jest to ten plik, o który nam chodzi. Jeżeli chcielibyśmy obejrzeć cały zbiór, trzeba we właściwy sposób ustalić rozmiary odpowiednich buforów. Do zaprojektowania formularza użyłem ponadto pojedynczych komponentów TDriveComboBox, TEdit, TDirectoryListBox, TFileListBox . Posłużą one do szybkiego określenia konkretnego zbioru danych. Aby pracowały one tak, jak w każdym standardowym dialogu Windows służącym do wyboru pliku, ich cechy należy ze sobą związać. Dla wygody uczynimy to w funkcji tworzącej nasz formularz:
void __fastcall TForm1::FormCreate(TObject *Sender)
{
DirectoryListBox1->FileList = FileListBox1;
DriveComboBox1->DirList = DirectoryListBox1;
FileListBox1->FileEdit = Edit1;
}
W kolejnym etapie musimy zaprojektować funkcję obsługi zdarzenia FileListBox1Change(). Zrobimy to następująco:
//-------------odczyt pliku z dysku---------------------------
void __fastcall TForm1::FileListBox1Change(TObject *Sender)
{
memset(Buffer_O, 0, cbOutQueue);
hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ);
if (hfile_s != HFILE_ERROR) _lread(hfile_s, &Buffer_O[0], cbOutQueue);
for (int i = 0; i <= cbOutQueue-1; i++)
if (Buffer_O[i] == NULL)
Buffer_O[i] = '.';
Memo1->Text = Buffer_O;
Memo2->Text = Buffer_O;
_lclose(hfile_s);
}
Oczywiście, jeżeli ktoś nie zgadza się z przedstawionym tu stylem programowania, równie dobrze kod powyższego zdarzenia może zapisać, korzystając wyłącznie ze standardowych funkcji Win32 API:
void __fastcall TForm1::FileListBox1Change(TObject *Sender)
{ HANDLE hfile_s; // identyfikator pliku źródłowego
DWORD dwSize; // liczba czytanych bajtów
memset(Buffer_O, 0, cbOutQueue);
// -- używamy funkcji Win32 API do czytania pliku -
hfile_s = CreateFile(FileListBox1->FileName.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hfile_s != INVALID_HANDLE_VALUE) ReadFile(hfile_s, &Buffer_O[0], sizeof(Buffer_O), &dwSize, NULL);
for (int i = 0; i <= cbOutQueue-1; i++)
if (Buffer_O[i] == NULL)
Buffer_O[i] = '-';
Memo1->Text = Buffer_O;
Memo2->Text = Buffer_O;
CloseHandle(hfile_s);
}
Budowa funkcji obsługi zdarzenia polegającego na wczytaniu wybranego pliku z dysku wymaga komentarza. Użyliśmy funkcji memset(), aby zapisać w buforze wyjściowym znak 0. Krótko mówiąc wyczyściliśmy wyjściowy bufor danych przed ulokowaniem tam informacji. Przypomnijmy w tym miejscu jej definicję:
void *memset(void *s, int c, size_t n);
lub
LPVOID memset(LPVOID s, int c, size_t n);
Funkcja ta umieszcza mniej znaczący bajt argumentu c w pierwszych n znakach tablicy s. Następnie korzystamy z metody c_str() zwracającej wskaźnik (char *) do pierwszego znaku C-łańcucha identyfikującego właściwość FileName obiektu FileListBox1: FileListBox1->FileName.c_str(), po czym kojarzymy z nim identyfikator pliku źródłowego hfile_s (source). Sprawdzając, czy przydzielony identyfikator nie jest pusty (lub błędny), wczytujemy wybrany plik do bufora wyjściowego Buffer_O (jeżeli bufor jest mniejszy niż rozmiar pliku, zobaczymy oczywiście tylko jego fragment). Zauważmy, że nie ma tu żadnej pętli. Dalsze instrukcje powodują zastąpienie kropkami lub kreskami ewentualnie wolnego obszaru bufora (wykonaliśmy to jedynie dla celów estetycznych). Następnie do cechy Text komponentów Memo1 oraz Memo2 wczytamy zawartość bufora. Zamknięcie pliku kończy działanie funkcji obsługi zdarzenia FileListBox1Change(). Jedyną jego rolą jest wyświetlenie wybranego przez nas zbioru. Oczywiście, że już w tym miejscu moglibyśmy wysłać zaznaczony plik, ale z praktycznego punktu widzenia zawsze lepiej jeszcze raz obejrzeć to, co mamy do wysłania. Odrobina ostrożności czasami się opłaca, być może w ostatniej chwili trzeba będzie coś jeszcze zmodyfikować? W następnym podrozdziale pokażemy, jak to zrobić. Najważniejszym fragmentem aplikacji będzie funkcja obsługi zdarzenia SendClick(), które jest skojarzone z przyciskiem Wyślij. Wykorzystamy wskaźnik postępu TProgessBar, aby mieć możliwość śledzenia operacji wysyłania pliku. W takich przypadkach dobrze jest, jeżeli program daje nam znać, że coś się dzieje. Jego maksymalny rozmiar będzie odpowiadał rozmiarowi pliku, który otrzymamy, korzystając z bardzo pożytecznej funkcji Win32 API GetFileSize():
DWORD GetFileSize(HANDLE hFile, LPDWORD lpFileSizeHigh);
Gdzie hFile jest identyfikatorem otwartego pliku, lpFileSizeHigh jest wskaźnikiem do 32-bitowej zmiennej reprezentującej rozmiar pliku. Wskaźnik postępu uczynimy aktywnym, korzystając z jego własności StepIt(). Przypatrzmy się teraz pętli while, w której pobieramy plik z dysku, umieszczając go jednocześnie bajt po bajcie w buforze wyjściowym, by w efekcie przetransmitować go za pomocą dobrze już nam znanej funkcji Write_Comm(). Zauważmy tu bardzo ważną rzecz - plik transmitujemy bajt po bajcie! Chociaż standardowa wielkość bloku transmisji (rekordu) do bufora może wynieść nawet 4 kB, to w transmisji szeregowej zalecaną wielkością jest 1 bajt. Cóż, każdy plik składa się z całkowitych wielokrotności tej liczby. Jeżeli ktoś ma wątpliwości, może ustalić rozmiar transferowanego bloku danych np. na 128 bajtów. Trzeba wówczas wykazać się dużą dozą cierpliwości aby zobaczyć końcowy efekt transferu danych choćby na sąsiednim komputerze. Treść funkcji obsługi zdarzenia wysyłającego plik zamieszczona jest poniżej.
void __fastcall TForm1::SendClick(TObject *Sender)
{
DWORD FileSizeHigh;
ProgressBar1->Max=0;
...
memset(Buffer_O, 0, cbOutQueue);
if ((_lopen(FileListBox1->FileName.c_str(), OF_READ)) != HFILE_ERROR)
{
hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ); ProgressBar1->Max=GetFileSize((HANDLE)hfile_s, &FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1)) // przeczytanie 1
// bajta i umieszczenie go
// w buforze wyjściowym
Write_Comm(hCommDev, 1); // transmisja 1 bajta
ProgressBar1->StepIt();
}
_lclose(hfile_s);

}
...
}
Sposób odbioru przychodzących do naszego portu znaków będzie analogiczny do tego, jaki zaprezentowaliśmy przy okazji odbioru łańcuchów.
Kod aplikacji realizującej transmisję plików
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//------RS_05.cpp----------
#include
#pragma hdrstop
#include "RS_05.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 1024 //rozmiar bufora danych wyjściowych
#define cbInQueue 1024 //rozmiar bufora danych wejściowych

//--------------------------------------------------------------------
TForm1 *Form1;

HFILE hfile_s; // identyfikator pliku źródłowego
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
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev, DWORD nNumberOfBytesToWrite)
{ DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0], nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL) > 0)
{ WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev, 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, &Buffer_I[0], nNumberOfBytesToRead, lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
DirectoryListBox1->FileList = FileListBox1;
DriveComboBox1->DirList = DirectoryListBox1;
FileListBox1->FileEdit = Edit1;
}
//------------wstępny odczyt pliku z dysku----------------------------
void __fastcall TForm1::FileListBox1Change(TObject *Sender)
{
memset(Buffer_O, 0, cbOutQueue);
hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ);
if (hfile_s != HFILE_ERROR)
_lread(hfile_s, &Buffer_O[0], cbOutQueue);
for (int i = 0; i <= cbOutQueue - 1; i++)
if (Buffer_O[i] == NULL)
Buffer_O[i] = '.';
Memo1->Text = Buffer_O;
Memo2->Text = Buffer_O;

_lclose(hfile_s);
}
//-----------------zamknięcie portu i aplikacji-----------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Close_Comm(hCommDev);
Application->Terminate();
}
//---------------inicjalizacja portu----------------------------------
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);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE) // wybór prędkości
dcb.BaudRate = CBR_1200; // transmisji
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_19200;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE; // sprawdzanie parzystości
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
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);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
break;
};
}
}
//----------------wysłanie pliku--------------------------------------
void __fastcall TForm1::SendClick(TObject *Sender)
{
DWORD FileSizeHigh;
ProgressBar1->Max = 0;
if (hCommDev > 0)
{ memset(Buffer_O, 0, cbOutQueue);
if ((_lopen(FileListBox1->FileName.c_str(),OF_READ)) != HFILE_ERROR)
{ hfile_s = _lopen(FileListBox1->FileName.c_str(), OF_READ); ProgressBar1->Max=GetFileSize((HANDLE)hfile_s, &FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1))
{
Write_Comm(hCommDev, 1); // wysłanie 1 bajta
ProgressBar1->StepIt();
}
_lclose(hfile_s);
FlushFileBuffers(hCommDev);
}
else
MessageBox(NULL, "Nie wybrano pliku do transmisji.", "Błąd !", MB_OK);
}
else
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
}
//----------------------odbiór pliku----------------------------------
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
memset(Buffer_I, 0, cbInQueue);
PurgeComm(hCommDev, PURGE_TXABORT);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0) // jeżeli odebrano jakieś bajty
Memo3->Text = Buffer_I;
for (int i = 0; i <= cbInQueue - 1; i ++)
if (Buffer_I[i] == NULL)
Buffer_I[i] = '.';
Memo3->Text = Buffer_I;
}
//--------------------------------------------------------------------
Spoglądając jeszcze raz na powyższe zapisy, zwróćmy uwagę, że już na tym etapie wprowadziliśmy szereg zabezpieczeń w postaci komunikatów uniemożliwiających wysłanie nieistniejącego lub nie otwartego zbioru danych. Testując powyższy program wysłałem do sąsiedniego komputera, na którym uruchomiony był Terminal dla Win16 pewien plik *.cpp. W odpowiedzi kolega przysłał mi zbiór bootlog.txt. Format jego wyświetlania nie jest może zbyt zachęcający, ale chcę byśmy poznali właściwości kilku komponentów, za pomocą których można oglądać przychodzące informacje. Czytając pliki z dysku można oczywiście równie dobrze posłużyć się rodziną funkcji FileCreate(), FileOpen(), FileSeek(), FileRead(), FileClose(), FileWrite().
Wykorzystanie komponentu TTimer
Być może niektórzy poczuli się nieco zawiedzeni faktem przedstawienia w poprzednim akapicie tylko jednego programu obsługującego pliki. Mimo iż obecny fragment pracy ma być poświęcony komponentowi TTimer, to postaramy się przy tej okazji przemycić jeszcze parę cennych informacji na temat operacji plikowych. Zaczniemy trochę przewrotnie. Pokażemy, jak można określić czas przybycia do naszego portu początku jakiejś większej porcji informacji, próbkując wybrane łącze. Zbudujemy tym razem już pokaźną aplikację. Dwa komponenty - pola edycji TRichEdit - pełnić będą rolę obszarów, w których będziemy wyświetlać dane lub pliki przeznaczone od wysłania oraz otrzymane informacje. Komponenty TOpenDialog, TSaveDialog oraz TMainMenu zapewnią naszej aplikacji wygodne otwieranie i zapisywanie plików. Komponent TTimer umożliwi czasowe próbkowanie wybranego portu szeregowego. Siedem przycisków typu TSpeedButton zgrupowanych w obszarze określonym komponentem TCoolBar pełnić będzie funkcje pomocne w edycji pliku. Dzięki pierwszemu z nich, nazwanemu przeze mnie FileOpen, będziemy mogli wybrać i otworzyć dany plik. Z przyciskiem tym skojarzona jest funkcja obsługi zdarzenia FileOpenClick(). Przyciskowi FileSave odpowiada zdarzenie FileSaveClick(), za pomocą którego będziemy mogli zapisać w postaci pliku dane lub informacje wprowadzane przez nas z klawiatury w obszarze komponentu RichEdit1. Zwrócimy uwagę, że aplikacja ta umożliwi nam już nie tylko wysyłanie plików, ale również danych wprowadzanych aktualnie z klawiatury. Następne przyciski zgrupowane w obszarze danych wysyłanych pełnić będą funkcje uproszczonego edytora IDE. Jak wszystkim wiadomo, edytor taki pozwala wykonywać działania na blokach tekstowych polegające na ich usuwaniu, przenoszeniu lub kopiowaniu w inne miejsce. Aby przekopiować blok tekstu, należy go najpierw zaznaczyć. Treść funkcji obsługi zdarzenia CopyTextClick() jest bardzo prosta:
RichEdit1->CopyToClipboard();
Użycie właściwości CopyToClipboard obiektu TRichEdit spowoduje przekopiowane zaznaczonego fragmentu tekstu do schowka (ang. clipboard). Można go potem wstawić w inne miejsce, korzystając z przycisku PasteText, z którym skojarzona jest funkcja obsługi zdarzenia PasteTextClick(). Zaznaczony fragment można też usunąć, naciskając przycisk CutText, odpowiadający wywołaniu zdarzenia CutTextClick(). Każdy tego typu program powinien mieć możliwość czyszczenia buforów komunikacyjnych. Zadanie to wykonamy, wywołując funkcję obsługi zdarzenia CleanBuffersClick():
void __fastcall TForm1::CleanBuffersClick(TObject *Sender)
{
for (int i = 0; i <= cbInQueue - 1; i ++)
{
Buffer_I[i] = NULL;
RichEdit1->Text = Buffer_I;
}
for (int i = 0; i <= cbOutQueue - 1; i ++)
{
Buffer_O[i] = NULL;
RichEdit2->Text = Buffer_O;
}
// memset(Buffer_O, 0, cbOutQueue);
// memset(Buffer_I, 0, cbInQueue);
ProgressBar1->Max = 0;
}
Należy zwrócić uwagę, że samo użycie funkcji memset() zapewni nam jedynie umieszczenie wartości 0 w danym buforze, nie spowoduje natomiast usunięcia znaków z pola edycji RichEdit2. Aby uniknąć tego typu dwuznacznych sytuacji, bardzo często stosuje się konstrukcje takie jak przedstawiono powyżej. Korzystając z prostej pętli for , do bufora jawnie wpisujemy NULL i taki "poprawiony" bufor przypisujemy cesze Text danego komponentu. Można oczywiście, w zależności od potrzeb, zdarzenie takie rozbić na dwa oddzielne, polegające na osobnym czyszczeniu buforów. W tym przykładzie jedynie dla prostoty zapisu oba bufory komunikacyjne czyszczę jednocześnie. Dla wygody Użytkownika wszystkie powyższe zdarzenia zostały zdublowane w treści komponentu TMainMenu. W obszarze danych odbieranych umieszczony został przycisk typu TSpeedButton, nazwany przez nas ReceiveFileSave, z którym skojarzona jest funkcja obsługi zdarzenia ReceiveFileSaveClick(), zapewniającego możliwość niezależnego zapisania na dysku danych odbieranych z portu szeregowego. Należy jednak pamiętać, że odebrana informacja zostanie zapisana w formacie Rich Text Format! 4Wszystkie początkowe ustawienia wykorzystywanych przez nas komponentów dialogowych, komponentu TTimer oraz opisy poszczególnych przycisków (cechy Hint) umieścimy w głównej funkcji naszego formularza:
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenDialog1->InitialDir = ExtractFilePath(ParamStr(0));
OpenDialog1->Filter =
"*.dat, *.txt, *.cpp, *.c |*.dat; *.txt; *.cpp; *.c";
SaveDialog1->InitialDir = OpenDialog1->InitialDir;
SaveDialog1->Filter = "*.*|*.*";

Timer1->Enabled = FALSE;
Timer1->Interval = TIMER_INTERVAL; // przedział czasu
// próbkowania łącza
CheckComm->Enabled = FALSE;

FileOpen->Hint = "Otwórz plik";
FileOpen->ShowHint = TRUE;
FileSave->Hint = "Zapisz";
FileSave->ShowHint = TRUE;
CopyText->Hint = "Kopiuj";
CopyText->ShowHint = TRUE;
PasteText->Hint = "Wklej";
PasteText->ShowHint = TRUE;
CutText->Hint = "Wytnij";
CutText->ShowHint = TRUE;
CleanBuffers->Hint = "Wyczyść bufory";
CleanBuffers->ShowHint = TRUE;
ReceiveFileSave->Hint = "Zapisz otrzymane";
ReceiveFileSave->ShowHint = TRUE;
}
Większość użytych przycisków pełnić będzie taką samą rolę jak we wcześniejszych programach, dlatego nie będziemy w tym miejscu szczegółowo omawiać ich znaczenia. Zastanowimy się jednak dokładniej nad parą zdarzeń ReceiveClick() (przycisk Odbierz, którego cechę Name określiłem jako Receive) oraz CheckCommClick() (przycisk Monitoruj łącze - CheckComm). Naciskając Odbierz, powodujemy wywołanie funkcji obsługi zdarzenia:
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
if (hCommDev > 0)
{
...
CheckComm->Enabled = TRUE;
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_RXCHAR);
bResult = Read_Comm(hCommDev, &Buffer_I[0], &Number_Bytes_Read, sizeof(Buffer_I));
if (bResult && Number_Bytes_Read != 0)
{
RichEdit2->Text = Buffer_I;
Edit2->Text= " Dane zostały przetransferowane.";
}
}
...
}
Możemy albo spokojnie czekać, aż coś się pojawi w buforze wejściowym, albo po uaktywnieniu przycisku Monitoruj łącze cyklicznie je próbkować w poszukiwaniu pierwszego znaku, który się tam znajdzie. Teraz już wiadomo, dlaczego wcześniej użyliśmy maski EV_RXCHAR. Po wybraniu monitoringu łącza aplikacja zapyta, czy naprawdę chcemy to wykonać. Jeżeli potwierdzimy, spowodujemy załączenie funkcji obsługi zdarzenia TimerOnTimer() oraz wyświetlenie odpowiedniego komunikatu w komponencie Edit2. W przeciwnym wypadku przycisk Monitoruj łącze pozostanie wygaszony i Timer będzie nieaktywny. Załóżmy, że nacisnęliśmy OK.
void __fastcall TForm1::CheckCommClick(TObject *Sender)
{
if (Application->MessageBox(" Łącze będzie monitorowane do czasu" " odebrania znaku." , "Uwaga!", MB_OKCANCEL) != IDOK)
{
CheckComm->Enabled = FALSE;
Timer1->Enabled = FALSE;
Abort();
}
else
{
Timer1->Enabled = TRUE; // uaktywnia czasowe próbkowanie
// łącza
Edit2->Text = "Łącze jest monitorowane.";
}
...
}
Uaktywnimy tym samym czasowe, w odstępach 1 milisekundy, wyzwalanie zdarzenia:
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
if (WaitCommEvent(hCommDev, &fdwEvtMask, NULL) > 0)// sprawdza czy
{ // nadszedł znak
Beep();
Edit2->Text = " Transfer danych.";
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
Timer1->Enabled = FALSE;
CheckComm->Enabled = FALSE;
}
}
Będzie ono aż do skutku cyklicznie sprawdzać, czy do portu identyfikowanego przez hCommDev przyszedł jakiś znak. Wykorzystałem tu znaną już, niezwykle pożyteczną funkcję WaitCommEvent() z uprzednio wybraną dla fdwEvtMask stałą EV_RXCHAR. Jeżeli widoczny powyżej warunek będzie spełniony, aplikacja da nam sygnał dźwiękowy, że coś już jest w buforze. Można oczywiście w miejscu Beep() umieścić jakiś komunikat, np. w stylu MessageBox(). Należy przy tym pamiętać, aby w takiego typu konstrukcjach warunkowych zawsze dla funkcji WaitCommEvent() ustalać nową maskę, najlepiej EV_TXEMPTY. W przeciwnym razie będziemy mieli kłopoty z wysłaniem czegokolwiek. Jeżeli po usłyszeniu sygnału (lub zobaczeniu innego komunikatu) naciśniemy powtórnie Odbierz, będziemy mogli obejrzeć otrzymane dane i ewentualnie zapisać je do pliku. Ten typ próbkowania łącza musi się sam wyłączać, dlatego w zapisie funkcji obsługi zdarzenia TimerOnTimer() umieściłem instrukcję:
Timer1->Enabled = FALSE;
Trzeba zawsze o tym pamiętać, gdyż w przeciwnym razie bardzo łatwo zawiesimy program (i to dość poważnie). Ujmując rzecz ogólnie, należy mieć sporo wyczucia przy korzystaniu z niezwykle użytecznego Timera. Kod aplikacji realizującej transmisję plików, wykorzystującej komponent TTimer
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//--------RS_06.cpp-----
#include
#pragma hdrstop
#include "RS_06.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 1024 //rozmiar bufora danych wyjściowych
#define cbInQueue 1024 //rozmiar bufora danych wejściowych
#define TIMER_INTERVAL 1 //przedział czasu próbkowania Timera
TForm1 *Form1;
AnsiString New_File; // przechowuje nazwę pliku
HFILE hfile_s; // identyfikator pliku
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_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
BOOL bResult ; // zmienna boolowska

//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev, DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
if (WriteFile(hCommDev, &Buffer_O[0],
nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev, 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, &Buffer_I[0], nNumberOfBytesToRead, lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
}
//-------------------------------------------------------------------- void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Timer1->Enabled = FALSE;
CheckFileSave();
Close_Comm(hCommDev);
Application->Terminate();
}
//--------------------------------------------------------------------
void __fastcall TForm1::CheckFileSave(void)
{
if (RichEdit1->Modified)
{
switch(MessageBox(NULL, "Zawartość pliku lub okna została" " zmieniona. Zapisać zmiany?", "Uwaga!", MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES : FileSaveClick(this);
case ID_CANCEL : Abort();
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenDialog1->InitialDir = ExtractFilePath(ParamStr(0));
OpenDialog1->Filter = "*.dat , *.txt, *.cpp, *.c | *.dat; *.txt; *.cpp; *.c";
SaveDialog1->InitialDir = OpenDialog1->InitialDir;
SaveDialog1->Filter = "*.*|*.*";
Timer1->Enabled = FALSE;
Timer1->Interval = TIMER_INTERVAL;
CheckComm->Enabled = FALSE;

FileOpen->Hint = "Otwórz plik.";
FileOpen->ShowHint = TRUE;
FileSave->Hint = "Zapisz.";
FileSave->ShowHint = TRUE;
CopyText->Hint = "Kopiuj.";
CopyText->ShowHint = TRUE;
PasteText->Hint = "Wklej.";
PasteText->ShowHint = TRUE;
CutText->Hint = "Wytnij.";
CutText->ShowHint = TRUE;
CleanBuffers->Hint = "Wyczyść bufory.";
CleanBuffers->ShowHint = TRUE;
ReceiveFileSave->Hint = "Zapisz otrzymane.";
ReceiveFileSave->ShowHint = TRUE;

}
//--------------------------------------------------------------------
void __fastcall TForm1::FileOpenClick(TObject *Sender) {
CheckFileSave();

if (OpenDialog1->Execute())
{
RichEdit1->Lines->LoadFromFile(OpenDialog1->FileName);
RichEdit1->Modified = FALSE; RichEdit1->ReadOnly = OpenDialog1->Options.Contains(ofReadOnly);
}

}
//--------------------------------------------------------------------
void __fastcall TForm1::FileSaveClick(TObject *Sender)
{
if (! strcmp(New_File.c_str(), LoadStr(256).c_str()))
SaveAs1Click(Sender);
else
{
RichEdit1->Lines->SaveToFile(New_File);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::CopyTextClick(TObject *Sender)
{
RichEdit1->CopyToClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::PasteTextClick(TObject *Sender)
{
RichEdit1->PasteFromClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::CutTextClick(TObject *Sender)
{
RichEdit1->CutToClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::UndoClick(TObject *Sender)
{
if (RichEdit1->HandleAllocated()) SendMessage(RichEdit1->Handle, EM_UNDO, 0, 0);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SelectAllClick(TObject *Sender)
{
RichEdit1->SelectAll();
}
//--------------------------------------------------------------------
void __fastcall TForm1::CleanBuffersClick(TObject *Sender) {
for (int i = 0; i <= cbInQueue - 1; i ++)
{
Buffer_I[i] = NULL;
RichEdit1->Text = Buffer_I;
}
for (int i = 0; i <= cbOutQueue - 1; i ++)
{
Buffer_O[i] = NULL;
RichEdit2->Text = Buffer_O;
}
//memset(Buffer_O, 0, cbOutQueue);
//memset(Buffer_I, 0, cbInQueue);
ProgressBar1->Max = 0;

}
//--------------------------------------------------------------------
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);
GetCommState(hCommDev, &dcb);
if (CheckBox3->Checked == TRUE) // wybór prędkości
dcb.BaudRate = CBR_1200;
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_19200;

dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych

//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
dcb.EofChar = FALSE;

SetCommState(hCommDev, &dcb);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendFileClick(TObject *Sender)
{
DWORD FileSizeHigh;

CheckComm->Enabled = FALSE;
ProgressBar1->Max = 0;

if ((_lopen(OpenDialog1->FileName.c_str(), OF_READ))!= HFILE_ERROR)
{
hfile_s =_lopen(OpenDialog1->FileName.c_str(), OF_READ );
ProgressBar1->Max = GetFileSize((HANDLE)hfile_s, &FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1))
{
Write_Comm(hCommDev, 1);
ProgressBar1->StepIt();
}
_lclose(hfile_s);

FlushFileBuffers(hCommDev);
}
else
MessageBox(NULL, "Nie wybrano pliku do transmisji.", "Błąd !", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendWrittenClick(TObject *Sender)
{
if (hCommDev > 0)
{
CheckComm->Enabled = FALSE;
try
{
strcpy(Buffer_O, RichEdit1->Lines->Text.c_str());
ProgressBar1->Max = 0;
ProgressBar1->Max = sizeof(RichEdit1->Text.c_str());
Write_Comm(hCommDev, strlen(Buffer_O));
ProgressBar1->StepIt();
FlushFileBuffers(hCommDev);
}
catch (...)
{
MessageBox(NULL, " Próba nadpisywania na pliku " " wykorzystywanym przez inny proces." " Uruchom ponownie aplikację. ", " Błąd transmisji ", MB_OK);
}
} else MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
if (hCommDev > 0)
{
CheckComm->Enabled = TRUE;
RichEdit2->Clear();
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_RXCHAR );
bResult = Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (bResult && Number_Bytes_Read != 0)
{ RichEdit2->Text = Buffer_I;
Edit2->Text= " Dane zostały przetransferowane.";
}
}
else
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);

}
//--------------------------------------------------------------------
void __fastcall TForm1::OpenClick(TObject *Sender)
{
CheckFileSave();

if (OpenDialog1->Execute())
{
RichEdit1->Lines->LoadFromFile(OpenDialog1->FileName);
RichEdit1->Modified = FALSE;
RichEdit1->ReadOnly = OpenDialog1->Options.Contains(ofReadOnly);
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SaveAs1Click(TObject *Sender)
{
if (SaveDialog1->Execute()) // dane będą zapisywane w
// formacie Rich!
{
RichEdit1->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
if (WaitCommEvent(hCommDev, &fdwEvtMask, NULL) > 0)// sprawdza czy
{ // nadszedł znak
Beep(); Edit2->Text = " Transfer danych.";
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
Timer1->Enabled = FALSE;
CheckComm->Enabled = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveFileSaveClick(TObject *Sender)
{
if (SaveDialog1->Execute())
{
RichEdit2->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit2->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::NewClick(TObject *Sender)
{
CheckFileSave();
RichEdit1->Lines->Clear();
RichEdit1->Modified = FALSE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::CheckCommClick(TObject *Sender)
{
if (Application->MessageBox(" Łącze będzie monitorowane do czasu" " odebrania znaku." , "Uwaga!", MB_OKCANCEL) != IDOK)
{
CheckComm->Enabled = FALSE;
Timer1->Enabled = FALSE;
Abort();
}
else
{
Timer1->Enabled = TRUE;
Edit2->Text = "Łącze jest monitorowane.";
}
/*if (MessageDlg(" Łącze będzie monitorowane do czasu odebrania"
" znaku.", mtConfirmation,
TMsgDlgButtons() << mbYes << mbNo, 0) == mrYes)
{
Timer1->Enabled = TRUE;
Edit2->Text = "Łącze jest monitorowane.";
}
else
{
CheckComm->Enabled = FALSE;
Timer1->Enabled = FALSE;
Abort();
}*/

}
//--------------------------------------------------------------------
Opisany program testowałem, łącząc się z pewnym w pełni zautomatyzowanym urządzeniem pomiarowym. Wysłałem z uprzednio przygotowanego standardowego pliku zapytanie o aktualną krzywą skalowania, jaką posługuje się ten przyrząd. W odpowiedzi miernik przysłał mi wszystkie niezbędne informacje. Mając dwa okna edycji mogę, spoglądając na otrzymane, dane wpisać w RichEdit1 swoje własne parametry nowej krzywej skalującej. Naciskając przycisk Wyślij wpisane, skojarzony z funkcją obsługi zdarzenia SendWrittenClick(), odpowiednio przeprogramuję miernik. Można go oczywiście testować łącząc się z innym komputerem, odbierając pliki lub pojedyncze znaki. Na powyższym przykładzie przedstawiono też różne sposoby wywoływania komunikatów Win32 API, takich jak: MessageBox() czy MessageDlg() oraz zaprezentowano ideę obsługi wyjątków pokazaną w funkcji obsługi zdarzenia SendWrittenClick(). Wszystko, co powiedzieliśmy na temat sposobów wyszukiwania początku ciągu znaków przychodzących do łącza, jest niewątpliwie pożyteczne, niemniej jednak w większości przypadków, z którymi spotykamy się w praktyce, bardziej interesuje nas możliwość cyklicznego odczytywania wskazań określonego przyrządu pomiarowego. Załóżmy, że chcemy mieć możliwość bieżącego odczytywania napięcia, natężenia prądu czy chociażby temperatury. Konstruując tego typu aplikacje, powinniśmy przewidzieć możliwość wyboru przedziału czasu próbkowania sygnałów, pojawiających się na wejściu wybranego portu szeregowego w czasie działania programu. Dosyć dobrze do tego celu nadaje się komponent TCSpinEdit. Dzięki odpowiedniemu wyborowi jego cechy Value będziemy mogli automatycznie dostosować do naszych potrzeb wartość cechy Interval (odstęp) komponentu TTimer. Pamiętamy, że odstęp czasu, w którym dokonujemy próbkowania łącza szeregowego, podajemy w milisekundach. Przykładowa aplikacja, za pomocą której będzie można odczytywać aktualne wskazania przyrządu, zbudowana będzie ze znanych nam już elementów. Dane odbierane wyświetlać będziemy za pomocą pola edycji TRichEdit. Aplikacja zaopatrzona będzie dodatkowo w przycisk uruchamiający pomiar ciągły i wyłączający go. W funkcji obsługi zdarzenia MeasureONClick(), które wywołujemy naciśnięciem przycisku Włącz pomiar, mamy:
void __fastcall TForm1::MeasureONClick(TObject *Sender)
{
if (hCommDev > 0) // powtórnie sprawdza czy port jest otwarty
{
strcpy(Buffer_O, query);
Timer1->Enabled = TRUE;
}
...
}

Rozkaz wysyłany do miernika zostanie skopiowany do obszaru pamięci wskazywanego przez Buffer_O. Ponadto wywołamy tu cyklicznie funkcję obsługi zdarzenia TimerOnTimer(), próbkującego wybrany port szeregowy:
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
Write_Comm(hCommDev, strlen(Buffer_O));
Sleep(800);
Beep();
FlushFileBuffers(hCommDev);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0)
RichEdit1->Text = Buffer_I;
}
Jedyną rolą tej funkcji jest wysłanie zapytania do urządzenia i odebranie odpowiedzi. Czynność ta może być wykonywana wielokrotnie, bez jakichkolwiek ograniczeń czasowych. Użytkownik sam decyduje, kiedy zakończyć pomiar, naciskając przycisk Wyłącz pomiar. Oczywiście, odebrane dane z reguły należy odpowiednio zapisać na dysku i dobrze by było, gdybyśmy mieli możliwość założenia i otwarcia pliku jeszcze przed rozpoczęciem pomiarów. Myślę jednak, że wszystko to, co powiedzieliśmy do tej pory na temat operacji plikowych, w zupełności wystarczy nawet mniej wprawnemu Czytelnikowi, aby poradził sobie z problemem. Dane będą musiały być zapisywane "on line" (czyli w trakcie), gdyż zastosowany przeze mnie sposób wywoływania funkcji Write_Comm(), wysyłającej zapytanie do urządzenia oraz Read_Comm()i czytającej odpowiedź miernika wyklucza jakiekolwiek dalsze buforowanie danych ponad to, co aktualnie wyświetlamy na ekranie. W tego typu programach nigdy nie stosuje się jakiegoś szczególnego sposobu przechowywania danych w pamięci. Powód jest prosty: nigdy nie wiemy, ile informacji tak naprawdę otrzymamy. Pomiar równie dobrze może trwać pięć sekund jak i pięć lub piętnaście godzin
Kod aplikacji próbkującej wybrane łącze szeregowe w z góry zadanych odstępach czasu w poszukiwaniu aktualnych wskazań miernika cyfrowego:
//--- kompilować z borlndmm.dll cc3250mt.dll -----------------------
//----RS_07.cpp-------------

#include
#pragma hdrstop
#include "RS_07.h"
#pragma package(smart_init)
#pragma link "CSPIN"
#pragma resource "*.dfm"

#define cbOutQueue 16 //rozmiar bufora danych wyjściowych
#define cbInQueue 16 //rozmiar bufora danych wejściowych

TForm1 *Form1;

LPCTSTR query = "CDAT?\r\n"; // przykładowe zapytanie o
// temperaturę, zakończone parą
// znaków CR LF
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_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 //--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev, DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0], nNumberOfBytesToWrite, &NumberOfBytesWritten , NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev, 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, &Buffer_I[0], nNumberOfBytesToRead, lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
Timer1->Enabled = FALSE;
Close_Comm(hCommDev);
Application->Terminate();
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
Timer1->Enabled = FALSE;
CSpinEdit1->Value = 100;
CSpinEdit1->ReadOnly = FALSE;
CSpinEdit1->Cursor = crNo;
CSpinEdit1->Hint = "Ręczne wpisywanie może być niebezpieczne !";
CSpinEdit1->ShowHint = TRUE;
CSpinEdit1->Increment = 100;
}
//--------------------------------------------------------------------
void __fastcall TForm1::CSpinEdit1Change(TObject *Sender)
{ if (CSpinEdit1->Value < 0) // uniemożliwia ustalenie wartości
// ujemnej
CSpinEdit1->Value = abs(CSpinEdit1->Value);
Timer1->Interval = CSpinEdit1->Value;
}
//--------------------------------------------------------------------
void __fastcall TForm1::MeasureOFFClick(TObject *Sender)
{
Timer1->Enabled = FALSE;
}
//--------------------------------------------------------------------
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);
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;
dcb.Parity = ODDPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 7; // bity danych
//-przykładowe ustawienia flag sterujących DCB-
dcb.fParity = TRUE; // sprawdzanie parzystości
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
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);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::MeasureONClick(TObject *Sender)
{
if (hCommDev > 0) // powtórnie sprawdza czy port jest otwarty
{
strcpy(Buffer_O, query);
Timer1->Enabled = TRUE;
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//--------------------------------------------------------------------
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{
Write_Comm(hCommDev, strlen(Buffer_O));
Sleep(100);
Beep();
FlushFileBuffers(hCommDev);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0) RichEdit1->Text = Buffer_I;
}
//--------------------------------------------------------------------
Śledząc kod powyższego wydruku, można zauważyć, że program ten obsługuje miernik, który w odpowiedzi na komendę-zapytanie CDAT?\r\n automatycznie dokonuje odpowiedniego pomiaru, dodatkowo sygnalizując ten fakt brzęczkiem. W tym przykładzie przedział czasu odczytu z łącza ustaliłem na 1000 milisekund. Można oczywiście robić to szybciej, niemniej jednak musimy pamiętać o funkcji synchronizacji naszego interfejsu. Testowany przyrząd nie jest w stanie zwrócić odpowiedzi (czyli dokonać nowego pomiaru) częściej niż raz na sekundę, dlatego szybsze odpytywanie nie ma sensu. Przed przystąpieniem do projektowania tego typu algorytmów należy w pierwszej kolejności dokładnie przeczytać instrukcję obsługi urządzenia. Zwróćmy też uwagę, że urządzenie to wymagało, by wysyłane polecenie zakończyć parą znaków \r\n, czyli powrót karetki (CR) i znak nowego wiersza (LF).
Aplikacja nie lubi milczeć
Jak zapewne zauważyliśmy, projektując wszystkie przedstawione do tej pory programy, zwracaliśmy baczną uwagę na to, by aplikacja powiadamiała nas o problemach, jakie napotyka w czasie działania. Korzystaliśmy ze standardowych funkcji Windows, takich jak:
extern PACKAGE void __fastcall ShowMessage(const System::AnsiString Msg);
extern PACKAGE int __fastcall MessageDlg(const System::AnsiString Msg, TMsgDlgType DlgType, TMsgDlgButtons Buttons, int HelpCtx);
gdzie:
enum TMsgDlgType { mtWarning, mtError, mtInformation, mtConfirmation, mtCustom };
typedef Set TMsgDlgButtons;
oraz z funkcji:
int __fastcall MessageBox(char * Text, char * Caption, int Flags)
; Wszystkie one zostały już użyte w odpowiednich kontekstach, dlatego nie ma potrzeby ponownego prezentowania sposobu umieszczenia ich w programie. Tu poruszamy temat komunikacji komputerowej poprzez interfejs RS 232C. Programy pisane przez nas mogą komunikować się z różnymi urządzeniami zewnętrznymi. Oprócz wysokiej sprawności i niezawodności muszą one posiadać nie spotykaną gdzie indziej cechę, polegającą na możliwości błyskawicznego zdiagnozowania, czy w ogóle jest się z kim łączyć, tzn. czy urządzenie zewnętrzne istnieje (jest włączone). Może zdarzyć się i taka sytuacja, że miernik najzwyczajniej w świecie może się popsuć w trakcie pomiaru lub z jakiś innych względów odmówić dalszej współpracy (uszkodzone łącze lub linia transmisyjna). Aplikacja sterująca przyrządem nie tylko nie powinna się wówczas zawiesić, ale jeszcze powiadomić nas o zaistniałej sytuacji. Stosowana przez nas do tej pory konstrukcja:
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0)
{
// wyświetl odebrane dane
}
jest mało praktyczna - jeżeli dane przestaną napływać w wyniku uszkodzenia przyrządu, to na ekranie nie zobaczymy nic. Jedyną jej zaletą jest fakt, że program powinien dalej działać. Powiedzmy, że chcemy mieć informację o tym, że urządzenie się wyłączyło. W tym celu skorzystamy z funkcji Win32 API, zwracającej typ ostatniego błędu:
DWORD GetLastError(VOID);
Win32 API umożliwia nam też ustalanie własnego typu wartości danego błędu. Wystarczy użyć:
VOID SetLastError(DWORD fdwError);
gdzie fdwError określa kod ostatniego błędu. Wygodny sposób wykorzystania tych funkcji np. w ostatnio omawianej funkcji obsługi zdarzenia TimerOnTimer() mógłby wyglądać następująco:
DWORD dwError;
...
void __fastcall TForm1::TimerOnTimer(TObject *Sender)
{ Write_Comm(hCommDev, strlen(Buffer_O));
Sleep(100);
Beep();
FlushFileBuffers(hCommDev);
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read == 0)
{
SetLastError(0xFFFFFFFF);
dwError = GetLastError();
RichEdit1->Text = (IntToStr(dwError));
}
else
RichEdit1->Text = Buffer_I;
}
Stosując GetLastError() nie tylko w obrębie programów komunikacyjnych, możemy spodziewać się najczęściej wartości 0xFFFFFFFF lub -1. Jeżeli zaś błąd nie wystąpił, oczekuje się ERROR_SUCCES, czyli 0. Należy pamiętać, że funkcja GetLastError() w głównej mierze bazuje na wątkach, dlatego pełne jej wykorzystanie może nastąpić tylko w ramach konkretnego wątku. Definiując błąd jako 0xFFFFFFFF, postąpiłem bardzo ostrożnie. Kody błędów funkcji Win32 API są 32-bitowe, przy czym bit numer 31 jest bitem bardziej znaczącym. Bit 29. jest z reguły zarezerwowany dla aplikacji, w których chcemy zdefiniować własne komunikaty. Należy go odpowiednio ustawić i wówczas nie napotkamy żadnego konfliktu z kodami błędów innych funkcji. Niemniej należy pamiętać, że będzie to prawdą wyłącznie wtedy, gdy korzystamy z funkcji Win32 API, które potrafią nam zwrócić kod ostatniego błędu. Niekiedy wykorzystywanie funkcji błędów w sposób uproszczony może okazać się zwodnicze, szczególnie w aplikacjach odczytujących wyniki pomiarów w postaci liczb. Powyższy przykład jest tego ilustracją. Bardzo często stykamy się z sytuacją, w której zwracany kod błędu w postaci liczby może dość poważnie wprowadzić w błąd Użytkownika. W naszym przykładzie mierzona w stopniach Celsjusza temperatura może równie dobrze przyjmować wartości ujemne, zatem przedstawione rozwiązanie w tym konkretnym wypadku nie wydaje się zbyt fortunne. Bardziej przejrzysty i elegancki sposób skorzystania z usług pary funkcji SetLastError() oraz GetLastError() Wykorzystałem tu również funkcję FormatMessage() umożliwiającą przedstawienie wybranego komunikatu Windows w bardzo wygodnej dla Użytkownika formie.
LPVOID MsgBuf;
...
if (Number_Bytes_Read == 0)
{ Timer1->Enabled = FALSE;
SetLastError(ERROR_READ_FAULT);
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &MsgBuf, 0, NULL );
MessageBox(NULL, (LPTSTR) MsgBuf, "Błąd transmisji", MB_OK|MB_ICONINFORMATION);

// zwolnienie bufora
LocalFree(MsgBuf);
}
else
RichEdit1->Text = Buffer_I;
Przedstawiony sposób umożliwia wzbogacenie pisanych programów jeszcze w szereg innych komunikatów sygnalizujących błędy Win32 API. Poniżej zamieszczam kilka najbardziej użytecznych:
ERROR_BAD_UNIT - odnalezienie urządzenia jest niemożliwe.
ERROR_NOT_READY - urządzenie nie jest gotowe.
ERROR_BAD_COMMAND - urządzenie nie rozpoznaje polecenia.
ERROR_BAD_LENGTH - program wydał polecenie, ale jego długość jest niewłaściwa.
ERROR_WRITE_FAULT - system nie może zapisywać do określonego urządzenia.
ERROR_READ_FAULT - system nie może czytać z określonego urządzenia.
ERROR_GEN_FAILURE - urządzenie podłączone do komputera nie działa.
ERROR_OPEN_FAILED - system nie może otworzyć określonego urządzenia lub pliku.
ERROR_IO_DEVICE - żądanie nie mogło być wykonane z powodu błędu urządzenia We-Wy.
ERROR_SERIAL_NO_DEVICE - żądane urządzenie szeregowe nie zostało pomyślnie zainicjalizowane. Program obsługi szeregowej zostanie usunięty z pamięci.
ERROR_MORE_WRITES - operacja szeregowa We-Wy została zakończona przez inny zapis do portu szeregowego.
ERROR_COUNTER_TIMEOUT - operacja szeregowa We-Wy została zakończona z powodu przekroczenia limitu czasu (tzw. błąd przeterminowania).
Alternatywnym, dużo prostszym ale równie skutecznym sposobem powiadomienia nas o wystąpieniu ewentualnego błędu w trakcie transmisji jest zastosowanie bardzo prostej konstrukcji, w której programista przyjmuje, że wystąpienie jakiejś unikalnej pary znaków sygnalizować będzie niepowodzenie przy odczycie danych.
if (Number_Bytes_Read > 0)
{
// pokaż wynik odczytu
}
else
{
Beep();
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
}
Metodę taką z powodzeniem stosuje się na etapie projektowania i testowania aplikacji, gdzie koncentrujemy się głównie na sprawdzeniu poprawności zaprojektowanego algorytmu, zaś elegancja jego działania odgrywa mniej znaczącą rolę. Należy zwrócić uwagę, że wywołania tego typu komunikatów możemy z powodzeniem umieścić w odpowiednim miejscu funkcji Read_Comm(). Dzięki temu zyskamy nieco na długości kodu, ale, jak się wydaje, algorytmy stracą wówczas na przejrzystości. Funkcję GetLastError() często stosujemy do sprawdzania rezultatu wykonania operacji otwarcia lub utworzenia pliku przeznaczonego do transferu. Jeżeli zechcemy mieć informację o tym, czy nowo otwarty plik nie jest już przypadkiem wykorzystywany przez inny trwający proces możemy posłużyć się następującą konstrukcją:
HANDLE hfile_s;
DWORD dwError;
LPVOID MsgBuf;
... hfile_s = CreateFile(FileListBox1->FileName.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
...
switch (dwError = GetLastError())
{
case ERROR_SHARING_VIOLATION : {
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &MsgBuf, 0, NULL );
MessageBox(NULL, (LPTSTR) MsgBuf, "Uwaga", MB_OK|MB_ICONINFORMATION);
LocalFree(MsgBuf);
break;
}
...
}
Przy próbie otwarcia do transmisji pliku aktualnie wykorzystywanego przez inny program lub pliku będącego częścią macierzystej aplikacji na ekranie ujrzymy pomocną informację.Nie oznacza to oczywiście, że takiego zbioru nie będziemy już w stanie przetransmitować, niemniej jednak przy wszelkich operacjach na tego rodzaju plikach należy zachować ostrożność.

Wykorzystanie elementów Win32 API w Delphi.
Przechodzimy obecnie do omówienia metod wykorzystywania interfejsu programisty w aplikacjach komunikacyjnych pisanych w Delphi. Zamierzeniem autora jest, by część książki traktująca o Object Pascalu nie była jedynie kalką tego, co zostało powiedziane wcześniej. Delphi ma swoje specyficzne właściwości, które postaramy się pokazać w kolejnych podrozdziałach.
Testowanie portu szeregowego - inaczej
Wszystko, co powiedzieliśmy do tej pory o strukturze kontroli portu szeregowego DCB, ogólnie rzecz biorąc będzie dalej aktualne. Być może niektóre z osób czytających rozdział poświęcony testowaniu łącza szeregowego za pomocą C++Buildera zastanawiały się, dlaczego przy okazji prezentacji struktury DCB w sposób bardzo wyraźny rozgraniczyliśmy opis zmiennych i pól bitowych wchodzących w jej skład. Mogłoby się wydawać, iż z poziomu Buildera był to zabieg dosyć sztuczny, niemniej jednak obecnie bardzo nam się przyda. Ktoś może być przekonany, że API jest czymś absolutnym (niezmiennym). Ogólnie jest to prawdą, poza małymi wyjątkami. Pierwszym tego typu przykładem może być właśnie sposób posługiwania się blokiem kontroli portu szeregowego w Delphi. Można np. skonfigurować wybrany port, odwołując się do zmiennych struktury DCB (w Object Pascalu deklaracja ta przybiera postać TDCB) w sposób analogiczny jak w Builderze i bez problemu osiągniemy ten cel. Trudności napotkamy jednak przy próbie skorzystania z jej pól bitowych. Powodem tego jest fakt, że Object Pascal korzysta ze specjalnego fragmentu Win32 API, gdzie DCB (TDCB) definiuje się następująco:
type
{$EXTERNALSYM _DCB}
_DCB = packed record
DCBlength: DWORD;
BaudRate: DWORD;
Flags: LongInt;
wReserved: Word;
XonLim: Word;
XoffLim: Word;
ByteSize: Byte;
Parity: Byte;
StopBits: Byte;
XonChar: CHAR;
XoffChar: CHAR;
ErrorChar: CHAR;
EofChar: CHAR;
EvtChar: CHAR;
wReserved1: Word;
end;
TDCB = _DCB;
{$EXTERNALSYM DCB}
DCB = _DCB;
PDCB = ^TDCB;
Użycie dyrektywy $EXTERNALSYM zapobiega jedynie pojawianiu się specyficznych symboli używanych przez Object Pascal w plikach nagłówkowych generowanych dla C++Buildera. Patrząc na treść powyższej definicji, można się domyśleć, że dostęp do zmiennych DCB z poziomu Delphi nie powinien stwarzać większego problemu. Inaczej jest ze znacznikami - tutaj niestety należałoby już znać ich wartości . Oto one:
fBinary = $0001;
fParity = $0002;
fOutxCtsFlow = $0004;
fOutxDsrFlow = $0008;
// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;
fDsrSensitivity = $0040;
fTXContinueOnXoff = $0080;
fOutX = $0100;
fInX = $0200;
fErrorChar = $0400;
fNull = $0800;
// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;
fAbortOnError = $4000;
Do tych znaczników odwołujemy się właśnie poprzez pole Flags, która to nazwa w tym kontekście jest zastrzeżona. Jeżeli chcielibyśmy w programie skonfigurować wybrany port szeregowy w ten sposób, aby sprawdzane były parzystość, sygnał CTS oraz np. by kontrola linii DTR była typu handshaking (patrz tabela 5.5), wystarczy napisać:
dcb.Flags := dcb_fParity or DTR_CONTROL_HANDSHAKE or dcb_fOutxCtsFlow;
lub, co jest równoważne:

dcb.Flags := $0002 or $0020 or $0004; Widzimy więc, że do Flags należy po prostu wpisać konkretną kombinację bitów, tworząc tym samym odpowiednią maskę bitową. Jeżeli chcemy, by dany parametr komunikacyjny był nieaktywny, nie należy się do niego odwoływać.Znacznik (ang. flag) oznacza stałą będącą szczególną opcją dla wybranej operacji. Może być on użyty pojedynczo lub z wykorzystaniem operatora OR jako kombinacji kilku znaczników - tworzy tym samym parametr w postaci unikalnej maski bitowej. Praktyczne wykorzystanie niektórych opisanych właściwości bloku kontroli portu szeregowego dostępnego w Windows z poziomu Delphi przedstawiono na listingu (dotyczy on modułu RS_11.pas projektu aplikacji testującej wybrany port szeregowy). Formularz naszego projektu składa się ze standardowych komponentów. Wyboru numeru portu oraz prędkości transmisji dokonujemy, zaznaczając poszczególne komponenty typu TCheckBox. Naciskając przycisk Otwórz port wywołujemy procedurę obsługi zdarzenia OpenCommClick(). Przycisk Test uruchamia zdarzenie TestCommClick(). Zamknięcia portu i aplikacji dokonuje się po uaktywnieniu CloseCommClick().
Kod głównego modułu RS_11.pas aplikacji testującej port szeregowy
unit RS_11;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
CloseComm: TButton;
CheckBox1: TCheckBox;
OpenComm: TButton;
Edit1: TEdit;
CheckBox2: TCheckBox;
CheckBox3: TCheckBox;
CheckBox4: TCheckBox;
TestComm: TButton;
Edit2: TEdit;
Edit3: TEdit;
Edit4: TEdit;
Edit5: TEdit;
Edit6: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure TestCommClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const // -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;
// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;
dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;
// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;
dcb_fAbortOnError = $4000;
var
hCommDev : THANDLE; // identyfikator portu
lpFileName : PChar; // przechowuje nazwę portu
// lpFileName : LPCTSTR;
dcb : TDCB; // blok kontroli urządzeń
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM2';
if (CheckBox2.Checked = TRUE) then
lpFileName:='COM1';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE,0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox3.Checked = TRUE) then
dcb.BaudRate := CBR_1200;
if (CheckBox4.Checked = TRUE) then
dcb.BaudRate := CBR_9600;
dcb.Parity := ODDPARITY;
dcb.StopBits := ONESTOPBIT;
dcb.ByteSize := 7;
// -- przykładowe ustawienia flag sterujących DCB --
dcb.Flags := dcb_fParity or $0020;
SetCommState(hCommDev, dcb);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny ' , mtError, [mbOk], 0);
end;
end;
//--------------------------------------------------------------------
procedure TForm1.TestCommClick(Sender: TObject);
begin
// -- sprawdzenie zmiennych struktury DCB --
if (hCommDev > 0) then
begin
case dcb.BaudRate of
CBR_1200: Edit1.Text := IntToStr(CBR_1200);
CBR_9600: Edit1.Text := IntToStr(CBR_9600);
end;
case dcb.Parity of
EVENPARITY: Edit2.Text := ' Parzysta';
ODDPARITY: Edit2.Text := 'Nieparzysta';
end;
case dcb.XoffChar of
char(17): Edit3.Text := 'DC1';
char(19): Edit3.Text := 'DC3';
end;
// -- sprawdzenie znaczników sterujących struktury DCB --
if (dcb.Flags and DTR_CONTROL_ENABLE) = DTR_CONTROL_ENABLE
then
Edit4.Text := 'Aktywna'
else
Edit4.Text := 'Handshaking';
if (dcb.Flags and dcb_fOutxCtsFlow) = dcb_fOutxCtsFlow
then
Edit5.Text := 'Sprawdzany'
else
Edit5.Text := 'Nie sprawdzany';
if (dcb.Flags and dcb_fParity) = dcb_fParity
then
Edit6.Text := 'Sprawdzana'
else
Edit6.Text := 'Nie sprawdzana';
end // koniec nadrzędnego if
else
ShowMessage('Port szeregowy nie jest aktywny');
end; //--------------------------------------------------------------------
end. Zwróćmy uwagę, że wszystkie znaczniki sterujące DCB oraz ich wartości potraktowane zostały jako stałe. Ogólnie rzecz biorąc nie musimy tego robić w odniesieniu do stałych symbolicznych, czyli makr DTR_CONTROL_x lub RTS_CONTROL_x, gdyż są one zdefiniowane w części Win32 API dostępnej w Object Pascalu i bezpośrednie użycie ich w programie nie nastręcza żadnych problemów. W części deklaracyjnej programu umieściłem je jedynie po to, by zachować całość obrazu. Warto w tym miejscu wskazać też na możliwość pewnego odmiennego sposobu wywołania funkcji CreateFile(). W najprostszym wypadku prawidłowe wykorzystanie jej w programie napisanym w Delphi wymaga użycia w odpowiednim miejscu słowa zarezerwowanego NIL. Zmienna, która przechowuje nazwę portu musi być typu PChar lub właściwszego dla Windows typu LPCTSTR.PRZYPOMNIJMY Słowo zarezerwowane NIL (ang. Not In List - nie znajdujący się na liście) oznacza standardową stałą, która może być wartością każdego typu wskaźnikowego. Standardowy typ PChar jest typem wskaźnikowym, wskazującym na C-łańcuchy .Zdarzają się w praktyce sytuacje, w których musimy bardzo szybko zainicjować port szeregowy, nie wdając się w stosowanie jakiś bardzo skomplikowanych odwołań do DCB. Przedstawię teraz taki sposób. Być może nie jest on zbyt elegancki oraz podatny na szybkie modyfikacje i właściwszy jest raczej dla Basica, niemniej jednak można go z powodzeniem używać w prostszych programach, pisząc zarówno w Delphi, jak i C++Builderze. W tym celu posłużymy się funkcjami Win32 API, z których pierwsza jest postaci:
BOOL BuildCommDCB(LPCTSTR lpDef, LPDCB lpDCB);
Parametr lpDef jest wskaźnikiem do C-łańcucha, zawierającego kompletną informację o wybranych parametrach transmisji, lpDCB wskazuje na blok kontroli urządzeń. Przykładowo ustalimy: prędkość transmisji jako 9600 b/s, parzystość jako ODDPARITY, 8 bitów danych oraz 1 bit stopu. Wówczas należałoby funkcję tę wywołać w sposób prezentowany poniżej:
BuildCommDCB('baud = 9600 parity = O data = 8 stop = 1', dcb);
lub, co jest równoważne:
BuildCommDCB('9600, O, 8, 1', dcb);
Przy próbie ustalenia prędkości na 110 b/s funkcja automatycznie ustali dwa bity stopu. Dzięki temu zostanie zachowana kompatybilność w Windows NT. Ponadto niemożliwym jest ustalenie protokołu XON-XOFF (przyjmuje się, że jest on nieaktywny) . Tym sposobem spowodujemy też wykonanie następujących przypisań:
fInX FALSE
fOutX FALSE
fOutxDsrFlow FALSE
fOutxCtsFlow FALSE
fDtrControl DTR_CONTROL_ENABLE
fRtsControl RTS_CONTROL_ENABLE
Jeżeli łańcuch lpDef uzupełnimy o znak p:
BuildCommDCB('9600, O, 8, 1, p', dcb);
to oznaczać będzie, że:
fInX FALSE
fOutX FALSE
fOutxDsrFlow TRUE
fOutxCtsFlow TRUE
fDtrControl DTR_CONTROL_HANDSHAKE
fRtsControl RTS_CONTROL_HANDSHAKE
Jeżeli z kolei łańcuch lpDef skonstruujemy według przepisu:
BuildCommDCB('9600, O, 8, 1, x', dcb);
uzupełniając go o znak x, wówczas należy spodziewać się następujących przypisań:
fInX TRUE
fOutX TRUE
fOutxDsrFlow FALSE
fOutxCtsFlow FALSE
fDtrControl DTR_CONTROL_ENABLE
fRtsControl RTS_CONTROL_ENABLE
Użycie omówionej funkcji nie powinno sprawić nam żadnego kłopotu. Procedura obsługi zdarzenia otwierającego wybrany port szeregowy do transmisji w jednym z możliwych wariantów może przybrać następującą postać:
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then lpFileName:='COM2';
if (CheckBox2.Checked = TRUE) then lpFileName:='COM1';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
// wywołanie SetupComm()
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
// dcb.Flags
BuildCommDCB('9600, O, 8, 1, p', dcb);
SetCommState(hCommDev, dcb)
// wywołanie GetCommMask()
// wywołanie SetCommMask()
end
else
case hCommDev of IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny' , mtError, [mbOk], 0);
end;
end;
Kolejna funkcja Win 32 API, za pomocą której można nie tylko błyskawicznie skonfigurować łącze szeregowe, ale również ustalić parametry czasów przeterminowania operacji zapisu i odczytu, ma następującą postać:
BOOL BuildCommDCBAndTimeouts(LPCTSTR lpDef, LPDCB lpDCB, LPCOMMTIMEOUTS lpCommTimeouts);
gdzie lpCommTimeouts jest wskaźnikiem do omawianej w poprzednim podrozdziale struktury COMMTIMEOUTS ( w Object Pascalu odpowiednikiem jej będzie strukturalny typ danych TCOMMTIMEOUTS). Jeżeli ciąg znaków wskazywany przez lpDCB uzupełnimy o podciąg TO=ON, oznaczać to będzie, że omawiana funkcja uaktywni wszystkie parametry czasowe transmisji wyspecyfikowane zgodnie z TCOMMTIMEOUTS. Jeżeli z kolei lpDCB uzupełnimy go o podciąg TO=OFF, to parametry czasowe transmisji pozostaną nieaktywne. Poniżej prezentowany jest jeden z możliwych sposobów wywołania funkcji BuildCommDCBAndTimeouts():
CommTimeouts : TCOMMTIMEOUTS;
...
BuildCommDCBAndTimeouts('baud=1200 parity=O data=7 stop=1 TO=ON', dcb, CommTimeouts);
Zajmijmy się teraz nieco dokładniejszym sposobem zdiagnozowania zasobów komunikacyjnych dostępnych w naszym PC. Sięgnijmy więc do dobrze nam już znanej struktury COMMPROP w części Win32 API, z którego korzysta Delphi. Definiowana jest ona jako typ TCOMMPROP. Poniżej zostały przedstawione wartości wszystkich dostępnych w TCOMMPROP stałych oferowanych przez Borland Delphi Run-Time Library Win32 API Interface Unit:
const
{ Serial provider type. }
SP_SERIALCOMM = $00000001;
{ Provider SubTypes }
PST_UNSPECIFIED = $00000000;
PST_RS232 = $00000001;
PST_PARALLELPORT = $00000002;
PST_RS422 = $00000003;
PST_RS423 = $00000004;
PST_RS449 = $00000005;
PST_MODEM = $00000006;
PST_FAX = $00000021;
PST_SCANNER = $00000022;
PST_NETWORK_BRIDGE = $00000100;
PST_LAT = $00000101;
PST_TCPIP_TELNET = $00000102;
PST_X25 = $00000103;
{ Provider capabilities flags }
PCF_DTRDSR = $0001;
PCF_RTSCTS = $0002;
PCF_RLSD = $0004;
PCF_PARITY_CHECK = $0008;
PCF_XONXOFF = $0010;
PCF_SETXCHAR = $0020;
PCF_TOTALTIMEOUTS = $0040;
PCF_INTTIMEOUTS = $0080;
PCF_SPECIALCHARS = $0100;
PCF_16BITMODE = $0200;
{ Comm provider settable parameters }
SP_PARITY = $0001;
SP_BAUD = $0002;
SP_DATABITS = $0004;
SP_STOPBITS = $0008;
SP_HANDSHAKING = $0010;
SP_PARITY_CHECK = $0020;
SP_RLSD = $0040;
{ Settable baud rates in the provider }
BAUD_075 = $00000001;
BAUD_110 = $00000002;
BAUD_134_5 = $00000004;
BAUD_150 = $00000008;
BAUD_300 = $00000010;
BAUD_600 = $00000020;
BAUD_1200 = $00000040;
BAUD_1800 = $00000080;
BAUD_2400 = $00000100;
BAUD_4800 = $00000200;
BAUD_7200 = $00000400;
BAUD_9600 = $00000800;
BAUD_14400 = $00001000;
BAUD_19200 = $00002000;
BAUD_38400 = $00004000;
BAUD_56K = $00008000;
BAUD_128K = $00010000;
BAUD_115200 = $00020000;
BAUD_57600 = $00040000;
BAUD_USER = $10000000;
{ Settable Data Bits }
DATABITS_5 = $0001;
DATABITS_6 = $0002;
DATABITS_7 = $0004;
DATABITS_8 = $0008;
DATABITS_16 = $0010;
DATABITS_16X = $0020;
{ Settable Stop and Parity bits }
STOPBITS_10 = $0001;
STOPBITS_15 = $0002;
STOPBITS_20 = $0004;
PARITY_NONE = $0100;
PARITY_ODD = $0200;
PARITY_EVEN = $0400;
PARITY_MARK = $0800;
PARITY_SPACE = $1000;
Object Pascal TCOMMPROP definiuje następująco:
type
PCommProp = ^TCommProp;
{$EXTERNALSYM _COMMPROP}
_COMMPROP = record
wPacketLength: Word;
wPacketVersion: Word;
dwServiceMask: DWORD;
dwReserved1: DWORD;
dwMaxTxQueue: DWORD;
dwMaxRxQueue: DWORD;
dwMaxBaud: DWORD;
dwProvSubType: DWORD;
dwProvCapabilities: DWORD;
dwSettableParams: DWORD;
dwSettableBaud: DWORD;
wSettableData: Word;
wSettableStopParity: Word;
dwCurrentTxQueue: DWORD;
dwCurrentRxQueue: DWORD;
dwProvSpec1: DWORD;
dwProvSpec2: DWORD;
wcProvChar: array[0..0] of WCHAR;
end;
TCommProp = _COMMPROP;
{$EXTERNALSYM COMMPROP}
COMMPROP = _COMMPROP;
Nic nie stoi na przeszkodzie, abyśmy - posiadając już pewne doświadczenia w tym względzie - spróbowali samodzielnie, z poziomu Delphi, dokładniej zdiagnozować wybrany port szeregowy z poziomu Delphi. W tym celu zaprojektujemy aplikację.Jej wygląd nie będzie zasadniczo różnić się od odpowiednika napisanego w C++Builderze. Jako nowość wprowadzimy tylko możliwość wyboru interesującego nas łącza szeregowego. W skład przedstawionego formularza wchodzą podwójnie użyte komponenty TCheckBox, TButton, TEdit, TLabel oraz TTrackBar. Pełną informację o aktualnie dostępnym parametrze komunikacyjnym otrzymamy przez wzajemną, płynną regulację położeń suwaków uruchamiających procedury obsługi zdarzeń TrackBar1Change() oraz Track2BarChange(). Zastosowaliśmy tu omówiony już proces maskowania z wykorzystaniem operatora iloczynu bitowego and (bitowe i). Program odczytuje wartość wybranej zmiennej udostępnianej przez TCOMMPROP a następnie, wybierając kolejne maski, sprawdza, czy włączone są konkretne bity odpowiedzialne za wybrane atrybuty transmisji. Posiadając ściągawkę w postaci wyżej przedstawionych wartości reprezentujących typ dostępnego parametru danej usługi komunikacyjnej, Czytelnik bez problemu może tak rozbudować algorytm, by móc kompletnie zdiagnozować wybrane łącze komunikacyjne. Być może niektórym pewien kłopot sprawi zastosowany tu zapis liczb w postaci szesnastkowej (heksadecymalnej). Pamiętamy, że w Pascalu liczby takie poprzedzone są znakiem $. Jeżeli rzeczywiście tak jest, zawsze można użyć ustawionego w trybie profesjonalnym Kalkulatora Windows. Kod głównego modułu RS_12.pas aplikacji testującej zasoby wybranego łącza komunikacyjnego
unit RS_12;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls;
type
TForm1 = class(TForm)
CloseComm: TButton;
OpenComm: TButton;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
Edit1: TEdit;
Edit2: TEdit;
Label1: TLabel;
Label2: TLabel;
TrackBar1: TTrackBar;
TrackBar2: TTrackBar;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
procedure TrackBar2Change(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
var
hCommDev : THANDLE;
lpFileName : PChar;
//lpFileName : LPCTSTR;
dcb : TDCB;
CommProp : TCOMMPROP; // właściwości portu
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM2';
if (CheckBox2.Checked = TRUE) then
lpFileName:='COM1';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
CommProp.dwProvSpec1:=COMMPROP_INITIALIZED;
GetCommProperties(hCommDev, CommProp);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny' , mtError, [mbOk], 0);
end;
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
case (TrackBar1.Position) of
1:
begin
TrackBar2.Max := 7;
Label1.Caption := 'dwSettableParams';
Edit1.Text := IntToStr(CommProp.dwSettableParams);
end;
2:
begin
TrackBar2.Max := 6;
Label1.Caption := 'wSettableData';
Edit1.Text := IntToStr(CommProp.wSettableData);
end;
3:
begin
TrackBar2.Max := 8;
Label1.Caption := 'wSettableStopParity';
Edit1.Text := IntToStr(CommProp.wSettableStopParity);
end;
end; // koniec case of
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar2Change(Sender: TObject);
begin
if (TrackBar1.Position = 1) then
begin
case (TrackBar2.Position) of
1:
begin
Label2.Caption := 'SP_PARITY';
Edit2.Text := IntToStr(CommProp.dwSettableParams and SP_PARITY);
end;
2:
begin
Label2.Caption := 'SP_BAUD';
Edit2.Text := IntToStr(CommProp.dwSettableParams and SP_BAUD);
end;
3:
begin
Label2.Caption := 'SP_DATABITS';
Edit2.Text := IntToStr(CommProp.dwSettableParams and SP_DATABITS);
end;
4:
begin
Label2.Caption := 'SP_STOPBITS'; Edit2.Text := IntToStr(CommProp.dwSettableParams and SP_STOPBITS);
end;
5:
begin
Label2.Caption := 'SP_HANDSHAKING'; Edit2.Text := IntToStr(CommProp.dwSettableParams and SP_HANDSHAKING);
end;
6:
begin
Label2.Caption := 'SP_PARITY_CHECK'; Edit2.Text := IntToStr(CommProp.dwSettableParams and SP_PARITY_CHECK);
end;
7:
begin
Label2.Caption := 'SP_RLSD'; Edit2.Text := IntToStr(CommProp.dwSettableParams and SP_RLSD);
end;
end; // koniec case of
end; // koniec if
//*******************************
if (TrackBar1.Position = 2) then
begin
case (TrackBar2.Position) of
1:
begin
Label2.Caption := 'DATABITS_5'; Edit2.Text := IntToStr(CommProp.wSettableData and DATABITS_5);
end;
2:
begin
Label2.Caption := 'DATABITS_6'; Edit2.Text := IntToStr(CommProp.wSettableData and DATABITS_6);
end;
3:
begin
Label2.Caption := 'DATABITS_7'; Edit2.Text := IntToStr(CommProp.wSettableData and DATABITS_7);
end;
4:
begin
Label2.Caption := 'DATABITS_8'; Edit2.Text := IntToStr(CommProp.wSettableData and DATABITS_8);
end;
5:
begin
Label2.Caption := 'DATABITS_16'; Edit2.Text := IntToStr(CommProp.wSettableData and DATABITS_16);
end;
6:
begin
Label2.Caption := 'DATABITS_16X'; Edit2.Text := IntToStr(CommProp.wSettableData and DATABITS_16X);
end;
end; // koniec case of
end;// koniec if
//********************************
if (TrackBar1.Position = 3) then
begin
case (TrackBar2.Position) of
1:
begin
Label2.Caption := 'STOPBITS_10';
Edit2.Text := IntToStr(CommProp.wSettableStopParity and STOPBITS_10);
end;
2:
begin
Label2.Caption := 'STOPBITS_15'; Edit2.Text := IntToStr(CommProp.wSettableStopParity and STOPBITS_15);
end;
3:
begin
Label2.Caption := 'STOPBITS_20'; Edit2.Text := IntToStr(CommProp.wSettableStopParity and STOPBITS_20);
end;
4:
begin
Label2.Caption := 'PARITY_NONE'; Edit2.Text := IntToStr(CommProp.wSettableStopParity and PARITY_NONE);
end;
5:
begin
Label2.Caption := 'PARITY_ODD'; Edit2.Text := IntToStr(CommProp.wSettableStopParity and PARITY_ODD);
end;
6:
begin
Label2.Caption := 'PARITY_EVEN'; Edit2.Text := IntToStr(CommProp.wSettableStopParity and PARITY_EVEN);
end;
7:
begin
Label2.Caption := 'PARITY_MARK'; Edit2.Text := IntToStr(CommProp.wSettableStopParity and PARITY_MARK);
end;
8:
begin
Label2.Caption := 'PARITY_SPACE'; Edit2.Text := IntToStr(CommProp.wSettableStopParity and PARITY_SPACE);
end;
end; // koniec case of
end;// koniec if
end;
//--------------------------------------------------------------------
end.
Jak się zapewne domyślamy, dalsze manipulowanie ustawieniami parametrów transmisji użycia opisanych już wcześniej funkcji GetCommConfig() i SetCommConfig() oraz odwołania się do struktury COMMCONFIG, która w wersji akceptowanej przez Object Pascala przybiera postać rekordu TCOMMCONFIG:
PCommConfig = ^TCommConfig;
{$EXTERNALSYM _COMMCONFIG}
_COMMCONFIG = record
dwSize: DWORD;
wVersion: Word;
wReserved: Word;
dcb: TDCB;
dwProviderSubType: DWORD;
dwProviderOffset: DWORD;
dwProviderSize: DWORD;
wcProviderData: array[0..0] of WCHAR;
end;
TCommConfig = _COMMCONFIG;
{$EXTERNALSYM COMMCONFIG}
COMMCONFIG = _COMMCONFIG
Nawiązanie połączenia
Analizując wszystko, co do tej pory zostało powiedziane, musieliśmy zauważyć, że konstrukcja części aplikacji, w której konfigurujemy port szeregowy oraz inicjujemy połączenie nie różni się zbyt drastycznie od jej odpowiednika napisanego w Builderze. Jedyną nowością jest trochę inny sposób odwoływania się do znaczników sterujących bloku kontroli portu szeregowego. Z tego powodu nie będę w tym miejscu szczegółowo omawiał budowy segmentu inicjalizująco-konfiguracyjnego. Przejdźmy od razu do przedstawienia konstrukcji części aplikacji wysyłającej i odbierającej komunikaty. W tym miejscu uwzględnimy możliwość ich budowy zarówno w postaci procedur jak i funkcji.
Segment wysyłający komunikaty
Zaczniemy od omówienia najprostszej z możliwych metod wysłania jakiegoś komunikatu poprzez łącze szeregowe z poziomu Delphi. W tym celu w sposób maksymalnie uproszczony skonstruujemy procedurę z zaledwie dwoma parametrami.
procedure TForm1.Write_Comm(hCommDev: THANDLE;
nNumberOfBytesToWrite: DWORD);
var
NumberOfBytesWritten : DWORD;
begin
...
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
WaitCommEvent(hCommDev, fdwEvtMask, NIL);
...
end;
Ustaliliśmy tutaj, że identyfikator wybranego portu zostanie przekazany procedurze przez parametr globalny hCommDev, zaś liczba bajtów do wysłania przez parametr nNumberOfBytesToWrite, wołany przez wartość. Ten ostatni posłuży do rezerwacji miejsca przeznaczonego do zajęcia przez wartość przekazywanego mu parametru aktualnego. Nie trudno zgadnąć, że w przyszłości będzie nim liczba znaków przeznaczonych do wysłania. Używanie w procedurach zmiennych oraz stałych globalnych jest bardzo wygodną i często stosowaną techniką programowania. Przedstawiony sposób przekazywania danych procedurze w tej konkretnej sytuacji jest najprostszym z możliwych. Zobaczymy dalej, że jest również niezawodny. Trzeba pamiętać, że parametry wołane przez wartość służą jedynie do przekazywania wartości parametrów aktualnych procedurze. Wynika z tego, że zmiana w procedurze parametru wołanego przez wartość będzie widoczna tylko w obrębie danej procedury. Parametr nNumberOfBytesToWrite zawsze będzie miał przy wyjściu z procedury wartość taką samą jaką miał przy wejściu. Pisząc programy komunikacyjne musimy dążyć do tego, by nigdy nie zmieniać wartości liczby bajtów do wysłania! Użyliśmy procedury dwuparametrowej, gdyż z reguły interesują nas dwie rzeczy: co i gdzie mamy wysłać. Wnikanie w mniej istotne szczegóły w tym przypadku nie jest wymagane. Alternatywnym sposobem zbudowania w Object Pascalu segmentu wysyłającego komunikaty do łącza szeregowego jest możliwość użycia funkcji. Wydaje się, że z praktycznego punktu widzenia wykorzystanie funkcji jest bardziej opłacalne. Wynika to z prostego faktu, mianowicie w odróżnieniu od procedur funkcje zawsze posiadają określony rezultat o typie jednoznacznie określonym przez typ wyniku funkcji. I w tym przypadku z powodzeniem wystarczy nam funkcja dwuparametrowa z liczbą bajtów do wysłania wołaną przez wartość:
function TForm1.Write_Comm(hCommDev: THANDLE; nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;
begin
...

WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
begin
...
Write_Comm := 1
end
else
Write_Comm := 0;
end;
Użycie funkcji umożliwi nam w przyszłości bardzo szybkie zdiagnozowanie rezultatu jej wykonania. Jeżeli w wyniku zaprogramowanego zdarzenia WaitCommEvent() otrzymamy wartość TRUE, wówczas rezultat wykonania całej funkcji będzie równy 1 (umówmy się, że wystąpienie jedynki traktować będziemy jako sukces). W przeciwny wypadku funkcja wysyłająca dane do portu szeregowego zwróci nam wartość 0.
Segment odbierający komunikaty
Projektując tę część aplikacji komunikacyjnej, podobnie jak poprzednio postaramy się wykorzystać wszystkie zalety Delphi. Jednak zanim zaczniemy pisać odpowiednie procedury i funkcje, przedstawię pascalowy odpowiednik wielce użytecznej struktury COMSTAT. Będzie nim zdefiniowany w Borland Delphi Run-Time Library Win32 API Interface Unit strukturalny typ danych TCOMSTAT:
type
TComStateFlag = (fCtlHold, fDsrHold, fRlsHold, fXoffHold, fXOffSent, fEof, fTxim);
TComStateFlags = set of TComStateFlag;
{$EXTERNALSYM _COMSTAT}
_COMSTAT = record
Flags: TComStateFlags;
Reserved: array[0..2] of Byte;
cbInQue: DWORD;
cbOutQue: DWORD;
end;
TComStat = _COMSTAT;
{$EXTERNALSYM COMSTAT}
COMSTAT = _COMSTAT;
PComStat = ^TComStat;
Użycie go w praktyce nie powinno nam sprawić jakichkolwiek problemów, gdyż odwołanie do jego poszczególnych elementów będzie analogiczne jak w rozdziale poświęconym Builderowi. Tradycyjnie zaczniemy od przedstawienia bardzo prostej, dwuparametrowej procedury, dzięki której będziemy mogli odbierać komunikaty przychodzące do portu szeregowego. Patrząc na poniższy przykład, zauważymy, że Buf_Size uczyniliśmy parametrem wołanym przez wartość z takich samych powodów jak poprzednio, czyli po to, by procedura nie zmieniała jego wartości.
var Stat : TCOMSTAT;
...
procedure TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD);
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue; ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
end
else
Number_Bytes_Read := 0;
end;
Pewną ciekawostką jest również sposób wywołania funkcji ClearCommError():
ClearCommError(hCommDev, Errors, @Stat);
Korzystając ze środowiska Delphi w trakcie pisania naszych programów, nigdy do tej pory nie potrzebowaliśmy jawnie odwoływać się do adresów zmiennych, oferowanych przez poszczególne strukturalne typu danych. Nie musieliśmy tego robić dlatego, że były one dla nas w pewnym sensie "statyczne". Jedynym celem odwołań do zawartości typów TDCB, TCOMMPROP czy TCOMMCONFIG było, jeżeli można tak powiedzieć, pewne sztywne ustalenie wybranych parametrów transmisji. Oznacza to, że w trakcie działania programu parametry te nie podlegają już fizycznie żadnym zmianom (przynajmniej jawnie). O takich parametrach w dużym uproszczeniu można powiedzieć, że są "statyczne". Zupełnie inaczej sprawa będzie wyglądać w przypadku konieczności czytania stanu buforów komunikacyjnych w trakcie działania aplikacji. Naturą rzeczy zawartość buforów w trakcie transmisji musi się zmieniać, zarówno w sensie ich aktualnego stopnia wypełnienia jak i rodzaju znaków aktualnie tam przechowywanych. Naszym zadaniem jest odpowiednie uchwycenie momentu tej zmiany. Twórcy Win32 API bardzo dokładnie przewidzieli taką sytuację. Traktując parametr Stat operatorem adresowym @, bardzo szybko otrzymamy adres miejsca, pod którym zapamiętany jest aktualny stan poszczególnych elementów TCOMSTAT. Potem pozostaje nam już tylko umiejętne wydobycie przechowywanej tam informacji, ale o tym mówiliśmy wcześniej. Przypomnijmy też, że zamiast operatora adresu możemy z takim samym skutkiem użyć standardowej funkcji adresowej Addr():
ClearCommError(hCommDev, Errors, Addr(Stat));
Alternatywnym sposobem skonstruowania segmentu odbierającego przychodzące do portu komunikaty jest zaprojektowanie go w postaci dwuparametrowej funkcji:
function TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue; ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
Przykładowe aplikacje
Przedstawię obecnie kompletne przykłady aplikacji napisanych w Delphi, wykorzystujących skonstruowane przez nas procedury oraz funkcje zapisu i odczytu danych z portu szeregowego. Formularz ten składa się z dobrze nam już znanych komponentów TCheckBox, TRichEdit oraz TButton. Starając się postępować konsekwentnie, zachowałem obowiązujące w naszej książce nazewnictwo poszczególnych zdarzeń, więc Czytelnik nie powinien mieć żadnych trudności z ich rozszyfrowaniem. Korzystając z zaprezentowanych programów testowałem, podobnie jak poprzednio, transmisję z innym egzemplarzem kontrolera temperatury. Wykorzystałem standardowe zapytanie o jego identyfikację: *IDN?+#13+#10. Owo zapytanie zakończone parą znaków CR (#13) LF (#10) można potraktować jako ciąg znaków wskazywanych przez parametr query. Zgodnie z prezentowaną wcześniej metodą, łańcuch ten skopiowałem do obszaru pamięci identyfikowanego przez bufor danych wyjściowych Buffer_O. W tym celu została zastosowana standardowa funkcja StrCopy(), za pomocą której można skopiować pierwotny łańcuch znaków do łańcucha docelowego (w naszym przykładzie: Buffer_O). Funkcja ta w wyniku daje nam wskaźnik na początek łańcucha docelowego. Należy jednak zwrócić uwagę na to, żeby długość ciągu znaków wysyłanych nie przekraczała rozmiaru zadeklarowanego bufora danych wyjściowych. W momencie, kiedy nie będziemy w stanie przewidzieć długości łańcucha znaków przeznaczonych do wysłania, lepiej jest skorzystać z funkcji:
function StrLCopy(Dest: PChar; const Source: PChar; MaxLen: Cardinal): PChar;
Kopiuje ona C-łańcuch Source do C-łańcucha Dest, zwracając wskaźnik na początek łańcucha Dest. Jednak w tym wypadku liczba kopiowanych znaków jest równa co najwyżej liczbie wyrażonej przez MaxLen, która może być określona za pomocą funkcji SizeOf(). Kod głównego modułu RS_13.pas aplikacji korzystającej z procedur wysyłających i odbierających komunikaty z łącza szeregowego
unit RS_13;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls;
type
TForm1 = class(TForm)
CloseComm: TButton;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
OpenComm: TButton;
Send: TButton;
Receive: TButton;
RichEdit1: TRichEdit;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SendClick(Sender: TObject);
procedure ReceiveClick(Sender: TObject);
private
{ Private declarations }
procedure Write_Comm(hCommDev: THANDLE; nNumberOfBytesToWrite: DWORD);
procedure Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości flag sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;
// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;
dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;
// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;
dcb_fAbortOnError = $4000;
cbInQueue = 256;
cbOutQueue = 256;
var query : PChar = '*IDN?'+#13+#10; // przykładowe zapytanie
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
procedure TForm1.Write_Comm(hCommDev: THANDLE;
nNumberOfBytesToWrite: DWORD);
var
NumberOfBytesWritten : DWORD;
begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
WaitCommEvent(hCommDev, fdwEvtMask, NIL);
end;
//--------------------------------------------------------------------
procedure TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD);
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,
Number_Bytes_Read, NIL);
end
else
Number_Bytes_Read := 0;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName := 'COM2';

hCommDev := CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE,0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate := CBR_1200;
//-- przykładowe ustawienia znaczników sterujących DCB --
dcb.Flags := RTS_CONTROL_DISABLE or dcb_fParity;
dcb.Parity := ODDPARITY;
dcb.StopBits := ONESTOPBIT;
dcb.ByteSize := 7;

SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny ', mtError, [mbOk], 0);
end;
end;
//--------------------------------------------------------------------
procedure TForm1.SendClick(Sender: TObject);
begin
StrCopy(Buffer_O, query);
RichEdit1.Text := Buffer_O;
Write_Comm(hCommDev, StrLen(Buffer_O));
FlushFileBuffers(hCommDev);
end;
//--------------------------------------------------------------------
procedure TForm1.ReceiveClick(Sender: TObject);
begin
Read_Comm(hCommDev, SizeOf(Buffer_I));
RichEdit1.Text := Buffer_I;
end;
//--------------------------------------------------------------------
end.
W bardzo podobny sposób problem transmisji szeregowej realizowanej w Object Pascalu można rozwiązać, wykorzystując w tym celu odpowiednio skonstruowane funkcje, tak jak pokazuje to poniższy, kompletny wydruk. .
Kod głównego modułu RS_14.pas aplikacji korzystającej z funkcji wysyłających i odbierających komunikaty z łącza szeregowego
unit RS_14;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls;
type
TForm1 = class(TForm)
CloseComm: TButton;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
OpenComm: TButton;
Send: TButton;
Receive: TButton;
RichEdit1: TRichEdit;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SendClick(Sender: TObject); procedure ReceiveClick(Sender: TObject);
private
{ Private declarations }
function Write_Comm(hCommDev: THANDLE;
nNumberOfBytesToWrite: DWORD): Integer;
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;
// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;
dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;
// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;
dcb_fAbortOnError = $4000;
cbInQueue = 256;
cbOutQueue = 256;
var
query : PChar = '*IDN?'+#13+#10; // przykładowe zapytanie
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
function TForm1.Write_Comm(hCommDev: THANDLE;
nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;
begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue; ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName := 'COM2';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate := CBR_1200;
//-przykładowe ustawienia znaczników sterujących DCB-
dcb.Flags := DTR_CONTROL_ENABLE or dcb_fParity;
dcb.Parity := ODDPARITY;
dcb.StopBits := ONESTOPBIT;
dcb.ByteSize := 7;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny ', mtError, [mbOk], 0);
end;
end;
//--------------------------------------------------------------------
procedure TForm1.SendClick(Sender: TObject);
begin
Repeat
StrCopy(Buffer_O, query);
RichEdit1.Text := Buffer_O;
FlushFileBuffers(hCommDev);
Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
end;
//--------------------------------------------------------------------
procedure TForm1.ReceiveClick(Sender: TObject);
begin
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
// Beep();
RichEdit1.Text := Buffer_I;
end
else
begin
RichEdit1.Text := 'Brak danych do odebrania';
Beep();
end;
end;
//--------------------------------------------------------------------
end.
Porównując przedstawione dwie techniki programowania, zgodzimy się chyba, że większe możliwości manewru będziemy mieli, wykorzystując funkcje. Możliwym było np. użycie w zdarzeniu wysyłającym dane instrukcji powtarzającej Repeat...Until, której koniec nastąpi w momencie wysłania ostatniego znaku z bufora wyjściowego. Również przy odczycie danych możemy bardzo prosto zdiagnozować, czy w ogóle jakikolwiek komunikat pojawił się w buforze wejściowym. Być może słabym punktem przedstawionego rozumowania jest fakt, że w końcowym efekcie skonstruowane przez nas funkcje zapisu i odczytu wywołaliśmy w procedurach reprezentujących odpowiednie zdarzenia. Możliwe są oczywiście inne rozwiązania, które jednak nie mieszczą się w temacie tej pracy. Jeżeli jednak ktoś bardziej pomysłowy nieco inaczej podejdzie do tego zagadnienia, książka ta w stu procentach spełni swoje zadanie. Skoro posiadamy już pewne wiadomości na temat konfigurowania i diagnozowania łącza oraz wysyłania i odbierania komunikatów poprzez interfejs szeregowy z poziomu Delphi, pora abyśmy zapoznali się z metodami transmitowania pojedynczych znaków oraz plików jako całości. Mimo że wiadomości tu zaprezentowane nie będą zbytnio odbiegały od tego, co powiedzieliśmy w części poświęconej C++Builderowi, to jednak aplikacje przedstawione w tym podrozdziale postaram się w miarę możliwości uatrakcyjnić. Wszystkie omówione nowości z powodzeniem można wykorzystać w aplikacjach pisanych w Builderze.
Wysyłamy znak po znaku
Podstawowa funkcja Win32 API służąca temu celowi jest oczywiście dostępna z poziomu Object Pascala. Niemniej jednak postaram się zaprezentować pewne dodatkowe możliwości, jakie możemy osiągnąć, korzystając z TransmitCommChar(). Zaprojektujmy w tym celu aplikację, której formularz będzie nieco odbiegał od swojego odpowiednika stworzonego w C++Builderze. Postaramy się skonstruować prosty terminal, za pomocą którego będzie można wysyłać pojedyncze znaki już w trakcie ich wpisywania z klawiatury. Dla zachowania całości obrazu uwzględnimy też możliwość wysłania pliku, którego rozmiar będzie dla nas nieistotny. Formularzskłada się z czterech doskonale znanych nam komponentów, za pomocą których w wersji podstawowej wybieramy numer portu oraz prędkość transmisji. Przyciskiem Otwórz port wywołujemy procedurę obsługi zdarzenia OpenCommClick(), zaś za pomocą Zamknij uaktywniamy CloseCommClick(). Naciskając przycisk Transmisja pliku, będziemy mogli wywołać zdarzenie, w którym wybrany przez nas plik zostanie w sposób nie buforowany przetransmitowany do otwartego wcześniej portu szeregowego. W dolnej części formularza umieściłem bardzo użyteczny komponent TStatusBar. W jego obszarze będziemy wyświetlać komunikaty o tym, czy wybrany port został otwarty oraz o końcu transmisji pliku. Podzieliłem go na dwie części - dwukrotnie klikając w nim, w edytorze statusów (Editing Status) umieściłem dwa panele: 0 - Status Panel oraz 1 - Status Panel. Wyświetlanie tam odpowiednich komunikatów jest rzeczą prostą, wystarczy w wybranym miejscu kodu napisać:
StatusBar1.Panels[0].Text := ' komunikat ';
W centralnej części formularza, w komponencie TGroupBox umieściłem obiekt typu TEdit. Tutaj właśnie będziemy wpisywali z klawiatury nasze komunikaty. Aby mogły być one transmitowane w trakcie pisania, należy okno edycji uczynić zdolnym do generowania zdarzeń. Postępujemy następująco: raz klikamy (tylko zaznaczamy) komponent Edit1 w inspektorze obiektów Object Inspector i przechodzimy od razu do karty zdarzeń Events. Zdarzeniu OnKeyPress przypiszemy Edit1KeyPress i naciśniemy Enter. W ten sposób dostaniemy się do wnętrza odpowiedniej procedury, która będzie generować interesujące nas zdarzenia. Jej nagłówek powinien wyglądać następująco:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
Pozostaje nam już teraz wypełnienie jej odpowiednią treścią. Kompletny kod zastosowanego algorytmu przedstawiony jest na wydruku
Kod głównego modułu RS_15.pas aplikacji realizującej transmisję nie buforowaną
unit RS_15;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ExtCtrls;
type
TForm1 = class(TForm)
CheckBox1: TCheckBox;
OpenComm: TButton;
CheckBox2: TCheckBox;
CheckBox3: TCheckBox;
CheckBox4: TCheckBox;
SendFile: TButton;
CloseComm: TButton;
StatusBar1: TStatusBar;
GroupBox1: TGroupBox;
Edit1: TEdit;
Label1: TLabel;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SendFileClick(Sender: TObject);
procedure Edit1KeyPress(Sender: TObject; var Key: Char);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;
// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

var
hCommDev : THANDLE; // identyfikator portu
lpFileName : PChar; // wskaźnik do nazwy portu
//lpFileName : LPCTSTR;
dcb : TDCB; // blok kontroli urządzeń
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName := 'COM2';
if (CheckBox2.Checked = TRUE) then
lpFileName := 'COM1';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);

if (CheckBox3.Checked = TRUE) then
dcb.BaudRate := CBR_1200;
if (CheckBox4.Checked = TRUE) then
dcb.BaudRate := CBR_19200;

dcb.Parity := ODDPARITY;
dcb.StopBits := ONESTOPBIT;
dcb.ByteSize := 7;
// -- przykładowe ustawienia znaczników sterujących DCB --
dcb.Flags := dcb_fParity or DTR_CONTROL_HANDSHAKE ;
// dcb.Flags := $0002 or $0020 ;
SetCommState(hCommDev, dcb);
StatusBar1.Panels[0].Text := 'Port '+lpFileName+ ' jest'+ ' otwarty';
end
else
case hCommDev of
IE_BADID:
Application.MessageBox('Niewłaściwa nazwa portu lub jest'+ ' on aktywny ', 'Uwaga !',MB_OK);
end;
end;
//---------transmisja pliku------------------------------------------
procedure TForm1.SendFileClick(Sender: TObject);
var
InFile : TextFile;
Fname : String;
chTransmit : char;
begin
if (hCommDev > 0) then
begin
Fname := 'ala.dat';
AssignFile(InFile, Fname);
try
Reset(InFile);
try
while not EOF(InFile) do
begin
read(InFile, chTransmit);
TransmitCommChar(hCommdev, chTransmit);
sleep(1);
end;
finally
StatusBar1.Panels[1].Text := 'Koniec transmisji pliku'; CloseFile(InFile);
end
except
on EInOutError do
ShowMessage('Błąd otwarcia pliku. Sprawdź, czy plik'+ ' istnieje.');
end;
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//---------wysłanie znaków z klawiatury-------------------------------
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if (hCommDev > 0) then
begin
if (Key = #13) then // jeżeli naciśniemy Enter
begin
TransmitCommChar(hCommDev, char(10)); // CR
TransmitCommChar(hCommDev, char(13)); // LF
end;
case Key of
#7 : TransmitCommChar(hCommDev, char(7)); // BEL
#8 : TransmitCommChar(hCommDev, char(8)); // BS
#32 : TransmitCommChar(hCommDev, char(32)); // spacja
#97 : TransmitCommChar(hCommDev, char(97)); // a
#98 : TransmitCommChar(hCommDev, char(98)); // b
#99 : TransmitCommChar(hCommDev, char(99));
#100: TransmitCommChar(hCommDev, char(100));
#101: TransmitCommChar(hCommDev, char(101));
#102: TransmitCommChar(hCommDev, char(102));
#103: TransmitCommChar(hCommDev, char(103));
#104: TransmitCommChar(hCommDev, char(104));
#105: TransmitCommChar(hCommDev, char(105));
#106: TransmitCommChar(hCommDev, char(106));
#107: TransmitCommChar(hCommDev, char(107));
#108: TransmitCommChar(hCommDev, char(108));
#109: TransmitCommChar(hCommDev, char(109));
#110: TransmitCommChar(hCommDev, char(110));
#111: TransmitCommChar(hCommDev, char(111));
#112: TransmitCommChar(hCommDev, char(112));
#113: TransmitCommChar(hCommDev, char(113));
#114: TransmitCommChar(hCommDev, char(114));
#115: TransmitCommChar(hCommDev, char(115));
#116: TransmitCommChar(hCommDev, char(116));
#117: TransmitCommChar(hCommDev, char(117));
#118: TransmitCommChar(hCommDev, char(118));
#119: TransmitCommChar(hCommDev, char(119));
#120: TransmitCommChar(hCommDev, char(120)); // x
#121: TransmitCommChar(hCommDev, char(121)); // y
#122: TransmitCommChar(hCommDev, char(122)); // z
end;
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
end.
Projektując zapis procedury obsługi ostatniego zdarzenia, na samym jej początku (oczywiście po upewnieniu się, czy port jest otwarty do transmisji), przewidziałem możliwość wysyłania znaków sterujących CR LF po naciśnięciu Enter. Mogę ponadto "zadzwonić" do sąsiedniego komputera, naciskając Ctrl+G, czyli transmitując znak dzwonienia lub na sąsiednim monitorze przejść na początek pisanego tekstu - BS. Uwzględniłem transmisję jedynie małych liter alfabetu łacińskiego oraz tylko niektórych dostępnych nam znaków sterujących, jednak każdy może znacznie rozszerzyć możliwości tej aplikacji. Zauważmy ponadto, że w procedurze obsługi zdarzenia SendFileClick() użyliśmy konstrukcji try...finally...end. W pierwszej kolejności zostaną wykonane instrukcje zawarte pomiędzy try oraz finally. Następnie zostaną wykonane instrukcje pomiędzy klauzulami finally oraz end, niezależnie od rezultatu wykonania pierwszej grupy instrukcji. Zewnętrzny blok try...except...end pokazuje ideę obsługi wyjątków. Dzięki zastosowaniu takiej konstrukcji dokonujemy rozdziału miejsca, w którym może wystąpić wyjątek (w naszym przykładzie będzie to próba otwarcia np. nieistniejącego pliku) od miejsca, w którym będzie on obsługiwany. Opisany mechanizm nosi nazwę strukturalnej obsługi wyjątków SEH (ang. Structural Exception Handling) i stosowany bywa wszędzie tam, gdzie żądamy, by aplikacja dalej funkcjonowała "w miarę normalnie" po wystąpieniu jakiegoś błędu.
Wysyłamy pliki
Wykonywanie w Delphi różnego rodzaju operacji plikowych w celu przesłania określonego zbioru danych w ogólnych zarysach nie różni się zbytnio od analogicznych czynności, które wykonywaliśmy z poziomu Buildera. Niemniej jednak pomiędzy tymi dwoma kompilatorami istnieją pewne różnice co do sposobu wywoływania niektórych funkcji API. Jedyną metodą na to, aby poznać te rozbieżności, jest napisanie odpowiedniego programu. Zaprojektujemy formularz wykorzystujący standardowe okna dialogowe Windows. Rysunek 5.16 pokazuje, jak może on wyglądać. W jego skład wchodzą pojedyncze komponenty TDirectoryListBox, TFileListBox, TDriveComboBox oraz TEdit. Uzupełniony jest ponadto o obiekt edycji TRichEdit, wskaźnik postępu wykonywanych operacji TProgressBar oraz po cztery komponenty TCheckBox i TButton. Projektując tego typu aplikacje, w pierwszej kolejności wszystkie obiekty dialogowe należy ze sobą powiązać, tak aby widziały się nawzajem. Można to zrobić za pomocą inspektora obiektów. Wszystkie wymagane relacje ustalimy w procedurze:
procedure TForm1.FormCreate(Sender: TObject);
begin
DirectoryListBox1.FileList := FileListBox1;
DriveComboBox1.DirList := DirectoryListBox1;
FileListBox1.FileEdit := Edit1;
ProgressBar1.Step := 1;
end;
Następnym krokiem jest zawsze przetestowanie wykonanych przypisań. Najlepiej, jeżeli w tym celu otworzymy, przeczytamy i wyświetlimy wybrany plik. Już tutaj natrafimy na nieco odmienny sposób wywołania dobrze nam znanych funkcji _lopen() oraz _lread(). Z poziomu Object Pascala wymagane jest aby, właściwość FileName obiektu TFileListBox odczytać za pomocą operacji rzutowania jako typ PChar. Należy postąpić tak dlatego, gdyż w rzeczywistości właściwość FileName jest typu String, zaś funkcja API _lopen() niechętnie operuje na takich zmiennych. Jednak anachroniczność ta, którą odziedziczyliśmy po klasycznym Pascalu, nie powinna być dla nas przeszkodą. Na szczęście korzystając z Delphi spotykamy ich już coraz mniej. Następną właściwością Object Pascala jest konieczność użycia operatora adresowego @ przy próbie wczytania danych do bufora za pomocą funkcji _lread(). Delphi potrzebuje jawnego odzyskania adresu miejsca w pamięci, pod którym aktualnie zapamiętywany jest bufor danych. Nie powinno to nikogo dziwić, chociażby ze względu na sposób deklaracji bufora:
Buffer_O : ARRAY[0..cbOutQueue] of Char;
który jest równoznaczny z zadeklarowaniem pewnego C-łańcucha. Cóż, Pascal był zawsze bardziej restrykcyjny niż C, jednak muszę przyznać, że niekiedy ma to swoje dobre strony. Poniższy przykład ilustruje jedną z metod użycia w programie napisanym w Delphi przedstawionych funkcji.
procedure TForm1.FileListBox1Change(Sender: TObject);
...
begin
hfile_s := _lopen(PChar(FileListBox1.FileName), OF_READ); if (hfile_s <> HFILE_ERROR) then
begin
...
_lread(hfile_s, @Buffer_O, cbOutQueue);
_lclose(hfile_s);
end;
end;
Kod modułu RS_16.pas
unit RS_16;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, FileCtrl;
type
TForm1 = class(TForm)
CloseComm: TButton;
CheckBox1: TCheckBox;
OpenComm: TButton;
CheckBox2: TCheckBox;
CheckBox3: TCheckBox;
CheckBox4: TCheckBox;
SendFile: TButton;
Receive: TButton;
RichEdit1: TRichEdit;
DriveComboBox1: TDriveComboBox;
DirectoryListBox1: TDirectoryListBox;
FileListBox1: TFileListBox;
Edit1: TEdit;
ProgressBar1: TProgressBar;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SendFileClick(Sender: TObject);
procedure ReceiveClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FileListBox1Change(Sender: TObject);
private
{ Private declarations }
function Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;
nNumberOfBytesToWrite: DWORD): Integer;
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}

const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;
// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;
dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $008
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;
// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;
dcb_fAbortOnError = $4000;
cbInQueue = 1024;
cbOutQueue = 1024;
var
hfile_s : HFILE; // identyfikator pliku źródłowego
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : LPCSTR;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
DirectoryListBox1.FileList := FileListBox1;
DriveComboBox1.DirList := DirectoryListBox1;
FileListBox1.FileEdit := Edit1;
ProgressBar1.Step := 1;
end;
//--------------------------------------------------------------------
procedure TForm1.FileListBox1Change(Sender: TObject);
var i : Integer;
begin

for i := 0 to cbOutQueue do
Buffer_O[i] := #0; // czyści bufor wyjściowy

hfile_s := _lopen(PChar(FileListBox1.FileName), OF_READ);

if (hfile_s <> HFILE_ERROR) then
begin
_lread(hfile_s, @Buffer_O, cbOutQueue);
RichEdit1.Text := Buffer_O;
_lclose(hfile_s);
end;
end;
//--------------------------------------------------------------------
function TForm1.Write_Comm(hCommDev: THANDLE; lpBuffer: PChar; nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;
begin
// EscapeCommFunction(hCommDev, SETRTS);
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,
NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
begin
// EscapeCommFunction(hCommDev, CLRRTS);
Write_Comm := 1
end
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin

if (CheckBox1.Checked = TRUE) then
lpFileName := 'COM2';
if (CheckBox2.Checked = TRUE) then
lpFileName := 'COM1';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox3.Checked = TRUE) then
dcb.BaudRate := CBR_1200;
if (CheckBox4.Checked = TRUE) then
dcb.BaudRate := CBR_9600;
//-przykładowe ustawienia flag sterujących DCB-
dcb.Flags := dcb_fParity;
dcb.Parity := ODDPARITY;
dcb.StopBits := TWOSTOPBITS;
dcb.ByteSize := 8;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny ', mtError, [mbOk], 0);
end;
end;
//-----------wysyłanie pliku--------------------------------------
procedure TForm1.SendFileClick(Sender: TObject);
var
i : Integer;
FileSizeHigh : DWORD;
begin
for i := 0 to cbOutQueue do
Buffer_O[i] := char(0); // czyści bufor wyjściowy
ProgressBar1.Max := 0;

if (hCommDev > 0) then
begin
if((_lopen(PChar(FileListBox1.FileName), OF_READ)) <> HFILE_ERROR) then
begin
hfile_s := _lopen(PChar(FileListBox1.FileName), OF_READ);
ProgressBar1.Max := GetFileSize(hfile_s, @FileSizeHigh);

while (_lread(hfile_s, @Buffer_O, 1) > 0) do begin
Write_Comm(hCommDev, Buffer_O, 1); // 1 bajt
ProgressBar1.StepIt();
end;
_lclose(hfile_s);

FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Nie wybrano pliku do'+ ' transmisji ', 'Uwaga !',MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//------------odbiór danych-------------------------------------------
procedure TForm1.ReceiveClick(Sender: TObject);
begin
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
RichEdit1.Text := Buffer_I;
end
else
begin
RichEdit1.Text := 'Brak danych do odebrania';
Beep(); end;
end;
//--------------------------------------------------------------------
end.

Poprawność zastosowanych przypisań sprawdziłem, nawiązując za pomocą przedstawionego programu współpracę z sąsiednim komputerem, na którym uruchomiony był Terminal dla Windows 3.11. Przesyłając pewien niewielki (ok. 3 kB) plik tekstowy, otrzymałem w pełni poprawną, obustronną transmisję. Można też zauważyć, że użyliśmy tutaj funkcji Write_Comm() o trzech (a nie jak poprzednio dwóch) parametrach. Uczyniłem tak dlatego, by uzyskać przede wszystkim pełną przejrzystość ale też i zgodność przypisań z _lread(). Zaprezentuję teraz trochę bardziej skomplikowaną w swojej budowie aplikację, za pomocą której możemy nadawać i odbierać oraz zapisywać na dysku zarówno pliki jak i dowolne, wpisywane z klawiatury lub kopiowane z innych edytorów (np. Word) ciągi znaków. Jest rzeczą bardzo ważną, że pisane przez nas programy komunikacyjne są w pełni kompatybilne z profesjonalnymi edytorami tekstu. Analogicznie jak w przypadku odpowiednika napisanego w C++Builderze zaprojektowałem w Object Pascalu uproszczony edytor IDE, którego obsługa ukryta jest w procedurach obsługi zdarzeń: CopyTextClick(), PasteTextClick()oraz CutTextClick(). Mając bardziej kompletny przykład w postaci formularza projektu p_RS_06.bpr, omówionego w części poświęconej Builderowi, bez problemu można samodzielnie uzupełnić takie miniśrodowisko edycyjne prezentowane na bieżącym przykładzie. Wygląd działającej aplikacji komunikacyjnej korzystającej z takiego prostego IDE zaś jej kod źródłowy RS_17.pas przedstawiono na wydruku Zastosowałem tu tylko jedno okno edycji, w którym wyświetlamy zarówno pliki lub inne komunikaty przeznaczone do wysłania, jak też dane odbierane z portu szeregowego. W przedstawionej niżej aplikacji uwzględniłem ponadto możliwość wyświetlania tekstu w linii tytułowej formularza. Głównym zadaniem tej linii będzie wyświetlanie nazwy aktualnie edytowanego pliku. Dodatkowo będzie się tam pojawiać również nazwa działającej aplikacji. Wszystkie wymienione czynności zostaną wykonane dzięki procedurze FormCaption():
procedure TForm1.FormCaption(const sFile_s: String);
begin
sFile := sFile_s;
Caption := Format('%s - %s', [ExtractFileName(sFile_s), Application.Title]);
end;
Testując przedstawiony program, zapoznamy się ponadto ze sposobami posługiwania się innego rodzaju oknami dialogowymi, za pomocą których możemy wczytać i ewentualnie zapisać różnego rodzaju dane. Projektując procedurę obsługi zdarzenia FileOpenClick(), wykorzystaliśmy standardowe właściwości dialogu TOpenDialog, za pomocą którego możemy wczytać większość dostępnych plików. Zdarzenie SaveAs1Click(), posługujące się dialogiem TSaveDialog, umożliwia zapisanie w postaci pliku aktualnej zawartości danego pola edycji.
Kod modułu RS_17.pas
unit RS_17;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, FileCtrl, ToolWin, Buttons, Menus;
type
TForm1 = class(TForm)
CloseComm: TButton;
OpenComm: TButton;
SendFile: TButton;
Receive: TButton;
RichEdit1: TRichEdit;
ProgressBar1: TProgressBar;
OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
CoolBar1: TCoolBar;
CopyText: TSpeedButton;
PasteText: TSpeedButton;
CutText: TSpeedButton;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
CheckBox3: TCheckBox;
CheckBox4: TCheckBox;
MainMenu1: TMainMenu;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SendFileClick(Sender: TObject);
procedure ReceiveClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CopyTextClick(Sender: TObject);
procedure PasteTextClick(Sender: TObject);
procedure CutTextClick(Sender: TObject);
procedure FileOpenClick(Sender: TObject);
procedure NewClick(Sender: TObject);
procedure SaveAs1Click(Sender: TObject);
procedure SendWrittenClick(Sender: TObject);
private
{ Private declarations }
sFile: String;
procedure FormCaption(const sFile_s: String);
procedure CheckFileSave;
procedure ShowFileOpen(const sFile_O: String);
function Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;
nNumberOfBytesToWrite: DWORD): Integer;
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}

const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;

// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

cbInQueue = 1024;
cbOutQueue = 1024;
var

hfile_s : HFILE; // identyfikator pliku źródłowego
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : LPCSTR;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
CheckFileSave;
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
OpenDialog1.InitialDir := ExtractFilePath(ParamStr(0));
SaveDialog1.InitialDir := OpenDialog1.InitialDir;
ProgressBar1.Step := 1;
end;
//--------------------------------------------------------------------
procedure TForm1.CopyTextClick(Sender: TObject);
begin
RichEdit1.CopyToClipboard;
end;
//-------------------------------------------------------------------
procedure TForm1.PasteTextClick(Sender: TObject);
begin
RichEdit1.PasteFromClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.CutTextClick(Sender: TObject);
begin
RichEdit1.CutToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.FormCaption(const sFile_s: String);
begin
sFile := sFile_s;
Caption := Format('%s - %s', [ExtractFileName(sFile_s), Application.Title]);
end;
//--------------------------------------------------------------------
procedure TForm1.CheckFileSave;
var
iCheckSave: Integer;
begin
if not RichEdit1.Modified then
Exit
else
begin
iCheckSave := MessageDlg(Format('Zawartość pliku lub okna'+ ' została zmieniona. Zapisać zmiany %s? ', [sFile]), mtConfirmation, mbYesNoCancel, 0);
case iCheckSave of
idYes: SaveAs1Click(Self);
idNo: {};
idCancel: Abort;
end;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.FileOpenClick(Sender: TObject);
begin
CheckFileSave;
if OpenDialog1.Execute then
begin
ShowFileOpen(OpenDialog1.FileName);
RichEdit1.ReadOnly := ofReadOnly in OpenDialog1.Options;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.NewClick(Sender: TObject);
begin
FormCaption('Bez nazwy');
RichEdit1.Lines.Clear;
RichEdit1.Modified := FALSE;
end;
//--------------------------------------------------------------------
procedure TForm1.SaveAs1Click(Sender: TObject);
begin
if SaveDialog1.Execute then
begin
if FileExists(SaveDialog1.FileName) then
if MessageDlg(Format('Plik zapisany ponownie %s', [SaveDialog1.FileName]), mtConfirmation, mbYesNoCancel, 0) <> idYes then Exit;
RichEdit1.Lines.SaveToFile(SaveDialog1.FileName);
FormCaption(SaveDialog1.FileName);
RichEdit1.Modified := FALSE;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.ShowFileOpen(const sFile_O: string);
begin
RichEdit1.Lines.LoadFromFile(sFile_O);
FormCaption(sFile_O);
RichEdit1.SetFocus;
RichEdit1.Modified := FALSE;
end;
//--------------------------------------------------------------------
function TForm1.Write_Comm(hCommDev: THANDLE; lpBuffer: PChar; nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;

begin
// EscapeCommFunction(hCommDev, SETRTS);
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,
NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
begin
// EscapeCommFunction(hCommDev, CLRRTS);
Write_Comm := 1
end
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue; ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName := 'COM1';
if (CheckBox2.Checked = TRUE) then
lpFileName := 'COM2';

hCommDev := CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);

if (CheckBox3.Checked = TRUE) then
dcb.BaudRate := CBR_1200;
if (CheckBox4.Checked = TRUE) then
dcb.BaudRate := CBR_19200;

//-przykładowe ustawienia znaczników sterujących DCB-
dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;
dcb.StopBits := TWOSTOPBITS;
dcb.ByteSize := 7;

SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny ', mtError, [mbOk], 0);
end;
end;
//-----------wysyłanie pliku--------------------------------------
procedure TForm1.SendFileClick(Sender: TObject);
var
i : Integer;
FileSizeHigh : DWORD;
begin
for i := 0 to cbOutQueue do
Buffer_O[i] := char(0); // czyści bufor wyjściowy

ProgressBar1.Max:=0;

if (hCommDev > 0) then
begin
if((_lopen(PChar(OpenDialog1.FileName), OF_READ)) <> HFILE_ERROR) then
begin
hfile_s := _lopen(PChar(OpenDialog1.FileName), OF_READ);
ProgressBar1.Max:=GetFileSize(hfile_s, @FileSizeHigh);
while (_lread(hfile_s, @Buffer_O, 1) > 0) do
begin
Write_Comm(hCommDev, Buffer_O, 1); // transmisja
// 1 bajtu
ProgressBar1.StepIt();
end;
_lclose(hfile_s);

FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Nie wybrano pliku do'+ ' transmisji ', 'Uwaga !',MB_OK);

end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);

end;
//------------odbiór danych-------------------------------------------
procedure TForm1.ReceiveClick(Sender: TObject);
begin
ProgressBar1.Max := 0;
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
RichEdit1.Text := Buffer_I;
end
else
begin
RichEdit1.Text := 'Brak danych do odebrania';
Beep();
end;
end;
//--------transmisja danych wpisanych---------------------------------
procedure TForm1.SendWrittenClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, PChar(RichEdit1.Text));
ProgressBar1.Max := 0;
ProgressBar1.Max := SizeOf(PChar(RichEdit1.Text));
Write_Comm(hCommDev, Buffer_O, StrLen(Buffer_O));
ProgressBar1.StepIt();
FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
end.
Ze względu na to, że korzystamy tu z TRichEdit, dane siłą rzeczy będą zapisane w tym formacie. Widoczne jest to przy próbie odczytu tak przetransmitowanego pliku. Jeżeli nie jest on wyświetlany w zgodnym formacie, tekst będzie poprzedzony specjalnymi znakami formatowania. Trudność tę można przezwyciężyć, jeżeli zastosujemy w zdarzeniach czytających i zapisujących pliki na dysku konstrukcje typowe dla Object Pascala. Procedury alternatywne do tych przedstawionych na wydruku 5.15, obsługujące typowe pliki tekstowe, z powodzeniem mogą przyjąć następującą budowę:
procedure TForm1.FileOpen_2Click(Sender: TObject);
var
InFile : TextFile;
sFname, sIndata : String;
begin
RichEdit1.Lines.Clear;
if OpenDialog1.Execute then
begin
sFname := OpenDialog1.Filename;
AssignFile(InFile, sFname);
Reset(InFile);
while not EOF(InFile) do
begin
ReadLn(InFile, sIndata);
RichEdit1.Lines.Add(sIndata);
end;
CloseFile(InFile);
Form1.Caption := 'Edycja [' + sFname + ' ]';
end;

end;
//--------------------------------------------------------------------
procedure TForm1.FileSave_2Click(Sender: TObject);
var
OutFile : TextFile;
sFname : String;
begin
if SaveDialog1.Execute then
begin
sFname := SaveDialog1.FileName;
AssignFile(OutFile, sFname);
Rewrite(OutFile);
WriteLn(OutFile, RichEdit1.Text);
CloseFile(OutFile);
Form1.Caption := 'Zapisany [ ' + sFname + ' ]';
end;
end;
//--------------------------------------------------------------------
Zauważmy, że po raz pierwszy mamy możliwość wykorzystania pętli, która do obiektu TRichEdit ładuje kolejne wiersze pobrane z pliku. Wiele miejsca poświęciliśmy na przedstawienie różnych sposobów odczytu i zapisu plików. Wbrew pozorom jest to zawsze bardzo ważny element aplikacji komunikacyjnej, gdyż zajmując się szeroko rozumianym sterowaniem czy komunikacją komputerową, prędzej czy później należy liczyć się z koniecznością obsługi wysyłanych lub otrzymywanych zbiorów danych. Bardzo często zachodzi też potrzeba przedstawienia informacji zawartych w plikach w postaci różnego rodzaju wydruków.
Timer w Delphi
Sposób wykorzystania właściwości komponentu TTimer w Object Pascalu nie różni się w istocie od tego, co zaprezentowaliśmy wcześniej. Niemniej jednak omówimy obecnie nieco dokładniej pewne aspekty wykorzystywania Timera w programach pomiarowych. zaprezentujemy jeden z możliwych sposobów jego użycia w aplikacji obsługującej pewne bardzo nowoczesne urządzenie ,służące do stabilizacji i odczytu temperatury. Jest to najnowsza odmiana rodziny mierników, za pomocą których testowałem poprzednie programy. Obsługa tego konkretnego modelu wymaga stosowania w połączeniu jedynie linii TxD, RxD oraz przewodu masy. Koniec wysyłanych przez siebie danych znaczy on parą znaków CR LF. Odczytywana temperatura (w stopniach Kelvina) wyświetlana jest w obiekcie edycji TRichEdit. Przy pomocy komponentu TTrackBar można prawie płynnie ustalać częstość odczytu wskazań przyrządu. Wykorzystane przeze mnie właściwości TTrackBar umożliwiają wykonanie w trakcie pomiaru płynnej synchronizacji generowanych przez aplikację zdarzeń ze zdarzeniami generowanymi przez miernik. Dzięki procedurze obsługi zdarzenia TimerONClick() skojarzonego z przyciskiem Rozpocznij pomiar uaktywniamy obsługę zdarzenia TimerOnTimer(), gdzie dokonuje się właściwy odczyt temperatury. Zauważmy, że dzięki zastosowaniu instrukcji Repeat...Until proces wysyłania zapytania będzie powtarzany, aż do momentu wysłania ostatniego znaku z bufora wyjściowego. Nie ma tu żadnych instrukcji opóźniających typu Sleep()! Jest jeszcze jedna, bardzo ważna zaleta takiej konstrukcji części algorytmu wysyłającego dane - nie musimy przy nadawaniu zbytnio przejmować się znacznikiem końca danych CR LF.
Kod modułu RS_18.pas aplikacji wykorzystującej komponent TTimer
unit RS_18;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ExtCtrls;
type
TForm1 = class(TForm)
CloseComm: TButton;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
OpenComm: TButton;
TimerON: TButton;
TimerOFF: TButton;
RichEdit1: TRichEdit;
Timer1: TTimer;
TrackBar1: TTrackBar;
Edit1: TEdit;
Label1: TLabel;
Label2: TLabel;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure TimerONClick(Sender: TObject);
procedure TimerOFFClick(Sender: TObject);
procedure TimerOnTimer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
private
{ Private declarations }
function Write_Comm(hCommDev: THANDLE; nNumberOfBytesToWrite: DWORD): Integer;
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;

// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;
cbInQueue = 16;
cbOutQueue = 16;
var
query : PChar = 'CDAT?'+#13+#10; // przykładowe zapytanie // zakończone parą znaków CR LF
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
begin
Timer1.Enabled := FALSE;
CloseHandle(hCommDev);
Application.Terminate();
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
Timer1.Enabled := FALSE;
Timer1.Interval := 1000;
TrackBar1.Max := 1000;
TrackBar1.Min := 1;
TrackBar1.Frequency := 100;
end;
//--------------------------------------------------------------------
function TForm1.Write_Comm(hCommDev: THANDLE; nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;
begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function TForm1.Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue; ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then lpFileName := 'COM2';
hCommDev := CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE,0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate := CBR_1200;
//-- przykładowe ustawienia znaczników sterujących DCB -- dcb.Flags := dcb_fParity;
dcb.Parity := ODDPARITY;
dcb.StopBits :=ONESTOPBIT;
dcb.ByteSize :=7;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of IE_BADID: Application.MessageBox('Niewłaściwa nazwa portu'+ ' lub jest on aktywny ', 'Uwaga !',MB_OK);
end;
end;
//--------------------------------------------------------------------
procedure TForm1.TimerONClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, query);
Timer1.Enabled := TRUE;
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.TimerOFFClick(Sender: TObject);
begin
Timer1.Enabled := FALSE;
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
Timer1.Interval := TrackBar1.Position;
Edit1.Text := IntToStr(TrackBar1.Position);
end;
//--------------------------------------------------------------------
procedure TForm1.TimerOnTimer(Sender: TObject);
begin
Repeat
// wysłanie zapytania
FlushFileBuffers(hCommDev);
Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);

if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then // odbiór
// danych
RichEdit1.Text := Buffer_I
else
RichEdit1.Text := 'Brak danych';
end;
//--------------------------------------------------------------------
end.
Spoglądając na treść podobnych algorytmów napisanych w C++Builderze, możesz z powodzeniem użyć instrukcji do...while w celu płynnego dostrojenia się do miernika. Cóż, nie ukrywam, że pisząc programy w Delphi trochę ściągaliśmy z Buildera. Może warto dla zachowania równowagi czasami postąpić odwrotnie? Aby wyłączyć generację zdarzenia cyklicznie wysyłającego i odbierającego dane wystarczy nacisnąć przycisk Zakończ pomiar, przez co wywołamy procedurę obsługi zdarzenia TimerOFFClick(). Musimy jeszcze zastanowić się nad tym, czy istnieje możliwość szybkiego stwierdzenia, z poziomu działającej aplikacji, że miernik rzeczywiście w jakiś sposób znaczy koniec wysyłanych przez siebie komunikatów. Możemy to stwierdzić, ale należy zdawać sobie sprawę z pewnych właściwości różnych komponentów edycyjnych. Spróbujemy wyświetlić rezultat pomiaru za pomocą trzech obiektów, których używaliśmy do tej pory: TRichEdit, TEdit oraz TMemo.. O ile w przypadku dwóch skrajnych komponentów edycyjnych nie ma różnicy co do formatu wyświetlania wartości aktualnie mierzonej temperatury, to w środkowym typu TEdit pojawiły się dwa kwadraciki zaraz po odczytanej wartości . To jest właśnie owa para znaków CR LF. Wygląd symboli reprezentujących wymienioną parę znaków z reguły nie zależy w Windows od użytej przez programistę funkcji, zamieniającej liczbę na odpowiedni łańcuch. W tym przykładzie użyłem prostych przypisań:
RichEdit1.Text := Buffer_I;
Edit2.Text := Buffer_I;
Memo1.Text := Buffer_I;
Równie dobrze o tym fakcie można się przekonać, odczytując liczbę rzeczywiście odebranych bajtów. Z reguły będzie ona o dwa większa niż ilości znaków aktualnie wyświetlonych na ekranie. Osoby zaniepokojone faktem pojawiania się jakiś dodatkowych symboli w oknie edycji pragnę od razu uspokoić. W żadnym wypadku nie zostają one uwzględniane przy zapisie danych do pliku. Informacja, którą daje nam nowoczesny przyrząd, jest bardzo dyskretna. Spoglądając na ostatni rysunek, powróćmy jeszcze na chwilę do niezwykle ważnego tematu, czyli problemu synchronizacji stworzonego programu z urządzeniem pomiarowym. W przykładzie napisanym w Builderze funkcję taką pełnił komponent TCSpinEdit. Obecnie zastosowaliśmy prosty suwak, którego zakres zmienności podzieliłem na 1000 części. Używając obiektu TTimer mogę więc minimalną częstość jego wyzwalania określić w przybliżeniu na 1 milisekundę. Jest to oczywiście w odniesieniu do tego konkretnego urządzenia wartość znacznie przesadzona, niemniej jednak oddaje chyba ideę synchronizacji działania aplikacji z pracą miernika. Stosując tak prostą metodę, można z dobrym przybliżeniem określić w miarę optymalną częstość odpytywania przyrządu. Jest to wielka zaleta techniki programowania obiektowo-zdarzeniowego, które niewątpliwie zmienia podejście do sposobu pisania tego typu aplikacji. Projektując warstwę synchronizacyjną programu, musimy mieć przede wszystkim na uwadze jej optymalność. Nie ma wielkiego sensu próbkowanie łącza w czasie krótszym niż możliwości danego przyrządu. Musimy też zdawać sobie sprawę z przeznaczenia naszego programu. W omawianym przykładzie posłużyłem się urządzeniem, którego głównym zadaniem jest bardzo dokładna stabilizacja i odczyt temperatury. To, że jest w stanie odpowiadać co kilkaset milisekund nie stanowi dla mnie niespodzianki. W praktyce producenci konkretnych przyrządów pomiarowych częstość próbkowania łącza, do którego urządzenie jest przyłączane, podają z pewną dość szeroką tolerancją. Dochodzimy więc do tego, że sam programista musi taki w miarę optymalny czas ustalić. Z reguły postępuje się w sposób następujący: wykorzystując jakiekolwiek zaprojektowane wcześniej zdarzenie, sprawdzamy moment, w którym przestajemy otrzymywać błędne (przekłamane) informacje pochodzące od urządzenia. Wcale to jednak nie znaczy, że jeżeli przy ustalonym przedziale próbkowania, np. 750 ms, otrzymuję w dość długim okresie prawidłowe wskazania przyrządu, to wszystko jest już w porządku. Może się na przykład okazać, że dla wartości 760 ms pomiar zacznie przebiegać bardziej miarowo. Dlatego należy mieć na uwadze dwie sprawy: bezbłędny odczyt w długim przedziale czasu oraz regularną jego powtarzalność co pewien (najkrótszy z możliwych) przedziałów czasowych. Z reguły pierwsze testy świeżo napisanej tego typu aplikacji trwają wiele godzin. Zobaczmy, jak to wygląda w praktyce. Na rysunku 5.21 zamieściłem testy programu sterującego miernikiem, odczytującym temperaturę pewnego układu fizycznego. Przyjąłem najprostsze z możliwych rozwiązań. Kawałek stali kwasoodpornej mający temperaturę początkową około 311 K szybko podgrzałem do temperatury około 326,5 K, następnie ochładzałem go, rejestrując wskazania przyrządu w funkcji czasu względnego (numeru pomiaru). Oba eksperymenty były przeprowadzane z nieco odmienną szybkością ochładzania materiału jedynie po to, by wyniki były rozróżnialne na wykresie, fakt ten nie wpływa w żadnym wypadku na wynik synchronizacji programu z urządzeniem. Zauważmy, że przy polepszającym się stopniu synchronizacji zmiana temperatury w funkcji kolejnego pomiaru widoczna jest w postaci schodków. Im regularniej będą one rozłożone oraz im mniejsza będzie ich szerokość, tym lepszy jest stopień synchronizacji miernika i programu zbierającego dane. Jeżeli dysponowalibyśmy bardzo szybkim przyrządem oraz poprawnie działającą aplikacją, tego typu wykres przedstawiałby linię gładką. Podobnie gładki wykres możemy otrzymać, przyjmując wystarczająco długi okres pomiędzy dwoma odczytami. Mówimy wówczas, że pomiary są uśredniane w czasie. Czytając wszystko, co zostało napisane do tej pory o obiekcie typu TTimer, zapewne niejeden Czytelnik może lekko powątpiewać w celowość powoływania się na możliwości uzyskania tak dokładnego przedziału czasu próbkowania łącza. Panuje bowiem opinia, że dokładność i powtarzalność działania tego komponentu są, delikatnie mówiąc, średniej jakości. Zgadzam się, że Timer nie jest bynajmniej sztandarowym osiągnięciem technologii informatycznych XX wieku. Z drugiej jednak strony jest powszechnie dostępnym i wygodnym narzędziem. Jeżeli tylko używa się go w sposób rozsądny, program może działać poprawnie. Timer służy do generowania zdarzeń w mniej lub bardziej jednakowych odstępach czasu. Nic nie stoi na przeszkodzie, by porównać jego działanie z funkcjonowaniem zegara systemowego, który można znaleźć w Panelu sterowania. Zbudowałem naprawdę prostą aplikację, której jedynym celem jest cykliczne, wraz z działaniem Timera, wyświetlanie kolejnych liczb Wykorzystany w tym przykładzie opis procedury obsługi zdarzenia TimerOnTimer() jest banalny:
var
intVar: int64;
...
intVar := 0;
...
procedure TForm1.TimerOnTimer(Sender: TObject);
begin
Inc(intVar);
RichEdit1.Text := IntToStr(IntVar);
Beep();
end;
Każda osoba, która zechce taką aplikację przetestować w sposób pokazany na powyższym rysunku, od razu zauważy, że w funkcjonowaniu tych dwóch zupełnie niezależnych programów występuje dość wyraźne podobieństwo. Powiedzmy, że właściwość Interval obiektu TTimer ustalimy na 1000 ms. Przyjrzyjmy się dokładnie - te dwa zegary po pewnym, nawet dość krótkim czasie, najzwyczajniej w świecie zaczną się zdudniać! Nic w tym dziwnego, korzystają z tych samych zasobów Windows. Oczywiście, że efektu takiego nigdy w 100% w tym systemie operacyjnym nie wyeliminujemy, możemy natomiast w pewnym stopniu go ograniczyć. Wystarczy, abyśmy czas próbkowania Timera ustalili trochę mniejszy, powiedzmy na 990 ms. Od razu zauważymy, że oba zegary zaczęły pracować bardziej stabilnie. Można w Windows znaleźć jeszcze parę podobnych ciekawostek, które dla osoby piszącej tego typu programy i znającej podstawy fizyki nie powinny stanowić żadnego wyzwania. Należy również być świadomym faktu, że używanie Timera nie zapewni nam jakiejś super szybkiej możliwości odczytywania danych, jednak dla większości spotykanych przypadków może on być całkiem użyteczny. Spotyka się ultraszybkie urządzenia, mierzące z dokładnością nanosekund różne parametry (w tym temperaturę np. gazu w przepływie), jednak przyrządów takich nie obsługują PC, zaś stosowanych protokołów transmisji danych na próżno szukać w jakiejkolwiek literaturze. Kończąc rozważania o Timerze, chciałbym przedstawić przykład kompletnej aplikacji obsługującej pewien woltomierz cyfrowy. Na rysunku 5.23 pokazano wygląd jej formularza, którego projekt zamieszczono w katalogu \KODY\DELPHI\RS_20\p_RS_20.dpr. Zastosowałem tu dobrze nam już znane komponenty. Konstrukcja algorytmu umożliwia zatrzymanie pomiaru w dowolnej chwili. Wówczas zawartość poszczególnych okien edycji można przekopiować do schowka (oczywiście klikając uprzednio w obszar jednego z nich). Wyniki takie bez problemu można już wstawić do dowolnego arkusza kalkulacyjnego czy innego programu graficznego akceptującego dane w postaci kolumn liczb. Aktywne własności ScrollBars komponentów TMemo umożliwiają ponadto wygodny przegląd całości pomiarów w trakcie działania programu. Dla wygody obsługi zdarzenia TimerOnTimer(), części programu wysyłające zapytanie do woltomierza oraz odczytujące jego wskazania zapisałem w oddzielnych funkcjach RS_Send() oraz RS_Receive(). Zastosowałem dodatkowo jeszcze jedną, dosyć ciekawą modyfikację - tuż po rozpoczęciu pomiaru przycisk Otwórz port staje się nieaktywny. Jest to jeden ze sposobów, w jaki można uchronić program przed nadmiernie dociekliwymi Użytkownikami, którzy w wolnych chwilach lubią zadawać sobie pytania będące niewątpliwie rezultatem głębszych przemyśleń, w stylu: Co się stanie, gdy w trakcie działania programu będę bez przerwy naciskał jakiś przycisk?
Kod modułu RS_20.pas aplikacji zbierającej dane z woltomierza cyfrowego
unit RS_20;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons;
type
TForm1 = class(TForm)
Start: TButton;
Suspend: TButton;
Resume: TButton;
CloseComm: TButton;
OpenComm: TButton;
Memo2: TMemo;
Timer1: TTimer;
Edit1: TEdit;
TrackBar1: TTrackBar;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Panel1: TPanel;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
Label4: TLabel;
Label5: TLabel;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
Memo1: TMemo;
procedure StartClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure SuspendClick(Sender: TObject);
procedure ResumeClick(Sender: TObject);
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure TimerOnTimer(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var

Form1: TForm1;
implementation
{$R *.DFM}


const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;

cbInQueue = 32; // rozmiary buforów danych
cbOutQueue = 32;
var
query : PChar = 'CDAT?'+#13+#10; // przykładowe zapytanie
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;

intVar: LongWord; // licznik pomiarów
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckProcess: Integer;
begin
iCheckProcess := MessageDlg('Zakończenie pomiaru i zamknięcie'+ ' aplikacji?', mtConfirmation, [mbYes, mbNo], 0);
case iCheckProcess of
idYes:
begin
Timer1.Enabled := FALSE;
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: Exit;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName := 'COM2';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate:=CBR_1200;
//-przykładowe ustawienia znaczników sterujących DCB-
dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;
dcb.StopBits := ONESTOPBIT;
dcb.ByteSize := 7;

SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID:
begin
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny', 'Uwaga !',MB_OK);
lpFileName:='';
end;
end;

end;
//--------------------------------------------------------------------
function Write_Comm(hCommDev: THANDLE;
nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;

begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function Read_Comm(hCommDev: THANDLE;
Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
function RS_Send: Integer;
begin
Repeat
FlushFileBuffers(hCommDev);
Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
Form1.Memo1.Lines.Add('');
Result := 0;
end;
//--------------------------------------------------------------------
function RS_Receive: Integer;
begin
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
// Beep();
end
else
begin
Form1.Memo2.Lines.Add('x0');
Beep();
Form1.Memo2.Lines.Add('');
end;
Result:=0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
OpenComm.Enabled := FALSE;
Timer1.Enabled:=TRUE;
Label1.Caption := 'Pomiar';
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);

end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
Timer1.enabled := FALSE;
Timer1.Interval := 1000;
TrackBar1.Position := 1000;
TrackBar1.Max := 2000;
TrackBar1.Min := 1;
TrackBar1.Frequency := 100;
OpenComm.Enabled := TRUE;
intVar := 0;
end;
//--------------------------------------------------------------------
procedure TForm1.SuspendClick(Sender: TObject);
begin
Timer1.Enabled := FALSE;
Label1.Caption := 'Wstrzymanie';
end;
//--------------------------------------------------------------------
procedure TForm1.ResumeClick(Sender: TObject);
begin
Timer1.Enabled := TRUE;
Label1.Caption := 'Pomiar';
end;
//--------------------------------------------------------------------
procedure TForm1.TimerOnTimer(Sender: TObject);
begin
StrCopy(Buffer_O, query);
RS_Send;
RS_Receive;
end;
//--------------------------------------------------------------------
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
Form1.Memo2.SelectAll;
Form1.Memo2.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
Form1.Memo1.SelectAll;
Form1.Memo1.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
Timer1.Interval := TrackBar1.Position;
Edit1.Text := IntToStr(TrackBar1.Position);
end;
//--------------------------------------------------------------------
end.

Aplikacje wielowątkowe
Możliwość tworzenia aplikacji wielowątkowych posługujących się zaletami programowania współbieżnego jest jedną z najbardziej atrakcyjnych technik, oferowanych w Windows przez 32-bitowy interfejs programisty. Teoretycznie rzecz biorąc, każdej części pisanego kodu można przyporządkować oddzielny wątek (ang. thread), stanowiący pewien obiekt wykorzystywany przez system operacyjny w ramach danego procesu. Każda projektowana przez nas aplikacja ma co najmniej jeden wątek główny, w którym możemy tworzyć kolejne, zwane wątkami drugorzędnymi. Wielowątkowość nierozerwalnie wiąże się z pojęciem wielozadaniowości. W Win32 API to właśnie wątki są tymi obiektami, które mogą ubiegać się o czas procesora. Nie ma wówczas możliwości całkowitego podporządkowania pracy procesora pojedynczemu wątkowi. System operacyjny sam decyduje, jaki czas należy przydzielić poszczególnym wątkom, po upływie którego mogą zostać wywłaszczone. Niekiedy nazywamy to wielozadaniowością z wywłaszczeniem. Nie będzie nas jednak interesować cała, niezwykle szeroka oferta programistyczna udostępniana przez wielowątkowość. Chociaż technika programowania współbieżnego oferuje nam olbrzymie możliwości, ma jednak i drugą stronę. Niewłaściwe je użycie może się okazać katastrofalne w skutkach dla działającej aplikacji. Trzeba zdawać sobie sprawę, że pisane przez nas programy są tworami dosyć specyficznymi. Technika ich projektowania znacznie odbiega od sposobu tworzenia stron WWW, skomplikowanych arkuszy kalkulacyjnych lub baz danych. O ile w przypadku wymienionych aplikacji po drugiej stronie jest zawsze inny człowiek (odbiorca), który bywa czasami wyrozumiały na popełnione przez nas niewielkie błędy, to projektując program sterujący jakimś urządzeniem, tego komfortu już nie mamy. Testy takich programów są zawsze bezlitosne, a ich ocena wyraża się prostą logiką zero-jedynkową (FALSE or TRUE). Gdy nieopatrznie wpiszemy jakąś komórkę w arkuszu kalkulacyjnym, błąd taki możemy naprawić stosunkowo prosto. Co się natomiast stanie, gdy do zasilacza wysokiego napięcia podłączonego do jakiegoś przyrządu wyślemy komendę: :VOLTage 550 zamiast prawidłowej :VOLTage 250? Różnica niby nieznaczna, tylko jedna cyfra... ale niekiedy po urządzeniu może pozostać jedynie wspomnienie, zaś na naszym koncie spory debet. Dlatego dalej skoncentrujemy się na pewnych podstawowych, ale skutecznych metodach posługiwania się techniką programowania współbieżnego z perspektywą użytecznego, a zarazem bezpiecznego jej wykorzystania w aplikacjach realizujących szeregową transmisję danych poprzez interfejs RS 232C.
Najważniejszy jest Użytkownik
Projektując każdą aplikację, musimy pamiętać, że oprócz poprawnego działania musi charakteryzować się jeszcze kilkoma bardzo ważnymi cechami. Dokładny opis podstawowych reguł dotyczących sposobu tworzenia programów o różnorodnym przeznaczeniu był tematem wielu publikacji, dlatego w tym miejscu przedstawię tylko najważniejsze spośród nich, mające zarazem bezpośrednie odniesienie to tego, co już stworzyliśmy. Wielowątkowe działanie naszych programów uwzględnimy, dostosowując je do nowych wymagań, jakie przed nimi zostaną postawione.
Użytkownik steruje programem
Dla większości z nas irytującą bywa sytuacja gdy podczas pracy z jakąś aplikacją uświadomimy sobie, że w pewnym momencie kontrolę nad nami zaczął sprawować komputer. Testując przedstawione do tej pory programy realizujące transmisję szeregową na pewno nie raz mieliśmy takie odczucie. Najbardziej widoczne jest to w przypadku programów transmitujących i odbierających pliki. Gdy zaczęliśmy transmisję wybranego wcześniej pliku praktycznie nie można było już nic zrobić. Aplikacja, pozostając nieruchomą na ekranie, nie reagowała na próbę naciśnięcia jakiegokolwiek przycisku aż do momentu zakończenia danego zadania. Pierwszym określeniem, jakie przychodzi mi wówczas na myśl, jest bezwładność i pewna ociężałość takiego produktu. Niewielu Użytkownikom podobają się tak działające aplikacje. Wyzwaniem będzie wówczas dla programisty wymyślenie sposobu, dzięki któremu program stanie się bardziej przyjazny wobec otoczenia.
Możliwość anulowania decyzji
Uwzględnienie tej opcji wynika bezpośrednio z poprzedniego punktu. Poprawnie zaprojektowane programy komunikacyjne powinny przynajmniej częściowo umożliwiać Użytkownikowi anulowanie, nawet w czasie transmisji niektórych podjętych wcześniej decyzji. Ponownie posłużmy się przykładem aplikacji transmitującej plik. Pamiętamy, że jednym ze sposobów zabezpieczenia się przed wysłaniem niechcianego zbioru danych jest obejrzenie go tuż przed transmisją w jednym z okien edycyjnych. Niestety, konstrukcja dotychczasowych programów umożliwiała nam jedynie obejrzenie pliku, nie mieliśmy natomiast żadnej możliwości przerwania w dowolnym momencie jego transmisji i ewentualnie wznowienia jej, nie ingerując zbytnio w tempo działania aplikacji. Ten punkt odnosi się głównie do problemu transmisji większych pakietów danych, gdyż trudno wyobrazić sobie sytuację anulowania decyzji w trakcie wysyłania jednego znaku.
Możliwość odbioru komunikatu nawet w trakcie wysyłania danych
Uwzględnienie tej opcji w naszych aplikacjach może wydać się nieco dziwne. Każdy Czytelnik zdaje sobie oczywiście sprawę z pewnych ograniczeń, jakie nakłada na nas sam fakt posługiwania się szeregową transmisją asynchroniczną. Wykorzystanie w naszych algorytmach tej własności wcale nie będzie wymagało zastosowania jakiegoś wyszukanego okablowania czy niezrozumiałej modyfikacji protokołu transmisji. Zupełnie wystarczy, jeżeli w pełni wykorzystamy poznane już zalety, związane z podwójnym buforowaniem danych.
Możliwość wysłania odrębnej informacji w trakcie transmisji pliku
Posługując się różnego rodzaju programami nadzorującymi proces transmisji szeregowej, możemy spotkać się z koniecznością wysłania jakiejś wiadomości (niekoniecznie bardzo krótkiej) w trakcie transmisji dłuższej porcji informacji. W takich sytuacjach musimy uwzględnić fakt zachwiania parytetu kolejki znaków w buforze wyjściowym. Wcześniej została omówiona funkcja TransmitCommChar(), jednak stosowanie jej bywa nieco uciążliwe, głównie z tego powodu, że argumentem jej może być tylko jeden znak. Naprawdę funkcjonalna aplikacja do transmisji szeregowej powinna posiadać opcję, pozwalającą na szybką modyfikację kolejki znaków będących już w buforze wyjściowym, jednak bez naruszenia ich fizycznej spójności. Być może postulaty dotyczące spodziewanego rozwoju pisanych do tej pory programów zawarte w powyższych punktach nieco zaniepokoiły niektóre osoby. Można by odnieść ze wszech miar błędne wrażenie, że to, co zrobiliśmy do tej pory, zostanie poddane jakiejś strasznie skomplikowanej modyfikacji w celu dostosowania stworzonych już i poprawnie działających przecież aplikacji do tych nowych warunków. Ktoś mógłby się spodziewać, że oto czeka nas żmudny proces poznawania wielu kolejnych funkcji Win32 API, struktur czy typów danych, nie mówiąc już o konieczności zapoznania się ze specyficznymi własnościami Delphi czy C++Buildera. Już w tym miejscu mogę obiecać, że choć nie unikniemy tego całkowicie, to jednak zrobię to w formie jak najbardziej przystępnej. Wszystkie zaprojektowane do tej pory aplikacje zachowają swój oryginalny kształt. Uwzględnimy możliwość ich pracy wielowątkowej, uwzględnimy tylko nieznacznie je wzbogacając. Pamiętając o wszystkim, czego dokonaliśmy do tej pory oraz mając na uwadze przedstawione nowe zadania, stojące przed naszymi programistycznymi produktami, nie pozostaje nam już nic innego, jak tylko uzupełnić stworzone już aplikacje o możliwość ich pracy wielowątkowej. Po przeczytaniu sporego fragmentu tej książki osoby preferujące Delphi mogły poczuć się nieco zawiedzione tym, że zawsze nowy temat rozpoczynałem do przykładów pisanych w C++Builderze. Aby im to wynagrodzić, tym razem zaczniemy od Object Pascala.
Delphi
Jak zapewne wiemy, istnieje w Delphi pewna klasa służąca implementacji mechanizmów, którymi charakteryzują się wątki. Jest nią TThread. Korzystając z jej właściwości oraz metod, z powodzeniem uwzględnić można bardzo wiele aspektów wielowątkowości. W celu utworzenia nowego wątku można posłużyć się niezwykle ciekawymi właściwościami parametrów konstruktora TThread.Create(), którego definicję przytoczę za Borland Delphi Visual Component Library:
constructor TThread.Create(CreateSuspended: Boolean);
var
Flags: DWORD;
begin
inherited Create;
AddThread;
FSuspended := CreateSuspended;
Flags := 0;
if CreateSuspended then Flags := CREATE_SUSPENDED;
FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), Flags, FThreadID);
end;
Przedstawiony konstruktor dokonuje wywołania funkcji BeginThread() zdefiniowanej w Borland Delphi Run-Time Library Win32 API Interface Unit:
function BeginThread(SecurityAttributes: Pointer; StackSize: LongWord; ThreadFunc: TThreadFunc; Parameter: Pointer; CreationFlags: LongWord; var ThreadId: LongWord): Integer;
var
P: PThreadRec;
begin
New(P);
P.Func := ThreadFunc;
P.Parameter := Parameter;
IsMultiThread := TRUE;
Result := CreateThread(SecurityAttributes, StackSize, @ThreadWrapper, P, CreationFlags, ThreadID);
end;
Jak zauważyliśmy, funkcja BeginThread() dokonuje z kolei wywoł-ania kolejnej o nazwie CreateThread(), tworząc tym samym nowy wątek. Należy jednak pamiętać, że samo utworzenie wątku wcale nie musi oznaczać jego automatycznego uruchomienia. Jeżeli w konstruktorze TThread.Create() jako wartość parametru CreateSuspended obierzemy FALSE (0), wątek zostanie natychmiast uruchomiony. W przeciwnym wypadku (TRUE lub 1) funkcja CreateThread() zostanie wywołana z parametrem CREATE_SUSPENDED, powodując, że działanie nowo utworzonego wątku będzie zawieszone do czasu wywołania metody TThread.Resume:
procedure TThread.Resume;
begin
if ResumeThread(FHandle) = 1 then FSuspended := FALSE;
end;
Powtórne zawieszenie działania wątku nastąpi oczywiście po wywołaniu:
procedure TThread.Suspend;
begin
FSuspended := True;
SuspendThread(FHandle);
end;
Sam proces realizacji wątku odbywa się w ramach metody Execute, wywoływanej w funkcji :
function ThreadProc(Thread: TThread): Integer;
var
FreeThread: Boolean;
begin
Thread.Execute;
FreeThread := Thread.FFreeOnTerminate;
Result := Thread.FReturnValue;
Thread.FFinished := True;
Thread.DoTerminate;
if FreeThread then Thread.Free;
EndThread(Result);
end;
Widzimy, że zakończenie wątku nastąpi dzięki wywołaniu procedury EndThread() z parametrem, którego wartość równa się właściwości FReturnValue (w omawianej klasie domyślnie przyjmowane jest 0), będącej zarazem kodem zakończenia danego wątku. Kod ten można odczytać, wykorzystując w tym celu funkcję GetExitCodeThread(). Dla porównania prześledźmy jeden z możliwych sposobów tworzenia nowego wątku przy wykorzystaniu niektórych funkcji Win32 API. Postępując zgodnie z ideą poprzedniego rozdziału, skoncentrujemy się na jednej z metod bezpośredniego odwołania do Win32 API, gdzie zdefiniowana jest funkcja CreateThread(), za pomocą której można utworzyć i uruchomić nowy wątek w obrębie przestrzeni adresowej odpowiedniego procesu. Funkcja ta zwraca identyfikator nowo utworzonego wątku.
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
W Object Pascalu skorzystamy z analogicznej definicji:
function CreateThread(SecurityAttributes: Pointer; StackSize: LongWord; ThreadFunc: TThreadFunc; Parameter: Pointer; CreationFlags: LongWord; var ThreadId: LongWord): Integer; stdcall;
lpThreadAttributes jest wskaźnikiem do struktury SECURITY_ATTRIBUTES, określającej pewne atrybuty zabezpieczeń nowego wątku. Mam nadzieję, że nie będzie nikogo razić, jeżeli dalej przedstawię definicje właściwe zarówno Win32 API jak i Borland Delphi Run-Time Library Win32 API Interface Unit
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
lub
PSecurityAttributes = ^TSecurityAttributes;
{$EXTERNALSYM _SECURITY_ATTRIBUTES} _SECURITY_ATTRIBUTES = record nLength: DWORD;
lpSecurityDescriptor: Pointer;
bInheritHandle: BOOL;
end;
TSecurityAttributes = _SECURITY_ATTRIBUTES;
{$EXTERNALSYM SECURITY_ATTRIBUTES}
SECURITY_ATTRIBUTES = _SECURITY_ATTRIBUTES;
W obu definicjach nLength jest rozmiarem struktury (rekordu). Przed przekazaniem tego rekordu (struktury) jako parametru w przypadku ogólnym należy wpisać do nLength wartość równą sizeof(SECURITY_ATTRIBUTES). lpSecurityDescriptor jest wskaźnikiem do deskryptora zabezpieczeń wątku jako obiektu. Jeżeli ustalimy NULL (w Pascalu NIL), obiektowi zostanie przydzielona wartość domyślna rodzaju zabezpieczeń w trakcie danego procesu, zaś identyfikator wątku nie będzie dziedziczony. Z kolei bInheritHandle specyfikuje, czy zwracany identyfikator utworzonego wątku jest dziedziczony przez nowy proces. Ustalenie jej jako TRUE zapewni, że ten identyfikator będzie mógł dziedziczyć każdy nowy proces. dwStackSize jest rozmiarem obszaru pamięci (w bajtach), zwanej stosem, z której korzysta dany proces. Jeżeli przyjmiemy tu wartość 0, rozmiar stosu dla nowego wątku będzie taki sam jak dla wątku głównego. Stos jest automatycznie alokowany w pamięci procesu i automatycznie zwalniany po wstrzymaniu działania wątku. Jeżeli deklarowany rozmiar stosu przewyższa ilość dostępnej pamięci, nie zostanie przydzielony identyfikator do nowego wątku. lpStartAddress stanowi wskaźnik do części aplikacji (lub funkcji) wykonywanej w danym wątku podając jednocześnie jej adres startowy. Funkcja może zawierać pojedynczy 32-bitowy argument, zwracając jednoczenie 32-bitową wartość. lpParameter specyfikuje pojedynczą 32-bitową wartość parametru przekazywanego wątkowi. dwCreationFlags podaje odpowiedni znacznik kontroli sposobu utworzenia nowego wątku. Jeżeli wyspecyfikujemy dobrze nam znany parametr CREATE_SUSPENDED, działanie wątku będzie zawieszone do czasu wywołania funkcji ResumeThread(). Jeżeli zostanie tu przypisana wartość 0, nowy wątek zostanie natychmiast uruchomiony. lpThreadId jest wskaźnikiem do 32-bitowej zmiennej identyfikującej wątek. Nie należy jednak w żadnym wypadku mylić jej z identyfikatorem nowego wątku. Opisana funkcja tworzy i przypisuje identyfikator do nowo powstałego wątku. Jeżeli wskaźnik do struktury zabezpieczeń obiektu nie jest używany, identyfikator ten może być wykorzystany przez dowolną funkcję, której wywołanie wymaga użycia unikalnego identyfikatora wątku jako obiektu Win32 API. Uruchomienie wątku zaczyna się od wywołania funkcji specyfikowanej przez lpStartAddress. Aby zatrzymać działający wątek, należy odwołać się do funkcji Win32 API:
VOID ExitThread(DWORD dwExitCode);
lub zdefiniowanej w Borland Delphi Run-Time Library procedury:
procedure ExitThread(ExitCode: Integer); stdcall;
gdzie dwExitCode (ExitCode) specyfikuje kod zakończenia danego wątku. Użycie tej funkcji (procedury) jest preferowaną metodą opuszczania działającego wątku. Gdy jest ona wywoływana (obojętnie czy w sposób jawny, czy też w inny), obszar aktualnego stosu zostanie zwolniony, zawieszając tym samym działanie wątku. W celu otrzymania kodu ostatniego wątku należy posłużyć się funkcją:
BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);
która w Borland Delphi Run-Time Library definiowana jest jako:
function GetExitCodeThread(hThread: THANDLE; var lpExitCode: DWORD): BOOL; stdcall;
hThread jest identyfikatorem wątku, (w Win NT musi być przydzielony z rodzajem dostępu THREAD_QUERY_INFORMATION), zaś lpExitCode jest wskaźnikiem do 32-bitowej zmiennej, reprezentującej kod zakończenia wątku. Technika wykorzystania klasy TThread oraz sposoby posługiwania się funkcją CreateThread(), są - jak być może zauważyliśmy - nieco skomplikowane i przyznam w tym miejscu, że mało przydatne do naszych celów. Przecież przyjęliśmy zasadę, że nie będziemy wiele zmieniać w konstrukcji dotychczasowych algorytmów. Istnieje dużo prostszy sposób implementacji wątków w programach sterujących transmisją szeregową. Powróćmy do prezentowanej już funkcji BeginThread(). Funkcję tę można z powodzeniem wykorzystać w aplikacjach pisanych zarówno w Delphi jak i C++Builderze. Umiejętne jej użycie zapewni nam uruchomienie osobnego wątku, bez potrzeby jawnego i bezpośredniego odwoływania się do funkcji Win32 API CreateThread() (co wcale nie oznacza, że znajomość jej jest bezużyteczna). Wielką zaletą posługiwania się BeginThread() jest fakt, że możemy w niej odwołać się do normalnej funkcji Pascala lub C++, która już dzięki temu będzie mogła być potraktowana jako osobny wątek. Rolę takiej funkcji z powodzeniem może pełnić typ:
TThreadFunc = function(Parameter: Pointer): Integer; TThreadFunc definiuje pewien typ funkcji, która już w momencie użycia traktowana jest jako adres startowy nowego wątku (obiektu Win32). Może być on przekazywany bezpośrednio do BeginThread() lub do funkcji Win32 API CreateThread(). 32-bitowy wskaźnik Parameter jest przekazywany bezpośrednio do BeginThread(). Dodam na marginesie, że dla naszych specyficznych celów stosowanie tego parametru nie jest wymogiem koniecznym wymogiem. Zobaczmy, jak praktycznie w bardzo prosty sposób można wprowadzić pewne elementy wielowątkowości do naszych aplikacji. Przede wszystkim należy skonstruować własną odmianę typu TThreadFunc. Nic prostszego - wystarczy odpowiednio wykorzystać na przykład procedurę obsługi zdarzenia SendFileClick() opisaną w module rs_17.pas (patrz wydruk 5.15). Zawartość nowej funkcji, nazwijmy ją RS_SendFile(), wypełnimy po prostu tamtym kodem w sposób, który zaprezentowałem poniżej. function RS_SendFile(P: Pointer): Integer;
var
i : Integer;
FileSizeHigh : DWORD;
begin
for i := 0 to cbOutQueue do Buffer_O[i] := char(0); // czyści bufor wyjściowy
Form1.ProgressBar1.Max:=0;
if (hCommDev > 0) then
begin
if((_lopen(PChar(Form1.OpenDialog1.FileName), OF_READ)) <> HFILE_ERROR) then
begin
hfile_s := _lopen(PChar(Form1.OpenDialog1.FileName), OF_READ);
Form1.ProgressBar1.Max:=GetFileSize(hfile_s, @FileSizeHigh);
while (_lread(hfile_s, @Buffer_O, 1) > 0) do
begin
Form1.Write_Comm(hCommDev, Buffer_O, 1);
Form1.ProgressBar1.StepIt();
end;
_lclose(hfile_s);
FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Nie wybrano pliku do'+ ' transmisji ', 'Uwaga !' ,MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
Result := 0;
end;
Nie pozostaje nam już teraz nic innego, jak tylko zaprojektować procedurę obsługi nowego zdarzenia SendFileClick(), w którym uruchomimy nasz wątek :
procedure TForm1.SendFileClick(Sender: TObject);
begin
hThread_SF := BeginThread (NIL, 0, @RS_SendFile, NIL, 0, ThreadID_SF);
end;
Program testowałem, nawiązując połączenie z innym komputerem, na którym uruchomiony był Terminal. Zastosowanie oddzielnego, drugorzędnego wątku dla części algorytmu realizującego wysyłanie plików zapewnia, że bez problemu można w trakcie transmisji danych równolegle przygotowywać w drugim oknie edycji jakiś własny komunikat do wysłania (ktoś może tak przeprojektować algorytm, by mieć możliwość wysłania w ten sposób pliku). Jeżeli wiadomość taką zechcemy nadać w trakcie transmitowanego już pliku, będzie miała rzecz jasna większy priorytet, ale tylko z tego powodu, że jest realizowana w wątku głównym. Tego typu właściwością powinny charakteryzować się wszystkie programy obsługujące urządzenia zewnętrzne. Możemy na przykład stanąć kiedyś przed koniecznością bardzo szybkiego wysłania komendy OFF, choćby i w trakcie pomiaru. Zwróćmy uwagę, że nawet w tej sytuacji struktura danych wysyłanych z pliku zostanie zachwiana tylko w ten sposób, że ostatnio wysyłany komunikat po prostu je uzupełni. Równie ciekawą cechą omawianego programu jest możliwość odebrania danych wysyłanych z innego komputera w czasie, gdy właśnie transmitujemy np. plik. Właściwość taką, poza wykonywaniem nadawania w oddzielnym wątku, może zapewnić umiejętne (jak zwykle to robimy) zastosowanie oddzielnych buforów dla danych transmitowanych i odbieranych. Testując przedstawioną aplikację, zauważymy też, że można ją swobodnie przesuwać po ekranie oraz zmieniać jej rozmiary zarówno w czasie transmisji jak i odbioru danych. Nie mamy również problemu z zakończeniem działania programu w momencie, który uznamy za stosowny. Jest to bardzo wygodna cecha tego typu aplikacji.
Kod modułu RS_21.pas aplikacji wykorzystującej elementy wielowątkowości
unit RS_21;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, FileCtrl, ToolWin, Buttons, Menus;
type
TForm1 = class(TForm)
CloseComm: TButton;
OpenComm: TButton;
SendFile: TButton;
Receive: TButton;
ProgressBar1: TProgressBar;
OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
CoolBar1: TCoolBar;
CopyText: TSpeedButton;
PasteText: TSpeedButton;
CutText: TSpeedButton;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
CheckBox3: TCheckBox;
CheckBox4: TCheckBox;
MainMenu1: TMainMenu;
RichEdit2: TRichEdit;
Label1: TLabel;
Label2: TLabel;
RichEdit1: TRichEdit;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SendFileClick(Sender: TObject);
procedure ReceiveClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CopyTextClick(Sender: TObject);
procedure PasteTextClick(Sender: TObject);
procedure CutTextClick(Sender: TObject);
procedure FileOpenClick(Sender: TObject);
procedure NewClick(Sender: TObject);
procedure SaveAs1Click(Sender: TObject);
procedure SendWrittenClick(Sender: TObject);
private
{ Private declarations }
sFile: string;
procedure FormCaption(const sFile_s: String);
procedure ShowFileOpen(const sFile_O: String);
function Write_Comm(hCommDev: THANDLE; lpBuffer: PChar; nNumberOfBytesToWrite: DWORD): Integer;
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}

const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
dcb_fOutxCtsFlow = $0004;
dcb_fOutxDsrFlow = $0008;

// -- fDtrControl --
DTR_CONTROL_ENABLE = $0010;
DTR_CONTROL_HANDSHAKE = $0020;

dcb_fDsrSensitivity = $0040;
dcb_fTXContinueOnXoff = $0080;
dcb_fOutX = $0100;
dcb_fInX = $0200;
dcb_fErrorChar = $0400;
dcb_fNull = $0800;

// -- fRtsControl --
RTS_CONTROL_ENABLE = $1000;
RTS_CONTROL_HANDSHAKE = $2000;
RTS_CONTROL_TOGGLE = $3000;

dcb_fAbortOnError = $4000;

cbInQueue = 1024;
cbOutQueue = 1024;
var
hThread_SF : THANDLE; // pseudoidentyfikator wątku
ThreadID_SF : Cardinal; // zmienna identyfikująca wątek

hfile_s : HFILE; // identyfikator pliku źródłowego
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : LPCSTR;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckSave: Integer;
begin
iCheckSave := MessageDlg(Format('Zamknięcie aplikacji ? %s ', [sFile]), mtConfirmation, mbYesNoCancel, 0);
case iCheckSave of
idYes:
begin
SuspendThread(ThreadID_SF);
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: {};
idCancel: Abort;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
OpenDialog1.InitialDir := ExtractFilePath(ParamStr(0));
SaveDialog1.InitialDir := OpenDialog1.InitialDir;
ProgressBar1.Step := 1;
RichEdit1.ScrollBars := ssBoth;
end;
//--------------------------------------------------------------------
procedure TForm1.CopyTextClick(Sender: TObject);
begin
RichEdit1.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.PasteTextClick(Sender: TObject);
begin
RichEdit1.PasteFromClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.CutTextClick(Sender: TObject);
begin
RichEdit1.CutToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.FormCaption(const sFile_s: String);
begin
sFile := sFile_s;
Caption := Format('%s - %s', [ExtractFileName(sFile_s), Application.Title]);
end;
//--------------------------------------------------------------------
procedure TForm1.FileOpenClick(Sender: TObject);
begin
if OpenDialog1.Execute then
begin
ShowFileOpen(OpenDialog1.FileName);
RichEdit1.ReadOnly := ofReadOnly in OpenDialog1.Options;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.NewClick(Sender: TObject);
begin
FormCaption('Bez nazwy');
RichEdit1.Lines.Clear;
end;
//--------------------------------------------------------------------
procedure TForm1.SaveAs1Click(Sender: TObject);
begin
if SaveDialog1.Execute then
begin
if FileExists(SaveDialog1.FileName) then
if MessageDlg(Format('Plik zapisany ponownie %s', [SaveDialog1.FileName]), mtConfirmation, mbYesNoCancel, 0) <> idYes then Exit;
RichEdit1.Lines.SaveToFile(SaveDialog1.FileName);
FormCaption(SaveDialog1.FileName);
RichEdit1.Modified := FALSE;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.ShowFileOpen(const sFile_O: string);
begin
RichEdit1.Lines.LoadFromFile(sFile_O);
FormCaption(sFile_O);
RichEdit1.SetFocus;
RichEdit1.Modified := FALSE;
end;
//--------------------------------------------------------------------
function TForm1.Write_Comm(hCommDev: THANDLE; lpBuffer: PChar;
nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;

begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite,
NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function TForm1.Read_Comm(hCommDev: THANDLE;
Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM1';
if (CheckBox2.Checked = TRUE) then
lpFileName:='COM2';


hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);

if (CheckBox3.Checked = TRUE) then
dcb.BaudRate:=CBR_1200;
if (CheckBox4.Checked = TRUE) then
dcb.BaudRate:=CBR_19200;

//-przykładowe ustawienia flag sterujących DCB-
dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;
dcb.StopBits := TWOSTOPBITS;
dcb.ByteSize := 8;

SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID: MessageDlg('Niewłaściwa nazwa portu '+lpFileName+ ' lub jest on aktywny ', mtError, [mbOk], 0);
end;
end;
//--------------------------------------------------------------------
function RS_SendFile(P: Pointer): Integer;
var
i : Integer;
FileSizeHigh : DWORD;

begin
for i := 0 to cbOutQueue do
Buffer_O[i] := char(0); // czyści bufor wyjściowy
Form1.ProgressBar1.Max:=0;

if (hCommDev > 0) then
begin
if((_lopen(PChar(Form1.OpenDialog1.FileName), OF_READ)) <> HFILE_ERROR) then
begin
hfile_s := _lopen(PChar(Form1.OpenDialog1.FileName), OF_READ);
Form1.ProgressBar1.Max:=GetFileSize(hfile_s, @FileSizeHigh);
while (_lread(hfile_s, @Buffer_O, 1) > 0) do
begin
Form1.Write_Comm(hCommDev, Buffer_O, 1);
Form1.ProgressBar1.StepIt();
end;
_lclose(hfile_s);

FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Nie wybrano pliku do'+ ' transmisji ', 'Uwaga !' ,MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
Result := 0;
end;
//---------wysyłanie pliku w oddzielnym wątku ------------------------
procedure TForm1.SendFileClick(Sender: TObject);
begin
hThread_SF := BeginThread (NIL, 0, @RS_SendFile, NIL, 0, ThreadID_SF);
end;
//---------------odbiór danych ---------------------------------------
procedure TForm1.ReceiveClick(Sender: TObject);
begin
// Form1.ProgressBar1.Max := 0;
if (Form1.Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.RichEdit2.Text := Buffer_I;
end
else
begin
Form1.RichEdit2.Text := 'Brak danych do odebrania';
Beep();
end;
end;
//--------transmisja danych ------------------------------------------
procedure TForm1.SendWrittenClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, PChar(Form1.RichEdit2.Text));
Form1.Write_Comm(hCommDev, Buffer_O, StrLen(Buffer_O));
FlushFileBuffers(hCommDev);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
end.
Funkcji BeginThread() tworzącej wątek, w którym transmitowane są dane z pliku, został przydzielony identyfikator, zadeklarowany następująco:
var
hThread_SF : THANDLE;
Tak naprawdę, hThread_SF występuje w roli tak zwanego pseudoidentyfikatora, mimo że jest (ale nie musi być) typu THANDLE odnośnego wątku. Tego typu pseudoidentyfikator jest zazwyczaj odnośnikiem do normalnego identyfikatora, nie stanowiąc tym samym odrębnego obiektu. Użycie funkcji CloseHandle() nie wywoła żadnych skutków. Zakończenie tego typu wątków drugorzędnych nastąpi po zamknięciu wątku głównego i nie musimy się tym zbytnio przejmować, tym bardziej, że samodzielnie nie rezerwowaliśmy dla procesu związanego z tym wątkiem żadnych specjalnych obszarów pamięci. Na nasze potrzeby nie było to konieczne. Przedstawionej aplikacji brakuje jeszcze opcji umożliwiającej wstrzymanie (zawieszenie) i przywrócenie w dowolnym momencie procesu transmisji danych. Owych cech w formularzu p_RS_21.dpr nie uwzględniłem tylko z tego powodu, by niepotrzebnie nie mnożyć przycisków lub innych opcji menu. Uwzględnienie ich wymaga użycia dwóch bardzo prostych funkcji Win32 API - pierwszej:
DWORD SuspendThread(HANDLE hThread);
za pomocą której możemy czasowo wstrzymać proces wykonywania wątku oraz następnej, wznawiającej jego wykonywanie:
DWORD ResumeThread(HANDLE hThread);
Wykorzystanie ich w naszym programie w najprostszym wypadku wymagałoby zbudowania dwóch nowych zdarzeń:
procedure TForm1.SuspendClick(Sender: TObject);
begin
SuspendThread(hThread_SF);
end;
procedure TForm1.ResumeClick(Sender: TObject);
begin
ResumeThread(hThread_SF);
end;
Myślę, że nie nikt nie będzie miał trudności z samodzielnym wkomponowaniem tych dwóch nowych zdarzeń do swojej aplikacji.
Konkurencja dla Timera
Pamiętamy wszystko, co powiedzieliśmy na temat wad i zalet obiektu TTimer. Aplikacje posługujące się nim również nie działały w sposób szczególnie elegancki. Regulując częstość odczytu danych z portu szeregowego czy to za pomocą komponentu TCSpinEdit, czy nawet za pomocą TTrackBar, nie byliśmy w stanie w sposób tak naprawdę płynny dostroić się do miernika. Wszystkie te operacje przebiegały w sposób "prawie" swobodny, zaś w czasie ich wykonywania pomiar był, niestety, wstrzymywany. Zobaczmy zatem, czym można zastąpić wymieniony komponent. Formularz ten oraz jego kod zmodyfikowałem nieznacznie w ten sposób, by przedstawiał aplikację współpracującą z bardzo dokładną wagą cyfrową, wykorzystując przy tym zalety funkcji BeginThread(). Zachowałem tu wszystkie zastosowane wcześniej właściwości edytora IDE. Wydruk pokazuje kompletny algorytm omawianego programu.
Kod modułu RS_22.pas aplikacji wykorzystującej elementy wielowątkowości przy obsłudze wagi cyfrowej
unit RS_22;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons;
type
TForm1 = class(TForm)
Start: TButton;
Suspend: TButton;
Resume: TButton;
CloseComm: TButton;
OpenComm: TButton;
Memo2: TMemo;
Edit1: TEdit;
TrackBar1: TTrackBar;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Panel1: TPanel;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
Label4: TLabel;
Label5: TLabel;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
Memo1: TMemo;
procedure StartClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure SuspendClick(Sender: TObject);
procedure ResumeClick(Sender: TObject);
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;

cbInQueue = 32; // rozmiary buforów danych
cbOutQueue = 32;
var
query : PChar = 'SI'+#13+#10; // rozkaz wysłania mierzonej wartości
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;

intVar : LongWord; // licznik pomiarów
intVarSleep : Cardinal; // licznik późnienia
bResult : BOOL; // "niema" zmienna logiczna
hThread_SR : THANDLE; // pseudoidentyfikator wątku
ThreadID_SR: Cardinal;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckProcess: Integer;
begin
iCheckProcess := MessageDlg('Zakończenie pomiaru i zamknięcie'+ ' aplikacji?', mtConfirmation, [mbYes, mbNo], 0);
case iCheckProcess of
idYes:
begin
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: Exit;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM1';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate:=CBR_4800;

//-przykładowe ustawienia znaczników sterujących DCB-
dcb.Flags := dcb_fParity;
dcb.Parity := NOPARITY;
dcb.StopBits :=ONESTOPBIT;
dcb.ByteSize :=8;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
end
else
case hCommDev of
IE_BADID:
begin
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny', 'Uwaga !',MB_OK);
lpFileName:='';
end;
end;
end;
//--------------------------------------------------------------------
function Write_Comm(hCommDev: THANDLE; nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;
begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
function RS_Send_Receive(P: Pointer): Integer;
begin
REPEAT
repeat // transmisja komunikatu
FlushFileBuffers(hCommDev);
until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);

//-------odczyt danych z portu--------
if ( Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
// Beep();
end
else
begin
Form1.Memo2.Lines.Add('x0'); // błędny odczyt
Beep();
Form1.Memo2.Lines.Add('');
end;
UNTIL( bResult = FALSE);
Result:=0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
OpenComm.Enabled := FALSE;
StrCopy(Buffer_O, query);
hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0, ThreadID_SR);
Label1.Caption := 'Pomiar';
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
TrackBar1.Position := 1000;
TrackBar1.Max := 2000;
TrackBar1.Min := 1;
TrackBar1.Frequency := 100;
OpenComm.Enabled := TRUE;
intVar := 0;
intVarSleep := 1000;
bResult := TRUE;
end;
//----------wstrzymanie pomiaru --------------------------------------
procedure TForm1.SuspendClick(Sender: TObject);
begin
SuspendThread(hThread_SR);
Label1.Caption := 'Wstrzymanie';
end;
//----------wznowienie pomiaru ---------------------------------------
procedure TForm1.ResumeClick(Sender: TObject);
begin
ResumeThread(hThread_SR);
Label1.Caption := 'Pomiar';
end;
//-----kopiowanie okna edycji Memo2 do schowka------------------------
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
Form1.Memo2.SelectAll;
Form1.Memo2.CopyToClipboard;
end;
//-----kopiowanie okna edycji Memo1 do schowka------------------------
procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
Form1.Memo1.SelectAll;
Form1.Memo1.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
intVarSleep := TrackBar1.Position; // sterowanie późnieniem
Edit1.Text := IntToStr(TrackBar1.Position);
end;
//-------------------------------------------------------------------
end.
Śledząc powyższe zapisy, bez trudu zauważymy, że części programu wysyłające i odbierające komunikaty zostały połączone w jednej funkcji RS_Send_Receive(). Zastosowałem w niej główną instrukcję powtarzającą REPEAT...UNTIL, która, jak widzimy, nigdy nie może ulec zakończeniu (wartość bResult jest zawsze ustalona jako TRUE - patrz procedura FormCreate()). Jedynym sposobem wstrzymania pomiaru jest albo zamknięcie aplikacji, albo użycie przycisku Wstrzymaj, wywołującego procedurę obsługi zdarzenia SuspendClick(). Proces zbierania danych może zostać przywrócony w dowolnym momencie poprzez uruchomienie procedury obsługi zdarzenia ResumeClick(). Funkcja RS_Send_Receive() jest najważniejszym argumentem dla BeginThread()wywoływanej w treści procedury obsługi zdarzenia StartClick(), które uruchamiamy przyciskiem Rozpocznij pomiar. Testując aplikację, przekonamy się, że proces wyboru odpowiedniego przedziału czasu pomiędzy dwoma kolejnymi odczytami wcale nie zawiesza transmisji. Argumentem funkcji Sleep() sterującej opóźnieniem, z jakim dokonywane są kolejne odczyty danych, jest zmienna intVarSleep, która przybiera aktualną wartość cechy Position komponentu TrackBar1. Należy oczywiście zauważyć, że konstrukcja funkcji RS_Send_Receive() również może również opierać się na wykorzystaniu prostszej w użyciu i trochę szybszej w działaniu instrukcji powtarzającej while...do, tak jak przedstawia to poniższy przykład:
function RS_Send_Receive(P: Pointer): Integer;
begin
while ((bResult = TRUE)) do
BEGIN
// --- wysyłanie zapytania ---
while(Write_Comm(hCommDev, StrLen(Buffer_O)) = 0) do FlushFileBuffers(hCommDev);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);
//-------odczyt danych z portu--------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
// Beep();
end
else
begin
Form1.Memo2.Lines.Add('x0');
Beep();
Form1.Memo2.Lines.Add('');
end;
END; // koniec zewnętrznego while
Result:=0;
end;
W "typowych" programach instrukcje powtarzające while...do oraz repeat...until działają z reguły nieco odmiennie. Pierwsza z nich powoduje cykliczne wykonywanie instrukcji warunkowej do czasu spełnienia określonego warunku, który jest sprawdzany przed wykonaniem danej instrukcji. Możliwa jest zatem sytuacja, gdy instrukcja ta nie zostanie wykonana ani razu. W naszym przypadku oznaczałoby to, że nie wysłaliśmy żadnego zapytania do urządzenia, czyli na pewno nie otrzymamy odpowiedzi. W tego typu programach nie może być dwuznaczności. Używając z kolei repeat...until będziemy pewni, że dany warunek będzie sprawdzany po wykonaniu bloku instrukcji, czyli dana instrukcja musi być wykonana przynajmniej raz. Jest to ważne, jeżeli chcemy być pewni wykonania np. funkcji FlushFileBuffers(). Testując przedstawiony algorytm, możemy wysnuć jeszcze jeden bardzo ważny wniosek, mianowicie: pseudoidentyfikator, np. hThread_SR, pełni odmienną rolę niż zmienna identyfikująca dany wątek, np. ThreadID_SR. Podstawowa różnica pomiędzy nimi jest taka, że instrukcja:
SuspendThread(ThreadID_SR);
po prostu nie będzie działać. Argumentami funkcji manipulujących wątkami mogą być tylko ich pseudoidentyfikatory. Jeszcze wyraźniej zauważymy to na przykładzie programu napisanego w C++Builderze. Analizując zaprezentowane przykłady, śmiało stwierdzimy, że Timera z powodzeniem może zastąpić jakaś instrukcja powtarzająca, która jednak nigdy nie może być zakończona i nie może być tam żadnej zmiennej sterującej. Jeżeli algorytm zostanie jeszcze uzupełniony o elementy wielowątkowości, całość będzie działać naprawdę dobrze, a my przestaniemy martwić się o prawidłowość i cykliczność taktowania obiektu typu TTimer.
C++ Builder
Zastosowanie opisanych wcześniej sposobów wykorzystywania elementów wielowątkowości w programach obsługujących łącze szeregowe RS 232C, tworzonych w C++Builderze nie powinno sprawić nam większych kłopotów. Jedynym, z którym może zetknąć się mniej doświadczony programista, jest nieco odmienny sposób deklarowania BeginThread. Builder określa go za pomocą instrukcji extern jak typ int:
extern PACKAGE int __fastcall BeginThread(void * SecurityAttributes, unsigned StackSize, TThreadFunc ThreadFunc, void * Parameter, unsigned CreationFlags, unsigned &ThreadId);
Ponadto TThreadFunc już w swojej definicji jest zapisany z operatorem wyłuskiwania:
typedef Integer __fastcall (*TThreadFunc)(Pointer Parameter);
co sugeruje, że w najprostszym przypadku przy wywołaniu konkretnej funkcji reprezentującej ten typ jako parametr BeginThread(), nie będzie potrzeby posługiwania się operatorem adresowym, tak jak schematycznie prezentują to poniższe zapisy.
int hThread_SW; // pseudoidentyfikator wątku
unsigned uThreadID_SW; // zmienna identyfikująca wątek
...
int __fastcall RS_SendWritten(Pointer Parameter)
{
...
return TRUE;
}
...
void __fastcall TForm1::SendWrittenClick(TObject *Sender)
{
...
hThread_SW = BeginThread (NULL, 0, RS_SendWritten, NULL, 0, uThreadID_SW);
...
}
Zarówno w przykładzie pokazanym w Delphi jak i tym w Builderze wskaźnik będący parametrem funkcji RS_SendWritten() należy do typu Pointer. Nie wskazuje on na żadną konkretną zmienną, służy natomiast do przekazywania adresów innych zmiennych. Rozpatrzmy to na przykładzie projektu \KODY\BUILDER\RS_08\p_RS_08.bpr, który jest niczym innym, jak uzupełnioną o elementy wielowątkowości wersją p_RS_06.bpr. Cała modyfikacja polega na wydzieleniu trzech wątków drugorzędnych określonych pseudoidentyfikatorami hThread_SF (dla części wysyłającej pliki - Send File), hThread_SW (dla części transmitującej dane wpisywane z klawiatury lub plik zmodyfikowane tuż przed wysłaniem - Send Written) oraz hThread_Rec (część odbierająca dane z łącza szeregowego - Receive). Konieczność utworzenia trzech wątków wynika z faktu, iż aplikacja nasza posługuje się dwoma komponentami edycyjnym, które w dużym uproszczeniu można traktować jako swego rodzaju okna dialogowe stworzone za pomocą biblioteki VCL (ang. Visual Component Library). Być może w Object Pascalu nie było to tak oczywiste, jednak Win32 API wymaga dla każdego wątku osobnego okna edycji. Ktoś może mieć wątpliwości: trzy wątki i dwa okna... jednak po chwili namysłu dojdziemy do wniosku, że wszystko się zgadza. Przecież jeden z komponentów typu TRichEdit (w tym wypadku RichEdit2) pełni podwójną rolę: wyświetlamy tam zawartość pliku i możemy wpisywać własne komunikaty przeznaczone do transmisji
Kod modułu RS_08.cpp aplikacji wykorzystującej elementy wielowątkowości przy wysyłaniu plików
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//--------RS_08.cpp-----------------------
#include
#pragma hdrstop
#include "RS_08.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 1024 //rozmiar bufora danych wyjściowych
#define cbInQueue 1024 //rozmiar bufora danych wejściowych
TForm1 *Form1;
AnsiString New_File; // przechowuje nazwę pliku
HFILE hfile_s; // identyfikator pliku
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_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;
COMSTAT Stat;
DWORD Errors;
BOOL bResult; // zmienna boolowska
int hThread_SF, hThread_SW, hThread_Rec; // pseudoidentyfikatory // wątków
unsigned uThreadID_SF, uThreadID_SW, uThreadID_Rec;
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev, DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0], nNumberOfBytesToWrite, &NumberOfBytesWritten, NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);
return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev, 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, &Buffer_I[0], nNumberOfBytesToRead, lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CheckFileSave(void)
{
if (RichEdit1->Modified)
{
switch(MessageBox(NULL, "Zawartość pliku lub okna została" " zmieniona. Zapisać zmiany?", "Uwaga!", MB_YESNOCANCEL | MB_ICONQUESTION))
{ case ID_YES : FileSaveClick(this);
case ID_CANCEL : Abort();
};
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenDialog1->InitialDir = ExtractFilePath(ParamStr(0));
OpenDialog1->Filter = "*.dat , *.txt, *.cpp, *.c | *.dat; *.txt; *.cpp; *.c";
SaveDialog1->InitialDir = OpenDialog1->InitialDir;
SaveDialog1->Filter = "*.*|*.*";

FileOpen->Hint = "Otwórz plik";
FileOpen->ShowHint = TRUE;
FileSave->Hint = "Zapisz";
FileSave->ShowHint = TRUE;
CopyText->Hint = "Kopiuj";
CopyText->ShowHint = TRUE;
PasteText->Hint = "Wklej";
PasteText->ShowHint = TRUE;
CutText->Hint = "Wytnij";
CutText->ShowHint = TRUE;
CleanBuffers->Hint = "Wyczyść bufory";
CleanBuffers->ShowHint = TRUE;
ReceiveFileSave->Hint = "Zapisz otrzymane";
ReceiveFileSave->ShowHint = TRUE;
RichEdit1->ScrollBars = ssBoth;
}
//--------------------------------------------------------------------
void __fastcall TForm1::FileOpenClick(TObject *Sender)
{
if (OpenDialog1->Execute())
{
RichEdit1->Lines->LoadFromFile(OpenDialog1->FileName);
RichEdit1->Modified = FALSE;
RichEdit1->ReadOnly =
OpenDialog1->Options.Contains(ofReadOnly);
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{
switch(MessageBox(NULL, " Działanie aplikacji zostanie" " zakończone.", "Uwaga!", MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES :
{
if (RichEdit1->Modified)
CheckFileSave();
Close_Comm(hCommDev);
Application->Terminate();
}
case ID_CANCEL : Abort();
};
}
//--------------------------------------------------------------------
void __fastcall TForm1::FileSaveClick(TObject *Sender)
{
if ( ! strcmp(New_File.c_str(),LoadStr(256).c_str()) )
SaveAs1Click(Sender);
else
{
RichEdit1->Lines->SaveToFile(New_File);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::SaveAs1Click(TObject *Sender)
{
if (SaveDialog1->Execute()) // dane będą zapisywane w
// formacie Rich !
{
RichEdit1->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit1->Modified = FALSE;
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveFileSaveClick(TObject *Sender)
{
if (SaveDialog1->Execute())
{
RichEdit2->Lines->SaveToFile(SaveDialog1->FileName);
RichEdit2->Modified = FALSE;
}
}
//---------------------------------------------------------------------
void __fastcall TForm1::NewClick(TObject *Sender)
{
RichEdit1->Lines->Clear();
RichEdit1->Modified = FALSE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::CutTextClick(TObject *Sender)
{
RichEdit1->CutToClipboard();
}
//-------------------------------------------------------------------
void __fastcall TForm1::CopyTextClick(TObject *Sender)
{
RichEdit1->CopyToClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::PasteTextClick(TObject *Sender)
{
RichEdit1->PasteFromClipboard();
}
//--------------------------------------------------------------------
void __fastcall TForm1::SelectAllClick(TObject *Sender)
{
RichEdit1->SelectAll();
}
//--------------------------------------------------------------------
void __fastcall TForm1::UndoClick(TObject *Sender)
{
if (RichEdit1->HandleAllocated())
SendMessage(RichEdit1->Handle, EM_UNDO, 0, 0);
}
//--------------------------------------------------------------------
void __fastcall TForm1::CleanBuffersClick(TObject *Sender)
{
for (int i = 0; i <= cbInQueue - 1; i++)
{
Buffer_I[i] = NULL;
RichEdit1->Text = Buffer_I;
}
for (int i = 0; i <= cbOutQueue - 1; i ++)
{
Buffer_O[i] = NULL;
RichEdit2->Text = Buffer_O;
}
//memset(Buffer_O, 0, cbOutQueue);
//memset(Buffer_I, 0, cbInQueue);
ProgressBar1->Max = 0;
}
//--------------------------------------------------------------------
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);
GetCommState(hCommDev, &dcb);

if (CheckBox3->Checked == TRUE) // wybór prędkości
dcb.BaudRate = CBR_1200;
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_19200;
dcb.Parity = NOPARITY; // ustawienie parzystości
dcb.StopBits = TWOSTOPBITS; // bity stopu
dcb.ByteSize = 8; // bity danych

//-przykładowe ustawienia znaczników sterujących DCB-
dcb.fParity = TRUE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fAbortOnError = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fErrorChar = FALSE;
dcb.fNull = FALSE;
dcb.EofChar = FALSE;

SetCommState(hCommDev, &dcb);

GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);

}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------------------------------------------------------------------
int __fastcall RS_SendFile(Pointer Parameter)
{
DWORD FileSizeHigh;

Form1->ProgressBar1->Max = 0;

if ((_lopen(Form1->OpenDialog1->FileName.c_str(),OF_READ))!= HFILE_ERROR)
{
hfile_s =_lopen(Form1->OpenDialog1->FileName.c_str(), OF_READ);
Form1->ProgressBar1->Max = GetFileSize((HANDLE)hfile_s, &FileSizeHigh);
while (_lread(hfile_s, &Buffer_O[0], 1))
{
Write_Comm(hCommDev, 1); // transmisja 1 bajta
Form1->ProgressBar1->StepIt();
}
_lclose(hfile_s);

FlushFileBuffers(hCommDev);
}
else
MessageBox(NULL, "Nie wybrano pliku do transmisji.", "Błąd !", MB_OK);
return TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendFileClick(TObject *Sender)
{
if (hCommDev > 0)
{
hThread_SF = BeginThread (NULL, 0, RS_SendFile, NULL, 0, uThreadID_SF);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//--------------------------------------------------------------------
int __fastcall RS_Receive(Pointer Parameter)
{
bResult = Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));

if(bResult && Number_Bytes_Read != 0) Form1->RichEdit2->Text = Buffer_I;
else
MessageBox(NULL, "W buforze wejściowym nie ma danych do" " odebrania ", " Uwaga ", MB_OK);

return TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::ReceiveClick(TObject *Sender)
{
if (hCommDev > 0)
{
hThread_Rec = BeginThread (NULL, 0, RS_Receive, NULL, 0, uThreadID_Rec);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//--------------------------------------------------------------------
int __fastcall RS_SendWritten(Pointer Parameter)
{
strcpy(Buffer_O, Form1->RichEdit2->Lines->Text.c_str());
Write_Comm(hCommDev, strlen(Buffer_O));
FlushFileBuffers(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::SendWrittenClick(TObject *Sender)
{
if (hCommDev > 0)
{
hThread_SW = BeginThread (NULL, 0, RS_SendWritten, NULL, 0, uThreadID_SW);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//--------------------------------------------------------------------
Posługując się przedstawionymi do tej pory algorytmami, jesteśmy w stanie przetransmitować pliki znacznych rozmiarów, nie troszcząc się zbytnio o bufor wyjściowy. Jeżeli interesuje nas jedynie przesłanie pliku, rozmiar bufora wyjściowego możemy tak naprawdę ustalić na 1 bajt (cbOutQueue = 1) - tyle ile pobieramy z dysku za pomocą funkcji _lread(). Jej użycie wydłuża oczywiście czas transmisji, niemniej jednak będziemy mieli pewność, że przeznaczony do wysłania zbiór danych dojdzie do adresata w całości. Należy dodać, że istnieje znacznie szybszy sposób przetransferowania porcji danych. W tym celu można od razu załadować zawartość okna edycji do bufora wyjściowego w sposób identyczny, jak w przypadku danych wysyłanych z klawiatury. Przykładowa funkcja mogłaby przybrać następującą postać:
int __fastcall RS_SendFile(Pointer Parameter)
{
...
strcpy(Buffer_O, Form1->RichEdit1->Lines->Text.c_str());
Write_Comm(hCommDev, strlen(Buffer_O));
return TRUE;
}
Niemniej jednak postępując w ten sposób, musimy już pamiętać o zadeklarowaniu odpowiedniego bufora dla danych wyjściowych. Ponadto należy zdawać sobie sprawę, że istnieje zawsze niebezpieczeństwo bardzo szybkiego przepełnienia bufora wejściowego odbiornika, jeżeli oczywiście nie używa on specjalnych protokołów transmisji. Dalsze omawianie zwartości powyższego wydruku nie będzie już chyba na naszym etapie rozważań wnosić nic nowego. Są jeszcze pewne subtelności związane z zastosowanym przez nas sposobem wykorzystania wątków w programach komunikacyjnych. Omówimy je już za chwilę.
Zamiast Timera
Kłopoty z tytułowym bohaterem tego podrozdziału nie są oczywiście własnością jedynie Delphi. Testując napisany wcześniej program (projekt p_RS_07.bpr), na pewno zauważyliśmy, że nie pracował on w sposób wybitnie elegancki. Działo się to również za sprawą użycia w nim komponentu TCSpinEdit, którego działanie bez szeregu uprzednich zabezpieczeń może być nieco zdradliwe. Zapewne większość Czytelników już się z tym zapoznała, testując tamtą aplikację. Lekarstwem na te zmartwienia może okazać się użycie w odpowiednim miejscu programu dwóch instrukcji do...while (lub oczywiście samych while), przy czym zewnętrzna, z logicznego punktu widzenia, nigdy nie może ulec zakończeniu. Funkcja RS_Send_Receive(), będąca integralną częścią naszej nowej aplikacji, jest właśnie tak zbudowana. Kolejną modyfikacją, jaką zastosowałem, jest użycie komponentu TUpDown. Ma on taką ciekawą własność, że wartość jego cechy Position może być wyświetlana tylko pośrednio, np. w polu edycji TEdit. Wówczas nawet złośliwe skasowanie zawartości danego pola edycji w żadnym razie nie wpłynie na sposób funkcjonowania programu, gdyż aktualna wartość jego wymienionej cechy będzie zawsze argumentem funkcji opóźniającej Sleep(). Całość została uzupełniona o dobrze znaną nam funkcję tworzącą wątek, którą wywołujemy w funkcji obsługi zdarzenia MeasureONClick().
Kod modułu RS_09.cpp aplikacji wykorzystującej elementy wielowątkowości przy obsłudze woltomierza cyfrowego
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//----RS_09.cpp-------------

#include
#pragma hdrstop
#include "RS_09.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
#define cbOutQueue 16 //rozmiar bufora danych wyjściowych
#define cbInQueue 16 //rozmiar bufora danych wejściowych

TForm1 *Form1;

LPCTSTR query = "CDAT?\r\n"; // zapytanie o mierzone napięcie
char Buffer_O[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I[cbInQueue]; // bufor danych wejściowych
DWORD Number_Bytes_Read; // liczba bajtów do czytania
HANDLE hCommDev; // identyfikator portu
LPCTSTR lpFileName;
DCB dcb;
DWORD fdwEvtMask;
COMSTAT Stat;
DWORD Errors;

BOOL bResult = TRUE;
int hThread_SR;
unsigned ThreadID_SR;
Cardinal intVar; // licznik pomiaru
//--------------------------------------------------------------------
int __fastcall Close_Comm(HANDLE hCommDev)
{
CloseHandle(hCommDev);
return TRUE;
}
//--------------------------------------------------------------------
int __fastcall Write_Comm(HANDLE hCommDev, DWORD nNumberOfBytesToWrite)
{
DWORD NumberOfBytesWritten;
if (WriteFile(hCommDev, &Buffer_O[0], nNumberOfBytesToWrite, &NumberOfBytesWritten , NULL) > 0)
{
WaitCommEvent(hCommDev, &fdwEvtMask, NULL);

return TRUE;
}
else
return FALSE;
}
//--------------------------------------------------------------------
int __fastcall Read_Comm(HANDLE hCommDev, 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, &Buffer_I[0], nNumberOfBytesToRead, lpNumberOfBytesRead, NULL);
}
else
*lpNumberOfBytesRead = 0;
return TRUE;
}
//--------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------
void __fastcall TForm1::CloseCommClick(TObject *Sender)
{ switch(MessageBox(NULL, " Działanie aplikacji zostanie" " zakończone.", "Uwaga!", MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES :
{
SuspendThread((HANDLE)hThread_SR);
Close_Comm(hCommDev);
Application->Terminate();
}
case ID_CANCEL : Abort();
}
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
OpenComm->Enabled = TRUE;
UpDown1->Position = 1000;
Edit1->Text = "1000";
intVar = 0;
}
//--------------------------------------------------------------------
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);
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;

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_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
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);
}
else
{
switch ((int)hCommDev)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
break;
};
}
}
//--------wysłanie zapytania i odbiór danych--------------------------
int __fastcall RS_Send_Receive(Pointer Parameter)
{
do {
do { //-- wysyłanie zapytania
//Beep();
FlushFileBuffers(hCommDev);
} while (Write_Comm(hCommDev, strlen(Buffer_O)) == 0);

Sleep(Form1->UpDown1->Position);

//-- odbiór danych
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I)); if (Number_Bytes_Read >0)
{
Form1->RichEdit2->Text = IntToStr(intVar++);
Form1->RichEdit1->Text = Buffer_I;
}
else
{
Beep();
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
}

} while (bResult); // koniec nadrzędnego DO

return TRUE;
}
//----------------pomiar----------------------------------------------
void __fastcall TForm1::MeasureONClick(TObject *Sender)
{
if (hCommDev > 0) // powtórnie sprawdza, czy port jest otwarty
{
OpenComm->Enabled = FALSE;
strcpy(Buffer_O, query);
hThread_SR = BeginThread (NULL, 0, RS_Send_Receive, NULL, 0, ThreadID_SR);
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//--------------wznowienie pomiaru------------------------------------
void __fastcall TForm1::MeasureResumeClick(TObject *Sender)
{
ResumeThread((HANDLE)hThread_SR);
}
//---------------wstrzymanie pomiaru----------------------------------
void __fastcall TForm1::MeasureSuspendClick(TObject *Sender)
{
SuspendThread((HANDLE)hThread_SR);
}
//---------------synchronizacja---------------------------------------
void __fastcall TForm1::UpDown1Click(TObject *Sender, TUDBtnType Button)
{
Edit1->Text = IntToStr(UpDown1->Position); // sterowanie
// opóźnieniem
}
//--------------------------------------------------------------------
Uważnie śledząc zapisy niektórych algorytmów zamieszczonych w naszej książce, na pewno wielu z Czytelników zauważyło, iż możliwym jest alternatywny sposób odwołania się do Write_Comm(), który w funkcji RS_Send_Receive() może przybrać formę:
BOOL bResult_Write = FALSE;
...
//-- wysyłanie zapytania
do {
FlushFileBuffers(hCommDev);
bResult_Write = Write_Comm(hCommDev, strlen(Buffer_O));
} while ( ! bResult_Write );
Pokazana metoda wysyłania zapytań i rozkazów w pętli do...while jest na pewno bardziej przejrzysta, niemniej jednak nie powinniśmy zauważyć większych różnic w tempie działania algorytmu, posługując się konstrukcją taką jak na wydruku 6.4 oraz zaprezentowaną powyżej. Inaczej będzie podczas odbierania komunikatów. Można by pomyśleć, iż równie dobrze funkcja Read_Comm() będzie działać w pętli skonstruowanej według następującego przepisu:
BOOL bResult_Read = FALSE;
...
//-- odbiór danych
do {
bResult_Read = Read_Comm(hCommDev, &Number_Bytes_Read,
sizeof(Buffer_I));
Form1->RichEdit2->Text = IntToStr(intVar++);
Form1->RichEdit1->Text = Buffer_I;

} while ( ! bResult_Read );
if ( ! bResult_Read )

Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
Niewątpliwie, sam proces odczytu danych z portu przebiegać będzie prawidłowo. Niemniej jednak będziemy mieli problem ze zdiagnozowaniem ewentualnych błędów w trakcie odbioru komunikatów. Wynika to z prostego faktu - nigdy nie wiemy, kiedy tak naprawdę urządzenie zacznie przesyłać kompletną odpowiedź, zaś nasza funkcja Read_Comm() wywoływana jest w pętli cyklicznie, przez co w buforze wejściowym może nieustannie pojawiać się jakiś znak, co spowoduje, że warunek instrukcji if nie będzie spełniony. Podobnie jak uczyniliśmy w Delphi, również w przedstawionym na wydruku 6.4 przykładzie funkcję RS_Send_Receive() można zapisać w nieco odmienny, ale równie poprawny sposób za pomocą samych instrukcji while. Musi być tu zawsze spełniony warunek kontynuacji:
int __fastcall RS_Send_Receive(Pointer Parameter)
{
while (bResult)
{
while (Write_Comm(hCommDev, strlen(Buffer_O)) == 0)
{
//Beep();
FlushFileBuffers(hCommDev);
}

Sleep(Form1->UpDown1->Position);
//-- odbiór danych
Read_Comm(hCommDev, &Number_Bytes_Read, sizeof(Buffer_I));
if (Number_Bytes_Read > 0)
{
Form1->RichEdit2->Text = IntToStr(intVar++);
Form1->RichEdit1->Text = Buffer_I;
}
else
{
Beep();
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
}
} // koniec nadrzędnego while
return TRUE;
}
Jest jeszcze jeden, na pozór drobny szczegół w wykorzystaniu w Builderze pseudoidentyfikatora wątku. W naszym przykładzie hThread_SR zadeklarowany został jako typ całkowity int. Deklaracja ta zgodna była z obowiązującymi w C++Builderze regułami implementacji funkcji BeginThread(). Pewną trudność napotkamy jednak, gdy zechcemy manipulować własnościami takiego pseudoidentyfikatora. Bardzo dobrym przykładem mogą być funkcje SuspendThread() oraz ResumeThread(). Testując nasz ostatni program, łatwo się przekonamy, że argumentami ich nie mogą być po prostu liczby całkowite. Jeżeli ktoś zechciałby dokładnie to sprawdzić, wystarczy w jakimś miejscu kodu wywołać jedną z tych funkcji w sposób następujący:
SuspendThread(hThread_SR);
Podane przez kompilator komunikaty o błędzie nie powinny nas zaskoczyć:
[C++ Error] RS_09.cpp(234): E2034 Cannot convert 'int' to 'void *'
[C++ Error] RS_09.cpp(234): E2342 Type mismatch in parameter
'hThread' (wanted 'void *', got 'int')
Okazuje się, że argumentami tych funkcji muszą być dane typu void * (LPVOID), zdolne do lokalizowania pewnych obszarów pamięci operacyjnej. Jeżeli przypomnimy sobie, co powiedzieliśmy na początku rozdziału piątego o danych typu HANDLE, będziemy mieli pełny obraz sytuacji. Jeżeli dla tego przypadku zadeklarowaliśmy zmienną typu int, to w celu zapewnienia poprawności wykonania omówionych funkcji należy wykorzystać po prostu metodę rzutowania typów, tak jak zostało to wykonane w funkcjach obsługi zdarzeń MeasureResumeClick() oraz MeasureSuspendClick(), które z powodzeniem mogą przyjąć równoważną postać:
//---------------wznowienie pomiaru----------------------------------
void __fastcall TForm1::MeasureResumeClick(TObject *Sender)
{
ResumeThread((LPVOID)hThread_SR);
}
//---------------wstrzymanie pomiaru----------------------------------
void __fastcall TForm1::MeasureSuspendClick(TObject *Sender) {
SuspendThread((LPVOID)hThread_SR);
}

Bez trudu zauważymy, że w pewnych przypadkach kompilator nie rozróżnia danych typu LPVOID oraz HANDLE. Jeżeli ktoś zechce być do końca dociekliwy, na pewno spróbuje identyfikator portu zadeklarować w sposób następujący:
LPVOID hCommDev;

Rezultat działania programu z tak określonym identyfikatorem portu powinien skłonić do pewnej refleksji osoby nadal uważające, że "uchwyt" HANDLE jest numerem nadanym określonemu portowi komunikacyjnemu (plikowi, oknu w programie lub innemu obiektowi Win32).

Wykorzystanie niektórych narzędzi graficznych
Zajmiemy się pewnymi graficznymi aspektami tworzonych w Windows aplikacji, obsługujących łącze szeregowe. Pisząc program sterujący jakimś urządzeniem, możemy spotkać się z potrzebą wizualizacji odczytywanych danych w trakcie wykonywania różnego rodzaju pomiarów. Wykresy sporządzane w trakcie zbierania danych siłą rzeczy będą miały naturę poglądową i wykonywane są z reguły dla wygody Użytkownika. Wszelkiego rodzaju wizualne opracowywanie danych pomiarowych odbywa się już po ukończeniu danego eksperymentu. Wykorzystuje się w tym celu standardowe narzędzia, takie jak Grapher, Excel czy Matlab. Ich dostępność sprawia, że wszelkie próby własnoręcznego tworzenia takich odpowiedników stają się obecnie bezcelowe. Część interfejsu programisty Win32 API, dzięki której możemy wykonywać operacje graficzne nosi nazwę GDI (ang. Graphic Device Interface). O wyglądzie konstruowanego wykresu decyduje wydzielony, prostokątny obszar roboczy formularza, zwany potocznie płótnem (ang. Canvas). Obszar ten, posiadając naturę obiektową, reprezentowany jest przez własność Canvas. Udostępnia on programiście wiele metod, które są niezwykle pomocne przy różnego rodzaju operacjach graficznych. Zarówno w Delphi jak i C++Builderze istnieją ponadto gotowe komponenty, za pomocą których bez wysiłku możemy bardzo szybko stworzyć nawet dosyć skomplikowany wykres. Ponieważ sposób prezentacji wykresów odpowiednich dla pisanych przez nas programów w istocie nie różni się niczym w środowiskach Delphi i C++Buildera, dlatego metody ich realizacji zostaną opisane przy wykorzystaniu Object Pascala.
Komponent TChart
Komponent typu wykres-diagram (ang. chart) służy do zunifikowania sposobu prezentacji grafiki w aplikacjach posługujących się różnego rodzaju wykresami. Jego bezsprzeczną zaletą jest to, że zastosowanie go do już stworzonych przez nas aplikacji absolutnie nie wiąże się z ich poważną przebudową, o czym przekonamy się za chwilę. TChart wywodzi się z klasy TPanel i jest jednym z najważniejszych komponentów udostępnianych przez bibliotekę TeeChart. Zawiera ona bogaty zbór wykresów, tzw. Chart Series Types. Wszystkie one wywodzą z pseudoabstrakcyjnej klasy TChartSeries W celu łatwiejszego zarządzania poszczególnymi rodzajami wykresów zdefiniowano abstrakcyjną klasę TCustomSeries, będącą wspólnym przodkiem dla TLineSeries, TAreaSeries oraz TPointSeries. Nadają się one doskonale do wizualizacji danych liczbowych otrzymywanych w wyniku transmisji szeregowej od konkretnego przyrządu pomiarowego. Pokażemy teraz, w jaki sposób można dołączyć do naszych programów wykres liniowy, ukrywający się pod właściwością Series komponentu TLineSeries. Jako bazowy posłuży nam projekt p_RS_22.dpr. Zmodyfikujmy go, dzieląc jego planszę na dwie części. Wszystkie komponenty służące do sterowania transmisją szeregową oraz wyświetlające otrzymywane dane rozmieścimy w jego górnej połowie. W dolnej części wstawimy obiekt typu TChart. Postarajmy się dopasować jego rozmiary do wolnej powierzchni naszego formularza. Klikając dwukrotnie w jego obszarze, dostaniemy się do pola edycji Editing Chart1. Klikając przyciskiem Add w aktywnej karcie Series, otworzymy galerię biblioteki TeeChart, czyli TeeChartGallery. Wybierzmy wykres typu Line. Dalej wybierzmy kartę Titles i zmieńmy tytuł wykresu np. na Wykres pomiaru. Jeżeli nie chcemy oglądać legendy pomiarów, w kolejnej karcie Legend odznaczmy jej cechę Visible. Postępując w identyczny sposób możemy określić inne cechy naszego wykresu (kolor, linie, tło, głębokość rzutu 3-wymiarowego, itp.) Po ustawieniu wszystkich żądanych parametrów Editing Chart1 zamknijmy przyciskiem Close. Pozostaje nam już tylko odpowiednie włączenie wykresu do kodu aplikacji. Zrobimy to w treści funkcji RS_Send_Receive(). Dane otrzymywane do tej pory od przyrządu pomiarowego były wyświetlane jedynie w komponentach edycyjnych lub ewentualnie zapisywane w postaci łańcuchów znaków w pliku na dysku. Tym razem należy zamienić je na konkretną postać numeryczną. Wykorzystamy w tym celu procedurę:
procedure val(S; var V; var Code: Integer);
gdzie S jest danym łańcuchem znaków (w naszym przypadku będzie on oczywiście odczytywany z bufora danych wejściowych Buffer_I), zaś V zwraca postać numeryczną danego ciągu znaków . Parametr Code przechowuje informacje dotyczące przebiegu operacji przekształcenia. Jeżeli przyjmuje ona wartość 0, oznacza to, że przekształcenie z postaci łańcucha na wartość numeryczną zostało wykonane poprawnie. Trzeba jednak dodać w tym miejscu, że sprawdzanie tego ostatniego warunku w naszych aplikacjach z oczywistych względów nie ma wielkiego sensu. W praktyce można spotkać się z sytuacją, w której urządzenie zwraca wyniki pomiaru w formacie pokazanym na rysunku 5.9. Należy wówczas zadbać o odpowiednie pozbycie się zbędnego zera. Zawsze można w tym celu użyć funkcji copy(). Wartości odkładane na osi Y wykresu przechowywane są w buforze wejściowym, zaś kolejnymi wartościami osi X będą po prostu kolejne punkty pomiarowe. Te dwie pary liczb należy uczynić widocznymi w naszym wykresie. W tym celu skorzystamy z metody AddXY():
function AddXY(Const AXValue, AYValue: Double; Const AXLabel: String; AColor: TColor) : Longint;
która w naszym programie zostanie użyta następująco:
Form1.Series1.AddXY(intVar, V, '',clTeeColor);
gdzie intVar jest dobrze znaną zmienną, przechowującą aktualny numer pomiaru. W miejsce AXLabel wstawiłem pusty znak po to, by na osi X pojawiały się jedynie kolejne punkty pomiarowe. Również parametr AColor został użyty opcjonalnie jako clTeeColor. Jeżeli ktoś zechciałby mieć wykres koloru np. zielonego, wystarczy wpisać: clGreen. Przykład bardzo uniwersalnej funkcji, w której zarówno wysyłamy zapytanie do urządzenia jak i odczytujemy, a następnie wyświetlamy w komponentach edycyjnych oraz na wykresie otrzymane dane, został przedstawiony poniżej:

function RS_Send_Receive(P : Pointer): Integer;
var j : Integer;
begin
while(bResult = TRUE) do
BEGIN
while(Write_Comm(hCommDev, StrLen(Buffer_O)) = 0) do
FlushFileBuffers(hCommDev);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);
//-------odczyt danych z portu--------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
val(Buffer_I, V, Code);
Form1.Series1.AddXY(intVar, V, '',clTeeColor);
end
else
begin
Form1.Memo2.Lines.Add('x0'); // brak lub błędna wartość
// odczytu
Beep();
Form1.Memo2.Lines.Add('');
for j := 0 to cbInQueue do
Buffer_I[j] := char(0);
end;
END; // koniec while
Result:=0;
end;
W porównaniu z jej poprzednimi wersjami zastosowałem tu jeszcze jedną modyfikację, polegającą na dokładnym wyczyszczeniu bufora wejściowego po jakimkolwiek dodatkowym i nieprzewidzianym błędzie w transmisji. Postępując nieco asekuracyjnie przy pisaniu programów obsługujących urządzenia pomiarowe, na pewno ani im ani sobie w niczym nie zaszkodzimy. Przykładowy projekt p_wykres.dpr, którego formularz pokazany jest na rysunku 7.2, realizuje proces odczytu temperatury aktualnie mierzonej miernikiem cyfrowym z jednoczesną jej wizualizacją na wykresie uzyskanym dzięki zastosowaniu komponentu typu TChart. Główną częścią kodu (patrz wydruk 7.1) tego projektu jest opisana wcześniej funkcja RS_Sen_Receive(). Dzięki prezentowanej aplikacji mamy możliwość nie tylko śledzenia pomiaru w postaci wykresu. W każdej chwili dotychczasowe wyniki można przekopiować poprzez schowek do dowolnego arkusza kalkulacyjnego czy innego zaawansowanego programu graficznego, gdzie mogą być poddane dalszej obróbce nawet w trakcie dalszego działania macierzystego programu. Przyciski umożliwiające kopiowanie danych w trakcie pomiaru zostały umieszczone w widocznych miejscach jedynie ze względów praktycznych. Ktoś o większym poczuciu estetyki odpowiednie opcje może zamieścić w dyskretnym menu (patrz rysunek 5.8). Dla wygody Użytkownika istnieje ponadto możliwość przedstawienia wykresu w postaci linii lub jej 3-wymiarowego rzutu. Wykorzystałem w tym celu właściwość View3D komponentu typu TChart. Zadania te realizowane są poprzez procedury obsługi zdarzeń Picture2Dclick() oraz Picture3Dclick(). Głębokość 3-wymiarowego rzutu można regulować, korzystając z właściwości Chart3DPercent. Nie została ona użyta w naszym programie, jednak jej ewentualny zapis będzie bardzo prosty:
procedure TForm1.Percent3DClick(Sender: TObject);
begin
Chart1.Chart3DPercent := PrecentNumber;
end;
gdzie PrecentNumber może być liczbą całkowitą z zakresu od 1 do 100 (domyślnie przyjmowana jest jako 15).
Kompletny kod przykładowego modułu rs_wykres.pas aplikacji wykorzystującej komponent TChart do wyświetlania na wykresie odebranych w wyniku transmisji szeregowej danych liczbowych.

unit rs_wykres;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons, TeEngine, Series, TeeProcs, Chart;
type
TForm1 = class(TForm)
Memo1: TMemo;
Memo2: TMemo;
Panel1: TPanel;
Panel2: TPanel;
Panel3: TPanel;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
OpenComm: TButton;
Start: TButton;
Suspend: TButton;
Resume: TButton;
CloseComm: TButton;
TrackBar1: TTrackBar;
Edit1: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Chart1: TChart;
Series1: TLineSeries;
Picture3D: TRadioButton;
Picture1D: TRadioButton;
procedure StartClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure SuspendClick(Sender: TObject);
procedure ResumeClick(Sender: TObject);
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
procedure Picture3DClick(Sender: TObject);
procedure Picture2DClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var

Form1: TForm1;

implementation

{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;

cbInQueue = 32; // rozmiary buforów danych
cbOutQueue = 32;

query_1 : PChar = '*IDN?'+#13+#10;
query_2 : PChar = 'CDAT?'+#13+#10; // przykładowe zapytania

var
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;

intVar : Integer; // licznik pomiarów
intVarSleep : Integer; // licznik późnienia
bResult : BOOL; // "niema" zmienna logiczna
hThread_SR : THANDLE;
ThreadID_SR: Cardinal;

Code, V : Integer;

//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckProcess: Integer;
begin
iCheckProcess := MessageDlg('Zakończenie pomiaru i zamknięcie'+ ' aplikacji?', mtConfirmation, [mbYes, mbNo], 0);
case iCheckProcess of
idYes:
begin
SuspendThread(hThread_SR);
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: Exit;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
TrackBar1.Position := 1000;
TrackBar1.Max := 2000;
TrackBar1.Min := 1;
TrackBar1.Frequency := 100;
OpenComm.Enabled := TRUE;
intVar := 0;
intVarSleep := 1000;
bResult := TRUE;
Form1.BorderIcons:=[biSystemMenu, biMinimize];
Form1.Series1.LinePen.Color := clBlue;
Form1.Series1.LinePen.Style := psSolid;
Picture3D.Checked := TRUE;
end;
//--------------------------------------------------------------------
function Write_Comm(hCommDev: THANDLE; nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;

begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue; ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
function RS_Send: Integer;
begin
Repeat
FlushFileBuffers(hCommDev);
Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Result := 0;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
var i : Integer;
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM2';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
BEGIN
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate:=CBR_1200;

//-przykładowe ustawienia znaczników sterujących DCB-
dcb.Flags := dcb_fParity;

dcb.Parity := ODDPARITY;
dcb.StopBits :=ONESTOPBIT;
dcb.ByteSize :=7;

SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);

StrCopy(Buffer_O, query_1);

RS_Send; // zapytanie o identyfikację urządzenia

Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
// -- wyświetlanie numeru przyrządu

Application.MessageBox(PChar(AnsiString(Buffer_I)), 'Identyfikacja urządzenia przyłączonego do wybranego'+ ' portu :' ,MB_OK);

OpenComm.Enabled := FALSE;
end
else
Application.MessageBox('Urządzenie nie odpowiada ', 'Uwaga !' ,MB_OK);

for i:=0 to cbInQueue do
begin
Buffer_O[i] := char(0);
Buffer_I[i] := char(0);
end;

Sleep(1000);
END
else
case hCommDev of
IE_BADID:
begin
lpFileName := '';
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny', 'Uwaga !',MB_OK);
end;
end;

end;
//--------------------------------------------------------------------
function RS_Send_Receive(P : Pointer): Integer;
var j : Integer;
begin

while(bResult = TRUE) do
BEGIN
while(Write_Comm(hCommDev, StrLen(Buffer_O)) = 0) do
FlushFileBuffers(hCommDev);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);
//-------odczyt danych z portu--------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));

Inc(intVar); // zliczanie kolejnych pomiarów

Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));

val(Buffer_I, V, Code);

Form1.Series1.AddXY(intVar, Round(V), '',clTeeColor);
end
else
begin
Form1.Memo2.Lines.Add('x0');
Beep();
Form1.Memo2.Lines.Add('');
for j := 0 to cbInQueue do
Buffer_I[j] := char(0);
end;
END; // koniec while
Result:=0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, query_2);
hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0, ThreadID_SR);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//----------wstrzymanie pomiaru --------------------------------------
procedure TForm1.SuspendClick(Sender: TObject);
begin
SuspendThread(hThread_SR);
end;
//----------wznowienie pomiaru ---------------------------------------
procedure TForm1.ResumeClick(Sender: TObject);
begin
ResumeThread(hThread_SR);
end;
//-----kopiowanie okna edycji Memo2 do schowka------------------------
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
Form1.Memo2.SelectAll;
Form1.Memo2.CopyToClipboard;
end;
//-----kopiowanie okna edycji Memo1 do schowka------------------------
procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
Form1.Memo1.SelectAll;
Form1.Memo1.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
intVarSleep := TrackBar1.Position; // sterowanie późnieniem
Edit1.Text := IntToStr(TrackBar1.Position);
end;
//---------wykres 3-wymiarowy--------------------------------------
procedure TForm1.Picture3DClick(Sender: TObject);
begin
Chart1.View3D := TRUE;
end;
//---------wykres 2-wymiarowy---------------------------------------
procedure TForm1.Picture2DClick(Sender: TObject);
begin
Chart1.View3D := FALSE;
end;
//--------------------------------------------------------------------
end.
Z wielu metod udostępnianych przez komponent TChart można również wykorzystać możliwość powiększania lub pomniejszania wykresu w trakcie działania programu. Temu celowi służą metody AnimatedZoom, ZoomPercent oraz UndoZoom. Ich ewentualne użycie w programie zapewnią nam procedury obsługi zdarzeń, zaprojektowane według następujących schematów:
procedure TForm1.ZoomINClick(Sender: TObject);
begin
Chart1.AnimatedZoom := TRUE;
Chart1.ZoomPercent(125); // powiększenie do 125%
end;
//--------------------------------------------------------------------
procedure TForm1.ZoomOutClick(Sender: TObject);
begin
Chart1.AnimatedZoom := TRUE;
Chart1.ZoomPercent(75); // pomniejszenie do 75%
end;
//--------------------------------------------------------------------
procedure TForm1.UndoZoomClick(Sender: TObject);
begin
Chart1.UndoZoom; // przywrócenie domyślnego rozmiaru
end;
//--------------------------------------------------------------------
Przedstawiony program testowany był podczas komunikacji z bardzo szybkim, nowoczesnym urządzeniem mierzącym temperaturę. Zastosowany algorytm działał poprawnie nawet dla przedziału czasu próbkowania łącza wynoszącego 100 ms, tzn. dla minimalnego czasu, w którym urządzenie było zdolne odczytać wysłaną komendę (zapytanie o mierzoną wielkość), przestroić się, dokonać pomiaru oraz zwrócić wartość aktualnie zmierzonej temperatury.
Komponent TPaintBox
Wszystkie komponenty wyższego rzędu realizujące grafikę mają właściwość Canvas, będącą obiektem klasy TCanvas. Różnego rodzaju wykresy można projektować w ramach obszaru roboczego formularza. Jeżeli jednak zechcemy, aby realizowane były w jego określonym fragmencie, wygodnie jest skorzystać z komponentu TPaintBox. Wszelkie współrzędne wykresu będą wyznaczone właśnie przez ten obiekt dzięki jego właściwościom Top, Left, Height oraz Width. Linie możemy rysować, używając metod MoveTo() oraz LineTo(). Z powodzeniem można też wykorzystać funkcję PolyLine(), której parametrem jest tablica punktów w sensie ich współrzędnych. W celu określenia współrzędnych odpowiednich punktów można skorzystać z bardzo prostej funkcji Point(), wywoływanej z dwoma parametrami. Funkcja ta zwraca rekord typu TPoint składający się z dwóch zmiennych: X oraz Y..Projektując kod omawianego programu, zastosowałem bardzo prosty sposób skalowania osi. W procedurze obsługi zdarzenia OpenCommClick() tuż po odczycie numeru przyrządu wywoływana jest powtórnie funkcja RS_Send(), dokonująca pierwszego pomiaru mierzonej wielkości. Pierwszy element tablicy YAxis[1] służy do dalszego skalowania osi i całego wykresu. Dane na wykresie wyświetlane są w porcjach po 400 punktów pomiarowych w trakcie działania osobnego wątku, w którym jednocześnie dokonuje się właściwy pomiar.
Kod modułu RS_23.pas aplikacji wykorzystującej metody komponentu TPaintBox
unit RS_23;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ExtCtrls, Buttons;
type
TForm1 = class(TForm)
Memo1: TMemo;
Memo2: TMemo;
Panel1: TPanel;
Panel2: TPanel;
Panel3: TPanel;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
OpenComm: TButton;
Start: TButton;
Suspend: TButton;
Resume: TButton;
CloseComm: TButton;
TrackBar1: TTrackBar;
Edit1: TEdit;
Edit2: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
PaintBox1: TPaintBox;
procedure StartClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure SuspendClick(Sender: TObject);
procedure ResumeClick(Sender: TObject);
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
// -- wartości znaczników sterujących portu szeregowego --
dcb_fBinary = $0001;
dcb_fParity = $0002;
cbInQueue = 32; // rozmiary buforów danych
cbOutQueue = 32;
query_1 : PChar = '*IDN?'+#13+#10;
query_2 : PChar = 'CDAT?'+#13+#10; // przykładowe zapytania
var
Buffer_O : ARRAY[0..cbOutQueue] of Char; // bufor wyjściowy
Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
intVar : Integer; // licznik pomiarów
intVarSleep : Integer; // licznik późnienia
bResult : BOOL; // "niema" zmienna logiczna
hThread_SR : THANDLE;
ThreadID_SR: Cardinal;
const
MaxMeasure = 3000; // maksymalna liczba punktów pomiarowych !
MarginY = 30;
MarginX = 40;
XAxisLenght = MarginX + 400;
var
V, Value, Code : Integer;
XAxis, YAxis : ARRAY[1..MaxMeasure] of Integer;

//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckProcess: Integer;
begin
iCheckProcess := MessageDlg('Zakończenie pomiaru i zamknięcie'+ ' aplikacji?', mtConfirmation, [mbYes, mbNo], 0);
case iCheckProcess of
idYes:
begin
SuspendThread(hThread_SR);
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: Exit;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
TrackBar1.Position := 1000;
TrackBar1.Max := 2000;
TrackBar1.Min := 1;
TrackBar1.Frequency := 100;
OpenComm.Enabled := TRUE;
intVar := 0;
intVarSleep := 1000;
bResult := TRUE;
Form1.PaintBox1.Canvas.Font.Size := 10;
Form1.PaintBox1.Canvas.Brush.Color := clBtnFace;
Form1.PaintBox1.Canvas.Font.Color:=clBlack;
Form1.BorderIcons:=[biSystemMenu, biMinimize];
Form1.label5.Visible := FALSE;
Form1.label6.Visible := FALSE;
Form1.Edit2.Visible := FALSE;
end;
//--------------------------------------------------------------------
function Write_Comm(hCommDev: THANDLE; nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;

begin
WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL);
if (WaitCommEvent(hCommDev, fdwEvtMask, NIL) = TRUE) then
Write_Comm := 1
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function Read_Comm(hCommDev: THANDLE; Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0 ) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue; ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead, Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
function RS_Send: Integer;
begin
Repeat
FlushFileBuffers(hCommDev);
Until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Result := 0;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
var i : Integer;
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM2';

hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
BEGIN
SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox2.Checked = TRUE) then
dcb.BaudRate:=CBR_1200;
//-przykładowe ustawienia znaczników sterujących DCB-
dcb.Flags := dcb_fParity;
dcb.Parity := ODDPARITY;
dcb.StopBits :=ONESTOPBIT;
dcb.ByteSize :=7;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
StrCopy(Buffer_O, query_1);
RS_Send;
Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Application.MessageBox(PChar(AnsiString(Buffer_I)), 'Identyfikacja urządzenia przyłączonego do wybranego'+ ' portu :' ,MB_OK);
OpenComm.Enabled := FALSE;
end
else
Application.MessageBox('Urządzenie nie odpowiada ', 'Uwaga !' ,MB_OK);
for i:=0 to cbInQueue do
begin
Buffer_O[i] := char(0);
Buffer_I[i] := char(0);
end;
StrCopy(Buffer_O, query_2);
RS_Send;
Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
val(Buffer_I, V, Code);
YAxis[1] := Round(0.5*V);
Form1.Memo2.Text:=AnsiString(Buffer_I);
end;
END
else
case hCommDev of
IE_BADID:
begin
lpFileName := '';
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny', 'Uwaga !',MB_OK);
end;
end;

end;
//--------------------------------------------------------------------
procedure PaintLine(Canvas: TCanvas; X, Y, Lenght: Integer);
begin
Canvas.PolyLine([Point(X, Y ), Point(X, Lenght)]);
end;
//--------------------------------------------------------------------
function RS_Send_Receive(P: Pointer): Integer;
var j : Integer;
begin

Form1.Edit2.Top := Form1.Height - 2*MarginY;
YAxis[1] := Form1.PaintBox1.Height - YAxis[1];
Form1.PaintBox1.Canvas.MoveTo(MarginX + 2, YAxis[1]);
Form1.PaintBox1.Canvas.Pen.Width := 2;
while( bResult = TRUE) do
BEGIN
while(Write_Comm(hCommDev, StrLen(Buffer_O)) = 0) do FlushFileBuffers(hCommDev);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);

//-------odczyt danych z portu---------------------------------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));

val(Buffer_I, V, Code);

YAxis[intVar] := Form1.PaintBox1.Height - Round(V*0.5);
Form1.PaintBox1.Canvas.Font.Color:=clBlack;
if (intVar < 400) then
begin
Form1.PaintBox1.Canvas.Pen.Color := clRed;
XAxis[intVar] := MarginX + 1 + intVar;
//Form1.PaintBox1.Canvas.Pixels[XAxis[intVar],
// YAxis[intVar]] := 255;
Form1.PaintBox1.Canvas.LineTo(XAxis[intVar],
YAxis[intVar]);
Form1.Edit2.Left := XAxis[intVar] + 20;
Form1.Edit2.Text := IntToStr(intVar);
end
ELSE BEGIN
case intVar of
400:
begin
Value := intVar;
Form1.PaintBox1.Canvas.Pen.Color := clBtnFace;
for j:=1 to Value do
Form1.PaintBox1.Canvas.LineTo(XAxis[j], YAxis[j]);
Form1.PaintBox1.Canvas.MoveTo(MarginX + 2, YAxis[Value - 1]);
end;
800:
begin
Value := intVar;
Form1.PaintBox1.Canvas.Pen.Color := clBtnFace;
for j := 400 to Value do
Form1.PaintBox1.Canvas.LineTo(XAxis[j], YAxis[j]);
Form1.PaintBox1.Canvas.MoveTo(MarginX + 2, YAxis[Value - 1]);
end;
1200:
begin
Value := intVar;
Form1.PaintBox1.Canvas.Pen.Color := clBtnFace;
for j := 800 to Value do
Form1.PaintBox1.Canvas.LineTo(XAxis[j], YAxis[j]);
Form1.PaintBox1.Canvas.MoveTo(MarginX + 2, YAxis[Value - 1]);
end;
1600:
begin
Value := intVar;
Form1.PaintBox1.Canvas.Pen.Color := clBtnFace;
for j := 1200 to Value do
Form1.PaintBox1.Canvas.LineTo(XAxis[j], YAxis[j]);
Form1.PaintBox1.Canvas.MoveTo(MarginX + 2, YAxis[Value - 1]);
end;
2000:
begin
Value := intVar;
Form1.PaintBox1.Canvas.Pen.Color := clBtnFace;
for j := 1600 to Value do
Form1.PaintBox1.Canvas.LineTo(XAxis[j], YAxis[j]);
Form1.PaintBox1.Canvas.MoveTo(MarginX + 2, YAxis[Value - 1]);
end;
end; // koniec case
Form1.PaintBox1.Canvas.Pen.Color := clRed;
XAxis[intVar] := MarginX + 1 + (intVar-Value);
Form1.PaintBox1.Canvas.LineTo(XAxis[intVar], YAxis[intVar]);
Form1.Edit2.Left := XAxis[intVar] + 20; Form1.Edit2.Text := IntToStr(intVar);
END;
end // koniec if
else
begin
Form1.Memo2.Lines.Add('x0');
Beep();
Form1.Memo2.Lines.Add('');
for j := 0 to cbInQueue do Buffer_I[j] := char(0);
end;
END; // koniec while
Result:=0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartClick(Sender: TObject);
var i, iScal : Integer;
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, query_2);
hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0, ThreadID_SR);
PaintBox1.Canvas.Pen.Width := 2;
PaintBox1.Canvas.Pen.Color := clBlack;
iScal := Round((Form1.PaintBox1.Height - MarginY)/YAxis[1]);
PaintBox1.Canvas.MoveTo(MarginX, Form1.PaintBox1.Height - MarginY);
PaintBox1.Canvas.LineTo(XAxisLenght, Form1.PaintBox1.Height - MarginY);
PaintBox1.Canvas.MoveTo(MarginX, Form1.PaintBox1.Height - MarginY);
PaintBox1.Canvas.LineTo(MarginX, Round(iScal/YAxis[1]));
PaintBox1.Canvas.Pen.Width := 1;
Form1.PaintBox1.Canvas.Font.Color:=clBlack;
i := 0;
Repeat // oś X
PaintLine(Form1.PaintBox1.Canvas, MarginX + i, Form1.PaintBox1.Height - MarginY, Form1.PaintBox1.Height - MarginY + 2 + 5);
i := i + 50;
Until(i > XAxisLenght - MarginX);
i:=0;
Repeat // oś Y
PaintBox1.Canvas.PolyLine([Point(MarginX, Form1.PaintBox1.Height - MarginY -i ), Point(MarginX - 10, Form1.PaintBox1.Height - MarginY -i)]);
PaintBox1.Canvas.TextOut(MarginX-40, Form1.PaintBox1.Height - 40 - i, IntToStr(Trunc(2.5*i)));
i := i + 20;
Until(i >= 1.5*YAxis[1]);
Form1.label5.Visible := TRUE;
Form1.label6.Visible := TRUE;
Form1.Edit2.Visible := TRUE;
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
//----------wstrzymanie pomiaru --------------------------------------
procedure TForm1.SuspendClick(Sender: TObject);
begin
SuspendThread(hThread_SR);
end;
//----------wznowienie pomiaru ---------------------------------------
procedure TForm1.ResumeClick(Sender: TObject);
begin
ResumeThread(hThread_SR);
end;
//-----kopiowanie okna edycji Memo2 do schowka------------------------
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
Form1.Memo2.SelectAll;
Form1.Memo2.CopyToClipboard;
end;
//-----kopiowanie okna edycji Memo1 do schowka------------------------
procedure TForm1.SpeedButton2Click(Sender: TObject);
begin
Form1.Memo1.SelectAll;
Form1.Memo1.CopyToClipboard;
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
intVarSleep := TrackBar1.Position; // sterowanie późnieniem
Edit1.Text := IntToStr(TrackBar1.Position);
end;
//--------------------------------------------------------------------
end.
Konstruując samodzielnie wykresy, wyświetlane w czasie działania aplikacji odczytującej dane pochodzące od jakiegoś urządzenia pomiarowego, należy pamiętać, że w przypadku prostego formularza czy obiektu typu TPaintBox, zawartość odpowiedniego obiektu TCanvas nie jest przechowywana w pamięci i może w pewnych warunkach ulec łatwemu zamazaniu przez np. następną uruchomioną aplikację. Tej wady nie mają wykresy rysowane na mapach bitowych.
Komponent TImage
Zarówno w Delphi jak i Builderze mapę bitową najlepiej jest wyświetlić za pomocą komponentu TImage. Można wstawić do niego mapę bitową z zewnętrznego pliku lub wykorzystać w tym celu edytor obrazów Image Editor, znajdujący się w opcji Tools głównego menu. Wstawiona do formularza mapa bitowa będzie stanowić tło dla naszego wykresu, posiadając oczywiście swój własny obiekt TCanvas. Możliwym jest zatem użycie narzędzi graficznych płótna, takich jak: pióro (własność Pen), pędzel (Brush) czy czcionka (Font). Posłużymy się również funkcjami (metodami) TextOut(), LineTo(), MoveTo(), PolyLine() oraz Point().Złudzenie całkowitego wypełnienia obszaru pod krzywą uzyskałem, rysując wykres za pomocą funkcji PolyLine() wywoływanej w treści procedury PaintLine(), identycznie jak zostało to przedstawione na wydruku powyższym Wydruk poniższy prezentuje część algorytmu, realizującego pomiar interesującej nas wielkości fizycznej (w tym przypadku temperatury) oraz wyświetlającego wyniki w postaci odpowiedniego wykresu.
Złudzenie całkowitego wypełnienia obszaru pod krzywą uzyskałem, rysując wykres za pomocą funkcji PolyLine() wywoływanej w treści procedury PaintLine(), identycznie jak zostało to przedstawione na wydruku 7.2. Wydruk 7.3 prezentuje część algorytmu, realizującego pomiar interesującej nas wielkości fizycznej (w tym przypadku temperatury) oraz wyświetlającego wyniki w postaci odpowiedniego wykresu.
Fragment kodu modułu RS_24.pas aplikacji wykorzystującej komponent TImage
const
MaxMeasure = 3000; // maksymalna liczba pomiarów
MarginY = 30;
MarginX = 40;
XAxisLenght = MarginX + 400;
var V, Value, Code : Integer;
XAxis, YAxis : ARRAY[1..MaxMeasure] of Integer;
...
function RS_Send_Receive(P: Pointer): Integer;
var j : Integer;
begin
while( bResult = TRUE) do
BEGIN
while(Write_Comm(hCommDev, StrLen(Buffer_O)) = 0) do
FlushFileBuffers(hCommDev);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);
//-------odczyt danych z portu----------------------------------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
val((Buffer_I), V, Code);
YAxis[intVar] := Form1.Image1.Height - Round(V*0.5);
Form1.Image1.Canvas.Font.Color:=clBlack;
if (intVar < 400) then
begin Form1.Image1.Canvas.Pen.Color := clRed;
XAxis[intVar] := MarginX + intVar;
PaintLine(Form1.Image1.Canvas, XAxis[intVar] , Form1.Image1.Height - MarginY-2, YAxis[intVar]);
Form1.Image1.Canvas.TextOut(MarginX + intVar, Form1.Image1.Height - MarginY + 5, IntToStr(intVar));
end
ELSE BEGIN
case intVar of
400:
begin
Value := intVar;
Form1.Image1.Canvas.Pen.Color := clBtnFace;
for j:=1 to Value do
PaintLine(Form1.Image1.Canvas, XAxis[j] , Form1.Image1.Height - MarginY -2, YAxis[j]);
Form1.Image1.Canvas.TextOut(MarginX + 400, Form1.Image1.Height - MarginY + 5, ' ');
end;
800:
begin
Value := intVar;
Form1.Image1.Canvas.Pen.Color := clBtnFace; for j := 400 to Value do PaintLine(Form1.Image1.Canvas, XAxis[j] , Form1.Image1.Height - MarginY -2, YAxis[j] );
Form1.Image1.Canvas.TextOut(MarginX + 400, Form1.Image1.Height - MarginY + 5, ' ');
end;
1200:
begin
Value := intVar;
Form1.Image1.Canvas.Pen.Color := clBtnFace;
for j := 800 to Value do
PaintLine(Form1.Image1.Canvas, XAxis[j] , Form1.Image1.Height - MarginY -2, YAxis[j]);
Form1.Image1.Canvas.TextOut(MarginX + 400, Form1.Image1.Height - MarginY + 5, ' ');
end;
1600:
begin
Value := intVar;
Form1.Image1.Canvas.Pen.Color := clBtnFace;
for j := 1200 to Value do
PaintLine(Form1.Image1.Canvas, XAxis[j] , Form1.Image1.Height - MarginY -2, YAxis[j]);
Form1.Image1.Canvas.TextOut(MarginX + 400, Form1.Image1.Height - MarginY + 5, ' ');
end;
2000:
begin
Value := intVar;
Form1.Image1.Canvas.Pen.Color := clBtnFace;
for j := 1600 to Value do PaintLine(Form1.Image1.Canvas, XAxis[j] , Form1.Image1.Height - MarginY -2, YAxis[j]);
Form1.Image1.Canvas.TextOut(MarginX + 400, Form1.Image1.Height - MarginY + 5, ' ');
end;
end; // koniec case
Form1.Image1.Canvas.Pen.Color := clRed;
XAxis[intVar] := MarginX + 1 + (intVar-Value);
PaintLine(Form1.Image1.Canvas, XAxis[intVar] , Form1.Image1.Height - MarginY -2, YAxis[intVar]);
Form1.Image1.Canvas.TextOut(MarginX + 1+ (intVar-Value), Form1.Image1.Height - MarginY + 5, IntToStr(intVar));
END;
end
else
begin
Form1.Memo2.Lines.Add('x0');
Beep();
Form1.Memo2.Lines.Add('');
for j := 0 to cbInQueue do
Buffer_I[j] := char(0);

end;
END; // koniec while
Result:=0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartClick(Sender: TObject);
var i, iScal : Integer;
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, query_2);
hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0, ThreadID_SR);
Image1.Canvas.Pen.Width := 2;
Image1.Canvas.Pen.Color := clBlack;
iScal := Round((Form1.Image1.Height - MarginY)/YAxis[1]);
Image1.Canvas.MoveTo(MarginX, Form1.Image1.Height - MarginY);
Image1.Canvas.LineTo(XAxisLenght, Form1.Image1.Height - MarginY); Image1.Canvas.MoveTo(MarginX, Form1.Image1.Height - MarginY);
Image1.Canvas.LineTo(MarginX, Round(iScal/YAxis[1]));
Image1.Canvas.Pen.Width := 1;
Form1.Image1.Canvas.Font.Color:=clBlack;
i := 0;
Repeat // oś X
PaintLine(Form1.Image1.Canvas, MarginX + i, Form1.Image1.Height - MarginY, Form1.Image1.Height - MarginY + 2 + 5);
i := i + 50;
Until(i > XAxisLenght - MarginX);
i := 0;
Repeat // oś Y
Image1.Canvas.PolyLine([Point(MarginX, Form1.Image1.Height - MarginY -i ), Point(MarginX - 10, Form1.Image1.Height - MarginY -i)]);
Image1.Canvas.TextOut(MarginX-40, Form1.Image1.Height - 40 - i, IntToStr(Trunc(2.5*i)));
i := i + 20;
Until(i >= 1.5*YAxis[1]);
Form1.label5.Visible := TRUE;
Form1.label6.Visible := TRUE;
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
Projektując powyższy algorytm, przewidziałem możliwość rejestracji co najwyżej 3000 punktów pomiarowych, których współrzędne przechowywane są w elementach jednowymiarowych tablic XAxis oraz YAxis. Jednak w praktyce liczba pomiarów może być wielokrotnie większa. Należy wówczas skorzystać z tablic deklarowanych dynamicznie, jeżeli oczywiście w dowolnej chwili zechcemy odtworzyć całą historię pomiaru. Pamiętać również należy, że używanie mapy bitowej uszczupli nieco zasoby systemu operacyjnego oraz pamięć naszego PC.
Samodzielne tworzenie mapy bitowej
W celu prostszego i efektywniejszego zarządzania mapami bitowymi zdefiniowano klasę TBitmap, wykorzystującą definicje typów Win32: HBITMAP oraz HPALETTE. Samodzielne utworzenie mapy bitowej wymaga zadeklarowania zmiennej typu TBitmap:
var
...
TheBitmap : TBitmap;
...
Następnie należy stworzyć i przypisać jej obiekt tego samego typu. Operację taką wraz z ustaleniem rozmiaru mapy bitowej, koloru jej obszaru oraz sposobu wyświetlania wygodnie jest wykonać w oddzielnej procedurze:
procedure BitMapCreate;
begin
TheBitmap := TBitmap.Create;
TheBitmap.Height := 265;
TheBitmap.Width := 521; TheBitmap.Canvas.Brush.Color := clBtnFace;
TheBitmap.Transparent := TRUE;
end;
Wywołując w odpowiednim miejscu programu taką procedurę, zainicjujemy obszar mapy bitowej, na którym można rysować dowolny wykres. Aby tak otrzymany wykres wyświetlić w danym miejscu formularza, należy użyć metody Draw(), która kopiuje mapę do określonego obszaru roboczego. Jeżeli chcielibyśmy zobaczyć nasz wykres, w miejscu formularza o współrzędnych np. 22, 240 wystarczy zapisać:
Form1.Canvas.Draw(22, 240, TheBitmap);
Pojedyncze użycie wymienionej metody zapewni jednorazowe wyświetlenie jednego punktu pomiarowego. W naszych aplikacjach operacje odczytu i wyświetlania danych wykonywane są cyklicznie. Musimy więc metodę tę wywoływać każdorazowo po dokonaniu kolejnego odczytu, który jest równoznaczny z uzupełnieniem o kolejny punkt aktualnie rysowanego wykresu. Trzeba przyznać, że komplikuje to nieco algorytm. Wydruk prezentuje funkcję, której zadaniem jest odczytywanie danych z portu szeregowego oraz ich graficzne ich przedstawienie. Funkcję tę wywołujemy w procedurze obsługi zdarzenia StartClick(), uruchamiającego odrębny wątek programu. Z tego właśnie powodu wszystkie operacje związane z odczytem danych, skalowaniem, rysowaniem osi oraz samego wykresu, który jest następnie odpowiednio "przewijany" w obszarze mapy bitowej muszą być zawarte w jednej funkcji.
Fragment kodu modułu RS_25.pas aplikacji wykorzystującej mapę bitową
//--------------------------------------------------------------------
...
function RS_Send_Receive(P: Pointer): Integer;
var iScal, i, j : Integer;
begin
BitMapCreate;
TheBitmap.Canvas.Pen.Width := 2;
TheBitmap.Canvas.Pen.Color := clBlack;
iScal := Round((TheBitmap.Height - MarginY)/YAxis[1]);
TheBitmap.Canvas.MoveTo(MarginX, TheBitmap.Height - MarginY);
TheBitmap.Canvas.LineTo(XAxisLenght, TheBitmap.Height - MarginY);
TheBitmap.Canvas.MoveTo(MarginX, TheBitmap.Height - MarginY);
TheBitmap.Canvas.LineTo(MarginX, Round(iScal/YAxis[1]));
TheBitmap.Canvas.Pen.Width := 1;
TheBitmap.Canvas.Font.Color:=clBlack;

i := 0;
Repeat // oś X
PaintLine(TheBitmap.Canvas, MarginX + i, TheBitmap.Height - MarginY, TheBitmap.Height - MarginY + 2 + 5);
i := i + 50;
Until(i > XAxisLenght - MarginX);
i:=0;
Repeat // oś Y
TheBitmap.Canvas.PolyLine([Point(MarginX, TheBitmap.Height - MarginY -i ), Point(MarginX - 10, TheBitmap.Height - MarginY -i)]);
TheBitmap.Canvas.TextOut(MarginX-40, TheBitmap.Height - 40 - i, IntToStr(Trunc(2.5*i)));
i := i + 20;
Until(i >= 1.5*YAxis[1]);
Form1.label5.Visible := TRUE;
Form1.label6.Visible := TRUE;
Form1.Canvas.Draw(22, 240, TheBitmap); // wyświetlanie mapy bitowej
while( bResult = TRUE) do
BEGIN
while(Write_Comm(hCommDev, StrLen(Buffer_O)) = 0) do FlushFileBuffers(hCommDev);
Form1.Memo1.Lines.Add('');
Sleep(intVarSleep);
//-------odczyt danych z portu----------------------------------
if ( Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.Memo2.Lines.Add(AnsiString(Buffer_I));
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
val(Buffer_I, V, Code);

YAxis[intVar] := TheBitmap.Height - Round(V*0.5);

TheBitmap.Canvas.Font.Color:=clBlack;

if (intVar < 400) then
begin
TheBitmap.Canvas.Pen.Color := clRed;
XAxis[intVar] := MarginX + intVar;
PaintLine(TheBitmap.Canvas, XAxis[intVar] , TheBitmap.Height - MarginY-2, YAxis[intVar] );
TheBitmap.Canvas.TextOut(MarginX + intVar, TheBitmap.Height - MarginY + 5, IntToStr(intVar));
Form1.Canvas.Draw(22, 240, TheBitmap);
end
ELSE BEGIN
case intVar of
400:
begin
Value := intVar;
TheBitmap.Canvas.Pen.Color := clBtnFace;
for j:=1 to Value do
PaintLine(TheBitmap.Canvas, XAxis[j] , TheBitmap.Height - MarginY -2, YAxis[j]);
TheBitmap.Canvas.TextOut(MarginX + 400, TheBitmap.Height - MarginY + 5, ' ');
Form1.Canvas.Draw(22, 240, TheBitmap);
end;
800:
begin
Value := intVar;
TheBitmap.Canvas.Pen.Color := clBtnFace;
for j := 400 to Value do
PaintLine(TheBitmap.Canvas, XAxis[j] , TheBitmap.Height - MarginY -2, YAxis[j]);
TheBitmap.Canvas.TextOut(MarginX + 400, TheBitmap.Height - MarginY + 5, ' ');
Form1.Canvas.Draw(22, 240, TheBitmap);
end;
1200:
begin
Value := intVar;
TheBitmap.Canvas.Pen.Color := clBtnFace;
for j := 800 to Value do
PaintLine(TheBitmap.Canvas, XAxis[j] , TheBitmap.Height - MarginY -2, YAxis[j]);
TheBitmap.Canvas.TextOut(MarginX + 400, TheBitmap.Height - MarginY + 5, ' ');
Form1.Canvas.Draw(22, 240, TheBitmap);
end;
1600:
begin
Value := intVar;
TheBitmap.Canvas.Pen.Color := clBtnFace;
for j := 1200 to Value do
PaintLine(TheBitmap.Canvas, XAxis[j] , TheBitmap.Height - MarginY -2, YAxis[j]);
TheBitmap.Canvas.TextOut(MarginX + 400, TheBitmap.Height - MarginY + 5, ' ');
Form1.Canvas.Draw(22, 240, TheBitmap);
end;
2000:
begin
Value := intVar;
TheBitmap.Canvas.Pen.Color := clBtnFace;
for j := 1600 to Value do
PaintLine(TheBitmap.Canvas, XAxis[j] , TheBitmap.Height - MarginY -2, YAxis[j]);
TheBitmap.Canvas.TextOut(MarginX+400, TheBitmap.Height - MarginY + 5, ' ');
Form1.Canvas.Draw(22, 240, TheBitmap);
end;
end; // koniec case
TheBitmap.Canvas.Pen.Color := clRed;
XAxis[intVar] := MarginX + 1 + (intVar-Value);
PaintLine(TheBitmap.Canvas, XAxis[intVar] , TheBitmap.Height - MarginY -2, YAxis[intVar]);
TheBitmap.Canvas.TextOut(MarginX + 1+ (intVar-Value), TheBitmap.Height - MarginY + 5, IntToStr(intVar));
Form1.Canvas.Draw(22, 240, TheBitmap);
END;
end
else
begin
Form1.Memo2.Lines.Add('x0');
Beep();
Form1.Memo2.Lines.Add('');
for j := 0 to cbInQueue do
Buffer_I[j] := char(0);
end;
END; // koniec while
Result:=0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
StrCopy(Buffer_O, query_2);
hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0, ThreadID_SR);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub jest on'+ ' aktywny ', 'Uwaga !',MB_OK);
end;
Przed zakończeniem działania programu obszar pamięci przydzielony tak skonstruowanej mapie bitowej musi być zwolniony. Czynimy to zwykle przy użyciu metody Free, dezaktywującej obiekt mapy. ...
TheBitmap.Free;

Przykładowe aplikacje wykorzystywane w systemach pomiarowych
Omówimy niektóre przykłady zastosowań aplikacji obsługujących przyrządy pomiarowe z wykorzystaniem standardu RS 232C. Istnieje pewna dziedzina wiedzy, obejmująca zarówno teoretyczne jak i praktyczne zagadnienia związane z pomiarami. Jest nią metrologia. Podobnie jak w innych gałęziach nauki i techniki, tak i w metrologii w ostatnich kilkudziesięciu latach dokonał się olbrzymi postęp. Od fazy, w której dominowały pomiary oparte na metodzie porównawczej (bezpośredniego porównywania mierzonych wielkości za pomocą mierników wychyłowo-wskaźnikowych) poprzez wykorzystywanie przyrządów elektrycznych, których wskazania były rejestrowane przez różnego rodzaju samopisy, dochodzimy do etapu, w którym dokonanie szybkiego i wiarygodnego pomiaru stało się niemożliwe bez wykorzystania komputera sprzęgniętego z urządzeniem pomiarowym. Wykorzystując komputer, mamy możliwość automatycznego sterowania procesem zbierania i przetwarzania danych. Współczesne przyrządy pomiarowe są bardzo zaawansowane pod względem technologicznym. Ich części składowe wykonywane są w postaci wysokospecjalizowanych układów scalonych lub hybrydowych, których konstrukcja objęta jest tajemnicą handlową. Urządzenia takie mają określone funkcje i parametry eksploatacyjne, które należy optymalnie wykorzystać. O możliwościach w pełni skomputeryzowanego systemu pomiarowego w coraz mniejszym stopniu decyduje wiedza o konstrukcji danego przyrządu, w coraz większym zaś specjalistyczne oprogramowanie.
Kontroler temperatury
Jako przykład wykorzystania poznanych do tej pory sposobów programowej obsługi łącza szeregowego RS 232C wybrałem kontroler temperatury firmy LakeShore. Jest on przykładem nowoczesnego wielofunkcyjnego miernika, za pomocą którego nie tylko można odczytywać aktualnie mierzoną temperaturę, ale przede wszystkim ją stabilizować. Wykorzystując specjalnie skonstruowaną grzałkę sterowaną z wymienionego urządzenia, mamy możliwość ciągłego utrzymywania danego układu w z góry zadanej temperaturze. Aktualna wartość mierzonej temperatury odczytywana jest za pomocą diody półprzewodnikowej. Korzystając z takich aplikacji mamy możliwość wyboru jednostek, w których odczytujemy temperaturę i ustalenia szybkości grzania (stopnie na minutę). Mamy też możliwość wyboru trybu pracy miernika: z wyłączoną lub z włączoną opcją grzania (grzanie szybkie lub pośrednie). Można również ustalić górna granicę temperatury, w której chcemy utrzymywać dany układ fizyczny bez względu na warunki zewnętrzne. Aplikacja obsługująca kontroler temperatury została napisana w Delphi, zaś jej kompletny został zamieszczony na wydruku Obsługa programu sprowadza się do umiejętnego wykorzystania poznanych już wcześniej komponentów oraz funkcji obsługujących transmisję szeregową. Jednak budowa algorytmu różni się nieco od prezentowanych wcześniej, dlatego przedstawię teraz jego ogólne założenia. Główne modyfikacje zostały wprowadzone w treści procedur obsługujących zdarzenia otwarcia portu szeregowego oraz odczytu danych. Uruchamiając program i otwierając wybrany port szeregowy do transmisji, od razu diagnozujemy aktualne ustawienia przyrządu. Tuż po otwarciu portu, w procedurze obsługi zdarzenia OpenCommClick() wielokrotnie wywoływana jest inna procedura:
procedure RS_Send (queryORcommand : PChar);
begin
repeat // transmisja zapytania lub komendy
FlushFileBuffers(hCommDev);
StrCopy(Buffer_O, queryORcommand);
until(Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
end;
gdzie w miejsce jej parametru queryORcommand podstawiamy w kolejności zapytania o typ diody, wartość pierwszego pomiaru. Dowiadujemy się też, czy przy poprzednim uruchomieniu programu sterującego ustalono i zapamiętano górną temperaturę grzania układu. Następnie pytamy o identyfikację przyrządu, aktualne jednostki oraz czy ustalono wcześniej szybkość grzania i czy włączono dany stopień grzania. Wszystkie te informacje będą Użytkownikowi bardzo pomocne, jeżeli chce mieć kompletną informację o parametrach wcześniejszych pomiarów. Informację o włączonym procesie podgrzewania próbki otrzymujemy, podświetlając odpowiedni komponent TShape znajdujący się obok odpowiadającego mu przycisku, tak aby w razie potrzeby ewentualnie włączone grzanie można było w miarę szybko wyłączyć. Po wstępnym zdiagnozowaniu stanu wskazań miernika dobrze by było, gdyby aplikacja od razu zaoferowała nam możliwość zapisu danych (w postaci np. pliku *.dat) na dysku. Dokonamy tego, wyświetlając komunikat:
wResult_Save := MessageDlg('Zapisać dane do pliku ? *.dat.', mtCustom, [mbYes, mbCancel, mbNo], 0);
case wResult_Save of
mrYes:
begin
if (SaveDialog1.Execute) then
begin
bResult_Yes := TRUE;
AssignFile(OutFile, SaveDialog1.FileName+'.dat');
Rewrite(OutFile);
end;
end;
mrNo : Exit;
end;
Tego rodzaju metoda poinformowania o możliwości zapamiętania danych na dysku nie jest być może zbyt elegancka, niemniej jednak - co wydaje się dużo ważniejsze - jest niezawodna. Postępując w ten sposób, na pewno nie zapomnimy zapamiętać efektu swojej pracy. Programy obsługujące przyrządy pomiarowe z reguły pracują przez wiele godzin, dlatego zapamiętywanie wskazań miernika w tablicach i zapisanie ich dopiero na końcu nie ma większego sensu. Dane muszą być zapisywane w trakcie pomiaru (on line). Operację tę realizuje funkcja RS_Send_Receive(). W jej treści, oprócz dokonywania właściwego pomiaru oraz cyklicznego zapisu danych na dysku, dowiadujemy się ponadto, jaki jest aktualnie stopień mocy grzania próbki (jeżeli oczywiści opcja ta jest włączona którymś z przycisków HeaterMedium lub HeaterFast).
function RS_Send_Receive(P: Pointer): Integer;
var
ivart, Code : Integer;
begin
REPEAT
Clean_Buffers;
{-- pytanie o aktualną moc grzejnika [%] --}
if (bResult_Heater = TRUE) then
begin
StrCopy(Buffer_O, query_HEAT);
repeat // transmisja komunikatu
FlushFileBuffers(hCommDev);
until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0)
and (bResult_Heater = TRUE) then
begin
val(Buffer_I, ivart, Code);
Form1.Gauge1.Progress := ivart;
end;
end
else
Form1.Gauge1.Progress := 0;
{-- pytanie o aktualnie mierzoną wartość --}
StrCopy(Buffer_O, query);
repeat // transmisja komunikatu
FlushFileBuffers(hCommDev);
until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Sleep(intVarSleep);
//-------odczyt danych z portu--------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
Form1.RichEdit1.Text := Buffer_I;
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
if (bResult_Yes = TRUE) then
WriteLN(OutFile, intVar, ' ', Form1.RichEdit1.Text);
end
else
begin
Form1.RichEdit1.Text := 'x0'; // błędny odczyt
Beep();
end;
UNTIL(bResult = FALSE);
Result := 0;
end;
Stopień mocy podgrzewania (od 1 do 100%) pokazywany jest dzięki komponentowi TGauge, zaś kolejny numer pomiaru wyświetlany jest w komponencie edycyjnym TMemo. Czynności zamiany skali temperatur dokonywane są w procedurach obsługi zdarzeń TemperatureKelvinClick() oraz TemperatureCelsiusClick(). Nie ograniczyłem się w nich jedynie do prostego sposobu wysłania rozkazu zmiany skali, zażądałem ponadto odczytu górnej granicy temperatury grzania właściwej dla danej skali:
...
RS_Send(query_SETP);
Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
val(Buffer_I, ivart, Code);
UpDown1.Position := ivart;
Edit2.Text := IntToStr(UpDown1.Position);
end;
Jej część całkowita wyświetlana jest w komponencie edycyjnym Edit2. Część ułamkowa tej liczby nie została uwzględniona, gdyż odczyt taki będzie z reguły pełnić funkcję jedynie orientacyjną. Jeżeli zajdzie potrzeba ponownego jej ustalenia i tak będziemy musieli uczynić to powtórnie, korzystając z procedur obsługi zdarzeń UpDown1Click() oraz UpDown2Click(). W treści drugiego z nich zamieściłem algorytm, dzięki któremu, manipulując cechami Position komponentów TUpDown, możemy płynnie ustalać górną temperaturę grzania z wymaganą dokładnością do jednego miejsca po kropce, jednocześnie wysyłając odpowiedni rozkaz do przyrządu. Aktualne wartości cech Position odpowiednich komponentów TUpDown zostaną przypisane cechom Text komponentów edycyjnych TEdit. Rozkaz wysyłamy używając funkcji RS_Send(), której argumentem jest akceptowana przez przyrząd komenda SETP (ang. Set Point), uzupełniony o aktualne cechy Text komponentów Edit3 (reprezentuje część całkowitą liczby) i Edit2 (część ułamkowa) oraz zakończona parą znaków CR LF.
procedure TForm1.UpDown2Click(Sender: TObject; Button: TUDBtnType);
begin
if (CheckBox8.Checked = FALSE) then
begin
if (UpDown2.Position = 10) then
begin
UpDown1.Position := UpDown1.Position + 1;
UpDown2.Position := 0;
end;
if (UpDown2.Position = 0) then
begin
UpDown1.Position := UpDown1.Position;
UpDown2.Position := 0;
end;
if (UpDown2.Position < 0) then
begin
UpDown1.Position := UpDown1.Position - 1;
UpDown2.Position := 9;
end;
Edit3.Text := IntToStr(UpDown2.Position);
Edit2.Text := IntToStr(UpDown1.Position);
RS_Send(PChar('SETP'+''+Edit2.Text+'.'+Edit3.Text+#13+#10));
StartMeasure.Enabled := TRUE;
end;
end;
W bardzo podobny sposób funkcjonują zdarzenia UpDown1Click() oraz UpDown3Click(), w których ustalamy stopień szybkości grzania w stopniach na minutę. Zamiar ustalenia szybkości podgrzewania sygnalizujemy, klikając w obszar komponentu CheckBox8. Trzeba jednak dodać w tym miejscu, że wyboru skali i, ewentualnie, górnej granicy temperatury należy wykonywać przed rozpoczęciem właściwego pomiaru. Rzadko się zdarza, by ktoś wpadł na cudowny pomysł zmieniania jednostek w trakcie eksperymentu. Jeżeli jednak zajdzie taka potrzeba, proces zbierania danych musi być czasowo wstrzymany, co automatycznie związane jest ze wstrzymaniem działania wątku, w którym odbywa się główna transmisja danych. W takich przypadkach należy dać czas urządzeniu na przestrojenie się. Podobnie rzecz się ma np. z ustalaniem tempa grzania czy stopnia jego szybkości. Najpierw ustalamy stopień a dopiero potem podajemy tempo - co jest równoznaczne z włączeniem grzejnika. Musimy pamiętać o zachowaniu kolejności działań. Projektując poniższy algorytm, starałem się tak zabezpieczyć aplikację, by w danej chwili dostępne były opcje, które aktualnie mogą być wykonywane.
Kod modułu RS_26.pas aplikacji obsługującej kontroler temperatury
unit RS_26;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Gauges, StdCtrls, ExtCtrls, Buttons, ComCtrls;
type
TForm1 = class(TForm)
GroupBox1: TGroupBox;
GroupBox2: TGroupBox;
GroupBox3: TGroupBox;
GroupBox4: TGroupBox;
GroupBox5: TGroupBox;
GroupBox6: TGroupBox;
GroupBox7: TGroupBox;
GroupBox8: TGroupBox;
GroupBox9: TGroupBox;
Memo1: TMemo;
Shape1: TShape;
Shape2: TShape;
Shape3: TShape;
HeaterMedium: TBitBtn;
HeaterOFF: TBitBtn;
HeaterFast: TBitBtn;
TemperatureKelvin: TBitBtn;
TemperatureCelsius: TBitBtn;
StartMeasure: TButton;
OpenComm: TButton;
SuspendMeasure: TButton;
ResumeMeasure: TButton;
CloseComm: TButton;
Bevel1: TBevel;
SaveDialog1: TSaveDialog;
CheckBox1: TCheckBox;
CheckBox2: TCheckBox;
CheckBox3: TCheckBox;
CheckBox4: TCheckBox;
CheckBox5: TCheckBox;
CheckBox6: TCheckBox;
CheckBox7: TCheckBox;
CheckBox8: TCheckBox;
RichEdit1: TRichEdit;
RichEdit2: TRichEdit;
TrackBar1: TTrackBar;
Edit1: TEdit;
Edit2: TEdit;
Edit3: TEdit;
Edit4: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
UpDown3: TUpDown;
UpDown1: TUpDown;
UpDown2: TUpDown;
Gauge1: TGauge;
StatusBar1: TStatusBar;
StaticText1: TStaticText;
procedure CloseCommClick(Sender: TObject);
procedure OpenCommClick(Sender: TObject);
procedure StartMeasureClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure TrackBar1Change(Sender: TObject);
procedure SuspendMeasureClick(Sender: TObject);
procedure HeaterOFFClick(Sender: TObject);
procedure HeaterMediumClick(Sender: TObject);
procedure ResumeMeasureClick(Sender: TObject);
procedure HeaterFastClick(Sender: TObject);
procedure TemperatureKelvinClick(Sender: TObject);
procedure TemperatureCelsiusClick(Sender: TObject);
procedure UpDown1Click(Sender: TObject; Button: TUDBtnType);
procedure UpDown2Click(Sender: TObject; Button: TUDBtnType);
procedure CheckBox8Click(Sender: TObject);
procedure UpDown3Click(Sender: TObject; Button: TUDBtnType);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
dcb_fBinary = $0001;
dcb_fParity = $0002;
cbInQueue = 32;
cbOutQueue = 32;
const
query_IDN : PChar = '*IDN?'+#13+#10;
query_ATYPE : PChar = 'ATYPE?'+#13+#10;
query_UNITS : PChar = 'CUNI?'+#13+#10;
query_HEATER : PChar = 'RANG?'+#13+#10;
query_RAMP : PChar = 'RAMP?'+#13+#10;
query_SETP : PChar = 'SETP?'+#13+#10;
query_HEAT : PChar = 'HEAT?'+#13+#10;
query : PChar = 'CDAT?'+#13+#10;
command_RANG0 : PChar = 'RANG 0'+#13+#10;
command_RANG2 : PChar = 'RANG 2'+#13+#10;
command_RANG3 : PChar = 'RANG 3'+#13+#10;
command_TK : PChar = 'CUNI K'+#13+#10;
command_TC : PChar = 'CUNI C'+#13+#10;
command_RAMP0 : PChar = 'RAMP 0'+#13+#10;
command_RAMP1 : PChar = 'RAMP 1'+#13+#10;
var

Buffer_O : ARRAY[0..cbOutQueue] of Char;
Buffer_I : ARRAY[0..cbInQueue] of Char;
Number_Bytes_Read : DWORD;
hCommDev : THANDLE;
lpFileName : PChar;
fdwEvtMask : DWORD;
Stat : TCOMSTAT;
Errors : DWORD;
dcb : TDCB;
intVar : LongWord;
intVarSleep : Cardinal;
bResult : BOOL;
hThread_SR : THANDLE;
ThreadID_SR: Cardinal;
OutFile : TextFile;
bResult_Yes : BOOL;
bResult_Heater : BOOL;
//--------------------------------------------------------------------
procedure TForm1.CloseCommClick(Sender: TObject);
var
iCheckProcess: Integer;
begin
iCheckProcess := MessageDlg('Zakończenie pomiaru i'+ ' zamknięcie aplikacji?', mtConfirmation, [mbYes, mbNo], 0);
case iCheckProcess of
idYes:
begin
SuspendThread(hThread_SR);
if (bResult_Yes = TRUE) then
CloseFile(OutFile);
CloseHandle(hCommDev);
Application.Terminate();
end;
idNo: Exit;
end;
end;
//--------------------------------------------------------------------
function Write_Comm(hCommDev: THANDLE;
nNumberOfBytesToWrite: DWORD): Integer;
var
NumberOfBytesWritten : DWORD;

begin
if (WriteFile(hCommDev, Buffer_O, nNumberOfBytesToWrite, NumberOfBytesWritten, NIL) = TRUE) then
begin WaitCommEvent(hCommDev, fdwEvtMask, NIL);
Write_Comm := 1;
end
else
Write_Comm := 0;
end;
//--------------------------------------------------------------------
function Read_Comm(hCommDev: THANDLE;
Buf_Size: DWORD): Integer;
var
nNumberOfBytesToRead: DWORD;
begin
ClearCommError(hCommDev, Errors, @Stat);
if (Stat.cbInQue > 0) then
begin
if (Stat.cbInQue > Buf_Size) then
nNumberOfBytesToRead := Buf_Size
else
nNumberOfBytesToRead := Stat.cbInQue;
ReadFile(hCommDev, Buffer_I, nNumberOfBytesToRead,
Number_Bytes_Read, NIL);
Read_Comm := 1;
end
else
begin
Number_Bytes_Read := 0;
Read_Comm := 0;
end;
end;
//--------------------------------------------------------------------
procedure RS_Send (queryORcommand : PChar);
begin
repeat // transmisja komunikatu
FlushFileBuffers(hCommDev);
StrCopy(Buffer_O, queryORcommand);
until(Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
end;
//--------------------------------------------------------------------
procedure Clean_Buffers;
var
i : Integer;
begin
for i := 0 to cbInQueue do
begin
Buffer_I[i] := ' ';
Buffer_O[i] := ' ';
end;
end;
//--------------------------------------------------------------------
procedure TForm1.OpenCommClick(Sender: TObject);
var
ivart, Code : Integer;
wResult_Save: Word;
begin
if (CheckBox1.Checked = TRUE) then
lpFileName:='COM1';
if (CheckBox2.Checked = TRUE) then
lpFileName:='COM2';
hCommDev:= CreateFile(lpFileName, GENERIC_READ or GENERIC_WRITE, 0, NIL, OPEN_EXISTING, 0, 0);
if (hCommDev <> INVALID_HANDLE_VALUE) then
begin SetupComm(hCommDev, cbInQueue, cbOutQueue);
dcb.DCBlength := sizeof(dcb);
GetCommState(hCommDev, dcb);
if (CheckBox3.Checked = TRUE) then
dcb.BaudRate:=CBR_300;
if (CheckBox4.Checked = TRUE) then
dcb.BaudRate:=CBR_1200;
dcb.Flags := dcb_fParity;
dcb.Parity := ODDPARITY;
dcb.StopBits :=ONESTOPBIT;
dcb.ByteSize :=7;
CheckBox5.Checked := TRUE;
CheckBox6.Checked := TRUE;
CheckBox7.Checked := TRUE;
StatusBar1.Panels[0].Text := 'Otwarty port: ' + lpFileName;
SetCommState(hCommDev, dcb);
GetCommMask(hCommDev, fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
{ -- pytanie o typ diody --}
RS_Send(query_ATYPE);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
StatusBar1.Panels[2].Text := 'Dioda typu: '+Buffer_I;
{-- pytanie o pierwszy pomiar --}
RS_Send(query);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then Form1.RichEdit1.Text := Buffer_I;
{ -- pytanie, czy ustalono górną temperaturę grzania --} RS_Send(query_SETP);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
val(Buffer_I, ivart, Code);
UpDown1.Position := ivart;
Edit2.Text := IntToStr(UpDown1.Position);
end;
{ -- pytanie o identyfikację przyrządu --}
RS_Send(query_IDN);
Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then StatusBar1.Panels[1].Text := 'Identyfikacja'+ ' urządzenia:' + Buffer_I;
{ -- pytanie o aktualne jednostki --}
RS_Send(query_UNITS);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
Form1.RichEdit2.Text := Buffer_I;
{-- pytanie, czy ustalono szybkość grzania --}

RS_Send(query_RAMP);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
if(Copy(Buffer_I, 1, 1) = '0') then
CheckBox8.Checked := FALSE;
if(Copy(Buffer_I, 1, 1) = '1') then
CheckBox8.Checked := TRUE;
end;
{ -- pytanie, czy włączono dany stopień grzania --}
RS_Send(query_HEATER);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
val(Buffer_I, ivart, Code);
if (ivart = 0) then
Shape2.Brush.Color := clBlack;
if (ivart = 2) then
Shape1.Brush.Color := clMaroon;
if (ivart = 3) then
Shape3.Brush.Color := clRed;
if (ivart <>0) then
bResult_Heater := TRUE;
end;
OpenComm.Enabled := FALSE;
UpDown1.Enabled := TRUE;
UpDown2.Enabled := TRUE;
CheckBox8.Enabled := TRUE;
{ -- pytanie o identyfikację przyrządu --}
RS_Send(query_IDN);
Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then StatusBar1.Panels[1].Text := 'Identyfikacja'+ ' urządzenia:' + Buffer_I;
Clean_Buffers;
wResult_Save := MessageDlg('Zapisać dane do pliku ? *.dat.', mtCustom, [mbYes, mbCancel, mbNo], 0);
case wResult_Save of
mrYes:
begin
if (SaveDialog1.Execute) then
begin
bResult_Yes := TRUE;
AssignFile(OutFile, SaveDialog1.FileName+'.dat');
Rewrite(OutFile);
end;
end;
mrNo : Exit;
end; // koniec case
end
else
case hCommDev of
IE_BADID:
begin
Application.MessageBox('Niewłaściwa nazwa portu'+ 'lub jest on aktywny', 'Uwaga !', MB_OK);
lpFileName:='';
end;
end;

end;
//--------------------------------------------------------------------
function RS_Send_Receive(P: Pointer): Integer;
var
ivart, Code : Integer;
begin
REPEAT
Clean_Buffers;
{-- pytanie o aktualną moc grzejnika [%] --}
if (bResult_Heater = TRUE) then
begin
StrCopy(Buffer_O, query_HEAT);
repeat // transmisja komunikatu
FlushFileBuffers(hCommDev);
until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Sleep(100);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0)
and (bResult_Heater = TRUE) then
begin
val(Buffer_I, ivart, Code);
Form1.Gauge1.Progress := ivart;
end;
end
else
Form1.Gauge1.Progress := 0;
{-- pytanie o aktualnie mierzoną wartość --}
StrCopy(Buffer_O, query);
repeat // transmisja komunikatu
FlushFileBuffers(hCommDev);
until (Write_Comm(hCommDev, StrLen(Buffer_O)) <> 0);
Sleep(intVarSleep);
//-------odczyt danych z portu--------
if ( Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0 ) then
begin
Form1.RichEdit1.Text := Buffer_I;
Inc(intVar); // zliczanie kolejnych pomiarów
Form1.Memo1.Lines.Add(AnsiString(IntToStr(intVar)));
if (bResult_Yes = TRUE) then
WriteLN(OutFile, intVar, ' ', Form1.RichEdit1.Text);
end
else
begin
Form1.RichEdit1.Text := 'x0'; // błędny odczyt
Beep();
end;
UNTIL(bResult = FALSE);
Result := 0;
end;
//--------------------------------------------------------------------
procedure TForm1.StartMeasureClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
Clean_Buffers;
StartMeasure.Enabled := FALSE;
ResumeMeasure.Enabled := FALSE;
UpDown1.Enabled := FALSE;
UpDown2.Enabled := FALSE;
UpDown3.Enabled := FALSE;
hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0, ThreadID_SR);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
SetWindowLong(Handle, GWL_EXSTYLE, 256 or WS_EX_CLIENTEDGE);
Width := Width + 1;

TrackBar1.Position := 1000;
TrackBar1.Max := 5000;
TrackBar1.Min := 1;
TrackBar1.Frequency := 500;
OpenComm.Enabled := TRUE;
StartMeasure.Enabled := TRUE;
intVar := 0;
intVarSleep := 1000;
Shape1.Brush.Color := clBtnFace;
Shape2.Brush.Color := clBtnFace;
Shape3.Brush.Color := clBtnFace;
UpDown1.Max := 1000;
UpDown2.Min := -10;
UpDown1.Enabled := FALSE;
UpDown2.Enabled := FALSE;
UpDown2.Max := 10;
UpDown3.Min := 0;
UpDown3.Max := 99;
UpDown3.Enabled := FALSE;
CheckBox8.Enabled := FALSE;
bResult := TRUE;
bResult_Yes := FALSE;
bResult_Heater := FALSE;
SaveDialog1.Filter := 'Data files (*.dat)|*.dat|All files'+ ' (*.*)|*.*';
SaveDialog1.InitialDir := ExtractFilePath(ParamStr(0));
end;
//--------------------------------------------------------------------
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
intVarSleep := TrackBar1.Position; // sterowanie późnieniem
Edit1.Text := IntToStr(TrackBar1.Position + 100);
end;
//--------------------------------------------------------------------
procedure TForm1.SuspendMeasureClick(Sender: TObject);
begin
Clean_Buffers;
SuspendThread(hThread_SR);
Memo1.Lines.Add('Wstrzymanie');
ResumeMeasure.Enabled := TRUE;
UpDown1.Enabled := TRUE;
UpDown2.Enabled := TRUE;
UpDown2.Enabled := TRUE;
if (bResult_Heater = FALSE) then
Gauge1.Progress := 0;
end;
//--------------------------------------------------------------------
procedure TForm1.HeaterOFFClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
if (SuspendThread(hThread_SR) <> 0) then
begin
RS_Send(command_RANG0);
StartMeasure.Enabled := TRUE;
ResumeMeasure.Enabled := FALSE;
Shape1.Brush.Color := clBtnFace;
Shape3.Brush.Color := clBtnFace;
Shape2.Brush.Color := clBlack;
bResult_Heater := FALSE;
Clean_Buffers;
end
else
Application.MessageBox('Pomiar należy czasowo wyłączyć ', 'Uwaga !',MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.HeaterMediumClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
if (SuspendThread(hThread_SR) <> 0) then
begin
RS_Send(command_RANG2);
StartMeasure.Enabled := TRUE;
ResumeMeasure.Enabled := FALSE;
Shape2.Brush.Color := clBtnFace;
Shape3.Brush.Color := clBtnFace;
Shape1.Brush.Color := clMaroon;
bResult_Heater := TRUE;
Clean_Buffers;
end
else
Application.MessageBox('Pomiar należy czasowo wyłączyć ', 'Uwaga !',MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.HeaterFastClick(Sender: TObject);
begin
if (hCommDev > 0) then
begin
if (SuspendThread(hThread_SR) <> 0) then
begin
RS_Send(command_RANG3);
StartMeasure.Enabled := TRUE;
ResumeMeasure.Enabled := FALSE;
Shape1.Brush.Color := clBtnFace;
Shape2.Brush.Color := clBtnFace;
Shape3.Brush.Color := clRed;
bResult_Heater := TRUE;
end
else
Application.MessageBox('Pomiar należy czasowo wyłączyć ', 'Uwaga !',MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.ResumeMeasureClick(Sender: TObject);
begin
Clean_Buffers;
ResumeThread(hThread_SR);
UpDown1.Enabled := FALSE;
UpDown2.Enabled := FALSE;
UpDown2.Enabled := FALSE;
end;
//--------------------------------------------------------------------
procedure TForm1.TemperatureKelvinClick(Sender: TObject);
var
ivart, Code : Integer;
begin
if (hCommDev > 0) then
begin
if (SuspendThread(hThread_SR) <> 0) then
begin
RS_Send(command_TK);
StartMeasure.Enabled := TRUE;
ResumeMeasure.Enabled := FALSE;
Form1.RichEdit2.Text := 'K';
Clean_Buffers;
RS_Send(query_SETP);
Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
val(Buffer_I, ivart, Code);
UpDown1.Position := ivart;
Edit2.Text := IntToStr(UpDown1.Position);
end;
Sleep(100);
RS_Send(query);
Sleep(100);
//-------odczyt danych z portu--------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
Form1.RichEdit1.Text := Buffer_I;
Clean_Buffers;
end
else
Application.MessageBox('Pomiar należy czasowo wyłączyć ', 'Uwaga !',MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.TemperatureCelsiusClick(Sender: TObject);
var
ivart, Code : Integer;
begin
if (hCommDev > 0) then
begin
if (SuspendThread(hThread_SR) <> 0) then
begin
RS_Send(command_TC);
StartMeasure.Enabled := TRUE;
ResumeMeasure.Enabled := FALSE;
Form1.RichEdit2.Text := 'C';
Clean_Buffers;
RS_Send(query_SETP);
Sleep(1000);
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
begin
val(Buffer_I, ivart, Code);
UpDown1.Position := ivart;
Edit2.Text := IntToStr(UpDown1.Position);
end;

Sleep(100);
RS_Send(query);
Sleep(100);

//-------odczyt danych z portu--------
if (Read_Comm(hCommDev, SizeOf(Buffer_I)) > 0) then
Form1.RichEdit1.Text := Buffer_I;
Clean_Buffers;
end
else
Application.MessageBox('Pomiar należy czasowo wyłączyć ', 'Uwaga !',MB_OK);
end
else
Application.MessageBox('Niewłaściwa nazwa portu lub'+ ' jest on aktywny ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.UpDown1Click(Sender: TObject; Button: TUDBtnType);
begin
if (CheckBox8.Checked = FALSE) then
begin
Edit2.Text := IntToStr(UpDown1.Position);
RS_Send(PChar('SETP'+''+Edit2.Text+'.'+Edit3.Text+#13+#10));
StartMeasure.Enabled := TRUE;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.UpDown2Click(Sender: TObject; Button: TUDBtnType);
begin
if (CheckBox8.Checked = FALSE) then
begin
if (UpDown2.Position = 10) then
begin
UpDown1.Position := UpDown1.Position + 1;
UpDown2.Position := 0;
end;
if (UpDown2.Position = 0) then
begin
UpDown1.Position := UpDown1.Position;
UpDown2.Position := 0;
end;
if (UpDown2.Position < 0) then
begin
UpDown1.Position := UpDown1.Position - 1;
UpDown2.Position := 9;
end;
Edit3.Text := IntToStr(UpDown2.Position);
Edit2.Text := IntToStr(UpDown1.Position);
RS_Send(PChar('SETP'+''+Edit2.Text+'.'+Edit3.Text+#13+#10));
StartMeasure.Enabled := TRUE;
end;
end;
//--------------------------------------------------------------------
procedure TForm1.CheckBox8Click(Sender: TObject);
begin
if (SuspendThread(hThread_SR) <> 0) then
begin
if (CheckBox8.Checked = TRUE) then
begin
RS_Send(command_RAMP1);
Sleep(100);
UpDown1.Enabled := FALSE;
UpDown2.Enabled := FALSE;
UpDown3.Enabled := TRUE;
end;
if (CheckBox8.Checked = FALSE) then
begin
RS_Send(command_RAMP0);
Sleep(100);
UpDown1.Enabled := TRUE;
UpDown2.Enabled := TRUE;
UpDown3.Enabled := FALSE;
end;
end
else
Application.MessageBox('Pomiar należy czasowo wyłączyć ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
procedure TForm1.UpDown3Click(Sender: TObject; Button: TUDBtnType);
begin
if (SuspendThread(hThread_SR) <> 0) then
begin
Edit4.Text := IntToStr(UpDown3.Position);
RS_Send(PChar('RAMPR'+' '+Edit4.Text+#13+#10));
StartMeasure.Enabled := TRUE;
Clean_Buffers;
end
else
Application.MessageBox('Pomiar należy czasowo wyłączyć ', 'Uwaga !',MB_OK);
end;
//--------------------------------------------------------------------
end.
Przedstawiony algorytm został opracowany w celu obsługi konkretnego przyrządu pomiarowego, niemniej jednak jego budowa będzie charakterystyczna dla większości aplikacji sterujących urządzeniami, z którymi można nawiązać komunikację za pomocą uniwersalnego języka zapytań. Nie uwzględniono tu jeszcze paru funkcji, jakie mogą spełniać mierniki tej klasy. Niektóre modele mogą ponadto pracować jako woltomierze czy omomierze. Samodzielne uzupełnienie aplikacji o dodatkowe funkcje właściwe konkretnemu modelowi nie powinno sprawić zainteresowanym Czytelnikom poważniejszych problemów. Wspominaliśmy też wcześniej, przy okazji omawiania sposobów transmisji i odbioru plików, o możliwości samodzielnego wyskalowania takich przyrządów za pomocą specjalnego ciągu danych (zazwyczaj dokładnie opisanych w instrukcji obsługi), zwanych krzywymi skalowania charakterystycznymi dla danego typu czujnika (diody), w jaki zaopatrzone jest urządzenie. Jeżeli ktoś zechciałby wzbogacić swoje programy o możliwości skalowania miernika, najlepiej do tego celu użyć aplikacji wielodokumentowych - MDI (ang. Multi Document Interface). Unikniemy w ten sposób zbyt wielu okien w jednym formularzu. Nie powinniśmy też mieć żadnych problemów z ewentualnym wzbogaceniem aplikacji o elementy grafiki. Połączenie z naszym formularzem np. komponentu typu TChart byłoby już tylko formalnością. Analizując powyższy kod, na pewno też zauważymy, że wielokrotnie, być może z przesadną dokładnością czyszczone były bufory komunikacyjne. Stało się to z powodu użycia tylko dwóch uniwersalnych buforów do wszystkich operacji nadawania i odbioru. Na powyższym przykładzie została też pokazana metoda odczytu więcej niż jednej wielkości pomiarowej zwracanej przez urządzenie. W tym wypadku były nimi: aktualna temperatura (zapytanie CDAT?) oraz stopień mocy grzania (zapytanie HEAT?). W bardzo podobny sposób można oprogramować, np. różnego rodzaju zasilacze. Przy obsłudze tego typu urządzenia z reguły interesuje nas nie tylko aktualnie mierzone napięcie. Równie ważny jest aktualny prąd. Wiele modeli takich przyrządów zwraca odpowiednie wielkości w odpowiedzi na standardowe zapytania: MEASURE:VOLTAGE? oraz MEASURE:CURRENT?
Aplikacja obsługująca kilka urządzeń
Przy okazji omawiania roli oprogramowania w odniesieniu do podstawowych funkcji interfejsu, wspomnieliśmy o możliwości podłączenia do jednego komputera wielu urządzeń, z których obsługą powinna sobie poradzić jedna aplikacja. W niniejszym fragmencie książki zajmiemy się tym właśnie problemem. Pokażemy jeden ze sposobów budowy tego rodzaju algorytmów na przykładzie napisanego w C++Builderze programu obsługującego znany nam już kontroler temperatury oraz precyzyjną laboratoryjną wagę elektroniczną WPS 72 firmy RADWAG. Z pełnym opisem funkcji obsługujących zdarzenia wykorzystywane w sterowaniu dwoma przykładowymi urządzeniami zapoznaliśmy się już w trakcie tej książki. Prezentowany sposób przydzielenia odrębnych identyfikatorów hCommDev_1 oraz hCommDev_2 dwóm różnym przyrządom podłączonym do odpowiednich łącz szeregowych, niezależnych buforów danych oraz zaprogramowanie ich pracy w dwóch niezależnych wątkach określonych odpowiednio pseudoidentyfikatorami hThread_SR_COM1 oraz hThread_SR_COM2 powoduje, że stają się one dla naszej aplikacji całkowicie rozróżnialne. Podobnie jak we wcześniejszym przykładzie, tak i tutaj pytanie o możliwość zapisu danych na dysku oraz funkcję zakładającą odpowiedni plik umieściłem w funkcjach obsługi zdarzeń OpenComm_1Click() oraz OpenComm_2Click(), otwierających odpowiednie porty szeregowe. Dane odbierane zarówno od kontrolera temperatury jak i wagi cyfrowej zapisywane są niezależnie do dwóch odrębnych plików. Pewne ważne funkcje, takie jak wstrzymywanie wątku, zamknięcie pliku czy zamknięcie portu szeregowego, zostały zdublowane dla każdego z obsługiwanych urządzeń. Bardzo często programiści postępują w ten sposób, zabezpieczając się tym samym przed próbami nieprawidłowego lub bezkrytycznego korzystania z programu (mamy ty przede wszystkim na myśli próby zamknięcia aplikacji z aktywnym portem szeregowym). Przykład kompletnego kodu aplikacji komunikującej się z dwoma różnymi urządzeniami został przedstawiony na wydruku pniższym
Kod modułu RS_10.cpp aplikacji obsługującej jednocześnie kontroler temperatury oraz wagę cyfrową
//--- kompilować z borlndmm.dll oraz cc3250mt.dll --------------
//----RS_10.cpp-------------
#include
#include
#pragma hdrstop
#include "RS_10.h"
#pragma package(smart_init)
#pragma resource "*.dfm"

#define cbOutQueue 32 //rozmiar bufora danych wyjściowych
#define cbInQueue 32 //rozmiar bufora danych wejściowych

TForm1 *Form1;

LPCTSTR query = "CDAT?\r\n"; // zapytanie o mierzoną temperaturę
LPCTSTR query_IDN = "*IDN?\r\n"; // identyfikacja

LPCTSTR query_weight = "SI\r\n"; // wskazania wagi
LPCTSTR command_TARE = "T\r\n"; // rozkaz tarowania wagi

char Buffer_O_COM2[cbOutQueue]; // bufor danych wyjściowych
char Buffer_I_COM2[cbInQueue]; // bufor danych wejściowych
char Buffer_I_COM1[cbInQueue];
char Buffer_O_COM1[cbOutQueue];

DWORD Number_Bytes_Read; // liczba bajtów do czytania
HANDLE hCommDev_1, hCommDev_2; // identyfikatory portów
LPCTSTR lpFileName_1, lpFileName_2;
DCB dcb;
DWORD fdwEvtMask;
COMSTAT Stat;
DWORD Errors;

BOOL bResult_2 = TRUE;
BOOL bResult_1 = TRUE;
BOOL bResult_Save1, bResult_Save2;
int hThread_SR_COM2;
int hThread_SR_COM1;
unsigned uThreadID_SR_COM2;
unsigned uThreadID_SR_COM1;
Cardinal intVar2, intVar1; // liczniki pomiarów

FILE *pstream2; // wskaźnik do pliku
FILE *pstream1; // wskaźnik do pliku
//--------------------------------------------------------------------
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;
}
//--------------------------------------------------------------------
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 COM2--------------------------------------------
void __fastcall TForm1::CloseComm_2Click(TObject *Sender)
{
SuspendThread((HANDLE)hThread_SR_COM2);
StatusBar1->Panels->Items[0]->Text = "Zamknięty port: COM2";
fclose(pstream2);
CloseHandle(hCommDev_2);
MeasureON_2->Enabled = TRUE;
OpenComm_2->Enabled = TRUE;
}
//---------zamknięcie COM1--------------------------------------------
void __fastcall TForm1::CloseComm_1Click(TObject *Sender)
{
SuspendThread((HANDLE)hThread_SR_COM1);
StatusBar1->Panels->Items[3]->Text = "Zamknięty port: COM1";
fclose(pstream1);
CloseHandle(hCommDev_1);
MeasureON_1->Enabled = TRUE;
OpenComm_1->Enabled = TRUE;
}
//--------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
SaveDialog1->InitialDir = ExtractFilePath(ParamStr(0));
SaveDialog1->Filter = "Data files (*.dat)|*.dat|All files" " (*.*)|*.*";
SaveDialog2->InitialDir = ExtractFilePath(ParamStr(0));
SaveDialog2->Filter = "Data files (*.dat)|*.dat|All files" " (*.*)|*.*";
TBorderIcons temporaryBI = BorderIcons;
temporaryBI >> biMaximize;
BorderIcons = temporaryBI;
OpenComm_1->Enabled = TRUE;
OpenComm_2->Enabled = TRUE;
TrackBar2->Position = 1000;
TrackBar1->Position = 1000;
TrackBar2->Max = 5000;
TrackBar1->Max = 5000;
TrackBar2->Min = 100;
TrackBar1->Min = 200;
TrackBar2->Frequency = 500;
TrackBar1->Frequency = 500;
bResult_Save2 = FALSE;
bResult_Save1 = FALSE;
intVar1 = 0;
intVar2 = 0;
}
//--otwarcie portu COM2-----------------------------------------------
void __fastcall TForm1::OpenComm_2Click(TObject *Sender)
{
int i;
if (CheckBox2->Checked == TRUE)
lpFileName_2 = "COM2";

hCommDev_2 = CreateFile(lpFileName_2, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev_2 != INVALID_HANDLE_VALUE)
{
SetupComm(hCommDev_2, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev_2, &dcb);
if (CheckBox3->Checked == TRUE)
dcb.BaudRate = CBR_300;
if (CheckBox4->Checked == TRUE)
dcb.BaudRate = CBR_1200;
if (CheckBox5->Checked == TRUE)
dcb.BaudRate = CBR_9600;
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_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
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_2, &dcb);
GetCommMask(hCommDev_2, &fdwEvtMask);
SetCommMask(hCommDev_2, EV_TXEMPTY);
StatusBar1->Panels->Items[0]->Text = "Otwarty port: COM2";
strcpy(Buffer_O_COM2, query_IDN);
do { //-- wysyłanie zapytania
//Beep();
FlushFileBuffers(hCommDev_2);
} while (Write_Comm(hCommDev_2, Buffer_O_COM2, strlen(Buffer_O_COM2)) == 0);
Sleep(1000);
Read_Comm(hCommDev_2, &Buffer_I_COM2[0], &Number_Bytes_Read, sizeof(Buffer_I_COM2));
if (Number_Bytes_Read > 0)
StatusBar1->Panels->Items[1]->Text = &Buffer_I_COM2[0];
for (i = 0; i <= cbInQueue - 1; i++)
{ Buffer_O_COM2[i] = NULL;
Buffer_I_COM2[i] = NULL;
};
if (Application->MessageBox(" Zapisać dane odbierane z portu" " szeregowego COM2 do pliku? " , "Uwaga!", MB_OKCANCEL) != IDOK)
{
Abort();
}
else
{
if (SaveDialog2->Execute())
{
bResult_Save2 = TRUE;
pstream2 = fopen(SaveDialog2->FileName.c_str(), "w+");
}
}
}
else
{
switch ((int)hCommDev_2)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
break;
};
}
}
//-------otwarcie portu COM1------------------------------------------
void __fastcall TForm1::OpenComm_1Click(TObject *Sender)
{
int i;
if (CheckBox1->Checked == TRUE)
lpFileName_1 = "COM1";

hCommDev_1 = CreateFile(lpFileName_1, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev_1 != INVALID_HANDLE_VALUE)
{
SetupComm(hCommDev_1, cbInQueue, cbOutQueue);
dcb.DCBlength = sizeof(dcb);
GetCommState(hCommDev_1, &dcb);
if (CheckBox6->Checked == TRUE)
dcb.BaudRate = CBR_4800;
if (CheckBox7->Checked == TRUE)
dcb.BaudRate = CBR_9600;
dcb.Parity = NOPARITY; // ustawienie parzystości
dcb.StopBits = ONESTOPBIT; // bity stopu
dcb.ByteSize = 8; // bity danych
//-przykładowe ustawienia znaczników sterujących DCB-
dcb.fParity = TRUE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
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_1, &dcb);
GetCommMask(hCommDev_1, &fdwEvtMask);
SetCommMask(hCommDev_1, EV_TXEMPTY);
StatusBar1->Panels->Items[3]->Text = "Otwarty port: COM1";
StatusBar1->Panels->Items[4]->Text = "Waga laboratoryjna WPS" " 72";
for (i = 0; i <= cbInQueue - 1; i++)
{
Buffer_O_COM1[i] = NULL;
Buffer_I_COM1[i] = NULL;
}
if (Application->MessageBox(" Zapisać dane odbierane z portu"
" szeregowego COM1 do pliku? " , "Uwaga!", MB_OKCANCEL) != IDOK)
{
Abort();
}
else
{
if (SaveDialog1->Execute())
{
bResult_Save1 = TRUE;
pstream1 = fopen(SaveDialog1->FileName.c_str(), "w+");
}
}
}
else
{
switch ((int)hCommDev_1)
{
case IE_BADID:
MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest" " aktywny.", "Błąd", MB_OK);
break;
};
}
}

//--wysłanie zapytania i odbiór danych przez COM2---------------------
int __fastcall RS_Send_Receive_COM2(Pointer Parameter)
{
do {
strcpy(Buffer_O_COM2, query);
do { //-- wysyłanie zapytania
//Beep();
FlushFileBuffers(hCommDev_2);
} while (Write_Comm(hCommDev_2, Buffer_O_COM2, strlen(Buffer_O_COM2)) == 0);
Sleep(Form1->TrackBar2->Position);
//-- odbiór danych
Read_Comm(hCommDev_2, &Buffer_I_COM2[0], &Number_Bytes_Read, sizeof(Buffer_I_COM2));
if (Number_Bytes_Read > 0)
{
Form1->RichEdit2->Text = IntToStr(intVar2++);
Form1->RichEdit1->Text = Buffer_I_COM2;
if (bResult_Save2 == TRUE) fprintf(pstream2, "%s %s\n", Form1->RichEdit2->Text, Form1->RichEdit1->Text);
}
else
{
Beep();
Form1->RichEdit1->Text = "0x"; // błędna wartość pomiaru
}
} while (bResult_2); // koniec nadrzędnego DO
return TRUE;
}
//------synchronizacja COM2-------------------------------------------
void __fastcall TForm1::TrackBar2Change(TObject *Sender)
{
Edit1->Text = IntToStr(TrackBar2->Position); // sterowanie
// opóźnieniem
}
//----pomiar COM2-----------------------------------------------------
void __fastcall TForm1::MeasureON_2Click(TObject *Sender)
{
if (hCommDev_2 > 0) // powtórnie sprawdza czy port jest otwarty
{
OpenComm_2->Enabled = FALSE;

hThread_SR_COM2 = BeginThread (NULL, 0, RS_Send_Receive_COM2, NULL, 0, uThreadID_SR_COM2);
MeasureON_2->Enabled = FALSE;
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//------wznowienie pomiaru COM2---------------------------------------
void __fastcall TForm1::MeasureResume_2Click(TObject *Sender)
{
ResumeThread((HANDLE)hThread_SR_COM2);
}
//------wstrzymanie pomiaru COM2--------------------------------------
void __fastcall TForm1::MeasureSuspend_2Click(TObject *Sender)
{
MeasureON_2->Enabled = FALSE;
SuspendThread((HANDLE)hThread_SR_COM2);
}
//--wysłanie zapytania i odbiór danych przez COM1---------------------
int __fastcall RS_Send_Receive_COM1(Pointer Parameter)
{
do {
strcpy(Buffer_O_COM1, query_weight);
do { //-- wysyłanie zapytania
//Beep();
FlushFileBuffers(hCommDev_1);
} while (Write_Comm(hCommDev_1, Buffer_O_COM1, strlen(Buffer_O_COM1)) == 0);
Sleep(Form1->TrackBar1->Position);
//-- odbiór danych
Read_Comm(hCommDev_1, &Buffer_I_COM1[0], &Number_Bytes_Read, sizeof(Buffer_I_COM1));
if (Number_Bytes_Read > 0)
{
Form1->RichEdit3->Text = IntToStr(intVar1++);
Form1->RichEdit4->Text = Buffer_I_COM1;
if (bResult_Save1 == TRUE)
fprintf(pstream1, "%s %s\n", Form1->RichEdit3->Text, Form1->RichEdit4->Text);
}
else
{
Beep();
Form1->RichEdit4->Text = "0x"; // błędna wartość pomiaru
}

} while (bResult_1); // koniec nadrzędnego DO
return TRUE;
}
//------synchronizacja COM1-------------------------------------------
void __fastcall TForm1::TrackBar1Change(TObject *Sender)
{
Edit2->Text = IntToStr(TrackBar1->Position); // sterowanie
// opóźnieniem
}
//---------pomiar COM1------------------------------------------------
void __fastcall TForm1::MeasureON_1Click(TObject *Sender)
{
int i;
if (hCommDev_1 > 0) // powtórnie sprawdza, czy port jest otwarty
{
for (i = 0; i <= cbInQueue - 1; i++)
{
Buffer_O_COM1[i] = NULL;
Buffer_I_COM1[i] = NULL;
}
OpenComm_1->Enabled = FALSE;
hThread_SR_COM1 = BeginThread (NULL, 0, RS_Send_Receive_COM1, NULL, 0, uThreadID_SR_COM1);
MeasureResume_1->Enabled = TRUE;
MeasureSuspend_1->Enabled = TRUE;
MeasureON_1->Enabled = FALSE;
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//----tarowanie wagi--------------------------------------------------
void __fastcall TForm1::TareClick(TObject *Sender)
{
int i;
if (hCommDev_1 > 0) // powtórnie sprawdza, czy port jest otwarty
{
SuspendThread((HANDLE)hThread_SR_COM1);
strcpy(Buffer_O_COM1, command_TARE);
do {
FlushFileBuffers(hCommDev_1);
} while (Write_Comm(hCommDev_1, Buffer_O_COM1, strlen(Buffer_O_COM1)) == 0);
Read_Comm(hCommDev_1, &Buffer_I_COM1[0], &Number_Bytes_Read, sizeof(Buffer_I_COM1));
if (Number_Bytes_Read > 0)
Form1->RichEdit4->Text = Buffer_I_COM1;
for (i = 0; i <= cbInQueue - 1; i++)
{
Buffer_O_COM1[i] = NULL;
Buffer_I_COM1[i] = NULL;
}
MeasureResume_1->Enabled = FALSE;
MeasureSuspend_1->Enabled = FALSE;
MeasureON_1->Enabled = TRUE;
}
else
MessageBox(NULL, "Port nie został otwarty do transmisji.", "Błąd", MB_OK);
}
//------wstrzymanie pomiaru COM1--------------------------------------
void __fastcall TForm1::MeasureSuspend_1Click(TObject *Sender)
{
SuspendThread((HANDLE)hThread_SR_COM1);
}
//-----wznowienie pomiaru COM1----------------------------------------
void __fastcall TForm1::MeasureResume_1Click(TObject *Sender)
{
ResumeThread((HANDLE)hThread_SR_COM1);
}
//------zakończenie działania aplikacji-------------------------------
void __fastcall TForm1::EndApplicationClick(TObject *Sender)
{
switch(MessageBox(NULL, " Działanie aplikacji zostanie" " zakończone.", "Uwaga!", MB_YESNOCANCEL | MB_ICONQUESTION))
{
case ID_YES :
{
SuspendThread((HANDLE)hThread_SR_COM2);
SuspendThread((HANDLE)hThread_SR_COM1);
fclose(pstream2);
fclose(pstream1);
CloseHandle(hCommDev_2);
CloseHandle(hCommDev_1);
Application->Terminate();
}
case ID_CANCEL : Abort();
}
}
//--------------------------------------------------------------------
W przedstawionym przykładzie powróciliśmy do funkcji Write_Comm() oraz Read_Comm(), dla których wskaźnik do bufora danych, czyli LPVOID lpBuffer był jednym z parametrów formalnych. Jest chyba rzeczą oczywistą, że bez takiego zabiegu mielibyśmy spore trudności z jednoczesnym wykorzystaniem tych dwóch uniwersalnych funkcji przy obsłudze dwóch niezależnych przyrządów pomiarowych. Tego rodzaju algorytmy możemy z powodzeniem stosować przy projektowaniu aplikacji obsługujących jednocześnie niewiele urządzeń. Jeżeli ktoś zechciałby skorzystać ze specjalnych kart rozszerzających lub wspomnianych w rozdziale 2. konwerterów, dających dostęp do większej liczby RS-ów, opisany algorytm siłą rzeczy może nie tyle się skomplikuje, ile poważnie wydłuży. Stanie się to głównie za sprawą konieczności każdorazowej pełnej inicjalizacji wybranego portu szeregowego w funkcji obsługi odrębnego zdarzenia. Już na wyżej przedstawionym prostym przykładzie inicjalizacji dwóch portów można było zauważyć, że te same funkcje, co prawda w różnych kontekstach, ale wywoływane były wielokrotnie. Co się stanie, gdy będziemy chcieli użyć powiedzmy 16 portów jednocześnie, które w dodatku będą pracować ze z góry zadanymi różnymi prędkościami i przy różnej długości słowa danych? Jeżeli nasza aplikacja ma być naprawdę przyjazna Użytkownikowi, nie unikniemy oczywiście umieszczenia na formularzu owych 16., odpowiednio nazwanych przycisków (lub innych komponentów). Jednak funkcja obsługi zdarzenia dla każdego z nich może być ta sama, wywoływana jedynie z odpowiednimi parametrami aktualnymi. W pierwszym przybliżeniu będą nimi na pewno: numer portu, szybkość transmisji, rodzaj parzystości, liczba bitów stopu oraz liczba bitów danych. Przykład tak skonstruowanej, bardzo uniwersalnej funkcji OpenSerialPort(), która oczywiście będzie typu HANDLE, wraz z jej przykładowymi wywołaniami zamieściłem w poniższym fragmencie kodu
Szkielet uniwersalnej funkcji otwierającej i ustalającej parametry transmisji wybranego portu szeregowego wraz z jej wywołaniami z przykładowymi parametrami aktualnymi
...
HANDLE hCommDev_1, hCommDev_2; // identyfikatory portów
...
HANDLE OpenSerialPort (DWORD NumPort, DWORD BaudRate, DWORD Parity, DWORD StopBits, DWORD ByteSize)
{
HANDLE hCommDev;
char CommName[5];
DCB dcb;

switch (NumPort) {
case 1:
strcpy (CommName, "COM1");
break;
case 2:
strcpy (CommName, "COM2");
break;
case 3:
strcpy (CommName, "COM3");
break;
case 4:
strcpy (CommName, "COM4");
break;
...
default:
return FALSE;
}

switch (BaudRate) {
case 110:
dcb.BaudRate = CBR_110;
break;
case 300:
dcb.BaudRate = CBR_300;
break;
case 600:
dcb.BaudRate = CBR_600;
break;
case 1200:
dcb.BaudRate = CBR_1200;
break;
case 2400:
dcb.BaudRate = CBR_2400;
break;
case 4800:
dcb.BaudRate = CBR_4800;
break;
case 9600:
dcb.BaudRate = CBR_9600;
break;
case 19200:
dcb.BaudRate = CBR_19200;
break;
case 38400:
dcb.BaudRate = CBR_38400;
break;
case 57600:
dcb.BaudRate = CBR_57600;
break;
case 115200:
dcb.BaudRate = CBR_115200;
break;
case 128000:
dcb.BaudRate = CBR_128000;
break;
case 256000:
dcb.BaudRate = CBR_256000;
break;
...
default:
return FALSE;
}

hCommDev = CreateFile (CommName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hCommDev != INVALID_HANDLE_VALUE)
{
SetupComm(hCommDev, cbInQueue, cbOutQueue);
GetCommState(hCommDev, &dcb);
if (Parity == ODDPARITY)
dcb.Parity = ODDPARITY;
if (Parity == NOPARITY)
dcb.Parity = NOPARITY;
...
if (StopBits == ONESTOPBIT)
dcb.StopBits = ONESTOPBIT;
if (StopBits == TWOSTOPBITS)
dcb.StopBits = TWOSTOPBITS;
...

if (ByteSize == 7)
dcb.ByteSize = 7;
if (ByteSize == 8)
dcb.ByteSize = 8;
...
dcb.fParity = TRUE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
...
SetCommState(hCommDev, &dcb);
GetCommMask(hCommDev, &fdwEvtMask);
SetCommMask(hCommDev, EV_TXEMPTY);
return hCommDev;
}
else
return FALSE;
}
//-------otwarcie portu np. COM2--------------------------------------
void __fastcall TForm1::OpenComm_2Click(TObject *Sender)
{
hCommDev_2 = OpenSerialPort (2, 1200, ODDPARITY, ONESTOPBIT, 7);
if (hCommDev_2 == 0) MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest aktywny" " lub niewłaściwie ustalono parametry transmisji.", "Błąd", MB_OK);
}
//-------otwarcie portu np. COM1--------------------------------------
void __fastcall TForm1::OpenComm_1Click(TObject *Sender)
{
hCommDev_1 = OpenSerialPort (1, 4800, NOPARITY, ONESTOPBIT, 8);
if (hCommDev_1 == 0) MessageBox(NULL, "Niewłaściwa nazwa portu lub port jest aktywny" " lub niewłaściwie ustalono parametry transmisji.", "Błąd", MB_OK);

}
//--------------------------------------------------------------------
Każdorazowe wywołanie OpenSerialPort()w kontekście odpowiedniego zdarzenia jest już rzeczą bardzo prostą. Jeżeli ktoś zechce przetestować pokazany fragment kodu, zauważy też, że wprowadzona konstrukcja funkcji otwierającej wybrany port szeregowy oraz inicjalizującej jego parametry transmisji jest bardzo czuła na próby błędnego przypisania szybkości transmisji, bitów stopu czy bitów danych. Na własne potrzeby listę parametrów opisanej funkcji można oczywiście znacznie rozszerzyć również o rodzaje kontroli transmisji. Na zakończenie powróćmy jeszcze na chwilę do aplikacji obsługujących wagę cyfrową. W przeciwieństwie do różnego rodzaju bardziej lub mniej wyszukanych przyrządów pomiarowych jest to urządzenie, z którym możemy spotkać się na co dzień, np. przy okazji wizyty w każdym dużym sklepie. Być może ktoś zastanawiał się, jak w dużej placówce handlowej może być zorganizowana kontrola sprzedaży artykułów, które należy uprzednio zważyć. Prawdopodobnie musi być do tego celu zaangażowany jakiś komputerowy system zbierania danych (oczywiście nie musi być on oparty o RS). Korzystając z tego, co już wiemy na temat sposobów realizacji transmisji szeregowej, każdy z nas taki prosty system będzie mógł samodzielnie zbudować. Powiem więcej, najważniejsze programy, które mogą być nam pomocne, mamy już opracowane. Korzystając z tej części projektu p_RS_10.bpr, który obsługuje wagę cyfrową, wykonałem cykl być może nieco zabawnych ważeń - w jakimś okresie ważyłem na przemian końcówki DB-25 oraz DB-9. Wyniki moich pomiarów były nieustannie zapisywane w trakcie doświadczenia na dysku w oddzielnym pliku, w formacie: numer pomiaru - odczyt wskazań wagi w gramach. Po każdorazowym zważeniu danej końcówki waga była tarowana specjalnym przyciskiem, który urządzenia tego typu muszą posiadać na płycie czołowej. Wykonawszy kilku takich prób (przyznam, że sprzedawcy w sklepach muszą być bardzo cierpliwi), używając jednego z ogólnie dostępnych programów graficznych, obejrzałem nasz wykres. Jest on pokazany na rysunku 8.3 . Ponieważ znałem stałą czasową pomiaru, czyli przedział czasu próbkowania łącza, z dobrą dokładnością na podstawie tak otrzymanego wykresu mogłem oszacować, w jakim czasie następowało dane ważenie. Specjaliści od marketingu mogą wysnuwać z tego wnioski o częstości odwiedzin przez klientów wybranego stoiska w danym czasie (np. w ciągu określonej pory dnia), zakładając rzecz jasna, że żaden sprzedawca nie waży towaru z braku lepszego zajęcia. Oczywiście, w prawdziwym sklepie nie wystarczy jedynie coś zważyć, należy ponadto wprowadzić odpowiedni kod produktu itp. Nam jednak chodzi głównie o ideę tego procesu. Równie ważną rzeczą jest fakt, że posługując się tego typu wykresami, mamy pełną kontrolę nad ilością sprzedanego towaru. Przy końcowym rozliczeniu wagi czy kasy fiskalnej wystarczy zsumować wartości odpowiednich maksimów i odjąć od znanej wartości (w tym przypadku wagi) towaru wyłożonego do sprzedaży przed otwarciem sklepu lub stoiska. Przedstawione przykłady wykorzystania programów zbierających dane z wagi cyfrowej oraz ich analiza zostały oczywiście wykonane w dużym uproszczeniu. Tak naprawdę nigdy żaden sprzedawca tak szybko nie waży, również zamiar ważenia jest z reguły w jakiś sposób wcześniej sygnalizowany, co wbrew pozorom jeszcze bardziej upraszcza cały problem związany z pełną rozróżnialnością produktów. Ponadto, do pełnej analizy takich wykresów niezbędne są dosyć skomplikowane algorytmy matematyczne, dzięki którym otrzymuje się jeszcze bardzo wiele innych, cennych informacji wykorzystywanych przez specjalistów. Mając do dyspozycji program podobny do tego, który został przedstawiony w niniejszym podrozdziale, bez problemu można do jednego PC podłączyć większą liczbę urządzeń z możliwością nieustannego zapamiętywania ich wskazań. Przy budowie tego rodzaju systemów zbierania danych opartych na RS z reguły wykorzystuje się opisane wcześniej konwertery sygnałów łącza szeregowego (stoiska handlowe bywają czasami bardzo rozległe). Nie wpływa to jednak na fakt, iż zasady obsługi protokołu transmisji danych poprzez łącze szeregowe pozostają te same i nie powinny stanowić już dla nas tajemnicy.