Using C to Program the Commodore 64 SID Chip


Why use C to Program the Commodore 64 SID Chip?

If you grew up with an original Commodore 64, you probably had a manual along with it that showed you how to program the SID chip. There was even a multi voice example that took forever to load its data and was impossible to read and debug. This is what it looked like (lines 1 and 2 mine) (GitHub link):

1 rem c64 programmers reference guide
2 rem pages 187-188
10 s=54272: forl=stos+24: pokel,0:next
20 dimh(2,200),l(2,200),c(2,200)
30 dimfq(11)
40 v(0)=17:v(1)=65:v(2)=33
50 pokes+10,8:pokes+22,128:pokes+23,244
60 fori=0to11:readfq(i):next
100 fork=0to2
110 i=0
120 readnm
130 ifnm=0then250
140 wa=v(k):wb=wa-1:ifnm<0thennm=-nm:wa=0:wb=0
150 dr%=nm/128:oc%=(nm-128*dr%)/16
160 nt=nm-128*dr%-16*oc%
170 fr=fq(nt)
180 ifoc%=7then200
190 forj=6tooc%step-1:fr=fr/2:next
200 hf%=fr/256:lf%=fr-256*hf%
210 ifdr%=1thenh(k,i)=hf%:l(k,i)=lf%:c(k,i)=wa:i=i+1:goto120
220 forj=1todr%-1:h(k,i)=hf%:l(k,i)=lf%:c(k,i)=wa:i=i+1:next
230 h(k,i)=hf%:l(k,i)=lf%:c(k,i)=wb
240 i=i+1:goto120
250 ifi>imthenim=i
260 next
500 pokes+5,0:pokes+6,240
510 pokes+12,85:pokes+13,133
520 pokes+19,10:pokes+20, 197
530 pokes+24,31
540 fori=0toim
550 pokes,l(0,i):pokes+7,l(1,i):pokes+14,l(2,i)
560 pokes+1,h(0,i):pokes+8,h(1,i):pokes+15,h(2,i)
570 pokes+4,c(0,i):pokes+11,c(1,i):pokes+18,c(2,i)
580 fort=1to80:next:next
590 fort=1to200:next:pokes+24,0
600 data34334,36376,38539,40830
610 data43258,45830,48556,51443
620 data54502,57743,61176,64814
1000 data594,594,594,596,596
1010 data1618,587,592,587,585,331,336
1020 data1097,583,585,585,585,587,587
1030 data1609,585,331,337,594,594,593
1040 data1618,594,596,594,592,587
1050 data1616,587,585,331,336,841,327
1060 data1607
1999 data0
2000 data583,585,583,583,327,329
2010 data1611,583,585,578,578,578
2020 data196,198,583,326,578
2030 data326,327,329,327,329,326,578,583
2040 data1606,582,322,324,582,587
2050 data329,327,1606,583
2060 data327,329,587,331,329
2070 data329,328,1609,578,834
2080 data324,322,327,585,1602
2999 data0
3000 data567,566,567,304,306,308,310
3010 data1591,567,311,310,567
3020 data306,304,299,308
3030 data304,171,176,306,291,551,306,308
3040 data310,308,310,306,295,297,299,304
3050 data1586,562,567,310,315,311
3060 data308,313,297
3070 data1586,567,560,311,309
3080 data308,309,306,308
3090 data1577,299,295,306,310,311,304
3100 data562,546, 1575
3999 data0

I copied this from a text / pdf version which was in all uppercase (because only one case is valid for BASIC keywords and such). It’s in lowercase above so that you can copy/paste into the VICE emulator, because that “one case” is the lower in lower/uppercase mode, and uppercase pastes in as graphics characters in upper/graphics mode. This example is a fairly impressive one, but it also takes 45 seconds to load all of the data and a couple of typos can mean hours of debugging. I took several hours to find what was ultimately mistakenly typing an I instead of a 1.

Spacing Matters in Commodore BASIC

While this code is not *required* to be so compact in spacing, those in-line spaces almost 100% impact the size of the program in memory and on disk. So if you’re taking up nearly all of the BASIC RAM, eliminating spaces actually matters. So you have code that reads pokes or fori or dimfq.

In BASIC, data has to be part of code but still explicitly read in every time

The DATA statements in BASIC can create a nice little data block, but it’s an implicit data block that requires explicit reading in. So you take up all of the space required for the DATA statements, but then load that data into arrays, and, in this case, take 45 seconds to read in and encode the data used.

Variable names are 2 characters max in Commodore BASIC

PIPE, PIKE, and PINE are the same variable, so if you set PIPE to 2, PIKE to 3, and PINE to 4, you will get 4 from all of them. However Commodore 64 BASIC will let you *think* that up to 5 characters is legal (you get a syntax error if you use something like THINGS=2)

Functions can only receive one argument and be named 2 chars (5 can be specified)

The def fn statement allows you to specify code like def fn xp(x) = x * 300 + 12, which then is required to be invoked by fn xp(x) or (fn xp(x)) + y * 12 in context. (The parentheses are probably unnecessary, but readability can be helped by using 2 extra bytes for the parens.)

Subroutines take no arguments and have to be referred to by line number

If you are good at modularizing your code, you’ll find Commodore 64 BASIC a bit limiting. This isn’t even compared to modern and higher level programming languages. Even a decent Assembler can at least track symbolic locations of the subroutines and variables.

Commodore BASIC is slow

Spacing and variable names have an impact on the performance of Commodore BASIC

Setting up for C programming

Install cc65 for your platform. For macOS, it’s available via brew install cc65 and in Ubuntu via sudo apt install cc65. (My Windows install of cc65 is via WSL2).

You may want to find the include files for reference for your platform. They’re in /usr/share/cc65/include for my Ubuntu install and /opt/homebrew/Cellar/cc65/2.19/share/cc65/include (your version may vary).

I created a c64_note_values.h header file for note #defines for all octave 0-7 notes. They’re proportional to their frequency values, but scaled up so that the low ranges can be defined by integer values. If you want to tweak the values and regenerate, this is the C file I created to generate it.

The rudimentary Canon in D (so far)

Thanks to Recreating the Commodore 64 User Guide code samples in cc65. Part four: Sound, I was inspired to try my own hand at the C code.

#include <stdio.h>
#include <stdlib.h>
#include <c64.h>
#include "c64_note_values.h"

const unsigned int notes[][8] = {
  { D3, A2, B2, Fs2, G2, D2, G2, A2 },
  { Fs5, E5, D5, Cs5, B4, A4, B4, Cs5 },
  { D5, Cs5, B4, A4, G4, Fs4, G4, E4 },
};


int main(void) {
  unsigned int i,d;
  unsigned int measure=0;

  SID.amp = 0x1F;
  SID.v1.ad = 0x0f;
  SID.v2.ad = 0x0f;
  SID.v3.ad = 0x0f;

  while(1) {
    for(i = 0; i < sizeof(notes[0]) / sizeof(int); i++) {
      SID.v1.freq = notes[0][i];
      SID.v1.ctrl = 0x11;
      if(measure) {
        SID.v2.freq = notes[1][i];
        SID.v2.ctrl = 0x11;
      }
      if(measure > 1) {
        SID.v3.freq = notes[2][i];
        SID.v3.ctrl = 0x11;
      }
      for(d=0; d<10000; d++) { }
      SID.v1.ctrl = 0x10;
      if(measure) {
        SID.v2.ctrl = 0x10;
      }
      if(measure > 1) {
        SID.v3.ctrl = 0x10;
      }
    }
    measure++;
    if(measure>2) { break; }
  }
  return EXIT_SUCCESS;
}

In this example, I haven’t actually created any functions yet, beyond the main.

SID is a struct pointer cast of the base memory location of the default SID chip. (See here for a “multiple SID” example if you enable in hardware or VICE emulator)

.v1 through .v3 align to the base address of the voice 1 through 3 controls.

.ad is an attack / decay value… 0-f for the two nibbles. If you want the note to start as quickly as possible, use 0, if you want the note to never fade out, use f for the second nibble. For this case, I’m using just that… 0x0f.

.freq is the scaled frequency value I created earlier from the header file which is in the notes arrays.

.ctrl value of 0x11 turns the note on and 0x10 turns it off. You’re actually setting the triangle waveform bit to true and the gate bit to on and off.

.amp is a volume number (actually volume and mode… location 54296 in the Commodore 64 memory map). LP is 1 and volume nibble is 0xf.

Compiling and running

Assuming that you’ve saved the file to canonind.c, the command to compile and link is cl65 -t c64 -O canonind.c (you may be able to remove the -t c64 parameter… my install appears to assume c64.)

Provided you have a successful build, you should have a canonind binary file. In VICE, you can use “File -> Smart attach disk/tape/cartridge…” or “Autostart disk/tape image…”. For the first version, you’ll want to click the “[Autostart]” button after selecting the extensionless filename. For the second version, you’ll want to look for All Files *.* instead of .d64 etc… Either version should autoplay once loaded.

See my video (choppy sound from VICE and morning voice warning) of the process of using C to program the Commodore 64 SID Chip here:


Leave a Reply

%d bloggers like this: