completed game

This commit is contained in:
2026-07-20 12:29:15 +03:00
parent 758fac98f6
commit c6e1c5b3b5
26 changed files with 1263 additions and 607 deletions
+13
View File
@@ -0,0 +1,13 @@
name: CI
on: [push, pull_request]
jobs:
core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update
- run: sudo apt-get install -y cc65 libsdl2-dev libsdl2-ttf-dev fonts-dejavu-core
- run: make debug
- run: make sanitize
- run: make test-functional
- run: make games/snake.bin bin/mos6502-sdl
+4
View File
@@ -76,5 +76,9 @@ dkms.conf
bin/
build/
games/snake.bin
snake.highscore
snake.state
tests/roms/*.bin
# End of https://www.toptal.com/developers/gitignore/api/c,visualstudiocode
+3
View File
@@ -0,0 +1,3 @@
{
"snyk.advanced.autoSelectOrganization": true
}
+19 -36
View File
@@ -1,45 +1,28 @@
# CLAUDE.md
# Contributor notes
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
## Commands
```sh
make # build → bin/mos6502
make clean # remove build/ and bin/
make # build bin/mos6502
make test # core checks
make debug # strict warning build and checks
make sanitize # ASan/UBSan build and checks
make snake # assemble and run the SDL game (requires SDL2 + cc65)
```
No test framework is wired up yet. Validation is planned via the **Klaus Dormann functional test suite** (see [docs/design.md](docs/design.md)): load `6502_functional_test.bin` at `$0000`, set PC to `$0400`, and confirm the emulator reaches the success loop rather than a trap.
## Architecture
This is a prototype MOS 6502 CPU emulator written in C. The design document at [docs/design.md](docs/design.md) is the authoritative reference — read it before making architectural changes.
- `CPU6502` contains CPU-visible and interrupt-line state.
- `Bus6502` owns RAM and Easy6502 MMIO; CPU code only uses bus accessors.
- `cpu_step` executes one instruction or interrupt and returns status/opcode/cycles.
- `src/opcode.c` has a 256-entry descriptor table for all 151 official encodings.
- `src/frontend.c` is an optional SDL2 frontend with a dependency-free stub.
- `tests/run_tests.c` is the in-tree test harness; `tests/run_suite.c` runs external ROMs.
### Module layout
The target is the NMOS 6502. Preserve decimal-mode flag behavior, zero-page
wrapping, the indirect-JMP page-wrap bug, stack push order, and page-cross cycle
rules. Do not silently accept undocumented opcodes.
| Module | Header | Source | Responsibility |
|---|---|---|---|
| CPU | [include/cpu.h](include/cpu.h) | [src/cpu.c](src/cpu.c) | `CPU6502` struct (A, X, Y, SP, P, PC), reset, state dump |
| Memory | [include/memory.h](include/memory.h) | [src/memory.c](src/memory.c) | Flat 64 KiB `uint8_t` array; `mem_read`/`mem_write`/`mem_reset` |
| Addressing | [include/addressing.h](include/addressing.h) | [src/addressing.c](src/addressing.c) | `addr_ctx_t` — callback-based bus abstraction (stub) |
| Opcode | [include/opcode.h](include/opcode.h) | [src/opcode.c](src/opcode.c) | Instruction decode/dispatch (stub) |
| Entry point | — | [src/main.c](src/main.c) | Instantiates `CPU6502` + `Memory`, calls reset, prints state |
### Key design decisions (from the design doc)
- **Memory access is always through `mem_read`/`mem_write`**, never direct array indexing from CPU logic. This keeps the door open for memory-mapped I/O and mirroring without touching the CPU.
- **`addr_ctx_t` uses function-pointer callbacks** (`read8`, `read16`) so the addressing layer is decoupled from the concrete `Memory` struct. Pass `userdata` to thread CPU + Memory through the callbacks.
- **Opcode dispatch will use a giant `switch` statement** (256 cases). The design doc explicitly rejects function-pointer tables and computed gotos in favour of compiler-optimised switch.
- **`PC` in `cpu.h` is currently typed `uint8_t`** — this is a known bug; it must be `uint16_t` to address the full 64 KiB space.
- **Status flags** are individual bit masks (`FLAG_C` through `FLAG_N`) applied to `cpu->P`.
- **Reset vector**: after `cpu_reset`, PC must be loaded from `$FFFC/$FFFD`; `cpu.c` currently leaves this for the caller.
- **Stack** lives at `$0100–$01FF`; SP is a page-relative offset, so the absolute address is `0x0100 | cpu->SP`.
- **Little-endian** 16-bit reads: LSB at lower address.
### Planned areas not yet implemented
- Addressing mode resolvers (`addressing.c` is empty)
- Opcode decode/execute loop (`opcode.c` is empty)
- Cycle counting
- Interrupt handling (NMI / IRQ / RESET vectors)
- BCD mode ADC/SBC
The virtual machine exposes game state at `$00FB`, high/game score at `$00FC/$00FD`, random input
at `$00FE`, keyboard input at `$00FF`, and a 32x32 framebuffer at
`$0200-$05FF`.
+74 -15
View File
@@ -1,22 +1,81 @@
CC = gcc
CFLAGS = -Wall -Wextra -O3 -Iinclude
SRC_DIR = src
OBJ_DIR = build
BIN_DIR = bin
CC ?= cc
CPPFLAGS = -Iinclude
CFLAGS ?= -O2 -std=c11 -Wall -Wextra -Wpedantic
SDL_CFLAGS := $(shell pkg-config --cflags sdl2 SDL2_ttf 2>/dev/null)
SDL_LIBS := $(shell pkg-config --libs sdl2 SDL2_ttf 2>/dev/null)
ifneq ($(SDL_LIBS),)
CPPFLAGS += -DHAVE_SDL_UI $(SDL_CFLAGS)
LDLIBS += $(SDL_LIBS)
endif
SOURCES = $(wildcard $(SRC_DIR)/*.c)
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
TARGET = $(BIN_DIR)/mos6502
CORE_SRC = src/addressing.c src/bus.c src/cpu.c src/loader.c src/memory.c src/opcode.c
APP_SRC = $(CORE_SRC) src/frontend.c src/main.c
APP_OBJ = $(APP_SRC:src/%.c=build/%.o)
TARGET = bin/mos6502
SDL_TARGET = bin/mos6502-sdl
.PHONY: all clean test test-rom test-functional fetch-roms debug sanitize snake check-deps FORCE
all: $(TARGET)
$(TARGET): $(OBJECTS)
@mkdir -p $(BIN_DIR)
$(CC) $(OBJECTS) -o $@
$(TARGET): $(APP_OBJ)
@mkdir -p bin
$(CC) $(CFLAGS) $(APP_OBJ) $(LDLIBS) -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@
# A separate target prevents a previously built dependency-free frontend stub
# from being reused after SDL2 is installed.
$(SDL_TARGET): $(APP_SRC)
@mkdir -p bin
$(CC) -Iinclude -DHAVE_SDL_UI $(CFLAGS) $(SDL_CFLAGS) $(APP_SRC) $(SDL_LIBS) -o $@
build/%.o: src/%.c
@mkdir -p build
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
bin/run_tests: FORCE $(CORE_SRC) tests/run_tests.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) -Itests $(CORE_SRC) tests/run_tests.c -o $@
test: bin/run_tests
./bin/run_tests
bin/run_suite: $(CORE_SRC) tests/run_suite.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_suite.c -o $@
test-rom: bin/run_suite
@test -n "$(ROM)" -a -n "$(START)" -a -n "$(SUCCESS)" || (echo "usage: make test-rom ROM=file LOAD=0 START=0x400 SUCCESS=0x3469"; exit 2)
./bin/run_suite "$(ROM)" "$(or $(LOAD),0)" "$(START)" "$(SUCCESS)" "$(or $(LIMIT),100000000)"
tests/roms/6502_functional_test.bin:
@mkdir -p tests/roms
curl -L --fail -o $@ https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/bin_files/6502_functional_test.bin
@echo "fa12bfc761e6f9057e4cc01a665a7b800ff01ae91f598af1e39a1201d01953fd $@" | sha256sum -c -
fetch-roms: tests/roms/6502_functional_test.bin
test-functional: tests/roms/6502_functional_test.bin bin/run_suite
./bin/run_suite $< 0 0x400 0x3469 100000000
debug: CFLAGS = -O0 -g3 -std=c11 -Wall -Wextra -Wpedantic -Werror
debug: clean all test
sanitize: CFLAGS = -O1 -g3 -std=c11 -Wall -Wextra -Wpedantic -fsanitize=address,undefined
sanitize: export ASAN_OPTIONS = detect_leaks=0
sanitize: clean all test
games/snake.bin: games/snake.s games/snake.cfg
ca65 games/snake.s -o build/snake.o
ld65 -C games/snake.cfg build/snake.o -o $@
check-deps:
@pkg-config --exists sdl2 || (echo "SDL2 development package is required"; exit 1)
@pkg-config --exists SDL2_ttf || (echo "SDL2_ttf development package is required (for example: sudo apt install libsdl2-ttf-dev)"; exit 1)
@command -v ca65 >/dev/null || (echo "cc65 (ca65/ld65) is required"; exit 1)
snake: check-deps games/snake.bin $(SDL_TARGET)
./$(SDL_TARGET) games/snake.bin --load 0x0600 --start 0x0600 --hz 1000000 --scale 16
clean:
rm -rf $(OBJ_DIR) $(BIN_DIR)
rm -rf build bin games/snake.bin
FORCE:
+67 -8
View File
@@ -1,15 +1,74 @@
# A mos-6502 emulator in c
[Design Doc ](./docs/design.md)
# MOS 6502 emulator
The source code in this repository has been developed exploratively, in order to validate and refine the ideas laid out in above document. As such, it is incomplete, may occasionally malfunction, and does not yet provide the fine bone porcelain rendering it usable in anger. It's a prototype, if you wish.
A portable NMOS MOS 6502 emulator in C with an Easy6502-compatible virtual
machine and an original Snake game.
## Links
The reusable core implements all 56 documented instructions (151 official
opcode encodings), NMOS decimal arithmetic, IRQ/NMI/BRK, stack behavior,
addressing quirks, and instruction cycle counts. Unsupported opcodes stop with
an explicit error.
http://en.wikipedia.org/wiki/MOS_Technology_6502
## Build and test
http://www.6502.org/documents/datasheets/mos/
```sh
make # core and command-line emulator
make test # unit/integration checks
make test-functional # official Klaus Dormann functional suite
make debug # strict -Werror debug build and tests
make sanitize # AddressSanitizer/UBSan build and tests
```
http://www.mdawson.net/vic20chrome/cpu/mos_6500_mpu_preliminary_may_1976.pdf
The core has no external dependencies. The graphical frontend uses SDL2 and
SDL2_ttf. Building the included game also needs `ca65` and `ld65` from cc65.
On Debian/Ubuntu install `libsdl2-dev`, `libsdl2-ttf-dev`, `fonts-dejavu-core`,
and `cc65`. Set `MOS6502_FONT` to another TrueType font path if desired.
http://rubbermallet.org/fake6502.c
```sh
make snake
```
The startup menu offers Start/Resume, New Game, Reset High Score, and Quit. Use
Up/Down or W/S to select and Enter or Space to activate. During play, WASD or
the arrow keys steer, Space or P pauses, R restarts after game over, and Escape
returns to the menu. D-pad and Start controls are supported on SDL game
controllers. The window title shows the current
score and persistent high score. The high score is stored in
`snake.highscore` in the working directory.
F5 saves the current game and F9 restores it. Food and game-over events have
lightweight synthesized sound effects. Save states are stored in `snake.state`.
The in-game arcade layout includes a framed pixel display, live score and high
score, control legend, responsive resizing, and a full playfield pause overlay.
Run another flat binary with:
```sh
bin/mos6502 program.bin --load 0x0600 --start 0x0600
```
Useful options are `--hz`, `--scale`, `--seed`, `--headless`, and `--trace`.
External functional ROMs can be run without modifying the application:
```sh
make test-rom ROM=6502_functional_test.bin LOAD=0 START=0x400 SUCCESS=0x3469
```
## Virtual machine
The machine provides 64 KiB RAM, a game-state byte at `$00FB`, a high-score byte
at `$00FC`, a game-score byte at `$00FD`, a random byte at `$00FE`, the latest keyboard character at `$00FF`,
and a 32x32 16-color framebuffer at `$0200-$05FF`.
This is deliberately a small game-oriented computer, not an NES emulator.
Original arcade Pac-Man used a Z80; running that ROM would require a different
CPU and hardware platform.
See [the design](docs/design.md) and [project status](docs/plan.md).
## Validation status
The in-tree tests exercise the CPU's major correctness boundaries. External
Klaus Dormann functional and decimal ROM validation is the next hardening gate;
the binaries are not vendored because their redistribution terms must be
respected.
+7 -7
View File
@@ -64,14 +64,14 @@ Upon power-up or a reset signal, the 6502 does not begin execution at a random a
# Opcode Decoding and Instruction Dispatch
The core of the emulator is the execution loop, which performs the Fetch-Decode-Execute sequence. Every 6502 instruction begins with a 1-byte opcode. There are 256 possible opcodes, though only 56 are officially documented.
## Dispatch Decision: Switch vs. Jump Tables
A critical design decision is how to dispatch these 256 opcodes. Three primary methods exist in C:
## Opcode Metadata and Dispatch
1. Giant Switch Statement: A 256-case switch block. Most compilers optimize this into a highly efficient jump table. It is readable and allows for easy inlining of small instruction logic.
1. Array of Function Pointers: An array where `dispatch[opcode]` points to a dedicated function. This is modular but incurs function call overhead and can be slower on modern CPUs due to pointer dereferencing.
1. Computed Gotos: A GCC extension (`goto *labels[opcode]`). This is the fastest for high-performance interpreters but is non-standard.
For this design document, a Giant Switch Statement is recommended. This approach balances performance, maintainability, and portability, allowing the compiler to perform optimal branch prediction.
The implementation uses a 256-entry metadata table. Each official opcode maps
to an operation, addressing mode, base cycle count, and page-cross policy. A
shared operation switch then executes the decoded instruction. This keeps all
151 official encodings auditable without duplicating instruction behavior or
using non-standard computed gotos. Empty table entries are deterministic
illegal-opcode errors.
# Addressing Mode Architecture
Addressing modes are the rules for calculating the effective memory address of an operand. Decoupling these from the instruction logic is essential to avoid a combinatorial explosion of code (e.g., LDA has 8 different opcodes for different modes).
+22 -476
View File
@@ -1,483 +1,29 @@
# MOS 6502 Emulator — Comprehensive Implementation Plan
# Project status
## Context
## Implemented
The repository at `/home/ian/dev/mos-6502` is a prototype MOS 6502 emulator in C. The skeleton compiles and a basic `Memory` + `CPU6502` exists, but the fetch–decode–execute loop, addressing modes, and instruction set are absent. The authoritative architecture spec lives at [docs/design.md](../../dev/mos-6502/docs/design.md).
- 64 KiB bus, reset vectors, deterministic RNG and Easy6502 keyboard/framebuffer MMIO.
- All official NMOS 6502 instructions and opcode encodings.
- Binary and NMOS decimal ADC/SBC, status flags, stack, BRK/RTI, IRQ and NMI.
- All official addressing modes, zero-page wrap, page penalties and indirect-JMP bug.
- Instruction-level cycle accounting and deterministic illegal-opcode errors.
- Safe flat-binary loader, headless runner, optional SDL2 frontend.
- Original ca65 Snake game with pause, persistent high score, and a reproducible `make snake` target.
- Strict debug, sanitizer and dependency-free test targets.
**Goal:** build a cycle-accurate NMOS 6502 emulator that passes the Klaus Dormann functional, decimal, and interrupt test suites, plus the `nestest` CPU log, and finally runs a real 6502 game (the canonical "6502 Snake" by Nick Morgan) end-to-end on a minimal memory-mapped framebuffer + keyboard.
## Remaining hardening
The plan is organised into phases that ship and verify independently. A future Claude instance should execute one phase at a time and only proceed once that phase's verification step passes.
1. Run the Klaus Dormann decimal suite and fix any discovered edge cases.
2. Expand table-driven tests to cover every official opcode/address-mode pairing.
3. Add a trace/disassembler runner for diagnosing external ROM failures.
---
The official Klaus Dormann functional suite passes at `$3469` after 30,646,176
instructions. Linux CI covers strict, sanitizer, functional-ROM, Snake, and SDL
UI builds.
## Phase 0 — Already Implemented (status snapshot)
## Future platforms
The repo skeleton compiles. What is in place:
- **Build system** ([Makefile](../../dev/mos-6502/Makefile)): `gcc -Wall -Wextra -O3 -Iinclude`, globs `src/*.c` → `build/*.o` → `bin/mos6502`. `make clean` wipes `build/` and `bin/`. New `.c` files under `src/` are picked up automatically.
- **Memory module** ([include/memory.h](../../dev/mos-6502/include/memory.h), [src/memory.c](../../dev/mos-6502/src/memory.c)): `Memory` is `uint8_t data[65536]`; `mem_read`, `mem_write`, `mem_reset` (memset zero) are direct array operations.
- **CPU module** ([include/cpu.h](../../dev/mos-6502/include/cpu.h), [src/cpu.c](../../dev/mos-6502/src/cpu.c)): `CPU6502` struct with A, X, Y, SP, PC, P; flag masks `FLAG_C/Z/I/D/B/U/V/N`; `cpu_reset` zeroes A/X/Y, sets `SP=0xFD`, `P=FLAG_U`, `PC=0x0000` (does **not** load reset vector); `cpu_print_state` dumps registers.
- **Addressing context stub** ([include/addressing.h](../../dev/mos-6502/include/addressing.h)): defines `addr_ctx_t` with `read8`, `read16`, `userdata` — a callback-based bus abstraction so addressing logic never touches `Memory` directly. `src/addressing.c` is empty.
- **Opcode module**: `src/opcode.c` is empty; `include/opcode.h` exists but is empty.
- **Entry point** ([src/main.c](../../dev/mos-6502/src/main.c)): `mem_reset` → `cpu_reset` → `cpu_print_state` → exit. No fetch–decode–execute loop.
### Bugs / discrepancies to carry into Phase 1
1. **`CPU6502.PC` is `uint8_t`** at [include/cpu.h:12](../../dev/mos-6502/include/cpu.h#L12). Must be `uint16_t`.
2. **`addr_ctx_t.read16` returns `void`** at [include/addressing.h:9](../../dev/mos-6502/include/addressing.h#L9). Must return `uint16_t`.
3. **SP reset value**: design doc says `0xFF`, code says `0xFD`. Real silicon converges on `0xFD` after the boot sequence — keep `0xFD`, treat the doc line as imprecise.
4. **Reset vector not loaded** — `cpu_reset` leaves `PC=0x0000` instead of fetching `$FFFC/$FFFD`.
---
## Phase 1 — Foundation Fixes
Correct types and reset semantics. No new behaviour.
**Files**: [include/cpu.h](../../dev/mos-6502/include/cpu.h), [include/addressing.h](../../dev/mos-6502/include/addressing.h), [src/cpu.c](../../dev/mos-6502/src/cpu.c), [src/main.c](../../dev/mos-6502/src/main.c).
**Changes**
- `uint8_t PC` → `uint16_t PC`.
- `read16` callback → `uint16_t (*read16)(uint16_t addr, void *userdata);`.
- `cpu_reset(CPU6502 *cpu, Memory *mem)` — after register init, `cpu->PC = mem_read(mem,0xFFFC) | (mem_read(mem,0xFFFD)<<8);`. `P = FLAG_U | FLAG_I` (I disabled at reset).
**Verify**: Write `0x34, 0x12` to `$FFFC/D` in `main.c`, run, confirm `PC: 0x1234`.
Reference: design.md §"Memory Map and Reserved Vectors", §"The Reset Sequence".
---
## Phase 2 — Memory Bus Abstraction
Every CPU access flows through `addr_ctx_t`. After this, swapping `Memory` for an MMIO bus is one line.
**Files**: [include/addressing.h](../../dev/mos-6502/include/addressing.h), [src/addressing.c](../../dev/mos-6502/src/addressing.c), new `src/bus.c`, [src/main.c](../../dev/mos-6502/src/main.c).
**Additions**
- `addr_ctx_t` gains `void (*write8)(uint16_t, uint8_t, void*)` and a `CPU6502 *cpu` field (resolvers need PC).
- `src/bus.c`: `bus_read8`, `bus_write8`, `bus_read16` — concrete `Memory*` callbacks.
- `addr_ctx_init(addr_ctx_t*, CPU6502*, Memory*)`.
**Verify**: `grep -n 'mem_read\|mem_write' src/cpu.c src/opcode.c` returns nothing (CPU never touches `Memory` directly). Reset-vector smoke test still passes through the ctx.
Reference: design.md §"Memory Access Implementation".
---
## Phase 3 — Test Harness Foundation (NEW)
Stand up an in-tree minimal test harness **before** writing the instruction set, so every later phase has assert-style verification. No external dependency — a header-only `assert(cond, msg)` is enough.
**Files**: new `tests/` directory, new `tests/test_framework.h`, new `tests/run_tests.c`, Makefile additions.
**Test framework**
- `tests/test_framework.h` provides:
- `TEST(name)` — macro that registers a function pointer in a static table.
- `EXPECT_EQ(actual, expected)`, `EXPECT_HEX8`, `EXPECT_HEX16`, `EXPECT_FLAG_SET(p,f)`, `EXPECT_FLAG_CLEAR(p,f)`.
- On failure, print `file:line — got 0xAB, expected 0xCD` and continue, summing pass/fail at end.
- `tests/run_tests.c` is the entry point that calls every registered test. Each module gets a `tests/test_<module>.c` (cpu, memory, addressing, opcode_loads, opcode_alu, …).
**Makefile additions**
- `bin/run_tests` target: compiles `src/*.c` *minus* `main.c`, plus `tests/*.c`, with `-Itests`.
- `make test` → builds and runs `bin/run_tests`, exits non-zero if any test fails.
- `make` (default `all`) builds both `bin/mos6502` and `bin/run_tests`.
**Initial tests** (verify Phases 1–2)
- `test_memory.c`: round-trip `mem_write` → `mem_read`; `mem_reset` zeroes everything.
- `test_cpu_reset.c`: reset vector at `$FFFC/D` is honoured; SP=0xFD; flags = `U|I`.
**Verify**: `make test` passes both initial tests.
---
## Phase 4 — Addressing Mode Resolvers
One resolver per mode, each returning effective address + page-cross flag.
**Files**: [include/addressing.h](../../dev/mos-6502/include/addressing.h), [src/addressing.c](../../dev/mos-6502/src/addressing.c), `tests/test_addressing.c`.
**API**
```c
typedef struct { uint16_t addr; uint8_t page_crossed; } addr_result_t;
addr_result_t am_imm (addr_ctx_t *c);
addr_result_t am_zp (addr_ctx_t *c);
addr_result_t am_zpx (addr_ctx_t *c);
addr_result_t am_zpy (addr_ctx_t *c);
addr_result_t am_abs (addr_ctx_t *c);
addr_result_t am_absx(addr_ctx_t *c);
addr_result_t am_absy(addr_ctx_t *c);
addr_result_t am_ind (addr_ctx_t *c); // emulate $xxFF page-wrap bug
addr_result_t am_inx (addr_ctx_t *c); // ZP wraps
addr_result_t am_iny (addr_ctx_t *c); // ZP wraps; sets page_crossed
int8_t am_rel (addr_ctx_t *c);
```
**NMOS quirks baked in now**
- `JMP ($xxFF)` reads high byte from `$xx00`, not `$(xxFF+1)`.
- ZP-wrap on ZPX/ZPY/INX/INY: pointer math stays in `$00–$FF`.
**Tests** (`tests/test_addressing.c`)
- Each mode: set up memory + registers, call resolver, assert `addr` and `page_crossed`.
- Edge cases: ZPX wrap (`zp=0xFF, X=0x02 → 0x01`); INY page-cross (`base=0x12FF, Y=0x01 → 0x1300, page_crossed=1`); JMP indirect at `$10FF`.
**Verify**: `make test` — addressing suite green.
Reference: design.md §"Addressing Mode Architecture".
---
## Phase 5 — Fetch-Decode-Execute Skeleton
Runnable loop, 256-case switch, `default:` traps.
**Files**: [include/opcode.h](../../dev/mos-6502/include/opcode.h), [src/opcode.c](../../dev/mos-6502/src/opcode.c), [src/main.c](../../dev/mos-6502/src/main.c).
**API**
```c
int cpu_step(CPU6502 *cpu, addr_ctx_t *ctx); // cycles used, -1 illegal
void cpu_run (CPU6502 *cpu, addr_ctx_t *ctx); // until trap or self-loop
```
`main.c` switches from one-shot dump to `cpu_run`.
**Tests**: hand-assemble `LDA #$42; BRK` (`A9 42 00`) at `$0400`, expect `A=0x42` after one step (BRK is a sentinel until Phase 8).
Reference: design.md §"Opcode Decoding and Instruction Dispatch".
---
## Phase 6 — Instruction Set in Waves
Each wave is a separately verifiable sub-phase with its own `tests/test_opcode_<wave>.c`.
### 6a — Loads / Stores
`LDA, LDX, LDY, STA, STX, STY`. Loads update Z/N; stores do not.
### 6b — Register Transfers
`TAX, TAY, TXA, TYA, TSX, TXS`. TXS does **not** set flags.
### 6c — Logic
`AND, ORA, EOR`. `BIT`: bit 7→N, bit 6→V, Z from `A&M`.
### 6d — Shifts / Rotates
`ASL, LSR, ROL, ROR` — accumulator and memory variants. C from outgoing bit.
### 6e — Increments / Decrements
`INC, DEC, INX, INY, DEX, DEY`.
### 6f — Branches
`BPL, BMI, BVC, BVS, BCC, BCS, BNE, BEQ`. Reserve cycle accounting `2 + taken + page_crossed`.
### 6g — Jumps / Subroutines
`JMP abs`, `JMP indirect` (with page-wrap bug), `JSR`, `RTS`. JSR pushes `PC-1`; RTS pops and `PC++`. Stack at `0x0100 | SP`.
### 6h — Stack
`PHA, PLA, PHP, PLP`. PHP pushes `B|U` set; PLP ignores B, forces U. Static helpers `push8/pull8/push16/pull16`.
### 6i — Flag Set/Clear
`CLC, SEC, CLI, SEI, CLD, SED, CLV`.
### 6j — Compares
`CMP, CPX, CPY`. C set if `R >= M`; N/Z from low byte of `R-M`.
### 6k — ADC / SBC (binary only)
ADC: `s=A+M+C; C=s>0xFF; V=(~(A^M)&(A^s)&0x80)!=0; A=s&0xFF`. SBC = ADC with `M^0xFF`. Assert `!(P&FLAG_D)` until Phase 7.
### 6l — NOP
`0xEA`. All other unimplemented opcodes still trap.
**Per-wave verification**
- Each wave ships ≥ 6 micro-tests covering happy-path + flag edge cases + addressing-mode coverage.
- After 6k, `bin/mos6502 6502_functional_test.bin` runs far into the suite (will trap in BCD or interrupts — expected).
Reference: design.md §"Implementation of the Instruction Set", §"The Arithmetic Logic Unit", §"Control Flow, Branches, and the Stack".
---
## Phase 7 — BCD Mode (Bruce Clark)
ADC/SBC honour the D flag.
**Files**: [src/opcode.c](../../dev/mos-6502/src/opcode.c), `tests/test_bcd.c`.
Split into `adc_binary/adc_decimal/sbc_binary/sbc_decimal`; public ADC/SBC selects on `P&FLAG_D`.
**NMOS specifics**
- N, V, Z computed from the **binary intermediate**; only C reflects decimal carry-out.
- SBC decimal: nibble subtract with borrow → adjust by `-6` / `-0x60`.
**Tests**
- `49 + 51 = 00 C=1`; `12 - 03 = 09`; `99 + 01 = 00 C=1`; `00 - 01 = 99 C=0`.
- Smoke-run `6502_decimal_test.bin` (loader from Phase 11; for now wire a one-shot in `tests/`).
Reference: design.md §"Advanced Decimal Mode (BCD)".
---
## Phase 8 — Interrupts (NMI, IRQ, BRK, RTI)
**Files**: [include/cpu.h](../../dev/mos-6502/include/cpu.h) (add `nmi_pending`, `irq_line`), [src/opcode.c](../../dev/mos-6502/src/opcode.c).
**Sequence per design.md §"Interrupt Sequence Logic"**
1. Push PCH then PCL.
2. Push P (BRK pushes `B|U` set; hardware push has B clear, U set).
3. Set `FLAG_I`.
4. Load PC from vector (`$FFFA/B` NMI, `$FFFE/F` IRQ/BRK).
5. BRK skips one byte (push `PC+2`).
RTI pops P (force U set, B clear) then PCL then PCH; **no** `+1` (unlike RTS).
**Step-boundary priority**: NMI > IRQ (only if `!I`) > normal fetch.
**Tests**
- NMI: set `nmi_pending`, step once, assert push order on stack and PC reload.
- BRK + RTI round-trip restores PC and flags (modulo B/U canonicalisation).
- IRQ masked while `I=1`, fires when cleared.
Reference: design.md §"Interrupts and Hardware Interactions".
---
## Phase 9 — Cycle Counting
`cpu_step` returns accurate cycle counts.
**Files**: [src/opcode.c](../../dev/mos-6502/src/opcode.c).
- `static const uint8_t base_cycles[256]` (canonical table).
- `+1` if `page_crossed` for **read** ops on ABSX/ABSY/INY (LDA/LDX/LDY/EOR/AND/ORA/ADC/SBC/CMP/BIT). Stores never get the penalty.
- Branches: `+1` taken, `+1` more if page crossed.
**Tests** (`tests/test_cycles.c`)
- `LDA #$xx`=2; `LDA $xxxx`=4; `LDA $xxxx,X` page-crossed=5; `JSR`=6; taken+page-cross branch=4.
Reference: design.md §"Cycle-Level Timing Accuracy".
---
## Phase 10 — Undocumented (Illegal) Opcodes
Klaus Dormann's *functional* test does not require these, but `nestest` and many real games do. Implement the stable subset.
**Files**: [src/opcode.c](../../dev/mos-6502/src/opcode.c), `tests/test_undocumented.c`.
**Stable opcodes to support**
- `LAX` (LDA + LDX combined load)
- `SAX` (store `A & X`)
- `DCP` (DEC + CMP)
- `ISB` / `ISC` (INC + SBC)
- `SLO` (ASL + ORA)
- `RLA` (ROL + AND)
- `SRE` (LSR + EOR)
- `RRA` (ROR + ADC)
- `ANC`, `ALR`, `ARR`, `AXS` (immediate-mode oddballs)
- All unstable / "magic constant" opcodes (`SHA`, `SHX`, `SHY`, `TAS`, `LAS`, `XAA`) → log + treat as NOP for now (rarely used, behaviour varies by die).
**Verify**: dedicated unit tests + `nestest` log compare in Phase 11.
---
## Phase 11 — Validation Suites
Drive the emulator against published test ROMs. Each is a separate Make target so failures are bisectable.
**Files**: new `src/loader.c` + `include/loader.h`, `tests/run_suite.c` (CLI: `bin/run_suite <bin> <load_addr> <start_pc> [--success-pc <pc>]`), Makefile targets.
**Loader**
- `int load_binary(Memory*, const char *path, uint16_t load_addr)` — fread into `mem->data + load_addr`.
**Trap detection** (extends `cpu_run`)
- If the same opcode at the same PC executes twice in a row and is a self-branch (`BNE *`, `JMP *`), halt and report `success` if PC matches `--success-pc`, else `trap at 0xXXXX` with last 8 PCs as breadcrumbs.
**Suites integrated**
| Suite | Source | Load | Start PC | Success PC | Make target |
|---|---|---|---|---|---|
| `6502_functional_test.bin` | Klaus Dormann | `$0000` | `$0400` | `$3469` | `make test-functional` |
| `6502_decimal_test.bin` | Bruce Clark | `$0200` | `$0200` | self-loop on `ERROR=0` | `make test-decimal` |
| `6502_interrupt_test.bin` | Klaus Dormann | `$000A` | `$0400` | documented in suite | `make test-interrupts` |
| `nestest.nes` (CPU-only mode) | Kevitivity | `$C000` | `$C000` | log compare | `make test-nestest` |
**Interrupt-test MMIO shim**
- `bus_write8` checks for `addr == 0xBFFC`: bit 0 → `cpu->irq_line`, bit 1 → `cpu->nmi_pending`. This is what the Dormann interrupt test uses to assert IRQ/NMI lines.
**`nestest` log compare**
- A separate `tests/nestest_runner.c` that, after each step, formats the CPU state in the canonical `nestest.log` form (`PC OP OPERANDS A:.. X:.. Y:.. P:.. SP:.. CYC:..`) and diffs against `nestest.log` line-by-line. First mismatch fails the test.
**ROMs**: documented under `tests/roms/README.md` with attribution and download URLs (do **not** commit binaries unless licensing allows; provide a `make fetch-roms` target).
**Verify**: `make test-all` runs every suite; all pass before moving to Phase 12.
Reference: design.md §"Validation through the Klaus Dormann Test Suite".
---
## Phase 12 — Performance Benchmark
Quantify MIPS so regressions are visible.
**Files**: `tests/bench.c`, Makefile target `make bench`.
- Run a tight loop (e.g. `LDA #$01; CLC; ADC #$01; BNE -4`) for N seconds, count instructions, print MIPS.
- Compare to the design doc's expected range (150–500 MIPS on a 3 GHz host, design.md §"Performance Calculation").
- `make bench` writes a single-line CSV row to `bench.csv` (timestamp, git sha, MIPS) for tracking over time.
---
## Phase 13 — Memory-Mapped I/O Bus (Game-Ready)
Move from "Memory is the bus" to a real bus that supports MMIO. This is what makes a game runnable.
**Files**: rewrite `src/bus.c` to a region-table dispatch, new `include/bus.h`.
**Bus design**
```c
typedef struct {
uint16_t start, end;
uint8_t (*read )(uint16_t, void*);
void (*write)(uint16_t, uint8_t, void*);
void *userdata;
} bus_region_t;
typedef struct {
bus_region_t regions[16];
int n;
} bus_t;
void bus_map(bus_t*, uint16_t start, uint16_t end, ...);
uint8_t bus_read8 (uint16_t, void*); // dispatches to region
void bus_write8(uint16_t, uint8_t, void*);
```
**Default mapping**
- `$0000–$BFFF`: RAM (48 KiB).
- `$FE00–$FE00`: RNG read (returns `rand() & 0xFF`) — required by 6502 Snake.
- `$FF00–$FF00`: keyboard read (latest scancode, non-blocking).
- `$0200–$05FF`: framebuffer write-through to RAM, mirrored to a renderer.
**Verify**: rerun the test suites against the new bus — all still pass.
---
## Phase 14 — Display + Input Frontend (NEW)
Render the framebuffer and capture input. Two backends, picked at compile time.
**Files**: new `src/frontend_sdl.c`, `src/frontend_term.c`, Makefile flags `FRONTEND=sdl|term` (default `term`).
**Terminal frontend** (zero-dependency)
- 32×32 framebuffer at `$0200–$05FF`. Each byte = 4-bit colour index.
- After every `cpu_step` (or every N cycles), redraw via ANSI 256-colour blocks (`\x1b[48;5;<idx>m `).
- Input via `termios` raw mode + non-blocking `read(STDIN)`; latest key written to `$FF`.
**SDL2 frontend** (optional)
- Same framebuffer, rendered via `SDL_CreateTexture` at scale 16x.
- `SDL_PollEvent` → keyboard scancode → `$FF`.
- Build with `make FRONTEND=sdl` (skip silently if `pkg-config --exists sdl2` fails).
**Verify**: `bin/mos6502 --frontend=term --rom=tests/roms/colorbars.bin` renders a static 4×4 colour grid by hand-assembling 16 `STA $0200,X; INX` writes.
---
## Phase 15 — Run a 6502 Game (capstone)
Run **6502 Snake** by Nick Morgan (the canonical Easy 6502 demo program). It uses exactly the MMIO map from Phase 13.
**Files**: `games/snake.asm` (source), `games/snake.bin` (assembled), `games/README.md` documenting controls.
**Flow**
1. Pull the public-domain `snake.asm` from Easy 6502 (https://skilldrick.github.io/easy6502/#snake) into `games/snake.asm`.
2. Assemble with `vasm6502_oldstyle` (or `cc65`'s `ca65`/`ld65`) → `games/snake.bin` (origin `$0600`).
3. Add a Make target `make snake`: builds emulator, assembles ROM, runs `bin/mos6502 --frontend=term games/snake.bin --start=0x0600`.
4. Reset vector convention: emulator writes `$0600` to `$FFFC/D` if `--start` is passed.
**Verify**: snake spawns, WASD moves it, eats apples, dies on self-collision.
**Stretch goals (optional follow-ups, not part of the capstone)**
- Port a second game: `games/tetris.bin` from a known 6502 implementation.
- Apple I emulation: map a 6850-style ACIA at `$D010–$D013`, load Wozmon at `$FF00`, get a working `\` prompt.
- Commodore PET BASIC ROM (requires sourcing a legally-redistributable image).
---
## Sequencing Summary
```
Phase 0 (done) → 1 → 2 → 3 (test harness) → 4 (modes) → 5 (loop)
→ 6a..6l (instruction waves, each ships green tests)
→ 7 (BCD) → 8 (interrupts) → 9 (cycles) → 10 (illegal ops)
→ 11 (validation suites — gate before moving on)
→ 12 (bench)
→ 13 (MMIO bus) → 14 (frontend) → 15 (snake game)
```
- Phases 1–5 strictly linear.
- Phase 6 waves are independent (order is suggested, not required).
- Phase 7 needs 6k; Phase 8 needs 6g+6h; Phase 9 can be folded in alongside 6 if convenient.
- **Hard gate**: Phase 11 must be fully green before starting Phase 13. Don't build a game on an emulator that fails Klaus Dormann.
- Phase 14 is independent of the bus rewrite as long as the framebuffer region exists.
---
## Critical Files
- [include/cpu.h](../../dev/mos-6502/include/cpu.h)
- [include/memory.h](../../dev/mos-6502/include/memory.h)
- [include/addressing.h](../../dev/mos-6502/include/addressing.h)
- [include/opcode.h](../../dev/mos-6502/include/opcode.h)
- [src/cpu.c](../../dev/mos-6502/src/cpu.c)
- [src/memory.c](../../dev/mos-6502/src/memory.c)
- [src/addressing.c](../../dev/mos-6502/src/addressing.c)
- [src/opcode.c](../../dev/mos-6502/src/opcode.c)
- [src/main.c](../../dev/mos-6502/src/main.c)
- [Makefile](../../dev/mos-6502/Makefile)
- New: `src/bus.c`, `src/loader.c`, `src/frontend_term.c`, `src/frontend_sdl.c`
- New: `tests/test_framework.h`, `tests/test_*.c`, `tests/run_tests.c`, `tests/run_suite.c`, `tests/bench.c`
- New: `games/snake.asm`, `games/snake.bin`
---
## End-to-End Verification
After Phase 15 the following must all succeed from a clean checkout:
```sh
make # builds bin/mos6502 and bin/run_tests
make test # all unit tests green
make fetch-roms # downloads validation ROMs into tests/roms/
make test-functional # Klaus Dormann functional — success at $3469
make test-decimal # Bruce Clark BCD — error count = 0
make test-interrupts # Klaus Dormann interrupt — success
make test-nestest # nestest CPU log diff — zero mismatches
make bench # prints MIPS, appends to bench.csv
make snake # opens snake game in terminal, playable
```
All green = emulator is functionally complete and validated against the standard 6502 test corpus, performant, and demonstrably running real software.
- Undocumented NMOS opcodes.
- Audio, save states and an interactive debugger.
- A real historical machine such as Apple I or NES. NES support is a separate major
project requiring the PPU, APU, controllers, cartridge parsing/mappers and tighter timing.
+6
View File
@@ -0,0 +1,6 @@
MEMORY {
ROM: start = $0600, size = $0300, file = %O, fill = no;
}
SEGMENTS {
CODE: load = ROM, type = ro;
}
+255
View File
@@ -0,0 +1,255 @@
; Original Snake game for this project's Easy6502-compatible machine.
; WASD moves. The playfield wraps at its edges.
.segment "CODE"
dirx=$00
diry=$01
head=$02
tail=$03
applex=$04
appley=$05
newx=$06
newy=$07
ptrlo=$08
ptrhi=$09
colour=$0a
score=$00fd
game_state=$00fb
snakex=$0900
snakey=$0a00
reset:
ldx #$00
lda #$00
clear: sta $0200,x
sta $0300,x
sta $0400,x
sta $0500,x
inx
bne clear
lda #$01
sta dirx
lda #$00
sta diry
sta score
sta game_state
sta tail
lda #$02
sta head
ldx #$00
lda #$08
sta snakex,x
lda #$10
sta snakey,x
inx
lda #$09
sta snakex,x
lda #$10
sta snakey,x
inx
lda #$0a
sta snakex,x
lda #$10
sta snakey,x
jsr new_apple
jsr redraw
game_loop:
jsr delay
jsr input
ldx head
lda snakex,x
clc
adc dirx
and #$1f
sta newx
lda snakey,x
clc
adc diry
and #$1f
sta newy
ldx tail
collision:
lda snakex,x
cmp newx
bne next_part
lda snakey,x
cmp newy
beq died
next_part:
cpx head
beq add_head
inx
jmp collision
add_head:
inc head
ldx head
lda newx
sta snakex,x
lda newy
sta snakey,x
lda #$01
sta colour
jsr plot
lda newx
cmp applex
bne move_tail
lda newy
cmp appley
bne move_tail
lda score
cmp #$ff
beq score_full
inc score
score_full:
jsr new_apple
jmp game_loop
move_tail:
ldx tail
lda snakex,x
sta newx
lda snakey,x
sta newy
lda #$00
sta colour
jsr plot
inc tail
jmp game_loop
died: lda #$01
sta game_state
lda #$02
sta colour
ldx tail
death_draw:
lda snakex,x
sta newx
lda snakey,x
sta newy
txa
pha
jsr plot
pla
tax
cpx head
beq death_wait
inx
jmp death_draw
death_wait:
lda $00ff
cmp #'r'
bne death_wait
jmp reset
input: lda $00ff
cmp #'w'
beq up
cmp #'s'
beq down
cmp #'a'
beq left
cmp #'d'
beq right
rts
up: lda diry
cmp #$01
beq input_done
lda #$00
sta dirx
lda #$ff
sta diry
rts
down: lda diry
cmp #$ff
beq input_done
lda #$00
sta dirx
lda #$01
sta diry
rts
left: lda dirx
cmp #$01
beq input_done
lda #$ff
sta dirx
lda #$00
sta diry
rts
right: lda dirx
cmp #$ff
beq input_done
lda #$01
sta dirx
lda #$00
sta diry
input_done:
rts
new_apple:
lda $00fe
and #$1f
sta applex
lda $00fe
and #$1f
sta appley
lda applex
sta newx
lda appley
sta newy
lda #$07
sta colour
jmp plot
redraw:
ldx tail
draw_loop:
lda snakex,x
sta newx
lda snakey,x
sta newy
txa
pha
lda #$01
sta colour
jsr plot
pla
tax
cpx head
beq draw_done
inx
jmp draw_loop
draw_done:
rts
; Plot colour at (newx,newy), using a 16-bit zero-page pointer.
plot: lda #$00
sta ptrhi
lda newy
ldx #$05
plot_shift:
asl
rol ptrhi
dex
bne plot_shift
clc
adc newx
sta ptrlo
lda ptrhi
adc #$02
sta ptrhi
ldy #$00
lda colour
sta (ptrlo),y
rts
; About 100,000 cycles at a 1 MHz CPU, for roughly 10 moves/second.
delay: ldx #$50
delay_outer:
ldy #$ff
delay_inner:
dey
bne delay_inner
dex
bne delay_outer
rts
+9 -10
View File
@@ -1,18 +1,17 @@
#ifndef ADDRESSING_H
#define ADDRESSING_H
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
typedef struct
{
uint8_t (*read8)(uint16_t addr, void *userdata);
void (*read16)(uint16_t addr, void *userdata);
typedef enum {
AM_IMP, AM_ACC, AM_IMM, AM_ZP, AM_ZPX, AM_ZPY, AM_ABS, AM_ABSX,
AM_ABSY, AM_IND, AM_INDX, AM_INDY, AM_REL
} addr_mode_t;
void *userdata;
// cpu registers
} addr_ctx_t;
typedef struct {
uint16_t address;
bool page_crossed;
} addr_result_t;
#endif
+24
View File
@@ -0,0 +1,24 @@
#ifndef BUS_H
#define BUS_H
#include <stdbool.h>
#include <stdint.h>
#include "memory.h"
typedef struct Bus6502 {
Memory memory;
uint8_t keyboard;
uint32_t rng_state;
bool easy6502_io;
bool framebuffer_dirty;
} Bus6502;
void bus_init(Bus6502 *bus);
uint8_t bus_read(Bus6502 *bus, uint16_t addr);
void bus_write(Bus6502 *bus, uint16_t addr, uint8_t value);
uint16_t bus_read16(Bus6502 *bus, uint16_t addr);
void bus_write16(Bus6502 *bus, uint16_t addr, uint16_t value);
void bus_set_keyboard(Bus6502 *bus, uint8_t key);
void bus_seed_rng(Bus6502 *bus, uint32_t seed);
#endif
+27 -19
View File
@@ -1,29 +1,37 @@
#ifndef CPU_H
#define CPU_H
#include <stdbool.h>
#include <stdint.h>
typedef struct
{
uint8_t A; // accumulator
uint8_t X; // index X
uint8_t Y; // index y
uint8_t SP; // stack pointer
uint8_t PC; // program counter
uint8_t P; // status flags
typedef struct Bus6502 Bus6502;
typedef struct CPU6502 {
uint8_t A, X, Y;
uint8_t SP;
uint16_t PC;
uint8_t P;
bool nmi_pending;
bool irq_asserted;
} CPU6502;
// status flag masks
#define FLAG_C 0x01
#define FLAG_Z 0x02
#define FLAG_I 0x04
#define FLAG_D 0x08
#define FLAG_B 0x10
#define FLAG_U 0x20
#define FLAG_V 0x40
#define FLAG_N 0x80
enum {
FLAG_C = 0x01, FLAG_Z = 0x02, FLAG_I = 0x04, FLAG_D = 0x08,
FLAG_B = 0x10, FLAG_U = 0x20, FLAG_V = 0x40, FLAG_N = 0x80
};
void cpu_reset(CPU6502 *cpu);
void cpu_print_state(CPU6502 *cpu);
typedef enum { CPU_STEP_OK = 0, CPU_STEP_ILLEGAL_OPCODE } cpu_step_status_t;
typedef struct {
cpu_step_status_t status;
uint8_t opcode;
uint8_t cycles;
} cpu_step_result_t;
void cpu_reset(CPU6502 *cpu, Bus6502 *bus);
cpu_step_result_t cpu_step(CPU6502 *cpu, Bus6502 *bus);
void cpu_request_nmi(CPU6502 *cpu);
void cpu_set_irq(CPU6502 *cpu, bool asserted);
void cpu_print_state(const CPU6502 *cpu);
#endif
+6
View File
@@ -0,0 +1,6 @@
#ifndef FRONTEND_H
#define FRONTEND_H
#include "bus.h"
#include "cpu.h"
int frontend_run(CPU6502 *cpu, Bus6502 *bus, unsigned hz, unsigned scale);
#endif
+11
View File
@@ -0,0 +1,11 @@
#ifndef LOADER_H
#define LOADER_H
#include <stddef.h>
#include <stdint.h>
#include "bus.h"
int load_binary(Bus6502 *bus, const char *path, uint16_t address,
size_t *bytes_loaded);
#endif
+11
View File
@@ -0,0 +1,11 @@
#ifndef OPCODE_H
#define OPCODE_H
#include <stddef.h>
#include <stdint.h>
#include "bus.h"
#include "cpu.h"
size_t cpu_disassemble(Bus6502 *bus, uint16_t pc, char *output, size_t size);
#endif
+2
View File
@@ -0,0 +1,2 @@
/* Addressing-mode types are public; resolution stays private to cpu_step. */
typedef int addressing_translation_unit_is_not_empty;
+47
View File
@@ -0,0 +1,47 @@
#include <string.h>
#include "bus.h"
void bus_init(Bus6502 *bus)
{
memset(bus, 0, sizeof(*bus));
bus->rng_state = 0x6502u;
}
static uint8_t random_byte(Bus6502 *bus)
{
uint32_t x = bus->rng_state;
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
bus->rng_state = x ? x : 0x6502u;
return (uint8_t)x;
}
uint8_t bus_read(Bus6502 *bus, uint16_t addr)
{
if (bus->easy6502_io) {
if (addr == 0x00FE) return random_byte(bus);
if (addr == 0x00FF) return bus->keyboard;
}
return mem_read(&bus->memory, addr);
}
void bus_write(Bus6502 *bus, uint16_t addr, uint8_t value)
{
mem_write(&bus->memory, addr, value);
if (addr >= 0x0200 && addr <= 0x05FF) bus->framebuffer_dirty = true;
}
uint16_t bus_read16(Bus6502 *bus, uint16_t addr)
{
uint8_t lo = bus_read(bus, addr);
uint8_t hi = bus_read(bus, (uint16_t)(addr + 1));
return (uint16_t)lo | ((uint16_t)hi << 8);
}
void bus_write16(Bus6502 *bus, uint16_t addr, uint16_t value)
{
bus_write(bus, addr, (uint8_t)value);
bus_write(bus, (uint16_t)(addr + 1), (uint8_t)(value >> 8));
}
void bus_set_keyboard(Bus6502 *bus, uint8_t key) { bus->keyboard = key; }
void bus_seed_rng(Bus6502 *bus, uint32_t seed) { bus->rng_state = seed ? seed : 1; }
+13 -15
View File
@@ -1,23 +1,21 @@
#include <stdio.h>
#include <string.h>
#include "bus.h"
#include "cpu.h"
void cpu_reset(CPU6502 *cpu)
void cpu_reset(CPU6502 *cpu, Bus6502 *bus)
{
cpu->A = 0;
cpu->X = 0;
cpu->Y = 0;
cpu->SP = 0xFD; // typical 6502 reset value??
cpu->P = FLAG_U; // unused flag always 1
cpu->PC = 0x0000; // loaded later from reset vector
memset(cpu, 0, sizeof(*cpu));
cpu->SP = 0xFD;
cpu->P = FLAG_U | FLAG_I;
cpu->PC = bus_read16(bus, 0xFFFC);
}
void cpu_print_state(CPU6502 *cpu)
void cpu_request_nmi(CPU6502 *cpu) { cpu->nmi_pending = true; }
void cpu_set_irq(CPU6502 *cpu, bool asserted) { cpu->irq_asserted = asserted; }
void cpu_print_state(const CPU6502 *cpu)
{
printf("CPU State:\n");
printf("A: 0x%02X\n", cpu->A);
printf("X: 0x%02X\n", cpu->X);
printf("Y: 0x%02X\n", cpu->Y);
printf("SP: 0x%02X\n", cpu->SP);
printf("PC: 0x%04X\n", cpu->PC);
printf("P: 0x%02X\n", cpu->P);
printf("PC:%04X A:%02X X:%02X Y:%02X P:%02X SP:%02X\n",
cpu->PC, cpu->A, cpu->X, cpu->Y, cpu->P, cpu->SP);
}
+269
View File
@@ -0,0 +1,269 @@
#include <stdio.h>
#include "frontend.h"
#ifdef HAVE_SDL_UI
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <stdlib.h>
#include <string.h>
static const uint32_t palette[16] = {
0x071018FF,0x62F294FF,0xFF5D73FF,0x8FFFAFFF,0x638CFFFF,0xD879FFFF,
0x4DE1FFFF,0xFFD166FF,0xFF934FFF,0x9B6B43FF,0xFF9AA9FF,0x34445AFF,
0x65758BFF,0xB6FFC9FF,0xA9BCFFFF,0xEAF2FFFF
};
static uint8_t load_high_score(void)
{
FILE *file=fopen("snake.highscore","rb"); int value;
if(!file) return 0;
value=fgetc(file); fclose(file);
return value==EOF?0:(uint8_t)value;
}
static void save_high_score(uint8_t score)
{
FILE *file=fopen("snake.highscore","wb");
if(file){fputc(score,file);fclose(file);}
}
static bool save_state(const CPU6502 *cpu, const Bus6502 *bus)
{
FILE *f=fopen("snake.state","wb");const char magic[8]="MOSST01";
if(!f)return false;
bool ok=fwrite(magic,1,8,f)==8&&fwrite(cpu,1,sizeof(*cpu),f)==sizeof(*cpu)&&
fwrite(bus->memory.data,1,MEM_SIZE,f)==MEM_SIZE;
return fclose(f)==0&&ok;
}
static bool load_state(CPU6502 *cpu, Bus6502 *bus)
{
FILE *f=fopen("snake.state","rb");char magic[8];CPU6502 saved;
if(!f)return false;
bool ok=fread(magic,1,8,f)==8&&!memcmp(magic,"MOSST01",8)&&
fread(&saved,1,sizeof(saved),f)==sizeof(saved)&&
fread(bus->memory.data,1,MEM_SIZE,f)==MEM_SIZE;
fclose(f);if(ok){*cpu=saved;bus->framebuffer_dirty=true;}return ok;
}
static void play_tone(SDL_AudioDeviceID device, int frequency, int milliseconds)
{
int16_t samples[4410];int count=44100*milliseconds/1000;
if(!device)return;
if(count>(int)(sizeof samples/sizeof samples[0]))count=(int)(sizeof samples/sizeof samples[0]);
for(int i=0;i<count;i++)samples[i]=((i*frequency/44100)&1)?3500:-3500;
SDL_QueueAudio(device,samples,(Uint32)(count*sizeof samples[0]));SDL_PauseAudioDevice(device,0);
}
static void update_title(SDL_Window *window, uint8_t score, uint8_t high, bool paused)
{
char title[96];
snprintf(title,sizeof title,"MOS 6502 Snake - Score: %u High: %u%s",
score,high,paused?" [PAUSED]":"");
SDL_SetWindowTitle(window,title);
}
static void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text,
SDL_Color color, int center_x, int y)
{
SDL_Surface *surface=TTF_RenderUTF8_Blended(font,text,color);
SDL_Texture *texture;
SDL_Rect target;
if(!surface) return;
texture=SDL_CreateTextureFromSurface(renderer,surface);
target=(SDL_Rect){center_x-surface->w/2,y,surface->w,surface->h};
SDL_FreeSurface(surface);
if(texture){SDL_RenderCopy(renderer,texture,NULL,&target);SDL_DestroyTexture(texture);}
}
static void render_menu(SDL_Renderer *renderer, TTF_Font *title_font,
TTF_Font *item_font, TTF_Font *small_font,
unsigned selected, bool started, uint8_t high)
{
const char *items[4]={started?"Resume Game":"Start Game","New Game",
"Reset High Score","Quit"};
char high_text[48];
SDL_RenderSetLogicalSize(renderer,832,560);
SDL_SetRenderDrawColor(renderer,7,12,22,255);SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer,14,25,40,255);
for(int x=-32;x<864;x+=32)for(int y=-32;y<592;y+=32){SDL_Rect tile={x,y,30,30};SDL_RenderDrawRect(renderer,&tile);}
draw_text(renderer,title_font,"SNAKE 6502",(SDL_Color){105,240,150,255},416,38);
draw_text(renderer,small_font,"A TINY 8-BIT ARCADE",(SDL_Color){115,135,165,255},416,95);
for(unsigned i=0;i<4;i++) {
SDL_Rect shadow={245,143+(int)i*67,350,50};
SDL_Rect card={241,139+(int)i*67,350,50};
SDL_SetRenderDrawColor(renderer,2,5,10,180);SDL_RenderFillRect(renderer,&shadow);
if(i==selected){SDL_SetRenderDrawColor(renderer,31,105,72,255);SDL_RenderFillRect(renderer,&card);SDL_SetRenderDrawColor(renderer,105,240,150,255);SDL_RenderDrawRect(renderer,&card);}
else {SDL_SetRenderDrawColor(renderer,20,31,48,255);SDL_RenderFillRect(renderer,&card);}
draw_text(renderer,item_font,items[i],i==selected?(SDL_Color){245,255,248,255}:(SDL_Color){170,184,205,255},416,149+(int)i*67);
}
snprintf(high_text,sizeof high_text,"HIGH SCORE %u",high);
draw_text(renderer,small_font,high_text,(SDL_Color){250,198,82,255},416,420);
draw_text(renderer,small_font,"W/S OR ARROWS TO MOVE - ENTER TO SELECT",
(SDL_Color){100,120,145,255},416,468);
SDL_RenderPresent(renderer);
}
static void render_game(SDL_Renderer *renderer, SDL_Texture *texture,
TTF_Font *title_font, TTF_Font *item_font,
TTF_Font *small_font, uint8_t score, uint8_t high,
bool paused, bool game_over)
{
SDL_Rect board_shadow={28,28,520,520},board={20,20,520,520};
SDL_Rect screen={24,24,512,512},panel={560,20,248,520};
char value[32];
SDL_RenderSetLogicalSize(renderer,832,560);
SDL_SetRenderDrawColor(renderer,5,10,18,255);SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer,1,3,8,255);SDL_RenderFillRect(renderer,&board_shadow);
SDL_SetRenderDrawColor(renderer,41,62,82,255);SDL_RenderFillRect(renderer,&board);
SDL_RenderCopy(renderer,texture,NULL,&screen);
SDL_SetRenderDrawColor(renderer,98,242,148,255);SDL_RenderDrawRect(renderer,&screen);
SDL_SetRenderDrawColor(renderer,15,25,39,255);SDL_RenderFillRect(renderer,&panel);
SDL_SetRenderDrawColor(renderer,31,51,72,255);SDL_RenderDrawRect(renderer,&panel);
draw_text(renderer,title_font,"SNAKE",(SDL_Color){98,242,148,255},684,45);
draw_text(renderer,small_font,"MOS 6502 ARCADE",(SDL_Color){108,132,158,255},684,96);
draw_text(renderer,small_font,"SCORE",(SDL_Color){130,150,175,255},684,145);
snprintf(value,sizeof value,"%03u",score);
draw_text(renderer,title_font,value,(SDL_Color){245,250,255,255},684,164);
draw_text(renderer,small_font,"HIGH SCORE",(SDL_Color){130,150,175,255},684,232);
snprintf(value,sizeof value,"%03u",high);
draw_text(renderer,item_font,value,(SDL_Color){255,209,102,255},684,253);
SDL_SetRenderDrawColor(renderer,38,55,73,255);{SDL_Rect line={588,310,192,1};SDL_RenderFillRect(renderer,&line);}
draw_text(renderer,small_font,"MOVE",(SDL_Color){98,242,148,255},684,334);
draw_text(renderer,small_font,"W A S D / ARROWS",(SDL_Color){190,202,218,255},684,359);
draw_text(renderer,small_font,"PAUSE",(SDL_Color){98,242,148,255},684,399);
draw_text(renderer,small_font,"SPACE / P",(SDL_Color){190,202,218,255},684,424);
draw_text(renderer,small_font,"ESC MENU",(SDL_Color){108,132,158,255},684,488);
if(paused||game_over){
SDL_SetRenderDrawBlendMode(renderer,SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer,3,8,14,215);SDL_RenderFillRect(renderer,&screen);
draw_text(renderer,title_font,game_over?"GAME OVER":"PAUSED",
game_over?(SDL_Color){255,93,115,255}:(SDL_Color){255,209,102,255},280,224);
draw_text(renderer,small_font,game_over?"PRESS R TO PLAY AGAIN":"PRESS SPACE OR P TO RESUME",
(SDL_Color){225,234,244,255},280,284);
SDL_SetRenderDrawBlendMode(renderer,SDL_BLENDMODE_NONE);
}
SDL_RenderPresent(renderer);
}
int frontend_run(CPU6502 *cpu, Bus6502 *bus, unsigned hz, unsigned scale)
{
SDL_Window *window=NULL; SDL_Renderer *renderer=NULL; SDL_Texture *texture=NULL;
SDL_GameController *controller=NULL;
SDL_AudioDeviceID audio=0;SDL_AudioSpec desired={0};
TTF_Font *title_font=NULL,*item_font=NULL,*small_font=NULL;
uint32_t pixels[32*32]; uint64_t last, freq; int64_t budget=0;
uint8_t score=0, high=load_high_score(), game_state=0; unsigned selected=0;
bool running=true, paused=false, menu=true, started=false;
const char *font_path=getenv("MOS6502_FONT");
if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_AUDIO|SDL_INIT_GAMECONTROLLER)!=0) goto fail;
if(TTF_Init()!=0) goto fail;
if(!font_path) font_path="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf";
title_font=TTF_OpenFont(font_path,42);item_font=TTF_OpenFont(font_path,24);small_font=TTF_OpenFont(font_path,14);
if(!title_font||!item_font||!small_font) goto fail;
desired.freq=44100;desired.format=AUDIO_S16SYS;desired.channels=1;desired.samples=512;
audio=SDL_OpenAudioDevice(NULL,0,&desired,NULL,0);
window=SDL_CreateWindow("MOS 6502 Snake",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,
32*(int)scale+320,32*(int)scale+48,SDL_WINDOW_RESIZABLE);
renderer=window?SDL_CreateRenderer(window,-1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC):NULL;
if(window&&!renderer) renderer=SDL_CreateRenderer(window,-1,SDL_RENDERER_SOFTWARE);
texture=renderer?SDL_CreateTexture(renderer,SDL_PIXELFORMAT_RGBA8888,SDL_TEXTUREACCESS_STREAMING,32,32):NULL;
if (!texture) goto fail;
for(int i=0;i<SDL_NumJoysticks()&&!controller;i++)
if(SDL_IsGameController(i)) controller=SDL_GameControllerOpen(i);
bus->memory.data[0x00FC]=high;
SDL_RenderSetLogicalSize(renderer,32,32); freq=SDL_GetPerformanceFrequency(); last=SDL_GetPerformanceCounter();
render_menu(renderer,title_font,item_font,small_font,selected,started,high);
while(running) {
SDL_Event event; uint64_t now=SDL_GetPerformanceCounter(), elapsed=now-last; last=now;
while(SDL_PollEvent(&event)) {
if(event.type==SDL_QUIT) running=false;
if(event.type==SDL_KEYDOWN) {
SDL_Keycode k=event.key.keysym.sym;
if(menu) {
if(!event.key.repeat && (k==SDLK_UP||k==SDLK_w)){selected=(selected+3)%4;render_menu(renderer,title_font,item_font,small_font,selected,started,high);}
else if(!event.key.repeat && (k==SDLK_DOWN||k==SDLK_s)){selected=(selected+1)%4;render_menu(renderer,title_font,item_font,small_font,selected,started,high);}
else if(!event.key.repeat && (k==SDLK_RETURN||k==SDLK_SPACE)) {
if(selected==0){menu=false;started=true;budget=0;bus->framebuffer_dirty=true;update_title(window,score,high,paused);}
else if(selected==1){cpu_reset(cpu,bus);score=0;paused=false;menu=false;started=true;budget=0;bus->framebuffer_dirty=true;update_title(window,score,high,false);}
else if(selected==2){high=0;bus->memory.data[0x00FC]=0;save_high_score(0);render_menu(renderer,title_font,item_font,small_font,selected,started,high);}
else running=false;
} else if(!event.key.repeat && k==SDLK_ESCAPE && started){menu=false;budget=0;bus->framebuffer_dirty=true;update_title(window,score,high,paused);}
}
else if(k==SDLK_ESCAPE) {
menu=true;selected=0;budget=0;render_menu(renderer,title_font,item_font,small_font,selected,started,high);
}
else if(!event.key.repeat && (k==SDLK_SPACE||k==SDLK_p)) {
paused=!paused; budget=0; bus->framebuffer_dirty=true; update_title(window,score,high,paused);
}
else if(!event.key.repeat && k==SDLK_F5) {
if(save_state(cpu,bus)) SDL_SetWindowTitle(window,"MOS 6502 Snake - State saved");
}
else if(!event.key.repeat && k==SDLK_F9) {
if(load_state(cpu,bus)){score=bus->memory.data[0x00FD];game_state=bus->memory.data[0x00FB];update_title(window,score,high,paused);}
}
else if(k==SDLK_w||k==SDLK_UP) bus_set_keyboard(bus,'w');
else if(k==SDLK_a||k==SDLK_LEFT) bus_set_keyboard(bus,'a');
else if(k==SDLK_s||k==SDLK_DOWN) bus_set_keyboard(bus,'s');
else if(k==SDLK_d||k==SDLK_RIGHT) bus_set_keyboard(bus,'d');
else if(k==SDLK_r) bus_set_keyboard(bus,'r');
}
if(!menu&&event.type==SDL_CONTROLLERBUTTONDOWN){
if(event.cbutton.button==SDL_CONTROLLER_BUTTON_DPAD_UP)bus_set_keyboard(bus,'w');
else if(event.cbutton.button==SDL_CONTROLLER_BUTTON_DPAD_DOWN)bus_set_keyboard(bus,'s');
else if(event.cbutton.button==SDL_CONTROLLER_BUTTON_DPAD_LEFT)bus_set_keyboard(bus,'a');
else if(event.cbutton.button==SDL_CONTROLLER_BUTTON_DPAD_RIGHT)bus_set_keyboard(bus,'d');
else if(event.cbutton.button==SDL_CONTROLLER_BUTTON_START){paused=!paused;budget=0;bus->framebuffer_dirty=true;}
}
}
if(paused||menu) budget=0;
else {
budget += (int64_t)(elapsed*hz);
if (budget > (int64_t)(freq*hz/4)) budget=(int64_t)(freq*hz/4);
}
while(running && !paused && !menu && budget>=(int64_t)freq) {
cpu_step_result_t step=cpu_step(cpu,bus);
if(step.status!=CPU_STEP_OK){fprintf(stderr,"illegal opcode $%02X at $%04X\n",step.opcode,(uint16_t)(cpu->PC-1));running=false;break;}
budget -= (int64_t)step.cycles*(int64_t)freq;
}
if(score!=bus->memory.data[0x00FD]) {
uint8_t old_score=score;score=bus->memory.data[0x00FD];
if(score>high){high=score;bus->memory.data[0x00FC]=high;save_high_score(high);}
if(score>old_score)play_tone(audio,880,70);
update_title(window,score,high,paused);
bus->framebuffer_dirty=true;
}
if(game_state!=bus->memory.data[0x00FB]){
game_state=bus->memory.data[0x00FB];bus->framebuffer_dirty=true;
if(game_state)play_tone(audio,150,220);
}
if(!menu && bus->framebuffer_dirty) {
for(unsigned i=0;i<1024;i++) pixels[i]=palette[bus->memory.data[0x0200+i]&15];
SDL_UpdateTexture(texture,NULL,pixels,32*sizeof(*pixels));
render_game(renderer,texture,title_font,item_font,small_font,score,high,paused,
bus->memory.data[0x00FB]!=0);
bus->framebuffer_dirty=false;
}
SDL_Delay(1);
}
save_high_score(high);if(audio)SDL_CloseAudioDevice(audio);if(controller)SDL_GameControllerClose(controller);TTF_CloseFont(small_font);TTF_CloseFont(item_font);TTF_CloseFont(title_font);SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);TTF_Quit();SDL_Quit();return 0;
fail:
fprintf(stderr,"SDL UI error: %s%s%s\n",SDL_GetError(),*TTF_GetError()?" / ":"",TTF_GetError());
if(small_font) TTF_CloseFont(small_font);
if(item_font) TTF_CloseFont(item_font);
if(title_font) TTF_CloseFont(title_font);
if(controller) SDL_GameControllerClose(controller);
if(audio) SDL_CloseAudioDevice(audio);
if(texture) SDL_DestroyTexture(texture);
if(renderer) SDL_DestroyRenderer(renderer);
if(window) SDL_DestroyWindow(window);
TTF_Quit();SDL_Quit();
return 1;
}
#else
int frontend_run(CPU6502 *cpu, Bus6502 *bus, unsigned hz, unsigned scale)
{
(void)cpu;(void)bus;(void)hz;(void)scale;
fprintf(stderr,"SDL2/SDL2_ttf support was not built; install their development files and rebuild\n"); return 1;
}
#endif
+19
View File
@@ -0,0 +1,19 @@
#include <errno.h>
#include <stdio.h>
#include "loader.h"
int load_binary(Bus6502 *bus, const char *path, uint16_t address,
size_t *bytes_loaded)
{
FILE *file = fopen(path, "rb");
long length;
size_t got;
if (!file) return errno ? errno : -1;
if (fseek(file, 0, SEEK_END) != 0 || (length = ftell(file)) < 0 ||
fseek(file, 0, SEEK_SET) != 0) { fclose(file); return -1; }
if ((unsigned long)length > 65536ul - address) { fclose(file); return EFBIG; }
got = fread(&bus->memory.data[address], 1, (size_t)length, file);
if (fclose(file) != 0 || got != (size_t)length) return EIO;
if (bytes_loaded) *bytes_loaded = got;
return 0;
}
+49 -9
View File
@@ -1,15 +1,55 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "bus.h"
#include "cpu.h"
#include "memory.h"
int main()
#include "frontend.h"
#include "loader.h"
#include "opcode.h"
static unsigned long parse_number(const char *s, const char *name, unsigned long max)
{
CPU6502 cpu;
Memory mem;
char *end; unsigned long value = strtoul(s, &end, 0);
if (!*s || *end || value > max) {
fprintf(stderr, "invalid %s: %s\n", name, s); exit(2);
}
return value;
}
mem_reset(&mem);
cpu_reset(&cpu);
static void usage(const char *program)
{
fprintf(stderr, "usage: %s ROM [--load ADDRESS] [--start ADDRESS] "
"[--hz HZ] [--scale N] [--seed N] [--headless] [--trace]\n", program);
}
cpu_print_state(&cpu);
return 0;
int main(int argc, char **argv)
{
Bus6502 bus; CPU6502 cpu; const char *rom = NULL;
uint16_t load = 0x0600, start = 0x0600; unsigned hz = 1000000, scale = 16;
uint32_t seed = 0x6502; bool headless = false, trace=false; size_t loaded; int rc;
for (int i=1;i<argc;i++) {
if (!strcmp(argv[i],"--load") && i+1<argc) load=(uint16_t)parse_number(argv[++i],"load address",0xFFFF);
else if (!strcmp(argv[i],"--start") && i+1<argc) start=(uint16_t)parse_number(argv[++i],"start address",0xFFFF);
else if (!strcmp(argv[i],"--hz") && i+1<argc) hz=(unsigned)parse_number(argv[++i],"frequency",100000000);
else if (!strcmp(argv[i],"--scale") && i+1<argc) scale=(unsigned)parse_number(argv[++i],"scale",128);
else if (!strcmp(argv[i],"--seed") && i+1<argc) seed=(uint32_t)strtoul(argv[++i],NULL,0);
else if (!strcmp(argv[i],"--headless")) headless=true;
else if (!strcmp(argv[i],"--trace")) trace=true;
else if (argv[i][0]=='-' || rom) { usage(argv[0]); return 2; }
else rom=argv[i];
}
if (!rom || !hz || !scale) { usage(argv[0]); return 2; }
bus_init(&bus); bus.easy6502_io=true; bus_seed_rng(&bus,seed);
rc=load_binary(&bus,rom,load,&loaded);
if (rc) { fprintf(stderr,"cannot load %s: %s\n",rom,strerror(rc)); return 1; }
bus_write16(&bus,0xFFFC,start); cpu_reset(&cpu,&bus);
printf("loaded %zu bytes at $%04X, starting at $%04X\n",loaded,load,start);
if (!headless) return frontend_run(&cpu,&bus,hz,scale);
for (unsigned long n=0;n<100000000ul;n++) {
if(trace){char instruction[48];cpu_disassemble(&bus,cpu.PC,instruction,sizeof instruction);printf("%04X %-18s A:%02X X:%02X Y:%02X P:%02X SP:%02X\n",cpu.PC,instruction,cpu.A,cpu.X,cpu.Y,cpu.P,cpu.SP);}
cpu_step_result_t step=cpu_step(&cpu,&bus);
if (step.status != CPU_STEP_OK) { fprintf(stderr,"illegal opcode $%02X at $%04X\n",step.opcode,(uint16_t)(cpu.PC-1));return 1; }
}
fprintf(stderr,"instruction limit reached at $%04X\n",cpu.PC); return 1;
}
+183
View File
@@ -0,0 +1,183 @@
#include <stdio.h>
#include "addressing.h"
#include "bus.h"
#include "cpu.h"
typedef enum {
O_BAD, O_ADC, O_AND, O_ASL, O_BCC, O_BCS, O_BEQ, O_BIT, O_BMI,
O_BNE, O_BPL, O_BRK, O_BVC, O_BVS, O_CLC, O_CLD, O_CLI, O_CLV,
O_CMP, O_CPX, O_CPY, O_DEC, O_DEX, O_DEY, O_EOR, O_INC, O_INX,
O_INY, O_JMP, O_JSR, O_LDA, O_LDX, O_LDY, O_LSR, O_NOP, O_ORA,
O_PHA, O_PHP, O_PLA, O_PLP, O_ROL, O_ROR, O_RTI, O_RTS, O_SBC,
O_SEC, O_SED, O_SEI, O_STA, O_STX, O_STY, O_TAX, O_TAY, O_TSX,
O_TXA, O_TXS, O_TYA
} operation_t;
typedef struct { operation_t op; addr_mode_t mode; uint8_t cycles; bool page; } opcode_t;
#define D(code, op, mode, cyc, pg) [code] = { O_##op, AM_##mode, cyc, pg }
static const opcode_t ops[256] = {
D(0x00,BRK,IMP,7,0), D(0x01,ORA,INDX,6,0), D(0x05,ORA,ZP,3,0), D(0x06,ASL,ZP,5,0),
D(0x08,PHP,IMP,3,0), D(0x09,ORA,IMM,2,0), D(0x0A,ASL,ACC,2,0), D(0x0D,ORA,ABS,4,0), D(0x0E,ASL,ABS,6,0),
D(0x10,BPL,REL,2,0), D(0x11,ORA,INDY,5,1), D(0x15,ORA,ZPX,4,0), D(0x16,ASL,ZPX,6,0),
D(0x18,CLC,IMP,2,0), D(0x19,ORA,ABSY,4,1), D(0x1D,ORA,ABSX,4,1), D(0x1E,ASL,ABSX,7,0),
D(0x20,JSR,ABS,6,0), D(0x21,AND,INDX,6,0), D(0x24,BIT,ZP,3,0), D(0x25,AND,ZP,3,0), D(0x26,ROL,ZP,5,0),
D(0x28,PLP,IMP,4,0), D(0x29,AND,IMM,2,0), D(0x2A,ROL,ACC,2,0), D(0x2C,BIT,ABS,4,0), D(0x2D,AND,ABS,4,0), D(0x2E,ROL,ABS,6,0),
D(0x30,BMI,REL,2,0), D(0x31,AND,INDY,5,1), D(0x35,AND,ZPX,4,0), D(0x36,ROL,ZPX,6,0),
D(0x38,SEC,IMP,2,0), D(0x39,AND,ABSY,4,1), D(0x3D,AND,ABSX,4,1), D(0x3E,ROL,ABSX,7,0),
D(0x40,RTI,IMP,6,0), D(0x41,EOR,INDX,6,0), D(0x45,EOR,ZP,3,0), D(0x46,LSR,ZP,5,0),
D(0x48,PHA,IMP,3,0), D(0x49,EOR,IMM,2,0), D(0x4A,LSR,ACC,2,0), D(0x4C,JMP,ABS,3,0), D(0x4D,EOR,ABS,4,0), D(0x4E,LSR,ABS,6,0),
D(0x50,BVC,REL,2,0), D(0x51,EOR,INDY,5,1), D(0x55,EOR,ZPX,4,0), D(0x56,LSR,ZPX,6,0),
D(0x58,CLI,IMP,2,0), D(0x59,EOR,ABSY,4,1), D(0x5D,EOR,ABSX,4,1), D(0x5E,LSR,ABSX,7,0),
D(0x60,RTS,IMP,6,0), D(0x61,ADC,INDX,6,0), D(0x65,ADC,ZP,3,0), D(0x66,ROR,ZP,5,0),
D(0x68,PLA,IMP,4,0), D(0x69,ADC,IMM,2,0), D(0x6A,ROR,ACC,2,0), D(0x6C,JMP,IND,5,0), D(0x6D,ADC,ABS,4,0), D(0x6E,ROR,ABS,6,0),
D(0x70,BVS,REL,2,0), D(0x71,ADC,INDY,5,1), D(0x75,ADC,ZPX,4,0), D(0x76,ROR,ZPX,6,0),
D(0x78,SEI,IMP,2,0), D(0x79,ADC,ABSY,4,1), D(0x7D,ADC,ABSX,4,1), D(0x7E,ROR,ABSX,7,0),
D(0x81,STA,INDX,6,0), D(0x84,STY,ZP,3,0), D(0x85,STA,ZP,3,0), D(0x86,STX,ZP,3,0), D(0x88,DEY,IMP,2,0),
D(0x8A,TXA,IMP,2,0), D(0x8C,STY,ABS,4,0), D(0x8D,STA,ABS,4,0), D(0x8E,STX,ABS,4,0),
D(0x90,BCC,REL,2,0), D(0x91,STA,INDY,6,0), D(0x94,STY,ZPX,4,0), D(0x95,STA,ZPX,4,0), D(0x96,STX,ZPY,4,0),
D(0x98,TYA,IMP,2,0), D(0x99,STA,ABSY,5,0), D(0x9A,TXS,IMP,2,0), D(0x9D,STA,ABSX,5,0),
D(0xA0,LDY,IMM,2,0), D(0xA1,LDA,INDX,6,0), D(0xA2,LDX,IMM,2,0), D(0xA4,LDY,ZP,3,0), D(0xA5,LDA,ZP,3,0), D(0xA6,LDX,ZP,3,0),
D(0xA8,TAY,IMP,2,0), D(0xA9,LDA,IMM,2,0), D(0xAA,TAX,IMP,2,0), D(0xAC,LDY,ABS,4,0), D(0xAD,LDA,ABS,4,0), D(0xAE,LDX,ABS,4,0),
D(0xB0,BCS,REL,2,0), D(0xB1,LDA,INDY,5,1), D(0xB4,LDY,ZPX,4,0), D(0xB5,LDA,ZPX,4,0), D(0xB6,LDX,ZPY,4,0),
D(0xB8,CLV,IMP,2,0), D(0xB9,LDA,ABSY,4,1), D(0xBA,TSX,IMP,2,0), D(0xBC,LDY,ABSX,4,1), D(0xBD,LDA,ABSX,4,1), D(0xBE,LDX,ABSY,4,1),
D(0xC0,CPY,IMM,2,0), D(0xC1,CMP,INDX,6,0), D(0xC4,CPY,ZP,3,0), D(0xC5,CMP,ZP,3,0), D(0xC6,DEC,ZP,5,0),
D(0xC8,INY,IMP,2,0), D(0xC9,CMP,IMM,2,0), D(0xCA,DEX,IMP,2,0), D(0xCC,CPY,ABS,4,0), D(0xCD,CMP,ABS,4,0), D(0xCE,DEC,ABS,6,0),
D(0xD0,BNE,REL,2,0), D(0xD1,CMP,INDY,5,1), D(0xD5,CMP,ZPX,4,0), D(0xD6,DEC,ZPX,6,0),
D(0xD8,CLD,IMP,2,0), D(0xD9,CMP,ABSY,4,1), D(0xDD,CMP,ABSX,4,1), D(0xDE,DEC,ABSX,7,0),
D(0xE0,CPX,IMM,2,0), D(0xE1,SBC,INDX,6,0), D(0xE4,CPX,ZP,3,0), D(0xE5,SBC,ZP,3,0), D(0xE6,INC,ZP,5,0),
D(0xE8,INX,IMP,2,0), D(0xE9,SBC,IMM,2,0), D(0xEA,NOP,IMP,2,0), D(0xEC,CPX,ABS,4,0), D(0xED,SBC,ABS,4,0), D(0xEE,INC,ABS,6,0),
D(0xF0,BEQ,REL,2,0), D(0xF1,SBC,INDY,5,1), D(0xF5,SBC,ZPX,4,0), D(0xF6,INC,ZPX,6,0),
D(0xF8,SED,IMP,2,0), D(0xF9,SBC,ABSY,4,1), D(0xFD,SBC,ABSX,4,1), D(0xFE,INC,ABSX,7,0)
};
#undef D
size_t cpu_disassemble(Bus6502 *b, uint16_t pc, char *out, size_t size)
{
static const char *names[]={"???","ADC","AND","ASL","BCC","BCS","BEQ","BIT","BMI","BNE","BPL","BRK","BVC","BVS","CLC","CLD","CLI","CLV","CMP","CPX","CPY","DEC","DEX","DEY","EOR","INC","INX","INY","JMP","JSR","LDA","LDX","LDY","LSR","NOP","ORA","PHA","PHP","PLA","PLP","ROL","ROR","RTI","RTS","SBC","SEC","SED","SEI","STA","STX","STY","TAX","TAY","TSX","TXA","TXS","TYA"};
opcode_t d=ops[bus_read(b,pc)];uint8_t a=bus_read(b,(uint16_t)(pc+1));uint8_t z=bus_read(b,(uint16_t)(pc+2));
const char *name=names[d.op];size_t bytes=1;
switch(d.mode){
case AM_ACC:snprintf(out,size,"%s A",name);break;
case AM_IMM:snprintf(out,size,"%s #$%02X",name,a);bytes=2;break;
case AM_ZP:snprintf(out,size,"%s $%02X",name,a);bytes=2;break;
case AM_ZPX:snprintf(out,size,"%s $%02X,X",name,a);bytes=2;break;
case AM_ZPY:snprintf(out,size,"%s $%02X,Y",name,a);bytes=2;break;
case AM_ABS:snprintf(out,size,"%s $%04X",name,(uint16_t)(a|z<<8));bytes=3;break;
case AM_ABSX:snprintf(out,size,"%s $%04X,X",name,(uint16_t)(a|z<<8));bytes=3;break;
case AM_ABSY:snprintf(out,size,"%s $%04X,Y",name,(uint16_t)(a|z<<8));bytes=3;break;
case AM_IND:snprintf(out,size,"%s ($%04X)",name,(uint16_t)(a|z<<8));bytes=3;break;
case AM_INDX:snprintf(out,size,"%s ($%02X,X)",name,a);bytes=2;break;
case AM_INDY:snprintf(out,size,"%s ($%02X),Y",name,a);bytes=2;break;
case AM_REL:snprintf(out,size,"%s $%04X",name,(uint16_t)(pc+2+(int8_t)a));bytes=2;break;
default:snprintf(out,size,"%s",name);break;
}
return bytes;
}
static void set_flag(CPU6502 *c, uint8_t f, bool on) { if (on) c->P |= f; else c->P &= (uint8_t)~f; }
static void set_nz(CPU6502 *c, uint8_t v) { set_flag(c, FLAG_Z, v == 0); set_flag(c, FLAG_N, v & 0x80); }
static void push(CPU6502 *c, Bus6502 *b, uint8_t v) { bus_write(b, (uint16_t)(0x100 | c->SP--), v); }
static uint8_t pull(CPU6502 *c, Bus6502 *b) { return bus_read(b, (uint16_t)(0x100 | ++c->SP)); }
static addr_result_t address(CPU6502 *c, Bus6502 *b, addr_mode_t mode)
{
addr_result_t r = {0, false}; uint16_t base; uint8_t zp;
switch (mode) {
case AM_IMM: r.address = c->PC++; break;
case AM_ZP: r.address = bus_read(b, c->PC++); break;
case AM_ZPX: r.address = (uint8_t)(bus_read(b, c->PC++) + c->X); break;
case AM_ZPY: r.address = (uint8_t)(bus_read(b, c->PC++) + c->Y); break;
case AM_ABS: r.address = bus_read16(b, c->PC); c->PC += 2; break;
case AM_ABSX: base = bus_read16(b, c->PC); c->PC += 2; r.address = base + c->X; r.page_crossed = (base & 0xFF00) != (r.address & 0xFF00); break;
case AM_ABSY: base = bus_read16(b, c->PC); c->PC += 2; r.address = base + c->Y; r.page_crossed = (base & 0xFF00) != (r.address & 0xFF00); break;
case AM_IND: base = bus_read16(b, c->PC); c->PC += 2; r.address = (uint16_t)bus_read(b, base) | ((uint16_t)bus_read(b, (uint16_t)((base & 0xFF00) | (uint8_t)(base + 1))) << 8); break;
case AM_INDX: zp = (uint8_t)(bus_read(b, c->PC++) + c->X); r.address = (uint16_t)bus_read(b, zp) | ((uint16_t)bus_read(b, (uint8_t)(zp + 1)) << 8); break;
case AM_INDY: zp = bus_read(b, c->PC++); base = (uint16_t)bus_read(b, zp) | ((uint16_t)bus_read(b, (uint8_t)(zp + 1)) << 8); r.address = base + c->Y; r.page_crossed = (base & 0xFF00) != (r.address & 0xFF00); break;
case AM_REL: r.address = (uint16_t)(c->PC + 1 + (int8_t)bus_read(b, c->PC)); c->PC++; break;
default: break;
}
return r;
}
static void interrupt(CPU6502 *c, Bus6502 *b, uint16_t vector, bool brk)
{
push(c, b, (uint8_t)(c->PC >> 8)); push(c, b, (uint8_t)c->PC);
push(c, b, (uint8_t)((c->P | FLAG_U | (brk ? FLAG_B : 0)) & (brk ? 0xFF : ~FLAG_B)));
c->P |= FLAG_I; c->PC = bus_read16(b, vector);
}
static void compare(CPU6502 *c, uint8_t reg, uint8_t val)
{ uint8_t out = (uint8_t)(reg - val); set_flag(c, FLAG_C, reg >= val); set_nz(c, out); }
static void adc(CPU6502 *c, uint8_t m)
{
uint8_t a = c->A, carry = !!(c->P & FLAG_C); uint16_t binary = (uint16_t)a + m + carry;
set_flag(c, FLAG_V, (~(a ^ m) & (a ^ (uint8_t)binary) & 0x80) != 0);
if (c->P & FLAG_D) {
uint16_t decimal = binary;
if ((a & 0x0F) + (m & 0x0F) + carry > 9) decimal += 6;
set_flag(c, FLAG_C, decimal > 0x99);
if (decimal > 0x99) decimal += 0x60;
c->A = (uint8_t)decimal;
set_flag(c, FLAG_Z, (uint8_t)binary == 0); set_flag(c, FLAG_N, binary & 0x80);
} else { set_flag(c, FLAG_C, binary > 0xFF); c->A = (uint8_t)binary; set_nz(c, c->A); }
}
static void sbc(CPU6502 *c, uint8_t m)
{
uint8_t a = c->A, carry = !!(c->P & FLAG_C); uint16_t binary = (uint16_t)a + (uint8_t)~m + carry;
uint8_t result = (uint8_t)binary;
set_flag(c, FLAG_V, ((a ^ result) & (a ^ m) & 0x80) != 0);
set_flag(c, FLAG_C, binary > 0xFF); set_nz(c, result);
if (c->P & FLAG_D) {
int16_t lo = (a & 15) - (m & 15) - (carry ? 0 : 1);
int16_t hi = (a >> 4) - (m >> 4);
if (lo < 0) { lo -= 6; hi--; }
if (hi < 0) hi -= 6;
c->A = (uint8_t)(((hi * 16) & 0xF0) | (lo & 0x0F));
} else c->A = result;
}
static bool is_branch(operation_t op)
{ return op >= O_BCC && op <= O_BVS && op != O_BIT && op != O_BRK; }
cpu_step_result_t cpu_step(CPU6502 *c, Bus6502 *b)
{
cpu_step_result_t out = {CPU_STEP_OK, 0, 0}; opcode_t d; addr_result_t ar; uint8_t v, old; bool take = false;
if (c->nmi_pending) { c->nmi_pending = false; interrupt(c,b,0xFFFA,false); out.cycles=7; return out; }
if (c->irq_asserted && !(c->P & FLAG_I)) { interrupt(c,b,0xFFFE,false); out.cycles=7; return out; }
out.opcode = bus_read(b, c->PC++); d = ops[out.opcode];
if (d.op == O_BAD) { out.status = CPU_STEP_ILLEGAL_OPCODE; return out; }
ar = address(c,b,d.mode); out.cycles = d.cycles + (d.page && ar.page_crossed);
v = (d.mode == AM_IMP || d.mode == AM_ACC || d.mode == AM_REL) ? 0 : bus_read(b,ar.address);
if (is_branch(d.op)) {
switch(d.op) { case O_BCC:take=!(c->P&FLAG_C);break; case O_BCS:take=c->P&FLAG_C;break; case O_BEQ:take=c->P&FLAG_Z;break; case O_BMI:take=c->P&FLAG_N;break; case O_BNE:take=!(c->P&FLAG_Z);break; case O_BPL:take=!(c->P&FLAG_N);break; case O_BVC:take=!(c->P&FLAG_V);break; case O_BVS:take=c->P&FLAG_V;break; default:break; }
if (take) { uint16_t from=c->PC; c->PC=ar.address; out.cycles += 1 + ((from&0xFF00)!=(c->PC&0xFF00)); } return out;
}
switch (d.op) {
case O_ADC: adc(c,v); break; case O_AND:c->A&=v;set_nz(c,c->A);break;
case O_ASL: old=d.mode==AM_ACC?c->A:v;set_flag(c,FLAG_C,old&0x80);old<<=1;set_nz(c,old);if(d.mode==AM_ACC)c->A=old;else bus_write(b,ar.address,old);break;
case O_BIT:set_flag(c,FLAG_Z,(c->A&v)==0);set_flag(c,FLAG_N,v&0x80);set_flag(c,FLAG_V,v&0x40);break;
case O_BRK:c->PC++;interrupt(c,b,0xFFFE,true);break;
case O_CLC:c->P&=~FLAG_C;break; case O_CLD:c->P&=~FLAG_D;break; case O_CLI:c->P&=~FLAG_I;break; case O_CLV:c->P&=~FLAG_V;break;
case O_CMP:compare(c,c->A,v);break; case O_CPX:compare(c,c->X,v);break; case O_CPY:compare(c,c->Y,v);break;
case O_DEC:v--;bus_write(b,ar.address,v);set_nz(c,v);break; case O_DEX:c->X--;set_nz(c,c->X);break; case O_DEY:c->Y--;set_nz(c,c->Y);break;
case O_EOR:c->A^=v;set_nz(c,c->A);break; case O_INC:v++;bus_write(b,ar.address,v);set_nz(c,v);break; case O_INX:c->X++;set_nz(c,c->X);break; case O_INY:c->Y++;set_nz(c,c->Y);break;
case O_JMP:c->PC=ar.address;break; case O_JSR:{uint16_t ret=c->PC-1;push(c,b,(uint8_t)(ret>>8));push(c,b,(uint8_t)ret);c->PC=ar.address;}break;
case O_LDA:c->A=v;set_nz(c,v);break; case O_LDX:c->X=v;set_nz(c,v);break; case O_LDY:c->Y=v;set_nz(c,v);break;
case O_LSR:old=d.mode==AM_ACC?c->A:v;set_flag(c,FLAG_C,old&1);old>>=1;set_nz(c,old);if(d.mode==AM_ACC)c->A=old;else bus_write(b,ar.address,old);break;
case O_NOP:break; case O_ORA:c->A|=v;set_nz(c,c->A);break; case O_PHA:push(c,b,c->A);break; case O_PHP:push(c,b,c->P|FLAG_B|FLAG_U);break;
case O_PLA:c->A=pull(c,b);set_nz(c,c->A);break; case O_PLP:c->P=(pull(c,b)&~FLAG_B)|FLAG_U;break;
case O_ROL:old=d.mode==AM_ACC?c->A:v;{bool nc=old&0x80;old=(uint8_t)((old<<1)|!!(c->P&FLAG_C));set_flag(c,FLAG_C,nc);}set_nz(c,old);if(d.mode==AM_ACC)c->A=old;else bus_write(b,ar.address,old);break;
case O_ROR:old=d.mode==AM_ACC?c->A:v;{bool nc=old&1;old=(uint8_t)((old>>1)|((c->P&FLAG_C)?0x80:0));set_flag(c,FLAG_C,nc);}set_nz(c,old);if(d.mode==AM_ACC)c->A=old;else bus_write(b,ar.address,old);break;
case O_RTI:c->P=(pull(c,b)&~FLAG_B)|FLAG_U;c->PC=pull(c,b);c->PC|=(uint16_t)pull(c,b)<<8;break;
case O_RTS:c->PC=pull(c,b);c->PC|=(uint16_t)pull(c,b)<<8;c->PC++;break; case O_SBC:sbc(c,v);break;
case O_SEC:c->P|=FLAG_C;break; case O_SED:c->P|=FLAG_D;break; case O_SEI:c->P|=FLAG_I;break;
case O_STA:bus_write(b,ar.address,c->A);break; case O_STX:bus_write(b,ar.address,c->X);break; case O_STY:bus_write(b,ar.address,c->Y);break;
case O_TAX:c->X=c->A;set_nz(c,c->X);break; case O_TAY:c->Y=c->A;set_nz(c,c->Y);break; case O_TSX:c->X=c->SP;set_nz(c,c->X);break;
case O_TXA:c->A=c->X;set_nz(c,c->A);break; case O_TXS:c->SP=c->X;break; case O_TYA:c->A=c->Y;set_nz(c,c->A);break;
default:break;
}
c->P |= FLAG_U;
return out;
}
+8
View File
@@ -0,0 +1,8 @@
# External validation ROMs
`make fetch-roms` downloads the official Klaus Dormann NMOS 6502 functional
test from the [upstream GPL-3.0 repository](https://github.com/Klaus2m5/6502_65C02_functional_tests).
The download is pinned by SHA-256 and is not committed here.
`make test-functional` fetches and runs it. Success is the documented loop at
`$3469`.
+27
View File
@@ -0,0 +1,27 @@
#include <stdio.h>
#include <stdlib.h>
#include "bus.h"
#include "cpu.h"
#include "loader.h"
static unsigned long number(const char *s)
{
char *end; unsigned long n=strtoul(s,&end,0);
if(!*s||*end||n>0xFFFF){fprintf(stderr,"invalid 16-bit number: %s\n",s);exit(2);}return n;
}
int main(int argc,char **argv)
{
Bus6502 bus;CPU6502 cpu;size_t loaded;uint16_t load,start,success;unsigned long limit=100000000;
if(argc<5||argc>6){fprintf(stderr,"usage: %s ROM LOAD START SUCCESS [LIMIT]\n",argv[0]);return 2;}
load=(uint16_t)number(argv[2]);start=(uint16_t)number(argv[3]);success=(uint16_t)number(argv[4]);
if(argc==6){char *end;limit=strtoul(argv[5],&end,0);if(!*argv[5]||*end||!limit){fprintf(stderr,"invalid limit\n");return 2;}}
bus_init(&bus);if(load_binary(&bus,argv[1],load,&loaded)){fprintf(stderr,"failed to load %s\n",argv[1]);return 1;}
bus_write16(&bus,0xFFFC,start);cpu_reset(&cpu,&bus);
for(unsigned long i=0;i<limit;i++){
uint16_t before=cpu.PC;cpu_step_result_t r=cpu_step(&cpu,&bus);
if(r.status!=CPU_STEP_OK){fprintf(stderr,"illegal $%02X at $%04X after %lu instructions\n",r.opcode,before,i);return 1;}
if(cpu.PC==success){printf("success at $%04X after %lu instructions (%zu-byte ROM)\n",success,i+1,loaded);return 0;}
}
fprintf(stderr,"limit reached at $%04X\n",cpu.PC);return 1;
}
+76
View File
@@ -0,0 +1,76 @@
#include <stdio.h>
#include <string.h>
#include "bus.h"
#include "cpu.h"
static int tests, failures;
#define CHECK(expr) do { tests++; if (!(expr)) { failures++; fprintf(stderr,"%s:%d: check failed: %s\n",__FILE__,__LINE__,#expr); } } while(0)
#define EQ8(a,b) CHECK((uint8_t)(a)==(uint8_t)(b))
#define EQ16(a,b) CHECK((uint16_t)(a)==(uint16_t)(b))
static void setup(CPU6502 *cpu, Bus6502 *bus, uint16_t pc)
{ bus_init(bus); bus_write16(bus,0xFFFC,pc); cpu_reset(cpu,bus); }
static cpu_step_result_t step(CPU6502 *c, Bus6502 *b) { return cpu_step(c,b); }
static void test_reset_and_bus(void)
{
CPU6502 c; Bus6502 b; setup(&c,&b,0xC123);
EQ16(c.PC,0xC123); EQ8(c.SP,0xFD); EQ8(c.P,FLAG_U|FLAG_I);
bus_write16(&b,0x1234,0xABCD); EQ16(bus_read16(&b,0x1234),0xABCD);
b.easy6502_io=true; bus_set_keyboard(&b,'w'); EQ8(bus_read(&b,0xFF),'w');
bus_seed_rng(&b,42); uint8_t x=bus_read(&b,0xFE); bus_seed_rng(&b,42); EQ8(bus_read(&b,0xFE),x);
}
static void test_load_store_and_modes(void)
{
CPU6502 c; Bus6502 b; setup(&c,&b,0x8000);
const uint8_t p[]={0xA2,0x02,0xA9,0x80,0x95,0xFF,0xA9,0x00,0xB5,0xFF};
memcpy(&b.memory.data[0x8000],p,sizeof p);
EQ8(step(&c,&b).cycles,2); EQ8(c.X,2); step(&c,&b); CHECK(c.P&FLAG_N);
step(&c,&b); EQ8(bus_read(&b,1),0x80); step(&c,&b); step(&c,&b); EQ8(c.A,0x80);
setup(&c,&b,0x80F0); c.X=1; b.memory.data[0x80F0]=0xBD; b.memory.data[0x80F1]=0xFF; b.memory.data[0x80F2]=0x12; b.memory.data[0x1300]=0x55;
EQ8(step(&c,&b).cycles,5); EQ8(c.A,0x55);
}
static void test_indirect_and_control(void)
{
CPU6502 c; Bus6502 b; setup(&c,&b,0x8000);
b.memory.data[0x8000]=0x6C;b.memory.data[0x8001]=0xFF;b.memory.data[0x8002]=0x12;
b.memory.data[0x12FF]=0x34;b.memory.data[0x1200]=0x12;b.memory.data[0x1300]=0x99;
step(&c,&b); EQ16(c.PC,0x1234);
setup(&c,&b,0x9000); b.memory.data[0x9000]=0x20;b.memory.data[0x9001]=0x00;b.memory.data[0x9002]=0xA0;b.memory.data[0xA000]=0x60;
EQ8(step(&c,&b).cycles,6);EQ16(c.PC,0xA000);EQ8(c.SP,0xFB);step(&c,&b);EQ16(c.PC,0x9003);EQ8(c.SP,0xFD);
}
static void test_alu_and_flags(void)
{
CPU6502 c; Bus6502 b; setup(&c,&b,0x8000);
const uint8_t p[]={0xA9,0x7F,0x18,0x69,0x01,0x38,0xE9,0x01,0xC9,0x7F}; memcpy(&b.memory.data[0x8000],p,sizeof p);
step(&c,&b);step(&c,&b);step(&c,&b);EQ8(c.A,0x80);CHECK(c.P&FLAG_V);CHECK(c.P&FLAG_N);
step(&c,&b);step(&c,&b);EQ8(c.A,0x7F);CHECK(c.P&FLAG_C);step(&c,&b);CHECK(c.P&FLAG_Z);CHECK(c.P&FLAG_C);
setup(&c,&b,0x8000); const uint8_t d[]={0xF8,0x18,0xA9,0x49,0x69,0x51};memcpy(&b.memory.data[0x8000],d,sizeof d);
step(&c,&b);step(&c,&b);step(&c,&b);step(&c,&b);EQ8(c.A,0x00);CHECK(c.P&FLAG_C);
setup(&c,&b,0x8000); const uint8_t s[]={0xF8,0x38,0xA9,0x00,0xE9,0x01};memcpy(&b.memory.data[0x8000],s,sizeof s);
step(&c,&b);step(&c,&b);step(&c,&b);step(&c,&b);EQ8(c.A,0x99);CHECK(!(c.P&FLAG_C));
}
static void test_stack_interrupts(void)
{
CPU6502 c; Bus6502 b; setup(&c,&b,0x8000); bus_write16(&b,0xFFFE,0x9000);b.memory.data[0x8000]=0x00;b.memory.data[0x9000]=0x40;
c.P=FLAG_U|FLAG_C;step(&c,&b);EQ16(c.PC,0x9000);EQ8(c.SP,0xFA);CHECK(b.memory.data[0x1FB]&FLAG_B);step(&c,&b);EQ16(c.PC,0x8002);CHECK(c.P&FLAG_C);CHECK(!(c.P&FLAG_B));
setup(&c,&b,0x8123);bus_write16(&b,0xFFFA,0xA000);cpu_request_nmi(&c);EQ8(step(&c,&b).cycles,7);EQ16(c.PC,0xA000);EQ8(b.memory.data[0x1FD],0x81);EQ8(b.memory.data[0x1FC],0x23);
}
static void test_branches_and_illegal(void)
{
CPU6502 c; Bus6502 b; setup(&c,&b,0x80FD);c.P&=~FLAG_Z;b.memory.data[0x80FD]=0xD0;b.memory.data[0x80FE]=0x01;
EQ8(step(&c,&b).cycles,4);EQ16(c.PC,0x8100);
b.memory.data[0x8100]=0x02;cpu_step_result_t r=step(&c,&b);CHECK(r.status==CPU_STEP_ILLEGAL_OPCODE);EQ8(r.opcode,0x02);
}
int main(void)
{
test_reset_and_bus();test_load_store_and_modes();test_indirect_and_control();
test_alu_and_flags();test_stack_interrupts();test_branches_and_illegal();
printf("%d checks, %d failures\n",tests,failures);return failures?1:0;
}