The companion repo for this post can be found here:
This is a post detailing my progress on building an Emulator in Rust. This isn't something I'm building from scratch, but in pursuit of learning rust, and finding something interesting and approachable to work on I came across this NES Ebook.
This post is going to have notes about both learning Rust, setting up the project as well as resources and notes about the NES architecture, which apart from a lifelong love of this platform, the underlying system components are a new venture. Unlike the author, most of my working experience is in Full Stack Javascript development, and this blog and it's content is documentation of my journey "closer to the metal" and deeper into distributed systems.
Goals:
- Better familiarity with Rust and WASM
- Distributed Systems content for this blog
- Build a web based NES emulator
I'm not going to regurgitate the points in the NES Ebook, it's pretty clearly easy to follow. Instead, however, I want to point out the pieces I found interesting or want to expand on. Namely where I can go a little deeper into rust or software architecture points, since that's what this blog is primarily focused with.
Where I want to expand upon the content in the Rust book is primarily testing, project structure and porting the emulator to run in the browser with web assembly.
Notes on The NES. The NES is a distributed system.
What's interesting is that CPU, PPU, and APU are independent of each other. This fact makes NES a distributed system in which separate components have to coordinate to generate one seamless gaming experience.
With every company becoming software, any process that can be moved to software, will be. With computing systems growing in complexity, modern applications no longer run in isolation. The vast majority of products and applications rely on distributed systems
Getting Started
- Bootstrapping the application
$ mkdir rustnes && cd rustnes
$ cargo init
- Project Module heirarchy basics
$ tree .
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── apu
│ │ └── mod.rs
│ ├── bus
│ │ └── mod.rs
│ ├── cpu
│ │ └── mod.rs
│ ├── joypads
│ │ └── mod.rs
│ ├── main.rs
│ ├── ppu
│ │ └── mod.rs
│ └── rom
│ └── mod.rs
CPU Implementation
NES implements typical von Neumann architecture: both data and the instructions are stored in memory. The executed code is data from the CPU perspective, and any data can potentially be interpreted as executable code. There is no way CPU can tell the difference. The only mechanism the CPU has is a
program_counter
register that keeps track of a position in the instructions stream.
NES CPU can address 65536 memory cells.
NES CPU uses Little-Endian addressing rather than Big-Endian: That means that the 8 least significant bits of an address will be stored before the 8 most significant bits.
There are no opcodes that occupy more than 3 bytes. CPU instruction size can be either 1, 2, or 3 bytes.
Relevant resources for this section: 6502 Instruction Reference 6502 OpCode tutorial Emulating the CPU
The CPU works in a constant cycle:
- Fetch next execution instruction from the instruction memory
- Decode the instruction
- Execute the Instruction
- Repeat the cycle
CPU Registers
There are 7 registers on the NES CPU, the 6502 Instruction Reference
Program Counter (PC)
The Program Counter holds the address for the next machine language instruction to be executed.
Stack Pointer
The stack pointer holds the address of the top of the memory space allocated for the stack. The NES Stack has 255 address alotted to it, from 0x0100 to 0x1FF.
Accumulator
This Register stores the result of arithmetic, logic and memory access operations. It's used as an input parameter for some operations
Index Register X (X)
Used as an offset in specific memory addressing modes. Can be used for auxillary storage needs such as holding temporary values or being used as a counter.
Index Register Y (Y)
similar use case as X.
Processor Status (P)
8-bit regsiter represents 7 status flags that can be set or unset depending on the result of the last executed instruction.
As instructions are executed a set of processor flags are set or clear to record the results of the operation. This flags and some additional control flags are held in a special status register. Each flag has a single bit within the register.
Flags
///
/// 7 6 5 4 3 2 1 0
/// N V _ B D I Z C
/// | | | | | | +--- Carry Flag
/// | | | | | +----- Zero Flag
/// | | | | +------- Interrupt Disable
/// | | | +--------- Decimal Mode (not used on NES)
/// | | +----------- Break Command
/// | +--------------- Overflow Flag
/// +----------------- Negative Flag
///
(C) Carry Flag
The carry flag is set if the last operation caused an overflow from bit 7 of the result or an underflow from bit 0. This condition is set during arithmetic, comparison and during logical shifts. It can be explicitly set using the Set Carry Flag instruction and cleared with Clear Carry Flag.
(Z) Zero Flag
The zero flag is set if the result of the last operation as was zero.
(I) Interrupt Disable
The interrupt disable flag is set if the program has executed a Set Interrupt Disable instruction. While this flag is set the processor will not respond to interrupts from devices until it is cleared by a Clear Interrupt Disable instruction.
(D) Decimal Mode
According to NESDEV wiki this is not used in the NES
While the decimal mode flag is set the processor will obey the rules of Binary Coded Decimal (BCD) arithmetic during addition and subtraction. The flag can be explicity set using Set Decimal Flag and cleared with Clear Decimal Flag
(B) Break Command
The break command bit is set when a BRK instruction has been executed and an interrupt has been generated to process it.
(V) oVerflow Flag
The overflow flag is set during arithmetic operations if the result has yielded an invalid 2's complement result (e.g. adding to positive numbers and ending up with a negative result: 64 + 64 => -128). It is determined by looking at the carry between bits 6 and 7 and between bit 7 and the carry flag.
(N) Negative Flag
The negative flag is set if the result of the last operation had bit 7 set to a one.
Adressing Modes
In short, the addressing mode is a property of an instruction that defines how the CPU should interpret the next 1 or 2 bytes in the instruction stream.
Code Highlights
Getting started
mod cpu;
pub struct CPU {
pub register_a: u8,
pub status: u8,
pub program_counter: u16,
}
impl CPU {
pub fn new() -> Self {
CPU {
register_a: 0,
status: 0,
program_counter: 0,
}
}
pub fn interpret(&mut self, program: Vec<u8>) {
self.program_counter = 0;
loop {
let opscode = program[self.program_counter as usize];
self.program_counter += 1;
match opscode {
_ => todo!("ops code todos")
}
}
}
}
Rust References
Rust Feature Highlights that were interesting along the way Rust Todo Macro