środa, 14 stycznia 2015

Port równoległy jako szybki rejestrator stanów logicznych

Trochę starego, pozornie niepotrzebnego sprzętu, Linux i (odrobinka) wiedzy z programowania może się przydać. Opiszę tutaj moją przygodę z LPT, czyli portem równoległym używanych w przeszłości głównie do łączenia z drukarkami. Chciałem tylko podejrzeć co się dzieje na wyjściach moich poczciwych Atmeg (mikrokontrolerów), kupno oscyloskopu wydawało się nadmierną rozrzutnością a wykorzystanie układów rejestrujących na USB nie miało w sobie znamion przygody...

Cel

Postawiłem sobie za cel możliwie szybki odczyt 4 kanałów, gdzie 0 V odpowiadał stanowi niskiemu, a 5 V stanowi wysokiemu.

Ryzyko

Należy pamiętać, że LPT jest zwykłym portem transmisji danych, którego prawdopodobnie można łatwo uszkodzić podając napięcia poza dopuszczalnym zakresem 0 - 5 V. Przy braku pewności co do jakości sygnału wejściowego warto pomyśleć o jakimś sprzętowym buforowaniu (zabezpieczeniu).

Charakterystyka LPT

Wszystko jest ładnie opisane na Wikipedii. Widać, że piny podzielone są na trzy rejestry: Data, Status i Control. Postanowiłem wykorzystać bity 6:3 rejestru "Status", które zawsze są traktowane jako wejście. Najbardziej znaczący bit 7 też jest wejściem, ale jego stan jest odwracany sprzętowo (podając 0 V widzimy "1"). Oczywiście nie ma problemu, by odwrócić odczyt z powrotem jak i wykorzystać niektóre z pozostałych pinów, no ale to wykracza nieco poza zakres początkowych eksperymentów.

Reasumując zostaną wykorzystane 4 piny (pierwsza kolumna to numeracja w 25-pinowej wtyczce LPT):
10     Ack     Status-6
12     Paper-Out     Status-5
13     Select     Status-4    
15     Error     Status-3

 Shooke CC-SA

Adresy rejestrów mojego LPT zaczynają się od 0x378, co jest wartością typową dla wbudowanego portu. To jest rejest "Data", "Status" będzie miał adres 0x379, a "Control" 0x37A. Zamiast szesnastkowego 0x379 zobaczysz w kodzie BASEPORT=0x378 i dalej "Status" jako BASEPORT+1 - dla wygody i uniknięcia głupich błędów. 

Komputer z odzysku



Z racji, że większość współczesnych komputerów nie ma już LPT lub istnieje ono tylko na płycie i wymaga dokupienia śledzia, postanowiłem wykorzystać stary, niepotrzebny już nikomu sprzęt - malutki komputer, którego przeszłości nie znam (przysięgam, że go nie ukradłem). Założenie było takie, że komputer będzie pracował bez monitora, nadzorowany z zewnątrz przez SSH.

Ten miniaturowy pecet jest oparty o procesor VIA Esther 1 GHz (vendor_id: CentaurHauls, wnuczek znanego swego czasu IDT WinChip?) z chłodzeniem pasywnym, ma 128 MiB RAM-u i pamięć stałą 32 MiB wciśniętą bezpośrednio w gniazdo IDE i widoczną jako zwykły dysk twardy. W sam raz do zainstalowania Tiny Core Linux.



Nie będę tutaj opisywał procesu instalacji Tiny Core Linux i konfiguracji SSH - bardzo dobra instrukcja znajduje się tutaj.

Za tester rejestratora posłużył układ Atmega 8, którego port PC 0:3 (4 nóżki) zostały połączone z pinami "Status" 3:6 (piny 10, 12, 13, 15) Oczywiście połączyć trzeba było też masę (piny 18-25) LPT z masą mikrokontrolera.

