You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
572 lines
16 KiB
572 lines
16 KiB
|
|
|
|
#include "chip.h" |
|
#include <assert.h> |
|
#include <ctype.h> |
|
#include <stdbool.h> |
|
#include <stddef.h> |
|
#include <stdint.h> |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <sys/types.h> |
|
#include <time.h> |
|
#include <unistd.h> |
|
|
|
/* |
|
* |
|
* https://tobiasvl.github.io/blog/write-a-chip-8-emulator/ |
|
* |
|
* https://github.com/Chromatophore/HP48-Superchip/blob/master/binaries/SCHIP_origin.txt |
|
* |
|
*/ |
|
|
|
uint8_t inbuilt_font[] = { |
|
0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 |
|
0x20, 0x60, 0x20, 0x20, 0x70, // 1 |
|
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 |
|
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 |
|
0x90, 0x90, 0xF0, 0x10, 0x10, // 4 |
|
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 |
|
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 |
|
0xF0, 0x10, 0x20, 0x40, 0x40, // 7 |
|
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 |
|
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 |
|
0xF0, 0x90, 0xF0, 0x90, 0x90, // A |
|
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B |
|
0xF0, 0x80, 0x80, 0x80, 0xF0, // C |
|
0xE0, 0x90, 0x90, 0x90, 0xE0, // D |
|
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E |
|
0xF0, 0x80, 0xF0, 0x80, 0x80 // F |
|
}; |
|
|
|
int chip_errored = 0; |
|
|
|
uint16_t get_random() { return rand(); } |
|
|
|
uint8_t DECODE_ARG0(uint16_t instr) { return ((0x0f00 & instr) >> 8); } |
|
|
|
uint8_t DECODE_ARG1(uint16_t instr) { return ((0x00f0 & instr) >> 4); } |
|
|
|
uint8_t DECODE_ARG2(uint16_t instr) { return ((0x000f & instr)); } |
|
|
|
uint16_t DECODE_ARG16(uint16_t instr) { return (0x0fff & instr); } |
|
|
|
uint8_t DECODE_ARG8(uint16_t instr) { return (0x00ff & instr); } |
|
|
|
void exit_interpreter() { |
|
info("got exit instruction."); |
|
exit(0); |
|
} |
|
|
|
void print_display(struct display *display); |
|
void print_chip_state(emu_state *state, uint16_t instr) { |
|
printf("program counter: %x instr %x\n", state->value->program_counter, |
|
instr); |
|
printf("pointer: %x\n", state->value->pointer_register); |
|
printf("registers: "); |
|
|
|
hexdump(state->value->registers, 16); |
|
printf("sound timer: %d, delay_timer: %d\n", state->value->sound_timer, |
|
state->value->delay_timer); |
|
printf("extended mode: %d\n", state->value->extended_display_mode); |
|
} |
|
|
|
void clear_display(emu_state *state) { |
|
memset(state->display.buffer, 0, sizeof(state->display_buffer)); |
|
} |
|
|
|
void scroll_vertical(emu_state *state, int8_t lines) { |
|
int row = DISPLAY_HEIGHT_BYTES * (state->value->extended_display_mode); |
|
int offset = row * (0x7f & lines); |
|
if (lines > 0) { |
|
bcopy(state->display.buffer, state->display.buffer + offset, |
|
sizeof(state->display) - offset); |
|
memset(state->display.buffer, 0, offset); |
|
} else { |
|
bcopy(state->display.buffer + offset, state->display.buffer, |
|
sizeof(state->display) - offset); |
|
memset(state->display.buffer, 0, offset); |
|
} |
|
} |
|
|
|
void scroll_horizontal(emu_state *state, int8_t direction) { |
|
int left = direction & 80; |
|
int width = DISPLAY_HEIGHT_BYTES * state->value->extended_display_mode; |
|
|
|
for (int i = 0; i < DISPLAY_HEIGHT_BITS; i++) { |
|
uint8_t last_saved = 0; |
|
for (int j = 0; j < width; j++) { |
|
uint8_t cell = state->display.buffer[i * width + j]; |
|
uint8_t saved = left ? cell & 0xff00 : cell & 0x00ff; |
|
cell = left ? cell << 4 : cell >> 4; |
|
state->display.buffer[i * width + j] = last_saved | cell; |
|
last_saved = left ? saved << 4 : saved >> 4; |
|
} |
|
} |
|
} |
|
|
|
void draw_16_sprite(emu_state *state, uint8_t x, uint8_t y) { |
|
uint16_t sprite_loc = state->value->pointer_register; |
|
|
|
int row_bytes = DISPLAY_WIDTH_BYTES; |
|
int screen_y = row_bytes * y; |
|
|
|
if (x % 8 != 0) { |
|
warn("Not supported."); |
|
} |
|
|
|
uint8_t old[4]; |
|
uint8_t new[4]; |
|
memcpy(old, &state->display.buffer[screen_y + (x / 8)], 4); |
|
|
|
state->display.buffer[screen_y + (x / 8)] ^= |
|
state->memory.all_memory[sprite_loc]; |
|
state->display.buffer[screen_y + (x / 8) + 1] ^= |
|
state->memory.all_memory[sprite_loc + 1]; |
|
|
|
screen_y = row_bytes * (y + 1); |
|
|
|
state->display.buffer[screen_y + (x / 8)] ^= |
|
state->memory.all_memory[sprite_loc + 2]; |
|
state->display.buffer[screen_y + (x / 8) + 1] ^= |
|
state->memory.all_memory[sprite_loc + 3]; |
|
|
|
memcpy(new, &state->display.buffer[screen_y + (x / 8)], 4); |
|
for (int i = 0; i < 4; i++) { |
|
if (old[i] ^ new[i]) { |
|
state->value->registers[0xf] = 1; |
|
} |
|
} |
|
// printf("%d\n", sprite_loc); |
|
hexdump(&state->memory.all_memory[sprite_loc], 4); |
|
// hexdump(state->display, sizeof(state->display)); |
|
} |
|
|
|
void decode_draw(emu_state *state, uint16_t instr) { |
|
// DXYN* Show N-byte sprite from M(I) at coords (VX,VY), VF := |
|
// collision. If N=0 and extended mode, show 16x16 sprite. |
|
uint8_t X = state->value->registers[DECODE_ARG0(instr)]; |
|
uint8_t Y = state->value->registers[DECODE_ARG1(instr)]; |
|
uint8_t N = DECODE_ARG2(instr); |
|
|
|
if (N == 0 && (state->value->extended_display_mode == 2)) { |
|
draw_16_sprite(state, X, Y); |
|
return; |
|
} |
|
|
|
uint16_t sprite_loc = state->value->pointer_register; |
|
int row_bytes = DISPLAY_WIDTH_BYTES; |
|
|
|
X = X % state->display.width; |
|
Y = Y % state->display.height; |
|
|
|
int xoffset = X % 8; |
|
|
|
if (N == 0) { |
|
N = 8; |
|
} |
|
|
|
uint8_t changed = 0; |
|
for (int i = 0; i < N; i++) { |
|
|
|
uint8_t ssprite = state->memory.all_memory[sprite_loc + i]; |
|
uint8_t new_l = |
|
state->display.buffer[row_bytes * Y + (X / 8)] ^ (ssprite >> xoffset); |
|
changed |= state->display.buffer[row_bytes * Y + (X / 8)] & new_l; |
|
|
|
uint8_t new_r = state->display.buffer[row_bytes * Y + (X / 8) + 1] ^ |
|
(ssprite << (8 - xoffset)); |
|
changed |= state->display.buffer[row_bytes * Y + (X / 8) + 1] & new_r; |
|
|
|
state->display.buffer[row_bytes * Y + (X / 8)] = new_l; |
|
state->display.buffer[row_bytes * Y + (X / 8) + 1] = new_r; |
|
|
|
Y += 1; |
|
} |
|
if (changed) { |
|
state->value->registers[0xf] = 1; |
|
} else { |
|
state->value->registers[0xf] = 0; |
|
} |
|
print_display(&state->display); |
|
} |
|
|
|
void decode_special(emu_state *state, uint16_t instr) { |
|
|
|
if (DECODE_ARG0(instr) == 0xc) { |
|
// 00CN* Scroll display N lines down |
|
uint8_t lines = DECODE_ARG2(instr); |
|
scroll_vertical(state, lines); |
|
return; |
|
} |
|
|
|
switch (instr) { |
|
case 0x00e0: |
|
// 00E0 Clear display |
|
clear_display(state); |
|
break; |
|
case 0x00ee: |
|
// 00EE Return from subroutine |
|
state->value->stack_top--; |
|
state->value->program_counter = |
|
state->value->stack[state->value->stack_top]; |
|
break; |
|
case 0x00fb: |
|
// 00FB* Scroll display 4 pixels right |
|
scroll_horizontal(state, 1); |
|
break; |
|
case 0x00fc: |
|
// 00FC* Scroll display 4 pixels left |
|
scroll_horizontal(state, -1); |
|
break; |
|
case 0x00fd: |
|
// 00FD* Exit CHIP interpreter |
|
exit_interpreter(); |
|
break; |
|
case 0x00fe: |
|
// 00FE* Disable extended screen mode |
|
state->value->extended_display_mode = 1; |
|
break; |
|
case 0x00ff: |
|
// 00FF* Enable extended screen mode for full-screen graphics |
|
state->value->extended_display_mode = 2; |
|
break; |
|
default: |
|
printf("%x", instr); |
|
error("Illegal instruction."); |
|
break; |
|
} |
|
} |
|
|
|
void decode_arith(emu_state *state, uint16_t instr) { |
|
assert((0xf000 & instr) == 0x8000); // invalid decode |
|
|
|
uint16_t temp = 0; |
|
uint8_t X = DECODE_ARG0(instr); |
|
uint8_t Y = DECODE_ARG1(instr); |
|
switch (instr & 0xf) { |
|
case 0: |
|
// 8XY0 VX := VY, VF may change |
|
state->value->registers[X] = state->value->registers[Y]; |
|
break; |
|
case 1: |
|
// 8XY1 VX := VX or VY, VF may change |
|
state->value->registers[X] = |
|
state->value->registers[X] | state->value->registers[Y]; |
|
break; |
|
case 2: |
|
// 8XY2 VX := VX and VY, VF may change |
|
state->value->registers[X] = |
|
state->value->registers[X] & state->value->registers[Y]; |
|
break; |
|
case 3: |
|
// 8XY3 VX := VX xor VY, VF may change |
|
state->value->registers[X] = |
|
state->value->registers[X] ^ state->value->registers[Y]; |
|
break; |
|
case 4: |
|
// 8XY4 VX := VX + VY, VF := carry |
|
temp = state->value->registers[X] + state->value->registers[Y]; |
|
if (temp > 255) |
|
state->value->registers[0xf] = 1; |
|
state->value->registers[X] = temp; |
|
break; |
|
case 5: |
|
// 8XY5 VX := VX - VY, VF := not borrow |
|
temp = state->value->registers[X] - state->value->registers[Y]; |
|
state->value->registers[X] = temp; |
|
if (X > Y) { |
|
state->value->registers[0xf] = 1; |
|
} else { |
|
state->value->registers[0xf] = 0; |
|
} |
|
break; |
|
case 7: |
|
// 8XY7 VX := VY - VX, VF := not borrow |
|
temp = state->value->registers[Y] - state->value->registers[X]; |
|
state->value->registers[Y] = temp; |
|
if (Y > X) { |
|
state->value->registers[0xf] = 1; |
|
} else { |
|
state->value->registers[0xf] = 0; |
|
} |
|
break; |
|
case 6: |
|
// 8XY6 VX := VX shr 1, VF := carry |
|
// ambiguous should allow set VX to value of VY |
|
state->value->registers[0xf] = (0x80 & state->value->registers[X]) >> 7; |
|
state->value->registers[X] = state->value->registers[X] >> 1; |
|
break; |
|
case 0xE: |
|
// 8XYE VX := VX shl 1, VF := carry |
|
// ambiguous should allow set VX to value of VY |
|
state->value->registers[0xf] = (0x1 & state->value->registers[X]); |
|
state->value->registers[X] = state->value->registers[X] << 1; |
|
break; |
|
default: |
|
error("Illegal instruction."); |
|
break; |
|
} |
|
} |
|
|
|
void decode_store(emu_state *state, uint16_t instr) { |
|
assert((0xf000 & instr) == 0xf000); // invalid decode |
|
|
|
uint8_t X = DECODE_ARG0(instr); |
|
assert(X < 16); |
|
|
|
state->value->registers[1] = 200; |
|
uint16_t temp = 0; |
|
|
|
switch (instr & 0x00ff) { |
|
case 0x07: |
|
// FX07 VX := delay_timer |
|
state->value->registers[X] = state->value->delay_timer; |
|
break; |
|
case 0x0A: |
|
// FX0A wait for keypress, store hex value of key in VX |
|
warn("Not implemented"); |
|
break; |
|
case 0x15: |
|
// FX15 delay_timer := VX |
|
state->value->delay_timer = state->value->registers[X]; |
|
break; |
|
case 0x18: |
|
// FX18 sound_timer := VX |
|
state->value->sound_timer = state->value->registers[X]; |
|
break; |
|
case 0x1e: |
|
// FX1E I := I + VX |
|
temp = state->value->pointer_register + state->value->registers[X]; |
|
if (temp > 255) { |
|
state->value->registers[0xf] = 1; |
|
} else { |
|
state->value->registers[0xf] = 0; |
|
} |
|
state->value->pointer_register = temp; |
|
break; |
|
case 0x29: |
|
// FX29 Point I to 5-byte font sprite for hex character VX |
|
state->value->pointer_register = |
|
offsetof(struct emu_value, font) + 5 * state->value->registers[X]; |
|
break; |
|
case 0x30: |
|
// FX30* Point I to 10-byte font sprite for digit VX (0..9) |
|
warn("Not implemented."); |
|
break; |
|
case 0x33: |
|
// FX33 Store BCD representation of VX in M(I)..M(I+2) |
|
warn("Not implemented."); |
|
break; |
|
case 0x55: |
|
// FX55 Store V0..VX in memory starting at M(I) |
|
warn("Not implemented."); |
|
break; |
|
case 0x65: |
|
// FX65 Read V0..VX from memory starting at M(I) |
|
for (int i = 0; i < X; i++) { |
|
state->value->registers[i] = |
|
state->memory.all_memory[state->value->pointer_register + i]; |
|
} |
|
// warn("Not implemented."); |
|
break; |
|
case 0x75: |
|
// FX75* Store V0..VX in RPL user flags (X <= 7) |
|
warn("Not implemented."); |
|
break; |
|
case 0x85: |
|
// FX85* Read V0..VX from RPL user flags (X <= 7) |
|
warn("Not implemented."); |
|
break; |
|
default: |
|
error("Illegal instruction"); |
|
break; |
|
} |
|
} |
|
|
|
void decode_instr(emu_state *state, uint16_t instr) { |
|
uint16_t temp = 0; |
|
switch ((instr & 0xf000) >> 0xc) { |
|
case 0: |
|
decode_special(state, instr); |
|
break; |
|
case 1: |
|
// 1NNN Jump to NNN |
|
state->value->program_counter = DECODE_ARG16(instr); |
|
break; |
|
case 2: |
|
// 2NNN Call subroutine at NNN |
|
state->value->stack[state->value->stack_top++] = |
|
state->value->program_counter; |
|
// TODO: stack protection |
|
assert(state->value->stack_top < |
|
(sizeof(state->value->stack) / sizeof(uint16_t))); |
|
state->value->program_counter = DECODE_ARG16(instr); |
|
break; |
|
case 3: |
|
// 3XKK Skip next instruction if VX == KK |
|
if (DECODE_ARG8(instr) == state->value->registers[DECODE_ARG0(instr)]) { |
|
state->value->program_counter += 2; |
|
} |
|
break; |
|
case 4: |
|
// 4XKK Skip next instruction if VX <> KK |
|
if (DECODE_ARG8(instr) != state->value->registers[DECODE_ARG0(instr)]) { |
|
state->value->program_counter += 2; |
|
} |
|
break; |
|
case 5: |
|
// 5XY0 Skip next instruction if VX == VY |
|
if (DECODE_ARG0(instr) == DECODE_ARG1(instr)) { |
|
state->value->program_counter += 2; |
|
} |
|
break; |
|
case 9: |
|
// 9XY0 Skip next instruction if VX <> VY |
|
if (DECODE_ARG0(instr) != DECODE_ARG1(instr)) { |
|
state->value->program_counter += 2; |
|
} |
|
break; |
|
case 6: |
|
// 6XKK VX := KK |
|
state->value->registers[DECODE_ARG0(instr)] = DECODE_ARG8(instr); |
|
break; |
|
case 7: |
|
// 7XKK VX := VX + KK |
|
temp = state->value->registers[DECODE_ARG0(instr)] + DECODE_ARG8(instr); |
|
|
|
if (temp > 255) { |
|
state->value->registers[0xf] = 1; |
|
} else { |
|
state->value->registers[0xf] = 0; |
|
} |
|
state->value->registers[DECODE_ARG0(instr)] = temp; |
|
break; |
|
case 8: |
|
// 8XY_ |
|
decode_arith(state, instr); |
|
break; |
|
case 0xa: |
|
// I := NNN |
|
state->value->pointer_register = DECODE_ARG16(instr); |
|
break; |
|
case 0xb: |
|
// BNNN Jump to NNN+V0 |
|
state->value->program_counter = |
|
DECODE_ARG16(instr) + state->value->registers[0]; |
|
|
|
// state->value->program_counter = DECODE_ARG8(instr) |
|
// + state->value->registers[DECODE_ARG0(instr)]; |
|
|
|
break; |
|
case 0xc: |
|
// CXKK VX := pseudorandom_number and KK |
|
state->value->registers[DECODE_ARG0(instr)] = |
|
get_random() & DECODE_ARG8(instr); |
|
break; |
|
case 0xd: |
|
// DXYN* Show N-byte sprite from M(I) at coords (VX,VY), VF := |
|
// collision. |
|
// If N = 0 show 8xN sprite |
|
// If N=0 and extended mode, show 16x16 sprite. |
|
decode_draw(state, instr); |
|
break; |
|
case 0xe: |
|
// EX9E Skip next instruction if key VX pressed |
|
// EXA1 Skip next instruction if key VX not pressed |
|
// skip |
|
warn("Not implemented."); |
|
break; |
|
case 0xf: |
|
// FX__ |
|
decode_store(state, instr); |
|
break; |
|
default: |
|
error("Illegal instruction."); |
|
break; |
|
} |
|
} |
|
|
|
void initialize(emu_state *state) { |
|
|
|
assert(sizeof(state->memory.values) <= sizeof(state->memory.reserved_memory)); |
|
|
|
state->value = &state->memory.values; |
|
state->value->delay_timer = 0; |
|
state->value->sound_timer = 0; |
|
state->value->extended_display_mode = 1; |
|
state->value->stack_top = 0; |
|
state->value->program_counter = 0x200; |
|
|
|
state->display.width = DISPLAY_WIDTH_BITS; |
|
state->display.height = DISPLAY_HEIGHT_BITS; |
|
state->display.buffer = state->display_buffer; |
|
|
|
memcpy(state->value->font, inbuilt_font, sizeof(inbuilt_font)); |
|
clear_display(state); |
|
} |
|
|
|
void load_program(emu_state *state, char *program, size_t program_len) { |
|
memcpy(state->memory.main_memory, program, program_len); |
|
} |
|
|
|
void print_display(struct display *display) { |
|
|
|
const int width_bytes = display->width / 8; |
|
const int height = display->height; |
|
|
|
for (int i = 0; i < height; i++) { |
|
for (int j = 0; j < width_bytes; j++) { |
|
for (int b = 7; b >= 0; b--) { |
|
if (1 & (display->buffer[(width_bytes * i) + j] >> b)) { |
|
putc('8', stdout); |
|
} else { |
|
putc(' ', stdout); |
|
} |
|
} |
|
} |
|
putc('\n', stdout); |
|
} |
|
} |
|
|
|
uint16_t fetch(emu_state *state) { |
|
uint16_t res = state->memory.all_memory[state->value->program_counter] << 8; |
|
res |= state->memory.all_memory[state->value->program_counter + 1]; |
|
state->value->program_counter += 2; |
|
return res; |
|
} |
|
|
|
void loop(emu_state *state) { |
|
|
|
state->value->delay_timer--; |
|
state->value->sound_timer--; |
|
|
|
uint16_t instr = fetch(state); |
|
#ifdef DEBUG |
|
print_chip_state(state, instr); |
|
putc('\n', stdout); |
|
#endif |
|
decode_instr(state, instr); |
|
} |
|
|
|
/* debugging */ |
|
void hexdump(const void *d, size_t datalen) { |
|
|
|
const uint8_t *data = d; |
|
size_t i, j = 0; |
|
|
|
for (i = 0; i < datalen; i += j) { |
|
printf("%4zx: ", i); |
|
for (j = 0; j < 16 && i + j < datalen; j++) |
|
printf("%02x ", data[i + j]); |
|
while (j++ < 16) |
|
printf(" "); |
|
printf("|"); |
|
for (j = 0; j < 16 && i + j < datalen; j++) |
|
putchar(isprint(data[i + j]) ? data[i + j] : '.'); |
|
printf("|\n"); |
|
} |
|
} |
|
|
|
void initialize(emu_state *state);
|
|
|