From 9a7617115fd620075f5c9d6f684ff297f88e5867 Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Sun, 5 Jan 2020 10:30:36 -0500 Subject: [PATCH] doc: add "Understanding the code" walkthrough --- doc/README.md | 7 +- doc/emulate.md | 35 --------- doc/understanding-code.md | 144 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 40 deletions(-) delete mode 100644 doc/emulate.md create mode 100644 doc/understanding-code.md diff --git a/doc/README.md b/doc/README.md index 8832c06..9be7c05 100644 --- a/doc/README.md +++ b/doc/README.md @@ -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 diff --git a/doc/emulate.md b/doc/emulate.md deleted file mode 100644 index 367c9c7..0000000 --- a/doc/emulate.md +++ /dev/null @@ -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 diff --git a/doc/understanding-code.md b/doc/understanding-code.md new file mode 100644 index 0000000..7edcb13 --- /dev/null +++ b/doc/understanding-code.md @@ -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.