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. | |||
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], | |||
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 |
@@ -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 | |||
* `parts/platforms/rc2014.inc` as `platform.inc` | |||
* `parts/shell/shell.asm` as `shell.asm` | |||
* Collapse OS parts in `/path/to/parts` | |||
* [scas][scas] | |||
* [romwrite][romwrite] and its specified dependencies | |||
* [GNU screen][screen] | |||
* 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 | |||
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 | |||