mirror of
https://github.com/hsoft/collapseos.git
synced 2024-11-23 12:48:05 +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
|
||||
#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
|
||||
|
||||
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
|
||||
might need explanation.
|
||||
|
||||
*** Quickies
|
||||
|
||||
or a: Equivalent to "cp 0", but results in a shorter opcode.
|
||||
|
||||
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
|
||||
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,
|
||||
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
|
||||
"unsetZ" routine exists for that, although that in certain circumstances,
|
||||
"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)"
|
||||
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,
|
||||
@ -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,
|
||||
0x03 and 0x04, then DE (being the "high" word) would be 0x0201 and HL would be
|
||||
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 "lib/util.asm"
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "lib/fmt.asm"
|
||||
.inc "basic/tok.asm"
|
||||
.equ BAS_RAMSTART USER_RAMSTART
|
||||
.inc "basic/main.asm"
|
||||
USER_RAMSTART:
|
||||
|
@ -1,10 +1,13 @@
|
||||
; *** Constants ***
|
||||
.equ BAS_SCRATCHPAD_SIZE 0x20
|
||||
; *** Variables ***
|
||||
; Value of `SP` when basic was first invoked. This is where SP is going back to
|
||||
; on restarts.
|
||||
.equ BAS_INITSP BAS_RAMSTART
|
||||
; **Pointer** to current line number
|
||||
.equ BAS_PCURLN @+2
|
||||
.equ BAS_RAMEND @+2
|
||||
.equ BAS_SCRATCHPAD @+2
|
||||
.equ BAS_RAMEND @+BAS_SCRATCHPAD_SIZE
|
||||
|
||||
; *** Code ***
|
||||
basStart:
|
||||
@ -39,24 +42,37 @@ basPrompt:
|
||||
.db "> ", 0
|
||||
|
||||
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
|
||||
ld hl, basCmds1
|
||||
ld hl, basCmds1+2
|
||||
.loop:
|
||||
ld a, 4
|
||||
ld a, b ; whitespace IDX
|
||||
call strncmp
|
||||
jr z, .found
|
||||
ld a, 6
|
||||
ld a, 8
|
||||
call addHL
|
||||
ld a, (hl)
|
||||
cp 0xff
|
||||
jr nz, .loop
|
||||
.unknown:
|
||||
ld hl, .sUnknown
|
||||
jr basPrintLn
|
||||
|
||||
.found:
|
||||
inc hl \ inc hl \ inc hl \ inc hl
|
||||
dec hl \ dec hl
|
||||
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:
|
||||
.db "Unknown command", 0
|
||||
@ -66,7 +82,17 @@ basPrintLn:
|
||||
call printstr
|
||||
jp printcrlf
|
||||
|
||||
basERR:
|
||||
ld hl, .sErr
|
||||
jr basPrintLn
|
||||
.sErr:
|
||||
.db "ERR", 0
|
||||
|
||||
; *** 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:
|
||||
ld hl, .sBye
|
||||
call basPrintLn
|
||||
@ -78,10 +104,20 @@ basBYE:
|
||||
.sBye:
|
||||
.db "Goodbye!", 0
|
||||
|
||||
basPRINT:
|
||||
call parseDecimal
|
||||
jp nz, basERR
|
||||
push ix \ pop de
|
||||
ld hl, BAS_SCRATCHPAD
|
||||
call fmtDecimal
|
||||
jp basPrintLn
|
||||
|
||||
; direct only
|
||||
basCmds1:
|
||||
.db "bye", 0
|
||||
.dw basBYE
|
||||
.db "bye", 0, 0, 0
|
||||
; statements
|
||||
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
|
||||
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)
|
||||
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
|
||||
; (advance HL and put a null char at the end of the string) and set 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