diff --git a/Cargo.lock b/Cargo.lock index ba0efaa..0a1f1f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,300 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] -name = "mos6502" +name = "aho-corasick" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "env_logger" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "humantime" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mos6502-cpu-emulator" +version = "0.1.0" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ncurses" +version = "5.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "numtoa" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pkg-config" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quick-error" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_syscall" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "termcolor" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ucd-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf8-ranges" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wincolor" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] +[metadata] +"checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c" +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" +"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +"checksum cc 1.0.36 (registry+https://github.com/rust-lang/crates.io-index)" = "a0c56216487bb80eec9c4516337b2588a4f2a2290d72a1416d930e4dcdb0c90d" +"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" +"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a" +"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" +"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" +"checksum libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)" = "c6785aa7dd976f5fbf3b71cfd9cd49d7f783c1ff565a858d71031c6c313aa5c6" +"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" +"checksum ncurses 5.99.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15699bee2f37e9f8828c7b35b2bc70d13846db453f2d507713b758fabe536b82" +"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" +"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" +"checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0a0bcab2fd7d1d7c54fa9eae6f43eddeb9ce2e7352f8518a814a4f65d60c58" +"checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96" +"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f" +"checksum termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea" +"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" +"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" +"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba" diff --git a/Cargo.toml b/Cargo.toml index c7f9de9..f739a05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,30 @@ [package] -name = "mos6502" +name = "mos6502-cpu-emulator" version = "0.1.0" authors = ["Alexander Akhmetov "] edition = "2018" +[lib] +name = "mos6502" +path = "src/lib.rs" +authors = ["Alexander Akhmetov "] +edition = "2018" + +[[bin]] +name = "apple1" +path = "src/apple1.rs" +required-features = ["build-binary"] + +[[bin]] +name = "mos6502-cli" +path = "src/cli.rs" +required-features = ["build-binary"] + [dependencies] +log = "0.4" +env_logger = {version = "0.6.1", optional = true } +clap = {version = "2.33.0", optional = true } +ncurses = {version = "5.99.0", optional = true} + +[features] +build-binary = ["clap", "env_logger", "ncurses"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5919f2f --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +replica1: + RUST_LOG=warn cargo run --features build-binary --bin apple1 -- -p roms/replica1.bin -l e000 + +apple30: + RUST_LOG=warn cargo run --features build-binary --bin apple1 -- -p roms/apple30.bin -l 280 + +minichess: + RUST_LOG=error cargo run --features build-binary --bin apple1 -- -p roms/ASMmchess.bin -l 300 + +functional-tests: + RUST_LOG=info cargo run --features build-binary --bin mos6502-cli roms/6502_functional_test.bin 400 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bdf731 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Apple1 emulator + +## How to use + +Run binary: + +``` +cargo run --features build-binary --bin apple1 +``` + +The command above starts Apple1 with WozMonitor at the address `0xFF00`. You should see the screen and the command line prompt: + +``` +\ + +``` + + +With optional flag `-p` you can load an additional program to the memory: + +``` +cargo run --features build-binary --bin apple1 -- -p asm/apple1hello.asm +``` +It will be loaded to the memory with starting address `0xE000`. To run it using Woz Monitor type `E000R` and press enter. +To see the hex content of the program: `E000..`, for example: `E000.E0FF`. + +## Debug + +You can disable the screen and enable debug logging: + +``` +RUST_LOG=debug cargo run --features build-binary --bin apple1 -- -p asm/apple1hello.asm +``` + +## Tests + +Run tests: + +``` +cargo test +``` + +Run tests with logging: + +``` +RUST_LOG=mos6502=debug cargo test -- --nocapture +``` + +## Apple 1 Basic + +* [BASIC source code listing](https://github.com/jefftranter/6502/blob/master/asm/a1basic/a1basic.s) +* [Disassembled BASIC](http://www.brouhaha.com/~eric/retrocomputing/apple/apple1/basic/) + + +There are two different ROMs, one of them is from Replica1 and it works. The original one, `roms/apple1basic.bin` does not work yet. Seems like it tries to read DSP at `0xD0F2` instead of `0xD012`. You can inspect this if you load it and then print hex data at `E3D5.E3DF` with WozMonitor. + + note: http://www.brielcomputers.com/phpBB3/viewtopic.php?f=10&t=404 + discussion about the same problem + +``` +E3D5.E3DF + +E3D5: 2C F2 D0 +E3D8: 30 FB 8D F2 D0 60 A0 06 +``` + +Replica1 basic content: + +``` +E3D5.E3DF + +E3D5: 2C 12 D0 +E3D8: 30 FB 8D 12 D0 60 A0 06 +``` + +### Start basic + +``` +RUST_LOG=error cargo run --features build-binary --bin apple1 -- -p roms/replica1.bin -b +``` + +and then type `E000R`. + + +## Resources + +* [6502 instruction set](https://www.masswerk.at/6502/6502_instruction_set.html#BIT) +* [Apple1 BASIC manual](https://archive.org/stream/apple1_basic_manual/apple1_basic_manual_djvu.txt) +* [www.applefritter.com](https://www.applefritter.com) +* [6502 memory test](http://www.willegal.net/appleii/6502mem.htm) +* [apple1 programs](http://hoop-la.ca/apple2/2008/retrochallenge.net.html) +* [apple1 programs 2](http://www.willegal.net/appleii/apple1-software.htm) +* [Woz Monitor description](https://www.sbprojects.net/projects/apple1/wozmon.php) +* [6502 instructions description with undocumented commands](http://www.zimmers.net/anonftp/pub/cbm/documents/chipdata/64doc) diff --git a/asm/apple1hello.asm b/asm/apple1hello.asm new file mode 100644 index 0000000..671970d --- /dev/null +++ b/asm/apple1hello.asm @@ -0,0 +1,38 @@ +LDA #$8D +JSR $FFEF + +LDA #$C8 +JSR $FFEF + +LDA #$C5 +JSR $FFEF + +LDA #$CC +JSR $FFEF +LDA #$CC +JSR $FFEF + +LDA #$CF +JSR $FFEF + +LDA #$A0 +JSR $FFEF + +LDA #$D7 +JSR $FFEF +LDA #$CF +JSR $FFEF +LDA #$D2 +JSR $FFEF +LDA #$CC +JSR $FFEF +LDA #$C4 +JSR $FFEF + +LDA #$A1 +JSR $FFEF + +LDA #$8D +JSR $FFEF + +JMP $FF1F diff --git a/asm/cycle.asm b/asm/cycle.asm new file mode 100644 index 0000000..08800cd --- /dev/null +++ b/asm/cycle.asm @@ -0,0 +1,7 @@ + LDX #$5 + LDY #$0 + DEX + INY + CPX #$0 + BNE #$FC + BRK diff --git a/asm/hello.asm b/asm/hello.asm new file mode 100644 index 0000000..0e40f19 --- /dev/null +++ b/asm/hello.asm @@ -0,0 +1,4 @@ + LDA #$0 + LDX #$1 + LDY #$2 + BRK diff --git a/asm/stack.asm b/asm/stack.asm new file mode 100644 index 0000000..656353a --- /dev/null +++ b/asm/stack.asm @@ -0,0 +1,14 @@ +LDX #$00 +LDY #$00 +TXA +STA $0200 +PHA +INX +INY +CPY #$10 +BNE #$F7 +PLA +STA $0200 +INY +CPY #$20 +BNE #$F9 diff --git a/asm/wozmon.asm b/asm/wozmon.asm new file mode 100644 index 0000000..2ddbb24 --- /dev/null +++ b/asm/wozmon.asm @@ -0,0 +1,260 @@ +;------------------------------------------------------------------------- +; +; The WOZ Monitor for the Apple 1 +; Written by Steve Wozniak 1976 +; +;------------------------------------------------------------------------- + + .CR 6502 + .OR $FF00 + .TF WOZMON.HEX,HEX,8 + +;------------------------------------------------------------------------- +; Memory declaration +;------------------------------------------------------------------------- + +XAML .EQ $24 Last "opened" location Low +XAMH .EQ $25 Last "opened" location High +STL .EQ $26 Store address Low +STH .EQ $27 Store address High +L .EQ $28 Hex value parsing Low +H .EQ $29 Hex value parsing High +YSAV .EQ $2A Used to see if hex value is given +MODE .EQ $2B $00=XAM, $7F=STOR, $AE=BLOCK XAM + +IN .EQ $0200,$027F Input buffer + +KBD .EQ $D010 PIA.A keyboard input +KBDCR .EQ $D011 PIA.A keyboard control register +DSP .EQ $D012 PIA.B display output register +DSPCR .EQ $D013 PIA.B display control register + +; KBD b7..b0 are inputs, b6..b0 is ASCII input, b7 is constant high +; Programmed to respond to low to high KBD strobe +; DSP b6..b0 are outputs, b7 is input +; CB2 goes low when data is written, returns high when CB1 goes high +; Interrupts are enabled, though not used. KBD can be jumpered to IRQ, +; whereas DSP can be jumpered to NMI. + +;------------------------------------------------------------------------- +; Constants +;------------------------------------------------------------------------- + +BS .EQ $DF Backspace key, arrow left key +CR .EQ $8D Carriage Return +ESC .EQ $9B ESC key +PROMPT .EQ "\" Prompt character + +;------------------------------------------------------------------------- +; Let's get started +; +; Remark the RESET routine is only to be entered by asserting the RESET +; line of the system. This ensures that the data direction registers +; are selected. +;------------------------------------------------------------------------- + +RESET CLD Clear decimal arithmetic mode + CLI + LDY #%0111.1111 Mask for DSP data direction reg + STY DSP (DDR mode is assumed after reset) + LDA #%1010.0111 KBD and DSP control register mask + STA KBDCR Enable interrupts, set CA1, CB1 for + STA DSPCR positive edge sense/output mode. + +; Program falls through to the GETLINE routine to save some program bytes +; Please note that Y still holds $7F, which will cause an automatic Escape + +;------------------------------------------------------------------------- +; The GETLINE process +;------------------------------------------------------------------------- + +NOTCR CMP #BS Backspace key? + BEQ BACKSPACE Yes + CMP #ESC ESC? + BEQ ESCAPE Yes + INY Advance text index + BPL NEXTCHAR Auto ESC if line longer than 127 + +ESCAPE LDA #PROMPT Print prompt character + JSR ECHO Output it. + +GETLINE LDA #CR Send CR + JSR ECHO + + LDY #0+1 Start a new input line +BACKSPACE DEY Backup text index + BMI GETLINE Oops, line's empty, reinitialize + +NEXTCHAR LDA KBDCR Wait for key press + BPL NEXTCHAR No key yet! + LDA KBD Load character. B7 should be '1' + STA IN,Y Add to text buffer + JSR ECHO Display character + CMP #CR + BNE NOTCR It's not CR! + +; Line received, now let's parse it + + LDY #-1 Reset text index + LDA #0 Default mode is XAM + TAX X=0 + +SETSTOR ASL Leaves $7B if setting STOR mode + +SETMODE STA MODE Set mode flags + +BLSKIP INY Advance text index + +NEXTITEM LDA IN,Y Get character + CMP #CR + BEQ GETLINE We're done if it's CR! + CMP #"." + BCC BLSKIP Ignore everything below "."! + BEQ SETMODE Set BLOCK XAM mode ("." = $AE) + CMP #":" + BEQ SETSTOR Set STOR mode! $BA will become $7B + CMP #"R" + BEQ RUN Run the program! Forget the rest + STX L Clear input value (X=0) + STX H + STY YSAV Save Y for comparison + +; Here we're trying to parse a new hex value + +NEXTHEX LDA IN,Y Get character for hex test + EOR #$B0 Map digits to 0-9 + CMP #9+1 Is it a decimal digit? + BCC DIG Yes! + ADC #$88 Map letter "A"-"F" to $FA-FF + CMP #$FA Hex letter? + BCC NOTHEX No! Character not hex + +DIG ASL + ASL Hex digit to MSD of A + ASL + ASL + + LDX #4 Shift count +HEXSHIFT ASL Hex digit left, MSB to carry + ROL L Rotate into LSD + ROL H Rotate into MSD's + DEX Done 4 shifts? + BNE HEXSHIFT No, loop + INY Advance text index + BNE NEXTHEX Always taken + +NOTHEX CPY YSAV Was at least 1 hex digit given? + BEQ ESCAPE No! Ignore all, start from scratch + + BIT MODE Test MODE byte + BVC NOTSTOR B6=0 is STOR, 1 is XAM or BLOCK XAM + +; STOR mode, save LSD of new hex byte + + LDA L LSD's of hex data + STA (STL,X) Store current 'store index'(X=0) + INC STL Increment store index. + BNE NEXTITEM No carry! + INC STH Add carry to 'store index' high +TONEXTITEM JMP NEXTITEM Get next command item. + +;------------------------------------------------------------------------- +; RUN user's program from last opened location +;------------------------------------------------------------------------- + +RUN JMP (XAML) Run user's program + +;------------------------------------------------------------------------- +; We're not in Store mode +;------------------------------------------------------------------------- + +NOTSTOR BMI XAMNEXT B7 = 0 for XAM, 1 for BLOCK XAM + +; We're in XAM mode now + + LDX #2 Copy 2 bytes +SETADR LDA L-1,X Copy hex data to + STA STL-1,X 'store index' + STA XAML-1,X and to 'XAM index' + DEX Next of 2 bytes + BNE SETADR Loop unless X = 0 + +; Print address and data from this address, fall through next BNE. + +NXTPRNT BNE PRDATA NE means no address to print + LDA #CR Print CR first + JSR ECHO + LDA XAMH Output high-order byte of address + JSR PRBYTE + LDA XAML Output low-order byte of address + JSR PRBYTE + LDA #":" Print colon + JSR ECHO + +PRDATA LDA #" " Print space + JSR ECHO + LDA (XAML,X) Get data from address (X=0) + JSR PRBYTE Output it in hex format +XAMNEXT STX MODE 0 -> MODE (XAM mode). + LDA XAML See if there's more to print + CMP L + LDA XAMH + SBC H + BCS TONEXTITEM Not less! No more data to output + + INC XAML Increment 'examine index' + BNE MOD8CHK No carry! + INC XAMH + +MOD8CHK LDA XAML If address MOD 8 = 0 start new line + AND #%0000.0111 + BPL NXTPRNT Always taken. + +;------------------------------------------------------------------------- +; Subroutine to print a byte in A in hex form (destructive) +;------------------------------------------------------------------------- + +PRBYTE PHA Save A for LSD + LSR + LSR + LSR MSD to LSD position + LSR + JSR PRHEX Output hex digit + PLA Restore A + +; Fall through to print hex routine + +;------------------------------------------------------------------------- +; Subroutine to print a hexadecimal digit +;------------------------------------------------------------------------- + +PRHEX AND #%0000.1111 Mask LSD for hex print + ORA #"0" Add "0" + CMP #"9"+1 Is it a decimal digit? + BCC ECHO Yes! output it + ADC #6 Add offset for letter A-F + +; Fall through to print routine + +;------------------------------------------------------------------------- +; Subroutine to print a character to the terminal +;------------------------------------------------------------------------- + +ECHO BIT DSP DA bit (B7) cleared yet? + BMI ECHO No! Wait for display ready + STA DSP Output character. Sets DA + RTS + +;------------------------------------------------------------------------- +; Vector area +;------------------------------------------------------------------------- + + .DA $0000 Unused, what a pity +NMI_VEC .DA $0F00 NMI vector +RESET_VEC .DA RESET RESET vector +IRQ_VEC .DA $0000 IRQ vector + +;------------------------------------------------------------------------- + + .LI OFF + diff --git a/hex/ASMmchess.txt b/hex/ASMmchess.txt new file mode 100644 index 0000000..469b644 --- /dev/null +++ b/hex/ASMmchess.txt @@ -0,0 +1,281 @@ +A2 58 BD 80 04 20 EF FF +CA 10 F7 20 E0 05 29 03 +F0 10 C9 01 F0 06 A0 FF +A9 00 F0 0A A0 FB A9 00 +F0 04 A0 FB A9 08 84 BE +85 BF EA EA A2 00 BD E6 +09 C9 AB F0 06 20 EF FF +E8 D0 F3 20 E0 05 29 0F +C9 0A 30 0B A5 FF 38 E9 +0A B0 FC 69 0A 29 0E 0A +AA BD EB 03 85 FD BD EC +03 85 FE A0 1B B1 FD 99 +C0 00 88 10 F8 A2 10 BD +EF 05 95 2F CA D0 F8 A2 +40 BD AF 06 95 6F CA D0 +F8 4C 00 04 FF FF FF FF +FF FF FF FF FF FF FF FF +FF FF FF FF FF FF FF FF +FF FF FF FF FF FF FF FF +FF FF FF FF FF A2 1F B5 +50 C5 FA F0 03 CA 10 F7 +86 FB 86 B0 D8 A2 FF 9A +A2 C8 86 B2 4C 0B 04 C9 +FF D0 04 A2 D9 D0 0E C9 +EE D0 04 A2 E4 D0 06 C9 +CC D0 0D A2 F3 BD 00 04 +F0 13 20 EF FF E8 D0 F5 +B5 F8 20 DC FF A9 A0 20 +EF FF CA D0 F3 A9 8D 20 +EF FF 60 B0 0A CC 0A E8 +0A 04 0B 20 0B 3C 0B 58 +0B 74 0B 90 0B AC 0B 00 +D8 A2 FF 9A A2 C8 86 B2 +20 00 09 20 3D 09 EA EA +EA EA 85 F3 C9 0C D0 0F +A2 1F B5 70 95 50 CA 10 +F9 86 DC A9 CC D0 12 C9 +0E D0 07 20 B2 07 A9 EE +D0 07 C9 14 D0 0B 20 A2 +08 85 FB 85 FA 85 F9 D0 +BF C9 0F D0 06 20 50 04 +4C 9D 03 4C 96 06 00 00 +85 B9 86 BA 84 BB 20 00 +09 A5 B9 A6 BA A4 BB 4C +4B 08 FF FF FF FF FF FF +FF FF FF FF FF FF FF FF +FF FF FF FF FF FF FF FF +FF FF FF FF FF FF FF FF +8D A0 A0 BF 8D 8D DA D4 +C9 CC C2 D2 C5 D0 D5 D3 +BD B2 8D DA D4 C9 CC C2 +BD B1 8D D9 C1 CC D0 A0 +CC C1 CD D2 CF CE BD B0 +8D 8D CD CF C3 AE CF CC +CE C5 C2 AE D7 D7 D7 8D +D3 C7 CE C9 CE CE C5 CA +A0 D2 C5 D4 C5 D0 A0 D9 +C2 8D 8D A9 C3 A8 D3 D3 +C5 C8 C3 CF D2 C3 C9 CD +8D C3 C8 C5 C3 CB CD C1 +D4 C5 8D 00 D2 C5 D6 C5 +D2 D3 C5 A0 D3 C9 C4 C5 +D3 8D 00 CE C5 D7 A0 C7 +C1 CD C5 8D 00 FF FF FF +8A 48 A2 08 A9 A1 20 EF +FF A9 AD 20 EF FF 20 EF +FF CA D0 F0 A9 A1 20 EF +FF A9 8D 20 EF FF 68 AA +60 84 F4 85 F5 86 F6 A9 +8D 20 EF FF 20 00 05 A2 +00 A0 00 A9 A1 20 EF FF +86 F7 84 F8 8A 0A 0A 0A +0A 65 F8 A2 1F D5 50 F0 +0E CA 10 F9 20 C9 05 4C +77 05 EA EA EA EA EA 8A +4A 4A 4A 4A EA 18 65 B8 +29 01 F0 04 A9 C2 D0 02 +A9 D7 20 EF FF 8A EA 29 +0F AA B5 30 20 EF FF A6 +F7 A4 F8 C8 C0 08 D0 B3 +A9 A1 20 BC 05 A9 8D 20 +EF FF 20 00 05 E8 E0 08 +D0 9F A4 F4 A9 8D 20 EF +FF A9 A0 20 EF FF A9 B0 +AA 20 EF FF A9 A0 20 EF +FF 20 EF FF E8 8A C9 B8 +D0 EF A9 8D 20 EF FF A5 +F5 A6 F6 60 20 EF FF A9 +A0 20 EF FF 8A 20 E5 FF +60 18 A5 F7 65 F8 29 01 +F0 04 A9 A0 D0 02 A9 AA +20 EF FF 20 EF FF 60 EA +E6 FF AD 11 D0 10 F9 AD +10 D0 20 EF FF 60 FF FF +CB D1 D2 D2 C2 C2 CE CE +D0 D0 D0 D0 D0 D0 D0 D0 +A6 B5 30 5C A5 B0 F0 08 +E0 08 D0 04 C5 E6 F0 2E +F6 E3 C9 01 D0 02 F6 E3 +50 1E A0 0F A5 B1 D9 60 +00 F0 03 88 10 F8 B9 A0 +00 D5 E4 90 04 94 E6 95 +E4 18 08 75 E5 95 E5 28 +E0 04 F0 03 30 31 60 A5 +E8 85 DD A9 00 85 B5 20 +4B 08 20 B2 07 20 00 07 +20 B2 07 A9 08 85 B5 20 +09 07 20 31 08 4C 80 09 +E0 F9 D0 0B A5 60 C5 B1 +D0 04 A9 00 85 B4 60 50 +FD A0 07 A5 B1 D9 60 00 +F0 05 88 F0 F1 10 F6 B9 +A0 00 D5 E2 90 02 95 E2 +C6 B5 A5 BE C5 B5 F0 03 +20 25 08 E6 B5 60 C9 08 +B0 12 20 EA 08 A2 1F B5 +50 C5 FA F0 03 CA 10 F7 +86 FB 86 B0 4C 0B 04 A0 +03 04 00 07 02 05 01 06 +10 17 11 16 12 15 14 13 +73 74 70 77 72 75 71 76 +60 67 61 66 62 65 64 63 +F0 FF 01 10 11 0F EF F1 +DF E1 EE F2 12 0E 1F 21 +0B 0A 06 06 04 04 04 04 +02 02 02 02 02 02 02 02 +A2 1F B5 70 95 50 CA 10 +F9 20 21 05 60 FF FF FF +A2 10 A9 00 95 DE CA 10 +FB A9 10 85 B0 C6 B0 10 +01 60 20 1E 08 A4 B0 A2 +08 86 B6 C0 08 10 41 C0 +06 10 2E C0 04 10 1F C0 +01 F0 09 10 0E 20 8E 07 +D0 FB F0 D9 20 9C 07 D0 +FB F0 D2 A2 04 86 B6 20 +9C 07 D0 FB F0 C7 20 9C +07 A5 B6 C9 04 D0 F7 F0 +BC A2 10 86 B6 20 8E 07 +A5 B6 C9 08 D0 F7 F0 AD +A2 06 86 B6 20 CA 07 50 +05 30 03 20 00 06 20 1E +08 C6 B6 A5 B6 C9 05 F0 +EB 20 CA 07 70 8F 30 8D +20 00 06 A5 B1 29 F0 C9 +20 F0 EE 4C 0D 07 20 CA +07 30 03 20 00 06 20 1E +08 C6 B6 60 20 CA 07 90 +02 50 F9 30 07 08 20 00 +06 28 50 F0 20 1E 08 C6 +B6 60 A2 0F 38 B4 60 A9 +77 F5 50 95 60 94 50 38 +A9 77 F5 50 95 50 CA 10 +EB 60 A5 B1 A6 B6 18 75 +8F 85 B1 29 88 D0 42 A5 +B1 A2 20 CA 30 0E D5 50 +D0 F9 E0 10 30 33 A9 7F +69 01 70 01 B8 A5 B5 30 +24 C5 BF 10 20 48 08 A9 +F9 85 B5 85 B4 20 4B 08 +20 B2 07 20 09 07 20 2E +08 28 68 85 B5 A5 B4 30 +04 38 A9 FF 60 18 A9 00 +60 A9 FF 18 B8 60 A6 B0 +B5 50 85 B1 60 20 4B 08 +20 B2 07 20 09 07 20 B2 +07 BA 86 B3 A6 B2 9A 68 +85 B6 68 85 B0 AA 68 95 +50 68 AA 68 85 B1 95 50 +4C 70 08 BA 86 B3 A6 B2 +9A A5 B1 48 A8 A2 1F D5 +50 F0 03 CA 10 F9 A9 CC +95 50 8A 48 A6 B0 B5 50 +94 50 48 8A 48 A5 B6 48 +BA 86 B2 A6 B3 9A 60 A6 +E4 E4 A0 D0 04 A9 00 F0 +0A A6 E3 D0 06 A6 EE D0 +02 A9 FF A2 04 86 B5 C5 +FA 90 0C F0 0A 85 FA A5 +B0 85 FB A5 B1 85 F9 60 +EA EA A6 DC 10 17 A5 F9 +D5 DC D0 0F CA B5 DC 85 +FB CA B5 DC 85 F9 CA 86 +DC D0 1A 85 DC A2 0C 86 +B5 86 FA A2 14 20 02 07 +A2 04 86 B5 20 00 07 A6 +FA E0 0F 90 12 A6 FB B5 +50 85 FA 86 B0 A5 F9 85 +B1 20 4B 08 4C FA 08 A9 +FF 60 A2 04 06 F9 26 FA +CA D0 F9 05 F9 85 F9 85 +B1 60 20 21 05 4C 00 04 +A9 8D 20 EF FF A2 03 A5 +FB 30 27 4A 4A 4A 4A 18 +65 B8 29 01 D0 04 A9 D7 +D0 02 A9 C2 20 EF FF B5 +F8 29 0F AA B5 30 20 EF +FF A2 02 A9 A0 20 EF FF +B5 F8 20 B7 03 60 FF FF +FF FF FF FF FF 20 E0 05 +C9 8D D0 03 A9 14 60 C9 +C7 D0 0A A9 00 85 B8 20 +F0 06 A9 0C 60 C9 D2 D0 +05 E6 B8 A9 0E 60 C9 CD +D0 03 A9 0F 60 C9 CC D0 +03 4C 00 03 C9 D1 D0 03 +4C 00 FF C9 D0 D0 06 20 +21 05 4C 3D 09 29 07 60 +18 A9 80 65 EB 65 EC 65 +ED 65 E1 65 DF 38 E5 F0 +E5 F1 E5 E2 E5 E0 E5 DE +E5 EF E5 E3 B0 02 A9 00 +4A 18 69 40 65 EC 65 ED +38 E5 E4 4A 18 69 90 65 +DD 65 DD 65 DD 65 DD 65 +E1 38 E5 E4 E5 E4 E5 E5 +E5 E5 E5 E0 A6 B1 E0 33 +F0 16 E0 34 F0 12 E0 22 +F0 0E E0 25 F0 0A A6 B0 +F0 09 B4 50 C0 10 10 03 +18 69 02 4C 77 08 8D 8D +CF D0 C5 CE C9 CE C7 D3 +8D 8D C3 CF CD D0 D5 D4 +C5 D2 AE AE AE AE AE AE +AE AE D7 C8 C9 D4 C5 A0 +C2 CC C1 C3 CB 8D C6 D2 +C5 CE C3 C8 A0 C4 C5 C6 +C5 CE C3 C5 AE AE AE B0 +A0 A0 A0 A0 A0 B1 8D C7 +C9 D5 CF C3 CF A0 D0 C9 +C1 CE CF AE AE AE AE AE +B2 A0 A0 A0 A0 A0 B3 8D +D2 D5 D9 A0 CC CF D0 C5 +DA AE AE AE AE AE AE AE +AE B4 A0 A0 A0 A0 A0 B5 +8D D1 D5 C5 C5 CE A7 D3 +A0 C9 CE C4 C9 C1 CE AE +AE AE B6 A0 A0 A0 A0 A0 +B7 8D C6 CF D5 D2 A0 CB +CE C9 C7 C8 D4 D3 AE AE +AE AE AE B8 A0 A0 A0 A0 +A0 B9 8D 8D C6 CF D2 A0 +D2 C1 CE C4 CF CD A0 CF +D0 C5 CE C9 CE C7 AC A0 +D0 D2 C5 D3 D3 A0 C5 CE +D4 C5 D2 8D BF AB 00 00 +99 22 06 45 32 0C 72 14 +01 63 63 05 64 43 0F 63 +41 05 52 25 07 44 34 0E +53 33 0F CC 99 22 07 55 +32 0D 45 06 00 63 14 01 +14 13 06 34 14 04 36 25 +06 52 33 0E 43 24 0F 44 +99 25 0B 25 01 00 33 25 +07 36 34 0D 34 34 0E 52 +25 0D 45 35 04 55 22 06 +43 33 0F CC 99 52 04 52 +52 06 75 44 06 52 41 04 +43 43 0F 43 25 06 52 32 +04 42 22 07 55 34 0F 44 +99 25 07 66 43 0E 55 55 +04 54 13 01 63 34 0E 33 +01 00 52 46 04 55 22 06 +43 33 0F CC 99 06 00 52 +11 06 34 22 0B 22 23 06 +64 14 04 43 44 06 75 25 +06 31 22 07 55 34 0F 44 +99 25 01 25 15 01 33 25 +07 72 01 00 63 11 04 66 +21 0A 56 22 06 53 35 0D +52 34 0E CC 99 35 0C 52 +52 06 62 44 06 52 06 00 +75 14 04 66 11 05 56 21 +0B 55 24 0F 42 25 06 43 +99 03 02 63 25 0B 25 41 +05 54 24 0E 72 01 00 36 +46 04 52 25 07 55 22 06 +43 33 0F CC 99 03 07 74 +14 01 52 52 04 36 23 0E +53 06 00 75 41 04 31 25 +06 52 22 07 55 34 0F 44 diff --git a/hex/convert.py b/hex/convert.py new file mode 100644 index 0000000..b64d491 --- /dev/null +++ b/hex/convert.py @@ -0,0 +1,28 @@ +#!/usr/bin/python3 +import sys + + +""" +Probably, some files can be converted with xxd tool +""" + + +def convert(in_filename, out_filename): + with open(in_filename, 'r') as in_f: + content = in_f.readlines() + + content = [l.lstrip(': ').strip().split() for l in content] + + byte_array = [] + for line in content: + for byte in line: + byte_array.append(bytearray.fromhex(byte)) + + byte_array = b''.join(byte_array) + + with open(out_filename, 'wb') as out_f: + out_f.write(byte_array) + + +if __name__ == '__main__': + convert(sys.argv[1], sys.argv[2]) diff --git a/roms/6502_functional_test.bin b/roms/6502_functional_test.bin new file mode 100644 index 0000000..c9a35e1 Binary files /dev/null and b/roms/6502_functional_test.bin differ diff --git a/roms/ASMmchess.bin b/roms/ASMmchess.bin new file mode 100644 index 0000000..3354a44 Binary files /dev/null and b/roms/ASMmchess.bin differ diff --git a/roms/apple1basic.bin b/roms/apple1basic.bin new file mode 100644 index 0000000..c0d1c19 Binary files /dev/null and b/roms/apple1basic.bin differ diff --git a/roms/apple1basic.md b/roms/apple1basic.md new file mode 100644 index 0000000..ff2cabe --- /dev/null +++ b/roms/apple1basic.md @@ -0,0 +1,8 @@ +load at E000 +enter at E000 + +run: + +```shell +RUST_LOG=debug cargo run --features build-binary --bin mos6502-cli roms/apple1basic.bin -b -a E000 +``` diff --git a/roms/apple30.bin b/roms/apple30.bin new file mode 100644 index 0000000..32fc73c Binary files /dev/null and b/roms/apple30.bin differ diff --git a/roms/nestest.bin b/roms/nestest.bin new file mode 100644 index 0000000..fc2a88c Binary files /dev/null and b/roms/nestest.bin differ diff --git a/roms/replica1.bin b/roms/replica1.bin new file mode 100644 index 0000000..78df339 Binary files /dev/null and b/roms/replica1.bin differ diff --git a/src/apple1.rs b/src/apple1.rs new file mode 100644 index 0000000..35c4eb7 --- /dev/null +++ b/src/apple1.rs @@ -0,0 +1,216 @@ +use std::fs; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, Sender}; +use std::{thread, time}; + +extern crate mos6502; +use mos6502::asm::assemble_file; +use mos6502::cpu::CPU; + +extern crate ncurses; + +extern crate clap; +use clap::{App, Arg}; + +#[macro_use] +extern crate log; +extern crate env_logger; + +const CR: u8 = 0x0D; +const KBD: u16 = 0xD010; +const KBDCR: u16 = 0xD011; +const DSP: u16 = 0xD012; +const BASIC_ADDR: u16 = 0xE000; +const WOZMON_ADDR: u16 = 0xFF00; + +struct Apple1 { + cpu: CPU, + disable_screen: bool, +} + +impl Apple1 { + pub fn new(disable_screen: bool, wozmon: &str) -> Apple1 { + let mut apple1 = Apple1 { + cpu: CPU::new(), + disable_screen, + }; + + let wozmon_bytes = fs::read(wozmon).expect("Can't read wozmon file"); + apple1.load(&wozmon_bytes, WOZMON_ADDR); + + fn read_callback(addr: u16) -> Option<(u16, u8)> { + match addr { + KBD => Some((KBDCR, 0)), + _ => None, + } + }; + + apple1.cpu.memory.register_read_callback(read_callback); + + fn write_callback(addr: u16, value: u8) -> Option<(u16, u8)> { + match (addr, value) { + (DSP, 0) => None, + (KBD, _) => None, + (KBDCR, _) => None, + (DSP, v) => Some((DSP, v | 0b1000_0000)), + _ => None, + } + }; + apple1.cpu.memory.register_write_callback(write_callback); + + apple1 + } + + pub fn run(&mut self) { + let (tx, rx): (Sender, Receiver) = mpsc::channel(); + if !self.disable_screen { + ncurses::initscr(); + ncurses::resize_term(60, 40); + ncurses::scrollok(ncurses::stdscr(), true); + ncurses::noecho(); + ncurses::raw(); + thread::spawn(move || Apple1::read_input(tx)); + } + + while !self.cpu.step() { + self.print_output_to_display(); + + if let Ok(c) = rx.try_recv() { + match c { + 0x03 => break, // ^c + 0x05 => { + self.print_status(); + continue; + } // ^e + _ => {} + }; + + self.write_kbd_input(c); + } + thread::sleep(time::Duration::from_micros(100)); + } + ncurses::endwin(); + } + + fn char_to_apple1(&self, c: u8) -> u8 { + let mut c = c.to_ascii_uppercase() as u8; + if c == 0xA { + c = CR; // CR instead of NL + } + c |= 0x80; // apple1 ascii + set bit 7 + c + } + + fn write_kbd_input(&mut self, c: u8) { + self.cpu.memory.set(KBD, self.char_to_apple1(c)); + self.cpu.memory.set(KBDCR, 0b1000_0000); + } + + fn read_input(tx: Sender) { + // reads user input from keyboard + loop { + tx.send(ncurses::getch() as u8).unwrap(); + } + } + + fn print_status(&mut self) { + let status = &format!("{}", &self.status_string()); + debug!("{}", status); + if !self.disable_screen { + ncurses::addstr(status); + ncurses::addstr("\n"); + }; + } + + fn status_string(&mut self) -> String { + format!( + "[apple1] pc=0x{:X} a=0x{:X} x=0x{:X} y=0x{:X} p=0b{:08b} video=0b{:08b} kbd=0b{:08b}", + self.cpu.pc, + self.cpu.a, + self.cpu.x, + self.cpu.y, + self.cpu.p, + self.cpu.memory.get(DSP), + self.cpu.memory.get(KBD), + ) + } + + pub fn load(&mut self, program: &[u8], addr: u16) { + self.cpu.load(program, addr); + } + + fn print_output_to_display(&mut self) { + let value = self.cpu.memory.get(DSP); + + if value & 0b1000_0000 != 0 { + self.cpu.memory.set(DSP, 0); + let value = value & 0b0111_1111; // remove 7 bit + if value == CR { + ncurses::addch('\n' as u64); + } else { + ncurses::addch(u64::from(value)); + } + info!("[apple1 -> screen] 0x{:X}", value); + ncurses::refresh(); + } else { + debug!("no video output"); + } + } +} + +fn main() { + env_logger::init(); + + let matches = App::new("mos6502-cli") + .version("0.0.1") + .author("Alexander Akhmetov") + .arg(Arg::with_name("binary").short("b").help("Load binary file")) + .arg( + Arg::with_name("disable-screen") + .short("s") + .help("Disable ncurses screen"), + ) + .arg( + Arg::with_name("start-addr") + .short("a") + .help("Start at addr") + .takes_value(true), + ) + .arg( + Arg::with_name("load-addr") + .short("l") + .help("Load program at addr") + .takes_value(true), + ) + .arg( + Arg::with_name("program") + .short("p") + .help("Load additional program to EA60") + .takes_value(true), + ) + .get_matches(); + + let start_at_addr = matches.value_of("start-addr").unwrap_or("FF00"); + let start_at_addr = + u16::from_str_radix(start_at_addr, 16).expect("Can't parse HEX start address"); + let load_program_at = matches.value_of("load-addr").unwrap_or("E000"); + let load_program_at = + u16::from_str_radix(load_program_at, 16).expect("Can't parse HEX start address"); + + let mut apple1 = Apple1::new(matches.is_present("disable-screen"), "sys/wozmon.bin"); + + if matches.is_present("program") { + let original_pc = apple1.cpu.pc; + let filename = matches.value_of("program").unwrap(); + if filename.ends_with("asm") { + apple1.load(&assemble_file(filename), load_program_at); + } else { + apple1.load(&fs::read(filename).unwrap(), load_program_at); + } + apple1.cpu.pc = original_pc; + } + + apple1.cpu.pc = start_at_addr; + + apple1.run(); +} diff --git a/src/asm.rs b/src/asm.rs index e69de29..4b8abd1 100644 --- a/src/asm.rs +++ b/src/asm.rs @@ -0,0 +1,147 @@ +use std::fs; + +use crate::operation; + +pub fn assemble_file(filename: &str) -> Vec { + let asm_content = fs::read_to_string(filename).expect("Can't read the asm file"); + assemble(&asm_content) +} + +pub fn assemble(program: &str) -> Vec { + let lines = program.split('\n'); + let mut bytes = vec![]; + + for line in lines { + let t_line = line.trim(); + if !t_line.is_empty() { + let (operation, mut args) = parse_line(t_line); + bytes.push(operation); + bytes.append(&mut args); + } + } + + bytes +} + +fn parse_line(line: &str) -> (u8, Vec) { + let split: Vec<&str> = line.splitn(2, ' ').collect(); + + let mut args = vec![]; + let mut addressing = operation::AddressingMode::Implied; + let operation_name = split[0]; + + if split.len() != 1 { + match split[1] { + arg if arg.starts_with("#$") => { + addressing = operation::AddressingMode::Immediate; + let arg = arg.trim_start_matches("#$"); + args.push(u8::from_str_radix(arg, 16).unwrap()); + } + arg if arg.starts_with('$') => { + let arg = arg.trim_start_matches('$'); + + // if arg.len() == 2 - we use zeropage addressing + match arg.len() { + 2 => { + addressing = operation::AddressingMode::ZeroPage; + args.push(u8::from_str_radix(arg, 16).unwrap()); + } + 4 => { + addressing = operation::AddressingMode::Absolute; + let arg = u16::from_str_radix(arg, 16).unwrap(); + args.push(arg as u8); + args.push((arg >> 8) as u8); + } + _ => panic!("Can't parse argument"), + } + } + _ => panic!("unknown operand"), + }; + } + + // first it looks only by name, if there are many opcodes with such name, + // it looks by name and addressing + // todo: change + let opcodes = operation::by_name(operation_name); + let opcode = match opcodes.len() { + 0 => panic!("unknown operation name: {}", operation_name), + 1 => opcodes[0], + _ => operation::by_name_and_addressing(operation_name, addressing), + }; + + (opcode.code, args) +} + +pub fn disassemble(buf: &[u8]) -> String { + let op = operation::by_code(buf[0]); + + let mut instruction = String::from(op.name); + + if op.length > 1 { + match op.addressing { + operation::AddressingMode::Immediate => instruction = format!("{} #$", instruction), + _ => instruction = format!("{} #", instruction), + } + instruction = format!("{}{:X}", instruction, buf[1]); + } + if op.length > 2 { + instruction = format!("{}{:X}", instruction, buf[2]); + } + + instruction +} + +#[cfg(test)] +mod tests { + use crate::asm::assemble; + + #[test] + fn test_assemble_one_line() { + let program = "BRK"; + let bytes = assemble(&program); + let expected: [u8; 1] = [0]; + assert_eq!(&bytes, &expected); + } + + #[test] + fn test_assemble_one_line_immediate_addressing() { + let program = "LDA #$AF"; + let bytes = assemble(&program); + let expected: [u8; 2] = [0xA9, 0xAF]; + assert_eq!(&bytes, &expected); + } + + #[test] + fn test_assemble() { + let program = "LDA #$c0 + TAX + INX + ADC #$c4 + BRK"; + let bytes = assemble(&program); + let expected: [u8; 7] = [0xa9, 0xc0, 0xaa, 0xe8, 0x69, 0xc4, 0x00]; + assert_eq!(&bytes, &expected); + } + + #[test] + fn test_assemble_2() { + let program = "LDA #$80 + STA $01 + ADC $01"; + let bytes = assemble(&program); + let expected: [u8; 6] = [0xa9, 0x80, 0x85, 0x01, 0x65, 0x01]; + assert_eq!(&bytes, &expected); + } + + #[test] + fn code_with_bne_instruction() { + let program = "LDX #$5 + DEX + CPX #$0 + BNE #$FD + BRK"; + let bytes = assemble(&program); + let expected: [u8; 8] = [0xa2, 0x05, 0xca, 0xe0, 0x00, 0xd0, 0xfd, 0x00]; + assert_eq!(&bytes, &expected); + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..37c2e5b --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,76 @@ +use std::fs; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, Sender}; +use std::{thread, time}; + +extern crate mos6502; +use mos6502::asm::assemble_file; +use mos6502::cpu::CPU; + +extern crate ncurses; + +extern crate clap; +use clap::{App, Arg}; + +#[macro_use] +extern crate log; +extern crate env_logger; + +fn main() { + env_logger::init(); + + let matches = App::new("mos6502-cli") + .version("0.1.0") + .author("Alexander Akhmetov") + .arg( + Arg::with_name("binary") + .short("b") + .help("Load binary file") + .index(1), + ) + .arg( + Arg::with_name("start-addr") + .short("a") + .help("Start at addr") + .index(2), + ) + .get_matches(); + + let start_at_addr = matches.value_of("start-addr").unwrap(); + let start_at_addr = + u16::from_str_radix(start_at_addr, 16).expect("Can't parse HEX start address"); + + let mut cpu = CPU::new(); + + fn write_callback(addr: u16, value: u8) -> Option<(u16, u8)> { + match (addr, value) { + (0x000F, v) => { + // info!("0x000f: 0x{:X}", v); + None + } + _ => None, + } + }; + cpu.memory.register_write_callback(write_callback); + + let filename = matches.value_of("binary").unwrap(); + cpu.load(&fs::read(filename).unwrap(), 0); + cpu.pc = start_at_addr; + + let mut trapped = 0; + let mut brk = false; + while !brk { + brk = cpu.step(); + if cpu.memory.get(cpu.pc) == 0xD0 && cpu.memory.get(cpu.pc + 1) == 0xFE { + trapped += 1; + } else { + trapped = 0; + } + if trapped == 3 { + brk = true; + info!("possible cycle detected"); + } + } + + info!("mem 000F: 0x{:X}", cpu.memory.get(0x000F)); +} diff --git a/src/cpu.rs b/src/cpu.rs index 1963c34..b109631 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,14 +1,41 @@ +use crate::mem::Memory; use crate::operation; use crate::utils; +const DEFAULT_FLAGS: u8 = 0b0011_0000; +const FLAG_NEGATIVE: u8 = 0b1000_0000; +const FLAG_OVERFLOW: u8 = 0b0100_0000; +const FLAG_B: u8 = 0b0001_0000; +const FLAG_DECIMAL: u8 = 0b0000_1000; +const FLAG_INTERRUPT: u8 = 0b0000_0100; +const FLAG_ZERO: u8 = 0b0000_0010; +const FLAG_CARRY: u8 = 0b0000_0001; + +const STACK_BOTTOM: u16 = 0x0100; + pub struct CPU { - a: u8, - x: u8, - y: u8, - s: u8, - p: u8, - pc: u16, - memory: [u8; 64 * 1024], + pub a: u8, + pub x: u8, + pub y: u8, + + // status register + // Bit No. 7 6 5 4 3 2 1 0 + // N O B D I Z C + // bit 5 is not used, supposed to be 1 all the time + // C - carry + // Z - zero + // I - interrupt disable + // D - decimal + // B - + // O - overflow + // N - negative + pub p: u8, + pub sp: u8, + + // + pub pc: u16, + pub memory: Memory, + pub cycle: usize, } impl CPU { @@ -17,232 +44,2522 @@ impl CPU { a: 0, x: 0, y: 0, - s: 0, - p: 0, + p: DEFAULT_FLAGS, + sp: 0xff, pc: 0, - memory: [0; 64 * 1024], + memory: Memory::new(), + cycle: 0, + } + } + + pub fn load(&mut self, buf: &[u8], addr: u16) { + self.memory.load(addr, buf); + self.pc = addr; + } + + pub fn step(&mut self) -> bool { + let operation = operation::by_code(self.memory.get(self.pc)); + info!( + "[cpu] [PC:${:04X} SP:${:02X} STP:${:02X} A:${:02X} X:${:02X} Y:${:02X} P:{:08b}] (op:{} code:${:02X} length:{} data:{:X?}) cycle:{}", + self.pc, self.sp, self.memory.get(STACK_BOTTOM + 0xff), self.a, self.x, self.y, self.p, operation.name, operation.code, + operation.length, + &self.memory.bytes()[(self.pc) as usize..(self.pc + operation.length as u16) as usize], + self.cycle + ); + + self.pc += u16::from(operation.length); + self.cycle += 1; + + match operation.name { + "BRK" => self.operation_brk(), + "RTI" => self.operation_rti(), + "LDA" => self.operation_lda(operation), + "SBC" => self.operation_sbc(operation), + "LDX" => self.operation_ldx(operation), + "EOR" => self.operation_eor(operation), + "AND" => self.operation_and(operation), + "ORA" => self.operation_ora(operation), + "LDY" => self.operation_ldy(operation), + "ADC" => self.operation_adc(operation), + "STA" => self.operation_sta(operation), + "STX" => self.operation_stx(operation), + "STY" => self.operation_sty(operation), + "INC" => self.operation_inc(operation), + "DEC" => self.operation_dec(operation), + "INX" => self.operation_inx(), + "INY" => self.operation_iny(), + "DEX" => self.operation_dex(), + "DEY" => self.operation_dey(), + "TAX" => self.operation_tax(), + "BNE" => self.operation_bne(operation), + "BEQ" => self.operation_beq(operation), + "BCC" => self.operation_bcc(operation), + "BCS" => self.operation_bcs(operation), + "BVC" => self.operation_bvc(operation), + "BVS" => self.operation_bvs(operation), + "BPL" => self.operation_bpl(operation), + "BMI" => self.operation_bmi(operation), + "CMP" => self.operation_cmp(operation), + "CPX" => self.operation_cpx(operation), + "CPY" => self.operation_cpy(operation), + "CLC" => self.operation_clc(), + "CLD" => self.operation_cld(), + "CLI" => self.operation_cli(), + "CLV" => self.operation_clv(), + "SEC" => self.operation_sec(), + "SED" => self.operation_sed(), + "SEI" => self.operation_sei(), + "JMP" => self.operation_jmp(operation), + "JSR" => self.operation_jsr(operation), + "RTS" => self.operation_rts(), + "LSR" => self.operation_lsr(operation), + "PHA" => self.operation_pha(), + "PLA" => self.operation_pla(), + "TXA" => self.operation_txa(), + "TXS" => self.operation_txs(), + "TSX" => self.operation_tsx(), + "PLP" => self.operation_plp(), + "PHP" => self.operation_php(), + "TYA" => self.operation_tya(), + "TAY" => self.operation_tay(), + "ROR" => self.operation_ror(operation), + "ROL" => self.operation_rol(operation), + "BIT" => self.operation_bit(operation), + "ASL" => self.operation_asl(operation), + "NOP" => {} + + _ => { + error!("unknown operation: code=0x{:X}", operation.code); + panic!("unknown operation"); + } + }; + + false + } + pub fn run(&mut self) { + while !self.step() {} + } + + fn operation_clc(&mut self) { + self.clear_carry(); + } + + fn operation_cli(&mut self) { + self.clear_interrupt(); + } + + fn operation_clv(&mut self) { + self.clear_overflow(); + } + + fn operation_cld(&mut self) { + self.clear_decimal(); + } + + fn operation_sec(&mut self) { + self.set_carry(); + } + + fn operation_sed(&mut self) { + self.set_decimal(); + } + + fn operation_sei(&mut self) { + self.set_interrupt(); + } + + fn operation_tax(&mut self) { + self.x = self.a; + + self.set_zero_if_needed(self.x); + self.set_negative_if_needed(self.x); + } + + fn operation_dex(&mut self) { + let (result, _) = self.x.overflowing_sub(1); + self.x = result; + + self.set_zero_if_needed(self.x); + self.set_negative_if_needed(self.x); + } + + fn operation_dey(&mut self) { + let (result, _) = self.y.overflowing_sub(1); + self.y = result; + + self.set_zero_if_needed(self.y); + self.set_negative_if_needed(self.y); + } + + fn operation_brk(&mut self) { + // set B flag (0b0001_0000) + self.set_b(); + // push return address (it's already incremented by 1, so we increment it only by 1) + self.stack_push_word(self.pc + 1); + self.stack_push(self.p); + + // now set interrupt flag and go to ISR + self.set_interrupt(); + let addr = utils::little_endian_to_u16(self.memory.get(0xFFFF), self.memory.get(0xFFFE)); + self.pc = addr; + } + + fn operation_rti(&mut self) { + self.p = self.stack_pop(); + self.pc = self.stack_pop_word(); + } + + fn set_b(&mut self) { + self.p |= FLAG_B; + } + + fn operation_inc(&mut self, operation: &operation::Operation) { + let addr = self.get_operand_address(operation); + let value = self.memory.get(addr); + let (result, _) = value.overflowing_add(1); + self.memory.set(addr, result); + + self.set_zero_if_needed(result); + self.set_negative_if_needed(result); + } + + fn operation_dec(&mut self, op: &operation::Operation) { + let addr = self.get_operand_address(op); + let value = self.memory.get(addr); + let (result, _) = value.overflowing_sub(1); + self.memory.set(addr, result); + + self.set_zero_if_needed(result); + self.set_negative_if_needed(result); + } + + fn operation_inx(&mut self) { + let (result, _) = self.x.overflowing_add(1); + self.x = result; + + self.set_negative_if_needed(self.x); + self.set_zero_if_needed(self.x); + } + + fn operation_iny(&mut self) { + let (result, _) = self.y.overflowing_add(1); + self.y = result; + + self.set_negative_if_needed(self.y); + self.set_zero_if_needed(self.y); + } + + fn operation_ora(&mut self, op: &operation::Operation) { + self.a |= self.get_operand(op); + + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn operation_and(&mut self, op: &operation::Operation) { + self.a &= self.get_operand(op); + + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn operation_eor(&mut self, op: &operation::Operation) { + self.a ^= self.get_operand(op); + + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn operation_ldx(&mut self, operation: &operation::Operation) { + self.x = self.get_operand(operation); + + self.set_negative_if_needed(self.x); + self.set_zero_if_needed(self.x); + } + + fn operation_ldy(&mut self, operation: &operation::Operation) { + self.y = self.get_operand(operation); + + self.set_negative_if_needed(self.y); + self.set_zero_if_needed(self.y); + } + + fn operation_lda(&mut self, op: &operation::Operation) { + self.a = self.get_operand(op); + + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn operation_sbc(&mut self, op: &operation::Operation) { + // SBC uses the complement of the carry + let operand = self.get_operand(op); + let carry = i16::from(1 - self.get_carry()); + let original_a = self.a; + let result = i16::from(original_a) - carry - i16::from(operand); + + self.a = result as u8; + + self.clear_overflow(); + if original_a as u8 >> 7 != self.a >> 7 { + self.set_overflow(); + } + + // clear carry if overflow in the bit 7 + if (result as u16) < 256 { + self.set_carry(); + } else { + self.clear_carry(); + } + + // http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html + self.clear_overflow(); + if ((255 - operand) ^ self.a) & (original_a ^ self.a) & 0x80 != 0 { + self.set_overflow(); + }; + + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn operation_adc(&mut self, op: &operation::Operation) { + let operand = u16::from(self.get_operand(op)); + let original_a = u16::from(self.a); + let result = original_a + operand + u16::from(self.get_carry()); + self.a = result as u8; + + // unsigned carry + self.clear_carry(); + if result > 255 { + self.set_carry(); + }; + + // http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html + self.clear_overflow(); + if (operand ^ result) & (original_a ^ result) & 0x80 != 0 { + self.set_overflow(); + }; + + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn operation_beq(&mut self, operation: &operation::Operation) { + if self.get_zero() == FLAG_ZERO { + self.branch(operation); + } + } + + fn operation_bne(&mut self, operation: &operation::Operation) { + if self.get_zero() != FLAG_ZERO { + self.branch(operation); + } + } + + fn branch(&mut self, operation: &operation::Operation) { + let offset = self.get_operand(operation); + if offset > 127 { + debug!("branch jump, offset=-{}", (256 - offset as isize)); + self.pc -= 256 - u16::from(offset); + } else { + debug!("branch jump, offset={}", offset); + self.pc += u16::from(offset); + } + } + + fn operation_bcc(&mut self, operation: &operation::Operation) { + if self.get_carry() == 0 { + self.branch(operation); + } + } + + fn operation_bvs(&mut self, operation: &operation::Operation) { + if self.get_overflow() != 0 { + self.branch(operation); + } + } + + fn operation_bvc(&mut self, operation: &operation::Operation) { + if self.get_overflow() == 0 { + self.branch(operation); + } + } + + fn operation_bcs(&mut self, operation: &operation::Operation) { + if self.get_carry() != 0 { + self.branch(operation); + } + } + + fn operation_bmi(&mut self, operation: &operation::Operation) { + if self.get_negative() == FLAG_NEGATIVE { + self.branch(operation); + } + } + + fn operation_bpl(&mut self, operation: &operation::Operation) { + if self.get_negative() == 0 { + self.branch(operation); + } + } + + fn operation_cpy(&mut self, operation: &operation::Operation) { + let operand = self.get_operand(operation); + self.compare(self.y, operand); + } + + fn operation_jmp(&mut self, op: &operation::Operation) { + match op.addressing { + operation::AddressingMode::IndirectAbsolute => { + let addr = self.get_operand_address(op); + self.pc = + utils::little_endian_to_u16(self.memory.get(addr + 1), self.memory.get(addr)); + } + _ => self.pc = self.get_operand_address(op), + } + } + + fn operation_rts(&mut self) { + self.pc = self.stack_pop_word() + 1; + } + + fn operation_jsr(&mut self, operation: &operation::Operation) { + self.stack_push_word(self.pc - 1); + self.pc = self.get_operand_address(operation); + } + + fn operation_asl(&mut self, _op: &operation::Operation) { + self.clear_carry(); + if self.a >> 7 == 1 { + self.set_carry(); // bit 7 to carry + } + + self.a <<= 1; + + if self.get_zero() != 0 { + // zero flag to 0 bit + self.a |= 0b0000_0001; + } + self.set_zero_if_needed(self.a); + self.set_negative_if_needed(self.a); + } + + fn operation_bit(&mut self, op: &operation::Operation) { + let operand = self.get_operand(op); + + self.set_zero_if_needed(self.a & operand); + + self.clear_overflow(); + if operand & 0b0100_0000 != 0 { + self.set_overflow(); + } + self.set_negative_if_needed(operand); + } + + fn operation_rol(&mut self, op: &operation::Operation) { + // rotate left, carry to 0 bit and 7 bit to carry + let mut value = match op.addressing { + operation::AddressingMode::AccumulatorAddressing => self.a, + _ => self.get_operand(op), + }; + + let original_carry = match self.get_carry() { + FLAG_CARRY => true, + _ => false, + }; + + if (value & 0x80) != 0 { + self.set_carry(); + } else { + self.clear_carry(); + } + + value = (value << 1) as u8; + + if original_carry { + value |= 0b0000_0001; + }; + + self.set_zero_if_needed(value); + self.set_negative_if_needed(value); + + match op.addressing { + operation::AddressingMode::AccumulatorAddressing => self.a = value, + _ => { + let addr = self.get_operand_address(op); + self.memory.set(addr, value); + } + } + } + + fn operation_ror(&mut self, op: &operation::Operation) { + // rotate right, carry to 7 bit and 0 bit to carry + let mut value = self.get_operand(op); + + let original_carry = match self.get_carry() { + FLAG_CARRY => true, + _ => false, + }; + + if (value & 0x01) != 0 { + self.set_carry(); + } else { + self.clear_carry(); + } + + value = (value >> 1) as u8; + + if original_carry { + value |= 0b1000_0000; + }; + + self.set_zero_if_needed(value); + self.set_negative_if_needed(value); + let addr = self.get_operand_address(op); + + match op.addressing { + operation::AddressingMode::AccumulatorAddressing => self.a = value, + _ => self.memory.set(addr, value), + } + } + + fn operation_lsr(&mut self, op: &operation::Operation) { + let operand = match op.addressing { + operation::AddressingMode::AccumulatorAddressing => self.a, + _ => self.get_operand(op), + }; + let zero_bit = operand & 0xF; + let value = operand >> 1; + + if zero_bit != 0 { + self.set_carry(); + } else { + self.clear_carry(); + } + + self.set_zero_if_needed(value); + + match op.addressing { + operation::AddressingMode::AccumulatorAddressing => self.a = value, + _ => { + let addr = self.get_operand_address(op); + self.memory.set(addr, value); + } + } + } + + fn operation_txa(&mut self) { + self.a = self.x; + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn operation_tay(&mut self) { + self.y = self.a; + self.set_negative_if_needed(self.y); + self.set_zero_if_needed(self.y); + } + + fn operation_txs(&mut self) { + self.sp = self.x; + } + + fn operation_tsx(&mut self) { + self.x = self.sp; + self.set_negative_if_needed(self.x); + self.set_zero_if_needed(self.x); + } + + fn operation_php(&mut self) { + self.stack_push(self.p); + } + + fn operation_plp(&mut self) { + self.p = self.stack_pop() | 0b0011_0000; + } + + fn operation_tya(&mut self) { + self.a = self.y; + self.set_negative_if_needed(self.y); + self.set_zero_if_needed(self.y); + } + + fn operation_pha(&mut self) { + self.stack_push(self.a); + } + + fn stack_push_word(&mut self, value: u16) { + let higher = ((value >> 8) & 0xFF) as u8; + self.stack_push(higher); + + let lower = (value & 0xFF) as u8; + self.stack_push(lower); + } + + fn stack_pop_word(&mut self) -> u16 { + let lower = self.stack_pop(); + let higher = self.stack_pop(); + + (u16::from(higher) << 8) | u16::from(lower) + } + + fn stack_push(&mut self, value: u8) { + self.memory.set(STACK_BOTTOM + u16::from(self.sp), value); + self.sp = (((self.sp as i16) - 1) & 0xFF) as u8; + } + + fn operation_pla(&mut self) { + self.a = self.stack_pop(); + self.set_negative_if_needed(self.a); + self.set_zero_if_needed(self.a); + } + + fn stack_pop(&mut self) -> u8 { + self.sp = (((self.sp as i16) + 1) & 0xFF) as u8; + self.memory.get(STACK_BOTTOM + u16::from(self.sp)) + } + + fn operation_cmp(&mut self, operation: &operation::Operation) { + let operand = self.get_operand(operation); + self.compare(self.a, operand); + } + + fn operation_cpx(&mut self, operation: &operation::Operation) { + let operand = self.get_operand(operation); + self.compare(self.x, operand); + } + + fn compare(&mut self, left_operand: u8, right_operand: u8) { + self.clear_zero(); + self.clear_carry(); + self.clear_negative(); + + if left_operand == right_operand { + self.set_zero(); + self.set_carry(); + } + + if left_operand > right_operand { + self.set_carry(); + } + + let result = (i16::from(left_operand) - i16::from(right_operand)) as u8; + self.set_negative_if_needed(result); + } + + fn get_overflow(&self) -> u8 { + self.p & FLAG_OVERFLOW + } + + fn set_overflow(&mut self) { + self.p |= FLAG_OVERFLOW; + } + + fn clear_overflow(&mut self) { + self.p &= !FLAG_OVERFLOW; + } + + fn set_decimal(&mut self) { + self.p |= FLAG_DECIMAL; + } + + fn set_interrupt(&mut self) { + self.p |= FLAG_INTERRUPT; + } + + fn set_carry(&mut self) { + self.p |= FLAG_CARRY; + } + + fn clear_carry(&mut self) { + self.p &= !FLAG_CARRY; + } + + fn get_negative(&self) -> u8 { + self.p & FLAG_NEGATIVE + } + + fn get_carry(&self) -> u8 { + self.p & FLAG_CARRY + } + + fn clear_decimal(&mut self) { + self.p &= !FLAG_DECIMAL; + } + + fn clear_interrupt(&mut self) { + self.p &= !FLAG_INTERRUPT; + } + + fn get_zero(&self) -> u8 { + self.p & FLAG_ZERO + } + + fn set_zero_if_needed(&mut self, result: u8) { + self.clear_zero(); + if result == 0 { + self.set_zero(); + } + } + + fn set_zero(&mut self) { + self.p |= FLAG_ZERO; + } + + fn clear_zero(&mut self) { + self.p &= !FLAG_ZERO; + } + + fn set_negative_if_needed(&mut self, result: u8) { + self.clear_negative(); + if result > 127 { + self.set_negative(); + } + } + + fn set_negative(&mut self) { + self.p |= FLAG_NEGATIVE; + } + + fn clear_negative(&mut self) { + self.p &= !FLAG_NEGATIVE; + } + + fn operation_sty(&mut self, operation: &operation::Operation) { + let addr = self.get_operand_address(operation); + self.memory.set(addr, self.y); + } + + fn operation_stx(&mut self, operation: &operation::Operation) { + let addr = self.get_operand_address(operation); + self.memory.set(addr, self.x); + } + + fn operation_sta(&mut self, op: &operation::Operation) { + let addr = self.get_operand_address(op); + self.memory.set(addr, self.a); + } + + fn get_operand(&mut self, op: &operation::Operation) -> u8 { + let addr = self.get_operand_address(op); + self.memory.get(addr) + } + + fn get_operand_address(&mut self, op: &operation::Operation) -> u16 { + match op.addressing { + operation::AddressingMode::Immediate => self.pc - 1, + operation::AddressingMode::ZeroPage => u16::from(self.next_byte_number()) & 0xFF, + operation::AddressingMode::ZeroPageIndexedX => { + (u16::from(self.next_byte_number()) + u16::from(self.x)) & 0xFF + } + operation::AddressingMode::ZeroPageIndexedY => { + (u16::from(self.next_byte_number()) + u16::from(self.y)) & 0xFF + } + operation::AddressingMode::Absolute => self.next_word_in_memory(), + operation::AddressingMode::IndirectAbsolute => self.next_word_in_memory(), + operation::AddressingMode::IndirectIndexedX => { + let addr = (u16::from(self.next_byte_number()) + u16::from(self.x)) & 0xFF; + utils::little_endian_to_u16(self.memory.get(addr + 1), self.memory.get(addr)) + } + operation::AddressingMode::IndirectIndexed => { + let addr = u16::from(self.next_byte_number()); + let value = + utils::little_endian_to_u16(self.memory.get(addr + 1), self.memory.get(addr)); + value + self.y as u16 + } + operation::AddressingMode::RelativeAddressing => self.pc - 1, + operation::AddressingMode::AbsoluteIndexedY => { + self.next_word_in_memory() + u16::from(self.y) + } + operation::AddressingMode::AbsoluteIndexedX => { + self.next_word_in_memory() + u16::from(self.x) + } + _ => { + error!("unknown addressing mode for operation code=0x{:X}", op.code); + panic!("unknown addressing mode"); + } + } + } + + fn next_byte_number(&mut self) -> u8 { + self.memory.get(self.pc - 1) + } + + fn next_word_in_memory(&mut self) -> u16 { + // returns next two bytes (self.pc - 1, self.pc - 2) as one u16 + // (converts from little endian) + utils::little_endian_to_u16(self.memory.get(self.pc - 1), self.memory.get(self.pc - 2)) + } +} + +#[cfg(test)] +mod tests { + use crate::cpu::*; + use crate::operation; + + const STACK_TOP: u16 = 0x01ff; + + #[test] + fn new() { + let c = CPU::new(); + + assert_eq!(c.a, 0); + assert_eq!(c.x, 0); + assert_eq!(c.y, 0); + assert_eq!(c.p, DEFAULT_FLAGS); + assert_eq!(c.pc, 0); + assert_eq!(c.sp, 0xff); + assert_eq!(&c.memory.bytes()[..], &[0; 64 * 1024][..]); + } + + #[test] + fn load() { + // test when it loads a three bytes dump at a specific location + let mut cpu = CPU::new(); + + let addr = 32768; + let bytes = [1; 3]; + + assert_eq!(&cpu.memory.bytes()[..], &[0; 64 * 1024][..]); + cpu.load(&bytes, addr); + + let exp_memory = [0, 1, 1, 1, 0]; + assert_eq!(&cpu.memory.bytes()[32767..32772], exp_memory); + + assert_eq!(cpu.pc, addr); + } + + #[test] + #[should_panic] + fn load_data_too_big() { + let mut cpu = CPU::new(); + cpu.load(&[1, 2, 3], 65535); + } + + #[test] + fn clear_carry() { + let mut cpu = CPU::new(); + + cpu.set_carry(); + assert_eq!(cpu.p & 0b0000_0001, 1); + + cpu.clear_carry(); + assert_eq!(cpu.p & 0b0000_0001, 0); + } + + #[test] + fn operation_sed() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("SED", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.p & FLAG_DECIMAL, 0); + + cpu.step(); + + assert_eq!(cpu.p & FLAG_DECIMAL, FLAG_DECIMAL); + } + + #[test] + fn operation_sei() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("SEI", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.p & FLAG_INTERRUPT, 0); + + cpu.step(); + + assert_eq!(cpu.p & FLAG_INTERRUPT, FLAG_INTERRUPT); + } + + #[test] + fn operation_sec() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("SEC", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.p & FLAG_CARRY, 0); + + cpu.step(); + + assert_eq!(cpu.p & FLAG_CARRY, FLAG_CARRY); + } + + #[test] + fn clear_overflow() { + let mut cpu = CPU::new(); + + cpu.set_overflow(); + assert_eq!(cpu.p & FLAG_OVERFLOW, FLAG_OVERFLOW); + + cpu.clear_overflow(); + assert_eq!(cpu.p & FLAG_OVERFLOW, 0); + } + + #[test] + fn clear_interrupt() { + let mut cpu = CPU::new(); + + cpu.p = FLAG_INTERRUPT; + cpu.clear_interrupt(); + assert_eq!(cpu.p & FLAG_INTERRUPT, 0); + } + + #[test] + fn clear_decimal() { + let mut cpu = CPU::new(); + + cpu.p = FLAG_DECIMAL; + cpu.clear_decimal(); + assert_eq!(cpu.p & FLAG_DECIMAL, 0); + } + + // ############# LDA + + #[test] + fn operation_lda() { + let mut cpu = CPU::new(); + + let program: [u8; 3] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 10, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.a, 0); + + cpu.step(); + + assert_eq!(cpu.a, 10); + } + + #[test] + fn operation_lda_zero_page() { + let mut cpu = CPU::new(); + + let program: [u8; 3] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::ZeroPage).code, + 10, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.memory.set(10, 55); + + assert_eq!(cpu.a, 0); + + cpu.step(); + + assert_eq!(cpu.a, 55); + } + + #[test] + fn operation_lda_absolute() { + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Absolute).code, + 0x00, + 0xff, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.memory.set(0xff00, 99); + + assert_eq!(cpu.a, 0); + + cpu.step(); + + assert_eq!(cpu.a, 99); + } + + #[test] + fn operation_lda_absolute_indexed_y() { + let mut cpu = CPU::new(); + cpu.y = 1; + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::AbsoluteIndexedY) + .code, + 0x01, + 0xff, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.memory.set(0xff02, 21); + + assert_eq!(cpu.a, 0); + + cpu.step(); + + assert_eq!(cpu.a, 21); + } + + #[test] + fn operation_lda_absolute_indexed_x() { + let mut cpu = CPU::new(); + cpu.x = 3; + + let program: [u8; 3] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::AbsoluteIndexedX) + .code, + 0x02, + 0xff, + ]; + cpu.load(&program, 0); + cpu.memory.set(0xff05, 11); + + assert_eq!(cpu.a, 0); + + cpu.step(); + + assert_eq!(cpu.a, 11); + } + // ############# LDX + + #[test] + fn operation_ldx() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("LDX", operation::AddressingMode::Immediate).code, + 10, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.x, 0); + + cpu.step(); + + assert_eq!(cpu.x, 10); + } + + #[test] + fn operation_ldx_zero_page() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("LDX", operation::AddressingMode::ZeroPage).code, + 10, + ]; + cpu.load(&program, 0); + cpu.memory.set(10, 55); + + assert_eq!(cpu.x, 0); + + cpu.step(); + + assert_eq!(cpu.x, 55); + } + + #[test] + fn operation_ldx_absolute() { + let mut cpu = CPU::new(); + + let program: [u8; 3] = [ + operation::by_name_and_addressing("LDX", operation::AddressingMode::Absolute).code, + 0x00, + 0xff, + ]; + cpu.load(&program, 0); + cpu.memory.set(0xff00, 99); + + assert_eq!(cpu.x, 0); + + cpu.step(); + + assert_eq!(cpu.x, 99); + } + + // ############# ADC + + #[test] + fn operation_adc_immediate_with_overflow() { + let mut cpu = CPU::new(); + cpu.a = 255; + + let program: [u8; 2] = [ + operation::by_name_and_addressing("ADC", operation::AddressingMode::Immediate).code, + 1, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.a, 255); + + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.p, 0b00110011); // overflow flag + } + + #[test] + fn test_adc_for_overflow_and_carry() { + // http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html + let experiments = [ + (80, 16, false, false), + (80, 80, false, true), + (80, 144, false, false), + (80, 208, true, false), + (208, 16, false, false), + (208, 80, true, false), + (208, 144, true, true), + (208, 208, true, false), + ]; + + for (a, operand, carry, overflow) in experiments.iter() { + let mut cpu = CPU::new(); + cpu.a = *a; + + let program: [u8; 2] = [ + operation::by_name_and_addressing("ADC", operation::AddressingMode::Immediate).code, + *operand, + ]; + cpu.load(&program, 0); + cpu.step(); + + if *carry { + assert_eq!(cpu.p & FLAG_CARRY, 0b0000_0001); + } else { + assert_eq!(cpu.p & FLAG_CARRY, 0b0000_0000); + } + if *overflow { + assert_eq!(cpu.p & FLAG_OVERFLOW, 0b0100_0000); + } else { + assert_eq!(cpu.p & FLAG_OVERFLOW, 0b0000_0000); + } + } + } + + #[test] + fn operation_adc_add_two_positive_numbers_and_get_negative_result_must_set_overflow() { + let mut cpu = CPU::new(); + cpu.a = 80; + + let program: [u8; 2] = [ + operation::by_name_and_addressing("ADC", operation::AddressingMode::Immediate).code, + 80, + ]; + cpu.load(&program, 0); + + cpu.step(); + + assert_eq!(cpu.a, 160); + assert_eq!(cpu.p, 0b1111_0000); + } + + #[test] + fn operation_adc_immediate() { + let mut cpu = CPU::new(); + cpu.a = 10; + + let program: [u8; 2] = [ + operation::by_name_and_addressing("ADC", operation::AddressingMode::Immediate).code, + 15, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.a, 10); + + cpu.step(); + + assert_eq!(cpu.a, 25); + assert_eq!(cpu.p, 0b00110000); // no overflow + } + + #[test] + fn operation_adc_absolute() { + let mut cpu = CPU::new(); + cpu.a = 10; + + let program: [u8; 3] = [ + operation::by_name_and_addressing("ADC", operation::AddressingMode::Absolute).code, + 0xff, + 0xaa, + ]; + cpu.load(&program, 0); + cpu.memory.set(43775, 15); // 0xaaff + + assert_eq!(cpu.a, 10); + + cpu.step(); + + assert_eq!(cpu.a, 25); + } + + #[test] + fn operation_stx_zeropage() { + let mut cpu = CPU::new(); + cpu.x = 5; + + let program: [u8; 3] = [ + operation::by_name_and_addressing("STX", operation::AddressingMode::ZeroPage).code, + 22, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(22), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(22), 5); + } + + #[test] + fn operation_stx() { + let mut cpu = CPU::new(); + cpu.x = 5; + + let program: [u8; 4] = [ + operation::by_name_and_addressing("STX", operation::AddressingMode::Absolute).code, + 12, + 0, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(12), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(12), 5); + } + + #[test] + fn operation_sta() { + let mut cpu = CPU::new(); + cpu.a = 10; + + let program: [u8; 4] = [ + operation::by_name_and_addressing("STA", operation::AddressingMode::Absolute).code, + 0x30, + 0x01, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(0x0130), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(0x0130), cpu.a); + } + + #[test] + fn operation_sta_indirect_indexed_x() { + let mut cpu = CPU::new(); + cpu.a = 10; + cpu.x = 5; + + // STA ($0010,X) + let program: [u8; 2] = [ + operation::by_name_and_addressing("STA", operation::AddressingMode::IndirectIndexedX) + .code, + 0x2, + ]; + cpu.memory.set(0x7, 0xCC); + cpu.memory.set(0x8, 0xDD); + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(0xDDCC), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(0xDDCC), 10); + } + + #[test] + fn operation_sta_zero_page_x_with_overflow() { + let mut cpu = CPU::new(); + cpu.a = 0xEE; + cpu.x = 0xCC; + + let program: [u8; 2] = [ + operation::by_name_and_addressing("STA", operation::AddressingMode::ZeroPageIndexedX) + .code, + 0xFF, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(0xCB), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(0xCB), 0xEE); + } + + #[test] + fn operation_sta_zero_page_x() { + let mut cpu = CPU::new(); + cpu.a = 0xEE; + cpu.x = 2; + + let program: [u8; 2] = [ + operation::by_name_and_addressing("STA", operation::AddressingMode::ZeroPageIndexedX) + .code, + 0x5, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(0x7), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(0x7), 0xEE); + } + + #[test] + fn operation_sta_indexed_indirect_y() { + let mut cpu = CPU::new(); + cpu.a = 0xFF; + cpu.y = 0x03; + + // STA ($10E0,)Y + // memory at location $00E0 holds a number 0xAA01 + // a must be stored at the address (0xAA01 + cpu.y) + let program: [u8; 2] = [ + operation::by_name_and_addressing("STA", operation::AddressingMode::IndirectIndexed) + .code, + 0x10, + ]; + cpu.memory.set(0x10, 0x01); + cpu.memory.set(0x11, 0xAA); + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(0xAA04), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(0xAA04), 0xFF); + assert_eq!(cpu.get_negative(), 0); + } + + #[test] + fn operation_sta_absolute_y() { + let mut cpu = CPU::new(); + cpu.a = 0xEE; + cpu.y = 2; + + let program: [u8; 3] = [ + operation::by_name_and_addressing("STA", operation::AddressingMode::AbsoluteIndexedY) + .code, + 0x2, + 0xE, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(0x0E04), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(0x0E04), 0xEE); + } + + #[test] + fn operation_sta_absolute() { + let mut cpu = CPU::new(); + cpu.a = 0xEE; + + let program: [u8; 3] = [ + operation::by_name_and_addressing("STA", operation::AddressingMode::Absolute).code, + 0x2, + 0xE, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.memory.get(0x0E02), 0); + + cpu.step(); + + assert_eq!(cpu.memory.get(0x0E02), 0xEE); + } + + #[test] + fn test_next_two_bytes() { + let mut cpu = CPU::new(); + let bytes = [0, 0x0A, 0x0B]; + cpu.load(&bytes, 0); + cpu.pc = 3; + assert_eq!(cpu.next_word_in_memory(), 0x0B0A); + } + + #[test] + fn test_next_two_bytes_with_only_lower_byte() { + let mut cpu = CPU::new(); + let bytes = [0, 10, 0]; + cpu.load(&bytes, 0); + cpu.pc = 3; + assert_eq!(cpu.next_word_in_memory(), 10); + } + + #[test] + fn test_simple_program() { + // loads 100 from the memory + // adds 7 to the a register + // stores a register at address 0x000f + let mut cpu = CPU::new(); + cpu.a = 10; + + let program: [u8; 7] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 100, + operation::by_name_and_addressing("ADC", operation::AddressingMode::Immediate).code, + 7, + operation::by_name_and_addressing("STA", operation::AddressingMode::Absolute).code, + 15, + 0, + ]; + cpu.load(&program, 0); + + for _ in 0..3 { + cpu.step(); + } + + assert_eq!(cpu.a, 107); + assert_eq!(cpu.memory.get(15), 107); + } + + #[test] + fn test_dex_below_zero_sets_negative_flag() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("DEX", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.x = 0; + + cpu.step(); + assert_eq!(cpu.x, 0xFF); + assert_eq!(cpu.p, 0b1011_0000); + } + + #[test] + fn test_dex_to_zero_sets_zero_flag() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("DEX", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.x = 1; + + cpu.step(); + assert_eq!(cpu.x, 0); + assert_eq!(cpu.p, 0b0011_0010); + } + + #[test] + fn test_dex() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("DEX", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.x = 10; + + cpu.step(); + assert_eq!(cpu.x, 9); + + cpu.pc = 0; + cpu.step(); + assert_eq!(cpu.x, 8); + } + + #[test] + fn operation_dey() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("DEY", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.y = 5; + + cpu.step(); + assert_eq!(cpu.y, 4); + + cpu.pc = 0; + cpu.step(); + assert_eq!(cpu.y, 3); + } + + #[test] + fn operation_dey_below_zero_sets_negative_flag() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("DEY", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.y = 0; + + cpu.step(); + assert_eq!(cpu.y, 0xFF); + assert_eq!(cpu.p, 0b1011_0000); + } + + #[test] + fn dey_to_zero_sets_zero_flag() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("DEY", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.y = 1; + + cpu.step(); + assert_eq!(cpu.y, 0); + assert_eq!(cpu.p, 0b0011_0010); + } + + #[test] + fn operation_inc() { + let mut cpu = CPU::new(); + + let program: [u8; 3] = [ + operation::by_name_and_addressing("INC", operation::AddressingMode::ZeroPage).code, + 100, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + cpu.memory.set(100, 10); + + cpu.step(); + assert_eq!(cpu.memory.get(100), 11); + + cpu.pc = 0; + cpu.step(); + assert_eq!(cpu.memory.get(100), 12); + } + + #[test] + fn operation_inx() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("INX", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.x, 0); + cpu.step(); + assert_eq!(cpu.x, 1); + + cpu.pc = 0; + cpu.step(); + assert_eq!(cpu.x, 2); + } + + #[test] + fn test_iny() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("INY", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code, + ]; + cpu.load(&program, 0); + + assert_eq!(cpu.y, 0); + cpu.step(); + assert_eq!(cpu.y, 1); + + cpu.pc = 0; + cpu.step(); + assert_eq!(cpu.y, 2); + } + + #[test] + fn test_tax_sets_zero() { + let mut cpu = CPU::new(); + + let program: [u8; 1] = + [operation::by_name_and_addressing("TAX", operation::AddressingMode::Implied).code]; + cpu.load(&program, 0); + cpu.a = 0; + cpu.x = 0xAA; + + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.x, 0); + assert_eq!(cpu.p, DEFAULT_FLAGS | FLAG_ZERO); + } + + #[test] + fn test_tax() { + let mut cpu = CPU::new(); + + let program: [u8; 1] = + [operation::by_name_and_addressing("TAX", operation::AddressingMode::Implied).code]; + cpu.load(&program, 0); + cpu.a = 0xA; + cpu.x = 0; + + cpu.step(); + + assert_eq!(cpu.a, 0xA); + assert_eq!(cpu.x, 0xA); + assert_eq!(cpu.p, DEFAULT_FLAGS); // no negative flag + } + + #[test] + fn cmp_equals_immediate() { + // L == R (L > 0; R > 0) + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 100, + operation::by_name_and_addressing("CMP", operation::AddressingMode::Immediate).code, + 100, + ]; + cpu.load(&program, 0); + + for _ in 0..2 { + cpu.step(); + } + + assert_eq!(cpu.get_zero(), FLAG_ZERO); + } + + #[test] + fn cmp_equals_immediate_both_negative() { + // L == R (L < 0; R < 0) + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 0xFE, // -1 + operation::by_name_and_addressing("CMP", operation::AddressingMode::Immediate).code, + 0xFE, + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + } + + #[test] + fn cmp_left_less_right_both_negative_immediate() { + // L < R and (L < 0 && R < 0) + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 0xFE, // -2 + operation::by_name_and_addressing("CMP", operation::AddressingMode::Immediate).code, + 0xFF, // -1 + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.get_carry(), 0); + } + + #[test] + fn cmp_left_less_right_left_negative_immediate() { + // L < R and (L < 0) + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 0xFE, // -2 + operation::by_name_and_addressing("CMP", operation::AddressingMode::Immediate).code, + 10, + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.get_carry(), FLAG_CARRY); // L > R (unsigned) + } + + #[test] + fn cmp_left_greater_right_right_negative_immediate() { + // L < R and (R < 0) + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 0xF, + operation::by_name_and_addressing("CMP", operation::AddressingMode::Immediate).code, + 0xFE, // -2 + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_carry(), 0); + } + + #[test] + fn cmp_not_equals_immediate() { + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 55, + operation::by_name_and_addressing("CMP", operation::AddressingMode::Immediate).code, + 99, + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + } + + #[test] + fn test_cpy_equals_immediate() { + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDY", operation::AddressingMode::Immediate).code, + 1, + operation::by_name_and_addressing("CPY", operation::AddressingMode::Immediate).code, + 1, + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), FLAG_ZERO); + } + + #[test] + fn test_cpy_not_equals_immediate() { + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDY", operation::AddressingMode::Immediate).code, + 1, + operation::by_name_and_addressing("CPY", operation::AddressingMode::Immediate).code, + 5, + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + } + + #[test] + fn test_cpx_equals_immediate() { + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDX", operation::AddressingMode::Immediate).code, + 1, + operation::by_name_and_addressing("CPX", operation::AddressingMode::Immediate).code, + 1, + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), FLAG_ZERO); + } + + #[test] + fn test_cpx_not_equals_immediate() { + let mut cpu = CPU::new(); + + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDX", operation::AddressingMode::Immediate).code, + 1, + operation::by_name_and_addressing("CPX", operation::AddressingMode::Immediate).code, + 5, + ]; + cpu.load(&program, 0); + + cpu.step(); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + } + + #[test] + fn test_bne_positive_offset() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("BNE", operation::AddressingMode::RelativeAddressing) + .code, + 0xA, + ]; + cpu.load(&program, 0); + cpu.step(); + + assert_eq!(cpu.pc, 0xA + 2); + } + + #[test] + fn test_bne_negative_offset() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("BNE", operation::AddressingMode::RelativeAddressing) + .code, + 250, + ]; + cpu.load(&program, 10); + cpu.step(); + + assert_eq!(cpu.pc, 4 + 2); // 10 - (256 - 250) + } + + #[test] + fn test_bne_branching() { + let mut cpu = CPU::new(); + + let program: [u8; 7] = [ + operation::by_name_and_addressing("LDX", operation::AddressingMode::Immediate).code, + 8, + operation::by_name_and_addressing("DEX", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("CPX", operation::AddressingMode::Immediate).code, + 3, + operation::by_name_and_addressing("BNE", operation::AddressingMode::RelativeAddressing) + .code, + 251, + ]; + cpu.load(&program, 0); + + // 16: 1 - ldx + 15 - cycle + for _ in 0..16 { + cpu.step(); } + + assert_eq!(cpu.x, 3); + assert_eq!(cpu.pc, 7); + assert_eq!(cpu.p, 0b0011_0011); } - pub fn load(&mut self, buf: &[u8], addr: u16) { - let program_end_addr = buf.len() + addr as usize; - if program_end_addr > 65536 { - panic!("Wrong address to load the program"); + #[test] + fn pha() { + let mut cpu = CPU::new(); + let program: [u8; 4] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 5, + operation::by_name_and_addressing("PHA", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("PHA", operation::AddressingMode::Implied).code, + ]; + + cpu.load(&program, 0x800); + + for _ in 0..3 { + cpu.step(); } - self.memory[(addr as usize)..program_end_addr].clone_from_slice(buf); - self.pc = addr; + assert_eq!(cpu.a, 5); + assert_eq!(cpu.sp, 0xfd); + assert_eq!(cpu.memory.get(STACK_TOP), 5); + assert_eq!(cpu.memory.get(STACK_TOP - 1), 5); + + let zero: [u8; 253] = [0; 253]; + assert_eq!( + &cpu.memory.bytes()[STACK_BOTTOM as usize..STACK_TOP as usize - 2], + &zero[..] + ); } - pub fn run(&mut self) { - loop { - let operation = operation::by_code(self.memory[self.pc as usize]); + #[test] + fn stack_pop_word() { + let mut cpu = CPU::new(); - match operation.name { - "BRK" => return, - "LDA" => self.operation_lda(operation), - "ADC" => self.operation_adc(operation), - "STA" => self.operation_sta(operation), - _ => panic!("unknown operation"), - }; + cpu.memory.set(STACK_TOP, 0x80); + cpu.memory.set(STACK_TOP - 1, 0x01); + cpu.sp = 0xfd; - self.pc += operation.length as u16; - } + assert_eq!(cpu.stack_pop_word(), 0x8001); } - fn operation_lda(&mut self, operation: &operation::Operation) { - self.a = self.get_operand(operation) as u8; + #[test] + fn stack_push_word() { + let mut cpu = CPU::new(); + let word: u16 = 0x8001; + + cpu.stack_push_word(word); + + assert_eq!(cpu.sp, 0xfd); + assert_eq!(cpu.memory.get(STACK_TOP), 0x80); + assert_eq!(cpu.memory.get(STACK_TOP - 1), 0x01); } - fn operation_adc(&mut self, operation: &operation::Operation) { - self.a += self.get_operand(operation) as u8; + #[test] + fn pla() { + let mut cpu = CPU::new(); + cpu.memory.set(0x0000, 0x68); + cpu.memory.set(STACK_TOP, 0xAB); + cpu.sp = 0xFE; + + cpu.step(); + + assert_eq!(cpu.pc, 0x0001); + assert_eq!(cpu.a, 0xAB); + assert_eq!(cpu.sp, 0xFF); } - fn operation_sta(&mut self, operation: &operation::Operation) { - let addr = self.next_two_byte_number(); - self.memory[addr as usize] = self.a; + #[test] + fn jsr_must_push_return_address() { + let mut cpu = CPU::new(); + let program: [u8; 3] = [ + operation::by_name_and_addressing("JSR", operation::AddressingMode::Absolute).code, + 0xD2, + 0xFF, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.pc, 0xFFD2); + assert_eq!(cpu.sp, 0xFD); + assert_eq!(cpu.stack_pop_word(), 0x802); } - fn get_operand(&self, operation: &operation::Operation) -> usize { - match operation.addressing { - operation::AddressingMode::Immediate => self.memory[self.pc as usize + 1] as usize, - operation::AddressingMode::Absolute => { - self.memory[self.next_two_byte_number() as usize] as usize - } - _ => panic!("unknown addressing mode"), - } + #[test] + fn lsr_zero_page() { + let mut cpu = CPU::new(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("LSR", operation::AddressingMode::ZeroPage).code, + 10, + ]; + cpu.memory.set(10, 0xFF); + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.memory.get(10), 0x7F); // 0xFF >> 1 + assert_eq!(cpu.get_carry(), 1); } - fn next_two_byte_number(&self) -> u16 { - // returns next two bytes (self.pc + 1, self.pc + 2) as one u16 - // (converts from little endian) - utils::little_endian_to_u16( - self.memory[self.pc as usize + 1], - self.memory[self.pc as usize + 2], - ) + #[test] + fn sbc_simple_test() { + let mut cpu = CPU::new(); + cpu.a = 10; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 10, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), FLAG_ZERO); } -} -#[cfg(test)] -mod tests { - use crate::cpu::CPU; - use crate::operation; + #[test] + fn sbc_immediate_zeros() { + let mut cpu = CPU::new(); + cpu.a = 0; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 0, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + assert_eq!(cpu.get_negative(), 0); + } #[test] - fn test_new() { - let c = CPU::new(); + fn sbc_immediate_one_minus_one() { + let mut cpu = CPU::new(); + cpu.a = 1; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 1, + ]; - assert_eq!(c.a, 0); - assert_eq!(c.x, 0); - assert_eq!(c.y, 0); - assert_eq!(c.s, 0); - assert_eq!(c.p, 0); - assert_eq!(c.pc, 0); - assert_eq!(&c.memory[..], &[0; 64 * 1024][..]); + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + assert_eq!(cpu.get_negative(), 0); } #[test] - fn test_load() { - // test when it loads a three bytes dump at a specific location + fn sbc_immediate_10_minus_5_minus_carry() { let mut cpu = CPU::new(); + cpu.a = 10; + cpu.clear_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 5, + ]; - let addr = 32768; - let bytes = [1; 3]; + cpu.load(&program, 0x800); + cpu.step(); - assert_eq!(&cpu.memory[..], &[0; 64 * 1024][..]); - cpu.load(&bytes, addr); + assert_eq!(cpu.a, 4); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + assert_eq!(cpu.get_negative(), 0); + } - let exp_memory = [0, 1, 1, 1, 0]; - assert_eq!(&cpu.memory[32767..32772], exp_memory); + #[test] + fn sbc_immediate_with_zero() { + let mut cpu = CPU::new(); + cpu.a = 1; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 5, + ]; - assert_eq!(cpu.pc, addr); + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 252); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); } #[test] - #[should_panic] - fn test_load_data_too_big() { + fn sbc_immediate_carry_but_no_overflow() { let mut cpu = CPU::new(); - cpu.load(&[1, 2, 3], 65535); + cpu.a = 0x50; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 0xf0, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 0x60); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), 0); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_overflow(), 0); + } + + #[test] + fn sbc_immediate_carry_and_overflow() { + let mut cpu = CPU::new(); + cpu.a = 0x50; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 0xb0, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 0xa0); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.get_overflow(), FLAG_OVERFLOW); + } + + #[test] + fn sbc_immediate_no_carry_and_overflow() { + let mut cpu = CPU::new(); + cpu.a = 0xd0; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 0x70, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 0x60); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_overflow(), FLAG_OVERFLOW); + } + + #[test] + fn sbc_immediate_no_carry_and_no_overflow() { + let mut cpu = CPU::new(); + cpu.a = 0xd0; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 0x30, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + + assert_eq!(cpu.a, 0xa0); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.get_overflow(), 0); } #[test] - fn test_operation_lda() { + fn sbc_immediate_with_overflow() { let mut cpu = CPU::new(); + cpu.a = 1; + cpu.set_carry(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("SBC", operation::AddressingMode::Immediate).code, + 5, + ]; + + cpu.load(&program, 0x800); + cpu.step(); + assert_eq!(cpu.a, 252); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + } + + #[test] + fn ror_absolute_zero_and_carry_zero() { + let mut cpu = CPU::new(); let program: [u8; 3] = [ - operation::by_name("LDA", operation::AddressingMode::Immediate).code, - 10, - operation::by_name("BRK", operation::AddressingMode::Immediate).code, + operation::by_name_and_addressing("ROR", operation::AddressingMode::Absolute).code, + 0x00, + 0x80, ]; - cpu.load(&program, 0); + + cpu.load(&program, 0x800); + cpu.step(); assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_carry(), 0); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.memory.get(0x8000), 0); + } + + #[test] + fn ror_absolute_zero_and_carry_one() { + let mut cpu = CPU::new(); + cpu.set_carry(); + let program: [u8; 3] = [ + operation::by_name_and_addressing("ROR", operation::AddressingMode::Absolute).code, + 0x00, + 0x80, + ]; - cpu.run(); + cpu.load(&program, 0x800); + cpu.step(); - assert_eq!(cpu.a, 10); + assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_carry(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.memory.get(0x8000), 0x80); } #[test] - fn test_operation_adc_immediate() { + fn bmi() { + let mut cpu = CPU::new(); + cpu.set_negative(); + let program: [u8; 2] = [ + operation::by_name_and_addressing("BMI", operation::AddressingMode::RelativeAddressing) + .code, + 0x06, + ]; + + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.pc, 0x0002 + 0x06); + } + + #[test] + fn bit_absolite_zero_flag() { let mut cpu = CPU::new(); - cpu.a = 10; let program: [u8; 3] = [ - operation::by_name("ADC", operation::AddressingMode::Immediate).code, - 15, - operation::by_name("BRK", operation::AddressingMode::Immediate).code, + operation::by_name_and_addressing("BIT", operation::AddressingMode::Absolute).code, + 0x10, + 0x11, ]; - cpu.load(&program, 0); + cpu.memory.set(0x1110, 0b0001_0000); + cpu.a = 0b1100_0000; - assert_eq!(cpu.a, 10); + cpu.load(&program, 0x0); + cpu.step(); - cpu.run(); + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_overflow(), 0); + } - assert_eq!(cpu.a, 25); + #[test] + fn bit_absolute_saves_6_and_7_bits() { + let mut cpu = CPU::new(); + + let program: [u8; 3] = [ + operation::by_name_and_addressing("BIT", operation::AddressingMode::Absolute).code, + 0x10, + 0x11, + ]; + cpu.memory.set(0x1110, 0b1100_0000); + cpu.a = 0b1100_0000; + + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.get_overflow(), FLAG_OVERFLOW); } #[test] - fn test_operation_adc_absolute() { + fn bit_absolute_saves_6_and_7_bits_when_they_are_0() { let mut cpu = CPU::new(); - cpu.a = 10; + cpu.set_negative(); + cpu.set_overflow(); + cpu.set_zero(); - let program: [u8; 4] = [ - operation::by_name("ADC", operation::AddressingMode::Absolute).code, - 0xff, - 0xaa, - operation::by_name("BRK", operation::AddressingMode::Immediate).code, + let program: [u8; 3] = [ + operation::by_name_and_addressing("BIT", operation::AddressingMode::Absolute).code, + 0x10, + 0x11, ]; - cpu.load(&program, 0); - cpu.memory[43775] = 15; // 0xaaff + cpu.memory.set(0x1110, 1); + cpu.a = 1; - assert_eq!(cpu.a, 10); + cpu.load(&program, 0x0); + cpu.step(); - cpu.run(); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_overflow(), 0); + } - assert_eq!(cpu.a, 25); + #[test] + fn bit_zero_page_zero_flag() { + let mut cpu = CPU::new(); + + let program: [u8; 2] = [ + operation::by_name_and_addressing("BIT", operation::AddressingMode::ZeroPage).code, + 0x10, + ]; + cpu.memory.set(0x10, 0b0001_0000); + cpu.a = 0b1100_0000; + + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_overflow(), 0); } #[test] - fn test_operation_sta() { + fn bit_zero_page_saves_6_and_7_bits() { let mut cpu = CPU::new(); - cpu.a = 10; - let program: [u8; 4] = [ - operation::by_name("STA", operation::AddressingMode::Absolute).code, - 50, - 0, - operation::by_name("BRK", operation::AddressingMode::Immediate).code, + let program: [u8; 2] = [ + operation::by_name_and_addressing("BIT", operation::AddressingMode::ZeroPage).code, + 0x10, ]; - cpu.load(&program, 0); + cpu.memory.set(0x10, 0b1100_0000); + cpu.a = 0b1100_0000; + + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.get_overflow(), FLAG_OVERFLOW); + } + + #[test] + fn bit_zero_page_saves_6_and_7_bits_when_they_are_0() { + let mut cpu = CPU::new(); + cpu.set_negative(); + cpu.set_overflow(); + cpu.set_zero(); - assert_eq!(cpu.memory[50], 0); + let program: [u8; 2] = [ + operation::by_name_and_addressing("BIT", operation::AddressingMode::ZeroPage).code, + 0x10, + ]; + cpu.memory.set(0x10, 1); + cpu.a = 1; - cpu.run(); + cpu.load(&program, 0x0); + cpu.step(); - assert_eq!(cpu.memory[50], cpu.a); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_overflow(), 0); } #[test] - fn test_next_two_bytes() { + fn rts() { let mut cpu = CPU::new(); - let bytes = [0, 0x0a, 0x0b]; - cpu.load(&bytes, 0); - assert_eq!(cpu.next_two_byte_number(), 2826); + + let program: [u8; 5] = [ + operation::by_name_and_addressing("LDA", operation::AddressingMode::Immediate).code, + 0x50, + operation::by_name_and_addressing("PHA", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("PHA", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("RTS", operation::AddressingMode::Implied).code, + ]; + + cpu.load(&program, 0x0); + + for _ in 0..4 { + cpu.step(); + } + + assert_eq!(cpu.a, 0x50); + assert_eq!(cpu.pc, 0x5051); + assert_eq!(cpu.p, 0b0011_0000); } #[test] - fn test_next_two_bytes_with_only_lower_byte() { + fn jmp_indirect() { let mut cpu = CPU::new(); - let bytes = [0, 10, 0]; - cpu.load(&bytes, 0); - assert_eq!(cpu.next_two_byte_number(), 10); + + let program: [u8; 3] = [ + operation::by_name_and_addressing("JMP", operation::AddressingMode::IndirectAbsolute) + .code, + 0x00, + 0x02, + ]; + + cpu.memory.set(0x0200, 0xCD); + cpu.memory.set(0x0201, 0xAB); + + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.pc, 0xABCD); } #[test] - fn test_simple_program() { - // loads 100 from the memory - // adds 7 to the a register - // stores a register at address 0x000f + fn asl_accumulator_80_to_zero() { let mut cpu = CPU::new(); - cpu.a = 10; - let program: [u8; 8] = [ - operation::by_name("LDA", operation::AddressingMode::Immediate).code, - 100, - operation::by_name("ADC", operation::AddressingMode::Immediate).code, - 7, - operation::by_name("STA", operation::AddressingMode::Absolute).code, - 15, - 0, - operation::by_name("BRK", operation::AddressingMode::Immediate).code, + let program: [u8; 1] = [operation::by_name_and_addressing( + "ASL", + operation::AddressingMode::AccumulatorAddressing, + ) + .code]; + cpu.a = 0x80; + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + } + + #[test] + fn asl_accumulator_shift() { + let mut cpu = CPU::new(); + + let program: [u8; 1] = [operation::by_name_and_addressing( + "ASL", + operation::AddressingMode::AccumulatorAddressing, + ) + .code]; + cpu.a = 1; + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.a, 0b10); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_overflow(), 0); + } + + #[test] + fn rol_accumulator_zero() { + let mut cpu = CPU::new(); + cpu.clear_zero(); + cpu.clear_carry(); + cpu.clear_negative(); + + let program: [u8; 1] = [operation::by_name_and_addressing( + "ROL", + operation::AddressingMode::AccumulatorAddressing, + ) + .code]; + + cpu.a = 0; + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_carry(), 0); + } + + #[test] + fn rol_accumulator_to_zero() { + let mut cpu = CPU::new(); + cpu.clear_zero(); + cpu.clear_carry(); + cpu.clear_negative(); + + let program: [u8; 1] = [operation::by_name_and_addressing( + "ROL", + operation::AddressingMode::AccumulatorAddressing, + ) + .code]; + + cpu.a = 0x80; + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.a, 0); + assert_eq!(cpu.get_zero(), FLAG_ZERO); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_carry(), FLAG_CARRY); + } + + #[test] + fn rol_accumulator_with_initial_carry() { + let mut cpu = CPU::new(); + cpu.clear_zero(); + cpu.set_carry(); + cpu.clear_negative(); + + let program: [u8; 1] = [operation::by_name_and_addressing( + "ROL", + operation::AddressingMode::AccumulatorAddressing, + ) + .code]; + + cpu.a = 0x40; + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.a, 0x81); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), FLAG_NEGATIVE); + assert_eq!(cpu.get_carry(), 0); + } + + #[test] + fn rol_accumulator() { + let mut cpu = CPU::new(); + cpu.clear_zero(); + cpu.clear_carry(); + cpu.clear_negative(); + + let program: [u8; 1] = [operation::by_name_and_addressing( + "ROL", + operation::AddressingMode::AccumulatorAddressing, + ) + .code]; + + cpu.a = 0x1; + cpu.load(&program, 0x0); + cpu.step(); + + assert_eq!(cpu.a, 0x2); + assert_eq!(cpu.get_zero(), 0); + assert_eq!(cpu.get_negative(), 0); + assert_eq!(cpu.get_carry(), 0); + } + + #[test] + fn php_and_then_plp() { + let mut cpu = CPU::new(); + + cpu.p = 0b1100_0101; + + let program: [u8; 2] = [ + operation::by_name_and_addressing("PHP", operation::AddressingMode::Implied).code, + operation::by_name_and_addressing("PLP", operation::AddressingMode::Implied).code, ]; - cpu.load(&program, 0); + cpu.load(&program, 0x800); - cpu.run(); + cpu.step(); - assert_eq!(cpu.a, 107); - assert_eq!(cpu.memory[15], 107); + assert_eq!(cpu.sp, 0xfe); + assert_eq!( + cpu.memory.get(STACK_BOTTOM + u16::from(cpu.sp) + 1), + 0b1100_0101 + ); + + cpu.p = 0; + + cpu.step(); + + assert_eq!(cpu.sp, 0xff); + assert_eq!(cpu.p, 0b1111_0101); } + #[test] + fn brk() { + let mut cpu = CPU::new(); + + let program = + [operation::by_name_and_addressing("BRK", operation::AddressingMode::Implied).code]; + cpu.load(&program, 0x800); + cpu.memory.set(0xfffe, 0x11); + cpu.memory.set(0xffff, 0x22); + + cpu.step(); + + assert_eq!(cpu.pc, 0x2211); + assert_eq!(cpu.sp, 0xfc); + assert_eq!(cpu.memory.get(STACK_BOTTOM + 0xff), 0x08); + assert_eq!(cpu.memory.get(STACK_BOTTOM + 0xfe), 0x02); + assert_eq!(cpu.memory.get(STACK_BOTTOM + 0xfd), 0b0011_0000); + } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4f44945 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +pub mod asm; +pub mod cpu; +pub mod mem; +mod operation; +mod utils; + +#[macro_use] +extern crate log; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index b307f83..0000000 --- a/src/main.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod cpu; -mod operation; -mod utils; -use cpu::CPU; - -fn main() { - println!("Hello, world!"); - let cpu = CPU::new(); -} diff --git a/src/mem.rs b/src/mem.rs new file mode 100644 index 0000000..c22faca --- /dev/null +++ b/src/mem.rs @@ -0,0 +1,65 @@ +pub type ReadCallback = fn(addr: u16) -> Option<(u16, u8)>; +pub type WriteCallback = fn(addr: u16, value: u8) -> Option<(u16, u8)>; + +pub struct Memory { + bytes: [u8; 64 * 1024], + read_callback: Option, + write_callback: Option, +} + +impl Default for Memory { + fn default() -> Self { + Self::new() + } +} + +impl Memory { + pub fn new() -> Memory { + Memory { + bytes: [0; 64 * 1024], + read_callback: None, + write_callback: None, + } + } + + pub fn load(&mut self, addr: u16, data: &[u8]) { + let program_end_addr = data.len() + addr as usize; + self.bytes[(addr as usize)..program_end_addr].clone_from_slice(data); + } + + pub fn register_read_callback(&mut self, cb: ReadCallback) { + self.read_callback = Some(cb); + } + + pub fn register_write_callback(&mut self, cb: WriteCallback) { + self.write_callback = Some(cb); + } + + pub fn set(&mut self, addr: u16, value: u8) { + self.bytes[addr as usize] = value; + + if let Some(cb) = self.write_callback { + self.process_callback(cb(addr, value)); + } + } + + fn process_callback(&mut self, result: Option<(u16, u8)>) { + if let Some((addr, value)) = result { + self.bytes[addr as usize] = value; + } + } + + pub fn get(&mut self, addr: u16) -> u8 { + let value = self.bytes[addr as usize]; + + if let Some(cb) = self.read_callback { + self.process_callback(cb(addr)); + } + + value + } + + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} diff --git a/src/operation.rs b/src/operation.rs index fb6118d..27eb38f 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,14 +1,16 @@ #[derive(PartialEq, Eq)] pub enum AddressingMode { - Immediate = 1, + Implied = 0, + Immediate, Absolute, ZeroPage, - Implied, IndirectAbsolute, - AbsoluteIndexed, - ZeroPageIndexed, - IndexedIndirect, + AbsoluteIndexedX, + AbsoluteIndexedY, + ZeroPageIndexedX, + ZeroPageIndexedY, IndirectIndexed, + IndirectIndexedX, RelativeAddressing, AccumulatorAddressing, } @@ -25,7 +27,93 @@ pub static OPERATIONS: &'static [Operation] = &[ name: "BRK", code: 0x00, length: 1, - addressing: AddressingMode::Immediate, + addressing: AddressingMode::Implied, + }, + Operation { + name: "RTI", + code: 0x40, + length: 1, + addressing: AddressingMode::Implied, + }, + // DEC + Operation { + name: "DEC", + code: 0xC6, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "DEC", + code: 0xD6, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "DEC", + code: 0xCE, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "DEC", + code: 0xDE, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + // INC + Operation { + name: "INC", + code: 0xEE, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "INC", + code: 0xE6, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "INC", + code: 0xF6, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "INX", + code: 0xE8, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "INY", + code: 0xC8, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "TAX", + code: 0xAA, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "TAY", + code: 0xA8, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "DEX", + code: 0xCA, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "DEY", + code: 0x88, + length: 1, + addressing: AddressingMode::Implied, }, Operation { name: "LDA", @@ -33,6 +121,110 @@ pub static OPERATIONS: &'static [Operation] = &[ length: 2, addressing: AddressingMode::Immediate, }, + Operation { + name: "LDA", + code: 0xa5, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "LDA", + code: 0xb5, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "LDA", + code: 0xad, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "LDA", + code: 0xB9, + length: 3, + addressing: AddressingMode::AbsoluteIndexedY, + }, + Operation { + name: "LDA", + code: 0xBD, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + Operation { + name: "LDA", + code: 0xA1, + length: 2, + addressing: AddressingMode::IndirectIndexedX, + }, + Operation { + name: "LDA", + code: 0xB1, + length: 2, + addressing: AddressingMode::IndirectIndexed, + }, + // LDX + Operation { + name: "LDX", + code: 0xa2, + length: 2, + addressing: AddressingMode::Immediate, + }, + Operation { + name: "LDX", + code: 0xa6, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "LDX", + code: 0xB6, + length: 2, + addressing: AddressingMode::ZeroPageIndexedY, + }, + Operation { + name: "LDX", + code: 0xae, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "LDX", + code: 0xBE, + length: 3, + addressing: AddressingMode::AbsoluteIndexedY, + }, + // LDY + Operation { + name: "LDY", + code: 0xA0, + length: 2, + addressing: AddressingMode::Immediate, + }, + Operation { + name: "LDY", + code: 0xA4, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "LDY", + code: 0xB4, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "LDY", + code: 0xAC, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "LDY", + code: 0xBC, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, Operation { name: "ADC", code: 0x69, @@ -41,7 +233,104 @@ pub static OPERATIONS: &'static [Operation] = &[ }, Operation { name: "ADC", - code: 0x60, + code: 0x6D, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "ADC", + code: 0x65, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "ADC", + code: 0x75, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "ADC", + code: 0x7D, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + Operation { + name: "ADC", + code: 0x79, + length: 3, + addressing: AddressingMode::AbsoluteIndexedY, + }, + // SBC + Operation { + name: "SBC", + code: 0xE9, + length: 2, + addressing: AddressingMode::Immediate, + }, + Operation { + name: "SBC", + code: 0xED, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "SBC", + code: 0xE5, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "SBC", + code: 0xF5, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "SBC", + code: 0xF1, + length: 2, + addressing: AddressingMode::IndirectIndexed, + }, + Operation { + name: "STX", + code: 0x8E, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "STX", + code: 0x86, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "STX", + code: 0x96, + length: 2, + addressing: AddressingMode::ZeroPageIndexedY, + }, + Operation { + name: "STY", + code: 0x8C, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "STY", + code: 0x84, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "STY", + code: 0x94, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "STA", + code: 0x8D, length: 3, addressing: AddressingMode::Absolute, }, @@ -49,48 +338,873 @@ pub static OPERATIONS: &'static [Operation] = &[ name: "STA", code: 0x85, length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "STA", + code: 0x95, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "STA", + code: 0x9D, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + Operation { + name: "STA", + code: 0x99, + length: 3, + addressing: AddressingMode::AbsoluteIndexedY, + }, + Operation { + name: "STA", + code: 0x81, + length: 2, + addressing: AddressingMode::IndirectIndexedX, + }, + Operation { + name: "STA", + code: 0x91, + length: 2, + addressing: AddressingMode::IndirectIndexed, + }, + Operation { + name: "BEQ", + code: 0xF0, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + Operation { + name: "BNE", + code: 0xD0, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + Operation { + name: "BVC", + code: 0x50, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + Operation { + name: "BVS", + code: 0x70, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + Operation { + name: "BIT", + code: 0x24, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "BIT", + code: 0x2C, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "BCC", + code: 0x90, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + Operation { + name: "BMI", + code: 0x30, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + Operation { + name: "BPL", + code: 0x10, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + Operation { + name: "BCS", + code: 0xB0, + length: 2, + addressing: AddressingMode::RelativeAddressing, + }, + // CPX + Operation { + name: "CPX", + code: 0xE0, + length: 2, + addressing: AddressingMode::Immediate, + }, + Operation { + name: "CPX", + code: 0xE4, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "CPX", + code: 0xEC, + length: 3, addressing: AddressingMode::Absolute, }, + // EOR + Operation { + name: "EOR", + code: 0x49, + length: 2, + addressing: AddressingMode::Immediate, + }, + // CMP + Operation { + name: "CMP", + code: 0xC9, + length: 2, + addressing: AddressingMode::Immediate, + }, + Operation { + name: "CMP", + code: 0xC5, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "CMP", + code: 0xD5, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "CMP", + code: 0xCD, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "CMP", + code: 0xDD, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + Operation { + name: "CMP", + code: 0xD9, + length: 3, + addressing: AddressingMode::AbsoluteIndexedY, + }, + Operation { + name: "CMP", + code: 0xC1, + length: 2, + addressing: AddressingMode::IndirectIndexedX, + }, + Operation { + name: "CMP", + code: 0xD1, + length: 2, + addressing: AddressingMode::IndirectIndexed, + }, + // CPY + Operation { + name: "CPY", + code: 0xC0, + length: 2, + addressing: AddressingMode::Immediate, + }, + Operation { + name: "CPY", + code: 0xC4, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "CPY", + code: 0xCC, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "CLC", + code: 0x18, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "CLD", + code: 0xD8, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "CLI", + code: 0x58, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "CLV", + code: 0xB8, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "SEC", + code: 0x38, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "SED", + code: 0xF8, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "SEI", + code: 0x78, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "NOP", + code: 0xEA, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "JMP", + code: 0x4C, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "JMP", + code: 0x6C, + length: 3, + addressing: AddressingMode::IndirectAbsolute, + }, + Operation { + name: "PHA", + code: 0x48, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "TXA", + code: 0x8A, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "TYA", + code: 0x98, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "TSX", + code: 0xBA, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "PLA", + code: 0x68, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "PLP", + code: 0x28, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "PHP", + code: 0x08, + length: 1, + addressing: AddressingMode::Implied, + }, + Operation { + name: "JSR", + code: 0x20, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "RTS", + code: 0x60, + length: 1, + addressing: AddressingMode::Implied, + }, + // LSR + Operation { + name: "LSR", + code: 0x46, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "LSR", + code: 0x56, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "LSR", + code: 0x4E, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "LSR", + code: 0x5E, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + Operation { + name: "LSR", + code: 0x4A, + length: 1, + addressing: AddressingMode::AccumulatorAddressing, + }, + // ROL + Operation { + name: "ROL", + code: 0x2A, + length: 1, + addressing: AddressingMode::AccumulatorAddressing, + }, + Operation { + name: "ROL", + code: 0x26, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "ROL", + code: 0x2E, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "ROL", + code: 0x36, + length: 3, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "ROL", + code: 0x3E, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + Operation { + name: "TXS", + code: 0x9A, + length: 1, + addressing: AddressingMode::Implied, + }, + // ROR + Operation { + name: "ROR", + code: 0x6A, + length: 1, + addressing: AddressingMode::AccumulatorAddressing, + }, + Operation { + name: "ROR", + code: 0x66, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "ROR", + code: 0x6E, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "TXS", + code: 0x9A, + length: 1, + addressing: AddressingMode::Implied, + }, + // ASL + Operation { + name: "ASL", + code: 0x0A, + length: 1, + addressing: AddressingMode::AccumulatorAddressing, + }, + Operation { + name: "ASL", + code: 0x06, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "ASL", + code: 0x16, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "ASL", + code: 0x0E, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "ASL", + code: 0x1E, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + // AND + Operation { + name: "AND", + code: 0x29, + length: 2, + addressing: AddressingMode::Immediate, + }, + // + // ORA + // + Operation { + name: "ORA", + code: 0x09, + length: 2, + addressing: AddressingMode::Immediate, + }, + Operation { + name: "ORA", + code: 0x05, + length: 2, + addressing: AddressingMode::ZeroPage, + }, + Operation { + name: "ORA", + code: 0x15, + length: 2, + addressing: AddressingMode::ZeroPageIndexedX, + }, + Operation { + name: "ORA", + code: 0x0D, + length: 3, + addressing: AddressingMode::Absolute, + }, + Operation { + name: "ORA", + code: 0x1D, + length: 3, + addressing: AddressingMode::AbsoluteIndexedX, + }, + Operation { + name: "ORA", + code: 0x11, + length: 2, + addressing: AddressingMode::IndirectIndexed, + }, + Operation { + name: "ORA", + code: 0x01, + length: 2, + addressing: AddressingMode::IndirectIndexedX, + }, + Operation { + name: "ORA", + code: 0x19, + length: 3, + addressing: AddressingMode::AbsoluteIndexedY, + }, ]; pub fn by_code(code: u8) -> &'static Operation { + debug!("searching for operation by code={:X}", code); let opcode = OPERATIONS.iter().find(|o| o.code == code); if let Some(opcode) = opcode { return opcode; } else { + error!("Unknown operation code: 0x{:X}", code); panic!("unknown operation code"); } } -pub fn by_name(name: &str, addressing: AddressingMode) -> &Operation { - let opcode = OPERATIONS - .iter() - .find(|o| o.name == name && o.addressing == addressing); +pub fn by_name(name: &str) -> Vec<&'static Operation> { + debug!("searching for operation by name={}", name); + OPERATIONS.iter().filter(|o| o.name == name).collect() +} - if let Some(opcode) = opcode { - return opcode; - } else { - panic!("unknown operation name"); - } +pub fn by_name_and_addressing(name: &str, addressing: AddressingMode) -> &'static Operation { + debug!("searching for operation by name={}", name); + OPERATIONS + .iter() + .find(|o| o.name == name && o.addressing == addressing) + .expect("unknown operation name") } #[cfg(test)] mod tests { - use crate::operation::{by_name, AddressingMode}; + use crate::operation::{by_name_and_addressing, AddressingMode}; macro_rules! test_opcode { ($test_name:ident, $name:expr, $mode:expr, $expected:expr) => { #[test] fn $test_name() { - assert_eq!(by_name($name, $mode).code, $expected); + assert_eq!(by_name_and_addressing($name, $mode).code, $expected); } }; } - test_opcode!(brk_immediate, "BRK", AddressingMode::Immediate, 0x00); + test_opcode!(brk, "BRK", AddressingMode::Implied, 0x00); + test_opcode!(rti, "RTI", AddressingMode::Implied, 0x40); + test_opcode!(tax, "TAX", AddressingMode::Implied, 0xAA); + test_opcode!(tay, "TAY", AddressingMode::Implied, 0xA8); + test_opcode!(txa, "TXA", AddressingMode::Implied, 0x8A); + test_opcode!(tya, "TYA", AddressingMode::Implied, 0x98); + test_opcode!(tsx, "TSX", AddressingMode::Implied, 0xBA); + + test_opcode!(php, "PHP", AddressingMode::Implied, 0x08); + test_opcode!(plp, "PLP", AddressingMode::Implied, 0x28); + + test_opcode!(clc, "CLC", AddressingMode::Implied, 0x18); + test_opcode!(cld, "CLD", AddressingMode::Implied, 0xD8); + test_opcode!(cli, "CLI", AddressingMode::Implied, 0x58); + test_opcode!(clv, "CLV", AddressingMode::Implied, 0xB8); + + test_opcode!(sec, "SEC", AddressingMode::Implied, 0x38); + test_opcode!(sed, "SED", AddressingMode::Implied, 0xF8); + test_opcode!(sei, "SEI", AddressingMode::Implied, 0x78); + + test_opcode!(nop, "NOP", AddressingMode::Implied, 0xEA); + + test_opcode!(inc_zero_page, "INC", AddressingMode::ZeroPage, 0xE6); + test_opcode!( + inc_zero_page_x, + "INC", + AddressingMode::ZeroPageIndexedX, + 0xF6 + ); + test_opcode!(inc_absolute, "INC", AddressingMode::Absolute, 0xEE); + + test_opcode!(dec_zero_page, "DEC", AddressingMode::ZeroPage, 0xC6); + test_opcode!( + dec_zero_page_x, + "DEC", + AddressingMode::ZeroPageIndexedX, + 0xD6 + ); + test_opcode!(dec_absolute, "DEC", AddressingMode::Absolute, 0xCE); + test_opcode!( + dec_absolute_x, + "DEC", + AddressingMode::AbsoluteIndexedX, + 0xDE + ); + + test_opcode!(inx, "INX", AddressingMode::Implied, 0xE8); + test_opcode!(iny, "INY", AddressingMode::Implied, 0xC8); + + test_opcode!(dex, "DEX", AddressingMode::Implied, 0xCA); + test_opcode!(dey, "DEY", AddressingMode::Implied, 0x88); + + test_opcode!(jmp_absolute, "JMP", AddressingMode::Absolute, 0x4C); + test_opcode!(pha, "PHA", AddressingMode::Implied, 0x48); + test_opcode!(pla, "PLA", AddressingMode::Implied, 0x68); + + test_opcode!(lsr_zero_page, "LSR", AddressingMode::ZeroPage, 0x46); + test_opcode!( + lsr_zero_page_x, + "LSR", + AddressingMode::ZeroPageIndexedX, + 0x56 + ); + test_opcode!(lsr_absolute, "LSR", AddressingMode::Absolute, 0x4E); + test_opcode!( + lsr_absolute_x, + "LSR", + AddressingMode::AbsoluteIndexedX, + 0x5E + ); + test_opcode!( + lsr_accumulator, + "LSR", + AddressingMode::AccumulatorAddressing, + 0x4A + ); + + test_opcode!(bit_zero_page, "BIT", AddressingMode::ZeroPage, 0x24); + test_opcode!(bit_absolute, "BIT", AddressingMode::Absolute, 0x2C); + + test_opcode!(and_immediate, "AND", AddressingMode::Immediate, 0x29); + + test_opcode!(ora_immediate, "ORA", AddressingMode::Immediate, 0x09); + test_opcode!(ora_zero_page, "ORA", AddressingMode::ZeroPage, 0x05); + test_opcode!( + ora_zero_page_x, + "ORA", + AddressingMode::ZeroPageIndexedX, + 0x15 + ); + test_opcode!(ora_absolute, "ORA", AddressingMode::Absolute, 0x0D); + test_opcode!( + ora_absolute_x, + "ORA", + AddressingMode::AbsoluteIndexedX, + 0x1D + ); + test_opcode!( + ora_absolute_y, + "ORA", + AddressingMode::AbsoluteIndexedY, + 0x19 + ); + + test_opcode!(cmp_immediate, "CMP", AddressingMode::Immediate, 0xC9); + test_opcode!(cmp_zero_page, "CMP", AddressingMode::ZeroPage, 0xC5); + test_opcode!(cmp_absolute, "CMP", AddressingMode::Absolute, 0xCD); + test_opcode!( + cmp_indirect_x, + "CMP", + AddressingMode::IndirectIndexedX, + 0xC1 + ); + test_opcode!(cmp_indirect_y, "CMP", AddressingMode::IndirectIndexed, 0xD1); + test_opcode!( + cmp_zero_page_x, + "CMP", + AddressingMode::ZeroPageIndexedX, + 0xD5 + ); + test_opcode!( + cmp_absolute_x, + "CMP", + AddressingMode::AbsoluteIndexedX, + 0xDD + ); + test_opcode!( + cmp_absolute_y, + "CMP", + AddressingMode::AbsoluteIndexedY, + 0xD9 + ); + + test_opcode!(cpx_immediate, "CPX", AddressingMode::Immediate, 0xE0); + test_opcode!(cpx_zero_page, "CPX", AddressingMode::ZeroPage, 0xE4); + test_opcode!(cpx_absolute, "CPX", AddressingMode::Absolute, 0xEC); + + test_opcode!(cpy_immediate, "CPY", AddressingMode::Immediate, 0xC0); + test_opcode!(cpy_zero_page, "CPY", AddressingMode::ZeroPage, 0xC4); + test_opcode!(cpy_absolute, "CPY", AddressingMode::Absolute, 0xCC); + test_opcode!(lda_immediate, "LDA", AddressingMode::Immediate, 0xa9); + test_opcode!( + lda_absolute_indexed_y, + "LDA", + AddressingMode::AbsoluteIndexedY, + 0xB9 + ); + test_opcode!( + lda_absolute_indexed_x, + "LDA", + AddressingMode::AbsoluteIndexedX, + 0xBD + ); + test_opcode!(lda_zero_page, "LDA", AddressingMode::ZeroPage, 0xa5); + test_opcode!( + lda_zero_page_x, + "LDA", + AddressingMode::ZeroPageIndexedX, + 0xB5 + ); + test_opcode!(lda_absolute, "LDA", AddressingMode::Absolute, 0xad); + test_opcode!( + lda_indirect_x, + "LDA", + AddressingMode::IndirectIndexedX, + 0xA1 + ); + test_opcode!(lda_indirect_y, "LDA", AddressingMode::IndirectIndexed, 0xB1); + + test_opcode!(ldx_immediate, "LDX", AddressingMode::Immediate, 0xa2); + test_opcode!(ldx_zero_page, "LDX", AddressingMode::ZeroPage, 0xa6); + test_opcode!( + ldx_zero_page_y, + "LDX", + AddressingMode::ZeroPageIndexedY, + 0xB6 + ); + test_opcode!(ldx_absolute, "LDX", AddressingMode::Absolute, 0xae); + test_opcode!( + ldx_absolute_y, + "LDX", + AddressingMode::AbsoluteIndexedY, + 0xBE + ); + + test_opcode!(ldy_immediate, "LDY", AddressingMode::Immediate, 0xA0); + test_opcode!(ldy_zero_page, "LDY", AddressingMode::ZeroPage, 0xA4); + test_opcode!( + ldy_zero_page_x, + "LDY", + AddressingMode::ZeroPageIndexedX, + 0xB4 + ); + test_opcode!(ldy_absolute, "LDY", AddressingMode::Absolute, 0xAC); + test_opcode!( + ldy_absolute_x, + "LDY", + AddressingMode::AbsoluteIndexedX, + 0xBC + ); + test_opcode!(adc_immediate, "ADC", AddressingMode::Immediate, 0x69); - test_opcode!(adc_absolute, "ADC", AddressingMode::Absolute, 0x60); - test_opcode!(sta_absolute, "STA", AddressingMode::Absolute, 0x85); + test_opcode!(adc_absolute, "ADC", AddressingMode::Absolute, 0x6D); + test_opcode!(adc_zero_page, "ADC", AddressingMode::ZeroPage, 0x65); + test_opcode!( + adc_zero_page_x, + "ADC", + AddressingMode::ZeroPageIndexedX, + 0x75 + ); + test_opcode!( + adc_absolute_x, + "ADC", + AddressingMode::AbsoluteIndexedX, + 0x7D + ); + test_opcode!( + adc_absolute_y, + "ADC", + AddressingMode::AbsoluteIndexedY, + 0x79 + ); + + test_opcode!(sbc_immediate, "SBC", AddressingMode::Immediate, 0xE9); + test_opcode!(sbc_absolute, "SBC", AddressingMode::Absolute, 0xED); + test_opcode!(sbc_zero_page, "SBC", AddressingMode::ZeroPage, 0xE5); + test_opcode!( + sbc_zero_page_x, + "SBC", + AddressingMode::ZeroPageIndexedX, + 0xF5 + ); + test_opcode!(sbc_indirect_y, "SBC", AddressingMode::IndirectIndexed, 0xF1); + + test_opcode!(stx_absolute, "STX", AddressingMode::Absolute, 0x8E); + test_opcode!(stx_zero_page, "STX", AddressingMode::ZeroPage, 0x86); + + test_opcode!(txs, "TXS", AddressingMode::Implied, 0x9A); + + test_opcode!(eor_immediate, "EOR", AddressingMode::Immediate, 0x49); + + test_opcode!( + asl_accumulator, + "ASL", + AddressingMode::AccumulatorAddressing, + 0x0A + ); + test_opcode!(asl_zero_page, "ASL", AddressingMode::ZeroPage, 0x06); + test_opcode!( + asl_zero_page_x, + "ASL", + AddressingMode::ZeroPageIndexedX, + 0x16 + ); + test_opcode!(asl_absolute, "ASL", AddressingMode::Absolute, 0x0e); + test_opcode!( + asl_absolute_x, + "ASL", + AddressingMode::AbsoluteIndexedX, + 0x1e + ); + + test_opcode!(sty_absolute, "STY", AddressingMode::Absolute, 0x8C); + test_opcode!( + sty_zero_page_x, + "STY", + AddressingMode::ZeroPageIndexedX, + 0x94 + ); + test_opcode!(sty_zero_page, "STY", AddressingMode::ZeroPage, 0x84); + + test_opcode!(sta_absolute, "STA", AddressingMode::Absolute, 0x8D); + test_opcode!(sta_zero_page, "STA", AddressingMode::ZeroPage, 0x85); + test_opcode!( + sta_zero_page_x, + "STA", + AddressingMode::ZeroPageIndexedX, + 0x95 + ); + test_opcode!( + stx_zero_page_y, + "STX", + AddressingMode::ZeroPageIndexedY, + 0x96 + ); + test_opcode!( + sta_absolute_x, + "STA", + AddressingMode::AbsoluteIndexedX, + 0x9D + ); + test_opcode!( + sta_indirect_x, + "STA", + AddressingMode::IndirectIndexedX, + 0x81 + ); + test_opcode!(sta_indirect_y, "STA", AddressingMode::IndirectIndexed, 0x91); + test_opcode!( + sta_absolute_y, + "STA", + AddressingMode::AbsoluteIndexedY, + 0x99 + ); + + test_opcode!(bne, "BNE", AddressingMode::RelativeAddressing, 0xD0); + test_opcode!(bvc, "BVC", AddressingMode::RelativeAddressing, 0x50); + test_opcode!(bvs, "BVS", AddressingMode::RelativeAddressing, 0x70); + test_opcode!(beq, "BEQ", AddressingMode::RelativeAddressing, 0xF0); + test_opcode!(bmi, "BMI", AddressingMode::RelativeAddressing, 0x30); + test_opcode!(bpl, "BPL", AddressingMode::RelativeAddressing, 0x10); + test_opcode!(bcc, "BCC", AddressingMode::RelativeAddressing, 0x90); + test_opcode!(bcs, "BCS", AddressingMode::RelativeAddressing, 0xB0); + + test_opcode!(jsr, "JSR", AddressingMode::Absolute, 0x20); + test_opcode!(rts, "RTS", AddressingMode::Implied, 0x60); + + test_opcode!(ror_zero_page, "ROR", AddressingMode::ZeroPage, 0x66); + test_opcode!(ror_absolute, "ROR", AddressingMode::Absolute, 0x6E); + test_opcode!( + ror_accumulator, + "ROR", + AddressingMode::AccumulatorAddressing, + 0x6A + ); + + test_opcode!(rol_zero_page, "ROL", AddressingMode::ZeroPage, 0x26); + test_opcode!( + rol_zero_page_x, + "ROL", + AddressingMode::ZeroPageIndexedX, + 0x36 + ); + test_opcode!( + rol_absolute_x, + "ROL", + AddressingMode::AbsoluteIndexedX, + 0x3E + ); + test_opcode!(rol_absolute, "ROL", AddressingMode::Absolute, 0x2E); + test_opcode!( + rol_accumulator, + "ROL", + AddressingMode::AccumulatorAddressing, + 0x2A + ); } diff --git a/src/utils.rs b/src/utils.rs index 76b17a6..77daa68 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,3 @@ -pub fn little_endian_to_u16(big: u8, little: u8) -> u16 { - (little as u16) << 8 | (big as u16) +pub fn little_endian_to_u16(bigger: u8, little: u8) -> u16 { + u16::from(bigger) << 8 | u16::from(little) } diff --git a/sys/wozmon.bin b/sys/wozmon.bin new file mode 100644 index 0000000..7280907 Binary files /dev/null and b/sys/wozmon.bin differ diff --git a/tests/cpu_test.rs b/tests/cpu_test.rs new file mode 100644 index 0000000..ea4ee07 --- /dev/null +++ b/tests/cpu_test.rs @@ -0,0 +1,137 @@ +extern crate mos6502; + +#[test] +fn test_simple_program() { + let mut cpu = mos6502::cpu::CPU::new(); + + let program = "LDA #$c0 + TAX + INX + ADC #$c4"; + + let bytes = mos6502::asm::assemble(&program); + cpu.load(&bytes, 0x600); + + for _ in 0..4 { + cpu.step(); + } + + assert_eq!(cpu.a, 0x84); + assert_eq!(cpu.x, 0xc1); + assert_eq!(cpu.y, 0x00); + assert_eq!(cpu.sp, 0xff); + assert_eq!(cpu.pc, 0x0606); + assert_eq!(cpu.p, 0b1011_0001); +} + +#[test] +fn test_simple_program_2() { + let mut cpu = mos6502::cpu::CPU::new(); + + let program = "LDA #$80 + STA $01 + ADC $01"; + + let bytes = mos6502::asm::assemble(&program); + cpu.load(&bytes, 0x600); + + for _ in 0..3 { + cpu.step(); + } + + assert_eq!(cpu.a, 0x00); + assert_eq!(cpu.x, 0x00); + assert_eq!(cpu.y, 0x00); + assert_eq!(cpu.sp, 0xff); + assert_eq!(cpu.pc, 0x0606); + assert_eq!(cpu.p, 0b0111_0011); +} + +#[test] +fn sta_with_absolute_addressing() { + let mut cpu = mos6502::cpu::CPU::new(); + + let program = "LDA #$80 + STA $FDE8"; + + let bytes = mos6502::asm::assemble(&program); + cpu.load(&bytes, 0x600); + + for _ in 0..2 { + cpu.step(); + } + + assert_eq!(cpu.a, 0x80); + assert_eq!(cpu.x, 0x00); + assert_eq!(cpu.y, 0x00); + assert_eq!(cpu.sp, 0xff); + assert_eq!(cpu.pc, 0x0605); + assert_eq!(cpu.p, 0b1011_0000); +} + +#[test] +fn program_cycle_with_branching() { + let mut cpu = mos6502::cpu::CPU::new(); + + /* + x = 5 + y = 0 + while x != 0 { + x -= 1 + y += 1 + } + */ + let program = "LDX #$5 + LDY #$0 + DEX + INY + CPX #$0 + BNE #$FA"; + + let bytes = mos6502::asm::assemble(&program); + cpu.load(&bytes, 0x800); + + for _ in 0..22 { + cpu.step(); + } + + assert_eq!(cpu.a, 0x00); + assert_eq!(cpu.x, 0x00); + assert_eq!(cpu.y, 0x05); + assert_eq!(cpu.sp, 0xff); + assert_eq!(cpu.pc, 0x80A); + assert_eq!(cpu.p, 0b0011_0011); +} + +#[test] +fn program_with_stack() { + let mut cpu = mos6502::cpu::CPU::new(); + + let program = "LDX #$00 + LDY #$00 + TXA + STA $0200 + PHA + INX + INY + CPY #$10 + BNE #$F5 + PLA + STA $0200 + INY + CPY #$20 + BNE #$F7"; + + cpu.load(&mos6502::asm::assemble(&program), 0x800); + + for _ in 0..194 { + cpu.step(); + } + + assert_eq!(cpu.a, 0x00); + assert_eq!(cpu.x, 0x10); + assert_eq!(cpu.y, 0x20); + assert_eq!(cpu.sp, 0xff); + assert_eq!(cpu.pc, 0x818); + assert_eq!(cpu.p, 0b0011_0011); +}