AC'97(Audio Codec '97)是Intel于1997年发布的音频标准。它主要由板载芯片使用,声卡(或调制解调器)使用频率较低。自 2004 年以来,它越来越多地被高清音频接口 (HD Audio) 取代。
我在这个(或多或少)教程中假设您有一个合理工作的PCI 您还应该熟悉声音文件(WAV,即未压缩)的结构,因此熟悉 PCM 或采样率等术语。
目录
[隐藏]
兼容设备
AC'97 兼容设备显示为基类代码为 04h(多媒体控制器)和子类代码为 01h(音频)的PCI设备。但一般来说这些只是声卡和相应的芯片,所以这类设备也可以是高清音频声卡或其他不兼容的音频设备。所以这是AC'97 设备的部分列表:
制造商 | 设备 |
0x8086(英特尔) | 0x2415(82801AA - 英特尔 ICH) |
0x8086(英特尔) | 0x2425(82801AB - 英特尔 ICH0) |
0x8086(英特尔) | 0x2445(82801BA - 英特尔 ICH2) |
0x1106(威盛) | 0x3058(可能是所有VIA主板上安装的AC'97芯片) |
不幸的是,用于控制卡的接口因制造商而异,因此它们将在不同的部分进行处理。
英特尔声卡
QEMU (0.10.0) 和VirtualBox (2.1.4) 使用 ME。不幸的是,本教程中提供的代码似乎只能在那里工作。
初始化
这些设备有两个I/O 室,第一个称为 NAM-BAR(Native Audio Mixer BAR),第二个称为 NABM-BAR(Native Audio Bus Master BAR)。混音器由 NAM-BAR 中的寄存器控制(即音量,采样率,...),NABM-BAR 中的寄存器控制播放(播放/暂停,声音缓冲区,...)。为了激活这些I/O空间并启动总线主控,PCI配置空间中“COMMAND”寄存器的值必须是“or-t”与0x05。一个例子:
<c>pciConfigWrite(总线,设备,功能,PCI_COMMAND,pciConfigRead(总线,设备,功能,PCI_COMMAND)| 5)</c>
然后必须按如下方式初始化设备:
重置
调整音量
必要时设置采样率
这可以做到,例如:
<c>无效延迟(int ms);//等待“ms”毫秒 uint16_t inw(int port); //从 I/O 端口“port”读取一个值 void outb(int port, uint8_t value); //在I/O端口“port”处输出值“value” void outw(int port, uint16_t value); //在I/O端口“port”输出值“value”。
// 这两个变量必须从 PCI 驱动程序(分别是第一个和第二个 I/O 空间)提供数据 int nambar; //NAM-BAR int nambar; //NABM-酒吧
整数量;//体积; 注意:0为最大音量,63为无声!
//代码(在任何函数中):outw(nambar + PORT_NAM_RESET, 42); //重置(这里可以是任何值)outb(nabmbar + PORT_NABM_GLB_CTRL_STAT, 0x02); //这里也是——0x02是强制delay(100); 音量 = 0;//最大声!outw(nambar + PORT_NAM_MASTER_VOLUME, (volume<<8) | volume); //一般的 音量(左右)outw(nambar + PORT_NAM_MONO_VOLUME, volume); //单声道输出的音量(可能是不必要的) outw(nambar + PORT_NAM_PC_BEEP, volume); //PC 扬声器的音量(如果不使用则不需要)outw(nambar + PORT_NAM_PCM_VOLUME, (volume<<8) | volume); //PCM 的音量(左和右)延迟(10);if (!(inw(nambar + PORT_NAM_EXT_AUDIO_ID) & 1)) { /* 采样率固定在 48 kHz */ } else {
outw(nambar + PORT_NAM_EXT_AUDIO_STC, inw(nambar + PORT_NAM_EXT_AUDIO_STC) | 1); //启用可变速率音频
延迟(10);
outw(nambar + PORT_NAM_FRONT_SPLRATE, 44100); //一般的 采样率:44100 赫兹
outw(nambar + PORT_NAM_LR_SPLRATE, 44100); //立体声采样率:44100 Hz
延迟(10);
//实际采样率现在在 PORT_NAM_FRONT_SPLRATE 或 PORT_NAM_LR_SPLRATE
</c>
之后,设备应该可以工作并播放声音。
输出音调(等)。
如果要播放音调(或任何其他此类内容),则必须将它们分解成单个片段,每个片段最多有65536 个样本。这些必须在内存中如下:
抵消 | 内容 |
+0x00 | 左侧的16 位样本 (int16_t) |
+0x02 | 右侧的16 位样本 (int16_t) |
注意:这是两个样本——每个通道一个。这导致每个缓冲区的最大大小为 65536 * 2 字节 = 128 KB。最多可以将 32 个缓冲区传输到设备,一个接一个地播放。即最大 4 MB (128 kB * 32) 或 23.8 秒 (44.1 kHz) 或 21.8 秒 (48 kHz)。您可以看到播放时间相对较短的数据量很大 - 因此在内存消耗方面可能夸大了 32 个以上的缓冲区。解决方案在于用新数据填充已播放的数据缓冲区,然后让它们再次播放。设备一次又一次地遍历所有缓冲区,而驱动程序一次又一次地刷新它们的内容。
现在让我们开始练习吧。缓冲区由八字节长的缓冲区描述符描述,结构如下:
抵消 | 数据类型 | 内容 |
+0x00 | uint32_t | 指向缓冲区的物理指针(与双字边界对齐) |
+0x04 | uint16_t | 样本中的缓冲区长度(立体声PCM:最大 0xFFFE,0x0000 表示:无数据) |
+0x06 | uint16_t | 参数:0x8000 = IOC;0x4000 = BUP;休息是保留的 |
IOC(完成时中断):如果设置,设备将在播放此缓冲区时发送中断。
BUP (Buffer Underrun Policy):如果这个缓冲区是最后一个要播放的或者下一个缓冲区还没有准备好,这个位就变得很重要:如果这个位没有设置那么这个缓冲区的最后一个样本将在播放后保留,如果已设置,发送“0”。通常它设置在要播放的最后一个缓冲区。
缓冲区描述符被输入到一个(逻辑上)称为缓冲区描述符列表的列表中。正如我所说,最多32 个描述符适合这里(即 256 字节的最大长度)。然后将此列表的起始地址、最后一个有效元素的编号(最多 31 个)传送到设备,最后您可以(象征性地)按下播放按钮。例如,它可能看起来像这样,最多可以播放 23.8 秒:
<c>结构buf_desc {
无效*缓冲区;
无符号短长度;
保留: 14;
无符号整数bup:1;
无符号整数ioc:1;
} __attribute__((打包));
结构buf_desc *BufDescList; //缓冲区描述符列表
//以下可以再次出现在任何函数中... int size; //要播放的数据长度,以字节为单位 int i; //索引 int final; //最后一个有效缓冲区 (i = 0; (i < 32) && size; i++) {
BufDescList[i].buffer = /* 相应缓冲区的物理地址 */;
if (size >= 0x20000) //超过 128 kB,所以缓冲区将满
{
//最大长度是 0xFFFE 而不是 0xFFFF!左和右
//必须是相等数量的样本,所以这个数字必须是偶数。
BufDescList[i].length = 0xFFFE;
大小-= 0x20000;//128 kB 距离
}
别的
{
//字节长度的一半,因为 16 位样本占用两个字节
BufDescList[i].length = 大小 >> 1;
大小= 0;// 现在没什么了
}
BufDescList[i].ioc = 1;
if (size) //另一个缓冲区
BufDescList[i].bup = 0;
else //没有更多的缓冲区
{
BufDescList[i].bup = 1;
最后=我;//最后一个有效缓冲区是这个
}
} outl(nabmbar + PORT_NABM_POBDBAR, (uint32_t)/*BufDescList的物理地址*/); outb(nabmbar + PORT_NABM_POLVI,最终);输出(nabmbar + PORT_NABM_POCONTROL,0x15);//播放,然后也产生中断!</c>
适用于QEMU 0.10.0 和VirtualBox 2.1.4... 如上所述,内存必须包含 16 位立体声样本,采样率为 44.1 kHz(或您设置的任何值)。
依恋
必要的常量:<c>#define PORT_NAM_RESET 0x0000
定义PORT_NAM_MASTER_VOLUME 0x0002
定义PORT_NAM_MONO_VOLUME 0x0006
定义PORT_NAM_PC_BEEP 0x000A
定义PORT_NAM_PCM_VOLUME 0x0018
定义PORT_NAM_EXT_AUDIO_ID 0x0028
定义PORT_NAM_EXT_AUDIO_STC 0x002A
定义PORT_NAM_FRONT_SPLRATE 0x002C
定义PORT_NAM_LR_SPLRATE 0x0032
定义PORT_NABM_POBDBAR 0x0010
定义PORT_NABM_POLVI 0x0015
定义PORT_NABM_POCONTROL 0x001B
定义PORT_NABM_GLB_CTRL_STAT 0x0060</c>
下面是德文原文。来自http://www.lowlevel.eu/wiki/AC97
AC97
AC'97 (Audio Codec '97) ist ein Audio-Standard, der von Intel im Jahre 1997 veröffentlicht wurde. Er wird hauptsächlich von On-Board-Chips und seltener von Soundkarten (bzw. Modems) verwendet. Seit 2004 wird er mehr und mehr von High Definition Audio Interface (HD Audio) abgelöst.
Ich gehe in diesem (mehr oder weniger) Tutorial davon aus, dass ihr einen einigermaßen funktionierenden PCI-Treiber habt, von dem ihr von einem Gerät mit einer bestimmten Hersteller- und Geräte-ID die BARs (in diesem Fall I/O-Räume) herausfinden könnt. Außerdem solltet ihr euch einigermaßen mit dem Aufbau von Sounddateien (WAV, also unkomprimiert) und somit auch mit Begriffen wie PCM oder Samplerate auskennen.
Inhaltsverzeichnis
[Verbergen]
Kompatible Geräte
AC'97-kompatible Geräte erscheinen als PCI-Geräte mit dem Basisklassencode 04h (Multimediacontroller) und dem Subklassencode 01h (Audio). Doch das sind nur Soundkarten und entsprechende Chips im Allgemeinen, es kann sich bei solchen Geräten also auch um HD-Audio-Soundkarten oder andere inkompatible Audiogeräte handeln. Daher hier eine unvollständige Liste von AC'97-Geräten:
Hersteller | Gerät |
0x8086 (Intel) | 0x2415 (82801AA - Intel ICH) |
0x8086 (Intel) | 0x2425 (82801AB - Intel ICH0) |
0x8086 (Intel) | 0x2445 (82801BA - Intel ICH2) |
0x1106 (VIA) | 0x3058 (vmtl. auf allen VIA-Mainboards verbauter AC'97-Chip) |
Leider sind die Schnittstellen zur Ansteuerung der Karten von Hersteller zu Hersteller unterschiedlich, sodass in separaten Abschnitten darauf eingegangen wird.
Intel-Soundkarten
QEMU (0.10.0) und VirtualBox (2.1.4) nutzen ICH. Die in diesem Tutorial vorgestellten Codes funktionieren leider anscheinend auch nur dort.
Initialisierung
Die Geräte besitzen zwei I/O-Räume, der erste heißt NAM-BAR (Native Audio Mixer BAR) und der zweite NABM-BAR (Native Audio Bus Master BAR). Mit den Registern in NAM-BAR wird der Mixer gesteuert (also Lautstärke, Samplerate, ...), mit denen in NABM-BAR wird das Abspielen gesteuert (Play/Pause, Soundbuffer, ...). Um diese I/O-Räume zu aktivieren und den Bus Master zu starten, muss der Wert des Registers „COMMAND“ im PCI-Konfigurationsraum mit 0x05 „ge-Or-t“ werden. Ein Beispiel:
<c>pciConfigWrite(Bus, Geraet, Funktion, PCI_COMMAND, pciConfigRead(Bus, Geraet, Funktion, PCI_COMMAND) | 5)</c>
Das Gerät muss dann wie folgt initialisiert werden:
Resetten
Lautstärke einstellen
evtl. Samplerate einstellen
Dies kann zum Beispiel so geschehen:
<c>void delay(int ms); //Wartet „ms“ Millisekunden uint16_t inw(int port); //Liest einen Wert vom I/O-Port „port“ ein void outb(int port, uint8_t value); //Gibt den Wert „value“ am I/O-Port „port“ aus void outw(int port, uint16_t value); //Gibt den Wert „value“ am I/O-Port „port“ aus
// Diese beiden Variablen müssen mit Daten vom PCI-Treiber gefüttert werden (erster bzw. zweiter I/O-Raum) int nambar; //NAM-BAR int nabmbar; //NABM-BAR
int volume; //Lautstärke; Achtung: 0 ist volle Lautstärke, 63 ist so gut wie stumm!
//Code (in einer beliebigen Funktion): outw(nambar + PORT_NAM_RESET, 42); //Resetten (jeder Wert ist hier möglich) outb(nabmbar + PORT_NABM_GLB_CTRL_STAT, 0x02); //Auch hier – 0x02 ist aber verpflichtend delay(100); volume = 0; //Am lautesten! outw(nambar + PORT_NAM_MASTER_VOLUME, (volume<<8) | volume); //Allg. Lautstärke (links und rechts) outw(nambar + PORT_NAM_MONO_VOLUME, volume); //Lautstärke für die Mono-Ausgabe (vmtl. unnötig) outw(nambar + PORT_NAM_PC_BEEP, volume); //Lautstärke für den PC-Lautsprecher (unnötig, wenn nicht benutzt) outw(nambar + PORT_NAM_PCM_VOLUME, (volume<<8) | volume); //Lautstärke für PCM (links und rechts) delay(10); if (!(inw(nambar + PORT_NAM_EXT_AUDIO_ID) & 1)) { /* Samplerate fix auf 48 kHz */ } else {
outw(nambar + PORT_NAM_EXT_AUDIO_STC, inw(nambar + PORT_NAM_EXT_AUDIO_STC) | 1); //Variable Rate Audio aktivieren
delay(10);
outw(nambar + PORT_NAM_FRONT_SPLRATE, 44100); //Allg. Samplerate: 44100 Hz
outw(nambar + PORT_NAM_LR_SPLRATE, 44100); //Stereo-Samplerate: 44100 Hz
delay(10);
//Tatsächliche Samplerate steht jetzt in PORT_NAM_FRONT_SPLRATE bzw. PORT_NAM_LR_SPLRATE
}</c>
Danach sollte das Gerät arbeiten und Töne abspielen können.
Töne (etc.) ausgeben
Sollen Töne (oder irgendwelche anderen Dinge in dieser Richtung) abgespielt werden, so müssen diese in Einzelstücke zu je maximal 65536 Samples zerlegt werden. Diese müssen wie folgt im Speicher stehen:
Offset | Inhalt |
+0x00 | 16-Bit-Sample (int16_t) links |
+0x02 | 16-Bit-Sample (int16_t) rechts |
Achtung: Das sind zwei Samples – pro Kanal eins. So ergibt sich pro Buffer eine maximale Größe von 65536 * 2 Bytes = 128 Kilobytes. Dem Gerät können maximal 32 Buffer übergeben werden, die es hintereinander abspielt. Das sind dann maximal 4 MB (128 kB * 32) oder 23,8 Sekunden (44,1 kHz) bzw. 21,8 Sekunden (48 kHz). Man sieht, dass es sich um eine große Datenmenge bei relativ kurzer Spieldauer handelt – daher sind viel mehr als 32 Buffer wohl auch vom Speicherverbrauch her übertrieben. Die Lösung liegt darin, schon abgespielte Datenbuffer mit neuen Daten zu füllen und erneut abspielen zu lassen. So durchläuft das Gerät alle Buffer immer wieder, während der Treiber deren Inhalte immer wieder auffrischt.
Kommen wir jetzt aber zur Praxis. Die Buffer werden durch acht Bytes lange Buffer Descriptors beschrieben, diese sind so aufgebaut:
Offset | Datentyp | Inhalt |
+0x00 | uint32_t | Physischer Pointer zum Buffer (An DWord-Grenzen ausgerichtet) |
+0x04 | uint16_t | Länge des Buffers in Samplen (Stereo-PCM: maximal 0xFFFE, 0x0000 bedeutet: keine Daten) |
+0x06 | uint16_t | Parameter: 0x8000 = IOC; 0x4000 = BUP; Rest ist reserviert |
IOC (Interrupt On Completion): Wenn gesetzt, so sendet das Gerät einen Interrupt, wenn dieser Buffer abgespielt wurde.
BUP (Buffer Underrun Policy): Dieses Bit wird dann wichtig, wenn dieser Buffer der letzte abzuspielende oder der nächste Buffer noch nicht bereit ist: Ist das Bit dann nicht gesetzt, so wird nach dem Abspielen das letzte Sample dieses Buffers gehalten, ist es gesetzt, so wird „0“ gesendet. Typischerweise ist es beim letzten abzuspielenden Buffer gesetzt.
Die Buffer Descriptors trägt man in eine Liste ein, die (logischerweise) Buffer Descriptor List genannt wird. Hier passen – wie gesagt – maximal 32 Deskriptoren rein (also 256 Bytes maximale Länge). Die Anfangsadresse dieser Liste wird dann dem Gerät mitgeteilt, dann die Nummer des letzten gültigen Elements (höchstens 31) und schließlich kann man (symbolisch) auf den Play-Knopf drücken. Das könnte zum Beispiel so aussehen, hiermit können maximal besagte 23,8 Sekunden abgespielt werden:
<c>struct buf_desc {
void *buffer;
unsigned short length;
int reserved : 14;
unsigned int bup : 1;
unsigned int ioc : 1;
} __attribute__((packed));
struct buf_desc *BufDescList; //Buffer Descriptor List
//Das folgende darf wieder in einer beliebigen Funktion stehen... int size; //Länge der zu spielenden Daten in Bytes int i; //Index int final; //Letzter gültiger Buffer for (i = 0; (i < 32) && size; i++) {
BufDescList[i].buffer = /* Physische Adresse des jeweiligen Buffers */;
if (size >= 0x20000) //Noch mehr als 128 kB, also wird der Buffer voll
{
//Maximale Länge ist 0xFFFE und NICHT 0xFFFF! Links und rechts
//müssen gleich viele Samples sein, daher muss diese Zahl gerade sein.
BufDescList[i].length = 0xFFFE;
size -= 0x20000; //128 kB weg
}
else
{
//Die Hälfte der Länge in Bytes, da 16-Bit-Samples zwei Bytes brauchen
BufDescList[i].length = size >> 1;
size = 0; //Nix mehr jetzt
}
BufDescList[i].ioc = 1;
if (size) //Noch ein Buffer
BufDescList[i].bup = 0;
else //Kein Buffer mehr
{
BufDescList[i].bup = 1;
final = i; //Letzter gültiger Buffer ist dieser hier
}
} outl(nabmbar + PORT_NABM_POBDBAR, (uint32_t)/*Physische Adresse von BufDescList*/); outb(nabmbar + PORT_NABM_POLVI, final); outb(nabmbar + PORT_NABM_POCONTROL, 0x15); //Abspielen, und danach auch Interrupt generieren!</c>
Funktioniert unter QEMU 0.10.0 und VirtualBox 2.1.4... Im Speicher müssen, wie oben gesagt, 16-Bit-Stereosamples mit einer Samplerate von 44,1 kHz (oder was ihr auch immer eingestellt habt), stehen.
Anhang
Nötige Konstanten: <c>#define PORT_NAM_RESET 0x0000
define PORT_NAM_MASTER_VOLUME 0x0002
define PORT_NAM_MONO_VOLUME 0x0006
define PORT_NAM_PC_BEEP 0x000A
define PORT_NAM_PCM_VOLUME 0x0018
define PORT_NAM_EXT_AUDIO_ID 0x0028
define PORT_NAM_EXT_AUDIO_STC 0x002A
define PORT_NAM_FRONT_SPLRATE 0x002C
define PORT_NAM_LR_SPLRATE 0x0032
define PORT_NABM_POBDBAR 0x0010
define PORT_NABM_POLVI 0x0015
define PORT_NABM_POCONTROL 0x001B
define PORT_NABM_GLB_CTRL_STAT 0x0060</c>