# How NanoCore Handles Its 256-Byte Memory Constraint and Addressing Architecture

> Discover how NanoCore manages its 256-byte memory limit using u8 addresses, a fixed RAM array, and wrapping arithmetic. Learn about its addressing architecture.

- Repository: [Afaan Bilal/nanocore](https://github.com/afaanbilal/nanocore)
- Tags: deep-dive
- Published: 2026-02-23

---

**NanoCore enforces its 256-byte memory limit by storing all addresses as `u8` values and declaring RAM as a fixed `[u8; 256]` array, using wrapping arithmetic to handle overflow and explicit bounds checking for stack and program loading operations.**

NanoCore is a minimal 8-bit CPU emulator implemented in Rust that operates within a deliberately constrained **256-byte memory space**. Every addressable component—from the program counter to register indices—uses 8-bit unsigned integers, ensuring all memory references naturally wrap at the `0xFF` boundary. This design choice simplifies the architecture while enforcing strict memory safety through Rust's type system and explicit bounds checks.

## The Foundation: Fixed 256-Byte Memory Array

The memory constraint originates in the core CPU structure defined in [`src/cpu.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/cpu.rs). The `CPU` struct declares a fixed-size array that physically cannot exceed 256 bytes, alongside 8-bit address registers:

```rust
/// src/cpu.rs
pub struct CPU {
    // ...
    pub memory: [u8; 256],   // 256-byte RAM
    pub pc: u8,              // 8-bit program counter
    pub sp: u8,              // 8-bit stack pointer
    // ...
}

```

Because `pc` and `sp` are `u8` types, they can only represent values `0x00` through `0xFF`. This hardware-enforced limit means that even if arithmetic overflows, the value wraps naturally within the valid address space.

## Program Loading with Overflow Protection

Before execution begins, NanoCore validates that the program fits within the 256-byte constraint. The `load_program` method in [`src/nanocore.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/nanocore.rs) checks that the combined size of the program and its start address does not exceed the memory array bounds:

```rust
/// src/nanocore.rs
pub fn load_program(&mut self, program: &[u8], start_address: u8) -> crate::EmulatorResult<()> {
    if (start_address as usize + program.len()) > 256 {
        return Err(crate::EmulatorError::ProgramTooLarge { /* ... */ });
    }
    for (i, &byte) in program.iter().enumerate() {
        self.cpu.memory[start_address.wrapping_add(i as u8) as usize] = byte;
    }
    self.cpu.pc = start_address;
    Ok(())
}

```

The use of `wrapping_add` ensures that if a program loads near the end of memory, the index rolls over to `0x00` rather than causing a panic or out-of-bounds access.

## Fetch-Decode-Execute Cycle and PC Wrapping

During each instruction cycle, NanoCore reads three consecutive bytes from memory using the program counter. Because the PC is a `u8`, all increment operations use wrapping arithmetic to maintain the 256-byte boundary:

```rust
/// src/nanocore.rs
let opcode = self.cpu.memory[self.cpu.pc as usize];
let byte_2 = self.cpu.memory[self.cpu.pc.wrapping_add(1) as usize];
let byte_3 = self.cpu.memory[self.cpu.pc.wrapping_add(2) as usize];
// ...
if !pc_override {
    self.cpu.pc = self.cpu.pc.wrapping_add(op.instruction_len());
}

```

This design guarantees that execution flows seamlessly from address `0xFF` back to `0x00`, creating a circular memory space without additional boundary checks during the fetch phase.

## Stack Operations Within Bounds

The stack pointer operates within a constrained range defined by `STACK_MIN` (typically `0xEA`) and `STACK_MAX` (`0xFF`). Push and pop operations explicitly validate these bounds before accessing memory:

```rust
/// src/nanocore.rs – PUSH operation
if self.cpu.sp == CPU::STACK_MIN {
    return Err(crate::EmulatorError::StackOverflow { sp: self.cpu.sp });
}
self.cpu.memory[self.cpu.sp as usize] = value;
self.cpu.sp = self.cpu.sp.wrapping_sub(1);

```

```rust
/// src/nanocore.rs – POP operation
if self.cpu.sp == CPU::STACK_MAX {
    return Err(crate::EmulatorError::StackUnderflow { sp: self.cpu.sp });
}
self.cpu.sp = self.cpu.sp.wrapping_add(1);
let value = self.cpu.memory[self.cpu.sp as usize];

```

These checks prevent stack corruption while the `wrapping_add` and `wrapping_sub` operations ensure the SP remains a valid `u8` value.

## Register-Indirect Addressing

Instructions such as `LDR`, `STR`, `LDA`, and `STORE` use registers to hold memory addresses. Since registers are defined as `u8` values, they inherently cannot request addresses outside the 256-byte range:

```rust
/// src/nanocore.rs – LDR example
let addr = self.cpu.registers[rs as usize];
let value = self.cpu.memory[addr as usize];
self.cpu.registers[rd as usize] = value;

```

The cast from `u8` to `usize` for array indexing is always safe because the maximum value (`255`) fits comfortably within the 256-element array bounds.

## Error Handling for Memory Violations

When operations violate the 256-byte constraint, NanoCore returns specific errors defined in [`src/error.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/error.rs):

```rust
/// src/error.rs
/// The 8-bit architecture limits total addressable memory to 256 bytes.
ProgramTooLarge { size: usize, start: u8, max: usize },

```

Additional error variants handle stack violations and invalid memory accesses, providing clear diagnostics when the emulator encounters boundary conditions.

## Summary

NanoCore maintains its strict 256-byte memory constraint through a combination of Rust type safety and explicit architectural decisions:

- **Fixed-size array**: The `memory` field in `CPU` is declared as `[u8; 256]`, physically preventing expansion.
- **8-bit addressing**: All pointers (`pc`, `sp`, registers) use `u8` types, naturally limiting values to `0x00..=0xFF`.
- **Wrapping arithmetic**: Operations use `wrapping_add` and `wrapping_sub` to handle overflow gracefully without panics.
- **Bounds validation**: Program loading and stack operations explicitly check limits before accessing memory.
- **Type-safe indexing**: Casts from `u8` to `usize` for array access are always safe given the 256-byte constraint.

## Frequently Asked Questions

### How does NanoCore prevent programs from exceeding the 256-byte memory limit?

NanoCore validates program size during the loading phase in [`src/nanocore.rs`](https://github.com/afaanbilal/nanocore/blob/main/src/nanocore.rs). The `load_program` method checks if `start_address + program.len()` exceeds 256 bytes, returning `EmulatorError::ProgramTooLarge` if the program would overflow the fixed `[u8; 256]` array.

### Why does NanoCore use wrapping arithmetic for the program counter?

The program counter (`pc`) is defined as a `u8`, which naturally overflows at 255. NanoCore uses `wrapping_add` when incrementing the PC to ensure that execution flows seamlessly from address `0xFF` back to `0x00`, creating a circular memory space without requiring additional boundary checks or risking panic conditions.

### What happens when the stack pointer reaches its minimum or maximum bounds?

NanoCore defines `STACK_MIN` (typically `0xEA`) and `STACK_MAX` (`0xFF`) in the `CPU` struct. When executing `PUSH`, the emulator checks if `sp == STACK_MIN` and returns `StackOverflow` if true. For `POP`, it checks `sp == STACK_MAX` and returns `StackUnderflow` if violated, preventing corruption outside the designated stack region.

### How does register-indirect addressing stay within the 256-byte limit?

Registers in NanoCore are `u8` values, meaning they can only hold addresses between `0x00` and `0xFF`. When instructions like `LDR` or `STR` use a register as a memory pointer, the value is cast to `usize` for array indexing. Since the maximum register value (255) is always less than the memory array length (256), this operation is guaranteed safe without additional bounds checking.