Repository navigation
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.
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.
| 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 |
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.
In AC97Core (created from AC97Driver.startDevice):
-
PCIDevice.getConfig().asHeaderType0()gives the type-0 header;getBaseAddresses()returns the parsedPCIBaseAddressentries (BAR probing is done by the framework: it writes0xFFFFFFFFand reads back the size). - The two I/O BARs are claimed with
ResourceManager.claimIOResource(device, base, size): BAR0 → NAM, BAR1 → NABM. Both are wrapped inAccessControllerUtils.doPrivilegedbecause claiming requires theResourcePermission("ioports")declared by the plugin. - The IRQ comes from
PCIHeaderType0.getInterruptLine(); the line is validated against 0..15 and claimed withrm.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.
-
Cold reset:
GLOB_CNT(NABM + 0x2C) bit 1 isAC'97 Cold Reset#, active low. The ICH PRM sequence is: clear the bit (assertAC_RESET#), wait 10 ms, set it again (releaseAC_RESET#), then pollGLOB_STAbit 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) with0x0808(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.
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 alignedMEMMODE_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 + lengthThe producer/consumer ring lives in AC97Core:
-
open(rate)resets the DMA register box (CR_RR, wait for the hardware to clear it), programsBDBAR, setsLVI = 0, clearsSR, and selects the sample rate. -
write(pcm, offset, length)splits the PCM data into ring-sized chunks. For each chunk:MemoryResource.setBytescopies into the next free buffer, the descriptor is filled in,LVIis advanced to that index, andCR_IOCE | CR_RPBMis written. While every descriptor is in flight ((lastValidIndex + 1) % n == currentIndex) the writer waits withplaybackLock.wait(250). -
handleInterruptruns on the JNodeIRQ-<n>thread. It readsSR, returns immediately if no interrupt bit of this engine is set (shared line), acknowledges by writingSR_LVBCI | SR_BCIS | SR_FIFOEback (write-1-to-clear), clears the latchedGLOB_STAplayback bit, refreshescurrentIndexfromCIVand 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.
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.
-
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_MASKonly 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 == LVIinitially); the driver starts with descriptor 0 empty and only fills from index 1. -
The DMA engine stops when it runs out of descriptors.
SR_DCHis set. AdvancingLVIor re-writingCR_RPBMresumes 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) % nis safe to write; the engine prefetches up toLVI(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_CNTreset writes,/* TODO: Handle WR or CR */), butGLOB_STAalready 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.antallinall/lib/jnode.xmlonly builds core/shell/net/fs/builder/gui/textui/distr/cli, and nothing defines${jnode-sound.jar}. AC'97 therefore lives in theguiproject next to the existing speaker drivers; wiring thesound/skeleton requires adding it to the macrodef and defining its jar property. -
AC97Coreclaims and releases in strict order (IRQ, NAM, NABM, BDL memory) and releases in reverse on any constructor failure, mirroringDefaultFDC's documented claim order discipline.
- Driver-Framework — hub for the driver/device/API model this driver plugs into
-
DeviceToDriverMapper — how
PCIDeviceToDriverMappermatches8086:2415 - Bus-Drivers / PCI-Capability-Structure — PCI bus enumeration and config space access
-
Resource-Management —
ResourceManager,IOResource,MemoryResource,IRQResource - DMA — JNode's ISA DMA abstraction (not used here; AC'97 is PCI bus mastering, but the memory-mode rules are the same)
- Glossary — AC'97, NAM, NABM, BDL, VRA