aboutsummaryrefslogtreecommitdiff
path: root/fenders.dasm
blob: 7bec9bc590093a52f33e00f9ad4c7201072a1c6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
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).