Sesję mam za sobą, zostało tylko zbieranie wpisów (na szczęście, nie muszę się tym zajmować – dzięki Ci Asiu) i w spokoju można zająć się odpoczywaniem, czyli pisaniem silnika. Na razie nie ma jednak czego pokazywać, a ponadto obiecałem przecież, że napiszę coś o programowaniu na niskim poziomie, a konkretnie “jak napisać program, aby działał robot”. Postanowiłem podzielić ten minicykl na dwie części – w pierwszej pokażę jak wykorzystać port LPT, aby pojazd na smyczy mógł zasuwać, a w drugiej zajmiemy się programowaniem mikrokontrolera.
Od razu chciałem coś wyjaśnić – z pewnością wielu z Was powie, że niepotrzebnie opisuję LPT, gdyż a) jest stare, b) jak ktoś będzie chciał zrobić coś konkretnego, to użyje mikrokontrolera, c) jest stare. Nie bez znaczenia pozostaje jednak fakt, że pierwszy etap tworzenia robota polega właśnie na wykorzystaniu portu LPT w jakimś starym komputerze, a dokładniej – należy odpowiednio reagować na przerwania. Poza tym to wyjście bardzo łatwo da się dostosować do naszych potrzeb i filozofia kodowania na tym złączu może przybliżyć także zainteresowanych do poradzenia sobie np. z USB (jednak ja do nich nie należę, jam skromny gracz-programista).
Dobrze, na początku jednak troszeczkę przygotowań. Po lewej
widzimy rysunek portu równoległego, który pozwoliłem sobie skopiować z odpowiedniej strony Wikipedii. Widzimy zatem, że w standardowej wersji mamy 25 dziwnie wyglądających bolców (złącza zwane samcami) lub 25 otworów na te bolce (złącza zwane samicami – jak widać, informatycy też ludzie). Tak czy inaczej, część z tych wejść/wyjść należy wykorzystać do naszych celów. Naszym szczególnym zainteresowaniem będą cieszyć się wyjścia DATA oraz wejścia ACK, PE, SEL, ERROR. Generalnie polecam zaznajomić się z tą stroną – dowiemy się tutaj między innymi, że LPT podzielone jest na blok danych, statusowy oraz kontrolny. Każdą część wykorzystamy, zatem spokojnie, nie palić się tak bardzo do tych samiczek/samców.
Być może część potrzebuje również wyjaśnienia co to jest to mistyczne “przerwanie”. Otóż, mamy dwa możliwe podejście do reagowania na jakiekolwiek zdarzenia – odpytywanie (polling) oraz system przerwań. Posłużę się przykładem, który zaproponował nasz wykładowca (pozdrawiam). Załóżmy, że siedzimy sobie w domu i czekamy na gościa (ciocia/wujek przychodzą na nasze urodziny i czekamy na prezent). Aby jednak umilić sobie oczekiwanie, bierzemy jakąś fascynującą książkę i czytamy. Jeżeli będziemy odpytywać, polegać to będzie na tym, że co chwilę będziemy się zrywać z wygodnego fotela, odkładać książkę i podchodzić do drzwi, aby spojrzeć czy goście już przyszli. Jeżeli natomiast pomyślimy i zastosujemy system przerwań (dzwonek do drzwi), będziemy mogli w spokoju wgłębiać się w powieść, aby potem nagle zdenerwować się, jak zadzwoni dzwonek (bo akurat przyszedł rachunek za prąd). Przerwanie, jak sama nazwa wskazuje, przerywa aktualnie wykonywany program, skacze do miejsca, gdzie znajduje się obsługa przerwania, a następnie powraca tam, gdzie byliśmy poprzednio.
Pozostaje jeszcze kwestia tego w czym będzie pisać. My akurat wykorzystywaliśmy (dzięki koledze – dzięki Ci Janie) Turbo C++ i nie mieliśmy dzięki niemu problemów ani z dziwnymi funkcjami języka C ani wstawkami assemblerowymi. Podejrzewam jednak, iż istnieje cała masa środowisk, w których można z powodzeniem pisać. Będziemy też potrzebowali biblioteki dos.h, która zawiera odpowiednie funkcje niskopoziomowe (choć nie jest niezbędna), ale jest ona dołączona w większości środowisk, zatem nie powinno być problemów.
Chciałbym też zwrócić uwagę na pewną charakterystyczną filozofię. Programowanie niskopoziomowe (czyli tak naprawdę sprzętowe) opiera się głównie na podaniu sekwencji jednego (kilku) bajtu(/ów) pod określony adres w komputerze tudzież ustawieniu paru bitów. Przypominam również, że 1 bajt = 8 bitów (1B = 8b). W ogóle polecam troszeczkę zaznajomić się z samym sposobem działania systemu – takimi podstawami. Tym niemniej, jak mój dalszy opis będzie niezrozumiały (a obawiam się, że miejscami mogę dobrze nie tłumaczyć), to śmiało linczować mnie w komentarzach i prosić o prostowanie.
Polecam naprawdę jeszcze raz tę stronę, dzięki której można dowiedzieć się wielu rzeczy, których tutaj nie opiszę dokładniej. Mam również pewne wątpliwości, czy mogę umieszczać przykładowe kawałki kodu – mam nadzieję, że nie będzie mi to poczytane za łamanie czegokolwiek i zdradę, ale jako chęć poduczenia zainteresowanych programowaniem niskopoziomowym – bądź co bądź, dosyć trudnym zagadnieniem, zwłaszcza dla osób, które przyzwyczajone są do obiektowości. I tak najważniejsze sprawy są tłumaczone na laboratoriach, wykładach, w książkach – warto mieć jednak również praktyczny poradnik przy sobie.
Dobrze, zatem zacznijmy może. Naszym zadaniem jest odbieranie przerwania na wejściu ACK, a następnie sprawdzaniu stanu lewego i prawego czujnika i odpowiednie sterowanie naszym pojazdem, aby cały czas utrzymywał się na trasie. Jedynka na czujniku oznacza “hej, jestem na trasie po tej stronie”, a zero “zabierz mnie stąd!”. Na silniki będą podawane odpowiednie sekwencje w postaci:
X X SP SL 1 0 1 0
Gdzie X to dowolna wartość (czyli najlepiej 0), SP to włączenie silnika prawego, a SL – lewego (skręcanie robota bowiem odbywa się w ten sposób, że jeden z silników jest wyłączony – kwestie technologiczno-mechaniczne). Sekwencja 1010 mówi nam tylko tyle, że cały czas jedziemy do przodu, jeżeli silnik pracuje. Należy również jeszcze pamiętać o przyjętej logice ujemnej – tak naprawdę zero oznacza, że silnik pracuje, a 1 – stoi. Dlaczego? Tak zaproponowali nasi elektronicy i okazało się to zbawienne w skutkach. Proponuję zacząć od rzeczy błahej, a mianowicie załączenia plików nagłówkowych oraz makr, które nam ułatwią sprawę.
1: #include "stdio.h"
2: #include "conio.h"
3: #include "dos.h"
4: #include "stdlib.h"
5: 6: #define LPT1_DATA 0x378
7: #define LPT1_STATUS 0x379
8: #define LPT1_CONTROL 0x37a
9: 10: #define IRQ7 0x0f
11: #define EOI 0x20
12: #define PORT_8259 0x21
13: 14: #define LEFT_SENSOR 0x20
15: #define RIGHT_SENSOR 0x08
16: 17: #define STOP 0x3a
18: #define UP 0x0a
19: #define LEFT 0x1a
20: #define RIGHT 0x2a
21: 22: int status_data;
23: int move_type;
24: void interrupt (*OldInterruptsLPT)();
Jak widać, mamy do czynienia z dużą inwazją liczb w postaci szesnastkowej. Ponadto narzuciłem już pewną rzecz – lewy czujnik mamy podpięty pod wejście PE, a prawy – pod ERR. To jest kwestia umowy, a wartości 0x20 oraz 0x08 wynikają z tego, jak wygląda port statustowy LPT (ponownie polecam tę stronę). Warto sobie uzmysłowić, że cały czas musimy myśleć bitowo – poczynając od prawej mamy jedynka i zera reprezentujące wartości 20, 21, 22, … aż do 27.
Zacznijmy jednak pisać. Najpierw kod, a potem trochę opowiem.
1: asm { 2: cli 3: } 4: 5: outportb(LPT1_CONTROL, inporti(LPT1_CONTROL) | 0x10); 6: outportb(PORT_8259, inporti(PORT_8259) & 0x7f); 7: outportb(LPT1_DATA, STOP); 8: printf("Nacisnij ENTER, aby ruszyc\n");
9: getch(); 10: OldInterruptsLPT = getvect(IRQ7); 11: setvect(IRQ7, InterruptsLPT); 12: outportb(LPT1_DATA, UP); 13: 14: asm { 15: sti 16: } 17: 18: getch(); 19: 20: asm { 21: cli 22: } 23: 24: outportb(LPT1_DATA, STOP); 25: outportb(LPT1_CONTROL, inporti(LPT1_CONTROL) & 0xef); 26: outportb(PORT_8259, inporti(PORT_8259) | 0x80); 27: 28: asm { 29: sti 30: } Wygląda strasznie, prawda? Spokojnie, najgorsze jeszcze przed nami – trzeba to w końcu omówić.
Instrukcja assemblera “cli” na naszym procesorze oznacza, że wyłączamy w tej chwili obsługę wszystkich przerwań (analogicznie “sti” je włącza). Po co to robimy? Wyobraźmy sobie sytuację, że spokojnie konfigurujemy sobie nasz port LPT, aby działał tak, jak tego chcemy, a tu nagle wyskoczy nam jakieś zwierzę futerkowe i naciśnie klawisz. Naciśnięcie klawisza to również zgłoszenie przerwania do systemu, a co za tym idzie – nasza konfiguracja zostaje przerwana i mimo, że zostanie wznowiona, to mogą pojawić się problemy, które trudno wyjaśnić (kolorowy ekran, kolorowy ekran z dziwnymi znaczkami, dym unoszący się ze stacji dyskietek itd.). Dlatego bezpieczniej jest czasami “zamknąć kolejkę”.
Następnie w końcu coś robimy z LPT – na piątym bicie (licząc od prawej) pod adresem portu kontrolnego LPT (0x37A) mamy informację, czy ACK będzie służyło jako źródło przerwań. Oczywiście, o to nam chodzi, zatem wykorzystujemy sumę bitową (operator |) i zmieniamy piąty bit na jedynkę. Jak widać, wykorzystujemy do tego funkcje z pakietu dos.h – outportb (wysłanie wartości na dany adres) i inportb (odebranie wartości z danego adresu). Dalej następuje odmaskowanie przerwania IRQ7 – ciekawe, prawda? Tak naprawdę, cały system przerwań dyrygowany jest przez układ w komputerze, który ma tajemniczy kryptonim 8259 (lub 8259A). Jeżeli chcemy cokolwiek mieszać przy przerwaniach, chcąc nie chcąc, musimy się tą “pięćdziesiątką dziewiątką” zainteresować. Tutaj, wykorzystując iloczyn bitowy (operator &), zmieniamy ósmy bit (pierwszy od lewej) na 0, co umożliwia odbieranie przerwań z tej strony (a jak się dalej okaże – z LPT). Na wszelki wypadek czekamy na reakcję użytkownika (aby przygotował sobie robota, wsadził mu baterie, nakarmił, itd.), a potem… Właśnie, co potem? Pod OldInterrupts zapisujemy naszą standardową procedurę obsługi przerwania (domyślną systemową), którą potem powrócimy. Odwołujemy się przy tym do wejścia IRQ7, pod które podpięte jest właśnie nasze LPT (pełną listę można znaleźć tutaj – jak widać, port równoległy LPT jest pod adresem 0x0F). Dalej wrzucamy naszą obsługę przerwania, czyli InterruptsLPT (którą sobie potem pokażemy). Od tej pory, jeżeli przyjdzie przerwanie od portu LPT, zostanie podjęta akcja, którą sami sobie napiszemy w tej funkcji InterruptsLPT. Nie pozostaje nam teraz nic innego jak zadać pojazdowi jechanie do przodu (czyli wysłać na port danych LPT (adres 0x378) odpowiednią wartość) i czekać na naciśnięcie dowolnego klawisza (które przerwie nam program i zatrzyma robota).
Po getch() następuje oczywiście sprzątanie po sobie – zatrzymujemy robota, zerujemy wcześniej ustawione bity, przywracamy starą obsługę przerwań i voila.
Jeszcze taka ciekawostka – o ile funkcje setvect oraz getvect nie są takie bardzo łatwe do napisania (a przynajmniej nie mieliśmy na to za dużo czasu – przykładowy kod znajduje się tutaj), o tyle outport i inport możemy sami w prosty sposób zaimplementować (o ile oczywiście uznajemy assemblera za język zrozumiały i nadający się do dziennego spożycia):
1: unsigned char inporti (unsigned short port)
2: { 3: unsigned char value;
4: asm { 5: mov dx, port 6: in al, dx
7: mov value, al
8: } 9: return value;
10: } 11: 12: void outporti (unsigned short port, unsigned char value)
13: { 14: asm { 15: mov dx, port 16: mov al, value
17: out dx, al
18: } 19: } 20: To jest po prostu wysłanie/odebranie odpowiedniej wartości za pomocą poleceń mov, in oraz out - prawdę mówiąc, jest to chyba jedna z kilku rzeczy, które umiem zrobić samodzielnie w assemblerze, zatem też nie mam się czym chwalić.
W porządku, w takim razie przejdźmy do naszej procedury obsługi przerwania (zwracam uwagę na słówko kluczowe “interrupt”).
1: void interrupt InterruptsLPT()
2: { 3: asm { 4: cli 5: } 6: 7: while (1)
8: { 9: 10: status_data = inporti(LPT1_STATUS); 11: status_data &= 0x28; 12: 13: if ((status_data & RIGHT_SENSOR) && (status_data & LEFT_SENSOR))
14: { 15: move_type = UP; 16: } 17: else if (!(status_data & RIGHT_SENSOR) && (status_data & LEFT_SENSOR))
18: { 19: move_type = LEFT; 20: } 21: else if ((status_data & RIGHT_SENSOR) && !(status_data & LEFT_SENSOR))
22: { 23: move_type = RIGHT; 24: } 25: else
26: { 27: move_type = STOP; 28: } 29: 30: outporti(LPT1_DATA, move_type); 31: 32: if (move_type == STOP || move_type == UP)
33: { 34: break;
35: } 36: } 37: 38: outporti(0x20, EOI); 39: 40: asm { 41: sti 42: } 43: } 44: Znowu stosujemy myśl taktyczną związaną z blokowaniem przerwań i działamy w pętli. Pozornie wiecznej, ale nie do końca. Tutaj następuje zmieszanie systemu przerwań z pollingiem – po wejściu w stan obsługi przerwania zwyczajnie przepytujemy stan czujników podpięte do portu statusowego (dla ułatwienia wyciągamy z niego tylko odpowiednie bity), a następnie sprawdzamy stan i działamy zgodnie z założeniami z tabelki:
| Lewy czujnik | Prawy czujnik | Decyzja |
| Na trasie | Na trasie | Do przodu |
| Na trasie | Poza trasą | W lewo |
| Poza trasą | Na trasie | W prawo |
| Poza trasą | Poza trasą | Stop / Cofamy się |
Oczywiście, to tylko przykład – równie dobrze można zbudować robota na czterech czujnikach i w różny sposób sterować silnikami. Powyższe ma tylko na celu zobrazowanie samej mechaniki działania. Z pętli wychodzimy oczywiście, gdy pozycja robota jest bezpieczna i zaraz potem wysyłamy słowo sterujące EOI (które jest pewną odmianą słowa OCW2 (no nie mówcie, że pierwszy raz słyszycie te nazwy!)), mówiące “kończ waść przerwanie, wstydu oszczędź” na adres 0x20.
Oczywiście, do naszego programiku można również dodać obsługę dźwięków. Robi się to poprzez zmuszenie do współpracy PC Speakera (tego słynnego głośniczka) oraz układu 8253, który jest licznikiem systemowym. Jak to wykonać – jest świetnie opisane tutaj.
To byłby koniec pierwszej części wesołego cyklu. Jeżeli teraz naszego robota przypniemy do złącza LPT i uruchomimy własnoręcznie napisany program, powinno być już lżej. Choć, znając życie, nie zadziała poprawnie za pierwszym razem.
W drugiej (i ostatniej) części powiemy o tym, jak pozbyć się kabla i załadować mikrokontroler z programem na grzbiet pojazdu. Co ciekawe, jest to dużo prostsze, aniżeli posługiwanie się naszym ogromnym komputerem – zresztą każdy oceni, jak zobaczy drugi artykuł.
Pozdrawiam, dziękuję i jak zawsze zachęcam do dyskusji – SceNtriC
Ps. Jeszcze raz proszę o wyrozumiałość, jeżeli chodzi o dzielenie się kodem źródłowym. Jeżeli jednak ktoś ma zastrzeżenia i uważa, że tego kodu tutaj nie powinno być (tzn. w takiej formie, jak jest teraz, że powinno być bardziej zaciemnione) - proszę śmiało mi dać znać. Za wszystkie kłopoty przepraszam.
2 komentarze:
Co stało za decyzją, że oprogramowanie po stronie PC napisaliście w środowisku pod system DOS i na tak niskim poziomie? Dostęp do portów COM czy LPT można przecież mieć z programów w Windows, pisanych w kodzie natywnym albo nawet w .NET.
Po prostu takie było polecenie - sam przedmiot nazywa się Architekturą Systemów Komputerowych i opiera się głównie na zabawie assemblerem. Ponieważ podejrzewam, iż jakby prowadzący zobaczył kod działający na .NET i nie mający nic do czynienia z DOSem, nie zaliczyłby nam projektu, zatem wszystko pisaliśmy (nie tylko my, ale inne grupy również) możliwie jak najniżej. Czasami nawet leżałem na podłodze pisząc i testując kod.
Pozdrawiam
Prześlij komentarz