├── .gitignore ├── emu.h ├── debug.h ├── opcode_handlers ├── stack.h ├── interrupts.h ├── transfer.h ├── flags.h ├── jump.h ├── branch.h ├── compare.h ├── incdec.h ├── arithmetic.h ├── store.h ├── load.h ├── logical.h └── shift.h ├── cpu.c ├── sample_programs ├── alphabet.s ├── colors.s ├── diskread │ ├── gendisk.c │ ├── diskread.s │ └── testdisk.bin ├── stdlib_test.s ├── echo.s ├── spam.s ├── README └── minigame.s ├── Makefile ├── generate_debug_names.py ├── cpu.h ├── io.h ├── LICENSE ├── stdlib └── stdio.s ├── debug.c ├── main.c ├── emu.c ├── functions.h ├── io.c ├── opcodes.h └── README /.gitignore: -------------------------------------------------------------------------------- 1 | testasm 2 | x6502 3 | debug-names.h 4 | a.o65 5 | sample_programs/diskread/gd 6 | -------------------------------------------------------------------------------- /emu.h: -------------------------------------------------------------------------------- 1 | #ifndef __6502_EMU__ 2 | #define __6502_EMU__ 3 | 4 | #include "cpu.h" 5 | 6 | void main_loop(cpu *m); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /debug.h: -------------------------------------------------------------------------------- 1 | #ifndef __6502_DEBUG__ 2 | #define __6502_DEBUG__ 3 | 4 | #include "cpu.h" 5 | 6 | #ifdef DEBUG 7 | #define DUMP_DEBUG(cpu) (dump_cpu(cpu)) 8 | #else 9 | #define DUMP_DEBUG(cpu) 10 | #endif 11 | 12 | void dump_cpu(cpu *m); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /opcode_handlers/stack.h: -------------------------------------------------------------------------------- 1 | case PHA: 2 | STACK_PUSH(m) = m->ac; 3 | break; 4 | 5 | case PHP: 6 | STACK_PUSH(m) = m->sr; 7 | break; 8 | 9 | case PLA: 10 | m->ac = STACK_POP(m); 11 | break; 12 | 13 | case PLP: 14 | m->sr = STACK_POP(m); 15 | break; 16 | -------------------------------------------------------------------------------- /cpu.c: -------------------------------------------------------------------------------- 1 | #include "cpu.h" 2 | #include 3 | #include 4 | 5 | cpu * new_cpu(uint16_t pc_start) { 6 | cpu *m = malloc(sizeof(cpu)); 7 | m->pc = pc_start; 8 | m->sr = FLAG_INTERRUPT; 9 | m->sp = 0xFF; 10 | m->interrupt_waiting = 0x00; 11 | memset(m->mem, 0xFF, MEMORY_SIZE); 12 | return m; 13 | } 14 | -------------------------------------------------------------------------------- /sample_programs/alphabet.s: -------------------------------------------------------------------------------- 1 | lda #$02 ; set WAIT_TERMINATE flag 2 | sta $FF02 3 | lda #$41 ; 41 == 'A' 4 | sta $FF00 5 | lda #$5A ; 5A == 'Z' 6 | loop inc $FF00 7 | cmp $FF00 8 | bne loop 9 | lda #$0A ; 0A == '\n' 10 | sta $FF00 11 | -------------------------------------------------------------------------------- /opcode_handlers/interrupts.h: -------------------------------------------------------------------------------- 1 | case BRK: 2 | set_flag(m, FLAG_BREAK, 1); 3 | m->interrupt_waiting = 1; 4 | break; 5 | 6 | case RTI: 7 | m->sr = STACK_POP(m); 8 | arg1 = STACK_POP(m); 9 | m->pc = mem_abs(arg1, STACK_POP(m), 0); 10 | break; 11 | 12 | case WAI: 13 | m->emu_flags |= EMU_FLAG_WAIT_FOR_INTERRUPT; 14 | break; 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=clang 2 | COPTS=-o x6502 -O3 -lpthread -Wall -lncurses 3 | DEBUGOPTS=-DDEBUG -O0 -g 4 | PYTHON=python 5 | 6 | all: release 7 | 8 | debug-names.h: generate_debug_names.py opcodes.h 9 | $(PYTHON) generate_debug_names.py > debug-names.h 10 | 11 | release: debug-names.h *.c *.h 12 | $(CC) $(COPTS) *.c 13 | 14 | debug: debug-names.h *.c *.h 15 | $(CC) $(COPTS) $(DEBUGOPTS) *.c 16 | -------------------------------------------------------------------------------- /sample_programs/colors.s: -------------------------------------------------------------------------------- 1 | lda #$01 2 | sta $FF02 3 | 4 | lda #$00 5 | sta $FEE8 6 | 7 | lda #$41 8 | ldx #$00 9 | ldy #$08 10 | 11 | loop 12 | sta $FB00,X 13 | inx 14 | sta $FB00,X 15 | inx 16 | sta $FB00,X 17 | inx 18 | 19 | inc $FEE8 20 | cpy $FEE8 21 | bne loop 22 | 23 | wai 24 | -------------------------------------------------------------------------------- /sample_programs/diskread/gendisk.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() { 6 | FILE *f = fopen("testdisk.bin", "w"); 7 | if (f == NULL) { 8 | printf("could not open file, err: %s", strerror(errno)); 9 | return 1; 10 | } 11 | fprintf(f, "hello, world, from disk!\n"); 12 | fputc(0, f); 13 | fseek(f, 0xFF00, SEEK_SET); 14 | fprintf(f, "hello from a seeked position.\n"); 15 | fclose(f); 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /opcode_handlers/transfer.h: -------------------------------------------------------------------------------- 1 | case TAX: 2 | m->x = m->ac; 3 | set_flags(m, m->x); 4 | break; 5 | 6 | case TAY: 7 | m->y = m->ac; 8 | set_flags(m, m->y); 9 | break; 10 | 11 | case TSX: 12 | m->x = m->sp; 13 | set_flags(m, m->x); 14 | break; 15 | 16 | case TXA: 17 | m->ac = m->x; 18 | set_flags(m, m->ac); 19 | break; 20 | 21 | case TXS: 22 | m->sp = m->x; 23 | break; 24 | 25 | case TYA: 26 | m->ac = m->y; 27 | set_flags(m, m->ac); 28 | break; 29 | -------------------------------------------------------------------------------- /opcode_handlers/flags.h: -------------------------------------------------------------------------------- 1 | case CLC: 2 | set_flag(m, FLAG_CARRY, 0); 3 | break; 4 | 5 | case CLD: 6 | set_flag(m, FLAG_DECIMAL, 0); 7 | break; 8 | 9 | case CLI: 10 | set_flag(m, FLAG_INTERRUPT, 0); 11 | break; 12 | 13 | case CLV: 14 | set_flag(m, FLAG_OVERFLOW, 0); 15 | break; 16 | 17 | case SEC: 18 | set_flag(m, FLAG_CARRY, 1); 19 | break; 20 | 21 | case SED: 22 | set_flag(m, FLAG_DECIMAL, 1); 23 | break; 24 | 25 | case SEI: 26 | set_flag(m, FLAG_INTERRUPT, 1); 27 | break; 28 | -------------------------------------------------------------------------------- /sample_programs/diskread/diskread.s: -------------------------------------------------------------------------------- 1 | cli 2 | lda #$02 ; set WAIT_TERMINATE flag 3 | sta $FF02 4 | lda #$03 5 | sta $FF03 6 | lda #$00 7 | sta $FF04 8 | lda #$00 9 | cmp $FF07 10 | beq printchar 11 | lda #'e' 12 | sta $FF00 13 | lda #'r' 14 | sta $FF00 15 | sta $FF00 16 | jmp done 17 | printchar: 18 | lda $FF05 19 | sta $FF00 20 | done: 21 | -------------------------------------------------------------------------------- /sample_programs/stdlib_test.s: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | lda #$02 ; set WAIT_TERMINATE flag 4 | sta iomode 5 | lda #str 8 | sta $F1 9 | jsr print 10 | 11 | lda #$00 12 | sta blck0_addrl 13 | sta blck0_addrh 14 | jsr fread 15 | jsr print 16 | 17 | lda #$00 18 | sta blck0_addrl 19 | lda #$FF 20 | sta blck0_addrh 21 | jsr fread 22 | jsr print 23 | 24 | ext 25 | 26 | str: .asc "hello, world, from .data!", $0A, $00 27 | -------------------------------------------------------------------------------- /opcode_handlers/jump.h: -------------------------------------------------------------------------------- 1 | case JMP_AB: 2 | arg1 = NEXT_BYTE(m); 3 | m->pc = mem_abs(arg1, NEXT_BYTE(m), 0); 4 | break; 5 | 6 | case JMP_IN: 7 | arg1 = NEXT_BYTE(m); 8 | r1 = mem_abs(arg1, NEXT_BYTE(m), 0); 9 | m->pc = mem_abs(m->mem[r1], m->mem[r1+1], 0); 10 | break; 11 | 12 | case JSR_AB: 13 | arg1 = NEXT_BYTE(m); 14 | r1 = mem_abs(arg1, NEXT_BYTE(m), 0); 15 | // we push the address of the byte immediately before where we want to 16 | // return to because reasons: 17 | r2 = m->pc + pc_offset - 1; 18 | STACK_PUSH(m) = (r2 & 0xFF00) >> 8; 19 | STACK_PUSH(m) = r2 & 0xFF; 20 | m->pc = r1; 21 | break; 22 | 23 | case RTS: 24 | arg1 = STACK_POP(m); 25 | m->pc = mem_abs(arg1, STACK_POP(m), 0) + 1; 26 | break; 27 | -------------------------------------------------------------------------------- /generate_debug_names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | def is_opcode(line): 4 | return line.startswith('#define') and 'OPCODE' not in line 5 | 6 | opcode_map = {} 7 | with open('opcodes.h') as f: 8 | for line in f: 9 | if is_opcode(line): 10 | _, op, code = line.strip().split() 11 | opcode_map[code] = op 12 | 13 | print ''' 14 | // AUTOGENERATED BY generate_debug_names.py, DO NOT MODIFY 15 | 16 | #ifndef __6502_DEBUG_NAMES__ 17 | #define __6502_DEBUG_NAMES__ 18 | 19 | #include 20 | 21 | char *inst_names[0x100]; 22 | char names_initialized = 0; 23 | 24 | void init_names() { 25 | if (names_initialized) { 26 | return; 27 | } 28 | names_initialized = 1; 29 | 30 | for (int i = 0; i < 0xFF; i++) { 31 | inst_names[i] = "unknown"; 32 | } 33 | %s 34 | } 35 | 36 | #endif 37 | ''' % '\n'.join(' inst_names[%s] = "%s";' % (code, op) 38 | for code, op in opcode_map.items()) 39 | -------------------------------------------------------------------------------- /sample_programs/echo.s: -------------------------------------------------------------------------------- 1 | ; echos everything to the terminal, transforming lowercase into uppercase 2 | 3 | #include 4 | 5 | ; load location of ISR and enable interrupts 6 | lda #isr 9 | sta $FFFF 10 | cli 11 | 12 | ; wait for input 13 | loop wai 14 | jmp loop 15 | 16 | ; start ISR 17 | isr pha 18 | 19 | ; check if char is less than 'a' 20 | cheka ldx #$60 ; == 'a' - 1 21 | cpx getc 22 | bcs noup ; if val is < a, don't transform it 23 | 24 | ; check if char is greater than 'z' 25 | chekz ldx #$7B ; == 'z' + 1 26 | cpx getc 27 | bcc noup ; if val is >= z + 1, don't transform it 28 | 29 | ; lower -> upper 30 | lda #$DF ; 'A' - 'a' 31 | adc getc 32 | jmp done 33 | 34 | ; print and return 35 | noup lda getc 36 | done sta putc 37 | pla 38 | rti 39 | -------------------------------------------------------------------------------- /sample_programs/spam.s: -------------------------------------------------------------------------------- 1 | counter = $0100 2 | supercounter = $0101 3 | page = $FF 4 | char = $FD 5 | 6 | lda #$01 ; go into vterm mode 7 | sta $FF02 8 | 9 | lda #$FF 10 | sta counter ; init counter 11 | 12 | lda #$FB 13 | sta page 14 | lda #$00 15 | sta page-1 16 | 17 | lda #$41 ; 41 == 'A' 18 | sta char 19 | 20 | ldy #$00 21 | 22 | loop: 23 | lda char 24 | sta (page-1),Y 25 | jsr incchar 26 | 27 | iny 28 | cpy #$00 29 | bne loop 30 | 31 | inc page 32 | lda page 33 | cmp #$FF 34 | bne loop 35 | 36 | wai 37 | .byt $FF 38 | 39 | incchar: 40 | inc char 41 | lda char 42 | cmp #$5B 43 | bne chardone 44 | lda #$41 45 | chardone: 46 | sta char 47 | rts 48 | -------------------------------------------------------------------------------- /opcode_handlers/branch.h: -------------------------------------------------------------------------------- 1 | case BCC_REL: 2 | s1 = NEXT_BYTE(m); 3 | if (!get_flag(m, FLAG_CARRY)) { 4 | branch_offset = s1; 5 | } 6 | break; 7 | 8 | case BCS_REL: 9 | s1 = NEXT_BYTE(m); 10 | if (get_flag(m, FLAG_CARRY)) { 11 | branch_offset = s1; 12 | } 13 | break; 14 | 15 | case BEQ_REL: 16 | s1 = NEXT_BYTE(m); 17 | if (get_flag(m, FLAG_ZERO)) { 18 | branch_offset = s1; 19 | } 20 | break; 21 | 22 | case BMI_REL: 23 | s1 = NEXT_BYTE(m); 24 | if (get_flag(m, FLAG_NEGATIVE)) { 25 | branch_offset = s1; 26 | } 27 | break; 28 | 29 | case BNE_REL: 30 | s1 = NEXT_BYTE(m); 31 | if (!get_flag(m, FLAG_ZERO)) { 32 | branch_offset = s1; 33 | } 34 | break; 35 | 36 | case BPL_REL: 37 | s1 = NEXT_BYTE(m); 38 | if (!get_flag(m, FLAG_NEGATIVE)) { 39 | branch_offset = s1; 40 | } 41 | break; 42 | 43 | case BVC_REL: 44 | s1 = NEXT_BYTE(m); 45 | if (!get_flag(m, FLAG_OVERFLOW)) { 46 | branch_offset = s1; 47 | } 48 | break; 49 | 50 | case BVS_REL: 51 | s1 = NEXT_BYTE(m); 52 | if (get_flag(m, FLAG_OVERFLOW)) { 53 | branch_offset = s1; 54 | } 55 | break; 56 | -------------------------------------------------------------------------------- /sample_programs/README: -------------------------------------------------------------------------------- 1 | Test 6502 assembler programs. Generate binaries with: 2 | 3 | xa -w -Istdlib/ sample_programs/test.s 4 | 5 | xa is a cross-assembler and utility suite for 65xx series processors. It's 6 | included in many package repositories as `xa65', including Homebrew on OSX in 7 | the Debian repositories. The source is available at 8 | http://www.floodgap.com/retrotech/xa/ 9 | 10 | Sample programs: 11 | 12 | alphabet.s Prints A through Z to the terminal. Demonstrates basic 13 | terminal output. 14 | colors.s Prints a bunch of A's in different colors using vterm 15 | mode. Demonstrates paints and vterm output. 16 | echo.s Echos everything read from stdin to stdout, transforming 17 | letters into uppercase. Demonstrates terminal input. 18 | minigame.s Move a character around a vterm with hjkl. Demonstrates 19 | a more complex vterm application. 20 | spam.s Writes A through Z repeatedly, filling the vterm. 21 | Demonstrates vterm output and how to do indirect 22 | addressing to address the whole vterm. 23 | stdlib_test.s Demo program displaying the functionality of the 24 | standard library. 25 | -------------------------------------------------------------------------------- /cpu.h: -------------------------------------------------------------------------------- 1 | #ifndef __6502_CPU__ 2 | #define __6502_CPU__ 3 | 4 | #include 5 | 6 | #define MEMORY_SIZE 65536 7 | #define STACK_START 0x0100 8 | 9 | #define FLAG_NEGATIVE 0x80 10 | #define FLAG_OVERFLOW 0x40 11 | #define FLAG_BREAK 0x10 12 | #define FLAG_DECIMAL 0x08 13 | #define FLAG_INTERRUPT 0x04 14 | #define FLAG_ZERO 0x02 15 | #define FLAG_CARRY 0x01 16 | 17 | // set if memory was modified during processing of the last instruction 18 | #define EMU_FLAG_DIRTY 0x01 19 | // set if the emulator should wait for an interrupt before continuing 20 | #define EMU_FLAG_WAIT_FOR_INTERRUPT 0x02 21 | 22 | typedef struct { 23 | // program counter 24 | uint16_t pc; 25 | // index registers 26 | uint8_t x, y; 27 | // stack pointer 28 | uint8_t sp; 29 | // accumulator 30 | uint8_t ac; 31 | // status register 32 | uint8_t sr; 33 | // emulator flag register (not in 6502 spec, not accessible from assembler) 34 | uint8_t emu_flags; 35 | // set to nonzero if there is an outstanding interrupt 36 | uint8_t interrupt_waiting; 37 | // RAM 38 | uint8_t mem[MEMORY_SIZE]; 39 | // stores the address of memory modified by the last instruction 40 | uint16_t dirty_mem_addr; 41 | // the opcode of the last instruction run. for debugging only. 42 | uint8_t last_opcode; 43 | } cpu; 44 | 45 | cpu * new_cpu(uint16_t pc_start); 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /io.h: -------------------------------------------------------------------------------- 1 | #ifndef __6502_IO__ 2 | 3 | #include 4 | 5 | #include "cpu.h" 6 | 7 | #define IO_PAINT 0xFEE8 8 | #define IO_PUTCHAR 0xFF00 9 | #define IO_GETCHAR 0xFF01 10 | #define IO_MODEFLAGS 0xFF02 11 | 12 | // writing to ADDRL, ADDRH or READ will trigger an interrupt that will read from 13 | // the disk address specified by ADDRL and ADDRH and write the result into READ. 14 | // if errors are encountered, ERR will be nonzero. 15 | #define IO_BLCK0_ADDRL 0xFF03 16 | #define IO_BLCK0_ADDRH 0xFF04 17 | #define IO_BLCK0_READ 0xFF05 18 | #define IO_BLCK0_WRITE 0xFF06 19 | #define IO_BLCK0_ERR 0xFF07 20 | 21 | #define IO_BLCK_ERR_EOF 0x01 22 | #define IO_BLCK_ERR_SEEK 0x02 23 | 24 | #define IO_VTERM_START 0xFB00 25 | #define IO_VTERM_END 0xFF00 26 | 27 | #define IO_MODEFLAG_VTERM 0x01 28 | #define IO_MODEFLAG_WAIT_HALT 0x02 29 | 30 | #define IO_PAINT_BLACK 0x00 31 | #define IO_PAINT_RED 0x01 32 | #define IO_PAINT_GREEN 0x02 33 | #define IO_PAINT_YELLOW 0x03 34 | #define IO_PAINT_BLUE 0x04 35 | #define IO_PAINT_MAGENTA 0x05 36 | #define IO_PAINT_CYAN 0x06 37 | #define IO_PAINT_WHITE 0x07 38 | 39 | // bitwise-OR these with one of the colors to modify them 40 | #define IO_PAINT_DIM 0x20 41 | #define IO_PAINT_UNDERLINE 0x40 42 | #define IO_PAINT_BOLD 0x80 43 | 44 | void set_block_source(FILE *source); 45 | void init_io(); 46 | void finish_io(); 47 | void handle_io(cpu *m); 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /opcode_handlers/compare.h: -------------------------------------------------------------------------------- 1 | case CMP_AB: 2 | arg1 = NEXT_BYTE(m); 3 | arg2 = NEXT_BYTE(m); 4 | cmp(m, m->mem[mem_abs(arg1, arg2, 0)], m->ac); 5 | break; 6 | 7 | case CMP_ABX: 8 | arg1 = NEXT_BYTE(m); 9 | arg2 = NEXT_BYTE(m); 10 | cmp(m, m->mem[mem_abs(arg1, arg2, m->x)], m->ac); 11 | break; 12 | 13 | case CMP_ABY: 14 | arg1 = NEXT_BYTE(m); 15 | arg2 = NEXT_BYTE(m); 16 | cmp(m, m->mem[mem_abs(arg1, arg2, m->y)], m->ac); 17 | break; 18 | 19 | case CMP_IMM: 20 | cmp(m, NEXT_BYTE(m), m->ac); 21 | break; 22 | 23 | case CMP_INX: 24 | cmp(m, m->mem[mem_indexed_indirect(m, NEXT_BYTE(m), m->x)], m->ac); 25 | break; 26 | 27 | case CMP_INY: 28 | cmp(m, m->mem[mem_indirect_index(m, NEXT_BYTE(m), m->y)], m->ac); 29 | break; 30 | 31 | case CMP_ZP: 32 | cmp(m, m->mem[NEXT_BYTE(m)], m->ac); 33 | break; 34 | 35 | case CMP_ZPX: 36 | cmp(m, m->mem[ZP(NEXT_BYTE(m) + m->x)], m->ac); 37 | break; 38 | 39 | case CPX_AB: 40 | arg1 = NEXT_BYTE(m); 41 | arg2 = NEXT_BYTE(m); 42 | cmp(m, m->mem[mem_abs(arg1, arg2, 0)], m->x); 43 | break; 44 | 45 | case CPX_IMM: 46 | cmp(m, NEXT_BYTE(m), m->x); 47 | break; 48 | 49 | case CPX_ZP: 50 | cmp(m, m->mem[NEXT_BYTE(m)], m->x); 51 | break; 52 | 53 | case CPY_AB: 54 | arg1 = NEXT_BYTE(m); 55 | arg2 = NEXT_BYTE(m); 56 | cmp(m, m->mem[mem_abs(arg1, arg2, 0)], m->y); 57 | break; 58 | 59 | case CPY_IMM: 60 | cmp(m, NEXT_BYTE(m), m->y); 61 | break; 62 | 63 | case CPY_ZP: 64 | cmp(m, m->mem[NEXT_BYTE(m)], m->y); 65 | break; 66 | -------------------------------------------------------------------------------- /opcode_handlers/incdec.h: -------------------------------------------------------------------------------- 1 | case INC_AB: 2 | arg1 = NEXT_BYTE(m); 3 | arg2 = NEXT_BYTE(m); 4 | r1 = mem_abs(arg1, arg2, 0); 5 | set_flags(m, ++m->mem[r1]); 6 | mark_dirty(m, r1); 7 | break; 8 | 9 | case INC_ABX: 10 | arg1 = NEXT_BYTE(m); 11 | arg2 = NEXT_BYTE(m); 12 | r1 = mem_abs(arg1, arg2, m->x); 13 | set_flags(m, ++m->mem[r1]); 14 | mark_dirty(m, r1); 15 | break; 16 | 17 | case INC_ZP: 18 | r1 = NEXT_BYTE(m); 19 | set_flags(m, ++m->mem[r1]); 20 | mark_dirty(m, r1); 21 | break; 22 | 23 | case INC_ZPX: 24 | r1 = ZP(NEXT_BYTE(m) + m->x); 25 | set_flags(m, ++m->mem[r1]); 26 | mark_dirty(m, r1); 27 | break; 28 | 29 | case INX: 30 | set_flags(m, ++m->x); 31 | break; 32 | 33 | case INY: 34 | set_flags(m, ++m->y); 35 | break; 36 | 37 | case DEC_AB: 38 | arg1 = NEXT_BYTE(m); 39 | arg2 = NEXT_BYTE(m); 40 | r1 = mem_abs(arg1, arg2, 0); 41 | set_flags(m, --m->mem[r1]); 42 | mark_dirty(m, r1); 43 | break; 44 | 45 | case DEC_ABX: 46 | arg1 = NEXT_BYTE(m); 47 | arg2 = NEXT_BYTE(m); 48 | r1 = mem_abs(arg1, arg2, m->x); 49 | set_flags(m, --m->mem[r1]); 50 | mark_dirty(m, r1); 51 | break; 52 | 53 | case DEC_ZP: 54 | r1 = NEXT_BYTE(m); 55 | set_flags(m, --m->mem[r1]); 56 | mark_dirty(m, r1); 57 | break; 58 | 59 | case DEC_ZPX: 60 | r1 = ZP(NEXT_BYTE(m) + m->x); 61 | set_flags(m, --m->mem[r1]); 62 | mark_dirty(m, r1); 63 | break; 64 | 65 | case DEX: 66 | set_flags(m, --m->x); 67 | break; 68 | 69 | case DEY: 70 | set_flags(m, --m->y); 71 | break; 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Will Haldean Brown 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. All advertising materials mentioning features or use of this software 12 | must display the following acknowledgement: 13 | This product includes software developed by the . 14 | 4. Neither the name of the nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /opcode_handlers/arithmetic.h: -------------------------------------------------------------------------------- 1 | case ADC_AB: 2 | arg1 = NEXT_BYTE(m); 3 | arg2 = NEXT_BYTE(m); 4 | add(m, m->mem[mem_abs(arg1, arg2, 0)]); 5 | break; 6 | 7 | case ADC_ABX: 8 | arg1 = NEXT_BYTE(m); 9 | arg2 = NEXT_BYTE(m); 10 | add(m, m->mem[mem_abs(arg1, arg2, m->x)]); 11 | break; 12 | 13 | case ADC_ABY: 14 | arg1 = NEXT_BYTE(m); 15 | arg2 = NEXT_BYTE(m); 16 | add(m, m->mem[mem_abs(arg1, arg2, m->y)]); 17 | break; 18 | 19 | case ADC_IMM: 20 | add(m, NEXT_BYTE(m)); 21 | break; 22 | 23 | case ADC_INX: 24 | add(m, m->mem[mem_indexed_indirect(m, NEXT_BYTE(m), m->x)]); 25 | break; 26 | 27 | case ADC_INY: 28 | add(m, m->mem[mem_indirect_index(m, NEXT_BYTE(m), m->y)]); 29 | break; 30 | 31 | case ADC_ZP: 32 | add(m, m->mem[NEXT_BYTE(m)]); 33 | break; 34 | 35 | case ADC_ZPX: 36 | add(m, m->mem[ZP(NEXT_BYTE(m) + m->x)]); 37 | break; 38 | 39 | case SBC_AB: 40 | arg1 = NEXT_BYTE(m); 41 | arg2 = NEXT_BYTE(m); 42 | sub(m, m->mem[mem_abs(arg1, arg2, 0)]); 43 | break; 44 | 45 | case SBC_ABX: 46 | arg1 = NEXT_BYTE(m); 47 | arg2 = NEXT_BYTE(m); 48 | sub(m, m->mem[mem_abs(arg1, arg2, m->x)]); 49 | break; 50 | 51 | case SBC_ABY: 52 | arg1 = NEXT_BYTE(m); 53 | arg2 = NEXT_BYTE(m); 54 | sub(m, m->mem[mem_abs(arg1, arg2, m->y)]); 55 | break; 56 | 57 | case SBC_IMM: 58 | sub(m, NEXT_BYTE(m)); 59 | break; 60 | 61 | case SBC_INX: 62 | sub(m, m->mem[mem_indexed_indirect(m, NEXT_BYTE(m), m->x)]); 63 | break; 64 | 65 | case SBC_INY: 66 | sub(m, m->mem[mem_indirect_index(m, NEXT_BYTE(m), m->y)]); 67 | break; 68 | 69 | case SBC_ZP: 70 | sub(m, m->mem[NEXT_BYTE(m)]); 71 | break; 72 | 73 | case SBC_ZPX: 74 | sub(m, m->mem[ZP(NEXT_BYTE(m) + m->x)]); 75 | break; 76 | -------------------------------------------------------------------------------- /stdlib/stdio.s: -------------------------------------------------------------------------------- 1 | ; defines useful constants and methods for writing 6502 assembler that targets x6502 2 | 3 | #define debug .byt $FC 4 | #define ext .byt $FF 5 | #define DEBUG debug 6 | #define EXT ext 7 | 8 | .( 9 | jmp prog 10 | 11 | +putc = $FF00 12 | +getc = $FF01 13 | +iomode = $FF02 14 | +paint = $FEE8 15 | 16 | +blck0_addrl = $FF03 17 | +blck0_addrh = $FF04 18 | +blck0_read = $FF05 19 | +blck0_write = $FF06 20 | +blck0_err = $FF07 21 | 22 | ; print reads two bytes from $00F0, treats them as a memory address, then 23 | ; reads from that memory address and prints those characters to the character 24 | ; device until a NUL character is found. the printed string must be less than 25 | ; 256 characters long. 26 | +print: 27 | .( 28 | pha 29 | ldy #$00 30 | loop: 31 | lda ($F0),Y 32 | cmp #$00 33 | beq done 34 | sta putc 35 | iny 36 | ; check that y hasn't overflowed 37 | cpy #$00 38 | beq done 39 | jmp loop 40 | done: 41 | pla 42 | rts 43 | .) 44 | 45 | ; fread reads up to 255 bytes into a memory region that starts at the address in 46 | ; $00F0 from the current position of the block device until a null character is 47 | ; encountered. 48 | +fread: 49 | .( 50 | pha 51 | ldy #$00 52 | 53 | loop: 54 | lda blck0_read 55 | ; TODO check read status 56 | 57 | sta ($F0),Y 58 | 59 | iny 60 | ; check that y hasn't overflowed 61 | cpy #$00 62 | beq done 63 | 64 | ; increment disk address, using carry addition 65 | clc 66 | lda #$01 67 | adc blck0_addrl 68 | sta blck0_addrl 69 | lda #$00 70 | adc blck0_addrh 71 | sta blck0_addrh 72 | 73 | jmp loop 74 | 75 | done: 76 | pla 77 | rts 78 | .) 79 | 80 | prog: 81 | .) 82 | -------------------------------------------------------------------------------- /debug.c: -------------------------------------------------------------------------------- 1 | #include "debug.h" 2 | #include "debug-names.h" 3 | #include 4 | 5 | #define MEM_PRINT_BYTES 16 6 | #define MAX_MEM_OFFSET (MEMORY_SIZE - MEM_PRINT_BYTES) 7 | 8 | void dump_cpu(cpu *m) { 9 | init_names(); 10 | 11 | int i; 12 | fprintf(stderr, "pc %04X\nx %02X y %02X sp %02X sr %02X ac %02X", 13 | m->pc, m->x, m->y, m->sp, m->sr, m->ac); 14 | 15 | fprintf(stderr, "\nlast opcode: %s (%02X)", 16 | inst_names[m->last_opcode], m->last_opcode); 17 | fprintf(stderr, ", next opcode: %s (%02X)", 18 | inst_names[m->mem[m->pc]], m->mem[m->pc]); 19 | 20 | fprintf(stderr, "\nflags n %d o %d b %d d %d i %d z %d c %d", 21 | (m->sr & FLAG_NEGATIVE) > 0, 22 | (m->sr & FLAG_OVERFLOW) > 0, 23 | (m->sr & FLAG_BREAK) > 0, 24 | (m->sr & FLAG_DECIMAL) > 0, 25 | (m->sr & FLAG_INTERRUPT) > 0, 26 | (m->sr & FLAG_ZERO) > 0, 27 | (m->sr & FLAG_CARRY) > 0); 28 | 29 | fprintf(stderr, "\nmem "); 30 | 31 | int mem_offset = m->pc - MEM_PRINT_BYTES / 2; 32 | // clamp to [0, MAX_MEM_OFFSET] 33 | mem_offset = mem_offset < 0 ? 0 : ( 34 | mem_offset > MAX_MEM_OFFSET ? MAX_MEM_OFFSET : mem_offset); 35 | 36 | for (i = 0; i < MEM_PRINT_BYTES; i++) { 37 | fprintf(stderr, "%02X ", m->mem[i + mem_offset]); 38 | } 39 | 40 | fprintf(stderr, "\n "); 41 | for (i = 0; i < m->pc - mem_offset; i++) { 42 | fprintf(stderr, " "); 43 | } 44 | fprintf(stderr, "^^ (%04x)", m->pc); 45 | 46 | fprintf(stderr, "\nstack "); 47 | for (i = 0; i < MEM_PRINT_BYTES; i++) { 48 | fprintf(stderr, "%02X ", m->mem[STACK_START + 0xFF - i]); 49 | } 50 | int off = 0xFF - m->sp; 51 | if (off < MEM_PRINT_BYTES) { 52 | fprintf(stderr, "\n "); 53 | for (i = 0; i < off; i++) { 54 | fprintf(stderr, " "); 55 | } 56 | fprintf(stderr, "^^"); 57 | } 58 | 59 | fprintf(stderr, "\n\n"); 60 | } 61 | -------------------------------------------------------------------------------- /opcode_handlers/store.h: -------------------------------------------------------------------------------- 1 | case STA_AB: 2 | arg1 = NEXT_BYTE(m); 3 | arg2 = NEXT_BYTE(m); 4 | r1 = mem_abs(arg1, arg2, 0); 5 | m->mem[r1] = m->ac; 6 | mark_dirty(m, r1); 7 | break; 8 | 9 | case STA_ABX: 10 | arg1 = NEXT_BYTE(m); 11 | arg2 = NEXT_BYTE(m); 12 | r1 = mem_abs(arg1, arg2, m->x); 13 | m->mem[r1] = m->ac; 14 | mark_dirty(m, r1); 15 | break; 16 | 17 | case STA_ABY: 18 | arg1 = NEXT_BYTE(m); 19 | arg2 = NEXT_BYTE(m); 20 | r1 = mem_abs(arg1, arg2, m->y); 21 | m->mem[r1] = m->ac; 22 | mark_dirty(m, r1); 23 | break; 24 | 25 | case STA_INX: 26 | r1 = mem_indexed_indirect(m, NEXT_BYTE(m), m->x); 27 | m->mem[r1] = m->ac; 28 | mark_dirty(m, r1); 29 | break; 30 | 31 | case STA_INY: 32 | r1 = mem_indirect_index(m, NEXT_BYTE(m), m->y); 33 | m->mem[r1] = m->ac; 34 | mark_dirty(m, r1); 35 | break; 36 | 37 | case STA_ZP: 38 | r1 = ZP(NEXT_BYTE(m)); 39 | m->mem[r1] = m->ac; 40 | mark_dirty(m, r1); 41 | break; 42 | 43 | case STA_ZPX: 44 | r1 = ZP(NEXT_BYTE(m) + m->x); 45 | m->mem[r1] = m->ac; 46 | mark_dirty(m, r1); 47 | break; 48 | 49 | case STX_ZP: 50 | r1 = ZP(NEXT_BYTE(m)); 51 | m->mem[r1] = m->x; 52 | mark_dirty(m, r1); 53 | break; 54 | 55 | case STX_ZPY: 56 | r1 = ZP(NEXT_BYTE(m) + m->y); 57 | m->mem[r1] = m->x; 58 | mark_dirty(m, r1); 59 | break; 60 | 61 | case STX_AB: 62 | arg1 = NEXT_BYTE(m); 63 | arg2 = NEXT_BYTE(m); 64 | r1 = mem_abs(arg1, arg2, 0); 65 | m->mem[r1] = m->x; 66 | mark_dirty(m, r1); 67 | break; 68 | 69 | case STY_ZP: 70 | r1 = ZP(NEXT_BYTE(m)); 71 | m->mem[r1] = m->y; 72 | mark_dirty(m, r1); 73 | break; 74 | 75 | case STY_ZPX: 76 | r1 = ZP(NEXT_BYTE(m) + m->x); 77 | m->mem[r1] = m->y; 78 | mark_dirty(m, r1); 79 | break; 80 | 81 | case STY_AB: 82 | arg1 = NEXT_BYTE(m); 83 | arg2 = NEXT_BYTE(m); 84 | r1 = mem_abs(arg1, arg2, 0); 85 | m->mem[r1] = m->y; 86 | mark_dirty(m, r1); 87 | break; 88 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include "cpu.h" 2 | #include "emu.h" 3 | #include "functions.h" 4 | #include "io.h" 5 | #include "opcodes.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | void usage() { 13 | printf("x6502: a simple 6502 emulator\n"); 14 | printf("usage: x6502 [OPTION]... [FILE] \n"); 15 | printf("options:\n"); 16 | printf("\t-b addr\t\tthe base address at which code will be loaded\n"); 17 | printf("\t-d file\t\ta binary file to back the block device\n"); 18 | printf("\t\t\t(optional, defaults to zero)\n"); 19 | } 20 | 21 | int main(int argc, char *argv[]) { 22 | int base_addr = 0x1000; 23 | char *blck0_file = NULL; 24 | 25 | int c; 26 | while ((c = getopt(argc, argv, "hb:d:")) != -1) { 27 | switch (c) { 28 | case 'b': 29 | base_addr = atoi(optarg); 30 | break; 31 | 32 | case 'd': 33 | blck0_file = optarg; 34 | break; 35 | 36 | case 'h': 37 | usage(); 38 | return 0; 39 | 40 | case '?': 41 | if (optopt == 'b' || optopt == 'd') { 42 | fprintf(stderr, "Option -%c requires an argument.\n", optopt); 43 | } 44 | usage(); 45 | return -1; 46 | } 47 | } 48 | 49 | if (optind == argc) { 50 | printf("no input file specified.\n"); 51 | usage(); 52 | return -1; 53 | } 54 | 55 | if (blck0_file != NULL) { 56 | debugf("using %s as a backing file for block device 0\n", blck0_file); 57 | FILE *blck0 = fopen(blck0_file, "r+"); 58 | if (blck0 == NULL) { 59 | fprintf(stderr, "block file %s does not exist.\n", blck0_file); 60 | return -1; 61 | } 62 | set_block_source(blck0); 63 | } 64 | 65 | FILE *in_f = fopen(argv[optind], "r"); 66 | int b; 67 | int i = base_addr; 68 | cpu *m = new_cpu(base_addr); 69 | while ((b = fgetc(in_f)) != EOF) { 70 | m->mem[i++] = (uint8_t) b; 71 | } 72 | main_loop(m); 73 | return 0; 74 | } 75 | -------------------------------------------------------------------------------- /opcode_handlers/load.h: -------------------------------------------------------------------------------- 1 | case LDA_AB: 2 | arg1 = NEXT_BYTE(m); 3 | arg2 = NEXT_BYTE(m); 4 | m->ac = m->mem[mem_abs(arg1, arg2, 0)]; 5 | set_flags(m, m->ac); 6 | break; 7 | 8 | case LDA_ABX: 9 | arg1 = NEXT_BYTE(m); 10 | arg2 = NEXT_BYTE(m); 11 | m->ac = m->mem[mem_abs(arg1, arg2, m->x)]; 12 | set_flags(m, m->ac); 13 | break; 14 | 15 | case LDA_ABY: 16 | arg1 = NEXT_BYTE(m); 17 | arg2 = NEXT_BYTE(m); 18 | m->ac = m->mem[mem_abs(arg1, arg2, m->y)]; 19 | set_flags(m, m->ac); 20 | break; 21 | 22 | case LDA_IMM: 23 | m->ac = NEXT_BYTE(m); 24 | set_flags(m, m->ac); 25 | break; 26 | 27 | case LDA_INX: 28 | m->ac = m->mem[mem_indexed_indirect(m, NEXT_BYTE(m), m->x)]; 29 | set_flags(m, m->ac); 30 | break; 31 | 32 | case LDA_INY: 33 | m->ac = m->mem[mem_indirect_index(m, NEXT_BYTE(m), m->y)]; 34 | set_flags(m, m->ac); 35 | break; 36 | 37 | case LDA_ZP: 38 | m->ac = m->mem[NEXT_BYTE(m)]; 39 | set_flags(m, m->ac); 40 | break; 41 | 42 | case LDA_ZPX: 43 | m->ac = m->mem[NEXT_BYTE(m) + m->x]; 44 | set_flags(m, m->ac); 45 | break; 46 | 47 | case LDX_AB: 48 | arg1 = NEXT_BYTE(m); 49 | arg2 = NEXT_BYTE(m); 50 | m->x = m->mem[mem_abs(arg1, arg2, 0)]; 51 | set_flags(m, m->x); 52 | break; 53 | 54 | case LDX_ABY: 55 | arg1 = NEXT_BYTE(m); 56 | arg2 = NEXT_BYTE(m); 57 | m->x = m->mem[mem_abs(arg1, arg2, m->y)]; 58 | set_flags(m, m->x); 59 | break; 60 | 61 | case LDX_IMM: 62 | m->x = NEXT_BYTE(m); 63 | set_flags(m, m->x); 64 | break; 65 | 66 | case LDX_ZP: 67 | m->x = m->mem[NEXT_BYTE(m)]; 68 | set_flags(m, m->x); 69 | break; 70 | 71 | case LDX_ZPY: 72 | m->x = m->mem[NEXT_BYTE(m) + m->y]; 73 | set_flags(m, m->x); 74 | break; 75 | 76 | case LDY_AB: 77 | arg1 = NEXT_BYTE(m); 78 | arg2 = NEXT_BYTE(m); 79 | m->y = m->mem[mem_abs(arg1, arg2, 0)]; 80 | set_flags(m, m->y); 81 | break; 82 | 83 | case LDY_ABX: 84 | arg1 = NEXT_BYTE(m); 85 | arg2 = NEXT_BYTE(m); 86 | m->y = m->mem[mem_abs(arg1, arg2, m->x)]; 87 | set_flags(m, m->y); 88 | break; 89 | 90 | case LDY_IMM: 91 | m->y = NEXT_BYTE(m); 92 | set_flags(m, m->y); 93 | break; 94 | 95 | case LDY_ZP: 96 | m->y = m->mem[NEXT_BYTE(m)]; 97 | set_flags(m, m->y); 98 | break; 99 | 100 | case LDY_ZPX: 101 | m->y = m->mem[NEXT_BYTE(m) + m->x]; 102 | set_flags(m, m->y); 103 | break; 104 | -------------------------------------------------------------------------------- /sample_programs/minigame.s: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | ; $00 low byte of character address 4 | ; $01 high byte of character address 5 | 6 | rows = 25 7 | cols = 40 8 | 9 | ; load location of ISR 10 | lda #isr 13 | sta $FFFF 14 | 15 | ; activate vterm mode 16 | lda #$01 17 | sta $FF02 18 | 19 | ; shiny pretty colors 20 | lda #$04 21 | sta paint 22 | 23 | ; register and addressing init 24 | lda #$00 25 | sta $00 26 | lda #$FB 27 | sta $01 28 | 29 | lda #$A4 30 | ldy #$00 31 | 32 | cli 33 | 34 | loop 35 | sta ($00),Y 36 | wai 37 | jmp loop 38 | 39 | isr: 40 | pha 41 | 42 | ; clear existing character 43 | lda #$20 44 | sta ($00),Y 45 | 46 | lda getc 47 | 48 | cmp #$71 ; 71 == 'q' 49 | bne checkj 50 | ext 51 | 52 | checkj: 53 | cmp #$6A ; 6A == 'j' 54 | bne checkk 55 | ; it's a j. move down. 56 | clc 57 | 58 | lda $00 59 | adc #cols 60 | sta $00 61 | 62 | lda $01 63 | adc #$00 64 | sta $01 65 | 66 | jmp checkbounds 67 | 68 | checkk: 69 | cmp #$6B ; 6B == 'k' 70 | bne checkh 71 | ; it's a k. move up. 72 | sec 73 | 74 | lda $00 75 | sbc #cols 76 | sta $00 77 | 78 | lda $01 79 | sbc #$00 80 | sta $01 81 | 82 | jmp checkbounds 83 | 84 | checkh: 85 | cmp #$68 ; 68 == 'h' 86 | bne checkl 87 | ; it's an h. move left. 88 | sec 89 | 90 | lda $00 91 | sbc #$02 92 | sta $00 93 | 94 | lda $01 95 | sbc #$00 96 | sta $01 97 | 98 | jmp checkbounds 99 | 100 | checkl: 101 | cmp #$6C ; 6C == 'l' 102 | bne done 103 | ; it's an l. move right. 104 | clc 105 | 106 | lda $00 107 | adc #$02 108 | sta $00 109 | 110 | lda $01 111 | adc #$00 112 | sta $01 113 | 114 | jmp checkbounds 115 | 116 | checkbounds: 117 | checked: 118 | done: 119 | pla 120 | rti 121 | -------------------------------------------------------------------------------- /emu.c: -------------------------------------------------------------------------------- 1 | #include "emu.h" 2 | 3 | #include 4 | #include "debug.h" 5 | #include "functions.h" 6 | #include "io.h" 7 | #include "opcodes.h" 8 | 9 | #define NEXT_BYTE(cpu) ((cpu)->mem[(cpu)->pc + (pc_offset++)]) 10 | 11 | void main_loop(cpu *m) { 12 | uint8_t opcode; 13 | uint8_t arg1, arg2, t1; 14 | int8_t s1; 15 | uint16_t r1, r2; 16 | 17 | // pc_offset is used to read from memory like a stream when processing 18 | // bytecode without modifying the pc. pc_start is the memory address of the 19 | // currently-executing opcode; if pc == pc_start at the end of a simulation 20 | // step, we add pc_offset to get the start of the next instruction. if pc != 21 | // pc_start, we branched so we don't touch the pc. 22 | uint8_t pc_offset = 0; 23 | uint16_t pc_start; 24 | 25 | // branch_offset is an offset that will be added to the program counter 26 | // after we move to the next instruction 27 | int8_t branch_offset = 0; 28 | 29 | init_io(); 30 | 31 | for (;;) { 32 | DUMP_DEBUG(m); 33 | 34 | reset_emu_flags(m); 35 | 36 | pc_offset = 0; 37 | branch_offset = 0; 38 | pc_start = m->pc; 39 | opcode = NEXT_BYTE(m); 40 | 41 | switch (opcode) { 42 | case NOP: 43 | break; 44 | 45 | #ifndef DISABLE_EXTENSIONS 46 | case EXT: 47 | goto end; 48 | 49 | case DUMP: 50 | dump_cpu(m); 51 | break; 52 | #endif 53 | 54 | #include "opcode_handlers/arithmetic.h" 55 | #include "opcode_handlers/branch.h" 56 | #include "opcode_handlers/compare.h" 57 | #include "opcode_handlers/flags.h" 58 | #include "opcode_handlers/incdec.h" 59 | #include "opcode_handlers/interrupts.h" 60 | #include "opcode_handlers/jump.h" 61 | #include "opcode_handlers/load.h" 62 | #include "opcode_handlers/logical.h" 63 | #include "opcode_handlers/shift.h" 64 | #include "opcode_handlers/stack.h" 65 | #include "opcode_handlers/store.h" 66 | #include "opcode_handlers/transfer.h" 67 | 68 | default: 69 | printf("ERROR: got unknown opcode %02x\n", opcode); 70 | goto end; 71 | } 72 | 73 | if (m->pc == pc_start) { 74 | m->pc += pc_offset; 75 | } 76 | m->pc += branch_offset; 77 | 78 | do { 79 | handle_io(m); 80 | // clear dirty memory flag immediately so that subsequent runs don't 81 | // redo whatever I/O operation is associated with the dirty memaddr 82 | m->emu_flags &= ~EMU_FLAG_DIRTY; 83 | } while ((m->emu_flags & EMU_FLAG_WAIT_FOR_INTERRUPT) && 84 | !m->interrupt_waiting); 85 | 86 | if (m->interrupt_waiting && !get_flag(m, FLAG_INTERRUPT)) { 87 | STACK_PUSH(m) = (m->pc & 0xFF00) >> 8; 88 | STACK_PUSH(m) = m->pc & 0xFF; 89 | STACK_PUSH(m) = m->sr; 90 | 91 | m->interrupt_waiting = 0x00; 92 | m->pc = mem_abs(m->mem[0xFFFE], m->mem[0xFFFF], 0); 93 | m->sr |= FLAG_INTERRUPT; 94 | } 95 | 96 | m->last_opcode = opcode; 97 | } 98 | end: 99 | finish_io(); 100 | } 101 | -------------------------------------------------------------------------------- /functions.h: -------------------------------------------------------------------------------- 1 | #ifndef __6502_FUNCTIONS__ 2 | #define __6502_FUNCTIONS__ 3 | 4 | #include 5 | 6 | #include "cpu.h" 7 | 8 | #define ZP(x) ((uint8_t) (x)) 9 | #define STACK_PUSH(m) (m)->mem[(m)->sp-- + STACK_START] 10 | #define STACK_POP(m) (m)->mem[++(m)->sp + STACK_START] 11 | 12 | #ifdef DEBUG 13 | #define debugf(...) fprintf(stderr, __VA_ARGS__) 14 | #else 15 | #define debugf(...) do {} while (0) 16 | #endif 17 | 18 | static inline size_t mem_abs(uint8_t low, uint8_t high, uint8_t off) { 19 | return (uint16_t) off + (uint16_t) low + ((uint16_t) high << 8); 20 | } 21 | 22 | static inline size_t mem_indirect_index(cpu *m, uint8_t addr, uint8_t off) { 23 | return mem_abs(m->mem[addr], m->mem[addr+1], off); 24 | } 25 | 26 | static inline size_t mem_indexed_indirect(cpu *m, uint8_t addr, uint8_t off) { 27 | return mem_abs(m->mem[addr+off], m->mem[addr+off+1], 0); 28 | } 29 | 30 | // set arg MUST be 16 bits, not 8, so that add results can fit into set. 31 | static inline void set_flag(cpu *m, uint8_t flag, uint16_t set) { 32 | if (set) { 33 | m->sr |= flag; 34 | } else { 35 | m->sr &= ~flag; 36 | } 37 | } 38 | 39 | static inline uint8_t get_flag(cpu *m, uint8_t flag) { 40 | return (m->sr & flag) > 0; 41 | } 42 | 43 | static inline uint8_t get_emu_flag(cpu *m, uint8_t flag) { 44 | return (m->emu_flags & flag) > 0; 45 | } 46 | 47 | // set flags for the result of a computation. set_flags should be called on the 48 | // result of any arithmetic operation. 49 | static inline void set_flags(cpu *m, uint8_t val) { 50 | set_flag(m, FLAG_ZERO, !val); 51 | set_flag(m, FLAG_NEGATIVE, val & 0x80); 52 | } 53 | 54 | static inline uint8_t bcd(uint8_t val) { 55 | // bcd is "binary coded decimal"; it treats the upper nibble and lower 56 | // nibble of a byte each as a decimal digit, so 01011000 -> 0101 1000 -> 58. 57 | // in other words, treat hex output as decimal output, so 0x58 is treated as 58 | // 58. this is dumb and adds a bunch of branching to opcode interpretation 59 | // that I Do Not Like. 60 | return 10 * (val >> 4) + (0x0F & val); 61 | } 62 | 63 | static inline void add(cpu *m, uint16_t r1) { 64 | // committing a cardinal sin for my sanity's sake. callers should initialize 65 | // r1 to the argument to the add. 66 | if (get_flag(m, FLAG_DECIMAL)) { 67 | r1 = bcd(r1) + bcd(m->ac) + get_flag(m, FLAG_CARRY); 68 | set_flag(m, FLAG_CARRY, r1 > 99); 69 | } else { 70 | r1 += m->ac + get_flag(m, FLAG_CARRY); 71 | set_flag(m, FLAG_CARRY, r1 & 0xFF00); 72 | } 73 | set_flag(m, FLAG_OVERFLOW, (m->ac & 0x80) != (r1 & 0x80)); 74 | set_flag(m, FLAG_ZERO, r1 == 0); 75 | set_flag(m, FLAG_NEGATIVE, r1 & 0x80); 76 | m->ac = r1; 77 | } 78 | 79 | static inline void sub(cpu *m, uint16_t r1) { 80 | if (get_flag(m, FLAG_DECIMAL)) { 81 | r1 = bcd(m->ac) - bcd(r1) - !get_flag(m, FLAG_CARRY); 82 | set_flag(m, FLAG_OVERFLOW, r1 > 99 || r1 < 0); 83 | } else { 84 | r1 = m->ac - r1 - !get_flag(m, FLAG_CARRY); 85 | set_flag(m, FLAG_OVERFLOW, 0xFF00 & r1); 86 | } 87 | set_flag(m, FLAG_CARRY, !(r1 & 0x8000)); 88 | set_flag(m, FLAG_NEGATIVE, r1 & 0x80); 89 | set_flag(m, FLAG_ZERO, r1 == 0); 90 | m->ac = r1; 91 | } 92 | 93 | static inline void cmp(cpu *m, uint8_t mem, uint8_t reg) { 94 | set_flag(m, FLAG_CARRY, reg >= mem); 95 | set_flag(m, FLAG_ZERO, reg == mem); 96 | set_flag(m, FLAG_NEGATIVE, 0x80 & (reg - mem)); 97 | } 98 | 99 | // called at the start of processing an instruction to reset instruction-local 100 | // emulator state 101 | static inline void reset_emu_flags(cpu *m) { 102 | m->emu_flags = 0x00; 103 | } 104 | 105 | // mark a memory address as dirty 106 | static inline void mark_dirty(cpu *m, uint16_t addr) { 107 | m->emu_flags |= EMU_FLAG_DIRTY; 108 | m->dirty_mem_addr = addr; 109 | } 110 | 111 | #endif 112 | -------------------------------------------------------------------------------- /opcode_handlers/logical.h: -------------------------------------------------------------------------------- 1 | case AND_IMM: 2 | m->ac &= NEXT_BYTE(m); 3 | set_flags(m, m->ac); 4 | break; 5 | 6 | case AND_ZP: 7 | m->ac &= m->mem[NEXT_BYTE(m)]; 8 | set_flags(m, m->ac); 9 | break; 10 | 11 | case AND_ZPX: 12 | m->ac &= m->mem[ZP(NEXT_BYTE(m) + m->x)]; 13 | set_flags(m, m->ac); 14 | break; 15 | 16 | case AND_AB: 17 | arg1 = NEXT_BYTE(m); 18 | arg2 = NEXT_BYTE(m); 19 | m->ac &= m->mem[mem_abs(arg1, arg2, 0)]; 20 | set_flags(m, m->ac); 21 | break; 22 | 23 | case AND_ABX: 24 | arg1 = NEXT_BYTE(m); 25 | arg2 = NEXT_BYTE(m); 26 | m->ac &= m->mem[mem_abs(arg1, arg2, m->x)]; 27 | set_flags(m, m->ac); 28 | break; 29 | 30 | case AND_ABY: 31 | arg1 = NEXT_BYTE(m); 32 | arg2 = NEXT_BYTE(m); 33 | m->ac &= m->mem[mem_abs(arg1, arg2, m->y)]; 34 | set_flags(m, m->ac); 35 | break; 36 | 37 | case AND_INX: 38 | m->ac &= m->mem[mem_indexed_indirect(m, NEXT_BYTE(m), m->x)]; 39 | set_flags(m, m->ac); 40 | break; 41 | 42 | case AND_INY: 43 | m->ac &= m->mem[mem_indirect_index(m, NEXT_BYTE(m), m->y)]; 44 | set_flags(m, m->ac); 45 | break; 46 | 47 | case EOR_IMM: 48 | m->ac ^= NEXT_BYTE(m); 49 | set_flags(m, m->ac); 50 | break; 51 | 52 | case EOR_ZP: 53 | m->ac ^= m->mem[NEXT_BYTE(m)]; 54 | set_flags(m, m->ac); 55 | break; 56 | 57 | case EOR_ZPX: 58 | m->ac ^= m->mem[ZP(NEXT_BYTE(m) + m->x)]; 59 | set_flags(m, m->ac); 60 | break; 61 | 62 | case EOR_AB: 63 | arg1 = NEXT_BYTE(m); 64 | arg2 = NEXT_BYTE(m); 65 | m->ac ^= m->mem[mem_abs(arg1, arg2, 0)]; 66 | set_flags(m, m->ac); 67 | break; 68 | 69 | case EOR_ABX: 70 | arg1 = NEXT_BYTE(m); 71 | arg2 = NEXT_BYTE(m); 72 | m->ac ^= m->mem[mem_abs(arg1, arg2, m->x)]; 73 | set_flags(m, m->ac); 74 | break; 75 | 76 | case EOR_ABY: 77 | arg1 = NEXT_BYTE(m); 78 | arg2 = NEXT_BYTE(m); 79 | m->ac ^= m->mem[mem_abs(arg1, arg2, m->y)]; 80 | set_flags(m, m->ac); 81 | break; 82 | 83 | case EOR_INX: 84 | m->ac ^= m->mem[mem_indexed_indirect(m, NEXT_BYTE(m), m->x)]; 85 | set_flags(m, m->ac); 86 | break; 87 | 88 | case EOR_INY: 89 | m->ac ^= m->mem[mem_indirect_index(m, NEXT_BYTE(m), m->y)]; 90 | set_flags(m, m->ac); 91 | break; 92 | 93 | case ORA_IMM: 94 | m->ac |= NEXT_BYTE(m); 95 | set_flags(m, m->ac); 96 | break; 97 | 98 | case ORA_ZP: 99 | m->ac |= m->mem[NEXT_BYTE(m)]; 100 | set_flags(m, m->ac); 101 | break; 102 | 103 | case ORA_ZPX: 104 | m->ac |= m->mem[ZP(NEXT_BYTE(m) + m->x)]; 105 | set_flags(m, m->ac); 106 | break; 107 | 108 | case ORA_AB: 109 | arg1 = NEXT_BYTE(m); 110 | arg2 = NEXT_BYTE(m); 111 | m->ac |= m->mem[mem_abs(arg1, arg2, 0)]; 112 | set_flags(m, m->ac); 113 | break; 114 | 115 | case ORA_ABX: 116 | arg1 = NEXT_BYTE(m); 117 | arg2 = NEXT_BYTE(m); 118 | m->ac |= m->mem[mem_abs(arg1, arg2, m->x)]; 119 | set_flags(m, m->ac); 120 | break; 121 | 122 | case ORA_ABY: 123 | arg1 = NEXT_BYTE(m); 124 | arg2 = NEXT_BYTE(m); 125 | m->ac |= m->mem[mem_abs(arg1, arg2, m->y)]; 126 | set_flags(m, m->ac); 127 | break; 128 | 129 | case ORA_INX: 130 | m->ac |= m->mem[mem_indexed_indirect(m, NEXT_BYTE(m), m->x)]; 131 | set_flags(m, m->ac); 132 | break; 133 | 134 | case ORA_INY: 135 | m->ac |= m->mem[mem_indirect_index(m, NEXT_BYTE(m), m->y)]; 136 | set_flags(m, m->ac); 137 | break; 138 | 139 | case BIT_AB: 140 | arg1 = NEXT_BYTE(m); 141 | arg2 = NEXT_BYTE(m); 142 | t1 = m->mem[mem_abs(arg1, arg2, 0)]; 143 | set_flag(m, FLAG_ZERO, !(t1 & m->ac)); 144 | set_flag(m, FLAG_OVERFLOW, t1 & 0x40); 145 | set_flag(m, FLAG_NEGATIVE, t1 & 0x80); 146 | break; 147 | 148 | case BIT_ZP: 149 | t1 = m->mem[NEXT_BYTE(m)]; 150 | set_flag(m, FLAG_ZERO, !(t1 & m->ac)); 151 | set_flag(m, FLAG_OVERFLOW, t1 & 0x40); 152 | set_flag(m, FLAG_NEGATIVE, t1 & 0x80); 153 | break; 154 | -------------------------------------------------------------------------------- /io.c: -------------------------------------------------------------------------------- 1 | #include "io.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "functions.h" 10 | 11 | #define VTERM_ROWS 25 12 | #define VTERM_COLS 40 13 | 14 | uint8_t io_modeflags = 0x00; 15 | uint8_t io_supports_paint; 16 | 17 | FILE *blck0 = NULL; 18 | 19 | WINDOW *window = NULL; 20 | 21 | void init_vterm(); 22 | void update_vterm(cpu *, uint16_t); 23 | void finish_vterm(); 24 | 25 | void set_block_source(FILE *source) { 26 | blck0 = source; 27 | } 28 | 29 | void init_io() { 30 | initscr(); 31 | cbreak(); 32 | noecho(); 33 | nodelay(stdscr, TRUE); 34 | keypad(stdscr, TRUE); 35 | curs_set(0); 36 | 37 | io_supports_paint = (has_colors() != FALSE); 38 | if (io_supports_paint) { 39 | start_color(); 40 | for (int i = 0; i < 8; i++) { 41 | init_pair(i, i, COLOR_BLACK); 42 | } 43 | } 44 | } 45 | 46 | void finish_io() { 47 | if (io_modeflags & IO_MODEFLAG_WAIT_HALT) { 48 | nodelay(stdscr, FALSE); 49 | printw("\nterminated, press any key to exit.\n"); 50 | getch(); 51 | } 52 | 53 | endwin(); 54 | } 55 | 56 | void init_vterm() { 57 | window = newwin(VTERM_ROWS + 2, VTERM_COLS + 2, 0, 0); 58 | box(window, 0, 0); 59 | wrefresh(window); 60 | } 61 | 62 | void finish_vterm() { 63 | endwin(); 64 | } 65 | 66 | void update_modeflags(uint8_t old_flags, uint8_t new_flags) { 67 | io_modeflags = new_flags; 68 | 69 | // if the vterm flag changed (avoids reinit every time flags change) 70 | if ((new_flags ^ old_flags) & IO_MODEFLAG_VTERM) { 71 | if (new_flags & IO_MODEFLAG_VTERM) { 72 | init_vterm(); 73 | } else { 74 | finish_vterm(); 75 | } 76 | } 77 | } 78 | 79 | void update_paint(uint8_t paint) { 80 | wattrset(window, 81 | COLOR_PAIR(paint & 0x0F) | 82 | (paint & IO_PAINT_DIM ? A_DIM : 0) | 83 | (paint & IO_PAINT_UNDERLINE ? A_UNDERLINE : 0) | 84 | (paint & IO_PAINT_BOLD ? A_BOLD : 0)); 85 | } 86 | 87 | void update_vterm(cpu *m, uint16_t dirty) { 88 | uint16_t offset = dirty - IO_VTERM_START; 89 | if (offset >= 1000) { 90 | // this is in the unused 24 bytes at the end of the page, ignore it 91 | return; 92 | } 93 | // 1 offsets to avoid overwriting the border 94 | uint8_t r = offset / VTERM_COLS + 1; 95 | uint8_t c = offset % VTERM_COLS + 1; 96 | mvwprintw(window, r, c, "%c", m->mem[dirty]); 97 | wrefresh(window); 98 | } 99 | 100 | void handle_io(cpu *m) { 101 | int read; 102 | if ((read = getch()) != ERR) { 103 | m->interrupt_waiting = 0x01; 104 | m->mem[IO_GETCHAR] = read; 105 | } 106 | 107 | if (get_emu_flag(m, EMU_FLAG_DIRTY)) { 108 | uint16_t addr = m->dirty_mem_addr; 109 | debugf("dirty address %04X has value %02x\n", addr, m->mem[addr]); 110 | 111 | if (addr == IO_PUTCHAR) { 112 | if (io_modeflags & IO_MODEFLAG_VTERM) { 113 | wprintw(window, "%c", m->mem[addr]); 114 | wrefresh(window); 115 | } else { 116 | addch(m->mem[addr]); 117 | } 118 | } else if (addr == IO_MODEFLAGS) { 119 | update_modeflags(io_modeflags, m->mem[IO_MODEFLAGS]); 120 | } else if (addr == IO_PAINT) { 121 | update_paint(m->mem[addr]); 122 | } else if (IO_VTERM_START <= addr && addr < IO_VTERM_END) { 123 | update_vterm(m, addr); 124 | } else if (addr == IO_BLCK0_ADDRL 125 | || addr == IO_BLCK0_ADDRH 126 | || addr == IO_BLCK0_READ) { 127 | if (blck0 == NULL) { 128 | finish_vterm(); 129 | fprintf(stderr, "tried to read from unattached block device\n"); 130 | exit(-1); 131 | return; 132 | } 133 | 134 | uint16_t read_addr = 135 | m->mem[IO_BLCK0_ADDRH] << 8 | m->mem[IO_BLCK0_ADDRL]; 136 | int res = fseek(blck0, read_addr, SEEK_SET); 137 | if (res) { 138 | debugf("ERROR: fseek returned %d\n", res); 139 | m->mem[IO_BLCK0_ERR] = IO_BLCK_ERR_SEEK; 140 | return; 141 | } 142 | 143 | res = fgetc(blck0); 144 | if (res == EOF) { 145 | debugf("ERROR: fgetc returned EOF\n"); 146 | m->mem[IO_BLCK0_ERR] = IO_BLCK_ERR_EOF; 147 | return; 148 | } 149 | m->mem[IO_BLCK0_READ] = 0xFF & res; 150 | m->mem[IO_BLCK0_ERR] = 0x00; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /opcode_handlers/shift.h: -------------------------------------------------------------------------------- 1 | case ASL_AB: 2 | arg1 = NEXT_BYTE(m); 3 | r1 = mem_abs(arg1, NEXT_BYTE(m), 0); 4 | set_flag(m, FLAG_CARRY, m->mem[r1] & 0x80); 5 | m->mem[r1] = (m->mem[r1] << 1) & 0xFE; 6 | set_flags(m, m->mem[r1]); 7 | mark_dirty(m, r1); 8 | break; 9 | 10 | case ASL_ABX: 11 | arg1 = NEXT_BYTE(m); 12 | r1 = mem_abs(arg1, NEXT_BYTE(m), m->x); 13 | set_flag(m, FLAG_CARRY, m->mem[r1] & 0x80); 14 | m->mem[r1] = (m->mem[r1] << 1) & 0xFE; 15 | set_flags(m, m->mem[r1]); 16 | mark_dirty(m, r1); 17 | break; 18 | 19 | case ASL_ACC: 20 | set_flag(m, FLAG_CARRY, m->ac & 0x80); 21 | m->ac = (m->ac << 1) & 0xFE; 22 | set_flags(m, m->ac); 23 | break; 24 | 25 | case ASL_ZP: 26 | arg1 = NEXT_BYTE(m); 27 | set_flag(m, FLAG_CARRY, m->mem[arg1] & 0x80); 28 | m->mem[arg1] = (m->mem[arg1] << 1) & 0xFE; 29 | set_flags(m, m->mem[arg1]); 30 | mark_dirty(m, arg1); 31 | break; 32 | 33 | case ASL_ZPX: 34 | arg1 = ZP(NEXT_BYTE(m) + m->x); 35 | set_flag(m, FLAG_CARRY, m->mem[arg1] & 0x80); 36 | m->mem[arg1] = (m->mem[arg1] << 1) & 0xFE; 37 | set_flags(m, m->mem[arg1]); 38 | mark_dirty(m, arg1); 39 | break; 40 | 41 | case LSR_AB: 42 | arg1 = NEXT_BYTE(m); 43 | r1 = mem_abs(arg1, NEXT_BYTE(m), 0); 44 | set_flag(m, FLAG_CARRY, m->mem[r1] & 0x01); 45 | m->mem[r1] = (m->mem[r1] >> 1) & 0x7F; 46 | set_flags(m, m->mem[r1]); 47 | mark_dirty(m, r1); 48 | break; 49 | 50 | case LSR_ABX: 51 | arg1 = NEXT_BYTE(m); 52 | r1 = mem_abs(arg1, NEXT_BYTE(m), m->x); 53 | set_flag(m, FLAG_CARRY, m->mem[r1] & 0x01); 54 | m->mem[r1] = (m->mem[r1] >> 1) & 0x7F; 55 | set_flags(m, m->mem[r1]); 56 | mark_dirty(m, r1); 57 | break; 58 | 59 | case LSR_ACC: 60 | set_flag(m, FLAG_CARRY, m->ac & 0x01); 61 | m->ac = (m->ac >> 1) & 0x7F; 62 | set_flags(m, m->ac); 63 | break; 64 | 65 | case LSR_ZP: 66 | arg1 = NEXT_BYTE(m); 67 | set_flag(m, FLAG_CARRY, m->mem[arg1] & 0x01); 68 | m->mem[arg1] = (m->mem[arg1] >> 1) & 0x7F; 69 | set_flags(m, m->mem[arg1]); 70 | mark_dirty(m, arg1); 71 | break; 72 | 73 | case LSR_ZPX: 74 | arg1 = ZP(NEXT_BYTE(m) + m->x); 75 | set_flag(m, FLAG_CARRY, m->mem[arg1] & 0x01); 76 | m->mem[arg1] = (m->mem[arg1] >> 1) & 0x7F; 77 | set_flags(m, m->mem[arg1]); 78 | mark_dirty(m, arg1); 79 | break; 80 | 81 | case ROL_AB: 82 | arg1 = NEXT_BYTE(m); 83 | r1 = mem_abs(arg1, NEXT_BYTE(m), 0); 84 | t1 = m->mem[r1] & 0x80; 85 | m->mem[r1] = ((m->mem[r1] << 1) & 0xFE) | get_flag(m, FLAG_CARRY); 86 | set_flag(m, FLAG_CARRY, t1); 87 | set_flags(m, m->mem[r1]); 88 | mark_dirty(m, r1); 89 | break; 90 | 91 | case ROL_ABX: 92 | arg1 = NEXT_BYTE(m); 93 | r1 = mem_abs(arg1, NEXT_BYTE(m), m->x); 94 | t1 = m->mem[r1] & 0x80; 95 | m->mem[r1] = ((m->mem[r1] << 1) & 0xFE) | get_flag(m, FLAG_CARRY); 96 | set_flag(m, FLAG_CARRY, t1); 97 | set_flags(m, m->mem[r1]); 98 | mark_dirty(m, r1); 99 | break; 100 | 101 | case ROL_ACC: 102 | t1 = m->ac & 0x80; 103 | m->ac = ((m->ac << 1) & 0xFE) | get_flag(m, FLAG_CARRY); 104 | set_flag(m, FLAG_CARRY, t1); 105 | set_flags(m, m->ac); 106 | break; 107 | 108 | case ROL_ZP: 109 | arg1 = NEXT_BYTE(m); 110 | t1 = m->mem[arg1] & 0x80; 111 | m->mem[arg1] = ((m->mem[arg1] << 1) & 0xFE) | get_flag(m, FLAG_CARRY); 112 | set_flag(m, FLAG_CARRY, t1); 113 | set_flags(m, m->mem[arg1]); 114 | mark_dirty(m, arg1); 115 | break; 116 | 117 | case ROL_ZPX: 118 | arg1 = ZP(NEXT_BYTE(m) + m->x); 119 | t1 = m->mem[arg1] & 0x80; 120 | m->mem[arg1] = ((m->mem[arg1] << 1) & 0xFE) | get_flag(m, FLAG_CARRY); 121 | set_flag(m, FLAG_CARRY, t1); 122 | set_flags(m, m->mem[arg1]); 123 | mark_dirty(m, arg1); 124 | break; 125 | 126 | case ROR_AB: 127 | arg1 = NEXT_BYTE(m); 128 | r1 = mem_abs(arg1, NEXT_BYTE(m), 0); 129 | t1 = m->mem[r1] & 0x01; 130 | m->mem[r1] = ((m->mem[r1] >> 1) & 0x7F) | (get_flag(m, FLAG_CARRY) << 7); 131 | set_flag(m, FLAG_CARRY, t1); 132 | set_flags(m, m->mem[r1]); 133 | mark_dirty(m, r1); 134 | break; 135 | 136 | case ROR_ABX: 137 | arg1 = NEXT_BYTE(m); 138 | r1 = mem_abs(arg1, NEXT_BYTE(m), m->x); 139 | t1 = m->mem[r1] & 0x01; 140 | m->mem[r1] = ((m->mem[r1] >> 1) & 0x7F) | (get_flag(m, FLAG_CARRY) << 7); 141 | set_flag(m, FLAG_CARRY, t1); 142 | set_flags(m, m->mem[r1]); 143 | mark_dirty(m, r1); 144 | break; 145 | 146 | case ROR_ACC: 147 | t1 = m->ac & 0x01; 148 | m->ac = ((m->ac >> 1) & 0x7F) | (get_flag(m, FLAG_CARRY) << 7); 149 | set_flag(m, FLAG_CARRY, t1); 150 | set_flags(m, m->ac); 151 | break; 152 | 153 | case ROR_ZP: 154 | arg1 = NEXT_BYTE(m); 155 | t1 = m->mem[arg1] & 0x01; 156 | m->mem[arg1] = ((m->mem[arg1] >> 1) & 0x7F) | (get_flag(m, FLAG_CARRY) << 7); 157 | set_flag(m, FLAG_CARRY, t1); 158 | set_flags(m, m->mem[arg1]); 159 | mark_dirty(m, arg1); 160 | break; 161 | 162 | case ROR_ZPX: 163 | arg1 = ZP(NEXT_BYTE(m) + m->x); 164 | t1 = m->mem[arg1] & 0x01; 165 | m->mem[arg1] = ((m->mem[arg1] >> 1) & 0x7F) | (get_flag(m, FLAG_CARRY) << 7); 166 | set_flag(m, FLAG_CARRY, t1); 167 | set_flags(m, m->mem[arg1]); 168 | mark_dirty(m, arg1); 169 | break; 170 | -------------------------------------------------------------------------------- /opcodes.h: -------------------------------------------------------------------------------- 1 | #ifndef __6502_OPCODES__ 2 | #define __6502_OPCODES__ 3 | 4 | #define OPCODE_CYCLES(opcode) 1 5 | 6 | // Opcode constants that have multiple addressing modes are named in the form 7 | // X_Y, where X is the instruction and Y is the addressing mode. The possible 8 | // addressing modes are: 9 | // AB: absolute mode, next two bytes are the low/high byte of an absolute 10 | // memory address 11 | // ABX: absolute,X, next two bytes are added to the value in register X to 12 | // get the memory address 13 | // ABY: same as ABX, except the value of register Y is used instead of X 14 | // ACC: accumulator, act on the value in the accumulator 15 | // IMM: immediate, next byte is a constant to be used instead of a lookup 16 | // IN: indirect, next two bytes are an absolute memory address of the 17 | // lower nibble of a memory address. That byte and the byte after will 18 | // be loaded and the address made of those two bytes will be used 19 | // INX: (indirect,X) mode, add X to the following byte, modulo 0xFF, and 20 | // lookup two bytes starting at that location. Those two bytes form 21 | // the memory address that will be used 22 | // INY: (indirect),Y mode, look up two bytes starting at address in the 23 | // following byte, add Y modulo 0xFFFF, and use the result as an 24 | // address 25 | // REL: relative, next byte contains a signed offset from the current PC. 26 | // ZP: zero-page, next byte is the low bits of the memory address (can 27 | // only address first 256 bytes of memory using ZP) 28 | // ZPX: zero-page,X, add next byte to X modulo 0xFF and use that as a 29 | // memory address 30 | 31 | // custom instruction set extensions 32 | #define DUMP 0xFC 33 | #define EXT 0xFF 34 | 35 | #define ADC_AB 0x6D 36 | #define ADC_ABX 0x7D 37 | #define ADC_ABY 0x79 38 | #define ADC_IMM 0x69 39 | #define ADC_INX 0x61 40 | #define ADC_INY 0x71 41 | #define ADC_ZP 0x65 42 | #define ADC_ZPX 0x75 43 | 44 | #define AND_AB 0x2D 45 | #define AND_ABX 0x3D 46 | #define AND_ABY 0x39 47 | #define AND_IMM 0x29 48 | #define AND_INX 0x21 49 | #define AND_INY 0x31 50 | #define AND_ZP 0x25 51 | #define AND_ZPX 0x35 52 | 53 | #define ASL_AB 0x0E 54 | #define ASL_ABX 0x1E 55 | #define ASL_ACC 0x0A 56 | #define ASL_ZP 0x06 57 | #define ASL_ZPX 0x16 58 | 59 | #define BCC_REL 0x90 60 | #define BCS_REL 0xB0 61 | #define BEQ_REL 0xF0 62 | 63 | #define BIT_AB 0x2C 64 | #define BIT_ZP 0x24 65 | 66 | #define BMI_REL 0x30 67 | #define BNE_REL 0xD0 68 | #define BPL_REL 0x10 69 | 70 | #define BRK 0x00 71 | 72 | #define BVC_REL 0x50 73 | #define BVS_REL 0x70 74 | 75 | #define CLC 0x18 76 | #define CLD 0xD8 77 | #define CLI 0x58 78 | #define CLV 0xB8 79 | 80 | #define CMP_AB 0xCD 81 | #define CMP_ABX 0xDD 82 | #define CMP_ABY 0xD9 83 | #define CMP_IMM 0xC9 84 | #define CMP_INX 0xC1 85 | #define CMP_INY 0xD1 86 | #define CMP_ZP 0xC5 87 | #define CMP_ZPX 0xD5 88 | 89 | #define CPX_AB 0xEC 90 | #define CPX_IMM 0xE0 91 | #define CPX_ZP 0xE4 92 | 93 | #define CPY_AB 0xCC 94 | #define CPY_IMM 0xC0 95 | #define CPY_ZP 0xC4 96 | 97 | #define DEC_AB 0xCE 98 | #define DEC_ABX 0xDE 99 | #define DEC_ZP 0xC6 100 | #define DEC_ZPX 0xD6 101 | 102 | #define DEX 0xCA 103 | #define DEY 0x88 104 | 105 | #define EOR_AB 0x4D 106 | #define EOR_ABX 0x5D 107 | #define EOR_ABY 0x59 108 | #define EOR_IMM 0x49 109 | #define EOR_INX 0x41 110 | #define EOR_INY 0x51 111 | #define EOR_ZP 0x45 112 | #define EOR_ZPX 0x55 113 | 114 | #define INC_AB 0xEE 115 | #define INC_ABX 0xFE 116 | #define INC_ZP 0xE6 117 | #define INC_ZPX 0xF6 118 | 119 | #define INX 0xE8 120 | #define INY 0xC8 121 | 122 | #define JMP_AB 0x4C 123 | #define JMP_IN 0x6C 124 | 125 | #define JSR_AB 0x20 126 | 127 | #define LDA_AB 0xAD 128 | #define LDA_ABX 0xBD 129 | #define LDA_ABY 0xB9 130 | #define LDA_IMM 0xA9 131 | #define LDA_INX 0xA1 132 | #define LDA_INY 0xB1 133 | #define LDA_ZP 0xA5 134 | #define LDA_ZPX 0xB5 135 | 136 | #define LDX_AB 0xAE 137 | #define LDX_ABY 0xBE 138 | #define LDX_IMM 0xA2 139 | #define LDX_ZP 0xA6 140 | #define LDX_ZPY 0xB6 141 | 142 | #define LDY_AB 0xAC 143 | #define LDY_ABX 0xBC 144 | #define LDY_IMM 0xA0 145 | #define LDY_ZP 0xA4 146 | #define LDY_ZPX 0xB4 147 | 148 | #define LSR_AB 0x4E 149 | #define LSR_ABX 0x5E 150 | #define LSR_ACC 0x4A 151 | #define LSR_ZP 0x46 152 | #define LSR_ZPX 0x56 153 | 154 | #define ORA_IMM 0x09 155 | #define ORA_ZP 0x05 156 | #define ORA_ZPX 0x15 157 | #define ORA_AB 0x0D 158 | #define ORA_ABX 0x1D 159 | #define ORA_ABY 0x19 160 | #define ORA_INX 0x01 161 | #define ORA_INY 0x11 162 | 163 | #define NOP 0xEA 164 | 165 | #define PHA 0x48 166 | #define PHP 0x08 167 | #define PLA 0x68 168 | #define PLP 0x28 169 | 170 | #define ROL_AB 0x2E 171 | #define ROL_ABX 0x3E 172 | #define ROL_ACC 0x2A 173 | #define ROL_ZP 0x26 174 | #define ROL_ZPX 0x36 175 | 176 | #define ROR_AB 0x6E 177 | #define ROR_ABX 0x7E 178 | #define ROR_ACC 0x6A 179 | #define ROR_ZP 0x66 180 | #define ROR_ZPX 0x76 181 | 182 | #define RTI 0x40 183 | #define RTS 0x60 184 | 185 | #define SBC_IMM 0xE9 186 | #define SBC_ZP 0xE5 187 | #define SBC_ZPX 0xF5 188 | #define SBC_AB 0xED 189 | #define SBC_ABX 0xFD 190 | #define SBC_ABY 0xF9 191 | #define SBC_INX 0xE1 192 | #define SBC_INY 0xF1 193 | 194 | #define SEC 0x38 195 | #define SED 0xF8 196 | #define SEI 0x78 197 | 198 | #define STA_AB 0x8D 199 | #define STA_ABX 0x9D 200 | #define STA_ABY 0x99 201 | #define STA_INX 0x81 202 | #define STA_INY 0x91 203 | #define STA_ZP 0x85 204 | #define STA_ZPX 0x95 205 | 206 | #define STX_ZP 0x86 207 | #define STX_ZPY 0x96 208 | #define STX_AB 0x8E 209 | 210 | #define STY_ZP 0x84 211 | #define STY_ZPX 0x94 212 | #define STY_AB 0x8C 213 | 214 | #define TAX 0xAA 215 | #define TAY 0xA8 216 | #define TSX 0xBA 217 | #define TXA 0x8A 218 | #define TXS 0x9A 219 | #define TYA 0x98 220 | 221 | #define WAI 0xCB 222 | 223 | #endif 224 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | +-------------------------------------------------------------+ 2 | | | 3 | | x6502 | 4 | | a simple 6502 CPU emulator | 5 | | | 6 | +-------------------------------------------------------------+ 7 | 8 | x6502 is an emulator for the 6502 class of processors. 9 | It currently supports the full instruction set of the 10 | 6502 (plus a few extensions) and has a rudimentary 11 | simulated I/O bus. It should be able to run arbitrary 12 | x6502 bytecode with ``correct'' results, although most 13 | binaries for common 6502 systems (Atari, C64, Apple II, 14 | etc) won't function as expected, since they expect I/O 15 | devices to be mapped into memory where there are 16 | currently none. 17 | 18 | x6502 is freely available under the original 4-clause 19 | BSD license, the full text of which is included in the 20 | LICENSE file. 21 | 22 | Building and running x6502 23 | 24 | To build x6502, just run `make' in the project root. You 25 | will need clang and Python installed. To use gcc, change 26 | the $(CC) var in the Makefile. No libraries beyond POSIX 27 | libc are used. This will produce the x6502 binary. 28 | 29 | x6502 takes the compiled 6502 object file as an 30 | argument, and runs it until it encounters an EXT 31 | instruction (EXT instructions are an extension to 6502 32 | bytecode, see below). You can use any 6502 assembler to 33 | compile to 6502 bytecode; `xa' is one that is bundled 34 | with Debian-based distros. Note that, by default, x6502 35 | loads code in at address 0x1000; you therefore need to 36 | either tell your assembler that that's the base address 37 | for the text section of your binary or override the 38 | default load address using the `-b' flag of x6502. Note 39 | that 0x1000 is the default load address for the `xa' 40 | assembler. 41 | 42 | If you want to compile a version of x6502 that dumps 43 | machine state after every instruction, run `make debug' 44 | instead of `make'. This will also disable compiler 45 | optimizations. This mode really does not play nice with 46 | vterm mode, so be warned. 47 | 48 | Extensions to the 6502 instruction set 49 | 50 | x6502 recognizes two instructions that are not in the 51 | original 6502 instruction set. These are: 52 | 53 | DEBUG (0xFC): prints debugging information about the 54 | current state of the emulator 55 | EXT (0xFF): stops the emulator and exits 56 | 57 | Both of these are defined as macros in `stdlib/stdio.s'. 58 | To disable these extensions, compile with 59 | -DDISABLE_EXTENSIONS (right now, this can be done by 60 | adding that flag to the Makefile). 61 | 62 | This also implements a subset of the 65C02 and 65C816 63 | instruction set, in particular the WAI (0xCB) 64 | instruction. The WAI instruction pauses the emulator 65 | until an I/O interrupt is thrown. 66 | 67 | I/O memory map: 68 | 69 | There are four I/O devices right now: a character input 70 | device, a character output device, a virtual terminal 71 | and a block device. Convenience constants and macros for 72 | the character I/O devices are defined in 73 | `stdlib/stdio.s' for use in user programs. Add stdlib to 74 | your include path and then add `#include ' to 75 | your program to use these constants. 76 | 77 | I/O options are controlled by setting bits on the I/O 78 | flag byte at address 0xFF02. The current set of 79 | supported flags are: 80 | 81 | VTERM_ENABLE (0x01): 82 | when set, activates vterm mode. 83 | WAIT_HALT (0x02): 84 | when set, waits for a keypress input before 85 | terminating upon receiving an EXT instruction. 86 | Non-vterm applications will probably want to set 87 | this flag, as some implementations of ncurses 88 | will clear the display when the emulator exits. 89 | 90 | When outputting characters, you can control the 91 | ``paint'' with which the characters are drawn. You can 92 | do so by modifying the PAINT flag at location 0xFEE8. 93 | Paints are an OR-ing of a color (bottom 4 bits) and a 94 | style (top 4 bits). Supported colors are: 95 | 96 | PAINT_BLACK 0x00 97 | PAINT_RED 0x01 98 | PAINT_GREEN 0x02 99 | PAINT_YELLOW 0x03 100 | PAINT_BLUE 0x04 101 | PAINT_MAGENTA 0x05 102 | PAINT_CYAN 0x06 103 | PAINT_WHITE 0x07 104 | 105 | Supported styles are: 106 | 107 | PAINT_DIM 0x20 108 | PAINT_UNDERLINE 0x40 109 | PAINT_BOLD 0x80 110 | 111 | Thus, as an example, an underlined, bold green character 112 | would have paint 0xC2. 113 | 114 | I/O devices: 115 | 116 | The character output device is mapped to 0xFF00. Any 117 | character written to FF00 is immediately echoed to the 118 | terminal. 119 | 120 | The character input device is mapped to 0xFF01. When a 121 | character is available on standard in, an interrupt is 122 | raised and FF01 is set to the character that was 123 | received. Note that one character is delivered per 124 | interrupt; if the user types ``abc'', they will get 125 | three interrupts one after the other. 126 | 127 | The virtual terminal is activated by setting the 128 | VTERM_ENABLE bit on the IO flag byte. After the flag is 129 | set, the data in memory addresses 0xFB00 through 0xFEE7 130 | are mapped to a 40x25 grid in the host terminal. Data in 131 | this region is stored in row-major format, and any write 132 | will trigger a refresh of the vterm. 133 | 134 | Note that even in vterm-mode, the putchar-esque 135 | character output device is still usable, and will put 136 | the character at the position directly after the 137 | position of the last write. 138 | 139 | A commented example of how to use the character I/O 140 | capabilities of x6502 is provided in 141 | sample_programs/echo.s, and an example of a vterm 142 | application is provided in sample_programs/spam.s 143 | 144 | A block device can be mapped in with control addresses 145 | at 0xFF03 through 0xFF07. To use the block device, you 146 | must specify a binary disk image to back the device 147 | using the -d flag. To read from the block device, write 148 | an address in the disk image to 0xFF03 and 0xFF04, with 149 | the low byte in 0xFF03. The value at that location in 150 | the disk image will be written to 0xFF05, which your 151 | program can then read. To write, set the memory address 152 | to write to using the same method, then write the 153 | desired byte to 0xFF06. If any of these operations 154 | return an error, the byte at 0xFF07 will be nonzero. 155 | 156 | Reading the source 157 | 158 | x6502 was written to be easy to understand and read. A 159 | good place to start is `cpu.h', which defines a few 160 | constants used throughout the code (mostly around CPU 161 | flags) as well as the `cpu' struct, which is used pretty 162 | much everywhere. 163 | 164 | `emu.c' is where the interesting stuff happens; this is 165 | the main loop of the emulator where opcodes are decoded 166 | as dispatched. It also handles interrupts and calls out 167 | to I/O handlers. 168 | 169 | The code for actual opcode interpretation is a little 170 | strange; there are lots of ``header'' files in the 171 | opcode_handlers directory that are not really header 172 | files at all. These files all contain code for handling 173 | opcode parsing and interpretation; with over 150 174 | opcodes, having all of the code to handle these in one 175 | file would be excessive and difficult to navigate, and 176 | dispatching out to functions to handle each opcode 177 | carries unnecessary overhead in what should be the 178 | tightest loop in the project. Thus, each of these header 179 | files is #included in emu.c in the middle of a switch 180 | statement, and gets access to the local scope within the 181 | main_loop function. It's weird but it gets the job done, 182 | and is the least bad of all considered options. 183 | 184 | The opcode handlers all use convenience functions 185 | defined in `functions.h', most of which are for the 186 | various addressing modes of the 6502 or for dealing with 187 | CPU flags. 188 | 189 | `io.c' is where the I/O bus lives; this is where we 190 | check to see if the emulated character device has been 191 | written to and where we raise an interrupt if we've 192 | gotten input from stdin. 193 | 194 | `generate_debug_names.py' reads the `opcodes.h' header 195 | and generates `debug-names.h', which contains a mapping 196 | from opcode to a string representation of that opcode. 197 | It's only used when dumping CPU state, either because 198 | the DEBUG flag was set at compile time or because a 199 | DEBUG instruction was hit in the binary. 200 | 201 | The rest of the files are pretty boring; `main.c' is 202 | pretty much only responsible for loading bytecode into 203 | memory and parsing command line arguments and `debug.c' is 204 | used to provide the `dump_cpu' function, which is a 205 | fascinating function consisting of almost nothing but 206 | printfs. 207 | 208 | TODO: 209 | - support buffered input, where the program can read 210 | more than one input character at once. 211 | 212 | THANKS: 213 | - voltagex on Github for sending me a patch to improve 214 | the sample_programs readme. 215 | - anatoly on HN for suggesting I add a bit on source 216 | code structure to the README. 217 | - shalmanese for coffee and pie. 218 | - daumiller for finding the subtraction bug. 219 | -------------------------------------------------------------------------------- /sample_programs/diskread/testdisk.bin: -------------------------------------------------------------------------------- 1 | hello, world, from disk! 2 | hello from a seeked position. 3 | --------------------------------------------------------------------------------