16位声霸卡的DSP编程
翻译:shepherd(zqw100@163.com)
原作信息:
题目:Programming the SoundBlaster 16 DSP
作者:by Ethan Brodsky (Version 3.1)
联系方式:ericbrodsky@psl.wisc.edu
写作日期:2/10/95
免责声明
本免责声明正文如下:
笔者声明,由于利用或误用本资料,最终导致的任何经济损失,或必然的/偶然的/其他形式的损失,笔者将不承担任何责任。本文可以被免费发放,但是在发放时请完整地保留本免责声明。
SB16简介
16位声霸卡(Sound Blaster 16,本文简写为SB16)可以处理FM(Frequency Modulation:频率调幅)和数字声音信号。其中数字信号的处理范围是:从8位5000HZ单声道,到16位44000HZ立体声(译者注:另外一种说法是到48000HZ)。这份常见问题文档,是关于SB16 DSP CT1341芯片数字音频信号的录制和回放的。理所当然的,有关更早声霸卡的编程知识必不可少。
译者补充的背景知识:
FM合成技术
它是运用特定的算法来简单模拟真实乐器声音。 其主要特点是电路简单、生产成本低,不需要大容量存储器支持即可模拟出多种声音。由于 FM是靠算法来合成某个声音,因此实现方法过于生硬、效果单一,所生成的声音与真实乐器产生的声音距离很大。很容易让人听出来是“电子音乐”。
DSP
DSP(Digital Signal Processor,数字信号处理器)是一种内含微处理器的专用芯片,它为当时的高档16位声卡实现180°环绕立体声再现立下了汗马功劳。
SB16的I/O端口
SB16的DSP芯片的可编程I/O服务端口地址,其基址是由主板跳线决定的(译者注:现在一般通过BIOS或者应用程序实现)。在SB16芯片中,有16个I/O端口被用作FM合成音乐,音响混合,DSP编程和CD-ROM访问。而下面列出的五个端口被用做DSP编程:
2x6h - DSP 复位
2xAh - DSP 读
2xCh - DSP 写(命令/数据),DSP 写缓冲区状态字(第7位)
2xEh - DSP 读缓冲区状态字(第7位), DSP 中断应答
2xFh - DSP 16位中断应答
译者补充背景资料:
端口中的X为基址,可以取1-6,通常情况下取2,即基址是220h。该基址可以由用户在相关配置文件中指定,也可以在程序中自己检测。
DSP复位
在进行DSP编程之前你必须将DSP复位。复位DSP需要按照以下步骤进行:
1,在复位端口(2X6)写入1
2,等待3毫秒
3,在复位端口(2X6)写入0
4,检测读缓冲区端口(2XE)状态,直到第7位为1。
5,检测读数据端口(2XA)状态,直到接受到AA。
DSP自身初始化通常需要大约100毫秒。经过这段时间以后如果返回的值仍然不是AA,或者没有任何数据返回,说明SB16卡未被安装,或者I/O地址不正确。
译者补充知识:
这里所说的第7位,是从第0位开始算的。整个复位过程,C语言的参考代码如下:
int RestDSP(int Test)
{
/* 重置DSP */
outportb(Test + 0x6, 1);
delay(3);
outportb(Test + 0x6, 0);
delay(80); /* 延时时间可以酌情调整 */
/* 如果重置成功则检查 */
if ((inportb(Test + 0xE) & 0x80 == 0x80)
&& (inportb(Test + 0xA) == 0xAA))
{
G_base = Test;
return 1;
}
else
{
return 0;
}
}
写DSP
向SB16写一个字节,需要按照以下步骤进行:
1,读写缓冲区状态端口2XC直到第7位被清除
2,将数值写入写端口2XC
读DSP
由SB16读一个字节,需要按照以下步骤进行:
1,读读缓冲区状态端口2XE直到第7位被设置
2,从读端口2XA读出一个字节
DMA控制器编程
DMA(Direct Memory Access,直接内存读取)控制器掌管着I/O设备和内存之间的数据传输,整个过程不需要CPU参与。一个INTEL 8237 DMAC集成电路被用来控制它,而一个IBM兼容机有两个DMA控制器:一个掌管8位另外一个掌管16位。同外部页面寄存器配对的DMA控制器,可以传输大于64KB的数据块。下面是有关I/O端口和必要的关于声卡寄存器的设置:
*DMA地址和计数寄存器的I/O端口地址
|====================================|
| 控制器 | I/O 地址 | 功能 |
|====================================|
| DMA 1 | 00 | 通道0地址 |
| 8位 | 01 | 通道0计数 |
| 从 | 02 | 通道1地址 |
| | 03 | 通道1计数 |
| | 04 | 通道2地址 |
| | 05 | 通道2计数 |
| | 06 | 通道3地址 |
| | 07 | 通道3计数 |
|====================================|
| DMA 2 | C0 | 通道4地址 |
| 16位 | C2 | 通道4计数 |
| 主 | C4 | 通道5地址 |
| | C6 | 通道5计数 |
| | C8 | 通道6地址 |
| | CA | 通道6计数 |
| | CC | 通道7地址 |
| | CE | 通道7计数 |
|====================================|
*控制寄存器的I/O端口地址
|========================================|
| 地址 | 操作 | 功能 |
|DMAC1 DMAC2 | | |
|========================================|
| 0A D4 | 写 |写单一掩码寄存器 |
| 0B D6 | 写 |写模式寄存器 |
| 0C D8 | 写 |清除翻转字节指示器 |
|========================================|
*页寄存器的I/O端口地址
|===============================|
| 地址 | 功能 |
|===============================|
| 81 | 8位 DMA 通道 2 页面 |
| 82 | 8位 DMA 通道 3 页面 |
| 83 | 8位 DMA 通道 1 页面 |
| 87 | 8位 DMA 通道 0 页面 |
| 89 | 16位 DMA 通道 6 页面 |
| 8A | 16位 DMA 通道 7 页面 |
| 8B | 16位 DMA 通道 5 页面 |
|===============================|
*模式寄存器的各位含义
|===============================|
| 位/值 |功能 |
|===============================|
|Bits 7:6 |状态选择 |
| 00 | 查询模式 |
| 01 | 单一模式 |
| 10 | 块模式 |
| 11 | 级联模式 |
|===============================|
| Bit 5 |地址增加/减少位 |
| 1 | 地址减少 |
| 0 | 地址增加 |
|===============================|
| Bit 4 |自动初始化设置位 |
| 1 | 自动初始化DMA |
| 0 | 单一周期DMA |
|===============================|
|Bits 3:2 |位传输 |
| 00 | 验证 |
| 01 | 写(到内存) |
| 10 | 读(从内存) |
| 11 | 非法 |
| 忽略 | 如果bits 7:6 = 11 |
|===============================|
|Bits 1:0 |通道选择 |
| 00 | 通道 0 (4) |
| 01 | 通道 1 (5) |
| 10 | 通道 2 (6) |
| 11 | 通道 3 (7) |
|===============================|
DMAC2用于16位I/O,DMAC1用于8位I/O。开始数据传送的过程很复杂,所以对于使用I/O的DMA传输方式,我将列出详细步骤。
1)计算你的内存缓冲区的绝对线性地址
LinearAddr := Seg(Ptr^)*16 + Ofs(Ptr^));
2)设置适当的掩码位,从而禁用对应声卡的DMA通道
Port[MaskPort] := 1 + (Channel mod 4);
3)清除翻转字节指示器
Port[ClrBytePtr] := AnyValue;
4)定义DMA传输模式
查询模式下,模式选择位应该被置为00,地址增加/减少位应置0。有关自动初始化模式位的恰当设定,我将在稍后讨论。回放模式下,传输位应置为10;而录音模式应置为01。通道选择位应该依照声卡的实际DMA通道设置。这里要注意,“读”意味着从内存读到声卡,“写”意味着从声卡写到系统内存。
Port[ModePort] := Mode + (Channel mod 4);
经常用到的方式如下:
48h+通道号 - 单一周期回放
58h+通道号 - 自动初始化回放
44h+通道号 - 单一周期录制
54h+通道号 - 自动初始化录制
5)写入内存缓冲区的偏移地址,低字节在前,高字节在后。对于16位数据,这个偏移地址应该是WORDS类型,从128KB的页面其实处算起。最早的计算16位参数的方法是:在计算偏移量之前讲线性地址除以2。
if SixteenBit
then
begin
BufOffset := (LinearAddr div 2) mod 65536;
Port[BaseAddrPort] := Lo(BufOffset);
Port[BaseAddrPort] := Hi(BufOffset);
end
else
begin
BufOffset := LinearAddr mod 65536;
Port[BaseAddrPort] := Lo(BufOffset);
Port[BaseAddrPort] := Hi(BufOffset);
end;
6)写入传输长度,低字节在前,高字节在后。对于8位传输,写入总字节数-1;对于16位传输,写入WORDS-1。
Port[CountPort] := Lo(TransferLength-1);
Port[CountPort] := Hi(TransferLength-1);
7)向DMA页面寄存器写入内存缓冲区页码
Port[PagePort] := LinearAddr div 65536;
8)清除适当的掩码位,从而允许声卡DMA通道
Port[MaskPort] := DMAChannel mod 4;
设置采样频率
同早期的声霸卡不同,SB16使用实际采样频率代替了时间片。在SB16上采用41H和42H的DSP命令来设置采样频率。41H用来设置输出,而42H用来设置输入。我听说在SB16上这两个命令其实做了同样的事情,但是我建议为了同未来的声卡兼容,最好采用指定的指令。设置采样频率的步骤如下:
1)写入命令字,输出写入41H,输入写入42H
2)写入采样频率的高字节,例如22050HZ则写入56H
3)写入采样频率的低字节,例如22050HZ责写入22H
数字音频I/O
为了记录或者播放音频,你应该采用以下的步骤:
1)申请一块不大于64KB的物理内存空间
2)安装一个中断服务程序
3)设置DMA控制器为后台传输模式
4)设置采样频率
5)向DSP写入I/O端口命令
6)写DSP写入I/O传输模式
7)向DSP写入块大小(低/高字节)
使用DMA的单一周期模式时,在中断中你需要:
1)将DMA控制器设置为下一个数据块
2)将DSP指向下一个数据块
3)如果有双缓冲,则复制下一个数据块
4)通过读端口对SB进行中断应答,8位是端口2XEH,16位是端口2XFH
5)应答中断完毕,向20H端口写入20H,如果声卡使用的是IRQ8-15,那么你还要向A0H写入20H
译者补充背景知识:
20H和A0H分别为主片和从片的中断控制器入口地址。IRQ 0-7对应主片,中断号为08H-0FH,;IRQ 8-15对应从片,中断号为70H-7FH。通常情况下IRQ0为定时器中断,IRQ1为键盘中断,IRQ5为声卡中断,RIQ6为键盘中断。用户的可编程中断为IRQ 10,11,12,15,都在从片上,如果使用的话则必须先打开主片的IRQ2。
DSP命令
D0-暂停由CXH命令引起的8位DMA模式数字音频初始化。可以应用在单一周期回放和自动初始化模式下。
D0-继续由D0H命令引起的8位DMA模式数字音频暂停。可以应用在单一周期回放和自动初始化模式下。
D5-暂停由BXH命令引起的16位DMA模式数字音频初始化。可以应用在单一周期回放和自动初始化模式下。
D6-继续由D5H命令引起的16位DMA模式数字音频暂停。可以应用在单一周期回放和自动初始化模式下。
D9-在本数据块操作之后,退出16位自动初始化模式的数字音频I/O操作。
DA-在本数据块操作之后,退出8位自动初始化模式的数字音频I/O操作。
E1-获得DSP版本号。在送出这个命令之后,DSP将会返回两字节的内容。第一个字节是主版本号,第二个字节是副版本号。一个SB16的DSP版本号应该大于等于4.00,在使用SB16专用命令前需要检查版本号。
BX-数字音频I/O的16位DMA模式
命令序列:命令,方式,低字节(长度-1),高字节(长度-1)
命令格式:|=======================================|
|D7 |D6 |D5 |D4 | D3 | D2 | D1 |D0 |
|=======================================|
| 1 | 0 | 1 | 1 | A/D | A/I |FIFO | 0 |
|=======================================|
|0=D/A |0=SC |0=off |
|1=A/D |1=AI |1=on |
|===================|
常用命令:
B8 - 16位单一周期输入
B0 - 16位单一周期输出
BE - 16位自动初始化输入
B6 - 16位自动初始化输出
模式:|=============================================|
|D7 |D6 | D5 | D4 |D3 |D2 |D1 |D0 |
|=============================================|
| 0 | 0 | Stereo | Signed | 0 | 0 | 0 | 0 |
|=============================================|
|0=单声道 |0=unsigned |
|1=立体声 |1=signed |
|=====================|
CX-数字音频I/O的8位DMA模式
基本用法与命令BX对应的16位数字音频I/O操作相同
常用命令:
C8 - 8位单一周期输入
C0 - 8位单一周期输出
CE - 8位自动初始化输入
C6 - 8位自动初始化输出
当声卡在需要时不能得到DMA时,系统将采用FIFO(first in first out,先进先出)算法来消除本采样周期内数据的不一致。如果FIFO被禁止,声卡会尝试使用DMA并立即声明:它需要一个采样。如果拥有较高优先级的其他设备占用了DMA,声卡将等待这个采样并且有可能降低采样频率。FIFO允许采样周期浮动而不影响音频质量,并且每当有信号传入DSP时FIFO将被自动清空。
在单一周期模式下,DSP在不断地改变,清空FIFO时有可能包含还没有来得及输出的数据。为了避免这个问题,在单一周期模式下应该关闭FIFO。
在自动初始化模式下,DSP是不会被改写的,可以打开FIFO以提高音质。
DMA的自动初始化
在单一周期DMA模式下,音频会在每个数据块的末尾停下,虽然中断程序可以开始另外一次传输,但是在音频输出中将会有一个停顿。这造成了在数据块之间出现滴答声,降低了音频质量。
在自动初始化模式下,声音输出在数据缓冲区的末尾循环-DMA控制器保持传输同样的内存数据块,而它们是在DMA初始化的时候就决定的。到达数据块末尾时,系统将依照存储的偏移和计数器寄存器,自动初始化当前偏移和计数寄存器,再次开始传递缓冲区数据。
为减少滴答声而被经常采用的方法是,申请一块内存缓冲区并把它分成两个数据块,将DMA控制器设置为整个缓冲区的长度,但是设置SB16为数据块的长度(也就是缓冲区长度的一半)。一个中断对应一个数据块,所以在播放缓冲区的时候会发生两次中断,一次在缓冲区的中间(就是第二个数据块的开始),一次在缓冲区的最后(就是第一个数据块的开始)。中断服务程序会复制数据到刚刚结束的那个数据区,这样数据在声卡需要输出的时候就准备好了。设置单一周期模式DMA传输的顺序和自动初始化模式下是基本一致的,不过是设置第四位的DMA模式寄存器和第三位的DSP命令不同而已。
自动初始化模式DMA的中断:
1)复制下一个数据单位到刚刚结束的那个输出数据块
2)通过读端口对SB进行中断应答,8位是端口2XEH,16位是端口2XFH
3)应答中断完毕,向20H端口写入20H,如果声卡使用的是IRQ8-15,那么你还要向A0H写入20H
立即停止音频:
8位模式下写入DSP命令D0H,16位模式下写入D5H。这样是立即停止,无需调用中断。
在当前数据块播放完毕之后停止音频:
8位模式下写入DSP命令DAH,16位模式下写入D9H。这样要等到当前数据块的末尾,如果系统没有为结束音频输出后的中断做好准备,这样可能会导致错误。
你仍然可以为单一周期模式而调整DSP,达到结束自动初始化模式的目的,声卡将在下一次中断后由A/I模式转变为S/C模式。然后它将继续在指定的长度上播放或录音,产生中断,停止。这会让你在数据结束之后正确的关闭输出音频,而不用担心剩余的DMA缓冲区被静音填满。这项技术或许对你有用,但是我建议你使用立即停止的命令,除非别的方法更合乎你的要求。