Skip to content

Design decisions

This page records the judgment calls taken during implementation that go beyond the frozen invariants in design principles. Each entry is a fact that is not visible from reading the code alone — the reasoning, the context, the alternative considered.

The list is curated from docs/development/status.md; that file is the chronological log per work session, this one is the searchable index.


Bus and memory

1. SystemBus is non-copyable and non-movable. It owns RAM via unique_ptr<Ram> and peripherals cache raw pointers into it. Moving the bus would invalidate them. If you need to relocate a bus, construct a new one.

2. Big-endian translation lives in SystemBus, not in Ram. Ram holds raw bytes as they appear on the wire; SystemBus::{encode,decode}_be() does the BE shuffle at the typed access boundary. Keeps RAM trivially snapshot-able and matches how a real memory controller behaves.

3. Single region per access. A byte-span access that straddles two regions returns ErrorCode::BusError. Real hardware latches one transaction against one target; the bus does not silently split.

4. MMIO requires 1 / 2 / 4-byte naturally aligned accesses. Anything else is rejected at the bus with BusError or AlignmentError. CPU alignment traps belong in the handlers, not in the bus.

5. Bus does not own peripherals. SystemBus::map_peripheral takes a non-owning IPeripheral*. The runtime owns unique_ptr<IPeripheral> and hands raw pointers to the bus.

26. 4 KB RAM mapped at physical address 0x0. Real GR712RC boots from ROM at address 0. The RTEMS idle loop does lda [%o0] 0x1c, %g0 where %o0 = 0xFFFFFFF0; SPARC V8 address wrap-around lands at physical address 0. Mapping 4 KB of RAM at address 0 avoids a spurious data_access_exception in the idle loop.


Decoder and ISA

8. FPop1/FPop2 decoded as InsnKind::FpOp. op = 10, op3 ∈ {0x34, 0x35}. The handler returns FpDisabled (tt = 0x04), the correct LEON3 behaviour when no FPU is present (PSR.EF = 0). Coprocessor opcodes (op3 ∈ {0x36, 0x37}) remain Unknown and map to IllegalInstruction.

9. Instruction-fetch vs data-access bus errors are distinguished. ExecStatus::InsnFetchError (tt=0x01, instruction_access_exception) for failed fetches; BusError (tt=0x09, data_access_exception) for load/store failures.

10. TADDccTV / TSUBccTV set icc and write rd even when trapping. SPARC V8 §B.30: the tagged-add/sub trap variant computes the result and condition codes first, then traps if V is set. The handler writes rd and icc before returning ExecStatus::TagOverflow. The spec makes the result "unpredictable"; deterministic write makes tests reproducible.

11. handlers.cpp split into category files. handlers_alu.cpp, handlers_branch.cpp, handlers_loadstore.cpp, handlers_regwin.cpp, handlers_special.cpp, with shared helpers (alu_op2, eval_cond) in handlers_internal.hpp. The public execute() dispatcher remains in handlers.cpp.

21. BA encoding uses disp22 = 0, not 1. Earlier test_bare_metal.cpp helpers encoded BA with disp22 = 1, which targets PC+4 (the delay slot itself). The fix sets disp22 = 0 so BA .+0 becomes a proper self-branch when intended.


Peripherals

12. MemCtrl (FTMCTRL) is a passive stub. MCFG1–MCFG4 are readable / writable with no side effects. MCFG3 bit 27 (reserved, reads as 1) is forced. No timing, no bank switching — just enough for the RTEMS memory probe.

13. IrqMP IFORCE write semantics. Writing IFORCE uses a clear-then-set protocol: the upper 16 bits clear bits, the lower 16 bits set bits (both masked to IRQ lines 1–15). Matches GRLIB behaviour where software can atomically set and clear force bits in one write.

14. IrqMP pending_mask(0) includes IFR0. CPU 0's pending mask is IPEND | IFR0 | (current_mask & IFR0). For CPU N>0, pending_mask(N) is IPEND | (current_mask & IFRN). Matches the GR712RC single-CPU force register design.

15. GPTimer control register writable mask is 0x2B. Bits 0 (EN), 1 (RS), 3 (IE), 5 (CH) are directly writable. Bit 2 (LD) is write-only — triggers a reload from the counter register then clears. Bit 4 (IP) uses write-0-clear (writing 1 has no effect, writing 0 clears the pending bit). Bit 6 (DH) is read-only 0.

16. GPTimer prescaler underflow logic. On tick(), the prescaler counter is decremented first; when it reaches zero, the prescaler value is reloaded and all enabled sub-timers are ticked.

17. GPTimer timer4 watchdog defaults. After reset, timer4 has EN=RS=IE=1 and counter/reload both set to 0xFFFF. Matches the GR712RC default where the watchdog is armed and must be disabled or fed by software.

18. ApbUart uses std::queue<uint8_t> for the RX FIFO (max 8). No TX FIFO is modelled — transmit() drains immediately via ICharacterDevice. Status bit 31 (FA, FIFO available) always reads as 1 since the queue is small enough not to fill.

19. PeripheralContext now includes ICharacterDevice*. Added for APBUart to inject console I/O. Default is nullptr; the runtime wires it to the configured character device.

20. All peripheral MMIO handlers reject non-word accesses. Byte and half-word reads/writes return AlignmentError. Stricter than GR712RC (which allows byte writes to the UART data register), but matches the MVP approach: defer narrowing to when RTEMS demands it.

22. hello_uart.S must enable CTRL.TE before transmitting. ApbUart drops writes to the data register unless TE is set in the control register. Any asm test that writes directly to APBUart MMIO must first st the TE bit into CTRL. Missing this is silent — the test runs but no output is produced.

27. GPTimer bootloader prescaler initialization. The emulator simulates the GR712RC ROM bootloader's timer setup by writing to the prescaler value and reload registers during initialize(). Without this, the prescaler counter starts at 0 and takes 0xFFFF ticks (≈ 65 ms) before the first underflow, greatly delaying the first timer interrupt.


Runtime and scheduling

6. Warning set is stricter than the CLAUDE.md minimum. lince::warnings enables, on top of -Wall -Wextra -Wpedantic -Werror: -Wshadow -Wnon-virtual-dtor -Wold-style-cast -Wcast-align -Wunused -Woverloaded-virtual -Wconversion -Wsign-conversion -Wnull-dereference -Wdouble-promotion -Wformat=2. Everything builds clean under this set — keep it that way.

7. tests/support/dummy_peripheral is a test fixture, not a module. Lives in the test tree; must not leak into lince_peripherals.

23. Emulator exposes injection, not construction, for services. set_character_device() and set_logger() swap the defaults at runtime. Lets the CLI wire StdoutCharDevice / StdoutLogger without forcing them into EmulatorConfig, and lets tests swap in CapturingCharDevice without touching config. SMP2 wrappers will use the same injection points.

25. Idle-time skip is bounded to 1 ms. When all cores are powered down, run_until() jumps simulated time forward to the next event. The GPTimer uses direct IInterruptSource::raise() calls rather than the EventScheduler, so next_event_time() does not see timer interrupts. Without a bound, time would jump to the deadline and the GPTimer's periodic interrupt would be delivered only once. The 1 ms bound (kMaxIdleNs) ensures timer interrupts arrive at roughly their expected rate.

28. Secondary cores start in power-down mode. On real GR712RC, only CPU 0 starts executing at reset. Secondary cores are parked in power-down mode (wr %g0, %asr19) and wait for the primary to release them via IRQMP. The emulator sets is_powered_down = true for all cores except CPU 0 after loading the ELF image.