6.2.1.4. Hangfelvétel és lejátszás 8 bites auto-init DMA segítségével
Ebben a fejezetben nem térek ki külön a megszakítás vezérlő és a DMA vezérlő ismertetésére, illetve programozására, mivel arról egy külön könyvet lehetne írni. Azonban a jobb megérthetőség érdekében a [4] alapján készült Pascal unitot teljes terjedelmében közlöm:
unit sb_aiDMA;
{$g+}
interface uses dos;
type
mymem = array[0..65534] of byte;
memptr = ^mymem;
const
SBbase : word = $220;{ Báziscím }
SB_IRQ : byte = $05; { Megszakítási vonal száma }
SB_DMA : byte = $01; { DMA csatorna száma }
SB_TYPE : byte = $00; { Hangkártya típus }
{ A DMA controller chip I/O címei }
DMA_cmd = $08; { 8 bites DMA parancsregiszter}
DMA_mask = $0a; { 8 bites DMA maszkregiszter }
DMA_bpt = $0c; { 8 bites DMA bájtmutató }
DMA_mode = $0b; { 8 bites DMA üzemmód regiszter }
DMA_stat = $08; { 8 bites DMA állapotregiszter }
DMA_req = $09; { 8 bites DMA szoftveres kérés regiszter }
DMA_init = $0d; { 8 bites DMA inicializáló regiszter }
{ A buffer feldolgozás jelzése a hívó programnak }
RfshRequest : boolean = false;
PlayMode = $58; { MEM->I/O auto-init mód (lejátszás)}
RecordMode = $54; { I/O->MEM auto-init mód (felvétel)}
var
DMAbuffer : memptr; { A teljes DMA buffer mutatója }
DMAptr : memptr; { A feldolgozandó bufferrész mutatója}
BufferSize : word; { A teljes DMA buffer mérete bájtban }
HalfSize : word; { A DMA buffer méretének fele }
{Sound Blaster DSP specifikus eljárások és függvények}
function GetBlasterENV(var Base:word; var IRQ,DMA,Typ:byte):boolean;
function DSPreset:boolean;
procedure DSPwrite(DSPdata:byte);
function DSPread:byte;
procedure SetSampleRate(SR:word);
procedure SetDMAblockSize(size:word);
{ Megszakítás vezérlő és DMA vezérlő specifikus eljárások }
procedure EnableIRQline(Line:byte);
procedure DisableIRQline(Line:byte);
procedure DMA_addr(a:pointer; var p,s,l:word);
procedure InitDMAcontroller(Transfermode:byte);
procedure DoneDMAcontroller;
procedure InitInterrupt(UserDefinedIRQ:pointer);
procedure DoneInterrupt;
{ DMA bufferkezelő eljárások és függvények }
function AllocDMAbuffer(size:word):integer;
procedure FreeDMAbuffer;
procedure ClearDMAbuffer;
procedure NextBuffer;
{ DMA átvitel vezérlő eljárások }
procedure StartAutoInitDMAplay;
procedure StartAutoInitDMArecord;
procedure StopAutoInitDMA;
implementation
const
{ A 8 bites DMA vezérlő regisztereinek I/O címei }
DMApages : array[0..3] of byte=($87,$83,$81,$82);
DMAoffsets : array[0..3] of byte=($00,$02,$04,$06);
DMAsizes : array[0..3] of byte=($01,$03,$05,$07);
{ Az aktuális DMA csatorna regisztereinek címei }
DMApage : byte = $83;
DMAoffset : word = $0002;
DMAsize : word = $0003;
var
{ Az eredeti megszakításvektort tároló változó }
OldSBorgIRQ : procedure;
{ A DMA buffer lefoglalásához szükséges változók }
DMAp1,DMAp2 : pointer;
DMAs1,DMAs2 : word;
function GetBlasterENV(var Base:word; var IRQ,DMA,Typ:byte):boolean;
var
s : string[128];
i : integer;
begin
GetBlasterENV:=false;
s:=getenv(‘BLASTER’); { Lekérdezzük a BLASTER környezeti változót }
if s[0]=#0 then exit; { Kilépünk, ha nincs ilyen változó }
{ Megállapítjuk az I/O báziscímet }
i:=pos(‘A2’,s);
if i>0 then val(‘$’+copy(s,i+1,3),Base,i);
if i<>0 then exit;
{ Megállapítjuk a megszakítási vonal számát }
i:=pos(‘ I’,s);
if i>0 then val(copy(s,i+2,1),IRQ,i);
if i<>0 then exit;
{ Megállapítjuk a DMA csatorna számát }
i:=pos(‘ D’,s);
if i>0 then val(copy(s,i+2,1),DMA,i);
if i<>0 then exit;
{ Megállapítjuk a kártya típusszámát }
i:=pos(‘ T’,s);
if i>0 then val(copy(s,i+2,1),Typ,i);
if i<>0 then exit;
GetBlasterENV:=true;
end;
A GetBlasterEnv függvénysegítségével kérdezzük le a BLASTER környezeti változót, amelyből megtudhatjuk a kártya báziscímét, az általa használt megszakítási vonal sorszámát és DMA csatorna számát.
function DSPreset:boolean; assembler;
asm
…{ Ezt a rutint már ismertettem a DSP programozása kapcsán}
end;
procedure DSPwrite(DSPdata:byte); assembler;
asm
… { Ezt a rutint már ismertettem a DSP programozása kapcsán}
end;
function DSPread:byte; assembler;
asm
… { Ezt a rutint már ismertettem a DSP programozása kapcsán}
end;
A fenti három szubrutinnal már találkozhattunk a DSP programozása kapcsán.
procedure SetSampleRate(SR:word);
var
TC : byte;
begin
TC:=256-round(1000000/SR); { Kiszámítjuk az időállandót, }
DSPwrite($40); { majd kiírjuk a DSP-re }
DSPwrite(TC);
end;
A SetSampleRate eljárás a mintavételezési frekvencia beállítására szolgál.
procedure SetDMAblocksize(size:word);
begin
DSPwrite($48); { Set DMA blocksize parancs }
dec(size); { A méretet csökkenteni kell!}
DSPwrite(LO(size)); { Kiírjuk az alsó bájtot }
DSPwrite(HI(size)); { Kiírjuk a felső bájtot }
end;
A SetDMABlocksize arra szolgál, hogy általa beállítsuk a DMA blokk méretét a DSP számára. Auto-init DMA használatakor a Set DMA blocksize DSP paranccsal kell megadni a DSP számára az átvitt bájtok számát.
procedure SB_AutoInitDMA_IRQ; assembler;
asm
push DS { Tároljuk a használt regisztereket }
push ax
push dx
mov ax,Seg @DATA { A DS szegmensregisztert az }
mov DS,ax { eredeti adatszegmensre állítjuk }
mov dx,SBbase
add dx,$0e { Olvassuk DSP data avail portot, }
in al,dx { ez jelzi a hangkártyának, hogy a }
{ megszakítást fogadtuk }
mov RfshRequest,TRUE { A buffer frissítése szükséges jel }
mov al,020h { EOI jelet küldünk a }
out 020h,al { megszakítás vezérlőnek }
pop dx
pop ax
pop DS { Visszaolvassuk a regiszterek értékét }
iret { Visszatérünk a megszakításból }
end;
Az SB_AutoInitDMA_IRQ egy megszakítási rutin. Az auto-init DMA adatátvitel folyamán a hangkártya minden blokk lejátszásakor, illetve felvételekor megszakítást generál. A megszakítási rutin nyugtázza a megszakításkérést a hangkártya felé, és igaz értékűre állítja RfshRequest változót, majd nyugtázza a megszakítást a megszakításvezérlőnek is. A DMA átvitel ez alatt is zavartalanul folyik tovább.
procedure NextBuffer; assembler;
asm
mov ax,DMAbuffer.word { Az aktuális mutató a buffer elejére }
cmp ax,DMAptr.word { mutat? }
jnz @1sthalf { Ugrás tovább, ha nem }
@2ndfalf:
add ax,HalfSize { Ha igen, a második fele következik }
@1sthalf:
mov DMAptr.word,ax { Egyébként az első }
end;
A buffer felének feldolgozásakor a NextBuffer eljárás segítségével válthatunk aktív buffert.
procedure EnableIRQline(Line:byte); assembler;
asm
mov cl,Line { A vonal száma a CL regiszterbe kerül }
and cl,00000111b { Csak az alsó három bitet hagyjuk meg }
mov ah,11111110b { Maszk: ez a bit nulla lesz }
rol ah,cl { A 0 bitet a megfelelő helyre toljuk }
in al,021h
and al,ah { Engedélyezzük az adott vonalat }
out 021h,al
end;
procedure DisableIRQline(Line:byte); assembler;
asm
mov cl,Line { A vonal száma a CL regiszterbe kerül }
and cl,00000111b { Csak az alsó három bitet hagyjuk meg }
mov ah,00000001b { Maszk: ez a bit egy lesz }
rol ah,cl { Az 1 bitet a megfelelő helyre toljuk }
in al,021h
or al,ah { Tiltjuk az adott vonalat }
out 021h,al
end;
Az EnableIRQline, illetve a DisableIRQline engedélyezi, illetve tiltja az adott megszakítási vonalat.
procedure DMA_addr(a:pointer; var p,s,l:word);
var
la : longint;
begin
{ la=20 bites fizikai cím, azaz 16*szegmens+offszet }
la:=(longint(seg(a^)) shl 4)+ofs(a^);
p:=la shr 16; { Lapszám: 0..15 közé fog esni }
s:=la and 65535; { Start Address a page-en belül }
l:=65536-s; { A page-ből még ennyi bájt van hátra }
end;
A DMA_addr segítségével az a pointer által mutatott címből előállítja a hozzá tartozó 64K-s DMA lapszámot és a lapon belüli eltolást. A lapszám: p, az eltolás: s, a lapon rendelkezésre álló bájtok száma pedig: l.
procedure InitDMAcontroller(Transfermode:byte);
var
p,s,l : word;
begin
{A táblázatokból kiolvassuk az adott csatornához tartozó I/O címeket}
DMApage := DMApages[SB_DMA]; { Lapregiszter }
DMAoffset := DMAoffsets[SB_DMA]; { DMA címregiszter }
DMAsize := DMAsizes[SB_DMA]; { Számláló regiszter }
port[DMA_mask]:=SB_DMA or 4; { Tiltjuk a DMA-t a csatornán }
port[DMA_bpt]:=$00; { Töröljük a bájtmutatót }
port[DMA_mode]:=Transfermode or SB_DMA; { auto-init mód a csatornára }
DMA_addr(DMAbuffer,p,s,l); { Kiszámítjuk a cím komponenseit }
port[DMApage]:=p; { Beállítjuk a lapregiszter, }
port[DMAoffset]:=LO(s); { majd a csatorna címregiszterét }
port[DMAoffset]:=HI(s);
port[DMAsize]:=LO(BufferSize-1); { Beállítjuk a számláló regisztert}
port[DMAsize]:=HI(BufferSize-1); { blokk mérete -1 ! }
port[DMA_mask]:=SB_DMA and 3; { Engedélyezzük a DMA-t }
end;
InitDMAcontroller: a 8 bites DMA vezérlő beállítása az auto-init DMA módhoz. Az eljárás beállítja a DMA vezérlő üzemmódját, a lapregiszter, a csatorna cím- és számlálóregiszterét.
procedure DoneDMAcontroller;
begin
port[DMA_mask]:=SB_DMA or 4; { Tiltjuk a DMA-t a csatornán }
end;
procedure InitInterrupt(UserDefinedIRQ:pointer);
begin
GetIntVec($08+SB_IRQ,@OldSBorgIRQ); { Tároljuk az eredeti vektort }
if UserDefinedIRQ=nil then
SetIntVec($08+SB_IRQ,@SB_AutoInitDMA_IRQ) { Beállítjuk az újat }
else
SetIntVec($08+SB_IRQ,UserDefinedIRQ);
EnableIRQline(SB_IRQ); { Engedélyezzük az adott vonalat}
end;
Az InitInterrupt eljárással állítjuk be a megszakítási vektort. Az eredeti megszakítási vektort elmenti, majd beállítja az általunk definiáltat.
procedure DoneInterrupt;
begin
DisableIRQline(SB_IRQ); { Tiltjuk az adott vonalat }
SetIntVec($08+SB_IRQ,@OldSBorgIRQ);{ Visszaállítjuk az eredeti vektort }
end;
procedure StartAutoInitDMAplay;
begin
DSPwrite($d1); { Parancs: DSP speaker on }
DSPwrite($1c); { Parancs: DSP auto-init DMA DAC}
end;
StartAutoInitDMAplay: az auto-init DMA lejátszás megkezdése.
procedure StartAutoInitDMArecord;
begin
DSPwrite($d3); { Parancs: DSP speaker off }
DSPwrite($2c); { Parancs: DSP auto-init DMA ADC}
end;
StartAutoinitDMArecord: az auto-init DMA felvétel elindítása.
procedure StopAutoInitDMA;
begin
DSPwrite($d3); { Parancs: DSP speaker off }
DSPwrite($d0); { Parancs: Stop DMA transfer }
DSPwrite($da); { Parancs: Stop auto-init DMA }
DSPwrite($d0); { Parancs: Stop DMA transfer }
end;
StopAutoInitDMA: az auto-init DMA átvitel leállítása.
procedure ClearDMAbuffer;
begin
fillchar(DMAbuffer^,BufferSize,$80);{ Minden minta nulla jelszintű }
end;
ClearDMAbuffer: a DMA buffer törlése.
function AllocDMAbuffer(size:word):integer;
var
p,s,l,i : word;
begin
DMAs1:=size; { Első menet }
DMAs2:=0;
AllocDMAbuffer:=-1;
if MaxAvail<size then exit;{ Azonnal kilépünk, ha nincs elég hely} getmem(DMAp1,DMAs1); { Lefoglaljuk a megfelelő blokkot }
DMA_addr(DMAp1,p,s,l); { Meghatározzuk a cím komponenseit }
if l>=size then { Van elég hely a lapon? }
begin { Ha igen… }
DMAbuffer:=DMAp1; { Ez lesz a DMAbuffer }
DMAptr:=DMAp1; { A DMAptr is ide mutat }
BufferSize:=size; { Beállítjuk a buffer méretét }
HalfSize:=BufferSize shr 1;{ Ennek fele a HalfSize }
AllocDMAbuffer:=1; { A lefoglal s sikeres volt (1. menet)}
ClearDMAbuffer; { Töröljük a buffert (csend) }
exit; { Visszatérünk a függvényből }
end;
inc(p); { A következő lap lesz a jó }
DMAs2:=l; { Itt még ennyi hely kell }
getmem(DMAp2,DMAs2); { Lefoglaljuk }
DMAbuffer:=ptr(p shl 12,$0000);{ Előállítjuk a buffer mutatóját }
DMAptr:=DMAbuffer; { A DMAptr is ide mutat }
BufferSize:=size; { Beállítjuk a buffer méretét }
HalfSize:=BufferSize shr 1; { Ennek fele a HalfSize }
AllocDMAbuffer:=2; { A lefoglal s sikeres volt (2. menet)}
ClearDMAbuffer; { Töröljük a buffert (csend) }
end;
AllocDMAbuffer: helyfoglalás a DMA buffer számára.
procedure FreeDMAbuffer;
begin
if DMAs2<>0 then freemem(DMAp2,DMAs2); { A terület felszabadítása }
if DMAs1<>0 then freemem(DMAp1,DMAs1); { A terület felszabadítása }
DMAs2:=0;
DMAs1:=0;
end;
FreeDMAbuffer: az AllocDMAbuffer eljárás által lefoglalt területek felszabadítása.
END.
A felvételre természetesen használhattunk volna 16 bites auto-init DMA átviteli módot is, de a tapasztalat az, hogy a Windows nem igazán örül neki, ha a 16 bites DMA csatornát szeretnénk használni, ugyanis azt előszeretettel lefoglalja saját erőforrásai számára. Emiatt már a kártya telepítésekor érhetnek minket meglepetések, hiszen csakis a 8 bites DMA csatornákat ajánlja fel az említett operációs rendszer a hangkártya számára. Erről egyébként több hardveres kézikönyv is említést tesz és azzal vígasztal minket, hogy ilyen esetben a 16 bites DMA átvitel is a 8 bites DMA csatornán valósul meg.
Esetünkben nem követelmény, hogy az általunk rögzítendő hanganyag CD, esetleg HI-FI minőségű legyen, de 44,1 kHz-es mintavételi frekvencia és 8 bites kvantálási hossz mellett is megfelelő minőséget lehet elérni, még akkor is, ha mono felvételt készítünk. Így nyugodtak lehetünk, hogy nem lesz túl nagy az információveszteség, legalább is nem akkora, hogy érdemes lenne a hangállomány méretét többszörösére növelni, nem mintha olyan óriási méretekről lenne szó.
[4] László József: Hangkártya programozása Pascal és Assembly nyelven, Computer Books, Budapest, 1999