From 6d5dab6c1add8d614933332679ed1d0951032358 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 4 Feb 2026 06:08:18 -0300 Subject: [PATCH] Update APU and MMIO specifications --- specs/tiny16-apu.txt | 270 ++++++++++++++++++++++++++----------------- specs/tiny16-vm.txt | 12 +- 2 files changed, 166 insertions(+), 116 deletions(-) diff --git a/specs/tiny16-apu.txt b/specs/tiny16-apu.txt index a9fa0bc..cf89e92 100644 --- a/specs/tiny16-apu.txt +++ b/specs/tiny16-apu.txt @@ -5,114 +5,151 @@ tiny16 APU Specification OVERVIEW -------- -5-channel audio synthesis with async sample generation. +4-channel unified audio synthesis with async sample generation, plus +memory-driven SFX and music playback. Sample Rate: 44,100 Hz Output: 32-bit float, mono -Channels: 2 pulse + 1 triangle + 1 noise + 1 wave +Channels: 4 unified channels (selectable waveform per channel) Phase: 32-bit accumulator per channel Frequency: 11-bit divisor (0-2047) Volume: 4-bit master + 4-bit per channel Envelope: 4-bit ADSR per channel Length: 8-bit length (frames @ 60 Hz) -Sweep: Pulse-only frequency sweep CHANNELS -------- -CH0, CH1 - Pulse Wave +CH0-CH3 - Unified Channels Frequency: 11-bit (0-2047) Volume: 4-bit (0-15) - Duty Cycle: 12.5%, 25%, 50%, 75% Envelope: ADSR (4-bit each) - Sweep: Rate (4-bit), Direction, Shift (3-bit) Length: 0-255 frames (60 Hz) - Use: Melody, harmony, leads + Waveforms: Selected per channel (see WAVEFORMS below) + Use: Melody, bass, drums, SFX -CH2 - Triangle Wave - Frequency: 11-bit (0-2047) - Volume: 4-bit (0-15) - Envelope: ADSR (4-bit each) - Length: 0-255 frames (60 Hz) - Use: Bass, sub-bass - -CH3 - Noise (LFSR) - Period: 4-bit (0-15), lower = higher pitch - Volume: 4-bit (0-15) - Mode: Long (hissy) / Short (metallic) - Envelope: ADSR (4-bit each) - Length: 0-255 frames (60 Hz) - Use: Drums, explosions, SFX -CH4 - Wave (32-sample wavetable) - Frequency: 11-bit (0-2047) - Volume: 4-bit (0-15) - Envelope: ADSR (4-bit each) - Length: 0-255 frames (60 Hz) - Wave RAM: 32 x 4-bit samples - Use: Custom timbres, instruments, SFX - - -MMIO REGISTERS (0xBF40-0xBF8F) +MMIO REGISTERS (0xBF40-0xBF9F) ------------------------------ 0xBF40 APU_CTRL RW bit 0: enable | bits 4-7: master volume -0xBF41 APU_STATUS R bits 0-4: channel active flags +0xBF41 APU_STATUS R bits 0-3: channel active flags + +Channel Registers (stride = 6 bytes per channel): +FREQ_LO, FREQ_HI, VOL, CTRL, ENV_AD, ENV_SR -Channel 0 - Pulse 1: -0xBF42 CH0_FREQ_LO W frequency bits 0-7 -0xBF43 CH0_FREQ_HI W frequency bits 8-10 (bits 0-2) -0xBF44 CH0_VOL RW bits 0-3: volume | bits 4-5: duty (00/01/10/11) +Channel 0 (0xBF42-0xBF47): +0xBF42 CH0_FREQ_LO RW frequency bits 0-7 +0xBF43 CH0_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform +0xBF44 CH0_VOL RW bits 0-3: volume 0xBF45 CH0_CTRL RW bit 0: enable | bit 1: trigger +0xBF46 CH0_ENV_AD RW hi nibble: attack | lo nibble: decay +0xBF47 CH0_ENV_SR RW hi nibble: sustain | lo nibble: release + +Channel 1 (0xBF48-0xBF4D): +0xBF48 CH1_FREQ_LO RW frequency bits 0-7 +0xBF49 CH1_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform +0xBF4A CH1_VOL RW bits 0-3: volume +0xBF4B CH1_CTRL RW bit 0: enable | bit 1: trigger +0xBF4C CH1_ENV_AD RW attack/decay +0xBF4D CH1_ENV_SR RW sustain/release + +Channel 2 (0xBF4E-0xBF53): +0xBF4E CH2_FREQ_LO RW frequency bits 0-7 +0xBF4F CH2_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform +0xBF50 CH2_VOL RW bits 0-3: volume +0xBF51 CH2_CTRL RW bit 0: enable | bit 1: trigger +0xBF52 CH2_ENV_AD RW attack/decay +0xBF53 CH2_ENV_SR RW sustain/release + +Channel 3 (0xBF54-0xBF59): +0xBF54 CH3_FREQ_LO RW frequency bits 0-7 +0xBF55 CH3_FREQ_HI RW bits 0-2: frequency bits 8-10 | bits 4-6: waveform +0xBF56 CH3_VOL RW bits 0-3: volume +0xBF57 CH3_CTRL RW bit 0: enable | bit 1: trigger +0xBF58 CH3_ENV_AD RW attack/decay +0xBF59 CH3_ENV_SR RW sustain/release + +Length Registers (frames @ 60 Hz): +0xBF5A CH0_LEN RW length in frames (60 Hz) +0xBF5B CH1_LEN RW length in frames +0xBF5C CH2_LEN RW length in frames +0xBF5D CH3_LEN RW length in frames +0xBF5E-0xBF5F Reserved + +Music System (0xBF60-0xBF74): +Per-channel registers (stride = 5): + ADDR_HI, ADDR_LO, LEN_HI, LEN_LO, CTRL + +Channel 0: +0xBF60 MUSIC_CH0_ADDR_HI +0xBF61 MUSIC_CH0_ADDR_LO +0xBF62 MUSIC_CH0_LEN_HI +0xBF63 MUSIC_CH0_LEN_LO +0xBF64 MUSIC_CH0_CTRL bit0=play | bit1=stop | bit2=loop + +Channel 1: +0xBF65 MUSIC_CH1_ADDR_HI +0xBF66 MUSIC_CH1_ADDR_LO +0xBF67 MUSIC_CH1_LEN_HI +0xBF68 MUSIC_CH1_LEN_LO +0xBF69 MUSIC_CH1_CTRL bit0=play | bit1=stop | bit2=loop + +Channel 2: +0xBF6A MUSIC_CH2_ADDR_HI +0xBF6B MUSIC_CH2_ADDR_LO +0xBF6C MUSIC_CH2_LEN_HI +0xBF6D MUSIC_CH2_LEN_LO +0xBF6E MUSIC_CH2_CTRL bit0=play | bit1=stop | bit2=loop + +Channel 3: +0xBF6F MUSIC_CH3_ADDR_HI +0xBF70 MUSIC_CH3_ADDR_LO +0xBF71 MUSIC_CH3_LEN_HI +0xBF72 MUSIC_CH3_LEN_LO +0xBF73 MUSIC_CH3_CTRL bit0=play | bit1=stop | bit2=loop + +0xBF74 MUSIC_STATUS R bits 0-3: channel active + +SFX System (0xBF90-0xBF95): +0xBF90 SFX_PLAY W trigger SFX by ID +0xBF91 SFX_STOP W stop SFX on channel +0xBF92 SFX_STATUS R bits 0-3: active SFX channels +0xBF93 SFX_TABLE_HI RW SFX table address high byte +0xBF94 SFX_TABLE_LO RW SFX table address low byte +0xBF95 SFX_COUNT RW number of SFX entries + + +SFX DATA FORMAT +--------------- + +SFX entries are 6 bytes each, starting at SFX_TABLE (addr_hi/addr_lo). +SFX_ID selects the entry offset: addr + (SFX_ID * 6). + +Byte layout: + [0] channel (0-3) + [1] freq_lo + [2] freq_hi (bits 0-2) | waveform (bits 4-6) + [3] volume (bits 0-3) + [4] env_ad (attack << 4 | decay) + [5] duration (frames @ 60 Hz) + -Channel 1 - Pulse 2: -0xBF46 CH1_FREQ_LO W frequency bits 0-7 -0xBF47 CH1_FREQ_HI W frequency bits 8-10 -0xBF48 CH1_VOL RW volume & duty (same as CH0) -0xBF49 CH1_CTRL RW enable & trigger - -Channel 2 - Triangle: -0xBF4A CH2_FREQ_LO W frequency bits 0-7 -0xBF4B CH2_FREQ_HI W frequency bits 8-10 -0xBF4C CH2_VOL RW bits 0-3: volume -0xBF4D CH2_CTRL RW bit 0: enable | bit 1: trigger - -Channel 3 - Noise: -0xBF4E CH3_PERIOD W bits 0-3: period (0=high, 15=low) -0xBF4F CH3_VOL RW bits 0-3: volume -0xBF50 CH3_CTRL RW bit 0: enable | bit 1: trigger | bit 2: mode - -Envelopes (AD = attack/decay, SR = sustain/release): -0xBF51 CH0_ENV_AD RW hi nibble: attack | lo nibble: decay -0xBF52 CH0_ENV_SR RW hi nibble: sustain | lo nibble: release -0xBF53 CH1_ENV_AD RW attack/decay -0xBF54 CH1_ENV_SR RW sustain/release -0xBF55 CH2_ENV_AD RW attack/decay -0xBF56 CH2_ENV_SR RW sustain/release -0xBF57 CH3_ENV_AD RW attack/decay -0xBF58 CH3_ENV_SR RW sustain/release - -Sweep + Length: -0xBF59 CH0_SWEEP RW hi nibble: rate | bit 3: dir | bits 0-2: shift -0xBF5A CH1_SWEEP RW sweep (same as CH0) -0xBF5B CH0_LEN RW length in frames (60 Hz) -0xBF5C CH1_LEN RW length in frames -0xBF5D CH2_LEN RW length in frames -0xBF5E CH3_LEN RW length in frames -0xBF5F Reserved - -Channel 4 - Wave: -0xBF60 WAVE_FREQ_LO W frequency bits 0-7 -0xBF61 WAVE_FREQ_HI W frequency bits 8-10 -0xBF62 WAVE_VOL RW bits 0-3: volume -0xBF63 WAVE_CTRL RW bit 0: enable | bit 1: trigger -0xBF64 WAVE_LEN RW length in frames (60 Hz) -0xBF65 WAVE_ENV_AD RW attack/decay -0xBF66 WAVE_ENV_SR RW sustain/release - -Wave RAM (32 x 4-bit samples): -0xBF70-0xBF8F WAVE_RAM RW each byte uses low 4 bits +MUSIC DATA FORMAT +----------------- + +Music tracks are sequences of fixed-size notes in memory. +Each note is 4 bytes, and LEN_HI/LEN_LO define the number of notes. + +Byte layout: + [0] note (0=rest, 1-60=C2-B6) + [1] volume (0-15) + [2] duration (frames @ 60 Hz) + [3] waveform (0-7, same as channel waveform index) + +Noise notes use the note value as a period index (0-15). If note > 15, +the period defaults to a mid value. FREQUENCY @@ -123,17 +160,22 @@ Formula: freq_value = 2048 - (44100 / freq_hz) -NOTE TABLE (A4 = 440 Hz) ------------------------- +NOTE TABLE (C2-B6) +------------------ -Rounded to nearest integer from the formula above. +Music notes map to a fixed 60-entry table (note = 1..60). Note 0 is a rest. +Values below are rounded to nearest integer from the formula above. +Oct 2: C=1374 C#=1412 D=1447 D#=1481 E=1513 F=1543 F#=1571 G=1598 + G#=1623 A=1647 A#=1670 B=1691 Oct 3: C=1711 C#=1730 D=1748 D#=1765 E=1780 F=1795 F#=1810 G=1823 G#=1836 A=1848 A#=1859 B=1869 Oct 4: C=1879 C#=1889 D=1898 D#=1906 E=1914 F=1922 F#=1929 G=1935 G#=1942 A=1948 A#=1953 B=1959 Oct 5: C=1964 C#=1968 D=1973 D#=1977 E=1981 F=1985 F#=1988 G=1992 G#=1995 A=1998 A#=2001 B=2003 +Oct 6: C=2006 C#=2008 D=2010 D#=2013 E=2015 F=2016 F#=2018 G=2020 + G#=2021 A=2023 A#=2024 B=2026 WAVEFORM GENERATION @@ -144,27 +186,46 @@ Phase Accumulator: phase += phase_inc (per sample, wraps at 32-bit) t = phase / 4294967296.0 (normalized 0-1) -Pulse: +Waveforms (waveform index in FREQ_HI bits 4-6): + 0 = Triangle + 1 = Tilted Saw (tsaw) + 2 = Saw + 3 = Square (50% duty) + 4 = Pulse (25% duty) + 5 = Organ (triangle + 2nd harmonic blend) + 6 = Noise (LFSR) + 7 = Phaser (two square waves with phase offset) + +Square/Pulse: sample = (t < duty_threshold) ? 1.0 : -1.0 - duty: 12.5%=0.125, 25%=0.25, 50%=0.5, 75%=0.75 + square duty = 50%, pulse duty = 25% Triangle: if t < 0.25: sample = t * 4 else if t < 0.75: sample = 2 - t * 4 else: sample = t * 4 - 4 +Saw: + sample = t * 2 - 1 + +Tilted Saw (tsaw): + if t < 0.125: sample = t * 8 + else: sample = 1 - (t - 0.125) * (2 / 0.875) + +Organ: + sample = (triangle * 0.7) + (triangle at 2x freq * 0.3) + +Phaser: + sample = 0.5 * (square(t) + square(t + phase_offset)) + Noise (LFSR): Long mode taps: 0,1,3,12 - Short mode taps: 0,6 sample = (lfsr / 32768.0) - 1.0 Noise period table (period 0-15 => divider in samples): [1, 2, 4, 8, 16, 32, 48, 64, 80, 96, 112, 128, 160, 202, 254, 380] - -Wave (CH4): - index = phase >> 27 (32 samples) - v = wave[index] & 0x0F - sample = (v / 7.5) - 1.0 + For channel noise, the period index is freq & 0x0F. + For music noise notes, the period index is (freq >> 7) & 0x0F. MIXING @@ -189,16 +250,8 @@ Attack ramps 0 -> 1.0. Decay ramps 1.0 -> sustain. Sustain holds. Release ramps Enable or trigger starts envelope; disable starts release. -SWEEP (Pulse channels) ----------------------- - -Rate is in units of (sample_rate / 120). Shift is 0-7. -On each sweep tick: delta = freq >> shift. -If direction=down, freq -= delta; otherwise freq += delta. Clamped to 0-2047. - - -EXAMPLE - Play A4 on CH0 ------------------------- +EXAMPLE - Play A4 on CH0 (Square Wave) +-------------------------------------- LOADI R6, 0xBF @@ -212,12 +265,12 @@ EXAMPLE - Play A4 on CH0 LOADI R0, 0x9C STORE R0, [R6:R7] LOADI R7, 0x43 - LOADI R0, 0x07 + LOADI R0, 0x37 ; freq_hi=0x07, waveform=3 (square) STORE R0, [R6:R7] - ; Volume=12, duty=50% + ; Volume=12 LOADI R7, 0x44 - LOADI R0, 0x2C ; duty=10, vol=1100 + LOADI R0, 0x0C STORE R0, [R6:R7] ; Enable channel @@ -231,7 +284,6 @@ RESET STATE All registers = 0x00 (APU disabled, all channels off) LFSR seed = 0xACE1 -Wave RAM default = 0x08 (midpoint) LIMITATIONS @@ -239,4 +291,4 @@ LIMITATIONS - No PCM/samples - Mono only -- 5 channels max +- 4 channels max diff --git a/specs/tiny16-vm.txt b/specs/tiny16-vm.txt index de1a6e6..9c39f13 100644 --- a/specs/tiny16-vm.txt +++ b/specs/tiny16-vm.txt @@ -70,7 +70,7 @@ Layout: 0x9000–0x91FF oam (512 bytes, 128 sprites x 4 bytes) 0x9200–0x923F palette (64 bytes, 4 palettes x 16 colors) 0xA000–0xBEFF stack (grows down, ~7.7KB) -0xBF00–0xBFFF MMIO control registers (256 bytes) +0xBF00–0xBFFF MMIO control registers (4 KB) 0xC000–0xFFFF framebuffer (16,384 bytes = 128×128) @@ -359,13 +359,11 @@ PPU (0xBF30-0xBF3F): - 0xBF33 : PPU_STATUS (R ) PPU status register bit 0: VBLANK (set after render, cleared on read) -TODO: Random (0xBF40-0xBF4F): -- 0xBF30 : RNG_VALUE (R ) read random byte -- 0xBF31 : RNG_SEED_LOW (W ) set seed low byte -- 0xBF32 : RNG_SEED_HIGH (W ) set seed high byte +APU (0xBF40-0xBF9F): +- See specs/tiny16-apu.txt for the full APU register map, SFX, and music formats. -TODO: System (0xBFF0-0xBFFF): -- 0xBFF0 : SYS_CONTROL (RW) system control flags +Reserved (0xBFF0-0xBFFF): +- Reserved for future system control registers. Framebuffer (0xC000-0xFFFF): - Base: 0xC000