mirror of
https://github.com/andwati/mos-6502.git
synced 2026-10-18 03:02:51 +00:00
completed game
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"snyk.advanced.autoSelectOrganization": true
|
||||
}
|
||||
@@ -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`.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,15 +1,74 @@
|
||||
# A mos-6502 emulator in c
|
||||
[Design Doc ](./docs/design.md)
|
||||
|
||||
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.
|
||||
|
||||
## Links
|
||||
|
||||
http://en.wikipedia.org/wiki/MOS_Technology_6502
|
||||
|
||||
http://www.6502.org/documents/datasheets/mos/
|
||||
|
||||
http://www.mdawson.net/vic20chrome/cpu/mos_6500_mpu_preliminary_may_1976.pdf
|
||||
|
||||
http://rubbermallet.org/fake6502.c
|
||||
|
||||
# MOS 6502 emulator
|
||||
|
||||
A portable NMOS MOS 6502 emulator in C with an Easy6502-compatible virtual
|
||||
machine and an original Snake game.
|
||||
|
||||
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.
|
||||
|
||||
## Build and test
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```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.
|
||||
|
||||
+8
-8
@@ -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).
|
||||
@@ -173,4 +173,4 @@ While an instruction-stepped emulator (Phase 0-10) is sufficient for many tasks,
|
||||
## Implementing Cycle Accuracy
|
||||
To achieve this, the m6502_step function must be modified to return the number of cycles used, or better yet, to "tick" external components during each internal micro-operation.
|
||||
|
||||
A common optimization is to use a lookup table for the base cycle counts of each opcode and then add cycles for "penalties" like page boundary crossings or branch completions
|
||||
A common optimization is to use a lookup table for the base cycle counts of each opcode and then add cycles for "penalties" like page boundary crossings or branch completions
|
||||
|
||||
+22
-476
@@ -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.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
MEMORY {
|
||||
ROM: start = $0600, size = $0300, file = %O, fill = no;
|
||||
}
|
||||
SEGMENTS {
|
||||
CODE: load = ROM, type = ro;
|
||||
}
|
||||
+255
@@ -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
|
||||
+10
-11
@@ -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;
|
||||
typedef struct {
|
||||
uint16_t address;
|
||||
bool page_crossed;
|
||||
} addr_result_t;
|
||||
|
||||
// cpu registers
|
||||
|
||||
} addr_ctx_t;
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
+28
-20
@@ -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;
|
||||
|
||||
#endif
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Addressing-mode types are public; resolution stays private to cpu_step. */
|
||||
typedef int addressing_translation_unit_is_not_empty;
|
||||
|
||||
@@ -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; }
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
+50
-10
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user