Hangkártyák programozása retro 12.

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

Hírdetés