Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overview

MB8 is an 8-bit microcomputer in the spirit of the ZX Spectrum and Commodore 64, initially inspired by CHIP-8. It ships with a tiny CP/M-like operating system layer and a minimal assembly-first toolchain.

What’s inside

  • 8-bit CPU with a compact ISA and pseudo-instructions for convenience.
  • Memory-mapped devices (RAM, ROM, GPU TTY, keyboard, disk) wired through a simple bus.
  • A small kernel plus user-space programs, all written in assembly.

Running the project

  1. Build all assembly artifacts (kernel, user programs, tests):
make all
  1. Run the VM, passing the kernel entrypoint first and then any user-space programs:
cargo run -- run ./kernel/main.bin ./user/sh.bin ./user/hw.bin ./user/ls.bin ./user/exit.bin ./user/help.bin

The kernel image is loaded at 0xE000, user programs are passed as extra binaries, and the OS provides basic CP/M-like services via syscalls.

Memory model

The MB8 VM exposes a single 64 KiB address space. All reads and writes go through the bus (crates/mb8/src/dev/bus.rs), which forwards them to RAM, ROM, or an MMIO device based on the address range.

Layout

RangeSizeDescription
0x00000xBFFF48 KiBRAM
0xC0000xDFFF8 KiBReserved MMIO (not wired yet)
0xE0000xEFFF4 KiBROM
0xF0000xF0FF256 BGPU registers
0xF1000xF1FF256 BKeyboard registers
0xF2000xF3FF512 BDisk registers and buffer
0xF4000xFFFF3072 BReserved MMIO (not wired yet)

The bus rejects the reserved regions with unimplemented!().

Bus

  • CPU memory accesses always call into the bus, which in turn calls the matching device read/write.
  • Devices own their buffers; the bus itself does not store data.

RAM (crates/mb8/src/dev/ram.rs)

  • Plain byte-addressable memory. Writes update the backing array; reads return what was last written.
  • RAM_SIZE = 0xC000. The stack grows downward (STACK_TOP = 0xBFFF, STACK_BOTTOM = 0xBF00).

ROM (crates/mb8/src/dev/rom.rs)

  • Backing store for program code (ROM_SIZE = 0x1000).
  • The device currently accepts writes from the bus, but programs should not rely on mutating ROM; this may be blocked in the future. ROM is meant to hold the kernel/boot image.

GPU (crates/mb8/src/dev/gpu.rs)

  • Registers live at 0xF000 (offsets relative to that base):
    • 0x0000 — mode register. 0x00 = off, 0x01 = TTY.
    • 0x0001 — TTY data register. When mode is TTY, each write pushes a character to the screen and advances the cursor.
  • Reading 0x0000 returns the current mode. Other reads are currently unimplemented.

Keyboard (crates/mb8/src/dev/keyboard.rs)

  • Registers at 0xF100 (offsets relative to that base):
    • 0x00STATUS. Returns 1 when keys are queued, otherwise 0.
    • 0x01DATA. Reading pops the next key code from the queue; returns 0 when empty.
  • Writes are ignored.

Disk (crates/mb8/src/dev/disk.rs)

  • Registers at 0xF200 (offsets relative to that base):
    • 0x0000BLOCK number to operate on.
    • 0x0001CMD (0x00 no-op, 0x01 read, 0x02 write).
    • 0x00020x0102 — 256-byte disk buffer used for reads/writes.
  • CMD operations move data between the internal image and the buffer; buffer reads/writes go directly to the 256-byte window.

Register set

Small and simple; each VM context (judge or bot) gets its own copies.

register mask description size
R0 - R7 0x00 - 0x07 General-purpose registers 8 bits
SP 0x0D Stack pointer; PUSH / POP move it upward 8 bits
PC 0x0E Program counter; steps by 2 bytes per instruction 12 bits
F 0x0F Flags register (Z/N/C) 8 bits

Notes:

  • F is overwritten by arithmetic/logic/shift ops. Jumps read it, but most other ops leave it untouched.
  • Context switches keep register sets separate: judge and each bot maintain their own registers (PC, SP, R0-R7).
  • Masks 0x08 - 0x0C are reserved for future expansion.

Flags

These live in the F register and are rewritten by arithmetic/logic/shift instructions:

