说实话越计卷作者用了16页(我还是删过的),来讲怎么控制声卡,其实真正归纳起来就那么几点。
★PART1:直接存储访问
1. 总线控制设备(bus master)
在硬件技术不发达的早期,处理器是最重要的总线主控制设备,它有权决定谁参与总线数据传输。考虑代码片断:mov [0x2000],dx,在执行这条指令时,处理器不但发出地址信号,也发出控制信号,控制信号用来表明该地址是发给内存的,还是发给外部设备的。所有设备都有译码电路,这些译码电路的输入就是地址和控制信号。以上指令执行的时候,内存的译码结果是打开通向总线的数据通路,而外部设备则保持同总线的脱离状态。相反地,in al,0x70指令是发给端口的,内存当然不会工作。而且,只有那个端口号相符的外部设备才会和数据总线连通,其它所有设备都保持同总线的脱离状态。
在过去的岁月中先后出现过多种不同的总线类型,它们的典型代表就是工业标准结构总线(Industrial Standard Architecture:ISA)。总线不单单是数据线路,还包括地址线路的控制信号线路,规定和数据和地址的宽度,以及各种控制信号的规程和电气特性。控制信号规定了设备之间互相交流的协同的方式,而不同的总线有不同的控制信号规程。ISA是面向单用户和简单应用环境的总线,结构并不复杂。所以,符合ISA信号规程的外部设备都很简单。
为了对总线有更好的控制能力,人们采用DMA(Direct Memory Access:DMA,也是一个总线设备)来进行协调控制。直接存储器访问的核心器件是DMA控制器(DMA Controller:DMAC)。一台计算机只有一个DMA控制器,由它负责所有外部设备的直接传输协调工作。而且赋予其总线主控能力高于处理器。当然在现在,DMA已经是一种很古老的技术了,即将被淘汰了,现在流行的都是PCI(E)技术。
在DMA主导的控制环境下,当外部设备需要发起一次针对内存某个区域的数据传输时,应该向DMAC发出请求。DMAC回应此请求,同时告诉处理器不要再使用总线。注意,这是干预处理器的工作,命令它让出总线。接着,由它主导,开始在该外部设备和内存之间直接传输数据。在早期的计算机系统中,DMAC是独立于处理器和外部设备之外的第三方(Thrid party)总线主控器。设备向DMAC发送DMA请求(DMA REQuest:DREQ);如果总线空闲,可以占用,DMAC用DMA确认信号(DMA ACKnology)回应。此后,就可以正常开始DMA传送。
2. Sound Blaster 16声卡
声卡是数字和声音的转换器件,录音的时候,声波可以推动磁场中的线圈,也可以使处于静电场中的两个电极间距改变,或者使碳精砂的疏密程度发生变化,又或者使压电陶瓷振动,从而产生音频电流。其中涉及模数转换和,这已经在数字电子技术会讲到的,里面涉及到比较复杂的电路构造(如果要深入学习的话),还要一点傅里叶变换啥的。采样率是反应模数转换的一个非常重要的参数,采样率越高,模拟得越精准。声卡有自己的微处理器,即I/O处理器,通常称为数字信号处理器(Digital Singal Processor:DSP)。
Sound Blaster 16声卡支持双声道(立体声)、每声道16位样本、最高44.1KHz的采样和回放。传统上,要访问该声卡,需要通过4个端口,
注:W缓冲区用来接受外部的命令和数据;R缓冲区用来存放供外部读取的数据。
初始化SB-16需要三个步骤:
1.向0x226号端口写数字0x01,并等待3毫秒;
2.向0x226号端口写数字0x00;
3.典型的SB-16声卡需要100毫秒来初始化自己。在此期间,可以不停地读取0x22e号端口,并判断数据的最高位(第7位)是否为“1”。如果是比特“1”,表明可以从0x22a端口读取声卡的状态。如果从0x22a号端口返回的内容是0xAA,表明声卡已经准备就绪。否则,意味着声卡没有安装,或者端口号不正确。
典型的程序可以写成这样:
播放声音是采样的逆过程,是连续地将数字转换成模拟信号。同时,将得到模拟信号放大,用来推动扬声器,我们就听到还原后的声音了。为了回放声音,最容易想到的办法是连续不断地向声卡传送数字,直到把所有的数字都传完,这称为直接模式(Direct Mode)。回放的难点不在于数-模转换,这个电路很简单。真正的难点在于如何把握回放的速度,也就是要精确地控制采样率。SB-16只支持8位单声道的直接模式,回放速率由用户决定,声卡不为此负责。需要对计算机系统中的定时器芯片编程,使它定时发出中断信号。定时器芯片是8254,也可以是用来代替8254的高精度事件定时器(High Precision Event Timer:HPET)。
定时器应当按采样率所要求的间隔定期发出中断信号,每次中断发生时,就往声卡发送一个数字。在直接模式下,采样率没有上限和下限,仅仅取决于定时器中断的频率和中断处理程序的效率。总的来说,直接模式是一个很别扭的回放模式。而声卡已经自定义了很多种回放模式,比如SB-16允许多种能自动控制数据传输的回放模式,但都需要DMA机制,而且同样需要中断。当声卡开始回放时,它会向DMAC发出直接存储器传输请求,在得到允许后,占用总线,从内存中获取数据并自动按设置的采样率进行回放。通常要由声音回放程序提供一小块内存,称为缓冲区。声音数据通常以文件的形式存放在硬盘和光盘上,在回放的时候,才一点一点地读到缓冲区,再从缓冲区传输到声卡。
传统上,SB-16声卡是连在8259主片的第5个中断输入引脚IR5上的。在计算机启动期间,BIOS程序会初始化8259,将主片的中断号定义成从0x08开始,所以IR5对应的中断号是0x0d。
设置中断程序和第九章的类似,具体看代码就知道了。
3. 初始化DMA
每一片82C37A芯片都有个各通道公用的屏蔽寄存器,用来允许或者关断外部DMA请求信号,每个通道都可以单独设置。屏蔽寄存器在主片上的端口号是0x0a;在从片上的端口号是0xd4。
在82C37A内部,每个通道都有自己的基准地址寄存器和当前地址寄存器。在DMA传送开始前,这两个寄存器的内容应当由软件设置成相同的值,都指向数据在内存中的起始物理地址或者末端物理地址。具体是起始物理地址还是末端物理地址,取决于是正向传送还是反向传送。当前地址寄存器的作用是在DMA传送的过程中提供物理地址。在传送的过程中,每传送一个字节,当前地址寄存器自动加一或者减一,以指向下一个数据所在的位置,而基准地址寄存器的内容始终不变。
数据在内存中所占用的区域通常称为缓冲区(buffer),根据需要,可大可小,但应当始终保持不变,它的起始物理地址或者末端物理地址就是要设置到基准地址寄存器和当前地址寄存器的数值。内存空间从来都是有限的,缓冲区可以定义得小一点,数据量大的时候,可以分批进行。比如说,声音文件位于硬盘上,大小是64KB,可以在内存中定义2KB的缓冲区,每次从硬盘上读取512个字节到缓冲区,再从缓冲区通过DMA传送到声卡,共分32个批次。82C37A支持自动预置模式,如果允许这种模式,那么,每当一个批次的数据传送完毕之后,它会自动用基准地址寄存器的内容来初始化当前地址寄存器,这就是基准地址寄存器的作用。每个DMA通道都有自己的基准字数寄存器和当前字数寄存器。在DMA传送开始前,软件应当把它们设置成相同的内容。当DMA传送开始时,每传送一个字节,当前字数寄存器的内容减一,而基准字数寄存器的内容始终不变。如果当前字数寄存器的内容为零,就说明一个批次传送完毕,82C37A芯片会产生一个传送过程结束的信号。同时,如果允许自动预置功能,82C37A就会重新把基准字数寄存器的内容写入当前字数寄存器。
首先要设置DMA缓冲区的起始地址和数据长度。DMA传送是自动进行的,它要求的是一个物理地址,也就是真实的地址(而不是逻辑地址)。82C37A主片的先后触发器口地址为0x0c,通过向该端口写入一个任意值,可以将它初始化到一个确定的状态。此后,第一次向地址寄存器和字数寄存器写入时,对应的是低字节;第二次写入时,对应的是高字节要设置82C37A主片通道1的基准地址寄存器和当前地址寄存器,可以通过端口0x02。
不过,当前地址寄存器确实只有16位,不足以形成20位地址。所以,每个82C37A的DMA通道还各自有一个8位的页面寄存器,使用它,可以提供20位物理地址的高4位,主片通道1的页面寄存器使用端口号是0x83接下来设置82C37A主片通道1的基准字数寄存器和当前字数寄存器,它们对应的端口号是0x03。DMA传送有好几种方式,分为单字节操作、数据块操作、请求操作和级联方式。单字节操作方式是,先由外部设备进行DMA请求,获得响应后,82C371占用总线,操作一个字节,然后交还总线控制权给处理器。即使是有很多数据,每操作一个字节,都要按以上步骤进行。
表面上看起来,这种操作方式不会很快。但事实上,它依然是很快的,毕竟用它来传送数据时,不需要处理器中转。而且处理器每次接到总线请求时,会立即在当前总线周期结束时让出总线。当处理器接到DMA请求时,它可能正在执行指令,可能正在按指令的要求访问总线(访问内存和I/O设备)。DMA会先把当前任务完成再把总线开放给处理器,如果当时并未访问总线,它可以立即让出总线控制权。数据块操作方式是,一旦DMA操作开始了,DMA控制器就一直占用总线,直到操作完成。在此期间,即使外部设备的DMA请求变得无效,82C37A也一直占用总线,暂停操作,直至DMA请求变为有效。
请求操作的方式是,是以否有DMA请求来决定,如果有DMA请求,则占用总线并进行DMA操作;当DMA请求无效或者操作完成时,释放总线。
设定82C37A具体的做法是,向主片或者从片的端口发送命令代码。主片上的端口号是0x0b;从片的端口号是0xd6。
校验操作在DMA操作期间进行的不是数据传送,而是对数据的正确性进行校验。
4. 启动音频播放
接着初始化和设置声卡。SB-16有多种播放模式,每种模式有不同的设置要求。因为教材采用的波形文件是8位单声道,采样率8000Hz,所以仅采用和介绍8位单声道自动初始化传输模式(8 bit Mono Auto-initialize Transfer)。
在这种模式下,DMA控制器也应当设置为自动预置状态,每当指定数量的字节传送之后,当前字数寄存器恢复为和基准字数寄存器一样。而对于声卡来说,需要在播放前设置数据块的大小为DMA缓冲区的一半。回放开始后,每次播放的数据量达到这个数值时, 声卡将会发出一个中断信号,使程序可以继续用新的数据填充已回放的区域。
首先是设置声卡回放速率时间常量。SB-16给出的计算方法是
时间常量 = 65536 - (256 000 000/(声道数 × 采样率))
计算出来的数值是16位的,在寄存器AX中,但SB-16只使用其高8位。
最后向0x22c端口写入命令字0xd1以打开扬声器,并且向0x22c端口写入命令字0x1c,以正式启动音频回放。
5. 音频回放中断处理
一旦下达了命令字0x1c,声卡就开始启动音频回放,这个过程是独立于处理器的。当它播放的内容长度达到DMA缓冲区的一半时,就会产生中断信号,一般情况下,中断号为5。退出自动初始化模式的方法是向0x22c端口写命令字0xda。中断发生时,如果声卡正在播放缓冲区的后半部分。当声卡接到此命令时,它不会立即停止工作,只有在当前数据块播放结束之后,它才会真正退出自动初始化模式。关闭扬声器的命令是向0x22c端口发送0xd3。
在正常的声卡中断处理过程中,应当读一下0x22f端口,作为对声卡中断的应答。
6. 中断号的总结
不过说实话,这一章的中断号实在是太多了,有必要来总结一下:
★PART2:本章习题
本章最后的练习要我们采用读取读取windows的wav文件的文件头来获得采样率和声道数以计算时间常量,这个太简单了,我们先来看下wav文件头是个什么
编译器提供了incbin指令来编译外部文件到二进制(INCluding external BINary files)它的功能是将指定的文件逐字地包含到编译后的结果中,从它在源程序中出现的位置开始。 这直接把文件头读取进来然后把0x16和0x18两个偏移量找到就可以了,但是由于是16位模式,长度上可能会有点限制。
incbin的后面需要三个参数,第一个参数是文件名。举个例子:
incbin “baby.wav”
这将会把baby.wav文件的内容原样包含进最终的编译结果中去。
incbin “baby.wav”,44
这句的意思是,在编译的结果中包含baby.wav文件的内容,但跳过该文件开头的40个字节。
如果该伪指令的后面出现有三个参数,比如
incbin “baby.wav”,44,50000
那么,编译器将会在编译结果中包含baby.wav文件的内容,但跳过该文件开头的44个字节,而且实际上仅仅包含50000个字节。
教材中的声音文件只有57.3KB,所以可以直接在用户程序定义一个数据段来存放声音文件(正常来讲应该是给声音文件专门安排一个段来存放的)。并且也是因为他只有一段,所以我们不必分段传输声音信息给声卡。
还有,上次第八章我的mbr程序都写错了,但是不知道为什么可以驱动第九章的程序,奇怪。(下面程序用第八章的mbr驱动就好了)。
1 ;=============================================================================== 2 SECTION header vstart=0 ;定义用户程序头部段 3 program_length dd program_end ;程序总长度[0x00] 4 5 ;用户程序入口点 6 code_entry dw start ;偏移地址[0x04] 7 dd section.code.start ;段地址[0x06] 8 9 realloc_tbl_len dw (header_end-realloc_begin)/4 10 ;段重定位表项个数[0x0a] 11 12 realloc_begin: 13 ;段重定位表 14 code_segment dd section.code.start 15 data_segment dd section.data.start 16 stack_segment dd section.stack.start 17 18 header_end: 19 ;=============================================================================== 20 SECTION code align=16 vstart=0 ;定义代码段(16字节对齐) 21 put_string: ;显示字符串(0结尾) 22 ;输入:DS:BX=串地址 23 push ax 24 push bx 25 push si 26 27 mov ah,0x0e ;INT 0x10第0x0e号功能 28 mov si,bx ;字符串起始偏移地址 29 mov bl,0x07 ;显示属性 30 31 .gchr: 32 mov al,[si] ;逐个取要显示的字符,0x10功能就是al是要显示的儿子字符,ah功能号为0x0e 33 or al,al ;如果AL内容为零,则 34 jz .rett ;跳转到过程返回指令 35 int 0x10 ;BIOS字符显示功能调用 36 inc si ;下一个字符 37 jmp .gchr 38 39 .rett: 40 pop si 41 pop bx 42 pop ax 43 ret 44 ;------------------------------------------------------------------------------- 45 write_dsp: 46 push dx 47 push ax 48 49 mov dx,0x22c ;不停读取0x22c端口,直到他的第七位变成1为止 50 .@22c: 51 in al,dx 52 and al,1000_0000b 53 jnz .@22c 54 55 pop ax 56 out dx,al 57 pop dx 58 59 ret 60 ;------------------------------------------------------------------------------- 61 read_dsp: 62 push dx 63 mov dx,0x22e 64 .@22e: 65 in al,dx 66 and al,0x80 ;监视22e端口的位7,直到它变成1 67 jz .@22e 68 69 mov dx,0x22a 70 in al,dx ;此时可以从22a端口读取数据,返回值必须是0xaa才是对头的 71 72 pop dx 73 ret 74 ;------------------------------------------------------------------------------- 75 dsp_interrupt: ;中断处理过程 76 push ax 77 push bx 78 push dx 79 80 ;退出自动初始化模式 81 mov al,0xda 82 call write_dsp 83 84 ;关闭扬声器 85 mov al,0xd3 86 call write_dsp 87 88 mov bx,done_msg 89 call put_string 90 mov bx,okay_msg 91 call put_string 92 93 mov dx,0x22f ;DSP中断应答 94 in al,dx 95 96 ;发送EOI命令到中断控制器(主片) 97 mov al,0x20 ;中断结束命令EOI 98 out 0x20,al ;发给主片 99 100 pop dx 101 pop bx 102 pop ax 103 104 iret 105 ;------------------------------------------------------------------------------- 106 start: 107 mov ax,[stack_segment] 108 mov ss,ax 109 mov sp,ss_pointer 110 111 mov ax,[data_segment] 112 mov ds,ax 113 114 mov dx,0x226 ;第一步,先写“1”到复位端口 115 mov al,1 116 out dx,al 117 118 xor ax,ax 119 _wait_int: 120 dec ax 121 loop _wait_int 122 123 out dx,al ;第二步,写“0”到复位端口 124 125 call read_dsp 126 cmp al,0xaa 127 je _setup 128 129 _error: 130 mov bx,err_msg ;如果返回值不是0xaa那么就说明声卡没有安装 131 call put_string 132 jmp _idle ;没有声卡就直接停机吧 133 134 _setup: 135 mov bx,intr_msg 136 call put_string 137 138 mov bx,0x0d 139 shl bx,2 ;8259A的IR5引脚的中断号,找到其在IVT的偏移地址 140 141 cli 142 push es 143 xor ax,ax 144 mov es,ax 145 mov word[es:bx],dsp_interrupt ;安装相应的中断处理过程 146 mov [es:bx+2],cs 147 pop es 148 sti 149 150 in al,0x21 ;8259主片的IMR 151 and al,1101_1111B ;开放IR5 152 out 0x21,al 153 154 mov bx,done_msg 155 call put_string 156 mov bx,dma_msg 157 call put_string 158 159 mov dx,0x0a ;注意0x0a是DMA的主片接口 160 mov al,00000_1_01B ;禁止操作,禁止DMA主片通道1 161 out dx,al 162 163 mov ax,ds 164 mov bx,16 165 mul bx 166 add ax,voice_data 167 adc dx,0 168 mov bx,dx ;BX:AX为20位的基地址 169 170 push ax ;先存一下ax的内容 171 xor al,al 172 out 0x0c,al ;DMAC1高低触发器清零 173 pop ax 174 175 ;第一次写入对应的是低字节,第二次对应的是高字节 176 mov dx,0x02 ;写通道1基址与当前地址寄存器 177 out dx,al ;低8位DMA地址 178 mov al,ah 179 out dx,al ;高8位DMA地址 180 181 ;0x83这个端口是82C37A的8位页面寄存器 182 mov dx,0x83 ;写DMA通道 1 的页面寄存器 183 mov al,bl 184 out dx,al 185 186 mov dx,0x03 ;写通道1的基字计数与当前字计数器 187 mov ax,init_msg-voice_data ;数据块(当缓冲区用)的大小 188 dec ax ;DMA要求实际大小减一 189 out dx,al ;缓冲区长度低8位 190 mov al,ah 191 out dx,al ;缓冲区长度高8位 192 193 mov al,0101_1001b ;设置DMAC1通道1工作方式:单字节传送/ 194 out 0x0b,al ;地址递增/自动预置/读传送/通道1 195 196 mov dx,0x0a ;DMAC1屏蔽寄存器 197 mov al,1 ;允许通道1接受请求 198 out dx,al 199 200 mov al,0x40 ;直接往0x22c接口写入0x40,表示准备写入回放速率时间常量 201 call write_dsp 202 203 mov ax,[voice_data+0x16] ;声道数量 204 mov bx,[voice_data+0x18] ;采样率 205 mul bx 206 207 ;dx:ax为采样率*声道数量,只用ax 208 mov cx,ax 209 mov ax,25600 210 xor dx,dx 211 mov bx,10000 212 mul bx 213 ;dx:ax为256000000 214 div cx 215 ;ax为(256000000/ans) 216 xor dx,dx 217 mov dx,65535 218 sub dx,ax 219 inc dx 220 mov ax,dx 221 222 xchg ah,al ;只使用结果的高8位 223 call write_dsp 224 225 mov bx,done_msg 226 call put_string 227 228 mov al,0x48 229 call write_dsp ;往0x22c写入0x48,表示我们要写入数据块的长度 230 ;对于8位段声道音频来说,数据块的长度是DMA缓冲区长度的一半减1 231 ;这样做的目的是允许声卡在播放到一半的时候,发出一个中断, 232 ;以方便立即开始填充已经回放的数据块,避免声音中断 233 mov ax,init_msg-voice_data ;数据块(当缓冲区用)的大小 234 shr ax,1 ;长度设为DMA的一半 235 dec ax 236 call write_dsp ;写低字节 237 xchg ah,al 238 call write_dsp 239 mov al,0xd1 ;打开喇叭输出 240 call write_dsp 241 mov al,0x1c ;启动DSP的传输的播放 242 call write_dsp 243 244 mov bx,play_msg 245 call put_string 246 _idle: 247 hlt 248 jmp _idle ;注意一定要jmp 249 ;------------------------------------------------------------------------------- 250 SECTION data align=16 vstart=0 251 voice_data incbin "baby.wav", 0 252 init_msg db 'Initializing sound blaster card...',0 253 intr_msg db 'Installing interrupt vector...',0 254 dma_msg db 'Setup DMA ...',0 255 done_msg db 'Done.',0x0d,0x0a,0 256 play_msg db 'Voice is playing now...',0 257 okay_msg db 'Finished,stop.',0 258 err_msg db 'Sound card init failed.',0 259 ;=============================================================================== 260 SECTION stack align=16 vstart=0 261 times 256 db 0 262 ss_pointer: 263 ;=============================================================================== 264 SECTION program_trail 265 program_end: