emul/hw: add TI-84+ emulator

I implement the screen using XCB which is much more friendly
than z80e's SDL+CMake for development machines that want to install
minimal dependencies (for example, a port-less OpenBSD rig).
This commit is contained in:
Virgil Dupras 2020-01-01 22:48:01 -05:00
parent 25fc0a3c72
commit 9216057db8
12 changed files with 674 additions and 4 deletions

View File

@ -76,6 +76,17 @@ bool emul_step()
}
}
bool emul_steps(unsigned int steps)
{
while (steps) {
if (!emul_step()) {
return false;
}
steps--;
}
return true;
}
void emul_loop()
{
while (emul_step());

View File

@ -21,5 +21,6 @@ typedef struct {
Machine* emul_init();
bool emul_step();
bool emul_steps(unsigned int steps);
void emul_loop();
void emul_printdebug();

View File

@ -49,7 +49,7 @@ int main(int argc, char *argv[])
}
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
fprintf(stderr, "Can't open %s\n", optarg);
fprintf(stderr, "Can't open %s\n", argv[1]);
return 1;
}
Machine *m = emul_init();

1
emul/hw/ti/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/ti84

6
emul/hw/ti/Makefile Normal file
View File

@ -0,0 +1,6 @@
OBJS = ti84.o t6a04.o kbd.o ../../emul.o ../../libz80/libz80.o
CFLAGS += `pkg-config --cflags xcb`
ti84: $(OBJS)
$(CC) `pkg-config --libs xcb` $(OBJS) -o $@

26
emul/hw/ti/README.md Normal file
View File

@ -0,0 +1,26 @@
# TI-84+ emulator
This emulates a TI-84+ with its screen and keyboard. This is suitable for
running the `ti84` recipe.
## Build
You need `xcb` and `pkg-config` to build this. If you have them, run `make`.
You'll get a `ti84` executable.
## Usage
Launch the emulator with `./ti84 /path/to/rom` (you can use the binary from the
`ti84` recipe. Use the small one, not the one having been filled to 1MB).
This will show a window with the LCD screen's content on it. Most applications,
upon boot, halt after initialization and stay halted until the ON key is
pressed. The ON key is mapped to the tilde (~) key.
Press ESC to quit.
As for the rest of the mappings, they map at the key level. For example, the 'Y'
key maps to '1' (which yields 'y' when in alpha mode). Therefore, '1' and 'Y'
map to the same calculator key. Backspace maps to DEL.
Left Shift maps to 2nd. Left Ctrl maps to Alpha.

101
emul/hw/ti/kbd.c Normal file
View File

@ -0,0 +1,101 @@
#include <string.h>
#include <ctype.h>
#include "kbd.h"
void kbd_init(KBD *kbd)
{
memset(kbd->pressed, 0xff, 8);
kbd->selected = 0xff;
}
uint8_t kbd_rd(KBD *kbd)
{
uint8_t res = 0xff;
for (int i=0; i<8; i++) {
if ((kbd->selected & (1<<i)) == 0) {
res &= kbd->pressed[i];
}
}
return res;
}
void kbd_wr(KBD *kbd, uint8_t val)
{
kbd->selected = val;
}
// The key is separated in two nibble. High nibble is group, low nibble is key.
void kbd_setkey(KBD *kbd, uint8_t key, bool pressed)
{
uint8_t group = kbd->pressed[key>>4];
if (pressed) {
group &= ~(1<<(key&0x7));
} else {
group |= 1<<(key&0x7);
}
kbd->pressed[key>>4] = group;
}
// Attempts to returns a key code corresponding to the specified char. 0 if
// nothing matches.
uint8_t kbd_trans(char c)
{
c = toupper(c);
switch (c) {
case 0x0a:
case 0x0d: return 0x10; // ENTER
case '+': return 0x11;
case '-': return 0x12;
case '*': return 0x13;
case '/': return 0x14;
case '^': return 0x15;
case '3': return 0x21;
case '6': return 0x22;
case '9': return 0x23;
case ')': return 0x24;
case '.': return 0x30;
case '2': return 0x31;
case '5': return 0x32;
case '8': return 0x33;
case '(': return 0x34;
case '0': return 0x40;
case '1': return 0x41;
case '4': return 0x42;
case '7': return 0x43;
case ',': return 0x44;
case 0x7f: return 0x67; // DEL
case '"': return 0x11;
case 'W': return 0x12;
case 'R': return 0x13;
case 'M': return 0x14;
case 'H': return 0x15;
case '?': return 0x20;
case 'V': return 0x22;
case 'Q': return 0x23;
case 'L': return 0x24;
case 'G': return 0x25;
case ':': return 0x30;
case 'Z': return 0x31;
case 'U': return 0x32;
case 'P': return 0x33;
case 'K': return 0x34;
case 'F': return 0x35;
case 'C': return 0x36;
case ' ': return 0x40;
case 'Y': return 0x41;
case 'T': return 0x42;
case 'O': return 0x43;
case 'J': return 0x44;
case 'E': return 0x45;
case 'B': return 0x46;
case 'X': return 0x51;
case 'S': return 0x52;
case 'N': return 0x53;
case 'I': return 0x54;
case 'D': return 0x55;
case 'A': return 0x56;
default: return 0;
}
}

