注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

DOS编程技术

讨论在纯DOS下的编程技术

 
 
 

日志

 
 
关于我

1984年大学毕业,1985年底有机会开始接触PC机,1986年开始在PC机上做开发工作,曾接触过MS-DOS、CP/M、UNIX、VMS、LINUX、iRMX等众多的操作系统并在上面从事技术开发,擅长做底层与硬件相关的软件开发,目前主要在DOS和LINUX平台下工作,主要从事软件,在硬件开发上也有一定造诣,亦有在8051系列、6502系列(凌阳)、z80系列、ARM、X86等各类平台下开发软硬件的经历。更详细情况可以参考http://resume.whowin.net

网易考拉推荐

在DOS下针对AC'97编程  

2008-04-22 14:52:57|  分类: 杂七杂八 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

    AC'97大多数应该听说过,可能有些人把它当成一种声卡,或者是声卡上的芯片等等,其实它仅仅是一种规范,符合AC'97规范的声卡,通常叫做AC'97声卡,但其实上面使用的芯片可能完全不一样。现在很多桥片中甚至已经集成了AC'97的规范进去,就不需要专门的声卡了。本文针对AMD的较新的一种桥片CS5536上集成的AC'97进行编程,进而说明如何对符合AC'97规范的声卡进行编程。以下为书写方便,把AC'97写成AC97。

1、AC97规范介绍

     AC97最重要的三个规范就是:1.使用独立的CODEC芯片,将数字电路和模拟电路分离;2.固定48K的采样率,其他频率的信号必须经过SRC转换处理;3.标准化的CODEC引脚定义。基本上说,符合这个规范的声卡就是AC97声卡。制定AC97规范的主要目的有两个:1.实现数模电路分离,保证音频质量;2.使声卡电路标准化、提高其兼容性能。

    就AC97规范而言是十分复杂的,基本上不可能在这里表达完整,所以我们并不打算在这里完整地解释该规范,我们仅就我们准备举的例子中所需要的内容来介绍。我们的程序范例准备完成一个简单的WAV文件的放音,而且,这个WAV文件还不能是很长的一个WAV文件。

    按照AC97的规范,AC97声卡大致有三个部分:Audio Codec Controller(ACC)、AC Link、Codec,如下图:

 在DOS下针对AC97编程 - whowin - DOS编程技术

    ACC负责与系统相连,然后透过AC Link与Codec通讯,在本例中,ACC通过AMD的GLIU与PCI总线相连,其实不用管什么GLIU,只要知道,本例中,ACC与PCI总线相接就可以了。ACC负责在系统存储器和Codec之间传送数据(使用DMA),本例的ACC与AC Link之间有8个通道,相应地为了支持这8个通道,ACC有8个DMA引擎。

    AC Link是一个5针的数字串行接口,AC97规范定义了这个接口的协议(AC Link Serial Interface Protocol),我们必须简单地介绍这个协议才有可能完成后面的编程。

    先要介绍一下PCM和时分多路复用的概念。PCM(Pulse Code Modulation),中文叫做脉冲编码调制,是一种编码方式,简单的说就是把声音的模拟信号变成数字信号的一种编码方式,我们使用的CD就是采用的PCM编码。AC97规范传送的声音编码都是PCM编码。所谓“多路复用”就是为了充分利用线路资源,在一条线路上传输几路而不是一路信息,“时分”指的是一种多路复用的方法,大致方法是,把一个传输通道进行时间分割以传送若干话路的信息,比如我们把1秒钟分成10份,则0--1/10秒传第1路数据,1/10--2/10秒传送第2路数据,......,9/10--10/10秒传送第10路数据,大致就是这么个原理,我们把单位时间内分成的每个等分叫做1个时隙(Time Slot),简称Slot,比如上面把1秒分成10份,可以说有10个时隙(Slot),Slot 0--Slot 9。

    回到正题,前面说过,AC Link有5条线,分别是:SYNC、BIT_CLK、RESET#、SDATA_OUT和SDATA_IN,可以看出单方向传送的数据线,只有一条,所以一定要使用多路复用技术,AC Link协议将48KHz(20.8us)分成了13个SLOT,除第Slot 0传送16 bit数据外,其余12个Slot均传送20 bit数据,在此把我们可能用到的信息格式介绍一下。

  • Slot 0:TAG
    Bit 15       1表示该帧数据有效
    Bit 14       1表示后面Slot 1中数据有效
    Bit 13       1表示后面Slot 2中数据有效
    Bit [12:3]   1表示后面Slot 3--12中数据有效
    Bit 2        备用
    Bit [1:0]    Codec ID field
  • Slot 1:Command Address
    bit 19       读或写标志,0--写标志,1--读标志
    bit [18:12]  64个16位寄存器索引号
    bit [11:0]   备用
  • Slot 2:Command Data
    bit [19:4]   控制寄存器的写入数据(操作为读时,要用0填满)
    bit [3:0]    备用(填0)
  • Slot 3:PCM放音左声道
    高16位有效,未用到的低位填0
  • Slot 4:PCM放音右声道
    高16位有效,未用到的低位填0
  • Slot 5:MODEM Line 1 DAC
    高16位有效,未用到的低位填0
  • Slot 6:PCM放音中央通道
    高16位有效,未用到的低位填0
  • Slot 7:PCM放音环绕左声道
    高16位有效,未用到的低位填0
  • Slot 8:PCM放音环绕右声道
    高16位有效,未用到的低位填0
  • Slot 9:PCM放音低音效果声道
    高16位有效,未用到的低位填0
  • Slot 10:未用

  • Slot 11:Modem Headset DAC

  • Slot 12:GPIO 控制

     另外,规范还详细定义了CODEC的27个寄存器,AC97声卡,如果作为PCI设备,则这些寄存器可以被映射成I/O地址,也可以被映射成存储器地址,从PCI的配置空间中可以得到基地址,然后加上AC97规范中定义的寄存器的偏移便可寻址到所有的CODEC寄存器,由于内容实在是比较多,让我一个一个汉字敲上去实在是太累,所以有关协议的介绍就暂告一段落,建议大家在完成下面的例子之前还是看看AC97规范的有关章节,可以从下面地址下载。

    http://blog.whowin.net/specification/ac97_r23.pdf(2017年3月23日注:链接已修复)

