Cesar — Architecture Manual

1. Overview

Cesar is the most complex of the UFRGS teaching machines: a 16-bit computer with eight registers, eight addressing modes, a two-operand instruction set, status flags with overflow, a call/return mechanism backed by a real stack, and memory-mapped keyboard/display areas. Its design is a pared-down PDP-11 — enough detail to teach “real” assembly without the weight of a production ISA.

Highlights:

Source of truth: fs/ArchSims.Core/Cesar.fs, fs/ArchSims.Core/Memory.fs, fs/ArchSims.Assemblers/CesarAssembler.fs.

2. Programmer’s Model

2.1 Registers

RegisterWidthConventional role
R0..R516 bitsGeneral-purpose
R616 bitsStack Pointer (used by JSR/RTS)
R716 bitsProgram Counter

R7 is the PC literally — it is read and written through the normal register mechanism, and every addressing-mode combination that acts on R7 has a programmer-visible meaning (see §4.2).

All registers are zero after reset. The 16-bit word is stored in memory big-endian (MemoryReadWordBigEndian / MemoryWriteWordBigEndian in Memory.fs).

2.2 Flags

FlagBit maskMeaning
Negative0b1000Result’s top bit (result > 0x7FFF)
Zero0b0100Result = 0
Overflow0b0010Signed arithmetic overflow
Carry0b0001Unsigned arithmetic overflow (for SUB/CMP, “no borrow”)

Also: Halted — set by HLT, cleared by every subsequent instruction (see §5.5). The initial state has Z = true, all others false.

CCC and SCC expose the flags directly by letting the programmer choose which bits to clear or set — the low four bits of the opcode select flags by the same mask shown above.

2.3 Instruction Register (IR)

Unlike Ramses, the Cesar IR stores the entire encoded instruction, including any extra bytes consumed during decoding of addressing modes against R7 (immediate operands, absolute addresses, indexing offsets). It is a byte array of length 1..6.

It also stores the decoded SourceOperand and TargetOperand as an Operand union:

This is programmer-visible via the CLI and should be surfaced in any UI: it captures “what this instruction is actually going to touch” in a single structure.

2.4 Memory layout

0x0000  ┌───────────────────────────┐
        │                           │
        │   Program + data + stack  │   Word-addressable (big-endian)
        │                           │
0xFFD9  ├───────────────────────────┤
0xFFDA  │  Keyboard (byte mapped)   │   2 bytes
0xFFDC  │  Display  (byte mapped)   │   36 bytes (0xFFDC..0xFFFF)
0xFFFF  └───────────────────────────┘

Addresses < 0xFFDA: reads and writes are 16-bit word operations (the low address byte and the next byte together form one word). Addresses ≥ 0xFFDA: reads and writes degrade to 8-bit byte operations — Cesar zero-extends on read and truncates on write (Cesar.fs:297-315).

UI implication: the display area is 36 bytes of ASCII; drawing them as a fixed 36-character strip (or 6×6 / 4×9 grid) gives you a working “screen” that Cesar programs can print to with a single MOV. The keyboard pair at 0xFFDA is the conventional place to pipe a typed character into the simulator.

R6 (stack pointer) is initialized to 0 after reset, which means the first JSR decrements it to 0xFFFE — already inside the memory-mapped area on the byte side of 0xFFDA only if you push very deep. Real programs normally start with MOV #something, R6 to set the stack.

3. Instruction Set

Instructions are grouped by the high nibble of the first opcode byte. Within a group, the low nibble (for the one-operand and branch groups) or the lower byte (for two-operand instructions) select sub-operations or operands.

High nibbleGroupMnemonics
0x0NOPNOP
0x1Clear flagsCCC [NZVC]
0x2Set flagsSCC [NZVC]
0x3Conditional branchesBR, BNE, BEQ, BPL, BMI, BVC, BVS, BCC, BCS, BGE, BLT, BGT, BLE, BHI, BLS
0x4JumpJMP
0x5Subtract-and-branchSOB
0x6Subroutine callJSR
0x7Subroutine returnRTS
0x8One-operand ALUCLR, NOT, INC, DEC, NEG, TST, ROR, ROL, ASR, ASL, ADC, SBC
0x9MOVMOV src, dst
0xA..0xETwo-operand ALUADD, SUB, CMP, AND, OR
0xFHaltHLT

3.1 Encoding

3.2 Branch semantics

All 15 conditional branches (BNE..BLS) plus the unconditional BR use a signed 8-bit displacement, giving a range of −128..+127 bytes from the next instruction. The assembler enforces this range and raises Label inacessível a partir de um branch when a forward/backward label is out of reach (CesarAssembler.fs:252).

