Książka adresowana jest do osób piszących swe aplikacje zarówno w Delphi jak i C++Builderze oraz pragnących zapoznać si...
284 downloads
1358 Views
3MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Książka adresowana jest do osób piszących swe aplikacje zarówno w Delphi jak i C++Builderze oraz pragnących zapoznać się ze sposobami realizacji transmisji szeregowej przy użyciu narzędzi oferowanych przez Win32 API. Zawiera ona szereg cennych wskazówek dotyczących metod konstruowania algorytmów, pomocnych w oprogramowaniu różnego rodzaju urządzeń, z którymi możliwa jest komunikacja poprzez interfejs RS 232C. W książce można znaleźć: •
opis metod realizacji nowoczesnej transmisji asynchronicznej poprzez interfejs RS 232C,
•
opis wszystkich niezbędnych funkcji oraz struktur najczęściej wykorzystywanych do realizacji transmisji szeregowej, oferowanych przez Win32 API,
•
metody realizacji transmisji buforowanej oraz nie buforowanej,
•
kompletne przykłady oryginalnych algorytmów stosowanych przy obsłudze łącza szeregowego,
•
przykłady oprogramowania konkretnych urządzeń posługujących się uniwersalnym językiem zapytań i wykorzystywanych w systemach pomiarowych opartych na RS 232C.
Dołączony do książki CD — ROM zawiera kody źródłowe oraz projekty prezentowanych w książce przykładowych aplikacji.
1
Spis treści Od autora Wprowadzenie Rozdział 1 Definicja interfejsu Rozdział 2 Nowoczesna transmisja asynchroniczna oraz standard RS 232C RTS — CTS handshaking Konwertery interfejsu RS 232C Protokół XON-XOFF Protokół ENQ-ACK Rola oprogramowania a podstawowe funkcje interfejsu Podsumowanie
Rozdział 3 Jak testować programy do transmisji szeregowej Mirror w MS DOS Terminal dla Windows 3.x oraz 9x Podsumowanie
2
Rozdział 4 RS 232C w MS DOS Borland C++ Borland Pascal Podsumowanie Ćwiczenia
Rozdział 5 Programowa obsługa interfejsu RS 232C w Windows Wykorzystanie elementów Win32 API w C++Builder. Część I Testowanie portu szeregowego Nawiązanie połączenia Segment inicjalizująco–konfiguracyjny Segment wysyłający komunikaty Segment odbierający komunikaty Przykładowa aplikacja Podsumowanie Ćwiczenia Wykorzystanie elementów Win32 API w C++Builder. Część II Wysyłamy znak po znaku Wysyłamy pliki Wykorzystanie komponentu TTimer Aplikacja nie lubi milczeć Podsumowanie Ćwiczenia Wykorzystanie elementów Win32 API w Delphi. Część I Testowanie portu szeregowego — inaczej Nawiązanie połączenia Segment wysyłający komunikaty Segment odbierający komunikaty
3
Przykładowe aplikacje Podsumowanie Ćwiczenia Wykorzystanie elementów Win32 API w Delphi. Część II Wysyłamy znak po znaku Wysyłamy pliki Timer w Delphi Podsumowanie Ćwiczenia
Rozdział 6 Aplikacje wielowątkowe Najważniejszy jest Użytkownik Użytkownik steruje programem Możliwość anulowania decyzji Możliwość odbioru komunikatu nawet w trakcie wysyłania danych Możliwość wysłania odrębnej informacji w trakcie transmisji pliku Delphi Konkurencja dla Timera C++Builder Zamiast Timera Podsumowanie
Rozdział 7 Wykorzystanie niektórych narzędzi graficznych Komponent TChart Komponent TPaintBox Komponent TImage Samodzielne tworzenie mapy bitowej Podsumowanie
4
Rozdział 8 Przykładowe aplikacje wykorzystywane w systemach pomiarowych Kontroler temperatury Aplikacja obsługująca kilka urządzeń Podsumowanie
Uzupełnienie 1 Uzupełnienie 2 Uzupełnienie 3 Skorowidz
5
Podziękowania Pragnę wyrazić podziękowanie firmie Lake Shore Cryotronics, Inc. za wyrażenie zgody na wykorzystanie do celów niniejszej książki znakomitych urządzeń pomiarowych. Osobne podziękowanie pragnę złożyć na ręce Pana Zbigniewa Joachimaka, przedstawiciela Lake Shore w Polsce. Dziękuję również firmie RADWAG za wyrażenie zgody na wykorzystanie doskonałej wagi na potrzeby tego opracowania. Panu mgrowi inż. Witoldowi Jureczko pragnę wyrazić swą wdzięczność za udostępnienie zebranych przez niego materiałów, które okazały się bardzo pomocne w redagowaniu jednego z rozdziałów książki.
Autor
6
Od Autora Wśród bardzo dostępnych publikacji na temat zastosowań współczesnych języków programowania brakuje przystępnego opisu wykorzystania ich w programach komunikacyjnych. Jest to szczególnie widoczne w odniesieniu do dwu obecnie najpopularniejszych środowisk programistycznych: Delphi oraz C++Buildera. Bardzo często słyszy się opinie, że można zbudować poprawnie działającą aplikację komunikacyjną bez konieczności odwoływania się do właściwości systemu operacyjnego. Czynność tę powinien za programistę automatycznie wykonywać BIOS lub sam system operacyjny. W DOS sytuacja ta jest zupełnie możliwa. Pewna znajomość Pascala czy C++ powinna w zupełności wystarczyć do zbudowania dobrze działającego programu komunikacyjnego. Stosowanie takiego podejścia w Windows jest nie do zaakceptowania. Niekiedy można przeczytać, że Delphi czy Builder same w sobie są już generatorami aplikacji, tzw. RAD (ang. Rapid Application Development). Być może jest to prawda, ale na pewno nie w odniesieniu do komunikacji komputerowej. Bardzo chciałbym, aby książka ta przekonała Czytelnika, że najlepsze efekty może dać tylko umiejętne połączenie właściwości sytemu operacyjnego z możliwościami oferowanymi przez nasz ulubiony kompilator.
7
Wprowadzenie W niniejszej książce podjęto próbę przybliżenia Czytelnikom zasad programowej obsługi transmisji szeregowej. Jest tu wszystko, co niezbędne do zaprojektowania i stworzenia prawdziwych programów obsługujących łącze szeregowe. Chociaż tytuł może wskazywać na typowo sprzętowy — hardware’owy jej charakter opracowania, jednak książka ta jest przeznaczona nie tylko dla stosunkowo wąskiego kręgu osób zajmujących się sterowaniem i komunikacją komputerową. Postaramy się przedstawić tutaj nowe spojrzenie na pewien fragment czegoś, co klasycznie określane jest mianem szeroko rozumianego hardware’u. Zamierzeniem autora jest, aby niniejsze opracowanie w jak najmniejszym stopniu dotyczyło ogólnie znanych wątków łącza szeregowego. Na problem transmisji szeregowej spojrzymy z o wiele szerszej perspektywy, z perspektywy inżynierii oprogramowania. W ciągu ostatniej dekady w tej dziedzinie informatyki nastąpił olbrzymi postęp. Już nie musimy się martwić, z jakich części i w jaki sposób zbudować określony przyrząd. Nie jest też problemem wyprowadzanie sygnałów sterujących z jakiś egzotycznych układów pomiarowych — wszystkie takie przyrządy zostały już dawno poddane standaryzacji. Tak naprawdę ważne dzisiaj jest to, w jaki sposób oprogramować konkretne urządzenie dostępne na rynku. Postaramy się odpowiedzieć na to pytanie, poznając zasady pisania takich algorytmów zarówno w Pascalu jak i w C++ oraz Delphi i C++Builderze. Takie ujęcie tematu powinno spowodować, że w książce tej znajdą coś dla siebie zarówno osoby piszące w Pascalu (Delphi) jak i osoby zainteresowane C++ (Builderem). Problem programowej obsługi łącza szeregowego w MS DOS ujmiemy na tyle, na ile jest on niezbędny dla zrozumienia ogólnych zasad tworzenia tego typu algorytmów. Chociaż jest to niewątpliwie bardzo ciekawe i kształcące zajęcie, to jednak programy tego typu nie są już perspektywiczne. Tak naprawdę będzie nas interesować, jak tworzyć prawdziwe i dobrze działające aplikacje dla Windows. Jednak w tym przypadku będziemy zmuszeni zastosować prawdziwą technologię oprogramowania, niezbędnym okaże się poznanie interfejsu programisty, który dostarcza nam Windows. Książka ta jest nie tylko prezentacją typów danych, funkcji czy struktur oferowanych nam przez system operacyjny, ale przede wszystkim zawiera dużo wskazówek dla programistów w postaci szczegółowo przedstawionych przykładowych aplikacji. Chociaż szczególnie dokładnie omówimy standard RS 232C, to nie zabraknie też informacji, w jaki sposób z poziomu Delphi czy C++Buildera przejść do obsługi innych wersji łącza szeregowego. Przedstawione wiadomości na temat uniwersalnego API Windows Czytelnik będzie mógł wykorzystać przy rozwiązywaniu wielu innych problemów, niekoniecznie związanych z transmisją danych przez łącze szeregowe. Nie brakuje oczywiście na rynku znakomitych programów, takich jak: TestPoint, LabView, LabWindows czy LabTech. Są to jednak relatywnie drogie produkty, których cena niejednokrotnie przewyższa wartość urządzenia, jakie należy oprogramować. Można oczywiście zamówić konkretną aplikację u producenta, ale to również wiąże się z dodatkowymi i niepotrzebnymi kosztami dla firmy, szkoły czy uczelni. Zresztą, umiejętność samodzielnego pisania programów o różnorodnym przeznaczeniu jest tą cechą, która wyróżnia programistę od innych osób zajmujących się szeroko rozumianą informatyką — informatyk nie zawsze bywa programistą. Jeżeli jednak Czytelnik z jakiś względów będzie musiał nauczyć się kiedyś
8
programów wymienionych na początku niniejszego akapitu, wiadomości nabyte podczas czytania tej książki na pewno zaprocentują i pozwolą łatwiej zrozumieć zasadę ich działania. Książka ta nie jest podręcznikiem w stylu „wstęp do programowania”, zakłada pewną znajomość podstaw Pascala i C++ oraz środowisk Delphi i Buildera. Z całą pewnością natomiast można ją traktować jako wstęp do szeroko rozumianej warstwy komunikacyjnej Windows 9x. Przygotowując niniejszy tekst, korzystałem z Borland Pascala v.7, C++ v. 3.1 w MS DOS oraz Delphi 5 i C++Buildera 5 dla Windows. Chociaż wymienione kompilatory mają szereg nowych i ciekawych właściwości, jednak ich dokładny opis nie był celem tej książki. Starałem się natomiast, aby zamieszczone algorytmy Czytelnik mógł bez większych problemów zaimplementować również w starszych wersjach Delphi czy Buildera.
9
Rozdział 1 Definicja interfejsu W książce tej często 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.
10
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
11
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. Możesz samodzielnie przekonać się, jaką prędkość transmisji szeregowej jest w wstanie zapewnić Twój PC. W tym celu należy uruchomić Panel sterowania, wybrać opcję System, potem Menedżer urządzeń, następnie Porty COM & LPT, dalej np. Port komunikacyjny COM2, potem Ustawienia portu i sprawdzić, jaką prędkość w bitach na sekundę możesz uzyskać. W moim PC mam niezłą płytę główną i mogę uzyskać prawie milion bitów na sekundę! Jak na popularny sprzęt uważam, że nieźle. Oczywiście muszę mieć jeszcze urządzenie zewnętrzne, mogące pracować z taką prędkością, ale to już inna historia. 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łącza1: 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
1 Niekiedy Universal Serial Bus próbuje się dosłownie tłumaczyć, utożsamiając go z uniwersalną szyną danych, co oczywiście nie jest prawdą.
12
Komentarz: Niespójność treści! (proponuję usunięcie tego fragmentu, zresztą bez szkody dla całości)
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.
13
Rozdział 2 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. W tabeli 2.1 przedstawiono używane obecnie w transmisji szeregowej znaki sterujące. W przyszłości będziemy z nich często korzystać.
14
Tabela 2.1. Pierwsze 32 znaki strony kodowej ASCII Liczba
Liczba
Znak
dzieś.
heksad.
0
00
NUL
1
01
☺ SOH
Nazwa angielska
Null
Znaczenie
Znak pusty
Start of Heading
Początek nagłówka zawierającego adres lub polecenie
2
02
☻ STX
Start of Text
Początek tekstu
3
03
♥ ETX
End of Text
Koniec tekstu
4
04
♦ EOT
End of Transmission
Koniec transmisji
5
05
♣ ENQ
Enquiry
Zapytanie
6
06
♠
Acknowledge
Potwierdzenie
7
07
● BEL
Bell
8
08
◘
BS
Backspace
9
09
○
HT
Horizontal Tabulation
Pozioma tabulacja
10
0A
◙
LF
Line Feed
Przejście kursora do
ACK
Dzwonek Usuwanie poprzedniego znaku
następnego wiersza 11
0B
♂
VT
Vertical Tabulation
12
0C
♀
FF
Form Feed, page eject
13
0D
♪
CR
Carriage Return
Pionowa tabulacja Przesuw strony Powrót kursora do początkowej pozycji w tym samym wierszu
14
0E
♫ SO
Shift Out alternate character set
15
0F
☼ SI
Shift In, resume default character set
16
10
► DLE
Data Link Escape
Następne znaki nie będą interpretowane jako znaki ASCII Powrót do domyślnej strony kodowej ACSII Zmiana znaczenia następnego znaku, który będzie interpretowany jako kombinacja bitów sterujących danym urządzeniem
17
11
◄ DC1
Device Control 1
XON kontrola przepływu danych
15
18
12
↕
DC2
Device Control 2
19
13
‼
DC3
Device Control 3
Sterowanie urządzeniem XOFF kontrola przepływu danych
20
14
¶
DC4
Device Control 4
Sterowanie urządzeniem
21
15
§
NAK
Negative Acknowledge
Meldunek błędu
22
16
▬ SYN
Synchronous Idle
Znak synchronizacyjny
23
17
↕_ ETB
End of Transmission Block
Koniec transmisji bloku danych
24
18
↑ CAN
Cancel
Anulowanie danych
25
19
↓ EM
End of Medium
Koniec zapisu danych
26
1A
→ SUB
Substitue Character
Zastąpienie znaku, znak wstawiony w odbiorniku w miejsce błędnie odebranego, np. z błędem parzystości
27
1B
← ESC
Escape
28
1C
∟ FS
File Separator
Rezygnacja bez potwierdzenia Separator plików
29
1D
↔ GS
Group Separator
Separator grup
30
1E
▲ RS
Record Separator
Separator zapisu
31
1F
▼ US
Unit Separator
Separator jednostek
127
7F
⌂ DEL
Delete
Unieważnienie znaku
...
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. Czytelników zainteresowanych budową i możliwością programowania takich układów odsyłam do znakomitej książki Piotra Metzgera Anatomia PC, wyd. Helion (1996).
16
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). Na rysunku 2.1. pokazano przebieg czasowy przykładowej ramki, symbolizującej wysłanie jednej litery ”a” reprezentowanej na ośmiu bitach — dziesiętnie 97, binarnie 01100001. Bit parzystości został ustawiony jako markparity, zastosowano też dwa bity stopu. Rysunek 2.1. Czasowy przebieg ramki na linii przesyłania danych przy wysłaniu litery „a”
Łą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,
17
dając amplitudę sygnałów ±8V. W tej sytuacji bitom 0 oraz 1 transmitowanego bajta odpowiadają napięcia odpowiednio +12V oraz –12V2. 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. Na rysunku 2.2 przedstawiono wygląd obydwu rodzajów tych złączy. Rysunek 2.2. Końcówki DB-9 i DB-25. Ciemne kółka oznaczają nieaktywne wyprowadzenia
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.
2 Zgodnie z zaleceniami protokołu V.28 CCITT (Międzynarodowy Komitet Doradczy ds. Telefonii i Telegrafii) logicznemu zeru powinien odpowiadać potencjał dodatni +3V...+15V, zaś logicznej jedynce potencjał ujemny –3V... – 15V.
18
Tabela 2.2. Wykaz sygnałów wykorzystywanych w RS 232C DB-25
DB-9
Opis sygnału
Kierunek sygnału
1
—
PG Protective Ground — masa ochronna
—
2
3
TxD Transmitted Data — dane wysyłane
Wyjście DTE (PC)
3
2
RxD Received Data — dane odbierane
Wejście DTE
4
7
RTS Request To Send — żądanie nadawania. PC zgłasza do urządzenia gotowość odbioru danych
Wyjście DTE
5
8
CTS Clear To Send — gotowość do wysyłania danych. Urządzenie potwierdza przyjęcie sygnału RTS
Wejście DTE
6
6
DSR Data Set Ready — odbiornik gotowy do odbioru danych wysłanych przez komputer
Wejście DTE
7
5
SG Signal Ground — masa sygnałowa
8
1
(RLSD) DCD Data Carier Detect — odbiór fali nośnej. Linia wykorzystywana głównie przez modemy
Wejście DTE
20
4
DTR Data Terminal Ready — gotowość komputera do odbierania-wysyłania danych.
Wyjście DTE
22
9
RI Ring Indicator — wskaźnik wywołania. Linia wykorzystywana głównie przez modemy
Wejście DTE
—
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 Jak już wspomniałem, zamierzeniem moim jest, aby książka ta jak najmniej dotyczyła wątków historycznych łącza szeregowego. Pełny opis funkcji linii magistrali interfejsu RS 232C Czytelnik może znaleźć w bogatej literaturze przedmiotu. Nie będziemy się również szczegółowo zajmować poszczególnymi trybami transmisji szeregowej. Dla nas tak naprawdę istotnym będzie
19
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. Ideę takiej konwersacji przedstawimy na przykładzie połączenia pomiędzy dwoma złączami DB-9. Rysunek 2.3. Przykład połączenia poprzez złącza DB-9 komputera z urządzeniem pracującym w pełnym trybie półdupleksowym
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. Patrząc na rysunek 2.3 ktoś
20
mógłby zauważyć, że 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 arytmetycznologiczne i tak na dobrą sprawę są już same w sobie komputerami. Innych produktów nie spotyka się obecnie zbyt często na rynku. Jeżeli w instrukcji obsługi kupionego urządzenia nie znajdziesz schematu podobnego do tego z rys. 2.3 oznaczać to będzie, że sprzedawca zrobił kolejny dobry interes, pozbywając się magazynowych zapasów. Rysunek 2.4 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 jak na rysunku 2.3 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. Rysunek 2.4. Pełna sekwencja stanów linii interfejsu RS 232C
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 rysunku 2.5 pokazano wygląd takiego złącza, w tabeli zaś 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.
21
Rysunek 2.5. Złącze modułowe RJ-11
Tabela 2.3. Specyfikacja linii sygnałowych złącza RJ-11 wg LakeShore RJ-11
Opis sygnału
1
RxD Received Data — dane odbierane
2
RxD Received Data — dane odbierane
3
Gnd Ground — masa sygnałowa
4
Gnd Ground — masa sygnałowa
5
TxD Transmitted Data — dane wysyłane
6
TxD Transmitted Data — dane wysyłane
22
Rysunek 2. 6. Przykład nowoczesnego układu połączeń złączy DB-9 oraz RJ-11 stosowanego przez LakeShore
Rysunek 2. 7. Przykład nowoczesnego układu połączeń złączy DB-25 oraz RJ-11 stosowanego przez LakeShore
23
Nieco inną specyfikację sygnałów w złączu RJ-11 podają ComputOne oraz SPECIALIX. Tabela 2.4. Specyfikacja linii sygnałowych złącza RJ-11 wg niektórych producentów RJ-11
ComputOne
SPECIALIX
1
DTR
RLSD (DCD)
2
TxD
RxD
3
Gnd
DTR / RTS
4
RLSD (DCD)
Gnd
5
RxD
TxD
6
RTS
CTS
Wielu producentów stosuje również złącza modułowe RJ-45. Poniżej zostały przedstawione niektóre stosowane rozwiązania. Rysunek 2.8. Złącze modułowe RJ-45
Tabela 2.5. Specyfikacja linii sygnałowych złącza RJ-45 wg niektórych producentów RJ-45
ComputOne
Chase
DIGIBOARD
MICRO-
MICRO-
ANEX1
ANEX2
1
—
DCD
DSR
RTS
—
RTS
CTS2
2
RTS
RTS
RTS
DTR
DTR
DTR
DTR2
3
RxD
PG1
PG1
TxD
TxD
RxD
TxD2
4
DCD
TxD
TxD
DCD
—
Gnd
Gnd2
5
Gnd
RxD
RxD
RxD
RxD
TxD
DCD2
6
TxD
Gnd
Gnd
Gnd
Gnd
Gnd
RxD2
24
EQUINOX
IBM
7
DTR
CTS
CTS
DSR
—
DSR
RTS2
8
CTS
DTR
DTR
CTS
CTS
CTS
—
1
Linia PG zwykle połączona jest z obudową. Jako potencjał odniesienia używana jest linia Gnd.
2
Identycznie jak w ComputOne, odwrócono tylko numerację styków.
Na rysunkach 2.6 i 2.7 pokazano przykłady możliwego układu połączeń pomiędzy komputerem zaopatrzonym w złącze DB-9 lub DB-25 a nowoczesnym urządzeniem wyprowadzającym sygnały poprzez RJ-11. Tutaj również widać, że linia wyjściowa TxD komputera połączona jest z odpowiednią linią wejściową RxD urządzenia. Pozostałe linie po stronie komputera pozostają zwarte lub niewykorzystane. Nowoczesne urządzenia nie wykorzystują zbyt wielu linii sterujących. Linie DTR (4), CTS (8), DSR (6) i DCD (1) pozostają zwarte. Linia RTS (7) — żądanie nadawania — nie jest wykorzystywana. Jak więc będzie wybierany kierunek transmisji? Taki sposób podłączenia, jaki przedstawiono na przykładzie rysunków 2.6 i 2.7 natychmiast sugeruje, że mamy do czynienia z inteligentnym urządzeniem „znaczącym” koniec wysyłanych przez siebie danych parą znaków CR LF, tzw. „terminatorem”. Program obsługujący taki przyrząd, napotykając przy odbiorze danych znaki CR LF, będzie już wiedział, że właśnie otrzymał kompletną informację i należy przejść do ewentualnego nadawania. Jeżeli tylko potrafimy w ten sam lub inny chytry sposób odpowiedzieć miernikowi, otrzymamy bezbłędną obustronną transmisję. Taka metoda programowej kontroli transmisji określana jest mianem Software flow control. Nieustanne śledzenie poziomów wielu sygnałów pojawiających się na złączu RS przechodzi powoli do historii. Jedyną trudnością do przezwyciężenia będzie wówczas problem zbudowania odpowiedniej aplikacji.
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.
25
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 pokazanych w tabeli 2.1 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 ¾ deklarowanego rozmiaru. Program sterujący urządzeniem wysyła znak XON, jeżeli bufor jest wypełniony mniej niż w ½. 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 (patrz tab. 2.3). 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ół, o czym Czytelnik może się przekonać zaglądając do Panelu sterowania i Właściwości portów komunikacyjnych oraz czytając dalej tę książkę.
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 (patrz tab. 2.1) 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
26
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.
27
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
28
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. Czytelników pragnących poszerzyć swoje wiadomości na ten temat odsyłam do książki Wojciecha Mielczarka Urządzenia pomiarowe i systemy kompatybilne ze standardem SCPI, wyd. Helion (1999).
Podsumowanie W rozdziale tym zostały zaprezentowane podstawowe wiadomości dotyczące szeregowej transmisji asynchronicznej oraz standardu RS 232C. Tematy te zostały potraktowane w sposób zwięzły, ale zupełnie wystarczający do zrozumienia zagadnień związanych z programową kontrolą łączy szeregowych. W książce tej, wraz z wprowadzaniem konkretnych algorytmów mogących obsługiwać komunikację szeregową, omówione zagadnienia będą stopniowo uzupełniane. Bardziej szczegółowe informacje dotyczące standardu RS 323C Czytelnik może znaleźć w bogatej literaturze przedmiotu oraz na licznych stronach www. Przedstawione też zostały rzadko spotykane przykłady nowoczesnych sposobów połączeń, coraz częściej stosowane w szeregowej transmisji danych. Zapoznaliśmy się też z najważniejszymi stosowanymi obecnie protokołami kontroli transmisji danych. Dowiedzieliśmy się również, jakimi cechami powinny charakteryzować się aplikacje, obsługujące transmisję szeregową.
29
Rozdział 3 Jak testować programy do transmisji szeregowej ? Celem tego rozdziału jest przedstawienie ogólnodostępnych programów, za pomocą których można testować łącze szeregowe. Są to bardzo wygodne w użyciu narzędzia, pozwalające bardzo szybko sprawdzić, czy napisany właśnie program nie jest przypadkiem zgodny jedynie sam z sobą. Osoby pragnące zapoznać się ze sposobami programowej realizacji transmisji szeregowej, a nie posiadające specjalistycznych urządzeń, z powodzeniem mogą traktować drugi komputer (niekoniecznie wysokiej klasy) jako swojego rodzaju tester. Niektóre z programów przedstawionych w dalszej części książki były testowane za pomocą 386 SX.
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 CAMAC3. Jeżeli Czytelnik spotka komputer przyłączony do CAMAC-a, Mirror najczęściej będzie znajdował się w katalogu \ASM. Po jego 3
Interfejs ten nie był omawiany, gdyż oficjalnie przestał być rekomendowanym standardem przesyłania danych w końcu lat 80. XX wieku.
30
uruchomieniu i przeczytaniu informacji o producencie należy nacisnąć dowolny klawisz, by przejść do wyboru opcji programu, tak jak pokazuje to rysunek 3.1.
Rysunek 3.1. Dostępne opcje Mirrora
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 — rysunek 3.2.
31
Rysunek 3.2. 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: Rysunek 3.3. Sposób posługiwania się menu
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
32
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 w trakcie tej książki 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 — patrz rysunek 3.4. Mamy tu szereg ciekawych możliwości, jednak dla nas najbardziej interesującą będzie Transmisja. Rysunek 3.4. Główne menu programu
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, tak jak pokazuje to rysunek 3.5.
33
Rysunek 3.5. Wybór parametrów transmisji szeregowej
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
34
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.
Opis wyżej wymienionych protokołów znacznie przekracza rozmiary tej książki, dlatego ograniczymy się jedynie do zwięzłych informacji na ich temat. Czytelnicy, którzy interesowali się już tymi zagadnieniami bez większego problemu będą mogli zaadaptować je w programach przedstawionych w dalszej części niniejszego opracowania. 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.
Podsumowanie Celem niniejszego rozdziału było dokonanie krótkiego przeglądu dostępnych programów, za pomocą których można testować poprawność transmisji szeregowej zarówno w MS DOS jak i w Windows. Szczególnie przydatny dla naszych celów okaże się Terminal, który równie dobrze działa w środowisku 16-, jak i 32-bitowym. Był on standardowo dostarczany wraz z Windows 3.x, dlatego z dotarciem to tego produktu Czytelnik nie powinien mieć większych trudności. Przed rozpoczęciem pracy z łączem szeregowym do dobrego zwyczaju należy wstępne przetestowanie jakości połączenia i to niezależnie od tego, czy posługujemy się kablem oryginalnym, czy też własnej konstrukcji. Do tego rodzaju testu najlepiej użyć jakiegoś standardowego programu.
35
Unikniemy w ten sposób możliwości wystąpienia szeregu przykrych niespodzianek przy uruchamianiu samodzielnie napisanych programów.
36
Rozdział 4 RS 232C w MS DOS Czytając ten rozdział, 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.
37
bioscom()
_bios_serialcom()
wartość cmd
stała symboliczna
0
_COM_INIT
inicjalizacja i otwarcie portu
1
_COM_SEND
ustawienie łącza w trybie do wysyłania
2
_COM_RECEIVE
ustawienie łącza w trybie do odbioru
3
_COM_STATUS
pobranie aktualnego statusu portu
znaczenie
Sposób ustalenia wszystkich cech charakterystycznych ramki danych określa parametr abyte:
bioscom()
znaczenie
_bios_serialcom()
rozmiar pola bitów danych 0x02
_COM_CHR7
7 bitów danych
0x03
_COM_CHR8
8 bitów danych
bity stopu 0x00
_COM_STOP1
1 bit stopu
0x04
_COM_STOP2
2 bity stopu
bity parzystości 0x00
_COM_NOPARITY
No parity (brak parzystości)
0x08
_COM_ODDPARITY
Odd parity (nieparzysta)
0x18
_COM_EVENPARITY
Even parity (parzysta)
prędkość transmisji 0x00
_COM_110
110 bitów/sek.
0x20
_COM_150
150
0x40
_COM_300
300
0x60
_COM_600
600
38
0x80
_COM_1200
1200
0xA0
_COM_2400
2400
0xC0
_COM_4800
4800
0xE0
_COM_9600
9600
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: 1 1 1 0 0 0 0 0
(0xE0)
dziesiętnie 224
(0x08)
8
(0x00)
0
(0x03)
3
(0xEB)
235
OR 0 0 0 0 1 0 0 0 OR 0 0 0 0 0 0 0 0 OR 0 0 0 0 0 0 1 1 __________________ abyte =
1 1 1 0 1 0 1 1
Zarówno bioscom(), jak i _bios_serialcom() zwracają wartość 16-bitową. Bardziej znaczący bajt tej liczby zawiera bity opisujące aktualny stan transmisji.
39
Bardziej znaczący bajt: bit 7 (Time out) 1 = błąd przekroczenia czasu nawiązania połączenia, tzw. błąd przeterminowania czasu 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:
40
Komentarz: To pleonazm. Albo „przeterminowania operacji”, albo „przekroczenia czasu operacji”.
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.
41
Zobaczmy, jak w praktyce można wykorzystać funkcję bioscom(). Przedstawiony na wydruku 4.1 algorytm realizuje naprzemienną transmisję szeregową, emulując prosty terminal. Jego kod znajduje się na załączonym CD w katalogu \KODY\CPP\dos_rs_1.cpp. 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. Rysunek 4.1. Plansza działającego programu dos_rs_1.cpp
Wydruk 4.1. Kod źródłowy programu napisanego w C++ realizującego naprzemienną transmisję szeregową //-----------dos_rs_1.cpp-----------------------------------#include #include #include <stdio.h> #include #include <string.h> #define WINDOW_IN() window (1,3,80,12) #define WINDOW_OUT() window (1,14,80,25) // 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
42
// okno do odbioru // okno do nadawania
// 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() #define rs_init() #define rs_send(output) #define rs_receive() int int int int
bioscom(STATUS, 0, COM) bioscom(INIT, SPEED | PARITY | STOP | \ ByteSize, COM) bioscom(SEND, output, COM) bioscom(RECEIVE, 0, COM)
STATUS = 3; INIT = 0; SEND = 1; 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 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);
43
}
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ś
44
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);
45
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. Poniżej została przedstawiona budowa interesujących nas rejestrów oraz opisano funkcje przerwania 14h, realizujące programową obsługę portu szeregowego. Dwa pierwsze rejestry ogólnego przeznaczenia: 16-bitowy AX 31
23
15
AH
7
AL
0
DL
0
32-bitowy EAX 16-bitowy DX 31
23
15
DH
7
32-bitowy EDX 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.
46
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. Format zawartych tam danych można opisać jako: bit 7
bit 6
bit 5
prędkość transmisji 0
0
bit stopu
brak
1 bit
0
150 b/s 0
0
0
300 b/s 0
1
0 parzysta
1
1
1
1
600 b/s 0
1
1
1200 b/s 1
0
0
2400 b/s 1
0
1
4800 b/s 1
1
0
47
0
bit 1
1
bit 0
liczba bitów danych 7 bitów 1
2 bity
nieparzysta 0
bit 2
kontrola parzystości
110 b/s 0
bit 3
bit 4
0 8 bitów
1
1
9600 b/s 1
1
1
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.
48
Jako ciekawostkę podajmy, że programy mogą obsłużyć: 256 8-bitowych portów wejścia-wyjścia ponumerowanych ze stałą jednobajtową od 0 do 255 lub 128 16-bitowych portów ponumerowanych od 0, 2, 4 do 254, lub 64 32-bitowe porty o numerach 0, 4, 8 do 252. Używając wartości rejestru DX, analogicznie możemy określić 8-bitowe porty oraz porty 16-bitowe o numeracji od 0, 2, 4 do 65534. Istnieje ponadto możliwość obsłużenia 32-bitowych portów ponumerowanych od 0, 4, 8 do 65532. Wynika to z faktu, iż procesor jest w stanie przesłać 8, 16 i 32 bity do urządzeń wejścia-wyjścia. 16-bitowe porty mają adresy będące wielokrotnością 2, zaś 32-bitowe porty są wyrównywane do adresów będących wielokrotnością 4.
Poniżej (na wydruku 4.2) 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;
Wydruk 4.2. 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;
49
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.
Program ten znajduje się w katalogu \KODY\Pascal\dos_rs_2.pas. Jego obsługa sprowadza się do wyboru kierunku transmisji znaków wpisywanych z klawiatury. Koniec przesyłania danych sygnalizujemy klawiszem Esc. Bez problemu można tym sposobem przesłać również plik. Można też, korzystając ze ściągawki w postaci wydruku programu dos_rs_1.cpp, pokusić się o sprawdzanie statusu portu i aktualnego stanu transmisji. Testowanie tego typu prostych algorytmów zawsze okazuje się wielce pouczające.
Podsumowanie Pisząc ten krótki rozdział, starałem się nie zanudzić Czytelnika zbyt wieloma operacjami bitowymi stosowanymi w programach komunikacyjnych pisanych w MS DOS. Jednak już teraz pragnę uprzedzić, że chociaż może się to wydać komuś dziwne, to do „bitów” powrócimy jeszcze w Windows. Celem tego fragmentu książki było zaprezentowanie ogólnych zasad tworzenia oprogramowania sterującego transmisją szeregową w DOS. Poznaliśmy dwie wielce użyteczne funkcje oferowane przez C++. Ich znajomość pozwala całkiem szybko zbudować poprawnie działający program komunikacyjny. Garść informacji o rejestrach i przerwaniach pozwoli nam w przyszłości w pełni docenić zalety programowania obiektowo-zdarzeniowego. Aplikacje pisane dla Windows też z nich korzystają, co prawda w sposób nie tak jawny, ale warto zdawać sobie sprawę z tego faktu.
50
Ćwiczenia 1.
Korzystając z funkcji _bios_serialcom(), napisz prosty program, realizujący transmisję plików.
2.
Uruchom program dos_rs_1.cpp skonfigurowany na port szeregowy, do którego przyłączona jest Twoja mysza. Oglądając efekt na ekranie, zobaczysz pewien znajomy znak (patrz tab. 2.1). Postaraj się wyjaśnić, co się stało.
3.
Traktując jako ściągawkę program dos_rs_2.pas Zmodyfikuj go tak, by parametry komunikacyjne danego portu szeregowego, takie jak: prędkość, liczbę bitów stopu, kontrolę parzystości, liczbę bitów danych można było wprowadzać z klawiatury. Postaraj się również uzupełnić go o opcję umożliwiającą sprawdzanie statusu łącza. Unikniesz wówczas odbioru przypadkowo generowanych znaków.
51
Rozdział 5 Programowa obsługa interfejsu RS 232C w Windows Czytając ten rozdział, zapoznasz się ze sposobami konstrukcji algorytmów, realizujących transmisję szeregową w środowisku Windows, które charakteryzuje się pewnymi cechami nie mającymi odpowiedników w MS DOS. Poznanie i umiejętne wykorzystanie tych cech sprawi, iż problem obsługi interfejsów szeregowych z poziomu Windows — uważany powszechnie za trudny — przestanie być dla nas tajemnicą. Pokażemy, w jaki sposób należy tworzyć aplikacje, służące do programowej obsługi łącza szeregowego RS 232C zarówno w C++Builderze jak i w Delphi. W moim odczuciu wśród programistów istnieje zauważalny podział na osoby programujące głównie w Delphi oraz na preferujące Buildera lub ogólnie C++ do Windows. Jednak zdaniem wielu osób uniwersalność jest jedną z tych zalet, jakie powinny charakteryzować programistę. W rozdziale tym przybliżymy Czytelnikowi podobieństwa i różnice w sposobie konstrukcji algorytmów realizujących transmisję szeregową, pisanych w Delphi oraz Builderze. W dalszej części książki spotkamy się z typami danych, których poznanie i zrozumienie ma kluczowe znaczenie w projektowaniu aplikacji, obsługujących urządzenia zewnętrzne. Zacznijmy od ich przypomnienia. W tabeli poniżej przedstawiono porównanie podstawowych typów zmiennych wykorzystywanych w kompilatorach, które będą dla nas istotne. Większości z nich można używać zamiennie, pisząc zarówno w Delphi jak i w C++Builderze.
51
Tabela 5.1. 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
Word
2
bez znaku
integer
unsigned short
Integer
4
Cardinal
4
bez znaku
Boolean
1
true/false
bool
true/false
unsigned char
ByteBool 1 WordBool
bez znaku
integer
int
integer
unsigned int
integer
true/false 2
LongBool
unsigned short
bez znaku
integer
true/false 4
bez znaku
AnsiChar
1
1 znak ANSI
character
char
WideChar
2
1 znak Unicode
character
wchar_t
Char
1
bez znaku
character
char
ANSIChar
AnsiString
ANSIChar
string
SmallString
255 bajtów
ANSIChar
string
SmallString<255>
255 lub ≈3GB
ANSIChar
AnsiString
AnsiString String[n]
≈3GB n = 1..255
integer
AnsiString
bajtów ShortString String Single
4
floating point number (liczba zmiennoprzecinkowa)
52
AnsiString float
Double
8
floating point
Extended
10
floating point
double
number long double
number Real
4
floating point
double
number Pointer
4
generic pointer
void *
(wskaźnik ogólny, adresowy) PChar
4
bez znaku
pointer to characters
unsigned char *
(daleki wskaźnik na C-łańcuch) PAnsiChar
4
Comp
8
bez znaku
pointer to ANSIChar
unsigned char *
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 pomocy1. Oczywiście, moglibyśmy silić się na oryginalność, wprowadzając 1
Niekiedy spotyka się próby dosłownego tłumaczenia takich nazw i tworzenie konstrukcji podobnych do: wskBlad lub fwsk, co ma symbolizować wskaźnik do danej Blad (czyt. Błąd) czy wskaźnik do file. Moim zdaniem trzymanie się międzynarodowych, ogólnie akceptowanych standardów jest zawsze dobrym zwyczajem.
53
własne zmienne, zrozumiałe tylko dla piszącego dany program — wówczas przykłady musiałyby być zapisane jako wręcz humorystyczna mieszanina języków polskiego i angielskiego. Trzeba też przyznać, że byłby to bardzo skuteczny sposób zaciemnienia obrazu API. Zrozumienie znaczenia nazw tam stosowanych okaże się w przyszłości niezwykle cenne, gdyż API można czytać jak książkę. Aby pomóc Czytelnikom, którzy nie zetknęli się dotąd z tymi pojęciami, poniżej przedstawiono ogólne zasady tworzenia niektórych przedrostków. Tabela 5.2. Ogólne zasady tworzenia przedrostków wg notacji węgierskiej.
Przedrostek
Skrót angielski
a
array
b
bool
by
byte unsigned char
Znaczenie
tablica zmienna logiczna TRUE lub FALSE (1 lub 0) znak (bajt)
ch
char
cb
count of bytes
liczba bajtów
znak
dw
double word
podwójne słowo
evt
event
zdarzenie
f
flag
znacznik
fdw
flag of double word
fn
function
h
handle
identyfikator (uchwyt)
i
integer
typ całkowity 4-bajtowy
id
(ID) identification
in
input
l
long int
lp
long pointer
lpc
long pointer to C- string
lpfdw
long pointer to flag of dw
zacznik typu dw funkcja
identyfikacja wejście, dane wejściowe typ całkowity długi 4-bajtowy wskaźnik typu long int wskaźnik typu long int do C-łańcucha wskaźnik typu lp do znacznika typu double word
wskaźnik typu lp do funkcji
lpfn
long pointer to function
n
short or int
typ krótki lub całkowity
np
near pointer
bliski wskaźnik
54
(w środowisku 32-bitowym to samo co lp) out
output
p
pointer
wyjście, dane wyjściowe (przetworzone) 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
u
unsigned
w
(word) unsigned int
wc
WCHAR
typ bez znaku słowo 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ą 32bitowe offsety w 4 GB segmencie (232 ≅ 4GB). Windows oferuje nam ponadto kilka typów danych, z których część tylko nieznacznie różni się sposobem zapisu w implementacjach Delphi i Buildera. Typy te mają najczęściej postać struktury lub klasy i są bardzo często wykorzystywane w warstwie komunikacyjnej programów. Tabela 5.3 przedstawia przykładowe, ogólnie stosowane sposoby ich deklaracji.
55
Tabela 5.3. Typowe sposoby deklaracji stosowane w programach komunikacyjnych
Delphi
dcb :
Znaczenie
TDCB;
hCommDev :
THANDLE;
C++ Builder
struktura kontroli portu
DCB
32-bitowy identyfikator
HANDLE
dcb;
hCommDev;
portu — jednorazowo nadana wartość identyfikująca port
Stat :
TCOMSTAT;
struktura zawierająca
COMSTAT
Stat;
dodatkowe informacje o porcie szeregowym
TOVERLAPPED; struktura zawierająca
Overlapped :
OVERLAPPED
Overlapped;
informacje używane przy transmisji asynchronicznej
byX :
BYTE;
bajt
wKey :
WORD;
słowo 2-bajtowe
dwErrors :
DWORD;
BYTE
podwójne słowo — 4 bajty
WORD
byX;
wKey;
DWORD
dwErrors;
(double word)
bResult :
BOOL;
znacznik TRUE-FALSE 1
uFun :
UINT;
unsigned int
56
BOOL
bResult;
0 UINT
uFun;
Zaopatrzeni w powyższą terminologię pójdźmy dalej i zobaczmy, do czego mogą nam być przydatne poszczególne struktury oraz funkcje interfejsu programisty — Win32 API.
Wykorzystanie elementów Win32 API w C++ Builder. Część I Poznawanie tajników obsługi portu szeregowego w Windows rozpoczniemy, z czysto praktycznych względów, od pisania programów w C++ Builderze . C++ posiada składnię taką jak API, dlatego prościej nam będzie zapoznać się z budową funkcji oraz struktur oferowanych przez interfejs programisty. Ułatwi to też zrozumienie, w jaki sposób i w jakiej kolejności należy umieszczać je w programie. Fundamentalne znaczenie ma struktura kontroli urządzeń zewnętrznych DCB (ang. Device Control Block). W Windows struktura DCB w pewnym sensie odpowiada funkcji 00h przerwania 14h BIOS-u. Udostępnia nam jednak nieporównywalnie większe możliwości programowej obsługi łącza szeregowego umożliwia bezpośrednie programowanie rejestrów układu UART. W poniższych tabelach przedstawiono specyfikację bloku kontroli urządzeń zewnętrznych DCB: Tabela 5.4. Zmienne struktury DCB reprezentujące dopuszczalne parametry ustawień portu szeregowego Typ
Zmienna
Znaczenie
Wartość, Stała symboliczna
DWORD
DCBlength
rozmiar struktury
należy wpisać
DWORD
BaudRate
określenie prędkości
CBR_110
CBR_19200
transmisji (bity/sek)
CBR_300
CBR_ 38400
CBR_600
CBR_56000
CBR_1200 CBR_57600 CBR_2400 CBR_115200 CBR_4800 CBR_128000 CBR_9600 CBR_256000
57
CBR_14400
WORD
wReserved
nie używane
WORD
XonLim
określenie minimalnej liczby
0
domyślnie: 65535,
bajtów w buforze wejściowym
w praktyce XonLim
przed wysłaniem specjalnego
ustala się jako ½ rozmiaru
znaku sterującego XON
deklarowanego wejściowego bufora danych
WORD
XoffLim
określenie maksymalnej liczby bajtów w buforze wejściowym
domyślnie: 65535, w praktyce XoffLim
przed wysłaniem specjalnego
ustala się jako ¾ rozmiaru
znaku sterującego XOFF
deklarowanego bufora wejściowego
BYTE
ByteSize
wybór liczby bitów danych
BYTE
Parity
określenie kontroli parzystości
5, 6, 7, 8
EVENPARITY parzysta MARKPARITY bit parzystości stale równy 1 NOPARITY brak kontroli
58
ODDPARITY nieparzysta
BYTE
StopBits
wybór bitów stopu
ONESTOPBIT 1 bit stopu ONE5STOPBITS w przypadku słowa 5bitowego bit stopu wydłużony o ½ TWOSTOPBITS 2 bity stopu
char
char
XonChar
XoffChar
określenie wartości znaku XON
standardowo
dla nadawania i odbioru
(char) DC1,
(wysłanie znaku przywraca transmisję)
dziesiętnie: 17
określenie wartości znaku XOFF
standardowo
dla nadawania i odbioru (wysłanie
(char) DC3,
XOFF wstrzymuje transmisję
dziesiętnie: 19
do czasu odebrania znaku XON)
char
ErrorChar
określenie wartości znaku zastępującego bajty otrzymane z błędem parzystości
59
opcjonalnie: 0 lub SUB
char
EofChar
określenie wartości znaku końca
opcjonalnie: 0
otrzymanych danych
char
EvtChar
określenie wartości znaku służącego do
opcjonalnie: 0
sygnalizowania wystąpienia danego zdarzenia
WORD
wReserved1
obecnie nie używane
Tabela 5.5. Pola bitowe reprezentujące dopuszczalne wartości znaczników sterujących struktury DCB Typ
Pole
Właściwości
bitowe
pola
Wartość Znaczenie Stała symboliczna
DWORD
fBinary
tryb binarny (Win 32 API
TRUE / 1
podtrzymuje jedynie ten tryb transmisji danych)
DWORD
fParity
umożliwia ustawienie sprawdzania parzystości — sposobu reakcji na
TRUE / 1 kontrola parzystości włączona
bit parzystości FALSE / 0 bit parzystości nie jest sprawdzany
60
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
DTR_CONTROL_DISABLE / 0
sygnału DTR
sygnał na linii DTR jest nieaktywny DTR_CONTROL_ENABLE / 1 sygnał na linii DTR jest aktywny
61
DTR_CONTROL_HANDSHAKE / 2 włączenie potwierdzania przyjęcia sygnału DTR — potwierdzenie musi być odebrane na linii DSR. Używane w trybie półdupleksowym ewentualne błędy transmisji w tym trybie są usuwane przez funkcję EscapeCommFunction().
DWORD fDsrSensitivity
specyfikacja wykorzystania
TRUE / 1
poziomu sygnału na linii DSR
otrzymane bajty są ignorowane, o ile linia DSR nie jest w stanie wysokim FALSE / 0 stan linii DSR jest ignorowany
DWORD fTXContinueOnXoff
kontrola przerwania transmisji
TRUE / 1
w przypadku przepełnienia
wymuszanie kontynuowania
bufora wejściowego i, ewentualnie, transmisji nawet po wystąpienia znaków XoffChar
wystąpieniu znaku XOFF i
oraz XonChar
wypełnieniu wejściowego bufora danych powyżej XoffLim bajtów
FALSE / 0 transmisja nie jest kontynuowana, dopóki bufor wejściowy nie zostanie opróżniony do pułapu XonLim bajtów i nie
62
nadejdzie znak XON potwierdzenia dalszego odbioru
DWORD
fOutX
programowe ustawienie protokołu
TRUE / 1
XON-XOFF w czasie wysyłania
transmisja zostaje przerwana
danych
po odebraniu znaku XoffChar i wznowiona po otrzymaniu znaku XonChar FALSE / 0 XON-XOFF w czasie wysyłania nie jest ustawiony
DWORD
fInX
programowe ustawienie protokołu
TRUE / 1
XON-XOFF w czasie odbioru
znak XoffChar jest wysyłany,
danych
kiedy bufor wejściowy jest pełny lub znajduje się w nim XoffLim bajtów;
znak XonChar jest wysyłany, kiedy bufor wejściowy pozostaje pusty lub znajduje się w nim XonLim bajtów FALSE / 0 XON-XOFF w czasie odbioru nie jest ustawiony
DWORD fErrorChar umożliwia zastąpienie bajtów
otrzymanych z błędem parzystości
63
TRUE / 1 zastąpienie jest wykonywane
znakiem ErrorChar
ponadto fParity musi być ustawione jako TRUE FALSE / 0 zastąpienie nie jest wykonane
DWORD
fNull
odrzucenie odebranych nieważnych 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
RTS_CONTROL_DISABLE / 0
linii RTS
sygnał na linii RTS jest nieaktywny RTS_CONTROL_ENABLE / 1 sygnał na linii RTS jest aktywny RTS_CONTROL_HANDSHAKE / 2 włączenie potwierdzania przyjęcia sygnału RTS (potwierdzenie musi być odebrane na linii CTS). Używane w trybie półdupleksowym. Sterownik podwyższa stan linii RTS, gdy wypełnienie bufora wejściowego jest mniejsze od ½ . Stan linii RTS zostaje obniżony, gdy bufor wypełniony jest w ¾ .
64
Ewentualne błędy transmisji w tym trybie usuwane są przez funkcję EscapeCommFunction().
RTS_CONTROL_TOGGLE / 3 linia RTS jest w stanie wysokim, jeżeli są bajty do transmisji i jest ona możliwa; po opróżnieniu bufora komunikacyjnego linia RTS pozostaje w stanie niskim
DWORD fAbortOnError ustawienie wstrzymywania
TRUE / 1
operacji nadawanie-odbiór
wszelkie operacje nadawania i
przy wykryciu błędu transmisji
odbioru są wstrzymywane, zaś dalsza komunikacja nie jest możliwa, dopóki błąd nie zostanie usunięty przez wywołanie funkcji ClearCommError() FALSE / 0 nawet jeżeli wystąpi błąd, transmisja jest kontynuowana — błąd może być usunięty przez wywołanie funkcji ClearCommError()
DWORD
fDummy2
zarezerwowane, nie używane
65
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.
66
? 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”;
67
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.
68
Wybrany przez nas port szeregowy ostatecznie skonfigurujemy zgodnie ze specyfikacją struktury DCB, za pomocą funkcji SetCommState(), która reinizjalizuje i uaktualnia wszystkie dostępne parametry w ustawieniach łącza szeregowego: BOOL SetCommState(HANDLE hCommDev, LPDCB lpdcb).
Jednak tutaj parametr, na który wskazuje lpdcb musi już zawierać informacje o nowych, wybranych przez nas parametrach ustawień portu komunikacyjnego. Należy też pamiętać, że funkcja SetCommState() nie zostanie wykonana pomyślnie, jeżeli posługując się strukturą DCB element XonChar ustalimy identycznie z XoffChar. Przed zakończeniem działania aplikacji otwarty port szeregowy należy koniecznie zamknąć i zwolnić obszar pamięci przydzielony na jego identyfikator, korzystając z: BOOL CloseHandle(HANDLE hCommDev).
We wszystkich przedstawionych powyżej funkcjach hCommDev w pełni identyfikuje dany port szeregowy, zawierając kompletną informację o tym, do którego łącza szeregowego będziemy wysyłać komunikaty. Ponieważ funkcje te mogą obsługiwać komunikaty wysyłane do wielu portów komunikacyjnych (jak również komunikaty odbierane od wiele portów), zatem każdy otwarty i zainicjalizowany port szeregowy będzie identyfikowany właśnie za pomocą swojego własnego hCommDev. Nie należy przydzielać tego samego identyfikatora do dwóch różnych portów komunikacyjnych, tak samo jak nie należy z jednym portem kojarzyć dwóch różnych identyfikatorów.
! Przy pisaniu aplikacji obsługujących łącze szeregowe należy koniecznie zamknąć port przed opuszczeniem programu. Jeżeli zechcesz korzystać z zegara systemowego przy obsłudze RS-a lub techniki programowania wielowątkowego, musisz pamiętać, że samo zamkniecie aplikacji nie powoduje automatycznego zamknięcia portu. Jego identyfikator dalej będzie przechowywany w pamięci. W pewnych przypadkach aplikacja z nie zamkniętym portem szeregowym może stać się programem rezydentnym i uniemożliwi powtórne otwarcie wybranego portu. Dobrym zwyczajem jest w pierwszej kolejności zaprojektowanie funkcji lub procedury obsługi zdarzenia zamykającego otwarty port. System operacyjny powinien być zawsze poinformowany o fakcie zamknięcia portu.
W praktyce zdarzają się jednak sytuacje, w których zamknięcie portu okaże się niemożliwe, np. z powodu jakiegoś błędu w algorytmie lub niewłaściwego sposobu wywołania danej funkcji. Mówimy wówczas, że program się załamał lub zawiesił. Ten problem powtarza się często w trakcie testowania programów komunikacyjnych. Nie należy wówczas od razu używać kombinacji Ctrl Alt Del lub czegoś bardziej drastycznego. W takich przypadkach trzeba rozwinąć z głównego menu opcję Project oraz wybrać Compile Unit, tak jak pokazano to na rys. 5.1. Nazwa działającej aplikacji powinna pojawić się na dolnym pasku zadań.
69
Rysunek 5.1. Przykładowy sposób wstrzymywania działania aplikacji p_test_1 z otwartym portem szeregowym
Po pojawieniu się informacji: Usuwanie sesji w toku. Zakończyć ? należy nacisnąć przycisk OK .
Po kolejnym komunikacie znów należy potwierdzić:
70
Postępując tak, w większości przypadków odzyskasz program oraz odblokujesz łącze szeregowe. Sposób ten okaże się szczególnie przydatny przy kłopotach z aplikacją komunikacyjną korzystającą z komponentu typu TTimer, generującego zdarzenia w równych odstępach czasu.
Testowanie portu szeregowego Mając na uwadze wszystko, co powiedzieliśmy do tej pory, spróbujemy napisać w C++Builderze prosty program wykorzystujący przedstawione powyżej funkcje Win32 API oraz niektóre zasoby struktury DCB. Zadaniem naszej aplikacji będzie ustawienie wybranych parametrów danego portu szeregowego, otwarcie go oraz odczytanie nowych ustawień. W tym celu stwórzmy nową standardową aplikację (polecenie File — New Application). Niech jej formularz składa się z dwóch przycisków TButton, pięciu komponentów typu TEdit oraz pięciu typu TLabel. Korzystając z inspektora obiektów (Object Inspector) oraz z karty właściwości (Proprties), cechę Name przycisku Button1 zmień na CloseComm, zaś jego cechę Caption na &Zamknij. Podobnie cechę Name przycisku Button2 zmień na OpenComm, zaś Caption na &Otwórz port. Cechy Caption komponentów TLabel zmień odpowiednio na Prędkość transmisji, Liczbę bitów danych, Parzystość, Bity stopu, Linia DTR. Cechy Text komponentów TEdit wyczyść. Formularz naszej aplikacji, wyglądającej podobnie jak na rysunku 5.2, znajduje na dołączonym CD w katalogu \KODY\BUILDER\RS_01\p_RS_01.bpr.
71
Rysunek 5.2. Formularz główny projektu p_RS_01.bpr
Wydruk 5.1. 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; DCB dcb; LPCTSTR lpFileName = "COM2"; //char *lpFileName = "COM2";
// identyfikator portu szeregowego // struktura kontroli portu // wskaźnik do nazwy portu
//-------------------------------------------------------------------__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 {
72
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);
73
break; case CBR_110: Edit1->Text = IntToStr(dcb.BaudRate); break; } //-------------sprawdzenie i wyświetlenie ustawionych bitów danychswitch (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";
74
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.
75
Jeżeli mimo wszystko port nie został otwarty prawidłowo, dobrze by było, gdyby aplikacja powiadomiła nas o tym fakcie. W tym celu można skorzystać z komunikatów Windows typu IE_ (ang. Identify Error — błąd identyfikacji portu — urządzenia lub jego ustawień). W poniższej tabeli przedstawiono najczęściej otrzymywane od Windows tego typu komunikaty: Tabela 5.6. Najczęściej używane komunikaty błędnej identyfikacji ustawień portu szeregowego
Identyfikacja komunikatu
IE_BADID
Wartość
Znaczenie
-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
IE_MEMORY
-4
odbiornik jest zablokowany
niewłaściwie ustalono rozmiar buforów
76
IE_NOPEN
-3
urządzenie nie jest otwarte do transmisji
IE_OPEN
-2
urządzenie pozostaje otwarte
Podczas lektury wydruku programu RS_01.cpp może zadziwić fakt, iż w pierwszej kolejności tuż po otwarciu portu wyszukaliśmy funkcją GetCommState() jego bieżące ustawienia. Następnie wybranym parametrom DCB przypisaliśmy nowe, własne wartości, które ostatecznie wpisaliśmy do struktury kontroli łącza szeregowego funkcją SetCommState(). Technika pisania tego typu aplikacji określana jest mianem przeładowywania lub przedefiniowania danych (ang. override). Użycie funkcji SetCommState() spowoduje prawdopodobnie poprawne przypisanie nowych wartości do DCB jednak pisząc program w ten sposób powodujemy tzw. zachodzenie na siebie lub przeciążania danych (ang. overload), co w pewnych wypadkach nie jest rzeczą pożądaną. Zachęcam Czytelnika piszącego aplikacje dla Windows do stosowania metod przeładowywania danych. Wówczas o wiele rzadziej będą się pojawiać na ekranie irytujące komunikaty w stylu: błąd w module kernel32.dll lub kernel32.exe. W celu dokładniejszego zapoznania się z możliwościami testowania systemów komunikacyjnych dostępnych w Windows poniżej przedstawiono pewną bardzo użyteczną strukturę oferowaną przez Win32 API. Zawarte w niej informacje mogą być wykorzystywane do pełnego odczytywania wszystkich istotnych parametrów interesującego nas łącza komunikacyjnego oraz usług potencjalnie przez nie oferowanych. Tabela 5.7. Zasoby struktury COMMPROP Typ
Element struktury
Znaczenie
Zawartość elementu, Maska określająca włączony bit
WORD
wPacketLength
Określa (w bajtach) rozmiar
należy odczytać,
porcji pakietu danych
zależy też od typu sterownika
77
WORD
wPacketVersion
DWORD dwServiceMask
wersja struktury
nr 2 w Win 9x
określenie maski bitowej,
SP_SERIALCOMM
wskazującej na typ aktualnie
jest zawsze określone
dostępnej usługi komunikacyjnej
DWORD dwReserved1
zarezerwowane, nie używane
DWORD dwMaxTxQueue
maksymalny rozmiar wewnętrznego 0 oznacza, że nie ustalono bufora wyjściowego nadajnika
maksymalnej wartości
(w bajtach)
DWORD dwMaxRxQueue
maksymalny rozmiar wewnętrznego 0 oznacza, że nie ustalono bufora wejściowego odbiornika
maksymalnej wartości
(w bajtach)
DWORD dwMaxBaud
maksymalna dostępna prędkość
BAUD_075
75 b/s
transmisji w bitach na sekundę
BAUD_110
110
BAUD_134_5
134.5
BAUD_150
150
BAUD_300
300
BAUD_600
600
BAUD_1200
1200
78
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
79
PST_X25 protokół X.251
DWORD dwProvCapabilities
określa maskę bitową
PCF_16BITMODE
identyfikującą rodzaj funkcji
tryb 16-bitowy
udostępnianych przez
PCF_DTRDSR
usługę komunikacyjną
kontrola DTR-DSR
(dostarczyciela usługi)
PCF_INTTIMEOUTS czas przeterminowania PCF_PARITY_CHECK sprawdzanie parzystości PCF_RLSD kontrola RLSD PCF_RTSCTS kontrola RTS-CTS PCF_SETXCHAR możliwość użycia protokołu XON/XOFF PCF_SPECIALCHARS specjalny znak PCF_TOTALTIMEOUTS kontrola czasu przeterminowania transmisji PCF_XONXOFF podtrzymanie protokołu XON-XOFF
1 Starsza, ale jeszcze wykorzystywana wersja protokołu stosowanego do projektowania sieci pakietowych i rozległych o przepustowości do 2 Mb/s. Jest stopniowo wypierany przez nowsze technologie Frame Relay, umożliwiające osiągnięcie przepustowości do 45 Mb/s.
80
DWORD dwSettableParams
specyfikacja maski bitowej
SP_BAUD prędkość
identyfikującej parametry
SP_DATABITS długość
transmisji podlegające ew.
słowa danych
zmianom
SP_HANDSHAKING kontrola przepływu danych SP_PARITY parzystość SP_PARITY_CHECK sprawdzanie parzystości SP_RLSD sygnał RLSD SP_STOPBITS bity stopu
DWORD dwSettableBaud
specyfikacja maski bitowej umożliwiającej ustawienie
tak samo jak w dwMaxBaud
prędkości transmisji
WORD
wSettableData
specyfikacja maski bitowej
DATABITS_5
identyfikującej możliwe do
DATABITS_6
użycia długości słowa danych
DATABITS_7 DATABITS_8 DATABITS_16 DATABITS_16X szczególna długość słowa danych
WORD
wSettableStopParity specyfikacja maski bitowej
STOPBITS_10 1 bit stopu
identyfikującej możliwe do
STOPBITS_15 1,5 bitu
użycia wartości bitów stopu
STOPBITS_20 2 bity
i kontroli parzystości
PARITY_NONE brak
81
PARITY_ODD nieparzysta PARITY_EVEN parzysta PARITY_MARK 1 PARITY_SPACE 0
DWORD dwCurrentTxQueue
aktualny maksymalny rozmiar
0 oznacza, że wartość ta
wewnętrznego bufora wyjściowego
nie jest dostępna
nadajnika (w bajtach)
DWORD dwCurrentRxQueue
DWORD dwProvSpec1
aktualny maksymalny rozmiar
0 oznacza, że wartość ta
wewnętrznego bufora wejściowego
nie jest aktualnie
odbiornika (w bajtach)
dostępna
specyfikacja formatu danych
w zależnoście od
wymaganych przez daną usługę
dwProvSubType
komunikacyjną
aplikacje powinny ignorować ten człon, chyba że mają szczegółowe informacje odnośnie formatu danych wymaganych przez dostarczyciela usługi
DWORD dwProvSpec2
WCHAR wcProvChar[1]
jak wyżej
jak wyżej
82
Jeżeli dwProvSubType przypisano PST_MODEM , musi nastąpić
odwołanie do struktur MODEMDEVCAPS oraz 1
MODEMSETTINGS . dwProvSpec1 oraz dwProvSpec2
nie są wówczas używane.
W Win32 API COMMPROP deklaruje się następująco: typedef struct _COMMPROP { WORD wPacketLength; ... } COMMPROP;
Powyższa deklaracja tworzy nowe słowo kluczowe typu COMMPROP (struktura). Zbudujmy teraz aplikację, za pomocą której będziemy mogli selektywnie odczytywać stan poszczególnych masek bitowych udostępnianych przez COMMPROP. Wykorzystamy tu znany nam już proces maskowania z wykorzystaniem operatora iloczynu bitowego & (bitowe i). Program będzie odczytywał wartość wybranego elementu struktury, a następnie poprzez wybranie kolejnych masek będzie selektywnie sprawdzał, czy włączone są konkretne bity odpowiedzialne za pewne parametry transmisji. Omawiany projekt znajduje się na dołączonym CD w katalogu \KODY\BUILDER\RS_02\p_RS_02.bpr. Do testowania wybierzmy elementy: dwSettableParams , reprezentowany na 32 bitach oraz wSettableData i wSettableStopParity , reprezentowane na 16 bitach. Zastosujemy nieco odbiegający od przedstawionego wcześniej projekt formularza. Składać się on będzie z dwóch dobrze nam już znanych przycisków, reprezentujących zdarzenia polegające na otwarciu portu oraz na jego zamknięciu. Zastosowałem ponadto dwa komponenty typu TTrackBar dwa TEdit oraz dwa typu TLabel, tak jak pokazuje to rysunek 5.3. Obsługa zdarzenia TrackBar1Change() polega na wybraniu interesującego nas elementu struktury COMMPROP oraz odczytaniu jego aktualnej wartości. Jeżeli zechcemy sprawdzić, czy włączony jest konkretny bit reprezentujący wybrany atrybut transmisji przechowywany w danym elemencie struktury, wystarczy przesunąć wskaźnik uruchamiający funkcję obsługi zdarzenia TrackBar2Change(). Funkcją, która zwraca aktualne własności portu komunikacyjnego identyfikowanego przez hCommDev będzie: BOOL GetCommProperties(HANDLE
1
hCommDev, LPCOMMPROP
Miłośnikom modemów specyfikację tych struktur prezentujemy w uzupełnieniu 3.
83
lpCommProp);
lpCommProp jest wskaźnikiem do struktury COMMPROP, której format danych w ogólnym przypadku należy najpierw zainicjalizować: CommProp.dwProvSpec1 = COMMPROP_INITIALIZED;
Informacje tam zawarte mogą być pomocne przy odwoływaniu się do rodziny funkcji SetCommState(), SetCommTimeouts() lub SetupComm(). Rysunek 5.3. Formularz główny projektu p_RS_02.bpr
Wydruk 5.2. 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; COMMPROP CommProp; LPCTSTR lpFileName="COM2";
// identyfikator portu // właściwości portu // wskaźnik do nazwy portu szeregowego
//-------------------------------------------------------------------__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //----------funkcja zamyka otwarty port szeregowy--------------------int __fastcall Close_Comm(HANDLE hCommDev)
84
{ 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);
85
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) {
86
case 1: { Label2->Caption = "DATABITS_5"; Edit2->Text=IntToStr(CommProp.wSettableData break; } case 2: { Label2->Caption = "DATABITS_6"; Edit2->Text=IntToStr(CommProp.wSettableData break; } case 3: { Label2->Caption = "DATABITS_7"; Edit2->Text=IntToStr(CommProp.wSettableData break; } case 4: { Label2->Caption = "DATABITS_8"; Edit2->Text=IntToStr(CommProp.wSettableData break; } case 5: { Label2->Caption = "DATABITS_16"; Edit2->Text=IntToStr(CommProp.wSettableData break; } case 6: { Label2->Caption = "DATABITS_16X"; Edit2->Text=IntToStr(CommProp.wSettableData break; } } //koniec switch } // koniec if
& DATABITS_5);
& DATABITS_6);
& DATABITS_7);
& DATABITS_8);
& DATABITS_16);
& DATABITS_16X);
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: {
87
&
&
&
&
Label2->Caption = "PARITY_ODD"; Edit2->Text=IntToStr(CommProp.wSettableStopParity PARITY_ODD); break; } case 6: { Label2->Caption = "PARITY_EVEN"; Edit2->Text=IntToStr(CommProp.wSettableStopParity PARITY_EVEN); break; } case 7: { Label2->Caption = "PARITY_MARK"; Edit2->Text=IntToStr(CommProp.wSettableStopParity PARITY_MARK); break; } case 8: { Label2->Caption = "PARITY_SPACE"; Edit2->Text=IntToStr(CommProp.wSettableStopParity PARITY_SPACE); break; } } //koniec switch } // koniec if
&
&
&
&
} //--------------------------------------------------------------------
Dla przykładu rozpatrzmy dwSettableParams typu DWORD, a więc reprezentowany na 32 bitach. Odczytując odpowiednią wartość, przekonaliśmy się, że cała zawarta tam informacja zapisana jest na 7 pierwszych bitach dwSettableParams. Użyliśmy operatora &, aby sprawdzić, czy włączone są poszczególne bity reprezentujące atrybuty związane z konkretnymi parametrami komunikacyjnymi. Patrząc na wartości w postaci binarnej łatwo zorientujemy się, jaki jest aktualny stan logiczny poszczególnych bitów zawartych w tej zmiennej i za co są one odpowiedzialne. wartość bit 7 dwSettableParams
maska
127
0
bit 6
bit 5
Bit 4 bit 3
1
1
1
bit 2
bit 1
bit 0
1
1
1
1
rezultat maskowania
SP_PARITY
1
0
0
0
0
0
0
0
1
SP_BAUD
2
0
0
0
0
0
0
1
0
SP_DATABITS
4
0
0
0
0
0
1
0
0
SP_STOPBITS
8
0
0
0
0
1
0
0
0
SP_HANDSHAKING
16
0
0
0
1
0
0
0
0
SP_PARITY_CHECK
32
0
0
1
0
0
0
0
0
SP_RLSD
64
0
1
0
0
0
0
0
0
88
W analogiczny sposób możemy przetestować wszystkie maski bitowe udostępniane przez COMMPROP, odpowiadające właściwym pozycjom konkretnych bitów. Jeżeli jako rezultat iloczynu bitowego wartości elementu struktury z maską określającą włączony bit otrzymamy wartość 0, oznaczać to będzie, że testowany bit jest wyłączony i dany parametr komunikacyjny nie jest aktualnie dostępny. Bieglejsi w temacie Czytelnicy zapewne już zorientowali się, jakie prezenty oferuje nam ta struktura. Manipulowanie bitami jest tu sprawą dobrania odpowiednich operatorów przesuwania, maskowania i dopełniania. Można np. skasować bity SP_PARITY i SP_BAUD w elemencie dwSettableParams: CommProp.dwSettableParams &= ~(SP_PARITY | SP_BAUD);
Podobnie w jakimś fragmencie aplikacji można zastosować warunek: if ((CommProp.dwSettableParams & (SP_PARITY | SP_BAUD)) == 0) { ... }
który będzie prawdziwy, gdy oba bity będą skasowane. Jednak osobom mniej zaawansowanym w operacjach bitowych odradzałbym jakiekolwiek próby ingerowania w zawartość COMMPROP. Przykładowy algorytm realizujący zamianę liczb z postaci dziesiętnej na binarną można znaleźć w Uzupełnieniu 2. Jeżeli mimo wszystko ktoś zechciałby bliżej zainteresować się tym tematem, powinien odwołać się do opisanej poniżej struktury, zawierającej informacje o stanie konfiguracji danego urządzenia komunikacyjnego. Tabela 5.8. Specyfikacja struktury COMMCONFIG. Typ
Element struktury
Znaczenie
DWORD
dwSize
WORD
wVersion
wersja struktury
WORD
wReserved
zarezerwowane
rozmiar struktury w bajtach
89
Zawartość
należy wpisać
należy odczytać
DCB
dcb
struktura kontroli portu
patrz DCB
szeregowego
DWORD
dwProviderSubType
identyfikacja typu dostarczanej
patrz COMMPROP
usługi komunikacyjnej i, tym samym, wymaganego formatu danych
DWORD
dwProviderOffset
określenie offsetu dla danych
0, jeżeli nie określono
wymaganych przez
typu danych
dostarczyciela usługi komunikacyjnej; offset (tzw. przesunięcie) określony jest zwykle w stosunku do początku struktury
DWORD
dwProviderSize
rozmiar danych (bajty)
zależnie od typu
wymaganych przez
usługi
usługę komunikacyjną (dostarczyciela usługi)
WCHAR
wcProviderData[1]
dane dostarczane wraz
jeżeli ustalono typ usługi:
z usługą (ponieważ
PST_RS232 lub
przewidywane jest w
PST_PARALLELPORT ,
przyszłości uzupełnienie
człon ten jest pomijany;
90
struktury, aplikacja
jeżeli ustalono
powinna używać
PST_MODEM ,
dwProviderOffset
należy odwołać się do
w celu określenia położenia
MODEMSETTINGS
wcProviderData)
Win32 API COMMCONFIG deklaruje następująco: typedef struct _COMM_CONFIG { DWORD dwSize; ... } COMMCONFIG, *LPCOMMCONFIG;
Powyższa deklaracja tworzy dwa nowe słowa kluczowe typu COMMCONFIG (struktura) oraz LPCOMMCONFIG (wskaźnik do struktury). Aktualną konfigurację łącza komunikacyjnego odczytamy, korzystając z funkcji API: BOOL GetCommConfig(HANDLE hCommDev, LPCOMMCONFIG lpCC, LPDWORD lpdwSize);
lpCC wskazuje na strukturę COMMCONFIG, zaś lpdwSize jest wskaźnikiem do 32-bitowej
zmiennej, określającej rozmiar struktury. Bieżącą konfigurację portu komunikacyjnego zapiszemy za pomocą: BOOL SetCommConfig(HANDLE hCommDev, LPBYTE lpCC, DWORD dwSize);
lpCC jest wskaźnikiem do COMMCONFIG, zaś dwSize określa w bajtach rozmiar struktury wskazywanej przez lpCC. Przed przekazaniem tej struktury jako parametru należy do elementu dwSize wpisać wartość równą sizeof(COMMCONFIG).
91
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:
92
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).
93
EV_ERR — wykryto błąd statusu linii. Możliwe są tutaj trzy przypadki: CE_FRAME — błąd protokołu ramki. Bit stopu oczekiwany przez urządzenie odbierające nadszedł za późno lub za wcześnie. Będzie to oznaczać, że nie uzgodniono wcześniej pomiędzy nadajnikiem a odbiornikiem identycznej prędkości transmisji lub(i) sposobu reagowania na bit parzystości, lub(i) liczby bitów danych, lub(i) liczby bitów stopu. CE_OVERRUN — błąd przepełnienia lub przekrywania się danych. Dane przychodzą do portu szybciej niż mogą być fiizycznie pobierane z bufora wejściowego odbiornika. Następuje wówczas przekrywanie się bajtów. Bajt danych znajdujący się w buforze zostanie zamazany przez następny przychodzący bajt. CE_RXPARITY — błąd niezgodności kontroli parzystości pomiędzy nadajnikiem a odbiornikiem. Otrzymywane dane mogą być niekompletne lub zafałszowane. EV_RING — wskaźnik wywołania, tzw. Ring Indicator został odebrany. EV_RLSD — sygnał RLSD (Receive Line Signal Detect) zmienił poziom. EV_RXCHAR — odebrano znak i umieszczono go w buforze wejściowym. EV_RXFLAG — odebrano ostatni znak sterujący i umieszczono go w buforze wejściowym. Znak ten musiał być wcześniej wyspecyfikowany zgodnie ze strukturą DCB. EV_TXEMPTY — ostatni znak z bufora wyjściowego został wysłany. Należy w tym miejscu zwrócić uwagę na fakt, że tak naprawdę powyższe stałe symboliczne reprezentowane są przez pewne maski bitowe, a jako takie podlegają one wszelkim bitowym operacjom logicznym, których przykłady przedstawiliśmy nieco wcześniej, przy okazji prezentacji struktury COMMPROP. maska
Wartość
EV_RXCHAR
1
0
0
0
0
0
0
0
0
1
EV_RXFLAG
2
0
0
0
0
0
0
0
1
0
EV_TXEMPTY
4
0
0
0
0
0
0
1
0
0
EV_CTS
8
0
0
0
0
0
1
0
0
0
EV_DSR
16
0
0
0
0
1
0
0
0
0
EV_RLSD
32
0
0
0
1
0
0
0
0
0
EV_BREAK
64
0
0
1
0
0
0
0
0
0
256
1
0
0
0
0
0
0
0
0
EV_RING
bit 8 bit 7
bit 6
94
bit 5 Bit 4
bit 3 bit 2
Bit 1 bit 0
EV_ERR
128
0
1
0
0
0
0
0
0
0
CE_OVERRUN
2
0
0
0
0
0
0
0
1
0
CE_RXPARITY
4
0
0
0
0
0
0
1
0
0
CE_FRAME
8
0
0
0
0
0
1
0
0
0
Jeżeli np. chcielibyśmy ustalić sposób reakcji na zdarzenie polegające na tym, że aplikacja w jakiś sposób wykryje fakt wysłania ostatniego znaku z bufora wyjściowego oraz dotyczące zmiany poziomu sygnału na linii CTS, wystarczy napisać: GetCommMask(hCommDev, &fdwEvtMask); SetCommMask(hCommDev, EV_TXEMPTY | EV_CTS);
Możliwe są również inne rozsądne kombinacje masek udostępnianych przez fdwEvtMask. Można je stosować w zależności od potrzeb. Zauważmy też, że najlepsze efekty w wykrywaniu ewentualnych błędów statusu linii dają tylko kombinacje masek CE_ z EV_ERR, co zostało zaznaczone w powyższym zestawieniu. Nie będziemy w tym miejscu przedstawiać szczegółowego kodu segmentu konfiguracyjnego aplikacji, gdyż poza ewentualnym wywołaniem przedstawionych wcześniej funkcji nie będzie się on zasadniczo różnił od kodu funkcji obsługi zdarzenia OpenCommClick(), zamieszczonego w wydruku programu RS_01.cpp. Zapewne dodamy tam w przyszłości kilka nowych elementów, które omówione zostaną w kolejnych podrozdziałach.
Win32 API rozróżnia pojęcia event oraz event mask. Zdarzenie (ang. event) określane jest jako zmiana występująca w aktualnym stanie obiektu, będąca źródłem odpowiednich komunikatów przekazywanych do aplikacji lub bezpośrednio do systemu. Reakcja obiektu na wystąpienie zdarzenia udostępniana jest aplikacji dzięki funkcji (procedurze) obsługi zdarzeń (event function lub event procedure), będącej wydzieloną częścią kodu. Maska zdarzenia (ang. event mask) utożsamiana jest z typową maską bitową, która może być wykorzystywana do rozpoznawania lub filtrowania jednego lub więcej komunikatów, będących jedną z metod reakcji obiektu na wystąpienie szczególnego zdarzenia.
95
Segment wysyłający komunikaty Najwyższy czas, abyśmy zapoznali się z jednym ze sposobów nawiązywnia komunikacji z urządzeniem zewnętrznym. Ponieważ nie wiem, czy Czytelnik podejmie próbę komunikacji z innym komputerem, czy też z jakimś konkretnym przyrządem, przedstawię tu metodę ogólną. Dobrym zwyczajem każdego programisty, a już szczególnie osoby stojącej przed problemem nawiązania komunikacji z jakimś urządzeniem za pośrednictwem interfejsów szeregowych jest to, by zawczasu przygotować się na trudności. Otóż istnieje pewna klasa przyrządów, może już nie najnowszych ale jeszcze wykorzystywanych, które mają pewną ciekawą właściwość. Nie można nawiązać z nimi kontaktu, dopóki komputer nie wyśle do urządzenia sygnału: żądanie nadawania. Innymi słowy trzeba uaktywnić linię RTS. Podobnie może też być w przypadku linii DTR (patrz rozdz. 2). Trudności napotkamy też, gdy będziemy musieli zastosować programową kontrolę przepływu danych z wykorzystaniem protokołu XON-XOFF. Może się również zdarzyć, że z jakiś względów będziemy chcieli nagle przerwać transmisję i zechcemy użyć popularnego przycisku Reset. Problemy te można rozwiązać, uzyskując dostęp do rejestrów układu UART. Jednak w Windows nie musimy tego robić. API daje nam gotową funkcję, którą należy tylko umiejętnie wykorzystać: BOOL EscapeCommFunction(HANDLE
hCommDev,
DWORD
dwFunc);
gdzie dwFunc możemy przypisać jedną ze stałych symbolicznych: CLRDTR — linia DTR przechodzi w stan nieaktywny; CLRRTS — linia RTS przechodzi w stan nieaktywny; SETDTR — stan aktywny linii DTR; SETRTS — stan aktywny linii RTS; SETXOFF — wstrzymanie transmisji po wystąpieniu znaku sterującego XOFF; SETXON — przywrócenie transmisji po otrzymaniu znaku sterującego XON; SETBREAK — wstrzymanie transmisji (dane znajdujące się w buforze wyjściowym a jeszcze nie wysłane nie są tracone); CLRBREAK — przywrócenie transmisji znaków. I w tym wypadku predefiniowane stałe reprezentują pewne maski bitowe, których bezpośrednie użycie lub wykorzystanie ich rozsądnych kombinacji może być bardzo pomocne w sterowaniu komunikacją.
maska
wartość bit 4
bit 3 bit 2
96
bit 1
bit 0
SETXOFF
1
0
0
0
0
1
SETXON
2
0
0
0
1
0
SETRTS
3
0
0
0
1
1
CLRRTS
4
0
0
1
0
0
SETDTR
5
0
0
1
0
1
CLRDTR
6
0
0
1
1
0
SETBREAK
8
0
1
0
0
0
CLRBREAK
9
0
1
0
0
1
Większość nowoczesnych przyrządów, z którymi komunikujemy się za pośrednictwem interfejsów szeregowych, nie wymaga już sprawdzania linii RTS i DTR czy stosowania specjalnych protokołów transmisyjnych. Urządzenia te są już same w sobie komputerami. Dlatego konieczność wywoływania EscapeCommFunction() jest w większości przypadków powodowana potrzebą wstrzymania na jakiś czas transmisji (bez utraty danych). API dostarcza dwóch prostszych w użyciu funkcji, których efekt działania jest identyczny jak przy zastosowaniu EscapeCommFunction(), wywołanej odpowiednio z SETBREAK lub CLRBREAK: BOOL SetCommBreak(HANDLE
hCommDev);
oraz BOOL ClearCommBreak(HANDLE
hCommDev);
Zasadniczą częścią segmentu wysyłającego komunikaty do portu szeregowego będzie zdefiniowana w Win32 API funkcja: BOOL WriteFile(HANDLE hCommDev, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);
Ogólnie rzecz biorąc, może ona zapisywać dane do dowolnego urządzenia (w tym również pliku) jednoznacznie wskazanego przez identyfikator hCommDev. W przypadku transmisji szeregowej może być z powodzeniem stosowana zarówno do jej wariantu synchronicznego jak i asynchronicznego. Funkcja ta zapisuje dane do obszaru pamięci (bufora danych) identyfikowanego przez wskaźnik lpBuffer. LPCVOID lpBuffer odpowiada klasycznej deklaracji wskaźnika ogólnego (adresowego) stałej, czyli: const void *Buffer. Rozmiar bufora ustala się w zależności od potrzeb, zasobów pamięci komputera oraz pojemności bufora danych urządzenia zewnętrznego. Następny parametr nNumberOfBytesToWrite określa liczbę bajtów do wysłania, zaś wskaźnik lpNumberOfBytesWritten wskazuje liczbę bajtów realnie wysłanych. Aby nie doszło do przekroczenia rozmiaru bufora danych wyjściowych, liczba bajtów faktycznie wysłanych może być mniejsza niż nNumberOfBytesToWrite, dlatego funkcja
97
umieszcza ją w zmiennej lpNumberOfBytesWritten stanowiącej przedostatni parametr. W ten sposób działa mechanizm ochrony dla wysyłanych danych. Ostatni parametr lpOverlapped jest wskaźnikiem struktury OVERLAPPED. Zawiera ona informacje o dodatkowych metodach kontroli transmisji, polegających na sygnalizowaniu aktualnego położenia pozycji wskaźnika transmitowanego pliku. Większość elementów tej struktury jest zarezerwowana przez system operacyjny. Jeżeli jednak chcielibyśmy skorzystać z jej usług należałoby w funkcji CreateFile() parametrowi dwFlagsAndAttributes przypisać znacznik FILE_FLAG_OVERLAPPED (tzw. nakładane wejście-wyjście). Trzeba wówczas samodzielnie (niemalże „ręcznie”) sygnalizować pozycję pliku w pamięci. Na potrzeby naszych rozważań nie ma to większego sensu, dlatego bez jakichkolwiek wyrzutów sumienia wskaźnik lpOverlapped zignorujemy, przypisując mu NULL. Zakładając, że celem naszym jest wysłanie tylko jednego znaku lub ciągu znaków, np. komunikatu czy nawet pliku, rozważania na temat konstrukcji segmentu wysyłającego moglibyśmy już w zasadzie zakończyć. Zastanówmy się jednak, co się stanie, jeżeli będziemy musieli za pomocą naszej aplikacji skierować do portu szeregowego dwa (lub więcej) następujące po sobie pojedyncze znaki w krótkim czasie, ale tak by odbiornik nie potraktował ich jako jednego ciągu. Zgoda odbiornika na przyjęcie kolejnego komunikatu może, ale nie musi być wymagana. Mówiąc krótko, odbiornik będzie musiał rozróżnić wysyłane znaki zarówno pod względem treści jak również czasu przybycia. Można próbować różnych sposobów, takich jak sztuczne wstrzymywanie procesu po wysłaniu pierwszego znaku czy chociażby użycie komponentu TTimer. Jest jednak prostszy sposób — możemy skorzystać z funkcji API: BOOL WaitCommEvent(HANDLE hCommDev, LPDWORD lpfdwEvtMask, LPOVERLAPPED lpOverlapped);
Wartości, na jakie może wskazywać lpfdwEvtMask, są formalnie identyczne z opisanymi wcześniej dla funkcji SetCommMask(), zaś wskaźnikowi do struktury OVERLAPPED należy po prostu przypisać NULL z powodu, który został już omówiony przy okazji prezentacji WriteFile(). Nietrudno zauważyć, że użycie w programie funkcji WaitCommEvent() będzie miało sens o tyle, o ile wcześniej (np. w warstwie konfiguracyjnej) użyjemy SetCommMask(). Aby uzyskać informację np. o tym, że ostatni znak z bufora komunikacyjnego został wysłany, a sygnał na linii CTS zmienił poziom (lub nastąpiło inne zaprogramowane zdarzenie) i można przystąpić do dalszej transmisji, w warstwie konfiguracyjnej aplikacji należy zapisać: //--otwarcie portu i ustawienie jego parametrów-------void __fastcall TForm1::OpenCommClick(TObject *Sender) { ... DWORD fdwEvtMask; ... GetCommMask(hCommDev, &fdwEvtMask); SetCommMask(hCommDev, EV_TXEMPTY | EV_CTS); ... }
Zwróćmy w tym miejscu uwagę na bardzo ważną rzecz, mianowicie na sposób, w jaki zainicjowaliśmy funkcję SetCommMask(). Alternatywnym sposobem przypisania parametrowi wejściowemu fdwEvtMask wartości EV_TXEMPTY lub np. EV_CTS będzie następująca konstrukcja:
98
//--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
99
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);
100
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
101
return FALSE; }
Jeżeli zdecydujemy się na taki zapis, wówczas jednym z parametrów wywoływanej funkcji API WriteFile() musi być jawnie użyty bufor danych wyjściowych Buffer_O. Standardowo argumenty funkcji w C++ przekazujemy przez wartość. W ten sposób powodujemy utworzenie kopii argumentu w wywoływanej funkcji, zapobiegając tym samym możliwości zmodyfikowania jego początkowej wartości. W przypadku naszej funkcji Write_Comm() będzie to nNumberOfBytesToWrite, czyli liczba bajtów do wysłania będąca jednocześnie parametrem funkcji Win32 API WriteFile(). Jeżeli z kolei funkcja ma zmodyfikować wartości zmiennych będących jej argumentami, parametry powinny być jawnie zadeklarowane jako wskaźniki. Jest jeszcze wiele rzeczy, których nie uwzględniliśmy. Naprawdę niezawodna aplikacja powinna mieć parę dodatkowych zabezpieczeń. Wszystkie je omówimy po kolei w następnych podrozdziałach. Obecnie ważne jest dla nas to, że użyliśmy wszystkich omówionych do tej pory funkcji Win32 API, poznaliśmy w jakiej kolejności należy je wywoływać. Po skompletowaniu całego programu przekonasz się, że mimo swojej prostoty będzie on funkcjonalny. Teraz, kiedy poznaliśmy, w jaki sposób można coś „powiedzieć” do urządzenia, czas najwyższy, abyśmy nauczyli się „słuchać” i „rozumieć” jego odpowiedzi.
Segment odbierający komunikaty
DRUGIE PRAWO SODDA: Wcześniej czy później i tak musi nastąpić najgorszy z możliwych splotów okoliczności. Uzupełnienie: Każdy system musi być zaprojektowany w taki sposób, aby stawić czoła najgorszemu z możliwych splotów okoliczności.
Każdy, kto kiedykolwiek interesował się komunikacją komputerową wie, że występowanie błędów w tym procesie jest czymś nieuniknionym i poniekąd naturalnym. Ustalenie sposobu reagowania na wystąpienie błędu w czasie transmisji szeregowej jest zawsze bardzo istotnym elementem aplikacji. Przypominamy sobie z poprzednich rozdziałów, że istnieje pewien sposób zabezpieczenia danych przed zafałszowaniem w czasie ich przekazu. Sposobem tym jest kontrola bitu parzystości. Jest on jednak mało efektywny. Korzystając z zasobów struktury DCB, można w pewnym stopniu zabezpieczyć się przed odbieraniem przekłamanych danych. Wystarczy w części konfiguracyjnej aplikacji odwołać się do jednego ze znaczników DCB, a mianowicie do fAbortOnError (patrz tabela 5. 5.), pisząc: dcb.fAbortOnError = TRUE;
102
co spowoduje wstrzymanie wykonywania wszelkich operacji wysyłania i odbierania danych przy wykryciu jakiegokolwiek błędu w komunikacji z portem szeregowym. Reinicjalizacja odbioru i nadawania poprzez port komunikacyjny identyfikowany przez hCommDev nastąpi po wywołaniu funkcji: BOOL ClearCommError(HANDLE hCommDev, LPDWORD lpErrors, LPCOMSTAT lpStat);
gdzie: lpErrors jest wskaźnikiem do 32-bitowej danej typu DWORD, reprezentującej jeden z typów
błędów:
CE_BREAK — wykryto przerwanie połączenia. CE_DNS — urządzenie przyłączone do łącza równoległego nie zostało określone (Win 9x). CE_FRAME — wystąpił błąd protokołu ramki danych. CE_IOE — podczas komunikacji nastąpił jakiś błąd wejścia-wyjścia ( Input-Output). CE_MODE — żądanie nadawania nie jest podtrzymywane lub identyfikator portu (urządzenia) ma błędną specyfikację. CE_OOP — urządzenie przyłączone do łącza równoległego sygnalizuje brak papieru, co jest typowe dla drukarek, faksów, niektórych faksmodemów (Win 9x). CE_OVERRUN — nastąpiło całkowite wypełnienie wejściowego bufora danych. Następny znak będzie zignorowany. CE_PTO — został przekroczony czas oczekiwania na połączenie z portem równoległym, tzw. przeterminowanie czasu połączenia (Win 9x). CE_RXOVER — bufor wejściowy został przepełniony. Albo nie ma w nim już fizycznie miejsca, albo został odebrany jakiś znak następujący po znaku końca pliku EOF. CE_RXPARITY — wykryto błąd niezgodności kontroli parzystości. CE_TXFULL — próba transmisji znaku przy całkowitym wypełnieniu bufora wyjściowego. Zbiór wartości, na które wskazuje lpErrors, można z powodzeniem traktować jako maski bitowe z możliwością wykonywania na nich odpowiednich działań. Zawarta tam pełna informacja może być aktualnie zapisana na 16 bitach.
maska
wartość
b
b
b
b
103
b
b
b b b b b b b B b b 1 0
15
14
13
12
11
10
9 8 7 6 5 4 3 2
CE_RXOVER
1
0
0
0
0
0
0
0 0 0 0 0 0 0 0 0 1
CE_OVERRUN
2
0
0
0
0
0
0
0 0 0 0 0 0 0 0 1 0
CE_RXPARITY
4
0
0
0
0
0
0
0 0 0 0 0 0 0 1 0 0
CE_FRAME
8
0
0
0
0
0
0
0 0 0 0 0 0 1 0 0 0
CE_BREAK
16
0
0
0
0
0
0
0 0 0 0 0 1 0 0 0 0
CE_TXFULL
256
0
0
0
0
0
0
0 1 0 0 0 0 0 0 0 0
CE_PTO
512
0
0
0
0
0
0
1 0 0 0 0 0 0 0 0 0
CE_IOE
1024
0
0
0
0
0
1
0 0 0 0 0 0 0 0 0 0
CE_DNS
2048
0
0
0
0
1
0
0 0 0 0 0 0 0 0 0 0
CE_OOP
4096
0
0
0
1
0
0
0 0 0 0 0 0 0 0 0 0
32768
1
0
0
0
0
0
0 0 0 0 0 0 0 0 0 0
CE_MODE
Widzimy więc, że funkcja ClearCommError() jest przydatna nie tylko do obsługi portu szeregowego. W programie sterującym łączem szeregowym można ją wywołać jedynie z parametrami domyślnymi. Będzie wówczas usuwała, w miarę swojej możliwości, wszystkie ewentualne błędy pojawiające się w trakcie transmisji. ClearCommError(hCommDev, &Errors, &Stat);
Wskaźnik lpStat wskazuje na strukturę COMSTAT. Chcąc wyjaśnić jego znaczenie, musimy odwołać się do tej struktury będącej również częścią Win32 API. Zawiera ona informacje o aktualnych zasobach i dodatkowych sposobach kontroli wybranego łącza szeregowego. Tabela 5.9 przedstawia znaczenie zawartej tam informacji. Tabela 5.9. Informacje zawarte w elementach struktury COMSTAT Typ Element struktury
Właściwości
Wartość zwracana , Znaczenie
DWORD
fCtsHold
Określa, czy transmisja jest wstrzymywana do czasu
104
TRUE / 1 Transmisja jest wstrzymana.
odebrania przez komputer sygnału CTS.
DWORD
fDsrHold
FALSE / 0 Transmisja nie jest wstrzymywana.
Określa, czy transmisja jest
TRUE / 1
wstrzymywana do czasu
Transmisja jest wstrzymana.
odebrania przez komputer sygnału DSR.
DWORD
fRlsdHold
FALSE / 0 Transmisja nie jest wstrzymywana.
Określa, czy transmisja jest
TRUE / 1
wstrzymywana do czasu załączenia stanu aktywnego na linii DCD.
Transmisja jest wstrzymana. FALSE / 0
Linia DCD oznacza też RLSD Transmisja nie jest wstrzymywana. (Received Line Signal Detect).
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
TRUE / 1
wstrzymywana po wysłaniu
Transmisja jest wstrzymana.
znaku sterującego XOFF.
Następnym wysłanym znakiem będzie XON, bez względu na aktualnie transmitowany znak. FALSE / 0 Transmisja nie jest wstrzymywana.
105
DWORD
fEof
Określa, czy został wykryty
TRUE / 1
znacznik końca pliku EOF.
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
należy je odczytać
w wyniku transmisji szeregowej, ale jeszcze nie przeczytanych
DWORD
cbOutQue
liczba bajtów danych pozostających do wysłania
106
należy je odczytać
Komentarz: Czy nie zapomniano tu podać znaczenia zaznaczonej wartości?
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():
107
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
108
trzema lub nawet dwoma parametrami formalnymi, co już w pełni będzie usprawiedliwiało użycie konwencji przekazywania parametrów __fastcall. Buffer_I : ARRAY[0..cbInQueue] of Char; // bufor wejściowy ... int __fastcall Read_Comm(HANDLE hCommDev, LPDWORD lpNumberOfBytesRead, DWORD Buf_Size) { ... { ... ReadFile(hCommDev, &Buffer_I[0], nNumberOfBytesToRead, lpNumberOfBytesRead, NULL); } else *lpNumberOfBytesRead = 0; return TRUE; }
Korzystając z tego zapisu w wywołaniu funkcji API ReadFile(), należy jawnie odwołać się do bufora danych wejściowych Buffer_I. Używając w deklaracji funkcji konwencji __fastcall należy się spodziewać, że trzy pierwsze parametry funkcji mogą być umieszczone w rejestrach EAX, EDX oraz ECX (jeżeli oczywiście jest to możliwe). Parametry 8-bitowe typu char (signed oraz unsigned) mogą być umieszczane w AL, DL, CL, parametry 16-bitowe typu short (signed oraz unsigned) w AX, DX, CX, zaś 32-bitowe typu int/long (signed oraz unsigned) w rejestrach EAX, EDX, ECX — zob. rozdział 4. Rejestry nie będą używane, jeżeli parametrami funkcji będą dane zmiennopozycyjne lub struktury. Parametry tego typu są odkładane na stosie.
Na zakończenie tej części naszych rozważań celowym będzie skomentowanie faktu umieszczenia funkcji ClearCommError() w segmencie odbierającym komunikaty przychodzące do portu szeregowego. Zapewne nie ma wśród nas nikogo, kto nie rozegrałby kiedykolwiek meczu piłkarskiego. Zawsze w lepszej sytuacji jest zawodnik podający piłkę, odbierający musi bardzo uważać, żeby dokładnie ją przyjąć i dalej rozegrać. Dokładnie tak samo jest przy komunikacji komputerowej. Wiem z własnego doświadczenia, że 90% błędów powstaje niestety po stronie odbierającego dane. Bardzo łatwo można się przekonać, że funkcja Read_Comm() bez ClearCommError() w wielu przypadkach po prostu by nie działała! Używaj jej zawsze, nawet jeżeli podstawiłeś dcb.fAbortOnError = FALSE (patrz tabela 5.5).
Przykładowa aplikacja
109
Zanim przejdziemy do bardziej ambitnych rozważań, zapoznamy się z ogólną metodą konstruowania w C++Builderze algorytmów pomocnych w realizacji transmisji szeregowej. Kompletnym przykładem aplikacji wykorzystującej skonstruowane przez nas funkcje zapisu i odczytu danych będzie projekt \KODY\BUILDER\RS_03\p_RS_03.bpr. Działanie aplikacji będzie polegało na wysłaniu odpowiedniego komunikatu do przyrządu pomiarowego oraz wyświetleniu i zapisaniu na dysku odpowiedzi. Z czysto praktycznych względów zastosujemy tu najprostszą metodę zapisu danych do pliku. Wygląd głównego formularza oraz jego kod RS_03.cpp przedstawione są poniżej.
Rysunek 5.4. Formularz główny projektu p_RS_03.bpr
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ż
110
zadeklarowany obszar pamięci (bufor danych). W tym przykładzie zadeklarowałem nieco przesadnie bufor danych o rozmiarze 64 bajtów zarówno do nadawania jak i odbioru. Ogólnie rzecz biorąc liczba przesyłanych bajtów może być całkowicie dowolna. Najczęściej używanymi są: 1, oznaczające przesłanie jednego znaku (nie buforowane) oraz 2, 8, 16, 32,..., 512, 1024 i 2048, co odpowiada fizycznemu rozmiarowi bloku danych akceptowanemu przez większość nowoczesnych urządzeń zewnętrznych. Ciąg znaków wskazanych przez query zostanie skopiowany do obszaru pamięci, którego pierwszy znak jest wskazany przez bufor danych wyjściowych Buffer_O. Czynność ta zostanie wykonana za pomocą znanej funkcji strcpy(Buffer_O, query). Rezultatem będzie dana typu char *, wskazująca pierwszy znak obszaru pamięci, do którego wykonano kopiowanie. Wydruk 5.3. Kod formularza aplikacji realizującej transmisję szeregową //----RS_03.cpp------------//--- kompilować z borlndmm.dll oraz cc3250mt.dll -------------#include #include <stdio.h> #pragma hdrstop #include "RS_03.h" #pragma package(smart_init) #pragma resource "*.dfm" #define cbOutQueue 64 #define cbInQueue 64
//rozmiar bufora danych wyjściowych //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,
111
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);
112
dcb.DCBlength = sizeof(dcb);
// aktualny rozmiar // struktury DCB
GetCommState(hCommDev, &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;
// wybór prędkości transmisji
//--parametry komunikacyjne------dcb.Parity = ODDPARITY; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 7;
// ustawienie parzystości // bity stopu // 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
113
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.
114
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.
maska
wartość
bit 4
bit 3
bit 2
bit 1
bit 0
PURGE_TXABORT
1
0
0
0
0
1
115
PURGE_RXABORT
2
0
0
0
1
0
PURGE_TXCLEAR
4
0
0
1
0
0
PURGE_RXCLEAR
8
0
1
0
0
0
Zauważmy też, że PurgeComm() można używać „profilaktycznie” w różnych częściach aplikacji, zależnie od naszych potrzeb. Należy tylko pamiętać, że wywołanie jej z PURGE_TXCLEAR lub PURGE_TXABORT ma sens jedynie przed odczytem danych, zaś z PURGE_RXCLEAR lub PURGE_RXABORT tylko przed wysłaniem danych. 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:
116
if (hCommDev > 0) { ... if (Number_Bytes_Read > 0) { ...
można oczywiście znacznie uprościć, pisząc je w postaci: if (hCommDev) { ... if (Number_Bytes_Read) { ...
Podobną sytuację będziemy mieli, jeżeli skorzystamy z instrukcji warunkowej if przy jawnym sprawdzaniu rezultatu wykonania funkcji API, zwracających wartość TRUE (1) lub FALSE (0). Ogólnie nie jest to wymagane i stosowane przez nas zapisy w większości wypadków można znacznie uprościć, np. zamiast if(WriteFile(...)>0) lub if(WriteFile(...)==TRUE), pisząc po prostu if(WriteFile(...)).W tym oraz dalszych przykładach jawne sprawdzanie podobnych warunków zostało zachowane również w celu utrzymania większej przejrzystości kodu.
Podsumowanie Czytając niniejszy podrozdział dowiedzieliśmy się już sporo na temat podstawowych sposobów programowej realizacji transmisji szeregowej w środowisku Windows za pomocą C++Buildera. Umiemy już wykorzystywać niektóre struktury oraz funkcje oferowane nam przez Win32 API, obsługujące operacje wejścia-wyjścia portu szeregowego. Dzięki zaznajomieniu się z notacją węgierską znaczenie wskaźników i zmiennych stosowanych w API przestało być dla nas tajemnicą. Poznaliśmy też, w jaki sposób można kontrolować ewentualne błędy występujące w trakcie transmisji oraz jak należy minimalizować ich wpływ na działanie aplikacji. Stworzyliśmy również programy testujące łącze szeregowe oraz inny program realizujący już prawdziwą transmisję. Wszystkie one zostały przedstawione w taki sposób, aby można było je samodzielnie modyfikować i ewentualnie uzupełnieniać. Dzięki temu wiemy, jakie wymagania będą w przyszłości stały przed tego typu aplikacjami.
Ćwiczenia 1.
Zmodyfikuj formularz oraz kod programu RS_01.cpp tak, aby można było odczytać kompletną informację o aktualnych ustawieniach wybranego portu szeregowego.
117
2.
Zmodyfikuj przykładowy program RS_03.cpp w taki sposób, aby można było samodzielnie po jego uruchomieniu określić nazwę pliku przechowującego odebrane dane oraz ścieżkę dostępu do niego. Postaraj się wykorzystać jedynie standardowe funkcje C++ podtrzymywane w Windows oraz komponent typu TEdit.
3.
Na podstawie projektu p_RS_03.bpr stwórz aplikację, w której funkcjom Write_Comm() oraz Read_Comm() będą przyporządkowane oddzielne zdarzenia. Spróbuj samodzielnie zaprojektować właściwe relacje pomiędzy nimi.
118
Wykorzystanie elementów Win32 API w C++ Builder. Część II W trakcie tego podrozdziału zapoznamy się z kolejnymi sposobami realizacji transmisji szeregowej poprzez interfejs RS 232C. Zostanie pokazany odmienny sposób odwoływania się do skonstruowanych przez nas funkcji Read_Comm() oraz Write_Comm(). Omówimy pewne aspekty transmisji nie buforowanej, pokażemy, jak można przesyłać i odbierać pliki oraz zapoznamy się z pewnymi dodatkowymi metodami kontroli poprawności działania użytych przez nas funkcji. Zobaczymy też, jak można czasowo próbkować port szeregowy, wykorzystując w tym celu komponent TTimer.
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(). Na rysunku 5.5 zaprezentowano wygląd formularza naszej aplikacji \KODY\BUILDER\RS_04\p_RS_04.bpr, zaś wydruk 5.4 przedstawia kompletny kod jej
głównego modułu RS_04.cpp. Śledząc go ktoś dociekliwy na pewno zauważy, że sposób użycia
119
TransmitCommChar() bardzo przypomina sposób wykorzystania makrowywołania rs_send()
zaprezentowanego w rozdziale 4. (patrz wydruk 4.1). Rysunek 5.5. Formularz projektu p_RS_04.bpr
Wydruk 5.4. Kod formularza aplikacji realizującej nie buforowaną transmisję szeregową //--- kompilować z borlndmm.dll oraz cc3250mt.dll -------------//----RS_04.cpp------------#include #include <stdio.h> #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(); } //--------------------------------------------------------------------
120
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; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 7;
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia flag sterujących DCBdcb.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)
121
{ 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); } //--------------------------------------------------------------------
122
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łączwyłą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. Wydruk 5.5. Zawartość pliku RS_04.h //-------------------------------------------------------------------#ifndef RS_04H #define RS_04H //-------------------------------------------------------------------#include #include #include <StdCtrls.hpp> #include #include <Buttons.hpp> #include #include
123
#include <ExtCtrls.hpp> //-------------------------------------------------------------------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 TSpeedButton TSpeedButton TSpeedButton TSpeedButton void void void void void void void void
*SpeedButton1; *SpeedButton2; *SpeedButton3; *SpeedButton4; *SpeedButton5;
__fastcall __fastcall __fastcall __fastcall __fastcall __fastcall __fastcall __fastcall
CloseCommClick(TObject *Sender); OpenCommClick(TObject *Sender); SendFileClick(TObject *Sender); SpeedButton1Click(TObject *Sender); SpeedButton2Click(TObject *Sender); SpeedButton3Click(TObject *Sender); SpeedButton4Click(TObject *Sender); 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
124
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 Czytając poprzednie podrozdziały 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(). Tabela 5.10. Informacje zawarte w strukturze COMMTIMEOUTS Typ
DWORD
Element struktury
ReadIntervalTimeout
Właściwości
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.
125
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.
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.
126
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;
127
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(). Powyższe deklaracje przedstawiłem w celu zachowania całości opisu tematu, ponadto mogą okazać się Czytelnikom w przyszłości pomocne. 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.
128
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.
129
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()1, 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)); ... }
1
Funkcje _lopen(), _lread(), _lwrite(), _lclose() niekiedy określa się mianem tzw. obsolete functions, czyli funkcji przestarzałych. Prostota ich użycia powoduje jednak, iż są często stosowane nie tylko przy operacjach dyskowych w aplikacjach obsługujących porty komunikacyjne. Między innymi z tego powodu zostały zachowane w Win32.
130
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 opisanych w rozdziale 3. 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ę, której wygląd przedstawiony jest na rysunku 5. 6, 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)
131
{ 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
132
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 FileListBox1C-łańcucha identyfikującego właściwość FileName obiektu 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 32bitowej 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. //----------wysyłanie pliku-----------------------------void __fastcall TForm1::SendClick(TObject *Sender) { DWORD FileSizeHigh; ProgressBar1->Max=0;
133
... 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. Kompletny kod głównego modułu RS_05.cpp projektu \KODY\BUILDER\RS_05\p_RS_05.bpr realizującego transmisję plików przedstawiony jest na wydruku 5.6.
Rysunek 5.6. Formularz główny działającego projektu p_RS_05.bpr
134
Wydruk 5.6. 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 #define cbInQueue 1024
//rozmiar bufora danych wyjściowych //rozmiar bufora danych wejściowych
//-------------------------------------------------------------------TForm1 *Form1; HFILE char char DWORD
hfile_s; Buffer_O[cbOutQueue]; Buffer_I[cbInQueue]; Number_Bytes_Read;
// identyfikator pliku źródłowego // bufor danych wyjściowych // bufor danych wejściowych // 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;
135
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) lpFileName = "COM1"; if (CheckBox2->Checked == TRUE) lpFileName = "COM2";
136
// wybór portu
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) dcb.BaudRate = CBR_1200; if (CheckBox4->Checked == TRUE) dcb.BaudRate = CBR_19200; dcb.Parity = ODDPARITY; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 7;
// wybór prędkości // transmisji
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia flag sterujących DCBdcb.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);
137
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) Memo3->Text = Buffer_I;
// jeżeli odebrano jakieś bajty
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().
138
Sposób ich użycia jest bardzo dokładnie opisany (wraz z przykładami) w plikach pomocy, dostępnych zarówno w Builderze jak i w Delphi2, więc nie będziemy ich tu przepisywać.
Wykorzystanie komponentu TTimer Być może niektórzy Czytelnicy 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 polegajace 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)
2
Pisząc w Delphi, możemy też skorzystać z wielce użytecznych procedur: BlockRead() oraz BlockWrite().
139
{
// //
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! Wszystkie 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;
140
CutText->Hint = "Wytnij"; CutText->ShowHint = TRUE; CleanBuffers->Hint = "Wyczyść bufory"; CleanBuffers->ShowHint = TRUE; ReceiveFileSave->Hint = "Zapisz otrzymane"; ReceiveFileSave->ShowHint = TRUE; }
Sposób rozmieszczenia na ekranie poszczególnych komponentów, z których będzie korzystać nasza aplikacja, pokazano na rysunku 5.7. Rysunek 5.7. Sposób rozmieszczenia poszczególnych komponentów w aplikacji realizującej transmisję szeregową z czasowym próbkowaniem łącza
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) {
141
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
142
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. Na rysunku 5.8 pokazano wygląd naszej aplikacji, której projekt znajduje się w katalogu \KODY\BUILDER\RS_06\p_RS_06.bpr . Wydruk 5.7 przedstawia kompletny kod jej głównego
modułu. Rysunek 5.8. Formularz główny projektu p_RS_06.bpr po uruchomieniu
Wydruk 5.7. 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
143
#define cbInQueue 1024 #define TIMER_INTERVAL 1
//rozmiar bufora danych wejściowych //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);
144
} 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; }
145
//-------------------------------------------------------------------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 ++)
146
{ 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) dcb.BaudRate = CBR_1200; if (CheckBox4->Checked == TRUE) dcb.BaudRate = CBR_19200; dcb.Parity = ODDPARITY; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 7;
// wybór prędkości
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia flag sterujących DCBdcb.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);
147
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) {
148
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) {
149
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()
150
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
151
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, zresztą do tego tematu jeszcze powrócimy w następnych rozdziałach. Na rysunku 5.9 zaprezentowano formularz projektu znajdującego się na załączonym krążku CD \KODY\BUILDER\RS_07\p_RS_07.bpr, obsługującego przyrząd dokonujący cyklicznego odczytu aktualnie mierzonej temperatury (w tym przypadku w stopniach Celsjusza). Wydruk 5.8 przedstawia zastosowany przeze mnie algorytm. Rysunek 5.9. Formularz główny projektu p_RS_07.bpr
Wydruk 5.8. 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"
152
#pragma package(smart_init) #pragma link "CSPIN" #pragma resource "*.dfm" #define cbOutQueue 16 #define cbInQueue 16
//rozmiar bufora danych wyjściowych //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); }
153
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) dcb.BaudRate=CBR_300;
154
// wybór prędkości transmisji
if (CheckBox4->Checked == TRUE) dcb.BaudRate=CBR_1200; if (CheckBox5->Checked == TRUE) dcb.BaudRate=CBR_9600; dcb.Parity = ODDPARITY; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 7;
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia flag sterujących DCBdcb.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)
155
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. W książce tej poruszamy temat komunikacji komputerowej poprzez interfejs RS 232C. Programy pisane przez nas mogą komunikować się z różnymi urządzeniami zewnętrznymi.
156
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; }
157
Przetestowałem nasz program z uwzględnieniem tej modyfikacji. W trakcie pomiaru odłączyłem miernik. Aplikacja wcale się nie zawiesiła, brzęczek (funkcja Beep()) był dalej aktywny, natomiast w polu edycji pojawił się następujący komunikat: Rysunek 5.10. Działanie aplikacji w momencie odłączenia przyrządu pomiarowego
Już teraz wiemy, że urządzenie zostało po prostu wyłączone i należy jakoś zareagować na zaistniałą sytuację. Odczytałem tu jedynie liczbę reprezentującą ostatnio wykryty błąd. Ktoś mógłby postąpić bardziej wyszukanie, wyświetlając np. odpowiedni komunikat czy uruchamiając jakiś wymyślny sygnał dźwiękowy. Ważne jest jednak to, że w prosty sposób możemy zdiagnozować nawet tak poważny błąd w transmisji jak jej zatrzymanie. Wszystko wraca do normy po powtórnym włączeniu miernika. 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
158
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() w kontekście naszego programu mógłby wyglądać tak, jak pokazano niżej. 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;
W tym wypadku, jeżeli miernik z jakiś przyczyn przestanie odpowiadać, na ekranie ujrzymy pomocną informację: Rysunek 5.11. Informacja pojawiająca się w trakcie działania aplikacji w przypadku utraty możliwości czytania z urządzenia
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.
159
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ć
160
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ę: Rysunek 5.12. Informacja otrzymywana przez aplikację przy próbie otwarcia pliku wykorzystywanego przez inny trwający obecnie proces 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ść.
161
Podsumowanie W podrozdziale omówiliśmy kolejne, podstawowe funkcje pomocne w procesie tworzenia oprogramowania komunikacyjnego. Czytając o nich poznaliśmy zarówno zalety jak i wady niektórych komponentów przy wyświetlaniu informacji odbieranych z portu szeregowego. Nabraliśmy też pewnych doświadczeń, projektując aplikacje przesyłające i odbierające pliki. Zapoznaliśmy się też z praktycznym wykorzystaniem komponentu TTimer, za pomocą którego możemy próbkować wybrany port szeregowy w poszukiwaniu nadchodzących danych. Większość wspomnianych tematów potraktowaliśmy w sposób dosyć ogólny, choć w miarę kompletny. Zauważyliśmy też zapewne, że w przyszłości coraz mniej uwagi będziemy zmuszeni poświęcać czasochłonnemu „drucikowaniu”, czyli łączeniu jakiś przewodów i domyślaniu się, który sygnał jest za co odpowiedzialny. Za to w coraz większym stopniu zaczyna absorbować nas sam proces tworzenia oprogramowania. Po przeczytaniu tego fragmentu książki nie powinno już stanowić dla nas problemu przesłanie poprzez interfejs RS 232C pojedynczego znaku, ciągu znaków czy nawet dosyć sporego pliku.
Ćwiczenia 1.
Zmodyfikuj przedstawiony na wydruku 5.6 kod programu RS_05.cpp tak, aby korzystał jedynie z funkcji CreateFile() oraz ReadFile() przy przesyłaniu plików.
2.
Zmodyfikuj przedstawiony na wydruku 5.7 kod programu RS_06.cpp tak, aby istniała możliwość przesyłania zaznaczonego (i ewentualnie skopiowanego do schowka) fragmentu pliku.
3.
Zmodyfikuj przedstawiony na wydruku 5.8 kod programu RS_07.cpp tak, aby zapisywał na dysku cyklicznie odbierane dane. Postaraj się nie tracić informacji w momencie odłączenia przyrządu lub innego komputera.
162
Wykorzystanie elementów Win32 API w Delphi. Część I 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)1 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;
1
Dalej należałoby formalnie posługiwać się pojęciem typu TDCB, czyli pewnego strukturalnego typu danych, ale, by niepotrzebnie nie komplikować dalszych wyjaśnień, pozostaniemy przy określeniu struktura DCB. Ta subtelność widoczna jest jedynie w deklaracji.
163
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ści2. 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;
2
Nazwy takie jak: fParity czy fOutxCtsFlow nie są podtrzymywane w Delphi, dlatego w programie będą traktowane jako stałe, które uprzednio należy odpowiednio zadeklarować.
164
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 wydruku 5.9 (dotyczy on modułu RS_11.pas projektu aplikacji \KODY\DELPHI\RS_11\p_RS_11.dpr, 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().
165
Komentarz: Czy można to słowo usunąć albo zastąpić innym (niepowtarzalny, specyficzny)?
Rysunek 5.13. Formularz główny projektu p_RS_11.dpr
Wydruk 5.9. 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;
166
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;
167
//-------------------------------------------------------------------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
168
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);
169
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)3. 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:
3
Jeżeli zajdzie potrzeba uwzględnienia wymienionego protokołu, należy jawnie odwołać się do DCB.
170
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()
171
dcb.DCBlength := sizeof(dcb); GetCommState(hCommDev, dcb); // dcb.Flags BuildCommDCB('9600,
O,
8,
1, p', dcb);
SetCommState(hCommDev, dcb)
end;
// 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;
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, LPCOMMTIMEOUTS
LPDCB lpDCB, 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:
172
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;
173
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
174
Komentarz: Składnia!!!
szeregowy z poziomu Delphi. W tym celu zaprojektujemy aplikację, której projekt zamieszczony jest na dołączonym krążku CD \KODY\DELPHI\RS_12\p_RS_12.dpr. 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 na rysunku 5.14 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. Rysunek 5.14. Formularz główny projektu p_RS_12.dpr
175
Wydruk 5.10. 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
176
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:
177
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
178
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';
179
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ącokonfiguracyjnego. 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.
180
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
181
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ę Czytelnikom 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;
182
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
183
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. Na rysunku 5.15 pokazano wygląd zaprojektowanego przeze mnie formularza, którego projekty wykorzystujące odpowiednio procedury i funkcje znajdują się w katalogach \KODY\DELPHI\RS_13\p_RS_13.dpr oraz \KODY\DELPHI\RS_14\p_RS_14.dpr. 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().
184
Rysunek 5.15. Formularz główny projektów p_RS_13.dpr oraz p_RS_14.dpr
Wydruk 5.11. 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;
185
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
186
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.
187
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. Wydruk 5.12. 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;
188
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,
189
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 DCBdcb.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
190
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.
Podsumowanie Głównym celem tego fragmentu książki było zaprezentowanie ogólnych zasad poruszania się w warstwie komunikacyjnej Borland Delphi Run-Time Library Win32 API Interface Unit. Zauważyliśmy, że korzystanie w Delphi z bloku kontroli portu szeregowego opiera się na nieco innych zasadach niż w C++Builderze. Jednak po zapoznaniu się z przedstawionymi tu przykładami nie powinno to stanowić już dla nas tajemnicy. Uważny Czytelnik mógł się też przekonać, że czasami warto rozróżniać elementy składowe struktur. Jeżeli rozróżnienia takiego dokonamy już na poziomie C++, w innych kompilatorach nie będziemy mieli żadnych kłopotów. Zostały również zaprezentowane różne możliwe sposoby konstrukcji programu komunikacyjnego właściwe dla Object Pascala. Nauczyliśmy się pisać zarówno odpowiednie funkcje, jak i procedury pomocne w transmisji ciągów znaków. Użycie tych elementów we właściwym miejscu aplikacji nie powinno sprawić nam już poważniejszych trudności.
Ćwiczenia
191
1.
Na podstawie projektu p_RS_12.dpr oraz jego kodu źródłowego RS_12.pas przetestuj i wyświetl wszystkie usługi komunikacyjne wraz z ich atrybutami, do których będziesz miał aktualnie dostęp.
2.
Zmodyfikuj kody RS_13.pas oraz RS_14.pas, tak aby można było odebrany komunikat zapisać na dysku.
192
Wykorzystanie elementów Win32 API w Delphi. Część II 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. Rysunek 5.16 przedstawia wygląd naszego formularza, którego projekt został umieszczony w katalogu \KODY\DELPHI\RS_15\p_RS_15.dpr. Składa się on 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
193
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 przeze mnie algorytmu przedstawiony jest na wydruku 5.13. Rysunek 5.16. Formularz główny projektu p_RS_15.dpr
Wydruk 5.13. 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;
194
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';
195
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
196
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ść
197
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 (patrz tabela 2.1). 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, jednak dużo prościej i czytelniej będzie, jeżeli postąpimy tak, jak w przypadku projektu \KODY\DELPHI\RS_16\p_RS_16.dpr. 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ą
198
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;
199
Rysunek 5.17. Wygląd formularza projektu p_RS_16.dpr
Wydruk 5.14. 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);
200
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 = $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 CloseHandle(hCommDev);
201
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;
202
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 DCBdcb.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
203
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
204
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 pokazano na rysunku 5.18, zaś jej kod źródłowy RS_17.pas przedstawiono na wydruku 5.15. Odpowiedni projekt znajduje się na CD w katalogu \KODY\DELPHI\RS_17\p_RS_17.dpr. 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.
205
Rysunek 5.18. Formularz projektu p_RS_17.dpr w trakcie działania
Wydruk 5.15. 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);
206
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;
207
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;
208
//-------------------------------------------------------------------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
209
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 DCBdcb.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--------------------------------------
210
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;
211
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;
212
//--------------------------------------------------------------------
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. Jednak zagadnienia związane z obsługą drukarek w aplikacjach pisanych w Delphi lub C++Builderze znacznie wykraczają poza ramy tej książki.
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. Wygląd działającej aplikacji pokazano na rysunku 5.19, zaś jej projekt dostępny jest w katalogu \KODY\DELPHI\RS_18\p_RS_18.dpr. 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. Komponent ten uczyniłem zdolnym do generowania zdarzenia TrackBar1Change(), tak jak pokazuje to wydruk 5.16. 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.
213
Rysunek 5.19. Wygląd formularza projektu p_RS_18.dpr
Wydruk 5.16. 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
214
{ 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;
215
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;
216
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?
217
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. Wyniki pokazane są na rysunku 5.20. 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ści4. 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. Rysunek 5.20. Wygląd formularza projektu p_RS_19.dpr wyświetlającego dane w różnych komponentach edycyjnych
4 W pewnych szczególnych okolicznościach, np. po wykryciu błędu odczytu, w obiekcie TMemo efekt ten może również wystąpić.
218
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 obiektowozdarzeniowego, 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).
219
Rysunek 5.21. Pomiar temperatury za pomocą aplikacji wykorzystującej komponent TTimer
Widoczna na rysunku krzywa A prezentuje wynik odczytu przy zastosowaniu najmniejszej z możliwych częstości próbkowania łącza, przy której nie zauważyłem już żadnych błędów transmisji. Test B został wykonany przy trochę mniejszej częstości odczytu przyrządu, dając, jak widzimy, dużo lepsze rezultaty. 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
220
celem jest cykliczne, wraz z działaniem Timera, wyświetlanie kolejnych liczb, tak jak pokazuje to rysunek 5.22. Rysunek 5.22. Porównanie stopnia synchronizacji działającego zegara systemowego z aplikacją wykorzystującą komponent TTimer
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
221
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? Rysunek 5.23. Wygląd formularza projektu p_RS_20.dpr obsługującego woltomierz cyfrowy
222
Wydruk 5.17. 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}
223
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 DCBdcb.Flags := dcb_fParity; dcb.Parity := ODDPARITY; dcb.StopBits := ONESTOPBIT; dcb.ByteSize := 7;
224
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
225
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
226
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.
Podsumowanie Pisząc ten rozdział, miałem na uwadze fakt, że z większością warstwy Win32 API, wykorzystywaną w programach posługujących się transmisją szeregową, zapoznaliśmy się już wcześniej. Na przykładzie konkretnych aplikacji omówiliśmy możliwości zastosowania tych funkcji w środowisku Delphi. Czytając obecny fragment książki, każdy mógł się zorientować, iż widoczna jeszcze na początku lat 90. różnica pomiędzy Pascalem i C++ w Windows została już praktycznie zatarta. Dlatego, moim zdaniem, bezprzedmiotowe jest prowadzenie sporów na temat wyższości Buildera nad współczesnym Object Pascalem. Oba wymienione środowiska programistyczne mogą się natomiast znakomicie uzupełniać, zaś ich poznanie z pewnością będzie świadczyło o naszej uniwersalności, która jest zawsze mile widziana wśród programistów.
Ćwiczenia 1.
Wykorzystując projekt p_RS_16.dpr, zmodyfikuj program w ten sposób, by można było zapisywać na dysku otrzymywane informacje.
2.
Uzupełnij projekt p_RS_17.dpr o możliwość wysyłania wybranego wiersza tekstu. W tym celu możesz posłużyć się funkcją Eoln().
3.
Uzupełnij projekt p_RS_18.dpr o możliwość cyklicznego zapisu otrzymywanych danych.
227
Rozdział 6 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. O naturze wątków napisano już bardzo wiele, wystarczy wymienić znakomite książki: „Delphi 3. Księga eksperta”, wyd. Helion (1998) czy „Delphi 4. Vademecum profesjonalisty. Tom 1”, wyd. Helion (1999). W rozdziale tym 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
229
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.
230
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.
231
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:
232
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);
233
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.
234
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.
235
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
236
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ątek1: procedure TForm1.SendFileClick(Sender: TObject); begin hThread_SF := BeginThread (NIL, 0, @RS_SendFile, NIL, 0, ThreadID_SF); end;
Na rysunku 6.1 pokazano formularz projektu p_RS_21.dpr znajdującego się w katalogu \KODY\DELPHI\RS_21\, który jest nieznaczną modyfikacją przedstawionego wcześniej
p_RS_17.dpr. Dodałem w nim jedynie nowe okno edycji w postaci drugiego komponentu TRichEdit. Główny moduł tej aplikacji RS_21.pas, którego kod zamieszczony jest na wydruku 6.1, został tylko w niewielkim stopniu zmieniony w porównaniu ze swoim poprzednikiem. Jedynymi uwzględnionymi w nim nowościami są właśnie opisane wcześniej funkcja: RS_SendFile() oraz procedura obsługi zdarzenia SendFileClick(). 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 1
Przy prezentowanym sposobie deklaracji funkcji RS_SendFile(P: Pointer) użycie operatora @ nie jest wymagane, natomiast gdyby była ona bezparametrowa, należy go wykorzystać.
237
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. Rysunek 6.1. Wygląd formularza projektu p_RS_21.dpr
Wydruk 6.1. 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;
238
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;
239
// -- fRtsControl -RTS_CONTROL_ENABLE = $1000; RTS_CONTROL_HANDSHAKE = $2000; RTS_CONTROL_TOGGLE = $3000; dcb_fAbortOnError = $4000; cbInQueue = cbOutQueue = var hThread_SF ThreadID_SF
1024; 1024; : THANDLE; // pseudoidentyfikator wątku : 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; //--------------------------------------------------------------------
240
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;
241
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 DCBdcb.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+
242
' 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
243
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 API2 — pierwszej: DWORD SuspendThread(HANDLE hThread);
za pomocą której możemy czasowo wstrzymać proces wykonywania wątku oraz następnej, wznawiającej jego wykonywanie:
2
Użycie ich w Win NT wymaga, aby identyfikator nowego wątku został przydzielony wraz z rodzajem dostępu THREAD_SUSPEND_RESUME.
244
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. Rysunek 6.2 przedstawia znany nam już formularz aplikacji obsługującej woltomierz cyfrowy, której projekt można znaleźć w katalogu \KODY\DELPHI\RS_22\p_RS_22.dpr. 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 6.2 pokazuje kompletny algorytm omawianego programu.
245
Rysunek 6.2. Wygląd formularza projektu p_RS_22.dpr
Wydruk 6.2. 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;
246
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
247
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 DCBdcb.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);
248
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
249
OpenComm.Enabled := FALSE; StrCopy(Buffer_O, query); hThread_SR := BeginThread (NIL, 0, @RS_Send_Receive, NIL, 0, ThreadID_SR); 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 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
250
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ć
251
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.
252
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. Rysunek 6.3 pokazuje wygląd omawianego formularza. Na wydruku 6.3 prezentowany jest jego kompletny kod źródłowy ze szczegółowym przedstawieniem kolejności użycia odpowiednich funkcji.
253
Rysunek 6.3. Wygląd formularza projektu p_RS_08.dpr
Wydruk 6.3. 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 #define cbInQueue 1024
//rozmiar bufora danych wyjściowych //rozmiar bufora danych wejściowych
TForm1 *Form1; AnsiString New_File; HFILE hfile_s; char Buffer_O[cbOutQueue]; char Buffer_I[cbInQueue]; DWORD Number_Bytes_Read; HANDLE hCommDev; // LPCTSTR lpFileName; // DCB dcb; // DWORD fdwEvtMask; COMSTAT Stat; DWORD Errors; BOOL bResult; //
// przechowuje nazwę pliku // identyfikator pliku // bufor danych wyjściowych // bufor danych wejściowych // liczba bajtów do czytania identyfikator portu wskaźnik do nazwy portu struktura kontroli portu szeregowego
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) {
254
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(); }; } }
255
//-------------------------------------------------------------------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;
256
} } //-------------------------------------------------------------------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;
257
} 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) dcb.BaudRate = CBR_1200; if (CheckBox4->Checked == TRUE) dcb.BaudRate = CBR_19200; dcb.Parity = NOPARITY; dcb.StopBits = TWOSTOPBITS; dcb.ByteSize = 8;
// wybór prędkości
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia znaczników sterujących DCBdcb.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) {
258
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;
259
} //-------------------------------------------------------------------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
260
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, której projekt widzimy na rysunku 6.4 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()3. 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().
3
Pisząc tego rodzaju aplikacje, należy zawsze wybierać komponenty umożliwiające pośrednie wyświetlanie wybranej wartości liczbowej w trakcie działania programu. Unikamy w ten sposób konsekwencji związanych z przypadkowym skasowaniem zawartości komponentu edycyjnego.
261
Rysunek 6.4. Formularz projektu p_RS_09.bpr
Wydruk 6.4. 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 #define cbInQueue 16
//rozmiar bufora danych wyjściowych //rozmiar bufora danych wejściowych
TForm1 *Form1; LPCTSTR char char DWORD HANDLE LPCTSTR DCB DWORD COMSTAT DWORD
query = "CDAT?\r\n"; Buffer_O[cbOutQueue]; Buffer_I[cbInQueue]; Number_Bytes_Read; // hCommDev; // lpFileName; dcb; fdwEvtMask; Stat; Errors;
BOOL bResult = TRUE; int hThread_SR; unsigned ThreadID_SR;
262
// zapytanie o mierzone napięcie // bufor danych wyjściowych // bufor danych wejściowych liczba bajtów do czytania identyfikator portu
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);
263
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) dcb.BaudRate = CBR_300; if (CheckBox4->Checked == TRUE) dcb.BaudRate = CBR_1200; if (CheckBox5->Checked == TRUE) dcb.BaudRate = CBR_9600;
// wybór prędkości transmisji
dcb.Parity = ODDPARITY; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 7;
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia znaczników sterujących DCBdcb.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 {
264
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); }
265
//---------------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.
266
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')
267
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).
Podsumowanie W niniejszym rozdziale zostały przedstawione informacje na temat rzadziej spotykanych w literaturze metod wykorzystania elementów techniki programowania wielowątkowego w aplikacjach realizujących transmisję szeregową. Chociaż wykorzystaliśmy bardzo prostą funkcję BeginThread() zdolną do generowania nowych wątków, jednak już samo jej odpowiednie użycie sprawiło, że posługujące się nią aplikacje komunikacyjne są bardziej funkcjonalne. Istnieje oczywiście możliwość pełnej implementacji zarówno w Delphi jak i Builderze klasy TThread. Trzeba jednak stwierdzić, że technika programowego wykorzystania wymienionej klasy jest szeroko opisana w dostępnych publikacjach, więc samodzielne próby jej użycia nie powinny sprawić Czytelnikom większych kłopotów. Opisane sposoby posługiwania się funkcją BeginThread() mają jeszcze jedna zaletę — nie potrzebujemy martwić się skomplikowanym ustalaniem priorytetów wątków jak również różnymi metodami ich synchronizacji. Możliwość
268
samodzielnego ustalania priorytetu wątku jest jedną z głównych zalet techniki programowania współbieżnego. Jednak w aplikacji sterującej konkretnym przyrządem pomiarowym trudno byłoby wprowadzać tego typu zależności. Na jakiej zasadzie można zaprogramować wyższy (lub niższy) priorytet np. dla funkcji wysyłającej komendy do urządzenia oraz funkcji czytającej jego wskazania?
269
Rozdział 7 Wykorzystanie niektórych narzędzi graficznych W rozdziale tym 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
270
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. Rysunek 7. 1. Możliwe do uzyskania standardowe typy wykresów
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
271
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ów1. 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:
1
W C++Builderze można skorzystać z funkcji strtol() lub strtoul(), których prototypy znajdują się w pliku stdlib.h.
272
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
273
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). Rysunek 7.2. Formularz działającej aplikacji posługującej się komponentem TChart (testowanym miernikiem było urządzenie produkcji LakeShore).
Wydruk 7.1. 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.
274
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}
275
const // -- wartości znaczników sterujących portu szeregowego -dcb_fBinary = $0001; dcb_fParity = $0002; cbInQueue = 32; cbOutQueue = 32;
// rozmiary buforów danych
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; intVarSleep : Integer; bResult : BOOL; hThread_SR : THANDLE; ThreadID_SR: Cardinal;
// licznik pomiarów // licznik późnienia // "niema" zmienna logiczna
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;
276
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);
277
GetCommState(hCommDev, dcb); if (CheckBox2.Checked = TRUE) then dcb.BaudRate:=CBR_1200; //-przykładowe ustawienia znaczników sterujących DCBdcb.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
278
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
279
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.
280
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. Przykład wywołania tej funkcji można znaleźć w treści procedury PaintLine() na wydruku 7.2. Rysunek 7.3 przedstawia formularz działającej aplikacji \KODY\DELPHI\RS_23\p_RS_23.dpr, w której dane otrzymywane od urządzenia pomiarowego wyświetlane są w postaci wykresu liniowego. Rysunek 7.3. Formularz działającej aplikacji p_RS_23.dpr
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
281
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. Wydruk 7.2. 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;
282
implementation {$R *.DFM} const // -- wartości znaczników sterujących portu szeregowego -dcb_fBinary = $0001; dcb_fParity = $0002; cbInQueue = 32; cbOutQueue = 32;
// rozmiary buforów danych
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; intVarSleep : Integer; bResult : BOOL; hThread_SR : THANDLE; ThreadID_SR: Cardinal;
// licznik pomiarów // licznik późnienia // "niema" zmienna logiczna
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);
283
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;
284
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 DCBdcb.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
285
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
286
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;
287
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)]);
288
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.
289
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(). Przykład działającego projektu aplikacji \KODY\DELPHI\RS_24\p_RS_24.dpr, wyświetlającej na wykresie rysowanym na mapie bitowej wyniki pomiaru temperatury pewnego układu fizycznego pokazano na rysunku 7.3. Rysunek 7.3. Formularz działającej aplikacji p_RS_24.dpr
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.
290
Wydruk 7.3. 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, '
291
');
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;
292
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;
293
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);
294
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 7.4 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. Wydruk 7.4. 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
295
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;
296
');
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);
297
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;
Kompletny projekt tej aplikacji można znaleźć na CD w katalogu \KODY\DELPHI\RS_25\p_RS_25.dpr. Porównując przedstawione sposoby konstruowania wykresów na mapach bitowych, na pewno zauważymy, że drugi sposób, chociaż być może bardziej elegancki, nie jest tak naprawdę funkcjonalny. Jeżeli ktoś ma do dyspozycji trochę słabszy monitor o mniejszej częstości odświeżania, na pewno zauważy charakterystyczne migotanie wykresu w trakcie odczytu danych. Związane jest to z koniecznością ciągłego wywoływania metody Draw().
Podsumowanie W niniejszym rozdziale zostały przedstawione najczęściej wykorzystywane metody sporządzania wykresów w aplikacjach obsługujących urządzenia pomiarowe. Zapoznaliśmy się z częścią niezwykle bogatych właściwości oferowanych przez komponent TChart. Umiemy też wykorzystywać go w naszych programach. Niestety, nie jest on dostępny we wszystkich rozpowszechnianych wersjach zarówno Delphi, jak i C++Buildera. Dlatego powinniśmy posiadać też pewne umiejętności samodzielnego projektowania tego typu wykresów. Wiąże się to oczywiście z koniecznością żmudnego skalowania osi i budowania algorytmów wyświetlających dane w odpowiednim miejscu formularza. Poznaliśmy metody konstruowania takich rysunków, zarówno bezpośrednio na płótnie formularza jak i w obszarach map bitowych.
298
Rozdział 8 Przykładowe aplikacje wykorzystywane w systemach pomiarowych „Często powtarzam, że jeżeli możesz zmierzyć to, o czym mówisz oraz opisać to za pomocą liczb, wiesz coś o tym, ale jeżeli nie jesteś w stanie opisać tego za pomocą liczb, twoja wiedza o tym jest niezadawalająca, niezależnie od tego, czego ona dotyczy; jest to zaledwie początek wiedzy, pierwszy krok na szczeblach nauki.” William Thomson1, 1858
W rozdziale tym 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
1
William Thomson (1824-1907) — jeden z najwybitniejszych uczonych angielskich. Był też jednym z głównych wykonawców projektu przeprowadzenia pierwszego kabla telegraficznego przez Atlantyk (1858). W roku 1892 otrzymał tytuł lorda i odtąd znany jest jako lord Kelvin.
299
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. Wygląd działającego projektu aplikacji \KODY\DELPHI\RS_26\p_RS_26.dpr, zaopatrzonego w najważniejsze podstawowe funkcje oferowane przez urządzenie pomiarowe pokazany jest na rysunku 8.1. Rysunek 8.1. Działająca aplikacja obsługująca kontroler temperatury
300
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 8.1. 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;
301
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
302
Form1.RichEdit1.Text := 'x0'; Beep(); end; UNTIL(bResult = FALSE); Result := 0; end;
// błędny odczyt
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
303
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. Wydruk 8.1. 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;
304
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;
305
implementation {$R *.DFM} const dcb_fBinary = $0001; dcb_fParity = $0002; cbInQueue = 32; cbOutQueue = 32; const query_IDN query_ATYPE query_UNITS query_HEATER query_RAMP query_SETP query_HEAT
: : : : : : :
PChar PChar PChar PChar PChar PChar PChar
= = = = = = =
'*IDN?'+#13+#10; 'ATYPE?'+#13+#10; 'CUNI?'+#13+#10; 'RANG?'+#13+#10; 'RAMP?'+#13+#10; 'SETP?'+#13+#10; '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'+
306
' 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;
307
//-------------------------------------------------------------------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
308
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
309
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);
310
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;
311
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);
312
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';
313
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);
314
//-------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;
315
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
316
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? Budowa aplikacji obsługującej tego rodzaju mierniki będzie bardzo podobna do zaprezentowanej w tym podrozdziale.
Aplikacja obsługująca kilka urządzeń W rozdziale 2., 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. Formularz aplikacji \KODY\BUILDER\RS_10\p_RS_10.bpr widocznej na rysunku 8.2 został podzielony na dwa obszary pełniące funkcję oddzielnych paneli sterowania dla różnych urządzeń zewnętrznych. Rysunek 8.2. Dwa urządzenia obsługiwane przez jedną aplikację – projekt p_RS_10.bpr
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
317
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 8.2. Wydruk 8.2. 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 <stdio.h> #pragma hdrstop #include "RS_10.h" #pragma package(smart_init) #pragma resource "*.dfm" #define cbOutQueue 32 #define cbInQueue 32
//rozmiar bufora danych wyjściowych //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 char char char
Buffer_O_COM2[cbOutQueue]; // bufor danych wyjściowych Buffer_I_COM2[cbInQueue]; // bufor danych wejściowych Buffer_I_COM1[cbInQueue]; Buffer_O_COM1[cbOutQueue];
DWORD HANDLE LPCTSTR DCB DWORD COMSTAT DWORD
Number_Bytes_Read; // liczba bajtów do czytania hCommDev_1, hCommDev_2; // identyfikatory portów lpFileName_1, lpFileName_2; dcb; fdwEvtMask; Stat; 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
318
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);
319
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; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 7;
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia znaczników sterujących DCBdcb.fParity = TRUE; // sprawdzanie parzystości dcb.fDtrControl = DTR_CONTROL_DISABLE;
320
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)
321
{ 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; dcb.StopBits = ONESTOPBIT; dcb.ByteSize = 8;
// ustawienie parzystości // bity stopu // bity danych
//-przykładowe ustawienia znaczników sterujących DCBdcb.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+");
322
} } } 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;
323
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)
324
{ 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)
325
{ 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. Wydruk 8.3. Szkielet uniwersalnej funkcji otwierającej i ustalającej parametry transmisji wybranego portu szeregowego wraz z jej wywołaniami z przykładowymi parametrami aktualnymi
326
... 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, break; case 2: strcpy (CommName, break; case 3: strcpy (CommName, break; case 4: strcpy (CommName, break; ... default: return FALSE; } switch (BaudRate) { case 110: dcb.BaudRate break; case 300: dcb.BaudRate break; case 600: dcb.BaudRate break; case 1200: dcb.BaudRate break; case 2400: dcb.BaudRate break; case 4800: dcb.BaudRate break; case 9600: dcb.BaudRate break; case 19200: dcb.BaudRate break; case 38400: dcb.BaudRate break; case 57600: dcb.BaudRate break; case 115200:
"COM1"); "COM2"); "COM3"); "COM4");
= CBR_110; = CBR_300; = CBR_600; = CBR_1200; = CBR_2400; = CBR_4800; = CBR_9600; = CBR_19200; = CBR_38400; = CBR_57600;
327
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); }
328
//-------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.32. 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.
2
O jakości testowanej wagi świadczy to, że była w stanie szybko zarejestrować fakt pozostawienia na szalce kawałka cyny po jednym z ważeń końcówki DB-25.
329
Rysunek 8.3. Rejestracja ważeń na wadze cyfrowej
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.
330
Podsumowanie W niniejszym rozdziale zostały zilustrowane przykłady praktycznego wykorzystania aplikacji obsługujących przyrządy pomiarowe poprzez łącze szeregowe RS 232C. Omówione urządzenia są typowymi, nowoczesnymi miernikami, wykorzystywanymi w systemach pomiarowych. Większość współczesnych zasilaczy, częstościomierzy, oscyloskopów czy innych wielofunkcyjnych mierników zaopatrzonych w łącze szeregowe obsługuje się w sposób, który został zaprezentowany. Jedyna różnica może polegać na wykorzystaniu innego zestawu komend lub zapytań, jednak zawsze są one dokładnie opisane w instrukcji obsługi urządzenia. Została też przedstawiona aplikacja, za pomocą której można obsługiwać wiele urządzeń jednocześnie. Wykorzystanie uniwersalnej funkcji otwierającej i ustalającej parametry transmisji wybranego portu szeregowego może bardzo pomóc w konstrukcji nawet dosyć skomplikowanego algorytmu.
331
Uzupełnienie 1. Specyfikacja funkcji CreateFile() – operacje plikowe HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
Parametry: lpFileName – wskaźnik do ciągu znaków zakończonych zerowym ogranicznikiem (C – łańcucha). Specyfikuje nazwę pliku, zasobów komunikacyjnych, potoku, urządzenia dyskowego (NT), połączenia sieciowego typu konsoli. Jeżeli *lpFileName reprezentuje ścieżkę dostępu, wówczas wartość domyślana długości łańcucha jest ograniczona do MAX_PATH znaków. Pracując w Win NT można pokonać ograniczenie co do długości omawianego ciągu znaków posługując się konwencją nazewnictwa katalogów UNC (ang. Universal Naming Convention). Zgodnie z nią do identyfikowania nazwy komputera stosowany jest podwójny ukośnik wsteczny \\ (ang. backslash), natomiast pojedynczy ukośnik wsteczny wskazuje katalog na dysku tego komputera np. "\\?\C:\katalog1\katalog2" co zostanie zinterpretowane jako "C:\katalog1\katalog2" lub "\\?\UNC\katalog1\katalog2\katalog3", który to zapis będzie zinterpretowany następująco: "\\katalog1\katalog2\katalog3". dwDesiredAccess – specyfikacja rodzaju dostępu do obiektu. Parametr ten może być kombinacją następujących wartości.
332
0 – przyznanie aplikacji aktualnego rodzaju dostępu. GENERIC_READ – dostęp do odczytu. GENERIC_WRITE – dostęp do zapisu. dwShareMode – wyszczególnia, w jaki sposób dany obiekt (plik) może być współdzielony.
0 – obiekt nie może być współdzielony. FILE_SHARE_DELETE – współdzielenie z operacjami usuwania (Win NT). FILE_SHARE_READ – tryb współdzielenia z operacjami czytania. FILE_SHARE_WRITE – tryb współdzielenia z operacjami zapisu. lpSecurityAttributes – wskaźnik do struktury SECURITY_ATTRIBUTES zawierającej deskryptor zabezpieczeń obiektu i określającej, czy zwracany identyfikator jest dziedziczony. typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID lpSecurityDescriptor; BOOL bInheritHandle; } SECURITY_ATTRIBUTES;
nLength – rozmiar struktury w bajtach. lpSecurityDescriptor – wskaźnik do deskryptora zabezpieczeń obiektu (Win NT). Jeżeli ustalono NULL zostanie wybrana wartość domyślna. bInheritHandle – wyszczególnia, czy zwracany przez CreateFile() identyfikator jest
dziedziczony przy tworzeniu nowego procesu. Wartość TRUE oznacza, że nowy proces dziedziczy ten identyfikator. DwCreationDistribution – rodzaje operacji wykonywanych na pliku.
CREATE_NEW – utworzenie nowego pliku. Funkcja nie będzie wykonana pomyślnie, jeżeli plik już istnieje. CREATE_ALWAYS – utworzenie nowego pliku niezależnie od tego czy już istnieje. Jeżeli plik istnieje, nowy zostanie zapisany na istniejącym. OPEN_EXISTING – otwarcie istniejącego pliku. Jeżeli plik nie istnieje funkcja nie będzie wykonana pomyślnie. OPEN_ALWAYS – otwarcie istniejącego pliku. Jeżeli takowy nie istnieje, zostanie stworzony identycznie jak przy pomocy CREATE_NEW. TRUNCATE_EXISTING – tuż po otwarciu plik jest okrojony do rozmiaru 0 bajtów. Wymagane jest wcześniejsze jego utworzenie przynajmniej z rodzajem dostępu GENERIC_WRITE. Funkcja nie będzie wykonana pomyślnie jeżeli plik nie istnieje.
333
dwFlagsAndAttributes – określenie atrybutów i flag pliku.
Atrybut: FILE_ATTRIBUTE_ARCHIVE – plik powinien zostać zarchiwizowany. FILE_ATTRIBUTE_COMPRESSED – plik lub katalog jest skompresowany. FILE_ATTRIBUTE_HIDDEN – plik ukryty. FILE_ATTRIBUTE_NORMAL – plik nie posiada innych atrybutów. Atrybut jest ważny tylko wówczas, gdy jest używany indywidualnie (bez innych). FILE_ATTRIBUTE_OFFLINE – dane zawarte w pliku nie są bezpośrednio udostępniane. FILE_ATTRIBUTE_READONLY – plik tylko do odczytu. FILE_ATTRIBUTE_SYSTEM – plik jest częścią lub jest używany wyłącznie przez system operacyjny. FILE_ATTRIBUTE_TEMPORARY – plik jest używany do czasowego przechowywania. Powinien być usunięty jeżeli nie jest wykorzystywany. Flaga: FILE_FLAG_WRITE_THROUGH – zawartość pliku zostaje zapisana pośrednio poprzez bufor. FILE_FLAG_OVERLAPPED – w przypadku operacji realizowanych w znaczącym przedziale czasu przez funkcje ReadFile(), WriteFile(), ConnectNamedPipe() i TransactNamedPipe(), można oczekiwać komunikatu ERROR_IO_PENDING – realizowana jest operacja nakładanego wejścia / wyjścia. W tym kontekście musi nastąpić odwołanie do struktury OVERLAPPED zawierającej informacje używane w operacjach nakładanego wejścia / wyjścia. typedef struct _OVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED;
Internal – zarezerwowane dla systemu operacyjnego. Człon ten staje się istotny, gdy funkcja GetOverlappedResult() zwróci rezultat wykonanej operacji nakładanego wejścia / wyjścia w
przypadku pliku, potoku lub urządzenia zewnętrznego. InternalHigh – zarezerwowane dla systemu. Specyfikuje długość transferowanych danych. Staje się istotny, gdy funkcja GetOverlappedResult() zwraca wartość TRUE. Offset – określa wskaźnik położenia (w)pliku przeznaczonym do transferu. Traktowany jest jako
offset wyznaczony w stosunku do początku pliku. OffsetHigh – część bardziej znacząca offsetu.
334
hEvent – wyszczególnienie sposobu określenia końca transferu danych.
FILE_FLAG_NO_BUFFERING – instruuje system operacyjny aby otworzył plik bez pośredniego buforowania jego zawartości. Aplikacja musi spełniać pewne wymagania pracując z plikiem tak otwartym. Najważniejszym z nich jest to, by dane zawarte w pliku były odczytywane w porcjach będących całkowitą wielokrotnością rozmiaru sektora wolumenu1. Dla przykładu przy rozmiarze sektora 512 bajtów dane mogą być czytane w porcjach po 512, 1024, 2048 ... bajtów. Uzyskanie informacji o rozmiarze sektora wolumenu może być dostępne dzięki funkcji Win32 API GetDiskFreeSpace(). Przy otwarciu pliku adres bufora używanego do operacji czytania i zapisywania musi być przedstawiony w postaci całkowitej wielokrotności rozmiaru sektora wolumenu. W celu określenia bufora można użyć funkcji VirtualAlloc() ustalającej adres w postaci całkowitej wielokrotności rozmiaru strony pamięci używanej przez system operacyjny. FILE_FLAG_RANDOM_ACCESS – sygnalizuje plik o swobodnym sposobie dostępu. Dostęp do każdego jego elementu możliwy jest przez bezpośrednie wskazanie odpowiedniego adresu danego elementu. FILE_FLAG_SEQUENTIAL_SCAN – sygnalizuje plik o sekwencyjnym sposobie dostępu. FILE_FLAG_DELETE_ON_CLOSE – system natychmiast usuwa z pamięci plik po tym jak przydzielony mu identyfikator zostanie zwolniony. Późniejsze operacje otwarcia pliku nie będą wykonywane pomyślnie, chyba że wcześniej tryb współdzielenia został ustalony jako FILE_SHARE_DELETE. FILE_FLAG_BACKUP_SEMANTICS – podaje czy plik jest otwierany lub utworzony jako kopia zapasowa SE_BACKUP_NAME lub jako plik odzyskany SE_RESTORE_NAME. FILE_FLAG_POSIX_SEMANTICS – wyszczególnienie rodzaju dostępu do pliku zgodnie z zasadami POSIX2. Funkcja CreateFile() prawidłowo wywołana zwraca identyfikator pliku. Jeżeli plik już istnieje przed wywołaniem funkcji z CREATE_ALWAYS lub OPEN_ALWAYS przypisanymi do dwCreationDistribution, funkcja GetLastError() zwróci wartość ERROR_ALREADY_EXISTS. Jeżeli plik nie istnieje GetLastError() zwraca 0. W przypadku gdy funkcja CreateFile() nie została wykonana pomyślnie należy oczekiwać wartości INVALID_HANDLE_VALUE. Windows NT: CreateFile() można użyć uzyskując dostęp do stacji dysków lub partycji dysku. Zwracany identyfikator, może zostać użyty w funkcji DeviceIoControl(). Uzyskując dostęp do
1
Wolumen jest fizyczną porcją pamięci na dysku twardym lub innym urządzeniu pamięci zewnętrznej np. CD ROM. Wolumen może być w całości zawarty na jednym urządzeniu pamięciowym (dysku) lub rozłożony na kilku takich urządzeniach.
2
POSIX (ang. Portable Operating System UNIX) zespół standaryzacyjny określający przenośną wersję systemu UNIX.
335
dysku twardego lpFileName powinno być reprezentowane przez łańcuch postaci \\.\PHYSICALDRIVEx , gdzie x jest numerem dysku. Numeracja dysków zaczyna się od 0. Użycie \\.\x umożliwia uzyskanie dostępu do stacji dysków x lub danej partycji na dysku twardym.
336
Uzupełnienie 2. Zamiana liczb z postaci dziesiętnej na binarną W niniejszym Uzupełnieniu przedstawimy prostą aplikację napisaną tym razem w Delphi i służącą do zamiany liczb z postaci dziesiętnej na binarną (dwójkową) – projekt \KODY\DELPHI\U_2\p_liczby.dpr. Do jej zaprojektowania użyłem dwóch komponentów TButton, komponentu typu TSpinEdit, przy pomocy którego możemy wybrać interesującą nas liczbę dziesiętną, komponentu TMemo, w którym wyświetlamy wyniki, oraz czterech opisowych komponentów TLabel, tak jak pokazuje to rysunek U.2.1. Z przyciskiem Zamień liczbę skojarzone jest procedura obsługi zdarzenia ShiftNumberClick(), w którym wywoływana jest z kolei procedura Shift_Number(). W treści tego zdarzenia zapisujemy też do pliku binarne.dat otrzymane wyniki. Do pliku poprzez zmienną sdata zostanie bezpośrednio zapisana zawartość cechy Text komponentu Memo1 oraz zawartość cechy Value komponentu SpinEdit1. Obie te informacje będą oddzielone parą znaków #13#10 CR LF – przejście do następnego wiersza (por. tab. 2.1). Naciskając przycisk Zamień liczbę powodujemy przekazanie poprzez parametr formalny number aktualnej cechy Value komponentu SpinEdit1do wnętrza procedury Shift_Number(). Procedura ta poprzez zmienną lokalną var_number przejmuje bieżącą wartość tego komponentu i przy pomocy prostych działań: DIV – dzielenie całkowite bez reszty oraz MOD – reszta z dzielenia całkowitego dokonuje rekurencyjnego przeliczania postaci dziesiętnej liczby na jej postać binarną. Przycisk Koniec zamyka aplikację.
337
Rysunek U.2.1 Formularz główny projektu p_liczby.dpr
Poniżej zamieszczony jest kompletny wydruk zastosowanego przeze mnie algorytmu. Wydruk U.2.1. Kod źródłowy modułu liczby.pas aplikacji zamieniającej liczby z postaci dziesiętnej na binarną. unit liczby; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Spin, Mask, ComCtrls; type TForm1 = class(TForm) SpinEdit1: TSpinEdit; ShiftNumber: TButton; Close: TButton; Memo1: TMemo; Label1: TLabel; Label2: TLabel; Label3: TLabel; Label4: TLabel; procedure CloseClick(Sender: TObject); procedure ShiftNumberClick(Sender: TObject); private { Private declarations } procedure Shift_Number(number: Cardinal); public { Public declarations } end; var
338
Form1: TForm1; implementation {$R *.DFM} procedure TForm1.Shift_Number(number: Cardinal); var var_number: Cardinal; begin if (number > 0) then begin Shift_Number(number DIV 2); var_number := (number MOD 2); Memo1.Lines.Add(IntToStr(var_number)); end; end; //-------------------------------------------------------------------procedure TForm1.ShiftNumberClick(Sender: TObject); var OutFile : TextFile; // zmienna plikowa fname, sdata : String; begin Memo1.Lines.Clear(); Shift_Number(SpinEdit1.Value); fname := 'binarne.dat'; // nazwa pliku AssignFile(OutFile, fname); // skojarzenie nazwy pliku ze zmienną // plikową Rewrite(OutFile); sdata :='Postać binarna liczby: '+IntToStr(SpinEdit1.Value)+#13+#10+ Memo1.Lines.Text; Write(OutFile, sdata); CloseFile(OutFile); // zamknięcie pliku end; //-------------------------------------------------------------------procedure TForm1.CloseClick(Sender: TObject); begin Application.Terminate; end; //-------------------------------------------------------------------end.
339
Uzupełnienie 3. Specyfikacja struktur MODEMDEVCAPS, MODEMSETTINGS oraz funkcji GetCommModemStatus() MODEMDEVCAPS Struktura MODEMDEVCAPS zawiera informacje o parametrach modemu. typedef struct modemdevcaps_tag { DWORD dwActualSize; DWORD dwRequiredSize; DWORD dwDevSpecificOffset; DWORD dwDevSpecificSize; DWORD DWORD DWORD DWORD DWORD DWORD DWORD
dwModemProviderVersion; dwModemManufacturerOffset; dwModemManufacturerSize; dwModemModelOffset; dwModemModelSize; dwModemVersionOffset; dwModemVersionSize;
DWORD DWORD DWORD DWORD DWORD DWORD DWORD DWORD
dwDialOptions; dwCallSetupFailTimer; dwInactivityTimeout; dwSpeakerVolume; dwSpeakerMode; dwModemOptions; dwMaxDTERate; dwMaxDCERate;
340
BYTE abVariablePortion [1]; } MODEMDEVCAPS, *PMODEMDEVCAPS, *LPMODEMDEVCAPS;
dwActualSize – wyszczególnienie rozmiaru (bajty) danych faktycznie zwracanych do aplikacji. Element ten musu być mniejszy od dwRequiredSize jeżeli aplikacja nie zarezerwowała wystarczającego obszaru pamięci dla części zmiennych struktury. dwRequiredSize – określa liczbę bajtów wymaganych dla całej struktury. Przed wykorzystaniem struktury jako parametru zalecane jest wpisanie w ten element wartości sizeof(MODEMDEVCAPS). dwDevSpecificOffset – określa offset elementu struktury zdefiniowanego przez dostarczyciela usługi. Offset określony jest w stosunku do początku struktury. dwDevSpecificSize – określa rozmiar w bajtach elementu struktury zdefiniowanego przez
dostarczyciela usługi. dwModemProviderVersion – wersja dostarczanej usługi. dwModemManufacturerOffset – offset łańcucha znaków zawierającego nazwę producenta
modemu. dwModemManufacturerSize – wyszczególnienie długości nazwy (bajty) producenta modemu.
Łańcuch znaków reprezentujący tą nazwę nie jest C – łańcuchem (nie jest zakończony zerowym ogranicznikiem). dwModemModelOffset – offset łańcucha znaków zawierającego nazwę modelu modemu. dwModemModelSize – wyszczególnienie w bajtach długości nazwy reprezentującej model
modemu. Ciąg znaków nie jest zakończony zerowym ogranicznikiem. dwModemVersionOffset – offset łańcucha znaków podających wersję dołączonego modemu. dwModemVersionSize – wyszczególnienie rozmiaru łańcucha znaków reprezentujących wersję
modemu. dwDialOptions – wyszczególnienie opcji dzwonienia, które są podtrzymywane przez modem. Człon ten może przyjąć wartości:
0 – nie ustawiono opcji dzwonienia. DIALOPTION_DIALBILLING – modem oczekuje na dźwięk billingu. DIALOPTION_DIALQUIET – oczekiwanie ciszy na linii (wolny dostęp). DIALOPTION_DIALDIALTONE – podtrzymanie oczekiwania na sygnał zgłoszenia. dwCallSetupFailTimer – licznik czasu braku transmisji. Określa maksymalny czas przekroczenia oczekiwania na połączenie (sekundy). dwInactivityTimeout – określa maksymalny czas bezczynności w połączeniu (w dziesiętnych
częściach sekundy).
341
dwSpeakerVolume – ustawiony poziom głośności do posłuchu linii telefonicznej za pomocą wbudowanego głośnika.
0 – nie ustawiono. MDMVOLFLAG_LOW – poziom głośności niski. MDMVOLFLAG_MEDIUM – poziom głośności średni. MDMVOLFLAG_HIGH – poziom głośności wysoki. dwSpeakerMode – tryby pracy głośnika podtrzymywane przez modem.
MDMSPKRFLAG_OFF MDMSPKRFLAG_DIAL MDMSPKRFLAG_ON MDMSPKRFLAG_CALLSETUP DwModemOptions – opcje modemu.
0 – nie ustalono. MDM_BLIND_DIAL – wybieranie numeru abonenta bez rozpoznawania sygnału zgłoszenia centrali. MDM_FLOWCONTROL_SOFT – programowa kontrola transmisji. MDM_CCITT_OVERRIDE – możliwość transmisji zgodnie z zaleceniami CCITT V.21, V.22 lub V.23. W przeciwnym wypadku istnieje możliwość transmisji zgodnej z BELL 212A oraz BELL 103. MDM_FORCED_EC – parametry styku z linią telefoniczną dla wybranych krajów europejskich (ang. EC – European Community). MDM_CELLULAR – wbudowany protokół optymalizacji transmisji do pracy na łączach telefonii komórkowej. MDM_SPEED_ADJUST – automatyczna zmiana prędkości transmisji bazująca na monitoringu jakości linii. MDM_COMPRESSION – wbudowany protokół kompresji danych. MDM_TONE_DIAL – protokół wykrywania sygnałów centrali np. zgłoszenia, zajętej linii, dzwonienia, zwrotnego dzwonienia (w zależności od modelu modemu). MDM_ERROR_CONTROL – wbudowany protokół kontroli i korekcji błędów. MDM_V23_OVERRIDE – transmisja zgodna z zaleceniami CCITT V.23. W przeciwnym wypadku transmisja będzie zgodna z CCITT V.21 oraz V.22. MDM_FLOWCONTROL_HARD – sprzętowa kontrola transmisji.
342
dwMaxDTERate – maksymalna prędkość transmisji w bitach na sekundę dla urządzenia DTE.
dwMaxDCERate – maksymalna prędkość transmisji w bitach na sekundę dla urządzenia DCE. abVariablePortion[1] – informacje dostarczyciela usługi telekomunikacyjnej.
MODEMSETTINGS Struktura MODEMSETTINGS zawiera informacje o konfiguracji modemu. typedef struct modemsettings_tag { DWORD dwActualSize; DWORD dwRequiredSize; DWORD dwDevSpecificOffset; DWORD dwDevSpecificSize; DWORD DWORD DWORD DWORD DWORD
dwCallSetupFailTimer; dwInactivityTimeout; dwSpeakerVolume; dwSpeakerMode; dwPreferredModemOptions;
DWORD dwNegotiatedModemOptions; DWORD dwNegotiatedDCERate; BYTE abVariablePortion[1]; } MODEMSETTINGS, *PMODEMSETTINGS, *LPMODEMSETTINGS;
dwActualSize – liczba bajtów aktualnie zwracanych danych. Element ten musi być mniejszy niż dwRequiredSize jeżeli aplikacja nie przydzieliła wystarczającej ilości pamięci dla części struktury, w której zdefiniowane są zmienne. dwRequiredSize – wyszczególnienie liczby bajtów wymaganych dla całej struktury MODEMDEVCAPS. W ogólnym wypadku przed użyciem struktury jako parametru należy do tego elementu wpisać rozmiar struktury sizeof(MODEMSETTINGS). dwDevSpecificOffset – offset części struktury zdefiniowanej przez dostawcę usługi. dwDevSpecificSize – rozmiar części struktury zdefiniowanej przez dostawcę usługi. dwCallSetupFailTimer – maksymalny czas liczony w sekundach, który modem powinien
odczekać zanim połączenie pomiędzy modemami zostanie ustalone. dwInactivityTimeout – licznik czasu bezczynności transmisji po ustalonym połączeniu. Jeżeli
nie wysłano lub odebrano danych w tym czasie połączenie jest automatycznie przerywane. dwSpeakerVolume – poziom głośności wbudowanego głośnika, jeżeli jest włączony.
MDMVOL_LOW – niski.
343
MDMVOL_MEDIUM – średni. MDMVOL_HIGH – wysoki. dwSpeakerMode – tryby pracy głośnika. Określa, kiedy głośnik powinien być włączony.
MDMSPKR_OFF – zawsze wyłączony. MDMSPKR_CALLSETUP – włączony dopóki połączenie jest ustalone. MDMSPKR_ON – zawsze włączony. MDMSPKR_DIAL – włączony dopóki połączenie jest ustalone, wyłączony w czasie dzwonienia. dwPreferredModemOptions – wyszczególnienie opcji modemu żądanych przez aplikację. dwNegotiatedModemOptions – wybrane opcje modemu (zob. dwModemOptions). dwNegotiatedDCERate – rzeczywiste ustawienia prędkości transmisji dla urządzenia DCE. abVariablePortion – zawiera informacje zdefiniowane przez dostawcę usługi (jeżeli takowe
istnieją).
GetCommModemStatus() Funkcja GetCommModemStatus() wyszukuje informacje odnośnie stanu sygnałów kontrolnych modemu. BOOL GetCommModemStatus(HANDLE hFile,
LPDWORD lpModemStat);
hFile – identyfikator urządzenia komunikacyjnego zwracany przez funkcję CreateFile(). lpModemStat – wskaźnik do 32 – bitowej zmiennej opisującej aktualny stan sygnałów
kontrolnych modemu. MS_CTS_ON – sygnał CTS jest aktywny. MS_DSR_ON - sygnał DSR jest aktywny. MS_RING_ON – wskaźnik wywołania jest aktywny. MS_RLSD_ON – sygnał RLSD jest aktywny. Zalecane jest używanie GetCommModemStatus() monitorując sygnały na liniach CTS, DSR, RLSD oraz RI przy pomocy funkcji WaitCommEvent().
344