A while back, I accidentally came across a (beautiful!) guitar rendition of a C64’s Last Ninja game music, and boy, did that bring back childhood memories.
The MOS6581 – SID was always my favourite sound chip of that generation, and I’ve wanted to build something around it for a long time. Luckily, I had some spare time, so I finally started a project. The plan was simple: get a SID playing on an FPGA over the weekend. Pretty optimistic, as it turned out.
The hardware
I decided to dig out my MiST board for the project. The board has an Altera Cyclone III EP3C25 FPGA on it, an ARM microcontroller that is capable of loading files from an SD card, audio output and even MIDI. Perfect for this project.

Borrowing a SID
The SID is a genuinely hard chip to model – analog filters, a non-linear DAC, quirky combined waveforms … People smarter than me have spent years getting it right. So I didn’t write my own, but lifted the SID core from the C64_MiSTer project. The SID implementation in the C64_MiSTer project comes from the reDIP-SID, which seems to be modelled after the reSID software emulator.
The gold standard for the software SID implementations is apparently the reSIDfp, which is shipped inside libsidplayfp. I wanted to test how close the MiSTer’s SID is to the reSIDfp, so I wrapped reSIDfp in a tiny C shim, bolted it to my CocoTB testbench, and built an oracle: feed the same stream of register writes into both the RTL and reSIDfp, then compare the audio.
Here’s the scorecard:
| What | Match to reSIDfp |
|---|---|
| Triangle / saw / pulse / noise | bit-exact |
| Combined waveforms | match (same ROM-baked quirks) |
| 8580 overall | within ±4% |
| 6581 overall | ~18% quieter |
| Filter engaged | close, but not exact |
That ~18% on the 6581 had me worried for a while, until I tracked it down: it’s entirely reSIDfp’s cubic DAC saturation — a soft-knee 1.1·v − 0.11·v³ curve that models the NMOS output buffers. The MiSTer core doesn’t model it; reSIDfp does. Swept across all 4096 DAC codes, the two R-2R ladder models otherwise agree to 0.01%. Neither is “wrong” — they just emphasise different bits of the analog chip. Good enough.

So: found some issues. Nothing major. Onward.
Fighting the toolchain (round 1: Quartus)
Cyclone III requires Altera Quartus, and the last version that supports this FPGA is 13 years old. I had 13.0sp1 installed and ran the build.
quartus_fit crashed. Not “your design is too big” — it placed everything using 2% of the interconnect and then died at the very start of routing with the very unhelpful “Current module quartus_fit ended unexpectedly.” Every single time the SID core was in the design. Take it out, builds fine. Put it back, instant death.
After far too long, I figured out that neither Quartus 13.0 nor 13.1 ran fine natively on my system, but using docker I finally managed to compile the design, after one more trap – the 32-bit quartus_sh runs out of memory (>2 GB) just elaborating the SID’s wave tables. Pass --64bit and it’s fine. Final builds: ~26 seconds, 12% of the chip.
I also tried lowering the SystemVerilog to plain Verilog with sv2v to dodge the whole mess. The conversion ran clean, quartus_map started happily… and then ballooned to 28 GB of RAM before I killed it. Turns out sv2v expands those ~10 kB of wave-table array literals into one gigantic single-line initialiser, and Quartus’s mapper does not cope. sv2v is great — just not for ROM-heavy RTL. Lesson learned.
Fighting the toolchain (round 2: Verilator)
For simulation, I use Verilator + CocoTB, and Verilator is much stricter than Quartus about SystemVerilog. Importing the MiSTer code took three recurring fixes:
- function ports declared
wire signed→input signed(thewireform is technically illegal; Quartus just shrugs and accepts it). - a few
wirearrays that are procedurally assigned → change toreg. - …and the fun one.
The third was a reg declared inside an always block with an initialiser:
always @(posedge clk) begin
reg signed [...] vd = 0; // <-- looks innocent
...
end
Quartus treats that as a one-time power-on init. Verilator treats it as an automatic variable and re-runs the init every clock, so vd was being zeroed between every write and read. The result: the filter output silently went dead. No error, no warning, just silence where there should’ve been sound. Hoisting the declaration up to module scope fixed it. That one took a while to figure out.
“Just play a SID file”
With the chip working, I figured I’d point it at a .sid file and call it a day. This is where I learned something I probably should’ve known: a .sid file isn’t music data — it’s a 6502 program.
Inside a .sid is the actual machine code of the tune’s player routine, which runs ~50 times a second (or even faster) and pokes the SID registers itself. To play one, you don’t need a SID — you need a C64: a 6510 CPU, 64 KB of RAM, CIA timers, the lot. Which is a fine project, and entirely not the one I wanted to do this weekend.
So I needed something else.
The VGM detour (or: don’t trust a confident robot)
Quick search. I asked Gemini, and it told me — with total confidence — that the VGM chiptune format natively supports the C64 SID, command byte 0xD4, just parse that and you’re done.
I spent a couple of hours building a VGM parser around this.
Well … VGM does not support the SID. There is no SID command, no SID clock field, nothing. 0xD4 is the Namco C140. The thing that confused me (and the robot) is a tool called sid2vgm, which doesn’t dump SID registers at all — it re-synthesises the SID’s output through a Yamaha OPL3 chip. It plays back on an OPL3. Completely useless for driving a real SID core.
A couple of wasted hours, but the parser FSM I’d written was salvageable, I just needed a format worth pointing it at.
Rolling my own: .sidraw
The trick I landed on: don’t run the 6502 on the FPGA — run it once, on my PC. libsidplayfp already emulates the whole C64 perfectly. So I wrote a small C++ tool, sidraw-dump, that loads a .sid file, runs the player through libsidplayfp, and intercepts every SID register write as it happens. Those writes plus their timing get serialised into a tight little binary format I called .sidraw — a 16-byte header and a stream of “write this register” / “wait N ticks” opcodes. The FPGA never has to know what a 6502 is; it just replays the writes.
One wrinkle made it more interesting: getting the timing right. libsidplayfp’s playback loop advances by scheduler events, not CPU cycles, and sometimes several events land on the same cycle (CPU + CIA + video all firing on an interrupt). Counting play() calls drifted from real cycles, which you could hear — the cycle-accurate dumps wandered ±100 ms over a tune. I added a three-line patch to libsidplayfp exposing a currentCycle() accessor (the scheduler’s true 1 MHz clock) and timestamped every write against that. Drift gone — a steady −2 ms across the whole song.
On the FPGA side, the salvaged parser drives a SID core over its register bus. For the first bring-up I baked a tune straight into block RAM — Monty on the Run, 24 seconds, cycle-accurate, sitting in 32 M9K blocks. It worked … sort of, but there were some quirks:

