diff options
author | Johannes Stoelp <johannes.stoelp@gmail.com> | 2025-04-21 22:32:52 +0200 |
---|---|---|
committer | Johannes Stoelp <johannes.stoelp@gmail.com> | 2025-04-21 22:32:52 +0200 |
commit | 20a854354918735c3289c5576a28fad18ca21757 (patch) | |
tree | 7ea3a785b1fd2dffd6b04ff800abc81016387dc8 | |
parent | a115fc9ad01a605aaad6886cd0fa8d05c328d403 (diff) | |
download | zig-playground-20a854354918735c3289c5576a28fad18ca21757.tar.gz zig-playground-20a854354918735c3289c5576a28fad18ca21757.zip |
mbr: add serial port (com) example
-rw-r--r-- | x86-bare-metal/mbr-com-serial/Makefile | 32 | ||||
-rw-r--r-- | x86-bare-metal/mbr-com-serial/com.zig | 106 | ||||
-rw-r--r-- | x86-bare-metal/mbr-com-serial/mbr.ld | 24 | ||||
-rw-r--r-- | x86-bare-metal/mbr-com-serial/mbr.zig | 50 |
4 files changed, 212 insertions, 0 deletions
diff --git a/x86-bare-metal/mbr-com-serial/Makefile b/x86-bare-metal/mbr-com-serial/Makefile new file mode 100644 index 0000000..9020ff8 --- /dev/null +++ b/x86-bare-metal/mbr-com-serial/Makefile @@ -0,0 +1,32 @@ +O := BUILD + +COMMON_FLAGS := -fno-unwind-tables -fno-strip -target x86-freestanding-code16 -O ReleaseSmall + +# Enable serial on stdio, such that in graphics mode it is enabled by default. +QEMU_ARGS ?= -serial stdio + +$(O)/boot: $(O)/boot.elf | check_ep dump_info + objcopy -O binary $< $@ + +$(O)/boot.elf: mbr.ld mbr.zig | $(O) + zig build-exe -femit-bin=$@ $(COMMON_FLAGS) -fno-entry --script $^ + +clean: + $(RM) -r $(O) + +$(O): + mkdir -p $(O) + +# Check entry point is exactly at 0x7c00. +check_ep: $(O)/boot.elf + readelf -h $< | awk '/Entry point address:/ { print $$4 }' | grep 0x7c00 >& /dev/null + +# Dump some debug info (disasm, elf load segments, code size). +dump_info: $(O)/boot.elf + objdump -Mintel -m i8086 --disassemble --visualize-jumps=extended-color $< + readelf -W -l $< + size $< + size $< | awk '/$(notdir $<)/ { print "MBR utilization " $$1/512 "%" }' + +run: $(O)/boot + qemu-system-i386 -hda $< $(QEMU_ARGS) diff --git a/x86-bare-metal/mbr-com-serial/com.zig b/x86-bare-metal/mbr-com-serial/com.zig new file mode 100644 index 0000000..d6137ea --- /dev/null +++ b/x86-bare-metal/mbr-com-serial/com.zig @@ -0,0 +1,106 @@ +// Support for COM serial ports [1]. +// +// COM ports are accessed via IO ports. Each port exposes a set of 1 byte +// registers relative to the ports base address. +// 0 Receive buffer (inb). +// 0 Transmit buffer (outb). +// 1 Interrupt enable. +// 2 Interrupt identification. +// 3 Line control. +// 4 Modem control. +// 5 Line status. +// 6 Modem status. +// 7 Scratch register. +// +// If DLAB bit in the line control register is '1', then offset 0/1 are +// remapped, to setup a 2 byte divisor value. +// 0 Low byte of the divisor. +// 1 High byte of the divisor. +// The COM port has an internal clock at 115200 Hz. +// +// The bios data area (DBA) may provide the base address of COM1-COM4 ports [2]. +// The first two ports have fairly standardized base addresses +// - COM1 0x3f8 +// - COM2 0x2f8 +// +// [1] https://wiki.osdev.org/Serial_Ports +// [2] https://wiki.osdev.org/Memory_Map_(x86) + +// COM port register offsets relative to COM port base address. +const REG_DATA: u16 = 0; +const REG_INTR_EN: u16 = 1; +const REG_LINE_CTRL: u16 = 3; +const REG_LINE_STAT: u16 = 5; +// Remapped iff LINE_CTRL.DLAB == 1. +const REG_DIVLO: u16 = 0; +const REG_DIVHI: u16 = 1; + +// Bit masks for LINE_CTRL register. +const BIT_DLAB: u8 = (1 << 7); + +// Bit masks for LINE_STAT register. +const BIT_RX_READY: u8 = (1 << 0); +const BIT_TX_READY: u8 = (1 << 5); + +/// Type to interact with x86 serial COM ports [1]. +/// +/// [1] https://wiki.osdev.org/Serial_Ports +pub const Com = struct { + // Widelysed standardized address for COM1 port (eg also in qemu). + pub const COM1: u16 = 0x3f8; + + base: u16, + + /// Initialize COM port at `base` address, with following configuration + /// - all interrupts disabled (polling) + /// - divisor = 1 (-> baud rate 115200) + /// - 8 data, 1 stop, no parity bits (8N1) + pub fn init(base: u16) Com { + // Disable all COM port interrupts. + outb(base + REG_INTR_EN, 0); + + // Set DLAB bit, to enable access to divisor registers. + outb(base + REG_LINE_CTRL, BIT_DLAB); + + // Set divisor=1, configuring a baud rate of 115200. + outb(base + REG_DIVLO, 1); + outb(base + REG_DIVHI, 0); + + // Unset DLAB bit, no parity, 1 stop bit, 8 data bits (8n1). + outb(base + REG_LINE_CTRL, 0b11); + + return .{ .base = base }; + } + + /// Write out `msg` to COM port. + pub fn puts(self: Com, msg: []const u8) void { + for (msg) |ch| { + self.putc(ch); + } + } + + /// Write out `char` to COM port. + pub fn putc(self: Com, char: u8) void { + while ((inb(self.base + REG_LINE_STAT) & BIT_TX_READY) == 0) {} + outb(self.base, char); + } +}; + +// -- UTILS --------------------------------------------------------------------- + +inline fn outb(port: u16, val: u8) void { + _ = asm volatile ("outb %[al], %[dx]" + : // No outputs. + : [al] "{al}" (val), + [dx] "{dx}" (port), + ); +} + +inline fn inb(port: u16) u8 { + var val: u8 = 0; + _ = asm volatile ("inb %[dx], %[al]" + : [al] "={al}" (val), + : [dx] "{dx}" (port), + ); + return val; +} diff --git a/x86-bare-metal/mbr-com-serial/mbr.ld b/x86-bare-metal/mbr-com-serial/mbr.ld new file mode 100644 index 0000000..9ada8c4 --- /dev/null +++ b/x86-bare-metal/mbr-com-serial/mbr.ld @@ -0,0 +1,24 @@ +/*OUTPUT_FORMAT(binary)*/ +OUTPUT_FORMAT(elf32-i386) +OUTPUT_ARCH(i386) +ENTRY(_entry) + +SECTIONS { + . = 0x7c00; + .boot : { *(.boot) } + .text : { *(.text) } + .data : { *(.data) } + .rodata : { *(.rodata) *(.rodata.str*) } + _boot_end = .; + + . = 0x7c00 + 510; + .mbr.magic : { + BYTE(0x55); + BYTE(0xaa); + } + + /*/DISCARD/ : { *(.*) }*/ + /*rest : { *(.*) }*/ + + ASSERT(_boot_end - 0x7c00 < 510, "boot sector must fit in 510 bytes") +} diff --git a/x86-bare-metal/mbr-com-serial/mbr.zig b/x86-bare-metal/mbr-com-serial/mbr.zig new file mode 100644 index 0000000..6c6fb77 --- /dev/null +++ b/x86-bare-metal/mbr-com-serial/mbr.zig @@ -0,0 +1,50 @@ +const Com = @import("com.zig").Com; + +// -- ENTRY POINT --------------------------------------------------------------- + +export fn _entry() linksection(".boot") callconv(.naked) noreturn { + asm volatile ( + // Disable interrupts. + \\cli + // Clear segment selectors. + \\xor %%ax, %%ax + \\mov %%ax, %%ds + \\mov %%ax, %%es + \\mov %%ax, %%ss + \\mov %%ax, %%fs + \\mov %%ax, %%gs + \\mov $0x7c00, %sp + // Long jump to set cs to 0x0000, as some BIOSes load the MBR + // to either 07c0:0000 or 0000:7c000. + \\ljmp $0x0, $main + ); +} + +// -- MAIN ---------------------------------------------------------------------- + +// Global access to COM0. +var com0: ?Com = null; + +// main should be "callconv(.naked)", once issue is fixed. +// https://github.com/ziglang/zig/issues/18183 +export fn main() noreturn { + com0 = com0 orelse Com.init(Com.COM1); + com0.?.puts("\n\nBooted into mbr.zig\n"); + + @panic("end reached, crashing kernel"); +} + +pub const panic = @import("std").debug.FullPanic(panicHandler); + +fn panicHandler(msg: []const u8, first_trace_addr: ?usize) noreturn { + _ = first_trace_addr; + + com0 = com0 orelse Com.init(Com.COM1); + com0.?.puts("\nPANIC: "); + com0.?.puts(msg); + com0.?.puts("\n"); + + while (true) { + asm volatile ("hlt"); + } +} |