1
0
mirror of https://github.com/hsoft/collapseos.git synced 2024-11-27 12:28:06 +11:00

basic: add a print cmd

It can only print a decimal literal. But still, that's a big step because
I hadn't implemented decimal formatting yet.
This commit is contained in:
Virgil Dupras 2019-11-18 13:40:23 -05:00
parent 019d05f64c
commit 1cea6e71e0
10 changed files with 302 additions and 34 deletions

14
CODE.md
View File

@ -32,6 +32,20 @@ Thus, code that glue parts together could look like:
MOD2_RAMSTART .equ MOD1_RAMEND MOD2_RAMSTART .equ MOD1_RAMEND
#include "mod2.asm" #include "mod2.asm"
## Register protection
As a general rule, all routines systematically protect registers they use,
including input parameters. This allows us to stop worrying, each time we call
a routine, whether our registers are all messed up.
Some routines stray from that rule, but the fact that they destroy a particular
register is documented. An undocumented register change is considered a bug.
Clean up after yourself, you nasty routine!
Another exception to this rule are "top-level" routines, that is, routines that
aren't designed to be called from other parts of Collapse OS. Those are
generally routines close to an application's main loop.
## Stack management ## Stack management
Keeping the stack "balanced" is a big challenge when writing assembler code. Keeping the stack "balanced" is a big challenge when writing assembler code.

View File

@ -1,6 +1,8 @@
This file describe tricks and conventions that are used throughout the code and This file describe tricks and conventions that are used throughout the code and
might need explanation. might need explanation.
*** Quickies
or a: Equivalent to "cp 0", but results in a shorter opcode. or a: Equivalent to "cp 0", but results in a shorter opcode.
xor a: sets A to 0 more efficiently than ld a, 0 xor a: sets A to 0 more efficiently than ld a, 0
@ -8,12 +10,16 @@ xor a: sets A to 0 more efficiently than ld a, 0
and 0xbf: Given a letter in the a-z range, changes it to its uppercase value and 0xbf: Given a letter in the a-z range, changes it to its uppercase value
if it's already uppercased, then it stays that way. if it's already uppercased, then it stays that way.
*** Z flag for results
Z if almost always used as a success indicator for routines. Set for success, Z if almost always used as a success indicator for routines. Set for success,
Reset for failure. "xor a" (destroys A) and "cp a" (preserves A) are used to Reset for failure. "xor a" (destroys A) and "cp a" (preserves A) are used to
ensure Z is set. To ensure that it is reset, it's a bit more complicated and ensure Z is set. To ensure that it is reset, it's a bit more complicated and
"unsetZ" routine exists for that, although that in certain circumstances, "unsetZ" routine exists for that, although that in certain circumstances,
"inc a \ dec a" or "or a" can work. "inc a \ dec a" or "or a" can work.
*** Little endian
z80 is little endian in its 16-bit loading operations. For example, "ld hl, (0)" z80 is little endian in its 16-bit loading operations. For example, "ld hl, (0)"
will load the contents of memory address 0 in L and memory address 1 in H. This will load the contents of memory address 0 in L and memory address 1 in H. This
little-endianess is followed by Collapse OS in most situations. When it's not, little-endianess is followed by Collapse OS in most situations. When it's not,
@ -25,3 +31,21 @@ are stored as "big endian pair of little endian 16-bit numbers". For example,
if "ld dehl, (0)" existed and if the first 4 bytes of memory were 0x01, 0x02, if "ld dehl, (0)" existed and if the first 4 bytes of memory were 0x01, 0x02,
0x03 and 0x04, then DE (being the "high" word) would be 0x0201 and HL would be 0x03 and 0x04, then DE (being the "high" word) would be 0x0201 and HL would be
0x0403. 0x0403.
*** DAA
When it comes to dealing with decimals, the DAA instruction, which look a bit
obscur, can be very useful. It transforms the result of a previous arithmetic
operation involving two BCD (binary coded decimal, one digit in high nibble,
the other digit in low nibble. For example, 0x99 represents 99) into a valid
BCD. For example, 0x12+0x19=0x2b, but after calling DAA, it will be 0x31.
To clear misunderstanding: this does **not** transform an arbitrary value into
BCD. For example, "ld a, 0xff \ daa" isn't going to magically give you a binary
coded 255 (how could it?). This is designed to be ran after an arithmetic
operation.
A common trick to transform an arbitrary number to BCD is to loop 8 times over
your bitstream, SLA your bits out of your binary value and then run
"adc a, a \ daa" over it (with provisions for carries if you expect numbers
over 99).

View File

