Skip to content

rrrh/tiny_sid_chip

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

302 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Triple SID Voice Synthesizer (TT-IHP)

Three time-multiplexed SID voices with 8-bit ADSR envelopes, sync and ring modulation, controlled via a flat parallel register interface, with 8-bit PWM audio output. Designed for a Tiny Tapeout 1x2 tile on the IHP SG13G2 130nm process at 24 MHz.

View the GDS layout


Table of Contents

  1. Overview
  2. Architecture
  3. Pin Mapping
  4. Register Reference
  5. Write Interface Protocol
  6. Audio Output and PWM
  7. Audio Recovery Filter
  8. Usage Guide
  9. Design Constraints

Overview

This design implements a triple-voice sound synthesizer inspired by the MOS 6581/8580 SID chip from the Commodore 64. Three independent voices share a single compute pipeline via time-multiplexing, each providing four classic waveform types (sawtooth, triangle, pulse, noise), a full 8-bit ADSR amplitude envelope with exponential decay, hard sync, and ring modulation in a Tiny Tapeout 1x2 tile.

A host microcontroller (Arduino, RP2040, ESP32, etc.) writes per-voice control registers through a simple flat parallel interface using 8-bit data and a rising-edge write strobe. The three voice outputs are mixed and output as an 8-bit PWM signal on uo_out[0] at ~94.1 kHz, requiring only a passive RC low-pass filter to produce analog audio.

Filters

I did not get the filters to work as analog macros. So: no filters.

Key Features

  • Three independent voices, time-multiplexed through one shared pipeline
  • Four waveforms per voice: sawtooth, triangle, pulse (variable width), noise
  • AND-combining of simultaneous waveforms (SID-compatible)
  • Hard sync modulation (circular: V0←V2, V1←V0, V2←V1)
  • Ring modulation (XOR sync source MSB into triangle)
  • 8-bit ADSR envelope per voice (256 amplitude levels, piecewise exponential decay)
  • Per-voice ADSR parameters (attack, decay, sustain, release)
  • 4-state envelope FSM (IDLE, ATTACK, DECAY, SUSTAIN) + releasing flag
  • 16 SID-accurate envelope rate settings from ~2.3 ms to ~8 s full traverse (matching MOS 6581/8580)
  • SID-compatible sustain mapping (nibble duplication: N → 0xNN)
  • 16-bit frequency register, 24-bit phase accumulator (~0.06 Hz resolution, matching original C64 SID)
  • 15-bit LFSR noise generator, accumulator-clocked from voice 0
  • 3-voice mixer with 10-bit accumulator and ÷4 scaling
  • 4-bit volume
  • Single 8-bit PWM audio output on uo_out[0] (~94.1 kHz carrier at 24 MHz)
  • Flat parallel write interface
  • Mod-6 pipeline: 1 MHz effective per voice at 6 MHz voice clock (24 MHz ÷4)

Source Files

File Description
src/tt_um_sid.v Top-level: register banks, voice pipeline, mixer, filter, pin mapping
src/pwm_audio.v 8-bit PWM audio output (255-clock period)

Architecture

