# TRS-80 Model 4p The TRS-80 (models 1, 3 and 4) are among the most popular z80 machines. They're very nicely designed and I got my hands on a 4p with two floppy disk drives and a RS-232 port. In this recipe, we're going to get Collapse OS running on it. # Reference documentation These documents are recommended: * TRS-80 Model 4/4P Technical Reference Manual * Disk System Owner's Manual - TRS-80 Model 4/4P * Service Manual - TRS-80 Model 4P, 4P Gate Array * FDC 1791-02 datasheet from Standard Microsystems Corporation # Memory map and interrupts Collapse OS runs on the 4P in memory mode 2: $0000-f3ff is all RAM, $f400-f7ff maps to the keyboard, $f800-ffff maps to video. Boot binary begins at $0000, HERESTART begin right after the binary, PS_ADDR is $f3ff. $10 bytes are allocated to drivers SYSVARS: 00 KBD input buffer (char) 01 KBD input buffer (shift flags) 02 KBD debouncing flag 03-05 GRID_MEM 06 Floppy drive selection mask 07 Floppy drive "current operation" (rd or wr) alias 09 FD0 Current disk offset 0b FD1 Current disk offset 0d Character under the cursor Except for RTC interrupts, all other interrupts are disabled. # Booting The bootloader, placed in sector 1 of track 0, directly pokes (the 4K boot ROM is not used) FDC ports in order to read tracks 1 and 2 (36 sectors, 9KB) into memory $0000 and then jump to $0000. It also does a few initializations that are then assumed by the OS: * 80x24 video mode, page 1 * Memory map 2 * Interrupt enabled, IM 1, with RTC interrupts enabled * "FAST" mode (4MHz) * External I/O disabled In case of an error (CRC error, Lost Data, sector not found), a character corresponding to the error is placed on the screen and we abort (infinite loop). # Keyboard The 4P doesn't poll its keyboard itself, software has to do it. To do it reliably, we do so during Real Time Clock interrupts (60Hz in 4MHz mode). During each poll, we do this: 1. Decode pressed key (7 first columns) 2. Debounce check. If no key is pressed, reset debounce flag. 3. If debounced, fill input buffer with char and 8th row, which contains shift status. If you look at the hardware keyboard mapping, you'll see that most of it is straightforward to decode, with exceptions (@ and the , to / range). During the interrupt, we don't care about the exceptions and simply record the first row yielding a nonzero keypress. Rows 0 to 5 have the particularity of having columns with cont- iguous ASCII code. This makes them rather straightforward to decode. Row 6 is special because the ASCII codes are hetero- genous, so we need a hardcoded map. Row 7 is for keys that, when pressed, aren't considered a "keypress" (shift keys). To avoid repeats, we debounce the keyboard after a keypress, that is, when a key is pressed in rows 0 to 6, we wait until we go back to a "no key pressed" state before recording another press. When (key?) polls for keypress, it checks the input buffer and also applies little shift rules and exceptions to the raw value in the buffer so that it yields the proper character. The keyboard doesn't yield the whole visible ASCII range in a straightforward manner. To allow a full range, we make left and right shift behave differently. Left shift is the "regular shift". It yields values on labels (shifted @ yields `). Right shift allows the reaching of chars like [] and {}. These are the yields: , --> [ = --> \ . --> ] / --> ^ 0 --> _ 1 --> { 2 --> | 3 --> } 4 --> ~ 5 --> DEL (it makes more sense when looking at the ASCII table.) You will notice, also, that we take extra step to ensure that when we check, in (key?) whether we have a key press, we only check the LSB. This might seem illogical at first, but this is because the polling interrupt might happen at any time, including during the "0 [*TO] KBDBUF" part. # BREAKING away In Collapse OS, the BREAK key gets a special treatment. It is checked during the polling interrupt and, when pressed, calls QUIT right away. This allows you to escape infinite loops. Because it's QUIT being called and not ABORT, PS is preserved. Because it can quit at any time (except when interrupts are disabled), you can end up with extra garbage on PS after QUIT. # Video Video in the 4P is very straightforward: the screen starts at $f800 and is 1 char per byte in memory. We always run in 80 columns mode and use the Grid subsystem (doc/grid.txt). We only support the 80x24 mode, which is enabled in the bootloader. The cursor is solid and doesn't blink. In CURSOR!, we simply replace the character at target pos with $bf (a solid rectangle) and place old character in UNDERCUR buffer in SYSVARS. The NEWLN implementation scrolls contents when the bottom of the screen is reached. # Floppy In our 179X FDC driver, we hardcode for MFM (double density). We seek (with verify) implicitly before each read or write operation and, like TRS-DOS, we enable Write Precompensation for tracks higher than 21. If an error occurs, "FD err" is raised, with the corresponding status number (which should normally contain the error). There isn't yet any auto-retry mechanism on error. This results in occasional failures (mostly CRC) which don't occur on TRS-DOS (I suspect it auto-retries on errors). Collapse OS doesn't yet have any way to format floppies. For now, they need to be formatted through TRS-DOS. # RS-232 The RS-232 driver implements TX> and RX". First, display the $5000-$503f range with the d5000 command (I always press Enter by mistake, but it's space you need to press). Then, you can begin punching in with h5000. This will bring up a visual indicator of the address being edited. Punch in the stuff with a space in between each byte and end the edit session with "x". # Getting your DCB address In the previous step, you need to set COM_DRV_ADDR to your "DCB" address for *cl. That address is your driver "handle". To get it, first get the address where the driver is loaded in memory. You can get this by running "device (b=y)". That address you see next to *cl? that's it. But that's not our DCB. To get your DBC, go explore that memory area. Right after the part where there's the *cl string, there's the DCB address (little endian). On my setup, the driver was loaded in $0ff4 and the DCB address was 8 bytes after that, with a value of $0238. Don't forget that z80 is little endian. 38 will come before 02. # Saving that program for later If you want to save yourself typing for later sessions, why not save the program you've painfully typed to disk? TRSDOS enables that easily. Let's say that you typed your program at $5000 and that you want to save it to RECV/CMD on your second floppy drive, you'd do: dump recv/cmd:1 (start=X'5000',end=X'5030',tra=X'5000') A memory range dumped this way will be re-loaded at the same offset through "load recv/cmd:1". Even better, TRA indicates when to jump after load when using the RUN command. Therefore, you can avoid all this work above in later sessions by simply typing "recv" in the DOS prompt. Note that you might want to turn "debug" off for these commands to run. I'm not sure why, but when the debugger is on, launching the command triggers the debugger. # Sending binary through the RS-232 port Once you're finished punching your program in memory, you can run it with g5000 (not space). If you've saved it to disk, run "recv" instead. Because it's an infinite loop, your screen will freeze. You can start sending your data. To that end, there's the tools/pingpong program. It takes a device and a filename to send. Before you send the binary, make it go through tools/ttysafe first (which just takes input from stdin and spits tty-safe content to stdout): ./ttysafe < os.bin > os.ttysafe On OpenBSD, the invocation can look like: doas ./pingpong /dev/ttyU0 os.ttysafe If everything goes well, the program will send your contents, verifying every byte echoed back, and then send a null char to indicate to the receiving end that it's finished sending. This will end the infinite loop on the TRS-80 side and return. That should bring you back to a refreshed debug display and you should see your sent content in memory, at the specified address ($3000 if you didn't change it). If there was no error during pingpong, the content should be exact. Nevertheless, I recommend that you manually validate a few bytes using TRSDOS debugger before carrying on. *debugging tip*: Sometimes, the communication channel can be a bit stubborn and always fail, as if some leftover data was consistently blocking the channel. It would cause a data mismatch at the very beginning of the process, all the time. What I do in these cases is start a "COMM *cl" session on one side and a screen session on the other, type a few characters, and try pingpong again. # Bringing it together Now that you have all you need to send binary contents to your TRS-80, you're ready to craft your disk! To do so, we'll use DEBUG's low level disk writing capabilities. It is invoked with a command has this signature: driveno,trackno,sector,r/w,addr,sectorcount Example: 1,0,1,w,3000,1 This writes a single sector at track 0, sector 1 (each sector is 256 bytes) using the contents of memory address $3000. Drive numbers are 0 and 1. First, you'll upload and write down boot.bin with this very command. Yes, the boot sector is sector 1, not sector 0. Weird but true. Then, you'll upload os.bin. It's a bit bigger than the bootloader and spans over multiple tracks, starting with track 1 (the bootloader loads beginning at track 1, sector 0). You might be tempted to write 18 sectors at once (there are 18 sectors per track), but TRS-DOS is a bit tricky for this because it seems to silently drop the write operation sometime. I've found that the sweet spot is to write 6 sectors at once. So, for a binary that is $1a00 bytes big, it would be: 1,1,0,w,3000,6 1,1,6,w,3600,6 1,1,c,w,3c00,6 1,2,0,w,4200,6 1,2,6,w,4800,2 If everything went well, you have your boot disk! Before you reboot, however, you might want to re-read those sectors in memory (replace "w" with "r") and quickly compare the first bytes of every sector with your reference binary to make sure that everything was written properly (you can zero-out a memory zone with "F". Example: "f3000,5000,0"). You're done! Pop the disk in the first drive, reboot, you should have a Collapse OS prompt. All this process was a bit inconvenient, but once you have a Collapse OS disk, receiving data and writing them to disk is a bit easier. Read on for details. # Using floopy drives As it is, your system fully supports reading and writing to both floppy drives. By default, floppy drive 1 is selected. You can select the active drive with FD0 and FD1. Then, use regular BLK works to interact with blocks. # Sending blkfs to floppy Collapse OS has RX to emit to it. That's all you need to have a full Collapse OS with access to disk blocks. First, make sure your floppies are formatted. Collapse OS is currently hardcoded to single side and double density, which means there's a limit of 180 blocks per disk. You'll need to send those blocks through RS-232. First, let's initialize the driver with CL$. It is hardcoded to "no parity, 8 bit words" and takes a "baud code" as an argument. It's a 0-15 value with these meanings: 00 50 01 75 02 110 03 134.5 04 150 05 300 06 600 07 1200 08 1800 09 2000 0a 2400 0b 3800 0c 4800 0d 7200 0e 9600 0f 19200 After CL$ is called, let's have the CL take over the prompt: ' TX> 'EMIT ! ' RX d1 Now, insert your formatted disk in drive 1 and push your blocks: tools/blkup /dev/ttyUSB0 0 d1 It takes a while, but you will end up having your first 180 blocks on floppy! Go ahead, LIST around. Then, repeat for other disks. Once you're done, you will want to go back to local control: ' (emit) *TO 'EMIT ' (key?) *TO 'KEY? Alternatively to all this, you can also use Collapse OS' XMODEM implementation at B150. Instead of taking over the prompt, you'd run "0 BLK@" followed by "RX>BLK". On the other side, you'd run your favorite XMODEM app ("rx" probably). # Floppy organisation Making blkfs span multiple disk is a bit problematic with regards to absolute block references in the code. You'll need to work a bit to design your very own Collapse OS floppy set. See /doc/usage.txt for details. The TRS-80 4P implementation of Collapse OS includes a very handy floppy management system through "disk words" with hardcoded offsets: D1 - 0 D2 - 200 D3 - 300 D4 - 400 D5 - 500 ND - no disk (yes, I skip blocks 180-199 entirely in my default media organisation for the 4P) These words indicate to COS which floppy is inserted and apply the proper offset to its block requests. At boot, both drives ar at "no disk" any dist request will fail with an "out of range" error. If you execute "D2", doing "242 LIST" will read block 42. doing "42 LIST" will generate an error. Here's the nice part: COS will auto-select the correct drive for the block you request. If, for example, you run "FD0 D1 FD1 D3" and then run "ARCHM Z80A", COS will automatically read block 1 on FD1 (for 301 of ARCHM) and then read block 2 on FD0 (for 002 of ASML in Z80A) and then 20-27 on FD1 and then block 3 on FD0 (for ASMH in Z80A). # Self-hosting As it is, your installment of Collapse OS is self-hosting using instructions from /doc/selfhost.txt. The difference is that instead of writing the binary you have in memory to EEPROM, you'll want to write it to disk. To that end, there is the MEM>BLK utility in B121 which allows writing memory spanning multiple sectors to disk. To write Collapse OS to the boot disk, you have to write your binary to the *half* of the 4th block (18 sectors per track is 4.5K per track, track 1 is there). MEM>BLK doesn't allow writing half blocks, but you can cheat a little bit with something like: ORG $200 - 4 8 MEM>BLK See what I did there? I simply fill the first 2 sectors of block 4 with whatever preceeds my binary. If you need to write the boot sector from within Collapse OS, don't run MEM>BLK because the computer's bootloader is a bit sensible to garbage. What you do is zero-out the whole block 0 like this: 0 BLK@ BLK( $400 0 FILL BLK!! Then, you can place the bootloader's content at BLK(+$100 and then call FLUSH to write it out.