Sunday, 25 October 2026

Cosmic Snake - Machine Code in REM statements on a VIC20

Cosmic Snake by Frank Lindsey is a bit of an unusual VIC20, not at least because it isn't a snake game, it is a not a bad Centipede (or is it Millipede ? or is it Caterpillar ?) style game.

In a recent video, TFW8b played all the games in the "Centipede Type Games" category on the Penultimate Cartridge

He did think it might be BASIC, so when it exited to BASIC at the end, he tried to list it.

The listing was full of "magic characters" and corrupted the display.

I was intrigued, so had a look at the listing.

It starts with some REM statements full of the aforementioned magic characters, this is line 9, the least magic of them all.

By line 100, it has returned to standard BASIC, including some SYS commands to call code in the 4000-500 range. That is where the BASIC program is on an unexpanded VIC20, starting at 4097 ($1001).

The BASIC code ran on until line 820.

What Is Going On?

Putting machine code in REM statements was a common trick used on the ZX81, but I can't remember seeing it used in Commodore BASIC before.

A simple example is the code to read the joystick port on a ZX81, as described in the post on the Minstrel Joystick Interface for RC2014 and Minstrel 4th

A REM statement is a place to add comments (or REMarks) to the code, it is purely there for readability and does not affect the game, other than making it slightly slower as the code has to be skipped by the BASIC interpreter.

It can be (ab)used as a place to store machine code.

You type a line, almost always at the very start of the program, as that location in memory is normally fixed. This line will contain a REM statement and sufficient characters for your machine code program (one character = one byte of code).

1 REM ......

You then POKE directly into memory where that line is stored, either by adding lines of code

10 POKE 16514,175

20 POKE 16515,71

etc.

or by directly typing in the POKE commands

POKE 16514,175

POKE 16515,71

etc.

Or if there is a lot of code, you can write a short loader program to get the user to type the bytes in and then poke them into RAM and then SAVE the program. Some of these loader programs ever computed a checksum to make sure you got it all correct.

Once you have entered the code, the REM statement now shows the magic characters, which are simple the character representations of the bytes of code.

Some of these are keywords, so you end up with things like the <= operator and the TAN keyword in the above example, both of which represented by a single byte.

In the ZX81 world, you then call that code with USR 16514 (or whatever is the address of the routine to call).

Onto the VIC20

There is a good reason why I have not seen this done in Commodore BASIC before, on the PET or VIC20.

Because of the structure of the BASIC program, you cannot have any zero bytes in your code. When the BASIC program is loaded, each line is parsed and if necessary, relinked, and when it finds a zero, it assumes it is the end of the line, so crops any code after that.

Just think about that for a moment. 

You cannot have any zero bytes in your code.

That means none of these type of instructions can be used, as they all assemble to code which contains a $00 byte.

LDA #$00

LDA $1400

JMP $1600

STA $9600,X

BRK

You have to be careful about variable and function locations to avoid any addresses ending in $00, adding padding or moving things around to avoid those locations. You also need to find other ways to end up with $00 in a register if you need it.

There are various other ways to get some assembler code on those machines, including placing the code into an area of RAM above the BASIC program, and saving them as a combined unit, so they get loaded together and the routines can be run by using the appropriate SYS 1234 style commands.

You can also store short sections of code in DATA statements at the end with code which are then READ and POKEd into a suitable area of RAM (e.g. the cassette buffer).

That is slow and inefficient, taking up to four bytes plus trimmings for each byte of code. This is normally only used for very short routines, such as this one from Tornado.

I was intrigued to see how the code in Cosmic Snake got around this, so started to reverse engineer it.

Code Layout

The code starts with the REM statements, of variable length, on line numbers 0, 1, 2, another line 2, 4, 5, 7, 8, 9, 10, yet another line 2 and finally line 12.

Line numbers don't actually have to be sequential, or unique, but it gets very confusing if they are not. See for example directory listings you get back from LOAD "$",8 on a Commodore disk drive.

Those are program listings, LOADed and LISTed like any other program.

After the REM statements, there is a short BASIC program with odd gaps, 100-125 in gaps of 5, then 200, then 300, 305 and then up to 700, 705, up to 720, then 800-820 also in 5s.

This is sprinkled with SYS calls, jumping into the code in the REM statements at various points.

The BASIC code is mostly doing things that can be a bit of a pain in assembler, such as the title page and high scores etc.

Storing Machine Code in REM Statements

The format of a line in Commodore BASIC is fairly simple:

  • 2 bytes with the address of the next line
  • 2 bytes for the line number
  • 1 or more bytes of program code, not including zero
  • 1 zero byte indicating the end of the line

