Extract "acia.asm" from shell

Also, come up with a way to make parts play well together memory-wise.
This commit is contained in:
Virgil Dupras 2019-04-13 16:01:20 -04:00
parent ac22a206ae
commit 6bb454232a
5 changed files with 262 additions and 180 deletions

View File

@ -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

97
parts/acia.asm Normal file
View File

@ -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

81
parts/shell.asm Normal file
View File

@ -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

View File

@ -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

View File

@ -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