2、有关AMD CS5536有关AC97部分的介绍

     CS5536是一颗功能十分强大的芯片,我们仅能就其中我们要用到的功能做一点简要的介绍。

    如果希望详细了解CS5536这颗芯片,可以在这里下载datasheet,篇幅很长,如果不使用这颗芯片,完全没有必要全部阅读。

    http://blog.whowin.net/specification/33238f_cs5536_ds.pdf(2017年3月23日注:链接已修复)

    前面说到,CS5536中的Codec控制器有8个通道,控制器负责从内存中向CODEC传输数据,所以在启动DMA之前,我们必须告诉控制器数据存放的内存地址,数据的长度,以及如何终止传输等等,还要告诉控制器启动哪一个或哪几个通道,CS5536的AC97控制器(ACC)为完成这些控制,有一系列的控制寄存器,偏移地址从00h--7Fh,数据均为32位。

    首先说如何告诉ACC数据的存储地址以及与其相关的事情,CS5536上的ACC使用一个叫做PRD(Physical Region Descriptor)表作为传输控制信息的表述,表的结构如下:

 在DOS下针对AC97编程 - whowin - DOS编程技术

     每一个PRD表占8个字节(2个DWORD),前1个DWORD表示数据存储的基地址,后一个DWORD的bit 0--bit 15表示数据的长度,只16bit能表示的最大值为65535,由于音频采样值为16bit,作为单声道音频流,数据必须与2个字节对齐,所以一个内存区域内存储的最大采样值(数据)为65534字节;对于双声道立体声,一个采样点位两个声道,需要4个字节,所以一个内存区域的最大采样数据为65532字节。

    PRD表一般都不止一个,它们在内存中依次放置,就是说,如果第1个PRD表放在0x1000这个内存地址上,则第2个PRD表应该放在0x1008这个位置,第3个PDR放在0x1010,......,以此类推,直到出现最后一个PRD表;最后一个PRD表的标志是EOT位置1或者JMP位置1,不能EOT和JMP同时置1,EOT(End of Transfer)置1表明这是最后一个PRD,这个PRD后停止数据传输;JMP(JUMP)置1表明跳跃到其他PRD,这是PRD的第1个DWORD不是指存放数据的内存地址,而是指JMP要跳跃到的下一个PRD表的内存地址,这种情况下表明Size的16个bit无效;有效地利用JMP位,可以方便地制造出循环播放的效果,我们可以在所有PRD的最后放置一个PRD,该PRD置JMP位,同时把跳跃地址指向第1个PRD,于是循环播放的效果就出来了。

    EOP(End of Page)是在还有下一个PRD时使用,当该页数据传输完毕后,ACC遇到EOP位,就会产生一个中断,同时转到下一个PRD,如果在下一个EOP出现之前,中断没有处理完毕的话,将出现一个错误,通常情况下会利用这个中断来填写另一个PRD表,以此来完成较长音频的播放,否则遇到很长的音频文件,岂不是要耗尽内存;但我们在本例中为了突出重点,不准备播放很长的音频文件,同时,不采用中断方式。

    好了,我们已经了解了PRD表,很显然,我们只要把第一个PRD表的地址告诉ACC就可以了,这要用到ACC的一个寄存器,偏移位置为:24h,ACC有8个这样的寄存器,偏移分别是:24h、2Ch、34h、3Ch、44h、4Ch、54h、5Ch,分别用于ACC的8个通道(是否还记得ACC有8个通道),在本例中,我们只用通道0,所以我们只用偏移位24h的寄存器,这个寄存器的名字叫:Audio Bus Master 7-0 PRD Table Address Registers(ACC_BM[x]_PRD),该寄存器32位长,其中bit 0和bit 1备用,bit 2--bit 31存放第1个PRD表的地址,为什么bit 0和bit 1备用呢?因为规定PRD的地址必须以4字节对齐,所以实际上bit 0和bit 1没有意义。

    如何和Codec进行通讯呢?这里还有两个ACC的寄存器必须要介绍,一个偏移量为08h的Codec状态寄存器(Codec Status Register)(ACC_CODEC_STATUS),另一个是偏移为0Ch的Codec控制寄存器(Codec Control Register)(ACC_CODEC_CNTL)。

    Codec控制寄存器:主要用于向Codec发出控制命令
      bit 31(RW_CMD):0--写Codec寄存器,1--读Codec寄存器
      bit 30:24(CMD_ADD):读/写Codec寄存器的地址,前面说过Codec寄存器地址为7位
      bit 23:22(COMM_SEL):与那个Codec进行通讯,00--Codec 1,01--Codec 2
      bit 16(CMD_NEW):当填写完命令后,将此位置1,当命令发出后,硬件将此位置0
      bit 15:0(CMD_DATA):只有写入命令时有效,欲写入Codec寄存器的数据。

    Codec状态寄存器:
      bit 31:24(STS_ADD):表明STS_DATA数据是哪个寄存器的
      bit 23(PRM_RDY_STS):1--主Codec准备好,如果此位不为1,软件不能存取相应的Codec
      bit 22(SEC_RDY_STS):1--第2个Codec准备好,如果此位不为1,软件不能存取相应的Codec
      bit 17(STS_NEW):当收到一个合法的Codec状态数据后,硬件会将此位置1,
      bit 15:0(STS_DATA):从Codec收到的Codec的状态数据

    在我们的例子中,只用主Codec和通道0。

    在本例中,Codec芯片使用的是ALC202,有关ALC202的datasheet可以在下面网址下载。

    http://blog.whowin.net/specification/alc202.pdf(2017年3月23日注:链接已修复)