25
emul/hw/ti/kbd.h Normal file
View File

@ -0,0 +1,25 @@
#include <stdint.h>
#include <stdbool.h>
// These are keycodes for special keys
#define KBD_ALPHA 0x57
#define KBD_2ND 0x65
// NOTE: We don't manage the ON key here.
typedef struct {
// Bitmask of pressed keys. Like on real hardware, 0xff means nothing
// pressed. The group 7 has no key, but for code simplicity, we have a neat
// array of 8 bytes.
uint8_t pressed[8];
// Selected groups. Active low.
uint8_t selected;
} KBD;
void kbd_init(KBD *kbd);
uint8_t kbd_rd(KBD *kbd);
void kbd_wr(KBD *kbd, uint8_t val);
// The key is separated in two nibble. High nibble is group, low nibble is key.
void kbd_setkey(KBD *kbd, uint8_t key, bool pressed);
// Attempts to returns a key code corresponding to the specified char. 0 if
// nothing matches.
uint8_t kbd_trans(char c);

141
emul/hw/ti/t6a04.c Normal file
View File

@ -0,0 +1,141 @@
#include <string.h>
#include "t6a04.h"
void t6a04_init(T6A04 *lcd)
{
memset(lcd->ram, 0, T6A04_RAMSIZE);
lcd->enabled = false;
lcd->incmode = T6A04_XINC;
lcd->offset = 0;
lcd->currow = 0;
lcd->curcol = 0;
lcd->just_moved = true;
}
uint8_t t6a04_cmd_rd(T6A04 *lcd)
{
return 0; // we are always ready for a new cmd
}
/*
* 0x00/0x01: 6/8 bit mode
* 0x02/0x03: enable/disable
* 0x04-0x07: incmodes
* 0x20-0x34: set col
* 0x40-0x7f: set Z offset
* 0x80-0xbf: set row
* 0xc0-0xff: set contrast
*/
void t6a04_cmd_wr(T6A04 *lcd, uint8_t val)
{
if ((val & 0xc0) == 0xc0) {
// contrast, ignoring
} else if (val & 0x80) {
lcd->currow = val & 0x3f;
lcd->just_moved = true;
} else if (val & 0x40) {
lcd->offset = val & 0x3f;
} else if (val & 0x20) {
lcd->curcol = val & 0x1f;
lcd->just_moved = true;
} else if (val & 0x18) {
// stuff we don't emulate
} else if (val & 0x04) {
lcd->incmode = val & 0x03;
} else if (val & 0x02) {
lcd->enabled = val & 0x01;
} else {
lcd->has8bitmode = val;
}
}
// Advance current position according to current incmode
static void _advance(T6A04 *lcd)
{
uint8_t maxY = lcd->has8bitmode ? 14 : 19;
switch (lcd->incmode) {
case T6A04_XDEC:
lcd->currow = (lcd->currow-1) & 0x3f;
break;
case T6A04_XINC:
lcd->currow = (lcd->currow+1) & 0x3f;
break;
case T6A04_YDEC:
if (lcd->curcol == 0) {
lcd->curcol = maxY;
} else {
lcd->curcol--;
}
break;
case T6A04_YINC:
if (lcd->curcol < maxY) {
lcd->curcol++;
} else {
lcd->curcol = 0;
}
break;
}
}
uint8_t t6a04_data_rd(T6A04 *lcd)
{
uint8_t res;
if (lcd->just_moved) {
// After a move command, the first read op is a noop.
lcd->just_moved = false;
return 0;
}
if (lcd->has8bitmode) {
int pos = lcd->currow * T6A04_ROWSIZE + lcd->curcol;
res = lcd->ram[pos];
} else {
// 6bit mode is a bit more complicated because the 6-bit number often
// spans two bytes. We manage this by loading two bytes into a uint16_t
// and then shift it right properly.
// bitpos represents the leftmost bit of our 6bit number.
int bitpos = lcd->curcol * 6;
// offset represents the shift right we need to perform from the two
// bytes following bitpos/8 so that we can have our number with a 6-bit
// mask.
// Example, col 3 has a bitpos of 18, which means that it loads bytes 2
// and 3. Its bits would be in bit pos 14:8, which means it has an
// offset of 8. There is always an offset and its always in the 3-10
// range
int offset = 10 - (bitpos % 8); // 10 is for 16bit - 6bit
int pos = (lcd->currow * T6A04_ROWSIZE) + (bitpos / 8);
uint16_t word = lcd->ram[pos] << 8;
word |= lcd->ram[pos+1];
res = (word >> offset) & 0x3f;
}
_advance(lcd);
return res;
}
void t6a04_data_wr(T6A04 *lcd, uint8_t val)
{
lcd->just_moved = false;
if (lcd->has8bitmode) {
int pos = lcd->currow * T6A04_ROWSIZE + lcd->curcol;
lcd->ram[pos] = val;
} else {
// See comments in t6a04_data_rd().
int bitpos = lcd->curcol * 6;
int offset = 10 - (bitpos % 8);
int pos = (lcd->currow * T6A04_ROWSIZE) + (bitpos / 8);
uint16_t word = lcd->ram[pos] << 8;
word |= lcd->ram[pos+1];
// word contains our current ram value. Let's fit val in this.
word &= ~(0x003f << offset);
word |= val << offset;
lcd->ram[pos] = word >> 8;
lcd->ram[pos+1] = word & 0xff;
}
_advance(lcd);
}
bool t6a04_pixel(T6A04 *lcd, uint8_t y, uint8_t x)
{
x = (x + lcd->offset) & 0x3f;
uint8_t val = lcd->ram[x * T6A04_ROWSIZE + (y / 8)];
return (val >> (7 - (y % 8))) & 1;
}

