Browse Source

Forth reboot underway!

pull/102/head
Virgil Dupras 4 years ago
parent
commit
6bf51ae57c
100 changed files with 0 additions and 13730 deletions
  1. +0
    -117
      CODE.md
  2. +0
    -111
      apps/README.md
  3. +0
    -26
      apps/at28w/glue.asm
  4. +0
    -78
      apps/at28w/main.asm
  5. +0
    -282
      apps/basic/README.md
  6. +0
    -49
      apps/basic/blk.asm
  7. +0
    -182
      apps/basic/buf.asm
  8. +0
    -10
      apps/basic/floppy.asm
  9. +0
    -140
      apps/basic/fs.asm
  10. +0
    -33
      apps/basic/glue.asm
  11. +0
    -531
      apps/basic/main.asm
  12. +0
    -142
      apps/basic/parse.asm
  13. +0
    -14
      apps/basic/sdc.asm
  14. +0
    -97
      apps/basic/tok.asm
  15. +0
    -32
      apps/basic/util.asm
  16. +0
    -104
      apps/basic/var.asm
  17. +0
    -69
      apps/ed/README.md
  18. +0
    -211
      apps/ed/buf.asm
  19. +0
    -150
      apps/ed/cmd.asm
  20. +0
    -43
      apps/ed/glue.asm
  21. +0
    -93
      apps/ed/io.asm
  22. +0
    -176
      apps/ed/main.asm
  23. +0
    -8
      apps/ed/util.asm
  24. +0
    -1
      apps/lib/README.md
  25. +0
    -44
      apps/lib/ari.asm
  26. +0
    -267
      apps/lib/expr.asm
  27. +0
    -115
      apps/lib/fmt.asm
  28. +0
    -238
      apps/lib/parse.asm
  29. +0
    -114
      apps/lib/util.asm
  30. +0
    -21
      apps/memt/glue.asm
  31. +0
    -33
      apps/memt/main.asm
  32. +0
    -4
      apps/sdct/README.md
  33. +0
    -29
      apps/sdct/glue.asm
  34. +0
    -72
      apps/sdct/main.asm
  35. +0
    -200
      apps/zasm/README.md
  36. +0
    -846
      apps/zasm/avr.asm
  37. +0
    -25
      apps/zasm/const.asm
  38. +0
    -336
      apps/zasm/directive.asm
  39. +0
    -88
      apps/zasm/glue.asm
  40. +0
    -44
      apps/zasm/gluea.asm
  41. +0
    -1234
      apps/zasm/instr.asm
  42. +0
    -291
      apps/zasm/io.asm
  43. +0
    -245
      apps/zasm/main.asm
  44. +0
    -45
      apps/zasm/parse.asm
  45. +0
    -340
      apps/zasm/symbol.asm
  46. +0
    -249
      apps/zasm/tok.asm
  47. +0
    -186
      apps/zasm/util.asm
  48. +0
    -16
      avr/README.md
  49. +0
    -10
      avr/avr.h
  50. +0
    -134
      avr/ops.txt
  51. +0
    -10
      avr/tn25.h
  52. +0
    -74
      avr/tn254585.h
  53. +0
    -9
      avr/tn45.h
  54. +0
    -9
      avr/tn85.h
  55. +0
    -20
      doc/README.md
  56. +0
    -63
      doc/blockdev.md
  57. +0
    -78
      doc/fs.md
  58. +0
    -163
      doc/glue-code.md
  59. +0
    -127
      doc/load-run-code.md
  60. +0
    -38
      doc/ti8x.md
  61. +0
    -243
      doc/trs80-4p.md
  62. +0
    -144
      doc/understanding-code.md
  63. +0
    -26
      doc/zasm.md
  64. +0
    -5
      emul/cfsin/count.bas
  65. +0
    -10
      emul/cfsin/hello.asm
  66. +0
    -3
      emul/cfsin/readme.txt
  67. +0
    -178
      emul/shell/glue.asm
  68. +0
    -208
      emul/shell/shell.c
  69. +0
    -34
      emul/shell/user.h
  70. +0
    -131
      emul/zasm/glue.asm
  71. BIN
      emul/zasm/kernel.bin
  72. +0
    -18
      emul/zasm/user.h
  73. BIN
      emul/zasm/zasm.bin
  74. +0
    -269
      emul/zasm/zasm.c
  75. +0
    -17
      kernel/README.md
  76. +0
    -136
      kernel/acia.asm
  77. +0
    -4
      kernel/ascii.h
  78. +0
    -8
      kernel/blkdev.h
  79. +0
    -302
      kernel/blockdev.asm
  80. +0
    -85
      kernel/core.asm
  81. +0
    -14
      kernel/err.h
  82. BIN
      kernel/fnt/3x5.bin
  83. BIN
      kernel/fnt/5x7.bin
  84. BIN
      kernel/fnt/7x7.bin
  85. +0
    -32
      kernel/fnt/mgm.asm
  86. +0
    -575
      kernel/fs.asm
  87. +0
    -13
      kernel/fs.h
  88. +0
    -275
      kernel/grid.asm
  89. +0
    -137
      kernel/kbd.asm
  90. +0
    -48
      kernel/mmap.asm
  91. +0
    -759
      kernel/sdc.asm
  92. +0
    -102
      kernel/sms/kbd.asm
  93. +0
    -205
      kernel/sms/pad.asm
  94. +0
    -174
      kernel/sms/vdp.asm
  95. +0
    -148
      kernel/stdio.asm
  96. +0
    -85
      kernel/str.asm
  97. +0
    -175
      kernel/ti/kbd.asm
  98. +0
    -350
      kernel/ti/lcd.asm
  99. +0
    -291
      kernel/trs80/floppy.asm
  100. +0
    -10
      kernel/trs80/kbd.asm

+ 0
- 117
CODE.md View File

@@ -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

+ 0
- 111
apps/README.md View File

@@ -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`.

+ 0
- 26
apps/at28w/glue.asm View File

@@ -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:

+ 0
- 78
apps/at28w/main.asm View File

@@ -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


+ 0
- 282
apps/basic/README.md View File

@@ -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.

+ 0
- 49
apps/basic/blk.asm View File

@@ -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

+ 0
- 182
apps/basic/buf.asm View File

@@ -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

+ 0
- 10
apps/basic/floppy.asm View File

@@ -1,10 +0,0 @@
; floppy-related basic commands

basFLUSH:
jp floppyFlush

basFloppyCmds:
.db "flush", 0
.dw basFLUSH
.db 0xff ; end of table


+ 0
- 140
apps/basic/fs.asm View File

@@ -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

+ 0
- 33
apps/basic/glue.asm View File

@@ -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:

+ 0
- 531
apps/basic/main.asm View File

@@ -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

+ 0
- 142
apps/basic/parse.asm View File

@@ -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

+ 0
- 14
apps/basic/sdc.asm View File

@@ -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

+ 0
- 97
apps/basic/tok.asm View File

@@ -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

+ 0
- 32
apps/basic/util.asm View File

@@ -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

+ 0
- 104
apps/basic/var.asm View File

@@ -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

+ 0
- 69
apps/ed/README.md View File

@@ -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.

+ 0
- 211
apps/ed/buf.asm View File

@@ -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

+ 0
- 150
apps/ed/cmd.asm View File

@@ -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

+ 0
- 43
apps/ed/glue.asm View File

@@ -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:

+ 0
- 93
apps/ed/io.asm View File

@@ -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

+ 0
- 176
apps/ed/main.asm View File

@@ -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


+ 0
- 8
apps/ed/util.asm View File

@@ -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

+ 0
- 1
apps/lib/README.md View File

@@ -1 +0,0 @@
Common code used by more than one app, but not by the kernel.

+ 0
- 44
apps/lib/ari.asm View File

@@ -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

+ 0
- 267
apps/lib/expr.asm View File

@@ -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

+ 0
- 115
apps/lib/fmt.asm View File

@@ -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

+ 0
- 238
apps/lib/parse.asm View File

@@ -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

+ 0
- 114
apps/lib/util.asm View File

@@ -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


+ 0
- 21
apps/memt/glue.asm View File

@@ -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:

+ 0
- 33
apps/memt/main.asm View File

@@ -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:


+ 0
- 4
apps/sdct/README.md View File

@@ -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.

+ 0
- 29
apps/sdct/glue.asm View File

@@ -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:

+ 0
- 72
apps/sdct/main.asm View File

@@ -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

+ 0
- 200
apps/zasm/README.md View File

@@ -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

+ 0
- 846
apps/zasm/avr.asm View File

@@ -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


+ 0
- 25
apps/zasm/const.asm View File

@@ -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

+ 0
- 336
apps/zasm/directive.asm View File

@@ -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)

+ 0
- 88
apps/zasm/glue.asm View File

@@ -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:

+ 0
- 44
apps/zasm/gluea.asm View File

@@ -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:


+ 0
- 1234
apps/zasm/instr.asm
File diff suppressed because it is too large
View File


+ 0
- 291
apps/zasm/io.asm View File

@@ -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

+ 0
- 245
apps/zasm/main.asm View File

@@ -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

+ 0
- 45
apps/zasm/parse.asm View File

@@ -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

+ 0
- 340
apps/zasm/symbol.asm View File

@@ -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

+ 0
- 249
apps/zasm/tok.asm View File

@@ -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

+ 0
- 186
apps/zasm/util.asm View File

@@ -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



+ 0
- 16
avr/README.md View File

@@ -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.

+ 0
- 10
avr/avr.h View File

@@ -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

+ 0
- 134
avr/ops.txt View File

@@ -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*


+ 0
- 10
avr/tn25.h View File

@@ -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


+ 0
- 74
avr/tn254585.h View File

@@ -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

+ 0
- 9
avr/tn45.h View File

@@ -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

+ 0
- 9
avr/tn85.h View File

@@ -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

+ 0
- 20
doc/README.md View File

@@ -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)

+ 0
- 63
doc/blockdev.md View File

@@ -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>

+ 0
- 78
doc/fs.md View File

@@ -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
>

+ 0
- 163
doc/glue-code.md View File

@@ -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

+ 0
- 127
doc/load-run-code.md View File

@@ -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`.

