diff --git a/src/arch.zig b/src/arch.zig index 9106f47..f495a23 100644 --- a/src/arch.zig +++ b/src/arch.zig @@ -1,15 +1,12 @@ -pub fn arch() type { - const builtin = @import("builtin"); +//! Helper module to select architecture-specific modules depending on what platform is +//! being targeted. - switch (comptime builtin.cpu.arch) { - .riscv64 => { - return @import("arch/riscv64.zig"); - }, - .aarch64 => { - return @import("arch/aarch64.zig"); - }, - else => { - @panic("Architecture is not supported"); - }, - } -} +const builtin = @import("builtin"); + +const impl = switch (builtin.cpu.arch) { + .riscv64 => @import("arch/riscv64.zig"), + .aarch64 => @import("arch/aarch64.zig"), + else => @compileError("Unsupported architecture"), +}; + +pub usingnamespace impl; diff --git a/src/arch/riscv64.zig b/src/arch/riscv64.zig index 643de87..6900aad 100644 --- a/src/arch/riscv64.zig +++ b/src/arch/riscv64.zig @@ -1,3 +1,5 @@ +//! RISC-V 64-bit platform-specific implementations. + const boot = @import("riscv64/boot.zig"); const regs = @import("riscv64/regs.zig"); const thread = @import("../thread.zig"); @@ -16,19 +18,23 @@ fn idleFunction() callconv(.naked) noreturn { asm volatile ("j ."); } +/// This CPU's HART (HARdware Thread) ID. pub threadlocal var tHartId: u32 = 0; +/// RISC-V task context pub const Context = extern struct { const STACK_SIZE: usize = 8192; - // Has to be exactly at offset 0x00, used in assembly + // Has to be exactly at offset 0x00, used in assembly. kstack: thread.KStack(STACK_SIZE), + /// Constructs an idle context struct. pub fn idle() @This() { const entry = @intFromPtr(&idleFunction); return Context.kernel(entry, 0); } + /// Constructs a kernel task context with entry point in `pc` and an `arg`ument. pub fn kernel(pc: usize, arg: usize) @This() { var ks = thread.KStack(STACK_SIZE).create(); const entry = @intFromPtr(&__rv64_task_enter_kernel); @@ -54,15 +60,18 @@ pub const Context = extern struct { return .{ .kstack = ks }; } + /// Low-level task context entry function. pub fn enter(self: *@This()) noreturn { __rv64_enter_task(self); } + /// Low-level task context switch function. pub fn switchFrom(self: *@This(), from: *@This()) void { __rv64_switch_task(self, from); } }; +/// Halts the CPU execution indefinitely, without ever returning. pub inline fn halt() noreturn { while (true) { _ = setInterruptMask(true); @@ -70,6 +79,8 @@ pub inline fn halt() noreturn { } } +/// Modifies the interrupt mask to either allow or block IRQs from being delivered to the CPU. +/// Returns the old IRQ mask. pub inline fn setInterruptMask(mask: bool) bool { const old = interruptMask(); if (mask) { @@ -80,19 +91,23 @@ pub inline fn setInterruptMask(mask: bool) bool { return old; } +/// Returns the current state of IRQ masking. pub fn interruptMask() bool { return regs.SSTATUS.read().SIE; } +/// Suspends the CPU until an interrupt is signalled. pub inline fn waitForInterrupt() void { asm volatile ("wfi"); } +/// Hint to the CPU that the code is executing a "busy-wait" or a "spin-wait" loop. pub inline fn spinHint() void { // Don't want to explicitly enable Zihintpause ext, so just paste this as raw opcode asm volatile (".word 0x0100000f"); } +/// Combined memory/compiler fence to ensure specific ordering of instructions and memory accesses. pub inline fn barrier(comptime ordering: std.builtin.AtomicOrder) void { switch (ordering) { .acquire => { @@ -109,6 +124,7 @@ pub inline fn barrier(comptime ordering: std.builtin.AtomicOrder) void { asm volatile ("" ::: "memory"); } +/// Set the CPU's thread pointer to some value. pub inline fn setThreadPointer(tp: usize) void { asm volatile ("mv tp, %[tp]" : diff --git a/src/arena.zig b/src/arena.zig index c25f6d0..ad741a9 100644 --- a/src/arena.zig +++ b/src/arena.zig @@ -1,17 +1,28 @@ +//! Simple bump allocator arena. + const physMemory = @import("mem/phys.zig"); const log = @import("debug.zig").log; const mem = @import("mem.zig"); +/// Bump allocator implementation. pub const Arena = struct { physBase: mem.PhysicalAddress, capacity: usize, len: usize, + /// Creates a new `Arena` of given `cap`acity. + /// + /// Requires initialized physical memory management. pub fn init(cap: usize) ?Arena { const physBase = physMemory.alloc_pages(cap / mem.vmm.PAGE_SIZE) orelse return null; return .{ .physBase = physBase, .capacity = cap, .len = 0 }; } + /// Allocates an object of type `T` within this arena. + /// + /// # Panics + /// + /// Panics if the arena runs out of memory. pub fn create(self: *@This(), comptime T: type) *T { if (self.len + @sizeOf(T) > self.capacity) { log.panic("Out of memory. Cannot allocate {} bytes", .{@sizeOf(T)}); diff --git a/src/debug.zig b/src/debug.zig index 8907e3d..3e61077 100644 --- a/src/debug.zig +++ b/src/debug.zig @@ -1,12 +1,20 @@ +//! Kernel debug output and logging functions. + const std = @import("std"); fn dummyWrite(_: u8) void {} +/// The main method of kernel logging. pub const log = struct { + /// Level to emit log records at. pub const Level = enum { + /// Debug. debug, + /// Info. info, + /// Warning. warn, + /// Error/Fatal. err, }; @@ -21,45 +29,55 @@ pub const log = struct { return data.len; } + /// Replaces the function to print debug bytes with a new one. pub fn setWriteFn(f: *const fn (u8) void) void { writeFn = f; } + /// Emit an `info`-level log record. pub inline fn info(comptime format: []const u8, args: anytype) void { writeln(.info, format, args); } + /// Emit a `debug`-level log record. pub inline fn debug(comptime format: []const u8, args: anytype) void { writeln(.debug, format, args); } + /// Emit a `warn`-level log record. pub inline fn warn(comptime format: []const u8, args: anytype) void { writeln(.warn, format, args); } + /// Emit a `err`-level log record. pub inline fn err(comptime format: []const u8, args: anytype) void { writeln(.err, format, args); } + /// Write raw byte data into the debugging output. pub fn writeRaw(data: []const u8) void { _ = writeWrapperFn(0, data) catch return; } + /// Write a formatted string (without logging prefix/suffix/newline) into the debugging output. pub fn write(comptime format: []const u8, args: anytype) void { writer.print(format, args) catch return; } + /// Write a formatted log record into the debugging output. pub fn writeln(comptime level: Level, comptime format: []const u8, args: anytype) void { const prefix = comptime logPrefix(level); const suffix = comptime logSuffix(level); writer.print(prefix ++ format ++ suffix ++ "\r\n", args) catch return; } + /// Helper function to emit a `Not yet implemented` message and panic. pub fn todo(comptime msg: []const u8, args: anytype) noreturn { err("Not yet implemented: " ++ msg, args); @panic("Not yet implemented"); } + /// Helper function to emit a `PANIC` message and panic. pub fn panic(comptime msg: []const u8, args: anytype) noreturn { err("PANIC: " ++ msg, args); @panic("Explicit kernel panic"); diff --git a/src/kernel.zig b/src/kernel.zig index 216821e..62e8fd0 100644 --- a/src/kernel.zig +++ b/src/kernel.zig @@ -1,13 +1,17 @@ -// export const _ = @import("arch/riscv64/boot.zig").rv64BspLowerEntry; -pub const arch = @import("arch.zig").arch(); +//! zing microkernel. + +pub const arch = @import("arch.zig"); pub const mem = @import("mem.zig"); pub const debug = @import("debug.zig"); pub const arena = @import("arena.zig"); pub const thread = @import("thread.zig"); +pub const util = @import("util.zig"); +pub const sync = @import("sync.zig"); pub const log = debug.log; pub const vmm = mem.vmm; +/// If set to `true`, will emit log messages for physical page allocations/deallocations. pub const TRACE_PHYSICAL_ALLOCATOR: bool = false; const std = @import("std"); @@ -25,6 +29,14 @@ noinline fn f1(arg: usize, c: usize) void { log.write("\x1B[1;{}H{}", .{ arg + 1, (c + arg) % 10 }); } +/// Platform-independent entry point for the kernel. +/// +/// # Invariants +/// +/// The following preconditions must be met to invoke this function: +/// +/// * Physical memory must be initialized. +/// * (optional) Logging should be set up. pub export fn kernel_main() callconv(.C) noreturn { log.write("\x1B[2J", .{}); var a = arena.Arena.init(256 * 0x1000) orelse @panic("Could not setup kernel arena"); @@ -39,6 +51,7 @@ pub export fn kernel_main() callconv(.C) noreturn { thread.enter(); } +/// Kernel's panic handler pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, return_address: ?usize) noreturn { _ = error_return_trace; diff --git a/src/mem.zig b/src/mem.zig index f8f8bb3..2cb4db1 100644 --- a/src/mem.zig +++ b/src/mem.zig @@ -1,3 +1,5 @@ +//! Platform-independent memory management functions. + const std = @import("std"); pub const vmm = @import("mem/vmm.zig"); @@ -5,18 +7,30 @@ pub const phys = @import("mem/phys.zig"); pub const TranslationLevel = vmm.TranslationLevel; +/// A representation for physical address. pub const PhysicalAddress = packed struct(u64) { raw: u64, + /// NULL/zero physical address constant. pub const NULL: @This() = .{ .raw = 0 }; + /// Base address to add to a given `PhysicalAddress` in order to "virtualize" it. pub var gVirtualizeBase: usize = 0; + /// Maximum `PhysicalAddress` that can be represented as a virtual address. pub var gVirtualizeSize: usize = 0; + /// Adds an `offset` to this `PhysicalAddress` pub fn add(self: @This(), offset: usize) @This() { return .{ .raw = self.raw + @as(u64, @intCast(offset)) }; } + /// "Virtualizes" a `PhysicalAddress` by turning it into a virtual address representation of + /// the memory pointed to by this physical address. + /// + /// # Panics + /// + /// Panics if the physical address points to a memory that cannot be represented by a virtual + /// address. pub fn virtualize(self: @This()) usize { if (self.raw > gVirtualizeSize) { @panic("Physical address out of virtualize bounds"); @@ -25,6 +39,12 @@ pub const PhysicalAddress = packed struct(u64) { return self.raw + gVirtualizeBase; } + /// "De-virtualizes" a previously "virtualized" physical address by mapping it back into its + /// physical form. + /// + /// # Panics + /// + /// Panics if the virtual address provided is outside of virtualizable memory range. pub fn from_virtualized(virt: usize) @This() { if ((virt < gVirtualizeBase) || (virt - gVirtualizeBase > gVirtualizeSize)) { @panic("Invalid virtualized physical address"); @@ -34,6 +54,8 @@ pub const PhysicalAddress = packed struct(u64) { } }; +/// Helper function to format a byte quantity into a human-readable size. +/// Writes its result to the `buffer` provided and returns a pointer to it. pub fn formatSize(buffer: []u8, size: u64) []const u8 { const KIBI: u64 = 1024; const MIBI: u64 = KIBI * 1024; diff --git a/src/mem/phys.zig b/src/mem/phys.zig index 8942e35..8e38fc0 100644 --- a/src/mem/phys.zig +++ b/src/mem/phys.zig @@ -1,3 +1,5 @@ +//! Physical memory management implementation. + const std = @import("std"); const kernel = @import("../kernel.zig"); @@ -9,13 +11,22 @@ const sync = @import("../sync.zig"); const Range = @import("../util/range.zig").Range; const Spinlock = sync.IrqSafeSpinlock; -pub const MemoryRegion = struct { name: []const u8, range: Range(u64) }; +/// Represents a single region of physical memory (reserved or available). +pub const MemoryRegion = struct { + /// Name string, used to represent where the memory comes from. + name: []const u8, + /// Byte range of the memory region. + range: Range(u64), +}; -const Page = extern struct { +/// Represents information about a single managed physical memory page. +pub const Page = extern struct { + /// Reference count of the page. Zero means the page is not allocated. refcount: u32 = 0, unused: [3]u32 = undefined, - fn isUsed(self: *const @This()) bool { + /// Returns `true` if the page is allocated/used. + pub fn isUsed(self: *const @This()) bool { return self.refcount != 0; } @@ -116,11 +127,21 @@ var gReservedRegions: std.BoundedArray(MemoryRegion, 16) = .{}; var gPhysicalMemoryLock = Spinlock{}; var gPhysicalMemory = PhysicalMemoryManager{ .pageArray = undefined }; +/// Adds an available memory region to the list. +/// +/// # Note +/// +/// Only meaningful to call before calling `init()`. pub fn addMemoryRegion(name: []const u8, base: u64, size: u64) void { log.info("Memory: '{s}', base 0x{x}, size 0x{x}", .{ name, base, size }); gMemoryRegions.append(.{ .name = name, .range = .{ .start = base, .len = size } }) catch @panic("memory regions overflow"); } +/// Adds an reserved memory region to the list. +/// +/// # Note +/// +/// Only meaningful to call before calling `init()`. pub fn addReservedRegion(name: []const u8, base: u64, size: u64) void { log.info("Reserved: '{s}', base 0x{x}, size 0x{x}", .{ name, base, size }); gReservedRegions.append(.{ .name = name, .range = .{ .start = base, .len = size } }) catch @panic("reserved regions overflow"); @@ -175,6 +196,12 @@ fn allocPageArray(pageCount: usize) []Page { @panic("TODO"); } +/// Initializes the physical memory management. +/// +/// # Notes +/// +/// Calls to `add***Region()` functions have no meaning past this point, so all the memory +/// present in the system, along with memory reservations, should be added **prior** to this point. pub fn init() void { var memoryStart: u64 = std.math.maxInt(u64); var memoryEnd: u64 = std.math.minInt(u64); @@ -189,7 +216,8 @@ pub fn init() void { } const memoryPages = (memoryEnd - memoryStart) / vmm.PAGE_SIZE; // == bitmap bits required - const pageArrayPages = (memoryPages + PhysicalMemoryManager.RECORDS_PER_PAGE - 1) / PhysicalMemoryManager.RECORDS_PER_PAGE; + const pageArrayPages = (memoryPages + PhysicalMemoryManager.RECORDS_PER_PAGE - 1) // + / PhysicalMemoryManager.RECORDS_PER_PAGE; const pageArray = allocPageArray(pageArrayPages); var availablePages: usize = 0; @@ -232,6 +260,7 @@ fn trace_free(page: mem.PhysicalAddress) void { log.debug("free 0x{x}", .{page.raw}); } +/// Allocates a single 4KiB physical memory page. pub fn alloc_page() ?mem.PhysicalAddress { gPhysicalMemoryLock.lock(); defer gPhysicalMemoryLock.release(); @@ -242,6 +271,7 @@ pub fn alloc_page() ?mem.PhysicalAddress { return page; } +/// Allocates a set of `count` contiguous 4KiB pages. pub fn alloc_pages(count: usize) ?mem.PhysicalAddress { gPhysicalMemoryLock.lock(); defer gPhysicalMemoryLock.release(); @@ -252,6 +282,13 @@ pub fn alloc_pages(count: usize) ?mem.PhysicalAddress { return pages; } +/// Deallocates a single page of physical memory. +/// +/// # Panics +/// +/// * If the `page` does not represent a valid managed page. +/// * If the `page` was not previously allocated. +/// * If a double free is detected. pub fn free_page(page: mem.PhysicalAddress) void { if (comptime kernel.TRACE_PHYSICAL_ALLOCATOR) { trace_free(page); @@ -261,7 +298,15 @@ pub fn free_page(page: mem.PhysicalAddress) void { gPhysicalMemory.free_page(page); } -// NOTE: Physical memory lock must be held +/// Returns a `Page` struct representing the given `page`. +/// +/// # Invariants +/// +/// The physical memory lock must be held. +/// +/// # Panics +/// +/// Will panic if the `page` does not represent a valid managed page. pub fn get_page(page: mem.PhysicalAddress) *Page { return gPhysicalMemory.get_page(page); } diff --git a/src/mem/vmm.zig b/src/mem/vmm.zig index 75e028f..e94be63 100644 --- a/src/mem/vmm.zig +++ b/src/mem/vmm.zig @@ -1,5 +1,9 @@ +//! Platform-independent virtual memory management definitions. + +/// Page size is 4KiB on all platforms. pub const PAGE_SIZE: usize = 0x1000; +/// Helper function to construct a "Translation Level" struct type from a bit shift. pub fn TranslationLevel(comptime shift: usize) type { return struct { pub const SHIFT: usize = shift; diff --git a/src/sync.zig b/src/sync.zig index af67634..7f52edb 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -1,19 +1,26 @@ +//! Kernel synchronization primitives. + const std = @import("std"); const arch = @import("kernel.zig").arch; +/// Basic spinlock implementation +// TODO not actually IRQ safe, lol. pub const IrqSafeSpinlock = struct { state: std.atomic.Value(bool) = .{ .raw = false }, + /// Acquires a lock over `self`. Returns `false` if the lock is already held by someone else. pub fn tryLock(self: *@This()) bool { return self.state.cmpxchgStrong(false, true, .acquire, .monotonic) orelse false; } + /// Acquires a lock over `self`. Will block until a lock can be acquired. pub fn lock(self: *@This()) void { while (!self.tryLock()) { arch.spinHint(); } } + /// Releases a lock over `self`. pub fn release(self: *@This()) void { self.state.store(false, .release); } diff --git a/src/thread.zig b/src/thread.zig index 73bfd16..b1984b2 100644 --- a/src/thread.zig +++ b/src/thread.zig @@ -1,3 +1,5 @@ +//! Thread management utilities and data structures. + const std = @import("std"); const arena = @import("arena.zig"); @@ -5,13 +7,19 @@ const arch = @import("kernel.zig").arch; const log = @import("debug.zig").log; const mem = @import("mem.zig"); +/// Per-CPU thread queue structure. pub const Queue = struct { + /// Idle task context. Used when there are no other tasks running. idle: arch.Context, + /// Current thread pointer. current: ?*Thread = null, + /// Thread queue head pointer. head: ?*Thread = null, + /// Pointer to this CPU's thread queue. pub threadlocal var thisCpu: ?*Queue = null; + /// Sets up a thread queue for the current CPU. pub fn initThisCpu(a: *arena.Arena) void { const idle = arch.Context.idle(); const q = a.create(Queue); @@ -19,6 +27,7 @@ pub const Queue = struct { thisCpu = q; } + /// Enters a task on this CPU. pub fn enter(self: *@This()) noreturn { if (self.head) |gt| { self.current = gt; @@ -29,6 +38,7 @@ pub const Queue = struct { } } + /// Yields CPU to the next available task. pub fn yield(self: *@This()) void { if (self.current) |curr| { // Switching from thread @@ -55,6 +65,7 @@ pub const Queue = struct { } } + /// Adds an available task to this queue. pub fn enqueue(self: *@This(), t: *Thread) void { if (self.head) |gt| { t.next = gt; @@ -69,13 +80,19 @@ pub const Queue = struct { } }; +/// Represents a single execution thread. pub const Thread = struct { + /// Arena. allocator: *arena.Arena, + /// Architecture-specific task context. archContext: arch.Context, + /// Next thread in the queue. next: ?*Thread = null, + /// Previous thread in the queue. prev: ?*Thread = null, + /// Creates a new (kernel) thread with given `pc` (entry point) and `arg`ument. pub fn create(a: *arena.Arena, pc: usize, arg: usize) *Thread { const thread = a.create(Thread); thread.* = .{ @@ -85,23 +102,34 @@ pub const Thread = struct { return thread; } + /// Enters the thread, does not return. pub fn enter(self: *@This()) noreturn { self.archContext.enter(); } + /// Switches from `from` to `self` thread. pub fn switchFrom(self: *@This(), from: *@This()) void { self.archContext.switchFrom(&from.archContext); } }; +/// Helper data structure to represent kernel stacks in task contexts. pub fn KStack(comptime SIZE: usize) type { return extern struct { // Has to be at exactly offset 0x00, used in assembly + /// Stack pointer. Aliases `data`. sp: *usize, + /// Stack data represented as a slice of `SIZE` machine-sized words. data: *[SIZE]usize, + /// Physical base address at which the stack is allocated. physicalBase: mem.PhysicalAddress, + /// Allocates a new kernel stack. + /// + /// # Panics + /// + /// Panics on Out-of-Memory condition. TODO Fix this. pub fn create() @This() { const physicalBase = mem.phys.alloc_pages(SIZE * @sizeOf(usize) / 0x1000) orelse @panic("OOM"); const ptr = @as(*[SIZE]usize, @ptrFromInt(physicalBase.virtualize())); @@ -109,6 +137,11 @@ pub fn KStack(comptime SIZE: usize) type { return .{ .data = ptr, .physicalBase = physicalBase, .sp = @ptrFromInt(@intFromPtr(&ptr[0]) + SIZE * @sizeOf(usize)) }; } + /// Pushes a machine-sized word onto the stack. + /// + /// # Panics + /// + /// Panics if a push would overflow the stack. pub fn push(self: *@This(), value: usize) void { if (self.sp == &self.data[0]) { @panic("KStack overflow"); @@ -119,14 +152,17 @@ pub fn KStack(comptime SIZE: usize) type { }; } +/// Adds a thread to some CPU queue for execution. pub fn enqueue(t: *Thread) void { Queue.thisCpu.?.enqueue(t); } +/// Enters thread execution on the current CPU. pub fn enter() noreturn { Queue.thisCpu.?.enter(); } +/// Yields this CPU's execution to a next thread. pub fn yield() void { Queue.thisCpu.?.yield(); } diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..fc44082 --- /dev/null +++ b/src/util.zig @@ -0,0 +1,2 @@ +pub const dtb = @import("util/dtb.zig"); +pub const range = @import("util/range.zig"); diff --git a/src/util/dtb.zig b/src/util/dtb.zig index b359e39..b0e7e64 100644 --- a/src/util/dtb.zig +++ b/src/util/dtb.zig @@ -1,3 +1,5 @@ +//! Flattened Device Tree manipulation utilities + const mem = @import("../mem.zig"); const log = @import("../debug.zig").log; const std = @import("std"); @@ -17,7 +19,14 @@ const fdt_header = extern struct { size_dt_struct: u32, }; -const fdt_op = enum(u32) { FDT_BEGIN_NODE = 1, FDT_END_NODE = 2, FDT_PROP = 3, FDT_NOP = 4, FDT_END = 9, _ }; +const fdt_op = enum(u32) { + FDT_BEGIN_NODE = 1, + FDT_END_NODE = 2, + FDT_PROP = 3, + FDT_NOP = 4, + FDT_END = 9, + _, +}; const fdt_prop = extern struct { len: u32, @@ -38,26 +47,37 @@ const FdtTag = union(enum) { end, }; +/// Represents a memory region described by some device tree node. +/// Can either mean a range of reserved memory or a range of available memory depending on the +/// function used to obtain it. pub const FdtMemoryRegion = struct { name: []const u8, base: u64, size: u64, }; +/// Represents a single node within a device tree. pub const FdtNode = struct { + /// Parent device tree pointer. fdt: *const Fdt, + /// Offset within the device tree raw data blob. off: usize, + /// Name string of the node. name: []const u8, + /// Depth of the node within the tree. depth: usize, + /// Returns an iterator over the node's properties. pub fn propIterator(self: *const @This()) FdtNodePropIterator { return .{ .node = self, .tagIter = self.fdt.tagIteratorAt(self.off) }; } + /// Returns an iterator over the node's children. pub fn children(self: *const @This()) FdtNodeIterator { return .{ .tagIter = self.fdt.tagIteratorAt(self.off), .depth = self.depth + 1, .depthLower = self.depth }; } + /// Looks up a child with given `name` within the node. pub fn child(self: *const @This(), name: []const u8) ?FdtNode { var iter = self.children(); while (iter.next()) |c| { @@ -68,6 +88,7 @@ pub const FdtNode = struct { return null; } + /// Looks up a property with given `name` within the node. pub fn property(self: *const @This(), name: []const u8) ?FdtNodeProp { var propIter = self.propIterator(); while (propIter.next()) |prop| { @@ -79,24 +100,33 @@ pub const FdtNode = struct { } }; +/// Represents a property of some node within a device tree. pub const FdtNodeProp = struct { + /// Node to which this property belongs. node: *const FdtNode, + /// Name string. name: []const u8, + /// Value of the property represented as a raw byte slice. value: []const u8, + /// Interprets the property's value as a list of strings. pub inline fn getStringArray(self: *const @This()) FdtStringArrayIterator { return .{ .prop = self }; } + /// Interprets the property's value as a single string. pub inline fn getString(self: *const @This()) []const u8 { var sa = self.getStringArray(); return sa.next() orelse ""; } + /// Returns the length of the property in full 32-bit cells. pub inline fn lenU32(self: *const @This()) usize { return self.value.len / @sizeOf(u32); } + /// Interprets the property's value as an array of 32-bit cells and returns a cell at a given + /// index. pub fn getU32(self: *const @This(), index: usize) ?u32 { if (index >= self.lenU32()) { return null; @@ -108,6 +138,15 @@ pub const FdtNodeProp = struct { return std.mem.bigToNative(u32, @as(*const u32, @ptrCast(@alignCast(&self.value[index * 4]))).*); } + /// Interprets the property's value as an array of tuples of cells with sizes described by + /// `sizes` and reads a single such tuple into the `output`. + /// + /// Returns `true` if a whole tuple can be read at given `index`. + /// + /// # Notes + /// + /// * `index` parameter means a 32-bit cell index, not a tuple index. + /// * Tuple length is assumed to be `@min(output.len, sizes.len)`. pub fn readCells(self: *const @This(), index: usize, output: []u64, sizes: []const usize) bool { const count = @min(output.len, sizes.len); const len = self.lenU32(); @@ -138,6 +177,7 @@ pub const FdtNodeProp = struct { } }; +/// An iterator over a string list property value. pub const FdtStringArrayIterator = struct { prop: *const FdtNodeProp, off: usize = 0, @@ -154,6 +194,7 @@ pub const FdtStringArrayIterator = struct { } }; +/// An iterator over available memory regions described by a device tree. pub const FdtMemoryRegionIterator = struct { nodeIter: FdtNodeIterator, cellSizes: [2]usize, @@ -178,6 +219,7 @@ pub const FdtMemoryRegionIterator = struct { } }; +/// An iterator over a device tree's node properties. pub const FdtNodePropIterator = struct { node: *const FdtNode, tagIter: FdtTagIterator, @@ -213,6 +255,7 @@ pub const FdtNodePropIterator = struct { } }; +/// An iterator over a device tree's nodes. pub const FdtNodeIterator = struct { tagIter: FdtTagIterator, depth: usize = 0, @@ -246,6 +289,7 @@ pub const FdtNodeIterator = struct { } }; +/// An iterator over a device tree's raw tags. pub const FdtTagIterator = struct { fdt: *const Fdt, raw: []const u8, @@ -295,13 +339,22 @@ pub const FdtTagIterator = struct { } }; +/// An error returned by the FDT manipulation functions. pub const FdtError = error{invalid_magic}; +/// Magic number describing a valid device tree (Big-Endian). pub const FDT_MAGIC: u32 = 0xD00DFEED; +/// Represents a Flattened Device Tree. pub const Fdt = struct { + /// Raw device tree bytes, including the header. bytes: []const u8, + /// Constructs a FDT struct from a physical address. + /// + /// # Errors + /// + /// * `invalid_magic` if the address provided does not have a valid magic number in its header. pub fn fromPhysicalAddress(phys: mem.PhysicalAddress) FdtError!@This() { const virt = phys.virtualize(); const hdr = @as(*const fdt_header, @ptrFromInt(virt)); @@ -313,6 +366,7 @@ pub const Fdt = struct { return .{ .bytes = x[0..totalsize] }; } + /// Returns the header pointer of this device tree. pub fn header(self: *const @This()) *const fdt_header { return @ptrCast(@alignCast(&self.bytes[0])); } @@ -322,18 +376,27 @@ pub const Fdt = struct { return self.bytes[off..]; } + /// Returns an iterator over the device tree's raw tags. pub fn tagIterator(self: *const @This()) FdtTagIterator { return self.tagIteratorAt(0); } + /// Returns an iterator over the device tree's raw tags at specific byte offset. pub fn tagIteratorAt(self: *const @This(), off: usize) FdtTagIterator { return .{ .raw = self.data(), .fdt = self, .off = off }; } + /// Returns an iterator over the device tree's nodes. pub fn nodeIterator(self: *const @This()) FdtNodeIterator { return .{ .tagIter = self.tagIterator() }; } + /// Returns the root node of this device tree. + /// + /// # Panics + /// + /// Panics if the device tree does not have a root node (which means the device tree blob is + /// malformed and the OS shouldn't be running anyway). pub fn rootNode(self: *const @This()) FdtNode { var nodeIter = self.nodeIterator(); while (nodeIter.next()) |node| { @@ -344,6 +407,7 @@ pub const Fdt = struct { @panic("Unreachable code"); } + /// Returns an iterator over available memory regions described by the device tree. pub fn memoryRegionIterator(self: *const @This()) FdtMemoryRegionIterator { const r = self.rootNode(); const addressCells = if (r.property("#address-cells")) |o| (if (o.getU32(0)) |p| p else 1) else 1; @@ -360,12 +424,15 @@ pub const Fdt = struct { return @ptrCast(self.bytes[off .. off + len]); } + /// Returns a string slice at given byte offset into the device tree's strings section. pub fn stringAt(self: *const @This(), off: usize) []const u8 { const raw = self.stringData()[off..]; const len = std.mem.len(raw); return @ptrCast(raw[0..len]); } + /// Adds information about the available and reserved memory regions described in this device + /// tree into the physical memory management structures. pub fn addPhysicalMemoryToSystem(self: *const @This()) void { var memoryRegions = self.memoryRegionIterator(); var cells: [2]u64 = undefined; @@ -387,6 +454,7 @@ pub const Fdt = struct { } } + /// Looks up a `/slash/separated/path` inside the device tree. pub fn find(self: *const @This(), path: []const u8) ?FdtNode { const trimmedPath = std.mem.trimLeft(u8, path, "/"); var pathElements = std.mem.splitScalar(u8, trimmedPath, '/'); @@ -492,6 +560,7 @@ pub const Fdt = struct { log.write("}},\r\n", .{}); } + /// Dumps the structured device tree into the log output. pub fn dump(self: *const @This()) void { dump_node(&self.rootNode(), 0); } diff --git a/src/util/list.zig b/src/util/list.zig deleted file mode 100644 index e69de29..0000000 diff --git a/src/util/range.zig b/src/util/range.zig index d0002c1..9ec441a 100644 --- a/src/util/range.zig +++ b/src/util/range.zig @@ -1,12 +1,19 @@ +//! Utilities for manipulating ranges. + +/// Non-inclusive range type over `T`. pub fn Range(comptime T: type) type { return struct { + /// Range start. start: T, + /// Range length. len: T, + /// Returns `start + len` of the range. pub fn end(self: *const @This()) T { return self.start + self.len; } + /// Returns a range representing an intersections of `self` with an`other` range. pub fn intersect(self: *const @This(), other: *const @This()) ?Range(T) { if (self.start < other.start) { const p = other.start - self.start;