; THIS IS A MODIFIED VERSION, for use with FujiChat ; COL80.COM, aka COL80E.COM, aka COL80HND.COM ; (and probably several other names) ; Original author unknown ; License unknown ; Disassembly and comments by Urchlay ; This is a widely-distributed software 80-column driver for the Atari ; 8-bit computers. It replaces the OS's E: driver, and uses GRAPHICS 8 ; for display, with 4x8 pixel character cells. ; Disassembly was done with da65, with many iterations of "edit the ; .info file, disassemble again", and the results were tweaked by hand ; into something assemblable by dasm (and fairly compatible with other ; assemblers). .processor 6502 START_ADDRESS = $9C2D ;START_ADDRESS = $7C28 ; works with BASIC ; xex segment header #1 .org START_ADDRESS-6 .word $FFFF .word START_ADDRESS .word END_ADDRESS .org START_ADDRESS ; ---------------------------------------------------------------------------- ; Zero page labels (OS equates) DOSINI = $000C ICAX1Z = $002A ICAX2Z = $002B TMPCHR = $0050 LMARGN = $0052 ROWCRS = $0054 COLCRS = $0055 DINDEX = $0057 SAVMSC = $0058 BUFCNT = $006B ; ---------------------------------------------------------------------------- ; Zero page labels (COL80 equates) screen_ptr_lo = $00CB screen_ptr_hi = $00CC font_ptr_lo = $00CD font_ptr_hi = $00CE ; ---------------------------------------------------------------------------- ; Non-zeropage RAM labels (OS equates) COLOR1 = $02C5 COLOR2 = $02C6 RUNAD = $02E0 INITAD = $02E2 MEMTOP = $02E5 SSFLAG = $02FF HATABS = $031A ICCOM = $0342 ICBAL = $0344 ICBAH = $0345 ; ---------------------------------------------------------------------------- ; Hardware (memory-mapped I/O, OS equates) CONSOL = $D01F AUDF1 = $D200 AUDC1 = $D201 ; ---------------------------------------------------------------------------- ; OS ROM labels s_dev_open_lo = $E410 ; (not named in OS sources) s_dev_open_hi = $E411 ; "" k_dev_get_lo = $E424 ; "" k_dev_get_hi = $E425 ; "" CIOV = $E456 ; Central Input/Output entry point ; ---------------------------------------------------------------------------- ; Start of COL80. The font is stored in packed form. Each group of 8 bytes ; defines two glyphs: the upper 4 bits of the 8 bytes, taken together, ; define the bitmap for the first glyph, and the lower 4 bits are the second. ; Note that the bits that make up a single character are spread across 8 ; bytes, so it's hard to visualize these even if you're used to reading hex ; dumps. ; The first 2 characters look like: ; .... .O.. ; $04 ; .... .O.. ; $04 ; O.O. .O.. ; $A4 ; OOO. .O.. ; $E4 ; OOO. .OOO ; $E7 ; .O.. .O.. ; $44 ; .... .O.. ; $04 ; .... .O.. ; $04 ; These are the ATASCII heart symbol (character code 0) and the ATASCII ; control-A line-drawing symbol (code 1). ; Note: unlike the ROM font, this font is stored in ATASCII order instead ; of the standard Atari character order imposed by the hardware. Like ; the ROM font, inverse characters are not stored here (the bitmaps get ; inverted by the driver) font_data: .include "new_font.s" ;.include "icetmod.s" right_margin: ; Default value is 79 decimal. Unsure why the author didn't use RMARGN at $53 .byte $4F ; ---------------------------------------------------------------------------- ; Start of COL80 code. ; Callback for CIO OPEN command. col80_open: jsr init_graphics_8 lda #$00 sta ROWCRS sta COLCRS sta BUFCNT lda #$4F sta right_margin rts ; ---------------------------------------------------------------------------- ; Assembly version of GRAPHICS 8+16 command. init_graphics_8: lda #$08 sta ICAX2Z lda #$0C sta ICAX1Z jsr open_s_dev ; Set COL80's default colors lda #$08 sta COLOR2 lda #$00 sta COLOR1 ; Protect ourselves from BASIC and the OS lda #START_ADDRESS sta MEMTOP+1 rts ; ---------------------------------------------------------------------------- ; Call the OPEN vector for the S: device, using the ROM vector table ; at $E410. The table stores address-minus-one of each routine, which is ; meant to actually be called via the RTS instruction (standard 6502 ; technique, but confusing the first time you encounter it) open_s_dev: lda s_dev_open_hi pha lda s_dev_open_lo pha rts ; ---------------------------------------------------------------------------- ; Callback for CIO CLOSE command. Note that the routine does nothing, really ; (the OS will mark the E: device as being closed, but COL80 doesn't do any ; cleanup). ; The SPECIAL and GET STATUS callbacks in col80_vector_tab also point here. col80_close: jmp return_success ; ---------------------------------------------------------------------------- ; Callback for the internal put-one-byte, used by the OS to implement the ; CIO PUT RECORD and PUT BYTES commands. This routine's one argument is ; the byte in the accumulator (the character to print). ; First, the routine checks for the cursor control characters it supports. ; COL80 only handles the EOL and clear-screen codes; trying to print ; backspaces, arrows, deletes, inserts, etc just causes their ATASCII ; graphics character to print instead. col80_putbyte: ; EOL (decimal 155)? cmp #$9B ;;; bne check_clear bne regular_char lda right_margin sta COLCRS jmp skip_write ;;;check_clear: ;;; .ifndef FUJICHAT ; save memory by not including clear_screen ;;; ; (also, this lets us print the } character) ;;; ; Clear (decimal 125)? ;;; cmp #$7D ;;; bne regular_char ;;; jmp clear_screen ;;; .endif ;;; ; See if this is an inverse video char (code >= 128) regular_char: tax bpl not_inverse lda #$FF sta inverse_mask bne skip_ninv not_inverse: lda #$00 sta inverse_mask skip_ninv: txa and #$7F ;.ifdef FUJICHAT ; mask out low ASCII sec sbc #$20 bcs not_low_ascii jmp return_success not_low_ascii: ;.endif sta TMPCHR lda DINDEX cmp #$08 beq graphics_ok ; If we're not in GRAPHICS 8 mode, reinitialize ourselves jsr col80_open graphics_ok: ; Call the routines that actually print the character jsr setup_font_ptr jsr setup_screen_ptr jsr write_font_data skip_write: ; Move the cursor 1 space to the right. This will ; advance us to the next line if we're at the margin, ; and scroll the screen if needed jsr advance_cursor check_ssflag: ; The OS keyboard interrupt handler will toggle SSFLAG (start/stop fla ; any time the user presses ctrl-1 ;lda SSFLAG ;bne check_ssflag jmp return_success ; ---------------------------------------------------------------------------- ; Scroll the screen up one line (8 scanlines). This has to move almost 8K of ; data, so it's noticeably slower than scrolling the GR.0 text screen. scroll_screen: lda SAVMSC sta screen_ptr_lo clc adc #$40 ; font_ptr_lo is actually being used here as a second pointer into ; screen RAM, instead of its usual use as a pointer into the ; font_data table sta font_ptr_lo lda SAVMSC+1 sta screen_ptr_hi adc #$01 sta font_ptr_hi ldx #$1D ldy #$00 scroll_line_loop: lda (font_ptr_lo),y sta (screen_ptr_lo),y dey bne scroll_line_loop inc font_ptr_hi inc screen_ptr_hi dex bne scroll_line_loop blank_bottom_row: lda SAVMSC clc adc #$C0 sta screen_ptr_lo lda SAVMSC+1 adc #$1C sta screen_ptr_hi lda #$00 tay blank_loop: sta (screen_ptr_lo),y dey bne blank_loop inc screen_ptr_hi ldy #$40 blank_tail: sta (screen_ptr_lo),y dey bpl blank_tail rts ; ---------------------------------------------------------------------------- ; Set up font_ptr_lo/hi to point to the font_data bitmap for the character in ; TMPCHR. Also sets lo_nybble_flag to let the caller know whether the ; bitmap is in the upper or lower 4 bits of the bytes pointed to. setup_font_ptr: lda #$00 sta font_ptr_hi sta lo_nybble_flag lda TMPCHR clc ror bcc font_hi_nybble ldx #$FF stx lo_nybble_flag font_hi_nybble: clc rol rol rol font_ptr_hi rol rol font_ptr_hi adc #font_data adc font_ptr_hi sta font_ptr_hi rts ; ---------------------------------------------------------------------------- ; Move the cursor one space to the right (to the next line if at the margin, ; and scroll screen if on the last row) advance_cursor: inc COLCRS lda right_margin cmp COLCRS bcs same_line lda LMARGN sta COLCRS lda ROWCRS ; $17 is 25 decimal, one row below the lowest on the screen cmp #$17 bcc no_scroll jsr scroll_screen ; Move to row 24 after scrolling lda #$16 sta ROWCRS no_scroll: inc ROWCRS same_line: rts ; ---------------------------------------------------------------------------- ; Clear the screen by setting all screen RAM bytes to zero. Slow, but not ; as slow as scrolling. ;;; .ifndef FUJICHAT ;;;clear_screen: ;;; lda SAVMSC ;;; sta screen_ptr_lo ;;; lda SAVMSC+1 ;;; sta screen_ptr_hi ;;; ldy #$00 ;;; ldx #$1D ;;; lda #$00 ;;; ;;;cls_loop: ;;; sta (screen_ptr_lo),y ;;; dey ;;; bne cls_loop ;;; inc screen_ptr_hi ;;; dex ;;; bne cls_loop ;;; jsr blank_bottom_row ;;; lda LMARGN ;;; sta COLCRS ;;; lda #$00 ;;; sta ROWCRS ;;; ; redundant JMP ;;; jmp return_success ;;; .endif ; ---------------------------------------------------------------------------- ; CIO expects the Y register to contain a status code. ; 1 means success (no error). Lots of COL80's routines ; jump here. return_success: ldy #$01 rts ; ---------------------------------------------------------------------------- ; Set screen_ptr_lo/hi to point to the address of the first byte of graphics ; data at the current cursor position. setup_screen_ptr: ldy ROWCRS lda SAVMSC clc adc row_low_offset_tab,y sta screen_ptr_lo lda SAVMSC+1 adc row_high_offset_tab,y sta screen_ptr_hi lda COLCRS lsr clc adc screen_ptr_lo bcc hi_byte_ok inc screen_ptr_hi hi_byte_ok: sta screen_ptr_lo rts ; ---------------------------------------------------------------------------- ; Tables of offsets for setup_screen_ptr, to avoid doing multiplication at ; runtime (the 6502 lacks a MUL instruction, so it's slow...) row_low_offset_tab: .byte $00,$40,$80,$C0,$00,$40,$80,$C0 .byte $00,$40,$80,$C0,$00,$40,$80,$C0 .byte $00,$40,$80,$C0,$00,$40,$80,$C0 row_high_offset_tab: .byte $00,$01,$02,$03,$05,$06,$07,$08 .byte $0A,$0B,$0C,$0D,$0F,$10,$11,$12 .byte $14,$15,$16,$17,$19,$1A,$1B,$1C ; ---------------------------------------------------------------------------- ; Copy pixel data from the font table to screen RAM. ; font_ptr_lo/hi must point to the correct character, and screen_ptr_lo/hi ; must point to the correct screen address for the current cursor position. ; This routine has separate execution paths for even- and odd-numbered ; cursor positions, since each byte of screen RAM holds data for two ; adjacent characters (and when printing to one of them, the other needs ; to be left undisturbed!) write_font_data: lda COLCRS clc ror bcc write_font_data_even ldx #$00 ldy #$00 get_font_nybble_odd: lda (font_ptr_lo),y bit lo_nybble_flag bne lo_nybble_odd ; glyph we want is stored in top 4 bits of font byte, ; shift it down to the bottom 4 bits lsr lsr lsr lsr lo_nybble_odd: eor inverse_mask and #$0F sta TMPCHR ldy scanline_offset_tab,x lda (screen_ptr_lo),y and #$F0 ora TMPCHR sta (screen_ptr_lo),y inx cpx #$07 bne screen_ptr_ok_odd inc screen_ptr_hi screen_ptr_ok_odd: cpx #$08 beq write_font_done_odd txa tay bne get_font_nybble_odd write_font_done_odd: rts ; ---------------------------------------------------------------------------- ; Write data to even-numbered columns, very similar to the above write_font_data_even: ldx #$00 ldy #$00 get_font_nybble_even: lda (font_ptr_lo),y bit lo_nybble_flag beq hi_nybble_even asl asl asl asl hi_nybble_even: eor inverse_mask and #$F0 sta TMPCHR ldy scanline_offset_tab,x lda (screen_ptr_lo),y and #$0F ora TMPCHR sta (screen_ptr_lo),y inx cpx #$07 bne screen_ptr_ok_even inc screen_ptr_hi screen_ptr_ok_even: cpx #$08 beq write_font_done_even txa tay bne get_font_nybble_even write_font_done_even: rts ; ---------------------------------------------------------------------------- scanline_offset_tab: .byte $00,$28,$50,$78,$A0,$C8,$F0,$18 ; ---------------------------------------------------------------------------- ; Callback for the internal get-one-byte, used by the OS to implement the ; CIO GET RECORD and GET BYTES commands. This routine takes no arguments, ; and returns the read byte in the accumulator. ; Internally, COL80 maintains a line buffer. Each time col80_getbyte is ; called, it returns the next character in the buffer. If the buffer's ; empty (or if the last call returned the last character), a new line ; of input is read from the user (and the first character is returned). ; This is exactly how the OS E: device works. col80_getbyte: lda BUFCNT beq get_line get_next_byte: ldx line_buffer_index lda line_buffer,x dec BUFCNT inc line_buffer_index jmp return_success ; ---------------------------------------------------------------------------- ; Get a line of input from the user, terminated by the Return key. get_line: lda #$00 sta BUFCNT sta line_buffer_index show_cursor: ; .ifdef FUJICHAT lda #$00 ; .else ; lda #$20 ; .endif sta TMPCHR lda #$FF sta inverse_mask jsr setup_font_ptr jsr setup_screen_ptr jsr write_font_data jsr get_keystroke cpy #$01 beq keystroke_ok ; .ifdef FUJICHAT dey ; yes, we really care about 1-byte optimizations ; .else ; ldy #$00 ; .endif sty line_buffer_index sty BUFCNT keystroke_ok: ; .ifdef FUJICHAT cmp #$20 bcc show_cursor ; ignore low ASCII ; .endif cmp #$9B bne check_backs_key jmp return_key_hit check_backs_key: cmp #$7E bne check_clear_key jmp backs_key_hit check_clear_key: cmp #$7D bne normal_key_hit jmp clear_key_hit normal_key_hit: ldx BUFCNT bpl buffer_character ; .ifdef FUJICHAT jmp show_cursor ; .else ; jmp beep ; .endif buffer_character: sta line_buffer,x jsr col80_putbyte inc BUFCNT jmp show_cursor return_key_hit: jsr print_space lda #$9B ldx BUFCNT sta line_buffer,x inc BUFCNT jsr col80_putbyte jmp get_next_byte clear_key_hit: ; .ifndef FUJICHAT ; jsr clear_screen ; .endif lda #$00 sta line_buffer_index sta BUFCNT jmp get_line backs_key_hit: jsr print_space lda BUFCNT beq backs_key_done dec COLCRS lda COLCRS clc adc #$01 cmp LMARGN bne backs_same_line lda right_margin sta COLCRS dec ROWCRS backs_same_line: dec BUFCNT backs_key_done: jmp show_cursor ; ---------------------------------------------------------------------------- ; Ring the margin bell. COL80 doesn't implement the ctrl-2 bell (character ; 253), and instead of using the GTIA keyclick speaker, it uses POKEY to ; make a beep ;;; .ifndef FUJICHAT ;;;beep: ldy #$00 ;;; ldx #$AF ;;; ;;;beep_delay_x: ;;; stx AUDF1 ;;; stx AUDC1 ;;; ;;;beep_delay_y: ;;; dey ;;; bne beep_delay_y ;;; dex ;;; cpx #$9F ;;; bne beep_delay_x ;;; jmp show_cursor ;;; .endif ; ---------------------------------------------------------------------------- ; Print a space character at the current cursor position. Does not ; update the cursor position. print_space: lda #$00 sta inverse_mask ;;; .ifndef FUJICHAT ;;; lda #$20 ;;; .endif sta TMPCHR jsr setup_font_ptr jsr setup_screen_ptr jsr write_font_data rts ; ---------------------------------------------------------------------------- ; Get a keystroke (blocking). Just calls the OS K: get-one-byte routine ; (call by pushing address-minus-one then doing an RTS) get_keystroke: lda k_dev_get_hi pha lda k_dev_get_lo pha rts ; ---------------------------------------------------------------------------- ; COL80 vector table, in the format required by the OS. Our HATABS entry ; will point to this table, and the OS will call the routines listed here ; via the "call by RTS" method (which is why they're address-minus-one). ; See the entry on HATABS in "Mapping the Atari" or the OS manual. col80_vector_tab: .word col80_open-1 .word col80_close-1 .word col80_getbyte-1 .word col80_putbyte-1 .word col80_close-1 .word col80_close-1 jmp col80_init END_ADDRESS = *-1 START_ADDRESS_2 = $03FD ; xex segment header #2 .word START_ADDRESS_2 .word END_ADDRESS_2 .rorg START_ADDRESS_2 ; ---------------------------------------------------------------------------- ; Various bits of runtime state here. It's unclear to me why the standard ; OS buffer location couldn't have been used instead (normally the top ; half of page 5), or why the other stuff couldn't have been stored in ; zero page, in locations used by the ROM E: handler (thus unused when ; it's replaced with COL80). line_buffer_index needs to be preserved ; across calls to col80_getbyte, but lo_nybble_flag and inverse_mask are ; freshly calculated every time they're used, so they could be almost ; anywhere. lo_nybble_flag: .byte $00 inverse_mask: .byte $00 line_buffer_index: .byte $12 ; ---------------------------------------------------------------------------- ; Initialization callback. The OS will call this on coldstart (or would do, ; if the driver were in ROM), and also on warmstart (because we stole the ; DOSINI vector). ; This routine is also the first thing that gets called by the mainline ; init code. Its job is to install COL80 in the handler table at HATABS. ; Actually the handler is first installed as X:, then the main init code ; fixes this up to E: unless the user is holding down SELECT. This allows ; the user to toggle between the 40-column ROM E: and COL80 without doing ; a full reboot. No idea if this was a documented feature or something the ; author used for development/debugging. ; .ifdef FUJICHAT ;col80_init = return_success ; .else col80_init: ldy #$00 next_hatab_slot: lda HATABS,y beq register_x_handler iny iny iny cpy #$20 bcc next_hatab_slot jmp return_success register_x_handler: lda #$58 sta HATABS,y lda #col80_vector_tab iny sta HATABS,y jmp return_success ; .endif ; ---------------------------------------------------------------------------- ; The OS jumps here on warmstart (also, this is the run address in our ; binary load file) dosini_entry_point: ;;; .ifndef FUJICHAT ;;; nop ;;; nop ;;; nop ;;; .endif main_entry_point: ;;; .ifndef FUJICHAT jsr col80_init ;;; lda CONSOL ;;; and #$04 ;;; beq no_e_handler ;;; .endif lda #$0C sta ICCOM ldx #$00 jsr CIOV ; .ifndef FUJICHAT ; note: will not work with BASIC! DOS/etc OK lda #$58 sta font_ptr_lo lda #$03 sta ICCOM lda #font_ptr_lo sta ICBAL lda #$00 sta ICBAH ldx #$00 jsr CIOV ; .endif ldy #$07 lda #col80_vector_tab iny sta HATABS,y no_e_handler: lda #START_ADDRESS sta MEMTOP+1 jmp return_success END_ADDRESS_2 = *-1 ; xex segment header #3 ;.word RUNAD ;.word RUNAD+1 .word INITAD .word INITAD+1 .word main_entry_point ; ---------------------------------------------------------------------------- ; (when does this actually get called? da65 can't find any references ; to it, and it's not a run or init address in the binary load file) ;;; .ifndef FUJICHAT ;;; lda #dosini_entry_point ;;; sta DOSINI+1 ;;; jmp main_entry_point ;;; .endif ; ---------------------------------------------------------------------------- ; There's absolutely no reason why this data needs to be included in the ; binary load file: the line buffer's initial contents are meaningless, they ; will be blown away the first time anything reads from the E: device. ; Notice the author was running his debugger in COL80 when he built the ; binary (ASCII "S COL80 7A00 7F80" command still in the buffer). ;.ifdef FUJICHAT line_buffer = $0400 ; cassette buffer ;.else ;line_buffer: ;.byte $53,$20,$43,$4F,$4C,$38,$30,$20 ;.byte $37,$41,$30,$30,$20,$37,$46,$38 ;.byte $30,$9B,$20,$20,$20,$20,$9B,$27 ;.byte $40,$40,$40,$40,$28,$28,$28,$28 ;.byte $40,$40,$40,$40,$40,$40,$40,$40 ;.byte $40,$40,$40,$40,$40,$40,$40,$40 ;.byte $9B,$FD,$FD,$FD,$FD,$9B ;.endif ; I've found a variant (modified version?) of this code, that doesn't ; include the line_buffer in the file (no reason for it to be there), ; or the $0C segment, and that has another segment, loaded at $6000, ; with the run address changed to $6000. The code looks like: ; .org $6000 ; jsr dosini_entry_point ; lda #$50 ; sta RMARGN ; lda #$00 ; sta COLOR2 ; also, the default colors have been changed in init_graphics_8. ; There are at least two binaries floating around that contain ; extra (garbage) bytes at the end, presumably from being transferred ; over XMODEM or similar. They are otherwise identical.