Condition decoding (see Cesar.fs:333-359):

MnemonicConditionMeaning (source-level)
BRalwaysunconditional
BNE/BEQZ clear / setnot equal / equal
BPL/BMIN clear / setpositive / minus
BVC/BVSV clear / setsigned overflow clear / set
BCC/BCSC clear / setunsigned: no carry / carry
BGE/BLTN = V / N ≠ Vsigned ≥ / <
BGT/BLE(N = V) ∧ ¬Z / (N ≠ V) ∨ Zsigned > / ≤
BHI/BLS¬C ∧ ¬Z / C ∨ Zunsigned > / ≤

3.3 JMP

JMP takes a one-operand target but forbids pure-register mode (mode = 0) — jumping “into” a register has no meaning. The implementation silently does nothing in that case (Cesar.fs:361-364); the disassembler prints JMP ?.

3.4 SOB (Subtract One and Branch if non-zero)

Single-instruction countdown loop. Byte 1 is an unsigned displacement subtracted from the next-instruction address, so SOB branches backward only — ideal for loop bodies. Stops branching when the register reaches zero.

3.5 JSR / RTS

JSR r, target:

  1. R6 ← R6 − 2 (decrement stack pointer)
  2. MEM[R6] ← r (push the “link” register; usually R7 for PC-linking)
  3. r ← R7 (save return address in the link register)
  4. R7 ← effective-address(target) (jump)

RTS r:

  1. R7 ← r (restore PC from link register)
  2. r ← MEM[R6] (pop the saved link)
  3. R6 ← R6 + 2

The idiom is JSR R7, :Sub paired with RTS R7 — the link register doubles as the return path, so the stack frame is just the saved return address. Any other register can serve as link if the programmer wants to preserve it across the call.

3.6 One-operand ALU group (opcodes 0x80..0x8B)

MnemonicEffectFlag rules (beyond N/Z)
CLRdst ← 0 (without reading it first)C=0, V=0
NOTdst ← ~dstC=1, V=0
INCdst ← dst + 1C on wrap-around, V on signed overflow
DECdst ← dst − 1C=1 iff no borrow, V on signed overflow
NEGdst ← −dstC=1 unless dst was 0, V for 0x8000
TSTreads dst, writes nothingC=0, V=0
ROR/ROL17-bit rotate through CV ← C XOR N (post-op)
ASR/ASLArithmetic shift right/leftV ← C XOR N (post-op); N flipped (sign-preserve heuristic)
ADCdst ← dst + Cstandard add
SBCdst ← dst − 1 + C (subtract with inverted carry)standard subtract

Note: CLR and TST are carefully special-cased — CLR does not read the operand first, TST does not write the result back. UIs should reflect that: CLR reads 0 memory words for the operand, TST reads but doesn’t mark the cell as “written”.

3.7 Two-operand ALU group (MOV, ADD, SUB, CMP, AND, OR)

All read both operands (except MOV, which does not read the target), compute a result, and write it back to the target (except CMP, which does not write). They all update N and Z; ADD/SUB/CMP also update V and C via the shared aluAdd helper; MOV/AND/OR explicitly zero V. SUB and CMP invert carry after the add-of-complement trick so that C=1 corresponds to “no borrow”.

4. Addressing Modes

Eight modes, uniformly available for both source and target operands. Encoded in a 3-bit field; the full 6-bit mmm rrr selector is used per operand.

#ModeBitsAssemblyEffective-operand semantics (for reg Rr)Extra bytes in IR (when Rr = R7)
0Register000R0Operand is Rr0
1Register post-increment001(R0)+Addr ← Rr; Rr ← Rr + 2+2 (immediate when Rr = R7)
2Register pre-decrement010-(R0)Rr ← Rr − 2; Addr ← Rr0
3Indexed011N(R0)Read word N from PC-stream; Addr ← Rr + N+2 always
4Register indirect100(R0)Addr ← Rr0
5Register post-increment indirect101((R0)+)Addr ← MEM[Rr]; Rr ← Rr + 2+2 (absolute when Rr = R7)
6Register pre-decrement indirect110(-(R0))Rr ← Rr − 2; Addr ← MEM[Rr]0
7Indexed indirect111(N(R0))Read word N from PC-stream; Addr ← MEM[Rr + N]+2 always

4.1 Three useful aliases

From Cesar.fs:77-79:

These three are the reason Cesar doesn’t need dedicated immediate/absolute encoding: combining R7 with the right addressing mode recycles the same orthogonal machinery.

