Also, come up with a way to make parts play well together memory-wise.pull/10/head
@@ -2,7 +2,37 @@ | |||||
Bits and pieces of code that you can assemble to build an OS for your machine. | Bits and pieces of code that you can assemble to build an OS for your machine. | ||||
These parts are made to be glued together in a single `main.asm` file you write | |||||
yourself. | |||||
As of now, the z80 assembler code is written to be assembled with [scas][scas], | As of now, the z80 assembler code is written to be assembled with [scas][scas], | ||||
but this is going to change in the future as a new hosted assembler is written. | but this is going to change in the future as a new hosted assembler is written. | ||||
## Defines | |||||
Each part can have its own constants, but some constant are made to be defined | |||||
externally. We already have some of those external definitions in platform | |||||
includes, but we can have more defines than this. | |||||
Each part has a "DEFINES" section listing the constant it expects to be defined. | |||||
Make sure that you have these constants defined before you include the file. | |||||
## Variable management | |||||
Each part can define variables. These variables are defined as addresses in | |||||
RAM. We know where RAM start from the `RAMSTART` constant in platform includes, | |||||
but because those parts are made to be glued together in no pre-defined order, | |||||
we need a system to align variables from different modules in RAM. | |||||
This is why each part that has variable expect a `<PARTNAME>_RAMSTART` | |||||
constant to be defined and, in turn, defines a `<PARTNAME>_RAMEND` constant to | |||||
carry to the following part. | |||||
Thus, code that glue parts together coould look like: | |||||
MOD1_RAMSTART .equ RAMSTART | |||||
#include "mod1.asm" | |||||
MOD2_RAMSTART .equ MOD1_RAMEND | |||||
#include "mod2.asm" | |||||
[scas]: https://github.com/KnightOS/scas | [scas]: https://github.com/KnightOS/scas |
@@ -0,0 +1,97 @@ | |||||
; acia | |||||
; | |||||
; Manage I/O from an asynchronous communication interface adapter (ACIA). | |||||
; provides "aciaPutC" to put c char on the ACIA as well as an input buffer. | |||||
; You have to call "aciaInt" on interrupt for this module to work well. | |||||
; | |||||
; "aciaInit" also has to be called on boot, but it doesn't call "ei" and "im 1", | |||||
; which is the responsibility of the main asm file, but is needed. | |||||
; *** DEFINES *** | |||||
; ACIA_CTL: IO port for the ACIA's control registers | |||||
; ACIA_IO: IO port for the ACIA's data registers | |||||
; ACIA_RAMSTART: Address at which ACIA-related variables should be stored in | |||||
; RAM. | |||||
; *** CONSTS *** | |||||
; size of the input buffer. If our input goes over this size, we echo | |||||
; immediately. | |||||
ACIA_BUFSIZE .equ 0x20 | |||||
; *** VARIABLES *** | |||||
; Our input buffer starts there | |||||
ACIA_BUF .equ ACIA_RAMSTART | |||||
; index, in the buffer, where our next character will go. 0 when the buffer is | |||||
; empty, BUFSIZE-1 when it's almost full. | |||||
ACIA_BUFIDX .equ ACIA_BUF+ACIA_BUFSIZE | |||||
ACIA_RAMEND .equ ACIA_BUFIDX+1 | |||||
aciaInit: | |||||
; initialize variables | |||||
xor a | |||||
ld (ACIA_BUFIDX), a ; starts at 0 | |||||
; setup ACIA | |||||
; CR7 (1) - Receive Interrupt enabled | |||||
; CR6:5 (00) - RTS low, transmit interrupt disabled. | |||||
; CR4:2 (101) - 8 bits + 1 stop bit | |||||
; CR1:0 (10) - Counter divide: 64 | |||||
ld a, 0b10010110 | |||||
out (ACIA_CTL), a | |||||
ret | |||||
; read char in the ACIA and put it in the read buffer | |||||
aciaInt: | |||||
push af | |||||
push hl | |||||
; Read our character from ACIA into our BUFIDX | |||||
in a, (ACIA_CTL) | |||||
bit 0, a ; is our ACIA rcv buffer full? | |||||
jr z, .end ; no? a interrupt was triggered for nothing. | |||||
call aciaBufPtr ; HL set, A set | |||||
; is our input buffer full? If yes, we don't read anything. Something | |||||
; is wrong: we don't process data fast enough. | |||||
cp ACIA_BUFSIZE | |||||
jr z, .end ; if BUFIDX == BUFSIZE, our buffer is full. | |||||
; increase our buf ptr while we still have it in A | |||||
inc a | |||||
ld (ACIA_BUFIDX), a | |||||
in a, (ACIA_IO) | |||||
ld (hl), a | |||||
.end: | |||||
pop hl | |||||
pop af | |||||
ei | |||||
reti | |||||
; Set current buffer pointer in HL. The buffer pointer is where our *next* char | |||||
; will be written. A is set to the value of (BUFIDX) | |||||
aciaBufPtr: | |||||
push bc | |||||
ld a, (ACIA_BUFIDX) | |||||
ld hl, ACIA_BUF | |||||
xor b | |||||
ld c, a | |||||
add hl, bc ; hl now points to INPTBUF + BUFIDX | |||||
pop bc | |||||
ret | |||||
; spits character in A in port SER_OUT | |||||
aciaPutC: | |||||
push af | |||||
.stwait: | |||||
in a, (ACIA_CTL) ; get status byte from SER | |||||
bit 1, a ; are we still transmitting? | |||||
jr z, .stwait ; if yes, wait until we aren't | |||||
pop af | |||||
out (ACIA_IO), a ; push current char | |||||
ret | |||||
@@ -0,0 +1,81 @@ | |||||
; shell | |||||
; | |||||
; Runs a shell over an asynchronous communication interface adapter (ACIA). | |||||
; for now, this unit is tightly coupled to acia.asm, but it will eventually be | |||||
; more general than that. | |||||
; Incomplete. For now, this outputs a welcome prompt and then waits for input. | |||||
; Whenever input is CR or LF, we echo back what we've received and empty the | |||||
; input buffer. This also happen when the buffer is full. | |||||
; *** CONSTS *** | |||||
CR .equ 0x0d | |||||
LF .equ 0x0a | |||||
shellInit: | |||||
; print prompt | |||||
ld hl, d_welcome | |||||
call printstr | |||||
call printcrlf | |||||
ret | |||||
shellLoop: | |||||
call chkbuf | |||||
jr shellLoop | |||||
; print null-terminated string pointed to by HL | |||||
printstr: | |||||
ld a, (hl) ; load character to send | |||||
or a ; is it zero? | |||||
ret z ; if yes, we're finished | |||||
call aciaPutC | |||||
inc hl | |||||
jr printstr | |||||
; no ret because our only way out is ret z above | |||||
printcrlf: | |||||
ld a, CR | |||||
call aciaPutC | |||||
ld a, LF | |||||
call aciaPutC | |||||
ret | |||||
; check if the input buffer is full or ends in CR or LF. If it does, prints it | |||||
; back and empty it. | |||||
chkbuf: | |||||
call aciaBufPtr | |||||
cp 0 | |||||
ret z ; BUFIDX is zero? nothing to check. | |||||
cp ACIA_BUFSIZE | |||||
jr z, .do ; if BUFIDX == BUFSIZE? do! | |||||
; our previous char is in BUFIDX - 1. Fetch this | |||||
dec hl | |||||
ld a, (hl) ; now, that's our char we have in A | |||||
inc hl ; put HL back where it was | |||||
cp CR | |||||
jr z, .do ; char is CR? do! | |||||
cp LF | |||||
jr z, .do ; char is LF? do! | |||||
; nothing matched? don't do anything | |||||
ret | |||||
.do: | |||||
; terminate our string with 0 | |||||
xor a | |||||
ld (hl), a | |||||
; reset buffer index | |||||
ld (ACIA_BUFIDX), a | |||||
; alright, let's go! | |||||
ld hl, ACIA_BUF | |||||
call printstr | |||||
call printcrlf | |||||
ret | |||||
; *** DATA *** | |||||
d_welcome: .byte "Welcome to Collapse OS", 0 |
@@ -1,177 +0,0 @@ | |||||
; shell | |||||
; | |||||
; Runs a shell over an asynchronous communication interface adapter (ACIA). | |||||
; Incomplete. For now, this outputs a welcome prompt and then waits for input. | |||||
; Whenever input is CR or LF, we echo back what we've received and empty the | |||||
; input buffer. This also happen when the buffer is full. | |||||
#include "platform.inc" | |||||
; *** CONSTS *** | |||||
CR .equ 0x0d | |||||
LF .equ 0x0a | |||||
; size of the input buffer. If our input goes over this size, we echo | |||||
; immediately. | |||||
BUFSIZE .equ 0x20 | |||||
; *** VARIABLES *** | |||||
; Our input buffer starts there | |||||
INPTBUF .equ RAMSTART | |||||
; index, in the buffer, where our next character will go. 0 when the buffer is | |||||
; empty, BUFSIZE-1 when it's almost full. | |||||
BUFIDX .equ INPTBUF+BUFSIZE | |||||
; *** CODE *** | |||||
jr init | |||||
.fill 0x38-$ | |||||
jr handleInterrupt | |||||
init: | |||||
di | |||||
; setup stack | |||||
ld hl, RAMEND | |||||
ld sp, hl | |||||
; initialize variables | |||||
xor a | |||||
ld (BUFIDX), a ; starts at 0 | |||||
; RC2014's serial I/O is based on interrupt mode 1. We'd prefer im 2, | |||||
; but for now, let's go with the simpler im 1. | |||||
im 1 | |||||
; setup ACIA | |||||
; CR7 (1) - Receive Interrupt enabled | |||||
; CR6:5 (00) - RTS low, transmit interrupt disabled. | |||||
; CR4:2 (101) - 8 bits + 1 stop bit | |||||
; CR1:0 (10) - Counter divide: 64 | |||||
ld a, 0b10010110 | |||||
out (ACIA_CTL), a | |||||
; print prompt | |||||
ld hl, d_welcome | |||||
call printstr | |||||
call printcrlf | |||||
; alright, ready to receive | |||||
ei | |||||
mainloop: | |||||
call chkbuf | |||||
jr mainloop | |||||
; read char in the ACIA and put it in the read buffer | |||||
handleInterrupt: | |||||
push af | |||||
push hl | |||||
; Read our character from ACIA into our BUFIDX | |||||
in a, (ACIA_CTL) | |||||
bit 0, a ; is our ACIA rcv buffer full? | |||||
jr z, .end ; no? a interrupt was triggered for nothing. | |||||
call getbufptr ; HL set, A set | |||||
; is our input buffer full? If yes, we don't read anything. Something | |||||
; is wrong: we don't process data fast enough. | |||||
cp BUFSIZE | |||||
jr z, .end ; if BUFIDX == BUFSIZE, our buffer is full. | |||||
; increase our buf ptr while we still have it in A | |||||
inc a | |||||
ld (BUFIDX), a | |||||
in a, (ACIA_IO) | |||||
ld (hl), a | |||||
.end: | |||||
pop hl | |||||
pop af | |||||
ei | |||||
reti | |||||
; spits character in A in port SER_OUT | |||||
printc: | |||||
push af | |||||
.stwait: | |||||
in a, (ACIA_CTL) ; get status byte from SER | |||||
bit 1, a ; are we still transmitting? | |||||
jr z, .stwait ; if yes, wait until we aren't | |||||
pop af | |||||
out (ACIA_IO), a ; push current char | |||||
ret | |||||
; print null-terminated string pointed to by HL | |||||
printstr: | |||||
ld a, (hl) ; load character to send | |||||
or a ; is it zero? | |||||
ret z ; if yes, we're finished | |||||
call printc | |||||
inc hl | |||||
jr printstr | |||||
; no ret because our only way out is ret z above | |||||
printcrlf: | |||||
ld a, CR | |||||
call printc | |||||
ld a, LF | |||||
call printc | |||||
ret | |||||
; check if the input buffer is full or ends in CR or LF. If it does, prints it | |||||
; back and empty it. | |||||
chkbuf: | |||||
call getbufptr | |||||
cp 0 | |||||
ret z ; BUFIDX is zero? nothing to check. | |||||
cp BUFSIZE | |||||
jr z, .do ; if BUFIDX == BUFSIZE? do! | |||||
; our previous char is in BUFIDX - 1. Fetch this | |||||
dec hl | |||||
ld a, (hl) ; now, that's our char we have in A | |||||
inc hl ; put HL back where it was | |||||
cp CR | |||||
jr z, .do ; char is CR? do! | |||||
cp LF | |||||
jr z, .do ; char is LF? do! | |||||
; nothing matched? don't do anything | |||||
ret | |||||
.do: | |||||
; terminate our string with 0 | |||||
xor a | |||||
ld (hl), a | |||||
; reset buffer index | |||||
ld (BUFIDX), a | |||||
; alright, let's go! | |||||
ld hl, INPTBUF | |||||
call printstr | |||||
call printcrlf | |||||
ret | |||||
; Set current buffer pointer in HL. The buffer pointer is where our *next* char | |||||
; will be written. A is set to the value of (BUFIDX) | |||||
getbufptr: | |||||
push bc | |||||
ld a, (BUFIDX) | |||||
ld hl, INPTBUF | |||||
xor b | |||||
ld c, a | |||||
add hl, bc ; hl now points to INPTBUF + BUFIDX | |||||
pop bc | |||||
ret | |||||
; *** DATA *** | |||||
d_welcome: .byte "Welcome to Collapse OS", 0 |
@@ -31,18 +31,69 @@ device I use in this recipe. | |||||
### Gathering parts | ### Gathering parts | ||||
* `parts/platforms/rc2014.inc` as `platform.inc` | |||||
* `parts/shell/shell.asm` as `shell.asm` | |||||
* Collapse OS parts in `/path/to/parts` | |||||
* [scas][scas] | * [scas][scas] | ||||
* [romwrite][romwrite] and its specified dependencies | * [romwrite][romwrite] and its specified dependencies | ||||
* [GNU screen][screen] | * [GNU screen][screen] | ||||
* A FTDI-to-TTL cable to connect to the Serial I/O module of the RC2014 | * A FTDI-to-TTL cable to connect to the Serial I/O module of the RC2014 | ||||
### Write main.asm | |||||
This is what your glue code would look like: | |||||
``` | |||||
#include "platforms/rc2014.inc" | |||||
jr init | |||||
.fill 0x38-$ | |||||
jr aciaInt | |||||
init: | |||||
di | |||||
; setup stack | |||||
ld hl, RAMEND | |||||
ld sp, hl | |||||
im 1 | |||||
call aciaInit | |||||
call shellInit | |||||
ei | |||||
call shellLoop | |||||
ACIA_RAMSTART .equ RAMSTART | |||||
#include "acia.asm" | |||||
#include "shell.asm" | |||||
``` | |||||
The `platform.inc` include is there to load all platform-specific constants | |||||
(such as `RAMSTART` and `RAMEND`). | |||||
Then come the reset vectors. If course, we have our first jump to our main init | |||||
routine, and then we have a jump to the interrupt handler defined in `acia.asm`. | |||||
We need to plug this one in so that we can receive characters from the ACIA. | |||||
Then comes the usual `di` to aoid interrupts during init, and stack setup. | |||||
We set interrupt mode to 1 because that's what `acia.asm` is written around. | |||||
Then, we init ACIA, shell, enable interrupt and give control of the main loop | |||||
to `shell.asm`. | |||||
What comes below is actual code include from the acia and shell modules. As you | |||||
can see, we need to tell each module where to put their variables. `shell.asm` | |||||
doesn't have variables, but if it did, we would have a `SHELL_RAMSTART .equ | |||||
ACIA_RAMEND` just below the `acia.asm` include. `ACIA_RAMEND` is defined in | |||||
`acia.asm`. | |||||
### Build the image | ### Build the image | ||||
We only have the shell to build, so it's rather straightforward: | We only have the shell to build, so it's rather straightforward: | ||||
scas -o rom.bin shell.asm | |||||
scas -I /path/to/parts -o rom.bin main.asm | |||||
### Write to the ROM | ### Write to the ROM | ||||