collapseos/doc/understanding-code.md

145 lines
6.3 KiB
Markdown
Raw Normal View History

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