Notka nadprogramowa: płytki testowe dla Atmeg 8/48/168/328 leżały u mnie bezużytecznie 4 lata. Po studiach (mechanicznych, więc nie dziwcie się jeśli zobaczycie tu jakieś herezje w temacie elektroniki lub programowania) kupiłem sobie je pod choinkę, na długie zimowe wieczory. Dopiero teraz, widząc na pudełku rok 2010 uznałem, tak dalej nie może być.
Jakież było moje zdziwienie, jak okazało się że mikrokontrolery po przełączeniu na zewnętrzny kwarc nie dają znaku życia. "Ok, pomyliłem się w fuse-bitach, zablokowałem układ", pomyślałem. Ale po trzecim układzie, gdy moja samoocena zaczęła spadać, zacząłem też szukać błędu gdzie indziej. Złożyłem gołą Atmegę z dwoma kondensatorami i kwarcem... działa! Jak później się okazało, "fabryczne" kondensatory SMD na płytkach testowych były wadliwe - przylutowałem chamsko w ich miejsce kondensatory przewlekane, okazało się, że ani jedna Atmega nie była zablokowana.

Pierwsza próba - test szybkości odczytu

Spróbujmy sprawdzić jak szybko można odczytywać rejestry portu LPT. Przyda się tu mały program w C, nazwijmy go bench_lpt:


Całość została skompilowana na lokalnym komputerze, przesłana na zdalny przez SSH i tam uruchomiona z prawami roota (Linux domyślnie nie pozwala każdemu użytkownikowi grzebać w portach):

gcc -m32 -o bench_lpt bench_lpt.c
scp bench_lpt tc@ip_lub_nazwa_zdalnego_hosta:/home/tc/bench_lpt
ssh tc@ip_lub_nazwa_zdalnego_hosta
time sudo ./bench_lpt 
real    0m 12.92s
user    0m 12.92s
sys    0m 0.00s 

Zwróć uwagę na argument -m32, który sprawia, że program jest kompilowany skrośnie na 64-bitowym systemie na system 32-bitowy. 

Wykonanie 10 milionów odczytów rejestru "Status" zajęło niecałe 13 sekund, co daje ponad 770 000 odczytów na sekundę (Hz), każdy po bajcie (rejestr Status jest ośmiobitowy). Dla porównania USB potrafi osiągnąć tylko 1000 Hz (dlaczego nikt nie produkuje myszek dla graczy na LPT?).

W kodzie warto zwrócić uwagę na ioperm, dzięki któremu można uzyskać dostęp do rejestrów portu i inb które umożliwia odczyt ich stanu.

Próba druga - rejestrowanie odczyt po odczycie

Drugą próbą, a zarazem pierwszym pomysłem na rejestrowanie danych było odczyt 4 bitów (Status 6:3), łączenie dwóch takich odczytów w jeden znak (char), który ma przecież 8 bitów i wypisanie go na standardowym wyjściu. Jeśli przekazać standardowe wyjście do pliku, którym znajduje się na tzw. ramdysku, to i tu osiągi były całkiem dobre.

Dalej prędk... eee, szybkość (jestem mechanikiem, nie powinienem robić takich błędów) odczytu przekraczała 700 kHz:

time sudo ./lptr_bin10m > /mnt/ramdisk/testbin.dat
real    0m 13.69s
user    0m 13.66s
sys    0m 0.02s

Oczywiście nowoutworzony plik zajmował dokładnie 5 MB (10 milionów odczytów, każdy po pół bajta).

Część właściwa

Trzecia próba - odczyt z pomiarem czasu

Poniższy kod możecie też znaleźć na GitHubie.
Cała operacja będzie składała się z dwóch etapów:
- rejestracji sygnałów przez prosty, szybki, napisany w C program na zdalnym komputerze z portem LPT
- podglądu (rysowania przebiegu) na lokalnym komputerze i generowania pliku z danymi w postaci czystego tekstu - za to ma odpowiadać wolniejszy, ale łatwo modyfikowalny skrypt w Pythonie.

Nim przejdziemy do tego, chcę zwrócić uwagę na problem szybkiego zapisu danych. Zapis na dysk twardy mógłby okazać się niewystarczająco szybki, lub niestabilny. Najprostszym sposobem jest utworzenie tzw. ramdysku. Mój zdalny komputer ma 128 MiB RAM, co jest wartością zdecydowanie wystarczającą - utworzyłem "ramdysk" 32 MiB:

mkdir /mnt/ramdisk
sudo mount -t tmpfs -o size=32m tmpfs /mnt/ramdisk

Zauważ, że użyłem systemu plików tmpfs. Więcej o różnicach między tmpfs a ramfs można przeczytać tutaj.

