Ramses — Architecture Manual
1. Overview
Ramses is an 8-bit didactic computer, the third in the family of hypothetical machines used in UFRGS computer architecture courses (Neander → Ahmes → Ramses → Cesar). It keeps Neander’s byte-sized word and 256-byte address space but adds:
- three general-purpose registers instead of a single accumulator,
- four addressing modes,
- subtraction, jump-on-carry, subroutine call, negate, shift-right instructions,
- a
Carryflag.
Everything in Ramses is still 8-bit: the word size, each register, the Program Counter, and memory addresses. This keeps programs small and makes register/memory state easy to show on a single screen — a useful constraint for UI design.
Source of truth for this document: fs/ArchSims.Core/Ramses.fs, fs/ArchSims.Core/Memory.fs, fs/ArchSims.Assemblers/RamsesAssembler.fs.
2. Programmer’s Model
2.1 Registers
| Register | Width | Encoding (bits 3..2) | Role |
|---|---|---|---|
RA | 8 bits | 00 | General-purpose |
RB | 8 bits | 01 | General-purpose |
RX | 8 bits | 10 | General-purpose; also used as index in Indexed mode |
PC | 8 bits | 11 | Program Counter |
PC is addressable as a register field in the encoding, but every instruction that writes through the register field simply writes whichever register the two-bit code selects. Practical programs almost always use RA, RB, RX.
The assembler accepts these names: A, B, X, PC (case-insensitive).
2.2 Instruction Register (IR)
A two-byte structure holding:
OpCode— the instruction byte just fetched (with register and addressing-mode fields already embedded),OperandAddress— the effective 8-bit address computed during fetch (0 for no-operand instructions).
The IR is programmer-visible in the CLI output and should be visible in the UI — it is the best single explanation of “what is about to execute”.
2.3 Flags
| Flag | Set by | Cleared on reset |
|---|---|---|
Negative | result bit 7 = 1 (i.e. result > 0x7F) | false |
Zero | result = 0 | true |
Carry | ADD/SUB: unsigned overflow; NEG: operand was 0; SHR: LSB | false |
Halted | HLT just executed | false |
Note: Zero is true in the initial state because the accumulator-like value 0 is zero. UIs should reflect this — flag lamps start -Z-, not all off.
N and Z are updated by every register-writing instruction (LDR, ADD, OR, AND, NOT, SUB, NEG, SHR). C is touched only by ADD, SUB, NEG, SHR. STR, JMP, Jxx, JSR, NOP, HLT leave the flags alone.
2.4 Memory
- Flat, byte-addressable, 256 bytes (addresses 0x00..0xFF).
- No memory-mapped I/O and no stack; subroutines use a self-linking convention (see
JSR). PCwraps naturally at 0xFF → 0x00.
The core instruments reads and writes via MemoryReadByte/MemoryWriteByte, keeping ReadCount/WriteCount counters that the CLI never prints but that a UI can display as a “memory accesses this step” indicator.
3. Instruction Encoding
Each instruction occupies one or two bytes. The first byte (“OpCode”) packs three fields:
bit 7 6 5 4 3 2 1 0
I I I I R R M M
| | |
| | +-- Address mode
| +-------- Register
+------------------ Instruction
Masks (see Ramses.fs):
InstructionMask = 0b11110000(0xF0)RegisterMask = 0b00001100(0x0C)AddressModeMask = 0b00000011(0x03)
Instructions that take no register (NOP, JMP, Jxx, JSR, HLT) simply ignore the R field. Instructions without an operand (NOP, NOT, NEG, SHR, HLT) occupy one byte and ignore the M field.
3.1 Instruction Table
| Mnemonic | OpCode (hi nibble) | Bytes | Operand | Writes | Affects flags | Notes |
|---|---|---|---|---|---|---|
NOP | 0000 (0x00) | 1 | — | — | — | No-op |
STR r | 0001 (0x10) | 2 | yes | MEM | — | MEM[addr] ← r |
LDR r | 0010 (0x20) | 2 | yes | r | N, Z | r ← MEM[addr] |
ADD r | 0011 (0x30) | 2 | yes | r | N, Z, C | r ← r + MEM[addr] |
OR r | 0100 (0x40) | 2 | yes | r | N, Z | r ← r OR MEM[addr] |
AND r | 0101 (0x50) | 2 | yes | r | N, Z | r ← r AND MEM[addr] |
NOT r | 0110 (0x60) | 1 | — | r | N, Z | r ← NOT r |
SUB r | 0111 (0x70) | 2 | yes | r | N, Z, C | r ← r − MEM[addr]; C = no borrow |
JMP | 1000 (0x80) | 2 | yes | PC | — | Unconditional |
JN | 1001 (0x90) | 2 | yes | PC | — | If N=1 |
JZ | 1010 (0xA0) | 2 | yes | PC | — | If Z=1 |
JC | 1011 (0xB0) | 2 | yes | PC | — | If C=1 |
JSR | 1100 (0xC0) | 2 | yes | MEM, PC | — | Subroutine call (see below) |
NEG r | 1101 (0xD0) | 1 | — | r | N, Z, C | r ← −r (two’s complement); C=1 iff original r = 0 |
SHR r | 1110 (0xE0) | 1 | — | r | N, Z, C | Logical right shift; C ← former LSB |
HLT | 1111 (0xF0) | 1 | — | — | Halted | Stops the CPU |
3.2 Notes on individual instructions
SUBuses an adder-with-carry trick (r + 256 − operand) and then inverts the carry so thatC=1means “no borrow” — matching the behavior tested in fs/ArchSims.Core.Tests/RamsesTests.fs.JSR addrstores the currentPCatMEM[addr]and setsPC ← addr + 1. Subroutines return by executing aJMPthrough that stored byte (usuallyJMP :Label,Iat the end of the subroutine). There is no hardware stack.JMP #immis syntactically legal but semantically degenerate: immediate mode for branch instructions reads the next byte as the target, soJMP #5is equivalent toJMP 5. The Fetch code checksinstruction >= Instruction.Jmpfor this special case (Ramses.fs:126).NEGis implemented as~r + 1; its carry rule is “set when the operand was zero” — the only case where negation produces a borrow.
4. Addressing Modes
Four modes, encoded in the two low bits of the OpCode:
| Mode | Bits | Assembly suffix | Effective address | Extra memory reads |
|---|---|---|---|---|
| Direct | 00 | N | N | 0 (fetch only) |
| Indirect | 01 | N,I | MEM[N] | +1 |
| Immediate | 02 | #N | N itself (the operand byte) | 0 |
| Indexed | 03 | N,X | (N + RX) mod 256 | 0 |
Important subtleties visible in Ramses.fs:123-128:
- Immediate + branch: when the instruction opcode is
>= JMP(0x80), immediate mode is treated as direct — the operand byte is the jump address. There is no “jump to literal” vs “jump to addr” distinction in practice. - Indirect is single-level:
MEM[operand-byte]is the final address; no further dereferencing. - Indexed wraps modulo 256 because
RX + operandis computed in 8 bits.
UI tip — when disassembling, show the effective address next to the mnemonic. The disassembler already produces e.g. LDR A 123 vs LDR A 123,I vs LDR A #123 vs LDR A 123,X. Supplementing that with → eff. addr = 146 during single-step makes indexed/indirect semantics tangible.
5. Execution Cycle
Step cpu performs Fetch then Execute. No interrupts, no cycle counting — one instruction per step.
Fetch (Ramses.fs:95):
- Read byte at
PC, store inIR.OpCode, incrementPC. - If the instruction has an operand, compute the effective address according to the mode and store it in
IR.OperandAddress. This may consume one more byte fromPC.
Execute (Ramses.fs:131):
- Decode register and instruction from
IR.OpCode. - Perform the operation, update the target register/memory and the flags as per the instruction table.
- Set
Haltediff the instruction wasHLT.
The reference implementation never throws on “invalid” opcodes — any byte whose high-nibble is not a recognized instruction is treated as NOP/HLT. The CLI’s RunningForever stop reason after 1000 instructions (fs/ArchSims.Core/Debugger.fs) guards against infinite loops.
6. Assembly Language
Parser: FParsec-based, defined in RamsesAssembler.fs.
6.1 Lexical rules
- Case-insensitive mnemonics and register names.
- Labels start with
:and follow with one letter then letters/digits::Start,:Loop1. - Comments start with
;and run to end of line. - Whitespace and comments separate tokens freely.
6.2 Directives / pseudo-ops
| Syntax | Meaning |
|---|---|
@N | Set assembly cursor to address N (0..255) |
:Label | Define a label at the current cursor |
N | A bare unsigned byte is emitted as a data constant |
6.3 Instruction syntax
NOP | HLT
NOT r | NEG r | SHR r r ∈ {A, B, X, PC}
JMP op | JN op | JZ op | JC op | JSR op
STR r, op | LDR r, op | ADD r, op
OR r, op | AND r, op | SUB r, op
Operand op is one of:
| Form | Mode | Example |
|---|---|---|
N | Direct | LDR A 10 |
N,I | Indirect | LDR A 10,I |
#N | Immediate | LDR A #10 |
N,X | Indexed | LDR A 10,X |
:Label | Direct | JMP :Loop |
:Label,I, :Label,X, #:Label | as above, with the label’s address as N |
Forward label references are resolved in a single deferred pass (RamsesAssembler.fs:114). Duplicate or unresolved labels raise a Portuguese error message (Label duplicado, Label indefinido).
6.4 Example — BitShift.Ramses.txt
:StartRA
LDR A #128
:RepeatRA
SHR A
JZ :StartRB
JMP :RepeatRA
:StartRB
LDR B #128
:RepeatRB
SHR B
JZ :StartRX
JMP :RepeatRB
:StartRX
LDR X #128
:RepeatRX
SHR X
JZ :Finish
JMP :RepeatRX
:Finish
HLT
Loads 0x80 into each register in turn and shifts right until zero — a visually obvious progression that makes a good first-launch demo for any UI.
7. Running a Program
CLI entry point: fs/ArchSims.CmdLine/Main.fs dispatches to RamsesCmdLine.fs.
ArchSims.CmdLine.exe BitShift.Ramses.txt -Cpu Ramses -Mode Interactive -Output Decimal -Speed 10
Output line format (decimal):
Ra: 0 Rb: 0 Rx: 0 PC: 2 IR: [LDR A 128 ] -Z-
- Three flag slots in fixed order:
N Z C(each either the letter or-). IRcolumn shows the disassembled current/last instruction.- Binary output prints each register as eight decimal 0/1 digits (one line per register), useful for teaching bit operations.
Save mode produces a binary file with prefix 0x03 'R' 'M' 'S' followed by the 256 bytes of memory — compatible with the historical Ramses .mem format.
8. UI Design Suggestions
A few recommendations that follow directly from the architecture:
- Fit on one screen. 256 bytes fit comfortably in a 16×16 hex grid. Highlight the current
PC, last read, last written (three distinct colors). The core already exposesReadCount/WriteCountper step if you want a “changed cells” animation. - Register panel. Show
RA,RB,RX,PCas four small tiles. Render each value in three bases simultaneously (hex, decimal, binary) — the architecture is small enough that showing all three costs nothing. - Flag lamps. Three flags plus
Halted. Model them as LEDs. RememberZstarts lit. - Instruction register view. The IR holds both the opcode byte and the effective address. Decode it visually: split the opcode byte into
IIII / RR / MMfields, color each bit-group, and show the mnemonic + effective operand underneath. This single widget explains the encoding to a student in a way a table cannot. - Disassembly column.
DisassembleInstructions(inRamses.fs) already walks a byte list and returns(text, size)pairs. Use it to render an “assembly view” alongside the hex grid and mark which bytes belong to the instruction currently in the IR. - Step vs Run. The debugger API (
DebuggerStep,DebuggerRun,DebuggerSetBreakpoint,DebuggerClearBreakpoint) matches a typical UI triad of Step / Run / Break. TheRunningForeversentinel after N steps gives you a natural “slow mode” progress indicator. - Addressing-mode explainer. When the user hovers an instruction with
,I,#, or,X, draw an arrow from the instruction’s operand byte to the effective memory cell. This is the single hardest concept in the Neander → Ramses step and a visual arrow teaches it in seconds. - Assemble-on-the-fly.
AssembleInstructionaccepts a single line and returns the bytes. A UI could expose this as “type a mnemonic, see its bytes appear” — a great exploration tool for the encoding table. - No stack means no call tree.
JSRself-modifies memory. Visualizing it as an arrow that writes a return byte into the called routine (rather than “pushes”) is honest and instructive. - Speed control. The CLI exposes
Speedas instructions-per-second; keep that for parity and expose 1, 10, 100, 1000 presets plus a “as fast as possible” toggle.