3、wav文件格式

    由于我们的例子是打开一个WAV文件并放音,所以我们有必要了解一下WAV文件的文件格式。

    WAVE文件作为多媒体中使用的声波文件格式之一,它是以RIFF格式为标准的。RIFF是英文Resource Interchange File Format的缩写,每个WAVE文件的头四个字节便是“RIFF”。WAVE文件由文件头和数据体两大部分组成。其中文件头又分为RIFF/WAV文件标识段和声音数据格式说明段两部分。WAVE文件各部分内容及格式见附表。

常见的声音文件主要有两种,分别对应于单声道(11.025KHz采样率、8Bit的采样值)和双声道(44.1KHz采样率、16Bit的采样值)。采样率是指:声音信号在“模→数”转换过程中单位时间内采样的次数。采样值是指每一次采样周期内声音模拟信号的积分值。

对于单声道声音文件,采样数据为八位的短整数(short int 00H-FFH);而对于双声道立体声声音文件,每次采样数据为一个16位的整数(int),高八位和低八位分别代表左右两个声道。

WAVE文件数据块包含以脉冲编码调制(PCM)格式表示的样本。WAVE文件是由样本组织而成的。在单声道WAVE文件中,声道0代表左声道,声道1代表右声道。在多声道WAVE文件中,样本是交替出现的。 

WAVE文件格式说明表

偏移地址    字节数     数据类型     内 容

00H           4         char      “RIFF”标志

04H           4         long int  文件长度

08H           4         char      “WAVE”标志

0CH           4         char      “fmt”标志

10H           4                   过渡字节(不定)

14H           2         int       格式类别(10H为PCM形式的声音数据)

16H           2         int       通道数,单声道为1,双声道为2

18H           2         int       采样率(每秒样本数),表示每个通道的播放速度,

1CH           4         long int  波形音频数据传送速率,其值为通道数×每秒数据位数×每样本的数据位数/8。播放软件利用此值可以估计缓冲区的大小。

20H           2         int       数据块的调整数(按字节算的),其值为通道数×每样本的数据位值/8。播放软件需要一次处理多个该值大小的字节数据,以便将其值用于缓冲区的调整。

22H           2                   每样本的数据位数,表示每个声道中各个样本的数据位数。如果有多个声道,对每个声道而言,样本大小都一样。

24H           4         char      数据标记符"data"

28H           4         long int  语音数据的长度

PCM数据的存放方式:
                样本1           样本2            样本3             样本4

8位单声道     0声道           0声道

8位立体声     0声道(左)       1声道(右)        0声道(左)        1声道(右)

16位单声道    0声道低字节      0声道高字节      0声道低字节      0声道高字节

16位立体声    0声道(左)低字节  0声道(左)高字节  1声道(右)低字节  1声道(右)高字节

 

WAVE文件的每个样本值包含在一个整数i中,i的长度为容纳指定样本长度所需的最小字节数。首先存储低有效字节,表示样本幅度的位放在i的高有效位上,剩下的位置为0,这样8位和16位的PCM波形样本的数据格式如下所示。

样本大小   数据格式        最大值        最小值

8位PCM     unsigned int    255            0

  16位PCM     int            32767         -32767

4、实例

    下面我们编这样一个程序,程序名:playwav.exe,带两个参数,第一个是WAV文件名,第二个是放音音量,实际使用时使用下面格式:playwav filename volume

    大致程序的流程如下,读入两个参数,搜寻AC97设备,初始化AC97,检查wav文件格式,读入文件并建立PRD表,启动AC97放音;为了简化,我们要求放音文件不能很大,因为我们只打算建立三个PRD表,一次性将WAV文件中的数据全部读入内存,下面是程序清单,为了说明方便,增加了行号,程序使用C++完成,在DJGPP下编译通过。

     由于程序太长,无法放在这里,请希望继续阅读的读者先自行下载源程序后再继续,源程序包中包括主程序playwav.cc;三个包含文件,ac97.h中定义了所有的AC97相关寄存器的偏移地址、AC97相关的常量并定义了一个类MYSOUND;typedef.h中为了程序方便定义了一些数据类型,不必过于关注;dosmem.h中主要定义了一个类DOS_MEM,主要在申请内存空间时使用,并不是本文的主题,大致明白怎么用就好了;源程序包中还有一个wav文件1.wav,用于测试。

    源文件下载:http://blog.whowin.net/source/ac97.rar(2017年3月23日注:链接已修复)

    程序的关键在ac97.h这个文件,我们将重点介绍其中的类MYSOUND。

    现在我们假定编译出来的可执行文件是playwav.exe,我们这样来执行这个文件以便测试:
      playwav 1.wav 90
    其中:1.wav是音频文件,90是音量。

    我们从MYSOUND的构造函数开始。构造函数主要执行了两个内部函数:CheckPCIBios()和FindCS5536()

  • CheckPCIBios:检查BIOS是否支持PCI,这个方法我在另一篇博文《遍历PCI设备》中曾经介绍过,如果BIOS不支持PCI,程序将无法运行。
  • FindCS5536:查找Codec控制器是否存在,前面介绍过,Codec控制器集成在CS5536中,ACC的VENDOR是0X1022(表示AMD公司),Device ID是0X2093,如果我们在PCI设备中能找到符合条件的设备,表明存在ACC,程序可以继续,查找PCI设备的方法在我的另一篇博文《遍历PCI设备》中曾经介绍过。
  • 回到构造函数,在找到ACC后,程序从配置空间中读出一些内容,其中最主要的是基地址,这个变量在后面的程序中经常用到。如何读取PCI的配置空间亦希望读者参考我以前的博文。

    在主程序(playwav.cc)中调用的MYSOUND中的第一个方法是LoadWavFile,现在我们回到ac97.h中分析LoadWavFile这个方法。

    LoadWavFile要求的入口参数只有一个文件名,其实就是我们执行playwav时的第一个参数1.wav,LoadWavFile方法主要调用了三个内部函数:OpenWavFile()、CheckWavFormat()和CreatePRD()

  • OpenWavFile():仅仅是以只读、二进制的方式打开wav文件,如果成功返回handle,否则返回NULL
  • CheckWavFormat():检查Wav文件的格式是否正确,该函数读取wav文件的文件头,并放到fileHead这个结构中,然后根据前面介绍的wav文件的格式检查其中的4个标志,如果符合则认为其是一个格式正确的wav文件。
  • CreatePRD():这个函数很关键,这个函数将按照规则建立音频数据的缓冲区,同时建立PRD表。首先,我们建立的缓冲区中的音频数据是16bits,2通道的,就是说,每一个采样点要站4个字节,前两个字节是左声道,后两个字节是右声道,低字节在前,高字节在后。
    一块缓冲区的长度不能超过65536,这在前面已经说过,为稳妥起见,我们决定一块缓冲区中仅存放65528个字节,也就是16382个采样点,
    我们首先要确定在这个缓冲区中可以从文件中读取多少个字节,对于8bit单声道数据,由于每个字节是一个采样点,所以只能读取16382个字节;对于16bit双声道数据,4个字节一个采样点,所以可以读取65528个字节;对于16bit单声道和8bit双声道数据,由于2个字节为一个采样点,所以可以读取32764个字节;可以读取的字节数存在变量m中,文件中还没有读的数据长度存在变量k中。
    wavBuffer用于临时存储wav文件中的音频数据,wavBuf[x]是实际按格式规范好的音频采样数据,本例中x小于8。
    我们首先读取整块的数据到wavBuffer中,然后根据其采样宽度和声道数放到wavBuf[x]中,注意,如果是8bit数据需要先扩展成16bit后再放入wavBuf[x]中。
    组织好数据后,开始设置PRD表,如果wav文件已经读完,则设置EOT位,否则设置EOP位。
    至此,数据及PRD表均已准备完毕,可以准备AC97设备开始放音了。
    从主程序中看到,在完成了CreatePRD()的调用后,调用了SetVolume()方法,该方法仅仅把命令行的地2个参数放到了MYSOUND类的变量volume中,在初始化AC97时将以此变量的值设置变量。
    下面主程序调用InitialAC97()来初始化AC97。
  • InitialAC97():该函数首先获得第一个PRD的地址,放到前面说过的位于偏移24h的ACC的PRD地址寄存器中,紧接着两行根据wav文件中的数据设置Codec的采样速率,最后两行设置了放音音量,我们把PCM_OUT的音量设为最大,然后使用主音量控制来控制音量。
    然后主程序调用了StartPlay()方法开始放音。
  • StartPlay():首先不断读取主Codec的状态,直到它就绪,一般情况下第一次读取就是就绪状态;然后向位于偏移20h的总线命令寄存器写入命令01h,该命令的含义是总线使能,意即开始根据PRD表向Codec传送数据,当数据传输完毕时,这个寄存器的bit 1:0将被自动置为00。
    至此,我们已经启动了AC97的放音,前面我们说过,为了简洁地说明问题,本例不使用中断方式,而采用查询方式来完成放音过程,这主要是为了规避介绍中断例程的编写方法,我们会注意到,当主程序调用完StartPlay()方法后,便进入一个循环,不断地查询MYSOUND的状态标志status,并不停地调用方法Process(),直到status==0为止。所以我们有必要来看一下status的含义和Process()都干了些什么。
  • status:0--表示MYSOUND目前并没有放音,1--表示MYSOUND目前正在放音。构造函数里,status第一次出现,此时status=0,表明没有放音;StartPlay()里第二次出现,status=1,表示MYSOUND正在放音;Process()里第三次出现,在放音结束后status=0,说明MYSOUNG结束放音过程。
  • Process():不停地检查总线的命令寄存器(就是当初启动传输的寄存器),前面说过党传输完成后,这个寄存器的bit 1:0将被自动置为00,该方法以此来判断传输是否完成;同时,该程序不停地检查总线IRQ状态寄存器,我们在前面也介绍过,ACC当在PRD中遇到EOP标志时,会产生中断,如果在下一个EOP来临之前不处理这个中断(读这个状态寄存器),则会产生错误,同时DMA的传输暂停,为了不造成这种现象,Process()不停地读这个状态寄存器。
    至此,程序基本介绍完了,过程有些繁琐,由于篇幅原因,很多问题不得不请大家自己去读一些规范,让我一个字一个字地敲上去太难了,希望这篇文章能给你一些帮助。
  评论这张
 
阅读(2056)| 评论(2)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018