41
emul/hw/ti/t6a04.h Normal file
View File

@ -0,0 +1,41 @@
#include <stdint.h>
#include <stdbool.h>
#define T6A04_ROWSIZE (120/8)
#define T6A04_RAMSIZE 64*T6A04_ROWSIZE
typedef enum {
T6A04_XDEC = 0,
T6A04_XINC = 1,
T6A04_YDEC = 2,
T6A04_YINC = 3
} T6A04_INCMODE;
typedef struct {
// RAM is organized in 64 rows of 120 pixels (there is some offscreen
// memory). Each byte holds 8 pixels. unset means no pixel, set means pixel.
uint8_t ram[T6A04_RAMSIZE];
bool enabled;
// Whether the 8bit mode is enabled.
bool has8bitmode;
// Current "increment mode"
T6A04_INCMODE incmode;
// current Z offset
uint8_t offset;
// Currently active row
uint8_t currow;
// Currently active col (actual meaning depends on whether we're in 8bit
// mode)
uint8_t curcol;
// True when a movement command was just made or if the LCD was just
// initialized. When this is true, a read operation on the data port will be
// invalid (returns zero and doesn't autoinc).
bool just_moved;
} T6A04;
void t6a04_init(T6A04 *lcd);
uint8_t t6a04_cmd_rd(T6A04 *lcd);
void t6a04_cmd_wr(T6A04 *lcd, uint8_t val);
uint8_t t6a04_data_rd(T6A04 *lcd);
void t6a04_data_wr(T6A04 *lcd, uint8_t val);
bool t6a04_pixel(T6A04 *lcd, uint8_t y, uint8_t x);

314
emul/hw/ti/ti84.c Normal file
View File

