diff --git a/.gitignore b/.gitignore index 5c8daed..1ac18e6 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,17 @@ build/ *~ #* .#* + +# Automatically generated source files +acorn.asm +asmb.asm +data.asm +eval.asm +exec.asm +main.asm +math.asm + +# ZDS build files +Debug/ +Release/ +*.wsp \ No newline at end of file diff --git a/README.md b/README.md index 384e103..e011819 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# BBCZ80 +# BBCZ80 for the Agon Light +This is a fork of BBCZ80 by R.T.Russell with a rewritten MOS layer for the Agon Light. + +## BBCZ80 BBC BASIC (Z80) v5 is an implementation of the BBC BASIC programming language for the Z80 CPU. It is largely compatible with Acorn's ARM BASIC V but with a few language extensions based on features of 'BBC BASIC for Windows' and 'BBC BASIC for SDL 2.0'. These extensions include the @@ -16,3 +19,23 @@ to build the Acorn Z80 Second Processor edition. Note that the name 'BBC BASIC' is used by permission of the British Broadcasting Corporation and is not transferrable to a derived or forked work. + +## Agon Light +The Agon Light version uses the following files from the original fork: + +- ACORN.Z80 +- ASMB.Z80 +- DATA.Z80 +- EVAL.Z80 +- EXEC.Z80 +- MAIN.Z80 +- MATH.Z80 + +All Agon specific source code and ZDSII project files are in the folder src/zds. + +### Building + +The files from the original fork are converted to work with the ZDS assembler by the Python script tools/transform_source.py +and written out to the folder src/zds. + +To build, load the project "BBC BASIC" into the Zilog ZDSII IDE and press F7. \ No newline at end of file diff --git a/src/zds/BBC Basic.zdsproj b/src/zds/BBC Basic.zdsproj new file mode 100644 index 0000000..a2d8bde --- /dev/null +++ b/src/zds/BBC Basic.zdsproj @@ -0,0 +1,220 @@ + +eZ80F92 + + + +.\agon_graphics.asm +.\agon_gpio.asm +.\agon_init.asm +.\agon_interrupt.asm +.\agon_misc.asm +.\agon_os.asm +.\agon_sound.asm +.\acorn.asm +.\asmb.asm +.\data.asm +.\eval.asm +.\exec.asm +.\main.asm +.\math.asm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/zds/agon_gpio.asm b/src/zds/agon_gpio.asm new file mode 100644 index 0000000..be171e8 --- /dev/null +++ b/src/zds/agon_gpio.asm @@ -0,0 +1,110 @@ +; +; Title: BBC Basic for AGON - GPIO functions +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 04/12/2024 +; +; Modinfo: + + INCLUDE "macros.inc" + INCLUDE "equs.inc" + + .ASSUME ADL = 0 + + SEGMENT CODE + + XDEF GPIOB_SETMODE + + XREF SWITCH_A + +; A: Mode +; B: Pins +; +GPIOB_SETMODE: CALL SWITCH_A + DW GPIOB_M0 ; Output + DW GPIOB_M1 ; Input + DW GPIOB_M2 ; Open Drain IO + DW GPIOB_M3 ; Open Source IO + DW GPIOB_M4 ; Interrupt, Dual Edge + DW GPIOB_M5 ; Alt Function + DW GPIOB_M6 ; Interrupt, Active Low + DW GPIOB_M7 ; Interrupt, Active High + DW GPIOB_M8 ; Interrupt, Falling Edge + DW GPIOB_M9 ; Interrupt, Rising Edge + +; Output +; +GPIOB_M0: RES_GPIO PB_DDR, B + RES_GPIO PB_ALT1, B + RES_GPIO PB_ALT2, B + RET + +; Input +; +GPIOB_M1: SET_GPIO PB_DDR, B + RES_GPIO PB_ALT1, B + RES_GPIO PB_ALT2, B + RET + +; Open Drain IO +; +GPIOB_M2: RES_GPIO PB_DDR, B + SET_GPIO PB_ALT1, B + RES_GPIO PB_ALT2, B + RET + +; Open Source IO +; +GPIOB_M3: SET_GPIO PB_DDR, B + SET_GPIO PB_ALT1, B + RES_GPIO PB_ALT2, B + RET + +; Interrupt, Dual Edge +; +GPIOB_M4: SET_GPIO PB_DR, B + RES_GPIO PB_DDR, B + RES_GPIO PB_ALT1, B + RES_GPIO PB_ALT2, B + RET + +; Alt Function +; +GPIOB_M5: SET_GPIO PB_DDR, B + RES_GPIO PB_ALT1, B + SET_GPIO PB_ALT2, B + RET + +; Interrupt, Active Low +; +GPIOB_M6: RES_GPIO PB_DR, B + RES_GPIO PB_DDR, B + SET_GPIO PB_ALT1, B + SET_GPIO PB_ALT2, B + RET + + +; Interrupt, Active High +; +GPIOB_M7: SET_GPIO PB_DR, B + RES_GPIO PB_DDR, B + SET_GPIO PB_ALT1, B + SET_GPIO PB_ALT2, B + RET + + +; Interrupt, Falling Edge +; +GPIOB_M8: RES_GPIO PB_DR, B + SET_GPIO PB_DDR, B + SET_GPIO PB_ALT1, B + SET_GPIO PB_ALT2, B + RET + +; Interrupt, Rising Edge +; +GPIOB_M9: SET_GPIO PB_DR, B + SET_GPIO PB_DDR, B + SET_GPIO PB_ALT1, B + SET_GPIO PB_ALT2, B + RET \ No newline at end of file diff --git a/src/zds/agon_graphics.asm b/src/zds/agon_graphics.asm new file mode 100644 index 0000000..6569d4c --- /dev/null +++ b/src/zds/agon_graphics.asm @@ -0,0 +1,156 @@ +; +; Title: BBC Basic for AGON - Graphics stuff +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 17/12/2024 +; +; Modinfo: +; 11/12/2024: Modified POINT_ to work with OSWORD +; 17/12/2024: Modified GETSCHR + + .ASSUME ADL = 0 + + INCLUDE "equs.inc" + INCLUDE "macros.inc" + INCLUDE "mos_api.inc" ; In MOS/src + + SEGMENT CODE + + XDEF MODE_ + XDEF COLOUR_ + XDEF POINT_ + XDEF GETSCHR + + XREF ACCS + XREF OSWRCH + XREF ASC_TO_NUMBER + XREF EXTERR + XREF EXPRI + XREF COMMA + XREF XEQ + XREF NXT + XREF BRAKET + XREF CRTONULL + XREF NULLTOCR + XREF CRLF + XREF EXPR_W2 + +; MODE n: Set video mode +; +MODE_: PUSH IX ; Get the system vars in IX + MOSCALL mos_sysvars ; Reset the semaphore + RES.LIL 4, (IX+sysvar_vpd_pflags) + CALL EXPRI + EXX + VDU 16H ; Mode change + VDU L + MOSCALL mos_sysvars +$$: BIT.LIL 4, (IX+sysvar_vpd_pflags) + JR Z, $B ; Wait for the result + POP IX + JP XEQ + +; +; Fetch a character from the screen +; - DE: X coordinate +; - HL: Y coordinate +; Returns +; - A: The character or FFh if no match +; - F: C if match, otherwise NC +; +GETSCHR: PUSH IX ; Get the system vars in IX + MOSCALL mos_sysvars ; Reset the semaphore + RES.LIL 1, (IX+sysvar_vpd_pflags) + VDU 23 + VDU 0 + VDU vdp_scrchar + VDU E + VDU D + VDU L + VDU H +$$: BIT.LIL 1, (IX+sysvar_vpd_pflags) + JR Z, $B ; Wait for the result + LD.LIL A, (IX+sysvar_scrchar) ; Fetch the result in A + OR A ; Check for 00h + SCF ; C = character map + JR NZ, $F ; We have a character, so skip next bit + XOR A ; Clear carry +$$: POP IX + RET + +; POINT(x,y): Get the pixel colour of a point on screen +; Parameters: +; - DE: X-coordinate +; - HL: Y-coordinate +; Returns: +; - A: Pixel colour +; +POINT_: PUSH IX ; Get the system vars in IX + MOSCALL mos_sysvars ; Reset the semaphore + RES.LIL 2, (IX+sysvar_vpd_pflags) + VDU 23 + VDU 0 + VDU vdp_scrpixel + VDU E + VDU D + VDU L + VDU H +$$: BIT.LIL 2, (IX+sysvar_vpd_pflags) + JR Z, $B ; Wait for the result +; +; Return the data as a 1 byte index +; + LD.LIL A, (IX+(sysvar_scrpixelIndex)) + POP IX + RET + +; COLOUR colour +; COLOUR L,P +; COLOUR L,R,G,B +; +COLOUR_: CALL EXPRI ; The colour / mode + EXX + LD A, L + LD (VDU_BUFFER+0), A ; Store first parameter + CALL NXT ; Are there any more parameters? + CP ',' + JR Z, COLOUR_1 ; Yes, so we're doing a palette change next +; + VDU 11h ; Just set the colour + VDU (VDU_BUFFER+0) + JP XEQ +; +COLOUR_1: CALL COMMA + CALL EXPRI ; Parse R (OR P) + EXX + LD A, L + LD (VDU_BUFFER+1), A + CALL NXT ; Are there any more parameters? + CP ',' + JR Z, COLOUR_2 ; Yes, so we're doing COLOUR L,R,G,B +; + VDU 13h ; VDU:COLOUR + VDU (VDU_BUFFER+0) ; Logical Colour + VDU (VDU_BUFFER+1) ; Palette Colour + VDU 0 ; RGB set to 0 + VDU 0 + VDU 0 + JP XEQ +; +COLOUR_2: CALL COMMA + CALL EXPRI ; Parse G + EXX + LD A, L + LD (VDU_BUFFER+2), A + CALL COMMA + CALL EXPRI ; Parse B + EXX + LD A, L + LD (VDU_BUFFER+3), A + VDU 13h ; VDU:COLOUR + VDU (VDU_BUFFER+0) ; Logical Colour + VDU FFh ; Physical Colour (-1 for RGB mode) + VDU (VDU_BUFFER+1) ; R + VDU (VDU_BUFFER+2) ; G + VDU (VDU_BUFFER+3) ; B + JP XEQ \ No newline at end of file diff --git a/src/zds/agon_init.asm b/src/zds/agon_init.asm new file mode 100644 index 0000000..88237e2 --- /dev/null +++ b/src/zds/agon_init.asm @@ -0,0 +1,244 @@ +; +; Title: BBC Basic for AGON - Initialisation Code +; Initialisation Code +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 14/12/2024 +; +; Modinfo: +; 14/12/2024: Fix for *BYE command + + SEGMENT __VECTORS + + XREF START + XREF ACCS + XREF TELL + + .ASSUME ADL = 0 + + INCLUDE "equs.inc" + +argv_ptrs_max: EQU 16 ; Maximum number of arguments allowed in argv + +; +; Start in mixed mode. Assumes MBASE is set to correct segment +; + JP _start ; Jump to start + DS 5 + +RST_08: RST.LIS 08h ; API call + RET + DS 5 + +RST_10: RST.LIS 10h ; Output + RET + DS 5 + +RST_18: RST.LIS 18h ; Block Output + RET + DS 5 + +RST_20: DS 8 +RST_28: DS 8 +RST_30: DS 8 + +; +; The NMI interrupt vector (not currently used by AGON) +; +RST_38: EI + RETI +; +; The header stuff is from byte 64 onwards +; + ALIGN 64 + + DB "MOS" ; Flag for MOS - to confirm this is a valid MOS command + DB 00h ; MOS header version 0 + DB 00h ; Flag for run mode (0: Z80, 1: ADL) + +_exec_name: DB "BBCBASIC.BIN", 0 ; The executable name, only used in argv + +; +; And the code follows on immediately after the header +; +_start: PUSH.LIL IY ; Preserve IY + + LD IY, 0 ; Preserve SPS + ADD IY, SP + PUSH.LIL IY + + EX (SP), HL ; Get the SPS part of the return address + PUSH.LIL HL + EX (SP), HL ; And restore it for BASIC + + PUSH.LIL AF ; Preserve the rest of the registers + PUSH.LIL BC + PUSH.LIL DE + PUSH.LIL IX + + LD A, MB ; Segment base + LD IX, argv_ptrs ; The argv array pointer address + CALL _set_aix24 ; Convert to a 24-bit address + PUSH.LIL IX + CALL _parse_params ; Parse the parameters + POP.LIL IX ; IX: argv + LD B, 0 ; C: argc + CALL _main ; Start user code + + POP.LIL IX ; Restore the registers + POP.LIL DE + POP.LIL BC + POP.LIL AF + + EX DE, HL ; DE: Return code from BASIC + POP.LIL HL ; The SPS part of the return address + POP.LIL IY ; Get the preserved SPS + LD SP, IY ; Restore SPS + EX (SP), HL ; Store the SPS part of the return address on the stack + EX DE, HL ; HL: Return code from BASIC + + POP.LIL IY ; Restore IY + RET.L ; Return to MOS + +; The main routine +; IXU: argv - pointer to array of parameters +; C: argc - number of parameters +; Returns: +; HL: Error code, or 0 if OK +; +_main: LD HL, ACCS ; Clear the ACCS + LD (HL), 0 + LD A, C + CP 2 + JR Z, _autoload ; 2 parameters = autoload + JR C, _startbasic ; 1 parameter = normal start +; CALL STAR_VERSION ; Output the AGON version + CALL TELL + DB "Usage:\n\r" + DB "RUN . \n\r", 0 + LD HL, 0 ; The error code + RET +; +_autoload: LD.LIL HL, (IX+3) ; HLU: Address of filename + LD DE, ACCS ; DE: Destination address +$$: LD.LIL A, (HL) ; Fetch the filename byte + LD (DE), A ; + INC.LIL HL ; Increase the source pointer + INC E ; We only need to increase E as ACCS is on a page boundary + JR NZ, $B ; Loop until we hit a 0 byte + DEC E + LD A, CR + LD (DE), A ; Replace the 0 byte with a CR for BBC BASIC +; +_startbasic: POP HL ; Pop the return address to init off SPS + PUSH.LIL HL ; Stack it on SPL (*BYE will use this as the return address) + JP START ; And start BASIC + +; Parse the parameter string into a C array +; Parameters +; - A: Segment base +; - HLU: Address of parameter string +; - IXU: Address for array pointer storage +; Returns: +; - C: Number of parameters parsed +; +_parse_params: LD BC, _exec_name ; Get the address of the app name in this segment + CALL _set_abc24 ; Convert it to a 24-bit address based upon segment base + LD.LIL (IX+0), BC ; ARGV[0] = the executable name + INC.LIL IX + INC.LIL IX + INC.LIL IX + CALL _skip_spaces ; Skip HL past any leading spaces +; + LD BC, 1 ; C: ARGC = 1 - also clears out top 16 bits of BCU + LD B, argv_ptrs_max - 1 ; B: Maximum number of argv_ptrs +; +_parse_params_1: PUSH BC ; Stack ARGC + PUSH.LIL HL ; Stack start address of token + CALL _get_token ; Get the next token + LD A, C ; A: Length of the token in characters + POP.LIL DE ; Start address of token (was in HL) + POP BC ; ARGC + OR A ; Check for A=0 (no token found) OR at end of string + RET Z +; + LD.LIL (IX+0), DE ; Store the pointer to the token + PUSH.LIL HL ; DE=HL + POP.LIL DE + CALL _skip_spaces ; And skip HL past any spaces onto the next character + XOR A + LD.LIL (DE), A ; Zero-terminate the token + INC.LIL IX + INC.LIL IX + INC.LIL IX ; Advance to next pointer position + INC C ; Increment ARGC + LD A, C ; Check for C >= A + CP B + JR C, _parse_params_1 ; And loop + RET + +; Get the next token +; Parameters: +; - HL: Address of parameter string +; Returns: +; - HL: Address of first character after token +; - C: Length of token (in characters) +; +_get_token: LD C, 0 ; Initialise length +$$: LD.LIL A, (HL) ; Get the character from the parameter string + OR A ; Exit if 0 (end of parameter string in MOS) + RET Z + CP 13 ; Exit if CR (end of parameter string in BBC BASIC) + RET Z + CP ' ' ; Exit if space (end of token) + RET Z + INC.LIL HL ; Advance to next character + INC C ; Increment length + JR $B + +; Skip spaces in the parameter string +; Parameters: +; - HL: Address of parameter string +; Returns: +; - HL: Address of next none-space character +; F: Z if at end of string, otherwise NZ if there are more tokens to be parsed +; +_skip_spaces: LD.LIL A, (HL) ; Get the character from the parameter string + CP ' ' ; Exit if not space + RET NZ + INC.LIL HL ; Advance to next character + JR _skip_spaces ; Increment length + +; Set the MSB of BC (U) to A +; Parameters: +; - BC: 16-bit address +; - A: Value to stick in U of BC +; Returns: +; - BCU +; +_set_abc24: PUSH.LIL HL ; Preserve HL + PUSH.LIL BC ; Stick BC onto SPL + LD.LIL HL, 2 ; HL: SP+2 + ADD.LIL HL, SP + LD.LIL (HL), A ; Store A in it + POP.LIL BC ; Fetch ammended BC + POP.LIL HL ; Restore HL + RET + +; Set the MSB of BC (U) to A +; Parameters: +; - IX: 16-bit address +; - A: Value to stick in U of BC +; Returns: +; - IXU +; +_set_aix24: PUSH.LIL IX ; Stick IX onto SPL + LD.LIL IX, 2 ; IX: SP+2 + ADD.LIL IX, SP + LD.LIL (IX), A ; Store A in it + POP.LIL IX ; Fetch ammended IX + RET + +; Storage for the argv array pointers +; +argv_ptrs: BLKP argv_ptrs_max, 0 ; Storage for the argv array pointers diff --git a/src/zds/agon_interrupt.asm b/src/zds/agon_interrupt.asm new file mode 100644 index 0000000..fdbd37d --- /dev/null +++ b/src/zds/agon_interrupt.asm @@ -0,0 +1,124 @@ +; +; Title: BBC Basic for AGON - Interrupts +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 04/12/2024 +; +; Modinfo: + + .ASSUME ADL = 0 + + INCLUDE "macros.inc" + INCLUDE "equs.inc" + INCLUDE "mos_api.inc" ; In MOS/src + + SEGMENT CODE + + XDEF VBLANK_INIT + XDEF VBLANK_STOP + XDEF VBLANK_HANDLER + + XREF ESCSET + XREF KEYDOWN ; In ram.asm + XREF KEYASCII ; In ram.asm + XREF KEYCOUNT ; In ram.asm + +; Hook into the MOS VBLANK interrupt +; +VBLANK_INIT: DI + + LD A, MB ; Get a 24-bit pointer to + LD HL, VBLANK_HANDLER ; this interrupt handler routine who's + CALL SET_AHL16 ; address is a 16-bit pointer in BBC BASIC's segment + + LD E, 32h ; Set up the VBlank Interrupt Vector + MOSCALL mos_setintvector + + PUSH.LIL HL ; HLU: Pointer to the MOS interrupt vector + POP.LIL DE ; DEU: Pointer to the MOS interrupt vector + + LD HL, VBLANK_HANDLER_JP + 1 ; Pointer to the JP address in this segment + LD A, MB ; Get the segment BBC BASIC is running in + LD (VBLANK_HANDLER_MB + 1), A ; Store in the interrupt handler + CALL SET_AHL16 ; Convert pointer to an absolute 24-bit address + LD.LIL (HL), DE ; Self-modify the code + EI + RET + +; Unhook the custom VBLANK interrupt +; +VBLANK_STOP: DI + LD HL, VBLANK_HANDLER_JP + 1 ; Pointer to the JP address in this segment + LD A, (VBLANK_HANDLER_MB + 1) ; The stored MB of the segment BBC BASIC is running in + PUSH AF ; Stack the MB for later + CALL SET_AHL16 ; Convert pointer to an absolute 24-bit address + LD.LIL DE, (HL) ; DEU: Address of MOS interrupt vector + PUSH.LIL DE ; Transfer to HL + POP.LIL HL + LD E, 32h + MOSCALL mos_setintvector ; Restore the MOS interrupt vector + POP AF ; Restore MB to this segment + LD MB, A + EI + RET + +; Set the MSB of HL (U) to A +; +SET_AHL16: PUSH.LIL HL + LD.LIL HL, 2 + ADD.LIL HL, SP + LD.LIL (HL), A + POP.LIL HL + RET + +; A safe LIS call to ESCSET +; +DO_KEYBOARD: MOSCALL mos_sysvars ; Get the system variables + LD HL, KEYCOUNT ; Check whether the keycount has changed + LD.LIL A, (IX + sysvar_vkeycount) ; by comparing the MOS copy + CP (HL) ; with our local copy + JR NZ, DO_KEYBOARD_1 ; Yes it has, so jump to the next bit +; +DO_KEYBOARD_0: XOR A ; Clear the keyboard values + LD (KEYASCII), A + LD (KEYDOWN), A + RET.LIL ; And return +; +DO_KEYBOARD_1: LD (HL), A ; Store the updated local copy of keycount + LD.LIL A, (IX + sysvar_vkeydown) ; Fetch key down value (1 = key down, 0 = key up) + OR A + JR Z, DO_KEYBOARD_0 ; If it is key up, then clear the keyboard values +; + LD (KEYDOWN), A ; Store the keydown value + LD.LIL A, (IX + sysvar_keyascii) ; Fetch key ASCII value + LD (KEYASCII), A ; Store locally + CP 1Bh ; Is it escape? + CALL Z, ESCSET ; Yes, so set the escape flags + RET.LIS ; Return to the interrupt handler + +; +; Interrupts in mixed mode always run in ADL mode +; + .ASSUME ADL = 1 + +VBLANK_HANDLER: DI + PUSH AF + PUSH HL + PUSH IX + LD A, MB + PUSH AF +VBLANK_HANDLER_MB: LD A, 0 ; This is self-modified by VBLANK_INIT + LD MB, A + CALL.LIS DO_KEYBOARD + POP AF + LD MB, A + POP IX + POP HL + POP AF +; +; Finally jump to the MOS interrupt +; +VBLANK_HANDLER_JP: JP 0 ; This is self-modified by VBLANK_INIT + + .ASSUME ADL = 0 + \ No newline at end of file diff --git a/src/zds/agon_misc.asm b/src/zds/agon_misc.asm new file mode 100644 index 0000000..ae48635 --- /dev/null +++ b/src/zds/agon_misc.asm @@ -0,0 +1,229 @@ +; +; Title: BBC Basic for AGON - Miscellaneous helper functions +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 04/12/2024 +; +; Modinfo: + + INCLUDE "equs.inc" + INCLUDE "macros.inc" + + .ASSUME ADL = 0 + + SEGMENT CODE + + XDEF ASC_TO_NUMBER + XDEF SWITCH_A + XDEF NULLTOCR + XDEF CRTONULL + XDEF CSTR_FNAME + XDEF CSTR_LINE + XDEF CSTR_FINDCH + XDEF CSTR_ENDSWITH + XDEF CSTR_CAT + + XREF OSWRCH + XREF KEYWDS + XREF KEYWDL + +; Read a number and convert to binary +; If prefixed with &, will read as hex, otherwise decimal +; Inputs: HL: Pointer in string buffer +; Outputs: HL: Updated text pointer +; DE: Value +; A: Terminator (spaces skipped) +; Destroys: A,D,E,H,L,F +; +ASC_TO_NUMBER: PUSH BC ; Preserve BC + LD DE, 0 ; Initialise DE + CALL SKIPSP ; Skip whitespace + LD A, (HL) ; Read first character + CP '&' ; Is it prefixed with '&' (HEX number)? + JR NZ, ASC_TO_NUMBER3 ; Jump to decimal parser if not + INC HL ; Otherwise fall through to ASC_TO_HEX +; +ASC_TO_NUMBER1: LD A, (HL) ; Fetch the character + CALL UPPRC ; Convert to uppercase + SUB '0' ; Normalise to 0 + JR C, ASC_TO_NUMBER4 ; Return if < ASCII '0' + CP 10 ; Check if >= 10 + JR C,ASC_TO_NUMBER2 ; No, so skip next bit + SUB 7 ; Adjust ASCII A-F to nibble + CP 16 ; Check for > F + JR NC, ASC_TO_NUMBER4 ; Return if out of range +ASC_TO_NUMBER2: EX DE, HL ; Shift DE left 4 times + ADD HL, HL + ADD HL, HL + ADD HL, HL + ADD HL, HL + EX DE, HL + OR E ; OR the new digit in to the least significant nibble + LD E, A + INC HL ; Onto the next character + JR ASC_TO_NUMBER1 ; And loop +; +ASC_TO_NUMBER3: LD A, (HL) + SUB '0' ; Normalise to 0 + JR C, ASC_TO_NUMBER4 ; Return if < ASCII '0' + CP 10 ; Check if >= 10 + JR NC, ASC_TO_NUMBER4 ; Return if >= 10 + EX DE, HL ; Stick DE in HL + LD B, H ; And copy HL into BC + LD C, L + ADD HL, HL ; x 2 + ADD HL, HL ; x 4 + ADD HL, BC ; x 5 + ADD HL, HL ; x 10 + EX DE, HL + ADD8U_DE ; Add A to DE (macro) + INC HL + JR ASC_TO_NUMBER3 +ASC_TO_NUMBER4: POP BC ; Fall through to SKIPSP here + +; Skip a space +; HL: Pointer in string buffer +; +SKIPSP: LD A, (HL) + CP ' ' + RET NZ + INC HL + JR SKIPSP + +; Skip a string +; HL: Pointer in string buffer +; +SKIPNOTSP: LD A, (HL) + CP ' ' + RET Z + INC HL + JR SKIPNOTSP + +; Convert a character to upper case +; A: Character to convert +; +UPPRC: AND 7FH + CP '`' + RET C + AND 5FH ; Convert to upper case + RET + +; Switch on A - lookup table immediately after call +; A: Index into lookup table +; +SWITCH_A: EX (SP), HL ; Swap HL with the contents of the top of the stack + ADD A, A ; Multiply A by two + ADD8U_HL ; Add to HL (macro) + LD A, (HL) ; follow the call. Fetch an address from the + INC HL ; table. + LD H, (HL) + LD L, A + EX (SP), HL ; Swap this new address back, restores HL + RET ; Return program control to this new address + +; Convert the buffer to a null terminated string and back +; HL: Buffer address +; +NULLTOCR: PUSH BC + LD B, 0 + LD C, CR + JR CRTONULL0 +; +CRTONULL: PUSH BC + LD B, CR + LD C, 0 +; +CRTONULL0: PUSH HL +CRTONULL1: LD A, (HL) + CP B + JR Z, CRTONULL2 + INC HL + JR CRTONULL1 +CRTONULL2: LD (HL), C + POP HL + POP BC + RET + +; Copy a filename to DE and zero terminate it +; HL: Source +; DE: Destination (ACCS) +; +CSTR_FNAME: LD A, (HL) ; Get source + CP 32 ; Is it space + JR Z, $F + CP CR ; Or is it CR + JR Z, $F + LD (DE), A ; No, so store + INC HL ; Increment + INC DE + JR CSTR_FNAME ; And loop +$$: XOR A ; Zero terminate the target string + LD (DE), A + INC DE ; And point to next free address + RET + +; Copy a CR terminated line to DE and zero terminate it +; HL: Source +; DE: Destination (ACCS) +; +CSTR_LINE: LD A, (HL) ; Get source + CP CR ; Is it CR + JR Z, $F + LD (DE), A ; No, so store + INC HL ; Increment + INC DE + JR CSTR_LINE ; And loop +$$: XOR A ; Zero terminate the target string + LD (DE), A + INC DE ; And point to next free address + RET + +; Find the first occurrence of a character (case sensitive) +; HL: Source +; C: Character to find +; Returns: +; HL: Pointer to character, or end of string marker +; +CSTR_FINDCH: LD A, (HL) ; Get source + CP C ; Is it our character? + RET Z ; Yes, so exit + OR A ; Is it the end of string? + RET Z ; Yes, so exit + INC HL + JR CSTR_FINDCH + +; Check whether a string ends with another string (case insensitive) +; HL: Source +; DE: The substring we want to test with +; Returns: +; F: Z if HL ends with DE, otherwise NZ +; +CSTR_ENDSWITH: LD A, (HL) ; Get the source string byte + CALL UPPRC ; Convert to upper case + LD C, A + LD A, (DE) ; Get the substring byte + CP C + RET NZ ; Return NZ if at any point the strings don't match + OR C ; Check whether both bytes are zero + RET Z ; If so, return, as we have reached the end of both strings + INC HL + INC DE + JR CSTR_ENDSWITH ; And loop + +; Concatenate a string onto the end of another string +; HL: Source +; DE: Second string +; +CSTR_CAT: LD A, (HL) ; Loop until we find the end of the first string + OR A + JR Z, CSTR_CAT_1 + INC HL + JR CSTR_CAT +; +CSTR_CAT_1: LD A, (DE) ; Copy the second string onto the end of the first string + LD (HL), A + OR A ; Check for end of string + RET Z ; And return + INC HL + INC DE + JR CSTR_CAT_1 ; Loop until finished \ No newline at end of file diff --git a/src/zds/agon_os.asm b/src/zds/agon_os.asm new file mode 100644 index 0000000..11cc877 --- /dev/null +++ b/src/zds/agon_os.asm @@ -0,0 +1,1084 @@ +; +; Title: BBC Basic for AGON - MOS stuff +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 17/12/2024 +; +; Modinfo: +; 08/12/2024: Added OSCLI and file I/O +; 11/12/2024: Added ESC key handling +; Added OSWORD +; 12/12/2024: Added OSRDCH, OSBYTE_81 and fixed *EDIT +; 17/12/2024: Added OSWORD_01, OSWORD_02, OSWORD_0E, GET$(x,y), fixed INKEY, POS, VPOS and autoload + + .ASSUME ADL = 0 + + INCLUDE "equs.inc" + INCLUDE "macros.inc" + INCLUDE "mos_api.inc" ; In MOS/src + + SEGMENT CODE + + XDEF OSWORD + XDEF OSBYTE + XDEF OSINIT + XDEF OSOPEN + XDEF OSSHUT + XDEF OSLOAD + XDEF OSSAVE + XDEF OSLINE + XDEF OSSTAT + XDEF OSWRCH + XDEF OSRDCH + XDEF OSBGET + XDEF OSBPUT + XDEF OSCLI + XDEF PROMPT + XDEF GETPTR + XDEF PUTPTR + XDEF GETEXT + XDEF TRAP + XDEF LTRAP + XDEF BYE + XDEF RESET + XDEF ESCSET + + XREF EXTERR + XREF VBLANK_INIT + XREF VBLANK_STOP + XREF USER + XREF COUNT + XREF COUNT0 + XREF COUNT1 + XREF GETCSR + XREF GETSCHR_1 + XREF NULLTOCR + XREF CRLF + XREF FLAGS + XREF OSWRCHPT + XREF OSWRCHCH + XREF OSWRCHFH + XREF KEYASCII + XREF KEYDOWN + XREF LISTON + XREF PAGE_ + XREF CSTR_FNAME + XREF CSTR_FINDCH + XREF CSTR_CAT + XREF CSTR_ENDSWITH + XREF CSTR_LINE + XREF NEWIT + XREF BAD + XREF CLEAN + XREF LINNUM + XREF BUFFER + XREF NXT + XREF ERROR_ + XREF XEQ + XREF LEXAN2 + XREF GETTOP + XREF FINDL + XREF DEL + XREF LISTIT + XREF ESCAPE + XREF ASC_TO_NUMBER + XREF CLOOP + XREF SCRAP + XREF POINT_ + XREF SOUND_ + XREF EXPRI + XREF COMMA + XREF BRAKET + XREF GETSCHR + XREF ZERO + XREF TRUE + +;OSINIT - Initialise RAM mapping etc. +;If BASIC is entered by BBCBASIC FILENAME then file +;FILENAME.BBC is automatically CHAINed. +; Outputs: DE = initial value of HIMEM (top of RAM) +; HL = initial value of PAGE (user program) +; Z-flag reset indicates AUTO-RUN. +; Destroys: A,D,E,H,L,F +; +OSINIT: CALL VBLANK_INIT + XOR A + LD (FLAGS), A ; Clear flags and set F = Z + LD HL, USER + LD DE, RAM_Top + LD E, A ; Page boundary + LD A, (ACCS) ; Return NZ if there is a file to chain + OR A + RET + +; PROMPT: output the input prompt +; +PROMPT: LD A,'>' ; Falls through to OSWRCH + +; OSWRCH: Write a character out to the ESP32 VDU handler via the MOS +; Parameters: +; - A: Character to write +; +OSWRCH: PUSH HL + LD HL, LISTON ; Fetch the LISTON variable + BIT 3, (HL) ; Check whether we are in *EDIT mode + JR NZ, OSWRCH_BUFFER ; Yes, so just output to buffer +; + LD HL, (OSWRCHCH) ; L: Channel # + DEC L ; If it is 1 + JR Z, OSWRCH_FILE ; Then we are outputting to a file +; + POP HL ; Otherwise + RST.LIS 10h ; Output the character to MOS + RET +; +OSWRCH_BUFFER: LD HL, (OSWRCHPT) ; Fetch the pointer buffer + CP 0AH ; Just ignore this + JR Z, OSWRCH_BUFFER2 + CP 0DH ; Is it the end of line? + JR NZ, OSWRCH_BUFFER1 ; No, so carry on + XOR A ; Turn it into a NUL character +OSWRCH_BUFFER1: LD (HL), A ; Echo the character into the buffer + INC HL ; Increment pointer + LD (OSWRCHPT), HL ; Write pointer back +OSWRCH_BUFFER2: POP HL + RET +; +OSWRCH_FILE: PUSH DE + LD E, H ; Filehandle to E + CALL OSBPUT ; Write the byte out + POP DE + POP HL + RET + +; OSRDCH +; +OSRDCH: CALL NXT ; Check if we are doing GET$(x,y) + CP '(' + JR Z, $F ; Yes, so skip to that functionality + MOSCALL mos_getkey ; Otherwise, read keyboard + CP 1Bh + JR Z, LTRAP1 + RET +; +$$: INC IY ; Skip '(' + CALL EXPRI ; Get the first parameter + EXX + PUSH HL + CALL COMMA ; Get the second parameter + CALL EXPRI + EXX + POP DE ; DE: X coordinate + CALL BRAKET ; Check for trailing bracket + JP GETSCHR ; Read the character + +; OSLINE: Invoke the line editor +; +OSLINE: LD E, 1 ; Default is to clear the buffer + +; Entry point to line editor that does not clear the buffer +; Parameters: +; - HL: addresses destination buffer (on page boundary) +; Returns: +; - A: 0 +; NB: Buffer filled, terminated by CR +; +OSLINE1: PUSH IY + PUSH HL ; Buffer address + LD BC, 256 ; Buffer length + MOSCALL mos_editline ; Call the MOS line editor + POP HL ; Pop the address + POP IY + PUSH AF ; Stack the return value (key pressed) + CALL NULLTOCR ; Turn the 0 character to a CR + CALL CRLF ; Display CRLF + POP AF + CP 1Bh ; Check if ESC terminated the input + JP Z, LTRAP1 ; Yes, so do the ESC thing + LD A, (FLAGS) ; Otherwise + RES 7, A ; Clear the escape flag + LD (FLAGS), A + CALL WAIT_VBLANK ; Wait a frame + XOR A ; Return A = 0 + LD (KEYDOWN), A + LD (KEYASCII), A + RET + +; +; ESCSET +; Set the escape flag (bit 7 of FLAGS = 1) if escape is enabled (bit 6 of FLAGS = 0) +; +ESCSET: PUSH HL + LD HL,FLAGS ; Pointer to FLAGS + BIT 6,(HL) ; If bit 6 is set, then + JR NZ,ESCDIS ; escape is disabled, so skip + SET 7,(HL) ; Set bit 7, the escape flag +ESCDIS: POP HL + RET + +; +; ESCTEST +; Test for ESC key +; +ESCTEST: CALL READKEY ; Read the keyboard + RET NZ ; Skip if no key is pressed + CP 1BH ; If ESC pressed then + JR Z,ESCSET ; jump to the escape set routine + RET + +; Read the keyboard +; Returns: +; - A: ASCII of the pressed key +; - F: Z if the key is pressed, otherwise NZ +; +READKEY: LD A, (KEYDOWN) ; Get key down + DEC A ; Set Z flag if keydown is 1 + LD A, (KEYASCII) ; Get key ASCII value + RET +; +; TRAP +; This is called whenever BASIC needs to check for ESC +; +TRAP: CALL ESCTEST ; Read keyboard, test for ESC, set FLAGS +; +LTRAP: LD A,(FLAGS) ; Get FLAGS + OR A ; This checks for bit 7; if it is not set then the result will + RET P ; be positive (bit 7 is the sign bit in Z80), so return +LTRAP1: LD HL,FLAGS ; Escape is pressed at this point, so + RES 7,(HL) ; Clear the escape pressed flag and + JP ESCAPE ; Jump to the ESCAPE error routine in exec.asm + +; RESET +; +RESET: RET ; Yes this is fine + +; OSOPEN +; HL: Pointer to path +; F: C Z +; x x OPENIN +; OPENOUT +; x OPENUP +; Returns: +; A: Filehandle, 0 if cannot open +; +OSOPEN: LD C, fa_read + JR Z, $F + LD C, fa_write | fa_open_append + JR C, $F + LD C, fa_write | fa_create_always +$$: MOSCALL mos_fopen + RET + +;OSSHUT - Close disk file(s). +; E = file channel +; If E=0 all files are closed (except SPOOL) +; Destroys: A,B,C,D,E,H,L,F +; +OSSHUT: PUSH BC + LD C, E + MOSCALL mos_fclose + POP BC + RET + +; OSBGET - Read a byte from a random disk file. +; E = file channel +; Returns +; A = byte read +; Carry set if LAST BYTE of file +; Destroys: A,B,C,F +; +OSBGET: PUSH BC + LD C, E + MOSCALL mos_fgetc + POP BC + RET + +; OSBPUT - Write a byte to a random disk file. +; E = file channel +; A = byte to write +; Destroys: A,B,C,F +; +OSBPUT: PUSH BC + LD C, E + LD B, A + MOSCALL mos_fputc + POP BC + RET + +; OSSTAT - Read file status +; E = file channel +; Returns +; F: Z flag set - EOF +; A: If Z then A = 0 +; Destroys: A,D,E,H,L,F +; +OSSTAT: PUSH BC + LD C, E + MOSCALL mos_feof + POP BC + CP 1 + RET + +; GETPTR - Return file pointer. +; E = file channel +; Returns: +; DEHL = pointer (0-&7FFFFF) +; Destroys: A,B,C,D,E,H,L,F +; +GETPTR: PUSH IY + LD C, E + MOSCALL mos_getfil ; HLU: Pointer to FIL structure + PUSH.LIL HL + POP.LIL IY ; IYU: Pointer to FIL structure + LD.LIL L, (IY + FIL.fptr + 0) + LD.LIL H, (IY + FIL.fptr + 1) + LD.LIL E, (IY + FIL.fptr + 2) + LD.LIL D, (IY + FIL.fptr + 3) + POP IY + RET + +; PUTPTR - Update file pointer. +; A = file channel +; DEHL = new pointer (0-&7FFFFF) +; Destroys: A,B,C,D,E,H,L,F +; +PUTPTR: PUSH IY + LD C, A ; C: Filehandle + PUSH.LIL HL + LD.LIL HL, 2 + ADD.LIL HL, SP + LD.LIL (HL), E ; 3rd byte of DWORD set to E + POP.LIL HL + LD E, D ; 4th byte passed as E + MOSCALL mos_flseek + POP IY + RET + +; GETEXT - Find file size. +; E = file channel +; Returns: +; DEHL = file size (0-&800000) +; Destroys: A,B,C,D,E,H,L,F +; +GETEXT: PUSH IY + LD C, E + MOSCALL mos_getfil ; HLU: Pointer to FIL structure + PUSH.LIL HL + POP.LIL IY ; IYU: Pointer to FIL structure + LD.LIL L, (IY + FIL.obj.objsize + 0) + LD.LIL H, (IY + FIL.obj.objsize + 1) + LD.LIL E, (IY + FIL.obj.objsize + 2) + LD.LIL D, (IY + FIL.obj.objsize + 3) + POP IY + RET + +;OSLOAD - Load an area of memory from a file. +; Inputs: HL addresses filename (CR terminated) +; DE = address at which to load +; BC = maximum allowed size (bytes) +; Outputs: Carry reset indicates no room for file. +; Destroys: A,B,C,D,E,H,L,F +; +OSLOAD: PUSH BC ; Stack the size + PUSH DE ; Stack the load address + LD DE, ACCS ; Buffer address for filename + CALL CSTR_FNAME ; Fetch filename from MOS into buffer + LD HL, ACCS ; HL: Filename + CALL EXT_DEFAULT ; Tack on the extension .BBC if not specified + CALL EXT_HANDLER ; Get the default handler + POP DE ; Restore the load address + POP BC ; Restore the size + OR A + JP Z, OSLOAD_BBC +; +; Load the file in as a text file +; +OSLOAD_TXT: XOR A ; Set file attributes to read + CALL OSOPEN ; Open the file + LD E, A ; The filehandle + OR A + LD A, 4 ; File not found error + JP Z, OSERROR ; Jump to error handler + CALL NEWIT ; Call NEW to clear the program space +; +OSLOAD_TXT1: LD HL, ACCS ; Where the input is going to be stored +; +; First skip any whitespace (indents) at the beginning of the input +; +$$: CALL OSBGET ; Read the byte into A + JR C, OSLOAD_TXT3 ; Is it EOF? + CP LF ; Is it LF? + JR Z, OSLOAD_TXT3 ; Yes, so skip to the next line + CP 21h ; Is it less than or equal to ASCII space? + JR C, $B ; Yes, so keep looping + LD (HL), A ; Store the first character + INC L +; +; Now read the rest of the line in +; +OSLOAD_TXT2: CALL OSBGET ; Read the byte into A + JR C, OSLOAD_TXT4 ; Is it EOF? + CP 20h ; Skip if not an ASCII character + JR C, $F + LD (HL), A ; Store in the input buffer + INC L ; Increment the buffer pointer + JP Z, BAD ; If the buffer is full (wrapped to 0) then jump to Bad Program error +$$: CP LF ; Check for LF + JR NZ, OSLOAD_TXT2 ; If not, then loop to read the rest of the characters in +; +; Finally, handle EOL/EOF +; +OSLOAD_TXT3: LD (HL), CR ; Store a CR for BBC BASIC + LD A, L ; Check for minimum line length + CP 2 ; If it is 2 characters or less (including CR) + JR C, $F ; Then don't bother entering it + PUSH DE ; Preserve the filehandle + CALL OSEDIT ; Enter the line in memory + CALL C,CLEAN ; If a new line has been entered, then call CLEAN to set TOP and write &FFFF end of program marker + POP DE +$$: CALL OSSTAT ; End of file? + JR NZ, OSLOAD_TXT1 ; No, so loop + CALL OSSHUT ; Close the file + SCF ; Flag to BASIC that we're good + RET +; +; Special case for BASIC programs with no blank line at the end +; +OSLOAD_TXT4: CP 20h ; Skip if not an ASCII character + JR C, $F + LD (HL), A ; Store the character + INC L + JP Z, BAD +$$: JR OSLOAD_TXT3 +; +; This bit enters the line into memory +; Also called from OSLOAD_TXT +; Returns: +; F: C if a new line has been entered (CLEAN will need to be called) +; +OSEDIT: XOR A ; Entry point after *EDIT + LD (COUNT),A + LD IY,ACCS + CALL LINNUM ; HL: The line number from the input buffer + CALL NXT ; Skip spaces + LD A,H ; HL: The line number will be 0 for immediate mode or when auto line numbering is used + OR L + JR Z,LNZERO ; Skip if there is no line number in the input buffer +; +; This bit does the lexical analysis and tokenisation +; +LNZERO: LD DE,BUFFER + LD C,1 ; LEFT MODE + PUSH HL + CALL LEXAN2 ; LEXICAL ANALYSIS + POP HL + LD (DE),A ; TERMINATOR + XOR A + LD B,A + LD C,E ; BC=LINE LENGTH + INC DE + LD (DE),A ; ZERO NEXT + LD A,H + OR L + LD IY,BUFFER ; FOR XEQ + JP Z,XEQ ; DIRECT MODE + PUSH BC + CALL FINDL + CALL Z,DEL + POP BC + LD A,C + OR A + RET Z + ADD A,4 + LD C,A ; LENGTH INCLUSIVE + PUSH DE ; LINE NUMBER + PUSH BC ; SAVE LINE LENGTH + EX DE,HL + PUSH BC + CALL GETTOP + POP BC + PUSH HL + ADD HL,BC + PUSH HL + INC H + XOR A + SBC HL,SP + POP HL + JP NC,ERROR_ ; "No room" + EX (SP),HL + PUSH HL + INC HL + OR A + SBC HL,DE + LD B,H ; BC=AMOUNT TO MOVE + LD C,L + POP HL + POP DE + JR Z,ATEND + LDDR ; MAKE SPACE +ATEND: POP BC ; LINE LENGTH + POP DE ; LINE NUMBER + INC HL + LD (HL),C ; STORE LENGTH + INC HL + LD (HL),E ; STORE LINE NUMBER + INC HL + LD (HL),D + INC HL + LD DE,BUFFER + EX DE,HL + DEC C + DEC C + DEC C + LDIR ; ADD LINE + SCF + RET +; +; Load the file in as a tokenised binary blob +; +OSLOAD_BBC: MOSCALL mos_load ; Call LOAD in MOS + RET NC ; If load returns with carry reset - NO ROOM + OR A ; If there is no error (A=0) + SCF ; Need to set carry indicating there was room + RET Z ; Return +; +OSERROR: PUSH AF ; Handle the MOS error + LD HL, ACCS ; Address of the buffer + LD BC, 256 ; Length of the buffer + LD E, A ; The error code + MOSCALL mos_getError ; Copy the error message into the buffer + POP AF + PUSH HL ; Stack the address of the error (now in ACCS) + ADD A, 127 ; Add 127 to the error code (MOS errors start at 128, and are trappable) + JP EXTERR ; Trigger an external error + +;OSSAVE - Save an area of memory to a file. +; Inputs: HL addresses filename (term CR) +; DE = start address of data to save +; BC = length of data to save (bytes) +; Destroys: A,B,C,D,E,H,L,F +; +OSSAVE: PUSH BC ; Stack the size + PUSH DE ; Stack the save address + LD DE, ACCS ; Buffer address for filename + CALL CSTR_FNAME ; Fetch filename from MOS into buffer + LD HL, ACCS ; HL: Filename + CALL EXT_DEFAULT ; Tack on the extension .BBC if not specified + CALL EXT_HANDLER ; Get the default handler + POP DE ; Restore the save address + POP BC ; Restore the size + OR A ; Is the extension .BBC + JR Z, OSSAVE_BBC ; Yes, so use that +; +; Save the file out as a text file +; +OSSAVE_TXT: LD A, (OSWRCHCH) ; Stack the current channel + PUSH AF + XOR A + INC A ; Make sure C is clear, A is 1, for OPENOUT + LD (OSWRCHCH), A + CALL OSOPEN ; Open the file + LD (OSWRCHFH), A ; Store the file handle for OSWRCH + LD IX, LISTON ; Required for LISTIT + LD HL, (PAGE_) ; Get start of program area + EXX + LD BC, 0 ; Set the initial indent counters + EXX +OSSAVE_TXT1: LD A, (HL) ; Check for end of program marker + OR A + JR Z, OSSAVE_TXT2 + INC HL ; Skip the length byte + LD E, (HL) ; Get the line number + INC HL + LD D, (HL) + INC HL + CALL LISTIT ; List the line + JR OSSAVE_TXT1 +OSSAVE_TXT2: LD A, (OSWRCHFH) ; Get the file handle + LD E, A + CALL OSSHUT ; Close it + POP AF ; Restore the channel + LD (OSWRCHCH), A + RET +; +; Save the file out as a tokenised binary blob +; +OSSAVE_BBC: MOSCALL mos_save ; Call SAVE in MOS + OR A ; If there is no error (A=0) + RET Z ; Just return + JR OSERROR ; Trip an error + +; Check if an extension is specified in the filename +; Add a default if not specified +; HL: Filename (CSTR format) +; +EXT_DEFAULT: PUSH HL ; Stack the filename pointer + LD C, '.' ; Search for dot (marks start of extension) + CALL CSTR_FINDCH + OR A ; Check for end of string marker + JR NZ, $F ; No, so skip as we have an extension at this point + LD DE, EXT_LOOKUP ; Get the first (default extension) + CALL CSTR_CAT ; Concat it to string pointed to by HL +$$: POP HL ; Restore the filename pointer + RET + +; Check if an extension is valid and, if so, provide a pointer to a handler +; HL: Filename (CSTR format) +; Returns: +; A: Filename extension type (0=BBC tokenised, 1=ASCII untokenised) +; +EXT_HANDLER: PUSH HL ; Stack the filename pointer + LD C, '.' ; Find the '.' + CALL CSTR_FINDCH + LD DE, EXT_LOOKUP ; The lookup table +; +EXT_HANDLER_1: PUSH HL ; Stack the pointer to the extension + CALL CSTR_ENDSWITH ; Check whether the string ends with the entry in the lookup + POP HL ; Restore the pointer to the extension + JR Z, EXT_HANDLER_2 ; We have a match! +; +$$: LD A, (DE) ; Skip to the end of the entry in the lookup + INC DE + OR A + JR NZ, $B + INC DE ; Skip the file extension # byte +; + LD A, (DE) ; Are we at the end of the table? + OR A + JR NZ, EXT_HANDLER_1 ; No, so loop +; + LD A,204 ; Throw a "Bad name" error + CALL EXTERR + DB "Bad name", 0 +; +EXT_HANDLER_2: INC DE ; Skip to the file extension # byte + LD A, (DE) + POP HL ; Restore the filename pointer + RET + +; Extension lookup table +; CSTR, TYPE +; - 0: BBC (tokenised BBC BASIC for Z80 format) +; - 1: Human readable plain text +; +EXT_LOOKUP: DB '.BBC', 0, 0 ; First entry is the default extension + DB '.TXT', 0, 1 + DB '.ASC', 0, 1 + DB '.BAS', 0, 1 + DB 0 ; End of table +; OSWORD +; +OSWORD: CP 01H ; GETIME + JR Z, OSWORD_01 + CP 02H ; PUTIME + JR Z, OSWORD_02 + CP 0EH ; GETIMS + JR Z, OSWORD_0E + CP 0FH ; PUTIMS + JR Z, $F + CP 07H ; SOUND + JR Z, OSWORD_07 + CP 08H ; ENVELOPE + JR Z, $F + CP 09H ; POINT + JR Z, OSWORD_09 + JP HUH ; Anything else trips an error +$$: RET ; Dummy return for unimplemented functions + +; GETIME: return current time in centiseconds +; +OSWORD_01: PUSH IX + MOSCALL mos_sysvars + LD B, 4 +$$: LD.LIL A, (IX + sysvar_time) + LD (HL), A + INC HL + INC.LIL IX + DJNZ $B + POP IX + RET + +; PUTIME: set time in centiseconds +; +OSWORD_02: PUSH IX + MOSCALL mos_sysvars + LD B, 4 +$$: LD A, (HL) + LD.LIL (IX + sysvar_time), A + INC HL + INC.LIL IX + DJNZ $B + POP IX + RET + +; SOUND channel,volume,pitch,duration +; Parameters: +; - HL: Pointer to data +; - 0,1: Channel +; - 2,3: Volume 0 (off) to 15 (full volume) +; - 4,5: Pitch 0 - 255 +; - 6,7: Duration -1 to 254 (duration in 20ths of a second, -1 = play forever) +; +OSWORD_07: EQU SOUND_ + +; OSWORD 0x09: POINT +; Parameters: +; - HL: Address of data +; - 0,1: X coordinate +; - 2,3: Y coordinate +; +OSWORD_09: LD DE,(SCRAP+0) + LD HL,(SCRAP+2) + CALL POINT_ + LD (SCRAP+4),A + RET + +; GETIMS - Get time from RTC +; +OSWORD_0E: PUSH IY + MOSCALL mos_getrtc + POP IY + RET + +; +; OSBYTE +; Parameters: +; - A: FX # +; - L: First parameter +; - H: Second parameter +; +OSBYTE: CP 0BH ; Keyboard auto-repeat delay + JR Z, OSBYTE_0B + CP 0CH ; Keyboard auto-repeat rate + JR Z, OSBYTE_0C + CP 13H ; Wait for vblank + JR Z, OSBYTE_13 + CP 76H ; Set keyboard LED + JR Z, OSBYTE_76 + CP 81H ; Read the keyboard + JP Z, OSBYTE_81 + CP 86H ; Get cursor coordinates + JP Z, OSBYTE_86 + CP 87H ; Fetch current mode and character under cursor + JP Z, OSBYTE_87 + CP A0H ; Fetch system variable + JP Z, OSBYTE_A0 +; +; Anything else trips an error +; +HUH: LD A,254 ; Bad command error + CALL EXTERR + DB "Bad command" + DEFB 0 + +; OSBYTE 0x0B (FX 11,n): Keyboard auto-repeat delay +; Parameters: +; - HL: Repeat delay +; +OSBYTE_0B: VDU 23 + VDU 0 + VDU vdp_keystate + VDU L + VDU H + VDU 0 + VDU 0 + VDU 255 + RET + +; OSBYTE 0x0C (FX 12,n): Keyboard auto-repeat rate +; Parameters: +; - HL: Repeat rate +; +OSBYTE_0C: VDU 23 + VDU 0 + VDU vdp_keystate + VDU 0 + VDU 0 + VDU L + VDU H + VDU 255 + RET + +; OSBYTE 0x13 (FX 19): Wait for vertical blank interrupt +; +OSBYTE_13: CALL WAIT_VBLANK + LD L, 0 ; Returns 0 + JP COUNT0 +; +; OSBYTE 0x76 (FX 118,n): Set Keyboard LED +; Parameters: +; - L: LED (Bit 0: Scroll Lock, Bit 1: Caps Lock, Bit 2: Num Lock) +; +OSBYTE_76: VDU 23 + VDU 0 + VDU vdp_keystate + VDU 0 + VDU 0 + VDU 0 + VDU 0 + VDU L + RET + +; OSBYTE 0x81: Read the keyboard +; Parameters: +; - HL = Time to wait (centiseconds) +; Returns: +; - F: Carry reset indicates time-out +; - H: NZ if timed out +; - L: The character typed +; Destroys: A,D,E,H,L,F +; +OSBYTE_81: EXX + BIT 7, H ; Check for minus numbers + EXX + JR NZ, OSBYTE_81_1 ; Yes, so do INKEY(-n) + CALL READKEY ; Read the keyboard + JR Z, $F ; Skip if we have a key + CALL WAIT_VBLANK ; Wait a frame + LD A, H ; Check loop counter + OR L + DEC HL ; Decrement + JR NZ, OSBYTE_81 ; And loop + RET ; H: Will be set to 255 to flag timeout +; +$$: LD HL, KEYDOWN ; We have a key, so + LD (HL), 0 ; clear the keydown flag + CP 1BH ; If we are pressing ESC, + JP Z, ESCSET ; Then handle ESC + LD H, 0 ; H: Not timed out + LD L, A ; L: The character + RET +; +; +; Check immediately whether a given key is being pressed +; Result is integer numeric +; +OSBYTE_81_1: MOSCALL mos_getkbmap ; Get the base address of the keyboard + INC HL ; Index from 0 + LD A, L ; Negate the LSB of the answer + NEG + LD C, A ; E: The positive keycode value + LD A, 1 ; Throw an "Out of range" error + JP M, ERROR_ ; if the argument < - 128 +; + LD HL, BITLOOKUP ; HL: The bit lookup table + LD DE, 0 + LD A, C + AND 00000111b ; Just need the first three bits + LD E, A ; DE: The bit number + ADD HL, DE + LD B, (HL) ; B: The mask +; + LD A, C ; Fetch the keycode again + AND 01111000b ; And divide by 8 + RRCA + RRCA + RRCA + LD E, A ; DE: The offset (the MSW has already been cleared previously) + ADD.LIL IX, DE ; IX: The address + LD.LIL A, (IX+0) ; A: The keypress + AND B ; Check whether the bit is set + JP Z, ZERO ; No, so return 0 + JP TRUE ; Otherwise return -1 +; +; A bit lookup table +; +BITLOOKUP: DB 01h, 02h, 04h, 08h + DB 10h, 20h, 40h, 80h + +; OSBYTE 0x86: Fetch cursor coordinates +; Returns: +; - L: X Coordinate (POS) +; - H: Y Coordinate (VPOS) +; +OSBYTE_86: PUSH IX ; Get the system vars in IX + MOSCALL mos_sysvars ; Reset the semaphore + RES.LIL 0, (IX+sysvar_vpd_pflags) + VDU 23 + VDU 0 + VDU vdp_cursor +$$: BIT.LIL 0, (IX+sysvar_vpd_pflags) + JR Z, $B ; Wait for the result + LD.LIL L, (IX + sysvar_cursorX) + LD.LIL H, (IX + sysvar_cursorY) + POP IX + RET + +; OSBYTE 0x87: Fetch current mode and character under cursor +; +OSBYTE_87: PUSH IX + CALL GETCSR ; Get the current screen position + CALL GETSCHR ; Read character from screen + LD L, A + MOSCALL mos_sysvars + LD.LIL H, (IX+sysvar_scrMode) ; H: Screen mode + POP IX + JP COUNT1 + +; OSBYTE 0xA0: Fetch system variable +; Parameters: +; - L: The system variable to fetch +; +OSBYTE_A0: PUSH IX + MOSCALL mos_sysvars ; Fetch pointer to system variables + LD.LIL BC, 0 + LD C, L ; BCU = L + ADD.LIL IX, BC ; Add to IX + LD.LIL L, (IX + 0) ; Fetch the return value + POP IX + JP COUNT0 + +; OSCLI +; +; +;OSCLI - Process a MOS command +; +OSCLI: CALL SKIPSP + CP CR + RET Z + CP '|' + RET Z + EX DE,HL + LD HL,COMDS +OSCLI0: LD A,(DE) + CALL UPPRC + CP (HL) + JR Z,OSCLI2 + JR C,OSCLI6 +OSCLI1: BIT 7,(HL) + INC HL + JR Z,OSCLI1 + INC HL + INC HL + JR OSCLI0 +; +OSCLI2: PUSH DE +OSCLI3: INC DE + INC HL + LD A,(DE) + CALL UPPRC + CP '.' ; ABBREVIATED? + JR Z,OSCLI4 + XOR (HL) + JR Z,OSCLI3 + CP 80H + JR Z,OSCLI4 + POP DE + JR OSCLI1 +; +OSCLI4: POP AF + INC DE +OSCLI5: BIT 7,(HL) + INC HL + JR Z,OSCLI5 + LD A,(HL) + INC HL + LD H,(HL) + LD L,A + PUSH HL + EX DE,HL + JP SKIPSP +; +OSCLI6: EX DE, HL ; HL: Buffer for command + LD DE, ACCS ; Buffer for command string is ACCS (the string accumulator) + PUSH DE ; Store buffer address + CALL CSTR_LINE ; Fetch the line + POP HL ; HL: Pointer to command string in ACCS + PUSH IY + MOSCALL mos_oscli ; Returns OSCLI error in A + POP IY + OR A ; 0 means MOS returned OK + RET Z ; So don't do anything + JP OSERROR ; Otherwise it's a MOS error + +SKIPSP: LD A,(HL) + CP ' ' + RET NZ + INC HL + JR SKIPSP + +UPPRC: AND 7FH + CP '`' + RET C + AND 5FH ; CONVERT TO UPPER CASE + RET + +; Each command has bit 7 of the last character set, and is followed by the address of the handler +; These must be in alphabetical order +; +COMDS: DB 'BY','E'+80h ; BYE + DW BYE + DB 'EDI','T'+80h ; EDIT + DW STAR_EDIT + DB 'F','X'+80h ; FX + DW STAR_FX +; DB 'VERSIO','N'+80h ; VERSION +; DW STAR_VERSION + DB FFh + +; *BYE +; +BYE: CALL VBLANK_STOP ; Restore MOS interrupts + POP.LIL IX ; The return address to init + LD HL, 0 ; The return code + JP (IX) + +; *EDIT linenum +; +STAR_EDIT: CALL ASC_TO_NUMBER ; DE: Line number to edit + EX DE, HL ; HL: Line number + CALL FINDL ; HL: Address in RAM of tokenised line + LD A, 41 ; F:NZ If the line is not found + JP NZ, ERROR_ ; Do error 41: No such line in that case +; +; Use LISTIT to output the line to the ACCS buffer +; + INC HL ; Skip the length byte + LD E, (HL) ; Fetch the line number + INC HL + LD D, (HL) + INC HL + LD IX, ACCS ; Pointer to where the copy is to be stored + LD (OSWRCHPT), IX + LD IX, LISTON ; Pointer to LISTON variable in RAM + LD A, (IX) ; Store that variable + PUSH AF + LD (IX), 09h ; Set to echo to buffer + CALL LISTIT + POP AF + LD (IX), A ; Restore the original LISTON variable + LD HL, ACCS ; HL: ACCS + LD E, L ; E: 0 - Don't clear the buffer; ACCS is on a page boundary so L is 0 + CALL OSLINE1 ; Invoke the editor + CALL OSEDIT + CALL C,CLEAN ; Set TOP, write out &FFFF end of program marker + JP CLOOP ; Jump back to immediate mode + +; OSCLI FX n +; +STAR_FX: CALL ASC_TO_NUMBER + LD C, E ; C: Save FX # + CALL ASC_TO_NUMBER + LD A, D ; Is first parameter > 255? + OR A + JR Z, STAR_FX1 ; Yes, so skip next bit + EX DE, HL ; Parameter is 16-bit + JR STAR_FX2 +; +STAR_FX1: LD B, E ; B: Save First parameter + CALL ASC_TO_NUMBER ; Fetch second parameter + LD L, B ; L: First parameter + LD H, E ; H: Second parameter +; +STAR_FX2: LD A, C ; A: FX # + JP OSBYTE + +; Helper Functions +; +WAIT_VBLANK: PUSH IX ; Wait for VBLANK interrupt + MOSCALL mos_sysvars ; Fetch pointer to system variables + LD.LIL A, (IX + sysvar_time + 0) +$$: CP.LIL A, (IX + sysvar_time + 0) + JR Z, $B + POP IX + RET \ No newline at end of file diff --git a/src/zds/agon_sound.asm b/src/zds/agon_sound.asm new file mode 100644 index 0000000..3e26ef0 --- /dev/null +++ b/src/zds/agon_sound.asm @@ -0,0 +1,155 @@ +; +; Title: BBC Basic for AGON - Audio stuff +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 11/12/2024 +; +; Modinfo: +; 11/12/2024: Modified SOUND_ to work with OSWORD + + .ASSUME ADL = 0 + + INCLUDE "equs.inc" + INCLUDE "macros.inc" + INCLUDE "mos_api.inc" ; In MOS/src + + SEGMENT CODE + + XDEF SOUND_ + + XREF OSWRCH + XREF VDU_BUFFER + XREF LTRAP + +; SOUND channel,volume,pitch,duration +; Parameters: +; - HL: Pointer to data +; - 0,1: Channel +; - 2,3: Volume 0 (off) to 15 (full volume) +; - 4,5: Pitch 0 - 255 +; - 6,7: Duration -1 to 254 (duration in 20ths of a second, -1 = play forever) +; +SOUND_: LD A, (HL) ; Channel + LD (VDU_BUFFER+0), A + XOR A ; Waveform + LD (VDU_BUFFER+1), A + INC HL + INC HL +; +; Calculate the volume +; + LD C, (HL) ; Volume + LD B, 6 ; C already contains the volume + MLT BC ; Multiply by 6 (0-15 scales to 0-90) + LD A, C + LD (VDU_BUFFER+2), A + INC HL + INC HL +; +; And the frequency +; + PUSH HL + LD L, (HL) + LD H, 0 + LD DE, SOUND_FREQ_LOOKUP + ADD HL, HL + ADD HL, DE + LD A, (HL) + LD (VDU_BUFFER+3), A + INC HL + LD A, (HL) + LD (VDU_BUFFER+4), A + POP HL + INC HL + INC HL +; +; And now the duration - multiply it by 50 to convert from 1/20ths of seconds to milliseconds +; + LD C, (HL) + LD B, 50 ; C contains the duration, so MLT by 50 + MLT BC + LD (VDU_BUFFER+5), BC +; + PUSH IX ; Get the system vars in IX + MOSCALL mos_sysvars ; Reset the semaphore +SOUND0: RES.LIL 3, (IX+sysvar_vpd_pflags) +; + VDU 23 ; Send the sound command + VDU 0 + VDU vdp_audio + VDU (VDU_BUFFER+0) ; 0: Channel + VDU (VDU_BUFFER+1) ; 1: Waveform (0) + VDU (VDU_BUFFER+2) ; 2: Volume (0-100) + VDU (VDU_BUFFER+3) ; 3: Frequency L + VDU (VDU_BUFFER+4) ; 4: Frequency H + VDU (VDU_BUFFER+5) ; 5: Duration L + VDU (VDU_BUFFER+6) ; 6: Duration H +; +; Wait for acknowledgement +; +$$: BIT.LIL 3, (IX+sysvar_vpd_pflags) + JR Z, $B ; Wait for the result + CALL LTRAP ; Check for ESC + LD.LIL A, (IX+sysvar_audioSuccess) + AND A ; Check if VDP has queued the note + JR Z, SOUND0 ; No, so loop back and send again +; + POP IX + RET + +; Frequency Lookup Table +; Set up to replicate the BBC Micro audio frequencies +; +; Split over 5 complete octaves, with 53 being middle C +; * C4: 262hz +; + A4: 440hz +; +; 2 3 4 5 6 7 8 +; +; B 1 49 97 145 193 241 +; A# 0 45 93 141 189 237 +; A 41 89+ 137 185 233 +; G# 37 85 133 181 229 +; G 33 81 129 177 225 +; F# 29 77 125 173 221 +; F 25 73 121 169 217 +; E 21 69 117 165 213 +; D# 17 65 113 161 209 +; D 13 61 109 157 205 253 +; C# 9 57 105 153 201 249 +; C 5 53* 101 149 197 245 +; +SOUND_FREQ_LOOKUP: DW 117, 118, 120, 122, 123, 131, 133, 135 + DW 137, 139, 141, 143, 145, 147, 149, 151 + DW 153, 156, 158, 160, 162, 165, 167, 170 + DW 172, 175, 177, 180, 182, 185, 188, 190 + DW 193, 196, 199, 202, 205, 208, 211, 214 + DW 217, 220, 223, 226, 230, 233, 236, 240 + DW 243, 247, 251, 254, 258, 262, 265, 269 + DW 273, 277, 281, 285, 289, 294, 298, 302 + DW 307, 311, 316, 320, 325, 330, 334, 339 + DW 344, 349, 354, 359, 365, 370, 375, 381 + DW 386, 392, 398, 403, 409, 415, 421, 427 + DW 434, 440, 446, 453, 459, 466, 473, 480 + DW 487, 494, 501, 508, 516, 523, 531, 539 + DW 546, 554, 562, 571, 579, 587, 596, 605 + DW 613, 622, 631, 641, 650, 659, 669, 679 + DW 689, 699, 709, 719, 729, 740, 751, 762 + DW 773, 784, 795, 807, 819, 831, 843, 855 + DW 867, 880, 893, 906, 919, 932, 946, 960 + DW 974, 988, 1002, 1017, 1032, 1047, 1062, 1078 + DW 1093, 1109, 1125, 1142, 1158, 1175, 1192, 1210 + DW 1227, 1245, 1263, 1282, 1300, 1319, 1338, 1358 + DW 1378, 1398, 1418, 1439, 1459, 1481, 1502, 1524 + DW 1546, 1569, 1592, 1615, 1638, 1662, 1686, 1711 + DW 1736, 1761, 1786, 1812, 1839, 1866, 1893, 1920 + DW 1948, 1976, 2005, 2034, 2064, 2093, 2123, 2154 + DW 2186, 2217, 2250, 2282, 2316, 2349, 2383, 2418 + DW 2453, 2489, 2525, 2562, 2599, 2637, 2675, 2714 + DW 2754, 2794, 2834, 2876, 2918, 2960, 3003, 3047 + DW 3091, 3136, 3182, 3228, 3275, 3322, 3371, 3420 + DW 3470, 3520, 3571, 3623, 3676, 3729, 3784, 3839 + DW 3894, 3951, 4009, 4067, 4126, 4186, 4247, 4309 + DW 4371, 4435, 4499, 4565, 4631, 4699, 4767, 4836 + + diff --git a/src/zds/eZ80F92_AGON_Flash.ztgt b/src/zds/eZ80F92_AGON_Flash.ztgt new file mode 100644 index 0000000..47720be --- /dev/null +++ b/src/zds/eZ80F92_AGON_Flash.ztgt @@ -0,0 +1,62 @@ + + + + Oscillator + 18432000 + + + 000000 + C0000 + FFFF + true + + + + 0 + false + 40000 + BFFFF + + 1 + false + true + + + + + 1 + 8 + 2 + 9 + + + 02 + 28 + C0 + C7 + + + 81 + 28 + 80 + BF + + + 02 + 00 + 00 + 00 + + + + 0 + ff + true + true + + 1 + + eZ80F92 + 1.0.1 + 1.00 + diff --git a/src/zds/equs.inc b/src/zds/equs.inc new file mode 100644 index 0000000..27d3ada --- /dev/null +++ b/src/zds/equs.inc @@ -0,0 +1,56 @@ +; +; Title: BBC Basic for AGON - Equs +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 05/12/2024 +; +; Modinfo: +; 05/12/2024: Removed Stack_Top + + XREF STAVAR + XREF ACCS + +RAM_Top: EQU 0FF00h + +; For GPIO +; PA not available on eZ80L92 +; +PA_DR: EQU 96h +PA_DDR: EQU 97h +PA_ALT1: EQU 98h +PA_ALT2: EQU 99h +PB_DR: EQU 9Ah +PB_DDR: EQU 9Bh +PB_ALT1: EQU 9Ch +PB_ALT2: EQU 9Dh +PC_DR: EQU 9Eh +PC_DDR: EQU 9Fh +PC_ALT1: EQU A0h +PC_ALT2: EQU A1h +PD_DR: EQU A2h +PD_DDR: EQU A3h +PD_ALT1: EQU A4h +PD_ALT2: EQU A5h + +GPIOMODE_OUT: EQU 0 ; Output +GPIOMODE_IN: EQU 1 ; Input +GPIOMODE_DIO: EQU 2 ; Open Drain IO +GPIOMODE_SIO: EQU 3 ; Open Source IO +GPIOMODE_INTD: EQU 4 ; Interrupt, Dual Edge +GPIOMODE_ALTF: EQU 5; ; Alt Function +GPIOMODE_INTAL: EQU 6 ; Interrupt, Active Low +GPIOMODE_INTAH: EQU 7 ; Interrupt, Active High +GPIOMODE_INTFE: EQU 8 ; Interrupt, Falling Edge +GPIOMODE_INTRE: EQU 9 ; Interrupt, Rising Edge + +; Originally in ram.asm +; +OC: EQU STAVAR+15*4 ; CODE ORIGIN (O%) +PC: EQU STAVAR+16*4 ; PROGRAM COUNTER (P%) +VDU_BUFFER: EQU ACCS ; Storage for VDU commands + +; Originally in main.asm +; +CR: EQU 0DH +LF: EQU 0AH +ESC: EQU 1BH diff --git a/src/zds/macros.inc b/src/zds/macros.inc new file mode 100644 index 0000000..5a9b0fe --- /dev/null +++ b/src/zds/macros.inc @@ -0,0 +1,52 @@ +; +; Title: BBC Basic Interpreter - Z80 version +; Useful macros +; Author: Dean Belfield +; Created: 04/12/2024 +; Last Updated: 04/12/2024 +; +; Modinfo: + +EXREG: MACRO rp1, rp2 + PUSH rp1 + POP rp2 + ENDMACRO + +ADD8U_DE: MACRO reg + ADD A, E + LD E, A + ADC A, D + SUB E + LD D, A + ENDMACRO + +ADD8U_HL: MACRO reg + ADD A, L + LD L, A + ADC A, H + SUB L + LD H, A + ENDMACRO + +VDU: MACRO VAL + LD A, VAL + CALL OSWRCH + ENDMACRO + +SET_GPIO: MACRO REG, VAL + IN0 A,(REG) + OR VAL + OUT0 (REG),A + ENDMACRO + +RES_GPIO: MACRO REG, VAL + PUSH BC + LD A, VAL + CPL + LD C, A + IN0 A,(REG) + AND C + OUT0 (REG),A + POP BC + ENDMACRO + \ No newline at end of file diff --git a/tools/transform_source.py b/tools/transform_source.py new file mode 100644 index 0000000..24e4e11 --- /dev/null +++ b/tools/transform_source.py @@ -0,0 +1,500 @@ +# Title: Z80 Source Transformer +# Author: Dean Belfield +# Created: 15/08/2024 +# Last Updated: 17/12/2024 +# Description: Convert Z80 assembler to work on various assemblers +# +# Modinfo: +# 01/12/2024: Improved the label parsing state machine +# 04/12/2024: Exported files for ZDS now assemble +# 06/12/2024: Added directives in hints and tweaked hints data +# 08/12/2024: Tweaked for OSCLI and file I/O +# 17/12/2024: Tweaked for v1.00-RC2 + +import sys +import os +import datetime +import re + +# Global stuff +# +now = datetime.datetime.now() +registers = ["A", "B", "C", "D", "E", "H", "L", "IXL", "IXH", "IYL", "IYH", "AF", "BC", "DE", "HL", "IX", "IY", "SP", "PC"] +reservedWords = { + "zds": ["AND", "OR", "MOD", "DIV", "IF", "ADDR", "COND", "CPL", "ERROR", "EVAL", "INT", "PAGE", "STRING", "TEXT", "VAR"], + "sjasmplus": [] +} + +# Match a single regex +# +def matchOne(regex, statement): + l = re.search(regex, statement) + if(l and len(l.groups()) > 0): + return l.group(1) + return None + +# Get a label from the statement +# +def getLabel(statement): + if(statement == None): + return None + m = matchOne(r"^(?:CALL|JR|JP)+\s+(?:C|NC|Z|NZ|M|P|PE|PO)+\s*,+\s*([a-zA-Z]+\w*)+", statement) + if(m): + return m + m = matchOne(r"^(?:DEFW|GLOBAL|EXTRN|CALL|CP|DJNZ|JR|JP|RST)+\s+([a-zA-Z]+\w*)+", statement) + if(m): + return m + m = matchOne(r"^(?:LD|IN)+\s+(?:A|B|C|D|E|H|L|BC|DE|HL|IX|IY|SP)\s*,+\s*\(*([a-zA-Z]+\w*)+\)*", statement) + if(m): + return m + return matchOne(r"^(?:LD|OUT)+\s+\(+([a-zA-Z]+\w*)+\)+\s*,+\s*(?:A|B|C|D|E|H|L|BC|DE|HL|IX|IY|SP)+", statement) + +# Replace a string only if it is at the start of the string +# +def replaceInstruction(string, find, replace): + if(string.startswith(find)): + return string.replace(find, replace, 1) + return string + +# Replace a string on a word boundary provided it is not at the start +# +def replaceOperator(string, find, replace): + if(not string.startswith(find) and not f"'{find}'" in string): + return re.sub(r"\b" + find + r"\b", replace, string) + return string + +# Represents a single line of source +# Members: +# - line: The original line +# - label: The label (if or ) +# +class Line: + def __init__(self, line): + self.statement = None + self.label = None + self.comment = None + self.statementLabel = None + + # Split the line into its component parts + # + state = 0 + for c in line.rstrip(): + + if(state == 0): # Check if beginning of line is a label or not + if(c == ";"): # It is a comment? + self.comment = c + state = 4 # Go to the comment read state + elif(c.isspace()): # Is it a space? + state = 2 # Go to the read statement state + else: + self.label = c + state = 1 # It's a label, so go to the read label state + + elif(state == 1): # Read a label in + if(c == ":" or c.isspace()): # Is it the end of the label? + state = 2 # Yes, so go to the read statement state + else: + self.label += c + + elif(state == 2): # Read the statement in - first skip whitespace + if(not c.isspace()): + self.statement = c + state = 3 + + elif(state == 3): # Read the rest of the statement in + self.statement += c + + elif(state == 4): # Read the comment in + self.comment += c + + # Now get the statement label + # + label = getLabel(self.statement) + if(label != None): + if(self.statement.startswith("GLOBAL") or self.statement.startswith("EXTRN") or not label.upper() in registers): + self.statementLabel = label + + # Do some line level refactoring + # + def refactor(self, target, indent, xdef): + if(self.label and self.label in reservedWords[target]): + self.label+="_" + + if(self.statementLabel and self.statementLabel in reservedWords[target]): + self.statement = replaceOperator(self.statement, self.statementLabel, self.statementLabel + "_") + + if(target == "zds"): + if(self.statement): + self.statement = replaceInstruction(self.statement, "EXTRN", "XREF") + self.statement = replaceInstruction(self.statement, "GLOBAL", "XDEF") + self.statement = replaceInstruction(self.statement, "DEFS", "DS") + self.statement = replaceInstruction(self.statement, "DEFW", "DW") + self.statement = replaceInstruction(self.statement, "DEFB", "DB") + self.statement = replaceInstruction(self.statement, "DEFM", "DB") + self.statement = replaceOperator(self.statement, "AND", "&") + self.statement = replaceOperator(self.statement, "OR", "|") + # + # TODO: This is a bit of a bodge, needs improving + # Replace escaped apostrophes ('') in the middle of strings + # + aposCount = self.statement.count("'") + if(aposCount > 2 and aposCount%2 == 0): + self.statement = self.statement.replace("''", "'", 1) + + elif(target == "sjasmplus"): + if(self.statement): + if(self.statement.startswith("EXTRN") or self.statement.startswith("GLOBAL")): + self.comment = f";\t{self.statement}" + self.statement = None + else: + if(self.label and self.label in xdef): + self.label = f"@{self.label}" + + if(self.label == None and self.statement == None): + return f"{self.comment or ''}" + else: + if(self.label): + return f"{(self.label + ':').ljust(indent)}{self.statement or ''}\t{self.comment or ''}" + else: + return f"{''.ljust(indent)}{self.statement}\t{self.comment or ''}" + +# The source file class +# Parameters: +# - filename: The filename of the source file +# +class Source: + def __init__(self, filename): + self.filename = filename + self.module = os.path.splitext(os.path.basename(filename))[0] + self.lines = [] + self.xdef = set() + self.xref = set() + self.target = None + self.indent = None + self.hints = {} + + # Set the target + # - target: zds or sjasmplus + # + def setTarget(self, target): + self.target = target + + # Set the indent + # - indent: number of spaces to pad labels out to + # + def setIndent(self, indent): + self.indent = indent + + # Set source hints + # - hints: Dictionary of manual fixes + # + def setHints(self, hints): + self.hints = hints + + # Add a comment + # - comment: the comment to add (must be prefixed with ';') + # + def insertLine(self, comment): + self.lines.append(Line(comment)) + + # Open the file for reading + # + def open(self): + full_path = os.path.expanduser(self.filename) + self.file = open(full_path, "r") + + # Read and process the file + # - ignoreFirstLine: set to true to ignore the first line + # + def read(self, ignoreFirstLine): + insert = not ignoreFirstLine + while(True): + line = self.file.readline() + if(not line): + break + if(insert): + self.lines.append(Line(line)) + insert = True + + # Close the file for reading + # + def close(self): + self.file.close() + + # Do any source level refactoring + # + def refactor(self): + output = [] + + # Add the generic autogeneration comment + # + output.append(Line(f";")) + output.append(Line(f";Automatically created from original source on {now.strftime('%Y-%m-%d %H:%M:%S')}")) + output.append(Line(f";")) + + if(self.target == "sjasmplus"): + output.append(Line(f"\tMODULE {self.module}")) + elif(self.target == "zds"): + output.append(Line(f"\t.ASSUME ADL = 0")) + + # Source specific directives + # + if("directives" in self.hints[self.target]): + for item in self.hints[self.target]["directives"]: + output.append(Line(item)) + + # Build up the xref and xdef lists + # + for line in self.lines: + if(line.statement): + # + # xref labels are referenced in this module and are external + # + if(line.statement.startswith("EXTRN")): self.xref.add(line.statementLabel) + # + # xdef labels are exported from this module and referenced elsewhere + # + if(line.statement.startswith("GLOBAL")): self.xdef.add(line.statementLabel) + # + # Source specific hints + # + if(self.target in self.hints): + if("hints" in self.hints[self.target]): + for item in self.hints[self.target]["hints"]: + if(item["hint"] in line.statement): + if("prepend" in item): + for p in item["prepend"]: + output.append(Line(p)) + if("update" in item): + line = Line(item["update"]) + + output.append(line) + + # Add the module directives for sjasmplus + # + if(self.target == "sjasmplus"): + output.append(Line(f"\tENDMODULE")) + + self.lines = output[:] + + # Export the source + # + def export(self): + filename = os.path.basename(self.filename) + if(self.target == "zds"): + filename = filename.replace(".Z80", ".ASM").lower() + dirname = os.path.join(os.path.dirname(self.filename), self.target) + if(not os.path.exists(dirname)): + os.makedirs(dirname) + file = open(os.path.join(dirname, filename), "w") + for line in self.lines: + output = line.refactor(self.target, self.indent, self.xdef) + if(output): + file.write(f"{output}\n") + file.close() + +# The project class +# +class Project: + def __init__(self): + self.filenames = [] + self.ignoreFirstLine = False + self.source = [] + self.indent = 8 + self.hints = {} + + # Set the array of filenames to import + # - filenames: array of paths to filenames + # + def setFilenames(self, filenames): + self.filenames = filenames + + # Ignore the first line of the source code + # - ignoreFirstLine: set to true to ignore + # + def setIgnoreFirstLine(self, ignoreFirstLine): + self.ignoreFirstLine = ignoreFirstLine + + # Set the target + # - target: zds or sjasmplus + # + def setTarget(self, target): + if(target not in ["sjasmplus", "zds"]): + raise Exception(f"Invalid target {target}") + self.target = target + print(f"Set target to {target}") + + # Set the indent + # - indent: number of spaces to pad labels out to + # + def setIndent(self, indent): + self.indent = indent + + # Set source hints + # - hints: Dictionary of manual fixes + # + def setHints(self, hints): + self.hints = hints + + # Parse the project + # + def parse(self): + for filename in self.filenames: + print(f"Loading {filename}") + s = Source(filename) + s.setTarget(self.target) + s.setIndent(self.indent) + if(filename in self.hints): + s.setHints(self.hints[filename]) + s.open() + s.read(self.ignoreFirstLine) + s.close() + s.refactor() + self.source.append(s) + + # Export the project + # + def export(self): + for s in self.source: + # Get list of labels in all the other sources that reference this - so this can export those labels + # + s.export() + +# Start here +# +os.chdir(os.path.dirname(os.path.abspath(__file__))) + +project = Project() +project.setIgnoreFirstLine(True) +project.setTarget("zds") +project.setIndent(16) +project.setFilenames([ + "../src/ACORN.Z80", + "../src/ASMB.Z80", + "../src/DATA.Z80", + "../src/EVAL.Z80", + "../src/EXEC.Z80", + "../src/MAIN.Z80", + "../src/MATH.Z80", +]) +project.setHints({ + "../src/ACORN.Z80": { + "zds": { + "directives": [ "\tSEGMENT CODE" ], + "hints": [ + { + "hint": "EQU\t0FFEEH", + "update": "\tXREF\tOSWRCH" + }, + { + "hint": "EQU\t0FFF1H", + "update": "\tXREF\tOSWORD" + }, + { + "hint": "EQU\t0FFF4H", + "update": "\tXREF\tOSBYTE" + } + ] + } + }, + "../src/ASMB.Z80": { + "zds": { + "directives": [ "\tSEGMENT CODE" ], + } + }, + "../src/DATA.Z80": { + "zds": { + "directives": [ + "\tDEFINE LORAM, SPACE = ROM", + "\tSEGMENT LORAM", + ";", + "\tXDEF\tFLAGS", + "\tXDEF\tOSWRCHPT", + "\tXDEF\tOSWRCHCH", + "\tXDEF\tOSWRCHFH", + "\tXDEF\tKEYDOWN", + "\tXDEF\tKEYASCII", + "\tXDEF\tKEYCOUNT", + "\tXDEF\tSCRAP", + "\tXDEF\tBUFFER", + "\tXDEF\tLISTON", + "\tXDEF\tPAGE_", + ";", + "FLAGS:\tDS\t1", + "OSWRCHPT:\tDS\t2", + "OSWRCHCH:\tDS\t1", + "OSWRCHFH:\tDS\t1", + "KEYDOWN:\tDS\t1", + "KEYASCII:\tDS\t1", + "KEYCOUNT:\tDS\t1", + "SCRAP:\tDS\t31", + ";", + "\tALIGN 256" + ], + } + }, + "../src/EVAL.Z80": { + "zds": { + "directives": [ + "\tSEGMENT CODE", + ";", + "\tXDEF\tCOUNT0", + "\tXDEF\tCOUNT1", + "\tXDEF\tZERO", + "\tXDEF\tTRUE" + ], + "hints": [ + { + "hint": "FUNTOK+($-FUNTBL)/2", + "prepend": [ + "FUNTBL_END:\tEQU\t$" + ], + "update": "TCMD:\tEQU\tFUNTOK+(FUNTBL_END-FUNTBL)/2" + } + ] + } + }, + "../src/EXEC.Z80": { + "zds": { + "directives": [ "\tSEGMENT CODE" ], + "hints": [ + { + "hint": "TCMD-128+($-CMDTAB)/2", + "prepend": [ + "CMDTAB_END:\tEQU\t$" + ], + "update": "TLAST:\tEQU\tTCMD-128+(CMDTAB_END-CMDTAB)/2" + } + ] + } + }, + "../src/MAIN.Z80": { + "zds": { + "directives": [ + "\tSEGMENT CODE", + ";", + "\tXDEF\tNEWIT", + "\tXDEF\tBAD", + "\tXDEF\tCLEAN", + "\tXDEF\tLINNUM", + "\tXDEF\tERROR_", + "\tXDEF\tGETTOP", + "\tXDEF\tDEL", + "\tXDEF\tLISTIT" + ], + "hints": [ + { + "hint": "\'Can\'\'t match \'", + "update": "\tDB\t\"Can\'t match \"" + } + ] + } + }, + "../src/MATH.Z80": { + "zds": { + "directives": [ "\tSEGMENT CODE" ], + } + } +}) +project.parse() +project.export() \ No newline at end of file