Skip to content

Latest commit

ย 

History

219 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

NanoCore: An 8-bit CPU Emulator

NanoCore is a meticulously crafted emulator for a custom 8-bit CPU. Designed with extreme minimalism in mind, this CPU operates within a strict 256-byte memory space, with all registers, the Program Counter (PC), and the Stack Pointer (SP) being 8-bit.

This project serves as an educational exercise in understanding the fundamental principles of computer architecture, low-level instruction set design, memory management under severe constraints, and assembly language programming.

Website: nanocore.afaan.dev ย |ย  DeepWiki: AfaanBilal/NanoCore

NanoCore TUI


โœจ Key Features

  • True 8-bit Architecture: All general-purpose registers (R0โ€“R15), Program Counter (PC), and Stack Pointer (SP) are 8-bit.
  • 256-byte Memory: The entire addressable memory space is limited to 256 bytes (0x00 to 0xFF).
  • Variable-Length Instruction Set: 1-byte, 2-byte, and 3-byte instructions to maximize opcode efficiency within the limited address space.
  • Modular Design: CPU cycle broken down into distinct Fetch, Decode, and Execute phases.
  • Inbuilt Two-Pass Assembler: Write NanoCore Assembly (.nca) instead of raw machine code.
  • Terminal User Interface: Fully functional TUI with breakpoints for interactive debugging.
  • Typed Error Handling: Stack overflow/underflow, division by zero, invalid operands, and invalid opcodes all surface as structured Rust errors.

๐Ÿ“ฆ Installation

Option 1 โ€” Compiled Binaries

Download pre-built binaries for your platform from the GitHub Releases page.

Option 2 โ€” cargo install

cargo install nanocore

Option 3 โ€” Build from Source

Requires Rust (stable).

git clone https://github.com/AfaanBilal/NanoCore.git
cd NanoCore
cargo build --release

๐Ÿš€ Usage

Run a program

# Run a pre-assembled binary
cargo run -- programs/test.ncb

# Auto-assemble and run a .nca source file
cargo run -- programs/fib.nca

# Print CPU state after each cycle
cargo run -- programs/test.nca -s

# Print each instruction as it executes
cargo run -- programs/test.nca -i

Assemble to binary

cargo run --bin nca -- -i example.nca -o example.ncb

Launch the TUI debugger

cargo run --bin tui -- programs/counter.nca

Run the test suite

cargo test

๐Ÿงฎ Architecture

Component Details
Registers R0โ€“R15, all 8-bit
Program Counter 8-bit (0x00โ€“0xFF)
Stack Pointer 8-bit (stack: 0xEBโ€“0xFF; 0xEA is the overflow guard)
Flags Zero (Z), Carry (C), Negative (N)
Memory 256 bytes total
Stack size 21 bytes (0xEBโ€“0xFF)
Max cycles 1024 per run

Memory Map

All 256 bytes are one flat, unprotected address space. The layout below is convention โ€” nothing enforces it.

Range Size Purpose
0x00โ€“0xA9 170 bytes Program and data โ€” programs load at 0x00 by default
0xAAโ€“0xE9 64 bytes Screen โ€” the TUI debugger renders this as an 8ร—8 grid
0xEA 1 byte Stack overflow guard โ€” never written
0xEBโ€“0xFF 21 bytes Stack

Stack Model

The stack grows downward from the top of memory. SP starts at 0xFF and always points at the next free slot, so the value on top is at memory[SP + 1] and an empty stack is SP == 0xFF.

Instruction Effect
PUSH Rd memory[SP] = Rd, then SP -= 1
POP Rd SP += 1, then Rd = memory[SP]
CALL/CALLR Pushes the return address (PC + 2), then jumps
RET Pops into PC
SP = 0xFF          ; empty
PUSH R0            ; memory[0xFF] = R0,  SP = 0xFE
PUSH R1            ; memory[0xFE] = R1,  SP = 0xFD
POP  R2            ; SP = 0xFE,  R2 = memory[0xFE]  (the old R1)

PUSH on a full stack (SP == 0xEA) raises StackOverflow; POP on an empty one (SP == 0xFF) raises StackUnderflow. Both are checked before the access, so 0xEA is never written โ€” hence the guard byte.

