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.