Each of the lines fit into that format. I will show line 9 as that is one of the clearer ones.

>C:1601  73 16 09 00 8f 58

It starts at address $1601. The first two bytes are $73 and $16, meaning the next line starts at $1673 (the 6502 is little-endian, so the first byte is the lest significant, or the little end).

Next is the line number, $09 and $00 make $0009 or line 9.

Then the code starts, $8F is the BASIC token for REM. Whatever follows between the $8F and the next $00 will be skipped over by the BASIC interpreter.

There is a single padding character, $58 or "X", which is used throughout the program. I suspect the original code was written with lines like

9 REM XXXXXXXXXXXXXXXXXXXXXXX

and the code was POKEd into the memory, or assembled there using an assembly monitor.

Inside (line) Number 9

We are now at $1607, which is a place where the code calls this routine, using SYS 5639 (the decimal equivalent).

; $1607 - sys5639 - 

.C:1607  A9 01       LDA #$01

.C:1609  8D 07 15    STA $1507

.C:160c  8D 08 15    STA $1508

.C:160f  AE 08 15    LDX $1508

.C:1612  BD 26 15    LDA $1526,X

.C:1615  C9 01       CMP #$01

.C:1617  F0 06       BEQ $161F

.C:1619  EE 07 15    INC $1507

.C:161c  20 49 15    JSR $1549

.C:161f  EE 08 15    INC $1508

.C:1622  AD 08 15    LDA $1508

.C:1625  C9 06       CMP #$06

.C:1627  D0 E6       BNE $160F

.C:1629  60          RTS

The code completes with an RTS and control is returned to BASIC.

This is called from line 125:

125 geta$:sys5639:fork=1to10:sys6315:next:ifa$=""then125

This appears to be waiting for a key to be pressed. I think this is the title animation, which includes a "cosmic snake" moving around the title.

Quite a nice touch, especially for an unexpanded VIC20 game.

Directly after that routine is another padding character, another X, which shows up as a CLI in assembler.

.C:162a  58          CLI

(one of my few gripes about the 6502 is CLI - CLear Interrupt disable bit, in other words, enable interrupts which seems the wrong way around. SEI is SEt Interrupt disable is similarly backwards. Other than that, the 6502 is great and far better than any other processor I might have a rather large box full of ...)

Another routine follows at $162B, which is called from elsewhere in the code, not directly from BASIC.

; $162B

.C:162b  A2 01       LDX #$01

.C:162d  A9 01       LDA #$01

.C:162f  9D 08 15    STA $1508,X

...

.C:1665  A9 09       LDA #$09

.C:1667  8D 0D 17    STA $170D

.C:166a  60          RTS

After that, there are seven bytes of padding, again $58 / CLI / "X", presumably to allow the code to grow during development, or the code was originally longer but was shortened at some point.

.C:166b  58          CLI

.C:166c  58          CLI

.C:166d  58          CLI

.C:166e  58          CLI

.C:166f  58          CLI

.C:1670  58          CLI

.C:1671  58          CLI

And finally you are allowed a single $00 to indicate the end of the line (which the disassembler interprets as a BRK)

.C:1672  00          BRK

Line 10 follows durectly after at 1673

; $1673 -  line 10 remx etc.

>C:1673  72 17 0a 00  8f 58 

That continues up to line 12, and that links to line 100, which is actual BASIC code.

; $1964

; 100 print"{clr}":poke5697,6:sys4224:sys4248:sys5765

>C.1964  87 19 64 00  99 22 93 22  3a 97 35 36  39 37 2c 36

>C:1974  3a 9e 34 32  32 34 3a 9e  34 32 34 38  3a 9e 35 37

>C:1984  36 35 00

The numbers in the SYS commands are stored as characters ($9E $35 $37 $36 $35 at the end is SYS5765).

That means there would never be zeroes there, only $30 which is "0", not $00. It also explains why BASIC can be quite slow to parse these lines each time as there is a lot of converting 4 or 5 character strings into numbers.

Reverse Engineering

I have disassembled all the code and various blocks of non-code which is either padding or variables or character data. I have not carried out a full reverse engineering to work out what all the code does.

Ed: Well, not at the time I wrote this anyway ....

I did spot a few interesting things.

I found a few occurrences of things like:

.C:18c8  A2 01       LDX #$01

.C:18ca  CA          DEX

.C:18cb  8A          TXA

This loads X with $01, then decrements it to get $00, and then transfers it into the accumulator.

Normally this would have been just LDA #$00 but that would have been assembled as $A9 $00, and the $00 would have be detected as an end of line marker.

This is the End ...

When looking through the binary file in a hex editor (as you do ....) I was interested to see it did not end with three zeroes, as a pure BASIC program normally would.

The first zero is the end of line marker for the last line, the next would be the address of the next line, and $0000 indicates the end of the program.

Looking at the code, line 820 is the last line, and I found that ended earlier in the file, ending at $1C75.

But the file goes on for another 211 extra bytes at the end?

It doesn't seem to be valid code or text or anything, although the strings "$0000" and "BRK" indicate this could be left over memory from the assembler? It seems like there is every form of zero, as if breaking free from the previous limitations.

The files does contain 3,400 bytes exactly (in decimal), so maybe that was a save from the assembler / monitor program, rather than a standard BASIC save?

Looking through the code, I can't see any references to $1C75 - $1D49, so maybe it is just garbage at the end of the file as you sometimes get with a "save slightly more than the file size, just to be sure" option.

I tried removing them, and cropping the file so it finished at the end of the code as it should, and it seems to run fine with no difference, so I will ignore it.

Ironically, after all the work to get the zero-free code into REM statements as part of the BASIC program, the author has managed to demonstrate a more viable alternative of placing code into the area after the BASIC program, where you can use $00 without recrimination. (just be careful about where BASIC stores its variables and heap etc.)

Reconstructing the Code

I decided to try to recreate the program from BASIC and assembly sources, to see how it would work. 

Yes, I know I could have been down the roller disco with the cool kids, but instead I was at home reverse engineering a 45 year old computer game....

I now have a BASIC program that is the same size and matches from line 100 onwards, but with all the REM statements full of X's.

My God, it's full of stars ....

Now all I need to do is add in the assembler code, the character data and various bits of padding which I think are variables?

I can do that with ACME assembler as it allows me to insert a binary file (the PRG of the BASIC program) and also to overwrite bits of it by adding code with the .ORG directive setting the location within the file.

...

Well, that wasn't too bad.

I now have two source files.

  1. A BASIC program which contains the REM statements full of Xs and the rest of the BASIC code
  2. An assembler files containing all the reverse engineered code

I have done it this way so the assembler file contains just the assembler code, and does not need to recreate all the BASIC line framework.

The assembler file also has several sections which contain short runs of non-padding bytes. They don't make viable code as far as I can tell.

Some are referenced in the code, so they could be variables or data, or they could just be random padding (for example to an extra byte to move a function from $1600 to $1601 so it can be used in a JMP or JSR call)

As it stands, this builds from BASIC and ASM sources a binary identical file (other than the random data at the end, which does not affect the game or get referenced)

I have not commented most of the assembler, I would need to go through and work out how it all works.

The next step would be going through and turning the literal function and variables addresses into labels and trying to work out what they all do, and looking for system addresses like the VIC chip and known zero page addresses etc.

But that is a job for another day

Ed: Well, I wasn't planning to, but I have now almost completed a fully commented disassembly of the code, a full dissection of which will follow in a later blog post

Adverts

The VIC20 Penultimate Cartridge +3 DCR is available now from the Future Was 8 bit.


All Tynemouth products are still available direct from me, and I can ship worldwide.

Including the limited edition 10th anniversary recreation of the very first Penultimate Cartridge:


International shipping is a bit complicated at the moment, so if you want to order from outside the UK, please contact me using the contact link at the top of this page.

See here for more information:

Sorry to have to do this, and I know this is going to affect sales, but please understand it is the only option I have right now.

Stop Press!

I am going to do another batch of Mini PET 40/80s with keyboard, with a few updates. 

These are pre-order now, shipping worldwide in November. £450 kit / £500 built.

Available exclusively from me, contact me using the link at the top of the page to get a price for shipping and any duties, based on your address.

The Mini PET 40/80 internal PET replacement boards are also available now, £350 kit / £375 built.

Again contact me if interested.


Patreon

If you enjoy posts like this, you can support me via Patreon, and get access to advance previews of blog posts, and exclusive posts. Paid Patreon supports will be getting the third part shortly, and maybe a forth part three will follow if there is any interest

You also get progress updates on new projects and other behind the scenes updates, as well as access to my Patreon only Discord server for even more regular updates, and to discuss your own projects.

Sunday, 18 October 2026

Loading Programs on the Timex Sinclair TS1500

This is the Timex Sinclair TS1500 I repaired nearly a decade ago (how time flies).

It is a nice machine, basically a 16K ZX81 in a ZX Spectrum case for the American market.

I don't think it was a big seller over there and was never released over here.

That means it is NTSC, and as such slow mode is about half the speed of a PAL ZX81 (fast mode is identical on both).

OK, 54.8% if you want to be specific.

The ULA does have a pin for PAL/NTSC selection, but it would seem wrong to convert it, and there does not seem to be an easy way to do that, it looks like that pin goes under the ULA.

Internally, it is a minimal design with a type of ULA © Timex and marked NCR. It has 16K of RAM from eight 4116 DRAM chips, an 8K BASIC ROM and couple of buffer chips in place of a load of resistors.

The redesigned ULA takes on a bit more functionality than the Sinclair version, with it's 68 pins vs the original 40, mostly the DRAM multiplexing I expect.

It has also unfortunately got a ZX Spectrum style DC-DC converter for the RAM, but this one is still going strong.

Loading from Tape

I was interested to see how the cassette input was implemented, as it looked like there was a lot more going on than the ZX81.

When I was looking at tape loading on the Minstrel 4th, I did have a fourth part planned where I looked at ZX81 compatibles, like the Lambda 8300 etc., but I think everyone had had enough for those, including me, so I left it for a while so I can spring it on you unexpectedly, round about now.

TS1500 Tape Output Schematic

The TS1500 is one of the earliest and probably the only official ZX81 compatible machine.

In searching around, I found several schematics, all of which were unfortunately wrong in one way or another.

This one is almost unreadable, but does turn out to have the correct value the others have wrong.

This one has no values, but elsewhere has the part numbers are correct, where others are wrong.

This one is the clearest, but one of the values is wrong.

I have redrawn a combination of all of those which matches the actual board.

The value of R19 is shown as 1KΩ on the least visible schematic above, and that appears to match the ZX80 and ZX81 schematics. The actual board also has 1KΩ fitted, the relevant parts are highlighted below.

The ZX81 schematic has a larger value resistor on the signal from the ULA, 1MΩ rather than 510KΩ, but has the same 1KΩ to ground.

The TS1500 also has the 47nF capacitor in parallel with the 1KΩ resistor.

The three ZX81 boards I repaired recently all had slight differences around this area. The capacitor C11, is the large brown ceramic disc on this board, 47nF, and the 1KΩ resistor R27 is at the bottom.

One of the other boards looked to be missing C11 and R27 from factory, no evidence they were ever fitted, the pads are solder levelled like the other no-fit parts.

I then spotted they appear to have been fitted directly on the pins of the mic jack to the side.

A third board looked like R27 was not fitted from factory, but C11 looks to have been desoldered at some point and both are now by the jack socket.

Very odd. Would moving those really make that much difference?

Something else that is odd is the unusual ground arrangement on the TS1500. Rather than directly grounding the output jack, as in the ZX80, ZX81, ZX Spectrum etc., the TS1500 has a 100Ω resistor in the ground path. 

Confirmed by the actual PCB, the ground plane at the top is clearly connected to the ear jack on the right, but not the mic jack on the left.

Tape Input

A big difference to the ZX81 is it is an active input.

The ZX81 had a simple RC filter.

The TS1500 has a different RC arrangement, and then a transistor buffer.

A common mistake on several of the schematics was labelling C14 as C19.

It is sort of understandable as C19 is in the right place, but that is part of the tape out circuit, as shown above (and confirmed by tracing out the board). The 47pF capacitor is actually C14, just below the modulator.

As before, I have redrawn a version which matches the board.

This seems to do a reasonable job.

This is an inverting amplifier, and I have confirmed there must be an inverter internally on the input as the signal will not work if fed into a Minstrel 3 unless it is inverted first.

But as with any of these circuits, if the input signal is not very good, it will miss bits.



Practical Issues

I had an unexpected problem when trying this out.

Normally for loading, I use my TZXduino style unit to play .p files from an SD card.

As part of this testing, I also wanted to test loading from tape, but I couldn't find my usual tape leads.

No problem, I have any number of stereo jack to jack leads, that should be fine, I was using that on a Minstrel 4th last week.

But nothing loaded?

Odd, probing around on the board, there was nothing, no signals.

It was then I noticed the jack sockets.

These are the same style as used on the ZX81 and ZX Spectrum, and is designed for a mono jack plug.

On the Minstrel 3, I have the sockets wired to only the tip and the sleeve, with no connection to the ring.

A mono TS (Tip, Sleeve) jack has two connections.

A stereo TRS (Tip, Ring, Sleeve) jack has three connections.

With a stereo jack plugged in, I get the left channel on the top and the ground signal from the sleeve.

The right channel on the ring is not connected. You might think I should connect that in parallel with the left channel, but I that is a deliberate choice.

The reason is it also allows you to plug in a mono jack.

By connecting to only T and S, that will allow a mono jack to be connected with signal on the tip and ground on the sleeve.

If I had wired the ring in parallel, it would be shorted to ground on a mono jack,

Why Are You Telling Me This?

That all works fine with Minstrels and mono and stereo cables, so I am used to using them interchangeably.

I normally have a mono plug to plug lead for loading from tape, but I have lost that somewhere.

Not a problem as I have loads of stereo jack to jack leads.

When I offered up the stereo jack plug, the issue became obvious.

The problem is where the mono jacks are connected. On most tape decks, like the Sony TCM-818 I am using, the green jack connects to the tip and sleeve and does not connect to the ring. That seems the best option.

The TS1500, like the ZX81 and ZX Spectrum, use a different style of jack plug which I hadn't really considered before, but when you look at how the connections line up with stereo and mono jacks, you can see the problem.

There are only two connections. The signal input is on the tip, which is fine, but the position of the ground connection is where the ring is normally connected.

That means the tape is connecting ground to the sleeve wire and leaving the ring disconnected, but the TS1500 is ground referenced to the ring and does not connect to the sleeve.

I must have loads of those standard black and grey mic and ear leads for the Sinclair machines, but could not see any of them, nor any mono jack plugs.

In the end, I made up this lead using a couple of mono jack to phono socket adapters and a phono to phono lead.

Not ideal, but it worked fine as the sleeve of the mono jack extends to where the TS1500 connects to, and where the TCM-818 connects to and we can load games.

Yay, success.

Minstrel ZXpand II

Of course, there is another option.

The TS1500 has a standard ZX81 expansion edge connector.

I also recently released the Minstrel ZXpand II, which has onboard ROM, so no longer needs to connect a wire to the ROM select block, allowing it to be used on a ZX81 or TS1000 (with internal RAM upgrades) or the TS1500 (which already has 16K).

That means you can use the Minstrel ZXpand II with the TS1500 in several ways.

Edge Connector

The easiest option is the edge connector. This fits with the TS1500 and leaves access for the power and TV connectors.

However, it blocks the cassette connectors, which are lower than the RF jack.

The joystick is partly blocked by the power cable, so not ideal.

Minstrel Expansion Bus

The other option is to use one of the unpowered Minstrel Expansion Bus backplanes. These give four slots and a passthrough edge connector.

The Minstrel ZXpand II will then sit at the back and leave access to the RF, ear, mic and power sockets on the TS1500, and the joystick port on the Minstrel ZXpand II.

You also have four slots for various other expansion cards.

The Overkill Mega Bus

You could also combine a powered and unpowered Minstrel Expansion Bus and have 8 available slots, but also be able to power the TS1500 by an external power supply, if you prefer.

(you can't use the powered board directly as it clashed with the video and cassette jacks)

The Minstrel ZXpand II works fine with the TS1500, and you can load your usual favourite games.

Just remember that it is NTSC, so runs a lot slower than a PAL ZX81 which affects some BASIC games, most C or assembler ones are fine.

It also lacks support for some of the high resolution graphics modes that the Minstrel 3 supports.


All items are still available, and I can ship worldwide.

Including the Minstrel 2, 3 and Minstrel 4th (Jupiter Ace / ZX80 / ZX81 / Lambda 8300 / RC2014,) as available with built in keyboards.

Of to fit into a ZX81 case as a replacement mainboard.

All versions are available in kit form, or pre-built, and optionally the new Minstrel ZXpand II SD card and joystick interface.

Also the Minstrel Expansion Bus, powered and passive. If you want to add more cards, or do something like that shown in this post with the TS1500 and the Minstrel ZXpand II.

International shipping is a bit complicated at the moment, so if you want to order from outside the UK, please contact me using the contact link at the top of this page.

See here for more information:

Sorry to have to do this, and I know this is going to affect sales, but please understand it is the only option I have right now.

Stop Press!

I am going to do another batch of Mini PET 40/80s with keyboard, with a few updates. 

These are pre-order now, shipping worldwide in November. £450 kit / £500 built.

Available exclusively from me, contact me using the link at the top of the page to get a price for shipping and any duties, based on your address.

The Mini PET 40/80 internal PET replacement boards are also available now, £350 kit / £375 built.

Again contact me if interested.


Patreon

If you enjoy posts like this, you can support me via Patreon, and get access to advance previews of blog posts, and exclusive posts. Paid Patreon supports will be getting the third part shortly, and maybe a forth part three will follow if there is any interest

You also get progress updates on new projects and other behind the scenes updates, as well as access to my Patreon only Discord server for even more regular updates, and to discuss your own projects.