hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/boot
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-12 10:59:26 +0200
committerhachem <im@hachem.wtf>2026-09-12 10:59:26 +0200
commitd6d908e3386e107b41241e9eda7dc546581524df (patch)
tree8219d02a7494152722ac7f32140d63c8a3b3daa7 /boot
parent4f540af34d7a88547d60bed7bd28850e6d0e18ab (diff)
feat: basic keyboard driver + tester shell
Diffstat (limited to 'boot')
-rw-r--r--boot/boot.zig43
-rw-r--r--boot/idt.zig115
-rw-r--r--boot/keyboard.zig42
-rw-r--r--boot/pic.zig22
-rw-r--r--boot/port.zig25
-rw-r--r--boot/serial.zig18
6 files changed, 223 insertions, 42 deletions
diff --git a/boot/boot.zig b/boot/boot.zig
index 1f50d83..e6342ce 100644
--- a/boot/boot.zig
+++ b/boot/boot.zig
@@ -6,6 +6,7 @@ const gdt = @import("gdt.zig");
const idt = @import("idt.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
+const keyboard = @import("keyboard.zig");
const bridge = @import("bridge");
// We need a panic handler otherwise zig won't be happy,
@@ -52,8 +53,8 @@ export fn _start() callconv(.c) noreturn {
gdt.load();
idt.init();
-
-
+ // NOTE: CPU interrupts stay off until the event loop's sti
+ // which is after the VM has run that\
serial.write("gdt + idt loaded\r\n");
// HHDM + memory map -> physical frame allocator -> kernel heap
@@ -84,23 +85,29 @@ export fn _start() callconv(.c) noreturn {
pmm.free(b);
pmm.free(c);
- // hand the kernel heap to revo and we're gucci
- bridge.run(heap.allocator(), serial.write);
+ // hand the kernel heap + raw framebuffer to revo and bring up the VM
+ const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{
+ .ptr = s.address,
+ .width = s.width,
+ .height = s.height,
+ .pitch = s.pitch,
+ } else null;
+ bridge.boot(heap.allocator(), serial.write, fb);
- if (framebuffer.get()) |screen| {
- // XOR texture
- var y: usize = 0;
- while (y < screen.height) : (y += 1) {
- var x: usize = 0;
- while (x < screen.width) : (x += 1) {
- const v: u32 = @intCast((x ^ y) & 0xFF);
- framebuffer.putpixel(screen, x, y, (v << 16) | (v << 8) | v);
- }
+ // - sleep until an interrupt
+ // - drain the keyboard
+ // - repeat.
+ //
+ // the cli/pop/sti-hlt bullshit closes the lost-wakeup race (a key
+ // arriving between "queue empty" and hlt would otherwise sit until
+ // the next keypress)
+ while (true) {
+ asm volatile ("cli");
+ if (keyboard.pop()) |sc| {
+ asm volatile ("sti");
+ bridge.onKey(sc);
+ } else {
+ asm volatile ("sti; hlt");
}
- serial.write("drew to the framebuffer\r\n");
- } else {
- serial.write("limine was a piece of shit.\r\n");
}
-
- while (true) asm volatile ("hlt");
}
diff --git a/boot/idt.zig b/boot/idt.zig
index 127fd8e..a9898d6 100644
--- a/boot/idt.zig
+++ b/boot/idt.zig
@@ -6,6 +6,8 @@
const gdt = @import("gdt.zig");
const serial = @import("serial.zig");
+const pic = @import("pic.zig");
+const keyboard = @import("keyboard.zig");
// 64bit interrupt gate
const Gate = packed struct {
@@ -26,7 +28,7 @@ const Idtr = packed struct {
var idt = [_]Gate{@bitCast(@as(u128, 0))} ** 256;
var idtr: Idtr = undefined;
-// what the common stub leaves on the stack, low address -> high.
+// what the common stub leaves on the stack, low address -> high.
// `mov rdi, rsp` hands the handler a pointer to this
const Frame = extern struct {
r15: u64,
@@ -55,17 +57,17 @@ const Frame = extern struct {
};
const names = [_][]const u8{
- "divide by zero", "debug",
- "non-maskable interrupt", "breakpoint",
- "overflow", "bound range exceeded",
- "invalid opcode", "device not available",
- "double fault", "coprocessor segment overrun",
- "invalid TSS", "segment not present",
- "stack-segment fault", "general protection fault",
- "page fault", "reserved",
- "x87 floating-point", "alignment check",
- "machine check", "SIMD floating-point",
- "virtualization", "control protection",
+ "divide by zero", "debug",
+ "non-maskable interrupt", "breakpoint",
+ "overflow", "bound range exceeded",
+ "invalid opcode", "device not available",
+ "double fault", "coprocessor segment overrun",
+ "invalid TSS", "segment not present",
+ "stack-segment fault", "general protection fault",
+ "page fault", "reserved",
+ "x87 floating-point", "alignment check",
+ "machine check", "SIMD floating-point",
+ "virtualization", "control protection",
};
fn name(vector: u64) []const u8 {
@@ -134,23 +136,95 @@ fn writeHex(value: u64) void {
export fn exceptionHandler(frame: *Frame) callconv(.c) noreturn {
serial.write("\r\n!!! CPU DID AN OOPSIE: ");
serial.write(name(frame.vector));
- serial.write("\r\n vector="); writeHex(frame.vector);
- serial.write(" err="); writeHex(frame.error_code);
- serial.write("\r\n rip="); writeHex(frame.rip);
- serial.write(" cs="); writeHex(frame.cs);
- serial.write("\r\n rflags="); writeHex(frame.rflags);
- serial.write(" rsp="); writeHex(frame.rsp);
+ serial.write("\r\n vector=");
+ writeHex(frame.vector);
+ serial.write(" err=");
+ writeHex(frame.error_code);
+ serial.write("\r\n rip=");
+ writeHex(frame.rip);
+ serial.write(" cs=");
+ writeHex(frame.cs);
+ serial.write("\r\n rflags=");
+ writeHex(frame.rflags);
+ serial.write(" rsp=");
+ writeHex(frame.rsp);
// page faults stash the offending address in CR2
if (frame.vector == 14) {
const cr2 = asm volatile ("mov %%cr2, %[out]"
: [out] "=r" (-> u64),
);
- serial.write("\r\n cr2="); writeHex(cr2);
+ serial.write("\r\n cr2=");
+ writeHex(cr2);
}
serial.write("\r\n");
while (true) asm volatile ("hlt");
}
+// hardware IRQ.
+// unlike exceptions these must return so we acknowledge the PIC and iretq
+// back to whatever we interrupted. the stub mirrors the exception one (dummy
+// error code + vector for a uniform Frame) but jumps to the returning path.
+fn irqStub(comptime vector: u8) fn () callconv(.naked) void {
+ return struct {
+ fn entry() callconv(.naked) void {
+ asm volatile ("pushq $0"); // dummy error code, keeps Frame uniform
+ asm volatile ("pushq %[v]\n jmp irqCommon"
+ :
+ : [v] "i" (@as(u32, vector)),
+ );
+ }
+ }.entry;
+}
+
+// same register save as isrCommon, but afterwards we restore everything, drop
+// the pushed vector + error code, and iretq instead of halting.
+export fn irqCommon() callconv(.naked) void {
+ asm volatile (
+ \\ push %rax
+ \\ push %rbx
+ \\ push %rcx
+ \\ push %rdx
+ \\ push %rsi
+ \\ push %rdi
+ \\ push %rbp
+ \\ push %r8
+ \\ push %r9
+ \\ push %r10
+ \\ push %r11
+ \\ push %r12
+ \\ push %r13
+ \\ push %r14
+ \\ push %r15
+ \\ mov %rsp, %rdi
+ \\ call irqDispatch
+ \\ pop %r15
+ \\ pop %r14
+ \\ pop %r13
+ \\ pop %r12
+ \\ pop %r11
+ \\ pop %r10
+ \\ pop %r9
+ \\ pop %r8
+ \\ pop %rbp
+ \\ pop %rdi
+ \\ pop %rsi
+ \\ pop %rdx
+ \\ pop %rcx
+ \\ pop %rbx
+ \\ pop %rax
+ \\ add $16, %rsp
+ \\ iretq
+ );
+}
+
+export fn irqDispatch(frame: *Frame) callconv(.c) void {
+ switch (frame.vector) {
+ pic.MASTER_OFFSET + 1 => keyboard.onIrq(), // IRQ1: keyboard
+ else => {},
+ }
+ pic.eoi(@intCast(frame.vector - pic.MASTER_OFFSET));
+}
+
fn setGate(vector: u8, handler: u64) void {
idt[vector] = .{
.offset_low = @truncate(handler),
@@ -166,6 +240,9 @@ fn setGate(vector: u8, handler: u64) void {
// fill the exception vectors and load the IDT
pub fn init() void {
inline for (0..32) |v| setGate(v, @intFromPtr(&stub(v)));
+ // IRQ1 (keyboard) -> vector 0x21
+ // the PIC is remapped separately in boot.
+ setGate(pic.MASTER_OFFSET + 1, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 1)));
idtr = .{ .limit = @sizeOf(@TypeOf(idt)) - 1, .base = @intFromPtr(&idt) };
asm volatile ("lidt (%[idtr])"
:
diff --git a/boot/keyboard.zig b/boot/keyboard.zig
new file mode 100644
index 0000000..8e3a58f
--- /dev/null
+++ b/boot/keyboard.zig
@@ -0,0 +1,42 @@
+// top half of the PS/2 keyboard driver.
+// the IRQ1 handler calls onIrq and reads the raw scancode out
+// of the controller and drops it in a ring buffer. thats ALLLLLL it does
+// no decoding, no allocation, nothing that could (should) fault or reenter
+// the only reason this exists is because the interrupt handler is not a safe
+// spot to execute revo code and shit can break easily and i am NOT going to debug
+// it so its just better to do it like this.
+//
+// since there is just one producer (ISR) and one consumer (the event loop)
+// i dont need to make a lock, the ISR just moves the tail and the loop just
+// moves the head
+
+const port = @import("port.zig");
+
+const DATA_PORT: u16 = 0x60;
+const SIZE: usize = 256; // u8 indices wrap mod 256 for free
+
+var buf: [SIZE]u8 = undefined;
+var head: u8 = 0; // next slot the loop will read
+var tail: u8 = 0; // next slot the ISR will write
+
+// called from the IRQ1 handler. read the scancode (which also ACKs the
+// controller so it'll send the next one) and enqueue it, dropping it on the
+// floor if the buffer is somehow full.
+pub fn onIrq() void {
+ const code = port.inb(DATA_PORT);
+ const t: *volatile u8 = &tail;
+ const h: *volatile u8 = &head;
+ if (t.* +% 1 == h.*) return; // full, drop
+ buf[t.*] = code;
+ t.* +%= 1;
+}
+
+// called from the event loop. next queued scancode, or null if empty.
+pub fn pop() ?u8 {
+ const t: *volatile u8 = &tail;
+ const h: *volatile u8 = &head;
+ if (h.* == t.*) return null;
+ const code = buf[h.*];
+ h.* +%= 1;
+ return code;
+}
diff --git a/boot/pic.zig b/boot/pic.zig
new file mode 100644
index 0000000..0bec4a8
--- /dev/null
+++ b/boot/pic.zig
@@ -0,0 +1,22 @@
+// this is just a 8259 PIC thing in zig, kinda defines only the constants the
+// IDT needs cause the actual remapping and masking is done in the kernel level
+// which is pretty cool.EOI has to stay here cause it runs inside the ISR tho,
+// so somewhere the VM cant really do much.
+//
+// see: https://wiki.osdev.org/8259_PIC
+
+const port = @import("port.zig");
+
+const MASTER_CMD: u16 = 0x20;
+const SLAVE_CMD: u16 = 0xA0;
+const EOI: u8 = 0x20; // end-of-interrupt command
+
+pub const MASTER_OFFSET: u8 = 0x20; // IRQ0..7 -> vectors 0x20..0x27
+pub const SLAVE_OFFSET: u8 = 0x28; // IRQ8..15 -> vectors 0x28..0x2F
+
+// tell the PIC we're done. the slave's interrupts also need the master EOI'd
+// since they arrive via the cascade.
+pub fn eoi(irq: u4) void {
+ if (irq >= 8) port.outb(SLAVE_CMD, EOI);
+ port.outb(MASTER_CMD, EOI);
+}
diff --git a/boot/port.zig b/boot/port.zig
new file mode 100644
index 0000000..05668d3
--- /dev/null
+++ b/boot/port.zig
@@ -0,0 +1,25 @@
+// x86 port I/O
+//
+// these are pretty important cause all drivers which rely
+// on serial rely on these 2 instructions so
+
+pub inline fn outb(port: u16, val: u8) void {
+ asm volatile ("outb %[val], %[port]"
+ :
+ : [val] "{al}" (val),
+ [port] "N{dx}" (port),
+ );
+}
+
+pub inline fn inb(port: u16) u8 {
+ return asm volatile ("inb %[port], %[result]"
+ : [result] "={al}" (-> u8),
+ : [port] "N{dx}" (port),
+ );
+}
+
+// a throwaway write to an unused port, ~1us. some 8259s need a beat between
+// back-to-back command writes on slow hardware; harmless on QEMU.
+pub inline fn wait() void {
+ outb(0x80, 0);
+}
diff --git a/boot/serial.zig b/boot/serial.zig
index 2e8c8d0..fab0e66 100644
--- a/boot/serial.zig
+++ b/boot/serial.zig
@@ -1,6 +1,6 @@
-// This is a minimal 16550 UART driver for COM1, this is mainly going
-// to be used for debugging since its really simple to setup.
-// Fun fact: i was drunk when i wrote this
+// this is a minimal 16550 UART driver for COM1, this is mainly going
+// to be used for debugging since its really simple to setup.
+// Fun fact: i was drunk when i wrote this
//
// see: https://wiki.osdev.org/Serial_Ports
@@ -38,15 +38,23 @@ pub fn init() void {
outb(COM1 + 4, 0x0B); // RTS/DSR set
}
-// COM1+5 (bit 5) = transmit holding register and
+// COM1+5 (bit 5) = transmit holding register and
// spin until it's clear to send
fn putc(c: u8) void {
while (inb(COM1 + 5) & 0x20 == 0) {}
outb(COM1, c);
}
+// a raw serial console needs \r\n, but revo (and most sane code) emits bare \n.
+// inject the \r ourselves, skipping it when the \n already has one so existing
+// "\r\n" strings don't turn into "\r\r\n".
pub fn write(s: []const u8) void {
- for (s) |c| putc(c);
+ var prev: u8 = 0;
+ for (s) |c| {
+ if (c == '\n' and prev != '\r') putc('\r');
+ putc(c);
+ prev = c;
+ }
}
// dump a u64 as decimal. handy for "usable: N MiB" type logging.