diff options
Diffstat (limited to 'fenders.dasm')
-rw-r--r-- | fenders.dasm | 570 |
1 files changed, 570 insertions, 0 deletions
diff --git a/fenders.dasm b/fenders.dasm new file mode 100644 index 0000000..7bec9bc --- /dev/null +++ b/fenders.dasm @@ -0,0 +1,570 @@ + +; Fender's 3-sector loader (boot code only) + +; A "game DOS" binary file loader. Install it on a disk full of binaries, +; it replaces the DOS boot loader. You don't need DOS.SYS or DUP.SYS on +; such a disk, so there's more room for games. When you boot, the 3-sector +; loader reads the directory and presents you with a menu of up to 20 +; files to load with a single keypress. + +; presumably written by someone named Fender, in the early 1980s + +; I have no idea what the licensing is (shareware, public domain, ???), +; but after all this time, I don't think anyone's going to sue me. + +; Disassembled with dis6502 +; Commented, labelled, and reformatted for DASM use by B. Watson, 20061105 + +; Can be assembled to a 384-byte object file with a command like: + +; dasm fenders.dasm -ofenders.obj -f3 + +; The object code can be installed on an existing single-density ATR +; floppy image on UN*X like so: + +; dd if=fenders.obj of=disk.atr bs=1 count=384 seek=16 conv=notrunc + +; (for an XFD image, leave off the "seek=16") + +; This is a standalone source file: it doesn't require any "system equates" +; include file. + +; Intended for use with DASM, but doesn't use macros or other fancy +; features, so it'll probably assemble on most 6502 assemblers. +; Your assembler may use a slightly different syntax for conditional +; assembly, if so search the file for ".if" and change as needed. +; Also, remove the "processor" line if you aren't using DASM: + + processor 6502 + +; This disassembly only contains the boot loader code, not the installer. +; The installer is 30 sectors long (10x the size of the loader), and +; pretty straightforward. + +; The installer allows you to customize the loader somewhat. It does +; this by changing the boot loader code before writing it. The options +; in the installer are: + +; Option | Label +; A. Cause COLDSTART on RESET | COLDSTART_ON_RESET +; B. Rotate color during load | SCREEN_OFF [*] +; C. Screen off after load | ROTATE_COLOR [*] +; D. Title | (see "screen" label near end of this file) +; F. Change density | DENSITY (TODO: support this) +; (the other options don't change the boot code) + +; [*] B and C options are mislabelled in the installer, selecting +; "Rotate color" actually toggles "Screen off", and vice versa. + +; Options A, B, and C cause the installer to modify the in-memory copy +; of the boot code before writing it to disk. I've used conditional +; assembly to simulate this: + +COLDSTART_ON_RESET .equ 0 ; 0=false, non-zero=true +ROTATE_COLOR .equ 0 ; 0=false, non-zero=true +SCREEN_OFF .equ 0 ; 0=false, non-zero=true + +; TODO: support conditional assembly for double density. +; Unlike the first 3 options, double density changes a lot of code. +; The original author doesn't change the code at runtime to support +; double-density; the installer contains complete copies of the +; single and double density boot code. +;DENSITY .equ 1 ; 1=single, 2=double + + +;;; OS equates + +;; OS ROM entry points + +; Question for you Atari SIO and OS experts: why does "Mappping the +; Atari" state that you're supposed to call $E543 for the disk, +; instead of $E459, but the author of this program used $E459 anyway? +SIOV .equ $e459 ; Serial I/O + +COLDSV .equ $e477 ; Cold Start (reboot) +KEYBDV .equ $e420 ; K: handler device table +keyb_get_lo .equ KEYBDV+4 ; pointer to "get byte" routine, minus 1 +keyb_get_hi .equ KEYBDV+5 ; (used by get_key) + + +;; OS zero page +BOOTQ .equ $09 ; Tells OS what to do when RESET is pressed: + ; (0 = reboot, 1 = warmstart, JSR through DOSVEC) + +;; OS and FMS page 2 RAM variables +SDMCTL .equ $022f ; used to turn off the screen when done +SDLSTL .equ $0230 ; display list start pointer +COLDST .equ $0244 ; "coldstart in progress" flag +RUNAD .equ $02e0 ; The run address of the loaded file +INITAD .equ $02e2 ; The init address of the loaded file (can be >1 per file) + +;; DCB, used for sector I/O parameters by SIOV (called by read_sector) +DDEVIC .equ $0300 ; Device serial bus ID (set to $31 by OS disk boot) +DUNIT .equ $0301 ; Drive number (we always use 1) +DCOMND .equ $0302 ; SIO command (set to $52, "Read sector", by OS) +DSTATS .equ $0303 ; Data direction register: $40 for "read" +DBUFLO .equ $0304 ; I/O buffer, lo byte +DBUFHI .equ $0305 ; I/O buffer, hi byte +DTIMLO .equ $0306 ; SIO timeout +DBYTLO .equ $0308 ; number of bytes to transfer (AKA sector size), lo byte +DBYTHI .equ $0309 ; ...hi byte +DAUX1 .equ $030a ; For "Read" command, the sector number (lo byte) +DAUX2 .equ $030b ; ...hi byte + +;; Hardware registers +COLPF1 .equ $d017 ; GTIA, used by read_sector to rotate the title color + +;; Local variables (zero page) +dest_ptr .equ $43 ; (AKA FMSZPG) 2 bytes, init to load address +end_address .equ $45 ; 2 bytes +save_pos .equ $49 ; 1 byte: saves X register while loaded init routine runs +menu_counter .equ $b0 ; tracks how many entries (filenames) are in the menu +dir_sector_lo .equ $b1 ; lo byte of current directory sector + ; (range $69-??, high byte is always 1) +menu_ptr .equ $b2 ; points to screen RAM, for printing filenames + +; Starting sector tables. For each file numbered N on the disk, +; do_dirent saves the low byte of its starting sector at +; start_sector_lo_tbl+N and its high byte at start_sector_hi_tbl+N +; Each table is only 32 bytes, but that's fine as we stop reading the +; directory after 20 files are found (all that will fit on the screen). +start_sector_lo_tbl .equ $c0 +start_sector_hi_tbl .equ $e0 + +;;; Local variables (non zero page) +buffer .equ $0b00 +sector_link_hi .equ $0b7d ; buffer + $7d +sector_link_lo .equ $0b7e ; buffer + $7e +sector_byte_count .equ $0b7f ; buffer + $7f + +;;; Bootable disk image starts here + .org $0700 + +;;; Standard Atari boot disk header (6 bytes) +boot_record: + .byte $00 ; ignored + .byte $03 ; number of sectors to read + .word boot_record ; load address + .word COLDSV ; init address, don't think this gets used + +;;; Actual code starts here +boot_continuation: ; OS starts running our code here. +OFFSET_COLDST_1 equ *-boot_record+1 +OFFSET_COLDST_2 equ *-boot_record+5 + .if COLDSTART_ON_RESET ; installer option A + LDY #$01 ; tell OS to reboot if RESET pressed + STY COLDST ; (coldstart in progress = true) + NOP + .else + LDY #$00 ; tell OS not to reboot if RESET pressed + STY COLDST ; (coldstart in progress = false) + INY + .endif + + ; either way, Y is now 1 + STY BOOTQ ; tell OS we booted successfully + STY DUNIT ; set drive #1 for later SIO use + + DEC DTIMLO ; decrease default disk I/O timeout + + ; set up our custom display list + LDA #<display_list ; 74 J + STA SDLSTL + LDA #>display_list ; 8 . + STA SDLSTL+1 + + ; set up to start reading the directory + LDA #<menu + STA menu_ptr + LDA #>menu + STA menu_ptr+1 + LDA #$69 ; Start loading directory at sector 361 ($0169) + STA dir_sector_lo + +read_dir_sector: + LDA dir_sector_lo + STA DAUX1 + LDA #$01 ; The directory is located at sectors 361-368, + STA DAUX2 ; so the high byte is always 1 + JSR read_sector + INC dir_sector_lo + DEX + + ; X reg is the pointer into the sector buffer, keep that in mind +do_dirent: + LDA buffer,X ; look at status byte + BEQ wait_for_input ; if it's 0, there are no more files, so we're done + BMI next_dirent ; if it's deleted (bit 7 true), skip it + AND #$01 ; open for write if bit 0 set + BNE next_dirent ; If it's open for writing, skip it + INC menu_counter + LDY menu_counter + + ; grab and save starting sector; we're building a table in RAM that + ; holds the starting sector for every file on the disk + LDA buffer+3,X + STA start_sector_lo_tbl,Y + LDA buffer+4,X + STA start_sector_hi_tbl,Y + TYA + + ; draw "A.", "B.", etc (menu choice letters) in the menu + ; (menu_ptr) points to column 0 of the current menu line, and gets + ; 20 added to it each time through the loop (we're using GR.1, which + ; has 20 bytes per screen line). + ; Y is the offset into the current line, AKA the column number + CLC + ADC #$a0 ; GR.2 offset into color 2 alphabet (128+32=160) + LDY #$03 ; Menu letter goes in column 3 of the current menu line + STA (menu_ptr),Y + INY + LDA #$8e ; blue period (no, really, a period that's blue) + STA (menu_ptr),Y ; Store in column 4 + INY + +next_char: + INY + LDA buffer+5,X ; buffer+5 holds the first ATASCII char of the filename + INX + SEC + SBC #$20 ; subtracting 32 makes them come out in color 1 + STA (menu_ptr),Y + CPY #$10 ; done with 16-byte dirent? + BNE next_char ; if not, do next character + + ; Set up pointer for next screen line + CLC + LDA menu_ptr + ADC #$14 ; skip to next GR.1 line ($14 = 20 bytes) + STA menu_ptr + BCC skip_ptr_hi + INC menu_ptr+1 ; inc hi byte if necessary +skip_ptr_hi: + LDA menu_counter + CMP #$14 ; see if the screen is full (max entries = 20) + BEQ wait_for_input ; yep, stop adding entries, or... + +next_dirent: + ; set X to point to the offset of the next dir entry + ; they occur every 16 bytes, so: X = (X mod 16) + 16 + TXA + AND #$f0 + CLC + ADC #$10 + TAX + + ; dirents only take up the first 128 bytes of a dir sector, even + ; on a double density disk, so: + ASL ; see if the high bit it set (A >= 128)... + BCC do_dirent ; no, we're still in the same dir sector, do next entry, or.. + BCS read_dir_sector ; yes, we need to load the next sector + +wait_for_input: + JSR get_key ; get_key waits for the user to press a key and returns its + ; ATASCII value in A + SEC + SBC #$40 ; Convert from ATASCII A-Z to numbers 1-26 + CMP menu_counter ; See if it's one of our valid menu entries + + ; do a "branch if less than or equal" to load_file: + BEQ load_file ; if equal, branch + BCS wait_for_input ; else if greater than, ignore the keypress and + ; wait for the user to press another key + ; else if less than, fall through to load_file + +load_file: ; A register has the file number (1 indexed; there's no file #0) + TAX + INC menu_dlist,X ; change selected entry from GR.1 to GR.2, so the user + ; can see which file he'd selected + + ; get starting sector of this file + LDA start_sector_lo_tbl,X + STA DAUX1 + LDA start_sector_hi_tbl,X + STA DAUX2 + + JSR try_read ; read the first sector... + DEX ; point X at first byte we just read + +; main loop of the program (see "Atari Binary Load Format", near end of file) +read_segment: + ; get the segment load address and save in dest_ptr + JSR get_next_byte + STA dest_ptr + JSR get_next_byte + STA dest_ptr+1 + AND dest_ptr + CMP #$ff ; If we just read two $FF bytes, throw them out + BEQ read_segment ; ...and read the next 2 bytes instead + + ; get the segment end address and save in end_address + JSR get_next_byte + STA end_address + JSR get_next_byte + STA end_address+1 + +load_byte: + JSR get_next_byte ; get next loaded data byte + STA (dest_ptr),Y ; store in destination RAM + INC dest_ptr ; bump destination pointer: lo byte + BNE check_seg_done ; skip the hi byte if the lo byte didn't wrap around + INC dest_ptr+1 ; bump hi byte, if needed + BEQ check_for_init ; stop loading this seg if address wraps $FFFF -> $0000 + ; else fall through to check_seg_done + +check_seg_done: + LDA end_address ; double-byte double compare, to see if we've + CMP dest_ptr ; finished loading all the data in the segment + LDA end_address+1 + SBC dest_ptr+1 ; (SBC instead of CMP because we care about the carry here) + BCS load_byte ; if we're not done, load the next byte of data + +check_for_init: + LDA INITAD + ORA INITAD+1 + BEQ read_segment ; if init addr is 0, we don't have one, go do next seg + STX save_pos ; else save byte position (X reg) and do the init + JSR do_init + LDX save_pos ; init routine returns here, reload byte position + LDY #$00 ; zero out init address so it doesn't run again next time + STY INITAD ; (unless of course the next segment(s) load at INITAD) + STY INITAD+1 + BEQ read_segment ; unconditional branch, go read next segment + + ; JSR do_init simulates indirect JSR +do_init: + JMP (INITAD) + +; WARNING: self-modifying code. read_sector modifies the operand +; of the CPX, setting it to the number of valid data bytes in the +; sector just read. The initial value of $7d represents the number of +; data bytes in a sector that's full of data (125 bytes of actual data). +get_next_byte: + CPX #$7d ; are we done with all the bytes in this sector? + BNE return_next_byte ; if not, go do the next one + + LDA DAUX1 ; have we read the last sector? + ORA DAUX2 ; (if so, the "next sector" stored at DAUX1/2 will be 0) + BNE try_read ; if not, go read the next sector +OFFSET_SCREENOFF equ *-boot_record + .if SCREEN_OFF ; installer option C, "turn off screen after loading" + STA SDMCTL ; turn off the screen (er, why?)... + .else + LDA SDMCTL ; don't turn off the screen + .endif + JMP (RUNAD) ; ...we're done loading, go run the binary! + +read_sector: + LDA #>buffer + STA DBUFHI ; set sector buffer (lo byte presumably defaults to 0?) + LDA #$80 ; sector size for SIO: 128 bytes for single density + STA DBYTLO + +try_read: + LDA #$40 ; data direction = read (bit 6 set, bit 7 clear)... + STA DSTATS ; DSTATS is where we store the data direction + JSR SIOV ; all set up, so call SIO + BMI try_read ; on error, just retry (forever, if need be) + + ; set up SIO sector number for next time: + LDA sector_link_hi ; The high 2 bits of the sector link (AKA next sector in + ; file) are stored in the LOW 2 bits of sector_link_hi, + ; along with the file number in bits 2-7 + AND #$03 ; mask off file number + STA DAUX2 + LDA sector_link_lo ; sector link, low 8 bits + STA DAUX1 + + ; rotate PF color +OFFSET_ROTCOLOR equ *-boot_record + .if ROTATE_COLOR ; installer option B + STA COLPF1 + .else + LDA COLPF1 + .endif + + ; save number of data bytes in sector + LDA sector_byte_count + AND #$7f ; hmm, is this really necessary? + STA get_next_byte+1 ; WARNING: self-modifying code (see above) + + ; init source and destination indices + LDY #$00 + LDX #$00 + +return_next_byte: + LDA buffer,X + INX + RTS + +; Simulate JSR through keyb_get_lo. This is one of those Atari +; "entry point minus one" vectors. To call it, you push it onto the +; stack and do an RTS (works because a real JSR saves the PC minus 1 on +; the stack). +; Becauses it gets the vector from ROM instead of hard-coding it in +; the get_key routine, this code will work on all Atari models (400/800, +; XL/XE). It's a compromise between hardcoding the address and doing +; a full-blown IOCB setup and CIO call. +get_key: + LDA keyb_get_hi + PHA + LDA keyb_get_lo + PHA + RTS + +;;; Data +display_list: ; display list, $084A - $0867 + ; 3 "blank 8 scans", a GR.2 line with LMS, then screen RAM address + .byte $70,$70,$70,$47,<screen,>screen ; "pppGh." +menu_dlist: + ; blank 8 scans, then 20 GR.1 lines, then a jump back to the + ; start of the display list. + .byte $70,$06,$06,$06,$06,$06,$06,$06 ; "p......." + .byte $06,$06,$06,$06,$06,$06,$06,$06 ; "........" + .byte $06,$06,$06,$06,$06,$41,<display_list,>display_list ; ".....AJ." + + ; screen RAM, $0868 - $087F and beyond +OFFSET_TITLE equ *-boot_record +screen: + ; $00 is a space, lowercase letters come out as caps (color reg 1) + .byte $00,$00,$00,$00 ; 4 spaces for centering + .byte "atari", $00, "arcade" ; "ATARI ARCADE" in green + .byte $00,$00,$00,$00 ; rest of the GR.2 line (20 bytes total) +menu: ; the filenames start getting stored here, 2nd line of the display + + ; 4 filler bytes, so we come out at an even 3 sectors (384 bytes) + .byte $00,$00,$00,$00 + +;;; End of code. Rest of file is boring documentation :) + +; Atari Binary Load File Format - A Primer + +; The binary load format was first defined for Atari DOS 1.0, and supported +; by all DOSes for the Atari. There's no formal standard that I'm aware of; +; the closest thing is probably the binary load implementation in Atari +; DOS 2.0S or 2.5 (the most widely-used DOSes from Atari), but that's +; copyrighted code, so it couldn't be used as a base to build on or as a +; module for another DOS. + +; Sadly, Atari didn't add a CIO SPECIAL (aka XIO from BASIC) command to load +; binary files as part of their DOS's public API... so anyone who wanted to +; write a game-loader or menu that runs under DOS would have to write their +; own loader (for compatibility with many DOSes), or just assume they'll +; be running under DOS 2 and JSR to the unpublished entry point within DOS +; (which changed between DOS 2 and 2.5). Some third-party DOSes (SpartaDOS, +; MyDOS) included a CIO/XIO call for loading and/or running binary files, +; which meant a menu program was easy to write in just a few lines of +; BASIC, but it wouldn't work on standard Atari DOS, and might not even +; be portable from Sparta to MyDOS, if they didn't choose the same CIO +; command for binary loading. + +; None of this DOS-related discussion is relevant to Fenders, though. +; The whole point of Fenders is to do away with the overhead of DOS, so: + +; - You don't have to keep copies of DOS.SYS (39 sectors) or DUP.SYS (42 +; sectors) on your disk. Together these take up over 10% of the available +; storage on a DOS 2 formatted disk. With Fenders you get all 707 sectors +; to use for game storage. + +; - You don't have to wait for a full-blown DOS boot. It takes a while +; to load DOS.SYS and DUP.SYS (81 sectors, taken together), which are a +; rather complete file management system and menu interface that you don't +; need for playing games. With Fenders, you get your menu almost instantly. + +; - Once DOS and DUP are loaded, you have to type "A Return Return" to +; get a disk directory, then "L Return FILENAME Return" to load a file. +; With Fenders, you type one letter. + +; There is no support in the OS ROM for the binary load format, or for +; the disk device at all at the CIO level (the ROM does have SIO routines +; for reading/writing raw sectors, which is what Fenders uses). There are +; many binary loader implementations on the Atari. Each third-party DOS +; generally had to roll its own, and there were numerous "No DOS" and +; "Game DOS" utilities (like Fenders) that had to implement read-only +; support for the DOS 2 filesystem along with a binary loader. Fenders +; does this in 330 bytes of code (including the user interface). + +; An Atari binary load file consists of one or more segments, each with +; its own load address and end address. + +; A segment is: + +; FFFF leader (bytes 0 and 1) - A two-byte leader: $FF, $FF +; Required for 1st segment, optional for others +; Load address (bytes 2 and 3 in standard 6502 LSB/MSB) +; End address (bytes 4 and 5) +; (Load address - End address + 1) bytes of data to be loaded + +; Fenders actually doesn't require the FFFF leader even for the first +; segment, and could deal with multiple FFFF leaders if present: it +; just skips over them. I haven't checked to see if Atari DOS does +; the same thing or not. + +; A well-formed binary load file must have an end address higher than the +; start address for every segment. It's undefined what happens otherwise +; (Fenders quits loading if the current address ever wraps around from +; $FFFF to 0000, I don't know what it does if the end address is the same +; as the start address). + +; There's no rule against having overlapping segments, but it's kind +; of an odd thing to do. + +; After each segment is loaded, a loader must check INITAD (a 16-bit +; vector at location $02E2) to see if the segment loaded anything at +; that address. If so, a JSR through the vector is performed, with the +; expectation that the loaded program's initialization routine will run +; and then do an RTS to return control to the loader. This is often used +; (or abused) by crackers to load a screen with their name/logo. In a +; real DOS, IOCB #1 is left open, so theoretically the init routine could +; read data from the file, located between segments as it were. Fenders +; doesn't do this, as it doesn't implement a CIO handler at all. Any program +; that relies on this behavior won't work with Fenders, which isn't much +; of a limitation (I doubt this capability has ever been used in the +; entire history of the Atari. It might be useful for something like an +; auto-relocating load?). Use of the init vector is optional. Since it's +; checked after every segment, there can be more than one init routine +; run this way. + +; After all segments have been loaded, a loader must check RUNAD (16-bit +; vector located at $2E0) to see if any segment loaded anything at that +; address. If so, a JSR through the vector is performed to run the main +; routine of the just-loaded program, which may or may not do an RTS to +; return control to the caller. In a real DOS, the RTS will generally redraw +; the DOS menu or print a new command prompt (whatever makes sense). Fenders +; does a JMP (RUNAD), not expecting the loaded program to return (most +; games just run forever). + +; If RUNAD never gets loaded, a real DOS will return to its menu or prompt +; without trying to run anything. You could use this to just load some +; data (a font, maybe) into memory. Fenders will crash if RUNAD never gets +; loaded, since it's set to 0 by the OS (a JMP (0) happens, transferring +; control off to never-never-land). Again, not much of a limitation, since +; it's intended for running games, not general-purpose data-file loading. + +; There is ABSOLUTELY NO protection against a binary file trying to +; load on top of Fenders while it's running (or the memory-mapped +; I/O registers, or zero page, or anywhere it wants to). There is also +; no protection against a loaded init routine messing up the zero page +; pointers or disk buffer. In practice, this doesn't happen often +; (and a "rude" file that did this would probably have problems loading +; under a standard DOS, too). + +; This information comes from various sources. I probably gleaned most of +; it from "Mapping the Atari" or the Compute! magazine "Insight: Atari" +; column, way back when. Both of these are available on the web now: + +; Mapping the Atari, De Re Atari, and a lot of other books can be +; found at http://www.atariarchives.org + +; Most of the old Compute! and Antic magazines have been scanned and +; archived at http://www.atarimagazines.com/ + +; There's also a "Digital ANALOG Project" that's got a lot of Analog +; magazine issues archived: http://www.cyberroach.com/analog/ + +; For a compact reference to the binary load format, see: +; http://www.atarimax.com/jindroush.atari.org/afmtexe.html + +; TODO: I want to add high speed I/O to Fenders, for use with the SIO2PC +; and AtariSIO (or APE). + |