Extending Commodore 64 BASIC
The original version of this post was published on March 16, 2014.
Back in the day, I used to write machine-language subroutines for the Commodore 64 that I would then call from a main program written in BASIC. I found it easier to use BASIC as the higher-order controller over a set of ML functions that usually did things for which CBM BASIC 2.0 was not well-suited.
In one case, I wrote an Xmodem file-transfer protocol handler. The routines to send data, receive data, read/write from/to files on disk, and computer checksums were all handled by machine-language routines.
I found it easier to experiment with the sometimes-touchy timing of the in-process handshakes using BASIC. ( Some Xmodem implementations on some BBS’s had personalities of their own. ) In my earliest programs, I would POKE addresses of data into predetermined spots in memory using BASIC and my machine language code would know where to look to pick up those parameters. Later, my friend Mike showed me a technique that allowed me to pass parameters right on the SYS command that invoked a particular machine-code routine.
While that technique worked well, I found myself memorizing numbers ( memory addresses or function indexes ) instead of some kind of mnemonic. The obvious answer was to change BASIC to incorporate my machine language routines as new commands. There were a few different techniques for doing this sort of thing. I just had to pick one. I experimented with a few and never really got around to using one I liked until I wrote the code below … 30 years later.
I decided to use the “@” symbol to identify my new commands. At this time, I am only adding imperative commands. I may address extended functions later, but there’s an old Transactor article that really covers that topic well. Each of my command names will only be a single alphabetic letter in length.
I have not seen the technique I’m using employed in the way I am using it. The traditional approach to implementing a “@” sentinel character followed by a single-character involves “wedging” the CHRGET routine at $0073. This allows you to “wedge” CHRGET to allow your own code to examine the input stream as BASIC is poring over the input text. This technique causes extra time to be spent over the life of the BASIC program’s execution. I wanted to try to pare this down a bit more.
I’ve called this program “the Plug-in BASIC kernel” as it is meant for budding assembly-language coders to tinker with their own BASIC dialects. I’ve implemented only two commands to demonstrate how to use the kernel. The commands are “@C” ( clear the screen ) and “@B” ( change the border, background, and foreground text colors. )
The current copy of this code can be found at: https://github.com/jimlawless/plugin-basic
// Plug-in BASIC Kernel
// Copyright (c) 2014, 2023 by
// Jim Lawless - jimbo@radiks.net
// MIT / X11 license
// See: https://github.com/jimlawless/plugin-basic/blob/main/LICENSE
//
// Written using KickAssembler
* = $c000
.encoding "petscii_mixed"
.var chrget = $0073
.var chrgot = $0079
.var snerr = $af08
.var newstt = $a7ae
.var gone = $a7e4
.var strout = $ab1e
jsr intromsg
lda #<newgone
sta $0308
lda #>newgone
sta $0309
rts
// table for commmands
table:
.word notimp-1 // @a
.word do_border-1 // @b
.word do_cls-1 // @c
.word notimp-1 // @d
.word notimp-1 // @e
.word notimp-1 // @f
.word notimp-1 // @g
.word notimp-1 // @h
.word notimp-1 // @i
.word notimp-1 // @j
.word notimp-1 // @k
.word notimp-1 // @l
.word notimp-1 // @m
.word notimp-1 // @n
.word notimp-1 // @o
.word notimp-1 // @p
.word notimp-1 // @q
.word notimp-1 // @r
.word notimp-1 // @s
.word notimp-1 // @t
.word notimp-1 // @u
.word notimp-1 // @v
.word notimp-1 // @w
.word notimp-1 // @x
.word notimp-1 // @y
.word notimp-1 // @z
newgone:
jsr chrget
php
cmp #'@'
beq newdispatch
// not our @ token ... jmp back
// into gone
plp
// jump past the JSR chrget call in gone
jmp gone+3
newdispatch:
plp
jsr dispatch
jmp newstt
dispatch:
jsr chrget
cmp #'a'
bcs contin1
jmp snerr
contin1:
cmp #'z'+1
bcc contin2
jmp snerr
contin2:
sec
sbc #'a'
cmp #'z'
asl
tax
lda table+1,x
pha
lda table,x
pha
jmp chrget
msg:
.text "plug-in basic command not implemented..."
.byte 0
notimp:
ldy #>msg
lda #<msg
jmp strout
intromsg:
ldy #>imsg
lda #<imsg
jmp strout
imsg:
.text "plug-in basic kernel v 0.02a"
.byte $0d
.text "by jim lawless"
// please add your vanity text here for any
// customizations you make
.byte 0
// syntax
// @c
// clear the screen
do_cls:
lda #147
jmp $ffd2
// syntax
// @b border,backgnd,char
// set border, background, and
// character color
do_border:
jsr $b79e // get byte into .x
stx $d020 // set border
jsr $aefd // skip comma
jsr $b79e // get byte into .x
stx $d021 // set background
jsr $aefd // skip comma
jsr $b79e // get byte into .x
stx $286 // set text color
rts
Here’s how the program works.
Instead of wedging CHRGET, the vector to GONE is changed. GONE is the routine that executes the next BASIC program token. The values at locations $308 and $309 are changed so that my newgone subroutine will first take a look at the next token.
lda #<newgone
sta $0308
lda #>newgone
sta $0309
rts
If that next token happens to be an “@” character, a custom dispatch routine. Otherwise, a branch three bytes past the original GONE routine occurs, popping the processor status from the original CHRGET call.
newgone:
jsr chrget
php
cmp #'@'
beq newdispatch
// not our @ token ... jmp back
// into gone
plp
// jump past the JSR chrget call in gone
jmp gone+3
In the new dispatch routine, the code checks to see if the next character is in the range of ‘a’ through ‘z’ inclusive. If so, the letter will be translated to a value in the range of 0 to 25 inclusive. It would then be doubled and used as an offset into a table of machine-language routine addresses.
newdispatch:
plp
jsr dispatch
jmp newstt
dispatch:
jsr chrget
cmp #'a'
bcs contin1
jmp snerr
contin1:
cmp #'z'+1
bcc contin2
jmp snerr
contin2:
sec
sbc #'a'
cmp #'z'
asl
tax
lda table+1,x
pha
lda table,x
pha
jmp chrget
In order to change the code to add your own commands, point the entry in this table to your own routine’s address:
// table for commmands
table:
.word notimp-1 // @a
.word do_border-1 // @b
.word do_cls-1 // @c
.word notimp-1 // @d
.word notimp-1 // @e
.word notimp-1 // @f
.word notimp-1 // @g
.word notimp-1 // @h
.word notimp-1 // @i
.word notimp-1 // @j
.word notimp-1 // @k
.word notimp-1 // @l
.word notimp-1 // @m
.word notimp-1 // @n
.word notimp-1 // @o
.word notimp-1 // @p
.word notimp-1 // @q
.word notimp-1 // @r
.word notimp-1 // @s
.word notimp-1 // @t
.word notimp-1 // @u
.word notimp-1 // @v
.word notimp-1 // @w
.word notimp-1 // @x
.word notimp-1 // @y
.word notimp-1 // @z
I have assembled the reference code to the common address $c000 (49152). To load the demo kernal, type:
load "plugin.prg",8,1
and then, type
sys 49152
…to activate it.
Let’s change the colors with the new @B command. The @B command expects three numeric-expression parameters: the border color, the background color, and the text color.
Now, let’s exercise the “@C” command to clear the screen. ( I find this one to be very handy. )
If you try a command that is not implemented, you’ll get a warning message, but it won’t generate a syntax error unless you’ve followed it with other parameters.
These commands can be incorporated into BASIC programs themselves. Let’s try changing the border and text colors from values 1 to 10 in a FOR loop:
You can download the source, the .prg file, and the D64 disk image containing plugin.prg and pbdemo at:
https://jimlawless.net/downloads/pluginbasic.zip
I’ve provided the above as something for others to tinker with.