Important

Return addresses and pushed data share the same 21 bytes. Nesting CALLs 21 deep exhausts the stack on its own, and an unbalanced PUSH inside a subroutine leaves RET popping a data byte and jumping to it.

Warning

Nothing protects the stack region. load_program only checks that the program fits in 256 bytes, so a program longer than 235 bytes overwrites the stack (and one over 170 bytes overwrites the screen). STORE/STR into 0xEBโ€“0xFF corrupts it silently โ€” there is no fault.


๐Ÿงฎ Instruction Set Architecture (ISA)

Tip

See programs/ for example programs and compiled binaries.

NanoCore features a small but complete instruction set across 7 categories.

Instruction Format

  • 1-byte: Opcode only.
  • 2-byte: Opcode + one 8-bit operand (register or address).
  • 3-byte: Opcode + two 8-bit operands (register + immediate or address).

Implemented Instructions

Opcode Bytes Mnemonic Description
0x00 1 HLT Halt execution
0x01 1 NOP No operation
0x02 3 LDI Rd val Load immediate val into Rd
0x03 3 LDA Rd addr Load from memory address into Rd
0x04 2 LDR Rd Rs Load from address in Rs into Rd
0x05 2 MOV Rd Rs Copy Rs into Rd
0x06 3 STORE addr Rd Store Rd into memory address
0x07 2 PUSH Rd Push Rd onto stack
0x08 2 POP Rd Pop top of stack into Rd
0x09 2 ADD Rd Rs Rd = Rd + Rs
0x0A 3 ADDI Rd val Rd = Rd + val
0x0B 2 SUB Rd Rs Rd = Rd - Rs
0x0C 3 SUBI Rd val Rd = Rd - val
0x0D 2 INC Rd Rd = Rd + 1
0x0E 2 DEC Rd Rd = Rd - 1
0x0F 2 AND Rd Rs Rd = Rd & Rs
0x10 2 OR Rd Rs Rd = Rd | Rs
0x11 2 XOR Rd Rs Rd = Rd ^ Rs
0x12 2 NOT Rd Rd = ~Rd
0x13 2 CMP Rd Rs Set Z/N/C from Rd - Rs (no store; C=borrow if Rd < Rs)
0x14 2 SHL Rd Rd = Rd << 1 (C = old bit 7)
0x15 2 SHR Rd Rd = Rd >> 1 (C = old bit 0)
0x16 2 JMP addr Unconditional jump
0x17 2 JZ addr Jump if Zero flag set
0x18 2 JNZ addr Jump if Zero flag clear
0x19 2 PRINT Rd Print Rd as ASCII character
0x1A 2 MUL Rd Rs Rd = Rd * Rs
0x1B 3 MULI Rd val Rd = Rd * val
0x1C 2 DIV Rd Rs Rd = Rd / Rs
0x1D 3 DIVI Rd val Rd = Rd / val
0x1E 2 MOD Rd Rs Rd = Rd mod Rs
0x1F 3 MODI Rd val Rd = Rd mod val
0x20 2 CALL addr Call subroutine (push return address)
0x21 1 RET Return from subroutine
0x22 2 ROL Rd Rotate Rd left by 1 (C = old bit 7)
0x23 2 ROR Rd Rotate Rd right by 1 (C = old bit 0)
0x24 2 IN Rd Read byte from stdin into Rd
0x25 2 JMPR Rd Jump to address in Rd
0x26 2 CALLR Rd Call subroutine at address in Rd
0x27 2 STR Rd Rs Store Rd to address held in Rs

Tip

All arithmetic is wrapping. R0 = 0x00, R1 = 0x01, ..., R15 = 0x0F.

Opcodes 0x28โ€“0xFF are unassigned. Executing one raises EmulatorError::InvalidOpcode, reporting the byte and the PC โ€” so running off the end of your code into a .DB block or jumping to a bad address fails loudly instead of drifting through it.

Flag Effects

The rule: an instruction that writes a register sets Z and N; one that writes memory or the stack sets nothing. CMP is the only exception โ€” it sets flags without writing a register, which is its entire job.

