Note: I made a few corrections in red. Most notably the placement of the utility in memory. See below. The disk image has been corrected.
I like to make utilities for BASIC. Something that helps me do something a little easier than would normally be possible in a vanilla version of Commodore V2 BASIC. I’ve written a graphic wedge, petscii plotter, explored how to use USR() differently. One of the things about that is I write the utility in an assembler and then often convert them into BASIC, DATA statements to load into my BASIC programs.
For the most part, this is not a big deal if the program is small enough. But once the programs get large, like the graphic wedge, there is a lot of room for error, and honestly, who wants to transcribe 3K of memory into data statements.
In the past, I’ve written a BASIC program to PEEK into memory and print up line numbers that get entered into the program and it works great. The problem is you need to type in the basic program into your existing program you want to add the data statements into and then erase it when it’s done. It would be much better to have a machine language routine that would do this for you. That’s what I set out to do.
My first attempt at this was to just build onto the BASIC program in memory by tacking on to the end of the BASIC program. I started by pulling the data from the ‘start of variable’ pointer located at 45, 46 ($2d, $2e) and coding my basic lines directly into memory. Even though I had some initial success, it proved to be very complicated as I seemed to be missing some details that would cause the editor to crash randomly after the data statements were written. I’m sure I didn’t get all the pointers set correctly when the routine was done. The other interesting thing that happened was that no matter what line numbers I decided to use for the data statements, they were always at the end of the BASIC program. This makes sense because when editing normally, BASIC, will insert your program line correctly at the time you hit enter. My routine was just placing the data statements at the end of the program regardless of the line numbers. It also wouldn’t care if you repeated line numbers. Just for fun, I had all my data statements numbered 0 as well. No issues with that, until I wanted to do some regular editing of the program and then things went crazy pretty quickly.
So I decided to go the same route as I did with my BASIC version of this utility where I would print program lines on the screen, fill the keyboard buffer up with carriage returns and place the cursor on the printed program lines before handing control back to the editor. The editor would essentially hit return over the printed lines, entering them into the program. The last line was a RUN statement that started the program up to print up the next set of lines. My machine language routine does the same thing except it only does one line at a time and uses an SYS statement to get back into the routine.
The next issue was how to take a byte in memory and turn it into a decimal number to be printed on the screen. I had the added difficult in that since I was doing this on the screen, I need to do both 16 bit number (for the BASIC line numbers) and 8 bit numbers for the data statements.
After doing some research, I found a solution from and article written by Jim Butterfield in the July 1983 issue of COMPUTE! magazine. In the article he uses BCD mode to convert 16 bits into decimal numbers ready for output to the screen. I’ve had to modify his code a little to make it work for my needs, but it was just what I needed to make it happen.
The program is a little longer than I had hoped. I wanted to fit it in the cassette buffer located at 828. I didn’t want to put it at 49152 because many of the utilities I write end up there. I ended up taking 512 bytes from the top of BASIC ram which feels relatively safe. The program adjusts the pointer at 55, 56 to make the space unavailable to BASIC.
Using the Program
To use the program you will need to follow these steps:
First, load your machine language routine followed by this utility. You must load it using load”datamaker”,8,1 to place it in the correct memory location. Once it’s loaded, perform a NEW command right away followed by SYS 32256 SYS 40448( Messed this up. I put it 512 bytes before 32768 not 40960 – this has been corrected). This will set the pointers for BASIC memory and the USR() function. Next, load your BASIC program that you want to add the data statements to.
I’ve added a second program to the image called datamaker.bas which is a BASIC loader for the utility. Simply load and run the program and it will install and start the utility for you as well has give you some instructions. (I used the utility to create the data statements in this program.)
With all of that loaded, write the USR() function in direct mode. Here’s what it should look like (the Z is arbitrary):
Z = USR(start)(end)(line_number)
Start is the beginning of your machine language routine, end is the end of your machine language routine, and line number is the starting BASIC line number for your data statements.
So making data statements for a routine located at 828 to 900 with your data statements starting at line number 1000, the command would look like this:
Z = USR(828)(900)(1000)
There is a caveat to this. If your memory locations or your line numbers are equal to or above 32768, you’ll need to enter them a little differently. This is because commodore BASIC will treat your numbers as signed integers with a value between -32768 and +32767. It’s not a huge issue. If your values are above 32767, enter them like this (49152 to 49500 as the example)
Z = USR(49152-65536)(49500-65536)(1000)
This will give you the correct unsigned 16 bit integer in the ml routine.
Once you hit enter, the program will clear the screen and print a program line at the top, followed by a SYS statement. It will then hand control back to the editor with a home key and two enters key presses in the keyboard buffer to make the editor enter the program line into your program. It with then hand control back to the utility.
Once the utility runs through all of the memory specified in the USR() function, it adds a -1 to the data statements and terminates. The only thing left is to write a loader in your BASIC program to read the data and poke it in the appropriate memory. A simple loader looks like this: (routine located at memory 828)
10 m=828
20 read b:if b=-1 then end
30 poke m,b
40 m=m+1
50 goto 20
Below is a full listing of the program. It was written for Kick Assembler.
/* DataMaker
Routine for making data statements of machine language
routines. 2020 Defiance Studios with a lot of help from a Jim
Butterfield's article 'Numberic Output, Part III' in COMPUTE!
Magazine, July 1983.
SYS 32256 sets up the USR() function
Z = USR(start)(end)(line_number)
start = starting address of your ML routine
end = ending address of your ML routine
line_number = starting BASIC line number for your data statements
NOTE: Numbers over 32767 need to have 65536 subtracted from their
value because of how BASIC handles signed integers.
example (below 32768)
Z = USR(828)(900)(1000)
example (above 32767)
Z = USR(49152-65536)(49300-65536)(1000)
*/
.label chrout = $ffd2 // Kernal print routine
.label keybuf = $0277 // keyboard buffer
.label frmnum = $ad8a // get and evaluate data for type mismatch
.label usradd = $0311 // pointer for USR funtion for ml
.label fac2ya = $b1aa // fac to int in y(lo) a(hi)
.encoding "petscii_upper" // encoded to use CHR$ codes and not Screen codes
*=$9e00 "Setup" // SYS 40448
Setup:
lda #<Start // get the start address of our USR routine
ldy #>Start // lo and hi
sta usradd // put the address in location 785 & 786
sty usradd+1 // so BASIC knows what to do with USR
dec $38 // reduce memtop by 512 bytes
dec $38
jmp $a65e // and run a clr statement
* = * "Main Routine"
Start: // USR() data starts as float in fac
jsr fac2ya // grab float from fac and
sty $fb // convert to 16bit signed integer with
sta $fc // lo in .y and hi in .a. Store in memory
jsr frmnum // grab data after USR() and put in fac
jsr fac2ya // grab float from fac and
sty mlEnd // convert to 16bit signed integer with
sta mlEnd+1
jsr frmnum // grab data after USR() and put in fac
jsr fac2ya // grab float from fac and
sty basicS // convert to 16bit signed integer with
sta basicS+1
clc // add 2 to mem end so it doesn't
lda mlEnd // come up short. More graceful way to
adc #$02 // do this I'm sure.
sta mlEnd
lda mlEnd+1
adc #$00
sta mlEnd+1
S2:
lda #$93 // clear the screen
jsr chrout
lda #$00 // set line counter to zero
sta lLength
LineNum:
lda basicS // get line number and
sta work // put it into the hi/lo
lda basicS+1 // work locations for
sta work1 // Jim's routine.
ldy #$00
jsr A1 // Jump to Jim's routine.
dataStatement:
ldx #$00 // create an index to write
L1: // 'DATA' after the line number
lda data,x // and leave a space
beq StartDecode
jsr chrout
inx
jmp L1
StartDecode: // grab a byte to be turned into
ldy #$00 // a DATA statement. $fb,$fc point
lda ($fb),y // to the current byte.
bne notZero // Check to see if byte is Zero
lda #$30 // if it is, just print the zero and
jsr chrout // increase line length by two
inc lLength // and skip Jim's routine.
inc lLength
jmp nZPass
notZero: // if not zero, load work with the
sta work // byte and work1 with zero since
lda #$00 // we are not working with a 16 bit number
sta work1
jsr A1 // and go to Jim's routine
nZPass:
lda lLength // check to make sure the line is not
cmp #23 // too long. If it is, time for a new line.
bcs NewLine
lda #$2c // if not, print a comma and go to
jsr chrout // the next momory.
jsr IncMem
cmp mlEnd+1 // check to make sure the current memory
bcc StartDecode // is less than the end memory.
lda $fb // hi byte first, then low
cmp mlEnd // add next byte if still <mlEnd
bcc StartDecode
lda #$2d // if we hit the end of our ml, print
jsr chrout // a -1 in the data statement to mark
lda #$31 // the end of data and skip writing
jsr chrout // the SYS statment afterwards.
jmp skipSys
NewLine: // Data line ready for processing, first
lda #$0d // go to the next line an increase the
jsr chrout // basic line number for the next line as
jsr IncBAS // well as the byte to processed.
jsr IncMem
ldx #$00
L2:
lda sys,x // print the SYS command after the program line
beq EL2 // to get back into the machine code after line
jsr chrout // processing.
inx
jmp L2
EL2:
lda #<S2 // this prints the number to SYS to. This is
sta work // soft coded so that this program can easily be
lda #>S2 // assembled in other locations without having
sta work1 // to manage this
jsr A1 // work and work1 are set and Jim's routine is called.
skipSys:
lda #$0d // if at the end of ML data, SYS is skipped to end the
jsr chrout // process. See above in nZPass where skipped from. A carriage
lda #$13 // return is added here in case USR() was called with a print
sta keybuf // statement to get it to the next line, then the
lda #$0d // keyboard buffer is filled with a home key, followed
sta keybuf+1 // by two carriage returns and the keyboard que is set to 3.
sta keybuf+2 // control is then handed back to the BASIC editor where
lda #$03 // the cursor has been set to the data line and enter was hit
sta $c6 // twice to enter the line and activate the SYS statement to
rts // get back into the ML routine.
/* This is a modified numeric output routine from
Jim Butterfield, published in July 1983 COMPUTE!
magazine, titled Numeric Output Part III.
input routine modified for this program */
A1:
lda #$00 // clear output area
sta out1
sta out2
sta out3
sta zsup
ldx #15 // get ready to move 16 bits out of work
B:
asl work // move bit out of work
rol work1
sei // disable IRQ and
sed // switch to decimal mode
lda out1 // move bit (decimally) into out
adc out1
sta out1
lda out2
adc out2
sta out2
lda out3
adc out3
sta out3
cld // clear decimal mode and
cli // enable IRQ
dex // repeat for next bit
bpl B
ldx #2 // prepare to output 3 bytes (6 digits, 16 bit numbers)
C:
lda out1,x // get bytes, high order first, for output
pha
lsr // output high order digit
lsr
lsr
lsr
jsr put
pla
and #$0f // get low order digit
jsr put
dex // get next byte
bpl C
rts
put: // zero supress routine
cmp zsup
bne D
rts
D: // convert numeric, kill zero suppression
ora #$30
sta zsup
E:
inc lLength // increase line length by one
jmp chrout // output character
// End Jim Butterfield routine
IncMem: // Add routine to increment the memory
clc // location of the current byte to be
lda $fb // written into the data statement.
adc #$01
sta $fb
lda $fc
adc #$00
sta $fc
rts
IncBAS: // Add routine to increment the BASIC
clc // line number by two for data statements.
lda basicS // This can be changed to any increment
adc #$02 // desired by the user here.
sta basicS
lda basicS+1
adc #$00
sta basicS+1
rts
// Variables used by this program
work:
.byte $00
work1:
.byte $00
out1:
.byte $00
out2:
.byte $00
out3:
.byte $00
zsup:
.byte $00
basicS:
.byte $00,$00
mlEnd:
.byte $00,$00
lLength:
.byte $00
data:
.text " DATA "
.byte $00
sys:
.text "SYS "
.byte $00