Commodore computers up to BASIC 2.0 (like the Commodore 64, the VIC-20 and the PET 2001) only had a very basic understanding of mass storage: There were physical device numbers that were mapped to the different busses, and the “KERNAL” library had “open”, “read”, “write” and “close” functions that worked on these devices. There were also higher-level “load” and “save” functions that could load and save arbitrary regions of memory: The first two bytes of the file would be the (little endian) start address of the memory block.
With no special knowledge of “block storage” devices like disk drives, BASIC 2.0, which was not only a programming laguage but basically the shell of Commodore computers, could not express commands like “format a disk”, “delete a file” or “show the directory”. All this functionality, as well as the file system implementation, was part of the firmware of the disk drives.
Sending a Command
Sending commands to the drive was done by using the “open” call with a “secondary address” of 15: The computer’s KERNAL just sent the file name and the secondary address over the IEC bus as if it were to open a file, but the floppy drive understood secondary address 15 as the command channel. So for example, deleting a file from BASIC looked like this:
OPEN 1,8,15,"S:FOO": CLOSE 1
“1” is the KERNAL’s file descriptor, “8” the device number and “15” the secondary address. Experts omitted the close, because it blocked on the completion of the operation.
Getting Data Back
While the “OPEN” line for disk commands was pretty verbose, it was still doable. Getting the error message of the last operation back was more tricky: It required a loop in BASIC that read bytes from channel 15 until EOF was reached.
Getting a directory listing would be in the same class of problem, since it requires the computer to send a command (and a file name mask) to the floppy and receive the data. Neither BASIC nor KERNAL knew how to do this, and since this was such a common operation, it wouldn’t have been possible to have the user type in a 4 line BASIC program just to dump the directory contents.
The BASIC Program Hack
Here comes the trick: If the program to load started with a “$” (followed by an optional mask), the floppy drive just returned the directory listing – formatted as a BASIC program. The user could then just “LOAD” the directory and “LIST” it if it were a BASIC program:
LOAD"$",8 SEARCHING FOR $ LOADING READY. LIST 0 "TEST DISK " 23 2A 20 "FOO" PRG 3 "BAR" PRG 641 BLOCKS FREE.
In this example, “TEST DISK” is the disk name, “23” the disk ID and “2A” the filesystem format/version (always 2A on 1540/1541/1551/1570/1571 – but this was only a redundant copy of the version information which was never read and could be changed). There are two files, 20 and 3 blocks in size respecively (a block is a 256 byte allocation unit on disk – since blocks are stored as linked lists there are only 254 bytes of payload), and both are of the “PRG” type.
Encoding of Commodore BASIC Programs
The floppy was aware of the encoding that Commodore BASIC (a derivative of Microsoft BASIC for 6502) used and prepared the directory listing in that way. A BASIC program in memory is a linked list of lines. Every line starts with a 2-byte pointer to the next line. A 0-pointer marks the end of the program. The next two bytes are the line number, followed by the zero-terminated encoded line contents.
The LIST command decodes a BASIC program in memory by following the linked list from the start of BASIC RAM. It prints the line number, a space, and the line contents. These contents have BASIC keywords encoded as 1-byte tokens starting at 0x80. Character below 0x80 are printed verbatim. Here is what 10 PRINT"HELLO WORLD!" would look like:
0801 0E 08 - next line starts at 0x080E 0803 0A 00 - line number 10 0805 99 - token for PRINT 0806 "HELLO!" - ASCII text of line 080D 00 - end of line 080E 00 00 - end of program
The example directory listing from above would be encoded by the floppy like this:
0801 21 08 - next line starts at 0x0821 0803 00 00 - line number 0 0805 '"TEST DISK " 23 2A ' 0820 00 - end of line 0821 21 08 - next line starts at 0x0821 0823 14 00 - line number 20 0825 ' "FOO" PRG ' 0840 00 - end of line [...]
A couple of things are interesting here:
- The line with the disk name and the ID is actually printed in inverted letters, which is done by having the “revert” character code as the first character of the first line, i.e. the floppy makes the assumption that the computer understands this convention.
- BASIC will print the file sizes as variable-with line numbers, so the floppy adds extra spaces to the beginning of the line contents to have all file names aligned.
- The floppy needs to populate the next line pointers for the linked list.
The Link Pointer
The obvious question here is: How can the floppy know where in the computer’s memory the BASIC program will live? The answer is: It doesn’t. The BASIC interpreter supports having its program anywhere in memory, and loading programs that were saved from other locations on memory – or possibly other Microsoft BASIC compatible computers with a different memory layout. The VIC-20 had BASIC RAM at 0x0401, the C64 at 0x0801 and the C128 at 0x1C01. Therefore, BASIC “rebinds” a program on load, searching for the zero-terminator of the lines and filling the (redundant) link pointers.
The floppy therefore only has to send non-zero values as the link pointers for BASIC to accept the directory listing as a program. In fact, a 1541 sends the directory with a 0x0401-base, which would be valid on a VIC-20. The reason for this is that the 1541 is only a 1540 with minor timing fixes for C64 support, and the 1540 is the floppy drive that was designed for the VIC-20.
Therefore, if you do LOAD"$",8,1 on a C64, the extra “,1” will be interpreted by the KERNAL LOAD code to load the file at its original address (as opposed to the beginning of BASIC RAM), and since there is screen RAM at 0x0400 on the C64, garbage will appear on the screen, because the character encoding of screen ram is incompatible with BASIC character encoding.
Directory Code in 61 Bytes
There are two problems with this “directory listing is a BASIC program” hack: Listing the directory overwrites a BASIC program in RAM, and listing the directory from inside an application is non-trivial.
Therefore, many many implementations to show a directory listing exist on the C64 – and I want to present my own one here, which is, to my knowledge, the shortest existing (and maybe shorted possible?) version. It is based on a 70 byte version published in “64’er Magazin” some time in the 80s, and I managed to get it down to 61 bytes.
,C000: A9 01 LDA #$01 ; filename length ,C002: AA TAX ,C003: A0 E8 LDY #$E8 ; there is a "$" at $E801 in ROM ,C005: 20 BD FF JSR $FFBD ; set filename ,C008: A9 60 LDA #$60 ,C00A: 85 B9 STA $B9 ; set secondary address ,C00C: 20 D5 F3 JSR $F3D5 ; OPEN (IEC bus version) ,C00F: 20 19 F2 JSR $F219 ; set default input device ,C012: A0 04 LDY #$04 ; skip 4 bytes (load address and link pointer) ,C014: 20 13 EE JSR $EE13 ; read byte ,C017: 88 DEY ,C018: D0 FA BNE $C014 ; loop ,C01A: A5 90 LDA $90 ,C01C: D0 19 BNE $C037 ; check end of file ,C01E: 20 13 EE JSR $EE13 ; read byte (block count low) ,C021: AA TAX ,C022: 20 13 EE JSR $EE13 ; read byte (block count high) ,C025: 20 CD BD JSR $BDCD ; print 16 bit integer ,C028: 20 13 EE JSR $EE13 ; read character ,C02B: 20 D2 FF JSR $FFD2 ; print character to stdout ,C02E: D0 F8 BNE $C028 ; loop until zero ,C030: 20 D7 AA JSR $AAD7 ; print carriage return character ,C033: A0 02 LDY #$02 ,C035: D0 DD BNE $C014 ; skip 2 bytes next time (link pointer) ,C037: 20 42 F6 JSR $F642 ; CLOSE ,C03A: 4C F3 F6 JMP $F6F3 ; reset default input device
(There is a similar implementation here.)
There are two limitations of this code though: It omits the extra space between the block number and the filename, leading to a slightly different output, and it cannot be interrupted.
20 thoughts on “LOAD"$",8”
An excellent and very interesting article.
Brought back a lot of old memories from the days, long before I had any form of internet connection, when I was myself investigating these very topics on my old C64, using the trusty old TFC-III monitor (wich conveniently enough had build in simple commands to comunicate with the 1541 and also display the directory without having BASIC-RAM destroyed).
Back then information was not easy to come by, but by exploring and testing a lot and eagerly reading the few articles on assembler and machine-coding that every now and the appeared in the old swedish magazine “DatorMagazin”, plus some disks with Public Domain software compiled by and bought from the same magazine, I actually managed to make myself a nice text-scroller that used sprites to display text in the bottom border while displaying a static background and playing music. It also had some raster-bars in the side borders.
I should really get the old 64 up from the basement some day and continue my journey inside it. Maybe it’s not to late to create that masterpiece demo I always wanted to. :-)
I wonder if my old disks will still read ok …
“All this functionality, as well as the file system implementation, was part of the firmware of the disk drives.”
This maybe isn’t quite as crazy as you make it sound, since the disk drive had the same amount of processing power (~1MHz 6502-class CPU) as the computer itself. :-)
Hah! I remember my first faltering steps at computing on a C64…
I had finished entering my first major program, and wanted to save it, so I inserted a disk to “just take a quick look” at the directory to see where I might save the program. Satisfied that the disk had ample free space, I decided to confirm that the program listing was ready for storage.
I typed ‘L’, and as I held shift (yes, I’d already learned a few cool tricks), it dawned on me that I had just used the same command to look at a directory listing. Although I wasn’t able to understand the finer points (as this article illustrates) at the time, I was able to make the connection that only one listing (of whatever type) could be held in memory at a time.
I never lost another program listing that way. :-)
Nice, but where were you 18 years ago?
I mean 28 years ago. geez, I’m old.
0821 21 08 – next line starts at 0x0821
Of course, this is wrong. The next line will not start at $0821. More probably, it would be:
0821 41 08 – next line starts at 0x0841
It seems to be a copy-n-paste error, though.
Furthermore, where did you get the line length from? With my test images, I always get $081F as first link pointer, not $0821. And counting it by hand, it makes sense. Did you count the two extra “‘” characters you added for marking the string?
“The VIC-20 had BASIC RAM at 0Ă0401”
Well, not really. An unexpanded VIC-20 has BASIC RAM at $1001, a VIC-20 with 3 KB RAM extension had it at $0401. If we had more than 3 KB RAM extension, BASIC start was at $1201.
(cf. http://www.zimmers.net/cbmpics/cbm/vic/memorymap.txt or the disassembly of RAMTAS in the VIC-20 at $FD8D-FDDC).
The hard-coded $0401 is there because of the fact that PETs were not able to relocate BASIC. That is, they could not fix the link pointer themselves. And in the PET, BASIC began at $0401.
The 1541 is a 1540 with timing fixes, right. And a 1540 is just a 2031LP with the (parallel) IEEE 488 bus removed and replaced by the (serial) Commodore bus. The ROMs are almost the same.
The 2031 and the 2031LP is a descendant of the 4040, and the 2xxx and 4xxx drives were all designed for the PETs. And the PETs had BASIC start at $0401, and they could not relocate.
Thus, the floppy generates valid linking, for the case that BASIC starts at $0401.
> “Nice, but where were you…”
Not sure if this is directed at me, but perhaps my comment inadvertently gives the impression I was just using the C64 recently. I meant when I was between 8 and 10 years old (my earliest computing experience). Looking back, I can’t believe I understood that stuff as well as I did (eventually). I think I was a lot smarter back then. :-)
Strik is nearly correct about $0401. PETs could fix the link pointer chain (and I think they even did) but they would not load a program at a different address than where it was saved from. So the directory listing had to have $0401 as start address. (The first PET rom even saved programs from $0400 onward, which was not a problem for newer PETs but would be for VIC-20, 64, etc due to the extra initial byte).
Also: the “0” in the first line, before the disk name, is the drive number. Always 0 on single drives, but could of course be 1 on double drives.
that listing is so wrong on multiple levels :) typical example of “lets optimize it to death” … and here is why:
– no error checking or handling whatsoever
– direct calls into kernal, will not work with one of the various kernal replacements
– even worse, direct calls into basic. thats just yuckidiyuck =P
you can do a proper routine without these flaws in just a few bytes more, no need to use something that stops working on half of the existing c64s :)
is much better already, with the only problem of calling bdcd – which can easily be avoided too.
What a weird and cool feeling, to finally learn the answer to a question I’m not sure I even asked, way back then! The C64 was my second computer, a TI 99/4A having been the first, and since it was such a superior gaming platform, I actually did *less* programming on it than I did on the TI.
Nonetheless, the oddities of 1541 communication were ever-present, and the explanation presented here really made me grin. Thank you!
I read this article completely concerning the resemblance of hottest and preceding technologies, it’s awesome article.
I just copied the routine into SMON monitor, running under VICE emulator – double-checked everything, but the procedure doesn’t work at all.
;BUT PERFECTLY COMPILABLE WITH ACME …. (acme.exe = 229902 bytes)
!to “readfile.prg”, cbm ; set output file and format
*= $801 ; set program counter
; encode SYS 2064 line in BASIC program space
lab2064 jmp main
main lda #$01 ; filename length
ldy #$e8 ; there is a “$” at $E801 in ROM
jsr $ffbd ; set filename
sta $b9 ; set secondary address
jsr $f3d5 ; OPEN (IEC bus version)
jsr $f219 ; set default input device
ldy #$04 ; skip 4 bytes (load address and link pointer)
readloop jsr $ee13 ; read byte
bne readloop ; loop
bne fileclose ; check end of file
jsr $ee13 ; read byte (block count low)
jsr $ee13 ; read byte (block count high)
jsr $bdcd ; print 16 bit integer
mainloop jsr $ee13 ; read character
jsr $ffD2 ; print character to stdout
bne mainloop ; loop until zero
jsr $aaD7 ; print carriage return character
bne readloop ; skip 2 bytes next time (link pointer)
fileclose jsr $f642 ; CLOSE
jmp $f6f3 ; reset default input device
I’m wondering if you agree to accept a little contest:
Who writes the shortest BASIC directory lister.
…using regular OPEN and GET# no SYS or any m.c. routines.
when the basic prg is done we execute a CLR then save the BASIC to a .prg file with CCS emulator.
So … who’s prg will be the shortest ? …
Ahh, the listing must be identical with the one obtained with LOAD”$”,8 !
Do not post the listing here yet only the size of the .PRG.
We will post the listings all together when everybody finished the lister.
This is a very nice thread indeed!
I have to concur with ‘gpz’ – although I see call to BASIC a far lesser evil (hardly anyone replaced the BASIC ROM) than e. g.
,C003: A0 E8 LDY #$E8 ; there is a “$” at $E801 in ROM
No, there is not. Neither in my main computer nor in countless other with custom KERNALs. I remember bashing my good old friend Polonus back in the days for doing something like that, which of course immediately crashed on my machine.