AC97
Aus Lowlevel
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 |
Initialisierung
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) |
QEMU (0.10.0) und VirtualBox (2.1.4) nutzen ICH. Die in diesem Tutorial vorgestellten Codes funktionieren leider anscheinend auch nur dort.
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:
pciConfigWrite(Bus, Geraet, Funktion, PCI_COMMAND, pciConfigRead(Bus, Geraet, Funktion, PCI_COMMAND) | 5)
Das Gerät muss dann wie folgt initialisiert werden:
- Resetten
- Lautstärke einstellen
- evtl. Samplerate einstellen
Dies kann zum Beispiel so geschehen:
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 }
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 ein 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:
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!
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:
#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
Hinweise zu den Codes
Die Codes stammen (ganz leicht modifiziert) aus dem Betriebssystem MyXomycota, stehen somit also unter der GPL. Da ich aber die Lizenz bestimmen kann, erlaube ich natürlich auch die Freigabe unter einer BSD-artigen Lizenz, der LGPL oder unter den CC-Lizenzen, die „by“ enthalten. Credits wären natürlich schön, wenn der Code größtenteils unverändert übernommen wird. Wer den Code unter anderen Lizenzen verwenden will, kann mich einfach fragen, ich sehe da keine Hindernisse. Ich bin fast täglich im IRC (irc.euirc.net#lost).

