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
 kod maszynowy   instrukcja   opis 
 E4 nn  IN AL, nn  pobiera do rejestru AL bajt z portu I/O o adresie nn 
 E5 nn  IN AX, nn  pobiera do rejestru AX słowo (16-bit) z portu I/O o adresie nn 
 E5 nn  IN EAX, nn  pobiera do rejestru EAX długie słowo (32-bit) z portu I/O o adresie nn 
 EC  IN AL,DX  pobiera do rejestru AL bajt z portu I/O o adresie podanym w rejestrze DX 
 ED  IN AX,DX  pobiera do rejestru AX słowo (16-bit) z portu I/O o adresie podanym w rejestrze DX 
 ED  IN EAX,DX  pobiera do rejestru EAX długie słowo (32-bit) z portu I/O o adresie podanym w rejestrze DX 

rozkaz OUT - wysyłanie danych do portu I/O
 kod maszynowy   instrukcja   opis 
 E6 nn  OUT nn, AL  wysyła bajt z rejestru AL do portu I/O o adresie nn 
 E7 nn  OUT nn, AX  wysyła słowo (16-bit) z rejestru AX do portu I/O o adresie nn 
 E7 nn  OUT nn, EAX  wysyła długie słowo (32-bit) z rejestru EAX do portu I/O o adresie nn 
 EE  OUT DX, AL  wysyła bajt z rejestru AL do portu I/O o adresie podanym w rejestrze DX 
 EF  OUT DX, AX  wysyła słowo (16-bit) z rejestru AX do portu I/O o adresie podanym w rejestrze DX 
 EF  OUT DX, EAX  wysyła długie słowo (32-bit) z rejestru EAX do portu I/O o adresie podanym w rejestrze DX 