flag mask description set by
Z 0x01 Result is zero. ADD, SUB, AND, OR, XOR, SHL, SHR (and pseudo-instructions that expand to them)
N 0x02 Copies bit 7 (sign) of the 8-bit result. ADD, SUB, AND, OR, XOR, SHL, SHR
C 0x04 Set when an 8-bit result wraps: carry on ADD/SHL/SHR, borrow on SUB. ADD, SUB, SHL, SHR

Notes:

  • Instructions not listed leave flags unchanged.
  • Pseudo-instructions (INC, DEC, CMP, CMPI, shifts) inherit flag behavior from the underlying ops.
  • Flags 0x08, 0x10, 0x20, 0x40, 0x80 are reserved for future use.

Instruction format

Every opcode is 16 bits wide (0xABCD):

  • A — instruction group.
  • B — sub-opcode or register nibble.
  • C — usually a register or the upper 4 bits of an address.
  • D — usually a register or the lower 4 bits of an address.

Example, ADD R0, R1 encodes as 0x1101:

0001 0001 0000 0001

Jump/load/store instructions treat XXX in 0xYXXX as a 12-bit address, covering the full 4 KiB memory bank.

Instruction Set

System instructions

NOP

Syntax:

NOP

Args: None

Encoding:

0000 0000 0000 0000

Hex: 0x0000

Flags: None

Description: Do nothing for one instruction cycle.


HALT

Syntax:

HALT

Args: None

Encoding:

0000 0001 0000 0000

Hex: 0x0100

Flags: None

Description: Stop the VM. Execution does not resume until a reset happens.


SYS

Syntax:

SYS

Args: None

Encoding:

0000 0010 0000 0000

Hex: 0x0200

Flags: None

Description: Enter the system call handler. Callers place the syscall ID in R0 and jump to 0xE500 (see system calls doc) to execute OS services.


Register-register instructions

MOV

Syntax:

MOV rD rS

Operation:

rD = rS

Args:

  • rD — destination register.
  • rS — source register.

Encoding:

0001 0000 DDDD SSSS

Hex: 0x10DS

Flags: None

Description: Copy the value from rS into rD.


ADD

Syntax:

ADD rD rS

Operation:

rD = rD + rS

Args:

  • rD — destination register.
  • rS — source register.

Encoding:

0001 0001 DDDD SSSS

Hex: 0x11DS

Flags: Updates Z, N, C.

Description: Add rS to rD.


SUB

Syntax:

SUB rD rS

Operation:

rD = rD - rS

Args:

  • rD — destination register.
  • rS — source register.

Encoding:

0001 0010 DDDD SSSS

Hex: 0x12DS

Flags: Updates Z, N, C.

Description: Subtract rS from rD.


AND

Syntax:

AND rD rS

Operation:

rD = rD & rS

Encoding:

0001 0011 DDDD SSSS

Hex: 0x13DS

Flags: Updates Z, N.

Description: Bitwise AND.


OR

Syntax:

OR rD rS

Operation:

rD = rD | rS

Encoding:

0001 0100 DDDD SSSS

Hex: 0x14DS

Flags: Updates Z, N.

Description: Bitwise OR.


XOR

Syntax:

XOR rD rS

Operation:

rD = rD ^ rS

Encoding:

0001 0101 DDDD SSSS

Hex: 0x15DS

Flags: Updates Z, N.

Description: Bitwise XOR.


SHR

Syntax:

SHR rD rS

Operation:

rD = rD >> rS

Encoding:

0001 0110 DDDD SSSS

Hex: 0x16DS

Flags: Updates Z, N; C is set only when the shifted result overflows 8 bits.

Description: Logical right shift by the amount in rS.


SHL

Syntax:

SHL rD rS

Operation:

rD = rD << rS

Encoding:

0001 0111 DDDD SSSS

Hex: 0x17DS

Flags: Updates Z, N, C.

Description: Logical left shift by the amount in rS.


CMP

Syntax:

CMP rD rS

Operation:

flags = rD - rS

Encoding:

0001 1000 DDDD SSSS

Hex: 0x18DS

Flags: Updates Z, N, C.
Description: Compare two registers and set flags as if subtracting rS from rD. Register values are not modified.

Register-immediate instructions

LDI

Syntax:

LDI rD imm8

Operation:

rD = imm8

Args:

  • rD — destination register.
  • imm8 — unsigned 8-bit immediate value.

Encoding:

0010 DDDD XXXX XXXX

Hex: 0x20DX

Flags: None.

Description: Load an 8-bit immediate into rD.

Jump instructions

JMP

Syntax:

JMP rH rL

Operation:

PC = (rH << 8) | rL

