Skip to content

Commit

Permalink
docs: implement preliminary docs (#34)
Browse files Browse the repository at this point in the history
Author @atirut-w 

Implement preliminary documentation thanks to mkdocs, moved README sections to this new doc
  • Loading branch information
atirut-w authored Oct 6, 2024
1 parent 839e681 commit 59bf5fe
Show file tree
Hide file tree
Showing 15 changed files with 908 additions and 3 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<img src="md_images/zeal8bitos.png" alt="Zeal 8-bit OS logo" />
<img src="docs/md_images/zeal8bitos.png" alt="Zeal 8-bit OS logo" />
</p>
<p align="center">
<a href="https://opensource.org/licenses/Apache-2.0">
Expand Down Expand Up @@ -280,7 +280,7 @@ The second page, page 1, is where user programs are copied and executed. Thus, a
The fourth page, page 3, is used to store the OS data for both the kernel and the drivers. When loading a user program, this page is switched to RAM, so that it's usable by the program, when a syscall occurs, it's switched back to the kernel RAM. Upon loading a user program, the SP (Stack Pointer) is set to `0xFFFF`. However, this may change in the near future.

To sum up, here is a diagram to show the usage of the memory:
<img src="md_images/mapping.svg" alt="Memory mapping diagram"/>
<img src="docs/md_images/mapping.svg" alt="Memory mapping diagram"/>

*If the user program's parameters are pointing to a portion of memory in page 3 (last page), there is a conflict as the kernel will always map its RAM page inside this exact same page during a syscall. Thus, it will remap user's page 3 into page 2 (third page) to access the program's parameters. Of course, in case the parameters are pointers, they will be modified to let them point to the new virtual address (in other words, a pointer will be subtracted by 16KB to let it point to page 2).

Expand All @@ -296,7 +296,7 @@ Ideally, 48KB of RAM should be mapped starting at `0x4000` and would go up to `0
* `KERNEL_RAM_START`: this marks the start address of the kernel RAM where the stack, all the variables used by the kernel AND drivers will be stored. Of course, it must be big enough to store all of these data. For information, the current kernel `BSS` section size is around 1KB. The stack depth depends on the target drivers' implementation. Allocating 1KB for the stack should be more than enough as long as no (big) buffers are stored on it. Overall allocating at least 3KB for the kernel RAM should be safe and future-proof.

To sum up, here is a diagram to show the usage of the memory:
<img src="md_images/mapping_nommu.svg" alt="Memory mapping diagram"/>
<img src="docs/md_images/mapping_nommu.svg" alt="Memory mapping diagram"/>

Regarding the user programs, the stack address will always be set to `KERNEL_RAM_START - 1` by the kernel before execution. It also corresponds to the address of its last byte available in its usable address space. This means that a program can determine the size of the available RAM by performing `SP - 0x4000`, which gives, in assembly:

Expand Down
58 changes: 58 additions & 0 deletions docs/details/drivers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
A driver consists of a structure containing:

* Name of the driver, maximum 4 characters (filled with NULL char if shorter). For example, `SER0`, `SER1`, `I2C0`, etc. Non-ASCII characters are allowed but not advised.
* The address of an `init` routine, called when the kernel boots.
* The address of `read` routine, where parameters and return address are the same as in the syscall table.
* The address of `write` routine, same as above.
* The address of `open` routine, same as above.
* The address of `close` routine, same as above.
* The address of `seek` routine, same as above.
* The address of `ioctl` routine, same as above.
* The address of `deinit` routine, called when unloading the driver.

Here is the example of a simple driver registration:
```asm
my_driver0_init:
; Register itself to the VFS
; Do something
xor a ; Success
ret
my_driver0_read:
; Do something
ret
my_driver0_write:
; Do something
ret
my_driver0_open:
; Do something
ret
my_driver0_close:
; Do something
ret
my_driver0_seek:
; Do something
ret
my_driver0_ioctl:
; Do something
ret
my_driver0_deinit:
; Do something
ret
SECTION DRV_VECTORS
DEFB "DRV0"
DEFW my_driver0_init
DEFW my_driver0_read
DEFW my_driver0_write
DEFW my_driver0_open
DEFW my_driver0_close
DEFW my_driver0_seek
DEFW my_driver0_ioctl
DEFW my_driver0_deinit
```

Registering a driver consists in putting this information (structure) inside a section called `DRV_VECTORS`. The order is very important as any driver dependency shall be resolved at compile-time. For example, if driver `A` depends on driver `B`, then `B`'s structure must be put before `A` in the section `DRV_VECTORS`.

At boot, the `driver` component will browse the whole `DRV_VECTORS` section and initialize the drivers one by one by calling their `init` routine. If this routine returns `ERR_SUCCESS`, the driver will be registered and user programs can open it, read, write, ioctl, etc...

A driver can be hidden to the programs, this is handy for disk drivers that must only be accessed by the kernel's file system layer. To do so, the `init` routine should return `ERR_DRIVER_HIDDEN`.
41 changes: 41 additions & 0 deletions docs/details/kernel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Used registers

Z80 presents multiple general-purpose registers, not all of them are used in the kernel, here is the scope of each of them:

| Register | Scope |
| ------------------ | ------------------------------ |
| AF, BC, DE, HL | System & application |
| AF', BC', DE', HL' | Interrupt handlers |
| IX, IY | Application (unused in the OS) |

This means that the OS won't alter IX and IY registers, so they can be used freely in the application.

The alternate registers (names followed by `'`) may only be used in the interrupt handlers[^1]. An application should not use these registers. If for some reason, you still have to use them, please consider disabling the interrupts during the time they are used:

```asm
my_routine:
di ; disable interrupt
ex af, af' ; exchange af with alternate af' registers
[...] ; use af'
ex af, af' ; exchange them back
ei ; re-enable interrupts
```

Keep in mind that disabling the interrupts for too long can be harmful as the system won't receive any signal from hardware (timers, keyboard, GPIOs...)

[^1]: They shall **not** be considered as non-volatile nonetheless. In other words, an interrupt handler shall not make the assumption that the data it wrote inside any alternate register will be kept until the next time it is called.

## Reset vectors

The Z80 provides 8 distinct reset vectors, as the system is meant to always be stored in the first virtual page of memory, these are all reserved for the OS:

| Vector | Usage |
| ------ | ------------------------------------------------------------------ |
| $00 | Software reset |
| $08 | Syscall |
| $10 | Jumps to the address in HL (can be used for calling HL) |
| $18 | _Unused_ |
| $20 | _Unused_ |
| $28 | _Unused_ |
| $30 | _Unused_ |
| $38 | Reserved for Interrupt Mode 1, usable by the target implementation |
38 changes: 38 additions & 0 deletions docs/details/memory-mapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Kernel configured with MMU

Zeal 8-bit OS can separate kernel RAM and user programs thanks to virtual pages. Indeed, as it is currently implemented, the kernel is aware of 4 virtual pages of 16KB.

The first page, page 0, shall not be switched as it contains the kernel code. This means that the OS binary is limited to 16KB, it must never exceed this size. When a user's program is being executed, any `syscall` will result in jumping in the first bank where the OS code resides. So if this page is switched for another purpose, no syscall, no interrupt nor communication with the kernel must happen, else, undefined behavior will occur.

The second page, page 1, is where user programs are copied and executed. Thus, all the programs for Zeal 8-bit OS shall be linked from address `0x4000` (16KB). When loading a program, the second and third pages are also mapped to usable RAM from the user program. Thus, a user program can have a maximum size of 48KB.

The fourth page, page 3, is used to store the OS data for both the kernel and the drivers. When loading a user program, this page is switched to RAM, so that it's usable by the program, when a syscall occurs, it's switched back to the kernel RAM. Upon loading a user program, the SP (Stack Pointer) is set to `0xFFFF`. However, this may change in the near future.

To sum up, here is a diagram to show the usage of the memory:
<img src="../../md_images/mapping.svg" alt="Memory mapping diagram"/>

*If the user program's parameters are pointing to a portion of memory in page 3 (last page), there is a conflict as the kernel will always map its RAM page inside this exact same page during a syscall. Thus, it will remap user's page 3 into page 2 (third page) to access the program's parameters. Of course, in case the parameters are pointers, they will be modified to let them point to the new virtual address (in other words, a pointer will be subtracted by 16KB to let it point to page 2).

## Kernel configured as no-MMU

To be able to port Zeal 8-bit OS to Z80-based computers that don't have an MMU/Memory mapper organized as shown above, the kernel has a new mode that can be chosen through the `menuconfig`: no-MMU.

In this mode, the OS code is still expected to be mapped in the first 16KB of the memory, from `0x0000` to `0x3FFF` and the rest is expected to be RAM.

Ideally, 48KB of RAM should be mapped starting at `0x4000` and would go up to `0xFFFF`, but in practice, it is possible to configure the kernel to expect less than that. To do so, two entries in the `menuconfig` must be configured appropriately:

* `KERNEL_STACK_ADDR`: this marks the end of the kernel RAM area, and, as its name states, will be the bottom of the kernel stack.
* `KERNEL_RAM_START`: this marks the start address of the kernel RAM where the stack, all the variables used by the kernel AND drivers will be stored. Of course, it must be big enough to store all of these data. For information, the current kernel `BSS` section size is around 1KB. The stack depth depends on the target drivers' implementation. Allocating 1KB for the stack should be more than enough as long as no (big) buffers are stored on it. Overall allocating at least 3KB for the kernel RAM should be safe and future-proof.

To sum up, here is a diagram to show the usage of the memory:
<img src="../../md_images/mapping_nommu.svg" alt="Memory mapping diagram"/>

Regarding the user programs, the stack address will always be set to `KERNEL_RAM_START - 1` by the kernel before execution. It also corresponds to the address of its last byte available in its usable address space. This means that a program can determine the size of the available RAM by performing `SP - 0x4000`, which gives, in assembly:

```asm
ld hl, 0
add hl, sp
ld bc, -0x4000
add hl, bc
; HL contains the size of the available RAM for the program, which includes the program's code and its stack.
```
103 changes: 103 additions & 0 deletions docs/details/syscalls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
The system relies on syscalls to perform requests between the user program and the kernel. Thus, this shall be the way to perform operations on the hardware. The possible operations are listed in the table below.

## Syscall table

| Num | Name | Param. 1 | Param. 2 | Param. 3 |
| --- | ------- | ------------ | ---------- | --------- |
| 0 | read | u8 dev | u16 buf | u16 size |
| 1 | write | u8 dev | u16 buf | u16 size |
| 2 | open | u16 name | u8 flags | |
| 3 | close | u8 dev | | |
| 4 | dstat | u8 dev | u16 dst | |
| 5 | stat | u16 name | u16 dst | |
| 6 | seek | u8 dev | u32 offset | u8 whence |
| 7 | ioctl | u8 dev | u8 cmd | u16 arg |
| 8 | mkdir | u16 path | | |
| 9 | chdir | u16 path | | |
| 10 | curdir | u16 path | | |
| 11 | opendir | u16 path | | |
| 12 | readdir | u8 dev | u16 dst | |
| 13 | rm | u16 path | | |
| 14 | mount | u8 dev | u8 letter | u8 fs |
| 15 | exit | u8 code | | |
| 16 | exec | u16 name | u16 argv | |
| 17 | dup | u8 dev | u8 ndev | |
| 18 | msleep | u16 duration | | |
| 19 | settime | u8 id | u16 time | |
| 20 | gettime | u8 id | u16 time | |
| 21 | setdate | u16 date | |
| 22 | getdate | u16 date | |
| 23 | map | u16 dst | u24 src | |
| 24 | swap | u8 dev | u8 ndev | |

Please check the [section below](#syscall-parameters) for more information about each of these call and their parameters.

!!! note
Some syscalls may be unimplemented. For example, on computers where directories are not supported,directories-related syscalls may be omitted.

## Syscall parameters

In order to perform a syscall, the operation number must be stored in register `L`, the parameters must be stored following these rules:

| Parameter name in API | Z80 Register |
| --------------------- | ------------ |
| u8 dev | `H` |
| u8 ndev | `E` |
| u8 flags | `H` |
| u8 cmd | `C` |
| u8 letter | `D` |
| u8 code | `H` |
| u8 fs | `E` |
| u8 id | `H` |
| u8 whence | `A` |
| u16 buf | `DE` |
| u16 size | `BC` |
| u16 name | `BC` |
| u16 dst | `DE` |
| u16 arg | `DE` |
| u16 path | `DE` |
| u16 argv | `DE` |
| u16 duration | `DE` |
| u16 time | `DE` |
| u16 date | `DE` |
| u24 src | `HBC` |
| u32 offset | `BCDE` |


And finally, the code must perform an `RST $08` instruction (please check [Reset vectors](kernel.md#reset-vectors)).

The returned value is placed in A. The meaning of that value is specific to each call, please check the documentation of the concerned routines for more information.

## Syscall parameters constraints

To maximize user programs compatibility with Zeal 8-bit OS kernel, regardless of whether the kernel was compiled in MMU or no-MMU mode, the syscalls parameters constraints are the same:

<b>Any buffer passed to a syscall shall **not** cross a 16KB virtual pages</b>

In other words, if a buffer `buf` of size `n` is located in virtual page `i`, its last byte, pointed by `buf + n - 1`, must also be located on the exact same page `i`.

For example, if `read` syscall is called with:
* `DE = 0x4000` and `BC = 0x1000`, the parameters are **correct**, because the buffer pointed by `DE` fits into page 1 (from `0x4000` to `0x7FFF`)
* `DE = 0x4000` and `BC = 0x4000`, the parameters are **correct**, because the buffer pointed by `DE` fits into page 1 (from `0x4000` to `0x7FFF`)
* `DE = 0x7FFF` and `BC = 0x2`, the parameters are **incorrect**, because the buffer pointed by DE is in-between page 1 and page2.

## Syscall `exec`

Even though Zeal 8-bit OS is a mono-tasking operating system, it can execute and keep several programs in memory. When a program A executes a program B thanks to the `exec` syscall, it shall provide a `mode` parameter that can be either `EXEC_OVERRIDE_PROGRAM` or `EXEC_PRESERVE_PROGRAM`:

* `EXEC_OVERRIDE_PROGRAM`: this option tells the kernel that program A doesn't need to be executed anymore, so program B will be loaded in the same address space as program A. In other words, program B will be loaded inside the same RAM pages as program A, it will overwrite it.
* `EXEC_PRESERVE_PROGRAM`: this option tells the kernel that program A needs to be kept in RAM until program B finishes its execution and calls the `exit` syscall. To do so, the kernel will allocate 3 new memory pages (`16KB * 3 = 48KB`) in which it stores newly loaded program B. Once program B exits, the kernel frees the previously allocated pages for program B, remaps program A's memory pages, and gives back the hand to program A. If needed, A can retrieve B's exit value.

The depth of the execution tree is defined in the `menuconfig`, thanks to option `CONFIG_KERNEL_MAX_NESTED_PROGRAMS`. It represents the maximum number of programs that can be stored in RAM at one time. For example, if the depth is 3, program A can call program B, program B can call program C, but program C cannot call any other program.
However, if a program invokes `exec` with `EXEC_OVERRIDE_PROGRAM`, the depth is **not** incremented as the new program to load will override the current one.
As such, if we take back the previous example, program C can call a program if and only if it invokes the `exec` syscall in `EXEC_OVERRIDE_PROGRAM` mode.

Be careful, when executing a sub-program, the whole opened device table, (including files, directories, and drivers), the current directory, and CPU registers will be **shared**.

This means that if program A opens a file with descriptor 3, program B will inherit this index, and thus, also be able to read, write, or even close that descriptor. Reciprocally, if B opens a file, directory, or driver and exits **without** closing it, program A will also have access to it. As such, the general guideline to follow is that before exiting, a program must always close the descriptors it opened. The only moment the table of opened devices and current directory are reset is when the initial program (program A in the previous example) exits. In that case, the kernel will close all the descriptors in the opened devices table, reopen the standard input and output, and reload the initial program.

This also means that when invoking the `exec` syscall in an assembly program, on success, all registers, except HL, must be considered altered because they will be used by the subprogram. So, if you wish to preserve `AF`, `BC`, `DE`, `IX` or `IY`, they must be pushed on the stack before invoking `exec`.

## Syscall documentation

The syscalls are all documented in the header files provided for both assembly and C, you will find [assembly headers here](https://github.com/Zeal8bit/Zeal-8-bit-OS/tree/main/kernel_headers/z88dk-z80asm) and [C headers here](https://github.com/Zeal8bit/Zeal-8-bit-OS/tree/main/kernel_headers/sdcc/include) respectively.
12 changes: 12 additions & 0 deletions docs/details/user-space.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Entry point

When a user program is executed, the kernel allocates 3 pages of RAM (48KB), reads the binary file to execute and loads it starting at virtual address `0x4000` by default. This entry point virtual address is configurable through the `menuconfig` with option `KERNEL_INIT_EXECUTABLE_ADDR`, but keep in mind that existing programs won't work anymore without being recompiled because they are not relocatable at runtime.

## Program parameters

As described below, the `exec` syscall takes two parameters: a binary file name to execute and a parameter.

This parameter must be a NULL-terminated string that will be copied and transmitted to the binary to execute through registers `DE` and `BC`:

* `DE` contains the address of the string. This string will be copied to the new program's memory space, usually on top of the stack.
* `BC` contains the length of that string (so, excluding the NULL-byte). If `BC` is 0, `DE` **must** be discarded by the user program.
Loading

0 comments on commit 59bf5fe

Please sign in to comment.