4.2 R7 interactions — read carefully

The test file fs/ArchSims.Core.Tests/CesarTests.fs enumerates every combination; consult it before writing a UI that tries to “pretty-print” R7 modes.

4.3 Assembly surface

The parser (CesarAssembler.fs:64-74) accepts:

Surface syntaxMode
R0Register
(R0)+Post-increment
-(R0)Pre-decrement
N(R0)Indexed
(R0)Register indirect
((R0)+)Post-increment indirect
(-(R0))Pre-decrement indirect
(N(R0))Indexed indirect
#NImmediate (compiled as (R7)+)
NDirect/absolute (compiled as ((R7)+))
:LabelDirect/absolute via label

5. Execution Cycle

5.1 Fetch

Fetch (Cesar.fs:171):

  1. Read byte at R7, store as first byte of the IR, increment R7.
  2. Classify the instruction from the high nibble.
  3. If it takes operand bytes, read the second byte, append to IR, decode addressing-mode fields.
  4. For two-operand instructions, decode and evaluate the source operand first (which may consume extra PC-stream bytes for R7-relative modes).
  5. Decode and evaluate the target operand.
  6. Append any extra PC-stream bytes consumed to the IR so that the disassembler and UI can render the full instruction.

Operand evaluation at fetch time is an important choice: side effects like R7 ← R7 + 2 happen during fetch, not execute. This matters if a UI wants to show “before fetch / after fetch / after execute” states distinctly.

5.2 Execute

Execute dispatches on the high nibble and, within a sub-group, on the low nibble. See Cesar.fs:280. Writes go to either a register (via _r.[i]) or memory (via MemoryWriteWordBigEndian / MemoryWriteByte depending on whether the address is below or at/above 0xFFDA).

5.3 ALU

All arithmetic goes through aluAdd a b carryIn:

SUB and CMP feed ~source + 1 through aluAdd with carry in and then invert the carry so C=1 means “no borrow”.

5.4 Stack and subroutines

R6 is the stack pointer. JSR/RTS push/pop one word at a time. No other instruction implicitly touches R6 — a UI does not need to draw a “stack view” at all unless the program uses JSR. When it does, highlighting R6 and the words immediately above it in the memory map is the cheapest way to visualize the stack.

5.5 Halt semantics

HLT sets _flags.Halted. The bit is cleared by the next instruction executed (the last line of Execute). Translation: halted state is valid only between step calls. The debugger checks Halted after each step and stops the run loop when set. UIs should reflect Halted as the state after the step, not as a permanent CPU state — a user who presses “Step” after HLT will see the halt flag drop on the next cycle.

6. Assembly Language

Parser: CesarAssembler.fs. Same general shape as the Ramses assembler.

6.1 Directives

SyntaxMeaning
@NSet cursor to address N (0..65535)
:LabelDefine a label at the current cursor
NEmit a data constant: 1 byte if 0..255, otherwise a 16-bit word

Word constants are stored big-endian to match memory.

6.2 Instruction syntax cheatsheet

NOP | HLT
CCC [flags] | SCC [flags]              flags ::= [N][Z][V][C]
BR N | BR :Label                       (15 branch mnemonics, same shape)
JMP op
SOB Rn, N | SOB Rn, :Label
JSR Rn, op
RTS Rn
CLR op | NOT op | INC op | DEC op | NEG op | TST op
ROR op | ROL op | ASR op | ASL op | ADC op | SBC op
MOV src, dst
ADD src, dst | SUB src, dst | CMP src, dst
AND src, dst | OR  src, dst

Parsing is case-insensitive for mnemonics and register names; labels and comments follow the same rules as Ramses.

Negative numbers are accepted everywhere (pint32), but constants outside 0..255 are emitted as full words. Example: -1 as a one-operand target in a MOV becomes the 16-bit word 0xFFFF.

6.3 Example — BitShift.Cesar.txt

:Start
    MOV #32768, R0
:Loop
    ROR R0
    BNE -4           ; 252 / :Loop
    
    ROR R1           ; Advances carry to next register / Finishes execution when in R7
    INC 4            ; Next register (self-modifying code)
    INC 8
    
    BR :Loop
@32774
    HLT              ; End of program

This program shifts a 1-bit through every register in turn by self-modifying the ROR instruction’s operand byte. The last shift lands in R7, which jumps to address 32774 where the HLT lives. Illustrates three Cesar-specific ideas at once: byte-level self-modifying code, R7 as an ordinary register, and using BR’s signed displacement negatively (-4) as a shorthand for a backward jump.

7. Running a Program

CLI: fs/ArchSims.CmdLine/CesarCmdLine.fs. Same command shape as Ramses:

ArchSims.CmdLine.exe BitShift.Cesar.txt -Cpu Cesar -Mode Interactive -Output Hexadecimal -Speed 500

Decimal output line:

R0:32768 R1:    0 R2:    0 R3:    0 R4:    0 R5:    0 R6:    0 PC:    3 IR: [MOV (R7)+, R0      ] -Z--

Binary mode prints two lines per step (4 registers each) plus the IR line.

Save mode writes memory with prefix 0x03 'C' '1' '6' — the historical Cesar .mem format.

8. UI Design Suggestions

  1. Register view. Eight 16-bit registers fit in a single column. Label R6 as “SP” and R7 as “PC” (or display both names). Show each value in hex and decimal. Highlight the one that just changed; highlight R6 and R7 whenever JSR/RTS/branch/jump fires.
  2. Flag lamps plus shortcuts. Four flags (N, Z, V, C) and Halted. Consider adding two one-click buttons next to them that assemble CCC NZVC / SCC NZVC and step — a didactic way to show CCC/SCC in isolation.
  3. Memory view — three zones. The 64 KiB address space benefits from a zoomable hex dump where:
    • program area (default focus) is shown as disassembled instructions alongside bytes,
    • the stack (memory around R6) is shown as a separate “stack” panel sliding upward from 0xFFD9,
    • the 36-byte display area (0xFFDC..0xFFFF) is a dedicated strip rendered as ASCII. Keyboard area (0xFFDA..0xFFDB) gets an input box that writes that byte on submit.
  4. Display as a first-class widget. The Samples/Demo-Cesar.cmd expects programs that “print” to 0xFFDC by MOV. A text-mode display showing those 36 bytes live, updating on every memory write in that range, is the single most motivating UI element — it is the difference between “calculator simulator” and “computer”.
  5. Disassembly gutter. DisassembleInstruction returns (text, size) pairs. Render the program area as a three-column view: address, raw bytes, disassembly. Highlight the instruction currently in the IR (using R7 minus the IR size before this step, because fetch has already advanced R7).
  6. Addressing-mode explainer. Eight modes are the main conceptual hurdle. On hover over an operand, animate the effective-address derivation: show (R3)+ decomposing into “read R3”, “fetch MEM[R3]”, “R3 ← R3 + 2”. The Operand union (NoOp / Reg / Addr) already carries the answer — just display it.
  7. Two-operand arrows. For MOV src, dst and friends, draw a ghost arrow from source operand’s resolved location to target. CMP is a useful special case: arrow stops at the ALU and nothing flows to the target — makes “CMP doesn’t write” obvious.
  8. Stack visualization. Trigger only when R6 is first written (or non-zero). Render the stack as an upward-growing column of words starting at R6. Distinct arrows for JSR (decrement + push) vs RTS (pop + increment) are clearer than a generic animation.
  9. SOB loop counter. A short visualization — decrement the target register on each SOB execution, show how many iterations remain. Cesar programs use this constantly.
  10. Branch range hints. When the user types an assembly line like BNE :Label, the assembler may fail with Label inacessível a partir de um branch. A UI could preview the computed displacement in real time and color the mnemonic red when the target is out of the −128..+127 window.
  11. Self-modifying code. Both sample programs rely on self-modifying bytes. A “last write” overlay on the program area is invaluable when the UI is stepped slowly — the user watches a new ROR operand byte get written and then re-executed.
  12. Memory access granularity indicator. Word vs byte access is invisible in the code but important above 0xFFD9. Marking the transition line in the memory view (e.g., a different background color for 0xFFDA..0xFFFF) prevents confusion when a MOV writes a single byte instead of the expected word.
  13. Step-through fidelity. Fetch has side effects (register increments, PC advance, operand pre-evaluation). If the UI offers a “sub-step” view, the natural stages are: (a) read opcode byte(s), (b) decode source operand, (c) decode target operand, (d) execute, (e) update flags. Each maps directly to lines in Fetch/Execute.
  14. Breakpoints. DebuggerSetBreakpoint address works on the fetched R7 value, so UI “click a disassembly line to break” maps cleanly. Keep the same API as Ramses for visual and code consistency.
  15. Speed. Because Cesar can run larger programs, a logarithmic speed slider (1, 10, 100, 1000, 10000 ops/s, unlimited) works better than linear. At “unlimited” the CLI’s 1000-step ceiling becomes a bottleneck — consider tuning DebuggerRun or exposing the step limit.