While it sounded sort of right, the waveform looked like it was passed through a strong high-pass filter, and there was a steady ~824 Hz tone haunting the silence between notes. After painstakingly checking the Verilog code, trying sv2v, simulating, compiling, checking the Quartus outputs, I was convinced it had something to do with the MiST’s analog filter or the DC blocking caps.

That wasn’t it. It turned out to be a high-pass filter in my audio interface’s line input (a Behringer UMC204HD) differentiating the SID’s near-DC idle output, leaving 824 Hz as the dominant residual. The synth was fine all along. Lesson: validate your measurement chain before you debug the thing you’re measuring.
Finally, after connecting it to another input, I got my first proper SID playback. Success!
Monty on the Run in .sidraw format, played from embedded FPGA RAM on the MiST board:
Oh, by the way … it’s also a MIDI synth!
The SID player was just a fun detour and a great way to test the SID implementation. The plan was actually to create a MIDI-controlled SID synthesizer.
I’m currently working on a different synth project, and I already have all the necessary plumbing for a MIDI synth written: a UART, a MIDI parser, channel routing, voice control, note-to-frequency lookup, DC blocking filter, SDM DAC. The SID is, after all, just a synth voice. So I plugged my MIDI front-end straight into it.
MIDI IN → UART → midi_parser → ch_msg_router → voice_ctrl → sid_driver → SID → audio
And because the .sidraw player drives its own dedicated SID instance, the two paths happily coexist — a tune can be playing while you play over the top of it. Both SIDs get mixed, DC-blocked (the SID core produces a ~4000-LSB DC bias that would otherwise eat a large chunk of the DAC range), and pushed out through a sigma-delta DAC to the board’s 1-bit audio pin.
So, plug a MIDI keyboard and some speakers into a MiST board, and you’ve got a standalone SID synth. Currently, it’s monophonic and there’s no MIDI CC control yet, but I’m working on it.
Next steps
- Polyphony. The whole architecture is built around giving each note its own entire SID (all three oscillators, its own filter) — four of them fit comfortably on this chip, eight with a bit of time-multiplexing. That’s the fun part and it’s next.
- MIDI CC. Cutoff, resonance, vibrato, the works — mapped to real knobs.
- A better filter. Tier 1 of the filter plan (free!) to get the cutoff curve tracking reSIDfp.
- Loading tunes off an SD card instead of baking them into the bitstream, with an on-screen menu — already working in simulation, hardware bring-up pending.
- MiST menu integration. This one is tricky – I don’t have a display with VGA, my VGA2HDMI doesn’t want to work, and my Startech USB3HDCAP doesn’t like Linux, so I will have to figure something out.
Check out the source files and the FPGA programming files on my Github page:
github.com/rkrajnc/sidsynth-mist
Stay tuned for the next post!


Be First to Comment