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:
parent
019d05f64c
commit
1cea6e71e0
14
CODE.md
14
CODE.md
@ -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.
|
||||||
|
24
TRICKS.txt
24
TRICKS.txt
@ -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).
|
||||||
|
@ -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:
|
||||||
|
@ -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
48
apps/basic/tok.asm
Normal 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
27
apps/lib/ari.asm
Normal 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
46
apps/lib/fmt.asm
Normal 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
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
70
tools/tests/unit/test_lib_fmt.asm
Normal file
70
tools/tests/unit/test_lib_fmt.asm
Normal 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:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user