This month we've got some powerful programming techniques to offer, including a way to keep your disk files straight and a versatile method to modify or control BASIC directly from disk. But first, let's finish up our discussion of graphics and character drawing from the previous two columns.
One thing we haven't pointed out in our discussions of graphics and character-drawing machine language was that the code could be saved as an object code file to disk or tape. This would save a lot of time over the BASIC programs which POKE the machine language into memory. If you have access to a machine language monitor, such as Supermon, you can use its save command to write the machine language to disk. After the code has been POKEd into memory, use the appropriate command to save the regions of memory given in the following table for the line- and character-drawing routines:
|Drawing Routine||Address Of First Byte||Address Of Last Byte|
Adding A Byte
The addresses are given in decimal and hex, with hex being the value in parentheses. It is important to note that the address is given for the last byte. Some machine language monitors, such as Supermon, require the ending address to be one byte beyond the last byte of the machine language program. You must be sure to enter the address of the last byte plus 1. For example, the two commands to save the routines to disk with Supermon might be:
S "0 : L DRAW C 64", 08, C000, C3A8
S "0 : CH DRAW C 64", 08, C400, C793
If you don't have a machine language monitor at your disposal, you can use the following BASIC program to accomplish the same thing.
10 REM PROGRAM TO WRITE OBJECT FILE 20 INPUT "ADDRESS OF FIRST BYTE"; SA 30 INPUT "ADDRESS OF LAST BYTE"; EA 40 INPUT "FILE NAME"; NM$ 50 OPEN 1, 8, 2, NM$ + ", P, W" 60 PRINT #l, CHR$(SA - INT(SA/256) * 256); 70 PRINT #l, CHR$(INT(SA/256)); 80 FOR I = SA TO EA 90 PRINT #1,CHR$ (PEEK(I)); : NEXT 100 CLOSE 1
The program asks for the address of the last byte, though it would not hurt if you entered that address plus 1. This would simply save one extra byte in the object file. If you wanted to, you could save the code as one file instead of two. This would save some unused memory which lies between the two routines, but would simplify reloading the routines. To load the object code, use the command:
LOAD "filename", 8, 1 NEW
where you supply the filename for the object code. The NEW command is necessary to restore some BASIC pointers which are corrupted by the load.
Though the object code file can simplify and speed up loading of the machine language routines, the BASIC program version (which POKEs the machine code) still has one advantage. The BASIC program version can be transferred from disk to disk very easily by using the BASIC LOAD and SAVE commands. The reason this doesn't work with machine language programs is that the starting and ending addresses of the code are not automatically communicated between the LOAD and SAVE commands.
If you have some machine language routines of your own, there may be occasions where it would be useful to convert the object file to a BASIC program version with the machine language in DATA statements. If you do the conversion by hand, the process will be very slow and error prone. To assist in the task, I used Program 1 to generate the DATA statements containing the line-drawing and character-drawing routines. This program generates DATA statements intended for use with the loader shown in Program 2.
The program works by writing the DATA statements to disk in the form of a BASIC program. If you would like to adapt this program to your own use, here is a brief explanation of what is involved.
The format of a saved BASIC program on disk is very simple. It starts with two bytes which specify the load address of the code, low byte followed by high byte. For BASIC programs, this address should be $801. Thus, the first two bytes should be 1 followed by 8. Following the load address comes a copy of the BASIC program the same as it would be found in memory. This would consist of a sequence of BASIC statements, each with the format shown in the table below:
|3||Line number, high|
|4||Line number, low|
|5 To N-1||BASIC statements|
A link is a pointer to the beginning of the next line of the BASIC program. Actually, we do not need to be concerned with writing a valid link. The links are automatically recomputed each time a BASIC program is loaded. Following the link is the line number. Note that the two-byte line number is stored with the high byte first, followed by the low byte. Following the line number is the text of the BASIC line. This will be the same as the text you type for the line, except that the keywords (FOR, GOSUB, etc.) and arithmetic operators will be converted to single bytes called tokens. The end of the BASIC line is marked with a zero byte (that is, the ASCII value of the byte is 0). The end of a BASIC program is marked by two zero bytes following the last line. This means there will be three zero bytes at the end of a BASIC program.
The subroutine at line 300 is responsible for writing the link, line number, and the DATA token for each line. The subroutine at line 400 is responsible for outputting a number in the list which follows the DATA keyword. This list will consist of eight numbers which specify object code bytes, followed by the sum of the previous eight numbers. This sum is checked automatically by the loader (Program 2). An error and a line number will be printed if the sum doesn't match.
To prepare a finished program, run Program 1. Next, load Program 2 and list it to the screen. Then load the DATA statements program generated in the first step. Finally, cursor to each line of the listed program and press RETURN. This will combine the DATA statements with the loader program.
If you wish, you can adapt Program 1 to write DATA statements which contain data other than object code. The advantage of saving data in this manner is that the data can be easily examined from BASIC. To make use of the data, it will have to be combined with the appropriate program. This could be accomplished with another BASIC program as well, using the techniques described for adding the DATA statements to Program 2.
Now let's take a look at a couple of utility programs that you may find useful. The first computes a cyclic redundancy check, CRC for short, on the data in a file. This may sound strange, but can be quite useful in cleaning up disk files and keeping program versions straight. The second utility allows BASIC to enter a BASIC program from a sequential disk file. This utility can add some powerful features to your BASIC programming.
Whenever data is transferred from one device to another, it's always a good idea to do something to verify that the data was transferred correctly. A simple method is called the checksum. Since the data is usually transferred in bytes, this method usually involves adding the bytes of data together to form a sum. This is done by both the receiver and the sender. Once the data has been sent, the sum is sent. The receiver compares this sum with the sum it computed. If the sums don't match, the data wasn't received correctly.
Though simple, the checksum method is not foolproof. If the sum the receiver receives matches the sum it computed, it does not guarantee that the data was received without error. There are several ways in which errors in the transmission won't show in the checksum. For one thing, it's possible for errors in different bytes to balance each other.
CRC's Work Better
When a better method is needed, the cyclic redundancy check is the one to turn to. The cyclic redundancy check (CRC) uses each bit of each data byte to compute the CRC value. As a result, a change in a single bit in the stream of bytes will have a significant change in the final CRC value.
You may think that you haven't used a CRC, but if you have the 1541 disk drive, you have been using it quite a lot. In fact, I don't know of any disk drive that doesn't go to the expense of computing a CRC to insure that data has been read from the disk correctly.
Where I work, we have a utility program which uses the CRC for a slightly different purpose. Over a period of time we tend to accumulate various versions of a file or program, spread among many diskettes. Often, many of the versions have the same filename. This can present problems when we're not sure which version we are dealing with on a particular disk. To handle this situation, we have a utility program which computes a CRC on the bytes in a file. By comparing the CRC computed for two different files, we can be certain if the two files are identical. Armed with the CRC for the most recent version, we can weed out the older versions with no problem.
To accomplish the same thing on the 64, you can use Program 1. This will load a small machine language routine which reads the data and computes the CRC. It loads into the cassette buffer, so it can be used as is only for disk files. Since this routine must return the CRC value, it must be called via the USR function. To link the routine to the USR function, enter the following in direct mode (no line number):
POKE 785,60: POKE 786,3
To obtain the CRC for a file, execute the following two commands:
OPEN l, 8, 2, "file, type, R" PRINT USR(O)
where file is the name of the desired file, and type is the associated file type, Sequential, Program, or User. The routine won't work on random access or relative files. It is important to use logical file 1, since the machine language routine is designed to read from that channel.
Now for the second utility. There are several methods for merging routines from one BASIC program into another. One way is to write a program which can merge two BASIC programs to form a third program. Another way, which is a little more flexible, is to have BASIC read the text from a file instead of the keyboard. I have worked with a couple of BASICs which can do this via an ENTER command. This not only allows you to merge a BASIC text file with the program already in memory, but allows you to enter BASIC text files that are transferred from other computer systems.
There is a way of fooling BASIC to input from the cassette instead of the keyboard while in command mode. Unfortunately this doesn't work when inputting from the disk. After inputting the first line and adding it to the program, BASIC makes a subroutine call which closes all serial bus channels. This prevents any further input from the disk file. If this subroutine call could be eliminated, we would be half way to making BASIC enter commands and program lines from the disk.
Fortunately, there is a solution: the submerged RAM. With RAM underneath the BASIC ROMs, we can copy BASIC to RAM and make any changes we want. Running Program 2 will load the required machine language routines into the cassette buffer.
Once the routines are loaded, you may enter a text file by executing the following two commands:
OPEN l, 8, 2, "file, S, R" SYS 828
where file is the name of the file to enter. It is assumed that the text file will be a Sequential file, though it could be a User file and work the same. It is important to use logical file 1, since the machine language routine inputs from that logical file.
If the text file you enter was generated by LISTing a program to a disk file, then the file will probably have a READY prompt at the end. This doesn't affect the entry of the program, though it will cause a SYNTAX ERROR message to be displayed when the READY prompt is encountered.
Listing BASIC To Disk
For those who haven't listed a BASIC program to disk, it's done approximately the same way as listing to a printer. For example, the following two commands will list lines 100–200 from the
BASIC program in memory to a disk file:
OPEN l, 8, 2, "file, S, W" : CMD 1 LIST 100–200 : PRINT #1 : CLOSE 1
where file is the name of the file in which to write the listing.
The routine works by first opening a channel to logical file 1. If this is successful, it then copies the BASIC ROMs to the RAM underneath. This task is simplified by the fact that writing to the ROMs will write the RAM underneath even though the ROMs are enabled.
Adding The Patches
Once copied, a couple of patches are made to the RAM copy of BASIC. The first patch modifies the subroutine call which is responsible for inputting a character. The subroutine call is modified to call our routine instead. The main difference in our routine is that it will fix things back to normal when the end of the file is reached. The second patch disables a subroutine call which closes all open channels on the serial bus when a line is added or deleted from a BASIC program in memory. The patch makes this subroutine call to go to a location which does an immediate return.
Once the patches are made, the BASIC ROMs are switched off. In addition, an input flag in page 0 is set to nonzero so that a carriage return will not be sent to the display as each line is input. When all this is complete, the setup routine returns to the patched version of BASIC. BASIC will think it is in command mode and begin inputting commands or program lines. BASIC is unaware that the text will be coming from a disk file instead of the keyboard.
When the end of the file is reached, the input file is closed, the BASIC ROMs are switched in, and the input flag is cleared. At this point, everything is back to normal, except that the text has been entered into the BASIC program. The result is exactly the same as if you'd typed it in yourself. This also means that lines that do not begin with a line number will be executed immediately instead of being added to the BASIC program.