mirror of
https://github.com/hsoft/collapseos.git
synced 2024-12-25 05:08:05 +11:00
Forth reboot underway!
This commit is contained in:
parent
b162ef84f5
commit
6bf51ae57c
117
CODE.md
117
CODE.md
@ -1,117 +0,0 @@
|
||||
## Code conventions
|
||||
|
||||
The code in this project follow certain project-wide conventions, which are
|
||||
described here. Kernel code and userspace code follow additional conventions
|
||||
which are described in `kernel/README.md` and `apps/README.md`.
|
||||
|
||||
## Defines
|
||||
|
||||
Each unit can have its own constants, but some constant are made to be defined
|
||||
externally. We already have some of those external definitions in platform
|
||||
includes, but we can have more defines than this.
|
||||
|
||||
Many units have a "DEFINES" section listing the constant it expects to be
|
||||
defined. Make sure that you have these constants defined before you include the
|
||||
file.
|
||||
|
||||
## Variable management
|
||||
|
||||
Each unit can define variables. These variables are defined as addresses in
|
||||
RAM. We know where RAM start from the `RAMSTART` constant in platform includes,
|
||||
but because those parts are made to be glued together in no pre-defined order,
|
||||
we need a system to align variables from different modules in RAM.
|
||||
|
||||
This is why each unit that has variable expect a `<PREFIX>_RAMSTART`
|
||||
constant to be defined and, in turn, defines a `<PREFIX>_RAMEND` constant to
|
||||
carry to the following unit.
|
||||
|
||||
Thus, code that glue parts together could look like:
|
||||
|
||||
MOD1_RAMSTART .equ RAMSTART
|
||||
#include "mod1.asm"
|
||||
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.
|
||||
|
||||
It is important to note, however, that shadow registers aren't preserved.
|
||||
Therefore, shadow registers should only be used in code that doesn't call
|
||||
routines or that call a routine that explicitly states that it preserves
|
||||
shadow registers.
|
||||
|
||||
Another important note is that routines returning success with Z generally don't
|
||||
preserve AF: too complicated. But otherwise, AF is often preserved. For example,
|
||||
register fiddling routines in core try to preserve AF.
|
||||
|
||||
## Z for success
|
||||
|
||||
The vast majority of routines use the Z flag to indicate success. When Z is set,
|
||||
it indicates success. When Z is unset, it indicates error. This follows the
|
||||
tradition of a zero indicating success and a nonzero indicating error.
|
||||
|
||||
Important note: only Z indicate success. Many routines return a meaningful
|
||||
nonzero value in A and still set Z to indicate success.
|
||||
|
||||
In error conditions, however, most of the time A is set to an error code.
|
||||
|
||||
In many routines, this is specified verbosely, but it's repeated so often that
|
||||
I started writing it in short form, "Z for success", which means what is
|
||||
described here.
|
||||
|
||||
## Stack management
|
||||
|
||||
Keeping the stack "balanced" is a big challenge when writing assembler code.
|
||||
Those push and pop need to correspond, otherwise we end up with completely
|
||||
broken code.
|
||||
|
||||
The usual "push/pop" at the beginning and end of a routine is rather easy to
|
||||
manage, nothing special about them.
|
||||
|
||||
The problem is for the "inner" push and pop, which are often necessary in
|
||||
routines handling more data at once. In those cases, we walk on eggshells.
|
||||
|
||||
A naive approach could be to indent the code between those push/pop, but indent
|
||||
level would quickly become too big to fit in 80 chars.
|
||||
|
||||
I've tried ASCII art in some places, where comments next to push/pop have "|"
|
||||
indicating the scope of the push/pop. It's nice, but it makes code complicated
|
||||
to edit, especially when dense comments are involved. The pipes have to go
|
||||
through them.
|
||||
|
||||
Of course, one could add descriptions next to each push/pop describing what is
|
||||
being pushed, and I do it in some places, but it doesn't help much in easily
|
||||
tracking down stack levels.
|
||||
|
||||
So, what I've started doing is to accompany each "non-routine" (at the
|
||||
beginning and end of a routine) push/pop with "--> lvl X" and "<-- lvl X"
|
||||
comments. Example:
|
||||
|
||||
push af ; --> lvl 1
|
||||
inc a
|
||||
push af ; --> lvl 2
|
||||
inc a
|
||||
pop af ; <-- lvl 2
|
||||
pop af ; <-- lvl 1
|
||||
|
||||
I think that this should do the trick, so I'll do this consistently from now on.
|
||||
|
||||
## String length
|
||||
|
||||
Pretty much every routine expecting a string have no provision for a string
|
||||
that doesn't have null termination within 0xff bytes. Treat strings of such
|
||||
lengths with extra precaution and distrust proper handling of existing routines
|
||||
for those strings.
|
||||
|
||||
[zasm]: ../apps/zasm/README.md
|
111
apps/README.md
111
apps/README.md
@ -1,111 +0,0 @@
|
||||
# User applications
|
||||
|
||||
This folder contains code designed to be "userspace" application. Unlike the
|
||||
kernel, which always stay in memory. Those apps here will more likely be loaded
|
||||
in RAM from storage, ran, then discarded so that another userspace program can
|
||||
be run.
|
||||
|
||||
That doesn't mean that you can't include that code in your kernel though, but
|
||||
you will typically not want to do that.
|
||||
|
||||
## Userspace convention
|
||||
|
||||
We execute a userspace application by calling the address it's loaded into.
|
||||
|
||||
This means that userspace applications must be assembled with a proper `.org`,
|
||||
otherwise labels in its code will be wrong.
|
||||
|
||||
The `.org`, it is not specified by glue code of the apps themselves. It is
|
||||
expected to be set either in the `user.h` file to through `zasm` 3rd argument.
|
||||
|
||||
That a userspace is called also means that an application, when finished
|
||||
running, is expected to return with a regular `ret` and a clean stack.
|
||||
|
||||
Whatever calls the userspace app (usually, it will be the shell), should set
|
||||
HL to a pointer to unparsed arguments in string form, null terminated.
|
||||
|
||||
The userspace application is expected to set A on return. 0 means success,
|
||||
non-zero means error.
|
||||
|
||||
A userspace application can expect the `SP` pointer to be properly set. If it
|
||||
moves it, it should take care of returning it where it was before returning
|
||||
because otherwise, it will break the kernel.
|
||||
|
||||
## Memory management
|
||||
|
||||
Apps in Collapse OS are design to be ROM-compatible, that is, they don't write
|
||||
to addresses that are part of the code's address space.
|
||||
|
||||
By default, apps set their RAM to begin at the end of the binary because in
|
||||
most cases, these apps will be ran from RAM. If they're ran from ROM, make sure
|
||||
to set `USER_RAMSTART` properly in your `user.h` to ensure that the RAM is
|
||||
placed properly.
|
||||
|
||||
Applications that are ran as a shell (the "shell" app, of course, but also,
|
||||
possibly, "basic" and others to come) need a manual override to their main
|
||||
`RAMSTART` constant: You don't want them to run in the same RAM region as your
|
||||
other userspace apps because if you do, as soon as you launch an app with your
|
||||
shell, its memory is going to be overwritten!
|
||||
|
||||
What you'll do then is that you'll reserve some space in your memory layout for
|
||||
the shell and add a special constant in your `user.h`, which will override the
|
||||
basic one (remember, in zasm, the first `.equ` for a given constant takes
|
||||
precedence).
|
||||
|
||||
For example, if you want a "basic" shell and that you reserve space right
|
||||
after your kernel RAM for it, then your `user.h` would contain
|
||||
`.equ BAS_RAMSTART KERNEL_RAMEND`.
|
||||
|
||||
You can also include your shell's code directly in the kernel by copying
|
||||
relevant parts of the app's glue unit in your kernel's glue unit. This is often
|
||||
simpler and more efficient. However, if your shell is a big program, it might
|
||||
run into zasm's limits. In that case, you'd have to assemble your shell
|
||||
separately.
|
||||
|
||||
## Common features
|
||||
|
||||
The folder `lib/` contains code shared in more than one apps and this has the
|
||||
effect that some concepts are exactly the same in many application. They are
|
||||
therefore sharing documentation, here.
|
||||
|
||||
### Number literals
|
||||
|
||||
There are decimal, hexadecimal and binary literals. A "straight" number is
|
||||
parsed as a decimal. Hexadecimal literals must be prefixed with `0x` (`0xf4`).
|
||||
Binary must be prefixed with `0b` (`0b01100110`).
|
||||
|
||||
Decimals and hexadecimal are "flexible". Whether they're written in a byte or
|
||||
a word, you don't need to prefix them with zeroes. Watch out for overflow,
|
||||
however.
|
||||
|
||||
Binary literals are also "flexible" (`0b110` is fine), but can't go over a byte.
|
||||
|
||||
There is also the char literal (`'X'`), that is, two quotes with a character in
|
||||
the middle. The value of that character is interpreted as-is, without any
|
||||
encoding involved. That is, whatever binary code is written in between those
|
||||
two quotes, it's what is evaluated. Only a single byte at once can be evaluated
|
||||
thus. There is no escaping. `'''` results in `0x27`. You can't express a newline
|
||||
this way, it's going to mess with the parser.
|
||||
|
||||
### Expressions
|
||||
|
||||
An expression is a bunch of literals or symbols assembled by operators.
|
||||
Supported operators are `+`, `-`, `*`, `/`, `%` (modulo), `&` (bitwise and),
|
||||
`|` (bitwise or), `^` (bitwise xor), `{` (shift left), `}` (shift right).
|
||||
Bitwise operator always operate on the whole 16-bits.
|
||||
|
||||
Shift operators break from the `<<` and `>>` tradition because the complexity
|
||||
if two-sized operator is significant and deemed not worth it. The shift
|
||||
operator shift the left operand X times, X being the right operand.
|
||||
|
||||
There is no parenthesis support yet.
|
||||
|
||||
Symbols have a different meaning depending on the application. In zasm, it's
|
||||
labels and constants. In basic, it's variables.
|
||||
|
||||
Expressions can't contain spaces.
|
||||
|
||||
Expressions can have an empty left operand. It will then be considered as 0.
|
||||
This allows signed integers, for example, `-42` to be expressed as expected.
|
||||
That form doesn't work well everywhere and is mostly supported for BASIC. In
|
||||
zasm, you're safer with `0-42`.
|
@ -1,26 +0,0 @@
|
||||
; at28w - Write to AT28 EEPROM
|
||||
;
|
||||
; Write data from the active block device into an eeprom device geared as
|
||||
; regular memory. Implements write polling to know when the next byte can be
|
||||
; written and verifies that data is written properly.
|
||||
;
|
||||
; Optionally receives a word argument that specifies the number or bytes to
|
||||
; write. If unspecified, will write until max bytes (0x2000) is reached or EOF
|
||||
; is reached on the block device.
|
||||
|
||||
; *** Requirements ***
|
||||
; blkGetB
|
||||
;
|
||||
; *** Includes ***
|
||||
|
||||
.inc "user.h"
|
||||
.inc "err.h"
|
||||
.equ AT28W_RAMSTART USER_RAMSTART
|
||||
|
||||
jp at28wMain
|
||||
|
||||
.inc "core.asm"
|
||||
.inc "lib/util.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "at28w/main.asm"
|
||||
USER_RAMSTART:
|
@ -1,78 +0,0 @@
|
||||
; *** Consts ***
|
||||
; Memory address where the AT28 is configured to start
|
||||
.equ AT28W_MEMSTART 0x2000
|
||||
|
||||
; Value mismatch during validation
|
||||
.equ AT28W_ERR_MISMATCH 0x10
|
||||
|
||||
; *** Variables ***
|
||||
.equ AT28W_MAXBYTES AT28W_RAMSTART
|
||||
.equ AT28W_RAMEND @+2
|
||||
; *** Code ***
|
||||
|
||||
at28wMain:
|
||||
ld de, AT28W_MAXBYTES
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr z, at28wInner ; no arg
|
||||
call parseHexadecimal ; --> DE
|
||||
jr z, at28wInner
|
||||
; bad args
|
||||
ld a, SHELL_ERR_BAD_ARGS
|
||||
ret
|
||||
|
||||
at28wInner:
|
||||
; Reminder: words in parseArgs aren't little endian. High byte is first.
|
||||
ld a, (AT28W_MAXBYTES)
|
||||
ld b, a
|
||||
ld a, (AT28W_MAXBYTES+1)
|
||||
ld c, a
|
||||
ld hl, AT28W_MEMSTART
|
||||
call at28wBCZero
|
||||
jr nz, .loop
|
||||
; BC is zero, default to 0x2000 (8k, the size of the AT28)
|
||||
ld bc, 0x2000
|
||||
.loop:
|
||||
call blkGetB
|
||||
jr nz, .loopend
|
||||
ld (hl), a
|
||||
ld e, a ; save expected data for verification
|
||||
; initiate polling
|
||||
ld a, (hl)
|
||||
ld d, a
|
||||
.wait:
|
||||
; as long as writing operation is running, IO/6 will toggle at each
|
||||
; read attempt. We know that write is finished when we read the same
|
||||
; value twice.
|
||||
ld a, (hl)
|
||||
cp d
|
||||
jr z, .waitend
|
||||
ld d, a
|
||||
jr .wait
|
||||
.waitend:
|
||||
|
||||
; same value was read twice. A contains our final value for this memory
|
||||
; address. Let's compare with what we're written.
|
||||
cp e
|
||||
jr nz, .mismatch
|
||||
inc hl
|
||||
dec bc
|
||||
call at28wBCZero
|
||||
jr nz, .loop
|
||||
|
||||
.loopend:
|
||||
; We're finished. Success!
|
||||
xor a
|
||||
ret
|
||||
|
||||
.mismatch:
|
||||
ld a, AT28W_ERR_MISMATCH
|
||||
ret
|
||||
|
||||
at28wBCZero:
|
||||
xor a
|
||||
cp b
|
||||
ret nz
|
||||
cp c
|
||||
ret
|
||||
|
@ -1,282 +0,0 @@
|
||||
# basic
|
||||
|
||||
This is a BASIC interpreter which has been written from scratch for Collapse OS.
|
||||
There are many existing z80 implementations around, some of them open source
|
||||
and most of them good and efficient, but because a lot of that code overlaps
|
||||
with code that has already been written for zasm, I believe that it's better to
|
||||
reuse those bits of code.
|
||||
|
||||
## Design goal
|
||||
|
||||
The reason for including a BASIC dialect in Collapse OS is to supply some form
|
||||
of system administration swiss knife. zasm, ed and the shell can do
|
||||
theoretically anything, but some tasks (which are difficult to predict) can
|
||||
possibly be overly tedious. One can think, for example, about hardware
|
||||
debugging. Poking and peeking around when not sure what we're looking for can
|
||||
be a lot more effective with the help of variables, conditions and for-loops in
|
||||
an interpreter.
|
||||
|
||||
Because the goal is not to provide a foundation for complex programs, I'm
|
||||
planning on intentionally crippling this BASIC dialect for the sake of
|
||||
simplicity.
|
||||
|
||||
The idea here is that the system administrator would build herself many little
|
||||
tools in assembler and BASIC would be the interactive glue to those tools.
|
||||
|
||||
If you find yourself writing complex programs in Collapse OS BASIC, you're on a
|
||||
wrong path. Back off, that program should be in assembler.
|
||||
|
||||
## Glueing
|
||||
|
||||
The `glue.asm` file in this folder represents the minimal basic system. There
|
||||
are additional modules that can be added that aren't added by default, such
|
||||
as `fs.asm` because they require kernel options that might not be available.
|
||||
|
||||
To include these modules, you'll need to write your own glue file and to hook
|
||||
extra commands through `BAS_FINDHOOK`. Look for examples in `tools/emul` and
|
||||
in recipes.
|
||||
|
||||
## Usage
|
||||
|
||||
Upon launch, a prompt is presented, waiting for a command. There are two types
|
||||
of command invocation: direct and numbered.
|
||||
|
||||
A direct command is executed immediately. Example: `print 42` will print `42`
|
||||
immediately.
|
||||
|
||||
A numbered command is added to BASIC's code listing at the specified line
|
||||
number. For example, `10 print 42` will set line 10 to the string `print 42`.
|
||||
|
||||
Code listing can be printed with `list` and can be ran with `run`. The listing
|
||||
is kept in order of lines. Line number don't need to be sequential. You can
|
||||
keep leeway in between your lines and then insert a line with a middle number
|
||||
later.
|
||||
|
||||
Some commands take arguments. Those are given by typing a whitespace after the
|
||||
command name and then the argument. Additional arguments are given the same way,
|
||||
by typing a whitespace.
|
||||
|
||||
### Numbers, expressions and variables
|
||||
|
||||
Numbers are stored in memory as 16-bit integers (little endian) and numbers
|
||||
being represented by BASIC are expressed as signed integers, in decimal form.
|
||||
Line numbers, however, are expressed and treated as unsigned integers: You can,
|
||||
if you want, put something on line "-1", but it will be the equivalent of line
|
||||
65535. When expressing number literals, you can do so either in multiple forms.
|
||||
See "Number literals" in `apps/README.md` for details.
|
||||
|
||||
Expressions are accepted wherever a number is expected. For example,
|
||||
`print 2+3` will print `5`. See "Expressions" in `apps/README.md`.
|
||||
|
||||
Inside a `if` command, "truth" expressions are accepted (`=`, `<`, `>`, `<=`,
|
||||
`>=`). A thruth expression that doesn't contain a truth operator evaluates the
|
||||
number as-is: zero if false, nonzero is true.
|
||||
|
||||
There are 26 one-letter variables in BASIC which can be assigned a 16-bit
|
||||
integer to them. You assign a value to a variable with `=`. For example,
|
||||
`a=42+4` will assign 46 to `a` (case insensitive). Those variables can then
|
||||
be used in expressions. For example, `print a-6` will print `40`. All variables
|
||||
are initialized to zero on launch.
|
||||
|
||||
### Arguments
|
||||
|
||||
Some commands take arguments and there are some common patterns regarding them.
|
||||
|
||||
One of them is that all commands that "return" something (`input`, `peek`,
|
||||
etc.) always to so in variable `A`.
|
||||
|
||||
Another is that whenever a number is expected, expressions, including the ones
|
||||
with variables in it, work fine.
|
||||
|
||||
### One-liners
|
||||
|
||||
The `:` character, when not inside a `""` literal, allows you to cram more than
|
||||
one instruction on the same line.
|
||||
|
||||
Things are special with `if`. All commands following a `if` are bound to that
|
||||
`if`'s condition. `if 0 foo:bar` doesn't execute `bar`.
|
||||
|
||||
Another special thing is `goto`. A `goto` followed by `:` will have the commands
|
||||
following the `:` before the goto occurs.
|
||||
|
||||
### Commands
|
||||
|
||||
There are two types of commands: normal and direct-only. The latter can only
|
||||
be invoked in direct mode, not through a code listing.
|
||||
|
||||
`list`: Direct-only. Prints all lines in the code listing, prefixing them
|
||||
with their associated line number.
|
||||
|
||||
`run`: Direct-only. Runs code from the listing, starting with the first one.
|
||||
If `goto` was previously called in direct mode, we start from that line instead.
|
||||
|
||||
`clear`: Direct-only. Clears the current code listing.
|
||||
|
||||
`print <what> [<what>]`: Prints the result of the specified expression,
|
||||
then CR/LF. Can be given multiple arguments. In that case, all arguments are
|
||||
printed separately with a space in between. For example, `print 12 13` prints
|
||||
`12 13<cr><lf>`
|
||||
|
||||
Unlike anywhere else, the `print` command can take a string inside a double
|
||||
quote. That string will be printed as-is. For example, `print "foo" 40+2` will
|
||||
print `foo 42`.
|
||||
|
||||
`goto <lineno>`: Make the next line to be executed the line number
|
||||
specified as an argument. Errors out if line doesn't exist. Argument can be
|
||||
an expression. If invoked in direct mode, `run` must be called to actually
|
||||
run the line (followed by the next, and so on).
|
||||
|
||||
`if <cond> <cmds>`: If specified condition is true, execute the rest of the
|
||||
line. Otherwise, do nothing. For example, `if 2>1 print 12` prints `12` and `if
|
||||
2<1 print 12` does nothing. The argument for this command is a "thruth
|
||||
expression".
|
||||
|
||||
`while <cond> <cmds>`: As long as specified condition is true, execute specified
|
||||
commands repeatedly.
|
||||
|
||||
`input [<prompt>]`: Prompts the user for a numerical value and puts that
|
||||
value in `A`. The prompted value is evaluated as an expression and then stored.
|
||||
The command takes an optional string literal parameter. If present, that string
|
||||
will be printed before asking for input. Unlike a `print` call, there is no
|
||||
CR/LF after that print.
|
||||
|
||||
`peek/deek <addr>`: Put the value at specified memory address into `A`. peek is for
|
||||
a single byte, deek is for a word (little endian). For example, `peek 42` puts
|
||||
the byte value contained in memory address 0x002a into variable `A`. `deek 42`
|
||||
does the same as peek, but also puts the value of 0x002b into `A`'s MSB.
|
||||
|
||||
`poke/doke <addr> <val>`: Put the value of specified expression into
|
||||
specified memory address. For example, `poke 42 0x102+0x40` puts `0x42` in
|
||||
memory address 0x2a (MSB is ignored) and `doke 42 0x102+0x40` does the same
|
||||
as poke, but also puts `0x01` in memory address 0x2b.
|
||||
|
||||
`in <port>`: Same thing as `peek`, but for a I/O port. `in 42` generates an
|
||||
input I/O on port 42 and stores the byte result in `A`.
|
||||
|
||||
`out <port> <val>`: Same thing as `poke`, but for a I/O port. `out 42 1+2`
|
||||
generates an output I/O on port 42 with value 3.
|
||||
|
||||
`getc`: Waits for a single character to be typed in the console and then puts
|
||||
that value in `A`.
|
||||
|
||||
`putc <char>`: Puts the specified character to the console.
|
||||
|
||||
`puth <char>`: Puts the specified character to the console, encoded in two
|
||||
hexadecimal digits. For example, `puth 0x42` yields `42`. This is useful for
|
||||
spitting binary contents to a console that has special handling of certain
|
||||
control characters.
|
||||
|
||||
`sleep <units>`: Sleep a number of "units" specified by the supplied
|
||||
expression. A "unit" depends on the CPU clock speed. At 4MHz, it is roughly 8
|
||||
microseconds.
|
||||
|
||||
`addr <what>`: This very handy returns (in `A`), the address you query for.
|
||||
You can query for two types of things: commands or special stuff.
|
||||
|
||||
If you query for a command, type the name of the command as an argument. The
|
||||
address of the associated routine will be returned.
|
||||
|
||||
Then, there's the *special stuff*. This is the list of things you can query for:
|
||||
|
||||
* `$`: the scratchpad.
|
||||
|
||||
`usr <addr>`: This calls the memory address specified as an expression
|
||||
argument. Before doing so, it sets the registers according to a specific
|
||||
logic: Variable `A`'s LSB goes in register `A`, variable `D` goes in register
|
||||
`DE`, `H` in `HL` `B` in `BC` and `X` in `IX`. `IY` can't be used because
|
||||
it's used for the jump. Then, after the call, the value of the registers are
|
||||
put back into the variables following the same logic.
|
||||
|
||||
Let's say, for example, that you want to use the kernel's `printstr` to print
|
||||
the contents of the scratchpad. First, you would call `addr $` to put the
|
||||
address of the scratchpad in `A`, then do `h=a` to have that address in `HL`
|
||||
and, if printstr is, for example, the 21st entry in your jump table, you'd do
|
||||
`usr 21*3` and see the scratchpad printed!
|
||||
|
||||
## Optional modules
|
||||
|
||||
As explained in "glueing" section abolve, this folder contains optional modules.
|
||||
Here's the documentation for them.
|
||||
|
||||
### blk
|
||||
|
||||
Block devices commands. Block devices are configured during kernel
|
||||
initialization and are referred to by numbers.
|
||||
|
||||
`bsel <blkid>`: Select the active block device. The active block device is
|
||||
the target of all commands below. You select it by specifying its number. For
|
||||
example, `bsel 0` selects the first configured device. `bsel 1` selects the
|
||||
second.
|
||||
|
||||
A freshly selected blkdev begins with its "pointer" at 0.
|
||||
|
||||
`bseek <lsw> <msw>`: Moves the blkdev "pointer" to the specified offset. The
|
||||
first argument is the offset's least significant half (blkdev supports 32-bit
|
||||
addressing). Is is interpreted as an unsigned integer.
|
||||
|
||||
The second argument is optional and is the most significant half of the address.
|
||||
It defaults to 0.
|
||||
|
||||
`getb`: Read a byte in active blkdev at current pointer, then advance the
|
||||
pointer by one. Read byte goes in `A`.
|
||||
|
||||
`putb <val>`: Writes a byte in active blkdev at current pointer, then
|
||||
advance the pointer by one. The value of the byte is determined by the
|
||||
expression supplied as an argument. Example: `putb 42`.
|
||||
|
||||
### fs
|
||||
|
||||
`fs.asm` provides those commands:
|
||||
|
||||
`fls`: prints the list of files contained in the active filesystem.
|
||||
|
||||
`fopen <fhandle> <fname>`: Open file "fname" in handle "fhandle". File handles
|
||||
are specified in kernel glue code and are in limited number. The kernel glue
|
||||
code also maps to blkids through the glue code. So to know what you're doing
|
||||
here, you have to look at your glue code.
|
||||
|
||||
In the emulated code, there are two file handles. Handle 0 maps to blkid 1 and
|
||||
handle 1 maps to blkid 2.
|
||||
|
||||
Once a file is opened, you can use the mapped blkid as you would with any block
|
||||
device (bseek, getb, putb).
|
||||
|
||||
`fnew <blkcnt> <fname>`: Allocates space of "blkcnt" blocks (each block is
|
||||
0x100 bytes in size) for a new file names "fname". Maximum blkcnt is 0xff.
|
||||
|
||||
`fdel <fname>`: Mark file named "fname" as deleted.
|
||||
|
||||
`ldbas <fname>`: loads the content of the file specified in the argument
|
||||
(as an unquoted filename) and replace the current code listing with this
|
||||
contents. Any line not starting with a number is ignored (not an error).
|
||||
|
||||
`basPgmHook`: That is not a command, but a routine to hook into
|
||||
`BAS_FINDHOOK`. If you do, whenever a command name isn't found, the filesystem
|
||||
is iterated to see if it finds a file with the same name. If it does, it loads
|
||||
its contents at `USER_CODE` (from `user.h`) and calls that address, with HL
|
||||
pointing to the the remaining args in the command line.
|
||||
|
||||
The user code called this way follows the *usr* convention for output, that is,
|
||||
it converts all registers at the end of the call and stores them in appropriate
|
||||
variables. If `A` is nonzero, an error is considered to have occurred.
|
||||
|
||||
It doesn't do var-to-register transfers on input, however. Only HL is passed
|
||||
through (with the contents of the command line).
|
||||
|
||||
### sdc
|
||||
|
||||
`sdc.asm` provides SD card related commands:
|
||||
|
||||
`sdci`: initializes a SD card for operation. This should be ran whenever you
|
||||
insert a new SD card.
|
||||
|
||||
`sdcf`: flushes current buffers to the SD card. This is done automatically, but
|
||||
only on a "needs to flush" basis, that is, when dirty buffers need to be
|
||||
swapped. This command ensures that all buffers are clean (not dirty).
|
||||
|
||||
### floppy
|
||||
|
||||
`floppy.asm` provides TRS-80 floppy related commands:
|
||||
|
||||
`flush`: Like `sdcf` above, but for floppies. Additionally, it invalidates all
|
||||
buffers, allowing you to swap disks and then read proper contents.
|
@ -1,49 +0,0 @@
|
||||
basBSEL:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix \ pop hl
|
||||
call blkSelPtr
|
||||
ld a, l
|
||||
jp blkSel
|
||||
|
||||
basBSEEK:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix ; --> lvl 1
|
||||
call rdExpr
|
||||
push ix \ pop de
|
||||
pop hl ; <-- lvl 1
|
||||
jr z, .skip
|
||||
; DE not supplied, set to zero
|
||||
ld de, 0
|
||||
.skip:
|
||||
xor a ; absolute mode
|
||||
call blkSeek
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
basGETB:
|
||||
call blkGetB
|
||||
ret nz
|
||||
ld (VAR_TBL), a
|
||||
xor a
|
||||
ld (VAR_TBL+1), a
|
||||
ret
|
||||
|
||||
basPUTB:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix \ pop hl
|
||||
ld a, l
|
||||
jp blkPutB
|
||||
|
||||
basBLKCmds:
|
||||
.db "bsel", 0
|
||||
.dw basBSEL
|
||||
.db "bseek", 0
|
||||
.dw basBSEEK
|
||||
.db "getb", 0
|
||||
.dw basGETB
|
||||
.db "putb", 0
|
||||
.dw basPUTB
|
||||
.db 0xff ; end of table
|
@ -1,182 +0,0 @@
|
||||
; *** Consts ***
|
||||
; maximum number of lines (line number maximum, however, is always 0xffff)
|
||||
.equ BUF_MAXLINES 0x100
|
||||
; Size of the string pool
|
||||
.equ BUF_POOLSIZE 0x1000
|
||||
|
||||
; *** Variables ***
|
||||
; A pointer to the first free line
|
||||
.equ BUF_LFREE BUF_RAMSTART
|
||||
; A pointer to the first free byte in the pool
|
||||
.equ BUF_PFREE @+2
|
||||
; The line index. Each record consists of 4 bytes: 2 for line number,
|
||||
; 2 for pointer to string in pool. Kept in order of line numbers.
|
||||
.equ BUF_LINES @+2
|
||||
; The line pool. A list of null terminated strings. BUF_LINES records point
|
||||
; to those strings.
|
||||
.equ BUF_POOL @+BUF_MAXLINES*4
|
||||
.equ BUF_RAMEND @+BUF_POOLSIZE
|
||||
|
||||
bufInit:
|
||||
ld hl, BUF_LINES
|
||||
ld (BUF_LFREE), hl
|
||||
ld hl, BUF_POOL
|
||||
ld (BUF_PFREE), hl
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Add line at (HL) with line number DE to the buffer. The string at (HL) should
|
||||
; not contain the line number prefix or the whitespace between the line number
|
||||
; and the comment.
|
||||
; Note that an empty string is *not* an error. It will be saved as a line.
|
||||
; Z for success.
|
||||
; Error conditions are:
|
||||
; * not enough space in the pool
|
||||
; * not enough space in the line index
|
||||
bufAdd:
|
||||
; Check whether we have enough pool space. This is done in all cases.
|
||||
call strlen
|
||||
inc a ; strlen doesn't include line termination
|
||||
exx ; preserve HL and DE
|
||||
ld hl, (BUF_PFREE)
|
||||
call addHL
|
||||
ld de, BUF_RAMEND
|
||||
sbc hl, de
|
||||
exx ; restore
|
||||
; no carry? HL >= BUF_RAMEND, error. Z already unset
|
||||
ret nc
|
||||
|
||||
; Check the kind of operation we make: add, insert or replace?
|
||||
call bufFind
|
||||
jr z, .replace ; exact match, replace
|
||||
call c, .insert ; near match, insert
|
||||
|
||||
; do we have enough index space?
|
||||
exx ; preserve HL and DE
|
||||
ld hl, (BUF_LFREE)
|
||||
ld de, BUF_POOL-4
|
||||
or a ; reset carry
|
||||
sbc hl, de
|
||||
exx ; restore
|
||||
; no carry? HL >= BUF_POOL, error. Z already unset
|
||||
ret nc
|
||||
|
||||
; We have enough space.
|
||||
; set line index data
|
||||
push de ; --> lvl 1
|
||||
ld (ix), e
|
||||
ld (ix+1), d
|
||||
ld de, (BUF_PFREE)
|
||||
ld (ix+2), e
|
||||
ld (ix+3), d
|
||||
|
||||
; Increase line index size
|
||||
ld de, (BUF_LFREE)
|
||||
inc de \ inc de \ inc de \ inc de
|
||||
ld (BUF_LFREE), de
|
||||
|
||||
; Fourth step: copy string to pool
|
||||
ld de, (BUF_PFREE)
|
||||
call strcpyM
|
||||
ld (BUF_PFREE), de
|
||||
pop de ; <-- lvl 1
|
||||
ret
|
||||
|
||||
; No need to add a new line, just replace the current one.
|
||||
.replace:
|
||||
ld (ix), e
|
||||
ld (ix+1), d
|
||||
push de
|
||||
ld de, (BUF_PFREE)
|
||||
ld (ix+2), e
|
||||
ld (ix+3), d
|
||||
call strcpyM
|
||||
ld (BUF_PFREE), de
|
||||
pop de
|
||||
ret
|
||||
|
||||
; An insert is exactly like an add, except that lines following insertion point
|
||||
; first.
|
||||
.insert:
|
||||
push hl
|
||||
push de
|
||||
push bc
|
||||
; We want a LDDR that moves from (BUF_LFREE)-1 to (BUF_LFREE)+3
|
||||
; for a count of (BUF_LFREE)-BUF_LINES
|
||||
ld hl, (BUF_LFREE)
|
||||
ld de, BUF_LINES
|
||||
or a ; clear carry
|
||||
sbc hl, de
|
||||
ld b, h
|
||||
ld c, l
|
||||
ld hl, (BUF_LFREE)
|
||||
ld d, h
|
||||
ld e, l
|
||||
dec hl
|
||||
inc de \ inc de \ inc de
|
||||
lddr
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Set IX to point to the beginning of the pool.
|
||||
; Z set if (IX) is a valid line, unset if the pool is empty.
|
||||
bufFirst:
|
||||
ld ix, BUF_LINES
|
||||
jp bufEOF
|
||||
|
||||
; Given a valid line record in IX, move IX to the next line.
|
||||
; This routine doesn't check that IX is valid. Ensure IX validity before
|
||||
; calling.
|
||||
bufNext:
|
||||
inc ix \ inc ix \ inc ix \ inc ix
|
||||
jp bufEOF
|
||||
|
||||
; Returns whether line index at IX is past the end of file, that is,
|
||||
; whether IX == (BUF_LFREE)
|
||||
; Z is set when not EOF, unset when EOF.
|
||||
bufEOF:
|
||||
push hl
|
||||
push de
|
||||
push ix \ pop hl
|
||||
or a ; clear carry
|
||||
ld de, (BUF_LFREE)
|
||||
sbc hl, de
|
||||
jr z, .empty
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
.empty:
|
||||
call unsetZ
|
||||
jr .end
|
||||
|
||||
; Given a line index in (IX), set HL to its associated string pointer.
|
||||
bufStr:
|
||||
ld l, (ix+2)
|
||||
ld h, (ix+3)
|
||||
ret
|
||||
|
||||
; Browse lines looking for number DE. Set IX to point to one of these :
|
||||
; 1 - an exact match
|
||||
; 2 - the first found line to have a higher line number
|
||||
; 3 - EOF
|
||||
; Set Z on an exact match, C on a near match, NZ and NC on EOF.
|
||||
bufFind:
|
||||
call bufFirst
|
||||
ret nz
|
||||
.loop:
|
||||
ld a, d
|
||||
cp (ix+1)
|
||||
ret c ; D < (IX+1), situation 2
|
||||
jr nz, .next
|
||||
ld a, e
|
||||
cp (ix)
|
||||
ret c ; E < (IX), situation 2
|
||||
ret z ; exact match!
|
||||
.next:
|
||||
call bufNext
|
||||
ret nz
|
||||
jr .loop
|
@ -1,10 +0,0 @@
|
||||
; floppy-related basic commands
|
||||
|
||||
basFLUSH:
|
||||
jp floppyFlush
|
||||
|
||||
basFloppyCmds:
|
||||
.db "flush", 0
|
||||
.dw basFLUSH
|
||||
.db 0xff ; end of table
|
||||
|
@ -1,140 +0,0 @@
|
||||
; FS-related basic commands
|
||||
; *** Variables ***
|
||||
; Handle of the target file
|
||||
.equ BFS_FILE_HDL BFS_RAMSTART
|
||||
.equ BFS_RAMEND @+FS_HANDLE_SIZE
|
||||
|
||||
; Lists filenames in currently active FS
|
||||
basFLS:
|
||||
ld iy, .iter
|
||||
jp fsIter
|
||||
.iter:
|
||||
ld a, FS_META_FNAME_OFFSET
|
||||
call addHL
|
||||
call printstr
|
||||
jp printcrlf
|
||||
|
||||
|
||||
basLDBAS:
|
||||
call fsFindFN
|
||||
ret nz
|
||||
call bufInit
|
||||
ld ix, BFS_FILE_HDL
|
||||
call fsOpen
|
||||
ld hl, 0
|
||||
ld de, SCRATCHPAD
|
||||
.loop:
|
||||
ld ix, BFS_FILE_HDL
|
||||
call fsGetB
|
||||
jr nz, .loopend
|
||||
inc hl
|
||||
or a ; null? hum, weird. same as LF
|
||||
jr z, .lineend
|
||||
cp LF
|
||||
jr z, .lineend
|
||||
ld (de), a
|
||||
inc de
|
||||
jr .loop
|
||||
.lineend:
|
||||
; We've just finished reading a line, writing each char in the pad.
|
||||
; Null terminate it.
|
||||
xor a
|
||||
ld (de), a
|
||||
; Ok, line ready
|
||||
push hl ; --> lvl 1. current file position
|
||||
ld hl, SCRATCHPAD
|
||||
call parseDecimalC
|
||||
jr nz, .notANumber
|
||||
call rdSep
|
||||
call bufAdd
|
||||
pop hl ; <-- lvl 1
|
||||
ret nz
|
||||
ld de, SCRATCHPAD
|
||||
jr .loop
|
||||
.notANumber:
|
||||
pop hl ; <-- lvl 1
|
||||
ld de, SCRATCHPAD
|
||||
jr .loop
|
||||
.loopend:
|
||||
cp a
|
||||
ret
|
||||
|
||||
|
||||
basFOPEN:
|
||||
call rdExpr ; file handle index
|
||||
ret nz
|
||||
push ix \ pop de
|
||||
ld a, e
|
||||
call fsHandle
|
||||
; DE now points to file handle
|
||||
call rdSep
|
||||
; HL now holds the string we look for
|
||||
call fsFindFN
|
||||
ret nz ; not found
|
||||
; Found!
|
||||
; FS_PTR points to the file we want to open
|
||||
push de \ pop ix ; IX now points to the file handle.
|
||||
jp fsOpen
|
||||
|
||||
; Takes one byte block number to allocate as well we one string arg filename
|
||||
; and allocates a new file in the current fs.
|
||||
basFNEW:
|
||||
call rdExpr ; file block count
|
||||
ret nz
|
||||
call rdSep ; HL now points to filename
|
||||
push ix \ pop de
|
||||
ld a, e
|
||||
jp fsAlloc
|
||||
|
||||
; Deletes filename with specified name
|
||||
basFDEL:
|
||||
call fsFindFN
|
||||
ret nz
|
||||
; Found! delete
|
||||
jp fsDel
|
||||
|
||||
|
||||
basPgmHook:
|
||||
; Cmd to find is in (DE)
|
||||
ex de, hl
|
||||
; (HL) is suitable for a direct fsFindFN call
|
||||
call fsFindFN
|
||||
ret nz
|
||||
; We have a file! Let's load it in memory
|
||||
ld ix, BFS_FILE_HDL
|
||||
call fsOpen
|
||||
ld hl, 0 ; addr that we read in file handle
|
||||
ld de, USER_CODE ; addr in mem we write to
|
||||
.loop:
|
||||
call fsGetB ; we use Z at end of loop
|
||||
ld (de), a ; Z preserved
|
||||
inc hl ; Z preserved in 16-bit
|
||||
inc de ; Z preserved in 16-bit
|
||||
jr z, .loop
|
||||
; Ready to jump. Return .call in IX and basCallCmd will take care
|
||||
; of setting (HL) to the arg string. .call then takes care of wrapping
|
||||
; the USER_CODE call.
|
||||
ld ix, .call
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.call:
|
||||
ld iy, USER_CODE
|
||||
call callIY
|
||||
call basR2Var
|
||||
or a ; Z set only if A is zero
|
||||
ret
|
||||
|
||||
basFSCmds:
|
||||
.db "fls", 0
|
||||
.dw basFLS
|
||||
.db "ldbas", 0
|
||||
.dw basLDBAS
|
||||
.db "fopen", 0
|
||||
.dw basFOPEN
|
||||
.db "fnew", 0
|
||||
.dw basFNEW
|
||||
.db "fdel", 0
|
||||
.dw basFDEL
|
||||
.db "fson", 0
|
||||
.dw fsOn
|
||||
.db 0xff ; end of table
|
@ -1,33 +0,0 @@
|
||||
; *** Requirements ***
|
||||
; printstr
|
||||
; printcrlf
|
||||
; stdioReadLine
|
||||
; strncmp
|
||||
;
|
||||
.inc "user.h"
|
||||
.inc "err.h"
|
||||
|
||||
call basInit
|
||||
jp basStart
|
||||
|
||||
; RAM space used in different routines for short term processing.
|
||||
.equ SCRATCHPAD_SIZE 0x20
|
||||
.equ SCRATCHPAD USER_RAMSTART
|
||||
|
||||
.inc "core.asm"
|
||||
.inc "lib/util.asm"
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "lib/fmt.asm"
|
||||
.equ EXPR_PARSE parseLiteralOrVar
|
||||
.inc "lib/expr.asm"
|
||||
.inc "basic/util.asm"
|
||||
.inc "basic/parse.asm"
|
||||
.inc "basic/tok.asm"
|
||||
.equ VAR_RAMSTART SCRATCHPAD+SCRATCHPAD_SIZE
|
||||
.inc "basic/var.asm"
|
||||
.equ BUF_RAMSTART VAR_RAMEND
|
||||
.inc "basic/buf.asm"
|
||||
.equ BAS_RAMSTART BUF_RAMEND
|
||||
.inc "basic/main.asm"
|
||||
USER_RAMSTART:
|
@ -1,531 +0,0 @@
|
||||
; *** 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 next line to run. If nonzero, it means that the next line is
|
||||
; the first of the list. This is used by GOTO to indicate where to jump next.
|
||||
; Important note: this is **not** a line number, it's a pointer to a line index
|
||||
; in buffer. If it's not zero, its a valid pointer.
|
||||
.equ BAS_PNEXTLN @+2
|
||||
; Points to a routine to call when a command isn't found in the "core" cmd
|
||||
; table. This gives the opportunity to glue code to configure extra commands.
|
||||
.equ BAS_FINDHOOK @+2
|
||||
.equ BAS_RAMEND @+2
|
||||
|
||||
; *** Code ***
|
||||
basInit:
|
||||
ld (BAS_INITSP), sp
|
||||
call varInit
|
||||
call bufInit
|
||||
xor a
|
||||
ld (BAS_PNEXTLN), a
|
||||
ld (BAS_PNEXTLN+1), a
|
||||
ld hl, unsetZ
|
||||
ld (BAS_FINDHOOK), hl
|
||||
ret
|
||||
|
||||
basStart:
|
||||
ld hl, .welcome
|
||||
call printstr
|
||||
call printcrlf
|
||||
jr basLoop
|
||||
|
||||
.welcome:
|
||||
.db "Collapse OS", 0
|
||||
|
||||
basLoop:
|
||||
ld hl, .sPrompt
|
||||
call printstr
|
||||
call stdioReadLine
|
||||
call printcrlf
|
||||
call parseDecimalC
|
||||
jr z, .number
|
||||
ld de, basCmds1
|
||||
call basCallCmds
|
||||
jr z, basLoop
|
||||
; Error
|
||||
call basERR
|
||||
jr basLoop
|
||||
.number:
|
||||
call rdSep
|
||||
call bufAdd
|
||||
jp nz, basERR
|
||||
jr basLoop
|
||||
.sPrompt:
|
||||
.db "> ", 0
|
||||
|
||||
; Tries to find command specified in (DE) (must be null-terminated) in cmd
|
||||
; table in (HL). If found, sets IX to point to the associated routine. If
|
||||
; not found, calls BAS_FINDHOOK so that we look through extra commands
|
||||
; configured by glue code.
|
||||
; Destroys HL.
|
||||
; Z is set if found, unset otherwise.
|
||||
basFindCmd:
|
||||
.loop:
|
||||
call strcmp
|
||||
call strskip
|
||||
inc hl ; point to routine
|
||||
jr z, .found ; Z from strcmp
|
||||
inc hl \ inc hl ; skip routine
|
||||
ld a, (hl)
|
||||
inc a ; was it 0xff?
|
||||
jr nz, .loop ; no
|
||||
dec a ; unset Z
|
||||
ret
|
||||
.found:
|
||||
call intoHL
|
||||
push hl \ pop ix
|
||||
ret
|
||||
|
||||
; Call command in (HL) after having looked for it in cmd table in (DE).
|
||||
; If found, jump to it. If not found, try (BAS_FINDHOOK). If still not found,
|
||||
; unset Z. We expect commands to set Z on success. Therefore, when calling
|
||||
; basCallCmd results in NZ, we're not sure where the error come from, but
|
||||
; well...
|
||||
basCallCmd:
|
||||
; let's see if it's a variable assignment.
|
||||
call varTryAssign
|
||||
ret z ; Done!
|
||||
push de ; --> lvl 1.
|
||||
ld de, SCRATCHPAD
|
||||
call rdWord
|
||||
; cmdname to find in (DE)
|
||||
; How lucky, we have a legitimate use of "ex (sp), hl"! We have the
|
||||
; cmd table in the stack, which we want in HL and we have the rest of
|
||||
; the cmdline in (HL), which we want in the stack!
|
||||
ex (sp), hl
|
||||
call basFindCmd
|
||||
jr z, .skip
|
||||
; not found, try BAS_FINDHOOK
|
||||
ld ix, (BAS_FINDHOOK)
|
||||
call callIX
|
||||
.skip:
|
||||
; regardless of the result, we need to balance the stack.
|
||||
; Bring back rest of the command string from the stack
|
||||
pop hl ; <-- lvl 1
|
||||
ret nz
|
||||
; cmd found, skip whitespace and then jump!
|
||||
call rdSep
|
||||
jp (ix)
|
||||
|
||||
; Call a series of ':'-separated commands in (HL) using cmd table in (DE).
|
||||
; Stop processing as soon as one command unsets Z.
|
||||
basCallCmds:
|
||||
; Commands are not guaranteed at all to preserve HL and DE, so we
|
||||
; preserve them ourselves here.
|
||||
push hl ; --> lvl 1
|
||||
push de ; --> lvl 2
|
||||
call basCallCmd
|
||||
pop de ; <-- lvl 2
|
||||
pop hl ; <-- lvl 1
|
||||
ret nz
|
||||
call toEnd
|
||||
ret z ; no more cmds
|
||||
; we met a ':', we have more cmds
|
||||
inc hl
|
||||
call basCallCmds
|
||||
; move the the end of the string so that we don't run cmds following a
|
||||
; ':' twice.
|
||||
call strskip
|
||||
ret
|
||||
|
||||
basERR:
|
||||
ld hl, .sErr
|
||||
call printstr
|
||||
jp printcrlf
|
||||
.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.
|
||||
;
|
||||
; Commands are expected to set Z on success.
|
||||
basLIST:
|
||||
call bufFirst
|
||||
jr nz, .end
|
||||
.loop:
|
||||
ld e, (ix)
|
||||
ld d, (ix+1)
|
||||
ld hl, SCRATCHPAD
|
||||
call fmtDecimal
|
||||
call printstr
|
||||
ld a, ' '
|
||||
call stdioPutC
|
||||
call bufStr
|
||||
call printstr
|
||||
call printcrlf
|
||||
call bufNext
|
||||
jr z, .loop
|
||||
.end:
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
|
||||
basRUN:
|
||||
call .maybeGOTO
|
||||
jr nz, .loop ; IX already set
|
||||
call bufFirst
|
||||
ret nz
|
||||
.loop:
|
||||
call bufStr
|
||||
ld de, basCmds2
|
||||
push ix ; --> lvl 1
|
||||
call basCallCmds
|
||||
pop ix ; <-- lvl 1
|
||||
jp nz, .err
|
||||
call .maybeGOTO
|
||||
jr nz, .loop ; IX already set
|
||||
call bufNext
|
||||
jr z, .loop
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.err:
|
||||
; Print line number, then return NZ (which will print ERR)
|
||||
ld e, (ix)
|
||||
ld d, (ix+1)
|
||||
ld hl, SCRATCHPAD
|
||||
call fmtDecimal
|
||||
call printstr
|
||||
ld a, ' '
|
||||
call stdioPutC
|
||||
jp unsetZ
|
||||
|
||||
; This returns the opposite Z result as the one we usually see: Z is set if
|
||||
; we **don't** goto, unset if we do. If we do, IX is properly set.
|
||||
.maybeGOTO:
|
||||
ld de, (BAS_PNEXTLN)
|
||||
ld a, d
|
||||
or e
|
||||
ret z
|
||||
; we goto
|
||||
push de \ pop ix
|
||||
; we need to reset our goto marker
|
||||
ld de, 0
|
||||
ld (BAS_PNEXTLN), de
|
||||
ret
|
||||
|
||||
basPRINT:
|
||||
; Do we have arguments at all? if not, it's not an error, just print
|
||||
; crlf
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr z, .end
|
||||
; Is our arg a string literal?
|
||||
call spitQuoted
|
||||
jr z, .chkAnother ; string printed, skip to chkAnother
|
||||
ld de, SCRATCHPAD
|
||||
call rdWord
|
||||
push hl ; --> lvl 1
|
||||
ex de, hl
|
||||
call parseExpr
|
||||
jr nz, .parseError
|
||||
ld hl, SCRATCHPAD
|
||||
call fmtDecimalS
|
||||
call printstr
|
||||
pop hl ; <-- lvl 1
|
||||
.chkAnother:
|
||||
; Do we have another arg?
|
||||
call rdSep
|
||||
jr z, .another
|
||||
; no, we can stop here
|
||||
.end:
|
||||
cp a ; ensure Z
|
||||
jp printcrlf
|
||||
.another:
|
||||
; Before we jump to basPRINT, let's print a space
|
||||
ld a, ' '
|
||||
call stdioPutC
|
||||
jr basPRINT
|
||||
.parseError:
|
||||
; unwind the stack before returning
|
||||
pop hl ; <-- lvl 1
|
||||
ret
|
||||
|
||||
|
||||
basGOTO:
|
||||
ld de, SCRATCHPAD
|
||||
call rdWord
|
||||
ex de, hl
|
||||
call parseExpr
|
||||
ret nz
|
||||
call bufFind
|
||||
jr nz, .notFound
|
||||
push ix \ pop de
|
||||
; Z already set
|
||||
jr .end
|
||||
.notFound:
|
||||
ld de, 0
|
||||
; Z already unset
|
||||
.end:
|
||||
ld (BAS_PNEXTLN), de
|
||||
ret
|
||||
|
||||
; evaluate truth condition at (HL) and set A to its value
|
||||
; Z for success (but not truth!)
|
||||
_basEvalCond:
|
||||
push hl ; --> lvl 1. original arg
|
||||
ld de, SCRATCHPAD
|
||||
call rdWord
|
||||
ex de, hl
|
||||
call parseTruth
|
||||
pop hl ; <-- lvl 1. restore
|
||||
ret
|
||||
|
||||
basIF:
|
||||
call _basEvalCond
|
||||
ret nz ; error
|
||||
or a
|
||||
ret z
|
||||
; expr is true, execute next
|
||||
; (HL) back to beginning of args, skip to next arg
|
||||
call toSepOrEnd
|
||||
call rdSep
|
||||
ret nz
|
||||
ld de, basCmds2
|
||||
jp basCallCmds
|
||||
|
||||
basWHILE:
|
||||
push hl ; --> lvl 1
|
||||
call _basEvalCond
|
||||
jr nz, .stop ; error
|
||||
or a
|
||||
jr z, .stop
|
||||
ret z
|
||||
; expr is true, execute next
|
||||
; (HL) back to beginning of args, skip to next arg
|
||||
call toSepOrEnd
|
||||
call rdSep
|
||||
ret nz
|
||||
ld de, basCmds2
|
||||
call basCallCmds
|
||||
pop hl ; <-- lvl 1
|
||||
jr basWHILE
|
||||
.stop:
|
||||
pop hl ; <-- lvl 1
|
||||
ret
|
||||
|
||||
basINPUT:
|
||||
; If our first arg is a string literal, spit it
|
||||
call spitQuoted
|
||||
call rdSep
|
||||
call stdioReadLine
|
||||
call parseExpr
|
||||
ld (VAR_TBL), de
|
||||
call printcrlf
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
basPEEK:
|
||||
call basDEEK
|
||||
ret nz
|
||||
; set MSB to 0
|
||||
xor a ; sets Z
|
||||
ld (VAR_TBL+1), a
|
||||
ret
|
||||
|
||||
basPOKE:
|
||||
call rdExpr
|
||||
ret nz
|
||||
; peek address in IX. Save it for later
|
||||
push ix ; --> lvl 1
|
||||
call rdSep
|
||||
call rdExpr
|
||||
push ix \ pop hl
|
||||
pop ix ; <-- lvl 1
|
||||
ret nz
|
||||
; Poke!
|
||||
ld (ix), l
|
||||
ret
|
||||
|
||||
basDEEK:
|
||||
call rdExpr
|
||||
ret nz
|
||||
; peek address in IX. Let's peek and put result in DE
|
||||
ld e, (ix)
|
||||
ld d, (ix+1)
|
||||
ld (VAR_TBL), de
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
basDOKE:
|
||||
call basPOKE
|
||||
ld (ix+1), h
|
||||
ret
|
||||
|
||||
basOUT:
|
||||
call rdExpr
|
||||
ret nz
|
||||
; out address in IX. Save it for later
|
||||
push ix ; --> lvl 1
|
||||
call rdSep
|
||||
call rdExpr
|
||||
push ix \ pop hl
|
||||
pop bc ; <-- lvl 1
|
||||
ret nz
|
||||
; Out!
|
||||
out (c), l
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
basIN:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix \ pop bc
|
||||
ld d, 0
|
||||
in e, (c)
|
||||
ld (VAR_TBL), de
|
||||
; Z set from rdExpr
|
||||
ret
|
||||
|
||||
basGETC:
|
||||
call stdioGetC
|
||||
ld (VAR_TBL), a
|
||||
xor a
|
||||
ld (VAR_TBL+1), a
|
||||
ret
|
||||
|
||||
basPUTC:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix \ pop hl
|
||||
ld a, l
|
||||
call stdioPutC
|
||||
xor a ; set Z
|
||||
ret
|
||||
|
||||
basPUTH:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix \ pop hl
|
||||
ld a, l
|
||||
call printHex
|
||||
xor a ; set Z
|
||||
ret
|
||||
|
||||
basSLEEP:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix \ pop hl
|
||||
.loop:
|
||||
ld a, h ; 4T
|
||||
or l ; 4T
|
||||
ret z ; 5T
|
||||
dec hl ; 6T
|
||||
jr .loop ; 12T
|
||||
|
||||
basADDR:
|
||||
call rdWord
|
||||
ex de, hl
|
||||
ld de, .specialTbl
|
||||
.loop:
|
||||
ld a, (de)
|
||||
or a
|
||||
jr z, .notSpecial
|
||||
cp (hl)
|
||||
jr z, .found
|
||||
inc de \ inc de \ inc de
|
||||
jr .loop
|
||||
.notSpecial:
|
||||
; not found, find cmd. needle in (HL)
|
||||
ex de, hl ; now in (DE)
|
||||
ld hl, basCmds1
|
||||
call basFindCmd
|
||||
jr z, .foundCmd
|
||||
; no core command? let's try the find hook.
|
||||
ld ix, (BAS_FINDHOOK)
|
||||
call callIX
|
||||
ret nz
|
||||
.foundCmd:
|
||||
; We have routine addr in IX
|
||||
ld (VAR_TBL), ix
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.found:
|
||||
; found special thing. Put in "A".
|
||||
inc de
|
||||
call intoDE
|
||||
ld (VAR_TBL), de
|
||||
ret ; Z set from .found jump.
|
||||
|
||||
.specialTbl:
|
||||
.db '$'
|
||||
.dw SCRATCHPAD
|
||||
.db 0
|
||||
|
||||
basUSR:
|
||||
call rdExpr
|
||||
ret nz
|
||||
push ix \ pop iy
|
||||
; We have our address to call. Now, let's set up our registers.
|
||||
; HL comes from variable H. H's index is 7*2.
|
||||
ld hl, (VAR_TBL+14)
|
||||
; DE comes from variable D. D's index is 3*2
|
||||
ld de, (VAR_TBL+6)
|
||||
; BC comes from variable B. B's index is 1*2
|
||||
ld bc, (VAR_TBL+2)
|
||||
; IX comes from variable X. X's index is 23*2
|
||||
ld ix, (VAR_TBL+46)
|
||||
; and finally, A
|
||||
ld a, (VAR_TBL)
|
||||
call callIY
|
||||
basR2Var: ; Just send reg to vars. Used in basPgmHook
|
||||
; Same dance, opposite way
|
||||
ld (VAR_TBL), a
|
||||
ld (VAR_TBL+46), ix
|
||||
ld (VAR_TBL+2), bc
|
||||
ld (VAR_TBL+6), de
|
||||
ld (VAR_TBL+14), hl
|
||||
cp a ; USR never errors out
|
||||
ret
|
||||
|
||||
; Command table format: Null-terminated string followed by a 2-byte routine
|
||||
; pointer.
|
||||
|
||||
; direct only
|
||||
basCmds1:
|
||||
.db "list", 0
|
||||
.dw basLIST
|
||||
.db "run", 0
|
||||
.dw basRUN
|
||||
.db "clear", 0
|
||||
.dw bufInit
|
||||
; statements
|
||||
basCmds2:
|
||||
.db "print", 0
|
||||
.dw basPRINT
|
||||
.db "goto", 0
|
||||
.dw basGOTO
|
||||
.db "if", 0
|
||||
.dw basIF
|
||||
.db "while", 0
|
||||
.dw basWHILE
|
||||
.db "input", 0
|
||||
.dw basINPUT
|
||||
.db "peek", 0
|
||||
.dw basPEEK
|
||||
.db "poke", 0
|
||||
.dw basPOKE
|
||||
.db "deek", 0
|
||||
.dw basDEEK
|
||||
.db "doke", 0
|
||||
.dw basDOKE
|
||||
.db "out", 0
|
||||
.dw basOUT
|
||||
.db "in", 0
|
||||
.dw basIN
|
||||
.db "getc", 0
|
||||
.dw basGETC
|
||||
.db "putc", 0
|
||||
.dw basPUTC
|
||||
.db "puth", 0
|
||||
.dw basPUTH
|
||||
.db "sleep", 0
|
||||
.dw basSLEEP
|
||||
.db "addr", 0
|
||||
.dw basADDR
|
||||
.db "usr", 0
|
||||
.dw basUSR
|
||||
.db 0xff ; end of table
|
@ -1,142 +0,0 @@
|
||||
; Parse an expression yielding a truth value from (HL) and set A accordingly.
|
||||
; 0 for False, nonzero for True.
|
||||
; How it evaluates truth is that it looks for =, <, >, >= or <= in (HL) and,
|
||||
; if it finds it, evaluate left and right expressions separately. Then it
|
||||
; compares both sides and set A accordingly.
|
||||
; If comparison operators aren't found, the whole string is sent to parseExpr
|
||||
; and zero means False, nonzero means True.
|
||||
; **This routine mutates (HL).**
|
||||
; Z for success.
|
||||
parseTruth:
|
||||
push ix
|
||||
push de
|
||||
ld a, '='
|
||||
call .maybeFind
|
||||
jr z, .foundEQ
|
||||
ld a, '<'
|
||||
call .maybeFind
|
||||
jr z, .foundLT
|
||||
ld a, '>'
|
||||
call .maybeFind
|
||||
jr z, .foundGT
|
||||
jr .simple
|
||||
.success:
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop de
|
||||
pop ix
|
||||
ret
|
||||
|
||||
.maybeFind:
|
||||
push hl ; --> lvl 1
|
||||
call findchar
|
||||
jr nz, .notFound
|
||||
; found! We want to keep new HL around. Let's pop old HL in DE
|
||||
pop de ; <-- lvl 1
|
||||
ret
|
||||
.notFound:
|
||||
; not found, restore HL
|
||||
pop hl ; <-- lvl 1
|
||||
ret
|
||||
|
||||
.simple:
|
||||
call parseExpr
|
||||
jr nz, .end
|
||||
ld a, d
|
||||
or e
|
||||
jr .success
|
||||
|
||||
.foundEQ:
|
||||
; we found an '=' char and HL is pointing to it. DE is pointing to the
|
||||
; beginning of our string. Let's separate those two strings.
|
||||
; But before we do that, to we have a '<' or a '>' at the left of (HL)?
|
||||
dec hl
|
||||
ld a, (hl)
|
||||
cp '<'
|
||||
jr z, .foundLTE
|
||||
cp '>'
|
||||
jr z, .foundGTE
|
||||
inc hl
|
||||
; Ok, we are a straight '='. Proceed.
|
||||
call .splitLR
|
||||
; HL now point to right-hand, DE to left-hand
|
||||
call .parseLeftRight
|
||||
jr nz, .end ; error, stop
|
||||
xor a ; clear carry and prepare value for False
|
||||
sbc hl, de
|
||||
jr nz, .success ; NZ? equality not met. A already 0, return.
|
||||
; Z? equality met, make A=1, set Z
|
||||
inc a
|
||||
jr .success
|
||||
|
||||
.foundLTE:
|
||||
; Almost the same as '<', but we have two sep chars
|
||||
call .splitLR
|
||||
inc hl ; skip the '=' char
|
||||
call .parseLeftRight
|
||||
jr nz, .end
|
||||
ld a, 1 ; prepare for True
|
||||
sbc hl, de
|
||||
jr nc, .success ; Left <= Right, True
|
||||
; Left > Right, False
|
||||
dec a
|
||||
jr .success
|
||||
|
||||
.foundGTE:
|
||||
; Almost the same as '<='
|
||||
call .splitLR
|
||||
inc hl ; skip the '=' char
|
||||
call .parseLeftRight
|
||||
jr nz, .end
|
||||
ld a, 1 ; prepare for True
|
||||
sbc hl, de
|
||||
jr z, .success ; Left == Right, True
|
||||
jr c, .success ; Left > Right, True
|
||||
; Left < Right, False
|
||||
dec a
|
||||
jr .success
|
||||
|
||||
.foundLT:
|
||||
; Same thing as EQ, but for '<'
|
||||
call .splitLR
|
||||
call .parseLeftRight
|
||||
jr nz, .end
|
||||
xor a
|
||||
sbc hl, de
|
||||
jr z, .success ; Left == Right, False
|
||||
jr c, .success ; Left > Right, False
|
||||
; Left < Right, True
|
||||
inc a
|
||||
jr .success
|
||||
|
||||
.foundGT:
|
||||
; Same thing as EQ, but for '>'
|
||||
call .splitLR
|
||||
call .parseLeftRight
|
||||
jr nz, .end
|
||||
xor a
|
||||
sbc hl, de
|
||||
jr nc, .success ; Left <= Right, False
|
||||
; Left > Right, True
|
||||
inc a
|
||||
jr .success
|
||||
|
||||
.splitLR:
|
||||
xor a
|
||||
ld (hl), a
|
||||
inc hl
|
||||
ret
|
||||
|
||||
; Given string pointers in (HL) and (DE), evaluate those two expressions and
|
||||
; place their corresponding values in HL and DE.
|
||||
.parseLeftRight:
|
||||
; let's start with HL
|
||||
push de ; --> lvl 1
|
||||
call parseExpr
|
||||
pop hl ; <-- lvl 1, orig DE
|
||||
ret nz
|
||||
push de ; --> lvl 1. save HL value in stack.
|
||||
; Now, for DE. (DE) is now in HL
|
||||
call parseExpr ; DE in place
|
||||
pop hl ; <-- lvl 1. restore saved HL
|
||||
ret
|
@ -1,14 +0,0 @@
|
||||
; SDC-related basic commands
|
||||
|
||||
basSDCI:
|
||||
jp sdcInitializeCmd
|
||||
|
||||
basSDCF:
|
||||
jp sdcFlushCmd
|
||||
|
||||
basSDCCmds:
|
||||
.db "sdci", 0
|
||||
.dw basSDCI
|
||||
.db "sdcf", 0
|
||||
.dw basSDCF
|
||||
.db 0xff ; end of table
|
@ -1,97 +0,0 @@
|
||||
; Whether A is a separator or end-of-string (null or ':')
|
||||
isSepOrEnd:
|
||||
or a
|
||||
ret z
|
||||
cp ':'
|
||||
ret z
|
||||
; continue to isSep
|
||||
|
||||
; Sets Z is A is ' ' or '\t' (whitespace)
|
||||
isSep:
|
||||
cp ' '
|
||||
ret z
|
||||
cp 0x09
|
||||
ret
|
||||
|
||||
; 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.
|
||||
rdSep:
|
||||
ld a, (hl)
|
||||
call isSep
|
||||
ret nz ; failure
|
||||
.loop:
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
call isSep
|
||||
jr z, .loop
|
||||
call isSepOrEnd
|
||||
jp z, .fail ; unexpected EOL. fail
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.fail:
|
||||
; A is zero at this point
|
||||
inc a ; unset Z
|
||||
ret
|
||||
|
||||
; Advance HL to the next separator or to the end of string.
|
||||
toSepOrEnd:
|
||||
ld a, (hl)
|
||||
call isSepOrEnd
|
||||
ret z
|
||||
inc hl
|
||||
jr toSepOrEnd
|
||||
|
||||
; Advance HL to the end of the line, that is, either a null terminating char
|
||||
; or the ':'.
|
||||
; Sets Z if we met a null char, unset if we met a ':'
|
||||
toEnd:
|
||||
ld a, (hl)
|
||||
or a
|
||||
ret z
|
||||
cp ':'
|
||||
jr z, .havesep
|
||||
inc hl
|
||||
call skipQuoted
|
||||
jr toEnd
|
||||
.havesep:
|
||||
inc a ; unset Z
|
||||
ret
|
||||
|
||||
; Read (HL) until the next separator and copy it in (DE)
|
||||
; DE is preserved, but HL is advanced to the end of the read word.
|
||||
rdWord:
|
||||
push af
|
||||
push de
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
call isSepOrEnd
|
||||
jr z, .stop
|
||||
ld (de), a
|
||||
inc hl
|
||||
inc de
|
||||
jr .loop
|
||||
.stop:
|
||||
xor a
|
||||
ld (de), a
|
||||
pop de
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Read word from HL in SCRATCHPAD and then intepret that word as an expression.
|
||||
; Put the result in IX.
|
||||
; Z for success.
|
||||
; TODO: put result in DE
|
||||
rdExpr:
|
||||
ld de, SCRATCHPAD
|
||||
call rdWord
|
||||
push hl
|
||||
ex de, hl
|
||||
call parseExpr
|
||||
push de \ pop ix
|
||||
pop hl
|
||||
ret
|
@ -1,32 +0,0 @@
|
||||
; Is (HL) a double-quoted string? If yes, spit what's inside and place (HL)
|
||||
; at char after the closing quote.
|
||||
; Set Z if there was a string, unset otherwise.
|
||||
spitQuoted:
|
||||
ld a, (hl)
|
||||
cp '"'
|
||||
ret nz
|
||||
inc hl
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
inc hl
|
||||
cp '"'
|
||||
ret z
|
||||
or a
|
||||
ret z
|
||||
call stdioPutC
|
||||
jr .loop
|
||||
|
||||
; Same as spitQuoted, but without the spitting
|
||||
skipQuoted:
|
||||
ld a, (hl)
|
||||
cp '"'
|
||||
ret nz
|
||||
inc hl
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
inc hl
|
||||
cp '"'
|
||||
ret z
|
||||
or a
|
||||
ret z
|
||||
jr .loop
|
@ -1,104 +0,0 @@
|
||||
; *** Variables ***
|
||||
; A list of words for each member of the A-Z range.
|
||||
.equ VAR_TBL VAR_RAMSTART
|
||||
.equ VAR_RAMEND @+52
|
||||
|
||||
; *** Code ***
|
||||
|
||||
varInit:
|
||||
ld b, VAR_RAMEND-VAR_RAMSTART
|
||||
ld hl, VAR_RAMSTART
|
||||
xor a
|
||||
.loop:
|
||||
ld (hl), a
|
||||
inc hl
|
||||
djnz .loop
|
||||
ret
|
||||
|
||||
; Check if A is a valid variable letter (a-z or A-Z). If it is, set A to a
|
||||
; valid VAR_TBL index and set Z. Otherwise, unset Z (and A is destroyed)
|
||||
varChk:
|
||||
call upcase
|
||||
sub 'A'
|
||||
ret c ; Z unset
|
||||
cp 27 ; 'Z' + 1
|
||||
jr c, .isVar
|
||||
; A > 'Z'
|
||||
dec a ; unset Z
|
||||
ret
|
||||
.isVar:
|
||||
cp a ; set Z
|
||||
ret
|
||||
|
||||
; Try to interpret line at (HL) and see if it's a variable assignment. If it
|
||||
; is, proceed with the assignment and set Z. Otherwise, NZ.
|
||||
varTryAssign:
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
dec hl
|
||||
cp '='
|
||||
ret nz
|
||||
ld a, (hl)
|
||||
call varChk
|
||||
ret nz
|
||||
; We have a variable! Its table index is currently in A.
|
||||
push ix ; --> lvl 1
|
||||
push hl ; --> lvl 2
|
||||
push de ; --> lvl 3
|
||||
push af ; --> lvl 4. save for later
|
||||
; Let's put that expression to read in scratchpad
|
||||
inc hl \ inc hl
|
||||
ld de, SCRATCHPAD
|
||||
call rdWord
|
||||
ex de, hl
|
||||
; Now, evaluate that expression now in (HL)
|
||||
call parseExpr ; --> number in DE
|
||||
jr nz, .exprErr
|
||||
pop af ; <-- lvl 4
|
||||
call varAssign
|
||||
xor a ; ensure Z
|
||||
.end:
|
||||
pop de ; <-- lvl 3
|
||||
pop hl ; <-- lvl 2
|
||||
pop ix ; <-- lvl 1
|
||||
ret
|
||||
.exprErr:
|
||||
pop af ; <-- lvl 4
|
||||
jr .end
|
||||
|
||||
; Given a variable **index** in A (call varChk to transform) and a value in
|
||||
; DE, assign that value in the proper cell in VAR_TBL.
|
||||
; No checks are made.
|
||||
varAssign:
|
||||
push hl
|
||||
add a, a ; * 2 because each element is a word
|
||||
ld hl, VAR_TBL
|
||||
call addHL
|
||||
; HL placed, write number
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Check if value at (HL) is a variable. If yes, returns its associated value.
|
||||
; Otherwise, jump to parseLiteral.
|
||||
parseLiteralOrVar:
|
||||
call isLiteralPrefix
|
||||
jp z, parseLiteral
|
||||
; not a literal, try var
|
||||
ld a, (hl)
|
||||
call varChk
|
||||
ret nz
|
||||
; It's a variable, resolve!
|
||||
add a, a ; * 2 because each element is a word
|
||||
push hl ; --> lvl 1
|
||||
ld hl, VAR_TBL
|
||||
call addHL
|
||||
ld e, (hl)
|
||||
inc hl
|
||||
ld d, (hl)
|
||||
pop hl ; <-- lvl 1
|
||||
inc hl ; point to char after variable
|
||||
cp a ; ensure Z
|
||||
ret
|
@ -1,69 +0,0 @@
|
||||
# ed - line editor
|
||||
|
||||
Collapse OS's `ed` is modeled after UNIX's ed (let's call it `Ued`). The goal
|
||||
is to have an editor that is tight on resources and that doesn't require
|
||||
ncurses-like screen management.
|
||||
|
||||
In general, we try to follow `Ued`'s conventions and the "Usage" section is
|
||||
mostly a repeat of `Ued`'s man page.
|
||||
|
||||
## Differences
|
||||
|
||||
There are a couple of differences with `Ued` that are intentional. Differences
|
||||
not listed here are either bugs or simply aren't implemented yet.
|
||||
|
||||
* Always has a prompt, `:`.
|
||||
* No size printing on load
|
||||
* Initial line is the first one
|
||||
* Line input is for one line at once. Less scriptable for `Ued`, but we can't
|
||||
script `ed` in Collapse OS anyway...
|
||||
* For the sake of code simplicity, some commands that make no sense are
|
||||
accepted. For example, `1,2a` is the same as `2a`.
|
||||
|
||||
## Usage
|
||||
|
||||
`ed` is invoked from the shell with a single argument: the name of the file to
|
||||
edit. If the file doesn't exist, `ed` errors out. If it exists, a prompt is
|
||||
shown.
|
||||
|
||||
In normal mode, `ed` waits for a command and executes it. If the command is
|
||||
invalid, a line with `?` is printed and `ed` goes back to waiting for a command.
|
||||
|
||||
A command can be invalid because it is unknown, malformed or if its address
|
||||
range is out of bounds.
|
||||
|
||||
### Commands
|
||||
|
||||
* `(addrs)p`: Print lines specified in `addrs` range. This is the default
|
||||
command. If only `(addrs)` is specified, it has the same effect.
|
||||
* `(addrs)d`: Delete lines specified in `addrs` range.
|
||||
* `(addr)a`: Appends a line after `addr`.
|
||||
* `(addr)i`: Insert a line before `addr`.
|
||||
* `w`: write to file. For now, `q` is implied in `w`.
|
||||
* `q`: quit `ed` without writing to file.
|
||||
|
||||
### Current line
|
||||
|
||||
The current line is central to `ed`. Address ranges can be expressed relatively
|
||||
to it and makes the app much more usable. The current line starts at `1` and
|
||||
every command changes the current line to the last line that the command
|
||||
affects. For example, `42p` changes the current line to `42`, `3,7d`, to 7.
|
||||
|
||||
### Addresses
|
||||
|
||||
An "address" is a line number. The first line is `1`. An address range is a
|
||||
start line and a stop line, expressed as `start,stop`. For example, `2,4` refer
|
||||
to lines 2, 3 and 4.
|
||||
|
||||
When expressing ranges, `stop` can be omitted. It will then have the same value
|
||||
as `start`. `42` is equivalent to `42,42`.
|
||||
|
||||
Addresses can be expressed relatively to the current line with `+` and `-`.
|
||||
`+3` means "current line + 3", `-5, +2` means "address range starting at 5
|
||||
lines before current line and ending 2 lines after it`.
|
||||
|
||||
`+` alone means `+1`, `-` means `-1`.
|
||||
|
||||
`.` means current line. It can usually be omitted. `p` is the same as `.p`.
|
||||
|
||||
`$` means the last line of the buffer.
|
211
apps/ed/buf.asm
211
apps/ed/buf.asm
@ -1,211 +0,0 @@
|
||||
; buf - manage line buffer
|
||||
;
|
||||
; *** Variables ***
|
||||
; Number of lines currently in the buffer
|
||||
.equ BUF_LINECNT BUF_RAMSTART
|
||||
; List of pointers to strings in scratchpad
|
||||
.equ BUF_LINES @+2
|
||||
; Points to the end of the scratchpad, that is, one byte after the last written
|
||||
; char in it.
|
||||
.equ BUF_PADEND @+ED_BUF_MAXLINES*2
|
||||
; The in-memory scratchpad
|
||||
.equ BUF_PAD @+2
|
||||
|
||||
.equ BUF_RAMEND @+ED_BUF_PADMAXLEN
|
||||
|
||||
; *** Code ***
|
||||
|
||||
; On initialization, we read the whole contents of target blkdev and add lines
|
||||
; as we go.
|
||||
bufInit:
|
||||
ld hl, BUF_PAD ; running pointer to end of pad
|
||||
ld de, BUF_PAD ; points to beginning of current line
|
||||
ld ix, BUF_LINES ; points to current line index
|
||||
ld bc, 0 ; line count
|
||||
; init pad end in case we have an empty file.
|
||||
ld (BUF_PADEND), hl
|
||||
.loop:
|
||||
call ioGetB
|
||||
jr nz, .loopend
|
||||
or a ; null? hum, weird. same as LF
|
||||
jr z, .lineend
|
||||
cp 0x0a
|
||||
jr z, .lineend
|
||||
ld (hl), a
|
||||
inc hl
|
||||
jr .loop
|
||||
.lineend:
|
||||
; We've just finished reading a line, writing each char in the pad.
|
||||
; Null terminate it.
|
||||
xor a
|
||||
ld (hl), a
|
||||
inc hl
|
||||
; Now, let's register its pointer in BUF_LINES
|
||||
ld (ix), e
|
||||
inc ix
|
||||
ld (ix), d
|
||||
inc ix
|
||||
inc bc
|
||||
ld (BUF_PADEND), hl
|
||||
ld de, (BUF_PADEND)
|
||||
jr .loop
|
||||
.loopend:
|
||||
ld (BUF_LINECNT), bc
|
||||
ret
|
||||
|
||||
; transform line index HL into its corresponding memory address in BUF_LINES
|
||||
; array.
|
||||
bufLineAddr:
|
||||
push de
|
||||
ex de, hl
|
||||
ld hl, BUF_LINES
|
||||
add hl, de
|
||||
add hl, de ; twice, because two bytes per line
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Read line number specified in HL and make HL point to its contents.
|
||||
; Sets Z on success, unset if out of bounds.
|
||||
bufGetLine:
|
||||
push de ; --> lvl 1
|
||||
ld de, (BUF_LINECNT)
|
||||
call cpHLDE
|
||||
pop de ; <-- lvl 1
|
||||
jp nc, unsetZ ; HL > (BUF_LINECNT)
|
||||
call bufLineAddr
|
||||
; HL now points to an item in BUF_LINES.
|
||||
call intoHL
|
||||
; Now, HL points to our contents
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Given line indexes in HL and DE where HL < DE < CNT, move all lines between
|
||||
; DE and CNT by an offset of DE-HL. Also, adjust BUF_LINECNT by DE-HL.
|
||||
; WARNING: no bounds check. The only consumer of this routine already does
|
||||
; bounds check.
|
||||
bufDelLines:
|
||||
; Let's start with setting up BC, which is (CNT-DE) * 2
|
||||
push hl ; --> lvl 1
|
||||
ld hl, (BUF_LINECNT)
|
||||
scf \ ccf
|
||||
sbc hl, de
|
||||
; mult by 2 and we're done
|
||||
sla l \ rl h
|
||||
push hl \ pop bc
|
||||
pop hl ; <-- lvl 1
|
||||
; Good! BC done. Now, let's adjust BUF_LINECNT by DE-HL
|
||||
push hl ; --> lvl 1
|
||||
scf \ ccf
|
||||
sbc hl, de ; HL -> nb of lines to delete, negative
|
||||
push de ; --> lvl 2
|
||||
ld de, (BUF_LINECNT)
|
||||
add hl, de ; adding DE to negative HL
|
||||
ld (BUF_LINECNT), hl
|
||||
pop de ; <-- lvl 2
|
||||
pop hl ; <-- lvl 1
|
||||
; Line count updated!
|
||||
; One other thing... is BC zero? Because if it is, then we shouldn't
|
||||
; call ldir (otherwise we're on for a veeeery long loop), BC=0 means
|
||||
; that only last lines were deleted. nothing to do.
|
||||
ld a, b
|
||||
or c
|
||||
ret z ; BC==0, return
|
||||
|
||||
; let's have invert HL and DE to match LDIR's signature
|
||||
ex de, hl
|
||||
; At this point we have higher index in HL, lower index in DE and number
|
||||
; of bytes to delete in BC. It's convenient because it's rather close
|
||||
; to LDIR's signature! The only thing we need to do now is to translate
|
||||
; those HL and DE indexes in memory addresses, that is, multiply by 2
|
||||
; and add BUF_LINES
|
||||
push hl ; --> lvl 1
|
||||
ex de, hl
|
||||
call bufLineAddr
|
||||
ex de, hl
|
||||
pop hl ; <-- lvl 1
|
||||
call bufLineAddr
|
||||
; Both HL and DE are translated. Go!
|
||||
ldir
|
||||
ret
|
||||
|
||||
; Insert string where DE points to memory scratchpad, then insert that line
|
||||
; at index HL, offsetting all lines by 2 bytes.
|
||||
bufInsertLine:
|
||||
call bufIndexInBounds
|
||||
jr nz, .append
|
||||
push de ; --> lvl 1, scratchpad ptr
|
||||
push hl ; --> lvl 2, insert index
|
||||
; The logic below is mostly copy-pasted from bufDelLines, but with a
|
||||
; LDDR logic (to avoid overwriting). I learned, with some pain involved,
|
||||
; that generalizing this code wasn't working very well. I don't repeat
|
||||
; the comments, refer to bufDelLines
|
||||
ex de, hl ; line index now in DE
|
||||
ld hl, (BUF_LINECNT)
|
||||
scf \ ccf
|
||||
sbc hl, de
|
||||
; mult by 2 and we're done
|
||||
sla l \ rl h
|
||||
push hl \ pop bc
|
||||
; From this point, we don't need our line index in DE any more because
|
||||
; LDDR will start from BUF_LINECNT-1 with count BC. We'll only need it
|
||||
; when it's time to insert the line in the space we make.
|
||||
ld hl, (BUF_LINECNT)
|
||||
call bufLineAddr
|
||||
; HL is pointing to *first byte* after last line. Our source needs to
|
||||
; be the second byte of the last line and our dest is the second byte
|
||||
; after the last line.
|
||||
push hl \ pop de
|
||||
dec hl ; second byte of last line
|
||||
inc de ; second byte beyond last line
|
||||
; HL = BUF_LINECNT-1, DE = BUF_LINECNT, BC is set. We're good!
|
||||
lddr
|
||||
.set:
|
||||
; We still need to increase BUF_LINECNT
|
||||
ld hl, (BUF_LINECNT)
|
||||
inc hl
|
||||
ld (BUF_LINECNT), hl
|
||||
; A space has been opened at line index HL. Let's fill it with our
|
||||
; inserted line.
|
||||
pop hl ; <-- lvl 2, insert index
|
||||
call bufLineAddr
|
||||
pop de ; <-- lvl 1, scratchpad offset
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
ret
|
||||
.append:
|
||||
; nothing to move, just put the line there. Let's piggy-back on the end
|
||||
; of the regular routine by carefully pushing the right register in the
|
||||
; right place.
|
||||
; But before that, make sure that HL isn't too high. The only place we
|
||||
; can append to is at (BUF_LINECNT)
|
||||
ld hl, (BUF_LINECNT)
|
||||
push de ; --> lvl 1
|
||||
push hl ; --> lvl 2
|
||||
jr .set
|
||||
|
||||
; copy string that HL points to to scratchpad and return its pointer in
|
||||
; scratchpad, in HL.
|
||||
bufScratchpadAdd:
|
||||
push de
|
||||
ld de, (BUF_PADEND)
|
||||
push de ; --> lvl 1
|
||||
call strcpyM
|
||||
inc de ; pad end is last char + 1
|
||||
ld (BUF_PADEND), de
|
||||
pop hl ; <-- lvl 1
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Sets Z according to whether the line index in HL is within bounds.
|
||||
bufIndexInBounds:
|
||||
push de
|
||||
ld de, (BUF_LINECNT)
|
||||
call cpHLDE
|
||||
pop de
|
||||
jr c, .withinBounds
|
||||
; out of bounds
|
||||
jp unsetZ
|
||||
.withinBounds:
|
||||
cp a ; ensure Z
|
||||
ret
|
150
apps/ed/cmd.asm
150
apps/ed/cmd.asm
@ -1,150 +0,0 @@
|
||||
; cmd - parse and interpret command
|
||||
;
|
||||
; *** Consts ***
|
||||
|
||||
; address type
|
||||
|
||||
.equ ABSOLUTE 0
|
||||
; handles +, - and ".". For +, easy. For -, addr is negative. For ., it's 0.
|
||||
.equ RELATIVE 1
|
||||
.equ EOF 2
|
||||
|
||||
; *** Variables ***
|
||||
|
||||
; An address is a one byte type and a two bytes line number (0-indexed)
|
||||
.equ CMD_ADDR1 CMD_RAMSTART
|
||||
.equ CMD_ADDR2 @+3
|
||||
.equ CMD_TYPE @+3
|
||||
.equ CMD_RAMEND @+1
|
||||
|
||||
; *** Code ***
|
||||
|
||||
; Parse command line that HL points to and set unit's variables
|
||||
; Sets Z on success, unset on error.
|
||||
cmdParse:
|
||||
ld a, (hl)
|
||||
cp 'q'
|
||||
jr z, .simpleCmd
|
||||
cp 'w'
|
||||
jr z, .simpleCmd
|
||||
ld ix, CMD_ADDR1
|
||||
call .readAddr
|
||||
ret nz
|
||||
; Before we check for the existence of a second addr, let's set that
|
||||
; second addr to the same value as the first. That's going to be its
|
||||
; value if we have to ",".
|
||||
ld a, (ix)
|
||||
ld (CMD_ADDR2), a
|
||||
ld a, (ix+1)
|
||||
ld (CMD_ADDR2+1), a
|
||||
ld a, (ix+2)
|
||||
ld (CMD_ADDR2+2), a
|
||||
ld a, (hl)
|
||||
cp ','
|
||||
jr nz, .noaddr2
|
||||
inc hl
|
||||
ld ix, CMD_ADDR2
|
||||
call .readAddr
|
||||
ret nz
|
||||
.noaddr2:
|
||||
; We expect HL (rest of the cmdline) to be a null char or an accepted
|
||||
; cmd, otherwise it's garbage
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr z, .nullCmd
|
||||
cp 'p'
|
||||
jr z, .okCmd
|
||||
cp 'd'
|
||||
jr z, .okCmd
|
||||
cp 'a'
|
||||
jr z, .okCmd
|
||||
cp 'i'
|
||||
jr z, .okCmd
|
||||
; unsupported cmd
|
||||
ret ; Z unset
|
||||
.nullCmd:
|
||||
ld a, 'p'
|
||||
.okCmd:
|
||||
ld (CMD_TYPE), a
|
||||
ret ; Z already set
|
||||
|
||||
.simpleCmd:
|
||||
; Z already set
|
||||
ld (CMD_TYPE), a
|
||||
ret
|
||||
|
||||
; Parse the string at (HL) and sets its corresponding address in IX, properly
|
||||
; considering implicit values (current address when nothing is specified).
|
||||
; advances HL to the char next to the last parsed char.
|
||||
; It handles "+" and "-" addresses such as "+3", "-2", "+", "-".
|
||||
; Sets Z on success, unset on error. Line out of bounds isn't an error. Only
|
||||
; overflows.
|
||||
.readAddr:
|
||||
ld a, (hl)
|
||||
cp '+'
|
||||
jr z, .plusOrMinus
|
||||
cp '-'
|
||||
jr z, .plusOrMinus
|
||||
cp '.'
|
||||
jr z, .dot
|
||||
cp '$'
|
||||
jr z, .eof
|
||||
|
||||
; inline parseDecimalDigit
|
||||
add a, 0xff-'9'
|
||||
sub 0xff-9
|
||||
|
||||
jr c, .notHandled
|
||||
; straight number
|
||||
ld a, ABSOLUTE
|
||||
ld (ix), a
|
||||
call parseDecimal
|
||||
ret nz
|
||||
dec de ; from 1-based to 0-base
|
||||
jr .end
|
||||
.dot:
|
||||
inc hl ; advance cmd cursor
|
||||
; the rest is the same as .notHandled
|
||||
.notHandled:
|
||||
; something else. It's probably our command. Our addr is therefore "."
|
||||
ld a, RELATIVE
|
||||
ld (ix), a
|
||||
xor a ; sets Z
|
||||
ld (ix+1), a
|
||||
ld (ix+2), a
|
||||
ret
|
||||
.eof:
|
||||
inc hl ; advance cmd cursor
|
||||
ld a, EOF
|
||||
ld (ix), a
|
||||
ret ; Z set during earlier CP
|
||||
.plusOrMinus:
|
||||
push af ; preserve that + or -
|
||||
ld a, RELATIVE
|
||||
ld (ix), a
|
||||
inc hl ; advance cmd cursor
|
||||
ld a, (hl)
|
||||
ld de, 1 ; if .pmNoSuffix
|
||||
|
||||
; inline parseDecimalDigit
|
||||
add a, 0xff-'9'
|
||||
sub 0xff-9
|
||||
|
||||
jr c, .pmNoSuffix
|
||||
call parseDecimal ; --> DE
|
||||
.pmNoSuffix:
|
||||
pop af ; bring back that +/-
|
||||
cp '-'
|
||||
jr nz, .end
|
||||
; we had a "-". Negate DE
|
||||
push hl
|
||||
ld hl, 0
|
||||
sbc hl, de
|
||||
ex de, hl
|
||||
pop hl
|
||||
.end:
|
||||
; we still have to save DE in memory
|
||||
ld (ix+1), e
|
||||
ld (ix+2), d
|
||||
cp a ; ensure Z
|
||||
ret
|
@ -1,43 +0,0 @@
|
||||
; *** Requirements ***
|
||||
; _blkGetB
|
||||
; _blkPutB
|
||||
; _blkSeek
|
||||
; _blkTell
|
||||
; fsFindFN
|
||||
; fsOpen
|
||||
; fsGetB
|
||||
; fsPutB
|
||||
; fsSetSize
|
||||
; printstr
|
||||
; printcrlf
|
||||
; stdioReadLine
|
||||
; stdioPutC
|
||||
;
|
||||
.inc "user.h"
|
||||
|
||||
; *** Overridable consts ***
|
||||
; Maximum number of lines allowed in the buffer.
|
||||
.equ ED_BUF_MAXLINES 0x800
|
||||
; Size of our scratchpad
|
||||
.equ ED_BUF_PADMAXLEN 0x1000
|
||||
|
||||
; ******
|
||||
|
||||
.inc "err.h"
|
||||
.inc "fs.h"
|
||||
.inc "blkdev.h"
|
||||
jp edMain
|
||||
|
||||
.inc "core.asm"
|
||||
.inc "lib/util.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "ed/util.asm"
|
||||
.equ IO_RAMSTART USER_RAMSTART
|
||||
.inc "ed/io.asm"
|
||||
.equ BUF_RAMSTART IO_RAMEND
|
||||
.inc "ed/buf.asm"
|
||||
.equ CMD_RAMSTART BUF_RAMEND
|
||||
.inc "ed/cmd.asm"
|
||||
.equ ED_RAMSTART CMD_RAMEND
|
||||
.inc "ed/main.asm"
|
||||
USER_RAMSTART:
|
@ -1,93 +0,0 @@
|
||||
; io - handle ed's I/O
|
||||
|
||||
; *** Consts ***
|
||||
;
|
||||
; Max length of a line
|
||||
.equ IO_MAXLEN 0x7f
|
||||
|
||||
; *** Variables ***
|
||||
; Handle of the target file
|
||||
.equ IO_FILE_HDL IO_RAMSTART
|
||||
; block device targeting IO_FILE_HDL
|
||||
.equ IO_BLK @+FS_HANDLE_SIZE
|
||||
; Buffer for lines read from I/O.
|
||||
.equ IO_LINE @+BLOCKDEV_SIZE
|
||||
.equ IO_RAMEND @+IO_MAXLEN+1 ; +1 for null
|
||||
; *** Code ***
|
||||
|
||||
; Given a file name in (HL), open that file in (IO_FILE_HDL) and open a blkdev
|
||||
; on it at (IO_BLK).
|
||||
ioInit:
|
||||
call fsFindFN
|
||||
ret nz
|
||||
ld ix, IO_FILE_HDL
|
||||
call fsOpen
|
||||
ld de, IO_BLK
|
||||
ld hl, .blkdev
|
||||
jp blkSet
|
||||
.fsGetB:
|
||||
ld ix, IO_FILE_HDL
|
||||
jp fsGetB
|
||||
.fsPutB:
|
||||
ld ix, IO_FILE_HDL
|
||||
jp fsPutB
|
||||
.blkdev:
|
||||
.dw .fsGetB, .fsPutB
|
||||
|
||||
ioGetB:
|
||||
push ix
|
||||
ld ix, IO_BLK
|
||||
call _blkGetB
|
||||
pop ix
|
||||
ret
|
||||
|
||||
ioPutB:
|
||||
push ix
|
||||
ld ix, IO_BLK
|
||||
call _blkPutB
|
||||
pop ix
|
||||
ret
|
||||
|
||||
ioSeek:
|
||||
push ix
|
||||
ld ix, IO_BLK
|
||||
call _blkSeek
|
||||
pop ix
|
||||
ret
|
||||
|
||||
ioTell:
|
||||
push ix
|
||||
ld ix, IO_BLK
|
||||
call _blkTell
|
||||
pop ix
|
||||
ret
|
||||
|
||||
ioSetSize:
|
||||
push ix
|
||||
ld ix, IO_FILE_HDL
|
||||
call fsSetSize
|
||||
pop ix
|
||||
ret
|
||||
|
||||
; Write string (HL) in current file. Ends line with LF.
|
||||
ioPutLine:
|
||||
push hl
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr z, .loopend ; null, we're finished
|
||||
call ioPutB
|
||||
jr nz, .error
|
||||
inc hl
|
||||
jr .loop
|
||||
.loopend:
|
||||
; Wrote the whole line, write ending LF
|
||||
ld a, 0x0a
|
||||
call ioPutB
|
||||
jr z, .end ; success
|
||||
; continue to error
|
||||
.error:
|
||||
call unsetZ
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
176
apps/ed/main.asm
176
apps/ed/main.asm
@ -1,176 +0,0 @@
|
||||
; ed - line editor
|
||||
;
|
||||
; A text editor modeled after UNIX's ed, but simpler. The goal is to stay tight
|
||||
; on resources and to avoid having to implement screen management code (that is,
|
||||
; develop the machinery to have ncurses-like apps in Collapse OS).
|
||||
;
|
||||
; ed has a mechanism to avoid having to move a lot of memory around at each
|
||||
; edit. Each line is an element in an doubly-linked list and each element point
|
||||
; to an offset in the "scratchpad". The scratchpad starts with the file
|
||||
; contents and every time we change or add a line, that line goes to the end of
|
||||
; the scratch pad and linked lists are reorganized whenever lines are changed.
|
||||
; Contents itself is always appended to the scratchpad.
|
||||
;
|
||||
; That's on a resourceful UNIX system.
|
||||
;
|
||||
; That doubly linked list on the z80 would use 7 bytes per line (prev, next,
|
||||
; offset, len), which is a bit much.
|
||||
;
|
||||
; We sacrifice speed for memory usage by making that linked list into a simple
|
||||
; array of pointers to line contents in scratchpad. This means that we
|
||||
; don't have an easy access to line length and we have to move a lot of memory
|
||||
; around whenever we add or delete lines. Hopefully, "LDIR" will be our friend
|
||||
; here...
|
||||
;
|
||||
; *** Variables ***
|
||||
;
|
||||
.equ ED_CURLINE ED_RAMSTART
|
||||
.equ ED_RAMEND @+2
|
||||
|
||||
edMain:
|
||||
; because ed only takes a single string arg, we can use HL directly
|
||||
call ioInit
|
||||
ret nz
|
||||
; diverge from UNIX: start at first line
|
||||
ld hl, 0
|
||||
ld (ED_CURLINE), hl
|
||||
|
||||
call bufInit
|
||||
|
||||
.mainLoop:
|
||||
ld a, ':'
|
||||
call stdioPutC
|
||||
call stdioReadLine ; --> HL
|
||||
; Now, process line.
|
||||
call printcrlf
|
||||
call cmdParse
|
||||
jp nz, .error
|
||||
ld a, (CMD_TYPE)
|
||||
cp 'q'
|
||||
jr z, .doQ
|
||||
cp 'w'
|
||||
jr z, .doW
|
||||
; The rest of the commands need an address
|
||||
call edReadAddrs
|
||||
jr nz, .error
|
||||
ld a, (CMD_TYPE)
|
||||
cp 'i'
|
||||
jr z, .doI
|
||||
; The rest of the commands don't allow addr == cnt
|
||||
push hl ; --> lvl 1
|
||||
ld hl, (BUF_LINECNT)
|
||||
call cpHLDE
|
||||
pop hl ; <-- lvl 1
|
||||
jr z, .error
|
||||
ld a, (CMD_TYPE)
|
||||
cp 'd'
|
||||
jr z, .doD
|
||||
cp 'a'
|
||||
jr z, .doA
|
||||
jr .doP
|
||||
|
||||
.doQ:
|
||||
xor a
|
||||
ret
|
||||
|
||||
.doW:
|
||||
ld a, 3 ; seek beginning
|
||||
call ioSeek
|
||||
ld de, 0 ; cur line
|
||||
.wLoop:
|
||||
push de \ pop hl
|
||||
call bufGetLine ; --> buffer in (HL)
|
||||
jr nz, .wEnd
|
||||
call ioPutLine
|
||||
jr nz, .error
|
||||
inc de
|
||||
jr .wLoop
|
||||
.wEnd:
|
||||
; Set new file size
|
||||
call ioTell
|
||||
call ioSetSize
|
||||
; for now, writing implies quitting
|
||||
; TODO: reload buffer
|
||||
xor a
|
||||
ret
|
||||
.doD:
|
||||
ld (ED_CURLINE), de
|
||||
; bufDelLines expects an exclusive upper bound, which is why we inc DE.
|
||||
inc de
|
||||
call bufDelLines
|
||||
jr .mainLoop
|
||||
.doA:
|
||||
inc de
|
||||
.doI:
|
||||
call stdioReadLine ; --> HL
|
||||
call bufScratchpadAdd ; --> HL
|
||||
; insert index in DE, line offset in HL. We want the opposite.
|
||||
ex de, hl
|
||||
ld (ED_CURLINE), hl
|
||||
call bufInsertLine
|
||||
call printcrlf
|
||||
jr .mainLoop
|
||||
|
||||
.doP:
|
||||
push hl
|
||||
call bufGetLine
|
||||
jr nz, .error
|
||||
call printstr
|
||||
call printcrlf
|
||||
pop hl
|
||||
call cpHLDE
|
||||
jr z, .doPEnd
|
||||
inc hl
|
||||
jr .doP
|
||||
.doPEnd:
|
||||
ld (ED_CURLINE), hl
|
||||
jp .mainLoop
|
||||
.error:
|
||||
ld a, '?'
|
||||
call stdioPutC
|
||||
call printcrlf
|
||||
jp .mainLoop
|
||||
|
||||
|
||||
; Transform an address "cmd" in IX into an absolute address in HL.
|
||||
edResolveAddr:
|
||||
ld a, (ix)
|
||||
cp RELATIVE
|
||||
jr z, .relative
|
||||
cp EOF
|
||||
jr z, .eof
|
||||
; absolute
|
||||
ld l, (ix+1)
|
||||
ld h, (ix+2)
|
||||
ret
|
||||
.relative:
|
||||
ld hl, (ED_CURLINE)
|
||||
push de
|
||||
ld e, (ix+1)
|
||||
ld d, (ix+2)
|
||||
add hl, de
|
||||
pop de
|
||||
ret
|
||||
.eof:
|
||||
ld hl, (BUF_LINECNT)
|
||||
dec hl
|
||||
ret
|
||||
|
||||
; Read absolute addr1 in HL and addr2 in DE. Also, check bounds and set Z if
|
||||
; both addresses are within bounds, unset if not.
|
||||
edReadAddrs:
|
||||
ld ix, CMD_ADDR2
|
||||
call edResolveAddr
|
||||
ld de, (BUF_LINECNT)
|
||||
ex de, hl ; HL: cnt DE: addr2
|
||||
call cpHLDE
|
||||
jp c, unsetZ ; HL (cnt) < DE (addr2). no good
|
||||
ld ix, CMD_ADDR1
|
||||
call edResolveAddr
|
||||
ex de, hl ; HL: addr2, DE: addr1
|
||||
call cpHLDE
|
||||
jp c, unsetZ ; HL (addr2) < DE (addr1). no good
|
||||
ex de, hl ; HL: addr1, DE: addr2
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
@ -1,8 +0,0 @@
|
||||
; Compare HL with DE and sets Z and C in the same way as a regular cp X where
|
||||
; HL is A and DE is X.
|
||||
cpHLDE:
|
||||
push hl
|
||||
or a ;reset carry flag
|
||||
sbc hl, de ;There is no 'sub hl, de', so we must use sbc
|
||||
pop hl
|
||||
ret
|
@ -1 +0,0 @@
|
||||
Common code used by more than one app, but not by the kernel.
|
@ -1,44 +0,0 @@
|
||||
; 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
|
||||
|
||||
; DE * BC -> DE (high) and HL (low)
|
||||
multDEBC:
|
||||
ld hl, 0
|
||||
ld a, 0x10
|
||||
.loop:
|
||||
add hl, hl
|
||||
rl e
|
||||
rl d
|
||||
jr nc, .noinc
|
||||
add hl, bc
|
||||
jr nc, .noinc
|
||||
inc de
|
||||
.noinc:
|
||||
dec a
|
||||
jr nz, .loop
|
||||
ret
|
@ -1,267 +0,0 @@
|
||||
; *** Requirements ***
|
||||
; ari
|
||||
;
|
||||
; *** Defines ***
|
||||
;
|
||||
; EXPR_PARSE: routine to call to parse literals or symbols that are part of
|
||||
; the expression. Routine's signature:
|
||||
; String in (HL), returns its parsed value to DE. Z for success.
|
||||
; HL is advanced to the character following the last successfully
|
||||
; read char.
|
||||
;
|
||||
; *** Code ***
|
||||
;
|
||||
; Parse expression in string at (HL) and returns the result in DE.
|
||||
; This routine needs to be able to mutate (HL), but it takes care of restoring
|
||||
; the string to its original value before returning.
|
||||
; Sets Z on success, unset on error.
|
||||
parseExpr:
|
||||
push iy
|
||||
push ix
|
||||
push hl
|
||||
call _parseAddSubst
|
||||
pop hl
|
||||
pop ix
|
||||
pop iy
|
||||
ret
|
||||
|
||||
; *** Op signature ***
|
||||
; The signature of "operators routines" (.plus, .mult, etc) below is this:
|
||||
; Combine HL and DE with an operator (+, -, *, etc) and put the result in DE.
|
||||
; Destroys HL and A. Never fails. Yes, that's a problem for division by zero.
|
||||
; Don't divide by zero. All other registers are protected.
|
||||
|
||||
; Given a running result in DE, a rest-of-expression in (HL), a parse routine
|
||||
; in IY and an apply "operator routine" in IX, (HL/DE --> DE)
|
||||
; With that, parse the rest of (HL) and apply the operation on it, then place
|
||||
; HL at the end of the parsed string, with A containing the last char of it,
|
||||
; which can be either an operator or a null char.
|
||||
; Z for success.
|
||||
;
|
||||
_parseApply:
|
||||
push de ; --> lvl 1, left result
|
||||
push ix ; --> lvl 2, routine to apply
|
||||
inc hl ; after op char
|
||||
call callIY ; --> DE
|
||||
pop ix ; <-- lvl 2, routine to apply
|
||||
; Here we do some stack kung fu. We have, in HL, a string pointer we
|
||||
; want to keep. We have, in (SP), our left result we want to use.
|
||||
ex (sp), hl ; <-> lvl 1
|
||||
jr nz, .end
|
||||
push af ; --> lvl 2, save ending operator
|
||||
call callIX
|
||||
pop af ; <-- lvl 2, restore operator.
|
||||
.end:
|
||||
pop hl ; <-- lvl 1, restore str pointer
|
||||
ret
|
||||
|
||||
; Unless there's an error, this routine completely resolves any valid expression
|
||||
; from (HL) and puts the result in DE.
|
||||
; Destroys HL
|
||||
; Z for success.
|
||||
_parseAddSubst:
|
||||
call _parseMultDiv
|
||||
ret nz
|
||||
.loop:
|
||||
; do we have an operator?
|
||||
or a
|
||||
ret z ; null char, we're done
|
||||
; We have an operator. Resolve the rest of the expr then apply it.
|
||||
ld ix, .plus
|
||||
cp '+'
|
||||
jr z, .found
|
||||
ld ix, .minus
|
||||
cp '-'
|
||||
ret nz ; unknown char, error
|
||||
.found:
|
||||
ld iy, _parseMultDiv
|
||||
call _parseApply
|
||||
ret nz
|
||||
jr .loop
|
||||
.plus:
|
||||
add hl, de
|
||||
ex de, hl
|
||||
ret
|
||||
.minus:
|
||||
or a ; clear carry
|
||||
sbc hl, de
|
||||
ex de, hl
|
||||
ret
|
||||
|
||||
; Parse (HL) as far as it can, that is, resolving expressions at its level or
|
||||
; lower (anything but + and -).
|
||||
; A is set to the last op it encountered. Unless there's an error, this can only
|
||||
; be +, - or null. Null if we're done parsing, + and - if there's still work to
|
||||
; do.
|
||||
; (HL) points to last op encountered.
|
||||
; DE is set to the numerical value of everything that was parsed left of (HL).
|
||||
_parseMultDiv:
|
||||
call _parseBitShift
|
||||
ret nz
|
||||
.loop:
|
||||
; do we have an operator?
|
||||
or a
|
||||
ret z ; null char, we're done
|
||||
; We have an operator. Resolve the rest of the expr then apply it.
|
||||
ld ix, .mult
|
||||
cp '*'
|
||||
jr z, .found
|
||||
ld ix, .div
|
||||
cp '/'
|
||||
jr z, .found
|
||||
ld ix, .mod
|
||||
cp '%'
|
||||
jr z, .found
|
||||
; might not be an error, return success
|
||||
cp a
|
||||
ret
|
||||
.found:
|
||||
ld iy, _parseBitShift
|
||||
call _parseApply
|
||||
ret nz
|
||||
jr .loop
|
||||
|
||||
.mult:
|
||||
push bc ; --> lvl 1
|
||||
ld b, h
|
||||
ld c, l
|
||||
call multDEBC ; --> HL
|
||||
pop bc ; <-- lvl 1
|
||||
ex de, hl
|
||||
ret
|
||||
|
||||
.div:
|
||||
; divide takes HL/DE
|
||||
ld a, l
|
||||
push bc ; --> lvl 1
|
||||
call divide
|
||||
ld e, c
|
||||
ld d, b
|
||||
pop bc ; <-- lvl 1
|
||||
ret
|
||||
|
||||
.mod:
|
||||
call .div
|
||||
ex de, hl
|
||||
ret
|
||||
|
||||
; Same as _parseMultDiv, but a layer lower.
|
||||
_parseBitShift:
|
||||
call _parseNumber
|
||||
ret nz
|
||||
.loop:
|
||||
; do we have an operator?
|
||||
or a
|
||||
ret z ; null char, we're done
|
||||
; We have an operator. Resolve the rest of the expr then apply it.
|
||||
ld ix, .and
|
||||
cp '&'
|
||||
jr z, .found
|
||||
ld ix, .or
|
||||
cp 0x7c ; '|'
|
||||
jr z, .found
|
||||
ld ix, .xor
|
||||
cp '^'
|
||||
jr z, .found
|
||||
ld ix, .rshift
|
||||
cp '}'
|
||||
jr z, .found
|
||||
ld ix, .lshift
|
||||
cp '{'
|
||||
jr z, .found
|
||||
; might not be an error, return success
|
||||
cp a
|
||||
ret
|
||||
.found:
|
||||
ld iy, _parseNumber
|
||||
call _parseApply
|
||||
ret nz
|
||||
jr .loop
|
||||
|
||||
.and:
|
||||
ld a, h
|
||||
and d
|
||||
ld d, a
|
||||
ld a, l
|
||||
and e
|
||||
ld e, a
|
||||
ret
|
||||
.or:
|
||||
ld a, h
|
||||
or d
|
||||
ld d, a
|
||||
ld a, l
|
||||
or e
|
||||
ld e, a
|
||||
ret
|
||||
|
||||
.xor:
|
||||
ld a, h
|
||||
xor d
|
||||
ld d, a
|
||||
ld a, l
|
||||
xor e
|
||||
ld e, a
|
||||
ret
|
||||
|
||||
.rshift:
|
||||
ld a, e
|
||||
and 0xf
|
||||
ret z
|
||||
push bc ; --> lvl 1
|
||||
ld b, a
|
||||
.rshiftLoop:
|
||||
srl h
|
||||
rr l
|
||||
djnz .rshiftLoop
|
||||
ex de, hl
|
||||
pop bc ; <-- lvl 1
|
||||
ret
|
||||
|
||||
.lshift:
|
||||
ld a, e
|
||||
and 0xf
|
||||
ret z
|
||||
push bc ; --> lvl 1
|
||||
ld b, a
|
||||
.lshiftLoop:
|
||||
sla l
|
||||
rl h
|
||||
djnz .lshiftLoop
|
||||
ex de, hl
|
||||
pop bc ; <-- lvl 1
|
||||
ret
|
||||
|
||||
; Parse first number of expression at (HL). A valid number is anything that can
|
||||
; be parsed by EXPR_PARSE and is followed either by a null char or by any of the
|
||||
; operator chars. This routines takes care of replacing an operator char with
|
||||
; the null char before calling EXPR_PARSE and then replace the operator back
|
||||
; afterwards.
|
||||
; HL is moved to the char following the number having been parsed.
|
||||
; DE contains the numerical result.
|
||||
; A contains the operator char following the number (or null). Only on success.
|
||||
; Z for success.
|
||||
_parseNumber:
|
||||
; Special case 1: number starts with '-'
|
||||
ld a, (hl)
|
||||
cp '-'
|
||||
jr nz, .skip1
|
||||
; We have a negative number. Parse normally, then subst from zero
|
||||
inc hl
|
||||
call _parseNumber
|
||||
push hl ; --> lvl 1
|
||||
ex af, af' ; preserve flags
|
||||
or a ; clear carry
|
||||
ld hl, 0
|
||||
sbc hl, de
|
||||
ex de, hl
|
||||
ex af, af' ; restore flags
|
||||
pop hl ; <-- lvl 1
|
||||
ret
|
||||
.skip1:
|
||||
; End of special case 1
|
||||
call EXPR_PARSE ; --> DE
|
||||
ret nz
|
||||
; Check if (HL) points to null or op
|
||||
ld a, (hl)
|
||||
ret
|
115
apps/lib/fmt.asm
115
apps/lib/fmt.asm
@ -1,115 +0,0 @@
|
||||
; *** Requirements ***
|
||||
; stdioPutC
|
||||
; divide
|
||||
;
|
||||
|
||||
; Same as fmtDecimal, but DE is considered a signed number
|
||||
fmtDecimalS:
|
||||
bit 7, d
|
||||
jr z, fmtDecimal ; unset, not negative
|
||||
; Invert DE. spit '-', unset bit, then call fmtDecimal
|
||||
push de
|
||||
ld a, '-'
|
||||
ld (hl), a
|
||||
inc hl
|
||||
ld a, d
|
||||
cpl
|
||||
ld d, a
|
||||
ld a, e
|
||||
cpl
|
||||
ld e, a
|
||||
inc de
|
||||
call fmtDecimal
|
||||
dec hl
|
||||
pop de
|
||||
ret
|
||||
|
||||
; 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
|
||||
|
||||
; Format the lower nibble of A into a hex char and stores the result in A.
|
||||
fmtHex:
|
||||
; The idea here is that there's 7 characters between '9' and 'A'
|
||||
; in the ASCII table, and so we add 7 if the digit is >9.
|
||||
; daa is designed for using Binary Coded Decimal format, where each
|
||||
; nibble represents a single base 10 digit. If a nibble has a value >9,
|
||||
; it adds 6 to that nibble, carrying to the next nibble and bringing the
|
||||
; value back between 0-9. This gives us 6 of that 7 we needed to add, so
|
||||
; then we just condtionally set the carry and add that carry, along with
|
||||
; a number that maps 0 to '0'. We also need the upper nibble to be a
|
||||
; set value, and have the N, C and H flags clear.
|
||||
or 0xf0
|
||||
daa ; now a =0x50 + the original value + 0x06 if >= 0xfa
|
||||
add a, 0xa0 ; cause a carry for the values that were >=0x0a
|
||||
adc a, 0x40
|
||||
ret
|
||||
|
||||
; Print the hex char in A as a pair of hex digits.
|
||||
printHex:
|
||||
push af
|
||||
|
||||
; let's start with the leftmost char
|
||||
rra \ rra \ rra \ rra
|
||||
call fmtHex
|
||||
call stdioPutC
|
||||
|
||||
; and now with the rightmost
|
||||
pop af \ push af
|
||||
call fmtHex
|
||||
call stdioPutC
|
||||
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Print the hex pair in HL
|
||||
printHexPair:
|
||||
push af
|
||||
ld a, h
|
||||
call printHex
|
||||
ld a, l
|
||||
call printHex
|
||||
pop af
|
||||
ret
|
@ -1,238 +0,0 @@
|
||||
; *** Requirements ***
|
||||
; lib/util
|
||||
; *** Code ***
|
||||
|
||||
; Parse the hex char at A and extract it's 0-15 numerical value. Put the result
|
||||
; in A.
|
||||
;
|
||||
; On success, the carry flag is reset. On error, it is set.
|
||||
parseHex:
|
||||
; First, let's see if we have an easy 0-9 case
|
||||
|
||||
add a, 0xc6 ; maps '0'-'9' onto 0xf6-0xff
|
||||
sub 0xf6 ; maps to 0-9 and carries if not a digit
|
||||
ret nc
|
||||
|
||||
and 0xdf ; converts lowercase to uppercase
|
||||
add a, 0xe9 ; map 0x11-x017 onto 0xFA - 0xFF
|
||||
sub 0xfa ; map onto 0-6
|
||||
ret c
|
||||
; we have an A-F digit
|
||||
add a, 10 ; C is clear, map back to 0xA-0xF
|
||||
ret
|
||||
|
||||
; Parse string at (HL) as a decimal value and return value in DE.
|
||||
; Reads as many digits as it can and stop when:
|
||||
; 1 - A non-digit character is read
|
||||
; 2 - The number overflows from 16-bit
|
||||
; HL is advanced to the character following the last successfully read char.
|
||||
; Error conditions are:
|
||||
; 1 - There wasn't at least one character that could be read.
|
||||
; 2 - Overflow.
|
||||
; Sets Z on success, unset on error.
|
||||
|
||||
parseDecimal:
|
||||
; First char is special: it has to succeed.
|
||||
ld a, (hl)
|
||||
; Parse the decimal char at A and extract it's 0-9 numerical value. Put the
|
||||
; result in A.
|
||||
; On success, the carry flag is reset. On error, it is set.
|
||||
add a, 0xff-'9' ; maps '0'-'9' onto 0xf6-0xff
|
||||
sub 0xff-9 ; maps to 0-9 and carries if not a digit
|
||||
ret c ; Error. If it's C, it's also going to be NZ
|
||||
; During this routine, we switch between HL and its shadow. On one side,
|
||||
; we have HL the string pointer, and on the other side, we have HL the
|
||||
; numerical result. We also use EXX to preserve BC, saving us a push.
|
||||
parseDecimalSkip: ; enter here to skip parsing the first digit
|
||||
exx ; HL as a result
|
||||
ld h, 0
|
||||
ld l, a ; load first digit in without multiplying
|
||||
|
||||
.loop:
|
||||
exx ; HL as a string pointer
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
exx ; HL as a numerical result
|
||||
|
||||
; same as other above
|
||||
add a, 0xff-'9'
|
||||
sub 0xff-9
|
||||
jr c, .end
|
||||
|
||||
ld b, a ; we can now use a for overflow checking
|
||||
add hl, hl ; x2
|
||||
sbc a, a ; a=0 if no overflow, a=0xFF otherwise
|
||||
ld d, h
|
||||
ld e, l ; de is x2
|
||||
add hl, hl ; x4
|
||||
rla
|
||||
add hl, hl ; x8
|
||||
rla
|
||||
add hl, de ; x10
|
||||
rla
|
||||
ld d, a ; a is zero unless there's an overflow
|
||||
ld e, b
|
||||
add hl, de
|
||||
adc a, a ; same as rla except affects Z
|
||||
; Did we oveflow?
|
||||
jr z, .loop ; No? continue
|
||||
; error, NZ already set
|
||||
exx ; HL is now string pointer, restore BC
|
||||
; HL points to the char following the last success.
|
||||
ret
|
||||
|
||||
.end:
|
||||
push hl ; --> lvl 1, result
|
||||
exx ; HL as a string pointer, restore BC
|
||||
pop de ; <-- lvl 1, result
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Call parseDecimal and then check that HL points to a whitespace or a null.
|
||||
parseDecimalC:
|
||||
call parseDecimal
|
||||
ret nz
|
||||
ld a, (hl)
|
||||
or a
|
||||
ret z ; null? we're happy
|
||||
jp isWS
|
||||
|
||||
; Parse string at (HL) as a hexadecimal value without the "0x" prefix and
|
||||
; return value in DE.
|
||||
; HL is advanced to the character following the last successfully read char.
|
||||
; Sets Z on success.
|
||||
parseHexadecimal:
|
||||
ld a, (hl)
|
||||
call parseHex ; before "ret c" is "sub 0xfa" in parseHex
|
||||
; so carry implies not zero
|
||||
ret c ; we need at least one char
|
||||
push bc
|
||||
ld de, 0
|
||||
ld b, d
|
||||
ld c, d
|
||||
|
||||
; The idea here is that the 4 hex digits of the result can be represented "bdce",
|
||||
; where each register holds a single digit. Then the result is simply
|
||||
; e = (c << 4) | e, d = (b << 4) | d
|
||||
; However, the actual string may be of any length, so when loading in the most
|
||||
; significant digit, we don't know which digit of the result it actually represents
|
||||
; To solve this, after a digit is loaded into a (and is checked for validity),
|
||||
; all digits are moved along, with e taking the latest digit.
|
||||
.loop:
|
||||
dec b
|
||||
inc b ; b should be 0, else we've overflowed
|
||||
jr nz, .end ; Z already unset if overflow
|
||||
ld b, d
|
||||
ld d, c
|
||||
ld c, e
|
||||
ld e, a
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
call parseHex
|
||||
jr nc, .loop
|
||||
ld a, b
|
||||
add a, a \ add a, a \ add a, a \ add a, a
|
||||
or d
|
||||
ld d, a
|
||||
|
||||
ld a, c
|
||||
add a, a \ add a, a \ add a, a \ add a, a
|
||||
or e
|
||||
ld e, a
|
||||
xor a ; ensure z
|
||||
|
||||
.end:
|
||||
pop bc
|
||||
ret
|
||||
|
||||
|
||||
; Parse string at (HL) as a binary value (010101) without the "0b" prefix and
|
||||
; return value in E. D is always zero.
|
||||
; HL is advanced to the character following the last successfully read char.
|
||||
; Sets Z on success.
|
||||
parseBinaryLiteral:
|
||||
ld de, 0
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
add a, 0xff-'1'
|
||||
sub 0xff-1
|
||||
jr c, .end
|
||||
rlc e ; sets carry if overflow, and affects Z
|
||||
ret c ; Z unset if carry set, since bit 0 of e must be set
|
||||
add a, e
|
||||
ld e, a
|
||||
inc hl
|
||||
jr .loop
|
||||
.end:
|
||||
; HL is properly set
|
||||
xor a ; ensure Z
|
||||
ret
|
||||
|
||||
; Parses the string at (HL) and returns the 16-bit value in DE. The string
|
||||
; can be a decimal literal (1234), a hexadecimal literal (0x1234) or a char
|
||||
; literal ('X').
|
||||
; HL is advanced to the character following the last successfully read char.
|
||||
;
|
||||
; As soon as the number doesn't fit 16-bit any more, parsing stops and the
|
||||
; number is invalid. If the number is valid, Z is set, otherwise, unset.
|
||||
parseLiteral:
|
||||
ld de, 0 ; pre-fill
|
||||
ld a, (hl)
|
||||
cp 0x27 ; apostrophe
|
||||
jr z, .char
|
||||
|
||||
; inline parseDecimalDigit
|
||||
add a, 0xc6 ; maps '0'-'9' onto 0xf6-0xff
|
||||
sub 0xf6 ; maps to 0-9 and carries if not a digit
|
||||
ret c
|
||||
; a already parsed so skip first few instructions of parseDecimal
|
||||
jp nz, parseDecimalSkip
|
||||
; maybe hex, maybe binary
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
inc hl ; already place it for hex or bin
|
||||
cp 'x'
|
||||
jr z, parseHexadecimal
|
||||
cp 'b'
|
||||
jr z, parseBinaryLiteral
|
||||
; nope, just a regular decimal
|
||||
dec hl \ dec hl
|
||||
jp parseDecimal
|
||||
|
||||
; Parse string at (HL) and, if it is a char literal, sets Z and return
|
||||
; corresponding value in E. D is always zero.
|
||||
; HL is advanced to the character following the last successfully read char.
|
||||
;
|
||||
; A valid char literal starts with ', ends with ' and has one character in the
|
||||
; middle. No escape sequence are accepted, but ''' will return the apostrophe
|
||||
; character.
|
||||
.char:
|
||||
inc hl
|
||||
ld e, (hl) ; our result
|
||||
inc hl
|
||||
cp (hl)
|
||||
; advance HL and return if good char
|
||||
inc hl
|
||||
ret z
|
||||
|
||||
; Z unset and there's an error
|
||||
; In all error conditions, HL is advanced by 3. Rewind.
|
||||
dec hl \ dec hl \ dec hl
|
||||
; NZ already set
|
||||
ret
|
||||
|
||||
|
||||
; Returns whether A is a literal prefix, that is, a digit or an apostrophe.
|
||||
isLiteralPrefix:
|
||||
cp 0x27 ; apostrophe
|
||||
ret z
|
||||
; continue to isDigit
|
||||
|
||||
; Returns whether A is a digit
|
||||
isDigit:
|
||||
cp '0' ; carry implies not zero for cp
|
||||
ret c
|
||||
cp '9' ; zero unset for a > '9', but set for a='9'
|
||||
ret nc
|
||||
cp a ; ensure Z
|
||||
ret
|
@ -1,114 +0,0 @@
|
||||
; Sets Z is A is ' ' or '\t' (whitespace)
|
||||
isWS:
|
||||
cp ' '
|
||||
ret z
|
||||
cp 0x09
|
||||
ret
|
||||
|
||||
; Advance HL to next WS.
|
||||
; Set Z if WS found, unset if end-of-string.
|
||||
toWS:
|
||||
ld a, (hl)
|
||||
call isWS
|
||||
ret z
|
||||
cp 0x01 ; if a is null, carries and unsets z
|
||||
ret c
|
||||
inc hl
|
||||
jr toWS
|
||||
|
||||
; Consume following whitespaces in HL until a non-WS is hit.
|
||||
; Set Z if non-WS found, unset if end-of-string.
|
||||
rdWS:
|
||||
ld a, (hl)
|
||||
cp 0x01 ; if a is null, carries and unsets z
|
||||
ret c
|
||||
call isWS
|
||||
jr nz, .ok
|
||||
inc hl
|
||||
jr rdWS
|
||||
.ok:
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Copy string from (HL) in (DE), that is, copy bytes until a null char is
|
||||
; encountered. The null char is also copied.
|
||||
; HL and DE point to the char right after the null char.
|
||||
strcpyM:
|
||||
ld a, (hl)
|
||||
ld (de), a
|
||||
inc hl
|
||||
inc de
|
||||
or a
|
||||
jr nz, strcpyM
|
||||
ret
|
||||
|
||||
; Like strcpyM, but preserve HL and DE
|
||||
strcpy:
|
||||
push hl
|
||||
push de
|
||||
call strcpyM
|
||||
pop de
|
||||
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. C is set if HL > DE
|
||||
strcmp:
|
||||
push hl
|
||||
push de
|
||||
|
||||
.loop:
|
||||
ld a, (de)
|
||||
cp (hl)
|
||||
jr nz, .end ; not equal? break early. NZ is carried out
|
||||
; to the caller
|
||||
or a ; If our chars are null, stop the cmp
|
||||
inc hl
|
||||
inc de
|
||||
jr nz, .loop ; Z is carried through
|
||||
|
||||
.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
|
||||
|
||||
; Given a string at (HL), move HL until it points to the end of that string.
|
||||
strskip:
|
||||
push bc
|
||||
ex af, af'
|
||||
xor a ; look for null char
|
||||
ld b, a
|
||||
ld c, a
|
||||
cpir ; advances HL regardless of comparison, so goes one too far
|
||||
dec hl
|
||||
ex af, af'
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Returns length of string at (HL) in A.
|
||||
; Doesn't include null termination.
|
||||
strlen:
|
||||
push bc
|
||||
xor a ; look for null char
|
||||
ld b, a
|
||||
ld c, a
|
||||
cpir ; advances HL to the char after the null
|
||||
.found:
|
||||
; How many char do we have? We have strlen=(NEG BC)-1, since BC started
|
||||
; at 0 and decreased at each CPIR loop. In this routine,
|
||||
; we stay in the 8-bit realm, so C only.
|
||||
add hl, bc
|
||||
sub c
|
||||
dec a
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; make Z the opposite of what it is now
|
||||
toggleZ:
|
||||
jp z, unsetZ
|
||||
cp a
|
||||
ret
|
||||
|
@ -1,21 +0,0 @@
|
||||
; memt
|
||||
;
|
||||
; Write all possible values in all possible addresses that follow the end of
|
||||
; this program. That means we don't test all available RAM, but well, still
|
||||
; better than nothing...
|
||||
;
|
||||
; If there's an error, prints out where.
|
||||
;
|
||||
; *** Requirements ***
|
||||
; printstr
|
||||
; stdioPutC
|
||||
;
|
||||
; *** Includes ***
|
||||
|
||||
.inc "user.h"
|
||||
jp memtMain
|
||||
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/fmt.asm"
|
||||
.inc "memt/main.asm"
|
||||
USER_RAMSTART:
|
@ -1,33 +0,0 @@
|
||||
memtMain:
|
||||
ld de, memtEnd
|
||||
.loop:
|
||||
ld b, 0
|
||||
.iloop:
|
||||
ld a, b
|
||||
ld (de), a
|
||||
ld a, (de)
|
||||
cp b
|
||||
jr nz, .notMatching
|
||||
djnz .iloop
|
||||
inc de
|
||||
xor a
|
||||
cp d
|
||||
jr nz, .loop
|
||||
cp e
|
||||
jr nz, .loop
|
||||
; we rolled over 0xffff, stop
|
||||
ld hl, .sOk
|
||||
xor a
|
||||
jp printstr ; returns
|
||||
.notMatching:
|
||||
ld hl, .sNotMatching
|
||||
call printstr
|
||||
ex de, hl
|
||||
ld a, 1
|
||||
jp printHexPair ; returns
|
||||
.sNotMatching:
|
||||
.db "Not matching at pos ", 0xd, 0xa, 0
|
||||
.sOk:
|
||||
.db "OK", 0xd, 0xa, 0
|
||||
memtEnd:
|
||||
|
@ -1,4 +0,0 @@
|
||||
# sdct - test SD Card
|
||||
|
||||
This program stress-tests a SD card by repeatedly reading and writing to it and
|
||||
verify that data stays the same.
|
@ -1,29 +0,0 @@
|
||||
; sdct
|
||||
;
|
||||
; We want to test reading and writing random data in random sequences of
|
||||
; sectors. Collapse OS doesn't have a random number generator, so we'll simply
|
||||
; rely on initial SRAM value, which tend is random enough for our purpose.
|
||||
;
|
||||
; How it works is simple. From its designated RAMSTART, it calls PutB until it
|
||||
; reaches the end of RAM (0xffff). Then, it starts over and this time it reads
|
||||
; every byte and compares.
|
||||
;
|
||||
; If there's an error, prints out where.
|
||||
;
|
||||
; *** Requirements ***
|
||||
; sdcPutB
|
||||
; sdcGetB
|
||||
; printstr
|
||||
; stdioPutC
|
||||
;
|
||||
; *** Includes ***
|
||||
|
||||
.inc "user.h"
|
||||
.equ SDCT_RAMSTART USER_RAMSTART
|
||||
|
||||
jp sdctMain
|
||||
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/fmt.asm"
|
||||
.inc "sdct/main.asm"
|
||||
USER_RAMSTART:
|
@ -1,72 +0,0 @@
|
||||
sdctMain:
|
||||
ld hl, .sWriting
|
||||
call printstr
|
||||
ld hl, 0
|
||||
ld de, SDCT_RAMSTART
|
||||
.wLoop:
|
||||
ld a, (de)
|
||||
; To avoid overwriting important data and to test the 24-bit addressing,
|
||||
; we set DE to 12 instead of zero
|
||||
push de ; <|
|
||||
ld de, 12 ; |
|
||||
call sdcPutB ; |
|
||||
pop de ; <|
|
||||
jr nz, .error
|
||||
inc hl
|
||||
inc de
|
||||
; Stop looping if DE == 0
|
||||
xor a
|
||||
cp e
|
||||
jr nz, .wLoop
|
||||
; print some kind of progress
|
||||
call printHexPair
|
||||
cp d
|
||||
jr nz, .wLoop
|
||||
; Finished writing
|
||||
ld hl, .sReading
|
||||
call printstr
|
||||
ld hl, 0
|
||||
ld de, SDCT_RAMSTART
|
||||
.rLoop:
|
||||
push de ; <|
|
||||
ld de, 12 ; |
|
||||
call sdcGetB ; |
|
||||
pop de ; <|
|
||||
jr nz, .error
|
||||
ex de, hl
|
||||
cp (hl)
|
||||
ex de, hl
|
||||
jr nz, .notMatching
|
||||
inc hl
|
||||
inc de
|
||||
; Stop looping if DE == 0
|
||||
xor a
|
||||
cp d
|
||||
jr nz, .rLoop
|
||||
cp e
|
||||
jr nz, .rLoop
|
||||
; Finished checking
|
||||
xor a
|
||||
ld hl, .sOk
|
||||
jp printstr ; returns
|
||||
.notMatching:
|
||||
; error position is in HL, let's preserve it
|
||||
ex de, hl
|
||||
ld hl, .sNotMatching
|
||||
call printstr
|
||||
ex de, hl
|
||||
jp printHexPair ; returns
|
||||
.error:
|
||||
ld hl, .sErr
|
||||
jp printstr ; returns
|
||||
|
||||
.sWriting:
|
||||
.db "Writing", 0xd, 0xa, 0
|
||||
.sReading:
|
||||
.db "Reading", 0xd, 0xa, 0
|
||||
.sNotMatching:
|
||||
.db "Not matching at pos ", 0xd, 0xa, 0
|
||||
.sErr:
|
||||
.db "Error", 0xd, 0xa, 0
|
||||
.sOk:
|
||||
.db "OK", 0xd, 0xa, 0
|
@ -1,200 +0,0 @@
|
||||
# z80 assembler
|
||||
|
||||
This is probably the most critical part of the Collapse OS project because it
|
||||
ensures its self-reproduction.
|
||||
|
||||
## Invocation
|
||||
|
||||
`zasm` is invoked with 2 mandatory arguments and an optional one. The mandatory
|
||||
arguments are input blockdev id and output blockdev id. For example, `zasm 0 1`
|
||||
reads source code from blockdev 0, assembles it and spit the result in blockdev
|
||||
1.
|
||||
|
||||
Input blockdev needs to be seek-able, output blockdev doesn't need to (zasm
|
||||
writes in one pass, sequentially.
|
||||
|
||||
The 3rd argument, optional, is the initial `.org` value. It's the high byte of
|
||||
the value. For example, `zasm 0 1 4f` assembles source in blockdev 0 as if it
|
||||
started with the line `.org 0x4f00`. This also means that the initial value of
|
||||
the `@` symbol is `0x4f00`.
|
||||
|
||||
## Running on a "modern" machine
|
||||
|
||||
To be able to develop zasm efficiently, [libz80][libz80] is used to run zasm
|
||||
on a modern machine. The code lives in `emul` and ran be built with `make`,
|
||||
provided that you have a copy libz80 living in `emul/libz80`.
|
||||
|
||||
The resulting `zasm` binary takes asm code in stdin and spits binary in stdout.
|
||||
|
||||
## Literals
|
||||
|
||||
See "Number literals" in `apps/README.md`.
|
||||
|
||||
On top of common literal logic, zasm also has string literals. It's a chain of
|
||||
characters surrounded by double quotes. Example: `"foo"`. This literal can only
|
||||
be used in the `.db` directive and is equivalent to each character being
|
||||
single-quoted and separated by commas (`'f', 'o', 'o'`). No null char is
|
||||
inserted in the resulting value (unlike what C does).
|
||||
|
||||
## Labels
|
||||
|
||||
Lines starting with a name followed `:` are labeled. When that happens, the
|
||||
name of that label is associated with the binary offset of the following
|
||||
instruction.
|
||||
|
||||
For example, a label placed at the beginning of the file is associated with
|
||||
offset 0. If placed right after a first instruction that is 2 bytes wide, then
|
||||
the label is going to be bound to 2.
|
||||
|
||||
Those labels can then be referenced wherever a constant is expected. They can
|
||||
also be referenced where a relative reference is expected (`jr` and `djnz`).
|
||||
|
||||
Labels can be forward-referenced, that is, you can reference a label that is
|
||||
defined later in the source file or in an included source file.
|
||||
|
||||
Labels starting with a dot (`.`) are local labels: they belong only to the
|
||||
namespace of the current "global label" (any label that isn't local). Local
|
||||
namespace is wiped whenever a global label is encountered.
|
||||
|
||||
Local labels allows reuse of common mnemonics and make the assembler use less
|
||||
memory.
|
||||
|
||||
Global labels are all evaluated during the first pass, which makes possible to
|
||||
forward-reference them. Local labels are evaluated during the second pass, but
|
||||
we can still forward-reference them through a "first-pass-redux" hack.
|
||||
|
||||
Labels can be alone on their line, but can also be "inlined", that is, directly
|
||||
followed by an instruction.
|
||||
|
||||
## Constants
|
||||
|
||||
The `.equ` directive declares a constant. That constant's argument is an
|
||||
expression that is evaluated right at parse-time.
|
||||
|
||||
Constants are evaluated during the second pass, which means that they can
|
||||
forward-reference labels.
|
||||
|
||||
However, they *cannot* forward-reference other constants.
|
||||
|
||||
When defining a constant, if the symbol specified has already been defined, no
|
||||
error occur and the first value defined stays intact. This allows for "user
|
||||
override" of programs.
|
||||
|
||||
It's also important to note that constants always override labels, regardless
|
||||
of declaration order.
|
||||
|
||||
## Expressions
|
||||
|
||||
See "Expressions" in `apps/README.md`.
|
||||
|
||||
## The Program Counter
|
||||
|
||||
The `$` is a special symbol that can be placed in any expression and evaluated
|
||||
as the current output offset. That is, it's the value that a label would have if
|
||||
it was placed there.
|
||||
|
||||
## The Last Value
|
||||
|
||||
Whenever a `.equ` directive is evaluated, its resulting value is saved in a
|
||||
special "last value" register that can then be used in any expression. This
|
||||
last value is referenced with the `@` special symbol. This is very useful for
|
||||
variable definitions and for jump tables.
|
||||
|
||||
Note that `.org` also affect the last value.
|
||||
|
||||
## Includes
|
||||
|
||||
The `.inc` directive is special. It takes a string literal as an argument and
|
||||
opens, in the currently active filesystem, the file with the specified name.
|
||||
|
||||
It then proceeds to parse that file as if its content had been copy/pasted in
|
||||
the includer file, that is: global labels are kept and can be referenced
|
||||
elsewhere. Constants too. An exception is local labels: a local namespace always
|
||||
ends at the end of an included file.
|
||||
|
||||
There an important limitation with includes: only one level of includes is
|
||||
allowed. An included file cannot have an `.inc` directive.
|
||||
|
||||
## Directives
|
||||
|
||||
**.db**: Write bytes specified by the directive directly in the resulting
|
||||
binary. Each byte is separated by a comma. Example: `.db 0x42, foo`
|
||||
|
||||
**.dw**: Same as `.db`, but outputs words. Example: `.dw label1, label2`
|
||||
|
||||
**.equ**: Binds a symbol named after the first parameter to the value of the
|
||||
expression written as the second parameter. Example:
|
||||
`.equ foo 0x42+'A'`. See "Constants" above.
|
||||
|
||||
**.fill**: Outputs the number of null bytes specified by its argument, an
|
||||
expression. Often used with `$` to fill our binary up to a certain
|
||||
offset. For example, if we want to place an instruction exactly at
|
||||
byte 0x38, we would precede it with `.fill 0x38-$`.
|
||||
|
||||
The maximum value possible for `.fill` is `0xd000`. We do this to
|
||||
avoid "overshoot" errors, that is, error where `$` is greater than
|
||||
the offset you're trying to reach in an expression like `.fill X-$`
|
||||
(such an expression overflows to `0xffff`).
|
||||
|
||||
**.org**: Sets the Program Counter to the value of the argument, an expression.
|
||||
For example, a label being defined right after a `.org 0x400`, would
|
||||
have a value of `0x400`. Does not do any filling. You have to do that
|
||||
explicitly with `.fill`, if needed. Often used to assemble binaries
|
||||
designed to run at offsets other than zero (userland).
|
||||
|
||||
**.out**: Outputs the value of the expression supplied as an argument to
|
||||
`ZASM_DEBUG_PORT`. The value is always interpreted as a word, so
|
||||
there's always two `out` instruction executed per directive. High byte
|
||||
is sent before low byte. Useful or debugging, quickly figuring our
|
||||
RAM constants, etc. The value is only outputted during the second
|
||||
pass.
|
||||
|
||||
**.inc**: Takes a string literal as an argument. Open the file name specified
|
||||
in the argument in the currently active filesystem, parse that file
|
||||
and output its binary content as is the code has been in the includer
|
||||
file.
|
||||
|
||||
**.bin**: Takes a string literal as an argument. Open the file name specified
|
||||
in the argument in the currently active filesystem and outputs its
|
||||
contents directly.
|
||||
|
||||
## Undocumented instructions
|
||||
|
||||
`zasm` doesn't support undocumented instructions such as the ones that involve
|
||||
using `IX` and `IY` as 8-bit registers. We used to support them, but because
|
||||
this makes our code incompatible with Z80-compatible CPUs such as the Z180, we
|
||||
prefer to avoid these in our code.
|
||||
|
||||
## AVR assembler
|
||||
|
||||
`zasm` can be configured, at compile time, to be a AVR assembler instead of a
|
||||
z80 assembler. Directives, literals, symbols, they're all the same, it's just
|
||||
instructions and their arguments that change.
|
||||
|
||||
Instructions and their arguments have a ayntax that is similar to other AVR
|
||||
assemblers: registers are referred to as `rXX`, mnemonics are the same,
|
||||
arguments are separated by commas.
|
||||
|
||||
To assemble an AVR assembler, use the `gluea.asm` file instead of the regular
|
||||
one.
|
||||
|
||||
Note about AVR and PC: In most assemblers, arithmetics for instructions
|
||||
addresses have words (two bytes) as their basic unit because AVR instructions
|
||||
are either 16bit in length or 32bit in length. All addresses constants in
|
||||
upcodes are in words. However, in zasm's core logic, PC is in bytes (because z80
|
||||
upcodes can be 1 byte).
|
||||
|
||||
The AVR assembler, of course, correctly translates byte PCs to words when
|
||||
writing upcodes, however, when you write your expressions, you need to remember
|
||||
to treat with bytes. For example, in a traditional AVR assembler, jumping to
|
||||
the instruction after the "foo" label would be "rjmp foo+1". In zasm, it's
|
||||
"rjmp foo+2". If your expression results in an odd number, the low bit of your
|
||||
number will be ignored.
|
||||
|
||||
Limitations:
|
||||
|
||||
* `CALL` and `JMP` only support 16-bit numbers, not 22-bit ones.
|
||||
* `BRLO` and `BRSH` are not there. Use `BRCS` and `BRCC` instead.
|
||||
* No `high()` and `low()`. Use `&0xff` and `}8`.
|
||||
|
||||
[libz80]: https://github.com/ggambetta/libz80
|
@ -1,846 +0,0 @@
|
||||
; Same thing as instr.asm, but for AVR instructions
|
||||
|
||||
; *** Instructions table ***
|
||||
|
||||
; List of mnemonic names separated by a null terminator. Their index in the
|
||||
; list is their ID. Unlike in zasm, not all mnemonics have constant associated
|
||||
; to it because it's generally not needed. This list is grouped by argument
|
||||
; categories, and then alphabetically. Categories are ordered so that the 8bit
|
||||
; opcodes come first, then the 16bit ones. 0xff ends the chain
|
||||
instrNames:
|
||||
; Branching instructions. They are all shortcuts to BRBC/BRBS. These are not in
|
||||
; alphabetical order, but rather in "bit order". All "bit set" instructions
|
||||
; first (10th bit clear), then all "bit clear" ones (10th bit set). Inside this
|
||||
; order, they're then in "sss" order (bit number alias for BRBC/BRBS).
|
||||
.db "BRCS", 0
|
||||
.db "BREQ", 0
|
||||
.db "BRMI", 0
|
||||
.db "BRVS", 0
|
||||
.db "BRLT", 0
|
||||
.db "BRHS", 0
|
||||
.db "BRTS", 0
|
||||
.db "BRIE", 0
|
||||
.db "BRCC", 0
|
||||
.db "BRNE", 0
|
||||
.db "BRPL", 0
|
||||
.db "BRVC", 0
|
||||
.db "BRGE", 0
|
||||
.db "BRHC", 0
|
||||
.db "BRTC", 0
|
||||
.db "BRID", 0
|
||||
.equ I_BRBS 16
|
||||
.db "BRBS", 0
|
||||
.db "BRBC", 0
|
||||
.equ I_LD 18
|
||||
.db "LD", 0
|
||||
.db "ST", 0
|
||||
; Rd(5) + Rr(5) (from here, instrTbl8)
|
||||
.equ I_ADC 20
|
||||
.db "ADC", 0
|
||||
.db "ADD", 0
|
||||
.db "AND", 0
|
||||
.db "ASR", 0
|
||||
.db "BCLR", 0
|
||||
.db "BLD", 0
|
||||
.db "BREAK", 0
|
||||
.db "BSET", 0
|
||||
.db "BST", 0
|
||||
.db "CLC", 0
|
||||
.db "CLH", 0
|
||||
.db "CLI", 0
|
||||
.db "CLN", 0
|
||||
.db "CLR", 0
|
||||
.db "CLS", 0
|
||||
.db "CLT", 0
|
||||
.db "CLV", 0
|
||||
.db "CLZ", 0
|
||||
.db "COM", 0
|
||||
.db "CP", 0
|
||||
.db "CPC", 0
|
||||
.db "CPSE", 0
|
||||
.db "DEC", 0
|
||||
.db "EICALL", 0
|
||||
.db "EIJMP", 0
|
||||
.db "EOR", 0
|
||||
.db "ICALL", 0
|
||||
.db "IJMP", 0
|
||||
.db "IN", 0
|
||||
.db "INC", 0
|
||||
.db "LAC", 0
|
||||
.db "LAS", 0
|
||||
.db "LAT", 0
|
||||
.db "LSL", 0
|
||||
.db "LSR", 0
|
||||
.db "MOV", 0
|
||||
.db "MUL", 0
|
||||
.db "NEG", 0
|
||||
.db "NOP", 0
|
||||
.db "OR", 0
|
||||
.db "OUT", 0
|
||||
.db "POP", 0
|
||||
.db "PUSH", 0
|
||||
.db "RET", 0
|
||||
.db "RETI", 0
|
||||
.db "ROR", 0
|
||||
.db "SBC", 0
|
||||
.db "SBRC", 0
|
||||
.db "SBRS", 0
|
||||
.db "SEC", 0
|
||||
.db "SEH", 0
|
||||
.db "SEI", 0
|
||||
.db "SEN", 0
|
||||
.db "SER", 0
|
||||
.db "SES", 0
|
||||
.db "SET", 0
|
||||
.db "SEV", 0
|
||||
.db "SEZ", 0
|
||||
.db "SLEEP", 0
|
||||
.db "SUB", 0
|
||||
.db "SWAP", 0
|
||||
.db "TST", 0
|
||||
.db "WDR", 0
|
||||
.db "XCH", 0
|
||||
.equ I_ANDI 84
|
||||
.db "ANDI", 0
|
||||
.db "CBR", 0
|
||||
.db "CPI", 0
|
||||
.db "LDI", 0
|
||||
.db "ORI", 0
|
||||
.db "SBCI", 0
|
||||
.db "SBR", 0
|
||||
.db "SUBI", 0
|
||||
.equ I_RCALL 92
|
||||
.db "RCALL", 0
|
||||
.db "RJMP", 0
|
||||
.equ I_CBI 94
|
||||
.db "CBI", 0
|
||||
.db "SBI", 0
|
||||
.db "SBIC", 0
|
||||
.db "SBIS", 0
|
||||
; 32-bit
|
||||
; ZASM limitation: CALL and JMP constants are 22-bit. In ZASM, we limit
|
||||
; ourselves to 16-bit. Supporting 22-bit would incur a prohibitive complexity
|
||||
; cost. As they say, 64K words ought to be enough for anybody.
|
||||
.equ I_CALL 98
|
||||
.db "CALL", 0
|
||||
.db "JMP", 0
|
||||
.db 0xff
|
||||
|
||||
; Instruction table
|
||||
;
|
||||
; A table row starts with the "argspecs+flags" byte, followed by two upcode
|
||||
; bytes.
|
||||
;
|
||||
; The argspecs+flags byte is separated in two nibbles: Low nibble is a 4bit
|
||||
; index (1-based, 0 means no arg) in the argSpecs table. High nibble is for
|
||||
; flags. Meaning:
|
||||
;
|
||||
; Bit 7: Arguments swapped. For example, if we have this bit set on the argspec
|
||||
; row 'A', 'R', then what will actually be read is 'R', 'A'. The
|
||||
; arguments destination will be, hum, de-swapped, that is, 'A' is going
|
||||
; in H and 'R' is going in L. This is used, for example, with IN and OUT.
|
||||
; IN has a Rd(5), A(6) signature. OUT could have the same signature, but
|
||||
; AVR's mnemonics has those args reversed for more consistency
|
||||
; (destination is always the first arg). The goal of this flag is to
|
||||
; allow this kind of syntactic sugar with minimal complexity.
|
||||
;
|
||||
; Bit 6: Second arg is a copy of the first
|
||||
; Bit 5: Second arg is inverted (complement)
|
||||
|
||||
; In the same order as in instrNames
|
||||
instrTbl:
|
||||
; Regular processing: Rd with second arg having its 4 low bits placed in C's
|
||||
; 3:0 bits and the 4 high bits being place in B's 4:1 bits
|
||||
; No args are also there.
|
||||
.db 0x02, 0b00011100, 0x00 ; ADC Rd, Rr
|
||||
.db 0x02, 0b00001100, 0x00 ; ADD Rd, Rr
|
||||
.db 0x02, 0b00100000, 0x00 ; AND Rd, Rr
|
||||
.db 0x01, 0b10010100, 0b00000101 ; ASR Rd
|
||||
.db 0x0b, 0b10010100, 0b10001000 ; BCLR s, k
|
||||
.db 0x05, 0b11111000, 0x00 ; BLD Rd, b
|
||||
.db 0x00, 0b10010101, 0b10011000 ; BREAK
|
||||
.db 0x0b, 0b10010100, 0b00001000 ; BSET s, k
|
||||
.db 0x05, 0b11111010, 0x00 ; BST Rd, b
|
||||
.db 0x00, 0b10010100, 0b10001000 ; CLC
|
||||
.db 0x00, 0b10010100, 0b11011000 ; CLH
|
||||
.db 0x00, 0b10010100, 0b11111000 ; CLI
|
||||
.db 0x00, 0b10010100, 0b10101000 ; CLN
|
||||
.db 0x41, 0b00100100, 0x00 ; CLR Rd (Bit 6)
|
||||
.db 0x00, 0b10010100, 0b11001000 ; CLS
|
||||
.db 0x00, 0b10010100, 0b11101000 ; CLT
|
||||
.db 0x00, 0b10010100, 0b10111000 ; CLV
|
||||
.db 0x00, 0b10010100, 0b10011000 ; CLZ
|
||||
.db 0x01, 0b10010100, 0b00000000 ; COM Rd
|
||||
.db 0x02, 0b00010100, 0x00 ; CP Rd, Rr
|
||||
.db 0x02, 0b00000100, 0x00 ; CPC Rd, Rr
|
||||
.db 0x02, 0b00010000, 0x00 ; CPSE Rd, Rr
|
||||
.db 0x01, 0b10010100, 0b00001010 ; DEC Rd
|
||||
.db 0x00, 0b10010101, 0b00011001 ; EICALL
|
||||
.db 0x00, 0b10010100, 0b00011001 ; EIJMP
|
||||
.db 0x02, 0b00100100, 0x00 ; EOR Rd, Rr
|
||||
.db 0x00, 0b10010101, 0b00001001 ; ICALL
|
||||
.db 0x00, 0b10010100, 0b00001001 ; IJMP
|
||||
.db 0x07, 0b10110000, 0x00 ; IN Rd, A
|
||||
.db 0x01, 0b10010100, 0b00000011 ; INC Rd
|
||||
.db 0x01, 0b10010010, 0b00000110 ; LAC Rd
|
||||
.db 0x01, 0b10010010, 0b00000101 ; LAS Rd
|
||||
.db 0x01, 0b10010010, 0b00000111 ; LAT Rd
|
||||
.db 0x41, 0b00001100, 0x00 ; LSL Rd
|
||||
.db 0x01, 0b10010100, 0b00000110 ; LSR Rd
|
||||
.db 0x02, 0b00101100, 0x00 ; MOV Rd, Rr
|
||||
.db 0x02, 0b10011100, 0x00 ; MUL Rd, Rr
|
||||
.db 0x01, 0b10010100, 0b00000001 ; NEG Rd
|
||||
.db 0x00, 0b00000000, 0b00000000 ; NOP
|
||||
.db 0x02, 0b00101000, 0x00 ; OR Rd, Rr
|
||||
.db 0x87, 0b10111000, 0x00 ; OUT A, Rr (Bit 7)
|
||||
.db 0x01, 0b10010000, 0b00001111 ; POP Rd
|
||||
.db 0x01, 0b10010010, 0b00001111 ; PUSH Rd
|
||||
.db 0x00, 0b10010101, 0b00001000 ; RET
|
||||
.db 0x00, 0b10010101, 0b00011000 ; RETI
|
||||
.db 0x01, 0b10010100, 0b00000111 ; ROR Rd
|
||||
.db 0x02, 0b00001000, 0x00 ; SBC Rd, Rr
|
||||
.db 0x05, 0b11111100, 0x00 ; SBRC Rd, b
|
||||
.db 0x05, 0b11111110, 0x00 ; SBRS Rd, b
|
||||
.db 0x00, 0b10010100, 0b00001000 ; SEC
|
||||
.db 0x00, 0b10010100, 0b01011000 ; SEH
|
||||
.db 0x00, 0b10010100, 0b01111000 ; SEI
|
||||
.db 0x00, 0b10010100, 0b00101000 ; SEN
|
||||
.db 0x0a, 0b11101111, 0b00001111 ; SER Rd
|
||||
.db 0x00, 0b10010100, 0b01001000 ; SES
|
||||
.db 0x00, 0b10010100, 0b01101000 ; SET
|
||||
.db 0x00, 0b10010100, 0b00111000 ; SEV
|
||||
.db 0x00, 0b10010100, 0b00011000 ; SEZ
|
||||
.db 0x00, 0b10010101, 0b10001000 ; SLEEP
|
||||
.db 0x02, 0b00011000, 0x00 ; SUB Rd, Rr
|
||||
.db 0x01, 0b10010100, 0b00000010 ; SWAP Rd
|
||||
.db 0x41, 0b00100000, 0x00 ; TST Rd (Bit 6)
|
||||
.db 0x00, 0b10010101, 0b10101000 ; WDR
|
||||
.db 0x01, 0b10010010, 0b00000100 ; XCH Rd
|
||||
; Rd(4) + K(8): XXXXKKKK ddddKKKK
|
||||
.db 0x04, 0b01110000, 0x00 ; ANDI Rd, K
|
||||
.db 0x24, 0b01110000, 0x00 ; CBR Rd, K (Bit 5)
|
||||
.db 0x04, 0b00110000, 0x00 ; CPI Rd, K
|
||||
.db 0x04, 0b11100000, 0x00 ; LDI Rd, K
|
||||
.db 0x04, 0b01100000, 0x00 ; ORI Rd, K
|
||||
.db 0x04, 0b01000000, 0x00 ; SBCI Rd, K
|
||||
.db 0x04, 0b01100000, 0x00 ; SBR Rd, K
|
||||
.db 0x04, 0b01010000, 0x00 ; SUBI Rd, K
|
||||
; k(12): XXXXkkkk kkkkkkkk
|
||||
.db 0x08, 0b11010000, 0x00 ; RCALL k
|
||||
.db 0x08, 0b11000000, 0x00 ; RJMP k
|
||||
; A(5) + bit: XXXXXXXX AAAAAbbb
|
||||
.db 0x09, 0b10011000, 0x00 ; CBI A, b
|
||||
.db 0x09, 0b10011010, 0x00 ; SBI A, b
|
||||
.db 0x09, 0b10011001, 0x00 ; SBIC A, b
|
||||
.db 0x09, 0b10011011, 0x00 ; SBIS A, b
|
||||
; k(16) (well, k(22)...)
|
||||
.db 0x08, 0b10010100, 0b00001110 ; CALL k
|
||||
.db 0x08, 0b10010100, 0b00001100 ; JMP k
|
||||
|
||||
; Same signature as getInstID in instr.asm
|
||||
; Reads string in (HL) and returns the corresponding ID (I_*) in A. Sets Z if
|
||||
; there's a match.
|
||||
getInstID:
|
||||
push bc
|
||||
push hl
|
||||
push de
|
||||
ex de, hl ; DE makes a better needle
|
||||
; haystack. -1 because we inc HL at the beginning of the loop
|
||||
ld hl, instrNames-1
|
||||
ld b, 0xff ; index counter
|
||||
.loop:
|
||||
inc b
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
inc a ; check if 0xff
|
||||
jr z, .notFound
|
||||
call strcmpIN
|
||||
jr nz, .loop
|
||||
; found!
|
||||
ld a, b ; index
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop de
|
||||
pop hl
|
||||
pop bc
|
||||
ret
|
||||
.notFound:
|
||||
dec a ; unset Z
|
||||
jr .end
|
||||
|
||||
; Same signature as parseInstruction in instr.asm
|
||||
; Parse instruction specified in A (I_* const) with args in I/O and write
|
||||
; resulting opcode(s) in I/O.
|
||||
; Sets Z on success. On error, A contains an error code (ERR_*)
|
||||
parseInstruction:
|
||||
; *** Step 1: initialization
|
||||
; Except setting up our registers, we also check if our index < I_ADC.
|
||||
; If we are, we skip regular processing for the .BR processing, which
|
||||
; is a bit special.
|
||||
; During this processing, BC is used as the "upcode WIP" register. It's
|
||||
; there that we send our partial values until they're ready to spit to
|
||||
; I/O.
|
||||
ld bc, 0
|
||||
ld e, a ; Let's keep that instrID somewhere safe
|
||||
; First, let's fetch our table row
|
||||
cp I_LD
|
||||
jp c, .BR ; BR is special, no table row
|
||||
jp z, .LD ; LD is special
|
||||
cp I_ADC
|
||||
jp c, .ST ; ST is special
|
||||
|
||||
; *** Step 2: parse arguments
|
||||
sub I_ADC ; Adjust index for table
|
||||
; Our row is at instrTbl + (A * 3)
|
||||
ld hl, instrTbl
|
||||
call addHL
|
||||
sla a ; A * 2
|
||||
call addHL ; (HL) is our row
|
||||
ld a, (hl)
|
||||
push hl \ pop ix ; IX is now our tblrow
|
||||
ld hl, 0
|
||||
or a
|
||||
jp z, .spit ; No arg? spit right away
|
||||
and 0xf ; lower nibble
|
||||
dec a ; argspec index is 1-based
|
||||
ld hl, argSpecs
|
||||
sla a ; A * 2
|
||||
call addHL ; (HL) is argspec row
|
||||
ld d, (hl)
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
ld h, d
|
||||
ld l, a ; H and L contain specs now
|
||||
bit 7, (ix)
|
||||
call nz, .swapHL ; Bit 7 set, swap H and L
|
||||
call _parseArgs
|
||||
ret nz
|
||||
; *** Step 3: place arguments in binary upcode and spit.
|
||||
; (IX) is table row
|
||||
; Parse arg values now in H and L
|
||||
; InstrID is E
|
||||
bit 7, (ix)
|
||||
call nz, .swapHL ; Bit 7 set, swap H and L again!
|
||||
bit 6, (ix)
|
||||
call nz, .cpHintoL ; Bit 6 set, copy H into L
|
||||
bit 5, (ix)
|
||||
call nz, .invL ; Bit 5 set, invert L
|
||||
ld a, e ; InstrID
|
||||
cp I_ANDI
|
||||
jr c, .spitRegular
|
||||
cp I_RCALL
|
||||
jr c, .spitRdK8
|
||||
cp I_CBI
|
||||
jr c, .spitk12
|
||||
cp I_CALL
|
||||
jr c, .spitA5Bit
|
||||
; Spit k(16)
|
||||
call .spit ; spit 16-bit const upcode
|
||||
; divide HL by 2 (PC deals with words, not bytes)
|
||||
srl h \ rr l
|
||||
; spit 16-bit K, LSB first
|
||||
ld a, l
|
||||
call ioPutB
|
||||
ld a, h
|
||||
jp ioPutB
|
||||
|
||||
.spitRegular:
|
||||
; Regular process which places H and L, ORring it with upcode. Works
|
||||
; in most cases.
|
||||
call .placeRd
|
||||
call .placeRr
|
||||
jr .spit
|
||||
|
||||
.spitRdK8:
|
||||
call .placeRd
|
||||
call .placeRr
|
||||
rr b ; K(8) start at B's 1st bit, not 2nd
|
||||
jr .spit
|
||||
|
||||
.spitk12:
|
||||
; k(12) in HL
|
||||
; We're doing the same dance as in _readk7. See comments there.
|
||||
call zasmIsFirstPass
|
||||
jr z, .spit
|
||||
ld de, 0xfff
|
||||
add hl, de
|
||||
jp c, unsetZ ; Carry? number is way too high.
|
||||
ex de, hl
|
||||
call zasmGetPC ; --> HL
|
||||
inc hl \ inc hl
|
||||
ex de, hl
|
||||
sbc hl, de
|
||||
jp c, unsetZ ; Carry? error
|
||||
ld de, 0xfff
|
||||
sbc hl, de
|
||||
; We're within bounds! Now, divide by 2
|
||||
ld a, l
|
||||
rr h \ rra
|
||||
; LSB in A
|
||||
ld c, a
|
||||
ld a, h
|
||||
and 0xf
|
||||
ld b, a
|
||||
jr .spit
|
||||
.spitA5Bit:
|
||||
ld a, h
|
||||
sla a \ rla \ rla
|
||||
or l
|
||||
ld c, a
|
||||
jr .spit
|
||||
|
||||
.spit:
|
||||
; LSB is spit *before* MSB
|
||||
ld a, (ix+2)
|
||||
or c
|
||||
call ioPutB
|
||||
.spitMSB:
|
||||
ld a, (ix+1)
|
||||
or b
|
||||
call ioPutB
|
||||
xor a ; ensure Z, set success
|
||||
ret
|
||||
|
||||
; Spit a branching mnemonic.
|
||||
.BR:
|
||||
; While we have our index in A, let's settle B straight: Our base
|
||||
; upcode is 0b11110000 for "bit set" types and 0b11110100 for "bit
|
||||
; clear" types. However, we'll have 2 left shift operation done on B
|
||||
; later on, so we need those bits shifted right.
|
||||
ld b, 0b111100
|
||||
cp I_BRBS
|
||||
jr z, .rdBRBS
|
||||
jr nc, .rdBRBC
|
||||
; We have an alias. Our "sss" value is index & 0b111
|
||||
; Before we get rid of that 3rd bit, let's see, is it set? if yes, we'll
|
||||
; want to increase B
|
||||
bit 3, a
|
||||
jr z, .skip1 ; 3rd bit unset
|
||||
inc b
|
||||
.skip1:
|
||||
and 0b111
|
||||
ld c, a ; can't store in H now, (HL) is used
|
||||
ld h, 7
|
||||
ld l, 0
|
||||
call _parseArgs
|
||||
ret nz
|
||||
; ok, now we can
|
||||
ld l, h ; k in L
|
||||
ld h, c ; bit in H
|
||||
.spitBR2:
|
||||
; bit in H, k in L.
|
||||
; Our value in L is the number of relative *bytes*. The value we put
|
||||
; there is the number of words. Therefore, relevant bits are 7:1
|
||||
ld a, l
|
||||
sla a \ rl b
|
||||
sla a \ rl b
|
||||
and 0b11111000
|
||||
; k is now shifted by 3, two of those bits being in B. Let's OR A and
|
||||
; H and we have our LSB ready to go.
|
||||
or h
|
||||
call ioPutB
|
||||
; Good! MSB now. B is already good to go.
|
||||
ld a, b
|
||||
jp ioPutB
|
||||
.rdBRBC:
|
||||
; In addition to reading "sss", we also need to inc B so that our base
|
||||
; upcode becomes 0b111101
|
||||
inc b
|
||||
.rdBRBS:
|
||||
ld h, 'b'
|
||||
ld l, 7
|
||||
call _parseArgs
|
||||
ret nz
|
||||
; bit in H, k in L.
|
||||
jr .spitBR2
|
||||
|
||||
.LD:
|
||||
ld h, 'R'
|
||||
ld l, 'z'
|
||||
call _parseArgs
|
||||
ret nz
|
||||
ld d, 0b10000000
|
||||
jr .LDST
|
||||
.ST:
|
||||
ld h, 'z'
|
||||
ld l, 'R'
|
||||
call _parseArgs
|
||||
ret nz
|
||||
ld d, 0b10000010
|
||||
call .swapHL
|
||||
; continue to .LDST
|
||||
|
||||
.LDST:
|
||||
; Rd in H, Z in L, base upcode in D
|
||||
call .placeRd
|
||||
; We're spitting LSB first, so let's compose it.
|
||||
ld a, l
|
||||
and 0b00001111
|
||||
or c
|
||||
call ioPutB
|
||||
; Now, MSB's bit 4 is L's bit 4. How convenient!
|
||||
ld a, l
|
||||
and 0b00010000
|
||||
or d
|
||||
or b
|
||||
; MSB composed!
|
||||
call ioPutB
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; local routines
|
||||
; place number in H in BC at position .......d dddd....
|
||||
; BC is assumed to be 0
|
||||
.placeRd:
|
||||
sla h \ rl h \ rl h \ rl h ; last RL H might set carry
|
||||
rl b
|
||||
ld c, h
|
||||
ret
|
||||
|
||||
; place number in L in BC at position ...rrrr. ....rrrr
|
||||
; BC is assumed to be either 0 or to be set by .placeRd, that is, that the
|
||||
; high 4 bits of C and lowest bit of B will be preserved.
|
||||
.placeRr:
|
||||
; let's start with the 4 lower bits
|
||||
ld a, l
|
||||
and 0x0f
|
||||
or c
|
||||
ld c, a
|
||||
ld a, l
|
||||
; and now those high 4 bits which go in B.
|
||||
and 0xf0
|
||||
rra \ rra \ rra
|
||||
or b
|
||||
ld b, a
|
||||
ret
|
||||
|
||||
.swapHL:
|
||||
ld a, h
|
||||
ld h, l
|
||||
ld l, a
|
||||
ret
|
||||
|
||||
.cpHintoL:
|
||||
ld l, h
|
||||
ret
|
||||
|
||||
.invL:
|
||||
ld a, l
|
||||
cpl
|
||||
ld l, a
|
||||
ret
|
||||
|
||||
; Argspecs: two bytes describing the arguments that are accepted. Possible
|
||||
; values:
|
||||
;
|
||||
; 0 - None
|
||||
; 7 - a k(7) address, relative to PC, *in bytes* (divide by 2 before writing)
|
||||
; 8 - a K(8) value
|
||||
; 'a' - A 5-bit I/O port value
|
||||
; 'A' - A 6-bit I/O port value
|
||||
; 'b' - a 0-7 bit value
|
||||
; 'D' - A double-length number which will fill whole HL.
|
||||
; 'R' - an r5 value: r0-r31
|
||||
; 'r' - an r4 value: r16-r31
|
||||
; 'z' - an indirect register (X, Y or Z), with our without post-inc/pre-dec
|
||||
; indicator. This will result in a 5-bit number, from which we can place
|
||||
; bits 3:0 to upcode's 3:0 and bit 4 at upcode's 12 in LD and ST.
|
||||
;
|
||||
; All arguments accept expressions, even 'r' ones: in 'r' args, we start by
|
||||
; looking if the arg starts with 'r' or 'R'. If yes, it's a simple 'rXX' value,
|
||||
; if not, we try parsing it as an expression and validate that it falls in the
|
||||
; correct 0-31 or 16-31 range
|
||||
argSpecs:
|
||||
.db 'R', 0 ; Rd(5)
|
||||
.db 'R', 'R' ; Rd(5) + Rr(5)
|
||||
.db 7, 0 ; k(7)
|
||||
.db 'r', 8 ; Rd(4) + K(8)
|
||||
.db 'R', 'b' ; Rd(5) + bit
|
||||
.db 'b', 7 ; bit + k(7)
|
||||
.db 'R', 'A' ; Rd(5) + A(6)
|
||||
.db 'D', 0 ; K(12)
|
||||
.db 'a', 'b' ; A(5) + bit
|
||||
.db 'r', 0 ; Rd(4)
|
||||
.db 'b', 0 ; bit
|
||||
|
||||
; Parse arguments from I/O according to specs in HL
|
||||
; H for first spec, L for second spec
|
||||
; Puts the results in HL
|
||||
; First arg in H, second in L.
|
||||
; This routine is not used in all cases, some ops don't fit this pattern well
|
||||
; and thus parse their args themselves.
|
||||
; Z for success.
|
||||
_parseArgs:
|
||||
; For the duration of the routine, argspec is in DE and final MSB is
|
||||
; in BC. We place result in HL at the end.
|
||||
push de
|
||||
push bc
|
||||
ld bc, 0
|
||||
ex de, hl ; argspecs now in DE
|
||||
call readWord
|
||||
jr nz, .end
|
||||
ld a, d
|
||||
call .parse
|
||||
jr nz, .end
|
||||
ld b, a
|
||||
ld a, e
|
||||
or a
|
||||
jr z, .end ; no arg
|
||||
call readComma
|
||||
jr nz, .end
|
||||
call readWord
|
||||
jr nz, .end
|
||||
ld a, e
|
||||
call .parse
|
||||
jr nz, .end
|
||||
; we're done with (HL) now
|
||||
ld c, a
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
ld h, b
|
||||
ld l, c
|
||||
pop bc
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Parse a single arg specified in A and returns its value in A
|
||||
; Z for success
|
||||
.parse:
|
||||
cp 'R'
|
||||
jr z, _readR5
|
||||
cp 'r'
|
||||
jr z, _readR4
|
||||
cp 'b'
|
||||
jr z, _readBit
|
||||
cp 'A'
|
||||
jr z, _readA6
|
||||
cp 'a'
|
||||
jr z, _readA5
|
||||
cp 7
|
||||
jr z, _readk7
|
||||
cp 8
|
||||
jr z, _readK8
|
||||
cp 'D'
|
||||
jr z, _readDouble
|
||||
cp 'z'
|
||||
jp z, _readz
|
||||
ret ; something's wrong
|
||||
|
||||
_readBit:
|
||||
ld a, 7
|
||||
jp _readExpr
|
||||
|
||||
_readA6:
|
||||
ld a, 0x3f
|
||||
jp _readExpr
|
||||
|
||||
_readA5:
|
||||
ld a, 0x1f
|
||||
jp _readExpr
|
||||
|
||||
_readK8:
|
||||
ld a, 0xff
|
||||
jp _readExpr
|
||||
|
||||
_readDouble:
|
||||
push de
|
||||
call parseExpr
|
||||
jr nz, .end
|
||||
ld b, d
|
||||
ld c, e
|
||||
; BC is already set. For good measure, let's set A to BC's MSB
|
||||
ld a, b
|
||||
.end:
|
||||
pop de
|
||||
ret
|
||||
|
||||
_readk7:
|
||||
push hl
|
||||
push de
|
||||
call parseExpr
|
||||
jr nz, .end
|
||||
; If we're in first pass, stop now. The value of HL doesn't matter and
|
||||
; truncation checks might falsely fail.
|
||||
call zasmIsFirstPass
|
||||
jr z, .end
|
||||
; DE contains an absolute value. Turn this into a -64/+63 relative
|
||||
; value by subtracting PC from it. However, before we do that, let's
|
||||
; add 0x7f to it, which we'll remove later. This will simplify bounds
|
||||
; checks. (we use 7f instead of 3f because we deal in bytes here, not
|
||||
; in words)
|
||||
ld hl, 0x7f
|
||||
add hl, de ; Carry cleared
|
||||
ex de, hl
|
||||
call zasmGetPC ; --> HL
|
||||
; The relative value is actually not relative to current PC, but to
|
||||
; PC after the execution of this branching op. Increase HL by 2.
|
||||
inc hl \ inc hl
|
||||
ex de, hl
|
||||
sbc hl, de
|
||||
jr c, .err ; Carry? error
|
||||
ld de, 0x7f
|
||||
sbc hl, de
|
||||
; We're within bounds! However, our value in L is the number of
|
||||
; relative *bytes*.
|
||||
ld a, l
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
.err:
|
||||
call unsetZ
|
||||
jr .end
|
||||
|
||||
_readR4:
|
||||
call _readR5
|
||||
ret nz
|
||||
; has to be in the 16-31 range
|
||||
sub 0x10
|
||||
jp c, unsetZ
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; read a rXX argument and return register number in A.
|
||||
; Set Z for success.
|
||||
_readR5:
|
||||
push de
|
||||
ld a, (hl)
|
||||
call upcase
|
||||
cp 'X'
|
||||
jr z, .rdXYZ
|
||||
cp 'Y'
|
||||
jr z, .rdXYZ
|
||||
cp 'Z'
|
||||
jr z, .rdXYZ
|
||||
cp 'R'
|
||||
jr nz, .end ; not a register
|
||||
inc hl
|
||||
call parseDecimal
|
||||
jr nz, .end
|
||||
ld a, 31
|
||||
call _DE2A
|
||||
.end:
|
||||
pop de
|
||||
ret
|
||||
.rdXYZ:
|
||||
; First, let's get a base value, that is, (A-'X'+26)*2, because XL, our
|
||||
; lowest register, is equivalent to r26.
|
||||
sub 'X'
|
||||
rla ; no carry from sub
|
||||
add a, 26
|
||||
ld d, a ; store that
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
call upcase
|
||||
cp 'H'
|
||||
jr nz, .skip1
|
||||
; second char is 'H'? our value is +1
|
||||
inc d
|
||||
jr .skip2
|
||||
.skip1:
|
||||
cp 'L'
|
||||
jr nz, .end ; not L either? then it's not good
|
||||
.skip2:
|
||||
; Good, we have our final value in D and we're almost sure it's a valid
|
||||
; register. Our only check left is that the 3rd char is a null.
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr nz, .end
|
||||
; we're good
|
||||
ld a, d
|
||||
jr .end
|
||||
|
||||
; Put DE's LSB into A and, additionally, ensure that the new value is <=
|
||||
; than what was previously in A.
|
||||
; Z for success.
|
||||
_DE2A:
|
||||
cp e
|
||||
jp c, unsetZ ; A < E
|
||||
ld a, d
|
||||
or a
|
||||
ret nz ; should be zero
|
||||
ld a, e
|
||||
; Z set from "or a"
|
||||
ret
|
||||
|
||||
; Read expr and return success only if result in under number given in A
|
||||
; Z for success
|
||||
_readExpr:
|
||||
push de
|
||||
push bc
|
||||
ld b, a
|
||||
call parseExpr
|
||||
jr nz, .end
|
||||
ld a, b
|
||||
call _DE2A
|
||||
jr nz, .end
|
||||
or c
|
||||
ld c, a
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop bc
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Parse one of the following: X, Y, Z, X+, Y+, Z+, -X, -Y, -Z.
|
||||
; For each of those values, return a 5-bit value than can then be interleaved
|
||||
; with LD or ST upcodes.
|
||||
_readz:
|
||||
call strlen
|
||||
cp 3
|
||||
jp nc, unsetZ ; string too long
|
||||
; Let's load first char in A and second in A'. This will free HL
|
||||
ld a, (hl)
|
||||
ex af, af'
|
||||
inc hl
|
||||
ld a, (hl) ; Good, HL is now free
|
||||
ld hl, .tblStraight
|
||||
or a
|
||||
jr z, .parseXYZ ; Second char null? We have a single char
|
||||
; Maybe +
|
||||
cp '+'
|
||||
jr nz, .skip
|
||||
; We have a +
|
||||
ld hl, .tblInc
|
||||
jr .parseXYZ
|
||||
.skip:
|
||||
; Maybe a -
|
||||
ex af, af'
|
||||
cp '-'
|
||||
ret nz ; we have nothing
|
||||
; We have a -
|
||||
ld hl, .tblDec
|
||||
; continue to .parseXYZ
|
||||
.parseXYZ:
|
||||
; We have X, Y or Z in A'
|
||||
ex af, af'
|
||||
call upcase
|
||||
; Now, let's place HL
|
||||
cp 'X'
|
||||
jr z, .fetch
|
||||
inc hl
|
||||
cp 'Y'
|
||||
jr z, .fetch
|
||||
inc hl
|
||||
cp 'Z'
|
||||
ret nz ; error
|
||||
.fetch:
|
||||
ld a, (hl)
|
||||
; Z already set from earlier cp
|
||||
ret
|
||||
|
||||
.tblStraight:
|
||||
.db 0b11100 ; X
|
||||
.db 0b01000 ; Y
|
||||
.db 0b00000 ; Z
|
||||
.tblInc:
|
||||
.db 0b11101 ; X+
|
||||
.db 0b11001 ; Y+
|
||||
.db 0b10001 ; Z+
|
||||
.tblDec:
|
||||
.db 0b11110 ; -X
|
||||
.db 0b11010 ; -Y
|
||||
.db 0b10010 ; -Z
|
||||
|
@ -1,25 +0,0 @@
|
||||
; *** Errors ***
|
||||
; We start error at 0x10 to avoid overlapping with shell errors
|
||||
; Unknown instruction or directive
|
||||
.equ ERR_UNKNOWN 0x11
|
||||
|
||||
; Bad argument: Doesn't match any constant argspec or, if an expression,
|
||||
; contains references to undefined symbols.
|
||||
.equ ERR_BAD_ARG 0x12
|
||||
|
||||
; Code is badly formatted (comma without a following arg, unclosed quote, etc.)
|
||||
.equ ERR_BAD_FMT 0x13
|
||||
|
||||
; Value specified doesn't fit in its destination byte or word
|
||||
.equ ERR_OVFL 0x14
|
||||
|
||||
.equ ERR_FILENOTFOUND 0x15
|
||||
|
||||
; Duplicate symbol
|
||||
.equ ERR_DUPSYM 0x16
|
||||
|
||||
; Out of memory
|
||||
.equ ERR_OOM 0x17
|
||||
|
||||
; *** Other ***
|
||||
.equ ZASM_DEBUG_PORT 42
|
@ -1,336 +0,0 @@
|
||||
; *** CONSTS ***
|
||||
|
||||
.equ D_DB 0x00
|
||||
.equ D_DW 0x01
|
||||
.equ D_EQU 0x02
|
||||
.equ D_ORG 0x03
|
||||
.equ D_FIL 0x04
|
||||
.equ D_OUT 0x05
|
||||
.equ D_INC 0x06
|
||||
.equ D_BIN 0x07
|
||||
.equ D_BAD 0xff
|
||||
|
||||
; *** Variables ***
|
||||
; Result of the last .equ evaluation. Used for "@" symbol.
|
||||
.equ DIREC_LASTVAL DIREC_RAMSTART
|
||||
.equ DIREC_SCRATCHPAD DIREC_LASTVAL+2
|
||||
.equ DIREC_RAMEND DIREC_SCRATCHPAD+SCRATCHPAD_SIZE
|
||||
; *** CODE ***
|
||||
|
||||
; 3 bytes per row, fill with zero
|
||||
dirNames:
|
||||
.db "DB", 0
|
||||
.db "DW", 0
|
||||
.db "EQU"
|
||||
.db "ORG"
|
||||
.db "FIL"
|
||||
.db "OUT"
|
||||
.db "INC"
|
||||
.db "BIN"
|
||||
|
||||
; This is a list of handlers corresponding to indexes in dirNames
|
||||
dirHandlers:
|
||||
.dw handleDB
|
||||
.dw handleDW
|
||||
.dw handleEQU
|
||||
.dw handleORG
|
||||
.dw handleFIL
|
||||
.dw handleOUT
|
||||
.dw handleINC
|
||||
.dw handleBIN
|
||||
|
||||
handleDB:
|
||||
push de
|
||||
push hl
|
||||
.loop:
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
ld hl, scratchpad
|
||||
call enterDoubleQuotes
|
||||
jr z, .stringLiteral
|
||||
call parseExpr
|
||||
jr nz, .badarg
|
||||
ld a, d
|
||||
or a ; cp 0
|
||||
jr nz, .overflow ; not zero? overflow
|
||||
ld a, e
|
||||
call ioPutB
|
||||
jr nz, .ioError
|
||||
.stopStrLit:
|
||||
call readComma
|
||||
jr z, .loop
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop hl
|
||||
pop de
|
||||
ret
|
||||
.ioError:
|
||||
ld a, SHELL_ERR_IO_ERROR
|
||||
jr .error
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jr .error
|
||||
.badarg:
|
||||
ld a, ERR_BAD_ARG
|
||||
jr .error
|
||||
.overflow:
|
||||
ld a, ERR_OVFL
|
||||
.error:
|
||||
or a ; unset Z
|
||||
jr .end
|
||||
|
||||
.stringLiteral:
|
||||
ld a, (hl)
|
||||
inc hl
|
||||
or a ; when we encounter 0, that was what used to
|
||||
jr z, .stopStrLit ; be our closing quote. Stop.
|
||||
; Normal character, output
|
||||
call ioPutB
|
||||
jr nz, .ioError
|
||||
jr .stringLiteral
|
||||
|
||||
handleDW:
|
||||
push de
|
||||
push hl
|
||||
.loop:
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
ld hl, scratchpad
|
||||
call parseExpr
|
||||
jr nz, .badarg
|
||||
ld a, e
|
||||
call ioPutB
|
||||
jr nz, .ioError
|
||||
ld a, d
|
||||
call ioPutB
|
||||
jr nz, .ioError
|
||||
call readComma
|
||||
jr z, .loop
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop hl
|
||||
pop de
|
||||
ret
|
||||
.ioError:
|
||||
ld a, SHELL_ERR_IO_ERROR
|
||||
jr .error
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jr .error
|
||||
.badarg:
|
||||
ld a, ERR_BAD_ARG
|
||||
.error:
|
||||
or a ; unset Z
|
||||
jr .end
|
||||
|
||||
handleEQU:
|
||||
call zasmIsLocalPass ; Are we in local pass? Then ignore all .equ.
|
||||
jr z, .skip ; they mess up duplicate symbol detection.
|
||||
; We register constants on both first and second pass for one little
|
||||
; reason: .org. Normally, we'd register constants on second pass only
|
||||
; so that we have values for forward label references, but we need .org
|
||||
; to be effective during the first pass and .org needs to support
|
||||
; expressions. So, we double-parse .equ, clearing the const registry
|
||||
; before the second pass.
|
||||
push hl
|
||||
push de
|
||||
push bc
|
||||
; Read our constant name
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
; We can't register our symbol yet: we don't have our value!
|
||||
; Let's copy it over.
|
||||
ld de, DIREC_SCRATCHPAD
|
||||
ld bc, SCRATCHPAD_SIZE
|
||||
ldir
|
||||
|
||||
; Now, read the value associated to it
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
ld hl, scratchpad
|
||||
call parseExpr
|
||||
jr nz, .badarg
|
||||
ld hl, DIREC_SCRATCHPAD
|
||||
; Save value in "@" special variable
|
||||
ld (DIREC_LASTVAL), de
|
||||
call symRegisterConst ; A and Z set
|
||||
jr z, .end ; success
|
||||
; register ended up in error. We need to figure which error. If it's
|
||||
; a duplicate error, we ignore it and return success because, as per
|
||||
; ".equ" policy, it's fine to define the same const twice. The first
|
||||
; value has precedence.
|
||||
cp ERR_DUPSYM
|
||||
; whatever the value of Z, it's the good one, return
|
||||
jr .end
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jr .error
|
||||
.badarg:
|
||||
ld a, ERR_BAD_ARG
|
||||
.error:
|
||||
call unsetZ
|
||||
.end:
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
.skip:
|
||||
; consume args and return
|
||||
call readWord
|
||||
jp readWord
|
||||
|
||||
handleORG:
|
||||
push de
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
call parseExpr
|
||||
jr nz, .badarg
|
||||
ex de, hl
|
||||
ld (DIREC_LASTVAL), hl
|
||||
call zasmSetOrg
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop de
|
||||
ret
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jr .error
|
||||
.badarg:
|
||||
ld a, ERR_BAD_ARG
|
||||
.error:
|
||||
or a ; unset Z
|
||||
jr .end
|
||||
|
||||
handleFIL:
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
call parseExpr
|
||||
jr nz, .badarg
|
||||
ld a, d
|
||||
cp 0xd0
|
||||
jr nc, .overflow
|
||||
.loop:
|
||||
ld a, d
|
||||
or e
|
||||
jr z, .loopend
|
||||
xor a
|
||||
call ioPutB
|
||||
jr nz, .ioError
|
||||
dec de
|
||||
jr .loop
|
||||
.loopend:
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.ioError:
|
||||
ld a, SHELL_ERR_IO_ERROR
|
||||
jp unsetZ
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jp unsetZ
|
||||
.badarg:
|
||||
ld a, ERR_BAD_ARG
|
||||
jp unsetZ
|
||||
.overflow:
|
||||
ld a, ERR_OVFL
|
||||
jp unsetZ
|
||||
|
||||
handleOUT:
|
||||
push de
|
||||
push hl
|
||||
; Read our expression
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
call zasmIsFirstPass ; No .out during first pass
|
||||
jr z, .end
|
||||
ld hl, scratchpad
|
||||
call parseExpr
|
||||
jr nz, .badarg
|
||||
ld a, d
|
||||
out (ZASM_DEBUG_PORT), a
|
||||
ld a, e
|
||||
out (ZASM_DEBUG_PORT), a
|
||||
jr .end
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jr .error
|
||||
.badarg:
|
||||
ld a, ERR_BAD_ARG
|
||||
.error:
|
||||
or a ; unset Z
|
||||
.end:
|
||||
pop hl
|
||||
pop de
|
||||
ret
|
||||
|
||||
handleINC:
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
; HL points to scratchpad
|
||||
call enterDoubleQuotes
|
||||
jr nz, .badfmt
|
||||
call ioOpenInclude
|
||||
jr nz, .badfn
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jr .error
|
||||
.badfn:
|
||||
ld a, ERR_FILENOTFOUND
|
||||
.error:
|
||||
call unsetZ
|
||||
ret
|
||||
|
||||
handleBIN:
|
||||
call readWord
|
||||
jr nz, .badfmt
|
||||
; HL points to scratchpad
|
||||
call enterDoubleQuotes
|
||||
jr nz, .badfmt
|
||||
call ioSpitBin
|
||||
jr nz, .badfn
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.badfmt:
|
||||
ld a, ERR_BAD_FMT
|
||||
jr .error
|
||||
.badfn:
|
||||
ld a, ERR_FILENOTFOUND
|
||||
.error:
|
||||
call unsetZ
|
||||
ret
|
||||
|
||||
; Reads string in (HL) and returns the corresponding ID (D_*) in A. Sets Z if
|
||||
; there's a match.
|
||||
getDirectiveID:
|
||||
ld a, (hl)
|
||||
cp '.'
|
||||
ret nz
|
||||
push hl
|
||||
push bc
|
||||
push de
|
||||
inc hl
|
||||
ld b, D_BIN+1 ; D_BIN is last
|
||||
ld c, 3
|
||||
ld de, dirNames
|
||||
call findStringInList
|
||||
pop de
|
||||
pop bc
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Parse directive specified in A (D_* const) with args in I/O and act in
|
||||
; an appropriate manner. If the directive results in writing data at its
|
||||
; current location, that data is directly written through ioPutB.
|
||||
; Each directive has the same return value pattern: Z on success, not-Z on
|
||||
; error, A contains the error number (ERR_*).
|
||||
parseDirective:
|
||||
push de
|
||||
; double A to have a proper offset in dirHandlers
|
||||
add a, a
|
||||
ld de, dirHandlers
|
||||
call addDE
|
||||
call intoDE
|
||||
push de \ pop ix
|
||||
pop de
|
||||
jp (ix)
|
@ -1,88 +0,0 @@
|
||||
; zasm
|
||||
;
|
||||
; Reads input from specified blkdev ID, assemble the binary in two passes and
|
||||
; spit the result in another specified blkdev ID.
|
||||
;
|
||||
; We don't buffer the whole source in memory, so we need our input blkdev to
|
||||
; support Seek so we can read the file a second time. So, for input, we need
|
||||
; GetB and Seek.
|
||||
;
|
||||
; For output, we only need PutB. Output doesn't start until the second pass.
|
||||
;
|
||||
; The goal of the second pass is to assign values to all symbols so that we
|
||||
; can have forward references (instructions referencing a label that happens
|
||||
; later).
|
||||
;
|
||||
; Labels and constants are both treated the same way, that is, they can be
|
||||
; forward-referenced in instructions. ".equ" directives, however, are evaluated
|
||||
; during the first pass so forward references are not allowed.
|
||||
;
|
||||
; *** Requirements ***
|
||||
; strncmp
|
||||
; upcase
|
||||
; findchar
|
||||
; blkSel
|
||||
; blkSet
|
||||
; fsFindFN
|
||||
; fsOpen
|
||||
; fsGetB
|
||||
; _blkGetB
|
||||
; _blkPutB
|
||||
; _blkSeek
|
||||
; _blkTell
|
||||
; printstr
|
||||
; printcrlf
|
||||
|
||||
.inc "user.h"
|
||||
|
||||
; *** Overridable consts ***
|
||||
; NOTE: These limits below are designed to be *just* enough for zasm to assemble
|
||||
; itself. Considering that this app is Collapse OS' biggest app, it's safe to
|
||||
; assume that it will be enough for many many use cases. If you need to compile
|
||||
; apps with lots of big symbols, you'll need to adjust these.
|
||||
; With these default settings, zasm runs with less than 0x1800 bytes of RAM!
|
||||
|
||||
; Maximum number of symbols we can have in the global and consts registry
|
||||
.equ ZASM_REG_MAXCNT 0xff
|
||||
|
||||
; Maximum number of symbols we can have in the local registry
|
||||
.equ ZASM_LREG_MAXCNT 0x20
|
||||
|
||||
; Size of the symbol name buffer size. This is a pool. There is no maximum name
|
||||
; length for a single symbol, just a maximum size for the whole pool.
|
||||
; Global labels and consts have the same buf size
|
||||
.equ ZASM_REG_BUFSZ 0x700
|
||||
|
||||
; Size of the names buffer for the local context registry
|
||||
.equ ZASM_LREG_BUFSZ 0x100
|
||||
|
||||
; ******
|
||||
|
||||
.inc "err.h"
|
||||
.inc "ascii.h"
|
||||
.inc "blkdev.h"
|
||||
.inc "fs.h"
|
||||
jp zasmMain
|
||||
|
||||
.inc "core.asm"
|
||||
.inc "zasm/const.asm"
|
||||
.inc "lib/util.asm"
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "zasm/util.asm"
|
||||
.equ IO_RAMSTART USER_RAMSTART
|
||||
.inc "zasm/io.asm"
|
||||
.equ TOK_RAMSTART IO_RAMEND
|
||||
.inc "zasm/tok.asm"
|
||||
.equ INS_RAMSTART TOK_RAMEND
|
||||
.inc "zasm/instr.asm"
|
||||
.equ DIREC_RAMSTART INS_RAMEND
|
||||
.inc "zasm/directive.asm"
|
||||
.inc "zasm/parse.asm"
|
||||
.equ EXPR_PARSE parseNumberOrSymbol
|
||||
.inc "lib/expr.asm"
|
||||
.equ SYM_RAMSTART DIREC_RAMEND
|
||||
.inc "zasm/symbol.asm"
|
||||
.equ ZASM_RAMSTART SYM_RAMEND
|
||||
.inc "zasm/main.asm"
|
||||
USER_RAMSTART:
|
@ -1,44 +0,0 @@
|
||||
; avra
|
||||
;
|
||||
; This glue code assembles as assembler for AVR microcontrollers. It looks a
|
||||
; lot like zasm, but it spits AVR binary. Comments have been stripped, refer
|
||||
; to glue.asm for details.
|
||||
|
||||
.inc "user.h"
|
||||
|
||||
; *** Overridable consts ***
|
||||
.equ ZASM_REG_MAXCNT 0xff
|
||||
.equ ZASM_LREG_MAXCNT 0x20
|
||||
.equ ZASM_REG_BUFSZ 0x700
|
||||
.equ ZASM_LREG_BUFSZ 0x100
|
||||
|
||||
; ******
|
||||
|
||||
.inc "err.h"
|
||||
.inc "ascii.h"
|
||||
.inc "blkdev.h"
|
||||
.inc "fs.h"
|
||||
jp zasmMain
|
||||
|
||||
.inc "core.asm"
|
||||
.inc "zasm/const.asm"
|
||||
.inc "lib/util.asm"
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "zasm/util.asm"
|
||||
.equ IO_RAMSTART USER_RAMSTART
|
||||
.inc "zasm/io.asm"
|
||||
.equ TOK_RAMSTART IO_RAMEND
|
||||
.inc "zasm/tok.asm"
|
||||
.inc "zasm/avr.asm"
|
||||
.equ DIREC_RAMSTART TOK_RAMEND
|
||||
.inc "zasm/directive.asm"
|
||||
.inc "zasm/parse.asm"
|
||||
.equ EXPR_PARSE parseNumberOrSymbol
|
||||
.inc "lib/expr.asm"
|
||||
.equ SYM_RAMSTART DIREC_RAMEND
|
||||
.inc "zasm/symbol.asm"
|
||||
.equ ZASM_RAMSTART SYM_RAMEND
|
||||
.inc "zasm/main.asm"
|
||||
USER_RAMSTART:
|
||||
|
1234
apps/zasm/instr.asm
1234
apps/zasm/instr.asm
File diff suppressed because it is too large
Load Diff
291
apps/zasm/io.asm
291
apps/zasm/io.asm
@ -1,291 +0,0 @@
|
||||
; I/Os in zasm
|
||||
;
|
||||
; As a general rule, I/O in zasm is pretty straightforward. We receive, as a
|
||||
; parameter, two blockdevs: One that we can read and seek and one that we can
|
||||
; write to (we never seek into it).
|
||||
;
|
||||
; This unit also has the responsibility of counting the number of written bytes,
|
||||
; maintaining IO_PC and of properly disabling output on first pass.
|
||||
;
|
||||
; On top of that, this unit has the responsibility of keeping track of the
|
||||
; current lineno. Whenever GetB is called, we check if the fetched byte is a
|
||||
; newline. If it is, we increase our lineno. This unit is the best place to
|
||||
; keep track of this because we have to handle ioRecallPos.
|
||||
;
|
||||
; zasm doesn't buffers its reads during tokenization, which simplifies its
|
||||
; process. However, it also means that it needs, in certain cases, a "putback"
|
||||
; mechanism, that is, a way to say "you see that character I've just read? that
|
||||
; was out of my bounds. Could you make it as if I had never read it?". That
|
||||
; buffer is one character big and is made with the expectation that ioPutBack
|
||||
; is always called right after a ioGetB (when it's called).
|
||||
;
|
||||
; ioPutBack will mess up seek and tell offsets, so thath "put back" should be
|
||||
; consumed before having to seek and tell.
|
||||
;
|
||||
; That's for the general rules.
|
||||
;
|
||||
; Now, let's enter includes. To simplify processing, we make include mostly
|
||||
; transparent to all other units. They always read from ioGetB and a include
|
||||
; directive should have the exact same effect as copy/pasting the contents of
|
||||
; the included file in the caller.
|
||||
;
|
||||
; By the way: we don't support multiple level of inclusion. Only top level files
|
||||
; can include.
|
||||
;
|
||||
; When we include, all we do here is open the file with fsOpen and set a flag
|
||||
; indicating that we're inside an include. When that flag is on, GetB, Seek and
|
||||
; Tell are transparently redirected to their fs* counterpart.
|
||||
;
|
||||
; When we reach EOF in an included file, we transparently unset the "in include"
|
||||
; flag and continue on the general IN stream.
|
||||
|
||||
; *** Variables ***
|
||||
.equ IO_IN_BLK IO_RAMSTART
|
||||
.equ IO_OUT_BLK IO_IN_BLK+BLOCKDEV_SIZE
|
||||
; Save pos for ioSavePos and ioRecallPos
|
||||
.equ IO_SAVED_POS IO_OUT_BLK+BLOCKDEV_SIZE
|
||||
; File handle for included source
|
||||
.equ IO_INCLUDE_HDL IO_SAVED_POS+2
|
||||
; blkdev for include file
|
||||
.equ IO_INCLUDE_BLK IO_INCLUDE_HDL+FS_HANDLE_SIZE
|
||||
; see ioPutBack below
|
||||
.equ IO_PUTBACK_BUF IO_INCLUDE_BLK+BLOCKDEV_SIZE
|
||||
.equ IO_IN_INCLUDE IO_PUTBACK_BUF+1
|
||||
.equ IO_PC IO_IN_INCLUDE+1
|
||||
; Current lineno in top-level file
|
||||
.equ IO_LINENO IO_PC+2
|
||||
; Current lineno in include file
|
||||
.equ IO_INC_LINENO IO_LINENO+2
|
||||
; Line number (can be top-level or include) when ioSavePos was last called.
|
||||
.equ IO_SAVED_LINENO IO_INC_LINENO+2
|
||||
; Handle for the ioSpitBin
|
||||
.equ IO_BIN_HDL IO_SAVED_LINENO+2
|
||||
.equ IO_RAMEND IO_BIN_HDL+FS_HANDLE_SIZE
|
||||
|
||||
; *** Code ***
|
||||
|
||||
ioInit:
|
||||
xor a
|
||||
ld (IO_PUTBACK_BUF), a
|
||||
ld (IO_IN_INCLUDE), a
|
||||
ld de, IO_INCLUDE_BLK
|
||||
ld hl, _ioIncBlk
|
||||
call blkSet
|
||||
jp ioResetCounters
|
||||
|
||||
ioGetB:
|
||||
ld a, (IO_PUTBACK_BUF)
|
||||
or a ; cp 0
|
||||
jr nz, .getback
|
||||
call ioInInclude
|
||||
jr z, .normalmode
|
||||
; We're in "include mode", read from FS
|
||||
push ix ; --> lvl 1
|
||||
ld ix, IO_INCLUDE_BLK
|
||||
call _blkGetB
|
||||
pop ix ; <-- lvl 1
|
||||
jr nz, .includeEOF
|
||||
cp 0x0a ; newline
|
||||
ret nz ; not newline? nothing to do
|
||||
; We have newline. Increase lineno and return (the rest of the
|
||||
; processing below isn't needed.
|
||||
push hl
|
||||
ld hl, (IO_INC_LINENO)
|
||||
inc hl
|
||||
ld (IO_INC_LINENO), hl
|
||||
pop hl
|
||||
ret
|
||||
|
||||
.includeEOF:
|
||||
; We reached EOF. What we do depends on whether we're in Local Pass
|
||||
; mode. Yes, I know, a bit hackish. Normally, we *should* be
|
||||
; transparently getting of include mode and avoid meddling with global
|
||||
; states, but here, we need to tell main.asm that the local scope if
|
||||
; over *before* we get off include mode, otherwise, our IO_SAVED_POS
|
||||
; will be wrong (an include IO_SAVED_POS used in global IN stream).
|
||||
call zasmIsLocalPass
|
||||
ld a, 0 ; doesn't affect Z flag
|
||||
ret z ; local pass? return EOF
|
||||
; regular pass (first or second)? transparently get off include mode.
|
||||
ld (IO_IN_INCLUDE), a ; A already 0
|
||||
ld (IO_INC_LINENO), a
|
||||
ld (IO_INC_LINENO+1), a
|
||||
; continue on to "normal" reading. We don't want to return our zero
|
||||
.normalmode:
|
||||
; normal mode, read from IN stream
|
||||
push ix ; --> lvl 1
|
||||
ld ix, IO_IN_BLK
|
||||
call _blkGetB
|
||||
pop ix ; <-- lvl 1
|
||||
cp LF ; newline
|
||||
ret nz ; not newline? return
|
||||
; inc current lineno
|
||||
push hl
|
||||
ld hl, IO_LINENO
|
||||
inc (hl)
|
||||
pop hl
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
.getback:
|
||||
push af
|
||||
xor a
|
||||
ld (IO_PUTBACK_BUF), a
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Put back non-zero character A into the "ioGetB stack". The next ioGetB call,
|
||||
; instead of reading from IO_IN_BLK, will return that character. That's the
|
||||
; easiest way I found to handle the readWord/gotoNextLine problem.
|
||||
ioPutBack:
|
||||
ld (IO_PUTBACK_BUF), a
|
||||
ret
|
||||
|
||||
ioPutB:
|
||||
push hl ; --> lvl 1
|
||||
ld hl, (IO_PC)
|
||||
inc hl
|
||||
ld (IO_PC), hl
|
||||
pop hl ; <-- lvl 1
|
||||
push af ; --> lvl 1
|
||||
call zasmIsFirstPass
|
||||
jr z, .skip
|
||||
pop af ; <-- lvl 1
|
||||
push ix ; --> lvl 1
|
||||
ld ix, IO_OUT_BLK
|
||||
call _blkPutB
|
||||
pop ix ; <-- lvl 1
|
||||
ret
|
||||
.skip:
|
||||
pop af ; <-- lvl 1
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
ioSavePos:
|
||||
ld hl, (IO_LINENO)
|
||||
call ioInInclude
|
||||
jr z, .skip
|
||||
ld hl, (IO_INC_LINENO)
|
||||
.skip:
|
||||
ld (IO_SAVED_LINENO), hl
|
||||
call _ioTell
|
||||
ld (IO_SAVED_POS), hl
|
||||
ret
|
||||
|
||||
ioRecallPos:
|
||||
ld hl, (IO_SAVED_LINENO)
|
||||
call ioInInclude
|
||||
jr nz, .include
|
||||
ld (IO_LINENO), hl
|
||||
jr .recallpos
|
||||
.include:
|
||||
ld (IO_INC_LINENO), hl
|
||||
.recallpos:
|
||||
ld hl, (IO_SAVED_POS)
|
||||
jr _ioSeek
|
||||
|
||||
ioRewind:
|
||||
call ioResetCounters ; sets HL to 0
|
||||
jr _ioSeek
|
||||
|
||||
ioResetCounters:
|
||||
ld hl, 0
|
||||
ld (IO_PC), hl
|
||||
ld (IO_LINENO), hl
|
||||
ld (IO_SAVED_LINENO), hl
|
||||
ret
|
||||
|
||||
; always in absolute mode (A = 0)
|
||||
_ioSeek:
|
||||
call ioInInclude
|
||||
ld a, 0 ; don't alter flags
|
||||
jr nz, .include
|
||||
; normal mode, seek in IN stream
|
||||
ld ix, IO_IN_BLK
|
||||
jp _blkSeek
|
||||
.include:
|
||||
; We're in "include mode", seek in FS
|
||||
ld ix, IO_INCLUDE_BLK
|
||||
jp _blkSeek ; returns
|
||||
|
||||
_ioTell:
|
||||
call ioInInclude
|
||||
jp nz, .include
|
||||
; normal mode, seek in IN stream
|
||||
ld ix, IO_IN_BLK
|
||||
jp _blkTell
|
||||
.include:
|
||||
; We're in "include mode", tell from FS
|
||||
ld ix, IO_INCLUDE_BLK
|
||||
jp _blkTell ; returns
|
||||
|
||||
; Sets Z according to whether we're inside an include
|
||||
; Z is set when we're *not* in includes. A bit weird, I know...
|
||||
ioInInclude:
|
||||
ld a, (IO_IN_INCLUDE)
|
||||
or a ; cp 0
|
||||
ret
|
||||
|
||||
; Open include file name specified in (HL).
|
||||
; Sets Z on success, unset on error.
|
||||
ioOpenInclude:
|
||||
call ioPrintLN
|
||||
call fsFindFN
|
||||
ret nz
|
||||
ld ix, IO_INCLUDE_HDL
|
||||
call fsOpen
|
||||
ld a, 1
|
||||
ld (IO_IN_INCLUDE), a
|
||||
ld hl, 0
|
||||
ld (IO_INC_LINENO), hl
|
||||
xor a
|
||||
ld ix, IO_INCLUDE_BLK
|
||||
call _blkSeek
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Open file specified in (HL) and spit its contents through ioPutB
|
||||
; Sets Z on success.
|
||||
ioSpitBin:
|
||||
call fsFindFN
|
||||
ret nz
|
||||
push hl ; --> lvl 1
|
||||
ld ix, IO_BIN_HDL
|
||||
call fsOpen
|
||||
ld hl, 0
|
||||
.loop:
|
||||
ld ix, IO_BIN_HDL
|
||||
call fsGetB
|
||||
jr nz, .loopend
|
||||
call ioPutB
|
||||
inc hl
|
||||
jr .loop
|
||||
.loopend:
|
||||
pop hl ; <-- lvl 1
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Return current lineno in HL and, if in an include, its lineno in DE.
|
||||
; If not in an include, DE is set to 0
|
||||
ioLineNo:
|
||||
push af
|
||||
ld hl, (IO_LINENO)
|
||||
ld de, 0
|
||||
call ioInInclude
|
||||
jr z, .end
|
||||
ld de, (IO_INC_LINENO)
|
||||
.end:
|
||||
pop af
|
||||
ret
|
||||
|
||||
_ioIncGetB:
|
||||
ld ix, IO_INCLUDE_HDL
|
||||
jp fsGetB
|
||||
|
||||
_ioIncBlk:
|
||||
.dw _ioIncGetB, unsetZ
|
||||
|
||||
; call printstr followed by newline
|
||||
ioPrintLN:
|
||||
call printstr
|
||||
jp printcrlf
|
@ -1,245 +0,0 @@
|
||||
; *** Variables ***
|
||||
|
||||
; A bool flag indicating that we're on first pass. When we are, we don't care
|
||||
; about actual output, but only about the length of each upcode. This means
|
||||
; that when we parse instructions and directive that error out because of a
|
||||
; missing symbol, we don't error out and just write down a dummy value.
|
||||
.equ ZASM_FIRST_PASS ZASM_RAMSTART
|
||||
; whether we're in "local pass", that is, in local label scanning mode. During
|
||||
; this special pass, ZASM_FIRST_PASS will also be set so that the rest of the
|
||||
; code behaves as is we were in the first pass.
|
||||
.equ ZASM_LOCAL_PASS @+1
|
||||
; What IO_PC was when we started our context
|
||||
.equ ZASM_CTX_PC @+1
|
||||
; current ".org" offset, that is, what we must offset all our label by.
|
||||
.equ ZASM_ORG @+2
|
||||
.equ ZASM_RAMEND @+2
|
||||
|
||||
; Takes 2 byte arguments, blkdev in and blkdev out, expressed as IDs.
|
||||
; Can optionally take a 3rd argument which is the high byte of the initial
|
||||
; .org. For example, passing 0x42 to this 3rd arg is the equivalent of beginning
|
||||
; the unit with ".org 0x4200".
|
||||
; Read file through blkdev in and outputs its upcodes through blkdev out.
|
||||
; HL is set to the last lineno to be read.
|
||||
; Sets Z on success, unset on error. On error, A contains an error code (ERR_*)
|
||||
zasmMain:
|
||||
; Parse args in (HL)
|
||||
; blkdev in
|
||||
call parseHexadecimal ; --> DE
|
||||
jr nz, .badargs
|
||||
ld a, e
|
||||
ld de, IO_IN_BLK
|
||||
call blkSel
|
||||
|
||||
; blkdev in
|
||||
call rdWS
|
||||
jr nz, .badargs
|
||||
call parseHexadecimal ; --> DE
|
||||
jr nz, .badargs
|
||||
ld a, e
|
||||
ld de, IO_OUT_BLK
|
||||
call blkSel
|
||||
|
||||
; .org high byte
|
||||
ld e, 0 ; in case we .skipOrgSet
|
||||
call rdWS
|
||||
jr nz, .skipOrgSet ; no org argument
|
||||
call parseHexadecimal ; --> DE
|
||||
jr nz, .badargs
|
||||
|
||||
.skipOrgSet:
|
||||
; Init .org with value of E
|
||||
; Save in "@" too
|
||||
ld a, e
|
||||
ld (ZASM_ORG+1), a ; high byte of .org
|
||||
ld (DIREC_LASTVAL+1), a
|
||||
xor a
|
||||
ld (ZASM_ORG), a ; low byte zero in all cases
|
||||
ld (DIREC_LASTVAL), a
|
||||
|
||||
; And then the rest.
|
||||
ld (ZASM_LOCAL_PASS), a
|
||||
call ioInit
|
||||
call symInit
|
||||
|
||||
; First pass
|
||||
ld hl, .sFirstPass
|
||||
call ioPrintLN
|
||||
ld a, 1
|
||||
ld (ZASM_FIRST_PASS), a
|
||||
call zasmParseFile
|
||||
jr nz, .end
|
||||
; Second pass
|
||||
ld hl, .sSecondPass
|
||||
call ioPrintLN
|
||||
xor a
|
||||
ld (ZASM_FIRST_PASS), a
|
||||
; before parsing the file for the second pass, let's clear the const
|
||||
; registry. See comment in handleEQU.
|
||||
ld ix, SYM_CONST_REGISTRY
|
||||
call symClear
|
||||
call zasmParseFile
|
||||
.end:
|
||||
jp ioLineNo ; --> HL, --> DE, returns
|
||||
|
||||
.badargs:
|
||||
; bad args
|
||||
ld a, SHELL_ERR_BAD_ARGS
|
||||
ret
|
||||
|
||||
.sFirstPass:
|
||||
.db "First pass", 0
|
||||
.sSecondPass:
|
||||
.db "Second pass", 0
|
||||
|
||||
; Sets Z according to whether we're in first pass.
|
||||
zasmIsFirstPass:
|
||||
ld a, (ZASM_FIRST_PASS)
|
||||
cp 1
|
||||
ret
|
||||
|
||||
; Sets Z according to whether we're in local pass.
|
||||
zasmIsLocalPass:
|
||||
ld a, (ZASM_LOCAL_PASS)
|
||||
cp 1
|
||||
ret
|
||||
|
||||
; Set ZASM_ORG to specified number in HL
|
||||
zasmSetOrg:
|
||||
ld (ZASM_ORG), hl
|
||||
ret
|
||||
|
||||
; Return current PC (properly .org offsetted) in HL
|
||||
zasmGetPC:
|
||||
push de
|
||||
ld hl, (ZASM_ORG)
|
||||
ld de, (IO_PC)
|
||||
add hl, de
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Repeatedly reads lines from IO, assemble them and spit the binary code in
|
||||
; IO. Z is set on success, unset on error. DE contains the last line number to
|
||||
; be read (first line is 1).
|
||||
zasmParseFile:
|
||||
call ioRewind
|
||||
.loop:
|
||||
call parseLine
|
||||
ret nz ; error
|
||||
ld a, b ; TOK_*
|
||||
cp TOK_EOF
|
||||
jr z, .eof
|
||||
jr .loop
|
||||
.eof:
|
||||
call zasmIsLocalPass
|
||||
jr nz, .end ; EOF and not local pass
|
||||
; we're in local pass and EOF. Unwind this
|
||||
call _endLocalPass
|
||||
jr .loop
|
||||
.end:
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Parse next token and accompanying args (when relevant) in I/O, write the
|
||||
; resulting opcode(s) through ioPutB and increases (IO_PC) by the number of
|
||||
; bytes written. BC is set to the result of the call to tokenize.
|
||||
; Sets Z if parse was successful, unset if there was an error. EOF is not an
|
||||
; error. If there is an error, A is set to the corresponding error code (ERR_*).
|
||||
parseLine:
|
||||
call tokenize
|
||||
ld a, b ; TOK_*
|
||||
cp TOK_INSTR
|
||||
jp z, _parseInstr
|
||||
cp TOK_DIRECTIVE
|
||||
jp z, _parseDirec
|
||||
cp TOK_LABEL
|
||||
jr z, _parseLabel
|
||||
cp TOK_EOF
|
||||
ret z ; We're finished, no error.
|
||||
; Bad token
|
||||
ld a, ERR_UNKNOWN
|
||||
jp unsetZ ; return with Z unset
|
||||
|
||||
_parseInstr:
|
||||
ld a, c ; I_*
|
||||
jp parseInstruction
|
||||
|
||||
_parseDirec:
|
||||
ld a, c ; D_*
|
||||
jp parseDirective
|
||||
|
||||
_parseLabel:
|
||||
; The string in (scratchpad) is a label with its trailing ':' removed.
|
||||
ld hl, scratchpad
|
||||
|
||||
call zasmIsLocalPass
|
||||
jr z, .processLocalPass
|
||||
|
||||
; Is this a local label? If yes, we don't process it in the context of
|
||||
; parseLine, whether it's first or second pass. Local labels are only
|
||||
; parsed during the Local Pass
|
||||
call symIsLabelLocal
|
||||
jr z, .success ; local? don't do anything.
|
||||
|
||||
ld ix, SYM_GLOBAL_REGISTRY
|
||||
call zasmIsFirstPass
|
||||
jr z, .registerLabel ; When we encounter a label in the first
|
||||
; pass, we register it in the symbol
|
||||
; list
|
||||
; At this point, we're in second pass, we've encountered a global label
|
||||
; and we'll soon continue processing our file. However, before we do
|
||||
; that, we should process our local labels.
|
||||
call _beginLocalPass
|
||||
jr .success
|
||||
.processLocalPass:
|
||||
ld ix, SYM_LOCAL_REGISTRY
|
||||
call symIsLabelLocal
|
||||
jr z, .registerLabel ; local label? all good, register it
|
||||
; normally
|
||||
; not a local label? Then we need to end local pass
|
||||
call _endLocalPass
|
||||
jr .success
|
||||
.registerLabel:
|
||||
push hl
|
||||
call zasmGetPC
|
||||
ex de, hl
|
||||
pop hl
|
||||
call symRegister
|
||||
jr nz, .error
|
||||
; continue to .success
|
||||
.success:
|
||||
xor a ; ensure Z
|
||||
ret
|
||||
.error:
|
||||
call unsetZ
|
||||
ret
|
||||
|
||||
_beginLocalPass:
|
||||
; remember were I/O was
|
||||
call ioSavePos
|
||||
; Remember where PC was
|
||||
ld hl, (IO_PC)
|
||||
ld (ZASM_CTX_PC), hl
|
||||
; Fake first pass
|
||||
ld a, 1
|
||||
ld (ZASM_FIRST_PASS), a
|
||||
; Set local pass
|
||||
ld (ZASM_LOCAL_PASS), a
|
||||
; Empty local label registry
|
||||
ld ix, SYM_LOCAL_REGISTRY
|
||||
jp symClear
|
||||
|
||||
|
||||
_endLocalPass:
|
||||
; recall I/O pos
|
||||
call ioRecallPos
|
||||
; recall PC
|
||||
ld hl, (ZASM_CTX_PC)
|
||||
ld (IO_PC), hl
|
||||
; unfake first pass
|
||||
xor a
|
||||
ld (ZASM_FIRST_PASS), a
|
||||
; Unset local pass
|
||||
ld (ZASM_LOCAL_PASS), a
|
||||
cp a ; ensure Z
|
||||
ret
|
@ -1,45 +0,0 @@
|
||||
; Parse string in (HL) and return its numerical value whether its a number
|
||||
; literal or a symbol. Returns value in DE.
|
||||
; HL is advanced to the character following the last successfully read char.
|
||||
; Sets Z if number or symbol is valid, unset otherwise.
|
||||
parseNumberOrSymbol:
|
||||
call isLiteralPrefix
|
||||
jp z, parseLiteral
|
||||
; Not a number. try symbol
|
||||
ld a, (hl)
|
||||
cp '$'
|
||||
jr z, .PC
|
||||
cp '@'
|
||||
jr z, .lastVal
|
||||
call symParse
|
||||
ret nz
|
||||
; HL at end of symbol name, DE at tmp null-terminated symname.
|
||||
push hl ; --> lvl 1
|
||||
ex de, hl
|
||||
call symFindVal ; --> DE
|
||||
pop hl ; <-- lvl 1
|
||||
ret z
|
||||
; not found
|
||||
; When not found, check if we're in first pass. If we are, it doesn't
|
||||
; matter that we didn't find our symbol. Return success anyhow.
|
||||
; Otherwise return error. Z is already unset, so in fact, this is the
|
||||
; same as jumping to zasmIsFirstPass
|
||||
; however, before we do, load DE with zero. Returning dummy non-zero
|
||||
; values can have weird consequence (such as false overflow errors).
|
||||
ld de, 0
|
||||
jp zasmIsFirstPass
|
||||
|
||||
.PC:
|
||||
ex de, hl
|
||||
call zasmGetPC ; --> HL
|
||||
ex de, hl ; result in DE
|
||||
inc hl ; char after last read
|
||||
; Z already set from cp '$'
|
||||
ret
|
||||
|
||||
.lastVal:
|
||||
; last val
|
||||
ld de, (DIREC_LASTVAL)
|
||||
inc hl ; char after last read
|
||||
; Z already set from cp '@'
|
||||
ret
|
@ -1,340 +0,0 @@
|
||||
; Manages both constants and labels within a same namespace and registry.
|
||||
;
|
||||
; Local Labels
|
||||
;
|
||||
; Local labels during the "official" first pass are ignored. To register them
|
||||
; in the global registry during that pass would be wasteful in terms of memory.
|
||||
;
|
||||
; What we do instead is set up a separate register for them and have a "second
|
||||
; first pass" whenever we encounter a new context. That is, we wipe the local
|
||||
; registry, parse the code until the next global symbol (or EOF), then rewind
|
||||
; and continue second pass as usual.
|
||||
;
|
||||
; What is a symbol name? The accepted characters for a symbol are A-Z, a-z, 0-9
|
||||
; dot (.) and underscore (_).
|
||||
; This unit doesn't disallow symbols starting with a digit, but in effect, they
|
||||
; aren't going to work because parseLiteral is going to get that digit first.
|
||||
; So, make your symbols start with a letter or dot or underscore.
|
||||
|
||||
; *** Constants ***
|
||||
; Size of each record in registry
|
||||
.equ SYM_RECSIZE 3
|
||||
|
||||
.equ SYM_REGSIZE ZASM_REG_BUFSZ+1+ZASM_REG_MAXCNT*SYM_RECSIZE
|
||||
|
||||
.equ SYM_LOC_REGSIZE ZASM_LREG_BUFSZ+1+ZASM_LREG_MAXCNT*SYM_RECSIZE
|
||||
|
||||
; Maximum name length for a symbol
|
||||
.equ SYM_NAME_MAXLEN 0x20
|
||||
|
||||
; *** Variables ***
|
||||
; A registry has three parts: record count (byte) record list and names pool.
|
||||
; A record is a 3 bytes structure:
|
||||
; 1b - name length
|
||||
; 2b - value associated to symbol
|
||||
;
|
||||
; We know we're at the end of the record list when we hit a 0-length one.
|
||||
;
|
||||
; The names pool is a list of strings, not null-terminated, associated with
|
||||
; the value.
|
||||
;
|
||||
; It is assumed that the registry is aligned in memory in that order:
|
||||
; names pool, rec count, reclist
|
||||
|
||||
; Global labels registry
|
||||
.equ SYM_GLOB_REG SYM_RAMSTART
|
||||
.equ SYM_LOC_REG @+SYM_REGSIZE
|
||||
.equ SYM_CONST_REG @+SYM_LOC_REGSIZE
|
||||
; Area where we parse symbol names into
|
||||
.equ SYM_TMPNAME @+SYM_REGSIZE
|
||||
.equ SYM_RAMEND @+SYM_NAME_MAXLEN+1
|
||||
|
||||
; *** Registries ***
|
||||
; A symbol registry is a 5 bytes record with points to the name pool then the
|
||||
; records list of the register and then the max record count.
|
||||
|
||||
SYM_GLOBAL_REGISTRY:
|
||||
.dw SYM_GLOB_REG, SYM_GLOB_REG+ZASM_REG_BUFSZ
|
||||
.db ZASM_REG_MAXCNT
|
||||
|
||||
SYM_LOCAL_REGISTRY:
|
||||
.dw SYM_LOC_REG, SYM_LOC_REG+ZASM_LREG_BUFSZ
|
||||
.db ZASM_LREG_MAXCNT
|
||||
|
||||
SYM_CONST_REGISTRY:
|
||||
.dw SYM_CONST_REG, SYM_CONST_REG+ZASM_REG_BUFSZ
|
||||
.db ZASM_REG_MAXCNT
|
||||
|
||||
; *** Code ***
|
||||
|
||||
symInit:
|
||||
ld ix, SYM_GLOBAL_REGISTRY
|
||||
call symClear
|
||||
ld ix, SYM_LOCAL_REGISTRY
|
||||
call symClear
|
||||
ld ix, SYM_CONST_REGISTRY
|
||||
jp symClear
|
||||
|
||||
; Sets Z according to whether label in (HL) is local (starts with a dot)
|
||||
symIsLabelLocal:
|
||||
ld a, '.'
|
||||
cp (hl)
|
||||
ret
|
||||
|
||||
symRegisterGlobal:
|
||||
push ix
|
||||
ld ix, SYM_GLOBAL_REGISTRY
|
||||
call symRegister
|
||||
pop ix
|
||||
ret
|
||||
|
||||
symRegisterLocal:
|
||||
push ix
|
||||
ld ix, SYM_LOCAL_REGISTRY
|
||||
call symRegister
|
||||
pop ix
|
||||
ret
|
||||
|
||||
symRegisterConst:
|
||||
push ix
|
||||
ld ix, SYM_CONST_REGISTRY
|
||||
call symRegister
|
||||
pop ix
|
||||
ret
|
||||
|
||||
; Register label in (HL) (minus the ending ":") into the symbol registry in IX
|
||||
; and set its value in that registry to the value specified in DE.
|
||||
; If successful, Z is set. Otherwise, Z is unset and A is an error code (ERR_*).
|
||||
symRegister:
|
||||
push hl ; --> lvl 1. it's the symbol to add
|
||||
|
||||
call _symIsFull
|
||||
jr z, .outOfMemory
|
||||
|
||||
; First, let's get our strlen
|
||||
call strlen
|
||||
ld c, a ; save that strlen for later
|
||||
|
||||
call _symFind
|
||||
jr z, .duplicateError
|
||||
|
||||
; Is our new name going to make us go out of bounds?
|
||||
push hl ; --> lvl 2
|
||||
push de ; --> lvl 3
|
||||
ld d, 0
|
||||
ld e, c
|
||||
add hl, de ; if carry set here, sbc will carry too
|
||||
ld e, (ix+2) ; DE --> pointer to record list, which is also
|
||||
ld d, (ix+3) ; the end of names pool
|
||||
; DE --> names end
|
||||
|
||||
sbc hl, de ; compares hl and de destructively
|
||||
pop de ; <-- lvl 3
|
||||
pop hl ; <-- lvl 2
|
||||
jr nc, .outOfMemory ; HL >= DE
|
||||
|
||||
; Success. At this point, we have:
|
||||
; HL -> where we want to add the string
|
||||
; IY -> target record where the value goes
|
||||
; DE -> value to register
|
||||
; SP -> string to register
|
||||
|
||||
; Let's start with the record
|
||||
ld (iy), c ; strlen
|
||||
ld (iy+1), e
|
||||
ld (iy+2), d
|
||||
|
||||
; Good! now, the string. Destination is in HL, source is in SP
|
||||
ex de, hl ; dest is in DE
|
||||
pop hl ; <-- lvl 1. string to register
|
||||
; Copy HL into DE until we reach null char
|
||||
call strcpyM
|
||||
|
||||
; Last thing: increase record count
|
||||
ld l, (ix+2)
|
||||
ld h, (ix+3)
|
||||
inc (hl)
|
||||
xor a ; sets Z
|
||||
ret
|
||||
|
||||
.outOfMemory:
|
||||
pop hl ; <-- lvl 1
|
||||
ld a, ERR_OOM
|
||||
jp unsetZ
|
||||
|
||||
.duplicateError:
|
||||
pop hl ; <-- lvl 1
|
||||
ld a, ERR_DUPSYM
|
||||
jp unsetZ ; return
|
||||
|
||||
; Assuming that IX points to a registry, find name HL in its names and make IY
|
||||
; point to the corresponding record. If it doesn't find anything, IY will
|
||||
; conveniently point to the next record after the last, and HL to the next
|
||||
; name insertion point.
|
||||
; If we find something, Z is set, otherwise unset.
|
||||
_symFind:
|
||||
push de
|
||||
push bc
|
||||
|
||||
call strlen
|
||||
ld c, a ; save strlen
|
||||
|
||||
ex de, hl ; easier if needle is in DE
|
||||
|
||||
; IY --> records
|
||||
ld l, (ix+2)
|
||||
ld h, (ix+3)
|
||||
; first byte is count
|
||||
ld b, (hl)
|
||||
inc hl ; first record
|
||||
push hl \ pop iy
|
||||
; HL --> names
|
||||
ld l, (ix)
|
||||
ld h, (ix+1)
|
||||
; do we have an empty reclist?
|
||||
xor a
|
||||
cp b
|
||||
jr z, .nothing ; zero count? nothing
|
||||
.loop:
|
||||
ld a, (iy) ; name len
|
||||
cp c
|
||||
jr nz, .skip ; different strlen, can't possibly match. skip
|
||||
call strncmp
|
||||
jr z, .end ; match! Z already set, IY and HL placed.
|
||||
.skip:
|
||||
; ok, next!
|
||||
|
||||
push de ; --> lvl 1
|
||||
ld de, 0x0003
|
||||
add iy, de ; faster and shorter than three inc's
|
||||
ld e, (iy-3) ; offset is also compulsory, so no extra bytes used
|
||||
; (iy-3) holds the name length of the string just processed
|
||||
add hl, de ; advance HL by (iy-3) characters
|
||||
pop de ; <-- lvl 1
|
||||
|
||||
djnz .loop
|
||||
; end of the chain, nothing found
|
||||
.nothing:
|
||||
call unsetZ
|
||||
.end:
|
||||
pop bc
|
||||
pop de
|
||||
ret
|
||||
|
||||
; For a given symbol name in (HL), find it in the appropriate symbol register
|
||||
; and return its value in DE. If (HL) is a local label, the local register is
|
||||
; searched. Otherwise, the global one. It is assumed that this routine is
|
||||
; always called when the global registry is selected. Therefore, we always
|
||||
; reselect it afterwards.
|
||||
symFindVal:
|
||||
push ix
|
||||
call symIsLabelLocal
|
||||
jr z, .local
|
||||
; global. Let's try consts first, then symbols
|
||||
push hl ; --> lvl 1. we'll need it again if not found.
|
||||
ld ix, SYM_CONST_REGISTRY
|
||||
call _symFind
|
||||
pop hl ; <-- lvl 1
|
||||
jr z, .found
|
||||
ld ix, SYM_GLOBAL_REGISTRY
|
||||
call _symFind
|
||||
jr nz, .end
|
||||
.found:
|
||||
; Found! let's fetch value
|
||||
ld e, (iy+1)
|
||||
ld d, (iy+2)
|
||||
jr .end
|
||||
.local:
|
||||
ld ix, SYM_LOCAL_REGISTRY
|
||||
call _symFind
|
||||
jr z, .found
|
||||
; continue to end
|
||||
.end:
|
||||
pop ix
|
||||
ret
|
||||
|
||||
; Clear registry at IX
|
||||
symClear:
|
||||
push af
|
||||
push hl
|
||||
ld l, (ix+2)
|
||||
ld h, (ix+3)
|
||||
; HL --> reclist count
|
||||
xor a
|
||||
ld (hl), a
|
||||
pop hl
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Returns whether register in IX has reached its capacity.
|
||||
; Sets Z if full, unset if not.
|
||||
_symIsFull:
|
||||
push hl
|
||||
ld l, (ix+2)
|
||||
ld h, (ix+3)
|
||||
ld l, (hl) ; record count
|
||||
ld a, (ix+4) ; max record count
|
||||
cp l
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Parse string (HL) as far as it can for a valid symbol name (see definition in
|
||||
; comment at top) for a maximum of SYM_NAME_MAXLEN characters. Puts the parsed
|
||||
; symbol, null-terminated, in SYM_TMPNAME. Make DE point to SYM_TMPNAME.
|
||||
; HL is advanced to the character following the last successfully read char.
|
||||
; Z for success.
|
||||
; Error conditions:
|
||||
; 1 - No character parsed.
|
||||
; 2 - name too long.
|
||||
symParse:
|
||||
ld de, SYM_TMPNAME
|
||||
push bc
|
||||
; +1 because we want to loop one extra time to see if the char is good
|
||||
; or bad. If it's bad, then fine, proceed as normal. If it's good, then
|
||||
; its going to go through djnz and we can return an error then.
|
||||
ld b, SYM_NAME_MAXLEN+1
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
; Set it directly, even if we don't know yet if it's good
|
||||
ld (de), a
|
||||
or a ; end of string?
|
||||
jr z, .end ; easy ending, Z set, HL set
|
||||
; Check special symbols first
|
||||
cp '.'
|
||||
jr z, .good
|
||||
cp '_'
|
||||
jr z, .good
|
||||
; lowercase
|
||||
or 0x20
|
||||
cp '0'
|
||||
jr c, .bad
|
||||
cp '9'+1
|
||||
jr c, .good
|
||||
cp 'a'
|
||||
jr c, .bad
|
||||
cp 'z'+1
|
||||
jr nc, .bad
|
||||
.good:
|
||||
; character is valid, continue!
|
||||
inc hl
|
||||
inc de
|
||||
djnz .loop
|
||||
; error: string too long
|
||||
; NZ is already set from cp 'z'+1
|
||||
; HL is one char too far
|
||||
dec hl
|
||||
jr .end
|
||||
.bad:
|
||||
; invalid char, stop where we are.
|
||||
; In all cases, we want to null-terminate that string
|
||||
xor a
|
||||
ld (de), a
|
||||
; HL is good. Now, did we succeed? to know, let's see where B is.
|
||||
ld a, b
|
||||
cp SYM_NAME_MAXLEN+1
|
||||
; Our result is the invert of Z
|
||||
call toggleZ
|
||||
.end:
|
||||
ld de, SYM_TMPNAME
|
||||
pop bc
|
||||
ret
|
@ -1,249 +0,0 @@
|
||||
; *** Consts ***
|
||||
.equ TOK_INSTR 0x01
|
||||
.equ TOK_DIRECTIVE 0x02
|
||||
.equ TOK_LABEL 0x03
|
||||
.equ TOK_EOF 0xfe ; end of file
|
||||
.equ TOK_BAD 0xff
|
||||
|
||||
.equ SCRATCHPAD_SIZE 0x40
|
||||
; *** Variables ***
|
||||
.equ scratchpad TOK_RAMSTART
|
||||
.equ TOK_RAMEND scratchpad+SCRATCHPAD_SIZE
|
||||
|
||||
; *** Code ***
|
||||
|
||||
; Sets Z is A is ';' or null.
|
||||
isLineEndOrComment:
|
||||
cp 0x3b ; ';'
|
||||
ret z
|
||||
; continue to isLineEnd
|
||||
|
||||
; Sets Z is A is CR, LF, or null.
|
||||
isLineEnd:
|
||||
or a ; same as cp 0
|
||||
ret z
|
||||
cp CR
|
||||
ret z
|
||||
cp LF
|
||||
ret z
|
||||
cp '\'
|
||||
ret
|
||||
|
||||
; Sets Z is A is ' ', ',', ';', CR, LF, or null.
|
||||
isSepOrLineEnd:
|
||||
call isWS
|
||||
ret z
|
||||
jr isLineEndOrComment
|
||||
|
||||
; Checks whether string at (HL) is a label, that is, whether it ends with a ":"
|
||||
; Sets Z if yes, unset if no.
|
||||
;
|
||||
; If it's a label, we change the trailing ':' char with a null char. It's a bit
|
||||
; dirty, but it's the easiest way to proceed.
|
||||
isLabel:
|
||||
push hl
|
||||
ld a, ':'
|
||||
call findchar
|
||||
ld a, (hl)
|
||||
cp ':'
|
||||
jr nz, .nomatch
|
||||
; We also have to check that it's our last char.
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
or a ; cp 0
|
||||
jr nz, .nomatch ; not a null char following the :. no match.
|
||||
; We have a match!
|
||||
; Remove trailing ':'
|
||||
xor a ; Z is set
|
||||
dec hl
|
||||
ld (hl), a
|
||||
jr .end
|
||||
.nomatch:
|
||||
call unsetZ
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Read I/O as long as it's whitespace. When it's not, stop and return the last
|
||||
; read char in A
|
||||
_eatWhitespace:
|
||||
call ioGetB
|
||||
call isWS
|
||||
ret nz
|
||||
jr _eatWhitespace
|
||||
|
||||
; Read ioGetB until a word starts, then read ioGetB as long as there is no
|
||||
; separator and put that contents in (scratchpad), null terminated, for a
|
||||
; maximum of SCRATCHPAD_SIZE-1 characters.
|
||||
; If EOL (\n, \r or comment) or EOF is hit before we could read a word, we stop
|
||||
; right there. If scratchpad is not big enough, we stop right there and error.
|
||||
; HL points to scratchpad
|
||||
; Sets Z if a word could be read, unsets if not.
|
||||
readWord:
|
||||
push bc
|
||||
; Get to word
|
||||
call _eatWhitespace
|
||||
call isLineEndOrComment
|
||||
jr z, .error
|
||||
ld hl, scratchpad
|
||||
ld b, SCRATCHPAD_SIZE-1
|
||||
; A contains the first letter to read
|
||||
; Are we opening a double quote?
|
||||
cp '"'
|
||||
jr z, .insideQuote
|
||||
; Are we opening a single quote?
|
||||
cp 0x27 ; '
|
||||
jr z, .singleQuote
|
||||
.loop:
|
||||
ld (hl), a
|
||||
inc hl
|
||||
call ioGetB
|
||||
call isSepOrLineEnd
|
||||
jr z, .success
|
||||
cp ','
|
||||
jr z, .success
|
||||
djnz .loop
|
||||
; out of space. error.
|
||||
.error:
|
||||
; We need to put the last char we've read back so that gotoNextLine
|
||||
; behaves properly.
|
||||
call ioPutBack
|
||||
call unsetZ
|
||||
jr .end
|
||||
.success:
|
||||
call ioPutBack
|
||||
; null-terminate scratchpad
|
||||
xor a
|
||||
ld (hl), a
|
||||
ld hl, scratchpad
|
||||
.end:
|
||||
pop bc
|
||||
ret
|
||||
.insideQuote:
|
||||
; inside quotes, we accept literal whitespaces, but not line ends.
|
||||
ld (hl), a
|
||||
inc hl
|
||||
call ioGetB
|
||||
cp '"'
|
||||
jr z, .loop ; ending the quote ends the word
|
||||
call isLineEnd
|
||||
jr z, .error ; ending the line without closing the quote,
|
||||
; nope.
|
||||
djnz .insideQuote
|
||||
; out of space. error.
|
||||
jr .error
|
||||
.singleQuote:
|
||||
; single quote is more straightforward: we have 3 chars and we put them
|
||||
; right in scratchpad
|
||||
ld (hl), a
|
||||
call ioGetB
|
||||
or a
|
||||
jr z, .error
|
||||
inc hl
|
||||
ld (hl), a
|
||||
call ioGetB
|
||||
cp 0x27 ; '
|
||||
jr nz, .error
|
||||
inc hl
|
||||
ld (hl), a
|
||||
jr .loop
|
||||
|
||||
; Reads the next char in I/O. If it's a comma, Set Z and return. If it's not,
|
||||
; Put the read char back in I/O and unset Z.
|
||||
readComma:
|
||||
call _eatWhitespace
|
||||
cp ','
|
||||
ret z
|
||||
call ioPutBack
|
||||
jp unsetZ
|
||||
|
||||
; Read ioGetB until we reach the beginning of next line, skipping comments if
|
||||
; necessary. This skips all whitespace, \n, \r, comments until we reach the
|
||||
; first non-comment character. Then, we put it back (ioPutBack) and return.
|
||||
;
|
||||
; If gotoNextLine encounters anything else than whitespace, comment or line
|
||||
; separator, we error out (no putback)
|
||||
|
||||
; Sets Z if we reached a new line. Unset if EOF or error.
|
||||
gotoNextLine:
|
||||
.loop1:
|
||||
; first loop is "strict", that is: we error out on non-whitespace.
|
||||
call ioGetB
|
||||
call isSepOrLineEnd
|
||||
ret nz ; error
|
||||
or a ; cp 0
|
||||
jr z, .eof
|
||||
call isLineEnd
|
||||
jr z, .loop3 ; good!
|
||||
cp 0x3b ; ';'
|
||||
jr z, .loop2 ; comment starting, go to "fast lane"
|
||||
jr .loop1
|
||||
.loop2:
|
||||
; second loop is the "comment loop": anything is valid and we just run
|
||||
; until EOL.
|
||||
call ioGetB
|
||||
or a ; cp 0
|
||||
jr z, .eof
|
||||
cp '\' ; special case: '\' doesn't count as a line end
|
||||
; in a comment.
|
||||
jr z, .loop2
|
||||
call isLineEnd
|
||||
jr z, .loop3
|
||||
jr .loop2
|
||||
.loop3:
|
||||
; Loop 3 happens after we reach our first line sep. This means that we
|
||||
; wade through whitespace until we reach a non-whitespace character.
|
||||
call ioGetB
|
||||
or a ; cp 0
|
||||
jr z, .eof
|
||||
cp 0x3b ; ';'
|
||||
jr z, .loop2 ; oh, another comment! go back to loop2!
|
||||
call isSepOrLineEnd
|
||||
jr z, .loop3
|
||||
; Non-whitespace. That's our goal! Put it back
|
||||
call ioPutBack
|
||||
.eof:
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Parse line in (HL) and read the next token in BC. The token is written on
|
||||
; two bytes (B and C). B is a token type (TOK_* constants) and C is an ID
|
||||
; specific to that token type.
|
||||
; Advance HL to after the read word.
|
||||
; If no token matches, TOK_BAD is written to B
|
||||
tokenize:
|
||||
call readWord
|
||||
jr z, .process ; read successful, process into token.
|
||||
; Error. It could be EOL, EOF or scraptchpad size problem
|
||||
; Whatever it is, calling gotoNextLine is appropriate. If it's EOL
|
||||
; that's obviously what we want to do. If it's EOF, we can check
|
||||
; it after. If it's a scratchpad overrun, gotoNextLine handles it.
|
||||
call gotoNextLine
|
||||
jr nz, .error
|
||||
or a ; Are we EOF?
|
||||
jr nz, tokenize ; not EOF? then continue!
|
||||
; We're EOF
|
||||
ld b, TOK_EOF
|
||||
ret
|
||||
.process:
|
||||
call isLabel
|
||||
jr z, .label
|
||||
call getInstID
|
||||
jr z, .instr
|
||||
call getDirectiveID
|
||||
jr z, .direc
|
||||
.error:
|
||||
; no match
|
||||
ld b, TOK_BAD
|
||||
jr .end
|
||||
.instr:
|
||||
ld b, TOK_INSTR
|
||||
jr .end
|
||||
.direc:
|
||||
ld b, TOK_DIRECTIVE
|
||||
jr .end
|
||||
.label:
|
||||
ld b, TOK_LABEL
|
||||
.end:
|
||||
ld c, a
|
||||
ret
|
@ -1,186 +0,0 @@
|
||||
; run RLA the number of times specified in B
|
||||
rlaX:
|
||||
; first, see if B == 0 to see if we need to bail out
|
||||
inc b
|
||||
dec b
|
||||
ret z ; Z flag means we had B = 0
|
||||
.loop: rla
|
||||
djnz .loop
|
||||
ret
|
||||
|
||||
callHL:
|
||||
jp (hl)
|
||||
ret
|
||||
|
||||
; HL - DE -> HL
|
||||
subDEFromHL:
|
||||
push af
|
||||
ld a, l
|
||||
sub e
|
||||
ld l, a
|
||||
ld a, h
|
||||
sbc a, d
|
||||
ld h, a
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Compares strings pointed to by HL and DE up to A count of characters in a
|
||||
; case-insensitive manner.
|
||||
; If equal, Z is set. If not equal, Z is reset.
|
||||
strncmpI:
|
||||
push bc
|
||||
push hl
|
||||
push de
|
||||
|
||||
ld b, a
|
||||
.loop:
|
||||
ld a, (de)
|
||||
call upcase
|
||||
ld c, a
|
||||
ld a, (hl)
|
||||
call upcase
|
||||
cp c
|
||||
jr nz, .end ; not equal? break early. NZ is carried out
|
||||
; to the called
|
||||
or a ; 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
|
||||
djnz .loop
|
||||
; Success
|
||||
; We went through all chars with success. Ensure Z
|
||||
cp a
|
||||
.end:
|
||||
pop de
|
||||
pop hl
|
||||
pop bc
|
||||
; 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
|
||||
|
||||
; strcmp, then next. Same thing as strcmp, but case insensitive and if strings
|
||||
; are not equal, make HL point to the character right after the null
|
||||
; termination. We assume that the haystack (HL), has uppercase chars.
|
||||
strcmpIN:
|
||||
push de ; --> lvl 1
|
||||
push hl ; --> lvl 2
|
||||
|
||||
.loop:
|
||||
ld a, (de)
|
||||
call upcase
|
||||
cp (hl)
|
||||
jr nz, .notFound ; not equal? break early.
|
||||
or a ; If our chars are null, stop the cmp
|
||||
jr z, .found
|
||||
inc hl
|
||||
inc de
|
||||
jr .loop
|
||||
.found:
|
||||
pop hl ; <-- lvl 2
|
||||
pop de ; <-- lvl 1
|
||||
; Z already set
|
||||
ret
|
||||
.notFound:
|
||||
; Not found, we skip the string
|
||||
call strskip
|
||||
pop de ; <-- lvl 2, junk
|
||||
pop de ; <-- lvl 1
|
||||
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.
|
||||
enterParens:
|
||||
ld a, (hl)
|
||||
cp '('
|
||||
ret nz ; nothing to do
|
||||
push hl
|
||||
ld a, 0 ; look for null char
|
||||
; advance until we get null
|
||||
.loop:
|
||||
cpi
|
||||
jp z, .found
|
||||
jr .loop
|
||||
.found:
|
||||
dec hl ; cpi over-advances. go back to null-char
|
||||
dec hl ; looking at the last char before null
|
||||
ld a, (hl)
|
||||
cp ')'
|
||||
jr nz, .doNotEnter
|
||||
; We have parens. While we're here, let's put a null
|
||||
xor a
|
||||
ld (hl), a
|
||||
pop hl ; back at the beginning. Let's advance.
|
||||
inc hl
|
||||
cp a ; ensure Z
|
||||
ret ; we're good!
|
||||
.doNotEnter:
|
||||
pop hl
|
||||
call unsetZ
|
||||
ret
|
||||
|
||||
; Scans (HL) and sets Z according to whether the string is double quoted, that
|
||||
; is, starts with a " and ends with a ". If it is double quoted, "enter" them,
|
||||
; that is, advance HL by one and transform the ending quote into a null char.
|
||||
; If the string isn't double-enquoted, HL isn't changed.
|
||||
enterDoubleQuotes:
|
||||
ld a, (hl)
|
||||
cp '"'
|
||||
ret nz
|
||||
push hl
|
||||
inc hl
|
||||
ld a, (hl)
|
||||
or a ; already end of string?
|
||||
jr z, .nomatch
|
||||
xor a
|
||||
call findchar ; go to end of string
|
||||
dec hl
|
||||
ld a, (hl)
|
||||
cp '"'
|
||||
jr nz, .nomatch
|
||||
; We have a match, replace ending quote with null char
|
||||
xor a
|
||||
ld (hl), a
|
||||
; Good, let's go back
|
||||
pop hl
|
||||
; ... but one char further
|
||||
inc hl
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.nomatch:
|
||||
call unsetZ
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Find string (HL) in string list (DE) of size B, in a case-insensitive manner.
|
||||
; Each string is C bytes wide.
|
||||
; Returns the index of the found string. Sets Z if found, unsets Z if not found.
|
||||
findStringInList:
|
||||
push de
|
||||
push bc
|
||||
.loop:
|
||||
ld a, c
|
||||
call strncmpI
|
||||
ld a, c
|
||||
call addDE
|
||||
jr z, .match
|
||||
djnz .loop
|
||||
; no match, Z is unset
|
||||
pop bc
|
||||
pop de
|
||||
ret
|
||||
.match:
|
||||
; Now, we want the index of our string, which is equal to our initial B
|
||||
; minus our current B. To get this, we have to play with our registers
|
||||
; and stack a bit.
|
||||
ld d, b
|
||||
pop bc
|
||||
ld a, b
|
||||
sub d
|
||||
pop de
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
# AVR include files
|
||||
|
||||
This folder contains header files that can be included in AVR assembly code.
|
||||
|
||||
These definitions are organized in a manner that is very similar to other
|
||||
modern AVR assemblers, but most bits definitions (`PINB4`, `WGM01`, etc.) are
|
||||
absent. This is because there's a lot of them, each symbol takes memory during
|
||||
assembly and machines doing the assembling might be tight in memory. AVR code
|
||||
post collapse will have to take the habit of using numerical masks accompanied
|
||||
by comments describing associated symbols.
|
||||
|
||||
To avoid repeats, those includes are organized in 3 levels. First, there's the
|
||||
`avr.h` file containing definitions common to all AVR models. Then, there's the
|
||||
"family" file containing definitions common to a "family" (for example, the
|
||||
ATtiny 25/45/85). Those definitions are the beefiests. Then, there's the exact
|
||||
model file, which will typically contain RAM and Flash boundaries.
|
10
avr/avr.h
10
avr/avr.h
@ -1,10 +0,0 @@
|
||||
; *** CPU registers aliases ***
|
||||
|
||||
.equ SREG_C 0 ; Carry Flag
|
||||
.equ SREG_Z 1 ; Zero Flag
|
||||
.equ SREG_N 2 ; Negative Flag
|
||||
.equ SREG_V 3 ; Two's Complement Overflow Flag
|
||||
.equ SREG_S 4 ; Sign Bit
|
||||
.equ SREG_H 5 ; Half Carry Flag
|
||||
.equ SREG_T 6 ; Bit Copy Storage
|
||||
.equ SREG_I 7 ; Global Interrupt Enable
|
134
avr/ops.txt
134
avr/ops.txt
@ -1,134 +0,0 @@
|
||||
The AVR instruction set is a bit more regular than z80's, which allows us for
|
||||
simpler upcode spitting logic (simplicity which is lost when we need to take
|
||||
into account all AVR models and instruction constraints on each models). This
|
||||
file categorizes all available ops with their opcode signature. X means upcode
|
||||
bit.
|
||||
|
||||
Categories are in descending order of "popularity"
|
||||
|
||||
Mnemonics with "*" are a bit special.
|
||||
|
||||
### 16-bit
|
||||
|
||||
## Plain
|
||||
|
||||
XXXX XXXX XXXX XXXX
|
||||
|
||||
BREAK, CLC, CLH, CLI, CLN, CLS, CLT, CLV, CLZ, EICALL, EIJMP, ELPM*, ICALL,
|
||||
IJMP, NOP, RET, RETI, SEC, SEH, SEI, SEN, SES, SET, SEV, SEZ, SLEEP, SPM*, WDR
|
||||
|
||||
## Rd(5)
|
||||
|
||||
XXXX XXXd dddd XXXX
|
||||
|
||||
ASR, COM, DEC, ELPM*, INC, LAC, LAS, LAT, LD*, LPM*, LSL*, LSR, NEG, POP, PUSH,
|
||||
ROR, ST*, SWAP, XCH
|
||||
|
||||
## Rd(5) + Rr(5)
|
||||
|
||||
XXXX XXrd dddd rrrr
|
||||
|
||||
ADC, ADD, AND, CLR, CP, CPC, CPSE, EOR, MOV, MUL, OR, ROL*, SBC, SUB,
|
||||
TST*
|
||||
|
||||
## k(7)
|
||||
|
||||
XXXX XXkk kkkk kXXX
|
||||
|
||||
BRCC, BRCS, BREQ, BRGE, BRHC, BRHS, BRID, BRIE, BRLO, BRLT, BRMI, BRNE, BRPL,
|
||||
BRSH, BRTC, BRTS, BRVC, BRVS
|
||||
|
||||
## Rd(4) + K(8)
|
||||
|
||||
XXXX KKKK dddd KKKK
|
||||
|
||||
ANDI, CBR*, CPI, LDI, ORI, SBCI, SBR, SUBI
|
||||
|
||||
## Rd(5) + bit
|
||||
|
||||
XXXX XXXd dddd Xbbb
|
||||
|
||||
BLD, BST, SBRC, SBRS
|
||||
|
||||
## A(5) + bit
|
||||
|
||||
XXXX XXXX AAAA Abbb
|
||||
|
||||
CBI, SBI, SBIC, SBIS
|
||||
|
||||
## Rd(3) + Rr(3)
|
||||
|
||||
XXXX XXXX Xddd Xrrr
|
||||
|
||||
FMUL, FMULS, FMULSU, MULSU
|
||||
|
||||
## Rd(4) + Rr(4)
|
||||
|
||||
XXXX XXXX dddd rrrr
|
||||
|
||||
MOVW, MULS
|
||||
|
||||
## Rd(5) + A(6)
|
||||
|
||||
XXXX XAAd dddd AAAA
|
||||
|
||||
IN, OUT
|
||||
|
||||
## Rd(4) + k(7)
|
||||
|
||||
XXXX Xkkk dddd kkkk
|
||||
|
||||
LDS*, STS*
|
||||
|
||||
## Rd(2) + K
|
||||
|
||||
XXXX XXXX KKdd KKKK
|
||||
|
||||
ADIW, SBIW
|
||||
|
||||
## Rd(4)
|
||||
|
||||
XXXX XXXX dddd XXXX
|
||||
|
||||
SER
|
||||
|
||||
## K(4)
|
||||
|
||||
XXXX XXXX KKKK XXXX
|
||||
|
||||
DES
|
||||
|
||||
## k(12)
|
||||
|
||||
XXXX kkkk kkkk kkkk
|
||||
|
||||
RCALL, RJMP
|
||||
|
||||
## SREG
|
||||
|
||||
XXXX XXXX Xsss XXXX
|
||||
|
||||
BCLR, BSET
|
||||
|
||||
## SREG + k(7)
|
||||
|
||||
XXXX XXkk kkkk ksss
|
||||
|
||||
BRBC, BRBS
|
||||
|
||||
### 32-bit
|
||||
|
||||
## k(22)
|
||||
|
||||
XXXX XXXk kkkk XXXk
|
||||
kkkk kkkk kkkk kkkk
|
||||
|
||||
CALL, JMP
|
||||
|
||||
## Rd(5) + k(16)
|
||||
|
||||
XXXX XXXd dddd XXXX
|
||||
kkkk kkkk kkkk kkkk
|
||||
|
||||
LDS*, STS*
|
||||
|
10
avr/tn25.h
10
avr/tn25.h
@ -1,10 +0,0 @@
|
||||
.equ FLASHEND 0x03ff ; Note: Word address
|
||||
.equ IOEND 0x003f
|
||||
.equ SRAM_START 0x0060
|
||||
.equ SRAM_SIZE 128
|
||||
.equ RAMEND 0x00df
|
||||
.equ XRAMEND 0x0000
|
||||
.equ E2END 0x007f
|
||||
.equ EEPROMEND 0x007f
|
||||
.equ EEADRBITS 7
|
||||
|
@ -1,74 +0,0 @@
|
||||
; *** Registers ***
|
||||
|
||||
.equ SREG 0x3f
|
||||
.equ SPH 0x3e
|
||||
.equ SPL 0x3d
|
||||
.equ GIMSK 0x3b
|
||||
.equ GIFR 0x3a
|
||||
.equ TIMSK 0x39
|
||||
.equ TIFR 0x38
|
||||
.equ SPMCSR 0x37
|
||||
.equ MCUCR 0x35
|
||||
.equ MCUSR 0x34
|
||||
.equ TCCR0B 0x33
|
||||
.equ TCNT0 0x32
|
||||
.equ OSCCAL 0x31
|
||||
.equ TCCR1 0x30
|
||||
.equ TCNT1 0x2f
|
||||
.equ OCR1A 0x2e
|
||||
.equ OCR1C 0x2d
|
||||
.equ GTCCR 0x2c
|
||||
.equ OCR1B 0x2b
|
||||
.equ TCCR0A 0x2a
|
||||
.equ OCR0A 0x29
|
||||
.equ OCR0B 0x28
|
||||
.equ PLLCSR 0x27
|
||||
.equ CLKPR 0x26
|
||||
.equ DT1A 0x25
|
||||
.equ DT1B 0x24
|
||||
.equ DTPS 0x23
|
||||
.equ DWDR 0x22
|
||||
.equ WDTCR 0x21
|
||||
.equ PRR 0x20
|
||||
.equ EEARH 0x1f
|
||||
.equ EEARL 0x1e
|
||||
.equ EEDR 0x1d
|
||||
.equ EECR 0x1c
|
||||
.equ PORTB 0x18
|
||||
.equ DDRB 0x17
|
||||
.equ PINB 0x16
|
||||
.equ PCMSK 0x15
|
||||
.equ DIDR0 0x14
|
||||
.equ GPIOR2 0x13
|
||||
.equ GPIOR1 0x12
|
||||
.equ GPIOR0 0x11
|
||||
.equ USIBR 0x10
|
||||
.equ USIDR 0x0f
|
||||
.equ USISR 0x0e
|
||||
.equ USICR 0x0d
|
||||
.equ ACSR 0x08
|
||||
.equ ADMUX 0x07
|
||||
.equ ADCSRA 0x06
|
||||
.equ ADCH 0x05
|
||||
.equ ADCL 0x04
|
||||
.equ ADCSRB 0x03
|
||||
|
||||
|
||||
; *** Interrupt vectors ***
|
||||
|
||||
.equ INT0addr 0x0001 ; External Interrupt 0
|
||||
.equ PCI0addr 0x0002 ; Pin change Interrupt Request 0
|
||||
.equ OC1Aaddr 0x0003 ; Timer/Counter1 Compare Match 1A
|
||||
.equ OVF1addr 0x0004 ; Timer/Counter1 Overflow
|
||||
.equ OVF0addr 0x0005 ; Timer/Counter0 Overflow
|
||||
.equ ERDYaddr 0x0006 ; EEPROM Ready
|
||||
.equ ACIaddr 0x0007 ; Analog comparator
|
||||
.equ ADCCaddr 0x0008 ; ADC Conversion ready
|
||||
.equ OC1Baddr 0x0009 ; Timer/Counter1 Compare Match B
|
||||
.equ OC0Aaddr 0x000a ; Timer/Counter0 Compare Match A
|
||||
.equ OC0Baddr 0x000b ; Timer/Counter0 Compare Match B
|
||||
.equ WDTaddr 0x000c ; Watchdog Time-out
|
||||
.equ USI_STARTaddr 0x000d ; USI START
|
||||
.equ USI_OVFaddr 0x000e ; USI Overflow
|
||||
|
||||
.equ INT_VECTORS_SIZE 15 ; size in words
|
@ -1,9 +0,0 @@
|
||||
.equ FLASHEND 0x07ff ; Note: Word address
|
||||
.equ IOEND 0x003f
|
||||
.equ SRAM_START 0x0060
|
||||
.equ SRAM_SIZE 256
|
||||
.equ RAMEND 0x015f
|
||||
.equ XRAMEND 0x0000
|
||||
.equ E2END 0x00ff
|
||||
.equ EEPROMEND 0x00ff
|
||||
.equ EEADRBITS 8
|
@ -1,9 +0,0 @@
|
||||
.equ FLASHEND 0x0fff ; Note: Word address
|
||||
.equ IOEND 0x003f
|
||||
.equ SRAM_START 0x0060
|
||||
.equ SRAM_SIZE 512
|
||||
.equ RAMEND 0x025f
|
||||
.equ XRAMEND 0x0000
|
||||
.equ E2END 0x01ff
|
||||
.equ EEPROMEND 0x01ff
|
||||
.equ EEADRBITS 9
|
@ -1,20 +0,0 @@
|
||||
# Collapse OS documentation
|
||||
|
||||
## User guide
|
||||
|
||||
* [The shell](../apps/basic/README.md)
|
||||
* [Load code in RAM and run it](load-run-code.md)
|
||||
* [Using block devices](blockdev.md)
|
||||
* [Using the filesystem](fs.md)
|
||||
* [Assembling z80 source from the shell](zasm.md)
|
||||
* [Writing the glue code](glue-code.md)
|
||||
* [Understanding the code](understanding-code.md)
|
||||
|
||||
## Hardware
|
||||
|
||||
Some consolidated documentation about various hardware supported by Collapse OS.
|
||||
Most of that information can already be found elsewhere, but the goal is to have
|
||||
the most vital documentation in one single place.
|
||||
|
||||
* [TI-83+/TI-84+](ti8x.md)
|
||||
* [TRS-80 model 4p](trs80-4p.md)
|
@ -1,63 +0,0 @@
|
||||
# Using block devices
|
||||
|
||||
The `blockdev.asm` part manage what we call "block devices", an abstraction over
|
||||
something that we can read a byte to, write a byte to, optionally at arbitrary
|
||||
offsets.
|
||||
|
||||
A Collapse OS system can define up to `0xff` devices. Those definitions are made
|
||||
in the glue code, so they are static.
|
||||
|
||||
Definition of block devices happen at include time. It would look like:
|
||||
|
||||
[...]
|
||||
BLOCKDEV_COUNT .equ 1
|
||||
#include "blockdev.asm"
|
||||
; List of devices
|
||||
.dw sdcGetB, sdcPutB
|
||||
[...]
|
||||
|
||||
That tells `blockdev` that we're going to set up one device, that its GetB and
|
||||
PutB are the ones defined by `sdc.asm`.
|
||||
|
||||
If your block device is read-only or write-only, use dummy routines. `unsetZ`
|
||||
is a good choice since it will return with the `Z` flag unset, indicating an
|
||||
error (dummy methods aren't supposed to be called).
|
||||
|
||||
Each defined block device, in addition to its routine definition, holds a
|
||||
seek pointer. This seek pointer is used in shell commands described below.
|
||||
|
||||
## Routine definitions
|
||||
|
||||
Parts that implement GetB and PutB do so in a loosely-coupled manner, but
|
||||
they should try to adhere to the convention, that is:
|
||||
|
||||
**GetB**: Get the byte at position specified by `HL`. If it supports 32-bit
|
||||
addressing, `DE` contains the high-order bytes. Return the result in
|
||||
`A`. If there's an error (for example, address out of range), unset
|
||||
`Z`. This routine is not expected to block. We expect the result to be
|
||||
immediate.
|
||||
|
||||
**PutB**: The opposite of GetB. Write the character in `A` at specified
|
||||
position. `Z` unset on error.
|
||||
|
||||
## Shell usage
|
||||
|
||||
`apps/basic/blk.asm` supplies 4 shell commands that you can add to your shell.
|
||||
See "Optional Modules/blk" in [the shell doc](../apps/basic/README.md).
|
||||
|
||||
### Example
|
||||
|
||||
Let's try an example: You glue yourself a Collapse OS with a mmap starting at
|
||||
`0xe000` as your 4th device (like it is in the shell emulator). Here's what you
|
||||
could do to copy memory around:
|
||||
|
||||
> m=0xe000
|
||||
> while m<0xe004 getc:poke m a:m=m+1
|
||||
[enter "abcd"]
|
||||
> bsel 3
|
||||
> i=0
|
||||
> while i<4 getb:puth a:i=i+1
|
||||
61626364> bseek 2
|
||||
> getb:puth a
|
||||
63> getb:puth a
|
||||
64>
|
78
doc/fs.md
78
doc/fs.md
@ -1,78 +0,0 @@
|
||||
# Using the filesystem
|
||||
|
||||
The Collapse OS filesystem (CFS) is a very simple FS that aims at implementation
|
||||
simplicity first. It is not efficient or featureful, but allows you to get
|
||||
play around with the concept of files so that you can conveniently run programs
|
||||
targeting named blocks of data with in storage.
|
||||
|
||||
The filesystem sits on a block device and there can only be one active
|
||||
filesystem at once.
|
||||
|
||||
Files are represented by adjacent blocks of `0x100` bytes with `0x20` bytes of
|
||||
metadata on the first block. That metadata tells the location of the next block
|
||||
which allows for block iteration.
|
||||
|
||||
To create a file, you must allocate blocks to it and these blocks can't be
|
||||
grown (you have to delete the file and re-allocate it). When allocating new
|
||||
files, Collapse OS tries to reuse blocks from deleted files if it can.
|
||||
|
||||
Once "mounted" (turned on with `fson`), you can list files, allocate new files
|
||||
with `fnew`, mark files as deleted with `fdel` and, more importantly, open files
|
||||
with `fopen`.
|
||||
|
||||
Opened files are accessed a independent block devices. It's the glue code that
|
||||
decides how many file handles we'll support and to which block device ID each
|
||||
file handle will be assigned.
|
||||
|
||||
For example, you could have a system with three block devices, one for ACIA and
|
||||
one for a SD card and one for a file handle. You would mount the filesystem on
|
||||
block device `1` (the SD card), then open a file on handle `0` with `fopen 0
|
||||
filename`. You would then do `bsel 2` to select your third block device which
|
||||
is mapped to the file you've just opened.
|
||||
|
||||
## Trying it in the emulator
|
||||
|
||||
The shell emulator in `tools/emul/shell` is geared for filesystem usage. If you
|
||||
look at `shell_.asm`, you'll see that there are 4 block devices: one for
|
||||
console, one for fake storage (`fsdev`) and two file handles (we call them
|
||||
`stdout` and `stdin`, but both are read/write in this context).
|
||||
|
||||
The fake device `fsdev` is hooked to the host system through the `cfspack`
|
||||
utility. Then the emulated shell is started, it checks for the existence of a
|
||||
`cfsin` directory and, if it exists, it packs its content into a CFS blob and
|
||||
shoves it into its `fsdev` storage.
|
||||
|
||||
To, to try it out, do this:
|
||||
|
||||
$ mkdir cfsin
|
||||
$ echo "Hello!" > cfsin/foo
|
||||
$ echo "Goodbye!" > cfsin/bar
|
||||
$ ./shell
|
||||
|
||||
The shell, upon startup, automatically calls `fson` targeting block device `1`,
|
||||
so it's ready to use:
|
||||
|
||||
> fls
|
||||
foo
|
||||
bar
|
||||
> fopen 0 foo
|
||||
> bsel 2
|
||||
> getb
|
||||
> puth a
|
||||
65
|
||||
> getb
|
||||
> puth a
|
||||
6C
|
||||
> getb
|
||||
> puth a
|
||||
6C
|
||||
> getb
|
||||
> puth a
|
||||
6F
|
||||
> getb
|
||||
> puth a
|
||||
21
|
||||
> fdel bar
|
||||
> fls
|
||||
foo
|
||||
>
|
163
doc/glue-code.md
163
doc/glue-code.md
@ -1,163 +0,0 @@
|
||||
# Writing the glue code
|
||||
|
||||
Collapse OS's kernel code is loosely knit. It supplies parts that you're
|
||||
expected to glue together in a "glue code" asm file. Here is what a minimal
|
||||
glue code for a shell on a Classic [RC2014][rc2014] with an ACIA link would
|
||||
look like:
|
||||
|
||||
|
||||
; The RAM module is selected on A15, so it has the range 0x8000-0xffff
|
||||
.equ RAMSTART 0x8000
|
||||
.equ RAMEND 0xffff
|
||||
.equ ACIA_CTL 0x80 ; Control and status. RS off.
|
||||
.equ ACIA_IO 0x81 ; Transmit. RS on.
|
||||
|
||||
jp init
|
||||
|
||||
; interrupt hook
|
||||
.fill 0x38-$
|
||||
jp aciaInt
|
||||
|
||||
.inc "err.h"
|
||||
.inc "ascii.h"
|
||||
.inc "core.asm"
|
||||
.inc "str.asm"
|
||||
.inc "parse.asm"
|
||||
.equ ACIA_RAMSTART RAMSTART
|
||||
.inc "acia.asm"
|
||||
|
||||
.equ STDIO_RAMSTART ACIA_RAMEND
|
||||
.equ STDIO_GETC aciaGetC
|
||||
.equ STDIO_PUTC aciaPutC
|
||||
.inc "stdio.asm"
|
||||
|
||||
; *** BASIC ***
|
||||
|
||||
; RAM space used in different routines for short term processing.
|
||||
.equ SCRATCHPAD_SIZE 0x20
|
||||
.equ SCRATCHPAD STDIO_RAMEND
|
||||
.inc "lib/util.asm"
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "lib/fmt.asm"
|
||||
.equ EXPR_PARSE parseLiteralOrVar
|
||||
.inc "lib/expr.asm"
|
||||
.inc "basic/util.asm"
|
||||
.inc "basic/parse.asm"
|
||||
.inc "basic/tok.asm"
|
||||
.equ VAR_RAMSTART SCRATCHPAD+SCRATCHPAD_SIZE
|
||||
.inc "basic/var.asm"
|
||||
.equ BUF_RAMSTART VAR_RAMEND
|
||||
.inc "basic/buf.asm"
|
||||
.equ BAS_RAMSTART BUF_RAMEND
|
||||
.inc "basic/main.asm"
|
||||
|
||||
init:
|
||||
di
|
||||
; setup stack
|
||||
ld sp, RAMEND
|
||||
im 1
|
||||
|
||||
call aciaInit
|
||||
call basInit
|
||||
ei
|
||||
jp basStart
|
||||
|
||||
Once this is written, you can build it with `zasm`, which takes code from stdin
|
||||
and spits binary to stdout. Because out code has includes, however, you need
|
||||
to supply zasm with include folders or files. The invocation would look like
|
||||
|
||||
emul/zasm/zasm kernel/ apps/ < glue.asm > collapseos.bin
|
||||
|
||||
## Building zasm
|
||||
|
||||
Collapse OS has its own assembler written in z80 assembly. We call it
|
||||
[zasm][zasm]. Even on a "modern" machine, it is that assembler that is used,
|
||||
but because it is written in z80 assembler, it needs to be emulated (with
|
||||
[libz80][libz80]).
|
||||
|
||||
So, the first step is to build zasm. Open `emul/README.md` and follow
|
||||
instructions there.
|
||||
|
||||
## Platform constants
|
||||
|
||||
The upper part of the code contains platform-related constants, information
|
||||
related to the platform you're targeting. You might want to put it in an
|
||||
include file if you're writing multiple glue code that targets the same machine.
|
||||
|
||||
In all cases, `RAMSTART` are necessary. `RAMSTART` is the offset at which
|
||||
writable memory begins. This is where the different parts store their
|
||||
variables.
|
||||
|
||||
`RAMEND` is the offset where writable memory stop. This is generally
|
||||
where we put the stack, but as you can see, setting up the stack is the
|
||||
responsibility of the glue code, so you can set it up however you wish.
|
||||
|
||||
`ACIA_*` are specific to the `acia` part. Details about them are in `acia.asm`.
|
||||
If you want to manage ACIA, you need your platform to define these ports.
|
||||
|
||||
## Header code
|
||||
|
||||
Then comes the header code (code at `0x0000`), a task that also is in the glue
|
||||
code's turf. `jr init` means that we run our `init` routine on boot.
|
||||
|
||||
`jp aciaInt` at `0x38` is needed by the `acia` part. Collapse OS doesn't dictate
|
||||
a particular interrupt scheme, but some parts might. In the case of `acia`, we
|
||||
require to be set in interrupt mode 1.
|
||||
|
||||
## Includes
|
||||
|
||||
This is the most important part of the glue code and it dictates what will be
|
||||
included in your OS. Each part is different and has a comment header explaining
|
||||
how it works, but there are a couple of mechanisms that are common to all.
|
||||
|
||||
### Defines
|
||||
|
||||
Parts can define internal constants, but also often document a "Defines" part.
|
||||
These are constant that are expected to be set before you include the file.
|
||||
|
||||
See comment in each part for details.
|
||||
|
||||
### RAM management
|
||||
|
||||
Many parts require variables. They need to know where in RAM to store these
|
||||
variables. Because parts can be mixed and matched arbitrarily, we can't use
|
||||
fixed memory addresses.
|
||||
|
||||
This is why each part that needs variable define a `<PARTNAME>_RAMSTART`
|
||||
constant that must be defined before we include the part.
|
||||
|
||||
Symmetrically, each part define a `<PARTNAME>_RAMEND` to indicate where its
|
||||
last variable ends.
|
||||
|
||||
This way, we can easily and efficiently chain up the RAM of every included part.
|
||||
|
||||
### Tables grafting
|
||||
|
||||
A mechanism that is common to some parts is "table grafting". If a part works
|
||||
on a list of things that need to be defined by the glue code, it will place a
|
||||
label at the very end of its source file. This way, it becomes easy for the
|
||||
glue code to "graft" entries to the table. This approach, although simple and
|
||||
effective, only works for one table per part. But it's often enough.
|
||||
|
||||
For example, to define block devices:
|
||||
|
||||
[...]
|
||||
.equ BLOCKDEV_COUNT 4
|
||||
.inc "blockdev.asm"
|
||||
; List of devices
|
||||
.dw fsdevGetB, fsdevPutB
|
||||
.dw stdoutGetB, stdoutPutB
|
||||
.dw stdinGetB, stdinPutB
|
||||
.dw mmapGetB, mmapPutB
|
||||
[...]
|
||||
|
||||
### Initialization
|
||||
|
||||
Then, finally, comes the `init` code. This can be pretty much anything really
|
||||
and this much depends on the part you select. But if you want a shell, you will
|
||||
usually end it with `basStart`, which never returns.
|
||||
|
||||
[rc2014]: https://rc2014.co.uk/
|
||||
[zasm]: ../emul/README.md
|
||||
[libz80]: https://github.com/ggambetta/libz80
|
@ -1,127 +0,0 @@
|
||||
# Load code in RAM and run it
|
||||
|
||||
Collapse OS likely runs from ROM code. If you need to fiddle with your machine
|
||||
more deeply, you will want to send arbitrary code to it and run it. You can do
|
||||
so with the shell's `poke` and `usr` commands.
|
||||
|
||||
For example, let's say that you want to run this simple code that you have
|
||||
sitting on your "modern" machine and want to execute on your running Collapse OS
|
||||
machine:
|
||||
|
||||
ld a, (0xa100)
|
||||
inc a
|
||||
ld (0xa100), a
|
||||
ret
|
||||
|
||||
(we must always return at the end of code that we call with `usr`). This will
|
||||
increase a number at memory address `0xa100`. First, compile it:
|
||||
|
||||
zasm < tosend.asm > tosend.bin
|
||||
|
||||
Now, we'll send that code to address `0xa000`:
|
||||
|
||||
> m=0xa000
|
||||
> while m<0xa008 getc:poke m a:m=m+1
|
||||
(resulting binary is 8 bytes long)
|
||||
|
||||
Now, at this point, it's a bit delicate. To pipe your binary to your serial
|
||||
connection, you have to close `screen` with CTRL+A then `:quit` to free your
|
||||
tty device. Then, you can run:
|
||||
|
||||
cat tosend.bin > /dev/ttyUSB0 (or whatever is your device)
|
||||
|
||||
You can then re-open your connection with screen. You'll have a blank screen,
|
||||
but if the number of characters sent corresponds to what you gave `poke`, then
|
||||
Collapse OS will be waiting for a new command. Go ahead, verify that the
|
||||
transfer was successful with:
|
||||
|
||||
> peek 0a000
|
||||
> puth a
|
||||
3A
|
||||
> peek 0a007
|
||||
> puth a
|
||||
C9
|
||||
|
||||
Good! Now, we can try to run it. Before we run it, let's peek at the value at
|
||||
`0xa100` (being RAM, it's random):
|
||||
|
||||
> peek 0xa100
|
||||
> puth a
|
||||
61
|
||||
|
||||
So, we'll expect this to become `62` after we run the code. Let's go:
|
||||
|
||||
> usr 0xa100
|
||||
> peek 0xa100
|
||||
> puth a
|
||||
62
|
||||
|
||||
Success!
|
||||
|
||||
## The upload tool
|
||||
|
||||
The serial connection is not always 100% reliable and a bad byte can slip in
|
||||
when you push your code and that's not fun when you try to debug your code (is
|
||||
this bad behavior caused by my logic or by a bad serial upload?). Moreover,
|
||||
sending contents manually can be a hassle.
|
||||
|
||||
To this end, there is a `upload` file in `tools/` (run `make` to build it) that
|
||||
takes care of loading the file and verify the contents. So, instead of doing
|
||||
`getc` followed by `poke` followed by your `cat` above, you would have done:
|
||||
|
||||
./upload /dev/ttyUSB0 a000 tosend.bin
|
||||
|
||||
This clears your basic listing and then types in a basic algorithm to receive
|
||||
and echo and pre-defined number of bytes. The `upload` tool then sends and read
|
||||
each byte, verifying that they're the same. Very handy.
|
||||
|
||||
## Labels in RAM code
|
||||
|
||||
If your code contains any label, make sure that you add a `.org` directive at
|
||||
the beginning of your code with the address you're planning on uploading your
|
||||
code to. Otherwise, those labels are going to point to wrong addresses.
|
||||
|
||||
## Calling ROM code
|
||||
|
||||
The ROM you run Collapse OS on already has quite a bit of code in it, some of
|
||||
it could be useful to programs you run from RAM.
|
||||
|
||||
If you know exactly where a routine lives in the ROM, you can `call` the address
|
||||
directly, no problem. However, getting this information is tedious work and is
|
||||
likely to change whenever you change the kernel code.
|
||||
|
||||
A good approach is to define yourself a jump table that you put in your glue
|
||||
code. A good place for this is in the `0x03` to `0x37` range, which is empty
|
||||
anyways (unless you set yourself up with some `rst` jumps) and is needed to
|
||||
have a proper interrupt hook at `0x38`. For example, your glue code could look
|
||||
like (important fact: `jp <addr>` uses 3 bytes):
|
||||
|
||||
jp init
|
||||
; JUMP TABLE
|
||||
jp printstr
|
||||
jp aciaPutC
|
||||
|
||||
.fill 0x38-$
|
||||
jp aciaInt
|
||||
|
||||
init:
|
||||
[...]
|
||||
|
||||
It then becomes easy to build yourself a predictable and stable jump header,
|
||||
something you could call `jumptable.inc`:
|
||||
|
||||
.equ JUMP_PRINTSTR 0x03
|
||||
.equ JUMP_ACIAPUTC 0x06
|
||||
|
||||
You can then include that file in your "user" code, like this:
|
||||
|
||||
#include "jumptable.inc"
|
||||
.org 0xa000
|
||||
ld hl, label
|
||||
call JUMP_PRINTSTR
|
||||
ret
|
||||
|
||||
label: .db "Hello World!", 0
|
||||
|
||||
If you load that code at `0xa000` and call it, it will print "Hello World!" by
|
||||
using the `printstr` routine from `core.asm`.
|
38
doc/ti8x.md
38
doc/ti8x.md
@ -1,38 +0,0 @@
|
||||
# TI-83+/TI-84+
|
||||
|
||||
Texas Instruments is well known for its calculators. Among those, two models
|
||||
are particularly interesting to us because they have a z80 CPU: the TI-83+ and
|
||||
TI-84+ (the "+" is important).
|
||||
|
||||
They lack accessible I/O ports, but they have plenty of flash and RAM. Collapse
|
||||
OS runs on it (see `recipes/ti84`).
|
||||
|
||||
I haven't opened one up yet, but apparently, they have limited scavenging value
|
||||
because its z80 CPU is packaged in a TI-specific chip. Due to its sturdy design,
|
||||
and its ample RAM and flash, we could imagine it becoming a valuable piece of
|
||||
equipment if found intact.
|
||||
|
||||
The best pre-collapse ressource about it is
|
||||
[WikiTI](http://wikiti.brandonw.net/index.php).
|
||||
|
||||
## Getting software on it
|
||||
|
||||
Getting software to run on it is a bit tricky because it needs to be signed
|
||||
with TI-issued private keys. Those keys have long been found and are included
|
||||
in `recipes/ti84`. With the help of the
|
||||
[mktiupgrade](https://github.com/KnightOS/mktiupgrade), an upgrade file can be
|
||||
prepared and then sent through the USB port with the help of
|
||||
[tilp](http://lpg.ticalc.org/prj_tilp/).
|
||||
|
||||
That, however, requires a modern computing environment. As of now, there is no
|
||||
way of installing Collapse OS on a TI-8X+ calculator from another Collapse OS
|
||||
system.
|
||||
|
||||
Because it is not on the roadmap to implement complex cryptography in Collapse
|
||||
OS, the plan is to build a series of pre-signed bootloader images. The
|
||||
bootloader would then receive data through either the Link jack or the USB port
|
||||
and write that to flash (I haven't verified that yet, but I hope that data
|
||||
written to flash this way isn't verified cryptographically by the calculator).
|
||||
|
||||
As modern computing fades away, those pre-signed binaries would become opaque,
|
||||
but at least, would allow bootstrapping from post-modern computers.
|
243
doc/trs80-4p.md
243
doc/trs80-4p.md
@ -1,243 +0,0 @@
|
||||
# TRS-80 Model 4p
|
||||
|
||||
## Ports
|
||||
|
||||
Address Read Write
|
||||
FC-FF Cassette in Cassette out, resets
|
||||
F8-FB Rd printer status Wr to printer
|
||||
F4-F7 - Drive select
|
||||
F3 FDC data reg FDC data reg
|
||||
F2 FDC sector reg FDC sector reg
|
||||
F1 FDC track reg FDC track reg
|
||||
F0 FDC status reg FDC cmd reg
|
||||
EC-EF Reset RTC INT Mode output
|
||||
EB RS232 recv holding reg RS232 xmit holding reg
|
||||
EA UART status reg UART/modem control
|
||||
E9 - Baud rate register
|
||||
E8 Modem status Master reset/enable
|
||||
UART control reg
|
||||
E4-E7 Rd NMI status Wr NMI mask reg
|
||||
E0-E3 Rd INT status Wr INT mask reg
|
||||
CF HD status HD cmd
|
||||
CE HD size/drv/hd HD size/drv/hd
|
||||
CD HD cylinder high HD cylinder high
|
||||
CC HD cylinder low HD cylinder low
|
||||
CB HD sector # HD sector #
|
||||
CA HD sector cnt HD sector cnt
|
||||
C9 HD error reg HD write precomp
|
||||
C8 HD data reg HD data reg
|
||||
C7 HD CTC chan 3 HD CTC chan 3
|
||||
C6 HD CTC chan 2 HD CTC chan 2
|
||||
C5 HD CTC chan 1 HD CTC chan 1
|
||||
C4 HD CTC chan 0 HD CTC chan 0
|
||||
C2-C3 HD device ID -
|
||||
C1 HD control reg HD Control reg
|
||||
C0 HD wr prot reg -
|
||||
94-9F - -
|
||||
90-93 - Sound option
|
||||
8C-8F Graphic sel 2 Graphic sel 2
|
||||
8B CRTC Data reg CRTC Data reg
|
||||
8A CRTC Control reg CRTC Control reg
|
||||
89 CRTC Data reg CRTC Data reg
|
||||
88 CRTC Control reg CRTC Control reg
|
||||
84-87 - Options reg
|
||||
83 - Graphic X reg
|
||||
82 - Graphic Y reg
|
||||
81 Graphics RAM Graphics RAM
|
||||
80 - Graphics options reg
|
||||
|
||||
Bit map
|
||||
|
||||
Address D7 D6 D5 D4 D3 D2 D1 D0
|
||||
F8-FB-Rd Busy Paper Select Fault - - - -
|
||||
EC-EF-Rd (any read causes reset of RTC interrupt)
|
||||
EC-EF-Wr - CPU - Enable Enable Mode Cass -
|
||||
Fast EX I/O Altset Select Mot on
|
||||
E0-E3-Rd - Recv Recv Xmit 10 Bus RTC C Fall C Rise
|
||||
Error Data Empty int Int Int Int
|
||||
E0-E3-Wr - Enable Enable En.Xmit Enable Enable Enable Enable
|
||||
Rec err Rec dat Emp 10 int RTC int CF int CR int
|
||||
90-93-Wr - - - - - - - Sound
|
||||
Bit
|
||||
84-87-Wr Page Fix upr Memory Memory Invert 80/64 Select Select
|
||||
mem bit 1 bit 0 video Bit 1 Bit 0
|
||||
|
||||
## System memory map
|
||||
|
||||
### Memory map 1 - model III mode
|
||||
|
||||
0000-1fff ROM A (8K)
|
||||
2000-2fff ROM B (4K)
|
||||
3000-37ff ROM C (2K) - less 37e8/37e9
|
||||
37e8-37e9 Printer Status Port
|
||||
3800-3bff Keyboard
|
||||
3c00-3fff Video RAM (page bit selects 1K or 2K)
|
||||
4000-7fff RAM (16K system)
|
||||
4000-ffff RAM (64K system)
|
||||
|
||||
### Memory map 2
|
||||
|
||||
0000-37ff RAM (14K)
|
||||
3800-3bff Keyboard
|
||||
3c00-3fff Video RAM
|
||||
4000-7fff RAM (16K) end of one 32K bank
|
||||
8000-ffff RAM (32K) second 32K bank
|
||||
|
||||
### Memory map 3
|
||||
|
||||
0000-7fff RAM (32K) bank 1
|
||||
8000-f3ff RAM (29K) bank 2
|
||||
f400-f7ff Keyboard
|
||||
f800-ffff Video RAM
|
||||
|
||||
### Memory map 4
|
||||
|
||||
0000-7fff RAM (32K) bank 1
|
||||
8000-ffff RAM (32K) bank 2
|
||||
|
||||
## TRSDOS memory map
|
||||
|
||||
0000-25ff Reserved for TRSDOS operations
|
||||
2600-2fff Overlay area
|
||||
3000-HIGH Free to use
|
||||
HIGH-ffff Drivers, filters, etc
|
||||
|
||||
Use `MEMORY` command to know value of `HIGH`
|
||||
|
||||
## Supervisor calls
|
||||
|
||||
SVC are made by loading the correct SVC number in A, other params in other regs,
|
||||
and then call `rst 0x28`.
|
||||
|
||||
Z is pretty much always used for success or as a boolean indicator. It is
|
||||
sometimes not specified when there's not enough tabular space, but it's there.
|
||||
When `-` is specified, it means that the routine either never returns or is
|
||||
always successful.
|
||||
|
||||
Num Name Args Res Desc
|
||||
00 IPL - - Reboot the system
|
||||
01 KEY - AZ Scan *KI, wait for char
|
||||
02 DSP C=char AZ Display character
|
||||
03 GET DE=F/DCB AZ Get one byte from device or file
|
||||
04 PUT DE=F/DCB C=char AZ Write one byte to device or file
|
||||
05 CTL DE=DBC C=func CAZ Output a control byte
|
||||
06 PRT C=char AZ Send character to printer
|
||||
07 WHERE - HL Locate origin of SVC
|
||||
08 KBD - AZ Scan keyboard and return
|
||||
09 KEYIN HL=buf b=len c=0 HLBZ Accept a line of input
|
||||
0a DSPLY HL=str AZ Display message line
|
||||
0b LOGER HL=str AZ Issue log message
|
||||
0c LOGOT HL=str AZ Display and log message
|
||||
0d MSG DE=F/DCB HL=str AZ Send message to device
|
||||
0e PRINT HL=str AZ Print message line
|
||||
0f VDCTL special spc Video functions
|
||||
10 PAUSE BC=delay - Suspend program execution
|
||||
11 PARAM DE=ptbl HL=str Z Parse parameter string
|
||||
12 DATE HL=recvbuf HLDE Get date
|
||||
13 TIME HL=recvbuf HLDE Get time
|
||||
14 CHNIO IX=DCB B=dir C=char - Pass control to next module in device chain
|
||||
15 ABORT - - Abort Program
|
||||
16 EXIT HL=retcode - Exit to TRSDOS
|
||||
18 CMNDI HL=cmd - Exec Cmd w/ return to system
|
||||
19 CMNDR HL=cmd HL Exec Cmd
|
||||
1a ERROR C=errno - Entry to post an error message
|
||||
1b DEBUG - - Enter DEBUG
|
||||
1c CKTSK C=slot Z Check if task slot in use
|
||||
1d ADTSK C=slot - Remove interrupt level task
|
||||
1e RMTSK DE=TCB C=slot - Add an interrupt level task
|
||||
1f RPTSK - - Replace task vector
|
||||
20 KLTSK - - Remove currently executing task
|
||||
21 CKDRV C=drvno Z Check drive
|
||||
22 DODIR C=drvno b=func ZBHL Do directory display/buffer
|
||||
23 RAMDIR HL=buf B=dno C=func AZ Get directory record or free space
|
||||
28 DCSTAT C=drvno Z Test if drive assigned in DCT
|
||||
29 SLCT C=drvno AZ Select a new drive
|
||||
2a DCINIT C=drvno AZ Initialize the FDC
|
||||
2b DCRES C=drvno AZ Reset the FDC
|
||||
2c RSTOR C=drvno AZ Issue a FDC RESTORE command
|
||||
2d STEPI C=drvno AZ Issue a FDC STEP IN command
|
||||
2e SEEK C=drvno DE=addr - Seek a cylinder
|
||||
2f RSLCT C=drvno - Test for drive busy
|
||||
30 RDHDR HL=buf DCE=addr AZ Read a sector header
|
||||
31 RDSEC HL=buf DCE=addr AZ Read a sector
|
||||
32 VRSEC DCE=addr AZ Verify sector
|
||||
33 RDTRK HL=buf DCE=addr AZ Read a track
|
||||
34 HDFMT C=drvno AZ Hard disk format
|
||||
35 WRSEC HL=buf DCE=addr AZ Write a sector
|
||||
36 WRSSC HL=buf DCE=addr AZ Write system sector
|
||||
37 WRTRK HL=buf DCE=addr AZ Write a track
|
||||
38 RENAM DE=FCB HL=str AZ Rename file
|
||||
39 REMOV DE=D/FCB AZ Remove file or device
|
||||
3a INIT HL=buf DE=FCB B=LRL AZ Open or initialize file
|
||||
3b OPEN HL=buf DE=FCB B=LRL AZ Open existing file or device
|
||||
3c CLOSE DE=FCB/DCB AZ Close a file or device
|
||||
3d BKSP DE=FCB AZ Backspace one logical record
|
||||
3e CKEOF DE=FCB AZ Check for EOF
|
||||
3f LOC DE=FCB BCAZ Calculate current logical record number
|
||||
40 LOF DE=FCB BCAZ Calculate the EOF logical record number
|
||||
41 PEOF DE=FCB AZ Position to end of file
|
||||
42 POSN DE=FCB BC=LRN AZ Position file
|
||||
43 READ DE=FCB HL=ptr AZ Read a record
|
||||
44 REW DE=FCB AZ Rewind file to beginning
|
||||
45 RREAD DE=FCB AZ Reread sector
|
||||
46 RWRIT DE=FCB AZ Rewrite sector
|
||||
47 SEEKSC DE=FCB - Seek cylinder and sector of record
|
||||
48 SKIP DE=FCB AZ Skip a record
|
||||
49 VER DE=FCB HLAZ Write and verify a record
|
||||
4a WEOF DE=FCB AZ Write end of file
|
||||
4b WRITE DE=FCB HL=ptr AZ Write a record
|
||||
4c LOAD DE=FCB HLAZ Load program file
|
||||
4d RUN DE=FCB HLAZ Run program file
|
||||
4e FSPEC HL=buf DE=F/DCB HLDE Assign file or device specification
|
||||
4f FEXT DE=FCB HL=str - Set up default file extension
|
||||
50 FNAME DE=buf B=DEC C=drv AZHL Get filename
|
||||
51 GTDCT C=drvno IY Get drive code table address
|
||||
52 GTDCB DE=devname HLAZ Get device control block address
|
||||
53 GTMOD DE=modname HLDE Get memory module address
|
||||
55 RDSSC HL=buf DCE=addr AZ Read system sector
|
||||
57 DIRRD B=dirent C=drvno HLAZ Directory record read
|
||||
58 DIRWR B=dirent C=drvno HLAZ Directory record write
|
||||
5a MUL8 C*E A Multiply C by E
|
||||
5b MUL16 HL*C HLA Multiply HL by C
|
||||
5d DIV8 E/C AE Divides E by C
|
||||
5e DIV16 HL/C HLA Divides HL by C
|
||||
60 DECHEX HL=str BCHL Convert Decimal ASCII to binary
|
||||
61 HEXDEC HL=num DE=buf DE Convert binary to decimal ASCII
|
||||
62 HEX8 C=num HL=buf HL Convert 1 byte to hex ASCII
|
||||
53 HEX16 DE=num HL=buf HL Convert 2 bytes to hex ASCII
|
||||
64 HIGH$ B=H/L HL=get/set HLAZ Get or Set HIGH$/LOW$
|
||||
65 FLAGS - IY Point IY to system flag table
|
||||
66 BANK B=func C=bank BZ Memory bank use
|
||||
67 BREAK HL=vector HL Set Break vector
|
||||
68 SOUND B=func - Sound generation
|
||||
|
||||
## Personal reverse engineering
|
||||
|
||||
This section below contains notes about my personal reverse engineering efforts.
|
||||
I'm not an expert in this, and also, I might not be aware of existing, better
|
||||
documentation making this information useless.
|
||||
|
||||
### Bootable disk
|
||||
|
||||
I'm wondering what makes a disk bootable to the TRS-80 and how it boots it.
|
||||
When I read the raw contents of the first sector of the first cylinder of the
|
||||
TRS-DOS disk, I see that, except for the 3 first bytes (`00fe14`), the rest of
|
||||
the contents is exactly the same as what is at memory offset `0x0203`, which
|
||||
seems to indicates that the bootloader simply loads that contents to memory,
|
||||
leaving the first 3 bytes of RAM to either random contents or some predefined
|
||||
value (I have `f8f800`).
|
||||
|
||||
A non-bootable disk starts with `00fe14`, but we can see the message "Cannot
|
||||
boot, DA TA DISK!" at offset `0x2a`.
|
||||
|
||||
I'm not sure what `00fe14` can mean. Disassembled, it's
|
||||
`nop \ rst 0x28 \ ld b, c`. It makes sense that booting would start with a
|
||||
service call with parameters set by the bootloader (so we don't know what that
|
||||
service call actually is), but I'm not sure it's what happens.
|
||||
|
||||
I don't see any reference to the `0x2a` offset in the data from the first
|
||||
sector, but anyways, booting with the non-bootable disk doesn't actually prints
|
||||
the aformentioned message, so it might be a wild goose chase.
|
||||
|
||||
In any case, making a disk bootable isn't a concern as long as Collapse OS uses
|
||||
the TRS-DOS drivers.
|
@ -1,144 +0,0 @@
|
||||
# Understanding the code
|
||||
|
||||
One of the design goals of Collapse OS is that its code base should be easily
|
||||
understandable in its entirety. Let's help with this with a little walthrough.
|
||||
We use the basic `rc2014` recipe as a basis for the walkthrough.
|
||||
|
||||
This walkthrough assumes that you know z80 assembly. It is recommended that you
|
||||
read code conventions in `CODE.md` first.
|
||||
|
||||
Code snippets aren't reproduced here. You have to follow along with code
|
||||
listing.
|
||||
|
||||
## Power on
|
||||
|
||||
You have a RC2014 classic built with an EEPROM that has the recipe's binary on
|
||||
it and you're linked to its serial I/O module. What happens when you power it
|
||||
on and press the reset button (I've always had to press the reset button for
|
||||
the RC2014 to power on properly. I don't know why. Must be some tricky sync
|
||||
issue with the components)?
|
||||
|
||||
A freshly booted Z80 starts executing address zero. That address is in your
|
||||
glue code. The first thing it does is thus `jp init`. Initialization is handled
|
||||
by `recipes/rc2014/glue.asm`.
|
||||
|
||||
As you can see, it's a fairly straightforward init. Stack at the end of RAM,
|
||||
interrupt mode 1 (which we use for the ACIA), then individual module
|
||||
initialization, and finally, BASIC's runloop.
|
||||
|
||||
## ACIA init
|
||||
|
||||
An Asynchronous Communication Interface Adaptor allows serial communication with
|
||||
another ACIA (ref http://alanclements.org/serialio.html ). The RC2014 uses a
|
||||
6850 ACIA IC and Collapse OS's `kernel/acia` module was written to interface
|
||||
with this kind of IC.
|
||||
|
||||
For this module to work, it needs to be wired to the z80 but in a particular
|
||||
manner (which oh! surprise, the RC2014's Serial I/O module is...): It should use
|
||||
two ports, R/W. One for access to its status register and one for its access to
|
||||
its data register. Also, its `INT` line should be wired to the z80 `INT` line
|
||||
for interrupts to work.
|
||||
|
||||
I won't go into much detail about the wiring: the 6850 seems to have been
|
||||
designed to be wired thus, so it would kind of be like stating the obvious.
|
||||
|
||||
`aciaInit` in `kernel/acia` is also straightforward. First, it initializes the
|
||||
input buffer. This buffer is a circular buffer that is filled with high priority
|
||||
during the interrupt handler at `aciaInt`. It's important that we process input
|
||||
at high priority to be sure not to miss a byte (there is no buffer overrun
|
||||
handling in `acia`. Unhandled data is simply lost).
|
||||
|
||||
That buffer will later be emptied by BASIC's main loop.
|
||||
|
||||
Once the input buffer is set up, all that is left is to set up the ACIA itself,
|
||||
which is configurable through `ACIA_CTL`. Comments in the code are
|
||||
self-explanatory. Make sure that you use serial config, on the other side, that
|
||||
is compatible with this config there.
|
||||
|
||||
## BASIC init
|
||||
|
||||
Then comes `basInit` at `apps/basic/main`. This is a bigger app, so there is
|
||||
more stuff to initialize, but still, it stays straightforward. I'm not going to
|
||||
explain every line, but give you a recipe for understanding. Every variable as,
|
||||
above its declaration line, a comment explaining what it does. Refer to it.
|
||||
|
||||
This init method is the first one we see that has sub-methods in it. To quickly
|
||||
find where they live, be aware that the general convention in Collapse OS code
|
||||
is to prefix every label with its module name. So, for example, `varInit` lives
|
||||
in `apps/basic/var`.
|
||||
|
||||
You can also see, in the initialization of `BAS_FINDHOOK`, a common idiom: the
|
||||
use of `unsetZ` (from `kernel/core`) as a noop that returns an error (in this
|
||||
case, it just means "command not found").
|
||||
|
||||
## Sending the prompt
|
||||
|
||||
We're now entering `basStart`, which simply prints Collapse OS' prompt and then
|
||||
enter its runloop. Let's examine what happens when we call `printstr` (from
|
||||
`kernel/stdio`).
|
||||
|
||||
`printstr` itself is easy. It iterates over `(HL)` and calls `STDIO_PUTC` for
|
||||
each char.
|
||||
|
||||
But what is `STDIO_PUTC`? It's a glue-defined routine. Let's go back to
|
||||
`glue.asm`. You see that `.equ STDIO_PUTC aciaPutC` line is? Well, there you
|
||||
have it. `call STDIO_PUTC`, in our context, is the exact equivalent of
|
||||
`call aciaPutC`. Let's go see it.
|
||||
|
||||
Whew! it's straightforward! We do two things here: wait until the ACIA is ready
|
||||
to transmit (if it's not, it means that it's still in the process of
|
||||
transmitting the previous character we asked it to transmit), then send that
|
||||
char straight to the data port.
|
||||
|
||||
## BASIC's runloop
|
||||
|
||||
Once the prompt is sent, we're entering BASIC's runloop at `basLoop`. This loops
|
||||
forever.
|
||||
|
||||
The first thing it does is to wait for a line to be entered using
|
||||
`stdioReadLine` from `kernel/stdio`. Let's see what this does.
|
||||
|
||||
Oh, this is a little less straightforward. This routine repeatedly calls
|
||||
`STDIO_GETC` and puts the result in a stdio-specific buffer, after having echoed
|
||||
back the received character so that the user sees what she types.
|
||||
|
||||
`STDIO_GETC` is blocking. It always returns a char.
|
||||
|
||||
As you can see in the glue unit, `STDIO_GETC` is mapped to `aciaGetC`. This
|
||||
routine waits until the ACIA buffer has something in it. Once it does, it reads
|
||||
one character from it and returns it.
|
||||
|
||||
Back to `stdioReadLine`, we check that we don't have special handling to do,
|
||||
that is, end of line or deletion. If we don't, we echo back the char, advance
|
||||
buffer pointer, wait for a new one.
|
||||
|
||||
If we receive a CR or LF, the line is complete, so we return to `basLoop` with
|
||||
a null-terminated input line in `(HL)`.
|
||||
|
||||
I won't cover the processing of the line by BASIC because it's a bit long and
|
||||
doesn't help holistic understanding very much, You can read the code.
|
||||
|
||||
Once the line is processed, that the associated command is found and called, we
|
||||
go back the the beginning of the loop for another ride.
|
||||
|
||||
## When do we receive a character?
|
||||
|
||||
In the above section, we simply wait until the buffer has something in it. But
|
||||
how will that happen? Through `aciaInt` interrupt.
|
||||
|
||||
When the ACIA receives a new character, it pulls the `INT` line low, which, in
|
||||
interrupt mode 1, calls `0x38`. In our glue code, we jump to `aciaInt`.
|
||||
|
||||
In `aciaInt`, the first thing we do is to check that we're concerned (the `INT`
|
||||
line can be triggered by other peripherals and we want to ignore those). To do
|
||||
so, we poll ACIA's status register and see if its receive buffer is full.
|
||||
|
||||
If yes, then we fetch that char from ACIA, put it in the buffer and return from
|
||||
interrupt. That's how the buffer gets full.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This walkthrough covers only one simple case, but I hope that it gives you keys
|
||||
to understanding the whole of Collapse OS. You should be able to start from any
|
||||
other recipe's glue code and walk through it in a way that is similar to what
|
||||
we've made here.
|
26
doc/zasm.md
26
doc/zasm.md
@ -1,26 +0,0 @@
|
||||
# Assembling z80 source from the shell
|
||||
|
||||
In its current state, Collapse OS has all you need to assemble z80 source
|
||||
from within the shell. What you need is:
|
||||
|
||||
* A mounted filesystem with `zasm` on it.
|
||||
* A block device to read from (can be a file from mounted CFS)
|
||||
* A block device to write to (can also be a file).
|
||||
|
||||
The emulated shell is already set up with all you need. If you want to run that
|
||||
on a real machine, you'll have to make sure to provide these requirements.
|
||||
|
||||
The emulated shell has a `hello.asm` file in its mounted filesystem that is
|
||||
ready to compile. It has two file handles 0 and 1, mapped to blk IDs 1 and 2.
|
||||
We will open our source file in handle 0 and our dest file in handle 1. Then,
|
||||
with the power of the `fs` module's autoloader, we'll load our newly compiled
|
||||
file and execute it!
|
||||
|
||||
Collapse OS
|
||||
> fnew 1 dest ; create destination file
|
||||
> fopen 0 hello.asm ; open source file in handle 0
|
||||
> fopen 1 dest ; open dest binary in handle 1
|
||||
> zasm 1 2 ; assemble source file into binary file
|
||||
> dest ; call newly compiled file
|
||||
Assembled from the shell
|
||||
> ; Awesome!
|
@ -1,5 +0,0 @@
|
||||
10 print "Count to 10"
|
||||
20 a=0
|
||||
30 a=a+1
|
||||
40 print a
|
||||
50 if a<10 goto 30
|
@ -1,10 +0,0 @@
|
||||
.inc "user.h"
|
||||
.org USER_CODE
|
||||
ld hl, sAwesome
|
||||
call printstr
|
||||
xor a ; success
|
||||
ret
|
||||
|
||||
sAwesome:
|
||||
.db "Assembled from the shell", 0x0d, 0x0a, 0
|
||||
|
@ -1,3 +0,0 @@
|
||||
The contents of this folder ends up in the emulated shell's fake block device,
|
||||
mounted as a CFS. The goal of the emulated shell being to tests apps, we compile
|
||||
all apps into this folder for use in the emulated shell.
|
@ -1,178 +0,0 @@
|
||||
.inc "blkdev.h"
|
||||
.inc "fs.h"
|
||||
.inc "err.h"
|
||||
.inc "ascii.h"
|
||||
.equ RAMSTART 0x2000
|
||||
.equ USER_CODE 0x4200
|
||||
.equ STDIO_PORT 0x00
|
||||
.equ FS_DATA_PORT 0x01
|
||||
.equ FS_ADDR_PORT 0x02
|
||||
|
||||
jp init
|
||||
|
||||
; *** JUMP TABLE ***
|
||||
jp strncmp
|
||||
jp upcase
|
||||
jp findchar
|
||||
jp blkSelPtr
|
||||
jp blkSel
|
||||
jp blkSet
|
||||
jp blkSeek
|
||||
jp blkTell
|
||||
jp blkGetB
|
||||
jp blkPutB
|
||||
jp fsFindFN
|
||||
jp fsOpen
|
||||
jp fsGetB
|
||||
jp fsPutB
|
||||
jp fsSetSize
|
||||
jp fsOn
|
||||
jp fsIter
|
||||
jp fsAlloc
|
||||
jp fsDel
|
||||
jp fsHandle
|
||||
jp printstr
|
||||
jp printnstr
|
||||
jp _blkGetB
|
||||
jp _blkPutB
|
||||
jp _blkSeek
|
||||
jp _blkTell
|
||||
jp printcrlf
|
||||
jp stdioGetC
|
||||
jp stdioPutC
|
||||
jp stdioReadLine
|
||||
|
||||
.inc "core.asm"
|
||||
.inc "str.asm"
|
||||
|
||||
.equ BLOCKDEV_RAMSTART RAMSTART
|
||||
.equ BLOCKDEV_COUNT 4
|
||||
.inc "blockdev.asm"
|
||||
; List of devices
|
||||
.dw fsdevGetB, fsdevPutB
|
||||
.dw stdoutGetB, stdoutPutB
|
||||
.dw stdinGetB, stdinPutB
|
||||
.dw mmapGetB, mmapPutB
|
||||
|
||||
|
||||
.equ MMAP_START 0xe000
|
||||
.inc "mmap.asm"
|
||||
|
||||
.equ STDIO_RAMSTART BLOCKDEV_RAMEND
|
||||
.equ STDIO_GETC emulGetC
|
||||
.equ STDIO_PUTC emulPutC
|
||||
.inc "stdio.asm"
|
||||
|
||||
.equ FS_RAMSTART STDIO_RAMEND
|
||||
.equ FS_HANDLE_COUNT 2
|
||||
.inc "fs.asm"
|
||||
|
||||
; *** BASIC ***
|
||||
|
||||
; RAM space used in different routines for short term processing.
|
||||
.equ SCRATCHPAD_SIZE STDIO_BUFSIZE
|
||||
.equ SCRATCHPAD FS_RAMEND
|
||||
.inc "lib/util.asm"
|
||||
.inc "lib/ari.asm"
|
||||
.inc "lib/parse.asm"
|
||||
.inc "lib/fmt.asm"
|
||||
.equ EXPR_PARSE parseLiteralOrVar
|
||||
.inc "lib/expr.asm"
|
||||
.inc "basic/util.asm"
|
||||
.inc "basic/parse.asm"
|
||||
.inc "basic/tok.asm"
|
||||
.equ VAR_RAMSTART SCRATCHPAD+SCRATCHPAD_SIZE
|
||||
.inc "basic/var.asm"
|
||||
.equ BUF_RAMSTART VAR_RAMEND
|
||||
.inc "basic/buf.asm"
|
||||
.equ BFS_RAMSTART BUF_RAMEND
|
||||
.inc "basic/fs.asm"
|
||||
.inc "basic/blk.asm"
|
||||
.equ BAS_RAMSTART BFS_RAMEND
|
||||
.inc "basic/main.asm"
|
||||
|
||||
init:
|
||||
di
|
||||
; setup stack
|
||||
ld sp, 0xffff
|
||||
call fsInit
|
||||
ld a, 0 ; select fsdev
|
||||
ld de, BLOCKDEV_SEL
|
||||
call blkSel
|
||||
call fsOn
|
||||
call basInit
|
||||
ld hl, basFindCmdExtra
|
||||
ld (BAS_FINDHOOK), hl
|
||||
jp basStart
|
||||
|
||||
basFindCmdExtra:
|
||||
ld hl, basFSCmds
|
||||
call basFindCmd
|
||||
ret z
|
||||
ld hl, basBLKCmds
|
||||
call basFindCmd
|
||||
ret z
|
||||
jp basPgmHook
|
||||
|
||||
emulGetC:
|
||||
; Blocks until a char is returned
|
||||
in a, (STDIO_PORT)
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
emulPutC:
|
||||
out (STDIO_PORT), a
|
||||
ret
|
||||
|
||||
fsdevGetB:
|
||||
ld a, e
|
||||
out (FS_ADDR_PORT), a
|
||||
ld a, h
|
||||
out (FS_ADDR_PORT), a
|
||||
ld a, l
|
||||
out (FS_ADDR_PORT), a
|
||||
in a, (FS_ADDR_PORT)
|
||||
or a
|
||||
ret nz
|
||||
in a, (FS_DATA_PORT)
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
fsdevPutB:
|
||||
push af
|
||||
ld a, e
|
||||
out (FS_ADDR_PORT), a
|
||||
ld a, h
|
||||
out (FS_ADDR_PORT), a
|
||||
ld a, l
|
||||
out (FS_ADDR_PORT), a
|
||||
in a, (FS_ADDR_PORT)
|
||||
cp 2 ; only A > 1 means error
|
||||
jr nc, .error ; A >= 2
|
||||
pop af
|
||||
out (FS_DATA_PORT), a
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.error:
|
||||
pop af
|
||||
jp unsetZ ; returns
|
||||
|
||||
.equ STDOUT_HANDLE FS_HANDLES
|
||||
|
||||
stdoutGetB:
|
||||
ld ix, STDOUT_HANDLE
|
||||
jp fsGetB
|
||||
|
||||
stdoutPutB:
|
||||
ld ix, STDOUT_HANDLE
|
||||
jp fsPutB
|
||||
|
||||
.equ STDIN_HANDLE FS_HANDLES+FS_HANDLE_SIZE
|
||||
|
||||
stdinGetB:
|
||||
ld ix, STDIN_HANDLE
|
||||
jp fsGetB
|
||||
|
||||
stdinPutB:
|
||||
ld ix, STDIN_HANDLE
|
||||
jp fsPutB
|
@ -1,208 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <termios.h>
|
||||
#include "../emul.h"
|
||||
#include "shell-bin.h"
|
||||
#include "../../tools/cfspack/cfs.h"
|
||||
|
||||
/* Collapse OS shell with filesystem
|
||||
*
|
||||
* On startup, if "cfsin" directory exists, it packs it as a afke block device
|
||||
* and loads it in. Upon halting, unpcks the contents of that block device in
|
||||
* "cfsout" directory.
|
||||
*
|
||||
* Memory layout:
|
||||
*
|
||||
* 0x0000 - 0x3fff: ROM code from shell.asm
|
||||
* 0x4000 - 0x4fff: Kernel memory
|
||||
* 0x5000 - 0xffff: Userspace
|
||||
*
|
||||
* I/O Ports:
|
||||
*
|
||||
* 0 - stdin / stdout
|
||||
* 1 - Filesystem blockdev data read/write. Reads and write data to the address
|
||||
* previously selected through port 2
|
||||
*/
|
||||
|
||||
//#define DEBUG
|
||||
#define MAX_FSDEV_SIZE 0x20000
|
||||
|
||||
// in sync with glue.asm
|
||||
#define RAMSTART 0x2000
|
||||
#define STDIO_PORT 0x00
|
||||
#define FS_DATA_PORT 0x01
|
||||
// Controls what address (24bit) the data port returns. To select an address,
|
||||
// this port has to be written to 3 times, starting with the MSB.
|
||||
// Reading this port returns an out-of-bounds indicator. Meaning:
|
||||
// 0 means addr is within bounds
|
||||
// 1 means that we're equal to fsdev size (error for reading, ok for writing)
|
||||
// 2 means more than fsdev size (always invalid)
|
||||
// 3 means incomplete addr setting
|
||||
#define FS_ADDR_PORT 0x02
|
||||
|
||||
static uint8_t fsdev[MAX_FSDEV_SIZE] = {0};
|
||||
static uint32_t fsdev_ptr = 0;
|
||||
// 0 = idle, 1 = received MSB (of 24bit addr), 2 = received middle addr
|
||||
static int fsdev_addr_lvl = 0;
|
||||
static int running;
|
||||
|
||||
static uint8_t iord_stdio()
|
||||
{
|
||||
int c = getchar();
|
||||
if (c == EOF) {
|
||||
running = 0;
|
||||
}
|
||||
return (uint8_t)c;
|
||||
}
|
||||
|
||||
static uint8_t iord_fsdata()
|
||||
{
|
||||
if (fsdev_addr_lvl != 0) {
|
||||
fprintf(stderr, "Reading FSDEV in the middle of an addr op (%d)\n", fsdev_ptr);
|
||||
return 0;
|
||||
}
|
||||
if (fsdev_ptr < MAX_FSDEV_SIZE) {
|
||||
#ifdef DEBUG
|
||||
fprintf(stderr, "Reading FSDEV at offset %d\n", fsdev_ptr);
|
||||
#endif
|
||||
return fsdev[fsdev_ptr];
|
||||
} else {
|
||||
fprintf(stderr, "Out of bounds FSDEV read at %d\n", fsdev_ptr);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t iord_fsaddr()
|
||||
{
|
||||
if (fsdev_addr_lvl != 0) {
|
||||
return 3;
|
||||
} else if (fsdev_ptr >= MAX_FSDEV_SIZE) {
|
||||
fprintf(stderr, "Out of bounds FSDEV addr request at %d / %d\n", fsdev_ptr, MAX_FSDEV_SIZE);
|
||||
return 2;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void iowr_stdio(uint8_t val)
|
||||
{
|
||||
if (val == 0x04) { // CTRL+D
|
||||
running = 0;
|
||||
} else {
|
||||
putchar(val);
|
||||
}
|
||||
}
|
||||
|
||||
static void iowr_fsdata(uint8_t val)
|
||||
{
|
||||
if (fsdev_addr_lvl != 0) {
|
||||
fprintf(stderr, "Writing to FSDEV in the middle of an addr op (%d)\n", fsdev_ptr);
|
||||
return;
|
||||
}
|
||||
if (fsdev_ptr < MAX_FSDEV_SIZE) {
|
||||
#ifdef DEBUG
|
||||
fprintf(stderr, "Writing to FSDEV (%d)\n", fsdev_ptr);
|
||||
#endif
|
||||
fsdev[fsdev_ptr] = val;
|
||||
} else {
|
||||
fprintf(stderr, "Out of bounds FSDEV write at %d\n", fsdev_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
static void iowr_fsaddr(uint8_t val)
|
||||
{
|
||||
if (fsdev_addr_lvl == 0) {
|
||||
fsdev_ptr = val << 16;
|
||||
fsdev_addr_lvl = 1;
|
||||
} else if (fsdev_addr_lvl == 1) {
|
||||
fsdev_ptr |= val << 8;
|
||||
fsdev_addr_lvl = 2;
|
||||
} else {
|
||||
fsdev_ptr |= val;
|
||||
fsdev_addr_lvl = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
while (1) {
|
||||
int c = getopt(argc, argv, "f:");
|
||||
if (c < 0) {
|
||||
break;
|
||||
}
|
||||
switch (c) {
|
||||
case 'f':
|
||||
fp = fopen(optarg, "r");
|
||||
if (fp == NULL) {
|
||||
fprintf(stderr, "Can't open %s\n", optarg);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "Initializing filesystem from %s\n", optarg);
|
||||
int i = 0;
|
||||
int c;
|
||||
while ((c = fgetc(fp)) != EOF && i < MAX_FSDEV_SIZE) {
|
||||
fsdev[i++] = c & 0xff;
|
||||
}
|
||||
if (i == MAX_FSDEV_SIZE) {
|
||||
fprintf(stderr, "Filesytem image too large.\n");
|
||||
return 1;
|
||||
}
|
||||
pclose(fp);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Usage: shell [-f fsdev]\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// Setup fs blockdev
|
||||
if (fp == NULL) {
|
||||
fprintf(stderr, "Initializing filesystem from cfsin\n");
|
||||
fp = fmemopen(fsdev, MAX_FSDEV_SIZE, "w");
|
||||
set_spit_stream(fp);
|
||||
if (spitdir("cfsin", "", NULL) != 0) {
|
||||
fprintf(stderr, "Can't initialize filesystem. Leaving blank.\n");
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
bool tty = isatty(fileno(stdin));
|
||||
struct termios termInfo;
|
||||
if (tty) {
|
||||
// Turn echo off: the shell takes care of its own echoing.
|
||||
if (tcgetattr(0, &termInfo) == -1) {
|
||||
printf("Can't setup terminal.\n");
|
||||
return 1;
|
||||
}
|
||||
termInfo.c_lflag &= ~ECHO;
|
||||
termInfo.c_lflag &= ~ICANON;
|
||||
tcsetattr(0, TCSAFLUSH, &termInfo);
|
||||
}
|
||||
|
||||
|
||||
Machine *m = emul_init();
|
||||
m->ramstart = RAMSTART;
|
||||
m->iord[STDIO_PORT] = iord_stdio;
|
||||
m->iord[FS_DATA_PORT] = iord_fsdata;
|
||||
m->iord[FS_ADDR_PORT] = iord_fsaddr;
|
||||
m->iowr[STDIO_PORT] = iowr_stdio;
|
||||
m->iowr[FS_DATA_PORT] = iowr_fsdata;
|
||||
m->iowr[FS_ADDR_PORT] = iowr_fsaddr;
|
||||
// initialize memory
|
||||
for (int i=0; i<sizeof(KERNEL); i++) {
|
||||
m->mem[i] = KERNEL[i];
|
||||
}
|
||||
// Run!
|
||||
running = 1;
|
||||
|
||||
while (running && emul_step());
|
||||
|
||||
if (tty) {
|
||||
printf("Done!\n");
|
||||
termInfo.c_lflag |= ECHO;
|
||||
termInfo.c_lflag |= ICANON;
|
||||
tcsetattr(0, TCSAFLUSH, &termInfo);
|
||||
emul_printdebug();
|
||||
}
|
||||
return 0;
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
.equ USER_CODE 0x4200 ; in sync with glue.asm
|
||||
|
||||
; *** JUMP TABLE ***
|
||||
.equ strncmp 0x03
|
||||
.equ upcase @+3
|
||||
.equ findchar @+3
|
||||
.equ blkSelPtr @+3
|
||||
.equ blkSel @+3
|
||||
.equ blkSet @+3
|
||||
.equ blkSeek @+3
|
||||
.equ blkTell @+3
|
||||
.equ blkGetB @+3
|
||||
.equ blkPutB @+3
|
||||
.equ fsFindFN @+3
|
||||
.equ fsOpen @+3
|
||||
.equ fsGetB @+3
|
||||
.equ fsPutB @+3
|
||||
.equ fsSetSize @+3
|
||||
.equ fsOn @+3
|
||||
.equ fsIter @+3
|
||||
.equ fsAlloc @+3
|
||||
.equ fsDel @+3
|
||||
.equ fsHandle @+3
|
||||
.equ printstr @+3
|
||||
.equ printnstr @+3
|
||||
.equ _blkGetB @+3
|
||||
.equ _blkPutB @+3
|
||||
.equ _blkSeek @+3
|
||||
.equ _blkTell @+3
|
||||
.equ printcrlf @+3
|
||||
.equ stdioGetC @+3
|
||||
.equ stdioPutC @+3
|
||||
.equ stdioReadLine @+3
|
||||
|
@ -1,131 +0,0 @@
|
||||
; Glue code for the emulated environment
|
||||
.equ RAMSTART 0x4000
|
||||
.equ USER_CODE 0x4800
|
||||
.equ STDIO_PORT 0x00
|
||||
.equ STDIN_SEEK 0x01
|
||||
.equ FS_DATA_PORT 0x02
|
||||
.equ FS_SEEK_PORT 0x03
|
||||
.equ STDERR_PORT 0x04
|
||||
.inc "err.h"
|
||||
.inc "ascii.h"
|
||||
.inc "blkdev.h"
|
||||
.inc "fs.h"
|
||||
|
||||
jp init ; 3 bytes
|
||||
; *** JUMP TABLE ***
|
||||
jp strncmp
|
||||
jp upcase
|
||||
jp findchar
|
||||
jp blkSel
|
||||
jp blkSet
|
||||
jp fsFindFN
|
||||
jp fsOpen
|
||||
jp fsGetB
|
||||
jp _blkGetB
|
||||
jp _blkPutB
|
||||
jp _blkSeek
|
||||
jp _blkTell
|
||||
jp printstr
|
||||
jp printcrlf
|
||||
|
||||
.inc "core.asm"
|
||||
.inc "str.asm"
|
||||
.equ BLOCKDEV_RAMSTART RAMSTART
|
||||
.equ BLOCKDEV_COUNT 3
|
||||
.inc "blockdev.asm"
|
||||
; List of devices
|
||||
.dw emulGetB, unsetZ
|
||||
.dw unsetZ, emulPutB
|
||||
.dw fsdevGetB, fsdevPutB
|
||||
|
||||
.equ STDIO_RAMSTART BLOCKDEV_RAMEND
|
||||
.equ STDIO_GETC noop
|
||||
.equ STDIO_PUTC stderrPutC
|
||||
.inc "stdio.asm"
|
||||
|
||||
.equ FS_RAMSTART STDIO_RAMEND
|
||||
.equ FS_HANDLE_COUNT 0
|
||||
.inc "fs.asm"
|
||||
|
||||
init:
|
||||
di
|
||||
ld hl, 0xffff
|
||||
ld sp, hl
|
||||
ld a, 2 ; select fsdev
|
||||
ld de, BLOCKDEV_SEL
|
||||
call blkSel
|
||||
call fsOn
|
||||
; There's a special understanding between zasm.c and this unit: The
|
||||
; addresses 0xff00 and 0xff01 contain the two ascii chars to send to
|
||||
; zasm as the 3rd argument.
|
||||
ld a, (0xff00)
|
||||
ld (.zasmArgs+4), a
|
||||
ld a, (0xff01)
|
||||
ld (.zasmArgs+5), a
|
||||
ld hl, .zasmArgs
|
||||
call USER_CODE
|
||||
; signal the emulator we're done
|
||||
halt
|
||||
|
||||
.zasmArgs:
|
||||
.db "0 1 XX", 0
|
||||
|
||||
; *** I/O ***
|
||||
emulGetB:
|
||||
; the STDIN_SEEK port works by poking it twice. First poke is for high
|
||||
; byte, second poke is for low one.
|
||||
ld a, h
|
||||
out (STDIN_SEEK), a
|
||||
ld a, l
|
||||
out (STDIN_SEEK), a
|
||||
in a, (STDIO_PORT)
|
||||
or a ; cp 0
|
||||
jr z, .eof
|
||||
cp a ; ensure z
|
||||
ret
|
||||
.eof:
|
||||
jp unsetZ
|
||||
|
||||
emulPutB:
|
||||
out (STDIO_PORT), a
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
stderrPutC:
|
||||
out (STDERR_PORT), a
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
fsdevGetB:
|
||||
ld a, e
|
||||
out (FS_SEEK_PORT), a
|
||||
ld a, h
|
||||
out (FS_SEEK_PORT), a
|
||||
ld a, l
|
||||
out (FS_SEEK_PORT), a
|
||||
in a, (FS_SEEK_PORT)
|
||||
or a
|
||||
ret nz
|
||||
in a, (FS_DATA_PORT)
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
fsdevPutB:
|
||||
push af
|
||||
ld a, e
|
||||
out (FS_SEEK_PORT), a
|
||||
ld a, h
|
||||
out (FS_SEEK_PORT), a
|
||||
ld a, l
|
||||
out (FS_SEEK_PORT), a
|
||||
in a, (FS_SEEK_PORT)
|
||||
or a
|
||||
jr nz, .error
|
||||
pop af
|
||||
out (FS_DATA_PORT), a
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.error:
|
||||
pop af
|
||||
jp unsetZ ; returns
|
||||
|
Binary file not shown.
@ -1,18 +0,0 @@
|
||||
.org 0x4800 ; in sync with USER_CODE in glue.asm
|
||||
.equ USER_RAMSTART 0x6000
|
||||
|
||||
; *** JUMP TABLE ***
|
||||
.equ strncmp 0x03
|
||||
.equ upcase @+3
|
||||
.equ findchar @+3
|
||||
.equ blkSel @+3
|
||||
.equ blkSet @+3
|
||||
.equ fsFindFN @+3
|
||||
.equ fsOpen @+3
|
||||
.equ fsGetB @+3
|
||||
.equ _blkGetB @+3
|
||||
.equ _blkPutB @+3
|
||||
.equ _blkSeek @+3
|
||||
.equ _blkTell @+3
|
||||
.equ printstr @+3
|
||||
.equ printcrlf @+3
|
Binary file not shown.
269
emul/zasm/zasm.c
269
emul/zasm/zasm.c
@ -1,269 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <libgen.h>
|
||||
#include "../emul.h"
|
||||
#include "../../tools/cfspack/cfs.h"
|
||||
#include "kernel-bin.h"
|
||||
#ifdef AVRA
|
||||
#include "avra-bin.h"
|
||||
#else
|
||||
#include "zasm-bin.h"
|
||||
#endif
|
||||
|
||||
/* zasm reads from a specified blkdev, assemble the file and writes the result
|
||||
* in another specified blkdev. In our emulator layer, we use stdin and stdout
|
||||
* as those specified blkdevs.
|
||||
*
|
||||
* This executable takes two arguments. Both are optional, but you need to
|
||||
* specify the first one if you want to get to the second one.
|
||||
* The first one is the value to send to z80-zasm's 3rd argument (the initial
|
||||
* .org). Defaults to '00'.
|
||||
* The second one is the path to a .cfs file to use for includes.
|
||||
*
|
||||
* Because the input blkdev needs support for Seek, we buffer it in the emulator
|
||||
* layer.
|
||||
*
|
||||
* Memory layout:
|
||||
*
|
||||
* 0x0000 - 0x3fff: ROM code from zasm_glue.asm
|
||||
* 0x4000 - 0x47ff: RAM for kernel and stack
|
||||
* 0x4800 - 0x57ff: Userspace code
|
||||
* 0x5800 - 0xffff: Userspace RAM
|
||||
*
|
||||
* I/O Ports:
|
||||
*
|
||||
* 0 - stdin / stdout
|
||||
* 1 - When written to, rewind stdin buffer to the beginning.
|
||||
*/
|
||||
|
||||
// in sync with zasm_glue.asm
|
||||
#define USER_CODE 0x4800
|
||||
#define STDIO_PORT 0x00
|
||||
#define STDIN_SEEK_PORT 0x01
|
||||
#define FS_DATA_PORT 0x02
|
||||
#define FS_SEEK_PORT 0x03
|
||||
#define STDERR_PORT 0x04
|
||||
|
||||
// Other consts
|
||||
#define STDIN_BUFSIZE 0x8000
|
||||
// When defined, we dump memory instead of dumping expected stdout
|
||||
//#define MEMDUMP
|
||||
//#define DEBUG
|
||||
// By default, we don't spit what zasm prints. Too noisy. Define VERBOSE if
|
||||
// you want to spit this content to stderr.
|
||||
//#define VERBOSE
|
||||
#define MAX_FSDEV_SIZE 0x80000
|
||||
|
||||
// STDIN buffer, allows us to seek and tell
|
||||
static uint8_t inpt[STDIN_BUFSIZE];
|
||||
static int inpt_size;
|
||||
static int inpt_ptr;
|
||||
static uint8_t middle_of_seek_tell = 0;
|
||||
|
||||
static uint8_t fsdev[MAX_FSDEV_SIZE] = {0};
|
||||
static uint32_t fsdev_ptr = 0;
|
||||
static uint8_t fsdev_seek_tell_cnt = 0;
|
||||
|
||||
static uint8_t iord_stdio()
|
||||
{
|
||||
if (inpt_ptr < inpt_size) {
|
||||
return inpt[inpt_ptr++];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t iord_stdin_seek()
|
||||
{
|
||||
if (middle_of_seek_tell) {
|
||||
middle_of_seek_tell = 0;
|
||||
return inpt_ptr & 0xff;
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
fprintf(stderr, "tell %d\n", inpt_ptr);
|
||||
#endif
|
||||
middle_of_seek_tell = 1;
|
||||
return inpt_ptr >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t iord_fsdata()
|
||||
{
|
||||
if (fsdev_ptr < MAX_FSDEV_SIZE) {
|
||||
return fsdev[fsdev_ptr++];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t iord_fsseek()
|
||||
{
|
||||
if (fsdev_seek_tell_cnt != 0) {
|
||||
return fsdev_seek_tell_cnt;
|
||||
} else if (fsdev_ptr >= MAX_FSDEV_SIZE) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void iowr_stdio(uint8_t val)
|
||||
{
|
||||
// When mem-dumping, we don't output regular stuff.
|
||||
#ifndef MEMDUMP
|
||||
putchar(val);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void iowr_stdin_seek(uint8_t val)
|
||||
{
|
||||
if (middle_of_seek_tell) {
|
||||
inpt_ptr |= val;
|
||||
middle_of_seek_tell = 0;
|
||||
#ifdef DEBUG
|
||||
fprintf(stderr, "seek %d\n", inpt_ptr);
|
||||
#endif
|
||||
} else {
|
||||
inpt_ptr = (val << 8) & 0xff00;
|
||||
middle_of_seek_tell = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void iowr_fsdata(uint8_t val)
|
||||
{
|
||||
if (fsdev_ptr < MAX_FSDEV_SIZE) {
|
||||
fsdev[fsdev_ptr++] = val;
|
||||
}
|
||||
}
|
||||
|
||||
static void iowr_fsseek(uint8_t val)
|
||||
{
|
||||
if (fsdev_seek_tell_cnt == 0) {
|
||||
fsdev_ptr = val << 16;
|
||||
fsdev_seek_tell_cnt = 1;
|
||||
} else if (fsdev_seek_tell_cnt == 1) {
|
||||
fsdev_ptr |= val << 8;
|
||||
fsdev_seek_tell_cnt = 2;
|
||||
} else {
|
||||
fsdev_ptr |= val;
|
||||
fsdev_seek_tell_cnt = 0;
|
||||
#ifdef DEBUG
|
||||
fprintf(stderr, "FS seek %d\n", fsdev_ptr);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static void iowr_stderr(uint8_t val)
|
||||
{
|
||||
#ifdef VERBOSE
|
||||
fputc(val, stderr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void usage()
|
||||
{
|
||||
fprintf(stderr, "Usage: zasm [-o org] [include-dir-or-file...] < source > binary\n");
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
char *init_org = "00";
|
||||
while (1) {
|
||||
int c = getopt(argc, argv, "o:");
|
||||
if (c < 0) {
|
||||
break;
|
||||
}
|
||||
switch (c) {
|
||||
case 'o':
|
||||
init_org = optarg;
|
||||
if (strlen(init_org) != 2) {
|
||||
fprintf(stderr, "Initial org must be a two-character hex string");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (argc-optind > 0) {
|
||||
FILE *fp = fmemopen(fsdev, MAX_FSDEV_SIZE, "w");
|
||||
set_spit_stream(fp);
|
||||
char *patterns[4] = {"*.h", "*.asm", "*.bin", 0};
|
||||
for (int i=optind; i<argc; i++) {
|
||||
int res;
|
||||
if (is_regular_file(argv[i])) {
|
||||
// special case: just one file
|
||||
res = spitblock(argv[i], basename(argv[i]));
|
||||
} else {
|
||||
res = spitdir(argv[i], "", patterns);
|
||||
}
|
||||
if (res != 0) {
|
||||
fprintf(stderr, "Error while building the include CFS.\n");
|
||||
fclose(fp);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
Machine *m = emul_init();
|
||||
m->iord[STDIO_PORT] = iord_stdio;
|
||||
m->iord[STDIN_SEEK_PORT] = iord_stdin_seek;
|
||||
m->iord[FS_DATA_PORT] = iord_fsdata;
|
||||
m->iord[FS_SEEK_PORT] = iord_fsseek;
|
||||
m->iowr[STDIO_PORT] = iowr_stdio;
|
||||
m->iowr[STDIN_SEEK_PORT] = iowr_stdin_seek;
|
||||
m->iowr[FS_DATA_PORT] = iowr_fsdata;
|
||||
m->iowr[FS_SEEK_PORT] = iowr_fsseek;
|
||||
m->iowr[STDERR_PORT] = iowr_stderr;
|
||||
// initialize memory
|
||||
for (int i=0; i<sizeof(KERNEL); i++) {
|
||||
m->mem[i] = KERNEL[i];
|
||||
}
|
||||
for (int i=0; i<sizeof(USERSPACE); i++) {
|
||||
m->mem[i+USER_CODE] = USERSPACE[i];
|
||||
}
|
||||
// glue.asm knows that it needs to fetch these arguments at this address.
|
||||
m->mem[0xff00] = init_org[0];
|
||||
m->mem[0xff01] = init_org[1];
|
||||
// read stdin in buffer
|
||||
inpt_size = 0;
|
||||
inpt_ptr = 0;
|
||||
int c = getchar();
|
||||
while (c != EOF) {
|
||||
inpt[inpt_ptr] = c & 0xff;
|
||||
inpt_ptr++;
|
||||
if (inpt_ptr == STDIN_BUFSIZE) {
|
||||
break;
|
||||
}
|
||||
c = getchar();
|
||||
}
|
||||
inpt_size = inpt_ptr;
|
||||
inpt_ptr = 0;
|
||||
|
||||
emul_loop();
|
||||
#ifdef MEMDUMP
|
||||
for (int i=0; i<0x10000; i++) {
|
||||
putchar(mem[i]);
|
||||
}
|
||||
#endif
|
||||
fflush(stdout);
|
||||
int res = m->cpu.R1.br.A;
|
||||
if (res != 0) {
|
||||
int lineno = m->cpu.R1.wr.HL;
|
||||
int inclineno = m->cpu.R1.wr.DE;
|
||||
if (inclineno) {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Error %d on line %d, include line %d\n",
|
||||
res,
|
||||
lineno,
|
||||
inclineno);
|
||||
} else {
|
||||
fprintf(stderr, "Error %d on line %d\n", res, lineno);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
# Kernel
|
||||
|
||||
Bits and pieces of code that you can assemble to build a kernel for your
|
||||
machine.
|
||||
|
||||
These parts are made to be glued together in a single `glue.asm` file you write
|
||||
yourself.
|
||||
|
||||
This code is designed to be assembled by Collapse OS' own [zasm][zasm].
|
||||
|
||||
## Scope
|
||||
|
||||
Units in the `kernel/` folder is about device driver, abstractions over them
|
||||
as well as the file system. Although a typical kernel boots to a shell, the
|
||||
code for that shell is not considered part of the kernel code (even if, most of
|
||||
the time, it's assembled in the same binary). Shells are considered userspace
|
||||
applications (which live in `apps/`).
|
136
kernel/acia.asm
136
kernel/acia.asm
@ -1,136 +0,0 @@
|
||||
; acia
|
||||
;
|
||||
; Manage I/O from an asynchronous communication interface adapter (ACIA).
|
||||
; provides "aciaPutC" to put c char on the ACIA as well as an input buffer.
|
||||
; You have to call "aciaInt" on interrupt for this module to work well.
|
||||
;
|
||||
; "aciaInit" also has to be called on boot, but it doesn't call "ei" and "im 1",
|
||||
; which is the responsibility of the main asm file, but is needed.
|
||||
|
||||
; *** DEFINES ***
|
||||
; ACIA_CTL: IO port for the ACIA's control registers
|
||||
; ACIA_IO: IO port for the ACIA's data registers
|
||||
; ACIA_RAMSTART: Address at which ACIA-related variables should be stored in
|
||||
; RAM.
|
||||
|
||||
; *** CONSTS ***
|
||||
; size of the input buffer. If our input goes over this size, we start losing
|
||||
; data.
|
||||
.equ ACIA_BUFSIZE 0x20
|
||||
|
||||
; *** VARIABLES ***
|
||||
; Our input buffer starts there. This is a circular buffer.
|
||||
.equ ACIA_BUF ACIA_RAMSTART
|
||||
|
||||
; The "read" index of the circular buffer. It points to where the next char
|
||||
; should be read. If rd == wr, the buffer is empty. Not touched by the
|
||||
; interrupt.
|
||||
.equ ACIA_BUFRDIDX ACIA_BUF+ACIA_BUFSIZE
|
||||
; The "write" index of the circular buffer. Points to where the next char
|
||||
; should be written. Should only be touched by the interrupt. if wr == rd-1,
|
||||
; the interrupt will *not* write in the buffer until some space has been freed.
|
||||
.equ ACIA_BUFWRIDX ACIA_BUFRDIDX+1
|
||||
.equ ACIA_RAMEND ACIA_BUFWRIDX+1
|
||||
|
||||
aciaInit:
|
||||
; initialize variables
|
||||
xor a
|
||||
ld (ACIA_BUFRDIDX), a ; starts at 0
|
||||
ld (ACIA_BUFWRIDX), a
|
||||
|
||||
; setup ACIA
|
||||
; CR7 (1) - Receive Interrupt enabled
|
||||
; CR6:5 (00) - RTS low, transmit interrupt disabled.
|
||||
; CR4:2 (101) - 8 bits + 1 stop bit
|
||||
; CR1:0 (10) - Counter divide: 64
|
||||
ld a, 0b10010110
|
||||
out (ACIA_CTL), a
|
||||
ret
|
||||
|
||||
; Increase the circular buffer index in A, properly considering overflow.
|
||||
; returns value in A.
|
||||
aciaIncIndex:
|
||||
inc a
|
||||
cp ACIA_BUFSIZE
|
||||
ret nz ; not equal? nothing to do
|
||||
; equal? reset
|
||||
xor a
|
||||
ret
|
||||
|
||||
; read char in the ACIA and put it in the read buffer
|
||||
aciaInt:
|
||||
push af
|
||||
push hl
|
||||
|
||||
; Read our character from ACIA into our BUFIDX
|
||||
in a, (ACIA_CTL)
|
||||
bit 0, a ; is our ACIA rcv buffer full?
|
||||
jr z, .end ; no? a interrupt was triggered for nothing.
|
||||
|
||||
; Load both read and write indexes so we can compare them. To do so, we
|
||||
; perform a "fake" read increase and see if it brings it to the same
|
||||
; value as the write index.
|
||||
ld a, (ACIA_BUFRDIDX)
|
||||
call aciaIncIndex
|
||||
ld l, a
|
||||
ld a, (ACIA_BUFWRIDX)
|
||||
cp l
|
||||
jr z, .end ; Equal? buffer is full
|
||||
|
||||
push de ; <|
|
||||
; Alrighty, buffer not full|. let's write.
|
||||
ld de, ACIA_BUF ; |
|
||||
; A already contains our wr|ite index, add it to DE
|
||||
call addDE ; |
|
||||
; increase our buf ptr whil|e we still have it in A
|
||||
call aciaIncIndex ; |
|
||||
ld (ACIA_BUFWRIDX), a ;
|
||||
; |
|
||||
; And finally, fetch the va|lue and write it.
|
||||
in a, (ACIA_IO) ; |
|
||||
ld (de), a ; |
|
||||
pop de ; <|
|
||||
|
||||
.end:
|
||||
pop hl
|
||||
pop af
|
||||
ei
|
||||
reti
|
||||
|
||||
|
||||
; *** STDIO ***
|
||||
; These function below follow the stdio API.
|
||||
|
||||
aciaGetC:
|
||||
push de
|
||||
.loop:
|
||||
ld a, (ACIA_BUFWRIDX)
|
||||
ld e, a
|
||||
ld a, (ACIA_BUFRDIDX)
|
||||
cp e
|
||||
jr z, .loop ; equal? nothing to read. loop
|
||||
|
||||
; Alrighty, buffer not empty. let's read.
|
||||
ld de, ACIA_BUF
|
||||
; A already contains our read index, add it to DE
|
||||
call addDE
|
||||
; increase our buf ptr while we still have it in A
|
||||
call aciaIncIndex
|
||||
ld (ACIA_BUFRDIDX), a
|
||||
|
||||
; And finally, fetch the value.
|
||||
ld a, (de)
|
||||
pop de
|
||||
ret
|
||||
|
||||
; spits character in A in port SER_OUT
|
||||
aciaPutC:
|
||||
push af
|
||||
.stwait:
|
||||
in a, (ACIA_CTL) ; get status byte from SER
|
||||
bit 1, a ; are we still transmitting?
|
||||
jr z, .stwait ; if yes, wait until we aren't
|
||||
pop af
|
||||
out (ACIA_IO), a ; push current char
|
||||
ret
|
||||
|
@ -1,4 +0,0 @@
|
||||
.equ BS 0x08
|
||||
.equ CR 0x0d
|
||||
.equ LF 0x0a
|
||||
.equ DEL 0x7f
|
@ -1,8 +0,0 @@
|
||||
.equ BLOCKDEV_SEEK_ABSOLUTE 0
|
||||
.equ BLOCKDEV_SEEK_FORWARD 1
|
||||
.equ BLOCKDEV_SEEK_BACKWARD 2
|
||||
.equ BLOCKDEV_SEEK_BEGINNING 3
|
||||
.equ BLOCKDEV_SEEK_END 4
|
||||
|
||||
.equ BLOCKDEV_SIZE 8
|
||||
|
@ -1,302 +0,0 @@
|
||||
; blockdev
|
||||
;
|
||||
; A block device is an abstraction over something we can read from, write to.
|
||||
;
|
||||
; A device that fits this abstraction puts the proper hook into itself, and then
|
||||
; the glue code assigns a blockdev ID to that device. It then becomes easy to
|
||||
; access arbitrary devices in a convenient manner.
|
||||
;
|
||||
; This module exposes a seek/tell/getb/putb API that is then re-routed to
|
||||
; underlying drivers. There will eventually be more than one driver type, but
|
||||
; for now we sit on only one type of driver: random access driver.
|
||||
;
|
||||
; *** Random access drivers ***
|
||||
;
|
||||
; Random access drivers are expected to supply two routines: GetB and PutB.
|
||||
;
|
||||
; GetB:
|
||||
; Reads one byte at address specified in DE/HL and returns its value in A.
|
||||
; Sets Z according to whether read was successful: Set if successful, unset
|
||||
; if not.
|
||||
;
|
||||
; Unsuccessful reads generally mean that requested addr is out of bounds (we
|
||||
; reached EOF).
|
||||
;
|
||||
; PutB:
|
||||
; Writes byte in A at address specified in DE/HL. Sets Z according to whether
|
||||
; the operation was successful.
|
||||
;
|
||||
; Unsuccessful writes generally mean that we're out of bounds for writing.
|
||||
;
|
||||
; All routines are expected to preserve unused registers except IX which is
|
||||
; explicitly protected during GetB/PutB calls. This makes quick "handle+jump"
|
||||
; definitions possible.
|
||||
|
||||
|
||||
; *** DEFINES ***
|
||||
; BLOCKDEV_COUNT: The number of devices we manage.
|
||||
|
||||
; *** CONSTS ***
|
||||
; *** VARIABLES ***
|
||||
; Pointer to the selected block device. A block device is a 8 bytes block of
|
||||
; memory with pointers to GetB, PutB, and a 32-bit counter, in that order.
|
||||
.equ BLOCKDEV_SEL BLOCKDEV_RAMSTART
|
||||
.equ BLOCKDEV_RAMEND @+BLOCKDEV_SIZE
|
||||
|
||||
; *** CODE ***
|
||||
; Put the pointer to the "regular" blkdev selection in DE
|
||||
blkSelPtr:
|
||||
ld de, BLOCKDEV_SEL
|
||||
|
||||
; Select block index specified in A and place them in routine pointers at (DE).
|
||||
; For example, for a "regular" blkSel, you will want to set DE to BLOCKDEV_SEL.
|
||||
; Sets Z on success, reset on error.
|
||||
; If A >= BLOCKDEV_COUNT, it's an error.
|
||||
blkSel:
|
||||
cp BLOCKDEV_COUNT
|
||||
jp nc, unsetZ ; if selection >= device count, error
|
||||
push af
|
||||
push de
|
||||
push hl
|
||||
|
||||
ld hl, blkDevTbl
|
||||
or a ; cp 0
|
||||
jr z, .end ; index is zero? don't loop
|
||||
push bc ; <|
|
||||
ld b, a ; |
|
||||
.loop: ; |
|
||||
ld a, 4 ; |
|
||||
call addHL ; |
|
||||
djnz .loop ; |
|
||||
pop bc ; <|
|
||||
.end:
|
||||
call blkSet
|
||||
pop hl
|
||||
pop de
|
||||
pop af
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Setup blkdev handle in (DE) using routines at (HL).
|
||||
blkSet:
|
||||
push af
|
||||
push de
|
||||
push hl
|
||||
push bc
|
||||
|
||||
ld bc, 4
|
||||
ldir
|
||||
; Initialize pos
|
||||
ld b, 4
|
||||
xor a
|
||||
ex de, hl
|
||||
call fill
|
||||
|
||||
pop bc
|
||||
pop hl
|
||||
pop de
|
||||
pop af
|
||||
ret
|
||||
|
||||
_blkInc:
|
||||
ret nz ; don't advance when in error condition
|
||||
push af
|
||||
push hl
|
||||
ld a, BLOCKDEV_SEEK_FORWARD
|
||||
ld hl, 1
|
||||
call _blkSeek
|
||||
pop hl
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Reads one byte from selected device and returns its value in A.
|
||||
; Sets Z according to whether read was successful: Set if successful, unset
|
||||
; if not.
|
||||
blkGetB:
|
||||
push ix
|
||||
ld ix, BLOCKDEV_SEL
|
||||
call _blkGetB
|
||||
pop ix
|
||||
ret
|
||||
_blkGetB:
|
||||
push hl
|
||||
push de
|
||||
call _blkTell
|
||||
call callIXI
|
||||
pop de
|
||||
pop hl
|
||||
jr _blkInc ; advance and return
|
||||
|
||||
; Writes byte in A in current position in the selected device. Sets Z according
|
||||
; to whether the operation was successful.
|
||||
blkPutB:
|
||||
push ix
|
||||
ld ix, BLOCKDEV_SEL
|
||||
call _blkPutB
|
||||
pop ix
|
||||
ret
|
||||
_blkPutB:
|
||||
push ix
|
||||
push hl
|
||||
push de
|
||||
call _blkTell
|
||||
inc ix ; make IX point to PutB
|
||||
inc ix
|
||||
call callIXI
|
||||
pop de
|
||||
pop hl
|
||||
pop ix
|
||||
jr _blkInc ; advance and return
|
||||
|
||||
; Reads B chars from blkGetB and copy them in (HL).
|
||||
; Sets Z if successful, unset Z if there was an error.
|
||||
blkRead:
|
||||
push ix
|
||||
ld ix, BLOCKDEV_SEL
|
||||
call _blkRead
|
||||
pop ix
|
||||
ret
|
||||
_blkRead:
|
||||
push hl
|
||||
push bc
|
||||
.loop:
|
||||
call _blkGetB
|
||||
jr nz, .end ; Z already unset
|
||||
ld (hl), a
|
||||
inc hl
|
||||
djnz .loop
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop bc
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Writes B chars to blkPutB from (HL).
|
||||
; Sets Z if successful, unset Z if there was an error.
|
||||
blkWrite:
|
||||
push ix
|
||||
ld ix, BLOCKDEV_SEL
|
||||
call _blkWrite
|
||||
pop ix
|
||||
ret
|
||||
_blkWrite:
|
||||
push hl
|
||||
push bc
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
call _blkPutB
|
||||
jr nz, .end ; Z already unset
|
||||
inc hl
|
||||
djnz .loop
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop bc
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Seeks the block device in one of 5 modes, which is the A argument:
|
||||
; 0 : Move exactly to X, X being the HL/DE argument.
|
||||
; 1 : Move forward by X bytes, X being the HL argument (no DE)
|
||||
; 2 : Move backwards by X bytes, X being the HL argument (no DE)
|
||||
; 3 : Move to the end
|
||||
; 4 : Move to the beginning
|
||||
|
||||
; Set position of selected device to the value specified in HL (low) and DE
|
||||
; (high). DE is only used for mode 0.
|
||||
;
|
||||
; When seeking to an out-of-bounds position, the resulting position will be
|
||||
; one position ahead of the last valid position. Therefore, GetB after a seek
|
||||
; to end would always fail.
|
||||
;
|
||||
; If the device is "growable", it's possible that seeking to end when calling
|
||||
; PutB doesn't necessarily result in a failure.
|
||||
blkSeek:
|
||||
push ix
|
||||
ld ix, BLOCKDEV_SEL
|
||||
call _blkSeek
|
||||
pop ix
|
||||
ret
|
||||
_blkSeek:
|
||||
cp BLOCKDEV_SEEK_FORWARD
|
||||
jr z, .forward
|
||||
cp BLOCKDEV_SEEK_BACKWARD
|
||||
jr z, .backward
|
||||
cp BLOCKDEV_SEEK_BEGINNING
|
||||
jr z, .beginning
|
||||
cp BLOCKDEV_SEEK_END
|
||||
jr z, .end
|
||||
; all other modes are considered absolute
|
||||
ld (ix+4), e
|
||||
ld (ix+5), d
|
||||
ld (ix+6), l
|
||||
ld (ix+7), h
|
||||
ret
|
||||
.forward:
|
||||
push bc ; <-|
|
||||
push hl ; <||
|
||||
ld l, (ix+6) ; || low byte
|
||||
ld h, (ix+7) ; ||
|
||||
pop bc ; <||
|
||||
add hl, bc ; |
|
||||
pop bc ; <-|
|
||||
ld (ix+6), l
|
||||
ld (ix+7), h
|
||||
ret nc ; no carry? no need to adjust high byte
|
||||
; carry, adjust high byte
|
||||
inc (ix+4)
|
||||
ret nz
|
||||
inc (ix+5)
|
||||
ret
|
||||
.backward:
|
||||
and a ; clear carry
|
||||
push bc ; <-|
|
||||
push hl ; <||
|
||||
ld l, (ix+6) ; || low byte
|
||||
ld h, (ix+7) ; ||
|
||||
pop bc ; <||
|
||||
sbc hl, bc ; |
|
||||
pop bc ; <-|
|
||||
ld (ix+6), l
|
||||
ld (ix+7), h
|
||||
ret nc ; no carry? no need to adjust high byte
|
||||
ld a, 0xff
|
||||
dec (ix+4)
|
||||
cp (ix+4)
|
||||
ret nz
|
||||
; we decremented from 0
|
||||
dec (ix+5)
|
||||
ret
|
||||
.beginning:
|
||||
xor a
|
||||
ld (ix+4), a
|
||||
ld (ix+5), a
|
||||
ld (ix+6), a
|
||||
ld (ix+7), a
|
||||
ret
|
||||
.end:
|
||||
ld a, 0xff
|
||||
ld (ix+4), a
|
||||
ld (ix+5), a
|
||||
ld (ix+6), a
|
||||
ld (ix+7), a
|
||||
ret
|
||||
|
||||
; Returns the current position of the selected device in HL (low) and DE (high).
|
||||
blkTell:
|
||||
push ix
|
||||
ld ix, BLOCKDEV_SEL
|
||||
call _blkTell
|
||||
pop ix
|
||||
ret
|
||||
_blkTell:
|
||||
ld e, (ix+4)
|
||||
ld d, (ix+5)
|
||||
ld l, (ix+6)
|
||||
ld h, (ix+7)
|
||||
ret
|
||||
|
||||
; This label is at the end of the file on purpose: the glue file should include
|
||||
; a list of device routine table entries just after the include. Each line
|
||||
; has 2 word addresses: GetB and PutB. An entry could look like:
|
||||
; .dw mmapGetB, mmapPutB
|
||||
blkDevTbl:
|
@ -1,85 +0,0 @@
|
||||
; core
|
||||
;
|
||||
; Routines used pretty much all everywhere. Unlike all other kernel units,
|
||||
; this unit is designed to be included directly by userspace apps, not accessed
|
||||
; through jump tables. The reason for this is that jump tables are a little
|
||||
; costly in terms of machine cycles and that these routines are not very costly
|
||||
; in terms of binary space.
|
||||
; Therefore, this unit has to stay small and tight because it's repeated both
|
||||
; in the kernel and in userspace. It should also be exclusively for routines
|
||||
; used in the kernel.
|
||||
|
||||
; add the value of A into DE
|
||||
addDE:
|
||||
push af
|
||||
add a, e
|
||||
jr nc, .end ; no carry? skip inc
|
||||
inc d
|
||||
.end:
|
||||
ld e, a
|
||||
pop af
|
||||
noop: ; piggy backing on the first "ret" we have
|
||||
ret
|
||||
|
||||
; add the value of A into HL
|
||||
; affects carry flag according to the 16-bit addition, Z, S and P untouched.
|
||||
addHL:
|
||||
push de
|
||||
ld d, 0
|
||||
ld e, a
|
||||
add hl, de
|
||||
pop de
|
||||
ret
|
||||
|
||||
|
||||
; copy (HL) into DE, then exchange the two, utilising the optimised HL instructions.
|
||||
; ld must be done little endian, so least significant byte first.
|
||||
intoHL:
|
||||
push de
|
||||
ld e, (hl)
|
||||
inc hl
|
||||
ld d, (hl)
|
||||
ex de, hl
|
||||
pop de
|
||||
ret
|
||||
|
||||
intoDE:
|
||||
ex de, hl
|
||||
call intoHL
|
||||
ex de, hl ; de preserved by intoHL, so no push/pop needed
|
||||
ret
|
||||
|
||||
intoIX:
|
||||
push ix
|
||||
ex (sp), hl ;swap hl with ix, on the stack
|
||||
call intoHL
|
||||
ex (sp), hl ;restore hl from stack
|
||||
pop ix
|
||||
ret
|
||||
|
||||
; Call the method (IX) is a pointer to. In other words, call intoIX before
|
||||
; callIX
|
||||
callIXI:
|
||||
push ix
|
||||
call intoIX
|
||||
call callIX
|
||||
pop ix
|
||||
ret
|
||||
|
||||
; jump to the location pointed to by IX. This allows us to call IX instead of
|
||||
; just jumping it. We use IX because we seldom use this for arguments.
|
||||
callIX:
|
||||
jp (ix)
|
||||
|
||||
callIY:
|
||||
jp (iy)
|
||||
|
||||
; Ensures that Z is unset (more complicated than it sounds...)
|
||||
; There are often better inline alternatives, either replacing rets with
|
||||
; appropriate jmps, or if an 8 bit register is known to not be 0, an inc
|
||||
; then a dec. If a is nonzero, 'or a' is optimal.
|
||||
unsetZ:
|
||||
or a ;if a nonzero, Z reset
|
||||
ret nz
|
||||
cp 1 ;if a is zero, Z reset
|
||||
ret
|
14
kernel/err.h
14
kernel/err.h
@ -1,14 +0,0 @@
|
||||
; Error codes used throughout the kernel
|
||||
|
||||
; The command that was type isn't known to the shell
|
||||
.equ SHELL_ERR_UNKNOWN_CMD 0x01
|
||||
|
||||
; Arguments for the command weren't properly formatted
|
||||
.equ SHELL_ERR_BAD_ARGS 0x02
|
||||
|
||||
.equ BLOCKDEV_ERR_OUT_OF_BOUNDS 0x03
|
||||
.equ BLOCKDEV_ERR_UNSUPPORTED 0x04
|
||||
|
||||
; IO routines (GetB, PutB) returned an error in a load/save command
|
||||
.equ SHELL_ERR_IO_ERROR 0x05
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,32 +0,0 @@
|
||||
; Font management
|
||||
;
|
||||
; There can only ever be one active font.
|
||||
;
|
||||
; *** Defines ***
|
||||
; FNT_DATA: Pointer to the beginning of the binary font data to work with.
|
||||
; FNT_WIDTH: Width of the font.
|
||||
; FNT_HEIGHT: Height of the font.
|
||||
;
|
||||
; *** Code ***
|
||||
|
||||
; If A is in the range 0x20-0x7e, make HL point to the beginning of the
|
||||
; corresponding glyph and set Z to indicate success.
|
||||
; If A isn't in the range, do nothing and unset Z.
|
||||
fntGet:
|
||||
cp 0x20
|
||||
ret c ; A < 0x20. Z was unset by cp
|
||||
cp 0x7f
|
||||
jp nc, unsetZ ; A >= 0x7f. Z might be set
|
||||
|
||||
push af ; --> lvl 1
|
||||
push bc ; --> lvl 2
|
||||
sub 0x20
|
||||
ld hl, FNT_DATA
|
||||
ld b, FNT_HEIGHT
|
||||
.loop:
|
||||
call addHL
|
||||
djnz .loop
|
||||
pop bc ; <-- lvl 2
|
||||
pop af ; <-- lvl 1
|
||||
cp a ; set Z
|
||||
ret
|
575
kernel/fs.asm
575
kernel/fs.asm
@ -1,575 +0,0 @@
|
||||
; fs
|
||||
;
|
||||
; Collapse OS filesystem (CFS) is not made to be convenient, but to be simple.
|
||||
; This is little more than "named storage blocks". Characteristics:
|
||||
;
|
||||
; * a filesystem sits upon a blockdev. It needs GetB, PutB, Seek.
|
||||
; * No directory. Use filename prefix to group.
|
||||
; * First block of each file has metadata. Others are raw data.
|
||||
; * No FAT. Files are a chain of blocks of a predefined size. To enumerate
|
||||
; files, you go through metadata blocks.
|
||||
; * Fixed allocation. File size is determined at allocation time and cannot be
|
||||
; grown, only shrunk.
|
||||
; * New allocations try to find spots to fit in, but go at the end if no spot is
|
||||
; large enough.
|
||||
; * Block size is 0x100, max block count per file is 8bit, that means that max
|
||||
; file size: 64k - metadata overhead.
|
||||
;
|
||||
; *** Selecting a "source" blockdev
|
||||
;
|
||||
; This unit exposes "fson" shell command to "mount" CFS upon the currently
|
||||
; selected device, at the point where its seekptr currently sits. This checks
|
||||
; if we have a valid first block and spits an error otherwise.
|
||||
;
|
||||
; "fson" takes an optional argument which is a number. If non-zero, we don't
|
||||
; error out if there's no metadata: we create a new CFS fs with an empty block.
|
||||
;
|
||||
; The can only be one "mounted" fs at once. Selecting another blockdev through
|
||||
; "bsel" doesn't affect the currently mounted fs, which can still be interacted
|
||||
; with (which is important if we want to move data around).
|
||||
;
|
||||
; *** Block metadata
|
||||
;
|
||||
; At the beginning of the first block of each file, there is this data
|
||||
; structure:
|
||||
;
|
||||
; 3b: Magic number "CFS"
|
||||
; 1b: Allocated block count, including the first one. Except for the "ending"
|
||||
; block, this is never zero.
|
||||
; 2b: Size of file in bytes (actually written). Little endian.
|
||||
; 26b: file name, null terminated. last byte must be null.
|
||||
;
|
||||
; That gives us 32 bytes of metadata for first first block, leaving a maximum
|
||||
; file size of 0xffe0.
|
||||
;
|
||||
; *** Last block of the chain
|
||||
;
|
||||
; The last block of the chain is either a block that has no valid block next to
|
||||
; it or a block that reports a 0 allocated block count.
|
||||
;
|
||||
; However, to simplify processing, whenever fsNext encounter a chain end of the
|
||||
; first type (a valid block with > 0 allocated blocks), it places an empty block
|
||||
; at the end of the chain. This makes the whole "end of chain" processing much
|
||||
; easier: we assume that we always have a 0 block at the end.
|
||||
;
|
||||
; *** Deleted files
|
||||
;
|
||||
; When a file is deleted, its name is set to null. This indicates that the
|
||||
; allocated space is up for grabs.
|
||||
;
|
||||
; *** File "handles"
|
||||
;
|
||||
; Programs will not typically open files themselves. How it works with CFS is
|
||||
; that it exposes an API to plug target files in a blockdev ID. This all
|
||||
; depends on how you glue parts together, but ideally, you'll have two
|
||||
; fs-related blockdev IDs: one for reading, one for writing.
|
||||
;
|
||||
; Being plugged into the blockdev system, programs will access the files as they
|
||||
; would with any other block device.
|
||||
;
|
||||
; *** Creating a new FS
|
||||
;
|
||||
; A valid Collapse OS filesystem is nothing more than the 3 bytes 'C', 'F', 'S'
|
||||
; next to each other. Placing them at the right place is all you have to do to
|
||||
; create your FS.
|
||||
|
||||
; *** DEFINES ***
|
||||
; Number of handles we want to support
|
||||
; FS_HANDLE_COUNT
|
||||
;
|
||||
; *** VARIABLES ***
|
||||
; A copy of BLOCKDEV_SEL when the FS was mounted. 0 if no FS is mounted.
|
||||
.equ FS_BLK FS_RAMSTART
|
||||
; Offset at which our FS start on mounted device
|
||||
; This pointer is 32 bits. 32 bits pointers are a bit awkward: first two bytes
|
||||
; are high bytes *low byte first*, and then the low two bytes, same order.
|
||||
; When loaded in HL/DE, the four bytes are loaded in this order: E, D, L, H
|
||||
.equ FS_START @+BLOCKDEV_SIZE
|
||||
; This variable below contain the metadata of the last block we moved
|
||||
; to. We read this data in memory to avoid constant seek+read operations.
|
||||
.equ FS_META @+4
|
||||
.equ FS_HANDLES @+FS_METASIZE
|
||||
.equ FS_RAMEND @+FS_HANDLE_COUNT*FS_HANDLE_SIZE
|
||||
|
||||
; *** DATA ***
|
||||
P_FS_MAGIC:
|
||||
.db "CFS", 0
|
||||
|
||||
; *** CODE ***
|
||||
|
||||
fsInit:
|
||||
xor a
|
||||
ld hl, FS_BLK
|
||||
ld b, FS_RAMEND-FS_BLK
|
||||
jp fill
|
||||
|
||||
; *** Navigation ***
|
||||
|
||||
; Seek to the beginning. Errors out if no FS is mounted.
|
||||
; Sets Z if success, unset if error
|
||||
fsBegin:
|
||||
call fsIsOn
|
||||
ret nz
|
||||
push hl
|
||||
push de
|
||||
push af
|
||||
ld de, (FS_START)
|
||||
ld hl, (FS_START+2)
|
||||
ld a, BLOCKDEV_SEEK_ABSOLUTE
|
||||
call fsblkSeek
|
||||
pop af
|
||||
pop de
|
||||
pop hl
|
||||
call fsReadMeta
|
||||
jp fsIsValid ; sets Z, returns
|
||||
|
||||
; Change current position to the next block with metadata. If it can't (if this
|
||||
; is the last valid block), doesn't move.
|
||||
; Sets Z according to whether we moved.
|
||||
fsNext:
|
||||
push bc
|
||||
push hl
|
||||
ld a, (FS_META+FS_META_ALLOC_OFFSET)
|
||||
or a ; cp 0
|
||||
jr z, .error ; if our block allocates 0 blocks, this is the
|
||||
; end of the line.
|
||||
ld b, a ; we will seek A times
|
||||
.loop:
|
||||
ld a, BLOCKDEV_SEEK_FORWARD
|
||||
ld hl, FS_BLOCKSIZE
|
||||
call fsblkSeek
|
||||
djnz .loop
|
||||
call fsReadMeta
|
||||
jr nz, .createChainEnd
|
||||
call fsIsValid
|
||||
jr nz, .createChainEnd
|
||||
; We're good! We have a valid FS block.
|
||||
; Meta is already read. Nothing to do!
|
||||
cp a ; ensure Z
|
||||
jr .end
|
||||
.createChainEnd:
|
||||
; We are on an invalid block where a valid block should be. This is
|
||||
; the end of the line, but we should mark it a bit more explicitly.
|
||||
; Let's initialize an empty block
|
||||
call fsInitMeta
|
||||
call fsWriteMeta
|
||||
; continue out to error condition: we're still at the end of the line.
|
||||
.error:
|
||||
call unsetZ
|
||||
.end:
|
||||
pop hl
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Reads metadata at current fsblk and place it in FS_META.
|
||||
; Returns Z according to whether the operation succeeded.
|
||||
fsReadMeta:
|
||||
push bc
|
||||
push hl
|
||||
ld b, FS_METASIZE
|
||||
ld hl, FS_META
|
||||
call fsblkRead ; Sets Z
|
||||
pop hl
|
||||
pop bc
|
||||
ret nz
|
||||
; Only rewind on success
|
||||
jr _fsRewindAfterMeta
|
||||
|
||||
; Writes metadata in FS_META at current fsblk.
|
||||
; Returns Z according to whether the fsblkWrite operation succeeded.
|
||||
fsWriteMeta:
|
||||
push bc
|
||||
push hl
|
||||
ld b, FS_METASIZE
|
||||
ld hl, FS_META
|
||||
call fsblkWrite ; Sets Z
|
||||
pop hl
|
||||
pop bc
|
||||
ret nz
|
||||
; Only rewind on success
|
||||
jr _fsRewindAfterMeta
|
||||
|
||||
_fsRewindAfterMeta:
|
||||
; return back to before the read op
|
||||
push af
|
||||
push hl
|
||||
ld a, BLOCKDEV_SEEK_BACKWARD
|
||||
ld hl, FS_METASIZE
|
||||
call fsblkSeek
|
||||
pop hl
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Initializes FS_META with "CFS" followed by zeroes
|
||||
fsInitMeta:
|
||||
push af
|
||||
push bc
|
||||
push de
|
||||
push hl
|
||||
ld hl, P_FS_MAGIC
|
||||
ld de, FS_META
|
||||
ld bc, 3
|
||||
ldir
|
||||
xor a
|
||||
ld hl, FS_META+3
|
||||
ld b, FS_METASIZE-3
|
||||
call fill
|
||||
pop hl
|
||||
pop de
|
||||
pop bc
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Create a new file with A blocks allocated to it and with its new name at
|
||||
; (HL).
|
||||
; Before doing so, enumerate all blocks in search of a deleted file with
|
||||
; allocated space big enough. If it does, it will either take the whole space
|
||||
; if the allocated space asked is exactly the same, or of it isn't, split the
|
||||
; free space in 2 and create a new deleted metadata block next to the newly
|
||||
; created block.
|
||||
; Places fsblk to the newly allocated block. You have to write the new
|
||||
; filename yourself.
|
||||
fsAlloc:
|
||||
push bc
|
||||
push de
|
||||
ld c, a ; Let's store our A arg somewhere...
|
||||
call fsBegin
|
||||
jr nz, .end ; not a valid block? hum, something's wrong
|
||||
; First step: find last block
|
||||
push hl ; keep HL for later
|
||||
.loop1:
|
||||
call fsNext
|
||||
jr nz, .found ; end of the line
|
||||
call fsIsDeleted
|
||||
jr nz, .loop1 ; not deleted? loop
|
||||
; This is a deleted block. Maybe it fits...
|
||||
ld a, (FS_META+FS_META_ALLOC_OFFSET)
|
||||
cp c ; Same as asked size?
|
||||
jr z, .found ; yes? great!
|
||||
; TODO: handle case where C < A (block splitting)
|
||||
jr .loop1
|
||||
.found:
|
||||
; We've reached last block. Two situations are possible at this point:
|
||||
; 1 - the block is the "end of line" block
|
||||
; 2 - the block is a deleted block that we we're re-using.
|
||||
; In both case, the processing is the same: write new metadata.
|
||||
; At this point, the blockdev is placed right where we want to allocate
|
||||
; But first, let's prepare the FS_META we're going to write
|
||||
call fsInitMeta
|
||||
ld a, c ; C == the number of blocks user asked for
|
||||
ld (FS_META+FS_META_ALLOC_OFFSET), a
|
||||
pop hl ; now we want our HL arg
|
||||
; TODO: stop after null char. we're filling meta with garbage here.
|
||||
ld de, FS_META+FS_META_FNAME_OFFSET
|
||||
ld bc, FS_MAX_NAME_SIZE
|
||||
ldir
|
||||
; Good, FS_META ready.
|
||||
; Ok, now we can write our metadata
|
||||
call fsWriteMeta
|
||||
.end:
|
||||
pop de
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Place fsblk to the filename with the name in (HL).
|
||||
; Sets Z on success, unset when not found.
|
||||
fsFindFN:
|
||||
push de
|
||||
call fsBegin
|
||||
jr nz, .end ; nothing to find, Z is unset
|
||||
ld a, FS_MAX_NAME_SIZE
|
||||
.loop:
|
||||
ld de, FS_META+FS_META_FNAME_OFFSET
|
||||
call strncmp
|
||||
jr z, .end ; Z is set
|
||||
call fsNext
|
||||
jr z, .loop
|
||||
; End of the chain, not found
|
||||
; Z already unset
|
||||
.end:
|
||||
pop de
|
||||
ret
|
||||
|
||||
; *** Metadata ***
|
||||
|
||||
; Sets Z according to whether the current block in FS_META is valid.
|
||||
; Don't call other FS routines without checking block validity first: other
|
||||
; routines don't do checks.
|
||||
fsIsValid:
|
||||
push hl
|
||||
push de
|
||||
ld a, 3
|
||||
ld hl, FS_META
|
||||
ld de, P_FS_MAGIC
|
||||
call strncmp
|
||||
; The result of Z is our result.
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Returns whether current block is deleted in Z flag.
|
||||
fsIsDeleted:
|
||||
ld a, (FS_META+FS_META_FNAME_OFFSET)
|
||||
or a ; Z flag is our answer
|
||||
ret
|
||||
|
||||
; *** blkdev methods ***
|
||||
; When "mounting" a FS, we copy the current blkdev's routine privately so that
|
||||
; we can still access the FS even if blkdev selection changes. These routines
|
||||
; below mimic blkdev's methods, but for our private mount.
|
||||
|
||||
fsblkGetB:
|
||||
push ix
|
||||
ld ix, FS_BLK
|
||||
call _blkGetB
|
||||
pop ix
|
||||
ret
|
||||
|
||||
fsblkRead:
|
||||
push ix
|
||||
ld ix, FS_BLK
|
||||
call _blkRead
|
||||
pop ix
|
||||
ret
|
||||
|
||||
fsblkPutB:
|
||||
push ix
|
||||
ld ix, FS_BLK
|
||||
call _blkPutB
|
||||
pop ix
|
||||
ret
|
||||
|
||||
fsblkWrite:
|
||||
push ix
|
||||
ld ix, FS_BLK
|
||||
call _blkWrite
|
||||
pop ix
|
||||
ret
|
||||
|
||||
fsblkSeek:
|
||||
push ix
|
||||
ld ix, FS_BLK
|
||||
call _blkSeek
|
||||
pop ix
|
||||
ret
|
||||
|
||||
fsblkTell:
|
||||
push ix
|
||||
ld ix, FS_BLK
|
||||
call _blkTell
|
||||
pop ix
|
||||
ret
|
||||
|
||||
; *** Handling ***
|
||||
|
||||
; Open file at current position into handle at (IX)
|
||||
fsOpen:
|
||||
push hl
|
||||
push af
|
||||
; Starting pos
|
||||
ld a, (FS_BLK+4)
|
||||
ld (ix), a
|
||||
ld a, (FS_BLK+5)
|
||||
ld (ix+1), a
|
||||
ld a, (FS_BLK+6)
|
||||
ld (ix+2), a
|
||||
ld a, (FS_BLK+7)
|
||||
ld (ix+3), a
|
||||
; file size
|
||||
ld hl, (FS_META+FS_META_FSIZE_OFFSET)
|
||||
ld (ix+4), l
|
||||
ld (ix+5), h
|
||||
pop af
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Place FS blockdev at proper position for file handle in (IX) at position HL.
|
||||
fsPlaceH:
|
||||
push af
|
||||
push de
|
||||
push hl
|
||||
; Move fsdev to beginning of block
|
||||
ld e, (ix)
|
||||
ld d, (ix+1)
|
||||
ld l, (ix+2)
|
||||
ld h, (ix+3)
|
||||
ld a, BLOCKDEV_SEEK_ABSOLUTE
|
||||
call fsblkSeek
|
||||
|
||||
; skip metadata
|
||||
ld a, BLOCKDEV_SEEK_FORWARD
|
||||
ld hl, FS_METASIZE
|
||||
call fsblkSeek
|
||||
|
||||
pop hl
|
||||
pop de
|
||||
|
||||
; go to specified pos
|
||||
ld a, BLOCKDEV_SEEK_FORWARD
|
||||
call fsblkSeek
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Sets Z according to whether HL is within bounds for file handle at (IX), that
|
||||
; is, if it is smaller than file size.
|
||||
fsWithinBounds:
|
||||
ld a, h
|
||||
cp (ix+5)
|
||||
jr c, .within ; H < (IX+5)
|
||||
jp nz, unsetZ ; H > (IX+5)
|
||||
; H == (IX+5)
|
||||
ld a, l
|
||||
cp (ix+4)
|
||||
jp nc, unsetZ ; L >= (IX+4)
|
||||
.within:
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Set size of file handle (IX) to value in HL.
|
||||
; This writes directly in handle's metadata.
|
||||
fsSetSize:
|
||||
push hl ; --> lvl 1
|
||||
ld hl, 0
|
||||
call fsPlaceH ; fs blkdev is now at beginning of content
|
||||
; we need the blkdev to be on filesize's offset
|
||||
ld hl, FS_METASIZE-FS_META_FSIZE_OFFSET
|
||||
ld a, BLOCKDEV_SEEK_BACKWARD
|
||||
call fsblkSeek
|
||||
pop hl ; <-- lvl 1
|
||||
; blkdev is at the right spot, HL is back to its original value, let's
|
||||
; write it both in the metadata block and in its file handle's cache.
|
||||
push hl ; --> lvl 1
|
||||
; now let's write our new filesize both in blkdev and in file handle's
|
||||
; cache.
|
||||
ld a, l
|
||||
ld (ix+4), a
|
||||
call fsblkPutB
|
||||
ld a, h
|
||||
ld (ix+5), a
|
||||
call fsblkPutB
|
||||
pop hl ; <-- lvl 1
|
||||
xor a ; ensure Z
|
||||
ret
|
||||
|
||||
; Read a byte in handle at (IX) at position HL and put it into A.
|
||||
; Z is set on success, unset if handle is at the end of the file.
|
||||
fsGetB:
|
||||
call fsWithinBounds
|
||||
jr z, .proceed
|
||||
; We want to unset Z, but also return 0 to ensure that a GetB that
|
||||
; doesn't check Z doesn't end up with false data.
|
||||
xor a
|
||||
jp unsetZ ; returns
|
||||
.proceed:
|
||||
push hl
|
||||
call fsPlaceH
|
||||
call fsblkGetB
|
||||
cp a ; ensure Z
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Write byte A in handle (IX) at position HL.
|
||||
; Z is set on success, unset if handle is at the end of the file.
|
||||
; TODO: detect end of block alloc
|
||||
fsPutB:
|
||||
push hl
|
||||
call fsPlaceH
|
||||
call fsblkPutB
|
||||
pop hl
|
||||
; if HL is out of bounds, increase bounds
|
||||
call fsWithinBounds
|
||||
ret z
|
||||
inc hl ; our filesize is now HL+1
|
||||
jp fsSetSize
|
||||
|
||||
; Mount the fs subsystem upon the currently selected blockdev at current offset.
|
||||
; Verify is block is valid and error out if its not, mounting nothing.
|
||||
; Upon mounting, copy currently selected device in FS_BLK.
|
||||
fsOn:
|
||||
push hl
|
||||
push de
|
||||
push bc
|
||||
; We have to set blkdev routines early before knowing whether the
|
||||
; mounting succeeds because methods like fsReadMeta uses fsblk* methods.
|
||||
ld hl, BLOCKDEV_SEL
|
||||
ld de, FS_BLK
|
||||
ld bc, BLOCKDEV_SIZE
|
||||
ldir ; copy!
|
||||
call fsblkTell
|
||||
ld (FS_START), de
|
||||
ld (FS_START+2), hl
|
||||
call fsReadMeta
|
||||
jr nz, .error
|
||||
call fsIsValid
|
||||
jr nz, .error
|
||||
; success
|
||||
xor a
|
||||
jr .end
|
||||
.error:
|
||||
; couldn't mount. Let's reset our variables.
|
||||
call fsInit
|
||||
ld a, FS_ERR_NO_FS
|
||||
or a ; unset Z
|
||||
.end:
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Sets Z according to whether we have a filesystem mounted.
|
||||
fsIsOn:
|
||||
; check whether (FS_BLK) is zero
|
||||
push hl
|
||||
ld hl, (FS_BLK)
|
||||
ld a, h
|
||||
or l
|
||||
jr nz, .mounted
|
||||
; not mounted, unset Z
|
||||
inc a
|
||||
jr .end
|
||||
.mounted:
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Iterate over files in active file system and, for each file, call (IY) with
|
||||
; the file's metadata currently placed. HL is set to FS_META.
|
||||
; Sets Z on success, unset on error.
|
||||
; There are no error condition happening midway. If you get an error, then (IY)
|
||||
; was never called.
|
||||
fsIter:
|
||||
call fsBegin
|
||||
ret nz
|
||||
.loop:
|
||||
call fsIsDeleted
|
||||
ld hl, FS_META
|
||||
call nz, callIY
|
||||
call fsNext
|
||||
jr z, .loop ; Z set? fsNext was successful
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
|
||||
; Delete currently active file
|
||||
; Sets Z on success, unset on error.
|
||||
fsDel:
|
||||
call fsIsValid
|
||||
ret nz
|
||||
xor a
|
||||
; Set filename to zero to flag it as deleted
|
||||
ld (FS_META+FS_META_FNAME_OFFSET), a
|
||||
jp fsWriteMeta
|
||||
|
||||
; Given a handle index in A, set DE to point to the proper handle.
|
||||
fsHandle:
|
||||
ld de, FS_HANDLES
|
||||
or a ; cp 0
|
||||
ret z ; DE already point to correct handle
|
||||
push bc
|
||||
ld b, a
|
||||
.loop:
|
||||
ld a, FS_HANDLE_SIZE
|
||||
call addDE
|
||||
djnz .loop
|
||||
pop bc
|
||||
ret
|
13
kernel/fs.h
13
kernel/fs.h
@ -1,13 +0,0 @@
|
||||
.equ FS_MAX_NAME_SIZE 0x1a
|
||||
.equ FS_BLOCKSIZE 0x100
|
||||
.equ FS_METASIZE 0x20
|
||||
|
||||
.equ FS_META_ALLOC_OFFSET 3
|
||||
.equ FS_META_FSIZE_OFFSET 4
|
||||
.equ FS_META_FNAME_OFFSET 6
|
||||
; Size in bytes of a FS handle:
|
||||
; * 4 bytes for starting offset of the FS block
|
||||
; * 2 bytes for file size
|
||||
.equ FS_HANDLE_SIZE 6
|
||||
.equ FS_ERR_NO_FS 0x5
|
||||
.equ FS_ERR_NOT_FOUND 0x6
|
275
kernel/grid.asm
275
kernel/grid.asm
@ -1,275 +0,0 @@
|
||||
; grid - abstraction for grid-like video output
|
||||
;
|
||||
; Collapse OS doesn't support curses-like interfaces: too complicated. However,
|
||||
; in cases where output don't have to go through a serial interface before
|
||||
; being displayed, we have usually have access to a grid-like interface.
|
||||
;
|
||||
; Direct access to this kind of interface allow us to build an abstraction layer
|
||||
; that is very much alike curses but is much simpler underneath. This unit is
|
||||
; this abstraction.
|
||||
;
|
||||
; The principle is simple: we have a cell grid of X columns by Y rows and we
|
||||
; can access those cells by their (X, Y) address. In addition to this, we have
|
||||
; the concept of an active cursor, which will be indicated visually if possible.
|
||||
;
|
||||
; This module provides PutC and GetC routines, suitable for plugging into stdio.
|
||||
; PutC, for obvious reasons, GetC, for less obvious reasons: We need to wrap
|
||||
; GetC because we need to update the cursor before calling actual GetC, but
|
||||
; also, because we need to know when a bulk update ends.
|
||||
;
|
||||
; *** Defines ***
|
||||
;
|
||||
; GRID_COLS: Number of columns in the grid
|
||||
; GRID_ROWS: Number of rows in the grid
|
||||
; GRID_SETCELL: Pointer to routine that sets cell at row D and column E with
|
||||
; character in A. If C is nonzero, this cell must be displayed,
|
||||
; if possible, as the cursor. This routine is never called with
|
||||
; A < 0x20.
|
||||
; GRID_GETC: Routine that gridGetC will wrap around.
|
||||
;
|
||||
; *** Consts ***
|
||||
.equ GRID_SIZE GRID_COLS*GRID_ROWS
|
||||
|
||||
; *** Variables ***
|
||||
; Cursor's column
|
||||
.equ GRID_CURX GRID_RAMSTART
|
||||
; Cursor's row
|
||||
.equ GRID_CURY @+1
|
||||
; Whether we scrolled recently. We don't refresh the screen immediately when
|
||||
; scrolling in case we have many lines being spit at once (refreshing the
|
||||
; display is then very slow). Instead, we wait until the next gridGetC call
|
||||
.equ GRID_SCROLLED @+1
|
||||
; Grid's in-memory buffer of the contents on screen. Because we always push to
|
||||
; display right after a change, this is almost always going to be a correct
|
||||
; representation of on-screen display.
|
||||
; The buffer is organized as a rows of columns. The cell at row Y and column X
|
||||
; is at GRID_BUF+(Y*GRID_COLS)+X.
|
||||
.equ GRID_BUF @+1
|
||||
.equ GRID_RAMEND @+GRID_SIZE
|
||||
|
||||
; *** Code ***
|
||||
|
||||
gridInit:
|
||||
xor a
|
||||
ld b, GRID_RAMEND-GRID_RAMEND
|
||||
ld hl, GRID_RAMSTART
|
||||
jp fill
|
||||
|
||||
; Place HL at row D and column E in the buffer
|
||||
; Destroys A
|
||||
_gridPlaceCell:
|
||||
ld hl, GRID_BUF
|
||||
ld a, d
|
||||
or a
|
||||
jr z, .setcol
|
||||
push de ; --> lvl 1
|
||||
ld de, GRID_COLS
|
||||
.loop:
|
||||
add hl, de
|
||||
dec a
|
||||
jr nz, .loop
|
||||
pop de ; <-- lvl 1
|
||||
.setcol:
|
||||
; We're at the proper row, now let's advance to cell
|
||||
ld a, e
|
||||
jp addHL
|
||||
|
||||
; Ensure that A >= 0x20
|
||||
_gridAdjustA:
|
||||
cp 0x20
|
||||
ret nc
|
||||
ld a, 0x20
|
||||
ret
|
||||
|
||||
; Push row D in the buffer onto the screen.
|
||||
gridPushRow:
|
||||
push af
|
||||
push bc
|
||||
push de
|
||||
push hl
|
||||
; Cursor off
|
||||
ld c, 0
|
||||
ld e, c
|
||||
call _gridPlaceCell
|
||||
ld b, GRID_COLS
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
call _gridAdjustA
|
||||
; A, C, D and E have proper values
|
||||
call GRID_SETCELL
|
||||
inc hl
|
||||
inc e
|
||||
djnz .loop
|
||||
|
||||
pop hl
|
||||
pop de
|
||||
pop bc
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Clear row D and push contents to screen
|
||||
gridClrRow:
|
||||
push af
|
||||
push bc
|
||||
push de
|
||||
push hl
|
||||
ld e, 0
|
||||
call _gridPlaceCell
|
||||
ld a, ' '
|
||||
ld b, GRID_COLS
|
||||
call fill
|
||||
call gridPushRow
|
||||
pop hl
|
||||
pop de
|
||||
pop bc
|
||||
pop af
|
||||
ret
|
||||
|
||||
gridPushScr:
|
||||
push de
|
||||
ld d, GRID_ROWS-1
|
||||
.loop:
|
||||
call gridPushRow
|
||||
dec d
|
||||
jp p, .loop
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Set character under cursor to A. C is passed to GRID_SETCELL as-is.
|
||||
gridSetCur:
|
||||
push de
|
||||
push hl
|
||||
push af ; --> lvl 1
|
||||
ld a, (GRID_CURY)
|
||||
ld d, a
|
||||
ld a, (GRID_CURX)
|
||||
ld e, a
|
||||
call _gridPlaceCell
|
||||
pop af \ push af ; <--> lvl 1
|
||||
ld (hl), a
|
||||
call _gridAdjustA
|
||||
call GRID_SETCELL
|
||||
pop af ; <-- lvl 1
|
||||
pop hl
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Call gridSetCur with C = 1.
|
||||
gridSetCurH:
|
||||
push bc
|
||||
ld c, 1
|
||||
call gridSetCur
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Call gridSetCur with C = 0.
|
||||
gridSetCurL:
|
||||
push bc
|
||||
ld c, 0
|
||||
call gridSetCur
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Clear character under cursor
|
||||
gridClrCur:
|
||||
push af
|
||||
ld a, ' '
|
||||
call gridSetCurL
|
||||
pop af
|
||||
ret
|
||||
|
||||
gridLF:
|
||||
call gridClrCur
|
||||
push de
|
||||
push af
|
||||
ld a, (GRID_CURY)
|
||||
; increase A
|
||||
inc a
|
||||
cp GRID_ROWS
|
||||
jr nz, .noscroll
|
||||
; bottom reached, stay on last line and scroll screen
|
||||
push hl
|
||||
push de
|
||||
push bc
|
||||
ld de, GRID_BUF
|
||||
ld hl, GRID_BUF+GRID_COLS
|
||||
ld bc, GRID_SIZE-GRID_COLS
|
||||
ldir
|
||||
ld hl, GRID_SCROLLED
|
||||
inc (hl) ; mark as scrolled
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
dec a
|
||||
.noscroll:
|
||||
; A has been increased properly
|
||||
ld d, a
|
||||
call gridClrRow
|
||||
ld (GRID_CURY), a
|
||||
xor a
|
||||
ld (GRID_CURX), a
|
||||
pop af
|
||||
pop de
|
||||
ret
|
||||
|
||||
gridBS:
|
||||
call gridClrCur
|
||||
push af
|
||||
ld a, (GRID_CURX)
|
||||
or a
|
||||
jr z, .lineup
|
||||
dec a
|
||||
ld (GRID_CURX), a
|
||||
pop af
|
||||
ret
|
||||
.lineup:
|
||||
; end of line, we need to go up one line. But before we do, are we
|
||||
; already at the top?
|
||||
ld a, (GRID_CURY)
|
||||
or a
|
||||
jr z, .end
|
||||
dec a
|
||||
ld (GRID_CURY), a
|
||||
ld a, GRID_COLS-1
|
||||
ld (GRID_CURX), a
|
||||
.end:
|
||||
pop af
|
||||
ret
|
||||
|
||||
gridPutC:
|
||||
cp LF
|
||||
jr z, gridLF
|
||||
cp BS
|
||||
jr z, gridBS
|
||||
cp ' '
|
||||
ret c ; ignore unhandled control characters
|
||||
|
||||
call gridSetCurL
|
||||
push af ; --> lvl 1
|
||||
; Move cursor
|
||||
ld a, (GRID_CURX)
|
||||
cp GRID_COLS-1
|
||||
jr z, .incline
|
||||
; We just need to increase X
|
||||
inc a
|
||||
ld (GRID_CURX), a
|
||||
pop af ; <-- lvl 1
|
||||
ret
|
||||
.incline:
|
||||
; increase line and start anew
|
||||
call gridLF
|
||||
pop af ; <-- lvl 1
|
||||
ret
|
||||
|
||||
gridGetC:
|
||||
ld a, (GRID_SCROLLED)
|
||||
or a
|
||||
jr z, .nopush
|
||||
; We've scrolled recently, update screen
|
||||
xor a
|
||||
ld (GRID_SCROLLED), a
|
||||
call gridPushScr
|
||||
.nopush:
|
||||
ld a, ' '
|
||||
call gridSetCurH
|
||||
jp GRID_GETC
|
137
kernel/kbd.asm
137
kernel/kbd.asm
@ -1,137 +0,0 @@
|
||||
; kbd - implement GetC for PS/2 keyboard
|
||||
;
|
||||
; It reads raw key codes from a FetchKC routine and returns, if appropriate,
|
||||
; a proper ASCII char to type. See recipes rc2014/ps2 and sms/kbd.
|
||||
;
|
||||
; *** Defines ***
|
||||
; Pointer to a routine that fetches the last typed keyword in A. Should return
|
||||
; 0 when nothing was typed.
|
||||
; KBD_FETCHKC
|
||||
|
||||
; *** Consts ***
|
||||
.equ KBD_KC_BREAK 0xf0
|
||||
.equ KBD_KC_EXT 0xe0
|
||||
.equ KBD_KC_LSHIFT 0x12
|
||||
.equ KBD_KC_RSHIFT 0x59
|
||||
|
||||
; *** Variables ***
|
||||
; Set to previously received scan code
|
||||
.equ KBD_PREV_KC KBD_RAMSTART
|
||||
; Whether Shift key is pressed. When not pressed, holds 0. When pressed, holds
|
||||
; 0x80. This allows for quick shifting in the glyph table.
|
||||
.equ KBD_SHIFT_ON @+1
|
||||
.equ KBD_RAMEND @+1
|
||||
|
||||
kbdInit:
|
||||
xor a
|
||||
ld (KBD_PREV_KC), a
|
||||
ld (KBD_SHIFT_ON), a
|
||||
ret
|
||||
|
||||
kbdGetC:
|
||||
call KBD_FETCHKC
|
||||
or a
|
||||
jr z, .nothing
|
||||
|
||||
; scan code not zero, maybe we have something.
|
||||
; Do we need to skip it?
|
||||
ex af, af' ; save fetched KC
|
||||
ld a, (KBD_PREV_KC)
|
||||
; Whatever the KC, the new A becomes our prev. The easiest way to do
|
||||
; this is to do it now.
|
||||
ex af, af' ; restore KC
|
||||
ld (KBD_PREV_KC), a
|
||||
ex af, af' ; restore prev KC
|
||||
; If F0 (break code) or E0 (extended code), we skip this code
|
||||
cp KBD_KC_BREAK
|
||||
jr z, .break
|
||||
cp KBD_KC_EXT
|
||||
jr z, .nothing
|
||||
ex af, af' ; restore saved KC
|
||||
; A scan code over 0x80 is out of bounds or prev KC tell us we should
|
||||
; skip. Ignore.
|
||||
cp 0x80
|
||||
jr nc, .nothing
|
||||
; No need to skip, code within bounds, we have something!
|
||||
call .isShift
|
||||
jr z, .shiftPressed
|
||||
; Let's see if there's a ASCII code associated to it.
|
||||
push hl ; --> lvl 1
|
||||
ld hl, KBD_SHIFT_ON
|
||||
or (hl) ; if shift is on, A now ranges in 0x80-0xff.
|
||||
ld hl, kbdScanCodes ; no flag changed
|
||||
call addHL
|
||||
ld a, (hl)
|
||||
pop hl ; <-- lvl 1
|
||||
or a
|
||||
jr z, kbdGetC ; no code.
|
||||
; We have something!
|
||||
cp a ; ensure Z
|
||||
ret
|
||||
.shiftPressed:
|
||||
ld a, 0x80
|
||||
ld (KBD_SHIFT_ON), a
|
||||
jr .nothing ; no actual char to return
|
||||
.break:
|
||||
ex af, af' ; restore saved KC
|
||||
call .isShift
|
||||
jr nz, .nothing
|
||||
; We had a shift break, update status
|
||||
xor a
|
||||
ld (KBD_SHIFT_ON), a
|
||||
; continue to .nothing
|
||||
.nothing:
|
||||
; We have nothing. Before we go further, we'll wait a bit to give our
|
||||
; device the time to "breathe". When we're in a "nothing" loop, the z80
|
||||
; hammers the device really fast and continuously generates interrupts
|
||||
; on it and it interferes with its other task of reading the keyboard.
|
||||
xor a
|
||||
.wait:
|
||||
inc a
|
||||
jr nz, .wait
|
||||
jr kbdGetC
|
||||
; Whether KC in A is L or R shift
|
||||
.isShift:
|
||||
cp KBD_KC_LSHIFT
|
||||
ret z
|
||||
cp KBD_KC_RSHIFT
|
||||
ret
|
||||
|
||||
; A list of the values associated with the 0x80 possible scan codes of the set
|
||||
; 2 of the PS/2 keyboard specs. 0 means no value. That value is a character that
|
||||
; can be read in a GetC routine. No make code in the PS/2 set 2 reaches 0x80.
|
||||
kbdScanCodes:
|
||||
; 0x00 1 2 3 4 5 6 7 8 9 a b c d e f
|
||||
.db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9,'`', 0
|
||||
; 0x10 9 = TAB
|
||||
.db 0, 0, 0, 0, 0,'q','1', 0, 0, 0,'z','s','a','w','2', 0
|
||||
; 0x20 32 = SPACE
|
||||
.db 0,'c','x','d','e','4','3', 0, 0, 32,'v','f','t','r','5', 0
|
||||
; 0x30
|
||||
.db 0,'n','b','h','g','y','6', 0, 0, 0,'m','j','u','7','8', 0
|
||||
; 0x40 59 = ;
|
||||
.db 0,',','k','i','o','0','9', 0, 0,'.','/','l', 59,'p','-', 0
|
||||
; 0x50 13 = RETURN 39 = '
|
||||
.db 0, 0, 39, 0,'[','=', 0, 0, 0, 0, 13,']', 0,'\', 0, 0
|
||||
; 0x60 8 = BKSP
|
||||
.db 0, 0, 0, 0, 0, 0, 8, 0, 0,'1', 0,'4','7', 0, 0, 0
|
||||
; 0x70 27 = ESC
|
||||
.db '0','.','2','5','6','8', 27, 0, 0, 0,'3', 0, 0,'9', 0, 0
|
||||
|
||||
; Same values, but shifted, exactly 0x80 bytes after kbdScanCodes
|
||||
; 0x00 1 2 3 4 5 6 7 8 9 a b c d e f
|
||||
.db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9,'~', 0
|
||||
; 0x10 9 = TAB
|
||||
.db 0, 0, 0, 0, 0,'Q','!', 0, 0, 0,'Z','S','A','W','@', 0
|
||||
; 0x20 32 = SPACE
|
||||
.db 0,'C','X','D','E','$','#', 0, 0, 32,'V','F','T','R','%', 0
|
||||
; 0x30
|
||||
.db 0,'N','B','H','G','Y','^', 0, 0, 0,'M','J','U','&','*', 0
|
||||
; 0x40 59 = ;
|
||||
.db 0,'<','K','I','O',')','(', 0, 0,'>','?','L',':','P','_', 0
|
||||
; 0x50 13 = RETURN
|
||||
.db 0, 0,'"', 0,'{','+', 0, 0, 0, 0, 13,'}', 0,'|', 0, 0
|
||||
; 0x60 8 = BKSP
|
||||
.db 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
; 0x70 27 = ESC
|
||||
.db 0, 0, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
@ -1,48 +0,0 @@
|
||||
; mmap
|
||||
;
|
||||
; Block device that maps to memory.
|
||||
;
|
||||
; *** DEFINES ***
|
||||
; MMAP_START: Memory address where the mmap begins
|
||||
; Memory address where the mmap stops, exclusively (we aren't allowed to access
|
||||
; that address).
|
||||
.equ MMAP_LEN 0xffff-MMAP_START
|
||||
|
||||
; Returns absolute addr of memory pointer in HL if HL is within bounds.
|
||||
; Sets Z on success, unset when out of bounds.
|
||||
_mmapAddr:
|
||||
push de
|
||||
ld de, MMAP_LEN
|
||||
or a ; reset carry flag
|
||||
sbc hl, de
|
||||
jr nc, .outOfBounds ; HL >= DE
|
||||
add hl, de ; old HL value
|
||||
ld de, MMAP_START
|
||||
add hl, de
|
||||
cp a ; ensure Z
|
||||
pop de
|
||||
ret
|
||||
.outOfBounds:
|
||||
pop de
|
||||
jp unsetZ
|
||||
|
||||
mmapGetB:
|
||||
push hl
|
||||
call _mmapAddr
|
||||
jr nz, .end
|
||||
ld a, (hl)
|
||||
; Z already set
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
||||
|
||||
|
||||
mmapPutB:
|
||||
push hl
|
||||
call _mmapAddr
|
||||
jr nz, .end
|
||||
ld (hl), a
|
||||
; Z already set
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
759
kernel/sdc.asm
759
kernel/sdc.asm
@ -1,759 +0,0 @@
|
||||
; sdc
|
||||
;
|
||||
; Manages the initialization of a SD card and implement a block device to read
|
||||
; and write from/to it, in SPI mode.
|
||||
;
|
||||
; Note that SPI can't really be used directly from the z80, so this part
|
||||
; assumes that you have a device that handles SPI communication on behalf of
|
||||
; the z80. This device is assumed to work in a particular way. See the
|
||||
; "rc2014/sdcard" recipe for details.
|
||||
;
|
||||
; That device has 3 ports. One write-only port to make CS high, one to make CS
|
||||
; low (data sent is irrelevant), and one read/write port to send and receive
|
||||
; bytes with the card through the SPI protocol. The device acts as a SPI master
|
||||
; and writing to that port initiates a byte exchange. Data from the slave is
|
||||
; then placed on a buffer that can be read by reading the same port.
|
||||
;
|
||||
; It's through that kind of device that this code below is supposed to work.
|
||||
;
|
||||
; *** SDC buffers ***
|
||||
;
|
||||
; SD card's lowest common denominator in terms of block size is 512 bytes, so
|
||||
; that's what we deal with. To avoid wastefully reading entire blocks from the
|
||||
; card for one byte read ops, we buffer the last read block. If a GetB or PutB
|
||||
; operation is within that buffer, then no interaction with the SD card is
|
||||
; necessary.
|
||||
;
|
||||
; As soon as a GetB or PutB operation is made that is outside the current
|
||||
; buffer, we load a new block.
|
||||
;
|
||||
; When we PutB, we flag the buffer as "dirty". On the next buffer change (during
|
||||
; an out-of-buffer request or during an explicit "flush" operation), bytes
|
||||
; currently in the buffer will be written to the SD card.
|
||||
;
|
||||
; We hold 2 buffers in memory, each targeting a different sector and with its
|
||||
; own dirty flag. We do that to avoid wasteful block writing in the case where
|
||||
; we read data from a file in the SD card, process it and write the result
|
||||
; right away, in another file on the same card (zasm), on a different sector.
|
||||
;
|
||||
; If we only have one buffer in this scenario, we'll end up loading a new sector
|
||||
; at each GetB/PutB operation and, more importantly, writing a whole block for
|
||||
; a few bytes each time. This will wear the card prematurely (and be very slow).
|
||||
;
|
||||
; With 2 buffers, we solve the problem. Whenever GetB/PutB is called, we first
|
||||
; look if one of the buffer holds our sector. If not, we see if one of the
|
||||
; buffer is clean (not dirty). If yes, we use this one. If both are dirty or
|
||||
; clean, we use any. This way, as long as writing isn't made to random
|
||||
; addresses, we ensure that we don't write wastefully because read operations,
|
||||
; even if random, will always use the one buffer that isn't dirty.
|
||||
|
||||
; *** Defines ***
|
||||
; SDC_PORT_CSHIGH: Port number to make CS high
|
||||
; SDC_PORT_CSLOW: Port number to make CS low
|
||||
; SDC_PORT_SPI: Port number to send/receive SPI data
|
||||
|
||||
; *** Consts ***
|
||||
.equ SDC_BLKSIZE 512
|
||||
.equ SDC_MAXTRIES 8
|
||||
|
||||
; *** Variables ***
|
||||
; This is a pointer to the currently selected buffer. This points to the BUFSEC
|
||||
; part, that is, two bytes before actual content begins.
|
||||
.equ SDC_BUFPTR SDC_RAMSTART
|
||||
; Count the number of times we tried a particular read or write operation. When
|
||||
; CRC check fail, we retry. After SDC_MAXTRIES failures, we stop.
|
||||
.equ SDC_RETRYCNT SDC_BUFPTR+2
|
||||
; Sector number currently in SDC_BUF1. Little endian like any other z80 word.
|
||||
.equ SDC_BUFSEC1 SDC_RETRYCNT+1
|
||||
; Whether the buffer has been written to. 0 means clean. 1 means dirty.
|
||||
.equ SDC_BUFDIRTY1 SDC_BUFSEC1+2
|
||||
; The contents of the buffer.
|
||||
.equ SDC_BUF1 SDC_BUFDIRTY1+1
|
||||
; CRC bytes for the buffer. They're placed after the contents because that makes
|
||||
; things easier processing-wise. Because the SD card sends them right after the
|
||||
; contents, all we need to do is read SDC_BLKSIZE+2.
|
||||
; IMPORTANT NOTE: This is big endian. The SD card sends the MSB first, so we
|
||||
; keep it in memory this way.
|
||||
.equ SDC_CRC1 SDC_BUF1+SDC_BLKSIZE
|
||||
|
||||
; second buffer has the same structure as the first.
|
||||
.equ SDC_BUFSEC2 SDC_CRC1+2
|
||||
.equ SDC_BUFDIRTY2 SDC_BUFSEC2+2
|
||||
.equ SDC_BUF2 SDC_BUFDIRTY2+1
|
||||
.equ SDC_CRC2 SDC_BUF2+SDC_BLKSIZE
|
||||
.equ SDC_RAMEND SDC_CRC2+2
|
||||
|
||||
; *** Code ***
|
||||
; Wake the SD card up. After power up, a SD card has to receive at least 74
|
||||
; dummy clocks with CS and DI high. We send 80.
|
||||
sdcWakeUp:
|
||||
out (SDC_PORT_CSHIGH), a
|
||||
ld b, 10 ; 10 * 8 == 80
|
||||
ld a, 0xff
|
||||
.loop:
|
||||
out (SDC_PORT_SPI), a
|
||||
nop
|
||||
djnz .loop
|
||||
ret
|
||||
|
||||
; Initiate SPI exchange with the SD card. A is the data to send. Received data
|
||||
; is placed in A.
|
||||
sdcSendRecv:
|
||||
out (SDC_PORT_SPI), a
|
||||
nop
|
||||
nop
|
||||
in a, (SDC_PORT_SPI)
|
||||
nop
|
||||
nop
|
||||
ret
|
||||
|
||||
sdcIdle:
|
||||
ld a, 0xff
|
||||
jp sdcSendRecv
|
||||
|
||||
; sdcSendRecv 0xff until the response is something else than 0xff for a maximum
|
||||
; of 20 times. Returns 0xff if no response.
|
||||
sdcWaitResp:
|
||||
push bc
|
||||
ld b, 20
|
||||
.loop:
|
||||
call sdcIdle
|
||||
inc a ; if 0xff, it's going to become zero
|
||||
jr nz, .end ; not zero? good, that's our command
|
||||
djnz .loop
|
||||
.end:
|
||||
; whether we had a success or failure, we return the result.
|
||||
; But first, let's bring it back to its original value.
|
||||
dec a
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; The opposite of sdcWaitResp: we wait until response if 0xff. After a
|
||||
; successful read or write operation, the card will be busy for a while. We need
|
||||
; to give it time before interacting with it again. Technically, we could
|
||||
; continue processing on our side while the card it busy, and maybe we will one
|
||||
; day, but at the moment, I'm having random write errors if I don't do this
|
||||
; right after a write, so I prefer to stay cautious for now.
|
||||
; This has no error condition and preserves A
|
||||
sdcWaitReady:
|
||||
push af
|
||||
; for now, we have no timeout for waiting. It means that broken SD
|
||||
; cards can cause infinite loops.
|
||||
.loop:
|
||||
call sdcIdle
|
||||
inc a ; if 0xff, it's going to become zero
|
||||
jr nz, .loop ; not zero? still busy. loop
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Sends a command to the SD card, along with arguments and specified CRC fields.
|
||||
; (CRC is only needed in initial commands though).
|
||||
; A: Command to send
|
||||
; H: Arg 1 (MSB)
|
||||
; L: Arg 2
|
||||
; D: Arg 3
|
||||
; E: Arg 4 (LSB)
|
||||
;
|
||||
; Returns R1 response in A.
|
||||
;
|
||||
; This does *not* handle CS. You have to select/deselect the card outside this
|
||||
; routine.
|
||||
sdcCmd:
|
||||
push bc
|
||||
|
||||
; Wait until ready to receive commands
|
||||
push af
|
||||
call sdcWaitResp
|
||||
pop af
|
||||
|
||||
ld c, 0 ; init CRC
|
||||
call .crc7
|
||||
call sdcSendRecv
|
||||
; Arguments
|
||||
ld a, h
|
||||
call .crc7
|
||||
call sdcSendRecv
|
||||
ld a, l
|
||||
call .crc7
|
||||
call sdcSendRecv
|
||||
ld a, d
|
||||
call .crc7
|
||||
call sdcSendRecv
|
||||
ld a, e
|
||||
call .crc7
|
||||
call sdcSendRecv
|
||||
; send CRC
|
||||
ld a, c
|
||||
or 0x01 ; ensure stop bit is set
|
||||
call sdcSendRecv
|
||||
|
||||
; And now we just have to wait for a valid response...
|
||||
call sdcWaitResp
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; push A into C and compute CRC7 with 0x09 polynomial
|
||||
; Note that the result is "left aligned", that is, that 8th bit to the "right"
|
||||
; is insignificant (will be stop bit).
|
||||
.crc7:
|
||||
push af
|
||||
xor c
|
||||
ld b, 8
|
||||
.loop:
|
||||
sla a
|
||||
jr nc, .noCRC
|
||||
; msb was set, apply polynomial
|
||||
xor 0x12 ; 0x09 << 1. We apply CRC on high 7 bits
|
||||
.noCRC:
|
||||
djnz .loop
|
||||
ld c, a
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Send a command that expects a R1 response, handling CS.
|
||||
sdcCmdR1:
|
||||
out (SDC_PORT_CSLOW), a
|
||||
call sdcCmd
|
||||
out (SDC_PORT_CSHIGH), a
|
||||
ret
|
||||
|
||||
; Send a command that expects a R7 response, handling CS. A R7 is a R1 followed
|
||||
; by 4 bytes. Those 4 bytes are returned in HL/DE in the same order as in
|
||||
; sdcCmd.
|
||||
sdcCmdR7:
|
||||
out (SDC_PORT_CSLOW), a
|
||||
call sdcCmd
|
||||
|
||||
; We have our R1 response in A. Let's try reading the next 4 bytes in
|
||||
; case we have a R3.
|
||||
push af
|
||||
call sdcIdle
|
||||
ld h, a
|
||||
call sdcIdle
|
||||
ld l, a
|
||||
call sdcIdle
|
||||
ld d, a
|
||||
call sdcIdle
|
||||
ld e, a
|
||||
pop af
|
||||
|
||||
out (SDC_PORT_CSHIGH), a
|
||||
ret
|
||||
|
||||
; Initialize a SD card. This should be called at least 1ms after the powering
|
||||
; up of the card. Sets result code in A. Zero means success, non-zero means
|
||||
; error.
|
||||
sdcInitialize:
|
||||
push hl
|
||||
push de
|
||||
push bc
|
||||
call sdcWakeUp
|
||||
|
||||
; Call CMD0 and expect a 0x01 response (card idle)
|
||||
; This should be called multiple times. We're actually expected to.
|
||||
; Let's call this for a maximum of 10 times.
|
||||
ld b, 10
|
||||
.loop1:
|
||||
ld a, 0b01000000 ; CMD0
|
||||
ld hl, 0
|
||||
ld de, 0
|
||||
call sdcCmdR1
|
||||
cp 0x01
|
||||
jp z, .cmd0ok
|
||||
djnz .loop1
|
||||
; Nothing? error
|
||||
jr .error
|
||||
.cmd0ok:
|
||||
|
||||
; Then comes the CMD8. We send it with a 0x01aa argument and expect
|
||||
; a 0x01aa argument back, along with a 0x01 R1 response.
|
||||
ld a, 0b01001000 ; CMD8
|
||||
ld hl, 0
|
||||
ld de, 0x01aa
|
||||
call sdcCmdR7
|
||||
cp 0x01
|
||||
jr nz, .error
|
||||
xor a
|
||||
cp h ; H is zero
|
||||
jr nz, .error
|
||||
cp l ; L is zero
|
||||
jr nz, .error
|
||||
ld a, d
|
||||
cp 0x01
|
||||
jp nz, .error
|
||||
ld a, e
|
||||
cp 0xaa
|
||||
jr nz, .error
|
||||
|
||||
; Now we need to repeatedly run CMD55+CMD41 (0x40000000) until we
|
||||
; the card goes out of idle mode, that is, when it stops sending us
|
||||
; 0x01 response and send us 0x00 instead. Any other response means that
|
||||
; initialization failed.
|
||||
.loop2:
|
||||
ld a, 0b01110111 ; CMD55
|
||||
ld hl, 0
|
||||
ld de, 0
|
||||
call sdcCmdR1
|
||||
cp 0x01
|
||||
jr nz, .error
|
||||
ld a, 0b01101001 ; CMD41 (0x40000000)
|
||||
ld hl, 0x4000
|
||||
ld de, 0x0000
|
||||
call sdcCmdR1
|
||||
cp 0x01
|
||||
jr z, .loop2
|
||||
or a ; cp 0
|
||||
jr nz, .error
|
||||
; Success! out of idle mode!
|
||||
jr .end
|
||||
|
||||
.error:
|
||||
ld a, 0x01
|
||||
.end:
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Read block index specified in DE and place the contents in buffer pointed to
|
||||
; by (SDC_BUFPTR).
|
||||
; If the operation is a success, updates buffer's sector to the value of DE.
|
||||
; After a block read, check that CRC given by the card matches the content. If
|
||||
; it doesn't, retries up to SDC_MAXTRIES times.
|
||||
; Returns 0 in A if success, non-zero if error.
|
||||
sdcReadBlk:
|
||||
xor a
|
||||
ld (SDC_RETRYCNT), a
|
||||
|
||||
push bc
|
||||
push hl
|
||||
|
||||
out (SDC_PORT_CSLOW), a
|
||||
.retry:
|
||||
ld hl, 0
|
||||
; DE already has the correct value
|
||||
ld a, 0b01010001 ; CMD17
|
||||
call sdcCmd
|
||||
or a ; cp 0
|
||||
jr nz, .error
|
||||
|
||||
; Command sent, no error, now let's wait for our data response.
|
||||
ld b, 20
|
||||
.loop1:
|
||||
call sdcWaitResp
|
||||
; 0xfe is the expected data token for CMD17
|
||||
cp 0xfe
|
||||
jr z, .loop1end
|
||||
cp 0xff
|
||||
jr nz, .error
|
||||
djnz .loop1
|
||||
jr .error ; timeout. error out
|
||||
.loop1end:
|
||||
; We received our data token!
|
||||
; Data packets follow immediately, we have 512+CRC of them to read
|
||||
ld bc, SDC_BLKSIZE+2
|
||||
ld hl, (SDC_BUFPTR) ; HL --> active buffer's sector
|
||||
; It sounds a bit wrong to set bufsec and dirty flag before we get our
|
||||
; actual data, but at this point, we don't have any error conditions
|
||||
; left, success is guaranteed. To avoid needlesssly INCing hl, let's
|
||||
; set sector and dirty along the way
|
||||
ld (hl), e ; sector number LSB
|
||||
inc hl
|
||||
ld (hl), d ; sector number MSB
|
||||
inc hl ; dirty flag
|
||||
xor a ; unset
|
||||
ld (hl), a
|
||||
inc hl ; actual contents
|
||||
.loop2:
|
||||
call sdcIdle
|
||||
ld (hl), a
|
||||
cpi ; a trick to inc HL and dec BC at the same time.
|
||||
; P/V indicates whether BC reached 0
|
||||
jp pe, .loop2 ; BC is not zero, loop
|
||||
; Success! while the card is busy, let's get busy too: let's check and
|
||||
; see if the CRC matches.
|
||||
push de ; <|
|
||||
call sdcCRC ; |
|
||||
; before we check the CRC r|esults, let's wait until card is ready
|
||||
call sdcWaitReady ; |
|
||||
; check CRC results |
|
||||
ld a, (hl) ; |
|
||||
cp d ; |
|
||||
jr nz, .crcMismatch ; |
|
||||
inc hl ; |
|
||||
ld a, (hl) ; |
|
||||
cp e ; |
|
||||
jr nz, .crcMismatch ; |
|
||||
pop de ; <|
|
||||
; Everything is fine and dandy!
|
||||
xor a ; success
|
||||
jr .end
|
||||
.crcMismatch:
|
||||
; CRC of the buffer's content doesn't match the CRC reported by the
|
||||
; card. Let's retry.
|
||||
pop de ; from the push right before call sdcCRC
|
||||
ld a, (SDC_RETRYCNT)
|
||||
inc a
|
||||
ld (SDC_RETRYCNT), a
|
||||
cp SDC_MAXTRIES
|
||||
jr nz, .retry ; we jump inside our main stack push. No need
|
||||
; to pop it.
|
||||
; Continue to error condition. Even if we went through many retries,
|
||||
; our stack is still the same as it was at the first call. We can return
|
||||
; normally (but in error condition).
|
||||
.error:
|
||||
; try to preserve error code
|
||||
or a ; cp 0
|
||||
jr nz, .end ; already non-zero
|
||||
inc a ; zero, adjust
|
||||
.end:
|
||||
out (SDC_PORT_CSHIGH), a
|
||||
pop hl
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Write the contents of buffer where (SDC_BUFPTR) points to in sector associated
|
||||
; to it. Unsets the the buffer's dirty flag on success.
|
||||
; Before writing the block, update the buffer's CRC field so that the correct
|
||||
; CRC is sent.
|
||||
; A returns 0 in A on success (with Z set), non-zero (with Z unset) on error.
|
||||
sdcWriteBlk:
|
||||
push ix
|
||||
ld ix, (SDC_BUFPTR) ; IX points to sector LSB
|
||||
xor a
|
||||
cp (ix+2) ; dirty flag
|
||||
pop ix
|
||||
ret z ; don't write if dirty flag is zero
|
||||
|
||||
push bc
|
||||
push de
|
||||
push hl
|
||||
|
||||
call sdcCRC ; DE -> new CRC. HL -> pointer to buf CRC
|
||||
ld (hl), d ; write computed CRC
|
||||
inc hl
|
||||
ld (hl), e
|
||||
|
||||
out (SDC_PORT_CSLOW), a
|
||||
ld hl, (SDC_BUFPTR) ; sector LSB
|
||||
ld e, (hl) ; sector LSB
|
||||
inc hl
|
||||
ld d, (hl) ; sector MSB
|
||||
ld hl, 0 ; high addr word always zero, DE already set
|
||||
ld a, 0b01011000 ; CMD24
|
||||
call sdcCmd
|
||||
or a ; cp 0
|
||||
jr nz, .error
|
||||
|
||||
; Before sending the data packet, we need to send at least one empty
|
||||
; byte.
|
||||
call sdcIdle
|
||||
|
||||
; data packet token for CMD24
|
||||
ld a, 0xfe
|
||||
call sdcSendRecv
|
||||
|
||||
; Sending our data token!
|
||||
ld bc, SDC_BLKSIZE+2 ; +2 for CRC. (as of now, however, that
|
||||
; CRC isn't properly updated. Because
|
||||
; CMD59 isn't enabled, it doesn't
|
||||
; matter)
|
||||
ld hl, (SDC_BUFPTR)
|
||||
inc hl ; sector MSB
|
||||
inc hl ; dirty flag
|
||||
inc hl ; beginning of contents
|
||||
|
||||
.loop:
|
||||
ld a, (hl)
|
||||
call sdcSendRecv
|
||||
cpi ; a trick to inc HL and dec BC at the same time.
|
||||
; P/V indicates whether BC reached 0
|
||||
jp pe, .loop ; BC is not zero, loop
|
||||
; Let's see what response we have
|
||||
call sdcWaitResp
|
||||
and 0b00011111 ; We ignore the first 3 bits of the response.
|
||||
cp 0b00000101 ; A valid response is "010" in bits 3:1 flanked
|
||||
; by 0 on its left and 1 on its right.
|
||||
jr nz, .error
|
||||
; good! Now, we need to let the card process this data. It will return
|
||||
; 0xff when it's not busy any more.
|
||||
call sdcWaitResp
|
||||
; Success! Now let's unset the dirty flag
|
||||
ld hl, (SDC_BUFPTR)
|
||||
inc hl ; sector MSB
|
||||
inc hl ; dirty flag
|
||||
xor a
|
||||
ld (hl), a
|
||||
|
||||
; Before returning, wait until card is ready
|
||||
call sdcWaitReady
|
||||
xor a
|
||||
jr .end
|
||||
.error:
|
||||
; try to preserve error code
|
||||
or a ; cp 0
|
||||
jr nz, .end ; already non-zero
|
||||
inc a ; zero, adjust
|
||||
.end:
|
||||
out (SDC_PORT_CSHIGH), a
|
||||
pop hl
|
||||
pop de
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Considering the first 15 bits of EHL, select the most appropriate of our two
|
||||
; buffers and, if necessary, sync that buffer with the SD card. If the selected
|
||||
; buffer doesn't have the same sector as what EHL asks, load that buffer from
|
||||
; the SD card.
|
||||
; If the dirty flag is set, we write the content of the in-memory buffer to the
|
||||
; SD card before we read a new sector.
|
||||
; Returns Z on success, not-Z on error (with the error code from either
|
||||
; sdcReadBlk or sdcWriteBlk)
|
||||
sdcSync:
|
||||
push de
|
||||
; Given a 24-bit address in EHL, extracts the 15-bit sector from it and
|
||||
; place it in DE.
|
||||
; We need to shift both E and H right by one bit
|
||||
srl e ; sets Carry
|
||||
ld d, e
|
||||
ld a, h
|
||||
rra ; takes Carry
|
||||
ld e, a
|
||||
|
||||
; Let's first see if our first buffer has our sector
|
||||
ld a, (SDC_BUFSEC1) ; sector LSB
|
||||
cp e
|
||||
jr nz, .notBuf1
|
||||
ld a, (SDC_BUFSEC1+1) ; sector MSB
|
||||
cp d
|
||||
jr z, .buf1Ok
|
||||
|
||||
.notBuf1:
|
||||
; Ok, let's check for buf2 then
|
||||
ld a, (SDC_BUFSEC2) ; sector LSB
|
||||
cp e
|
||||
jr nz, .notBuf2
|
||||
ld a, (SDC_BUFSEC2+1) ; sector MSB
|
||||
cp d
|
||||
jr z, .buf2Ok
|
||||
|
||||
.notBuf2:
|
||||
; None of our two buffers have the sector we need, we'll need to load
|
||||
; a new one.
|
||||
|
||||
; We select our buffer depending on which is dirty. If both are on the
|
||||
; same status of dirtiness, we pick any (the first in our case). If one
|
||||
; of them is dirty, we pick the clean one.
|
||||
push de ; <|
|
||||
ld de, SDC_BUFSEC1 ; |
|
||||
ld a, (SDC_BUFDIRTY1) ; |
|
||||
or a ; | is buf1 dirty?
|
||||
jr z, .ready ; | no? good, that's our buffer
|
||||
; yes? then buf2 is our buffer. ; |
|
||||
ld de, SDC_BUFSEC2 ; |
|
||||
; |
|
||||
.ready: ; |
|
||||
; At this point, DE points to one o|f our two buffers, the good one.
|
||||
; Let's save it to SDC_BUFPTR |
|
||||
ld (SDC_BUFPTR), de ; |
|
||||
; |
|
||||
pop de ; <|
|
||||
|
||||
; We have to read a new sector, but first, let's write the current one
|
||||
; if needed.
|
||||
call sdcWriteBlk
|
||||
jr nz, .end ; error
|
||||
; Let's read our new sector in DE
|
||||
call sdcReadBlk
|
||||
jr .end
|
||||
|
||||
.buf1Ok:
|
||||
ld de, SDC_BUFSEC1
|
||||
ld (SDC_BUFPTR), de
|
||||
; Z already set
|
||||
jr .end
|
||||
|
||||
.buf2Ok:
|
||||
ld de, SDC_BUFSEC2
|
||||
ld (SDC_BUFPTR), de
|
||||
; Z already set
|
||||
; to .end
|
||||
.end:
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Computes the CRC-16, with polynomial 0x1021 of buffer at (SDC_BUFPTR) and
|
||||
; returns its value in DE. Also, make HL point to the first byte of the CRC
|
||||
; associated to (SDC_BUFPTR).
|
||||
sdcCRC:
|
||||
push af
|
||||
push bc
|
||||
ld hl, (SDC_BUFPTR)
|
||||
inc hl \ inc hl \ inc hl ; HL points to contents
|
||||
ld bc, SDC_BLKSIZE
|
||||
ld de, 0
|
||||
.loop:
|
||||
push bc ; <|
|
||||
ld b, 8 ; |
|
||||
ld a, (hl) ; |
|
||||
xor d ; |
|
||||
ld d, a ; |
|
||||
.inner: ; |
|
||||
sla e ; | Sets Carry
|
||||
rl d ; | Takes and sets carry
|
||||
jr nc, .noCRC ; |
|
||||
; msb was set, apply polyno|mial
|
||||
ld a, d ; |
|
||||
xor 0x10 ; |
|
||||
ld d, a ; |
|
||||
ld a, e ; |
|
||||
xor 0x21 ; |
|
||||
ld e, a ; |
|
||||
.noCRC: ; |
|
||||
djnz .inner ; |
|
||||
pop bc ; <|
|
||||
|
||||
cpi ; inc HL, dec BC, sets P/V on BC=0
|
||||
jp pe, .loop ; BC is not zero, loop
|
||||
; At this point, HL points to the right place: the first byte of the
|
||||
; recorded CRC.
|
||||
pop bc
|
||||
pop af
|
||||
ret
|
||||
|
||||
sdcInitializeCmd:
|
||||
call sdcInitialize
|
||||
ret nz
|
||||
call .setBlkSize
|
||||
ret nz
|
||||
call .enableCRC
|
||||
ret nz
|
||||
; At this point, our buffers are unnitialized. We could have some logic
|
||||
; that determines whether a buffer is initialized in appropriate SDC
|
||||
; routines and act appropriately, but why bother when we could, instead,
|
||||
; just buffer the first two sectors of the card on initialization? This
|
||||
; way, no need for special conditions.
|
||||
; initialize variables
|
||||
ld hl, SDC_BUFSEC1
|
||||
ld (SDC_BUFPTR), hl
|
||||
ld de, 0
|
||||
call sdcReadBlk ; read sector 0 in buf1
|
||||
ret nz
|
||||
ld hl, SDC_BUFSEC2
|
||||
ld (SDC_BUFPTR), hl
|
||||
inc de
|
||||
jp sdcReadBlk ; read sector 1 in buf2, returns
|
||||
|
||||
; Send a command to set block size to SDC_BLKSIZE to the SD card.
|
||||
; Returns zero in A if a success, non-zero otherwise
|
||||
.setBlkSize:
|
||||
push hl
|
||||
push de
|
||||
|
||||
ld a, 0b01010000 ; CMD16
|
||||
ld hl, 0
|
||||
ld de, SDC_BLKSIZE
|
||||
call sdcCmdR1
|
||||
; Since we're out of idle mode, we expect a 0 response
|
||||
; We need no further processing: A is already the correct value.
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Enable CRC checks through CMD59
|
||||
.enableCRC:
|
||||
push hl
|
||||
push de
|
||||
|
||||
ld a, 0b01111011 ; CMD59
|
||||
ld hl, 0
|
||||
ld de, 1 ; 1 means CRC enabled
|
||||
call sdcCmdR1
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Flush the current SDC buffer if dirty
|
||||
sdcFlushCmd:
|
||||
ld hl, SDC_BUFSEC1
|
||||
ld (SDC_BUFPTR), hl
|
||||
call sdcWriteBlk
|
||||
ret nz
|
||||
ld hl, SDC_BUFSEC2
|
||||
ld (SDC_BUFPTR), hl
|
||||
jp sdcWriteBlk ; returns
|
||||
|
||||
; *** blkdev routines ***
|
||||
|
||||
; Make HL point to its proper place in SDC_BUF.
|
||||
; EHL currently is a 24-bit offset to read in the SD card. E=high byte,
|
||||
; HL=low word. Load the proper sector in memory and make HL point to the
|
||||
; correct data in the memory buffer.
|
||||
_sdcPlaceBuf:
|
||||
call sdcSync
|
||||
ret nz ; error
|
||||
; At this point, we have the proper buffer in place and synced in
|
||||
; (SDC_BUFPTR). Only the 9 low bits of HL are important.
|
||||
push de
|
||||
ld de, (SDC_BUFPTR)
|
||||
inc de ; sector MSB
|
||||
inc de ; dirty flag
|
||||
inc de ; contents
|
||||
ld a, h ; high byte
|
||||
and 0x01 ; is first bit set?
|
||||
jr z, .read ; first bit reset? we're in the "lowbuf" zone.
|
||||
; DE already points to the right place.
|
||||
; We're in the highbuf zone, let's inc DE by 0x100, which, as it turns
|
||||
; out, is quite easy.
|
||||
inc d
|
||||
.read:
|
||||
; DE is now placed either on the lower or higher half of the active
|
||||
; buffer and all we need is to increase DE the lower half of HL.
|
||||
ld a, l
|
||||
call addDE
|
||||
ex de, hl
|
||||
pop de
|
||||
; Now, HL points exactly at the right byte in the active buffer.
|
||||
xor a ; ensure Z
|
||||
ret
|
||||
|
||||
sdcGetB:
|
||||
push hl
|
||||
call _sdcPlaceBuf
|
||||
jr nz, .end ; NZ already set
|
||||
|
||||
; This is it!
|
||||
ld a, (hl)
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
||||
|
||||
sdcPutB:
|
||||
push hl
|
||||
push af ; let's remember the char we put, _sdcPlaceBuf
|
||||
; destroys A.
|
||||
call _sdcPlaceBuf
|
||||
jr nz, .error
|
||||
|
||||
; HL points to our dest. Recall A and write
|
||||
pop af
|
||||
ld (hl), a
|
||||
|
||||
; Now, let's set the dirty flag
|
||||
ld a, 1
|
||||
ld hl, (SDC_BUFPTR)
|
||||
inc hl ; sector MSB
|
||||
inc hl ; point to dirty flag
|
||||
ld (hl), a ; set dirty flag
|
||||
xor a ; ensure Z
|
||||
jr .end
|
||||
.error:
|
||||
; preserve error code
|
||||
ex af, af'
|
||||
pop af
|
||||
ex af, af'
|
||||
call unsetZ
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
@ -1,102 +0,0 @@
|
||||
; kbd - implement FetchKC for SMS PS/2 adapter
|
||||
;
|
||||
; Implements KBD_FETCHKC for the adapter described in recipe sms/kbd. It does
|
||||
; so for both Port A and Port B (you hook whichever you prefer).
|
||||
|
||||
; FetchKC on Port A
|
||||
smskbdFetchKCA:
|
||||
; Before reading a character, we must first verify that there is
|
||||
; something to read. When the adapter is finished filling its '164 up,
|
||||
; it resets the latch, which output's is connected to TL. When the '164
|
||||
; is full, TL is low.
|
||||
; Port A TL is bit 4
|
||||
in a, (0xdc)
|
||||
and 0b00010000
|
||||
jr nz, .nothing
|
||||
|
||||
push bc
|
||||
in a, (0x3f)
|
||||
; Port A TH output, low
|
||||
ld a, 0b11011101
|
||||
out (0x3f), a
|
||||
nop
|
||||
nop
|
||||
in a, (0xdc)
|
||||
; bit 3:0 are our dest bits 3:0. handy...
|
||||
and 0b00001111
|
||||
ld b, a
|
||||
; Port A TH output, high
|
||||
ld a, 0b11111101
|
||||
out (0x3f), a
|
||||
nop
|
||||
nop
|
||||
in a, (0xdc)
|
||||
; bit 3:0 are our dest bits 7:4
|
||||
rlca \ rlca \ rlca \ rlca
|
||||
and 0b11110000
|
||||
or b
|
||||
ex af, af'
|
||||
; Port A/B reset
|
||||
ld a, 0xff
|
||||
out (0x3f), a
|
||||
ex af, af'
|
||||
pop bc
|
||||
ret
|
||||
|
||||
.nothing:
|
||||
xor a
|
||||
ret
|
||||
|
||||
; FetchKC on Port B
|
||||
smskbdFetchKCB:
|
||||
; Port B TL is bit 2
|
||||
in a, (0xdd)
|
||||
and 0b00000100
|
||||
jr nz, .nothing
|
||||
|
||||
push bc
|
||||
in a, (0x3f)
|
||||
; Port B TH output, low
|
||||
ld a, 0b01110111
|
||||
out (0x3f), a
|
||||
nop
|
||||
nop
|
||||
in a, (0xdc)
|
||||
; bit 7:6 are our dest bits 1:0
|
||||
rlca \ rlca
|
||||
and 0b00000011
|
||||
ld b, a
|
||||
in a, (0xdd)
|
||||
; bit 1:0 are our dest bits 3:2
|
||||
rlca \ rlca
|
||||
and 0b00001100
|
||||
or b
|
||||
ld b, a
|
||||
; Port B TH output, high
|
||||
ld a, 0b11110111
|
||||
out (0x3f), a
|
||||
nop
|
||||
nop
|
||||
in a, (0xdc)
|
||||
; bit 7:6 are our dest bits 5:4
|
||||
rrca \ rrca
|
||||
and 0b00110000
|
||||
or b
|
||||
ld b, a
|
||||
in a, (0xdd)
|
||||
; bit 1:0 are our dest bits 7:6
|
||||
rrca \ rrca
|
||||
and 0b11000000
|
||||
or b
|
||||
ex af, af'
|
||||
; Port A/B reset
|
||||
ld a, 0xff
|
||||
out (0x3f), a
|
||||
ex af, af'
|
||||
pop bc
|
||||
ret
|
||||
|
||||
.nothing:
|
||||
xor a
|
||||
ret
|
||||
|
@ -1,205 +0,0 @@
|
||||
; pad - read input from MD controller
|
||||
;
|
||||
; Conveniently expose an API to read the status of a MD pad A. Moreover,
|
||||
; implement a mechanism to input arbitrary characters from it. It goes as
|
||||
; follow:
|
||||
;
|
||||
; * Direction pad select characters. Up/Down move by one, Left/Right move by 5\
|
||||
; * Start acts like Return
|
||||
; * A acts like Backspace
|
||||
; * B changes "character class": lowercase, uppercase, numbers, special chars.
|
||||
; The space character is the first among special chars.
|
||||
; * C confirms letter selection
|
||||
;
|
||||
; This module is currently hard-wired to sms/vdp, that is, it calls vdp's
|
||||
; routines during padGetC to update character selection.
|
||||
;
|
||||
; *** Consts ***
|
||||
;
|
||||
.equ PAD_CTLPORT 0x3f
|
||||
.equ PAD_D1PORT 0xdc
|
||||
|
||||
.equ PAD_UP 0
|
||||
.equ PAD_DOWN 1
|
||||
.equ PAD_LEFT 2
|
||||
.equ PAD_RIGHT 3
|
||||
.equ PAD_BUTB 4
|
||||
.equ PAD_BUTC 5
|
||||
.equ PAD_BUTA 6
|
||||
.equ PAD_START 7
|
||||
|
||||
; *** Variables ***
|
||||
;
|
||||
; Button status of last padUpdateSel call. Used for debouncing.
|
||||
.equ PAD_SELSTAT PAD_RAMSTART
|
||||
; Current selected character
|
||||
.equ PAD_SELCHR @+1
|
||||
; When non-zero, will be the next char returned in GetC. So far, only used for
|
||||
; LF that is feeded when Start is pressed.
|
||||
.equ PAD_NEXTCHR @+1
|
||||
.equ PAD_RAMEND @+1
|
||||
|
||||
; *** Code ***
|
||||
|
||||
padInit:
|
||||
ld a, 0xff
|
||||
ld (PAD_SELSTAT), a
|
||||
xor a
|
||||
ld (PAD_NEXTCHR), a
|
||||
ld a, 'a'
|
||||
ld (PAD_SELCHR), a
|
||||
ret
|
||||
|
||||
; Put status for port A in register A. Bits, from MSB to LSB:
|
||||
; Start - A - C - B - Right - Left - Down - Up
|
||||
; Each bit is high when button is unpressed and low if button is pressed. For
|
||||
; example, when no button is pressed, 0xff is returned.
|
||||
padStatus:
|
||||
; This logic below is for the Genesis controller, which is modal. TH is
|
||||
; an output pin that swiches the meaning of TL and TR. When TH is high
|
||||
; (unselected), TL = Button B and TR = Button C. When TH is low
|
||||
; (selected), TL = Button A and TR = Start.
|
||||
push bc
|
||||
ld a, 0b11111101 ; TH output, unselected
|
||||
out (PAD_CTLPORT), a
|
||||
in a, (PAD_D1PORT)
|
||||
and 0x3f ; low 6 bits are good
|
||||
ld b, a ; let's store them
|
||||
; Start and A are returned when TH is selected, in bits 5 and 4. Well
|
||||
; get them, left-shift them and integrate them to B.
|
||||
ld a, 0b11011101 ; TH output, selected
|
||||
out (PAD_CTLPORT), a
|
||||
in a, (PAD_D1PORT)
|
||||
and 0b00110000
|
||||
sla a
|
||||
sla a
|
||||
or b
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; From a pad status in A, update current char selection and return it.
|
||||
; Sets Z if current selection was unchanged, unset if changed.
|
||||
padUpdateSel:
|
||||
call padStatus
|
||||
push hl ; --> lvl 1
|
||||
ld hl, PAD_SELSTAT
|
||||
cp (hl)
|
||||
ld (hl), a
|
||||
pop hl ; <-- lvl 1
|
||||
jr z, .nothing ; nothing changed
|
||||
bit PAD_UP, a
|
||||
jr z, .up
|
||||
bit PAD_DOWN, a
|
||||
jr z, .down
|
||||
bit PAD_LEFT, a
|
||||
jr z, .left
|
||||
bit PAD_RIGHT, a
|
||||
jr z, .right
|
||||
bit PAD_BUTB, a
|
||||
jr z, .nextclass
|
||||
jr .nothing
|
||||
.up:
|
||||
ld a, (PAD_SELCHR)
|
||||
inc a
|
||||
jr .setchr
|
||||
.down:
|
||||
ld a, (PAD_SELCHR)
|
||||
dec a
|
||||
jr .setchr
|
||||
.left:
|
||||
ld a, (PAD_SELCHR)
|
||||
dec a \ dec a \ dec a \ dec a \ dec a
|
||||
jr .setchr
|
||||
.right:
|
||||
ld a, (PAD_SELCHR)
|
||||
inc a \ inc a \ inc a \ inc a \ inc a
|
||||
jr .setchr
|
||||
.nextclass:
|
||||
; Go to the beginning of the next "class" of characters
|
||||
push bc
|
||||
ld a, (PAD_SELCHR)
|
||||
ld b, '0'
|
||||
cp b
|
||||
jr c, .setclass ; A < '0'
|
||||
ld b, ':'
|
||||
cp b
|
||||
jr c, .setclass
|
||||
ld b, 'A'
|
||||
cp b
|
||||
jr c, .setclass
|
||||
ld b, '['
|
||||
cp b
|
||||
jr c, .setclass
|
||||
ld b, 'a'
|
||||
cp b
|
||||
jr c, .setclass
|
||||
ld b, ' '
|
||||
; continue to .setclass
|
||||
.setclass:
|
||||
ld a, b
|
||||
pop bc
|
||||
; continue to .setchr
|
||||
.setchr:
|
||||
; check range first
|
||||
cp 0x7f
|
||||
jr nc, .tooHigh
|
||||
cp 0x20
|
||||
jr nc, .setchrEnd ; not too low
|
||||
; too low, probably because we overdecreased. Let's roll over
|
||||
ld a, '~'
|
||||
jr .setchrEnd
|
||||
.tooHigh:
|
||||
; too high, probably because we overincreased. Let's roll over
|
||||
ld a, ' '
|
||||
; continue to .setchrEnd
|
||||
.setchrEnd:
|
||||
ld (PAD_SELCHR), a
|
||||
jp unsetZ
|
||||
.nothing:
|
||||
; Z already set
|
||||
ld a, (PAD_SELCHR)
|
||||
ret
|
||||
|
||||
; Repeatedly poll the pad for input and returns the resulting "input char".
|
||||
; This routine takes a long time to return because it waits until C, B or Start
|
||||
; was pressed. Until this is done, this routine takes care of updating the
|
||||
; "current selection" directly in the VDP.
|
||||
padGetC:
|
||||
ld a, (PAD_NEXTCHR)
|
||||
or a
|
||||
jr nz, .nextchr
|
||||
call padUpdateSel
|
||||
jp z, padGetC ; nothing changed, loop
|
||||
; pad status was changed, let's see if an action button was pressed
|
||||
ld a, (PAD_SELSTAT)
|
||||
bit PAD_BUTC, a
|
||||
jr z, .advance
|
||||
bit PAD_BUTA, a
|
||||
jr z, .backspace
|
||||
bit PAD_START, a
|
||||
jr z, .return
|
||||
; no action button pressed, but because our pad status changed, update
|
||||
; VDP before looping.
|
||||
ld a, (PAD_SELCHR)
|
||||
call gridSetCurH
|
||||
jp padGetC
|
||||
.return:
|
||||
ld a, LF
|
||||
ld (PAD_NEXTCHR), a
|
||||
; continue to .advance
|
||||
.advance:
|
||||
ld a, (PAD_SELCHR)
|
||||
; Z was already set from previous BIT instruction
|
||||
jp gridSetCurL
|
||||
.backspace:
|
||||
ld a, BS
|
||||
; Z was already set from previous BIT instruction
|
||||
ret
|
||||
.nextchr:
|
||||
; We have a "next char", return it and clear it.
|
||||
cp a ; ensure Z
|
||||
ex af, af'
|
||||
xor a
|
||||
ld (PAD_NEXTCHR), a
|
||||
ex af, af'
|
||||
ret
|
@ -1,174 +0,0 @@
|
||||
; vdp - console on SMS' VDP
|
||||
;
|
||||
; Implement PutC on the console. Characters start at the top left. Every PutC
|
||||
; call converts the ASCII char received to its internal font, then put that
|
||||
; char on screen, advancing the cursor by one. When reaching the end of the
|
||||
; line (33rd char), wrap to the next.
|
||||
;
|
||||
; In the future, there's going to be a scrolling mechanism when we reach the
|
||||
; bottom of the screen, but for now, when the end of the screen is reached, we
|
||||
; wrap up to the top.
|
||||
;
|
||||
; When reaching a new line, we clear that line and the next to help readability.
|
||||
;
|
||||
; *** Defines ***
|
||||
; FNT_DATA: Pointer to 7x7 font data.
|
||||
; *** Consts ***
|
||||
;
|
||||
.equ VDP_CTLPORT 0xbf
|
||||
.equ VDP_DATAPORT 0xbe
|
||||
.equ VDP_COLS 32
|
||||
.equ VDP_ROWS 24
|
||||
|
||||
; *** Code ***
|
||||
|
||||
vdpInit:
|
||||
ld hl, .initData
|
||||
ld b, .initDataEnd-.initData
|
||||
ld c, VDP_CTLPORT
|
||||
otir
|
||||
|
||||
; Blank VRAM
|
||||
xor a
|
||||
out (VDP_CTLPORT), a
|
||||
ld a, 0x40
|
||||
out (VDP_CTLPORT), a
|
||||
ld bc, 0x4000
|
||||
.loop1:
|
||||
xor a
|
||||
out (VDP_DATAPORT), a
|
||||
dec bc
|
||||
ld a, b
|
||||
or c
|
||||
jr nz, .loop1
|
||||
|
||||
; Set palettes
|
||||
xor a
|
||||
out (VDP_CTLPORT), a
|
||||
ld a, 0xc0
|
||||
out (VDP_CTLPORT), a
|
||||
ld hl, .paletteData
|
||||
ld b, .paletteDataEnd-.paletteData
|
||||
ld c, VDP_DATAPORT
|
||||
otir
|
||||
|
||||
; Define tiles
|
||||
xor a
|
||||
out (VDP_CTLPORT), a
|
||||
ld a, 0x40
|
||||
out (VDP_CTLPORT), a
|
||||
ld hl, FNT_DATA
|
||||
ld c, 0x7e-0x20 ; range of displayable chars in font.
|
||||
; Each row in FNT_DATA is a row of the glyph and there is 7 of them.
|
||||
; We insert a blank one at the end of those 7. For each row we set, we
|
||||
; need to send 3 zero-bytes because each pixel in the tile is actually
|
||||
; 4 bits because it can select among 16 palettes. We use only 2 of them,
|
||||
; which is why those bytes always stay zero.
|
||||
.loop2:
|
||||
ld b, 7
|
||||
.loop3:
|
||||
ld a, (hl)
|
||||
out (VDP_DATAPORT), a
|
||||
; send 3 blanks
|
||||
xor a
|
||||
out (VDP_DATAPORT), a
|
||||
nop ; the VDP needs 16 T-states to breathe
|
||||
out (VDP_DATAPORT), a
|
||||
nop
|
||||
out (VDP_DATAPORT), a
|
||||
inc hl
|
||||
djnz .loop3
|
||||
; Send a blank row after the 7th row
|
||||
xor a
|
||||
out (VDP_DATAPORT), a
|
||||
nop
|
||||
out (VDP_DATAPORT), a
|
||||
nop
|
||||
out (VDP_DATAPORT), a
|
||||
nop
|
||||
out (VDP_DATAPORT), a
|
||||
dec c
|
||||
jr nz, .loop2
|
||||
|
||||
; Bit 7 = ?, Bit 6 = display enabled
|
||||
ld a, 0b11000000
|
||||
out (VDP_CTLPORT), a
|
||||
ld a, 0x81
|
||||
out (VDP_CTLPORT), a
|
||||
ret
|
||||
|
||||
; VDP initialisation data
|
||||
.initData:
|
||||
; 0x8x == set register X
|
||||
.db 0b00000100, 0x80 ; Bit 2: Select mode 4
|
||||
.db 0b00000000, 0x81
|
||||
.db 0b11111111, 0x82 ; Name table: 0x3800
|
||||
.db 0b11111111, 0x85 ; Sprite table: 0x3f00
|
||||
.db 0b11111111, 0x86 ; sprite use tiles from 0x2000
|
||||
.db 0b11111111, 0x87 ; Border uses palette 0xf
|
||||
.db 0b00000000, 0x88 ; BG X scroll
|
||||
.db 0b00000000, 0x89 ; BG Y scroll
|
||||
.db 0b11111111, 0x8a ; Line counter (why have this?)
|
||||
.initDataEnd:
|
||||
.paletteData:
|
||||
; BG palette
|
||||
.db 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
.db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
; Sprite palette (inverted colors)
|
||||
.db 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
.db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
.paletteDataEnd:
|
||||
|
||||
; Convert ASCII char in A into a tile index corresponding to that character.
|
||||
; When a character is unknown, returns 0x5e (a '~' char).
|
||||
vdpConv:
|
||||
; The font is organized to closely match ASCII, so this is rather easy.
|
||||
; We simply subtract 0x20 from incoming A
|
||||
sub 0x20
|
||||
cp 0x5f
|
||||
ret c ; A < 0x5f, good
|
||||
ld a, 0x5e
|
||||
ret
|
||||
|
||||
; grid routine. Sets cell at row D and column E to character A. If C is one, we
|
||||
; use the sprite palette.
|
||||
vdpSetCell:
|
||||
call vdpConv
|
||||
; store A away
|
||||
ex af, af'
|
||||
push bc
|
||||
ld b, 0 ; we push rotated bits from D into B so
|
||||
; that we'll already have our low bits from the
|
||||
; second byte we'll send right after.
|
||||
; Here, we're fitting a 5-bit line, and a 5-bit column on 16-bit, right
|
||||
; aligned. On top of that, our righmost bit is taken because our target
|
||||
; cell is 2-bytes wide and our final number is a VRAM address.
|
||||
ld a, d
|
||||
sla a ; should always push 0, so no pushing in B
|
||||
sla a ; same
|
||||
sla a ; same
|
||||
sla a \ rl b
|
||||
sla a \ rl b
|
||||
sla a \ rl b
|
||||
ld c, a
|
||||
ld a, e
|
||||
sla a ; A * 2
|
||||
or c ; bring in two low bits from D into high
|
||||
; two bits
|
||||
out (VDP_CTLPORT), a
|
||||
ld a, b ; 3 low bits set
|
||||
or 0x78 ; 01 header + 0x3800
|
||||
out (VDP_CTLPORT), a
|
||||
pop bc
|
||||
|
||||
; We're ready to send our data now. Let's go
|
||||
ex af, af'
|
||||
out (VDP_DATAPORT), a
|
||||
|
||||
; Palette select is on bit 3 of MSB
|
||||
ld a, 1
|
||||
and c
|
||||
rla \ rla \ rla
|
||||
out (VDP_DATAPORT), a
|
||||
ret
|
||||
|
148
kernel/stdio.asm
148
kernel/stdio.asm
@ -1,148 +0,0 @@
|
||||
; stdio
|
||||
;
|
||||
; Allows other modules to print to "standard out", and get data from "standard
|
||||
; in", that is, the console through which the user is connected in a decoupled
|
||||
; manner.
|
||||
;
|
||||
; Those GetC/PutC routines are hooked through defines and have this API:
|
||||
;
|
||||
; GetC: Blocks until a character is read from the device and return that
|
||||
; character in A.
|
||||
;
|
||||
; PutC: Write character specified in A onto the device.
|
||||
;
|
||||
; *** Accepted characters ***
|
||||
;
|
||||
; For now, we're in muddy waters in this regard. We try to stay close to ASCII.
|
||||
; Anything over 0x7f is undefined. Both CR and LF are interpreted as "line end".
|
||||
; Both BS and DEL mean "delete previous character".
|
||||
;
|
||||
; When outputting, newlines are marked by CR and LF. Outputting a character
|
||||
; deletion is made through BS then space then BS.
|
||||
;
|
||||
; *** Defines ***
|
||||
; STDIO_GETC: address of a GetC routine
|
||||
; STDIO_PUTC: address of a PutC routine
|
||||
;
|
||||
; *** Consts ***
|
||||
; Size of the readline buffer. If a typed line reaches this size, the line is
|
||||
; flushed immediately (same as pressing return).
|
||||
.equ STDIO_BUFSIZE 0x40
|
||||
|
||||
; *** Variables ***
|
||||
; Line buffer. We read types chars into this buffer until return is pressed
|
||||
; This buffer is null-terminated.
|
||||
.equ STDIO_BUF STDIO_RAMSTART
|
||||
|
||||
; Index where the next char will go in stdioGetC.
|
||||
.equ STDIO_RAMEND @+STDIO_BUFSIZE
|
||||
|
||||
stdioGetC:
|
||||
jp STDIO_GETC
|
||||
|
||||
stdioPutC:
|
||||
jp STDIO_PUTC
|
||||
|
||||
; print null-terminated string pointed to by HL
|
||||
printstr:
|
||||
push af
|
||||
push hl
|
||||
|
||||
.loop:
|
||||
ld a, (hl) ; load character to send
|
||||
or a ; is it zero?
|
||||
jr z, .end ; if yes, we're finished
|
||||
call STDIO_PUTC
|
||||
inc hl
|
||||
jr .loop
|
||||
|
||||
.end:
|
||||
pop hl
|
||||
pop af
|
||||
ret
|
||||
|
||||
; print B characters from string that HL points to
|
||||
printnstr:
|
||||
push bc
|
||||
push hl
|
||||
.loop:
|
||||
ld a, (hl) ; load character to send
|
||||
call STDIO_PUTC
|
||||
inc hl
|
||||
djnz .loop
|
||||
|
||||
.end:
|
||||
pop hl
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Prints a line terminator. This routine is a bit of a misnomer because it's
|
||||
; designed to be overridable to, for example, printlf, but we'll live with it
|
||||
; for now...
|
||||
printcrlf:
|
||||
push af
|
||||
ld a, CR
|
||||
call STDIO_PUTC
|
||||
ld a, LF
|
||||
call STDIO_PUTC
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Repeatedly calls stdioGetC until a whole line was read, that is, when CR or
|
||||
; LF is read or if the buffer is full. Sets HL to the beginning of the read
|
||||
; line, which is null-terminated.
|
||||
;
|
||||
; This routine also takes care of echoing received characters back to the TTY.
|
||||
; It also manages backspaces properly.
|
||||
stdioReadLine:
|
||||
push bc
|
||||
ld hl, STDIO_BUF
|
||||
ld b, STDIO_BUFSIZE-1
|
||||
.loop:
|
||||
; Let's wait until something is typed.
|
||||
call STDIO_GETC
|
||||
; got it. Now, is it a CR or LF?
|
||||
cp CR
|
||||
jr z, .complete ; char is CR? buffer complete!
|
||||
cp LF
|
||||
jr z, .complete
|
||||
cp DEL
|
||||
jr z, .delchr
|
||||
cp BS
|
||||
jr z, .delchr
|
||||
|
||||
; Echo the received character right away so that we see what we type
|
||||
call STDIO_PUTC
|
||||
|
||||
; Ok, gotta add it do the buffer
|
||||
ld (hl), a
|
||||
inc hl
|
||||
djnz .loop
|
||||
; buffer overflow, complete line
|
||||
.complete:
|
||||
; The line in our buffer is complete.
|
||||
; Let's null-terminate it and return.
|
||||
xor a
|
||||
ld (hl), a
|
||||
ld hl, STDIO_BUF
|
||||
pop bc
|
||||
ret
|
||||
|
||||
.delchr:
|
||||
; Deleting is a tricky business. We have to decrease HL and increase B
|
||||
; so that everything stays consistent. We also have to make sure that
|
||||
; We don't do buffer underflows.
|
||||
ld a, b
|
||||
cp STDIO_BUFSIZE-1
|
||||
jr z, .loop ; beginning of line, nothing to delete
|
||||
dec hl
|
||||
inc b
|
||||
; Char deleted in buffer, now send BS + space + BS for the terminal
|
||||
; to clear its previous char
|
||||
ld a, BS
|
||||
call STDIO_PUTC
|
||||
ld a, ' '
|
||||
call STDIO_PUTC
|
||||
ld a, BS
|
||||
call STDIO_PUTC
|
||||
jr .loop
|
@ -1,85 +0,0 @@
|
||||
; Fill B bytes at (HL) with A
|
||||
fill:
|
||||
push bc
|
||||
push hl
|
||||
.loop:
|
||||
ld (hl), a
|
||||
inc hl
|
||||
djnz .loop
|
||||
pop hl
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Increase HL until the memory address it points to is equal to A for a maximum
|
||||
; of 0xff bytes. Returns the new HL value as well as the number of bytes
|
||||
; iterated in A.
|
||||
; If a null char is encountered before we find A, processing is stopped in the
|
||||
; same way as if we found our char (so, we look for A *or* 0)
|
||||
; Set Z if the character is found. Unsets it if not
|
||||
findchar:
|
||||
push bc
|
||||
ld c, a ; let's use C as our cp target
|
||||
ld b, 0xff
|
||||
|
||||
.loop: ld a, (hl)
|
||||
cp c
|
||||
jr z, .match
|
||||
or a ; cp 0
|
||||
jr z, .nomatch
|
||||
inc hl
|
||||
djnz .loop
|
||||
.nomatch:
|
||||
inc a ; unset Z
|
||||
jr .end
|
||||
.match:
|
||||
; We ran 0xff-B loops. That's the result that goes in A.
|
||||
ld a, 0xff
|
||||
sub b
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop bc
|
||||
ret
|
||||
|
||||
|
||||
; Compares strings pointed to by HL and DE up to A count of characters. If
|
||||
; equal, Z is set. If not equal, Z is reset.
|
||||
strncmp:
|
||||
push bc
|
||||
push hl
|
||||
push de
|
||||
|
||||
ld b, a
|
||||
.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
|
||||
djnz .loop
|
||||
; We went through all chars with success, but our current Z flag is
|
||||
; unset because of the cp 0. Let's do a dummy CP to set the Z flag.
|
||||
cp a
|
||||
|
||||
.end:
|
||||
pop de
|
||||
pop hl
|
||||
pop bc
|
||||
; 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
|
||||
|
||||
; Transforms the character in A, if it's in the a-z range, into its upcase
|
||||
; version.
|
||||
upcase:
|
||||
cp 'a'
|
||||
ret c ; A < 'a'. nothing to do
|
||||
cp 'z'+1
|
||||
ret nc ; A >= 'z'+1. nothing to do
|
||||
; 'a' - 'A' == 0x20
|
||||
sub 0x20
|
||||
ret
|
@ -1,175 +0,0 @@
|
||||
; kbd
|
||||
;
|
||||
; Control TI-84+'s keyboard.
|
||||
;
|
||||
; *** Constants ***
|
||||
.equ KBD_PORT 0x01
|
||||
|
||||
; Keys that have a special meaning in GetC. All >= 0x80. They are interpreted
|
||||
; by GetC directly and are never returned as-is.
|
||||
.equ KBD_KEY_ALPHA 0x80
|
||||
.equ KBD_KEY_2ND 0x81
|
||||
|
||||
; *** Variables ***
|
||||
; active long-term modifiers, such as a-lock
|
||||
; bit 0: A-Lock
|
||||
.equ KBD_MODS KBD_RAMSTART
|
||||
.equ KBD_RAMEND @+1
|
||||
|
||||
; *** Code ***
|
||||
|
||||
kbdInit:
|
||||
ld a, 1 ; begin with A-Lock on
|
||||
ld (KBD_MODS), a
|
||||
ret
|
||||
|
||||
; Wait for a digit to be pressed and sets the A register ASCII value
|
||||
; corresponding to that key press.
|
||||
;
|
||||
; This routine waits for a key to be pressed, but before that, it waits for
|
||||
; all keys to be de-pressed. It does that to ensure that two calls to
|
||||
; waitForKey only go through after two actual key presses (otherwise, the user
|
||||
; doesn't have enough time to de-press the button before the next waitForKey
|
||||
; routine registers the same key press as a second one).
|
||||
;
|
||||
; Sending 0xff to the port resets the keyboard, and then we have to send groups
|
||||
; we want to "listen" to, with a 0 in the group bit. Thus, to know if *any* key
|
||||
; is pressed, we send 0xff to reset the keypad, then 0x00 to select all groups,
|
||||
; if the result isn't 0xff, at least one key is pressed.
|
||||
kbdGetC:
|
||||
push bc
|
||||
push hl
|
||||
|
||||
; During this GetC loop, register C holds the modificators
|
||||
; bit 0: Alpha
|
||||
; bit 1: 2nd
|
||||
; Initial value should be zero, but if A-Lock is on, it's 1
|
||||
ld a, (KBD_MODS)
|
||||
and 1
|
||||
ld c, a
|
||||
|
||||
; loop until a digit is pressed
|
||||
.loop:
|
||||
ld hl, .dtbl
|
||||
; we go through the 7 rows of the table
|
||||
ld b, 7
|
||||
; is alpha mod enabled?
|
||||
bit 0, c
|
||||
jr z, .inner ; unset? skip next
|
||||
ld hl, .atbl ; set? we're in alpha mode
|
||||
.inner:
|
||||
ld a, (hl) ; group mask
|
||||
call .get
|
||||
cp 0xff
|
||||
jr nz, .something
|
||||
; nothing for that group, let's scan the next group
|
||||
ld a, 9
|
||||
call addHL ; go to next row
|
||||
djnz .inner
|
||||
; found nothing, loop
|
||||
jr .loop
|
||||
.something:
|
||||
; We have something on that row! Let's find out which char. Register A
|
||||
; currently contains a mask with the pressed char bit unset.
|
||||
ld b, 8
|
||||
inc hl
|
||||
.findchar:
|
||||
rrca ; is next bit unset?
|
||||
jr nc, .gotit ; yes? we have our char!
|
||||
inc hl
|
||||
djnz .findchar
|
||||
.gotit:
|
||||
ld a, (hl)
|
||||
or a ; is char 0?
|
||||
jr z, .loop ; yes? unsupported. loop.
|
||||
call .debounce
|
||||
cp KBD_KEY_ALPHA
|
||||
jr c, .result ; A < 0x80? valid char, return it.
|
||||
jr z, .handleAlpha
|
||||
cp KBD_KEY_2ND
|
||||
jr z, .handle2nd
|
||||
jp .loop
|
||||
.handleAlpha:
|
||||
; Toggle Alpha bit in C. Also, if 2ND bit is set, toggle A-Lock mod.
|
||||
ld a, 1 ; mask for Alpha
|
||||
xor c
|
||||
ld c, a
|
||||
bit 1, c ; 2nd set?
|
||||
jp z, .loop ; unset? loop
|
||||
; we've just hit Alpha with 2nd set. Toggle A-Lock and set Alpha to
|
||||
; the value A-Lock has.
|
||||
ld a, (KBD_MODS)
|
||||
xor 1
|
||||
ld (KBD_MODS), a
|
||||
ld c, a
|
||||
jp .loop
|
||||
.handle2nd:
|
||||
; toggle 2ND bit in C
|
||||
ld a, 2 ; mask for 2ND
|
||||
xor c
|
||||
ld c, a
|
||||
jp .loop
|
||||
|
||||
.result:
|
||||
; We have our result in A, *almost* time to return it. One last thing:
|
||||
; Are in in both Alpha and 2nd mode? If yes, then it means that we
|
||||
; should return the upcase version of our letter (if it's a letter).
|
||||
bit 0, c
|
||||
jr z, .end ; nope
|
||||
bit 1, c
|
||||
jr z, .end ; nope
|
||||
; yup, we have Alpha + 2nd. Upcase!
|
||||
call upcase
|
||||
.end:
|
||||
pop hl
|
||||
pop bc
|
||||
ret
|
||||
.get:
|
||||
ex af, af'
|
||||
ld a, 0xff
|
||||
di
|
||||
out (KBD_PORT), a
|
||||
ex af, af'
|
||||
out (KBD_PORT), a
|
||||
in a, (KBD_PORT)
|
||||
ei
|
||||
ret
|
||||
.debounce:
|
||||
; wait until all keys are de-pressed
|
||||
; To avoid repeat keys, we require 64 subsequent polls to indicate all
|
||||
; depressed keys
|
||||
push af ; --> lvl 1
|
||||
push bc ; --> lvl 2
|
||||
.pressed:
|
||||
ld b, 64
|
||||
.wait:
|
||||
xor a
|
||||
call .get
|
||||
inc a ; if a was 0xff, will become 0 (nz test)
|
||||
jr nz, .pressed ; non-zero? something is pressed
|
||||
djnz .wait
|
||||
|
||||
pop bc ; <-- lvl 2
|
||||
pop af ; <-- lvl 1
|
||||
ret
|
||||
|
||||
; digits table. each row represents a group. first item is group mask.
|
||||
; 0 means unsupported. no group 7 because it has no keys.
|
||||
.dtbl:
|
||||
.db 0xfe, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
.db 0xfd, 0x0d, '+' ,'-' ,'*', '/', '^', 0, 0
|
||||
.db 0xfb, 0, '3', '6', '9', ')', 0, 0, 0
|
||||
.db 0xf7, '.', '2', '5', '8', '(', 0, 0, 0
|
||||
.db 0xef, '0', '1', '4', '7', ',', 0, 0, 0
|
||||
.db 0xdf, 0, 0, 0, 0, 0, 0, 0, KBD_KEY_ALPHA
|
||||
.db 0xbf, 0, 0, 0, 0, 0, KBD_KEY_2ND, 0, 0x7f
|
||||
|
||||
; alpha table. same as .dtbl, for when we're in alpha mode.
|
||||
.atbl:
|
||||
.db 0xfe, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
.db 0xfd, 0x0d, '"' ,'w' ,'r', 'm', 'h', 0, 0
|
||||
.db 0xfb, '?', 0, 'v', 'q', 'l', 'g', 0, 0
|
||||
.db 0xf7, ':', 'z', 'u', 'p', 'k', 'f', 'c', 0
|
||||
.db 0xef, ' ', 'y', 't', 'o', 'j', 'e', 'b', 0
|
||||
.db 0xdf, 0, 'x', 's', 'n', 'i', 'd', 'a', KBD_KEY_ALPHA
|
||||
.db 0xbf, 0, 0, 0, 0, 0, KBD_KEY_2ND, 0, 0x7f
|
@ -1,350 +0,0 @@
|
||||
; lcd
|
||||
;
|
||||
; Implement PutC on TI-84+ (for now)'s LCD screen.
|
||||
;
|
||||
; The screen is 96x64 pixels. The 64 rows are addressed directly with CMD_ROW
|
||||
; but columns are addressed in chunks of 6 or 8 bits (there are two modes).
|
||||
;
|
||||
; In 6-bit mode, there are 16 visible columns. In 8-bit mode, there are 12.
|
||||
;
|
||||
; Note that "X-increment" and "Y-increment" work in the opposite way than what
|
||||
; most people expect. Y moves left and right, X moves up and down.
|
||||
;
|
||||
; *** Z-Offset ***
|
||||
;
|
||||
; This LCD has a "Z-Offset" parameter, allowing to offset rows on the
|
||||
; screen however we wish. This is handy because it allows us to scroll more
|
||||
; efficiently. Instead of having to copy the LCD ram around at each linefeed
|
||||
; (or instead of having to maintain an in-memory buffer), we can use this
|
||||
; feature.
|
||||
;
|
||||
; The Z-Offet goes upwards, with wrapping. For example, if we have an 8 pixels
|
||||
; high line at row 0 and if our offset is 8, that line will go up 8 pixels,
|
||||
; wrapping itself to the bottom of the screen.
|
||||
;
|
||||
; The principle is this: The active line is always the bottom one. Therefore,
|
||||
; when active row is 0, Z is FNT_HEIGHT+1, when row is 1, Z is (FNT_HEIGHT+1)*2,
|
||||
; When row is 8, Z is 0.
|
||||
;
|
||||
; *** 6/8 bit columns and smaller fonts ***
|
||||
;
|
||||
; If your glyphs, including padding, are 6 or 8 pixels wide, you're in luck
|
||||
; because pushing them to the LCD can be done in a very efficient manner.
|
||||
; Unfortunately, this makes the LCD unsuitable for a Collapse OS shell: 6
|
||||
; pixels per glyph gives us only 16 characters per line, which is hardly
|
||||
; usable.
|
||||
;
|
||||
; This is why we have this buffering system. How it works is that we're always
|
||||
; in 8-bit mode and we hold the whole area (8 pixels wide by FNT_HEIGHT high)
|
||||
; in memory. When we want to put a glyph to screen, we first read the contents
|
||||
; of that area, then add our new glyph, offsetted and masked, to that buffer,
|
||||
; then push the buffer back to the LCD. If the glyph is split, move to the next
|
||||
; area and finish the job.
|
||||
;
|
||||
; That being said, it's important to define clearly what CURX and CURY variable
|
||||
; mean. Those variable keep track of the current position *in pixels*, in both
|
||||
; axes.
|
||||
;
|
||||
; *** Requirements ***
|
||||
; fnt/mgm
|
||||
;
|
||||
; *** Constants ***
|
||||
.equ LCD_PORT_CMD 0x10
|
||||
.equ LCD_PORT_DATA 0x11
|
||||
|
||||
.equ LCD_CMD_6BIT 0x00
|
||||
.equ LCD_CMD_8BIT 0x01
|
||||
.equ LCD_CMD_DISABLE 0x02
|
||||
.equ LCD_CMD_ENABLE 0x03
|
||||
.equ LCD_CMD_XDEC 0x04
|
||||
.equ LCD_CMD_XINC 0x05
|
||||
.equ LCD_CMD_YDEC 0x06
|
||||
.equ LCD_CMD_YINC 0x07
|
||||
.equ LCD_CMD_COL 0x20
|
||||
.equ LCD_CMD_ZOFFSET 0x40
|
||||
.equ LCD_CMD_ROW 0x80
|
||||
.equ LCD_CMD_CONTRAST 0xc0
|
||||
|
||||
; *** Variables ***
|
||||
; Current Y position on the LCD, that is, where re're going to spit our next
|
||||
; glyph.
|
||||
.equ LCD_CURY LCD_RAMSTART
|
||||
; Current X position
|
||||
.equ LCD_CURX @+1
|
||||
; two pixel buffers that are 8 pixels wide (1b) by FNT_HEIGHT pixels high.
|
||||
; This is where we compose our resulting pixels blocks when spitting a glyph.
|
||||
.equ LCD_BUF @+1
|
||||
.equ LCD_RAMEND @+FNT_HEIGHT*2
|
||||
|
||||
; *** Code ***
|
||||
lcdInit:
|
||||
; Initialize variables
|
||||
xor a
|
||||
ld (LCD_CURY), a
|
||||
ld (LCD_CURX), a
|
||||
|
||||
; Clear screen
|
||||
call lcdClrScr
|
||||
|
||||
; We begin with a Z offset of FNT_HEIGHT+1
|
||||
ld a, LCD_CMD_ZOFFSET+FNT_HEIGHT+1
|
||||
call lcdCmd
|
||||
|
||||
; Enable the LCD
|
||||
ld a, LCD_CMD_ENABLE
|
||||
call lcdCmd
|
||||
|
||||
; Hack to get LCD to work. According to WikiTI, we're not sure why TIOS
|
||||
; sends these, but it sends it, and it is required to make the LCD
|
||||
; work. So...
|
||||
ld a, 0x17
|
||||
call lcdCmd
|
||||
ld a, 0x0b
|
||||
call lcdCmd
|
||||
|
||||
; Set some usable contrast
|
||||
ld a, LCD_CMD_CONTRAST+0x34
|
||||
call lcdCmd
|
||||
|
||||
; Enable 8-bit mode.
|
||||
ld a, LCD_CMD_8BIT
|
||||
call lcdCmd
|
||||
|
||||
ret
|
||||
|
||||
; Wait until the lcd is ready to receive a command
|
||||
lcdWait:
|
||||
push af
|
||||
.loop:
|
||||
in a, (LCD_PORT_CMD)
|
||||
; When 7th bit is cleared, we can send a new command
|
||||
rla
|
||||
jr c, .loop
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Send cmd A to LCD
|
||||
lcdCmd:
|
||||
out (LCD_PORT_CMD), a
|
||||
jr lcdWait
|
||||
|
||||
; Send data A to LCD
|
||||
lcdDataSet:
|
||||
out (LCD_PORT_DATA), a
|
||||
jr lcdWait
|
||||
|
||||
; Get data from LCD into A
|
||||
lcdDataGet:
|
||||
in a, (LCD_PORT_DATA)
|
||||
jr lcdWait
|
||||
|
||||
; Turn LCD off
|
||||
lcdOff:
|
||||
push af
|
||||
ld a, LCD_CMD_DISABLE
|
||||
call lcdCmd
|
||||
out (LCD_PORT_CMD), a
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Set LCD's current column to A
|
||||
lcdSetCol:
|
||||
push af
|
||||
; The col index specified in A is compounded with LCD_CMD_COL
|
||||
add a, LCD_CMD_COL
|
||||
call lcdCmd
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Set LCD's current row to A
|
||||
lcdSetRow:
|
||||
push af
|
||||
; The col index specified in A is compounded with LCD_CMD_COL
|
||||
add a, LCD_CMD_ROW
|
||||
call lcdCmd
|
||||
pop af
|
||||
ret
|
||||
|
||||
; Send the glyph that HL points to to the LCD, at its current position.
|
||||
; After having called this, the LCD's position will have advanced by one
|
||||
; position
|
||||
lcdSendGlyph:
|
||||
push af
|
||||
push bc
|
||||
push hl
|
||||
push ix
|
||||
|
||||
ld a, (LCD_CURY)
|
||||
call lcdSetRow
|
||||
ld a, (LCD_CURX)
|
||||
srl a \ srl a \ srl a ; div by 8
|
||||
call lcdSetCol
|
||||
|
||||
; First operation: read the LCD memory for the "left" side of the
|
||||
; buffer. We assume the right side to always be empty, so we don't
|
||||
; read it. After having read each line, compose it with glyph line at
|
||||
; HL
|
||||
|
||||
; Before we start, what is our bit offset?
|
||||
ld a, (LCD_CURX)
|
||||
and 0b111
|
||||
; that's our offset, store it in C
|
||||
ld c, a
|
||||
|
||||
ld a, LCD_CMD_XINC
|
||||
call lcdCmd
|
||||
ld ix, LCD_BUF
|
||||
ld b, FNT_HEIGHT
|
||||
; A dummy read is needed after a movement.
|
||||
call lcdDataGet
|
||||
.loop1:
|
||||
; let's go get that glyph data
|
||||
ld a, (hl)
|
||||
ld (ix), a
|
||||
call .shiftIX
|
||||
; now let's go get existing pixel on LCD
|
||||
call lcdDataGet
|
||||
; and now let's do some compositing!
|
||||
or (ix)
|
||||
ld (ix), a
|
||||
inc hl
|
||||
inc ix
|
||||
djnz .loop1
|
||||
|
||||
; Buffer set! now let's send it.
|
||||
ld a, (LCD_CURY)
|
||||
call lcdSetRow
|
||||
|
||||
ld hl, LCD_BUF
|
||||
ld b, FNT_HEIGHT
|
||||
.loop2:
|
||||
ld a, (hl)
|
||||
call lcdDataSet
|
||||
inc hl
|
||||
djnz .loop2
|
||||
|
||||
; And finally, let's send the "right side" of the buffer
|
||||
ld a, (LCD_CURY)
|
||||
call lcdSetRow
|
||||
ld a, (LCD_CURX)
|
||||
srl a \ srl a \ srl a ; div by 8
|
||||
inc a
|
||||
call lcdSetCol
|
||||
|
||||
ld hl, LCD_BUF+FNT_HEIGHT
|
||||
ld b, FNT_HEIGHT
|
||||
.loop3:
|
||||
ld a, (hl)
|
||||
call lcdDataSet
|
||||
inc hl
|
||||
djnz .loop3
|
||||
|
||||
; Increase column and wrap if necessary
|
||||
ld a, (LCD_CURX)
|
||||
add a, FNT_WIDTH+1
|
||||
ld (LCD_CURX), a
|
||||
cp 96-FNT_WIDTH
|
||||
jr c, .skip ; A < 96-FNT_WIDTH
|
||||
call lcdLinefeed
|
||||
.skip:
|
||||
pop ix
|
||||
pop hl
|
||||
pop bc
|
||||
pop af
|
||||
ret
|
||||
; Shift glyph in (IX) to the right C times, sending carry into (IX+FNT_HEIGHT)
|
||||
.shiftIX:
|
||||
dec c \ inc c
|
||||
ret z ; zero? nothing to do
|
||||
push bc ; --> lvl 1
|
||||
xor a
|
||||
ld (ix+FNT_HEIGHT), a
|
||||
.shiftLoop:
|
||||
srl (ix)
|
||||
rr (ix+FNT_HEIGHT)
|
||||
dec c
|
||||
jr nz, .shiftLoop
|
||||
pop bc ; <-- lvl 1
|
||||
ret
|
||||
|
||||
; Changes the current line and go back to leftmost column
|
||||
lcdLinefeed:
|
||||
push af
|
||||
ld a, (LCD_CURY)
|
||||
call .addFntH
|
||||
ld (LCD_CURY), a
|
||||
call lcdClrLn
|
||||
; Now, lets set Z offset which is CURROW+FNT_HEIGHT+1
|
||||
call .addFntH
|
||||
add a, LCD_CMD_ZOFFSET
|
||||
call lcdCmd
|
||||
xor a
|
||||
ld (LCD_CURX), a
|
||||
pop af
|
||||
ret
|
||||
.addFntH:
|
||||
add a, FNT_HEIGHT+1
|
||||
cp 64
|
||||
ret c ; A < 64? no wrap
|
||||
; we have to wrap around
|
||||
xor a
|
||||
ret
|
||||
|
||||
; Clears B rows starting at row A
|
||||
; B is not preserved by this routine
|
||||
lcdClrX:
|
||||
push af
|
||||
call lcdSetRow
|
||||
.outer:
|
||||
push bc ; --> lvl 1
|
||||
ld b, 11
|
||||
ld a, LCD_CMD_YINC
|
||||
call lcdCmd
|
||||
xor a
|
||||
call lcdSetCol
|
||||
.inner:
|
||||
call lcdDataSet
|
||||
djnz .inner
|
||||
ld a, LCD_CMD_XINC
|
||||
call lcdCmd
|
||||
xor a
|
||||
call lcdDataSet
|
||||
pop bc ; <-- lvl 1
|
||||
djnz .outer
|
||||
pop af
|
||||
ret
|
||||
|
||||
lcdClrLn:
|
||||
push bc
|
||||
ld b, FNT_HEIGHT+1
|
||||
call lcdClrX
|
||||
pop bc
|
||||
ret
|
||||
|
||||
lcdClrScr:
|
||||
push bc
|
||||
ld b, 64
|
||||
call lcdClrX
|
||||
pop bc
|
||||
ret
|
||||
|
||||
lcdPutC:
|
||||
cp LF
|
||||
jp z, lcdLinefeed
|
||||
cp BS
|
||||
jr z, .bs
|
||||
push hl
|
||||
call fntGet
|
||||
jr nz, .end
|
||||
call lcdSendGlyph
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
||||
.bs:
|
||||
ld a, (LCD_CURX)
|
||||
or a
|
||||
ret z ; going back one line is too complicated.
|
||||
; not implemented yet
|
||||
sub FNT_WIDTH+1
|
||||
ld (LCD_CURX), a
|
||||
ret
|
@ -1,291 +0,0 @@
|
||||
; floppy
|
||||
;
|
||||
; Implement a block device around a TRS-80 floppy. It uses SVCs supplied by
|
||||
; TRS-DOS to do so.
|
||||
;
|
||||
; *** Floppy buffers ***
|
||||
;
|
||||
; The dual-buffer system is exactly the same as in the "sdc" module. See
|
||||
; comments there.
|
||||
;
|
||||
; *** Consts ***
|
||||
; Number of sector per cylinder. We only support single density for now.
|
||||
.equ FLOPPY_SEC_PER_CYL 10
|
||||
.equ FLOPPY_MAX_CYL 40
|
||||
.equ FLOPPY_BLKSIZE 256
|
||||
|
||||
; *** Variables ***
|
||||
; This is a pointer to the currently selected buffer. This points to the BUFSEC
|
||||
; part, that is, two bytes before actual content begins.
|
||||
.equ FLOPPY_BUFPTR FLOPPY_RAMSTART
|
||||
; Sector number currently in FLOPPY_BUF1. Little endian like any other z80 word.
|
||||
.equ FLOPPY_BUFSEC1 @+2
|
||||
; Whether the buffer has been written to. 0 means clean. 1 means dirty.
|
||||
.equ FLOPPY_BUFDIRTY1 @+2
|
||||
; The contents of the buffer.
|
||||
.equ FLOPPY_BUF1 @+1
|
||||
|
||||
; second buffer has the same structure as the first.
|
||||
.equ FLOPPY_BUFSEC2 @+FLOPPY_BLKSIZE
|
||||
.equ FLOPPY_BUFDIRTY2 @+2
|
||||
.equ FLOPPY_BUF2 @+1
|
||||
.equ FLOPPY_RAMEND @+FLOPPY_BLKSIZE
|
||||
|
||||
; *** Code ***
|
||||
floppyInit:
|
||||
; Make sure that both buffers are flagged as invalid and not dirty
|
||||
xor a
|
||||
ld (FLOPPY_BUFDIRTY1), a
|
||||
ld (FLOPPY_BUFDIRTY2), a
|
||||
dec a
|
||||
ld (FLOPPY_BUFSEC1), a
|
||||
ld (FLOPPY_BUFSEC2), a
|
||||
ret
|
||||
|
||||
; Returns whether D (cylinder) and E (sector) are in proper range.
|
||||
; Z for success.
|
||||
_floppyInRange:
|
||||
ld a, e
|
||||
cp FLOPPY_SEC_PER_CYL
|
||||
jp nc, unsetZ
|
||||
ld a, d
|
||||
cp FLOPPY_MAX_CYL
|
||||
jp nc, unsetZ
|
||||
xor a ; set Z
|
||||
ret
|
||||
|
||||
; Read sector index specified in E and cylinder specified in D and place the
|
||||
; contents in buffer pointed to by (FLOPPY_BUFPTR).
|
||||
; If the operation is a success, updates buffer's sector to the value of DE.
|
||||
; Z on success
|
||||
floppyRdSec:
|
||||
call _floppyInRange
|
||||
ret nz
|
||||
|
||||
push bc
|
||||
push hl
|
||||
|
||||
ld a, 0x28 ; @DCSTAT
|
||||
ld c, 1 ; hardcoded to drive :1 for now
|
||||
rst 0x28
|
||||
jr nz, .end
|
||||
|
||||
ld hl, (FLOPPY_BUFPTR) ; HL --> active buffer's sector
|
||||
ld (hl), e ; sector
|
||||
inc hl
|
||||
ld (hl), d ; cylinder
|
||||
inc hl ; dirty
|
||||
inc hl ; data
|
||||
ld a, 0x31 ; @RDSEC
|
||||
rst 0x28 ; sets proper Z
|
||||
.end:
|
||||
pop hl
|
||||
pop bc
|
||||
ret
|
||||
|
||||
; Write the contents of buffer where (FLOPPY_BUFPTR) points to in sector
|
||||
; associated to it. Unsets the the buffer's dirty flag on success.
|
||||
; Z on success
|
||||
floppyWrSec:
|
||||
push ix
|
||||
ld ix, (FLOPPY_BUFPTR) ; IX points to sector
|
||||
xor a
|
||||
cp (ix+2) ; dirty flag
|
||||
pop ix
|
||||
ret z ; don't write if dirty flag is zero
|
||||
|
||||
push hl
|
||||
push de
|
||||
push bc
|
||||
ld hl, (FLOPPY_BUFPTR) ; sector
|
||||
ld e, (hl)
|
||||
inc hl ; cylinder
|
||||
ld d, (hl)
|
||||
call _floppyInRange
|
||||
jr nz, .end
|
||||
ld c, 1 ; drive
|
||||
ld a, 0x28 ; @DCSTAT
|
||||
rst 0x28
|
||||
jr nz, .end
|
||||
inc hl ; dirty
|
||||
xor a
|
||||
ld (hl), a ; undirty the buffer
|
||||
inc hl ; data
|
||||
ld a, 0x35 ; @WRSEC
|
||||
rst 0x28 ; sets proper Z
|
||||
.end:
|
||||
pop bc
|
||||
pop de
|
||||
pop hl
|
||||
ret
|
||||
|
||||
; Considering the first 15 bits of EHL, select the most appropriate of our two
|
||||
; buffers and, if necessary, sync that buffer with the floppy. If the selected
|
||||
; buffer doesn't have the same sector as what EHL asks, load that buffer from
|
||||
; the floppy.
|
||||
; If the dirty flag is set, we write the content of the in-memory buffer to the
|
||||
; floppy before we read a new sector.
|
||||
; Returns Z on success, NZ on error
|
||||
floppySync:
|
||||
push de
|
||||
; Given a 24-bit address in EHL, extracts the 16-bit sector from it and
|
||||
; place it in DE, following cylinder and sector rules.
|
||||
; EH is our sector index, L is our offset within the sector.
|
||||
|
||||
ld d, e ; cylinder
|
||||
ld a, h ; sector
|
||||
; Let's process D first. Because our maximum number of sectors is 400
|
||||
; (40 * 10), D can only be either 0 or 1. If it's 1, we set D to 25 and
|
||||
; add 6 to A
|
||||
inc d \ dec d
|
||||
jr z, .loop1 ; skip
|
||||
ld d, 25
|
||||
add a, 6
|
||||
.loop1:
|
||||
cp FLOPPY_SEC_PER_CYL
|
||||
jr c, .loop1end
|
||||
sub FLOPPY_SEC_PER_CYL
|
||||
inc d
|
||||
jr .loop1
|
||||
.loop1end:
|
||||
ld e, a ; write final sector in E
|
||||
; Let's first see if our first buffer has our sector
|
||||
ld a, (FLOPPY_BUFSEC1) ; sector
|
||||
cp e
|
||||
jr nz, .notBuf1
|
||||
ld a, (FLOPPY_BUFSEC1+1) ; cylinder
|
||||
cp d
|
||||
jr z, .buf1Ok
|
||||
|
||||
.notBuf1:
|
||||
; Ok, let's check for buf2 then
|
||||
ld a, (FLOPPY_BUFSEC2) ; sector
|
||||
cp e
|
||||
jr nz, .notBuf2
|
||||
ld a, (FLOPPY_BUFSEC2+1) ; cylinder
|
||||
cp d
|
||||
jr z, .buf2Ok
|
||||
|
||||
.notBuf2:
|
||||
; None of our two buffers have the sector we need, we'll need to load
|
||||
; a new one.
|
||||
|
||||
; We select our buffer depending on which is dirty. If both are on the
|
||||
; same status of dirtiness, we pick any (the first in our case). If one
|
||||
; of them is dirty, we pick the clean one.
|
||||
push de ; --> lvl 1
|
||||
ld de, FLOPPY_BUFSEC1
|
||||
ld a, (FLOPPY_BUFDIRTY1)
|
||||
or a ; is buf1 dirty?
|
||||
jr z, .ready ; no? good, that's our buffer
|
||||
; yes? then buf2 is our buffer.
|
||||
ld de, FLOPPY_BUFSEC2
|
||||
|
||||
.ready:
|
||||
; At this point, DE points to one of our two buffers, the good one.
|
||||
; Let's save it to FLOPPY_BUFPTR
|
||||
ld (FLOPPY_BUFPTR), de
|
||||
|
||||
pop de ; <-- lvl 1
|
||||
|
||||
; We have to read a new sector, but first, let's write the current one
|
||||
; if needed.
|
||||
call floppyWrSec
|
||||
jr nz, .end ; error
|
||||
; Let's read our new sector in DE
|
||||
call floppyRdSec
|
||||
jr .end
|
||||
|
||||
.buf1Ok:
|
||||
ld de, FLOPPY_BUFSEC1
|
||||
ld (FLOPPY_BUFPTR), de
|
||||
; Z already set
|
||||
jr .end
|
||||
|
||||
.buf2Ok:
|
||||
ld de, FLOPPY_BUFSEC2
|
||||
ld (FLOPPY_BUFPTR), de
|
||||
; Z already set
|
||||
; to .end
|
||||
.end:
|
||||
pop de
|
||||
ret
|
||||
|
||||
; Flush floppy buffers if dirty and then invalidates them.
|
||||
; We invalidate them so that we allow the case where we swap disks after a
|
||||
; flush. If we didn't invalidate the buffers, reading a swapped disk after a
|
||||
; flush would yield data from the previous disk.
|
||||
floppyFlush:
|
||||
ld hl, FLOPPY_BUFSEC1
|
||||
ld (FLOPPY_BUFPTR), hl
|
||||
call floppyWrSec
|
||||
ld hl, FLOPPY_BUFSEC2
|
||||
ld (FLOPPY_BUFPTR), hl
|
||||
call floppyWrSec
|
||||
call floppyInit
|
||||
xor a ; ensure Z
|
||||
ret
|
||||
|
||||
; *** blkdev routines ***
|
||||
|
||||
; Make HL point to its proper place in FLOPPY_BUF.
|
||||
; EHL currently is a 24-bit offset to read in the floppy. E=high byte,
|
||||
; HL=low word. Load the proper sector in memory and make HL point to the
|
||||
; correct data in the memory buffer.
|
||||
_floppyPlaceBuf:
|
||||
call floppySync
|
||||
ret nz ; error
|
||||
; At this point, we have the proper buffer in place and synced in
|
||||
; (FLOPPY_BUFPTR). Only L is important
|
||||
ld a, l
|
||||
ld hl, (FLOPPY_BUFPTR)
|
||||
inc hl ; sector MSB
|
||||
inc hl ; dirty flag
|
||||
inc hl ; contents
|
||||
; DE is now placed on the data part of the active buffer and all we need
|
||||
; is to increase DE by L.
|
||||
call addHL
|
||||
; Now, HL points exactly at the right byte in the active buffer.
|
||||
xor a ; ensure Z
|
||||
ret
|
||||
|
||||
floppyGetB:
|
||||
push hl
|
||||
call _floppyPlaceBuf
|
||||
jr nz, .end ; NZ already set
|
||||
|
||||
; This is it!
|
||||
ld a, (hl)
|
||||
cp a ; ensure Z
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
||||
|
||||
floppyPutB:
|
||||
push hl
|
||||
push af ; --> lvl 1. let's remember the char we put,
|
||||
; _floppyPlaceBuf destroys A.
|
||||
call _floppyPlaceBuf
|
||||
jr nz, .error
|
||||
|
||||
; HL points to our dest. Recall A and write
|
||||
pop af ; <-- lvl 1
|
||||
ld (hl), a
|
||||
|
||||
; Now, let's set the dirty flag
|
||||
ld a, 1
|
||||
ld hl, (FLOPPY_BUFPTR)
|
||||
inc hl ; sector MSB
|
||||
inc hl ; point to dirty flag
|
||||
ld (hl), a ; set dirty flag
|
||||
xor a ; ensure Z
|
||||
jr .end
|
||||
.error:
|
||||
; preserve error code
|
||||
ex af, af'
|
||||
pop af ; <-- lvl 1
|
||||
ex af, af'
|
||||
call unsetZ
|
||||
.end:
|
||||
pop hl
|
||||
ret
|
@ -1,10 +0,0 @@
|
||||
; kbd - TRS-80 keyboard
|
||||
;
|
||||
; Implement GetC for TRS-80's keyboard using the system's SVCs.
|
||||
|
||||
trs80GetC:
|
||||
push de ; altered by SVC
|
||||
ld a, 0x01 ; @KEY
|
||||
rst 0x28 ; --> A
|
||||
pop de
|
||||
ret
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user