From 6bb454232a41fc3f74adc58098a87381afe7799e Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Sat, 13 Apr 2019 16:01:20 -0400 Subject: [PATCH] Extract "acia.asm" from shell Also, come up with a way to make parts play well together memory-wise. --- parts/README.md | 30 +++++++++ parts/acia.asm | 97 +++++++++++++++++++++++++++ parts/shell.asm | 81 +++++++++++++++++++++++ parts/shell/shell.asm | 177 -------------------------------------------------- recipes/rc2014.md | 57 +++++++++++++++- 5 files changed, 262 insertions(+), 180 deletions(-) create mode 100644 parts/acia.asm create mode 100644 parts/shell.asm delete mode 100644 parts/shell/shell.asm diff --git a/parts/README.md b/parts/README.md index ea2087f..a70f056 100644 --- a/parts/README.md +++ b/parts/README.md @@ -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 `_RAMSTART` +constant to be defined and, in turn, defines a `_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 diff --git a/parts/acia.asm b/parts/acia.asm new file mode 100644 index 0000000..520a9a5 --- /dev/null +++ b/parts/acia.asm @@ -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 + diff --git a/parts/shell.asm b/parts/shell.asm new file mode 100644 index 0000000..8afc349 --- /dev/null +++ b/parts/shell.asm @@ -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 diff --git a/parts/shell/shell.asm b/parts/shell/shell.asm deleted file mode 100644 index 27b0882..0000000 --- a/parts/shell/shell.asm +++ /dev/null @@ -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 diff --git a/recipes/rc2014.md b/recipes/rc2014.md index eefb580..73486b0 100644 --- a/recipes/rc2014.md +++ b/recipes/rc2014.md @@ -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