nn - osmiobitowy adres portu w przestrzeni I/O procesora, zakres wartosci: 0x00...0xFF;
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:
  • rejestr danych (data register)
  • rejestr stanu (status register)
  • rejestr sterujący (control register)

    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:

     nazwa rejestru   adres dla LPT1   adres dla LPT2   adres dla LPT3   adres dla LPT4 
     dane  0x378  0x278  0x3BC (*)  0x2BC
     status  0x379  0x279  0x3BD  0x2BD
     kontrolny  0x37A  0x27A  0x3BE  0x2BE
    * - adres typowy dla kart Hercules (video+LPT)

    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:

     nazwa rejestru   bit 7   bit 6   bit 5   bit 4   bit 3   bit 2   bit 1   bit 0 
     dane   D7   D6   D4   D4   D3   D2   D1   DO 
     pin 9   pin 8   pin 7   pin 6   pin 5   pin 4   pin 3   pin 2 
     status   BUSY   /ACK   PAPER END   SELECT   /ERROR          
     pin 11   pin 10   pin 12   pin 13   pin 15          
     kontrolny            /SEL IN   /ERROR   /INIT   /AUTO FEED   /STROBE 
              pin 17   pin 15   pin 16   pin 14   pin 1 

    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)
    void WriteDataToPort ( unsigned short port_address, unsigned char port_data ) {
        __asm {
            ; do rejestru DX podajemy adres portu I/O
            mov dx, port_address
            ; do rejestru AL podajemy bajt do wysłania na port
            mov al, port_data
            ; i wysyłamy...
            out dx, al
        }
    }
    unsigned char ReadDataFromPort ( unsigned short port_address ) {
        unsigned char port_data;
        __asm {
            ; do rejestru DX podajemy adres portu I/O
            mov dx, port_address
            ; odczytujemy wskazany port
            in al, dx
            ; i zwracamy wynik...
            mov port_data, al
        }
        return ( port_data );
    }

    Wersja w Pascalu (tu: Borland Delphi 7 Personal)
    procedure WriteDataToPort ( port_address : word; port_data : byte );
    begin
        asm
            // do rejestru DX podajemy adres portu I/O
            mov dx, port_address
            // do rejestru AL podajemy bajt do wysłania na port
            mov al, port_data
            // i wysyłamy...
            out dx, al
        end;
    end;
    function ReadDataFromPort ( port_address : word ) : byte;
    var port_data : byte;
    begin
        asm
            // do rejestru DX podajemy adres portu I/O
            mov dx, port_address
            // odczytujemy wskazany port
            in al, dx
            mov port_data, al
        end;
        // i zwracamy odczytaną wartość...
        ReadDataFromPort := port_data;
    end;


    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:
  • 'Intel Architecture Software Developer's Manual, Volume 1: Basic Architecture' (szczególnie 9.5 Protected-mode I/O)
  • 'Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference Manual' (opis instrukcji IN oraz OUT)

    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:
  • wartość pola IOPL (I/O Privilege Level) w rejestrze EFLAGS
  • zawartość mapy zezwoleń na dostęp do I/O czyli IOPM, która jest umieszczona w segmencie TSS aplikacji

    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.

    Taką usługę można sobie napisać samodzielnie przy pomocy dostarczanego przez Microsoft pakietu 'Driver Development Kit', lub poszukać jej gdzieś w sieci. Jednym z bardziej znanych tego typu serwisem jest giveio.sys, którego autorem jest Dale Roberts. Szczegółowe informacje o budowie tego sterownika, zasadzie działania oraz jego kod źródłowy znajdują się w artykule 'Direct Port I/O and Windows NT'. Polecam tę lekturę, ogrom pracy badawczej nad tą szczególną stroną Win32 naprawdę robi wrażenie.

    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:
  • instalowanie usługi


  • zmiana trybu uruchamiania na tryb automatyczny
    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:

        (...)
        #include <windows.h>
        (...)
        int _tmain( int argc, _TCHAR* argv[] ) {
            HANDLE hGiveIoDrv;
            // wywołanie drivera giveio
            hGiveIoDrv = ::CreateFile (
                    "\\\\.\\giveio",
                    GENERIC_READ,
                    0,
                    NULL,
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    0 );
            // sprawdzenie czy jest dostępny
            if ( hGiveIoDrv == INVALID_HANDLE_VALUE ) {
                // jeżeli nie - komunikat o błędzie i koniec pracy
                ::MessageBox (0, "błąd dostępu do giveio...", "?!", MB_ICONSTOP);
                return ( 0 );
            }
            else {
                // zwolnienie uchwytu, nie będzie już potrzebny
                ::CloseHandle( hGiveIoDrv );
            }
            // poniższe wywołanie zadziała poprawnie
            WriteDataToPort ( 0x378, 0xAA );
            return ( 0 );
        }
        (...)
        uses (...), Windows;
        (...)
        procedure TMainForm.FormCreate( Sender: TObject );
        var hGiveIoDrv : THandle;
        begin
            // wywołanie drivera giveio
            hGiveIoDrv := CreateFile (
                    '\\.\giveio',
                    GENERIC_READ,
                    0,
                    nil,
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    0 );
            // sprawdzenie czy jest dostępny
            if hGiveIoDrv = INVALID_HANDLE_VALUE then
                begin
                    // jeżeli nie - komunikat o błędzie i koniec pracy
                    ShowMessage ('błąd dostępu do giveio...');
                    Application.Terminate;
                end
            else
                begin
                    // zwolnienie uchwytu, nie będzie już potrzebny
                    CloseHandle( hGiveIoDrv );
                end;
            // to wywołanie zadziałą poprawnie
            // i nic złego się nie wydarzy
            WriteDataToPort ( $378, $AA );
        end;

    A wszystko to tylko dlatego, aby wyświetlić wzorek na ośmiu zielonych diodach led podłączonych do kabla od drukarki...



    ...po prostu czasem brak mi słów.

  • Natasza Biecek 2004-2017/~, e-mail