NanoCore: An 8-bit CPU Emulator

August 10, 2026 · View on GitHub

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

ComponentDetails
RegistersR0–R15, all 8-bit
Program Counter8-bit (0x00–0xFF)
Stack Pointer8-bit (stack: 0xEB–0xFF; 0xEA is the overflow guard)
FlagsZero (Z), Carry (C), Negative (N)
Memory256 bytes total
Stack size21 bytes (0xEB–0xFF)
Max cycles1024 per run

Memory Map

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

RangeSizePurpose
0x000xA9170 bytesProgram and data — programs load at 0x00 by default
0xAA–`0xE9$64 \text{bytes}\text{Screen} — \text{the} \text{TUI} \text{debugger} \text{renders} \text{this} \text{as} \text{an} 8 \times 8 \text{grid}
$0xEA`1 byteStack overflow guard — never written
0xEB0xFF21 bytesStack

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.

InstructionEffect
PUSH Rdmemory[SP] = Rd, then SP -= 1
POP RdSP += 1, then Rd = memory[SP]
CALL/CALLRPushes the return address (PC + 2), then jumps
RETPops 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 0xEB0xFF 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

OpcodeBytesMnemonicDescription
0x001HLTHalt execution
0x011NOPNo operation
0x023LDI Rd valLoad immediate val into Rd
0x033LDA Rd addrLoad from memory address into Rd
0x042LDR Rd RsLoad from address in Rs into Rd
0x052MOV Rd RsCopy Rs into Rd
0x063STORE addr RdStore Rd into memory address
0x072PUSH RdPush Rd onto stack
0x082POP RdPop top of stack into Rd
0x092ADD Rd RsRd = Rd + Rs
0x0A3ADDI Rd valRd = Rd + val
0x0B2SUB Rd RsRd = Rd - Rs
0x0C3SUBI Rd valRd = Rd - val
0x0D2INC RdRd = Rd + 1
0x0E2DEC RdRd = Rd - 1
0x0F2AND Rd RsRd = Rd & Rs
0x102OR Rd RsRd = Rd | Rs
0x112XOR Rd RsRd = Rd ^ Rs
0x122NOT RdRd = ~Rd
0x132CMP Rd RsSet Z/N/C from Rd - Rs (no store; C=borrow if Rd < Rs)
0x142SHL RdRd = Rd << 1 (C = old bit 7)
0x152SHR RdRd = Rd >> 1 (C = old bit 0)
0x162JMP addrUnconditional jump
0x172JZ addrJump if Zero flag set
0x182JNZ addrJump if Zero flag clear
0x192PRINT RdPrint Rd as ASCII character
0x1A2MUL Rd RsRd = Rd * Rs
0x1B3MULI Rd valRd = Rd * val
0x1C2DIV Rd RsRd = Rd / Rs
0x1D3DIVI Rd valRd = Rd / val
0x1E2MOD Rd RsRd = Rd mod Rs
0x1F3MODI Rd valRd = Rd mod val
0x202CALL addrCall subroutine (push return address)
0x211RETReturn from subroutine
0x222ROL RdRotate Rd left by 1 (C = old bit 7)
0x232ROR RdRotate Rd right by 1 (C = old bit 0)
0x242IN RdRead byte from stdin into Rd
0x252JMPR RdJump to address in Rd
0x262CALLR RdCall subroutine at address in Rd
0x272STR Rd RsStore Rd to address held in Rs

Tip

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

Opcodes 0x280xFF 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.

InstructionZCNNotes
LDI, LDA, LDR, MOV, IN, POPFrom the loaded / popped value
STORE, STR, PUSHWrite memory — no register result to flag
INC, DECWraps silently — no C on 0xFF0x00
ADD, ADDI, SUB, SUBIC = carry-out (ADD) / borrow (SUB)
MUL, MULIC set when the product exceeds 0xFF
DIV, DIVI, MOD, MODIC always cleared — 8-bit division cannot overflow
AND, OR, XOR, NOT
CMPFrom Rd - Rs; C = borrow. No register written
SHL, ROLC = old bit 7
SHR, RORC = old bit 0
JMP, JMPR, JZ, JNZJZ / 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

DirectiveDescription
.CONST name valDefine a named constant
.DB byte ...Embed raw bytes at current position
.STRING "text"Embed a null-terminated ASCII string

📂 Code Structure

FileDescription
src/cpu.rsCPU state — registers, PC, SP, memory, flags
src/nanocore.rsMain emulator — load, run, cycle, fetch/decode/execute
src/assembler.rsTwo-pass assembler core
src/lib.rsLibrary exports and Op enum (instruction set)
src/error.rsTyped error definitions
src/bin/nca.rsnca assembler binary
src/bin/tui.rstui 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.