diff --git a/ac6502/ac6502.cfg b/ac6502/ac6502.cfg index eb7f79fc..a99b312d 100644 --- a/ac6502/ac6502.cfg +++ b/ac6502/ac6502.cfg @@ -1,27 +1,52 @@ # SPDX-FileCopyrightText: 2022-2026 Willis Blackburn # # SPDX-License-Identifier: MIT +# +# Linker configuration for VC83 BASIC as an ac6502 cartridge. +# +# Cartridge ROM overlays $C000-$FFFF. The BIOS Kernal jump table at +# $A000 and its implementation at $A100-$B7FF remain accessible, as +# does the character-set ROM at $B800-$BFFF. BASIC runs from ROM and +# uses RAM at $0800-$7FFF for its workspace (buffers, stacks, BSS) and +# the user program / variables / string heap. +# +# Zero page layout (per 6502-BIOS/BIOS.inc): +# $00-$03 System pointers (BIOS) +# $04-$23 Reserved by native BIOS BASIC (unused here) +# $24-$35 System scratch (CF, XFER, XModem, Monitor) +# $3A-$FF Free for user programs -> VC83 BASIC zero page lives here +# +# See https://github.com/acwright/6502 for more info -FEATURES { - STARTADDRESS: default = $0800; -} SYMBOLS { + __STACKSIZE__: type = weak, value = $0000; + __ZPSTART__: type = weak, value = $003A; + __ZPSIZE__: type = weak, value = $00C6; + __RAM_START__: type = weak, value = $0800; __HIMEM__: type = weak, value = $8000; + __ROM_START__: type = weak, value = $C000; } MEMORY { - ZEROPAGE: file = "", start = $0080, size = $0080; - MAIN: file = %O, start = %S, size = __HIMEM__ - %S, define = yes; - BSS: file = "", start = __ONCE_RUN__, size = __HIMEM__ - __ONCE_RUN__; + ZEROPAGE: file = "", start = __ZPSTART__, size = __ZPSIZE__; + MAIN: file = "", start = __RAM_START__, size = __HIMEM__ - __RAM_START__, define = yes; + # AT28C256 is 32 KB. The chip's lower half ($0000-$3FFF in the file) + # is filled with zeros; the upper half ($4000-$7FFF) carries the + # cartridge ROM that the CPU sees at $C000-$FFFF. PAD is the first + # region written to the output file so the linker emits zeros there. + PAD: file = %O, start = $8000, size = $4000, fill = yes, fillval = $00; + ROM: file = %O, start = __ROM_START__, size = $FFFA - __ROM_START__, fill = yes, fillval = $00; + VECTORS: file = %O, start = $FFFA, size = $0006, fill = yes, fillval = $00; } SEGMENTS { - ZEROPAGE: load = ZEROPAGE, type = zp; - STARTUP: load = MAIN, type = ro; - CODE: load = MAIN, type = ro; - VEC: load = MAIN, type = ro, define = yes; - XVEC: load = MAIN, type = ro, define = yes; - FUNC: load = MAIN, type = ro, define = yes; - XFUNC: load = MAIN, type = ro, define = yes; - PARSER: load = MAIN, type = ro; - ONCE: load = MAIN, type = ro, define = yes; - BSS: load = BSS, type = bss, define = yes; + ZEROPAGE: load = ZEROPAGE, type = zp; + STARTUP: load = ROM, type = ro; + CODE: load = ROM, type = ro; + VEC: load = ROM, type = ro, define = yes; + XVEC: load = ROM, type = ro, define = yes; + FUNC: load = ROM, type = ro, define = yes; + XFUNC: load = ROM, type = ro, define = yes; + PARSER: load = ROM, type = ro; + ONCE: load = ROM, type = ro, define = yes; + BSS: load = MAIN, type = bss, define = yes, align = $100; + VECTORS: load = VECTORS, type = ro; } diff --git a/ac6502/ac6502.inc b/ac6502/ac6502.inc index bcaa4fb1..ecd81457 100644 --- a/ac6502/ac6502.inc +++ b/ac6502/ac6502.inc @@ -1,13 +1,93 @@ ; SPDX-FileCopyrightText: 2022-2026 Willis Blackburn ; ; SPDX-License-Identifier: MIT +; +; ac6502 target include -- symbolic names for BIOS Kernal routines and +; system variables used by VC83 BASIC. See 6502-BIOS/BIOS.inc for the +; full documentation of the Kernal jump table. +; +; See https://github.com/acwright/6502-BIOS for more info + +BAS_LINBUF_SIZE = 240 + +CH_CTRLC = $03 +CH_BEL = $07 +CH_BKSP = $08 +CH_LF = $0A +CH_CR = $0D +CH_ESC = $1B +CH_SPACE = $20 +CH_DEL = $7F + +; --- Zero-page system pointers (BIOS) --- +STR_PTR := $02 ; $02-$03 - BIOS string pointer + +; --- BIOS Kernal jump table ($A000-$A0FF) --- +Chrout := $A000 ; Output character (routed by IO_MODE) +Chrin := $A003 ; Non-blocking input char (C=1 if available) +WriteBuffer := $A006 +ReadBuffer := $A009 +BufferSize := $A00C +SetIOMode := $A00F +GetIOMode := $A012 +InitVideo := $A015 +VideoClear := $A018 +VideoPutChar := $A01B +VideoSetCursor := $A01E +VideoGetCursor := $A021 +VideoScroll := $A024 +VideoSetColor := $A027 +VideoChroutRaw := $A02A +InitSID := $A02D +Beep := $A030 +SidPlayNote := $A033 +SidSilence := $A036 +SidSetVolume := $A039 +FsLoadFile := $A03C +FsSaveFile := $A03F +FsDeleteFile := $A042 +InitKB := $A045 +ReadJoystick1 := $A048 +ReadJoystick2 := $A04B +InitSC := $A04E +SerialChrout := $A051 +XModemLoad := $A054 +XModemSave := $A057 +RtcReadTime := $A05A +RtcReadDate := $A05D +RtcWriteTime := $A060 +RtcWriteDate := $A063 +RtcReadNVRAM := $A066 +RtcWriteNVRAM := $A069 +StReadSector := $A06C +StWriteSector := $A06F +StWaitReady := $A072 +SysDelay := $A075 +KernalInit := $A078 ; Initialize all hardware; caller resets SP, must CLI after +KernalVersion := $A07B + +; --- System RAM variables used here --- +IRQ_PTR := $0300 +BRK_PTR := $0302 +NMI_PTR := $0304 +IO_MODE := $0306 +HW_PRESENT := $030D +BOOT_VECTOR := $035B -BAS_LINBUF_SIZE = 240 +; Legacy aliases used by earlier ac6502 code. +CHROUT := Chrout +CHRIN := Chrin -CH_CR = $0D ; Carriage return -CH_LF = $0A ; Line feed -CH_SPACE = $20 ; Space -CH_BKSP = $08 ; Backspace +; --- HW_PRESENT bit masks --- +HW_RAM_L = %00000001 +HW_RAM_H = %00000010 +HW_RTC = %00000100 +HW_CF = %00001000 +HW_SC = %00010000 +HW_GPIO = %00100000 +HW_SID = %01000000 +HW_VID = %10000000 -CHROUT := $A000 -CHRIN := $A003 +; --- RTC / storage RAM variables used by kernal routines --- +RTC_BUF_CENT := $030B ; Century register for RTC read/write +RAM_BANK_L := $83FF ; Write-only bank latch for low RAM card diff --git a/ac6502/ac6502_extension.s b/ac6502/ac6502_extension.s index 6e8fec36..96ecf0ae 100644 --- a/ac6502/ac6502_extension.s +++ b/ac6502/ac6502_extension.s @@ -1,21 +1,552 @@ ; SPDX-FileCopyrightText: 2022-2026 Willis Blackburn ; ; SPDX-License-Identifier: MIT +; +; ac6502 BASIC extensions. Adds hardware-specific statements and +; functions that mirror the Integer BASIC built into the 6502-BIOS: +; CLS, LOCATE, COLOR, SOUND, VOL, PAUSE, WAIT, TIME, DATE, SETTIME, +; SETDATE, NVRAM, BANK, MEM, SYS and JOY(), INKEY(), NVRAM(). +; +; Missing hardware is handled gracefully: statements print "NO DEVICE" +; (or silently skip for video/sound commands, matching the BIOS); the +; hardware-reading functions return 0. + +.setcpu "65C02" + +; --- Parser name tables and argument parsers -------------------------------- .segment "PARSER" ex_statement_name_table: - name_table_end + name_table_entry "CLS" +: name_table_entry "LOCATE" + JUMP pvm_arg_2 +: name_table_entry "COLOR" + JUMP pvm_arg_2 +: name_table_entry "SOUND" + JUMP ex_pvm_arg_3 +: name_table_entry "VOL" + JUMP pvm_expression +: name_table_entry "PAUSE" + JUMP pvm_expression +: name_table_entry "WAIT" + JUMP pvm_arg_2 +: name_table_entry "TIME" +: name_table_entry "DATE" +: name_table_entry "SETTIME" + JUMP ex_pvm_arg_3 +: name_table_entry "SETDATE" + JUMP ex_pvm_arg_4 +: name_table_entry "NVRAM" + JUMP pvm_arg_2 +: name_table_entry "BANK" + JUMP pvm_expression +: name_table_entry "MEM" +: name_table_entry "SYS" + JUMP pvm_expression +: name_table_end + +; Helper PVM fragments for 3- and 4-argument statements. + +ex_pvm_arg_3: + CALL pvm_expression + ARGSEP + CALL pvm_expression + ARGSEP + JUMP pvm_expression + +ex_pvm_arg_4: + CALL pvm_expression + ARGSEP + CALL pvm_expression + ARGSEP + CALL pvm_expression + ARGSEP + JUMP pvm_expression ex_function_name_table: - name_table_end + name_table_entry "JOY" +: name_table_entry "INKEY" +: name_table_entry "NVRAM" +: name_table_end + +; --- Statement dispatch vectors --------------------------------------------- .segment "XVEC" ex_statement_vectors: + .word exec_cls-1 + .word exec_locate-1 + .word exec_color-1 + .word exec_sound-1 + .word exec_vol-1 + .word exec_pause-1 + .word exec_wait-1 + .word exec_time-1 + .word exec_date-1 + .word exec_settime-1 + .word exec_setdate-1 + .word exec_nvram_w-1 + .word exec_bank-1 + .word exec_mem-1 + .word exec_sys-1 + +; --- Function dispatch table ------------------------------------------------ .segment "XFUNC" ex_function_table: + .word fun_joy-1 + .byte 1 | PROLOG_POP_INT | EPILOG_PUSH_INT + .word fun_inkey-1 + .byte 1 | PROLOG_POP_INT | EPILOG_PUSH_INT + .word fun_nvram-1 + .byte 1 | PROLOG_POP_INT | EPILOG_PUSH_INT + +; --- Implementations -------------------------------------------------------- .code + +; --------------------------------------------------------------------------- +; Small utilities +; --------------------------------------------------------------------------- + +; Print a null-terminated string pointed to by AX (no newline). +ex_print_cstr: + stax BC + ldy #0 +@next: + lda (BC),y + beq @done + jsr putch + iny + bne @next +@done: + rts + +; Print a null-terminated string pointed to by AX followed by a newline. +ex_print_cstr_nl: + jsr ex_print_cstr + jmp newline + +; Print "NO DEVICE" + newline. +ex_no_device: + ldax #ex_str_no_device + jmp ex_print_cstr_nl + +ex_str_no_device: + .byte "NO DEVICE", 0 + +; Print A as two decimal digits (A must be 0-99). +ex_print_2d: + ldx #0 +@tens: + cmp #10 + bcc @done + sec + sbc #10 + inx + bne @tens ; unconditional (X was just incremented so it's non-zero) +@done: + pha + txa + clc + adc #'0' + jsr putch + pla + clc + adc #'0' + jmp putch + +; Print A as two hex digits. +ex_print_2h: + pha + lsr a + lsr a + lsr a + lsr a + jsr ex_print_nib + pla + and #$0F +ex_print_nib: + cmp #10 + bcc @digit + clc + adc #'A' - 10 + jmp putch +@digit: + clc + adc #'0' + jmp putch + +; Convert AX (16-bit signed int) to FP0 and print as a number. +ex_print_ax: + jsr int_to_fp + jmp print_number + +; Check for Ctrl-C / ESC in the keyboard buffer. Raise ERR_STOPPED if found. +ex_break_check: + jsr Chrin ; Non-blocking (C=1 if a char is waiting) + bcc @done + cmp #CH_ESC + beq @brk + cmp #CH_CTRLC + bne @done +@brk: + lda #ERR_STOPPED + jmp on_raise +@done: + rts + +; --------------------------------------------------------------------------- +; Video statements: CLS, LOCATE, COLOR +; --------------------------------------------------------------------------- + +exec_cls: + bit HW_PRESENT ; HW_VID is bit 7 + bpl @skip + jmp VideoClear +@skip: + rts + +exec_locate: + jsr evaluate_argument_list + jsr pop_int_fp0 ; col + sta D ; D = col + jsr pop_int_fp0 ; row + tay ; Y = row + ldx D ; X = col + bit HW_PRESENT + bpl @skip + jmp VideoSetCursor +@skip: + rts + +exec_color: + jsr evaluate_argument_list + jsr pop_int_fp0 ; bg + and #$0F + sta D ; D = bg nibble + jsr pop_int_fp0 ; fg + asl a + asl a + asl a + asl a + ora D ; (fg<<4) | bg + bit HW_PRESENT + bpl @skip + jmp VideoSetColor +@skip: + rts + +; --------------------------------------------------------------------------- +; Sound statements: SOUND voice, freq, dur / VOL n +; --------------------------------------------------------------------------- + +exec_sound: + jsr evaluate_argument_list + jsr pop_int_fp0 ; dur -> AX + pha ; push dur_lo on CPU stack + txa + pha ; push dur_hi on CPU stack + jsr pop_int_fp0 ; freq (Hz) -> AX (A=lo, X=hi) + ; Convert Hz to SID register: reg = Hz*16 + Hz - Hz/4 (= Hz * 16.75) + ; Matches the conversion in the BIOS BASIC BasCmdSound routine. + sta B ; B = Hz_lo (original) + stx C ; C = Hz_hi (original) + lda B ; copy into D:E for the shifted accumulator + sta D + lda C + sta E + asl D ; D:E = Hz * 16 (4 left shifts) + rol E + asl D + rol E + asl D + rol E + asl D + rol E + clc ; D:E = Hz * 17 + lda D + adc B + sta D + lda E + adc C + sta E + lsr C ; B:C = Hz / 4 (2 right shifts) + ror B + lsr C + ror B + sec ; D:E = Hz * 17 - Hz/4 = Hz * 16.75 + lda D + sbc B + sta D + lda E + sbc C + sta E + ; Push converted SID freq (D=lo, E=hi) so we can pop voice next + lda E + pha ; push freqHi on CPU stack + lda D + pha ; push freqLo on CPU stack + jsr pop_int_fp0 ; voice (1-3) -> A + dec a ; convert to 0-indexed (0-2) + sta E ; E = voice + lda HW_PRESENT + and #HW_SID + beq @no_sid + pla ; freqLo + tax ; X = freqLo + pla ; freqHi + tay ; Y = freqHi + lda E ; A = voice (0-indexed) + jsr SidPlayNote + pla ; dur_hi + tax ; X = dur_hi + pla ; dur_lo + jsr SysDelay + jmp SidSilence +@no_sid: + pla ; discard freqLo + pla ; discard freqHi + pla ; discard dur_hi + pla ; discard dur_lo + rts + +exec_vol: + jsr evaluate_argument_list + jsr pop_int_fp0 ; level + sta D + lda HW_PRESENT + and #HW_SID + beq @skip + lda D + jmp SidSetVolume +@skip: + rts + +; --------------------------------------------------------------------------- +; Timing statements: PAUSE n / WAIT addr, mask +; --------------------------------------------------------------------------- + +exec_pause: + jsr evaluate_argument_list + jsr pop_int_fp0 ; count (AX = lo/hi centiseconds) + jmp SysDelay + +exec_wait: + jsr evaluate_argument_list + jsr pop_int_fp0 ; mask + sta D ; D = mask + jsr pop_int_fp0 ; address + stax BC ; BC = pointer +@loop: + jsr ex_break_check + ldy #0 + lda (BC),y + and D + beq @loop + rts + +; --------------------------------------------------------------------------- +; Time / date statements +; --------------------------------------------------------------------------- + +exec_time: + lda HW_PRESENT + and #HW_RTC + bne :+ + jmp ex_no_device +: jsr RtcReadTime ; A=hours, X=minutes, Y=seconds + phy ; save seconds + phx ; save minutes + jsr ex_print_2d ; hours + lda #':' + jsr putch + pla ; minutes + jsr ex_print_2d + lda #':' + jsr putch + pla ; seconds + jsr ex_print_2d + jmp newline + +exec_date: + lda HW_PRESENT + and #HW_RTC + bne :+ + jmp ex_no_device +: jsr RtcReadDate ; A=day, X=month, Y=year; RTC_BUF_CENT=century + pha ; save day (last out) + phx ; save month + phy ; save year (first out) + lda RTC_BUF_CENT + jsr ex_print_2d ; century + pla ; year + jsr ex_print_2d + lda #'-' + jsr putch + pla ; month + jsr ex_print_2d + lda #'-' + jsr putch + pla ; day + jsr ex_print_2d + jmp newline + +exec_settime: + jsr evaluate_argument_list + jsr pop_int_fp0 ; ss + sta C ; C = seconds + jsr pop_int_fp0 ; mm + sta D ; D = minutes + jsr pop_int_fp0 ; hh + sta E ; E = hours + lda HW_PRESENT + and #HW_RTC + bne :+ + jmp ex_no_device +: lda E ; A = hours + ldx D ; X = minutes + ldy C ; Y = seconds + jmp RtcWriteTime + +exec_setdate: + jsr evaluate_argument_list + jsr pop_int_fp0 ; dd + sta B ; B = day + jsr pop_int_fp0 ; mm + sta C ; C = month + jsr pop_int_fp0 ; yy + sta D ; D = year + jsr pop_int_fp0 ; cc + sta RTC_BUF_CENT + lda HW_PRESENT + and #HW_RTC + bne :+ + jmp ex_no_device +: lda B ; A = day + ldx C ; X = month + ldy D ; Y = year + jmp RtcWriteDate + +; --------------------------------------------------------------------------- +; NVRAM addr, value (write) +; --------------------------------------------------------------------------- + +exec_nvram_w: + jsr evaluate_argument_list + jsr pop_int_fp0 ; value + sta D ; D = value + jsr pop_int_fp0 ; address + sta E ; E = address + lda HW_PRESENT + and #HW_RTC + bne :+ + jmp ex_no_device +: ldx E ; X = address + lda D ; A = value + jmp RtcWriteNVRAM + +; --------------------------------------------------------------------------- +; System statements: BANK, MEM, SYS +; --------------------------------------------------------------------------- + +exec_bank: + jsr evaluate_argument_list + jsr pop_int_fp0 ; bank number + sta D ; D = bank + lda HW_PRESENT + and #HW_RAM_L + bne :+ + jmp ex_no_device +: lda D + sta RAM_BANK_L + rts + +exec_mem: + jsr fun_fre ; A=lo, X=hi of free bytes + jsr ex_print_ax ; print free bytes + ldax #ex_str_free + jsr ex_print_cstr_nl + ldax #ex_str_hw + jsr ex_print_cstr + lda #'$' + jsr putch + lda HW_PRESENT + jsr ex_print_2h + jsr newline + ldax #ex_str_io + jsr ex_print_cstr + lda IO_MODE + beq @video + ldax #ex_str_serial + jmp ex_print_cstr_nl +@video: + ldax #ex_str_video + jmp ex_print_cstr_nl + +ex_str_free: .byte " FREE", 0 +ex_str_hw: .byte "HW=", 0 +ex_str_io: .byte "IO=", 0 +ex_str_video: .byte "VIDEO", 0 +ex_str_serial: .byte "SERIAL", 0 + +exec_sys: + jsr evaluate_argument_list + jsr pop_int_fp0 ; address + stax BC + jsr ex_sys_call + rts +ex_sys_call: + jmp (BC) + +; --------------------------------------------------------------------------- +; Functions +; --------------------------------------------------------------------------- + +; JOY(n) -- return joystick bitmask for port n (1 or 2); 0 if GPIO absent. +fun_joy: + sta D ; save port number + lda HW_PRESENT + and #HW_GPIO + beq @none + lda D + cmp #2 + bcs @p2 ; n >= 2 -> port 2 + jsr ReadJoystick1 + ldx #0 + rts +@p2: + jsr ReadJoystick2 + ldx #0 + rts +@none: + lda #0 + tax + rts + +; INKEY(x) -- return ASCII code of a pending key, or 0 if none. +; The argument is ignored (vc83 functions require at least one arg). +fun_inkey: + jsr Chrin ; C=1 if char available + bcs @got + lda #0 +@got: + ldx #0 + rts + +; NVRAM(addr) -- read RTC NVRAM byte; returns 0 if RTC absent. +fun_nvram: + sta D ; D = address + lda HW_PRESENT + and #HW_RTC + beq @none + ldx D ; X = address + jsr RtcReadNVRAM ; returns byte in A + ldx #0 + rts +@none: + lda #0 + tax + rts diff --git a/ac6502/ac6502_init.s b/ac6502/ac6502_init.s index df77ab20..a890248b 100644 --- a/ac6502/ac6502_init.s +++ b/ac6502/ac6502_init.s @@ -1,24 +1,39 @@ ; SPDX-FileCopyrightText: 2022-2026 Willis Blackburn ; ; SPDX-License-Identifier: MIT +; +; ac6502 BASIC workspace (buffers and interpreter stacks) and the +; target-specific one-time initialization hook. All mutable storage is +; placed in the BSS segment so __BSS_SIZE__ correctly reflects how much +; RAM is consumed before the user program area. BASIC's program_ptr is +; computed from __BSS_RUN__ + __BSS_SIZE__ (see program.s). +; +; See https://github.com/acwright/6502 for more info + +.segment "BSS" -; Buffers +; Align to a page boundary so that `stack` (which follows two +; page-sized buffers) lands on a page boundary. control.s asserts +; `