Args:

  • rH — register containing the high byte of the absolute address.
  • rL — register containing the low byte of the absolute address.

Encoding:

0011 0000 HHHH LLLL

Hex: 0x30HL

Flags: None.

Description: Absolute jump using two registers to form a 16-bit address.


JR

Syntax:

JR off8

Operation:

PC = PC + sign_extend(off8)

Args:

  • off8 — signed 8-bit offset.

Encoding:

0011 0001 OOOO OOOO

Hex: 0x31OO

Flags: None.

Description: Relative jump that always branches by the signed offset.


JZR

Syntax:

JZR off8

Operation:

if Z == 1 { PC = PC + sign_extend(off8) }

Encoding:

0011 0010 OOOO OOOO

Hex: 0x32OO

Flags: Reads Z.

Description: Relative jump taken only when the zero flag is set.


JNZR

Syntax:

JNZR off8

Operation:

if Z == 0 { PC = PC + sign_extend(off8) }

Encoding:

0011 0011 OOOO OOOO

Hex: 0x33OO

Flags: Reads Z.

Description: Relative jump taken only when the zero flag is clear.


JCR

Syntax:

JCR off8

Operation:

if C == 1 { PC = PC + sign_extend(off8) }

Encoding:

0011 0100 OOOO OOOO

Hex: 0x34OO

Flags: Reads C.

Description: Relative jump taken only when the carry flag is set.


JNCR

Syntax:

JNCR off8

Operation:

if C == 0 { PC = PC + sign_extend(off8) }

Encoding:

0011 0101 OOOO OOOO

Hex: 0x35OO

Flags: Reads C.

Description: Relative jump taken only when the carry flag is clear.


Stack instructions

CALL

Syntax:

CALL rH rL

Operation:

push(PC)
PC = (rH << 8) | rL

Args:

  • rH — high byte register.
  • rL — low byte register.

Encoding:

0100 0000 HHHH LLLL

Hex: 0x40HL

Flags: None.

Description: Push the current PC onto the stack and jump to the absolute address formed by rH/rL.


RET

Syntax:

RET

Args: None

Encoding:

0100 0001 0000 0000

Hex: 0x4100

Flags: None.

Description: Pop the return address from the stack and jump to it.


PUSH

Syntax:

PUSH rS

Args:

  • rS — register to push.

Encoding:

0100 0010 0000 SSSS

Hex: 0x420S

Flags: None.

Description: Decrement SP, store rS on the stack.


POP

Syntax:

POP rD

Args:

  • rD — destination register.

Encoding:

0100 0011 0000 DDDD

Hex: 0x430D

Flags: None.

Description: Load a byte from the stack to rD and increment SP.


Memory instructions

LD

Syntax:

LD rD rH rL

Operation:

rD = MEM[(rH << 8) | rL]

Args:

  • rD — destination register.
  • rH/rL — registers holding the high/low bytes of the source address.

Encoding:

0101 DDDD HHHH LLLL

Hex: 0x5DHL

Flags: None.

Description: Read one byte from RAM at the 16-bit address formed by rH/rL and place it in rD.


ST

Syntax:

ST rS rH rL

Operation:

MEM[(rH << 8) | rL] = rS

Args:

  • rS — source register.
  • rH/rL — registers holding the high/low bytes of the destination address.

Encoding:

0110 SSSS HHHH LLLL

Hex: 0x6SHL

Flags: None.

Description: Write one byte from rS to RAM at the 16-bit address composed from rH/rL.


Pseudo-instructions

Assembler-only helpers from asm/ext.asm. They rewrite into core opcodes and often use R7 plus the stack as scratch.


LDI (16-bit)

Syntax:

LDI rH rL imm16

Expands to:

LDI rH (imm16 >> 8)
LDI rL (imm16 & 0xFF)

Scratch: none
Flags: none
Description: Load a 16-bit immediate into two registers.


CALL (abs)

Syntax:

CALL addr16

Expands to:

LDI R6 (addr16 >> 8)
LDI R7 (addr16 & 0xFF)
CALL R6 R7

Scratch: uses R6, R7
Flags: none
Description: Absolute subroutine call to a 16-bit address.


JMP (abs)

Syntax:

JMP addr16

Expands to:

LDI R6 (addr16 >> 8)
LDI R7 (addr16 & 0xFF)
JMP R6 R7

Scratch: uses R6, R7
Flags: none
Description: Absolute jump to a 16-bit address.


JR (abs)

Syntax:

