@@ -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 |
@@ -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. |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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: | |||
@@ -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. |
@@ -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 |
@@ -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* | |||
@@ -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> |
@@ -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 | |||
> |
@@ -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`. |
@@ -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. |
@@ -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. |
@@ -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 | |||
@@ -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 |
@@ -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/`). |
@@ -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 |
@@ -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 | |||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 | |||
@@ -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 |