Skip to content

AC97 Audio Driver

opencode edited this page Oct 10, 2026 · 5 revisions

AC97 Audio Driver

Intel AC'97 (ICH / 82801AA) audio driver: PCI setup, NAM/NABM register sets, the Buffer Descriptor List as JNode DMA memory, and the PCM output pipeline.

Overview

JNode's audio support originally consisted of the PC speaker driver only (org.jnode.driver.sound.speaker.pc.PCSpeakerDriver, which toggles port 0x61 and programs PIT channel 2). The AC'97 driver adds a real DMA audio path for the Intel ICH AC'97 controller found in most chipsets since the late 1990s, and emulated by both QEMU (-device AC97,audiodev=...) and VirtualBox.

The controller is a PCI device (vendor 0x8086, device 0x2415, class 04:01). It exposes two I/O register sets through PCI BARs:

BAR Name Contents
BAR0 NAM (Native Audio Mixer) 16-bit codec registers: volumes, powerdown, sample rate, codec id
BAR1 NABM (Native Audio Bus Master) DMA engine register boxes, global control/status

The driver lives in gui/src/driver/org/jnode/driver/sound/ac97/ and is packaged in the org.jnode.driver.sound.ac97 plugin (descriptor in gui/descriptors/), which contributes to the org.jnode.driver.mappers extension point. The pre-existing org.jnode.driver.sound.* plugins (speaker, speaker.pc, sound.command) follow the same layout, and everything is registered in all/conf/default-plugin-list.xml.

There is no JNode PCM mixer / javax.sound service to integrate with; the driver is consumed directly through the DeviceAPI mechanism. See Driver-Framework and Resource-Management for the surrounding framework.

Key Components

Class / File Role
gui/src/driver/org/jnode/driver/sound/ac97/AC97Constants.java PCIConstants-derived constants: PCI ids, NAM offsets, NABM offsets, SR/CR bits, BDL entry layout, valid sample rates
gui/src/driver/org/jnode/driver/sound/ac97/AC97Driver.java Driver subclass bound by the mapper; PCI sanity checks in verifyConnect, owns the AC97Core, registers AC97API on the device
gui/src/driver/org/jnode/driver/sound/ac97/AC97Core.java All hardware access: resource claiming, AC-link cold reset, codec init, DMA engine control, IRQHandler
gui/src/driver/org/jnode/driver/sound/ac97/BufferDescriptorList.java Allocates and manages the BDL + sample buffers as JNode DMA memory
gui/src/driver/org/jnode/driver/sound/ac97/AC97API.java The DeviceAPI interface used by consumers
gui/src/driver/org/jnode/driver/sound/ac97/AC97Utils.java Static lookup helper (mirrors SpeakerUtils), square-wave tone generator
gui/src/driver/org/jnode/driver/sound/ac97/command/AC97PlayCommand.java Shell command ac97play [frequency] [duration]
gui/descriptors/org.jnode.driver.sound.ac97.xml Plugin descriptor with the 8086:2415 mapper
gui/descriptors/org.jnode.driver.sound.ac97.command.xml Plugin descriptor with the ac97play alias

How It Works

1. Driver binding

The plugin descriptor contributes a mapper to the PCI bus extension point:

<mapper id="8086:2415" name="Intel ICH AC'97"
        driver-class="org.jnode.driver.sound.ac97.AC97Driver"
        class="org.jnode.driver.bus.pci.PCIDeviceToDriverMapper"/>

PCIDeviceToDriverMapper matches on vendorId:deviceId (hex, colon separated) at match level MATCH_DEVICE, which is queried before any class-code mapper. AbstractPCIDeviceToDriverMapper instantiates the driver through its ConfigurationElement constructor (falling back to the no-arg constructor), so the driver class must be public with that exact signature. AC97Driver.verifyConnect re-checks the ids and throws DriverException for anything else; the device manager catches this and simply leaves the device undriven.

2. PCI setup

In AC97Core (created from AC97Driver.startDevice):

  • PCIDevice.getConfig().asHeaderType0() gives the type-0 header; getBaseAddresses() returns the parsed PCIBaseAddress entries (BAR probing is done by the framework: it writes 0xFFFFFFFF and reads back the size).
  • The two I/O BARs are claimed with ResourceManager.claimIOResource(device, base, size): BAR0 → NAM, BAR1 → NABM. Both are wrapped in AccessControllerUtils.doPrivileged because claiming requires the ResourcePermission("ioports") declared by the plugin.
  • The IRQ comes from PCIHeaderType0.getInterruptLine(); the line is validated against 0..15 and claimed with rm.claimIRQ(device, line, this, true) — shared, because PCI lines are frequently shared and the handler ignores interrupts that are not its own.
  • Bus mastering is enabled through the PCI command register:
config.setCommand(config.getCommand()
    | PCIConstants.PCI_COMMAND_IO
    | PCIConstants.PCI_COMMAND_MEMORY
    | PCIConstants.PCI_COMMAND_MASTER);

Without PCI_COMMAND_MASTER the DMA engine may not drive PCI bus cycles at all.

3. Codec bring-up

  • Cold reset: GLOB_CNT (NABM + 0x2C) bit 1 is AC'97 Cold Reset#, active low. The ICH PRM sequence is: clear the bit (assert AC_RESET#), wait 10 ms, set it again (release AC_RESET#), then poll GLOB_STA bit 8 (GLOB_STA_PCR, primary codec ready) for up to a second. Extensions cleared at the same time: GLOB_CNT_ACLINK_OFF (bit 3, shutdown) and the 2/4/6-channel mask (bits 21:20) which is forced back to stereo.
  • Power: NAM register 0x26 (NAM_POWERDOWN_CTRL_STAT) — the four section power bits (0x000F: ADC, DAC, analog, reference) are OR'ed with the ready-status bits (0x0F00), which must be preserved.
  • Register reset: writing anything to NAM 0x00 resets all mixer registers to defaults. Do this before programming volumes — most volume registers default to muted (master = 0x8000), which is the classic "the driver runs but there is no sound" bug.
  • Volumes: NAM 0x02 (master) and 0x04 (headphone) are written with 0x0000 (unmuted, 0 dB); NAM 0x18 (PCM out) with 0x0808 (0 dB, 5-bit fields per channel).
  • Variable rate: if NAM 0x28 bit 0 (NAM_EI_VRA) is set, VRA is enabled in NAM 0x2A bit 0 and the rate is programmed into NAM 0x2C. Without VRA the codec only accepts 48 kHz.

4. Buffer Descriptor List as JNode memory

The BDL is the scatter/gather table the DMA engine walks. Each of the up to 32 entries is 8 bytes:

Offset Bits Meaning
0x00 31:1 Physical address of the sample buffer (word aligned; bit 0 must be 0)
0x04 31 BD_IOC — interrupt on completion of this buffer
30 BD_BUP — buffer underrun policy (repeat last sample)
29:16 reserved
15:0 number of 16-bit samples (2 samples = 1 stereo frame; max 65536, must be even)

BufferDescriptorList claims one contiguous block with

mem = rm.claimMemoryResource(owner, null, total, ResourceManager.MEMMODE_ALLOC_DMA);
bdl   = mem.claimChildResource(entryCount * 8, BDL_ALIGN);   // 128-byte aligned
buf[i] = mem.claimChildResource(framesPerBuffer * 4, BUFFER_ALIGN); // 8-byte aligned

MEMMODE_ALLOC_DMA makes MemoryResourceImpl allocate from Unsafe.getMinAddress() in 64 KB steps, i.e. from the low DMA-compatible zone. AC'97's BDBAR only holds 32 bits, so any memory works in principle, but the DMA allocator guarantees a physically contiguous block with a stable address, which is what a bus-master descriptor needs. The default geometry is 16 descriptors x 1024 stereo frames (~21 ms per buffer, ~340 ms of ring).

The descriptor is written through MemoryResource.setInt, which is a plain little-endian 32-bit store — the same byte order the controller expects on x86:

descriptors.setInt(ofs, buffer.getAddress().toInt());                       // pointer
descriptors.setInt(ofs + 4, samples | (ioc ? BD_IOC : 0));                  // control + length

5. Playback pipeline

