PET 2001 Emulator — V. 1.1
Proudly announcing version 1.1 of the PET 2001 online emulator.
On the bright side of staying at home, there is the realization, how much there is to do, since there isn’t much to do (else). To illustrate the oxymoron and for the benefit of any users, I revamped the mechanism for mounting and loading files of the PET 2001 online emulator.
Features
First things first, what are the benefits of this revamp?/p>
- Now, first, there’s now support for accessing disk and tape images from BASIC, including directory listings. (T64 tape archives are handled transparently as floppies mounted in device 8.)
- Secondly, operations are much more like on the real thing and you can
LOAD
a specific file from inside BASIC. - Thirdly, as this enables BASIC programs to load overlays, this also adds support for an entire class of multi-file programs. (However, there’s still no support for
OPEN
andCLOSE
and the related commandsPRINT#
,INPUT#
, andGET#
.) - Finally, this allows to add another bit of comfort regarding file handling. E.g., the tool bar adapts nicely to the type of media mounted currently and provides suitable feedback. There’s now a well behaved “Mount” button instead of the generic file-upload and a custom file display actually reflects the mounted media and its type. (This works both for mounting files via the system file dialog and for drag and drop: the file display will always tell you, to which kind of media a
LOAD
command will refer to.) - Moreover, clicking the new media icon next to the file name will trigger an appropriate action to activate the respective file or archive. For a single file, this will trigger a prompt for loading and running the respective file after a reset. The kind of load to be performed (BASIC or binary) will be determined from the file type. For a disk or tape archive, this will bring up the directory dialog already known from the “Directory” button.
BASIC Operations
Principally, any LOAD
operations are respective to the currently mounted media. Technically, a single file format doesn’t have a file name, as far as the emulator is concerend, and any name will do. If no media is mounted, the emulator will fall back to a tiny dummy file already found in Thomas Skibo’s original version:
100 REM HI
The most prominent new feature is probably the support of disk directories (also implemented analogously for tape archive). This includes all the usual options and the full syntax, as there are
- Drive numbers as in “
$0:
”. This must always come first and addresses a specific drive (0 or 1 in order to address dual drive units). The drive number is here ignored, as there’s always just a single drive emulated. - Wildcards: “
?
” (any character) and “*
” (any number of characters) are supported, as well as alternative search strings (separated by “,
”).
E.g, “$A*,Z?RO
” will list any files either beginning with “A
” or matching the expression “Z?RO
” (like “ZERO
” or “ZORO
”). A well known trick is using “LOAD "$$",8
” to request just the disk title and the amount of available space (here always zero, since the images are mounted read-only), which is technically a directory filter for files named “$
”. - Type selectors as in “
=<type>
”. (A type selector must be always the last expression.)
E.g., "$=P
" for listing “PRG
”-files only, or “$=D
” to list deleted files (which are normally ignored). - An example for a fully quallified directory search is, “
LOAD "$0:A*,S*=P",8
”, which will list allPRG
files on drive0
with names starting either with “A” or “S”. - As on the “real thing”, files not closed (still opened) will show an asterisk (“
*
”) before the file type, and locked files will be indicated by a less-than (“<
”) following immediately after the file type. - BASIC refers to the
LOAD
vector in the Kernal jump table, which transfers control to the BASICLOAD
routine at “$F3C2
”. - This diverts to a subroutine at “
$F43E
” to collect the various parameters. Amongst them, there’s also the secondary address, which is stored in the zeropage at “$D3
”. - The control is then eventually diverted to the IEEE specific subroutine at
$F332
— where the secondary address is actually overwritten for the benefit of the IEEE device negotiations! Meaning, when the name is sent to the IEEE device, all notion of the secondary address has been lost! As we may observe, the altered secondary address is eventually loaded again and output to the IEEE device, immediately followed by retrieving and storing of the start address — and not much further below, we’re already recieving the first bits of the file.
T64 tape archives are handled transparently like floppies mounted in device 8. Thankfully, the T64 format already features shadow file types for 1451 emulation. If these are present, these will be used to generate the directory, otherwise, appropriate values are generated from the tape data. The file size will be displayed in block sizes of 254 bytes, just like on a floppy disk. The file format (“T64”) will be displayed instead of the disk ID and the major revision of the format will be shown instead of the DOS type.
Technically, the directory listings are not a function of the core emulation, but are generated by the respective file parsers.
Intrinsics
To start at the beginning, the emulation already featured a number of utilities for parsing file formats and extracting listings and files from archives, like disk images. Also, for this purpose, in order to access a mounted archive file again, the emulator has already some awareness of the type of the mounted file. So things should be pretty easy. Thomas Skibo’s code already meticolously emulates the IEEE exchange. That is, to a certain point. It stops at the negotiations over the file search (as soon as LOAD state is entered) and rather acknowledges the file, whatever it may be, and then procedes to hand over whatever file is currently in store, byte by byte, to the emulated PET. For this purpose, there’s a tiny dummy file already predefined (“100 REM HI
”) to fall back to. Otherwise, it is the same file mounted/loaded previously. So, all we had to do is to intercept at the exact point, when the LOAD state is entered and the filename has been transferred to the IEEE device, then look up if there’s any file mounted and jam the respective data into the store. Of course, we’ll have to check, if it is about a directory listing and generate the respective program on the fly. (In case you didn’t know, the classic Commodore 8-bit machines loaded directory listings as a BASIC program. Block sizes feature as line numbers. Since the lines are actually connected as a linked list, the sequence, or even the uniqueness of the line numbers doesn’t matter.)
BTW, did you know how the reverse video is generated for the title line of a directory listing? The line number starts with the drive number (either 0 or 1, here always 0) as the line number and the actual BASIC text is displayed in reverse video. How is this done? Its actually pretty simple: the very first character is just the PETSCII code “[RVS ON]
” (decimal 18
). Since this code fortunately isn’t in the shifted range, it is treated as a normal ASCII character. That is, as far as the LIST
command is concerned. While it does list nicely — and quite effectfully so —, trying to execute such a line will result in a syntax error. However, it may be still a nice trick for displaying a REM
statement in reverse video.
But back to the matter of intercepting the LOAD state. It’s easy enough to find in the code, but, as everything is implemented, we encounter a problem unforseen: How do we actually get to know about the secondary address provided in BASIC, as in “LOAD "*",8,1
”, which loads a binary program from the provided start address (instead of converting it to the normal start of BASIC at $0401
)?
In Search for Waldo, AKA the Secondary BASIC Load Address
Turns out, BASIC ignores its own secondary LOAD address. For whatever reason. (Maybe, it’s because of the shortened file search negotiations?) Meaning, we have to fix the start address (the very first two bytes of a program) the very moment, as we jam the respective file into our store. At this point in time, the device address, the file name, etc, are known. Surely, the secondary address, as well, isn’t it? Actually, it isn’t, not on the IEEE side of things. As it happens, the IEEE protocol uses a secondary address of its own, which is distinct for read and write access. So the IEEE device doesn’t know anything about this.
Plan B: what about BASIC? Surely, BASIC takes note of the secondary address provided by the LOAD
command! Let’s find out about this and dive into the ROM!
And here are the relevant bits (well, bytes) and pieces from the listing of the “new” ROM, known as ROM 2.0, sometimes also referred to as BASIC 3.0 (while the print isn’t pretty, this version features some helpful annotations — here edited and enriched with further comments by me):
As we may observe, the flow of control is as follows
How can we recover the secondary address? More so, triggered by a timing determined on the IEEE side of things?
We’ve already determinded that the zero page address is overwritten at this point. So, is the respective secondary LOAD address stored somewhere else? Actually, it is, but somewhat hidden in plain sight! And this is due to a quite special trait to the Commodore BASIC code. Namely, it’s the use of the X-register.
You may probably know that the 6502 processor has just 3 registers to be used by a program, the accumulator, the X-register, and the Y-register. Now, Commodore BASIC makes frequent use of the accumulator and the Y-register, even of the processor stack to which values are often pushed to and pulled from, but it makes a peculiar limited use of the X-register. Actually, it’s used more like a store than a processor register, even more like a semi-permanent store than the stack or some of the zeropage addresses. So often, while the code traverses over several, even numerous subroutines, the X-register remains untouched. Like a magician, Commodore BASIC produces a value supposedly lost out of thin air, or rather, out of its sleeve, which is the X-register.
As is actually the case here, with the secondary LOAD address. While the code visits a number of subroutines, neither of them accesses the X-register and the value is still untouched as it was at $F45D
, when the address was originally stored in the dedicated zeropage address (“STX $D3
”). Therefore, getting the appropriate mode of the LOAD
instruction is a matter of wireing up a few additional pipes from the top-level code down to the encapsulated realms of the emulated CPU to retrieve the value burried in the X-register.
See Mom, no hands!
No need to determine the ROM version and to peek into some zeropage addresses! It’s all in the X-register. (Somehow, this feels much cleaner.) If the contents of the X-register is 1
, we provide the file as-is for a binary load, otherwise, we fix up the start address to $0401
and are done, as well.
Amazing Round Trips
BTW, this is what happens, when you start a program from a directory dialog: First, the utility dedicated to the specific format has to have parsed and decoded that image file already, in order to present said directory listing. Now, the request is passed to that utility in order to verify that there is indeed an item with that particular index and to retrieve the respective file name. Then, the utility instructs the top-level control structure of the emulator, which handles all the UI related tasks, to load (and, maybe, also run) that program. The control part now assembles a suitable string to input into Commodore BASIC in direct mode, in order to perform that tasks, and passes this to a tiny bot in the keyboard module, which then types that statement character by character into the emulated PET. Here, the actual emulation takes over and the keyboard input is processed by the ROM routines running in the emulator, which eventually send the filename, etc, to the emulated IEEE device. This is, where we eventually intercept the loading process and pass the filename back to the top-level command structure, which identifies the kind of loaded media. As this happens to be an archive, the respective file utility is called to retrieve that file, which is then jammed into the store in the IEEE emulation, from where it is sent back to the emulated PET via the IEEE protocol. And by this we have actually loaded a file. — As simple as that! ;-)
Update
As it is, the search for the secondary LOAD address turned out to be somewhat luxurious, i.e., superfluous. This is, because the PET always loads a program absolutely. (The necessity of relocating BASIC programs arose only with the VIC-20/VIC-1001, which featured distinct BASIC start addresses with certain memory expansions. Moreover, as there hadn’t been any Commodore BASIC machine previous to the PET, considerations regarding compatibility weren’t an issue with the PET.) Therefore, implanting the behavior of later Commodore 8-bits, where “LOAD
"*",8,1
” is actually a thing, to a PET is somewhat problematic and may actually break things, as we invert the standard behavior by this.
So I settled for another method to force a program load to the standard start address at “$0401
”, namely, specifying the drive number. Usually, this is “0
” and not specified at all, it is only a thing with dual drive units. A drive number of “8
” isn’t to be encountered in real life at all, so we can easily use this to specify a relative loading mode. Also, it’s easy to remember, as in “LOAD
"8:NAME",8
” (and some of an opposite to “LOAD
"NAME",8,1
”, as well).
By this, we’re now at version 1.1.1.
Further Support — V1.2?
Otherwise, there isn’t much to write home about. We still lack support for OPEN
and CLOSE
, but this may not be that far fetched anymore. (It’s actually more a matter of contriving suitable test files.) However, the use case of this is rather limited (maybe a few text adventures?) and not that much of a high priority.
I guess, the next thing to be addresses, may be sound.
And, before I forget, there’s another improvement to the emulator, namely a “Donate” button to support said developments… :-)
Let’s see. And in the meantime…
— That’s all, folks! —
Norbert Landsteiner,
Vienna, 2020-04-10