-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: implement preliminary docs (#34)
Author @atirut-w Implement preliminary documentation thanks to mkdocs, moved README sections to this new doc
- Loading branch information
Showing
15 changed files
with
908 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.