@@ -3,6 +3,16 @@ | |||||
This folder contains a couple of tools running under the [libz80][libz80] | This folder contains a couple of tools running under the [libz80][libz80] | ||||
emulator. | emulator. | ||||
## Not real hardware | |||||
In the few emulated apps described below, we don't try to emulate real hardware | |||||
because the goal here is to facilitate userspace development. | |||||
These apps run on imaginary hardware and use many cheats to simplify I/Os. | |||||
For real hardware emulation (which helps developing drivers), see the `hw` | |||||
folder. | |||||
## Build | ## Build | ||||
First, make sure that the `libz80` git submodule is checked out. If not, run | First, make sure that the `libz80` git submodule is checked out. If not, run | ||||
@@ -21,10 +31,6 @@ device that is suitable for Collapse OS's filesystem to run on. | |||||
Through that, it becomes easier to develop userspace applications for Collapse | Through that, it becomes easier to develop userspace applications for Collapse | ||||
OS. | OS. | ||||
We don't try to emulate real hardware to ease the development of device drivers | |||||
because so far, I don't see the advantage of emulation versus running code on | |||||
the real thing. | |||||
By default, the shell initialized itself with a CFS device containing the | By default, the shell initialized itself with a CFS device containing the | ||||
contents of `cfsin/` at launch (it's packed on the fly). You can specify an | contents of `cfsin/` at launch (it's packed on the fly). You can specify an | ||||
alternate CFS device file (it has to be packaed already) through the `-f` flag. | alternate CFS device file (it has to be packaed already) through the `-f` flag. | ||||
@@ -0,0 +1,10 @@ | |||||
# Hardware emulation | |||||
In this folder, we emulate some of the hardware used in recipes. The emulation | |||||
is done in a simplistic manner, just enough to verify that the driver code for | |||||
it works generally well. No tricky stuff implemented. | |||||
This kind of emulation is useful for detecting obvious regressions without | |||||
having to get the code on actual hardware for the upteenth time. | |||||
To use, go to the appropriate subfolder and read README there. |
@@ -0,0 +1 @@ | |||||
/classic |
@@ -0,0 +1,4 @@ | |||||
OBJS = acia.o classic.o ../../emul.o ../../libz80/libz80.o | |||||
classic: $(OBJS) | |||||
$(CC) $(OBJS) -o $@ |
@@ -0,0 +1,11 @@ | |||||
# RC2014 emulation | |||||
This emulates a RC2014 classic with 8K of ROM, 32K of RAM and an ACIA hooked to | |||||
stdin/stdout. | |||||
Run `make` to build. | |||||
## Usage | |||||
Run `./classic /path/to/rom` (for example, `os.bin` from RC2014's recipe). | |||||
Serial I/O is hooked to stdin/stdout. `CTRL+D` to quit. |
@@ -0,0 +1,82 @@ | |||||
#include "acia.h" | |||||
static void _check_irq(ACIA *acia) | |||||
{ | |||||
// do we have RDRF? | |||||
if ((acia->status & 0x01) && (acia->control & 0x80)) { | |||||
acia->status |= 0x80; | |||||
} | |||||
// do we have TDRE? | |||||
if ((acia->status & 0x02) && ((acia->control & 0xe0) == 0xe0)) { | |||||
acia->status |= 0x80; | |||||
} | |||||
} | |||||
void acia_init(ACIA *acia) | |||||
{ | |||||
acia->status = 0x02; // TDRE | |||||
acia->control = 0x00; | |||||
acia->rx = 0; | |||||
acia->tx = 0; | |||||
acia->in_int = false; | |||||
} | |||||
bool acia_has_irq(ACIA *acia) | |||||
{ | |||||
if (acia->in_int) { | |||||
return false; | |||||
} | |||||
acia->in_int = acia->status & 0x80; | |||||
return acia->in_int; | |||||
} | |||||
bool acia_hasrx(ACIA *acia) | |||||
{ | |||||
return acia->status & 0x01; // RDRF | |||||
} | |||||
bool acia_hastx(ACIA *acia) | |||||
{ | |||||
return !(acia->status & 0x02); // TRDE | |||||
} | |||||
uint8_t acia_read(ACIA *acia) | |||||
{ | |||||
acia->status |= 0x02; // TRDE high | |||||
_check_irq(acia); | |||||
return acia->tx; | |||||
} | |||||
void acia_write(ACIA *acia, uint8_t val) | |||||
{ | |||||
acia->status |= 0x01; // RDRF high | |||||
acia->rx = val; | |||||
_check_irq(acia); | |||||
} | |||||
uint8_t acia_ctl_rd(ACIA *acia) | |||||
{ | |||||
return acia->status; | |||||
} | |||||
void acia_ctl_wr(ACIA *acia, uint8_t val) | |||||
{ | |||||
acia->control = val; | |||||
_check_irq(acia); | |||||
} | |||||
uint8_t acia_data_rd(ACIA *acia) | |||||
{ | |||||
acia->status &= ~0x81; // RDRF and IRQ low | |||||
acia->in_int = false; | |||||
return acia->rx; | |||||
} | |||||
void acia_data_wr(ACIA *acia, uint8_t val) | |||||
{ | |||||
acia->tx = val; | |||||
acia->status &= ~0x82; // TRDE and IRQ low | |||||
acia->in_int = false; | |||||
_check_irq(acia); | |||||
} | |||||
@@ -0,0 +1,39 @@ | |||||
#include <stdint.h> | |||||
#include <stdbool.h> | |||||
typedef struct { | |||||
// Bit 7: interrupt status, low when interrupt request pending. | |||||
// Bit 6: Parity error | |||||
// Bit 5: Receiver overrun | |||||
// Bit 4: Framing error | |||||
// Bit 3: Clear To Send | |||||
// Bit 2: Data Carrier Detected | |||||
// Bit 1: Transmit Data Register Empty (TDRE) | |||||
// Bit 0: Receive Data Register Full (RDRF) | |||||
// We care about bits 7, 1, 0, maybe 5 later. | |||||
uint8_t status; | |||||
// Bit 7: interrupt enable | |||||
// Bits 6:5: RTS + transmit interrupt enable | |||||
// Bits 4:2: parity + stop bit | |||||
// Bits 1:0: speed divider | |||||
// We don't actually care about any of those except the interrupt enable | |||||
// bits. | |||||
uint8_t control; | |||||
uint8_t rx; | |||||
uint8_t tx; | |||||
// Will be set to true the first time acia_has_irq() is called when IRQ is | |||||
// set. Then, as long as it stays true, acia_has_irq() will return false. | |||||
// When IRQ status is reset, so is in_int. | |||||
bool in_int; | |||||
} ACIA; | |||||
void acia_init(ACIA *acia); | |||||
bool acia_has_irq(ACIA *acia); | |||||
bool acia_hasrx(ACIA *acia); | |||||
bool acia_hastx(ACIA *acia); | |||||
uint8_t acia_read(ACIA *acia); | |||||
void acia_write(ACIA *acia, uint8_t val); | |||||
uint8_t acia_ctl_rd(ACIA *acia); | |||||
void acia_ctl_wr(ACIA *acia, uint8_t val); | |||||
uint8_t acia_data_rd(ACIA *acia); | |||||
void acia_data_wr(ACIA *acia, uint8_t val); |
@@ -0,0 +1,126 @@ | |||||
/* RC2014 classic | |||||
* | |||||
* - 8K of ROM in range 0x0000-0x2000 | |||||
* - 32K of RAM in range 0x8000-0xffff | |||||
* - ACIA in ports 0x80 (ctl) and 0x81 (data) | |||||
* | |||||
* ACIA is hooked to stdin/stdout. CTRL+D exits when in TTY mode. | |||||
*/ | |||||
#include <stdint.h> | |||||
#include <stdio.h> | |||||
#include <unistd.h> | |||||
#include <termios.h> | |||||
#include "../../emul.h" | |||||
#include "acia.h" | |||||
#define RAMSTART 0x8000 | |||||
#define ACIA_CTL_PORT 0x80 | |||||
#define ACIA_DATA_PORT 0x81 | |||||
#define MAX_ROMSIZE 0x2000 | |||||
static ACIA acia; | |||||
static uint8_t iord_acia_ctl() | |||||
{ | |||||
return acia_ctl_rd(&acia); | |||||
} | |||||
static uint8_t iord_acia_data() | |||||
{ | |||||
return acia_data_rd(&acia); | |||||
} | |||||
static void iowr_acia_ctl(uint8_t val) | |||||
{ | |||||
acia_ctl_wr(&acia, val); | |||||
} | |||||
static void iowr_acia_data(uint8_t val) | |||||
{ | |||||
acia_data_wr(&acia, val); | |||||
} | |||||
int main(int argc, char *argv[]) | |||||
{ | |||||
if (argc != 2) { | |||||
fprintf(stderr, "Usage: ./classic /path/to/rom\n"); | |||||
return 1; | |||||
} | |||||
FILE *fp = fopen(argv[1], "r"); | |||||
if (fp == NULL) { | |||||
fprintf(stderr, "Can't open %s\n", optarg); | |||||
return 1; | |||||
} | |||||
Machine *m = emul_init(); | |||||
m->ramstart = RAMSTART; | |||||
int i = 0; | |||||
int c; | |||||
while ((c = fgetc(fp)) != EOF && i < MAX_ROMSIZE) { | |||||
m->mem[i++] = c & 0xff; | |||||
} | |||||
pclose(fp); | |||||
if (i == MAX_ROMSIZE) { | |||||
fprintf(stderr, "ROM image too large.\n"); | |||||
return 1; | |||||
} | |||||
bool tty = isatty(fileno(stdin)); | |||||
struct termios term, saved_term; | |||||
if (tty) { | |||||
// Turn echo off: the shell takes care of its own echoing. | |||||
if (tcgetattr(0, &term) == -1) { | |||||
printf("Can't setup terminal.\n"); | |||||
return 1; | |||||
} | |||||
saved_term = term; | |||||
term.c_lflag &= ~ECHO; | |||||
term.c_lflag &= ~ICANON; | |||||
term.c_cc[VMIN] = 0; | |||||
term.c_cc[VTIME] = 0; | |||||
tcsetattr(0, TCSADRAIN, &term); | |||||
} | |||||
acia_init(&acia); | |||||
m->iord[ACIA_CTL_PORT] = iord_acia_ctl; | |||||
m->iord[ACIA_DATA_PORT] = iord_acia_data; | |||||
m->iowr[ACIA_CTL_PORT] = iowr_acia_ctl; | |||||
m->iowr[ACIA_DATA_PORT] = iowr_acia_data; | |||||
char tosend = 0; | |||||
while (emul_step()) { | |||||
// Do we have an interrupt? | |||||
if (acia_has_irq(&acia)) { | |||||
Z80INT(&m->cpu, 0); | |||||
} | |||||
// Is the RC2014 transmitting? | |||||
if (acia_hastx(&acia)) { | |||||
putchar(acia_read(&acia)); | |||||
fflush(stdout); | |||||
} | |||||
// Do we have something to send? | |||||
if (!tosend) { | |||||
char c; | |||||
if (read(fileno(stdin), &c, 1) == 1) { | |||||
if (c == 4) { // CTRL+D | |||||
// Stop here | |||||
break; | |||||
} | |||||
tosend = c; | |||||
} else if (!tty) { | |||||
// This means we reached EOF | |||||
break; | |||||
} | |||||
} | |||||
if (tosend && !acia_hasrx(&acia)) { | |||||
acia_write(&acia, tosend); | |||||
tosend = 0; | |||||
} | |||||
} | |||||
if (tty) { | |||||
printf("Done!\n"); | |||||
tcsetattr(0, TCSADRAIN, &saved_term); | |||||
emul_printdebug(); | |||||
} | |||||
return 0; | |||||
} |