JR label

Expands to: relative JR with the computed offset.

Scratch: none
Flags: none
Description: Jump to a label using a computed relative offset (assembler checks the range).


JZR (abs)

Syntax:

JZR label

Expands to: relative JZR with the computed offset.

Scratch: none
Flags: reads Z
Description: Jump to a label when zero flag is set, using a computed offset.


JNZR (abs)

Syntax:

JNZR label

Expands to: relative JNZR with the computed offset.

Scratch: none
Flags: reads Z
Description: Jump to a label when zero flag is clear, using a computed offset.


ZERO

Syntax:

ZERO rD

Expands to:

LDI rD 0

Scratch: none
Flags: none
Description: Clear a register.


INC

Syntax:

INC rD

Expands to:

PUSH R7
LDI R7 1
ADD rD R7
POP R7

Scratch: uses R7, stack
Flags: from ADD (Z/N/C)
Description: Increment a register by one.


DEC

Syntax:

DEC rD

Expands to:

PUSH R7
LDI R7 1
SUB rD R7
POP R7

Scratch: uses R7, stack
Flags: from SUB (Z/N/C)
Description: Decrement a register by one.


INC16

Syntax:

INC16 rH rL

Expands to:

CMPI rL 0xFF
JZR inc_hi
INC rL
JR end
inc_hi:
LDI rL 0
INC rH
end:
NOP

Scratch: stack via INC
Flags: from CMPI, INC (Z/N/C)
Description: Increment a 16-bit register pair in-place.


NOT

Syntax:

NOT rD

Expands to:

PUSH R7
LDI R7 0xFF
XOR rD R7
POP R7

Scratch: uses R7, stack
Flags: from XOR (Z/N, clears C)
Description: Bitwise invert a register.


CMP

Syntax:

CMP rA rB

Expands to:

PUSH rA
SUB rA rB
POP rA

Scratch: stack
Flags: from SUB (Z/N/C)
Description: Compare two registers; flags reflect rA - rB, operands restored.


CMPI

Syntax:

CMPI rD imm

Expands to:

PUSH R7
LDI R7 imm
SUB R7 rD
POP R7

Scratch: uses R7, stack
Flags: from SUB (Z/N/C)
Description: Compare a register with an immediate; flags reflect imm - rD.


SHRI

Syntax:

SHRI rD imm

Expands to:

PUSH R7
LDI R7 imm
SHR rD R7
POP R7

Scratch: uses R7, stack
Flags: from SHR (Z/N/C)
Description: Shift right by an immediate count.


SHLI

Syntax:

SHLI rD imm

Expands to:

PUSH R7
LDI R7 imm
SHL rD R7
POP R7

Scratch: uses R7, stack
Flags: from SHL (Z/N/C)
Description: Shift left by an immediate count.


SWAP

Syntax:

SWAP rA rB

Expands to:

PUSH rA
MOV rA rB
POP rB

Scratch: stack
Flags: none
Description: Exchange the values of two registers.


MUL

Syntax:

MUL rD rA rB

Expands to:

ZERO rD
PUSH rB
iter:
    ADD rD rA
    DEC rB
    CMPI rB 0
    JNZR iter
POP rB

Scratch: uses rB, stack
Flags: from ADD, DEC, CMPI (Z/N/C)
Description: Unsigned multiply by repeated addition; destroys rB during the loop, restores it after.

Standard Library

Helper macros from asm/std.asm. Include them after cpu.asm/ext.asm to get simple data routines.

MEMCPY

  • Syntax: MEMCPY i len srchi srclo dsthi dstlo
  • Inputs: i loop counter (start at 0), len stop value, srchi:srclo source pointer, dsthi:dstlo destination pointer.
  • Behavior: Copies bytes from source to destination until i == len. Both pointers are incremented with INC16. Restores i each iteration via push/pop.
  • Scratch: uses stack to save i; flags from CMP, INC, INC16.

STRCMP

  • Syntax: STRCMP i j srchi srclo dsthi dstlo
  • Inputs: srchi:srclo first string pointer, dsthi:dstlo second string pointer.
  • Outputs: i receives 0 when strings match, 1 otherwise.
  • Behavior: Walks both zero-terminated strings byte-by-byte. Returns early on mismatch, or when a 0x00 terminator is reached on both sides.
  • Scratch: uses i, j, flags from CMP, CMPI, JZR/JNZR, and increments addresses with INC16.

System Calls