@ -11,7 +11,10 @@
.inc "core.asm" .inc "core.asm"
.inc "lib/util.asm" .inc "lib/util.asm"
.inc "lib/ari.asm"
.inc "lib/parse.asm" .inc "lib/parse.asm"
.inc "lib/fmt.asm"
.inc "basic/tok.asm"
.equ BAS_RAMSTART USER_RAMSTART .equ BAS_RAMSTART USER_RAMSTART
.inc "basic/main.asm" .inc "basic/main.asm"
USER_RAMSTART: USER_RAMSTART:

View File

@ -1,10 +1,13 @@
; *** Constants ***
.equ BAS_SCRATCHPAD_SIZE 0x20
; *** Variables *** ; *** Variables ***
; Value of `SP` when basic was first invoked. This is where SP is going back to ; Value of `SP` when basic was first invoked. This is where SP is going back to
; on restarts. ; on restarts.
.equ BAS_INITSP BAS_RAMSTART .equ BAS_INITSP BAS_RAMSTART
; **Pointer** to current line number ; **Pointer** to current line number
.equ BAS_PCURLN @+2 .equ BAS_PCURLN @+2
.equ BAS_RAMEND @+2 .equ BAS_SCRATCHPAD @+2
.equ BAS_RAMEND @+BAS_SCRATCHPAD_SIZE
; *** Code *** ; *** Code ***
basStart: basStart:
@ -39,24 +42,37 @@ basPrompt:
.db "> ", 0 .db "> ", 0
basDirect: basDirect:
; First, get cmd length
call fnWSIdx
cp 7
jr nc, .unknown ; Too long, can't possibly fit anything.
; A contains whitespace IDX, save it in B
ld b, a
ex de, hl ex de, hl
ld hl, basCmds1 ld hl, basCmds1+2
.loop: .loop:
ld a, 4 ld a, b ; whitespace IDX
call strncmp call strncmp
jr z, .found jr z, .found
ld a, 6 ld a, 8
call addHL call addHL
ld a, (hl) ld a, (hl)
cp 0xff cp 0xff
jr nz, .loop jr nz, .loop
.unknown:
ld hl, .sUnknown ld hl, .sUnknown
jr basPrintLn jr basPrintLn
.found: .found:
inc hl \ inc hl \ inc hl \ inc hl dec hl \ dec hl
call intoHL call intoHL
jp (hl) push hl \ pop ix
; Bring back command string from DE to HL
ex de, hl
ld a, b ; cmd's length
call addHL
call rdWS
jp (ix)
.sUnknown: .sUnknown:
.db "Unknown command", 0 .db "Unknown command", 0
@ -66,7 +82,17 @@ basPrintLn:
call printstr call printstr
jp printcrlf jp printcrlf
basERR:
ld hl, .sErr
jr basPrintLn
.sErr:
.db "ERR", 0
; *** Commands *** ; *** Commands ***
; A command receives its argument through (HL), which is already placed to
; either:
; 1 - the end of the string if the command has no arg.
; 2 - the beginning of the arg, with whitespace properly skipped.
basBYE: basBYE:
ld hl, .sBye ld hl, .sBye
call basPrintLn call basPrintLn
@ -78,10 +104,20 @@ basBYE:
.sBye: .sBye:
.db "Goodbye!", 0 .db "Goodbye!", 0
basPRINT:
call parseDecimal
jp nz, basERR
push ix \ pop de
ld hl, BAS_SCRATCHPAD
call fmtDecimal
jp basPrintLn
; direct only ; direct only
basCmds1: basCmds1:
.db "bye", 0
.dw basBYE .dw basBYE
.db "bye", 0, 0, 0
; statements ; statements
basCmds2: basCmds2:
.db 0xff ; end of table .dw basPRINT
.db "print", 0
.db 0xff, 0xff, 0xff ; end of table

48
apps/basic/tok.asm Normal file
View File

@ -0,0 +1,48 @@
; Expect at least one whitespace (0x20, 0x09) at (HL), and then advance HL
; until a non-whitespace character is met.
; HL is advanced to the first non-whitespace char.
; Sets Z on success, unset on failure.
; Failure is either not having a first whitespace or reaching the end of the
; string.
; Sets Z if we found a non-whitespace char, unset if we found the end of string.
rdWS:
ld a, (hl)
call isSep
ret nz ; failure
.loop:
inc hl
ld a, (hl)
call isSep
jr z, .loop
or a ; cp 0
jp z, .fail
cp a ; ensure Z
ret
.fail:
; A is zero at this point
inc a ; unset Z
ret
; Find the first whitespace in (HL) and returns its index in A
; Sets Z if whitespace is found, unset if end of string was found.
; In the case where no whitespace was found, A returns the length of the string.
fnWSIdx:
push hl
push bc
ld b, 0
.loop:
ld a, (hl)
call isSep
jr z, .found
or a
jr z, .eos
inc hl
inc b
jr .loop
.eos:
inc a ; unset Z
.found: ; Z already set from isSep
ld a, b
pop bc
pop hl
ret

