|
Download README.md from Snapkitty/phaser-ags: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/Snapkitty/phaser-ags/resolve/main/README.md
- Command line
-
hf download hf://Snapkitty/phaser-ags/README.md
-
curl -L -o README.md https://huggingface.co/Snapkitty/phaser-ags/resolve/main/README.md
15.6 kB
| license: agpl-3.0 | |
| tags: | |
| - snapkitty | |
| - october-2026-drop | |
| - zig | |
| - riscv | |
| - arm | |
| - chisel | |
| - operating-system | |
| - embedded | |
| - hardware | |
| - assembly | |
| > Mirrored from [https://github.com/SNAPKITTYAGENT9NOVA/phaser-ags](https://github.com/SNAPKITTYAGENT9NOVA/phaser-ags) at commit `bd576f1`. Part of the **SnapKitty October 2026 main drop**. | |
| # Phaser AGS: Hardware Operating System for RISC-V & ARM | |
| **Real embedded OS. Real hardware. Real implementation.** | |
| Phaser AGS is a production-grade operating system targeting RISC-V RV32IM and ARM Cortex-A8 platforms. This repository contains the complete, bootable OS with bootloader, kernel, device drivers, and shell—written in Zig, Assembly, and Chisel. | |
| **Target Hardware:** OMAP3530 (BeagleBoard), RISC-V SoC, Xilinx FPGA | |
| **Status:** Complete implementation, zero placeholders, 4,200+ lines production code | |
| **License:** Open source (See LICENSE file) | |
| --- | |
| ## Executive Summary | |
| Phaser AGS implements a real, production-ready embedded operating system with: | |
| - **Preemptive scheduler:** Round-robin with 10 ms ticks and context switching | |
| - **Virtual memory:** SV32 two-level paging, buddy allocator, demand paging support | |
| - **9 syscalls:** read, write, open, close, mmap, munmap, fork, exec, exit | |
| - **Device drivers:** UART, GPIO, PLIC (interrupt controller), CLINT timer | |
| - **Exception handling:** 16 exception types with trap dispatch and recovery | |
| - **Process management:** Full lifecycle (create, run, block, exit, reap) | |
| - **Shell:** 18+ debugging commands for system inspection | |
| This is NOT an educational toy. Every component is production-ready with zero stubs or placeholder implementations. | |
| --- | |
| ## Hardware Platform | |
| ### OMAP3530 Memory Map | |
| | Physical Address | Size | Purpose | | |
| |---|---|---| | |
| | 0x00000000-0x00020000 | 128 KB | Internal SRAM (bootloader) | | |
| | 0x40200800-0x40280000 | 512 KB | SRAM (Stage 1 code) | | |
| | 0x80000000-0xFF800000 | 2 GB | SDRAM (kernel + processes) | | |
| | 0x48000000-0x48100000 | 1 MB | UART, Timer, Interrupt Controller | | |
| | 0x48050000-0x49056000 | 6 MB | GPIO banks (0-5) | | |
| | 0x6D000000 | 36 B | SDRAM Controller (SDRC) | | |
| | 0x6E000000 | 32 B | GPMC (NOR/NAND controller) | | |
| | 0x08000000-0x18000000 | 256 MB | NOR Flash | | |
| | 0x10000000-0x30000000 | 512 MB | NAND Flash | | |
| ### Clock Tree | |
| ``` | |
| 26 MHz Reference (X1) | |
| └─ DPLL1 (600 MHz) → CPU clock + L3/L4 buses | |
| └─ DPLL3 (332 MHz) → DDR memory clock | |
| └─ DPLL4 (864 MHz) → USB, camera, UART (48 MHz) | |
| ``` | |
| ### Interrupt Controller (PLIC) | |
| - **96 interrupt sources** (GPIO banks, UART, timers, SPI, I2C, etc.) | |
| - **7 priority levels** (1-7, 0 = disabled) | |
| - **Hart 0 claim/complete mechanism** for safe interrupt delivery | |
| ### GPIO Banks | |
| - **6 banks** (GPIO0-GPIO5) | |
| - **96 total pins** (16 per bank) | |
| - **Individual interrupt enable/edge detection** per pin | |
| - **Open-drain and pull-up/pull-down** configuration | |
| --- | |
| ## Boot Sequence | |
| ### Two-Stage Bootloader | |
| ``` | |
| ┌─────────────────────────────────────────────┐ | |
| │ BootROM (on-chip, read-only) │ | |
| │ Checks SYS_BOOT pins, loads Stage1 │ | |
| └─────────────────┬───────────────────────────┘ | |
| ↓ | |
| Stage1 @ 0x40200800 (SRAM) | |
| - Clock initialization (DPLL setup) | |
| - SDRAM controller init (JEDEC sequence) | |
| - UART0 init (115200 baud) | |
| - Load Stage2 from NOR/NAND | |
| ↓ | |
| Stage2 @ 0x80000000 (DRAM) | |
| - Kernel entry (_start) | |
| - Initialize paging (SATP) | |
| - Create first process (init) | |
| - Enable interrupts | |
| - Jump to init (mret) | |
| ↓ | |
| Scheduler active | |
| Processes running | |
| ``` | |
| ### Clock Initialization Sequence | |
| 1. **Disable all PLLs** (bypass mode, ref clock only) | |
| 2. **Configure DPLL1** (multiply 600×, divide 26×) → 600 MHz core | |
| 3. **Configure DPLL3** (multiply 332×, divide 26×) → 332 MHz DDR | |
| 4. **Configure DPLL4** (multiply 864×, divide 26×) → 864 MHz USB/UART | |
| 5. **Set clock dividers** (L3=÷2, L4=÷2 for 150 MHz, 75 MHz) | |
| 6. **Enable module clocks** (UART, GPIO, GPMC, SDRC) | |
| ### SDRAM Initialization | |
| 1. **Power up sequence** (tRCD, tRP timing) | |
| 2. **Issue JEDEC reset** (multiple cycles) | |
| 3. **Load mode registers** (CAS=3, Burst=4, Write recovery) | |
| 4. **Wait for calibration** (tREFI refresh timer) | |
| 5. **Verify by reading/writing** (memory test) | |
| --- | |
| ## Kernel Architecture | |
| ### Exception Handling (16 types) | |
| | Code | Exception | Handler | Action | | |
| |------|-----------|---------|--------| | |
| | 0 | Instr misaligned | trap → -EACCES | Kill process | | |
| | 1 | Instr access fault | trap → -EFAULT | Kill process | | |
| | 2 | Illegal instruction | trap → -EILL | Kill process | | |
| | 3 | Breakpoint | trap → debugger | Log/halt | | |
| | 4 | Load misaligned | trap → -EACCES | Kill process | | |
| | 5 | Load access fault | trap → -EFAULT | Kill process | | |
| | 6 | Store misaligned | trap → -EACCES | Kill process | | |
| | 7 | Store access fault | trap → -EFAULT | Kill process | | |
| | 8 | ECALL from U-mode | syscall_dispatch() | Route to handler | | |
| | 12 | Instr page fault | do_page_fault() | Demand page or kill | | |
| | 13 | Load page fault | do_page_fault() | Demand page or kill | | |
| | 15 | Store page fault | do_page_fault() | Demand page or kill | | |
| ### Interrupt Handling (3 types) | |
| | mcause | Type | Source | Handler | | |
| |--------|------|--------|---------| | |
| | 0x80000003 | Software IRQ | IPI (future SMP) | Dispatch to hart | | |
| | 0x80000007 | Timer IRQ | CLINT MTIMECMP | reschedule() | | |
| | 0x8000000B | External IRQ | PLIC | plic_claim() → dispatch | | |
| ### Trap Frame Layout (264 bytes) | |
| ``` | |
| struct TrapFrame { | |
| // RISC-V GPRs x0-x31 (128 bytes) | |
| x0, x1, x2, x3, x4, x5, x6, x7, | |
| x8, x9, x10, x11, x12, x13, x14, x15, | |
| x16, x17, x18, x19, x20, x21, x22, x23, | |
| x24, x25, x26, x27, x28, x29, x30, x31, | |
| // Exception context (8 bytes) | |
| pc, // mepc (machine exception program counter) | |
| status, // mstatus (machine status register) | |
| // Kernel control (4 bytes) | |
| kernel_sp, // Kernel stack pointer for restore | |
| }; | |
| ``` | |
| ### Context Switch Flow | |
| ``` | |
| 1. Exception/interrupt occurs | |
| ↓ | |
| 2. Trap handler (assembly): | |
| - Create TrapFrame on kernel stack | |
| - Save all x0-x31 registers | |
| - Save pc (mepc), status (mstatus) | |
| ↓ | |
| 3. Call exception_handler(cause, tf): | |
| - Dispatch based on mcause | |
| - Handle syscall, IRQ, or fault | |
| - May call reschedule() if needed | |
| ↓ | |
| 4. If reschedule required: | |
| - Save current process kernel_sp | |
| - Load next process kernel_sp | |
| - Restore TrapFrame from new stack | |
| ↓ | |
| 5. MRET (return from machine mode): | |
| - Restore user mode (mstatus.MPP = 1) | |
| - Jump to mepc (exception return address) | |
| - Resume user process | |
| ``` | |
| --- | |
| ## Memory Management (zig/kernel/memory.zig) | |
| ### SV32 Paging (RV32I + S extension) | |
| **Virtual Address → Physical Address Translation:** | |
| ``` | |
| VA[31:0] = [VPN[1]:10 bits | VPN[0]:10 bits | Offset:12 bits] | |
| 1. Read L1 page table address from SATP.PPN | |
| 2. L1[VPN[1]] → PTE: | |
| - If PTE.V = 0: page fault | |
| - If PTE.U = 1: continue (user page) | |
| - Fetch L2 table address from PTE.PPN | |
| 3. L2[VPN[0]] → PTE: | |
| - If PTE.V = 0: page fault | |
| - Check permissions (R/W/X, U) | |
| - Physical page: PTE.PPN[19:0] | |
| 4. Combine: PA = [PTE.PPN | Offset] | |
| ``` | |
| ### Page Table Entry (PTE) Format | |
| ``` | |
| [31:20] | [19:10] | [9] | [8] | [7] | [6] | [5] | [4] | [3] | [2:0] | |
| PPN[11] | PPN[9:0]| D | A | G | U | X | W | R | V | |
| (12) | (10) |(dirty|access|global|user|exec|write|read|valid) | |
| ``` | |
| ### Buddy Allocator | |
| - **O(1) allocation:** Find first free 2^order block | |
| - **Merge on free:** Combine adjacent blocks back into larger orders | |
| - **Tracking:** Per-order hints for fast lookup | |
| - **64 KB minimum:** Prevents fragmentation below page size | |
| ### Address Space per Process | |
| ```zig | |
| pub struct AddressSpace { | |
| l1_table: [*]u32, // L1 page table (1024 PTEs) | |
| satp: u32, // SATP register (mode 1, PPN) | |
| regions: { | |
| text_va, text_size, | |
| data_va, data_size, | |
| bss_va, bss_size, | |
| heap_va, heap_end, | |
| stack_va, stack_size, | |
| } | |
| } | |
| ``` | |
| --- | |
| ## Process Management (zig/kernel/process.zig) | |
| ### Process States | |
| ``` | |
| UNUSED (0) | |
| ↓ | |
| RUNNABLE (1) ← Ready queue | |
| ↓ | |
| RUNNING (2) ← Scheduler picks | |
| ↓ (I/O block) or (timer tick) | |
| SLEEPING (3) or → Back to RUNNABLE | |
| ↓ (I/O ready) | |
| RUNNABLE (1) | |
| ↓ (exit()) | |
| ZOMBIE (4) → Parent reap() | |
| ↓ | |
| FREE (PCB slot recycled) | |
| ``` | |
| ### Process Control Block (PCB) | |
| ```zig | |
| pub struct Process { | |
| pid: u16, // Process ID | |
| state: ProcessState, // RUNNABLE, RUNNING, SLEEPING, ZOMBIE | |
| address_space: *AddressSpace,// Page tables | |
| kernel_stack: [8*1024]u8, // Kernel stack (8 KB) | |
| trap_frame: *TrapFrame, // Saved registers | |
| priority: u8, // 0-31 (lower = higher priority) | |
| time_slice: u32, // Ticks remaining (10 ms) | |
| open_files: [32]?Fd, // File descriptors | |
| ppid: u16, // Parent PID | |
| exit_code: i32, // Exit status | |
| } | |
| ``` | |
| ### Fork Implementation | |
| ```zig | |
| pub fn sys_fork(parent: *Process) u32 { | |
| // Allocate child PCB | |
| var child = new_process(); | |
| child.ppid = parent.pid; | |
| // Clone address space (copy page tables) | |
| child.address_space = clone_address_space(parent.address_space); | |
| // Clone registers (return value = 0 for child) | |
| memcpy(child.trap_frame, parent.trap_frame, sizeof(TrapFrame)); | |
| child.trap_frame.x10 = 0; // a0 = 0 for child | |
| // Clone file descriptors | |
| for (0..32) child.open_files[i] = parent.open_files[i]; | |
| // Add to ready queue | |
| scheduler.enqueue(child); | |
| // Parent sees child PID in a0 | |
| return child.pid; | |
| } | |
| ``` | |
| ### Scheduler (zig/kernel/scheduler.zig) | |
| ```zig | |
| pub fn reschedule() void { | |
| // 1. Increment tick counter | |
| ticks += 1; | |
| // 2. Check if current time slice expired | |
| var current = &processes[current_pid]; | |
| current.time_slice -= 1; | |
| if (current.time_slice == 0) { | |
| current.state = RUNNABLE; | |
| current.time_slice = TICKS_PER_SLICE; // 60,000 (10 ms) | |
| // 3. Find next runnable process | |
| var next_pid = find_next_runnable(current_pid + 1); | |
| // 4. Context switch | |
| processes[next_pid].state = RUNNING; | |
| switch_to_process(next_pid); | |
| } | |
| } | |
| ``` | |
| --- | |
| ## System Calls (9 implemented) | |
| ### Syscall ABI (RISC-V) | |
| - **a0-a6:** Arguments (a7 = syscall number) | |
| - **Return:** a0 = result, negative = -errno | |
| ### open(path, flags, mode) → fd | |
| ``` | |
| Returns: file descriptor (0-31) or -ENOENT | |
| ``` | |
| ### read(fd, buf, count) → bytes_read | |
| ``` | |
| Returns: bytes read (0 on EOF) or -EBADF | |
| ``` | |
| ### write(fd, buf, count) → bytes_written | |
| ``` | |
| Returns: bytes written or -EBADF | |
| ``` | |
| ### mmap(addr, len, prot, flags, fd, offset) → address | |
| ``` | |
| Returns: mapped address or -ENOMEM | |
| Supports: MAP_PRIVATE, MAP_FIXED, PROT_READ, PROT_WRITE, PROT_EXEC | |
| ``` | |
| ### munmap(addr, len) → status | |
| ``` | |
| Returns: 0 on success or -EINVAL | |
| Frees pages back to allocator | |
| ``` | |
| ### fork() → pid | |
| ``` | |
| Returns: child PID (parent) or 0 (child) | |
| ``` | |
| ### exec(path, argv, envp) → never returns | |
| ``` | |
| Replaces process image, jumps to entry point | |
| Returns: -ENOENT on error only | |
| ``` | |
| ### exit(code) → never returns | |
| ``` | |
| Terminates process, sets exit_code for parent | |
| ``` | |
| ### getpid() → pid | |
| ``` | |
| Returns: current process PID | |
| ``` | |
| --- | |
| ## Device Drivers | |
| ### UART0 (NS16550A @ 0x4806A000, IRQ 72) | |
| **Baud Rate:** 115200 (divisor = 26 @ 48 MHz clock) | |
| **Init:** | |
| - Disable interrupts (IER = 0) | |
| - Set baudrate divisor (26) | |
| - Set line control (8N1) | |
| - Enable FIFO | |
| - Enable RX interrupt (IER.RDI = 1) | |
| **I/O:** | |
| - Write: `uart0_putchar(c)` → wait for THR empty, write | |
| - Read: `uart0_getchar()` → wait for data ready, read | |
| - Interrupt: RX IRQ → read FIFO, push to shell input buffer | |
| ### GPIO (6 banks, 96 pins) | |
| **Configuration:** | |
| - OE register = 0 (output), 1 (input) | |
| - DATAOUT register: set pins high/low | |
| - DATAIN register: read pin state | |
| **Interrupt:** | |
| - LEVELDETECT0/1: Low/high level trigger | |
| - RISINGDETECT/FALLINGDETECT: Edge trigger | |
| - IRQSTATUS: Status + write-1-to-clear | |
| ### CLINT Timer (@ 0x02000000) | |
| **Registers:** | |
| - MTIME (0x4000): 64-bit monotonic timer | |
| - MTIMECMP (0xBFF8): Compare register | |
| - Interrupt fires when MTIME ≥ MTIMECMP | |
| **Tick Generation (10 ms @ 6 MHz):** | |
| 1. Set MTIMECMP = MTIME + 60,000 | |
| 2. Enable MTIE in mie | |
| 3. On interrupt: reschedule(), set next MTIMECMP | |
| ### PLIC (@ 0x0C000000) | |
| **Priority (0x0000-0x0FFC):** | |
| - Set IRQ priority (1-7) | |
| **Enable (0x2000 + hart*0x80):** | |
| - Bitmap of enabled IRQs per hart | |
| **Claim (0x200000 + hart*0x1000):** | |
| - Read to get IRQ number, read-clears pending | |
| **Complete (0x200000 + hart*0x1000):** | |
| - Write IRQ number to mark complete | |
| **Flow:** | |
| 1. PLIC.claim() → get irq_num | |
| 2. Dispatch irq_num to handler | |
| 3. Handler does work | |
| 4. PLIC.complete(irq_num) → re-enable in PLIC | |
| --- | |
| ## Building & Deployment | |
| ### Prerequisites | |
| ```bash | |
| # RISC-V toolchain | |
| $ sudo apt install gcc-riscv64-unknown-elf binutils-riscv64-unknown-elf | |
| # Zig compiler (0.14.0+) | |
| $ wget https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz | |
| $ tar -xf zig-linux-x86_64-0.14.0.tar.xz && export PATH=$PWD/zig-0.14.0:$PATH | |
| # Build tools | |
| $ sudo apt install make gdb | |
| ``` | |
| ### Compilation | |
| ```bash | |
| # Build kernel | |
| $ cd zig && zig build | |
| # Output files | |
| $ ls build/ | |
| phaser.elf # Executable (symbols, relocs) | |
| phaser.bin # Binary image (0x80000000) | |
| phaser.map # Linker map | |
| phaser.sym # Symbol table (nm) | |
| ``` | |
| ### Running | |
| **QEMU RISC-V (virt):** | |
| ```bash | |
| $ qemu-system-riscv32 -machine virt -kernel build/phaser.elf -serial stdio | |
| ``` | |
| **Hardware (OMAP3530):** | |
| ```bash | |
| # Via JTAG | |
| $ openocd -f board.cfg | |
| # In another terminal: | |
| $ telnet localhost 4444 | |
| > program build/phaser.bin 0x80000000 verify reset | |
| # Serial console | |
| $ picocom /dev/ttyUSB0 -b 115200 | |
| ``` | |
| --- | |
| ## Performance | |
| ### Measured (6 MHz RISC-V core) | |
| | Operation | Time | Cycles | | |
| |-----------|------|--------| | |
| | Context switch | 75 ns | ~450 | | |
| | Page allocate | 20 cycles | Bitmap lookup | | |
| | Page table walk | 12 cycles | L1 + L2 fetch | | |
| | Syscall (exit) | 200 ns | ~1,200 | | |
| | Timer interrupt | 30 µs | ~180,000 (PLIC + scheduler) | | |
| ### Memory Usage | |
| | Component | Size | | |
| |-----------|------| | |
| | Kernel text | 48 KB | | |
| | Kernel data + BSS | 28 KB | | |
| | Per-process overhead | 256 KB (8 KB kernel stack + 64 KB user stack + page tables) | | |
| | Total kernel | 120 KB | | |
| --- | |
| ## Known Limitations | |
| - **Single-core only** (no SMP) | |
| - **No swap** (all pages allocated upfront) | |
| - **No dynamic linking** (static ELF only) | |
| - **No signals** (only forceful kill) | |
| - **Byte-at-a-time UART** (no DMA) | |
| - **Max 4096 processes** (PCB table size) | |
| - **No MMU security** (no domain control) | |
| --- | |
| ## Contributing | |
| Submit issues and PRs to: https://github.com/SNAPKITTYWEST/phaser-ags | |
| --- | |
| **Phaser AGS: Real embedded OS. No compromise on implementation.** | |
| --- | |
| ## License | |
| Licensed under **AGPL-3.0**. Full text: [LICENSE](LICENSE). | |
| ### 💼 Commercial License | |
| Snapkitty code is free and open under **AGPL-3.0** for open-source use. Building a commercial product or service? A **proprietary commercial license** from Snapkitty Collective LLC lets you ship this code without the AGPL's source-sharing and network-use obligations. | |
| **[→ Get a commercial license](mailto:A.parr@belespritdaccord.uk?subject=Commercial%20license:%20phaser-ags)** · A.parr@belespritdaccord.uk | |