System calls live at 0xE500 (kernel/syscalls.asm). To invoke one, load the call ID into R0 and CALL 0xE500. Inputs and outputs travel through the registers listed below; all other registers are caller-saved.

  • 0x01 — SYS_GPU_MODE
    Input: R1 mode byte (0x00 off, 0x01 TTY). Writes the GPU mode register at 0xF000.

  • 0x02 — SYS_WRITE
    Input: R1 character byte. Sends it to the GPU TTY data register at 0xF001.

  • 0x03 — SYS_WRITELN
    Input: R1:R2 address of a zero-terminated string. Streams characters to the TTY data register until 0x00.

  • 0x04 — SYS_WAIT_FOR_KEY
    Blocks until the keyboard status register (0xF100) is non-zero. No outputs.

  • 0x05 — SYS_READ_KEY
    Output: R0 key code popped from the keyboard data register (0xF101). Returns 0 if the queue was empty.

  • 0x06 — SYS_DISK_SET_BLOCK
    Input: R1 block index. Stores it in the disk block register at 0xF200 for later operations.

  • 0x07 — SYS_DISK_READ_BLOCK
    Uses the previously selected block and copies it into the disk buffer window (0xF2020xF302).

  • 0x08 — SYS_DISK_WRITE_BLOCK
    Flushes the current disk buffer window into the previously selected block.

  • 0x09 — SYS_FS_LIST
    Input: R1:R2 destination buffer. Copies the directory block (block 0) from disk into RAM via MEMCPY.

  • 0x0A — SYS_FS_FIND
    Input: R1:R2 filename pointer.
    Output: R0 status (0 success, 1 not found), R1 block index, R2 file size.

  • 0x0B — SYS_FS_READ
    Input: R1:R2 filename pointer, R3:R4 destination buffer.
    Output: R0 status (0 success, 1 not found). On success it loads the file into the buffer using the disk buffer window.

  • 0x0C — SYS_FS_WRITE
    Currently unimplemented placeholder.

  • 0x0D — SYS_FS_DELETE
    Currently unimplemented placeholder.

  • 0x0E — SYS_EXEC
    Input: R1:R2 filename pointer. Loads the file into RAM at 0x1000 (user entry) and jumps to it.
    Output: R0 status (0 success, 1 not found).

  • 0x0F — SYS_EXIT
    No inputs. Returns control to the kernel entrypoint at 0xE000 (used by user programs to quit).

Assembler syntax

We assemble with customasm.

Writing a program for the VM

  • Always include ../asm/cpu.asm first (see user/sh.asm). It defines the memory banks so your ROM segment assembles with a base address of 0x1000.
  • When the program is launched, that ROM image is copied into RAM starting at 0x1000 and execution begins at your entry label.

Includes

  • Always include asm/cpu.asm to get the core ISA and register definitions.
  • Optionally include asm/ext.asm to unlock pseudo-instructions like INC, JMP addr, CMPI, etc.
#include "../asm/cpu.asm"
#include "../asm/ext.asm" ; optional

Banks and layout

  • Kernel: uses #bank rom (4 KiB at 0xE000) for executable code — see kernel/main.asm.
  • User programs: include cpu.asm and you automatically get the RAM bank defined there; you typically do not write your own #bank directives.
  • #addr <hex> — set the write pointer inside the active bank. Handy to place data at a specific RAM offset.

Example RAM data:

#addr 0x0100      ; place data in general RAM (RAM bank is already active)
SPRITE:
    #d8 0b1111_0000
    #d8 0b1000_1000

Example ROM code:

#bank rom         ; only in the kernel image
start:
    LDI R0 0x42
    HALT

Data and literals

  • #d8 emits bytes (comma-separated or one-per-line).
  • Labels mark addresses (label:). You can jump to labels or use them for data pointers.
  • Constants use NAME = value.
  • Literals: decimal, hex (0xFF), binary (0b1010_1010), or single-character strings ("A" stores ASCII).

Comments

  • Everything after ; on a line is ignored.

Building and running

  • Build: customasm file.asm → produces file.bin.
  • Run: cargo run -- run file.bin (add --bot other.bin to load a bot alongside the judge).

Examples

User-space programs now live under user/. A good starting point is the shell at user/sh.asm: the kernel loads it into RAM at 0x1000 and jumps to it after boot. Build it with make user and run via cargo run -- run ./kernel/main.bin ./user/sh.bin ./user/hw.bin ./user/ls.bin ./user/exit.bin ./user/help.bin.