27
apps/lib/ari.asm Normal file
View File

@ -0,0 +1,27 @@
; Borrowed from Tasty Basic by Dimitri Theulings (GPL).
; Divide HL by DE, placing the result in BC and the remainder in HL.
divide:
push hl ; --> lvl 1
ld l, h ; divide h by de
ld h, 0
call .dv1
ld b, c ; save result in b
ld a, l ; (remainder + l) / de
pop hl ; <-- lvl 1
ld h, a
.dv1:
ld c, 0xff ; result in c
.dv2:
inc c ; dumb routine
call .subde ; divide using subtract and count
jr nc, .dv2
add hl, de
ret
.subde:
ld a, l
sub e ; subtract de from hl
ld l, a
ld a, h
sbc a, d
ld h, a
ret

46
apps/lib/fmt.asm Normal file
View File

@ -0,0 +1,46 @@
; Format the number in DE into the string at (HL) in a decimal form.
; Null-terminated. DE is considered an unsigned number.
fmtDecimal:
push ix
push hl
push de
push af
push hl \ pop ix
ex de, hl ; orig number now in HL
ld e, 0
.loop1:
call .div10
push hl ; push remainder. --> lvl E
inc e
ld a, b ; result 0?
or c
push bc \ pop hl
jr nz, .loop1 ; not zero, continue
; We now have C digits to print in the stack.
; Spit them!
push ix \ pop hl ; restore orig HL.
ld b, e
.loop2:
pop de ; <-- lvl E
ld a, '0'
add a, e
ld (hl), a
inc hl
djnz .loop2
; null terminate
xor a
ld (hl), a
pop af
pop de
pop hl
pop ix
ret
.div10:
push de
ld de, 0x000a
call divide
pop de
ret

View File

@ -26,3 +26,29 @@ strcpy:
pop hl pop hl
ret ret
; Compares strings pointed to by HL and DE until one of them hits its null char.
; If equal, Z is set. If not equal, Z is reset.
strcmp:
push hl
push de
.loop:
ld a, (de)
cp (hl)
jr nz, .end ; not equal? break early. NZ is carried out
; to the called
or a ; If our chars are null, stop the cmp
jr z, .end ; The positive result will be carried to the
; caller
inc hl
inc de
jr .loop
.end:
pop de
pop hl
; Because we don't call anything else than CP that modify the Z flag,
; our Z value will be that of the last cp (reset if we broke the loop
; early, set otherwise)
ret

View File

@ -98,32 +98,6 @@ strncmpI:
; early, set otherwise) ; early, set otherwise)
ret ret
; Compares strings pointed to by HL and DE until one of them hits its null char.
; If equal, Z is set. If not equal, Z is reset.
strcmp:
push hl
push de
.loop:
ld a, (de)
cp (hl)
jr nz, .end ; not equal? break early. NZ is carried out
; to the called
cp 0 ; If our chars are null, stop the cmp
jr z, .end ; The positive result will be carried to the
; caller
inc hl
inc de
jr .loop
.end:
pop de
pop hl
; Because we don't call anything else than CP that modify the Z flag,
; our Z value will be that of the last cp (reset if we broke the loop
; early, set otherwise)
ret
; If string at (HL) starts with ( and ends with ), "enter" into the parens ; If string at (HL) starts with ( and ends with ), "enter" into the parens
; (advance HL and put a null char at the end of the string) and set Z. ; (advance HL and put a null char at the end of the string) and set Z.
; Otherwise, do nothing and reset Z. ; Otherwise, do nothing and reset Z.

View File

@ -0,0 +1,70 @@
jp test
.inc "core.asm"
.inc "lib/util.asm"
.inc "lib/ari.asm"
.inc "lib/fmt.asm"
testNum: .db 1
test:
ld sp, 0xffff
call testFmtDecimal
; success
xor a
halt
testFmtDecimal:
ld ix, .t1
call .test
ld ix, .t2
call .test
ld ix, .t3
call .test
ld ix, .t4
call .test
ld ix, .t5
call .test
ret
.test:
ld e, (ix)
ld d, (ix+1)
ld hl, sandbox
call fmtDecimal
ld hl, sandbox
push ix \ pop de
inc de \ inc de
call strcmp
jp nz, fail
jp nexttest
.t1:
.dw 1234
.db "1234", 0
.t2:
.dw 9999
.db "9999", 0
.t3:
.dw 0
.db "0", 0
.t4:
.dw 0x7fff
.db "32767", 0
.t5:
.dw 0xffff
.db "65535", 0
nexttest:
ld a, (testNum)
inc a
ld (testNum), a
ret
fail:
ld a, (testNum)
halt
; used as RAM
sandbox: