Laboratorium Podstaw Informatyki
Kierunek Elektrotechnika
Ćwiczenie HW-2
Elementy programowania sprzętu.
Obsługa karty z przetwornikami analogowo-cyfrowymi w trybie DMA.
Zakład Metrologii AGH
Kraków 2001
1. Układ kontrolera bezpośredniego dostępu do pamięci (DMA - Direct Memory Access).
Zadaniem układu DMA jest wykonywanie przesłań, czyli transferów danych pomiędzy urządzeniem i pamięcią, lub pamięcią i pamięcią, bez udziału procesora. Ściśle mówiąc udział procesora sprowadza się tylko do wcześniejszego zaprogramowania układu DMA. Zaprogramowanie to polega na podaniu adresu pamięci, ilości bajtów do przesłania, kierunku (do pamięci/ z pamięci, co jest równoważne: czytanie/pisanie), oraz trybu w jakim to przesłanie ma być wykonane. W niniejszej instrukcji nie będziemy zajmować się dokładnym opisem kontrolera DMA, bo od tego jest jego karta katalogowa. Opis ten ograniczymy do minimum pozwalającego na zaprogramowanie kontrolera DMA do transferu danych pomiędzy pamięcią, a kartą przetworników cyfra/analog.
Jak już zostało powiedziane aby kontroler mógł wykonać transfer należy podać adres pamięci i ilość danych, w bajtach, do przesłania. Jak pamiętamy z poprzednich ćwiczeń adres fizyczny, ten co jest podawany na magistralę, jest otrzymywany poprzez przesunięcie wartości segmentu o cztery bity w prawo, a następnie zsumowanie go z wartością offsetu. Właśnie w ten sposób otrzymuje się 20 bitowy adres fizyczny. Dalej ten 20 bitowy adres jest dzielony na starsze 4 bity i 16 młodszych. Starsze 4 bity wpisuje się do specjalnego rejestru strony (Page Register), młodsze zaś do układu DMA. Gdy kontrolę nad magistralą przejmuje układ DMA, to wystawia on 16 bitów, a pozostałe 4 pochodzą właśnie z tego rejestru. Oczywiście dzieje się to równocześnie. Jeśli w języku C mamy wskaźnik do typu char: char *data; to aby uzyskać adres fizyczny (ten co jest wystawiany na magistralę) musimy napisać:
char *data;
long f_adr = ((long)FP_SEG(data) << 4) + (long)FP_OFF(data);.
Konwersja do long jest konieczna, bo jak pamiętamy adres fizyczny to 20 bitów, a int ma tylko 16. Dalej musimy wpisać to do kontrolera DMA.
outportb(DMA_FF, 0); //wyzeruj przerzutnik flip-flop
outportb(CH1_BASE, f_adr & 0xFF); //najpierw młodsza część
outportb(CH1_BASE, (f_adr >> 8) & 0xFF); //potem starsza część
Oraz do dodatkowego rejestru, zwanego rejestrem stron.
outportb(DMAPAGE_CH1, f_adr >> 16); //najstarsze 4 bity z 20
Jak widać sposób wpisywania młodszej części adresu (16 bitów) do układu DMA jest dość skąplikowany, jest tak dlatego, że kontroler DMA 8237 jest układem, który został opracowany już dość dawno temu i dlatego posiada 8 bitową magistralę danych, przez którą komunikuje się z otoczeniem. Aby przesłać mu daną lub adres 16 bitowy trzeba to zrobić na dwa razy. Najpierw wpisuje się część mniej znaczącą (młodszą), a potem część bardziej znaczącą (starszą). Aby kontroler wiedział którą część teraz dostaje został wyposażony w przerzutnik zwany flip-flop. Przerzutnik ten, po każdym zapisie zmienia znak na przeciwny. Aby upewnić się, że kiedy my będziemy wpisywać dane stan przerzutnika będzie się zgadzał z naszą intencją musimy go wyzerować. Do tego celu służy rejestr oznaczony jako DMA_FF. Wpisanie czegokolwiek do tego rejestru zeruje przerzutnik flip-flop i pierwszy zapis do "podwójnego" rejestru będzie rozumiany jako zapis części mniej znaczącej.
Oprócz adresu musimy podać ilość danych które mają być przesłane. Ilość ta jest wartością 16 bitową i tak jak adres musi być wpisana na dwa razy. Musimy to więc zrobić w następujący sposób:
outportb(DMA_FF, 0); //wyzeruj przerzutnik flip-flop
outportb(CH1_COUNT, ile_danych & 0xFF); //młodsza część
outportb(CH1_COUNT, ile_danych >> 8); //starsza część
Jak do tej pory to dość beztrosko wpisywaliśmy sobie różne rzeczy do różnych rejestrów. W rzeczywistości jednak takie postępowanie może się źle skończyć. Co bowiem będzie gdy podczas naszych modyfikacji zawartości rejestrów zostanie uruchomiony transfer DMA? Aby uniknąć takich przykrych niespodzianek kontroler DMA jest wyposażony w rejestr masek. Rejestr ten służy do zamaskowania, czyli zablokowania żądań transmisji DMA. Po ustawieniu w nim bitu odpowiadającego danemu kanałowi kontroler nie będzie wykonywał transferów dla danego kanału, a my będziemy mogli spokojnie przeprogramować jego rejestry. Tak więc zanim zaczniemy modyfikować zawartość rejestrów danego kanału DMA najpierw ustawmy odpowiednie bity w rejestrze maski. Poniżej przedstawiony jest ten rejestr, widziany pod adresem DMA_MASK.
Np. chcąc modyfikować rejestry dla kanału 1 należy do rejestru DMA_MASK wpisać liczbę 5. Powoduje to zablokowanie transferów dla kanału 1.
outportb(DMA_MASK, 5); //zamaskuj (zablokuj) kanał 1
Oprócz adresu w pamięci i liczby danych, które chcemy przesłać należy podać tryb w którym to przesyłanie będzie się odbywać. Robimy to przy użyciu rejestru DMA_MODE, który przedstawiony jest poniżej.
Jak widać w rejestrze tym można ustawić kierunek transmisji, tzn. czytanie (read) lub pisanie (write). Zmniejszanie lub zwiększanie adresu, po wykonaniu każdego przesłania. Tryb autoinicjalizacji polegający na tym, że po przesłaniu jednego całego bloku danych układ automatycznie programuje się do przesłania następnego bloku, bez udziału procesora. Ostatnią rzeczą ustawianą w tym rejestrze jest tryb pracy. Możliwe są cztery tryby pracy.
Demand mode - tryb na żądanie: polega na tym, że transfer odbywa się tak długo jak długo aktywny jest sygnał żądania transferu.
Single mode - tryb pojedynczy: polega na tym, że wykonywane jest tylko jedno przesłanie, bez względu na czas trwania żądania. Chcąc przesłać następny bajt należy wycofać żądanie, a następnie je ponowić.
Block mode - tryb blokowy: jest przeciwieństwem trybu pojedynczego, w odpowiedzi na jedno, nawet krótko podtrzymywane żądanie transmitowany jest cały blok.
Cascade mode - tryb pracy kaskadowej dwóch układów 8237.
Oczywiście po zakończeniu programowania należy odmaskować (odblokować) dany kanał, czyli umożliwić mu realizację żądanych transferów.
W komputerze klasy PC znajdują się dwa układy typu DMA 8237 firmy Intel. Jeden układ DMA 8237 może obsłużyć 4 kanały.
2. Programowanie karty SoundBlaster do pracy w trybie DMA
Tak jak poprzednio przesunięcia adresów interesujących nas rejestrów względem adresu bazowego są znane i wynoszą:
#define BaseAddr0 0x220 /* pierwszy możliwy adres bazowy */
#define Reset 0x06 /* zerowanie karty */
#define RdData 0x0A /* odczyt danych z karty */
#define WrData 0x0C /* zapis danych do karty SB*/
#define WrCmd 0x0C /* zapis rozkazów do karty SB*/
#define StatWrBuf 0x0C /* status bufora wyjściowego (Write-Buffer Status) */
#define StatRdBuf 0x0E /* status bufora wejściowego (Read-Buffer Status) /
/* komendy dla karty SoundBlaster */
#define KONVERT_CA 0x10 /* komenda konwersji cyfra-analog bez DMA*/
#define KONVERT_AC 0x20 /* komenda konwersji analog-cyfra bez DMA*/
#define KONWERT_DMA_CA 0x14 /*komenda konwersji cyfra-analog z DMA*/
#define KONWERT_DMA_AC 0x24 /*komenda konwersji analog-cyfra z DMA*/
#define SET_TIME_CONST 0x40 /*ustaw okres próbkowania */
#define SpeakerOn 0xD1 /* włącz głośnik */
#define SpeakerOff 0xD3 /* wyłącz głośnik */
Urządzeniem, które będzie zgłaszało żądania transferów do kontrolera DMA, w naszym przypadku, będzie karta typu SoundBlaster. Po jej zresetowaniu i zdetektowaniu, tak jak to robiliśmy na poprzednich ćwiczeniach, musimy ją zaprogramować do transmisji w trybie DMA, mono, 8bitów na próbkę, bez autoinicjalizacji. Do programowania transmisji w trybie DMA służy komenda KONWERT_DMA_CA. Komende tą wpisujemy do rejestru WrCmd, ale dopiero, kiedy się upewnimy, że SoundBlaster jest na nią gotowy. Aby sprawdzić jego gotowość na przyjęcie komendy odczytujemy rejestr StatWrBuf, tak długo, aż odczytana wartość będzie miała wyzerowany najstarszy, czyli 7 bit (aż bit 7 stanie się równy ZERO).
Komendę KONWERT_DMA_CA uzupełniają dwa parametry o rozmiarze bajtu, które określają ilość 8 bitowych próbek - 1, które chcemy odtworzyć. Parametry te wpisujemy również do rejestru WrCmd i również, po upewnieniu się, że karta jest na nie gotowa.
Dodatkowo, wcześniej należy włączyć głośnik poprzez wpisanie do rejestru WrCmd komendy: SpeakerOn, oczywiście po upewnieniu się, że karta jest na niego gotowa.
Oprócz włączenia głośnika należy zaprogramować częstotliwość próbkowania przetworników na karcie. Częstotliwość ta określa ile próbek na sekundę będzie transmitowane poprzez kanał DMA do przetwornika cyfra/analog. Częstotliwość tą programujemy poprzez komendę SET_TIME_CONST z jednym parametrem o rozmiarze bajtu. Parametr ten jest wyliczany wg. wzoru:
unsigned char tc = (unsigned char)(256 - (1000000/ częstotliwość_próbkowania));
W naszym przypadku częstotliwość_próbkowania 11000. Po komendzie SET_TIME_CONST, jeśli SoundBlaster jest gotowy wysyłamy mu wartość tc.
3. Przydzielanie pamięci dla transferu DMA
Do tej pory pamięć dla programu przydzielaliśmy na dwa sposoby: statycznie w momencie kompilacji oraz dynamicznie poprzez użycie funkcji malloc. W przypadku kontrolera DMA sprawa jest nieco bardziej skomplikowana. Nie wystarczy przydzielić pamięć, trzeba również zapewnić aby została ona przydzielona we właściwym miejscu. Na tych zajęciach nieco sobie ten problem uprościmy i rozwiążemy go w sposób wprawdzie mało elegancki, ale ze to bardzo prosty. Wykorzystamy mianowicie pewien fragment pamięci, który jest nie używany przez system operacyjny DOS. Adres tego fragmentu posiada segment i offset odpowiednio równe 0x6000 i 0x0000. Aby uzyskać wskaźnik do tego obszaru należy skorzystać z makra MK_FP. Aby zapewnić, że wskaźniki będą 32 bitowe, a więc będą złożone z segmentu i offsetu należy kompilować program w modelu pamięci LARGE.
4. Podsumowane kroków algorytmu programowania karty SoundBlaster do pracy w trybie DMA.
1. Zresetuj i zdetektuj kartę.
2. Wczytaj zbiór do odtworzenia do pamięci pod adres:
char *data = MK_FP(0x6000, 0x0000);
3. Zamaskuj kanał 1 DMA
outportb(DMA_MASK, 5);
4. Wpisz adres pamięci w której znajdują się próbki dzwiękowe, ktore mają być odtworzone, do układu
DMA i rejestru strony
long f_adr = ((long)FP_SEG(data) << 4) + (long)FP_OFF(data);.
outportb(DMA_FF, 0); //wyzeruj przerzutnik flip-flop
outportb(CH1_BASE, f_adr & 0xFF); //młodsza część
outportb(CH1_BASE, (f_adr >> 8) & 0xFF); //starsza część
outportb(DMA_PAGE+3, f_adr >> 16); //najstarsze 4 bity z 20
5. Wpisz ilość bajtów do przesłania
outportb(DMA_FF, 0); //wyzeruj przerzutnik flip-flop
outportb(CH1_COUNT, ile_danych & 0xFF); //młodsza część
outportb(CH1_COUNT, ile_danych >> 8); //starsza część
6. Zaprogramuj tryb pracy układu:
czytanie (transfer z pamięci do karty), autoinicjalizacja wyłączona, zwiększanie adresu,transfer
w trybie pojedyńczym, tak więc do rejestru DMA_MODE wpisujemy wartość 0x49
outportb(DMA_MODE, 0x49);
7. Włącz głośnik.
8. Ustaw częstotliwość próbkowania przy użyciu komendy SET_TIME_CONST.
9. Zaprogramuj kartę SoundBlaster przy użyciu komendy KONWERT_DMA_CA, po które podaje się ilość bajtów do odtworzenia. Najpierw mniej znacząca część, potem bardziej
10. odblokuj układ DMA
outportb(DMA_MASK, 1);
11. Karta odtwarz próbki dzwiękowe z pamięci, poczekaj aż skończy.
12. Wyłącz głośnik, zakończ program.
Pamiętaj, że program musi być kompilowany w modelu LARGE.