Z is set when the result is 0x00, N from bit 7 of the result. C is written only where marked โ€” every other instruction leaves the previous C intact.

Instruction Z C N Notes
LDI, LDA, LDR, MOV, IN, POP โœ” โ€” โœ” From the loaded / popped value
STORE, STR, PUSH โ€” โ€” โ€” Write memory โ€” no register result to flag
INC, DEC โœ” โ€” โœ” Wraps silently โ€” no C on 0xFF โ†’ 0x00
ADD, ADDI, SUB, SUBI โœ” โœ” โœ” C = carry-out (ADD) / borrow (SUB)
MUL, MULI โœ” โœ” โœ” C set when the product exceeds 0xFF
DIV, DIVI, MOD, MODI โœ” โœ” โœ” C always cleared โ€” 8-bit division cannot overflow
AND, OR, XOR, NOT โœ” โ€” โœ”
CMP โœ” โœ” โœ” From Rd - Rs; C = borrow. No register written
SHL, ROL โœ” โœ” โœ” C = old bit 7
SHR, ROR โœ” โœ” โœ” C = old bit 0
JMP, JMPR, JZ, JNZ โ€” โ€” โ€” JZ / JNZ read Z, never write it
CALL, CALLR, RET โ€” โ€” โ€”
PRINT โ€” โ€” โ€”
HLT, NOP โ€” โ€” โ€”

Note

Loads set Z/N, so a MOV or LDI between a CMP and a JZ clobbers the comparison. The flip side is useful: since there is no TST, MOV Rd Rs doubles as a test-for-zero on Rs.

Tip

PUSH and STORE preserve flags, so a subroutine prologue can save registers without destroying the caller's comparison. POP does not โ€” it loads a register.


๐Ÿ› ๏ธ Assembly Language

NanoCore Assembly (.nca) files are plain text. The assembler performs two passes โ€” first to map labels and constants, then to emit bytecode.

Syntax

; Comment
.CONST MAX 10          ; Named constant
.DB 0x01 0x02 0x03     ; Embed raw bytes
.STRING "Hello"        ; Embed ASCII string (null-terminated)

start:                 ; Label
    LDI R0 0
    LDI R1 MAX         ; Use constant
loop:
    ADD R0 R1
    DEC R2
    JNZ loop
    HLT

Directives

Directive Description
.CONST name val Define a named constant
.DB byte ... Embed raw bytes at current position
.STRING "text" Embed a null-terminated ASCII string

๐Ÿ“‚ Code Structure

File Description
src/cpu.rs CPU state โ€” registers, PC, SP, memory, flags
src/nanocore.rs Main emulator โ€” load, run, cycle, fetch/decode/execute
src/assembler.rs Two-pass assembler core
src/lib.rs Library exports and Op enum (instruction set)
src/error.rs Typed error definitions
src/bin/nca.rs nca assembler binary
src/bin/tui.rs tui debugger binary entry point
src/tui/ TUI implementation (ratatui)
programs/ Example .nca source files and .ncb binaries

๐Ÿ“ Example Program โ€” Fibonacci Sequence

; Print the fibonacci sequence (two-digit)
start:
    LDI R0 0
    LDI R1 1
    LDI R2 12
    LDI R12 32
loop:
    JMP print_digits

post_print:
    MOV R3 R1
    ADD R1 R0
    MOV R0 R3
    DEC R2
    JNZ loop
end:
    HLT

print_digits:
    PUSH R10
    PUSH R11
    MOV R10 R0
    DIVI R10 10
    JZ unit_digit
    ADDI R10 48
    PRINT R10
unit_digit:
    MOV R11 R0
    MODI R11 10
    ADDI R11 48
    PRINT R11
print_space:
    PRINT R12
    POP R11
    POP R10
    JMP post_print

๐Ÿค Contributing

All contributions are welcome. Please create an issue first for any feature request or bug. Then fork the repository, create a branch, make your changes, and open a pull request.


๐Ÿ“„ License

NanoCore is released under the MIT License. See LICENSE for details.

About

NanoCore 8-bit CPU emulator in Rust

Topics

Resources

Stars

290 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages