AC97

Aus Lowlevel
Wechseln zu: Navigation, Suche

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.

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

  1. define PORT_NAM_MASTER_VOLUME 0x0002
  2. define PORT_NAM_MONO_VOLUME 0x0006
  3. define PORT_NAM_PC_BEEP 0x000A
  4. define PORT_NAM_PCM_VOLUME 0x0018
  5. define PORT_NAM_EXT_AUDIO_ID 0x0028
  6. define PORT_NAM_EXT_AUDIO_STC 0x002A
  7. define PORT_NAM_FRONT_SPLRATE 0x002C
  8. define PORT_NAM_LR_SPLRATE 0x0032
  9. define PORT_NABM_POBDBAR 0x0010
  10. define PORT_NABM_POLVI 0x0015
  11. define PORT_NABM_POCONTROL 0x001B
  12. define PORT_NABM_GLB_CTRL_STAT 0x0060</c>

VIA-Soundkarten

Diese Seite oder Abschnitt ist zwar komplett, es wird aber folgende Verbesserungen gewünscht:

Testen und erweitern

Hilf Lowlevel, den Artikel zu verbessern.

Initialisierung

VIAs AC'97-Soundkarte ist Funktion 5 der Southbridge, die sich ihre IO-Adressräume mit dem integrierten Modem (MC97) in Funktion 6 teilt. Wir behandeln im Folgenden nur AC'97. Der für uns relevante IO-Raum für DMA residiert im ersten PCI-BAR.

Die Karte bezieht Sounddaten aus aus mehreren Puffern, die in SGD-Tabellen ("Scatter/Gather DMA Table") durch Deskriptoren folgenden Formats beschrieben werden. Die physischen Adressen der Tabellen sind in die 32-bit-Register AC97_VIA_R_SGD_TABLE_BASE bzw. AC97_VIA_W_SGD_TABLE_BASE zu schreiben und an 2-Byte-Grenzen auszurichten: <c>typedef struct { uint32_t buf; uint32_t len  : 24; uint32_t reserved : 5; uint32_t stop  : 1; // Transfer am Ende des Blocks unterbrechen. Um fortzufahren, setze Bit 2 in Register 0 (Audio SGD Read Channel Status) uint32_t flag  : 1; // Transfer am Ende des Blocks unterbrechen. Löst einen FLAG-Interrupt aus. uint32_t eol  : 1; // Letzter Eintrag. Löst einen EOL-Interrupt aus. } __attribute__((packed)) ac97Via_SGDEntry_t;</c>

Zugriff auf den AC97-Codec

Der Zugriff auf den AC97-Codec gestaltet sich schwieriger als bei der o.g. AC97-Implementation von Intel, weil dessen Register nicht in einen eigenen Adressraum gemappt sind, sondern über das 4 Bytes lange Register an Offset 0x80 gelesen und geschrieben werden müssen. Der folgende Code implementiert den Zugriff auf diese Register: <c>void codec_sendCommand(ac97Via_t* ac97, uint8_t reg, bool primary, uint16_t data) { while (inl(ac97->iobase + AC97_VIA_ACCESS_CODEC) & AC97_VIA_CODEC_BUSY) // Warte, bis der Codec Zeit hat ; if (primary) outl(ac97->iobase + AC97_VIA_ACCESS_CODEC, AC97_VIA_CODEC_SEL_PRIM | AC97_VIA_CODEC_WRITE | ((uint32_t)reg << 16) | data); else outl(ac97->iobase + AC97_VIA_ACCESS_CODEC, AC97_VIA_CODEC_SEL_SEC | AC97_VIA_CODEC_WRITE | ((uint32_t)reg << 16) | data); }

uint16_t codec_readStatus(ac97Via_t* ac97, uint8_t reg, bool primary) { while (inl(ac97->iobase + AC97_VIA_ACCESS_CODEC) & AC97_VIA_CODEC_BUSY) // Warte, bis der Codec Zeit hat ; if (primary) { outl(ac97->iobase + AC97_VIA_ACCESS_CODEC, AC97_VIA_CODEC_SEL_PRIM | AC97_VIA_CODEC_READ | AC97_VIA_CODEC_PRIM_VALID | ((uint32_t)reg << 16)); while (!(inl(ac97->iobase + AC97_VIA_ACCESS_CODEC) & AC97_VIA_CODEC_PRIM_VALID)) // Warte, bis der Codec die Daten geliefert hat ; return inl(ac97->iobase + AC97_VIA_ACCESS_CODEC) & 0xFFFF; } else { outl(ac97->iobase + AC97_VIA_ACCESS_CODEC, AC97_VIA_CODEC_SEL_SEC | AC97_VIA_CODEC_READ | AC97_VIA_CODEC_SEC_VALID | ((uint32_t)reg << 16)); while (!(inl(ac97->iobase + AC97_VIA_ACCESS_CODEC) & AC97_VIA_CODEC_SEC_VALID)) // Warte, bis der Codec die Daten geliefert hat ; return inl(ac97->iobase + AC97_VIA_ACCESS_CODEC) & 0xFFFF; } }</c>

Anhang

Definitionen der oben verwendeten Konstanten: <c>// Register-Offsets

  1. define AC97_VIA_R_SGD_CONTROL 0x01
  2. define AC97_VIA_R_SGD_TABLE_BASE 0x04
  3. define AC97_VIA_W_SGD_CONTROL 0x11
  4. define AC97_VIA_W_SGD_TABLE_BASE 0x14
  5. define AC97_VIA_ACCESS_CODEC 0x80

// Bits des AC97_VIA_ACCESS_CODEC-Registers

  1. define AC97_VIA_CODEC_SEL_PRIM 0
  2. define AC97_VIA_CODEC_PRIM_VALID BIT(25)
  3. define AC97_VIA_CODEC_SEL_SEC BIT(30)
  4. define AC97_VIA_CODEC_SEC_VALID BIT(27)
  5. define AC97_VIA_CODEC_BUSY BIT(24)
  6. define AC97_VIA_CODEC_WRITE 0
  7. define AC97_VIA_CODEC_READ BIT(23)</c>

Weblinks