Virtus Cyber Academy

Virtus OS Stdlib Service Reference

3,946 words · ~18 min read
Markdown PDF

*VCA-CSA-101 cross-chapter quick-reference handout. Anchors: §11.2 (compiler-side spec) + §12.4-§12.9 (OS-side implementation). *

Purpose: complete signature reference for every standard-library service the Virtus compiler emits calls against and the Virtus OS v1 implements. Print and pin during Lab 11.1 (library-call codegen), Lab 11.3 (Virtus Console end-to-end), Labs 12.1-12.5 (OS implementation + capstone). The compiler trusts this surface to exist; the OS contracts to provide it; drift between the two is a curriculum bug.


At a glance

Property Value
Modules 9 primary (Math, Memory, Output, Console, Screen, GamePad, Sound, Sys, GPIO) + 2 helpers (String, Array)
Total services ~34 services (varies by minor revision)
Calling convention Left-to-right argument push; void methods return placeholder zero; one word on stack at return (per Ch 8 §8.4 + Ch 11 §11.4)
Implementation OS-resident; called via jalr (no ecall in v1; CSA-201 introduces the trap mechanism)
Total OS source ~1,500 lines of student-authored HLL + ~30 lines bootstrap assembly
Replaces Ch 11 stub-OS bundle (Lab 11.3 stubs return trivial defaults)
CSA-201 evolution Adds privilege boundary + ecall trap; library list grows by ~30%; mitigations toggleable

Calling convention (central)

Every stdlib service obeys the Ch 8 calling convention. No special-case for OS code; OS code is user code at a different address.

Property Specification
Argument passing Left-to-right onto VM-level stack; callee reads argument 0..argument m-1
Implicit this None for stdlib calls (all stdlib services are functions, not methods)
Return value One word on top of stack at return; void methods leave placeholder zero
Caller pop pattern do M.f(...) discards via pop temp 0
Caller-saved state Per Ch 8 §8.6 (RET + LCL + ARG + THIS + THAT)
Argument count after call len(args). No implicit-this for stdlib

Virtus.syscall. Kernel-mediated dispatch (R7.x post-discovery addition)

Added 2026-04-29 per audit cleanup. R7.x silicon-bring-up shipped a single dispatcher entry point that mediates all stdlib calls; per Ch 12 §12.3.2 prose, the dispatcher is structurally a regular Jack function with a 30-branch if-chain. There is no privilege-mode trap or syscall ABI distinct from Jack's calling convention.

Property Specification
Signature Virtus.syscall(num, arg0, arg1) → result
Source stdlib/Virtus.virtus:75-117 (30-branch if-else dispatcher)
Dispatch type O(N) on N services; ~150 RV32I-Lite instructions per call (avg)
Calling convention Same Jack convention as every other stdlib service (left-to-right stack push; one-word return)
ABI distinction None. Virtus.syscall is a regular Jack function; not an ecall-style trap
Failure sentinel Returns -1 for invalid num

num → service mapping (canonical roster)

num range Service family Canonical service-num assignments
0 Math.add (canonical syscall test entry) syscall(0, a, b) → a + b
1 Math.multiply syscall(1, a, b) → a * b
2 Math.divide syscall(2, a, b) → a / b
3 Math.sqrt syscall(3, n, _) → floor(sqrt(n))
4 Math.abs syscall(4, n, _) → |n|
10 Memory.alloc syscall(10, size, _) → ptr
11 Memory.dealloc syscall(11, ptr, _) → 0
12 Memory.peek syscall(12, addr, _) → M[addr]
13 Memory.poke syscall(13, addr, value) → 0
20 Output.printChar syscall(20, ch, _) → 0
21 Output.printString syscall(21, str_ptr, _) → 0
30 Screen.drawPixel syscall(30, packed_xy, color) → 0 (packed_xy = (x << 16) | y)
31 Screen.setColor syscall(31, color, _) → 0
32 Screen.clear syscall(32, _, _) → 0
40 GamePad.readButton syscall(40, n, _) → button_state (renamed from Keyboard.readButton per CR2; DS2 12-button MVP bitmap per GamePad section below)
41 GamePad.poll syscall(41, _, _) → button_word (renamed from Keyboard.poll per CR2; raw 16-bit DS2 bitmap)
50 Sound.play syscall(50, samples_ptr, length) → 0
60 Sys.halt syscall(60, _, _) → does not return
61 Sys.error syscall(61, code, _) → does not return
70-79 String.* (helpers) String.new / String.appendChar / String.charAt / String.length
80-89 Array.* (helpers) Array.new / Array.dispose
default (invalid num) Returns -1 sentinel

The dispatcher's source is at stdlib/Virtus.virtus:75-117; check the actual source for the canonical num assignments at any given commit (the table above reflects the R7.3 canonical roster; future revisions may renumber). Per Ch 12 §12.3.2, the indirection is forward-compatible with CSA-201's ecall privileged-mode dispatch, when CSA-201 lands, Virtus.syscall(num, arg0, arg1) becomes ecall with num/arg0/arg1 in registers; the user-side call site does not change.

Pedagogical note for Lab 12.X: students implementing a syscall (extending the dispatcher with a new service) are implementing a function call, not a privilege-boundary trap. Per Ch 12 §12.3.2's closing pivot. "when you reach Lab 12.X and find yourself implementing a syscall, you may be surprised that you are just implementing a function call."


Math. Multiply, divide, square root on an ALU that has none

RV32I-Lite has no mul, no div, no shift instructions. Every Math service is software. Cycle costs are the chapter's most-feelable demonstration of the cost of "missing" instructions.

Service Signature Returns Cycle cost Error semantics
Math.multiply(a, b) (int, int) → int a × b (32-bit two's-complement; wraps on overflow) ~1,000 cycles (200 with mask-table opt) Overflow wraps silently
Math.divide(a, b) (int, int) → int a ÷ b (signed; truncation toward zero) ~800 cycles Trap on b = 0 via system-control halt at 0x8003000C; trap on 0x80000000 / -1 (overflow)
Math.sqrt(n) (int) → int floor(sqrt(n)) via integer Newton iteration ~5,000 cycles (uses Math.divide internally) n < 0 returns -1 sentinel
Math.abs(n) (int) → int |n| ~5 cycles Math.abs(0x80000000) returns 0x80000000 (Java convention; two's-complement is asymmetric)

Algorithm summary:

CSA-201 forward-pointer. Every Math service shrinks by 50-500× when the M extension's mul/div arrive, the gap is the speedup the student personally measures on the same student-silicon bitstream (Tang Primer 25K canonical Phase-1 baseline; Tang Nano 20K advanced-track variant). CSA-301 extends to Math.sin / Math.cos / Math.log (FP extension required, F/D. Not in CSA-201, only later electives).


Memory. Heap allocator over 16 KiB

Free-list allocator with first-fit placement, split-on-allocate, and adjacent-block coalescing on free. Heap region: 0x00010000. 0x00013FFF.

Service Signature Returns Notes
Memory.init() () → void - Initialises free list to one 16,384-byte block at 0x00010000
Memory.alloc(size) (int) → int Heap pointer or 0 on OOM First-fit; minimum block 24 bytes; word-aligns; sets sentinel 0xDEADBEEF
Memory.dealloc(ptr) (int) → void - Sentinel-checks for double-free; inserts in address order; coalesces with adjacent free neighbors
Memory.peek(addr) (int) → int M[addr] Bare load. Useful for OS-internal bookkeeping; not type-safe
Memory.poke(addr, value) (int, int) → void - Bare store. Same caveat

Block header (8 bytes): {size, next}.

Error semantics:

XD-strand attack vectors (exploited in advanced security strand):

CSA-201 forward-pointer. Adds canary words around payload, segregated free lists, ASLR for heap base; Ch 12 §12.5.4 catalogues the catalogue. CSA-201 §5 extends to bump allocator + slab allocator + tracing GC as performance comparisons.


Output. Text output to console

Two services. Text is rendered to the framebuffer's text region as 8×8-pixel glyphs (font supplied as .bss constant in the chapter's reference repo).

Service Signature Returns Notes
Output.printChar(c) (int) → void - Writes one character at current text cursor; advances cursor; wraps at line end; scrolls at screen end
Output.printString(s) (String) → void - Iterates String.length calls of printChar (library composition demo)

Argument-count after call: 1 for both (no implicit this; stdlib functions are static-class).

CSA-201 evolution. Adds Output.printInt(n), Output.printFloat(f), Output.println(), formatted output Output.printf(...). CSA-301's GUI library replaces text with proper text-rendering and font-table support.


Console. Text-mode tile output (the pedagogically-first I/O path)

Tile-mode character output over the IP Pack's HDMI tile-map peripheral. Logical surface: 80 × 30 character cells (640 × 480 video output via 8 × 16 glyph ROM), 7-bit ASCII chars, 16-color CGA/EGA palette per cell (4 bits foreground + 4 bits background). AXI4-Lite slave window at MMIO base 0x80100000 (per peripheral-ip-pack/sw/hdmi_demo/hdmi_tile_map.h).

Why Console comes before Screen in pedagogical sequence. Text output is the first I/O path every running program needs. Console.printString("hello") is the smallest possible "the silicon is alive" demonstration. Pixel-mode (Screen, §below) requires that the student already have a framebuffer-update model in head; tile-mode lets the student write working programs from Lab 11.3 onward without touching pixel arithmetic at all. CSA-101 introduces Console first, Screen second, and the chapter's bring-up sequencing reflects that.

Service Signature Returns Notes
Console.init() () → void - Sets cursor to (0, 0); writes default palette (CGA 16-color); enables CTRL bit 0; clears tile-map RAM to space-on-black
Console.clear() () → void - Fills tile-map RAM with (0x20 << 8) | current_color (space char on current fg/bg); resets cursor to (0, 0)
Console.printChar(ch) (int) → void - Writes ASCII ch (low 7 bits) into the cell at current cursor; advances cursor; wraps at column 80; scrolls at row 30; honours \n (0x0A) for newline + \r (0x0D) for carriage return
Console.printString(s) (String) → void - Iterates String.length(s) calls of printChar; library-composition demo
Console.println(s) (String) → void - printString(s) then printChar(0x0A); the convention every later capstone uses
Console.setCursor(row, col) (int, int) → void - Direct cursor placement; clamps row ∈ [0..29], col ∈ [0..79]; out-of-range silently clamped (not trapped)
Console.setColor(fg, bg) (int, int) → void - Updates current-fg/bg state; subsequent printChar uses it; fg, bg[0..15] palette indices
Console.scroll() () → void - Shifts all rows up by one; bottom row cleared to space-on-current-color; called automatically on cursor-overflow but exposed for capstone use
Console.setPalette(idx, rgb565) (int, int) → void - Writes 16-bit RGB565 color into palette[idx] register at 0x80100100 + idx*4; advanced; default palette is the canonical CGA 16-color set so most students never touch this

Calling convention. Same Ch 8 left-to-right stack-push + jalr + void-returns-zero convention every other stdlib module obeys. No special-case for Console. OS code is user code at a different address (per Ch 12 §12.3 + §12.3.2 framing).

Hardware-side memory map (sourced from peripheral-ip-pack/hdl/hdmi_tile_map/hdmi_tile_map_axi.v + sw/hdmi_demo/hdmi_tile_map.h):

Offset (relative to 0x80100000) Width Name Direction Purpose
+0x000 32 CTRL R/W bit 0 = enable; bits 31:1 reserved (read-as-zero)
+0x004 32 STATUS R bit 0 = in_active (asserted while video region scanning); bits 31:1 reserved
+0x008 32 FRAME R Frame counter (incremented once per vblank in clk_core domain)
+0x100..+0x13C 32 (× 16 entries) palette[0..15] R/W Low 16 bits = RGB565; high 16 bits ignored. Default = CGA 16-color (palette[0] = black; palette[15] = white)
+0x4000..+0x6FFC 32 (× 2400 entries) tile_map_ram[0..2399] R/W cell_index = row × 80 + col (row ∈ [0..29], col ∈ [0..79]); cell_value = (char_code << 8) | (fg << 4) | bg; char_code = 7-bit ASCII (low 7 bits used by char_rom); fg, bg = 4-bit palette indices

Cell encoding (central for Lab 11.3 + Lab 12.X capstones writing direct cell values):

bit:  31         16 15        8 7    4 3    0
      ┌────────────┬───────────┬──────┬──────┐
        reserved   char_code   fg    bg  
        (ignored)   (ASCII)   (4b)  (4b) 
      └────────────┴───────────┴──────┴──────┘

16-color palette (CGA/EGA canonical). Default values written at Console.init time:

idx Name RGB565 idx Name RGB565
0 Black 0x0000 8 Dark gray 0x52AA
1 Blue 0x0010 9 Light blue 0x435F
2 Green 0x0400 10 Light green 0x07E6
3 Cyan 0x0410 11 Light cyan 0x07FF
4 Red 0x8000 12 Light red 0xF9C7
5 Magenta 0x8010 13 Light magenta 0xF81F
6 Brown 0x8200 14 Yellow 0xFFE6
7 Light gray 0xC638 15 White 0xFFFF

Cross-chapter cross-cuts:

CSA-201 evolution. Adds Console.printInt(n), Console.printHex(n), formatted output Console.printf(...). The CGA/EGA palette opens to a full 6-6-5 palette-RAM model (256-color or beyond). Tile-map RAM expands to a paged-tile model (multiple foreground/background tile sets selectable per cell). Most CSA-201 driver-track work targets the CSA-101 tile-map peripheral as the substrate.


Screen. Pixels, lines, rectangles, circles

Pixel-mode counterpart to Console (§above). Console is text/tile-mode @ MMIO 0x80100000; Screen is pixel-mode @ MMIO 0x80000000. The two peripherals coexist; capstones may use either or both.

Drawing primitives over the IP Pack's HDMI framebuffer. Logical surface: 96 × 64 pixels @ 16 bits-per-pixel (RGB565); virtual framebuffer in OS .bss (12 KiB); IP Pack physical framebuffer at 0x80000000.

Service Signature Returns Notes
Screen.init() () → void - Zeroes virtual framebuffer; initialises dirty-row tracking
Screen.clear() () → void - Fills virtual framebuffer with current background color (~1,500 sw instructions)
Screen.setColor(c) (int) → void - Updates foreground-color register; subsequent drawPixel(x, y, sentinel) uses it
Screen.drawPixel(x, y, color) (int, int, int) → void - Read-modify-write on containing 32-bit word; out-of-bounds silently dropped
Screen.drawLine(x1, y1, x2, y2, color) (int, int, int, int, int) → void - Bresenham midpoint algorithm; correct in all 8 octants; integer-only
Screen.drawRectangle(x, y, w, h, color) (int, int, int, int, int) → void - 4 calls to drawLine (top/bottom/left/right edges)
Screen.drawCircle(cx, cy, r, color) (int, int, int, int) → void - Bresenham midpoint circle; 8-octant symmetry

Color encoding (RGB565):

Read-modify-write hazard (Ch 12 §12.6.5): two pixels per 32-bit word means every drawPixel is an RMW; under preemption (CSA-201) this becomes a torn-write hazard requiring atomic primitives. Virtus OS v1 has no concurrency, so the hazard is a forward-pointer, not a present problem.

CSA-201 evolution. Adds Screen.fillRectangle, Screen.drawTriangle, Screen.blit(src, dst, w, h) for sprite blits, and the atomic-write primitives that close the RMW hazard. CSA-301's con-101 retro-FPU course adds floating-point support for proper geometry math.


GamePad: DS2 controller polling

Renamed from Keyboard. The service interface and decoder pattern are identical; the canonical hardware target is the DS2 controller (PMOD_DS2x2 ships with the lab kit; SNES adapter scope was dropped). The 12-button MVP mapping below is a strict subset of the DS2's richer state (analog sticks + analog pressure + 8 face buttons); forward-stretch labs in CSA-201 + CON-101 expose the richer state. The original 8-button SNES set (B/Y/Select/Start/Up/Down/Left/Right) maps as a strict subset of the 12-button surface; SNES historical hardware is anchored in the Ch 5 §5.7 Architecture Comparison Sidebar (8-bit/PPU/PSG era), where it stays as historical pedagogy. The PS/2 Keyboard service is documented separately.

Polled (not interrupt-driven in v1) at the IP Pack's dedicated GamePad decoder at AXI4-Lite slave window 0x80140000 (per Findings §25.2 canonical address map; M0.5 deferred per A7. Base reserved). Bitmap layout matches the DS2 gamepad's 12-button MVP roster (physical DS2 has 8 face buttons + 4 directional + analog L1/L2/R1/R2. Full surface deferred to CSA-201 driver track). (Pre-Audit-#2 placeholder address 0x80020000 superseded 2026-04-30 by canonical manifold-aligned address; see also handouts/cross-chapter-axi-manifold-base-addresses.md for the full MMIO map.)

Service Signature Returns Notes
GamePad.init() () → void - Zeroes 4-frame state-history buffer for software debounce
GamePad.readButton(n) (int) → int 1 if button n pressed, 0 otherwise Single-bit query; n0..15
GamePad.poll() () → int 16-bit button bitmap (raw) lw from 0x80140000 (canonical per Findings §25.2; M0.5 deferred per A7)
GamePad.pollDebounced() () → int 16-bit rising-edge bitmap 4-frame state-machine; only emits press events on 0→1 transition with prior 3 frames = 0

DS2 button bit layout (12-button MVP mapping):

Bit Button Bit Button
0 × (Cross / "B" in SNES analog) 8 △ (Triangle / "X" in SNES analog)
1 □ (Square / "Y" in SNES analog) 9 ○ (Circle / "A" in SNES analog)
2 Select 10 L1
3 Start 11 R1
4 Up 12 L2 (analog; 0/1 in MVP)
5 Down 13 R2 (analog; 0/1 in MVP)
6 Left 14 L3 (analog-stick click)
7 Right 15 R3 (analog-stick click)

SNES historical mapping (for capstones porting retro game-controller protocols): the 8-button SNES set (B/Y/Select/Start/Up/Down/Left/Right) maps directly to bits 0-7 of the DS2 surface above (DS2 face buttons × / □ on bits 0/1; D-pad on bits 4-7; Select / Start on bits 2/3). DS2 bits 8-15 expose the richer state SNES did not have. Per feedback_hardware_superset_mapping.md: provide a mapping/adapter at the firmware/IP-Pack layer rather than rewriting the curriculum to match the controller's richer surface.

CSA-201 evolution. Adds interrupt-driven gamepad (GamePad.IRQ_handler) that pushes events into a ring buffer; CSA-201's driver-track lab opens the GamePad decoder itself (Verilog reimplementation from DS2 protocol datasheet). Adds analog-stick + analog-pressure exposure once 12-button MVP is operational. CSA-301 adds USB-HID gamepad + USB-HID keyboard support.


Sound: VCP coprocessor commanding

Heterogeneous-multi-processor service. The Virtus Co-Processor (VCP) runs concurrently with the main RV32I-Lite CPU; communication via shared memory at 0x80010000 plus an IRQ line.

Service Signature Returns Notes
Sound.init() () → void - Initialises VCP shared-memory region; registers IRQ handler
Sound.play(samples_ptr, length) (int, int) → void - Writes 6 words to VCP shared memory; VCP starts playing within 1 cycle; main CPU returns immediately
Sound.IRQ_handler() () → void - (Internal. Wired by crt0.S) Refills sample buffer on VCP underrun; supports double-buffered streaming

Sample format: 16-bit signed PCM at 22 kHz.

Cycle-counter measurement. Audio is free, with-audio cycles equal without-audio cycles within measurement noise. This is the property that makes coprocessors valuable in production embedded silicon. Lab 12.4 Part B confirms.

Shared-memory region (8 fields × 4 bytes):

Offset Field Direction
0x80010000 samples_ptr CPU → VCP
0x80010004 length CPU → VCP
0x80010008 read_position VCP → CPU
0x8001000C go CPU → VCP
0x80010010 irq_status VCP → CPU
0x80010014 irq_ack CPU → VCP
0x80010018 repeat CPU → VCP
0x8001001C volume CPU → VCP

Bootstrap-time microcode load (informational; not part of the Sound API). The VCP MMIO peripheral region is 512 B (0x80010000-0x800101FF); the 32 B shared-memory register file above occupies +0x000..+0x01F, a 32 B Control Region (CPU → VCP local_ram[0x00..0x1F] mirror. Industry-pattern doorbell + scratchpad / NVMe submission queue / NIC descriptor ring; cross-cut #1 hybrid C+B confirmed 2026-04-28) occupies +0x020..+0x03F, the 256 B microcode RAM occupies +0x100..+0x1FF, and +0x040..+0x0FF is reserved. CPU writes microcode bytes to the microcode-RAM window during boot before issuing the first go; this is one-time bootstrap done by crt0.S, not by Sound.play. CPU writes Sound.play parameters (samples_ptr / length / volume / repeat) to the Control Region; HDL mirrors into VCP local data RAM; microcode reads with normal ld. See virtus-peripheral-ip-pack/docs/vcp-design-memo.md §5 + vca-csa-101/docs/bus-protocol.md VCP MMIO sub-map.

CSA-201 evolution. Adds Sound.stop, Sound.setVolume, multi-channel mixing (VCP microcode rewrite required); con-101 adds full-band DSP via the Virtus Console retro-FPU. CSA-301 (advanced electives) explores multi-coprocessor offload patterns.


Sys. System control

System-level utilities. Sys.init is the program entry point called by the bootstrap _start.

Service Signature Returns Notes
Sys.init() () → void - Performs final OS-level setup (currently nothing in v1; reserved for CSA-201's expansion); calls Main.main
Sys.halt() () → void (never returns) Writes any value to system-control halt register at 0x8003000C; FPGA freezes CPU clock
Sys.wait(ms) (int) → void - Busy-loop based on cycle counter at 0x80030000; ~27,000 cycles per ms at 27 MHz
Sys.error(msg_str) (String) → void (never returns) Prints msg_str via Output.printString; halts via Sys.halt

Bootstrap sequence (Ch 12 §12.10.1):

  1. CPU resets at 0x00000000.
  2. _start initialises sp, segment-base registers, BSS.
  3. Library init calls in dependency order: Math.init, Memory.init, Console.init, Screen.init, GamePad.init, Sound.init, GPIO.init (renamed from Keyboard.init per CR2; expanded to canonical 9-primary-module init order per CR1/CR2/CR3 baseline).
  4. Wires IRQ vector at _irq_vector.
  5. Calls Sys.init.
  6. Sys.init calls Main.main.
  7. Main.main runs the user's program.
  8. Main.main returns (or calls Sys.halt).
  9. Control returns to _start's _halt: label.
  10. Sys.halt writes the system-control register; CPU clock freezes.

CSA-201 evolution. Adds Sys.fork, Sys.exec, Sys.exit (process-management; requires preemption + scheduler); Sys.error becomes structured exception with stack-unwind. ecall mechanism replaces direct-call into Sys services for the user/supervisor split.


GPIO. General-purpose I/O escape hatch

GPIO is the canonical escape hatch for student capstones that need to drive arbitrary external hardware (LEDs, switches, sensors, breadboard-prototyped peripherals) without authoring a dedicated stdlib service per device. AXI4-Lite slave window at MMIO base 0x80130000, 16-bit pin width per silicon default (N_PINS=16 parameter in gpio_axi.v; M0.5 widening to 24/32-bit is a 1-line parameter change per HDL header).

Service Signature Returns Notes
GPIO.read(pin) (int) → int 1 if pin's IN bit is high, 0 otherwise Reads bit pin of IN register at 0x80130008; pin ∈ [0..15]; out-of-range returns 0 (not trapped)
GPIO.write(pin, value) (int, int) → void - Writes bit pin of OUT register at 0x80130004; only takes effect when DIR[pin] = 1 (output); value ∈ {0, 1}; non-zero treated as 1
GPIO.setDirection(pin, dir) (int, int) → void - Writes bit pin of DIR register at 0x80130000; dir = 0 → input, dir = 1 → output; default (post-init) is all-input
GPIO.pollEdge(pin) (int) → int 1 if edge fired since last poll, 0 otherwise; ALSO clears the sticky bit (W1C) Reads bit pin of INT_STATUS register at 0x8013000C, returns it, then writes a 1 to clear (write-1-to-clear convention per HDL)
GPIO.init() () → void - Sets DIR to all-input (defensive default; prevents hot-plugged peripherals from being driven before student configures direction); clears INT_STATUS

Calling convention. Same Ch 8 left-to-right stack-push + jalr + void-returns-zero convention every other stdlib module obeys. No special-case for GPIO. OS code is user code at a different address (per Ch 12 §12.3 + §12.3.2 framing).

Hardware-side memory map:

Offset (relative to 0x80130000) Width Name Direction Purpose
+0x000 32 (low 16 used; bits 31:16 reserved) DIR R/W Per-pin direction; bit n = direction of pin n (0 = input, 1 = output). Reset value = 0x00000000 (all-input by default)
+0x004 32 (low 16 used) OUT R/W Per-pin output value; drives pin n when DIR[n] = 1. Reset value = 0x00000000
+0x008 32 (low 16 used) IN R Per-pin sampled input value (synchronized through 2 flip-flops to cross from external clock domain)
+0x00C 32 (low 16 used) INT_STATUS R/W1C Per-pin edge-trigger sticky flag; bit n = 1 if edge detected on pin n since last clear; write 1 to clear the bit (W1C convention); ORed across all bits drives irq output to CPU (M0-2 stretch; INT_ENABLE per-pin masking deferred to M0.5 per HDL header)

Direction encoding (DIR register; per-pin):

bit:  31           16 15  ... 1   0
      ┌─────────────┬─────────────┐
        reserved     pin direction    0 = INPUT  (DIR_INPUT)
        (RAZ; WI)    (1 bit/pin)      1 = OUTPUT (DIR_OUTPUT)
      └─────────────┴─────────────┘

Helper macro: gpio_pin_mask(pin) = 1u << pin. Used to construct read-modify-write masks for setting/clearing individual bits across DIR / OUT / INT_STATUS.

Cross-chapter cross-cuts:

CSA-201 evolution. Adds INT_ENABLE per-pin masking register at +0x010 (deferred from M0-2 per HDL header note); adds GPIO.installInterruptHandler(pin, handler_addr) that registers a handler called from Sys.IRQ_dispatch when INT_STATUS[pin] fires. Widens N_PINS parameter from 16 → 24 or 32 (1-line HDL change). GPIO becomes the canonical example of a stdlib service that needs careful permission gating once CSA-201 introduces the U/S-mode privilege boundary. Driving a pin that's wired to a power supply or a destructive output (motor controller, high-current LED array, irreversible-write peripheral) without permission gating is a real safety hazard. Students who care about Lab 12.5 capstone safety care about CSA-201's GPIO trap path: ecall traps to S-mode, the kernel checks the per-process GPIO permission bitmap, and either dispatches GPIO.write or returns -1. The Sys.error channel from §12.3.2 is what carries the failure case.


String. Heap-allocated immutable-ish strings

Helper module. Strings are objects with a length field, maxLength field, and flat character buffer. The compiler relies on String.appendChar for string-literal expansion (Ch 11 §11.5).

Service Signature Returns Notes
String.new(maxLength) (int) → String New String object Memory.alloc-backed; sets length=0
String.dispose(s) (String) → void - Memory.dealloc
String.length(s) (String) → int Current character count
String.charAt(s, j) (String, int) → int ASCII character at index j
String.setCharAt(s, j, c) (String, int, int) → void -
String.appendChar(s, c) (String, int) → String s (returns receiver) Returns receiver to enable method chaining. See Ch 11 §11.5.1 for compiler's chained-emission discipline
String.eraseLastChar(s) (String) → void -
String.intValue(s) (String) → int Parses leading decimal digits
String.setInt(s, i) (String, int) → void - Mutates s to decimal representation of i

Compiler relies on the contract: String.appendChar must return its receiver. If the OS implements it as void, the compiler's chained-appendChar emission for string literals fails.

13-character literal "hello, world!" emits ~27 VM commands (one String.new, 13 String.appendChar chain). See Ch 11 §11.5 + the bloat reckoning at §11.9.

CSA-201 evolution. Strings become interned via constant-pool optimisation in the compiler; production-runtime equivalent of Java's String.intern(). Reduces 13-char literal emission from ~27 VM commands to 1 (push constant <interned-pointer>).


Array. Heap-allocated fixed-size arrays

Helper module. Thin wrappers over Memory.alloc / Memory.dealloc.

Service Signature Returns Notes
Array.new(size) (int) → Array Heap-allocated array of size words Memory.alloc(size * 4) underneath
Array.dispose(a) (Array) → void - Memory.dealloc(a)

Element access is via VM that segment indexing. let arr = Array.new(10); let arr[3] = 42; translates to pop pointer 1; pop that 0 per Ch 7 §7.6.3 idioms.

CSA-201 evolution. Adds Array.length, Array.copy, Array.fill, bounds checking. CSA-301 introduces List, Map, Set as standard collections.


Forward-compatibility note (CSA-201 / CSA-301 / con-101)

The 14-service v1 surface from Ch 11 §11.2 is the minimum the curriculum supports. CSA-201's Virtus OS v2 grows by ~30%, mostly in two directions:

Direction Why Examples
Privilege-separated services ecall + supervisor mode Sys.fork / Sys.exec / process-isolated Memory.alloc
Higher-level abstractions Richer language tier Output.printf / Math.sin (FP) / String.intern / List.add

The compiler does not need to change for most of these, the standard-library registry (Ch 11 §11.2.1) is just a dictionary the student extends. Stable IR + extensible library = the architecture every modern language ecosystem ships.

con-101's Virtus Console retro-FPU introduces F-extension floating-point support; Math.sin, Math.cos, Math.log arrive there. CSA-301's GUI library introduces window/widget services on top of Screen; the structure is Screen + retained-mode tree of objects + event dispatch.


OS-side implementation size budget

Library Approximate size (RV32I-Lite instructions) Lab
Math ~120 12.1
Memory ~180 12.2
String ~150 (paired w/ 12.2)
Array ~30 (paired w/ 12.2)
Output ~60 (font glyph blit) (paired w/ 12.3)
Screen ~220 12.3
Keyboard ~80 12.4 (Part A)
Sound ~120 (+ ~50 VCP microcode) 12.4 (Part B)
Sys ~60 (init in §12.10)
Total ~1,020 RV32I-Lite instructions
Plus bootstrap crt0.S ~30 (supplied)
Grand total ~1,050 instructions / ~1,500 source lines

A student reading at five lines per minute can read the whole OS in an afternoon. This is the smallest scale at which a working operating system can exist.


Where to read more