在DOS下针对AC'97编程
文章目录
AC'97大多数应该听说过,可能有些人把它当成一种声卡,或者是声卡上的芯片等等,其实它仅仅是一种规范,符合AC'97规范的声卡,通常叫做AC'97声卡,但其实上面使用的芯片可能完全不一样。现在很多桥片中甚至已经集成了AC'97的规范进去,就不需要专门的声卡了。本文针对AMD的较新的一种桥片CS5536上集成的AC'97进行编程,进而说明如何对符合AC'97规范的声卡进行编程。以下为书写方便,把AC'97写成AC97。本文是我2008年的作品,2023年重新整理发布,仅为存档,其中的程序并没有再次验证,特此说明。
1、AC97规范介绍
-
AC97最重要的三个规范就是:
- 使用独立的 CODEC 芯片,将数字电路和模拟电路分离;
- 固定 48K 的采样率,其他频率的信号必须经过SRC转换处理;
- 标准化的 CODEC 引脚定义。
-
基本上说,符合这个规范的声卡就是AC97声卡。制定AC97规范的主要目的有两个:1.实现数模电路分离,保证音频质量;2.使声卡电路标准化、提高其兼容性能;
-
就AC97规范而言是十分复杂的,基本上不可能在这里表达完整,所以我们并不打算在这里完整地解释该规范,我们仅就我们准备举的例子中所需要的内容来介绍;
-
我们的程序范例准备完成一个简单的WAV文件的放音,而且,这个WAV文件还不能是很长的一个WAV文件。
-
按照AC97的规范,AC97声卡大致有三个部分:Audio Codec Controller(ACC)、AC Link、Codec,如下图:
-
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
1 2 3 4 5 6
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
1 2 3
bit 19 读或写标志,0--写标志,1--读标志 bit [18:12] 64个16位寄存器索引号 bit [11:0] 备用
-
Slot 2:Command Data
1 2
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规范的有关章节,可以从下面地址下载。
2、有关AMD CS5536有关AC97部分的介绍
-
CS5536是一颗功能十分强大的芯片,我们仅能就其中我们要用到的功能做一点简要的介绍;
-
如果希望详细了解CS5536这颗芯片,可以在这里下载datasheet,篇幅很长,如果不使用这颗芯片,完全没有必要全部阅读。
-
前面说到,CS5536中的Codec控制器有8个通道,控制器负责从内存中向CODEC传输数据,所以在启动DMA之前,我们必须告诉控制器数据存放的内存地址,数据的长度,以及如何终止传输等等,还要告诉控制器启动哪一个或哪几个通道,CS5536的AC97控制器(ACC)为完成这些控制,有一系列的控制寄存器,偏移地址从00h–7Fh,数据均为32位;
-
首先说如何告诉ACC数据的存储地址以及与其相关的事情,CS5536上的ACC使用一个叫做PRD(Physical Region Descriptor)表作为传输控制信息的表述,表的结构如下:
-
每一个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 _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发出控制命令
1 2 3 4 5
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状态寄存器:
1 2 3 4 5
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可以在下面网址下载。
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,用于测试。
-
源文件下载:
-
程序的关键在ac97.h这个文件,我们将重点介绍其中的类MYSOUND。
-
现在我们假定编译出来的可执行文件是playwav.exe,我们这样来执行这个文件以便测试:
1
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小于8;
-
我们首先读取整块的数据到wavBuffer中,然后根据其采样宽度和声道数放到wavBuf 中,注意,如果是8bit数据需要先扩展成16bit后再放入wavBuf 中。
-
组织好数据后,开始设置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()不停地读这个状态寄存器。
-
至此,程序基本介绍完了,过程有些繁琐,由于篇幅原因,很多问题不得不请大家自己去读一些规范,让我一个字一个字地敲上去太难了,希望这篇文章能给你一些帮助。
欢迎访问我的博客:https://whowin.cn
email: hengch@163.com
文章作者 whowin
上次更新 2008-04-22