• ASM:《X86汇编语言-从实模式到保护模式》越计卷:实模式下对DMA和Sound Blaster声卡的控制


           说实话越计卷作者用了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:

     

  • 相关阅读:
    DDD之3实体和值对象
    DDD之2领域概念
    DDD之1微服务设计为什么选择DDD
    SOFA入门
    COLA的扩展性使用和源码研究
    kafka可插拔增强如何实现?
    请设计一个核心功能稳定适合二开扩展的软件系统
    如何保证kafka消息不丢失
    kafka高吞吐量之消息压缩
    kafka消息分区机制原理
  • 原文地址:https://www.cnblogs.com/Philip-Tell-Truth/p/5369339.html
Copyright © 2020-2023  润新知