Taki ramdysk zniknie oczywiście po restarcie. By temu zapobiec trzeba w /etc/fstab dodać linię:

tmpfs       /mnt/ramdisk tmpfs   nodev,nosuid,noexec,nodiratime,size=32M   0 0

Z racji użycia Tiny Core Linux musiałem w /opt/bootlocal.sh dodać:

/bin/mkdir /mnt/ramdisk
/bin/mount /mnt/ramdisk

w /opt/.filetool.lst dodać:

/etc/fstab

Oraz zapisać dane poleceniem:

sudo filetool.sh -b

Notka poboczna: jedynym domyślnie dostępnym edytorem tekstu w Tiny Core Linux jest vi. Nie jest on tak straszny jak początkowo się wydaje, jest tylko inny niż pozostałe (windowsowate) edytory. Mi wystarczyła wiedza, że "i" włącza tryb wstawiania, "Esc" wraca do trybu komend, komenda :q to wyjście a :wq to wyjście z zapisem.

Do rejestrowania stanu portu LPT posłużył mi tym razem nieco bardziej skomplikowany program, którego w listing w C widzisz poniżej. Zapisuje on tylko stan rejestru (a właściwie 4 interesujących nas bitów - zauważ r = r & 0b01111000;) gdy ulega on zmianie, do tego zapisując czas w którym ta zmiana nastąpiła. Jeśli port nie otrzymuje zmiennych danych, to program kończy działanie po wykonaniu określonej liczby pętli. 


Kompilacja programu i uruchomienie może wyglądać następująco:

gcc -m32 -static lpr_timed.c -o lpr_timed-static
sudo ./lpr_timed-static -o /mnt/ramdisk/test.bin
Loops: 7610340


Argument -static jest używany do statycznego linkowania - plik wykonywalny jest znacznie większy, ale mogłem dzięki temu uniknąć problemów ze zbyt starym glibc na zdalnym komputerze. Jak widać, w ciągu 10 sekund, program wykonał ponad 7 milionów pętli odczytu, przy czym plik wyjściowy miał zaledwie ok. 100 KiB. Czas został zapisany w postaci "float", stan rejestru jako "int" zajmujących po 4 bajty.

Czas trwania odczytu i dopuszczalną liczbę pętli można zmienić np. na 5 sekund i 5000000 pętli:
sudo ./lpr_timed-static -t 5 -l 5000000 -o /mnt/ramdisk/test.bin
 
Dlaczego nie ograniczam czasu w przypadku braku zmian stanu portu? Bo to wymagałoby odczytu zegara (clock_gettime(CLOCK_MONOTONIC, &end);) wewnątrz szybkiej pętli próbkowania portu LPT i mogłoby je spowolnić.


Na zrzucie powyżej widoczny podgląd zdekodowanych danych, oraz dodatkowo wartości zaimportowane do LibreOffice. Do wykonania podglądu, oraz wygenerowania logu w postaci czystego tekstu wykorzystałem poniższy skrypt w Pythonie oraz bardzo wygodny moduł Pythona pyqtgraph.


Przesłanie danych z zdalnego komputera oraz wygenerowanie podglądu:
gcc -m32 -o bench_lpt bench_lpt.c
scp tc@ip_lub_nazwa_zdalnego_hosta:/mnt/ramdisk/test.bin test.bin
python pybingraph.py


Skrypt otwiera plik binarny test.bin, wyświetla podgląd i zapisuje dane w pliku tekstowym test.csv, który można otworzyć w arkuszu kalkulacyjnym. Krotka bits = (0b00001000, 0b00010000, 0b00100000, 0b01000000) definiuje które bity i w jakiej kolejności mają być wyświetlone.

Kilka obserwacji

Przeanalizujmy wynik działania poniższego programu na Atmedze 8 taktowanej kwarcem 8 MHz. Aplikacja ta jest niczym innym jak programowym, czterokanałowym generatorem PWM.


Spójrzmy w fragment zanotowanych wyników (czas, bity PC3:PC0):

9.952934, 1, 1, 1, 1
9.952948, 1, 1, 1, 0
9.953118, 1, 1, 0, 0
9.953954, 1, 0, 0, 0
9.957483, 0, 0, 0, 0

