diff --git a/emul/emul.c b/emul/emul.c index e39dd07..9cd45c3 100644 --- a/emul/emul.c +++ b/emul/emul.c @@ -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()); diff --git a/emul/emul.h b/emul/emul.h index 8b47400..3134804 100644 --- a/emul/emul.h +++ b/emul/emul.h @@ -21,5 +21,6 @@ typedef struct { Machine* emul_init(); bool emul_step(); +bool emul_steps(unsigned int steps); void emul_loop(); void emul_printdebug(); diff --git a/emul/hw/rc2014/classic.c b/emul/hw/rc2014/classic.c index e7ce493..1f58e49 100644 --- a/emul/hw/rc2014/classic.c +++ b/emul/hw/rc2014/classic.c @@ -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(); diff --git a/emul/hw/ti/.gitignore b/emul/hw/ti/.gitignore new file mode 100644 index 0000000..42e210f --- /dev/null +++ b/emul/hw/ti/.gitignore @@ -0,0 +1 @@ +/ti84 diff --git a/emul/hw/ti/Makefile b/emul/hw/ti/Makefile new file mode 100644 index 0000000..a5d3186 --- /dev/null +++ b/emul/hw/ti/Makefile @@ -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 $@ + diff --git a/emul/hw/ti/README.md b/emul/hw/ti/README.md new file mode 100644 index 0000000..930d655 --- /dev/null +++ b/emul/hw/ti/README.md @@ -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. diff --git a/emul/hw/ti/kbd.c b/emul/hw/ti/kbd.c new file mode 100644 index 0000000..54f1945 --- /dev/null +++ b/emul/hw/ti/kbd.c @@ -0,0 +1,101 @@ +#include +#include + +#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<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; + } +} + diff --git a/emul/hw/ti/kbd.h b/emul/hw/ti/kbd.h new file mode 100644 index 0000000..7c605b1 --- /dev/null +++ b/emul/hw/ti/kbd.h @@ -0,0 +1,25 @@ +#include +#include + +// 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); diff --git a/emul/hw/ti/t6a04.c b/emul/hw/ti/t6a04.c new file mode 100644 index 0000000..e93af99 --- /dev/null +++ b/emul/hw/ti/t6a04.c @@ -0,0 +1,141 @@ +#include +#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; +} diff --git a/emul/hw/ti/t6a04.h b/emul/hw/ti/t6a04.h new file mode 100644 index 0000000..8b0e027 --- /dev/null +++ b/emul/hw/ti/t6a04.h @@ -0,0 +1,41 @@ +#include +#include + +#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); diff --git a/emul/hw/ti/ti84.c b/emul/hw/ti/ti84.c new file mode 100644 index 0000000..79c817f --- /dev/null +++ b/emul/hw/ti/ti84.c @@ -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 +#include +#include + +#include + +#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; +} diff --git a/recipes/ti84/Makefile b/recipes/ti84/Makefile index cd4d28d..0bafac6 100644 --- a/recipes/ti84/Makefile +++ b/recipes/ti84/Makefile @@ -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