LPT - XP dostęp do portu równoległego w Windows XP czyli giveio i osiem ledów |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Port równoległy LPT (ang. Line Printer Terminal) to jeden z niewielu elementów typowego komputera PC, którego funkcja i sposób działania pozostały prawie niezmienne od wielu lat. W pierwotnym zamyśle LPT miał służyć tylko jako sprzęg do podłączania różnego rodzaju urządzeń drukujących. Jednak równoległy sposób transmisji danych i w sumie niekłopotliwa obsługa programowa sprawiły, że powstało wiele urządzań współpracujących z LPT, a służących zupełnie odmiennym celom. Z najbardziej popularnych, to chociażby zewnętrzne napedy ZIP, JAZ lub skanery. Także wielu producentów różnego rodzaju urządzeń laboratoryjnych czy warsztatowych (np. emulatory EPROM, programatory) decydowało się na LPT jako główny sposób komunikacji komputera PC ze swoim produktem. I nic w tym dziwnego, równoległa transmisja zapewnia LPT sporą przepustowość, a jej oprogramowanie jest banalne w porównaniu z interfejsem szeregowym RS232. W nowoczesnych maszynach port równoległy jest integralnym składnikiem płyty głównej, której obwody zawierają wszystkie niezbędne elementy do jego obsługi. Popatrzmy więc na płytę główną jak na zwyczajny system mikroprocesorowy. Port równoległy to zestaw kilku (typowo trzech) rejestrów obecnych w przestrzeni I/O (wejścia/wyjścia) mikroprocesora. Współczesne procesory niejako dziedziczą po swoim przodku (CPU Intel 8086) sposób obsługi urządzeń peryferyjnych i przestrzeń I/O ogólnie mówiąc jest rozłączna z przestrzenią pamięci operacyjnej RAM. Do obsługi portów I/O służą dedykowane rozkazy procesora, które umożliwiają zapis 8/16/32-bitowych danych do dowolnie zaadresowanego portu, a także ich odczyt. Rozkazy te nazywają się IN oraz OUT, ich kody maszynowe oraz mnemoniki prezentuje poniższa tabela: rozkaz IN - pobieranie danych z portu I/O
rozkaz OUT - wysyłanie danych do portu I/O
DX - rejestr zawierajacy adres portu w przestrzeni I/O procesora, zakres wartosci 0x0000...0xFFFF, w typowych komputerach PC z reguły używany jest zakres 0x0000...0x0400; Typowy port LPT to trzy specjalne rejestry: Istnieje pojęcie adresu bazowego i jest nim adres rejestru danych, rejestr stanu i rejestr kontrolny mają przypisane kolejne wartości. Zależnie od konfiguracji płyty głównej (lub karty rozszerzającej I/O) adres bazowy może mieć różne wartości, pokazuje to poniższa tabelka:
Rejestr statusu pozwala odczytywac dane z portu LPT czyli zwraca stan wybranych wyprowadzeń złącza DB25. Rejestr kontrolny typowo jest jednokierunkowy - możemy przy jego pomocy tylko wystawiać dane. Rejestr danych jest... to zależy. W starszych płytach głównych sprzętowa realizacja rejestru danych to zwykły, dobrze znany układ 74(LS)373 co jak łatwo zgadnąć gwarantowało tylko i wyłącznie jeden kierunek przesyłu danych - na zewnątrz. W nowoczesnych portach LPT rejestr danych jest dwukierunkowy - można nie tylko pisać, ale też odczytywac z niego dane. Wymaga to drobnych zabiegów programowych, ale generalnie jest możliwe. Poniżej rysunek-rozpiska, jakie funkcje pełnią poszczególne wyprowadzenia typowego złącza równoległego: oraz tabelka, pokazująca jak mają się wyprowadzenia złącza do odpowiednich bitów w rejestrach portu:
Jeżeli chodzi o powyższą tabelę (oraz rysunek) - w kontekście podanych dalej linków, łatwo zauważyć pewną niespójność nazewnictwa, szczególnie dla dodatkowych sygnałów kontrolno-sterujących. Niestety, ile źródeł, tyle nazw. Dlatego podczas rysowania schematów i pisania dokumentacji polecam także operowanie numerami wyprowadzeń złącza a nie tylko nazwami sygnałów. O LPT można jeszcze pisać długo, ale pragnę uniknąć powielania większości dostępnych już w sieci treści. Wskażę zatem tylko jedną stronkę Parallel Port Central , z niej wychodząc doszukamy się wielu innych ciekawostek. Skoro tyle już wiemy o samym sprzęcie - spróbujmy dostać się do niego naszym programem. Teoretycznie nie jest to skomplikowane. Niezależnie od tego czy preferujemy jezyk Pascal czy C/C++, zawsze możemy do naszego programu dodać wstawkę assemblerową, kóra umożliwi zapis/odczyt wartości z dowolnego portu I/O, w szczególności z naszego LPT. Wersja w C/C++ (tu: Microsoft Visual C++ .NET)
Wersja w Pascalu (tu: Borland Delphi 7 Personal)
Podane fragmenty kodu z powodzeniem zadziałają w systemach Windows 95/98, ale na platformach Win32 wywodzących się z linii NT, czyli Windows NT/2000/XP spotka nas niemiła niespodzianka. Próba wywołania funkcji na przykład zapisu do portu: WriteDataToPort ( 0x378, 0xAA ); // MSVC WriteDataToPort ( $378, $AA ); // D7 będzie miała fatalne skutki dla naszych apliakcji - wystąpi wyjątek, którego aplikacja użytkownika nie jest już w stanie obsłużyć. Tu kończy życie aplikacja w C++: A tu programik w Delphi: Oczywiście natychmiast powstają pytania - dlaczego tak się dzieje oraz jak się tych wyjątków pozbyć. Fragment poniżej, jako nieco zagmartwany, kieruję tylko do wyjątowo dociekliwych... Najpierw poczytajmy, co w tym temacie ma nam do powiedzenia firma Microsoft: 'Port I/O with inp() and outp() Fails on Windows NT'. Znajdziemy tam hasła - 'privileged processor instruction', '(non)privileged mode', etc... O co właściwie chodzi? Popatrzmy zatem na oficjalną dokumentację Intela: Dowiemy się następujących rzeczy (w formie ekstraktu ze wskazanych pdf-ów): Wykonanie instrukcji IN,OUT (oraz kilku innych) przez procesor pracujący w trybie chronionym, a tak właśnie jest w przypadku Win32, jest kontrolowane przez dwa parametry: Procesor obsługuje rozkazy OUT, IN według następującego algorytmu (tu przykład dla OUT): if ( (PE == 1) && ( (CPL > IOPL ) || (VM == 1) ) ) { // procesor pracuje w trybie chronionym i CPL > IOPL // lub w trybie virtual-8086 if ( bit_mapy_IOPM ( adres_portu ) == 1 ) { // operacja I/O jest niedozwolona i wygeneruje wyjatek // ogólnego zabezpieczenia #GP(0) } else { // operacja I/O jest dopuszczalna i zostanie wykonana out_port ( adres_portu ) = dana_dla_portu } } else { // gry procesor pracuje w trybie chronionym i zachodzi CPL <= IOPL // lub w trybie adresowania rzeczywistego (real mode) // operacja I/O jest dopuszczalna i zostanie wykonana out_port ( adres_portu ) = dana_dla_portu } /źródło: Instruction set reference, OUT, stona 3-315/ Każde zadanie wykonywane przez procesor ma swój własny segment TSS (Task State Segment), w którym przechowywane są charakterystyczne dla niego dane. Są to między innymi: kopie wartości rejestrów roboczych EAX,ECX,EDX,etc..., rejestrów segmentowych ES,CS,DS,etc..., wskaźnik na poprzednio wykonywany task. Sporo tego. Nas jednak najbardziej interesuje przechowywana w TSS wartość rejestru EFLAGS. Bity 12 i 13 tego rejestru określają wspomniany powyżej IOPL (I/O Privilege Level). Każdy uruchomiony program ma swój poziom uprzywilejowania (CPL - Current Privilege Level) i aby mógł się dostać do I/O bez generowania wyjątku, wartość ta musi być mniejsza lub równa wartości IOPL (CPL <= IOPL). Jeżeli nasz program ma zbyt mały poziom uprzywilejowania (CPL > IOPL), wtedy sprawdzana jest mapa indywidualnych uprawnień do I/O, także siedząca w segmencie stanu zadania TSS. Mapa ta nazywa się IOPM - I/O Permission Map. IOPM można postrzegać jako swego rodzaju maskę bitową, kolejne bity odpowiadają kolejnym adresom portów I/O. I jeżeli przy zbyt małym poziomie uprzywilejowania którykolwiek z tych bitów będzie 1 - zostanie wygenerowany wyjątek GP (General Protection exception), co spowoduje że system operacyjny zakończy działanie takiej aplikacji. Warto wiedzieć, że mapa uprawnień do I/O nie musi (choć może) opisywać wszystkich możliwych portów przestrzeni I/O. Może wybiórczo zabezpieczać tylko pewną ich część, wtedy pozostałe porty I/O będą traktowane tak, jakby dostęp do nich był zabroniony. Więcej informacji o poziomach zabezpieczeń (zwanych także żargonowo ringami /ring=pierścień/) w architekturze Intel-a znajdziemy w całkiem przystępnie napisanym opracowaniu 'Intel 80386 Programmer's Reference Manual'. A my pomału wracamy do naszej okienkowej rzczywistości. Na stronach Microsoft TechNet znajdziemy opracowanie 'Windows Architecture', z którego dowiemy się, że system Windows wykorzystuje tylko dwa z czterech możliwych poziomów zabezpieczeń - Ring 0 oraz Ring 3. Z najwyższym poziomem uprzywilejowania pracuje kod jądra systemu (Ring 0), aplikacje użytkownika pracują na Ringu 3. Oczywiście, w czasie wykonywania, aplikacje użytkownika wywołują różnorakie funkcje systemowe, z których spora część pracuje na Ringu 0 (w trybie jądra), ale na działanie ich kodu nie mamy wpływu. Po zakończeniu wykonywania usługi systemowej, wątek (zadanie) przełączane jest na Ring 3, co skutecznie uniemożliwia mu wykonywanie zabezpieczonych operacji np. blokuje dostęp do I/O. Tu możemy pokusić się o pierwsze wnioski - co należy zrobić, aby z poziomu aplikacji użytkownika dostać się do I/O systemu. Poziomu uprzywilejowania (czyli wartości CPL) nie możemy zmienić, ale możemy za to jak gdyby 'znieczulić' system, na fakt że nasz kod próbuje manipulować w przestrzeniu I/O procesora. Jak to zrobić? Wystarczy wyzerować mapę IOPM właściwą dla naszego procesu (aplikacji). Wprawdzie podczas wykonywania intrukcji IN,OUT warunki mówiące o zbyt małym poziomie uprzywilejowania będą spełnione, ale za to wyzerowane bity w IOPM powiedzą, że jednak możemy coś sobie do portu I/O zapisać lub z niego odczytać. Potrzebujemy więc specjalnej usługi systemowej, pracującej w trybie jądra (na Ringu 0), która wywołana z poziomu naszej aplikacji tak jej poustawia bity w mapie IOPM, aby wykonanie bezpośrednich operacji I/O nie zrobiło jej krzywdy. Oryginalne archiwum z oprogramowaniem znajduje się tu: directio.zip a lokalna kopia samego giveio.sys (wraz z programikiem do instalowania sterownika loaddrv.exe ) tutaj: giveio.zip Instalacja sterownika giveio w systemach NT/2000/XP Pliki giveio.sys i loaddrv.exe proszę rozpakować do foldera c:\giveio. Kolejne czynności prezentują zrzuty ekranu: otwieramy "Panel Sterowania": klikamy w ikonę "System": wybieramy zakładkę "Sprzęt" i klikamy przycisk "Menedżer urządzeń": z menu "Widok" wybieramy opcję "Pokaż ukryte urządzenia": w gałęzi "Sterowniki niezgodne z Plug and Play" odszukujemy "giveio" i wywołujemy okno właściwości: parametr "Uruchamianie - Typ" przestawiamy z "Żądanie" na "Automatycznie": Wykonując z linii poleceń komendę "loaddrv status giveio" upewniamy się, że konfiguracja sterownika została zmieniona. I to wszystko. Sterownik giveio jest bardzo wygodny do użycia, ponieważ do wykonania swojej pracy nie wymaga jakichś specjalnych zabiegów. W systemie zarejestrowany jest pod symboliczną nazwą "\\.\giveio". Wystarczy proste odwołanie do tego zasobu (jak do pliku - jego otwarcie), w tym momencie działający driver zmodyfikuje IOPM naszego procesu, co pozwoli na swobodne operacje I/O. Takie wywołanie drivera powinno mieć miejsce tylko raz, najlepiej gdzieś w funkcjach odpowiedzialnych za rozruch aplikacji. W Delphi dobrym miejscem wydaje się być handler obsługi zdarzenia OnCreate() formy głównej. W MSVC można wywołanie giveio umieścić w kodzie metody wirtualnej InitInstance() klasy dziedziczącej z CWinApp. Dla aplikacji konsolowych (obojetnie czy w Delphi czy w MSVC) - gdzieś na początku programu, aby przed pierwszą próbą zapisu lub odczytu z portów I/O. We wszystkich przypadkach uchwyt (handle) do drivera, uzyskany funkcją API o nazwie CreateFile(), porównujemy ze stałą INVALID_HANDLE_VALUE, która jest zdefiniowana w plikach nagłówkowych (lub unitach) dedykowanych Windows. Gdy uchwyt jest poprawny, po prostu zamykamy go funkcją CloseHandle(). Gdy uchwyt jest niepoprawny - mamy kłopot: giveio nie działa, może w ogóle nie jest zainstalowany, a może podaliśmy złą nazwę zasobu? Trzeba chwilkę pomyśleć, ale tak czy inaczej nie wolno dopuścić do pełnego uruchomienia aplikacji, ponieważ zakończy się to tak, jak na zrzutkach ekranu na samym początku tego artykułu. Na koniec przykładowe wywołania giveio, w MSVC i Delphi:
|