; 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 ; 8 . STA SDLSTL+1 ; set up to start reading the directory 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 ; "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 ; ".....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).