@ -0,0 +1,314 @@
/* TI-84+
*
* A plain TI-84 with its built-in keyboard as an input and its LCD screen
* as an output.
*
* Uses XCB to render the screen and record keystrokes.
*/
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <xcb/xcb.h>
#include "../../emul.h"
#include "t6a04.h"
#include "kbd.h"
#define RAMSTART 0x8000
#define KBD_PORT 0x01
#define INTERRUPT_PORT 0x03
#define LCD_CMD_PORT 0x10
#define LCD_DATA_PORT 0x11
#define MAX_ROMSIZE 0x2000
static xcb_connection_t *conn;
static xcb_screen_t *screen;
/* graphics contexts */
static xcb_gcontext_t fg;
/* win */
static xcb_drawable_t win;
// pixels to draw. We draw them in one shot.
static xcb_rectangle_t rectangles[96*64];
static Machine *m;
static T6A04 lcd;
static bool lcd_changed;
static KBD kbd;
static bool on_was_pressed;
static uint8_t iord_lcd_cmd()
{
return t6a04_cmd_rd(&lcd);
}
static uint8_t iord_lcd_data()
{
return t6a04_data_rd(&lcd);
}
static uint8_t iord_kbd()
{
return kbd_rd(&kbd);
}
static uint8_t iord_interrupt()
{
return on_was_pressed ? 1 : 0;
}
static void iowr_lcd_cmd(uint8_t val)
{
t6a04_cmd_wr(&lcd, val);
}
static void iowr_lcd_data(uint8_t val)
{
lcd_changed = true;
t6a04_data_wr(&lcd, val);
}
static void iowr_kbd(uint8_t val)
{
kbd_wr(&kbd, val);
}
static void iowr_interrupt(uint8_t val)
{
if ((val & 1) == 0) {
on_was_pressed = false;
}
}
// TIL: XCB doesn't have a builtin way to translate a keycode to an ASCII char.
// Using Xlib looks complicated. This will probably not work in many cases (non
// query keyboards and all...), but for now, let's go with this.
static uint8_t keycode_to_tikbd(xcb_keycode_t kc)
{
switch (kc) {
case 0x0a: return 0x41; // 1
case 0x0b: return 0x31; // 2
case 0x0c: return 0x21; // 3
case 0x0d: return 0x42; // 4
case 0x0e: return 0x32; // 5
case 0x0f: return 0x22; // 6
case 0x10: return 0x43; // 7
case 0x11: return 0x33; // 8
case 0x12: return 0x23; // 9
case 0x13: return 0x40; // 0
case 0x14: return 0x12; // -
case 0x15: return 0x11; // +
case 0x16: return 0x67; // DEL
case 0x18: return 0x23; // Q
case 0x19: return 0x12; // W
case 0x1a: return 0x45; // E
case 0x1b: return 0x13; // R
case 0x1c: return 0x42; // T
case 0x1d: return 0x41; // Y
case 0x1e: return 0x32; // U
case 0x1f: return 0x54; // I
case 0x20: return 0x43; // O
case 0x21: return 0x33; // P
case 0x22: return 0x34; // (
case 0x23: return 0x24; // )
case 0x24: return 0x10; // Return
case 0x25: return KBD_ALPHA; // LCTRL
case 0x26: return 0x56; // A
case 0x27: return 0x52; // S
case 0x28: return 0x55; // D
case 0x29: return 0x35; // F
case 0x2a: return 0x25; // G
case 0x2b: return 0x15; // H
case 0x2c: return 0x44; // J
case 0x2d: return 0x34; // K
case 0x2e: return 0x24; // L
case 0x2f: return 0x30; // :
case 0x30: return 0x11; // "
case 0x32: return KBD_2ND; // Lshift
case 0x34: return 0x31; // Z
case 0x35: return 0x51; // X
case 0x36: return 0x36; // C
case 0x37: return 0x22; // V
case 0x38: return 0x46; // B
case 0x39: return 0x53; // N
case 0x3a: return 0x14; // M
case 0x3b: return 0x44; // ,
case 0x3c: return 0x30; // .
case 0x3d: return 0x20; // ?
case 0x41: return 0x40; // Space
default: return 0;
}
}
void create_window()
{
uint32_t mask;
uint32_t values[2];
/* Create the window */
win = xcb_generate_id(conn);
mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
values[0] = screen->white_pixel;
values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS |
XCB_EVENT_MASK_KEY_RELEASE;
xcb_create_window(
conn,
screen->root_depth,
win,
screen->root,
0, 0,
150, 150,
10,
XCB_WINDOW_CLASS_INPUT_OUTPUT,
screen->root_visual,
mask, values);
fg = xcb_generate_id(conn);
mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;
values[0] = screen->black_pixel;
values[1] = 0;
xcb_create_gc(conn, fg, screen->root, mask, values);
/* Map the window on the screen */
xcb_map_window(conn, win);
}
bool get_pixel(int x, int y)
{
return t6a04_pixel(&lcd, x, y);
}
void draw_pixels()
{
xcb_get_geometry_reply_t *geom;
geom = xcb_get_geometry_reply(conn, xcb_get_geometry(conn, win), NULL);
xcb_clear_area(
conn, 0, win, 0, 0, geom->width, geom->height);
// Figure out inner size to maximize a 96x64 screen (1.5 aspect ratio)
int psize = geom->height / 64;
if (geom->width / 96 < psize) {
// width is the constraint
psize = geom->width / 96;
}
int innerw = psize * 96;
int innerh = psize * 64;
int innerx = (geom->width - innerw) / 2;
int innery = (geom->height - innerh) / 2;
int drawcnt = 0;
for (int i=0; i<96; i++) {
for (int j=0; j<64; j++) {
if (get_pixel(i, j)) {
int x = innerx + (i*psize);
int y = innery + (j*psize);
rectangles[drawcnt].x = x;
rectangles[drawcnt].y = y;
rectangles[drawcnt].height = psize;
rectangles[drawcnt].width = psize;
drawcnt++;
}
}
}
if (drawcnt) {
xcb_poly_fill_rectangle(
conn, win, fg, drawcnt, rectangles);
}
lcd_changed = false;
xcb_flush(conn);
}
void event_loop()
{
if (!emul_step()) {
// We're done
return;
}
while (1) {
emul_step();
if (lcd_changed) {
// To avoid overdrawing, we'll let the CPU run a bit to finish its
// drawing operation.
emul_steps(100);
draw_pixels();
}
xcb_generic_event_t *e = xcb_poll_for_event(conn);
if (!e) {
continue;
}
switch (e->response_type & ~0x80) {
/* ESC to exit */
case XCB_KEY_RELEASE:
case XCB_KEY_PRESS: {
xcb_key_press_event_t *ev = (xcb_key_press_event_t *)e;
if (ev->detail == 0x09) return;
if (ev->detail == 0x31 && e->response_type == XCB_KEY_PRESS) {
// tilde, mapped to ON
on_was_pressed = true;
Z80INT(&m->cpu, 0);
Z80Execute(&m->cpu); // unhalts the CPU
}
uint8_t key = keycode_to_tikbd(ev->detail);
if (key) {
kbd_setkey(&kbd, key, e->response_type == XCB_KEY_PRESS);
}
break;
}
case XCB_EXPOSE: {
draw_pixels();
break;
}
default: {
break;
}
}
free(e);
}
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: ./ti84 /path/to/rom\n");
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
fprintf(stderr, "Can't open %s\n", argv[1]);
return 1;
}
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;
}
t6a04_init(&lcd);
kbd_init(&kbd);
lcd_changed = false;
on_was_pressed = false;
m->iord[KBD_PORT] = iord_kbd;
m->iord[INTERRUPT_PORT] = iord_interrupt;
m->iord[LCD_CMD_PORT] = iord_lcd_cmd;
m->iord[LCD_DATA_PORT] = iord_lcd_data;
m->iowr[KBD_PORT] = iowr_kbd;
m->iowr[INTERRUPT_PORT] = iowr_interrupt;
m->iowr[LCD_CMD_PORT] = iowr_lcd_cmd;
m->iowr[LCD_DATA_PORT] = iowr_lcd_data;
conn = xcb_connect(NULL, NULL);
screen = xcb_setup_roots_iterator(xcb_get_setup(conn)).data;
create_window();
draw_pixels();
event_loop();
emul_printdebug();
return 0;
}

View File

@ -1,4 +1,4 @@
TARGET = os.rom
TARGET = os.bin
BASEDIR = ../..
ZASM = $(BASEDIR)/emul/zasm/zasm
KERNEL = $(BASEDIR)/kernel
@ -9,10 +9,13 @@ MKTIUPGRADE = mktiupgrade
all: $(TARGET)
$(TARGET): glue.asm
$(ZASM) $(KERNEL) $(APPS) < glue.asm > $@
os.rom: $(TARGET)
cp $(TARGET) $@
truncate -s 1M $@
os.8xu: $(TARGET)
$(MKTIUPGRADE) -p -k keys/0A.key -d TI-84+ $(TARGET) $@ 00
os.8xu: os.rom
$(MKTIUPGRADE) -p -k keys/0A.key -d TI-84+ os.rom $@ 00
.PHONY: send
send: os.8xu