diff options
| author | 2026-05-29 21:59:53 -0400 | |
|---|---|---|
| committer | 2026-05-29 22:04:38 -0400 | |
| commit | fe456c2014c8d1d88b7fd5b24ebda7f7b4c53460 (patch) | |
| tree | a1ae3682aede1ea7550f4be76a79118bd6502902 | |
| download | quail-fe456c2014c8d1d88b7fd5b24ebda7f7b4c53460.tar.gz quail-fe456c2014c8d1d88b7fd5b24ebda7f7b4c53460.zip | |
basic physical memory page allocator
Signed-off-by: Matthew Wozniak <me@woz.blue>
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | build.zig | 50 | ||||
| -rw-r--r-- | build.zig.zon | 81 | ||||
| -rw-r--r-- | src/asm/boot.S | 42 | ||||
| -rw-r--r-- | src/asm/trap.S | 6 | ||||
| -rw-r--r-- | src/ld/virt.ld | 46 | ||||
| -rw-r--r-- | src/main.zig | 39 | ||||
| -rw-r--r-- | src/mem.zig | 41 | ||||
| -rw-r--r-- | src/uart.zig | 80 |
9 files changed, 387 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dca1103 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +.zig-cache/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..536762d --- /dev/null +++ b/build.zig @@ -0,0 +1,50 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.resolveTargetQuery(.{ + .cpu_arch = .riscv64, + .abi = .none, + .os_tag = .freestanding, + .ofmt = .elf, + }); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "kernel", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .code_model = .medany, + }), + }); + exe.root_module.addAssemblyFile(b.path("src/asm/boot.S")); + exe.root_module.addAssemblyFile(b.path("src/asm/trap.S")); + exe.linker_script = b.path("src/ld/virt.ld"); + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + const run_cmd = b.addSystemCommand(&.{ + // zig fmt: off + "qemu-system-riscv64", + "-machine", "virt", + "-cpu", "rv64", + "-smp", "4", + "-m", "128M", + // "-drive", "if=none,format=raw,file=hdd.dsk,id=foo", + // "-device", "virtio-blk-device,scsi=off,drive=foo", + "-nographic", + "-serial", "mon:stdio", + "-bios", "none", + "-device", "virtio-rng-device", + "-device", "virtio-net-device", + "-device", "virtio-tablet-device", + "-device", "virtio-keyboard-device", + "-S", "-s", + "-kernel" + // zig fmt: on + }); + run_cmd.addArtifactArg(exe); + run_step.dependOn(&run_cmd.step); + run_cmd.step.dependOn(b.getInstallStep()); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..bc1fe44 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,81 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save <url>`, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .os, + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0x6ab04511e57166b1, // Changing this has security and trust implications. + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.17.0-dev.387+31f157d80", + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save <url>` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/asm/boot.S b/src/asm/boot.S new file mode 100644 index 0000000..bd50b4c --- /dev/null +++ b/src/asm/boot.S @@ -0,0 +1,42 @@ +# bootloader for my kernel +.option norvc +.section .data +.section .text.init +.global _start +_start: + # any harts not bootstrapping need to wait for an IPI + csrr t0, mhartid + bnez t0, 3f + # satp should be zero, but make sure + csrw satp, zero +.option push +.option norelax + la gp, __global_pointer$ +.option pop + # clear bss + la a0, _bss_start + la a1, _bss_end + bgeu a0, a1, 2f +1: + sd zero, (a0) + addi a0, a0, 8 + bltu a0, a1, 1b +2: + # set up stack + la sp, _stack_end + # set kmain to the return address and then return + li t0, (0b11 << 11) | (1 << 7) | (1 << 3) + la t1, kmain + la t2, asm_trap_vector + li t3, 0 # (1 << 3) | (1 << 7) | (1 << 11) + csrw mstatus, t0 + csrw mepc, t1 + csrw mtvec, t2 + csrw mie, t3 + mret + + +# wait for interrupt +3: + wfi + j 3b diff --git a/src/asm/trap.S b/src/asm/trap.S new file mode 100644 index 0000000..672efcd --- /dev/null +++ b/src/asm/trap.S @@ -0,0 +1,6 @@ +.section .text +.global asm_trap_vector +asm_trap_vector: + # We get here when the CPU is interrupted + # for any reason. + mret diff --git a/src/ld/virt.ld b/src/ld/virt.ld new file mode 100644 index 0000000..33532c4 --- /dev/null +++ b/src/ld/virt.ld @@ -0,0 +1,46 @@ +OUTPUT_ARCH( "riscv" ) +ENTRY( _start ) + +MEMORY +{ + ram : org = 0x80000000, len = 128M +} + +SECTIONS +{ + .text : { + _text_start = .; + *(.text.init) + *(.text .text.*) + _text_end = .; + } >ram + + .rodata : { + _rodata_start = .; + *(.rodata .rodata.*) + _rodata_end = .; + } >ram + + .data : { + . = ALIGN(4096); + _data_start = .; + *(.sdata .sdata.*) + *(.data .data.*) + _data_end = .; + } >ram + + .bss : { + _bss_start = .; + *(.sbss .sbss.*) + *(.bss .bss.*) + _bss_end = .; + } >ram + + . = ALIGN(4096); + _stack_start = .; + _stack_end = . + 0x80000; + _heap_start = _stack_end; + _memory_end = ORIGIN(ram) + LENGTH(ram); + _heap_size = _memory_end - _heap_start; +} + diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..7896a09 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,39 @@ +const uart = @import("uart.zig"); +const mem = @import("mem.zig"); +const std = @import("std"); + +fn logFn( + comptime message_level: std.log.Level, + comptime scope: @EnumLiteral(), + comptime format: []const u8, + args: anytype, +) void { + return std.log.defaultLogFileTerminal( + message_level, + scope, + format, + args, + uart.terminal, + ) catch {}; +} + +pub const std_options: std.Options = .{ + .logFn = logFn, + .page_size_max = 4096, + .page_size_min = 4096, +}; + +pub const panic = std.debug.FullPanic(panicFn); + +fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn { + std.log.err("kernel panic at {?}", .{first_trace_addr}); + std.log.err("{s}", .{msg}); + while (true) {} +} + +export fn kmain() callconv(.c) noreturn { + uart.init(0x1000_0000); + std.log.info("hello, world", .{}); + mem.init(); + while (true) {} +} diff --git a/src/mem.zig b/src/mem.zig new file mode 100644 index 0000000..b361b88 --- /dev/null +++ b/src/mem.zig @@ -0,0 +1,41 @@ +const std = @import("std"); + +const Page = extern struct { + next: *Page, +}; + +var head = @extern(*align(4096) Page, .{ .name = "_heap_start" }); + +pub fn init() void { + // how many pages do we have? + const page_count = @intFromPtr(@extern( + *const anyopaque, + .{ .name = "_heap_size" }, + )) / 4096; + + // go through every page and make it point it to the next one + const pages: []Page = @as([*]Page, @ptrCast(head))[0..page_count]; + for (0..page_count - 1) |i| { + pages[i].next = &pages[i + 1]; + } + + std.log.scoped(.mem).info( + "{} physical pages of 4096 B = {} B free", + .{ page_count, page_count * 4096 }, + ); +} + +// allocates 1 page by taking it off the front of the linked list +pub fn alloc() []align(4096) u8 { + const page = @as([*]align(4096) u8, @ptrCast(head))[0..4096]; + head = head.next; + return page; +} + +// free the page by putting it back on the front of the linked list +pub fn free(mem: []align(4096) u8) void { + std.debug.assert(mem.len == 4096); + const page: *Page = @ptrCast(mem.ptr); + page.next = head; + head = page; +} diff --git a/src/uart.zig b/src/uart.zig new file mode 100644 index 0000000..661084c --- /dev/null +++ b/src/uart.zig @@ -0,0 +1,80 @@ +const std = @import("std"); + +var uart_ptr: ?[*]u8 = null; + +pub fn init(base_addr: usize) void { + uart_ptr = @ptrFromInt(base_addr); + // Set the word length to 8 bits. Offset 3 is the LCR (line control + // register). + // + // The bottom 2 bits set the word length: + // 0 = 5 bits + // 1 = 6 bits + // 2 = 7 bits + // 3 = 8 bits + const lcr: u8 = 3 << 0; + uart_ptr.?[3] = lcr; + // Now enable the FIFO, which is the bottom bit of the FIFO control + // register (offset 2) + uart_ptr.?[2] = 1 << 0; + // Enable receiver buffer interrupts, which is the bottom bit of the IER + // (interrupt enable register) + uart_ptr.?[1] = 1 << 0; + // Per the datasheet, we are to set the clock divisor to: + // divisor = ceil(clock_hz/(baud_sps * 16)) + // divisor = ceil(22_729_000/(115200 * 16)) = 13 + const divisor: u16 = 13; + const divisor_least: u8 = divisor & 0xff; + const divisor_most: u8 = divisor >> 8; + // To write the divisor, we have to open the divisor latch on bit 7 of the + // line control register. + uart_ptr.?[3] = lcr | 1 << 7; + // Now we write the divisor + uart_ptr.?[0] = divisor_least; + uart_ptr.?[1] = divisor_most; + // Now we lock it again + uart_ptr.?[3] = lcr; +} + +pub fn get() ?u8 { + if (uart_ptr.?[5] & 1 != 0) { + return uart_ptr.?[0]; + } else { + return null; + } +} + +pub fn put(c: u8) void { + uart_ptr.?[0] = c; +} + +fn drain(_: *std.Io.Writer, data: []const []const u8, splat: usize) !usize { + if (uart_ptr == null) return std.Io.Writer.Error.WriteFailed; + var written: u32 = 0; + for (data, 0..) |item, i| { + if (i == data.len - 1) { + for (0..splat) |_| for (item) |c| { + put(c); + written += 1; + }; + } else { + for (item) |c| { + put(c); + written += 1; + } + } + } + return written; +} + +pub var writer: std.Io.Writer = .{ + .buffer = &.{}, + .vtable = &.{ + .drain = drain, + }, +}; + +pub var terminal: std.Io.Terminal = .{ + .writer = &writer, + .mode = .escape_codes, +}; |