Signal flow:

  1. The host writes per-voice registers (frequency, pulse width, waveform, attack/decay, sustain/release) via the flat parallel interface. All registers are per-voice. A rising edge on ui_in[7] latches the data.

  2. A ÷4 clock divider produces a 6 MHz clock enable from the 24 MHz system clock. A mod-6 slot counter (gated by the 6 MHz enable) cycles through 6 slots. Slots 0-2 compute voices 0-2 respectively, slot 4 latches the mixer output and preloads voice 0's pipeline registers, and slots 3 and 5 are idle/preload slots. Each voice is updated once per 6-slot frame (1 MHz effective per voice).

  3. Each voice's 24-bit phase accumulator advances by the 16-bit frequency register value (zero-extended to 24 bits) every frame. This provides ~0.06 Hz frequency resolution, matching the original C64 SID accumulator geometry. Hard sync resets the accumulator when the sync source voice's MSB has a rising edge.

  4. The waveform generator derives sawtooth, triangle, pulse, and noise outputs from the accumulator state and a shared 15-bit LFSR (clocked from voice 0's accumulator bit 19). Selected waveforms are AND-combined into an 8-bit value. Ring modulation XORs the sync source's accumulator MSB into the triangle waveform's fold bit.

  5. The 8-bit waveform is multiplied by the 8-bit ADSR envelope to produce a 16-bit product; the upper 8 bits are taken as the voice output.

  6. The mixer accumulates three 8-bit voice outputs into a 10-bit accumulator over 3 slots, then divides by 4 (right-shift by 2) to produce an 8-bit mix sample for the PWM module.

  7. pwm_audio converts the filtered sample into a PWM signal at ~94.1 kHz (running at full 24 MHz). An external RC low-pass filter recovers analog audio.


Pin Mapping

Input Pins (ui_in)

Pin Signal Description
ui_in[4:0] reg_addr Register address (0--24)
ui_in[6:5] -- Unused
ui_in[7] wr_en Write enable (rising-edge triggered)

Data Input Pins (uio_in)

Pin Signal Description
uio_in[7:0] wr_data 8-bit write data. All 8 pins are inputs.

Output Pins (uo_out)

Pin Signal Description
uo_out[7] pwm_out PWM audio output (filtered or bypass). Connect to RC filter.
uo_out[6:0] -- Tied low.

Bidirectional Pin Direction

All uio pins are configured as inputs (uio_oe = 0x00).


Register Reference

Seven registers per voice (voice_sel 0--2), plus four filter/volume registers (voice_sel 3).

Register 0: Frequency Low Byte

Bit:   7    6    5    4    3    2    1    0
     [              freq_lo[7:0]              ]

Register 1: Frequency High Byte

Bit:   7    6    5    4    3    2    1    0
     [              freq_hi[7:0]              ]

The 16-bit frequency register {freq_hi, freq_lo} is zero-extended and added to the 24-bit phase accumulator each voice cycle (1 MHz effective rate, 24 MHz ÷4 ÷6 slots). The oscillator frequency is:

f_out = freq_reg × 1,000,000 / 16,777,216  ≈  freq_reg × 0.0596 Hz

Frequency calculation:

freq_reg = round(desired_Hz × 16,777,216 / 1,000,000)
         ≈ desired_Hz × 16.777

Resolution: ~0.06 Hz. Range: 0.06 Hz (reg=1) to ~3906 Hz (reg=65535). This matches the original C64 SID accumulator geometry (24-bit acc, 16-bit freq reg). Higher audio frequencies are produced as harmonics of the waveform generators.

freq_reg freq_hi freq_lo Output Frequency Note
0 0x00 0x00 0 Hz Silence
1 0x00 0x01 ~0.06 Hz Lowest pitch
4396 0x11 0x2C ~262 Hz ~C4
7382 0x1C 0xD6 ~440 Hz ~A4
35112 0x89 0x28 ~2,093 Hz ~C7
65535 0xFF 0xFF ~3,906 Hz Maximum fundamental

Register 2: Pulse Width Low Byte

Bit:   7    6    5    4    3    2    1    0
     [              pw_lo[7:0]                ]

Register 3: Pulse Width High Nibble

Bit:   7    6    5    4    3    2    1    0
     [    (unused)       ][ pw_hi[3:0]        ]

The 12-bit pulse width {pw_hi, pw_lo} sets the pulse waveform duty cycle by comparison with the accumulator upper 12 bits (acc[23:12] >= pw):

  • pw = 0x000: Pulse always high (near DC)
  • pw = 0x800: ~50% duty cycle (square wave)
  • pw = 0xFFF: Narrow pulse (~0.02% duty)

Register 4: Waveform Control (8-bit, per-voice)

Bit:   7      6      5        4        3     2     1     0
     [noise][pulse][sawtooth][triangle][test][ring][sync][gate]
Bit Name Description
0 gate Set to 1 to start a note (attack). Clear to 0 to release.
1 sync Hard sync: resets phase accumulator on sync source voice MSB rising edge. Circular routing: V0←V2, V1←V0, V2←V1.
2 ring Ring modulation: XORs sync source voice accumulator MSB into triangle waveform fold bit, producing bell-like timbres.
3 test Forces oscillator accumulator to 0 while held.
4 triangle Enable triangle waveform.
5 sawtooth Enable sawtooth waveform.
6 pulse Enable pulse waveform (duty cycle set by reg 2).
7 noise Enable noise waveform (shared 15-bit LFSR, accumulator-clocked from voice 0).

When multiple waveform bits are set, their outputs are bitwise AND-combined (starting from 0xFF, each enabled waveform ANDs its value). This matches the real SID's behavior where simultaneous waveforms produce a bitwise AND of their individual outputs.

Register 5: Attack / Decay Rates (8-bit, per-voice)

Bit:   7    6    5    4    3    2    1    0
     [  decay_rate[3:0]  ][ attack_rate[3:0] ]
Field Bits Description
attack_rate [3:0] How fast the envelope rises from 0 to 255
decay_rate [7:4] How fast the envelope falls from 255 to sustain level (with exponential decay)

Register 6: Sustain Level / Release Rate (8-bit, per-voice)

Bit:   7    6    5    4    3    2    1    0
     [ release_rate[3:0] ][sustain_level[3:0]]
Field Bits Description
sustain_level [3:0] Sustain amplitude (0--15). The 8-bit envelope holds at {sustain_level, sustain_level} (nibble duplication: 0x00, 0x11, ..., 0xFF), matching the original SID.
release_rate [7:4] How fast the envelope falls to 0 after gate off (with exponential decay)

Filter Registers

The filter is not implemented. Only the volume is present.

Register 3 (addr 0x1B): Mode / Volume

Bit:   7    6    5    4    3    2    1    0
     [V3OFF][HP ][BP ][LP ][  filt_vol[3:0]   ]
Field Bits Description
filt_vol [3:0] Master volume (0--15). Post-ADC digital shift-add
[7:4] unused

Envelope Rate Table

The ADSR uses per-voice 15-bit rate counters matching the original MOS 6581/8580 SID. Each of the 16 rate settings selects a non-power-of-2 period from a lookup table. The rate counter decrements each voice cycle (1 MHz); when it reaches zero, a rate tick fires and the counter reloads. The 8-bit envelope steps by ±1 per tick (attack) or per tick gated by the exponential counter (decay/release), so a full linear 0→255 traverse takes 256 rate ticks.

Rate Period (cycles) Full Traverse (256 ticks) SID Equivalent
0 9 ~2.3 ms 2 ms Attack
1 32 ~8.2 ms 8 ms Attack
2 63 ~16.1 ms 16 ms Attack
3 95 ~24.3 ms 24 ms Attack
4 149 ~38.1 ms 38 ms Attack
5 220 ~56.3 ms 56 ms Attack
6 267 ~68.4 ms 68 ms Attack
7 313 ~80.1 ms 80 ms Attack
8 392 ~100 ms 100 ms Attack
9 977 ~250 ms 250 ms Attack
10 1,954 ~500 ms 500 ms Attack
11 3,126 ~800 ms 800 ms Attack
12 3,907 ~1.0 s 1 s Attack
13 11,720 ~3.0 s 3 s Attack
14 19,532 ~5.0 s 5 s Attack
15 31,251 ~8.0 s 8 s Attack

Formula: traverse_time = 256 × period / 1,000,000 seconds.

These are the same 16 period values used by the real MOS 6581/8580 SID, providing the full range from 2 ms to 8 s for attack and (with exponential counter) 6 ms to 24 s for decay/release.

Exponential Decay

During decay and release, a secondary 5-bit exponential counter acts as a post-divider on the rate counter ticks, matching the original SID behavior. The exponential counter decrements on each rate tick; an envelope step only occurs when both the rate counter and exponential counter reach zero. The exponential counter reloads from a breakpoint-based period table:

Envelope ≥ Expo period Effective divisor
93 1 ×1 (full speed)
54 2 ×2
26 4 ×4
14 8 ×8
6 16 ×16
< 6 30 ×30

Attack bypasses the exponential counter entirely (always linear). The breakpoints and divisors match the original MOS 6581/8580 SID, including the ×30 slowdown at very low envelope values (env < 6) that the original implements via its exponential counter.

Sustain Level Mapping

The 4-bit sustain register maps to an 8-bit level by nibble duplication, matching the original SID:

Sustain (4-bit) Envelope (8-bit) Sustain (4-bit) Envelope (8-bit)
0 0x00 (0) 8 0x88 (136)
1 0x11 (17) 9 0x99 (153)
2 0x22 (34) 10 0xAA (170)
3 0x33 (51) 11 0xBB (187)
4 0x44 (68) 12 0xCC (204)
5 0x55 (85) 13 0xDD (221)
6 0x66 (102) 14 0xEE (238)
7 0x77 (119) 15 0xFF (255)

ADSR Envelope FSM

The envelope uses a 4-state FSM with a separate releasing flag:

         gate ON              env=255           env ≤ sustain
  IDLE ──────────► ATTACK ──────────► DECAY ──────────► SUSTAIN
   ▲                  │                 │                  │
   │                  │ gate OFF        │ gate OFF         │ gate OFF
   │                  ▼                 ▼                  ▼
   └──── env=0 ◄── RELEASING ◄──────────────────────────────
  • IDLE: Envelope is 0. Waits for gate rising edge to transition to ATTACK.
  • ATTACK: Envelope increments toward 255 at the attack rate (linear). Transitions to DECAY when envelope reaches 255.
  • DECAY: Envelope decrements toward sustain level with exponential decay. Transitions to SUSTAIN when envelope reaches {sustain_level, sustain_level}.
  • SUSTAIN: Envelope holds at sustain level.
  • RELEASING: Gate off sets the releasing flag from any active state. Envelope decrements toward 0 with exponential decay. When envelope reaches 0, transitions to IDLE.

Comparison with original SID: The original uses 3 states (ATTACK, DECAY_SUSTAIN, RELEASE) without a separate IDLE state. Our 4-state FSM with an explicit releasing flag is functionally equivalent but avoids the SID's ADSR delay bug, where retriggering can cause the envelope to stall for up to ~33 ms when the new rate counter period is less than the current counter value.


Write Interface Protocol

The register interface uses a simple parallel bus with rising-edge write strobe. No SPI or I2C protocol is needed.

Write Sequence

To write one register:

  1. Set ui_in[2:0] = register address (0-6)
  2. Set ui_in[4:3] = voice select (0, 1, or 2)
  3. Set uio_in[7:0] = data byte
  4. Pulse ui_in[7] high for at least one clock cycle
  5. Return ui_in[7] low before the next write

The register latches on the rising edge of ui_in[7]. The minimum write cycle is 3 clock cycles (125 ns at 24 MHz): one to set up address/data, one with WE high, one with WE low.

Timing Diagram

            ┌───────┐                         ┌───────┐
  ui_in[7]  │       │                         │       │
 ───────────┘       └─────────────────────────┘       └─────
                ^                                 ^
          write latched                     write latched

  ui_in[4:0]  <  voice | addr  >             < voice | addr  >

  uio_in      <    data byte   >             <  data byte    >

Write Function (C / Arduino)

// Write an 8-bit value to a SID register
// addr: 0-6  voice: 0-3 (3=filter)  data: 0-255
void sid_write(uint8_t addr, uint8_t data, uint8_t voice) {
    uint8_t ui = (addr & 0x07) | ((voice & 0x03) << 3);
    set_ui_in(ui);           // address + voice, WE=0
    set_uio_in(data);        // data byte
    set_ui_in(ui | 0x80);    // assert WE (rising edge triggers write)
    set_ui_in(ui);           // deassert WE
}

Audio Output and PWM

How It Works

The pwm_audio module converts the 8-bit mixer output into a pulse-width modulated signal. A free-running 8-bit counter cycles from 0 to 254 (period = 255 clocks). The output is high when the counter is less than the sample value:

pwm_out = (count < sample) ? 1 : 0

Signal Characteristics

Parameter Value
Input resolution 8 bits (unsigned, 0--255)
Output 1-bit PWM on uo_out[0]
PWM period 255 clocks
PWM frequency @ 24 MHz ~94.1 kHz
Duty cycle range 0% (sample=0) to 100% (sample=255)
Audio bandwidth Up to ~23.5 kHz (Nyquist)

Audio Recovery Filter

The PWM output on uo_out[0] swings between 0 and VDD. A second-order passive RC low-pass filter recovers the analog audio signal. The ~94.1 kHz PWM carrier is well above the 20 kHz audio band.

Recommended Circuit

uo_out[0] ---[R1]---+---[R2]---+---[Cac]---> Audio Out
                     |          |
                    [C1]       [C2]
                     |          |
                    GND        GND

Component Values

R (per stage) C (per stage) Per-stage fc Combined rolloff @ 94 kHz
3.3 kΩ 2.2 nF ~22 kHz -12 dB/oct (2nd order) ~-25 dB
  • Cac = 1 µF ceramic -- DC blocking capacitor after the filter.
  • For driving low-impedance loads (headphones), add a unity-gain op-amp buffer after the filter.
  • A third-order (three-stage, same values) filter can be added for better carrier rejection.

Usage Guide

Minimal Wiring

MCU                    TT Chip                   Audio
-----------           ----------------          -------
GPIO (D0)   --------> ui_in[0]  addr[0]
GPIO (D1)   --------> ui_in[1]  addr[1]
GPIO (D2)   --------> ui_in[2]  addr[2]
GPIO (D3)   --------> ui_in[3]  voice[0]
GPIO (D4)   --------> ui_in[4]  voice[1]
GPIO (WE)   --------> ui_in[7]  write enable
GPIO (D5-12)--------> uio_in[7:0]  data bus
                       uo_out[0] --[3.3k]--+--[3.3k]--+--[1uF]--> amp
                                           |          |
                                        [2.2nF]    [2.2nF]
                                           |          |
                                          GND        GND

Playing a Note (Voice 0, Sawtooth ~440 Hz)

// freq_reg = round(440 * 16777216 / 1000000) = 7382 (0x1CD6)
sid_write(0, 0xD6, 0);  // freq_lo
sid_write(1, 0x1C, 0);  // freq_hi
sid_write(4, 0x00, 0);  // attack=0 (fastest), decay=0
sid_write(5, 0x0F, 0);  // sustain=15 (max), release=0
sid_write(6, 0x21, 0);  // sawtooth + gate ON

delay(500);              // hold note for 500 ms

sid_write(6, 0x20, 0);  // gate OFF (release begins)

Three-Voice Chord (C Major)

// C4 ≈ 262 Hz → freq_reg = round(262 * 16777216 / 1000000) = 4396 (0x112C)
sid_write(0, 0x2C, 0); sid_write(1, 0x11, 0);
sid_write(4, 0x00, 0); sid_write(5, 0x0F, 0);
sid_write(6, 0x21, 0);  // Voice 0: sawtooth C4

// E4 ≈ 330 Hz → freq_reg = round(330 * 16777216 / 1000000) = 5536 (0x15A0)
sid_write(0, 0xA0, 1); sid_write(1, 0x15, 1);
sid_write(4, 0x00, 1); sid_write(5, 0x0F, 1);
sid_write(6, 0x11, 1);  // Voice 1: triangle E4

// G4 ≈ 392 Hz → freq_reg = round(392 * 16777216 / 1000000) = 6577 (0x19B1)
sid_write(0, 0xB1, 2); sid_write(1, 0x19, 2);
sid_write(2, 0x00, 2); sid_write(3, 0x08, 2);  // pw = 0x800 (50%)
sid_write(4, 0x00, 2); sid_write(5, 0x0F, 2);
sid_write(6, 0x41, 2);  // Voice 2: pulse G4

Sound Recipes

Simple Square Wave (8-bit Game Style)

sid_write(0, freq_reg & 0xFF, v);
sid_write(1, freq_reg >> 8, v);
sid_write(2, 0x00, v); sid_write(3, 0x08, v);  // pw = 0x800, 50% duty
sid_write(4, 0x00, v);    // instant attack/decay
sid_write(5, 0x0F, v);    // max sustain, instant release
sid_write(6, 0x41, v);    // pulse + gate

Drum Hit (Noise with Fast Decay)

sid_write(0, 0xFF, v);    // high freq noise
sid_write(4, 0xA0, v);    // instant attack, rate-10 decay
sid_write(5, 0xA0, v);    // sustain=0, rate-10 release
sid_write(6, 0x81, v);    // noise + gate
// Naturally decays to silence

Pad (Triangle with Slow Envelope)

sid_write(0, freq_reg & 0xFF, v);
sid_write(1, freq_reg >> 8, v);
sid_write(4, 0x8A, v);    // attack=10 (~524ms), decay=8 (~524ms)
sid_write(5, 0x8C, v);    // sustain=12, release=8
sid_write(6, 0x11, v);    // triangle + gate

Bell (Ring Modulation)

// Voice 0: modulator at higher frequency
// 931 Hz → freq_reg = round(931 * 16777216 / 1000000) = 15619 (0x3D03)
sid_write(0, 0x03, 0); sid_write(1, 0x3D, 0);  // ~931 Hz modulator
sid_write(6, 0x11, 0);   // triangle + gate (modulator)

// Voice 1: carrier with ring mod enabled
// C4 ≈ 262 Hz → freq_reg = 4396 (0x112C)
sid_write(0, 0x2C, 1); sid_write(1, 0x11, 1);  // ~262 Hz carrier (C4)
sid_write(4, 0x50, 1);   // instant attack, rate-5 decay
sid_write(5, 0x54, 1);   // sustain=4, rate-5 release
sid_write(6, 0x15, 1);   // triangle + ring + gate

Frequency Table (Equal Temperament, A4=440 Hz)

freq_reg = round(Hz × 16,777,216 / 1,000,000)
Note Hz freq_reg freq_hi freq_lo
C3 130.8 2194 0x08 0x92
D3 146.8 2463 0x09 0x9F
E3 164.8 2765 0x0A 0xCD
G3 196.0 3289 0x0C 0xD9
A3 220.0 3691 0x0E 0x6B
C4 261.6 4389 0x11 0x25
E4 329.6 5530 0x15 0x9A
G4 392.0 6577 0x19 0xB1
A4 440.0 7382 0x1C 0xD6
C5 523.3 8779 0x22 0x4B
A5 880.0 14764 0x39 0xAC
C6 1046.5 17558 0x44 0x96
C7 2093.0 35112 0x89 0x28

Reset and Initialization

After power-on or chip reset (rst_n asserted low), all registers are cleared to zero. All voices are silent. No initialization sequence is required.

To silence the output at any time:

  • Clear the gate bit: sid_write(6, waveform & 0xFE, voice)
  • Set frequency to 0: sid_write(0, 0, v)
  • Set the test bit: sid_write(6, 0x08, v)

Design Constraints

Parameter Value
Target technology IHP SG13G2 130nm SiGe BiCMOS
Tile size Tiny Tapeout 1x2
Core supply (VDD) 1.2V
I/O supply (VDDIO) 3.3V
System clock 24 MHz (41.7 ns period)
Voice pipeline clock 6 MHz (÷4 clock enable)
Pipeline Mod-6 slot counter (1 MHz effective per voice)
Core utilization ~77% (with CTS clock tree)
Voice count 3 (time-multiplexed)
Frequency resolution ~0.06 Hz (16-bit freq, 24-bit acc, 1 MHz effective)
Envelope depth 8-bit (256 levels, exponential decay)
ADSR rate counters 15-bit per-voice (16 SID-accurate periods, 5-bit exponential counter)
Noise generator 15-bit LFSR, accumulator-clocked from voice 0
Voice output 8-bit (8×8 waveform×envelope product, upper byte)
PWM output frequency ~94.1 kHz
Audio bandwidth Up to ~47 kHz (Nyquist)
Write interface speed 1 register per 125 ns (3 clocks)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors