doc: add "Understanding the code" walkthrough
This commit is contained in:
parent
c7ef8bf915
commit
9a7617115f
@ -1,10 +1,5 @@
|
||||
# Collapse OS documentation
|
||||
|
||||
## Assembly guide
|
||||
|
||||
* [Writing the glue code](glue-code.md)
|
||||
* [Running Collapse OS on an emulator](emulate.md)
|
||||
|
||||
## User guide
|
||||
|
||||
* [The shell](../apps/basic/README.md)
|
||||
@ -12,6 +7,8 @@
|
||||
* [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
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
# Running Collapse OS on an emulator
|
||||
|
||||
The quickest way to give Collapse OS a whirl is to use `tools/emul` which is
|
||||
built around [libz80][libz80]. Everything is set up, you just have to run
|
||||
`make`, then `shell/shell`.
|
||||
|
||||
To emulate something at a lower level, I recommend using Alan Cox's [RC2014
|
||||
emulator][rc2014-emul]. It runs Collapse OS fine but you have to write the
|
||||
glue code yourself. One caveat, also, is that it requires a ROM image bigger
|
||||
than 8K, so you have to pad the binary.
|
||||
|
||||
A working Makefile for a project with a glue code being called `main.asm` could
|
||||
look like:
|
||||
|
||||
TARGET = os.bin
|
||||
ZASM = ~/collapseos/tools/emul/zasm/zasm
|
||||
ROM = os.rom
|
||||
|
||||
.PHONY: all
|
||||
all: $(ROM)
|
||||
$(TARGET): main.asm
|
||||
$(ZASM) < $< > $@
|
||||
|
||||
$(ROM): $(TARGET)
|
||||
cp $< $@
|
||||
dd if=/dev/null of=$@ bs=1 count=1 seek=8192
|
||||
|
||||
.PHONY: run
|
||||
run: $(ROM)
|
||||
~/RC2014/rc2014 -r $(ROM)
|
||||
|
||||
`CTRL+\` stops the emulation.
|
||||
|
||||
[libz80]: https://github.com/ggambetta/libz80
|
||||
[rc2014-emul]: https://github.com/EtchedPixels/RC2014
|
144
doc/understanding-code.md
Normal file
144
doc/understanding-code.md
Normal file
@ -0,0 +1,144 @@
|
||||
# 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.
|
Loading…
Reference in New Issue
Block a user