+ 0
- 38
doc/ti8x.md View File

@@ -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.

+ 0
- 243
doc/trs80-4p.md View File

@@ -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.

+ 0
- 144
doc/understanding-code.md View File

@@ -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.

+ 0
- 26
doc/zasm.md View File

@@ -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!

+ 0
- 5
emul/cfsin/count.bas View File

@@ -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

+ 0
- 10
emul/cfsin/hello.asm View File

@@ -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


+ 0
- 3
emul/cfsin/readme.txt View File

@@ -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.

+ 0
- 178
emul/shell/glue.asm View File

@@ -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

+ 0
- 208
emul/shell/shell.c View File

@@ -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;
}

+ 0
- 34
emul/shell/user.h View File

@@ -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


+ 0
- 131
emul/zasm/glue.asm View File

@@ -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


BIN
emul/zasm/kernel.bin View File


+ 0
- 18
emul/zasm/user.h View File

@@ -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

BIN
emul/zasm/zasm.bin View File


+ 0
- 269
emul/zasm/zasm.c View File

@@ -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;
}


+ 0
- 17
kernel/README.md View File

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

+ 0
- 136
kernel/acia.asm View File

@@ -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


+ 0
- 4
kernel/ascii.h View File

@@ -1,4 +0,0 @@
.equ BS 0x08
.equ CR 0x0d
.equ LF 0x0a
.equ DEL 0x7f

+ 0
- 8
kernel/blkdev.h View File

@@ -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


+ 0
- 302
kernel/blockdev.asm View File

@@ -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:

+ 0
- 85
kernel/core.asm View File

@@ -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

+ 0
- 14
kernel/err.h View File

@@ -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


BIN
kernel/fnt/3x5.bin View File


BIN
kernel/fnt/5x7.bin View File


BIN
kernel/fnt/7x7.bin View File


+ 0
- 32
kernel/fnt/mgm.asm View File

@@ -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

+ 0
- 575
kernel/fs.asm View File

@@ -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

+ 0
- 13
kernel/fs.h View File

@@ -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

+ 0
- 275
kernel/grid.asm View File

@@ -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

+ 0
- 137
kernel/kbd.asm View File

@@ -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

+ 0
- 48
kernel/mmap.asm View File

@@ -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

+ 0
- 759
kernel/sdc.asm View File

@@ -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

+ 0
- 102
kernel/sms/kbd.asm View File

@@ -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


+ 0
- 205
kernel/sms/pad.asm View File

@@ -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

+ 0
- 174
kernel/sms/vdp.asm View File

@@ -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


+ 0
- 148
kernel/stdio.asm View File

@@ -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

+ 0
- 85
kernel/str.asm View File

@@ -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

+ 0
- 175
kernel/ti/kbd.asm View File

@@ -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

+ 0
- 350
kernel/ti/lcd.asm View File

@@ -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

+ 0
- 291
kernel/trs80/floppy.asm View File

@@ -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

+ 0
- 10
kernel/trs80/kbd.asm View File

@@ -1,10 +0,0 @@
; kbd - TRS-80 keyboard
;
; Implement GetC for TRS-80's keyboard using the system's SVCs.

trs80GetC:
push de ; altered by SVC
ld a, 0x01 ; @KEY
rst 0x28 ; --> A
pop de
ret

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save