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.

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&nbsp;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&nbsp;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).

Weblinks

Persönliche Werkzeuge