Widać, że wyłączenie PC0 zajęło 14 mikrosekund. W programie odpowiada za to jedna pętla w przerwaniu po 50 cyklach procesora. Czy wykonując 5 pętli, każda po 10 cyklach otrzymamy to samo? Zamieńmy:

uint8_t pwmarray[4] = {1, 10, 50, 200};
OCR2 = 50;

na

uint8_t pwmarray[4] = {5, 50, 120, 200};
OCR2 = 10;


Wyniki to:

9.990124, 1, 1, 1, 1
9.990205, 1, 1, 1, 0
9.991048, 1, 1, 0, 0
9.992504, 1, 0, 0, 0
9.994389, 0, 0, 0, 0

PC0 przeszedł na stan niski dopiero po 81 mikrosekundach. Oczywiście samo 50 cykli procesora trwa tylko 6,25 µs, reszta to narzut pętli i instrukcji warunkowych. To mi uświadomiło jak beznadziejnym pomysłem może być wywoływanie przerwań co kilka cykli procesora (niski preskaler, niska wartość rejestru porównawczego OCR).

Dokonajmy jeszcze jednego testu, zamiast włączać wszystkie bity od PC0 do PC4 jednocześnie, włączajmy je sekwencyjnie modyfikując pętlę wewnątrz przerwania:

    for (i = 0; i < 4; ++i)
    {
        if (count >= pwmarray[i]) 
        {
            if (count < 255) //jesli licznik mniejszy niz 255 i wiekszy od wartosci w tablicy to porty są na stan niski
            {                
                PORTC &= ~(_BV(i)); //porty startuja od PC0
            }
            else //jeśli równy 255 to porty sa na stan wysoki wlaczane
            {
                PORTC |= _BV(i);
            }   
        }    
    }  


Fragment zanotowanych danych:
9.993909, 0, 0, 0, 1
9.993913, 0, 0, 1, 1
9.993917, 0, 1, 1, 1
9.993921, 1, 1, 1, 1
9.993936, 1, 1, 1, 0
9.994109, 1, 1, 0, 0
9.994965, 1, 0, 0, 0
9.998606, 0, 0, 0, 0

Widać, że zmiana każdego z kolejnych bitów na stan wysoki (PORTC |= _BV(i);) zajmuje 4 µs. Inaczej mówiąc powyższa pętla for musi potrzebować około 30 cykli mojej Atmegi. Szczerze mówiąc, nie sądziłem, że rejestrowanie stanów logicznych przez LPT będzie wystarczająco szybka by obserwować pojedyncze przejścia takiej pętli, nie wspominając o pomiarze czasu ich trwania.

Dosyć na dziś. Do tematu pewnie jeszcze kiedyś powrócę, gdy znów postanowię się oderwać od szarej rzeczywistości, chwycić lutownicę i popatrzeć na czarny ekran z białymi literkami. To naprawdę potrafi być relaksujące. Nie, nie mam kucyka.

PS Z LPT i drabinki R2R można zrobić też kartę dźwiękową, mono, 8 bitów rozdzielczości. Nawet Pulse Audio potrafi transmitować dźwięk jako ciąg 8-bitowych liczb całkowitych bez znaku... Ale to projekt na inny wieczór.

4 komentarze:

  1. P.S. Można też zrobić Covoxa stereo. https://www.flickr.com/photos/drhabber/sets/72157650463483346/ Polecam - więcej dłubania, ale różnica w brzmieniu jest kolosalna.

    OdpowiedzUsuń
  2. Widzę, że posiadasz bardzo specjalistyczną wiedzę. Z Twoich postów naprawdę dużo można się dowiedzieć. Mało jest wartościowych blogów z merytoryczna treścią, a Twój jak najbardziej przypadł mi do gustu. Niestety nigdy nie byłem dobry z działań na kodzie, ale na pewno coś z tego wyciągnę :). Dzięki!

    OdpowiedzUsuń
  3. Jestem bardzo zainteresowany tym blogiem. Regularne dodawanie wpisów i świetne redagowanie i konkretyzowanie materiałów. Można dodać do ulubionych!

    OdpowiedzUsuń
  4. Dla mnie ten blog jest nieprzeciętny.

    Dziękuję :D

    OdpowiedzUsuń