The producer/consumer ring lives in AC97Core:

  • open(rate) resets the DMA register box (CR_RR, wait for the hardware to clear it), programs BDBAR, sets LVI = 0, clears SR, and selects the sample rate.
  • write(pcm, offset, length) splits the PCM data into ring-sized chunks. For each chunk: MemoryResource.setBytes copies into the next free buffer, the descriptor is filled in, LVI is advanced to that index, and CR_IOCE | CR_RPBM is written. While every descriptor is in flight ((lastValidIndex + 1) % n == currentIndex) the writer waits with playbackLock.wait(250).
  • handleInterrupt runs on the JNode IRQ-<n> thread. It reads SR, returns immediately if no interrupt bit of this engine is set (shared line), acknowledges by writing SR_LVBCI | SR_BCIS | SR_FIFOE back (write-1-to-clear), clears the latched GLOB_STA playback bit, refreshes currentIndex from CIV and wakes the writer.

Ordering matters and is cheap here: descriptor contents and sample data must be visible to the DMA controller before LVI is advanced or the run bit is set. x86 is DMA coherent, so no device-side cache flush is needed; ensureRunning() performs a dummy read of CIV as a store-buffer fence, and JNode's port I/O (IOResource.outPort*) is a VM-intrinsic call, which the compiler never moves across.

When CIV catches up with LVI the engine halts itself (SR_DCH); the next LVI advance (or the re-written run bit) restarts it. On QEMU this is literally what the LVI write handler does.

6. Consumer usage

AC97Utils.findDevice() uses DeviceUtils.getDevicesByAPI(AC97API.class), exactly mirroring SpeakerUtils. The shell command ac97play 440 500 opens the device at 48 kHz, generates a square wave as 16-bit LE stereo PCM in ~25 ms chunks, pushes them through AC97API.write (which rate-limits the caller to the ring depth), waits for the ring to drain, and closes. No interrupt, descriptor or register knowledge is needed at the call site.

Gotchas & Non-Obvious Behavior

  • Volume registers default to muted. Master volume (0x8000) and PCM out (0x8808) are muted after any NAM register reset; a driver that only writes the codec reset gets silence.
  • BDL length is in samples, not bytes, and counts channels. A 1024-frame stereo buffer is 2048 samples / 4096 bytes. An odd sample count is forbidden by the spec (BD_MAX_SAMPLES / BD_LENGTH_MASK only allow 65535 useful entries).
  • The BDBAR low bits are hardwired to 0. The list must be at least 8-byte aligned; 128 bytes is the common choice. Each sample buffer pointer must be word aligned so a sample never straddles a DWord.
  • Empty descriptors are legal and are skipped. A descriptor with length 0 is processed instantly (QEMU does exactly this when CIV == LVI initially); the driver starts with descriptor 0 empty and only fills from index 1.
  • The DMA engine stops when it runs out of descriptors. SR_DCH is set. Advancing LVI or re-writing CR_RPBM resumes it; the driver does the latter after every refill so it works on both real hardware and emulators.
  • Never modify a descriptor the controller may have prefetched. Only the slot at (lastValidIndex + 1) % n is safe to write; the engine prefetches up to LVI (PIV).
  • Writing NAM register 0x00 resets every mixer register (on real hardware and on QEMU, which implements mixer_reset). Program volumes, power and VRA afterwards, not before.
  • ACC_SEMA (NABM + 0x34) is not used by this driver; the ICH mixer registers are plain I/O port accesses and the semaphore is only needed for read-modify-write sequences on some chipsets.
  • The IRQ handler runs on a dedicated high-priority thread, not on an interrupt stack, but it should still stay short: read status, acknowledge, update currentIndex, notifyAll.
  • Cold reset on QEMU is a no-op (the emulator ignores GLOB_CNT reset writes, /* TODO: Handle WR or CR */), but GLOB_STA already reports the primary codec ready, so the driver's wait succeeds immediately. Real ICH hardware needs the full sequence.
  • The dormant sound/ subproject is not part of the build. jnode.antall in all/lib/jnode.xml only builds core/shell/net/fs/builder/gui/textui/distr/cli, and nothing defines ${jnode-sound.jar}. AC'97 therefore lives in the gui project next to the existing speaker drivers; wiring the sound/ skeleton requires adding it to the macrodef and defining its jar property.
  • AC97Core claims and releases in strict order (IRQ, NAM, NABM, BDL memory) and releases in reverse on any constructor failure, mirroring DefaultFDC's documented claim order discipline.

Related Pages

Clone this wiki locally