Skip to content

AC97 Audio Driver

opencode edited this page Oct 10, 2026 · 4 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.
  • Always allocate all 32 BDL entries. A shorter list is prefetched past its end (CIV/PIV are 5-bit and walk modulo 32), which silently plays JNode heap memory as PCM. The 32 x 1024 frame default ring is 128 KB of DMA memory and ~341 ms of audio.
  • Prebuffer before starting the engine. Starting the DMA on the first filled descriptor turns JVM warm-up time into an audible silent gap at the start of every first stream after boot; queue a few descriptors first.
  • Do not rewrite CR_RPBM while the engine is running. It restarts the descriptor walk from the prefetched index; advance LVI instead and only re-write the control register when SR_DCH says the engine halted.
  • 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.

Validation

Validated 2026-10-10 on QEMU 10.2.1 (-accel tcg, -device AC97,audiodev=snd0, -audiodev wav,id=snd0,path=...), ISO from sh build.sh cd-x86-lite:

00:00:54,093 INFO  [AC97Core]: AC'97 at 0,4,0 NAM=0x0000C000 NABM=0x0000C400
                            IRQ=11 BufferDescriptorList addr=0x00004000 entries=16 framesPerBuffer=1024
00:00:54,198 INFO  [AC97Core]: AC'97 codec 8384:7600 VRA=true
Check Result
Build sh build.sh cd-x86-lite OK, both plugins packaged into all/build/plugins/
plugin org.jnode.driver.sound.ac97 state active
device pci(0,4,0) started driver:org.jnode.driver.sound.ac97.AC97Driver
BDL address 0x00004000 - low DMA memory, as designed
ac97play 440 500 696 KB of PCM captured on the wav backend
Tone analysis square wave on both L and R, 444.4 Hz as programmed (half period 54 samples; measured 49.5 samples on the 44100 Hz wav stream - QEMU resamples the 48 kHz voice to the backend rate)
ac97play 880 300 OK, second tone captured after the first
plugin --unload org.jnode.driver.sound.ac97 clean stop, resources released, no errors
Boot log after all commands zero driver exceptions

Bugs found by these runs:

  1. BufferDescriptorList.setDescriptor originally validated the buffer address with (address & BD_LENGTH_MASK) != address, which is a "below 64 KB" test, not a "below 4 GB" test. It threw IllegalStateException on the first ring wrap (buffer at 0x00010080) after one full ring had played. Now a real check on Address.toLong(), plus the same check for the BDL itself in the constructor.
  2. The BDL was allocated with only 16 entries. The CIV and PIV index registers are 5 bit wide, and the controller walks them modulo 32 regardless of the last valid index, so the prefetch ran past the end of a 16-descriptor list into the first sample buffer. Sample PCM data reinterpreted as a descriptor yields a pointer into the JNode heap (1-2 MB), which is then played as audio: sustained garbage with peaks around 24000 where the tone itself peaks at ~4500. Notes shorter than the ring (16 x 1024 frames = 341 ms) played clean, which is what hid it at first. Fixed by always allocating the full BDL_MAX_ENTRIES (32) - exactly what Linux does (lvi = ICH_REG_LVI_MASK). Cost: 128 KB of DMA memory instead of 64 KB.
  3. ensureRunning() re-wrote CR_RPBM after every ring refill. Writing the run bit restarts the descriptor walk from the prefetched index, so this discarded buffered position every ~21 ms and caused drops/repeats. The control register is now written only when the engine is stopped: at stream start, and after SR_DCH shows it halted itself.
  4. The engine was started on the first filled descriptor. On a cold JVM (class loading, JIT warm up) the producer needs tens of milliseconds to generate the next chunk, and the controller turns that into a silent gap at the start of the first tone of every boot. Fixed with prebuffering: the control register is only written once PREBUFFER_DESCRIPTORS (4, about 85 ms of audio) are queued, or when the stream has no data left to queue. Every real audio driver prebuffers for exactly this reason; the symptom only appears on the first stream after boot because the JVM is warm by then.

Observations that are hardware/emulator behaviour, not driver bugs:

  • QEMU's wav backend records at 44100 Hz and resamples the 48 kHz AC'97 voice; a written frequency appears in the file multiplied by 44100/48000.
  • After the ring drains, the engine halts with RPBM still set, so QEMU emulates the BD_BUP policy and repeats the last sample: capture continues for a couple of seconds past the tone.
  • QEMU logs audio: Could not create a backend for voice 'ac97.pi'/'ac97.mc' with the wav backend - it only supports playback; this driver only uses the PCM out engine.
  • AC97Utils.playTone rounds the half period with rate / (2 * f), so a requested 523 Hz comes out as 533 Hz (~2% sharp). Pitch-accurate rounding needs (rate + f) / (2 * f).
  • After the fixes, a 7-note arpeggio (ac97play 523 220 ... ac97play 1046 500 ...) captures as 7 clean bursts, each ~2% sharp as above, with both channels identical and no garbage at any ring wrap - the melody outlives the 341 ms ring three times over.
  • The first activation of the PCM out voice warps the first milliseconds of a stream (the codec backend warms up its rate converter), which is audible as a chirp on the opening note. playTone therefore prepends 30 ms of digital silence and uses a 15 ms attack / 20 ms release envelope; with the prebuffering above, the captured first note is 30 ms of silence, then a clean linear ramp, then a steady square - no warp, no gap.
  • QEMU's wav backend records at 44100 Hz and resamples the 48 kHz AC'97 voice, so square edges show one interpolated sample at each transition (a single 91 or -3437 next to 4471). That is the capture path, not the DMA data.

Related Pages

Clone this wiki locally