A Commodore 64 diagnostic utility that identifies 24+ variants of the SID (Sound Interface Device) chip — including real hardware, FPGA clones, microcontroller emulators, and PC emulators.
Original release: https://csdb.dk/release/?id=176909 Author: funfun/triangle 3532 Syntax: KickAssembler (converted from ACME original)
STORY.md — Full technical article: detection methods, reverse-engineered protocols, SIDFX secondary probing, self-modifying code patterns, hardware testing methodology, and lessons learned. For sceners, SID musicians, and hardware developers.
Representative photos of the chips and boards the detector is validated against — see pictures/ for the full set.
Reference chip collection: 6581 R2 / R3 / R4AR and 8580 R5 silicon on foam.
C64 with FM-YAM cartridge, Commodore FM Sound Module (SFX Sound Expander), and Ultimate II+ — used to verify the V1.4.x OPL $DF40/$DF50/$DF60 detection path.
When run on a C64 (or emulator), the program probes SID hardware registers, measures timing characteristics, and displays the results on screen. It detects:
| Category | Chips / Variants |
|---|---|
| Real chips | 6581 R2, R3, R4, R4AR · 8580 |
| FPGA clones | FPGASID (6581 mode · 8580 mode) |
| Microcontroller | ARMSID · ARM2SID · Swinsid Ultimate · Swinsid Nano · Swinsid Micro · SIDKick-pico · KungFuSID · BackSID · PD SID · SIDFX · ULTISID (U64) · uSID64 |
| Emulators | VICE 3.3 ResID 6581/8580 · VICE 3.3 FastSID · HOXS64 · Frodo · YACE64 · EMU64 · C64DBG |
| FM expansion | CBM SFX Sound Expander · FM-YAM (Yamaha OPL2 / YM3812 at $DF40) |
| MIDI interfaces | Sequential Circuits · Namesoft · DATEL/Siel/JMS · Passport · Maplin (6850 ACIA) |
| Machine type | C64 · C128 · TC64 (Turbo Chameleon 64) |
| Clock | PAL · NTSC |
| Stereo | Scans D400/D500/D600/D700/DE00–DFFF for additional SID chips |
| Info pages | Press I on the result screen for per-chip detail; CRSR LEFT/RIGHT to flip pages |
| Quality page | Press Q for a per-slot audio-quality fingerprint (sidcheck grade + $D418 decay) — one row per detected SID |
| Key | Action |
|---|---|
| SPACE | Restart the full detection sequence (raster-stable; silences all voices) |
| I | Per-chip info pages (CRSR LEFT/RIGHT or B/M to flip; SPACE returns) |
| Q | Quality Fingerprint page (sidcheck grade + $D418 decay per slot) |
| D | Debug page — raw detection values, UCI/SIDFX state (D again → page 2) |
| R | README / help viewer (W/S to scroll) |
| T | SID sound test — plays the 3-voice melody on every detected slot |
| P | SID Tracker View — live per-voice display while music plays |
| L | TLR sid-detect2 second-SID detector (copied to $0801 and run) |
The program runs a sequential detection pipeline at startup. Each step either identifies the chip and jumps to the result display, or falls through to the next step.
Communicates with the SIDFX cartridge using its SCI serial protocol over the D41E/D41F register pins. Sends a "PNP" login packet ($80 $50 $4E $50) and reads back a 4-byte vendor/product ID. Checks for the expected signature $45 $4C $12 $58.
data1 = $30→ SIDFX founddata1 = $31→ SIDFX not present
When SIDFX is found, D41D (SW2/SW1/PLY) and D41E (SID1/SID2 types) are saved. After detection, sidfx_populate_sid_list populates sid_list: slot 1 = D400 with SID1 type, slot 2 = secondary address (D420/D500/DE00 per SW1 bits 5:4) with SID2 type. Before writing the secondary type, sfx_probe_skpico probes the secondary for SIDKick Pico identity: enters config mode by writing $FF to secondary+$1F, then reads VERSION_STR[0]='S' and [1]='K' at secondary+$1D — if confirmed, the slot type becomes $0B/$0E (SIDKick Pico 8580/6581) instead of the generic 6581/8580 from SIDFX D41E.
Writes the ASCII string "DIS" to voice-3 registers (D41D/D41E/D41F), waits, then reads back D41B/D41C/D41D. Real SID chips do not echo writes; clone chips do:
| Echo in D41B | Meaning |
|---|---|
'S' = $53 |
Swinsid Ultimate |
'N' = $4E |
ARMSID family |
For ARMSID, D41C and D41D further discriminate:
- D41C =
'O'($4F) and D41D ='R'($53) → ARM2SID - D41C =
'O'($4F) and D41D ≠'R'→ ARMSID
Self-modifying code:
Checkarmsidpatches the high-byte of all its SID register addresses at runtime (e.g.cas_d418+2,cas_d41D+2) so the same routine works for both D400 and D500 (stereo second-SID scan). This is the most complex routine in the codebase.
These three checks run in sequence after ARMSID. All use D41D–D41F register probe protocols.
PD SID (checkpdsid): Writes 'P' to D41D, 'D' to D41E, reads D41E back — expects 'S'. data1 = $09.
BackSID (checkbacksid): Writes $42 to D41C, $B5 to D41D, $1D to D41E, reads D41F — expects $42 echoed. data1 = $0A.
SIDKick-pico (checkskpico): Enters config mode by writing $FF to D41F. The VERSION_STR pointer is manual — write $E0+i to D41E to select byte i, then read D41D. No auto-increment on reads.
| D41E write | D41D read | Meaning |
|---|---|---|
$E0 |
$53 = 'S' |
byte[0] of VERSION_STR |
$E1 |
$4B = 'K' |
byte[1] of VERSION_STR |
If both match → SIDKick-pico (data1 = $0B). Confirmed against firmware v0.22 DAC64. Full VERSION_STR: SK\x10\x09\x03\x0F0.22/DAC64.
Enters FPGASID configuration mode by writing the magic cookie $81/$65 to D419/D41A, then sets bit 7 of D41E. Reads D419 and D41A back and checks for the identify signature $1D/$F5. If matched:
- D41F =
$3F→ FPGASID 8580 mode (data1 = $06) - D41F =
$00→ FPGASID 6581 mode (data1 = $07)
Uses the voice-3 oscillator readback technique (reference: 1541 Ultimate detection code):
- Writes
$48(gate bit set) to D412 (voice 3 control) - Shifts right and writes the sawtooth waveform value to D412
- Reads D41B (oscillator 3 output) — real SIDs echo predictable values; emulators/empty sockets do not
Then reads D41B a second time and checks for $03. From that value, MODE6581/MODE8580 look-up tables classify the exact sub-revision.
Uses the noise-waveform mirror test: activates noise waveform on voice 3, then reads D41B at $D400 + $20 offsets. A real SID at that address produces non-zero random values; a mirrored (non-SID) address always reads zero.
Scans: D400, D420, D440 … DE00, DE20, DEE0, DF00 … DFE0
Runs after FPGASID, before the real SID check. Writes the config unlock sequence $F0 $10 $63 $00 $FF to D41F, then reads D41F twice with a ~3 ms gap.
- Both reads must be in
$E0–$FCrange (not$FF) - Both reads must agree within
$02of each other (stable — the chip holds the value)
A decaying NOSID bus typically reads $FF (the last written value), or if it has drifted into $E0–$FE, the two reads will differ by more than $02. Either condition rejects the chip.
data1 = $0D→ uSID64
Runs before SwinSID Nano to prevent false positives on real 6581/8580. Uses the voice-3 oscillator readback (see Step 4e). checkrealsid only writes to D412/D40F — never D41F — so it is safe to run early. If a real SID is confirmed here, checkswinsidnano is skipped entirely.
Runs at Step 0.5 (before SIDFX), provided no real SID was found at Step 0.25. Uses D41B (OSC3) activity counting with noise waveform at maximum frequency ($FFFF):
Stage 1 — Change-count gate: Reads D41B 8 times back-to-back and counts how many consecutive pairs differ. A real 6581/8580 LFSR advances every CPU clock at $FFFF frequency — all 7 pairs always change (cnt = 7). The SwinSID Nano AVR updates at ~44 kHz (much slower than the 985 kHz C64 clock), so some reads catch the same LFSR value (cnt = 3–7, hits 7 in ~40% of windows). Stage 1 retries up to 3 times and only rejects if all attempts give cnt = 7 (guaranteed real-SID speed). P(all 3 fail for SwinSID Nano) ≈ 6%.
Stage 2 — Activity confirmation at 62 ms: After a 50 ms wait, counts changes in another 8-read window. Requires cnt ≥ 3. Filters out a fully-dead NOSID bus that happens to have passed Stage 1.
Known limitation: A C64 with an Ultimate II+ cartridge and virtual SID disabled generates FPGA-sourced bus noise at ~44 kHz that is indistinguishable from the SwinSID Nano oscillator. Such a setup will be reported as SwinSID Nano rather than NOSID.
data1 = $08→ SwinSID Nano (or NOSID + U2+ with virtual SID off)
Runs after all other hardware checks have failed (last chance before SwinSID Nano / NOSID). Writes $A5 (firmware-update magic) to D41D, waits ~6 ms, reads D41D back.
| Read back | Meaning |
|---|---|
$5A |
New firmware: ARM acknowledged the update-start magic → KungFuSID |
$A5 |
Old firmware: register echoes the last write (all SID registers are stored in RAM array) → KungFuSID |
| anything else | Not KungFuSID |
Old firmware reason: kff_read_handler returns SID[register_addr] for every address except the firmware-update register on new firmware. Writing $A5 stores it; reading back returns $A5. Real SID chips, ARMSID, FPGASID, and SIDKick all produce different values.
data1 = $0C→ KungFuSID
Sets the volume register D418 = $1F then counts CPU cycles until it decays to zero. Different emulators decay at different rates. The routine:
- Samples 6 times (controlled by
NumberInts = $06) - Averages the samples with
ArithmeticMean(16-bit accumulator, integer division) - Compares the average against a
MODE6581/MODE8580/MODEUNKNtable to print the emulator name
This fingerprint distinguishes VICE ResID, VICE FastSID, HOXS64, Frodo, YACE64, and EMU64.
Runs only when no primary chip was identified (data4=$00). A family-agnostic scan adapted from TLR's sid-detect2: walks $D400–$D7E0 and $DE00–$DFE0 in $20 strides and, at each unclassified slot, runs a retriggered-sawtooth OSC3 count-up test (the readback must increment across three reads — proof an oscillator is advancing there). New finds are appended to sid_list as type $11 ("TLR-generic"); a dedupe pass collapses duplicates against the family-specific results, keeping the refined type.
Probes $DF40/$DF50/$DF60 for a Yamaha OPL family chip (YM3526 / YM3812) — the CBM SFX Sound Expander and FM-YAM both decode there identically. Detection uses an (status & $E0) == 0 two-read check: a real OPL drives status into $00–$1F; open bus has the high bits set. Reported on the next free stereo row as DF40 SFX/FM FOUND. One detection path covers both products (see STORY.md §23).
Probes the four documented C64 MIDI interfaces for a 6850 ACIA reset signature: write $03 (master reset) to the control register, then status reads & $73 == $02 (transmit-empty, nothing received, no errors). Address pairs, first-hit-wins: Sequential/Namesoft $DE00/$DE02, DATEL/Siel/JMS $DE04/$DE06, Passport $DE08, Maplin $DF00. A two-read consistency check rejects open-bus jitter; the $DF00 probe is guarded against SIDFX / U64 / SIDKick-pico FM / ARM2SID SFX- ownership. When no SID was found, the cart name overwrites the NOSID row. See STORY.md §24.
Patches the NMI vector to RTI, then checks whether a raster IRQ fires at line $137 (which only exists on PAL machines with 312 lines). Result stored in KERNAL variable $02A6:
1= PAL (~50 Hz)0= NTSC (~60 Hz)
All timing loops in the detection chain are calibrated to this value.
- Reads
$D030(C128 speed register; returns$FFon open C64 bus → C64 identified) - If not
$FF, writes$2Ato$D0FEand reads it back:- Returns
$FC→ Commodore 128 - Returns other → Turbo Chameleon 64 (TC64)
- Returns
Result stored in za7 ($A7).
| Address | Contents |
|---|---|
$0801 |
BASIC stub: SYS 9216 (→ $2400) |
$1800 |
Embedded SID tune 1 (Triangle Intro, $1806 play) |
$0A00 |
Embedded TLR sid-detect2 (copied to $0801 on L) |
$2400 |
Main program — start: and all detection subroutines ($2400–~$5A99) |
$5B00 |
tlr_sweep family-agnostic baseline scan (V1.5.01) |
$6000 |
Detection result tables (num_sids, sid_list_l/h/t, sid_map), screen data, info-page text, string labels, colour table |
$9200 |
SID Tracker View code (V1.4.33) |
$A000 |
Embedded SID tune 2 (Delirious 9, under BASIC ROM) |
$C000 |
Tracker shadow SID ($C000–$C01F) |
$C020 |
Tune-selector segment (V1.4.35) |
$C300 |
Quality Fingerprint page code + tables (V1.5.02) |
| Address | Name | Purpose |
|---|---|---|
$A4 |
data1 |
Primary result (chip type code) |
$A5 |
data2 |
Secondary result (echo char / sub-type) |
$A6 |
data3 |
Tertiary result (ARM2SID 'R' discriminator) |
$A7 |
za7 |
Machine type: $FF=C64, $FC=C128, other=TC64 |
$F7 |
sidnum_zp |
Number of SID chips found |
$F9–$FA |
sptr_zp |
SID base address pointer (e.g. $D4:$00) |
$FC–$FD |
mptr_zp |
Mirror-scan address pointer |
| Code | Chip |
|---|---|
$01 |
6581 |
$02 |
8580 |
$04 |
Swinsid Ultimate |
$05 |
ARMSID / ARM2SID |
$06 |
FPGASID 8580 |
$07 |
FPGASID 6581 |
$08 |
Swinsid Nano |
$09 |
PD SID |
$0A |
BackSID |
$0B |
SIDKick-pico (8580) |
$0C |
KungFuSID |
$0D |
uSID64 |
$0E |
SIDKick-pico (6581) |
$10 |
Second SID found |
$20–$21/$24–$26 |
ULTISID 8580 |
$22–$23 |
ULTISID 6581 |
$30 |
SIDFX |
$31 |
No SIDFX |
$F0 |
No SID / Unknown |
siddetector v1.5.05
row 2: armsid.....: [result]
row 3: swinsid....: [result]
row 4: fpgasid....: [result]
row 5: 6581 sid...: [result]
row 6: 8580 sid...: [result]
row 7: sidkick....: [result]
row 8: backsid....: [result]
row 9: kungfusid..: [result]
row 10: pd sid.....: [result]
row 11: nosid......: [result]
row 12: sidfx......: [result]
row 13: pal/ntsc...: [PAL/NTSC] [C64/C128/TC64]
row 14: usid64.....: [result]
row 15: $d418 decay: [value]
row 16: stereo sid.: [address + chip name per slot]
Spacebar restarts the full detection sequence (raster-stable restart via $D012 spin). Pressing SPACE also silences all SID voices immediately.
Press P to launch the SID Tracker View — a dedicated screen that plays the built-in Triangle Intro (Michael Troelsen / Fun Fun 1988) and shows per-voice state live while music plays:
- NOTE / WAVE / GATE / ADSR / FREQ columns for voices 1–3
- VU bar per voice with a white → grey → yellow → orange → red gradient
(voice 3 reads real
$D41CENV3; voices 1/2 use a software envelope follower with gate-edge detection) - Live OSC3 scope across rows 15–22 (40-sample burst of
$D41B)
Press P, SPACE, or Q on the tracker screen to stop and return to detection.
Technical note: SID voice registers are write-only, so to display what the
player is writing to voices 1 and 2 the tracker patches the player binary
at $1800–$1FFF, rewriting every STA $D4xx to STA $C0xx. The raster
IRQ then copies $C000–$C01F → $D400–$D41F each frame after the play
call, so audio is unaffected. On exit, an undo table restores the
original bytes for a clean re-entry.
Press I to enter the info page for the detected chip. Navigate with CRSR LEFT/RIGHT; SPACE returns to the main screen. 17 pages are available (one per chip type), browsable in any order.
Press Q to enter the Quality Fingerprint page (added V1.5.02). It paints one row per detected SID, combining two orthogonal accuracy fingerprints:
D400 QUALITY 4/5 (GOOD 8580 ) D418=N15
D420 QUALITY 2/5 (BAD ARMSID) D418=N18
- sidcheck grade (
0–5, banded AWFUL / BAD / GOOD / BEST) — the combined-waveform OSC3 readback test from Wonderland XIII / Censor Designs, lifted from the VICE testprog suite and rebased to run persid_listslot. The cycle-critical writes target absolute$D4xxoperands that are runtime-patched per slot (a 36-site patch list generated at assemble time). $D418decay (Nnn) — the same volume-decay measurement used on the main screen's row 15, surfaced for every slot so implementations can be compared side by side.
The chip-name column is resolved through sid_type_index, the single code→name table shared with the debug page. SPACE or Q returns to the main screen.
Note: the decay column reads N00 under VICE because reSID doesn't model $D418 read-decay like real silicon; the sidcheck grade is meaningful in both. Real-hardware decay values are captured by make hw_test (TEST 9). The sidcheck result table is PAL-calibrated.
make # assemble siddetector.asm → siddetector.prg (requires Java + KickAss.jar)
make run # launch in the patched WinVICE 3.9
make clean # remove siddetector.prg
# Variant-specific launches (patched VICE only)
make run-armsid make run-arm2sid make run-swinu make run-swinnano
make run-fpgasid8580 make run-fpgasid6581 make run-pdsid make run-kungfusid
make run-backsid make run-usid64 make run-sidfx make run-skpico8580
make run-skpico6581 make run-none
# MixSID / stereo (8580 @ D400 + personality @ D420)
make stereo-armsid make stereo-arm2sid make stereo-swinu
make stereo-fpgasid make stereo-sidfx
# Regression harnesses
make ci # 32 unit tests (~30 s)
make ci-full # ci + 14 variant goldens (~4 min)
make test-variants # variant sweep alone
make update-variant-goldens # after intentional UI or personality changeRequirements:
- Java runtime
- KickAssembler at
C:/debugger/kickasm/KickAss.jar - Patched WinVICE 3.9 at
C:/Users/mit/claude/c64server/vice-sidvariant/GTK3VICE-3.9-win64/bin/x64sc.exe(build recipe:docs/VICE_PROXY_BUILD.md; patch:patches/vice-sidvariant-v1.patch). All VICE-based tests andmake run-*/stereo-*targets require this binary — the stock VICE doesn't know the-sidvariantflag.
The repo ships a patched fork of VICE 3.9 (in ../vice-sidvariant/) with a
-sidvariant <name> CLI flag. Each emulated SID slot can wear a chip-family
personality that responds to that chip's detection magic-cookie protocol —
ARMSID, ARM2SID, SwinSID U/Nano, FPGASID 6581/8580, PDsid, KungFuSID,
BackSID, SIDKick-pico 6581/8580, SIDFX, uSID64. Audio still comes from ResID;
only the detection protocol is intercepted.
This makes CI exercise every chip family without plugging anything into a
real C64. The window title shows the active personality (e.g. VICE (C64SC) [SidVariant=sidfx]) so you always know what's loaded.
make ci-full runs 32 unit tests + a 14-case variant sweep that byte-diffs
each variant's detection screen against a stored golden (in
tests/variant_goldens/). If a regression changes what a variant looks
like, the diff is printed and CI fails loudly.
Full design rationale and per-chip protocol tables:
docs/ARMSID_PROXY_PLAN.md— ultraplan with the 9-phase build-out.docs/VICE_PROXY_BUILD.md— reproducible build from MSYS2.docs/VICE_PROXY_USAGE.md— flag catalogue + make targets.docs/test_matrix.html— combined HW + VICE test dashboard (open in browser).patches/vice-sidvariant-v1.patch— ~2 kLOC source diff against pristine VICE 3.9 (upstream-ready, GPLv2+ inherited).
Tests are written in 6502 assembly (KickAssembler syntax) and run inside WinVICE. Each test preset the relevant zero-page inputs (data1, data2, data3, za7), calls an embedded copy of the dispatch logic, compares the returned result code against the expected value, and displays PASS / FAIL on the C64 screen. The final pass count is written to $07E8 (off-screen RAM) for inspection in the VICE monitor.
make test # ArithmeticMean unit tests (4 cases)
make test_dispatch # ARMSID / FPGASID dispatch tests (8 cases)
make test_suite # Full suite — all scenarios (43 cases)After VICE opens, all results are visible on screen immediately. To check the pass count in the VICE monitor (Alt+M):
mem $07E8 $07E8 # shows pass count; $2B (43) = all passed for test_suite
| File | Tests | Covers |
|---|---|---|
tests/test_arith.asm |
4 | ArithmeticMean — pure computation |
tests/test_dispatch.asm |
8 | ARMSID / ARM2SID / FPGASID dispatch |
tests/test_suite.asm |
43 | All dispatch stages (see table below) |
Each file has a matching .mon VICE moncommands file that loads symbols and sets a breakpoint at td_spin (the completion spin-loop).
| Section | Tests | Stage | Inputs → Expected result |
|---|---|---|---|
| S1 | T01–T03 | Machine type | za7=$FF → C64 · $FC → C128 · other → TC64 |
| S2 | T04–T05 | SIDFX | data1=$30 → found · $31 → not found |
| S3 | T06–T10 | Swinsid/ARMSID | $04 → Swinsid-U · $05/$4F/$53 → ARM2SID · $05/$4F/other → ARMSID · no-match cases |
| S4 | T11–T13 | FPGASID | $06 → 8580 · $07 → 6581 · other → no match |
| S5 | T14–T16 | Real SID | $01 → 6581 · $02 → 8580 · other → no match |
| S6 | T17–T18 | Second SID / no sound | $10 → second SID · other → no sound |
| S7 | T19–T22 | ArithmeticMean | [10,20,30]=20 · [5×6]=5 · [100,50,75,25]=62 · empty=0 |
| S8 | T23 | FPGA stereo | data1=$06 at $D500 → recorded in sid_list |
| S9 | T24–T27 | New chips | $09 → PDsid · $0A → BackSID · $0B → SIDKick-pico · $0C → KungFuSID |
| S10 | T28–T29 | ARM2SID SFX | emul_mode=$01 → SFX only · $02 → SID+SFX |
| S11 | T30–T31 | SKpico FM | skpico_fm=$04/$05 → FM Sound Expander at $DF00 |
| S12 | T32 | FM-YAM OPL2 | fmyam_detected=$01 → FM-YAM/OPL2 at $DF40 |
| S13 | T33–T35 | Quality band | score 0 → AWFUL · 5 → BEST · $FF → BAD-clamp |
| S14 | T36–T43 | Chip-type index | sid_type_index code→slot: $01→6581 · $02→8580 · $08→Nano · $09→PDsid · $0E→SKpico-6581 · $30→SIDFX · $F0→NoSID · $0F→UNKWN |
Because the actual detection routines (Checkarmsid, checkfpgasid, etc.) probe real SID hardware registers, they cannot be tested deterministically inside VICE — VICE's SID emulates an 8580 and returns fixed values. The tests therefore target the dispatch logic: the code that reads data1/data2/data3 (already set by the detection routines) and branches to the correct chip identification. Each dispatch routine is an embedded copy of the relevant branch block from siddetector.asm, with jsr $AB1E / jsr $E50C / jmp end replaced by a result-code write to dispatch_result.
- Identify the zero-page inputs for the scenario
- Add a dispatch routine that mirrors the branch logic from
siddetector.asm - Write a test block: set inputs → call dispatch → compare
dispatch_result→ print PASS/FAIL - Increment
pass_counton pass - Update the total expected count in
test_done(cmp #N)
| Area | Why |
|---|---|
Checkarmsid hardware probe |
Writes to SID regs; VICE returns fixed 8580 values |
checkfpgasid magic-cookie |
Config mode only exists on real FPGASID hardware |
checkrealsid OSC3 readback |
Depends on real sawtooth waveform decay |
checksecondsid noise mirror |
Depends on real noise-waveform $D41B randomness |
calcandloop decay timing |
Emulator timing differs from hardware by design |
- Added V1.5.05: Single source of truth for chip-type → name. The Q page and the debug page previously carried two independent code→name mappings that had drifted (that drift produced the V1.5.04
$01/$02swap and$0Emismap). They now share onesid_type_indexresolver + a 17-bytesid_code_to_slottable feeding row-alignedsidname_short_*(6-char Q page) andsidname_long_*(debug page) tables — adding a chip is one row in each and they cannot disagree. ULTISID ($20–$26) stays special-cased in each printer (debug names all 7 filter-curve variants; the 6-char Q page only distinguishes 8580 vs 6581 family). Unit suite grown to 43 tests (T36–T43 exercisesid_type_indexdirectly — the keystone guard against this drift class);hw_test.pyTEST 9 captures the per-slot Q-page sidcheck grade +$D418decay off real hardware into the run report. Binary shrank 49222 → 49184 bytes. - Fixed V1.5.04: Q-page chip-name table holes + a pre-existing debug-page swap.
quality_print_chiptyperouted five valid type codes to "UNKWN" —$09PDsid,$0ESIDKick-pico 6581 (was mislabelled "USID64"),$10secondsid, and$20/$21/$22ULTISID — nowPDSID/SKPI65/2NDSID/ULTI85/ULTI65. Separately,dbg_print_sid_typenamehad type$01and$02mapped to the wrong strings (6581 shown as "8580 FOUND" and vice-versa) since the routine was written; the canonical convention is$01=6581, $02=8580(line 753 / 5894). Also retireddo_quit/goodbye_text/EXITINTRO(~30 bytes dead code since V1.5.02 rebound the Q key), and fixed thetaskkill //Fquoting in the new smoke scripts. - Fixed V1.5.03: Two latent Q-page bugs surfaced by interactive WinVICE smoke-testing (neither was touched by the unit suite or the variant goldens). (1)
qc_pt_ptrwas declared in the$C300code segment, not zero page — KickAssembler silently truncates a non-ZP label in(label),yindirect-indexed addressing to its low byte, sosta (qc_pt_ptr),yassembled assta ($A9),yand corrupted memory on every Q-press; moved to$C1. (2)calcandloop_qinherited thetxs/tsx"save X" trick fromcalcandloop, which is only safe because the original tail-callsjmp funny_printand never returns — the Q-page copy returns viarts, so each iteration buried the return address; fixed by restoring X from a zero-page slot instead. - Added V1.5.02: Quality Fingerprint page (Q key). One row per detected SID combining two orthogonal accuracy fingerprints: a sidcheck combined-waveform OSC3 grade (
0–5, banded AWFUL/BAD/GOOD/BEST, lifted from the Wonderland XIII / Censor Designs VICE testprog) and the$D418volume decay. The cycle-critical sidcheck writes target absolute$D4xxoperands that are runtime-patched persid_listslot via a 36-site patch list generated at assemble time by aqrec()macro, so the original's timing is preserved across all eight possible slots. Lives in a fresh$C300RAM segment. SPACE or Q returns to the main screen. - Added V1.5.01: TLR family-agnostic baseline sweep (
tlr_sweep, adapted from TLR's sid-detect2). Walks$D400–$D7E0and$DE00–$DFE0in$20strides and, at every unclassified slot, runs a retriggered-sawtooth count-up test (OSC3 must increment across three reads) before the family-specific scans. New finds are appended tosid_listas type$11("TLR-generic"); a dedupe pass collapses duplicates, keeping the refined type. Gated todata4=$00(no primary chip identified) — a broader gate false-positives on mirrors and disturbs ARMSID DIS state. - Added V1.4.45: MIDI cartridge detection. New
checkmidiroutine probes the four documented C64 MIDI carts (codebase.c64.org reference) via the 6850 ACIA master-reset signature: Sequential Circuits / Namesoft at$DE00/$DE02, DATEL/Siel/JMS at$DE04/$DE06, Passport at$DE08, Maplin at$DF00. First-hit wins (only one MIDI cart can be attached at a time per the codebase reference). Result is printed on row 11 (the NOSID line) at column 13, aligned with the other detection rows. Sequential and Namesoft share the polled-read fingerprint (only IRQ vs NMI line routing differs) and are both reported asSEQUENTIAL MIDI. The$DF00(Maplin) probe is guarded against SIDFX / U64 / SIDKick-pico FM / ARM2SID SFX- ownership. The patched WinVICE 3.9 was rebuilt with--enable-midito support headlessmake run-midi-{sequential,passport,datel,namesoft,maplin}and 5 newvariant_smoke.pygolden-fingerprinted test cases. Side-fix: the legacy diagnostic write at$5801(which was clobbering bytes overlapping the MIDI string area) is now stored at a labelleddiag_step4_resultbyte.variant_smoke.pyalso gained a-defaultflag to insulate runs fromvice.inisettings persisted by interactivemake stereo-*/make sfxsessions. - Added V1.4.44: Polish + test infra. Restart progress bar shares row 24 with the "*** RESTARTING ***" banner instead of using a separate row (bar fills outside the banner range, recolours banner letters green inside it — one row total). Behavioural U64 fallback:
is_u64 = 1when the fingerprint scan finds 4+ slots even if the$DF1FUCI probe atstart:reads$FF(UCI disabled / remapped). "TUNEFUL EIGHT READY ON U64" banner overlays row 11 (NOSID...:) whensidnum_zp == 8ANDis_u64 == 1.$01 = 6581 / $02 = 8580is now canonical across code + docs; six stale comment sites and two reversed branches inu64_fingerprint_scan/uci_type_for_addrcorrected (your ULTISID slots now correctly display as6581 INT).utfa_draincapped at 256 iterations (was unbounded — could hang when UCI was unresponsive). Newscripts/check_memorymap.py [--fix]verifies addresses indocs/MEMORYMAP.mdagainst the livesiddetector.sym; wired intomake cias a doc-drift guard. Newmake test-tuneful-eightruns a non-destructive end-to-end U64 8-SID hardware test: snapshot the live config, applybin/tt8-ultimate.cfg, run siddetector, verifysidnum_zp == 8 + is_u64 = 1(all 8 slots labelled6581 INT), restore the pre-test config in afinallyblock. - Added V1.4.37: Write-coupling scan (
u64_fingerprint_scan) for 8-SID Ultimate 64 "Tuneful Eight" configurations — independently verified to find all 8 slots (D400 D420 D480 D4A0 D500 D520 D580 D5A0) on real hardware across three consecutive runs. The existingchecksecondsidnoise-mirror trick falsely rejects U64 UltiSID slots because every FPGA slot runs its voice 3 oscillator continuously, so reads ofcandidate $D41Bare non-zero even when primary noise is enabled. The new scan instead silences primary D400 voice 3, snapshots each already-found slot's OSC3 baseline per candidate, writes a saw+gate activation to the candidate, and rejects only if the activation propagates to primary or any found slot's OSC3 (i.e. the candidate is a write-mirror).is_u64($DF1F != $FF) gate dropped because U64 configs that disable UCI return$FFthere yet still have multi-SID hardware; on a single-SID C64 every candidate write mirrors to primary, so the test correctly rejects all candidates anyway. Bypasseduci_type_for_addr's UCI-FIFO drain loop (hangs forever when$DF1C/$DF1Eboth return$FF) — chip-type comes from a directcheckrealsidcall. - Added V1.4.36: 8-SID enumeration for Ultimate 64 "Tuneful Eight" config.
num_sids/sid_list_l/h/textended from 8 to 9 bytes (slot 0 reserved, slots 1..8 active);s_s_addcap raised from 7 to 8;csfp_l_l_found(FPGASID-stereo path) cap raised from 7 to 8;sidstereo_printnow renders up to 8 SID rows at screen rows 16..23. Row 24 stays as the action bar. U64 detection itself was already in place via$DF1F != $FF→is_u64 = 1. Verified the hardware exposes 8 addressable SIDs by cross-checking with TLR's sid-detect2. - Added V1.4.35: Second tune in tracker view (1/2 keys) and a progress bar across the SPACE-restart wait. Triangle Intro stays at
$1800; Delirious 9 (Troelsen / Genesis Project, 1990) is SIDwinder-relocated to$A000–$B39Band embedded as a separate segment.tune_selectpatches the IRQ play / init JSR operands, thetracker_patch_oncescan range, and the title row pointer; pressing1/2in the tracker view stops IRQ play, silences SID, repatches the new player and re-inits in place. BASIC ROM is banked out ($01=$36) only inside the IRQ play call and aroundtune_player_init/tracker_patch_onceso$AB1E(BASIC STROUT) stays usable elsewhere. The new tune-management block lives in a fresh$C020segment above the shadow SID. The 2 s SPACE-restart dwell now fills row 23 with green solid blocks (one cell per ~54 ms) instead of busy-waiting silently. - Added V1.4.33: SID Tracker View (P key) — dedicated screen during music playback showing per-voice NOTE/WAVE/GATE/ADSR/FREQ, VU bars with white→red gradient, and live OSC3 scope for voice 3. Player writes are redirected to shadow RAM at
$C000–$C01Fvia one-shot binary patch so voice 1/2 registers can be read back (normally write-only). Tracker code lives at$9200(below BASIC ROM) to stay CPU-visible without bank switching. See MEMORYMAP.md. - Fixed V1.3.84: SIDFX secondary D420 probing: (1) removed D41D echo test — SIDFX write-buffers unmapped registers ($1D–$1F), causing any chip at D420 to echo back the written value (not chip-specific). (2) When primary SID is ARMSID, skip DIS probe at D420 entirely — ARMSID snoops CS2 DIS writes and drives $4E aggressively on all D4xx bus reads, contaminating D43B. Falls back to SIDFX-reported type (6581/8580). DIS probe still works for D420 with non-ARMSID primary, and for D5xx+ regardless of primary. Added D41B ACK in
sfx_probe_dis_echobefore secondary reads (harmless for non-ARMSID primaries). - Fixed V1.3.83: Detection confidence indicator — if
checkrealsidneeded retries due to VIC bad-line DMA steals, a*is appended after "6581 FOUND"/"8580 FOUND" on the main screen.retry_zp($B2) tracks how many of the 3 attempts were used. - Fixed V1.3.81: Multi-SID sound test now plays the full 3-voice melody on every detected SID slot (not just a triangle tone).
snd_patch_pageself-modifies all 31sta $D4xxinstructions inst_soundtestto the target SID page. - Fixed V1.3.80: Stereo ARMSID@D400 + SwinSID U/ARMSID@D5xx —
s_s_arm_call_realnow allowssfx_probe_dis_echowhen primary is ARMSID (data4=$05); the probe reads fromcandidate+$1Bso D400 ARMSID snooping the DIS writes does not corrupt the result. Requires dual ARMSID/SwinSID U hardware to verify. - Fixed V1.3.79: SwinSID Ultimate fiktivloop false positive — AVR OSC3 returns 0 with noise enabled, causing
checksecondsidto falsely detect D500 as a second SID. Fixed by skippingfiktivloopwhen primary is SwinSID U (data4=$04). - Fixed V1.3.79: Stereo 6581@D400 + SwinSID U@D500 —
s_s_arm_call_realnow triessfx_probe_dis_echobeforecheckrealsidwhen primary is a real SID; SwinSID U echo returns 'S' and is correctly identified. - Fixed V1.3.73: 8580@D400 + ARMSID@D420 (MixSID, C09 config) now correctly detected
- Fixed V1.3.74: 6581@D400 + ARMSID@D420 (MixSID, C08 config) now correctly detected
- ARM2SID stereo D400+D500 not yet verified on hardware
- Stereo slots D700 and DF00 not yet tested on hardware
- Swinsid Nano with NOSID+U2+ (Ultimate II+ with virtual SID off) is indistinguishable — reported as SwinSID Nano; accepted limitation
- FPGASID in SIDFX SID1 slot is undetectable: SIDFX drives D419/D41A (POT registers) with real joystick data, masking FPGASID's identify-mode readback signature; additionally, the SIDFX SCI state machine reacts to D41E writes, disrupting the magic-cookie handshake. No software workaround is possible. Reported as SIDFX with unknown SID type.
- Quality page
$D418decay readsN00under VICE — reSID emulates the audio path faithfully but does not model the analog volume-register read-decay the measurement depends on. The sidcheck grade is meaningful under emulation; real decay values come frommake hw_teston a U64. Not yet captured/calibrated on hardware. - Quality page sidcheck
resulttable is PAL-calibrated; NTSC scores may shift. Per-family expected-grade reference not yet built (awaiting hardware decay data).


