diff --git a/src/kernel/arch/x86/arch.zig b/src/kernel/arch/x86/arch.zig index 761f197..88397d1 100644 --- a/src/kernel/arch/x86/arch.zig +++ b/src/kernel/arch/x86/arch.zig @@ -8,6 +8,7 @@ const irq = @import("irq.zig"); const isr = @import("isr.zig"); const pit = @import("pit.zig"); const rtc = @import("rtc.zig"); +const serial = @import("serial.zig"); const paging = @import("paging.zig"); const syscalls = @import("syscalls.zig"); const mem = @import("../../mem.zig"); @@ -16,6 +17,8 @@ const pmm = @import("pmm.zig"); const vmm = @import("../../vmm.zig"); const log = @import("../../log.zig"); const tty = @import("../../tty.zig"); +const Serial = @import("../../serial.zig").Serial; +const panic = @import("../../panic.zig").panic; const MemProfile = mem.MemProfile; /// The virtual end of the kernel code @@ -251,6 +254,31 @@ pub fn haltNoInterrupts() noreturn { } } +/// +/// Write a byte to serial port com1. Used by the serial initialiser +/// +/// Arguments: +/// IN byte: u8 - The byte to write +/// +fn writeSerialCom1(byte: u8) void { + serial.write(byte, serial.Port.COM1); +} + +/// +/// Initialise serial communication using port COM1 and construct a Serial instance +/// +/// Return: serial.Serial +/// The Serial instance constructed with the function used to write bytes +/// +pub fn initSerial() Serial { + serial.init(serial.DEFAULT_BAUDRATE, serial.Port.COM1) catch |e| { + panic(@errorReturnTrace(), "Failed to initialise serial: {}", .{e}); + }; + return Serial{ + .write = writeSerialCom1, + }; +} + /// /// Initialise the system's memory. Populates a memory profile with boot modules from grub, the amount of available memory, the reserved regions of virtual and physical memory as well as the start and end of the kernel code /// diff --git a/src/kernel/arch/x86/serial.zig b/src/kernel/arch/x86/serial.zig new file mode 100644 index 0000000..3388857 --- /dev/null +++ b/src/kernel/arch/x86/serial.zig @@ -0,0 +1,178 @@ +const arch = @import("arch.zig"); +const panic = @import("../../panic.zig").panic; +const testing = @import("std").testing; + +/// The I/O port numbers associated with each serial port +pub const Port = enum(u16) { + COM1 = 0x3F8, + COM2 = 0x2F8, + COM3 = 0x3E8, + COM4 = 0x2E8, +}; + +/// Errors thrown by serial functions +pub const SerialError = error{ + /// The given baudrate is outside of the allowed range + InvalidBaudRate, + + /// The given char len is outside the allowed range. + InvalidCharacterLength, +}; + +/// The LCR is the line control register +const LCR: u16 = 3; + +/// Maximum baudrate +const BAUD_MAX: u32 = 115200; + +/// 8 bits per serial character +const CHAR_LEN: u8 = 8; + +/// One stop bit per transmission +const SINGLE_STOP_BIT: bool = true; + +/// No parity bit +const PARITY_BIT: bool = false; + +/// Default baudrate +pub const DEFAULT_BAUDRATE = 38400; + +/// +/// Compute a value that encodes the serial properties +/// Used by the line control register +/// +/// Arguments: +/// IN char_len: u8 - The number of bits in each individual byte. Must be 0 or between 5 and 8 (inclusive). +/// IN stop_bit: bool - If a stop bit should included in each transmission. +/// IN parity_bit: bool - If a parity bit should be included in each transmission. +/// IN msb: u1 - The most significant bit to use. +/// +/// Return: u8 +/// The computed lcr value. +/// +/// Error: SerialError +/// InvalidCharacterLength - If the char_len is less than 5 or greater than 8. +/// +fn lcrValue(char_len: u8, stop_bit: bool, parity_bit: bool, msb: u1) SerialError!u8 { + if (char_len != 0 and (char_len < 5 or char_len > 8)) + return SerialError.InvalidCharacterLength; + // Set the msb and OR in all arguments passed + const val = char_len & 0x3 | + @intCast(u8, @boolToInt(stop_bit)) << 2 | + @intCast(u8, @boolToInt(parity_bit)) << 3 | + @intCast(u8, msb) << 7; + return val; +} + +/// +/// The serial controller accepts a divisor rather than a raw badrate, as that is more space efficient. +/// This function computes the divisor for a desired baudrate. Note that multiple baudrates can have the same divisor. +/// +/// Arguments: +/// baud: u32 - The desired baudrate. Must be greater than 0 and less than BAUD_MAX. +/// +/// Return: u16 +/// The computed divisor. +/// +/// Error: SerialError +/// InvalidBaudRate - If baudrate is 0 or greater than BAUD_MAX. +/// +fn baudDivisor(baud: u32) SerialError!u16 { + if (baud > BAUD_MAX or baud == 0) + return SerialError.InvalidBaudRate; + return @truncate(u16, BAUD_MAX / baud); +} + +/// +/// Checks if the transmission buffer is empty, which means data can be sent. +/// +/// Arguments: +/// port: Port - The port to check. +/// +/// Return: bool +/// If the transmission buffer is empty. +/// +fn transmitIsEmpty(port: Port) bool { + return arch.inb(@enumToInt(port) + 5) & 0x20 > 0; +} + +/// +/// Write a byte to a serial port. Waits until the transmission queue is empty. +/// +/// Arguments: +/// char: u8 - The byte to send. +/// port: Port - The port to send the byte to. +/// +pub fn write(char: u8, port: Port) void { + while (!transmitIsEmpty(port)) { + arch.halt(); + } + arch.outb(@enumToInt(port), char); +} + +/// +/// Initialise a serial port to a certain baudrate +/// +/// Arguments +/// IN baud: u32 - The baudrate to use. Cannot be more than MAX_BAUDRATE +/// IN port: Port - The port to initialise +/// +/// Error: SerialError +/// InvalidBaudRate - The baudrate is 0 or greater than BAUD_MAX. +/// +pub fn init(baud: u32, port: Port) SerialError!void { + // The baudrate is sent as a divisor of the max baud rate + const divisor: u16 = try baudDivisor(baud); + const port_int = @enumToInt(port); + // Send a byte to start setting the baudrate + arch.outb(port_int + LCR, lcrValue(0, false, false, 1) catch |e| { + panic(@errorReturnTrace(), "Failed to initialise serial output setup: {}", .{e}); + }); + // Send the divisor's lsb + arch.outb(port_int, @truncate(u8, divisor)); + // Send the divisor's msb + arch.outb(port_int + 1, @truncate(u8, divisor >> 8)); + // Send the properties to use + arch.outb(port_int + LCR, lcrValue(CHAR_LEN, SINGLE_STOP_BIT, PARITY_BIT, 0) catch |e| { + panic(@errorReturnTrace(), "Failed to setup serial properties: {}", .{e}); + }); + // Stop initialisation + arch.outb(port_int + 1, 0); +} + +test "lcrValue computes the correct value" { + // Check valid combinations + inline for ([_]u8{ 0, 5, 6, 7, 8 }) |char_len| { + inline for ([_]bool{ true, false }) |stop_bit| { + inline for ([_]bool{ true, false }) |parity_bit| { + inline for ([_]u1{ 0, 1 }) |msb| { + const val = try lcrValue(char_len, stop_bit, parity_bit, msb); + const expected = char_len & 0x3 | + @boolToInt(stop_bit) << 2 | + @boolToInt(parity_bit) << 3 | + @intCast(u8, msb) << 7; + testing.expectEqual(val, expected); + } + } + } + } + + // Check invalid char lengths + testing.expectError(SerialError.InvalidCharacterLength, lcrValue(4, false, false, 0)); + testing.expectError(SerialError.InvalidCharacterLength, lcrValue(9, false, false, 0)); +} + +test "baudDivisor" { + // Check invalid baudrates + inline for ([_]u32{ 0, BAUD_MAX + 1 }) |baud| { + testing.expectError(SerialError.InvalidBaudRate, baudDivisor(baud)); + } + + // Check valid baudrates + var baud: u32 = 1; + while (baud <= BAUD_MAX) : (baud += 1) { + const val = try baudDivisor(baud); + const expected = @truncate(u16, BAUD_MAX / baud); + testing.expectEqual(val, expected); + } +} diff --git a/src/kernel/kmain.zig b/src/kernel/kmain.zig index 6335842..4d6e895 100644 --- a/src/kernel/kmain.zig +++ b/src/kernel/kmain.zig @@ -7,12 +7,11 @@ const arch = @import("arch.zig").internals; const tty = @import("tty.zig"); const vga = @import("vga.zig"); const log = @import("log.zig"); -const serial = @import("serial.zig"); const pmm = @import("pmm.zig"); +const serial = @import("serial.zig"); const vmm = if (is_test) @import(mock_path ++ "vmm_mock.zig") else @import("vmm.zig"); const mem = if (is_test) @import(mock_path ++ "mem_mock.zig") else @import("mem.zig"); const panic_root = if (is_test) @import(mock_path ++ "panic_mock.zig") else @import("panic.zig"); -const options = @import("build_options"); const heap = @import("heap.zig"); comptime { @@ -38,11 +37,9 @@ pub fn panic(msg: []const u8, error_return_trace: ?*builtin.StackTrace) noreturn } export fn kmain(boot_payload: arch.BootPayload) void { - serial.init(serial.DEFAULT_BAUDRATE, serial.Port.COM1) catch |e| { - panic_root.panic(@errorReturnTrace(), "Failed to initialise serial: {}", .{e}); - }; + const serial_stream = serial.init(); - if (build_options.rt_test) log.runtimeTests(); + log.init(serial_stream); const mem_profile = arch.initMem(boot_payload) catch |e| panic_root.panic(@errorReturnTrace(), "Failed to initialise memory profile: {}", .{e}); var fixed_allocator = mem_profile.fixed_allocator; @@ -74,5 +71,5 @@ export fn kmain(boot_payload: arch.BootPayload) void { tty.print("Hello Pluto from kernel :)\n", .{}); // The panic runtime tests must run last as they never return - if (options.rt_test) panic_root.runtimeTests(); + if (build_options.rt_test) panic_root.runtimeTests(); } diff --git a/src/kernel/log.zig b/src/kernel/log.zig index 450d7c1..7126604 100644 --- a/src/kernel/log.zig +++ b/src/kernel/log.zig @@ -1,5 +1,6 @@ -const serial = @import("serial.zig"); +const build_options = @import("build_options"); const std = @import("std"); +const Serial = @import("serial.zig").Serial; const fmt = std.fmt; /// The errors that can occur when logging @@ -15,8 +16,10 @@ pub const Level = enum { ERROR, }; +var serial: Serial = undefined; + fn logCallback(context: void, str: []const u8) LoggingError!usize { - serial.writeBytes(str, serial.Port.COM1); + serial.writeBytes(str); return str.len; } @@ -76,6 +79,18 @@ pub fn logError(comptime format: []const u8, args: var) void { log(Level.ERROR, format, args); } +/// +/// Initialise the logging stream using the given Serial instance. +/// +/// Arguments: +/// IN ser: Serial - The serial instance to use when logging +/// +pub fn init(ser: Serial) void { + serial = ser; + + if (build_options.rt_test) runtimeTests(); +} + pub fn runtimeTests() void { inline for (@typeInfo(Level).Enum.fields) |field| { const level = @field(Level, field.name); diff --git a/src/kernel/serial.zig b/src/kernel/serial.zig index 7e0d08c..cebe0e6 100644 --- a/src/kernel/serial.zig +++ b/src/kernel/serial.zig @@ -1,217 +1,56 @@ const arch = @import("arch.zig").internals; -const panic = @import("panic.zig").panic; -const testing = @import("std").testing; const build_options = @import("build_options"); -/// The I/O port numbers associated with each serial port -pub const Port = enum(u16) { - COM1 = 0x3F8, - COM2 = 0x2F8, - COM3 = 0x3E8, - COM4 = 0x2E8, -}; +pub const Serial = struct { + /// Function that writes a single byte to the serial stream + pub const Write = fn (byte: u8) void; -/// Errors thrown by serial functions -const SerialError = error{ - /// The given baudrate is outside of the allowed range - InvalidBaudRate, + write: Write, - /// The given char len is outside the allowed range. - InvalidCharacterLength, -}; - -/// The LCR is the line control register -const LCR: u16 = 3; - -/// Maximum baudrate -const BAUD_MAX: u32 = 115200; - -/// 8 bits per serial character -const CHAR_LEN: u8 = 8; - -/// One stop bit per transmission -const SINGLE_STOP_BIT: bool = true; - -/// No parity bit -const PARITY_BIT: bool = false; - -/// Default baudrate -pub const DEFAULT_BAUDRATE = 38400; - -/// -/// Compute a value that encodes the serial properties -/// Used by the line control register -/// -/// Arguments: -/// IN char_len: u8 - The number of bits in each individual byte. Must be 0 or between 5 and 8 (inclusive). -/// IN stop_bit: bool - If a stop bit should included in each transmission. -/// IN parity_bit: bool - If a parity bit should be included in each transmission. -/// IN msb: u1 - The most significant bit to use. -/// -/// Return: u8 -/// The computed lcr value. -/// -/// Error: SerialError -/// InvalidCharacterLength - If the char_len is less than 5 or greater than 8. -/// -fn lcrValue(char_len: u8, stop_bit: bool, parity_bit: bool, msb: u1) SerialError!u8 { - if (char_len != 0 and (char_len < 5 or char_len > 8)) - return SerialError.InvalidCharacterLength; - // Set the msb and OR in all arguments passed - const val = char_len & 0x3 | - @intCast(u8, @boolToInt(stop_bit)) << 2 | - @intCast(u8, @boolToInt(parity_bit)) << 3 | - @intCast(u8, msb) << 7; - return val; -} - -/// -/// The serial controller accepts a divisor rather than a raw badrate, as that is more space efficient. -/// This function computes the divisor for a desired baudrate. Note that multiple baudrates can have the same divisor. -/// -/// Arguments: -/// baud: u32 - The desired baudrate. Must be greater than 0 and less than BAUD_MAX. -/// -/// Return: u16 -/// The computed divisor. -/// -/// Error: SerialError -/// InvalidBaudRate - If baudrate is 0 or greater than BAUD_MAX. -/// -fn baudDivisor(baud: u32) SerialError!u16 { - if (baud > BAUD_MAX or baud == 0) - return SerialError.InvalidBaudRate; - return @truncate(u16, BAUD_MAX / baud); -} - -/// -/// Checks if the transmission buffer is empty, which means data can be sent. -/// -/// Arguments: -/// port: Port - The port to check. -/// -/// Return: bool -/// If the transmission buffer is empty. -/// -fn transmitIsEmpty(port: Port) bool { - return arch.inb(@enumToInt(port) + 5) & 0x20 > 0; -} - -/// -/// Write a byte to a serial port. Waits until the transmission queue is empty. -/// -/// Arguments: -/// char: u8 - The byte to send. -/// port: Port - The port to send the byte to. -/// -pub fn write(char: u8, port: Port) void { - while (!transmitIsEmpty(port)) { - arch.halt(); - } - arch.outb(@enumToInt(port), char); -} - -/// -/// Write a slice of bytes to a serial port. See write for more detailed information. -/// -/// Arguments: -/// str: []const u8 - The bytes to send. -/// port: Port - The port to send the bytes to. -/// -pub fn writeBytes(str: []const u8, port: Port) void { - for (str) |char| { - write(char, port); - } -} - -/// -/// Initialise a serial port to a certain baudrate -/// -/// Arguments -/// IN baud: u32 - The baudrate to use. Cannot be more than MAX_BAUDRATE -/// IN port: Port - The port to initialise -/// -/// Error: SerialError -/// InvalidBaudRate - The baudrate is 0 or greater than BAUD_MAX. -/// -pub fn init(baud: u32, port: Port) SerialError!void { - // The baudrate is sent as a divisor of the max baud rate - const divisor: u16 = try baudDivisor(baud); - const port_int = @enumToInt(port); - // Send a byte to start setting the baudrate - arch.outb(port_int + LCR, lcrValue(0, false, false, 1) catch |e| { - panic(@errorReturnTrace(), "Failed to initialise serial output setup: {}", .{e}); - }); - // Send the divisor's lsb - arch.outb(port_int, @truncate(u8, divisor)); - // Send the divisor's msb - arch.outb(port_int + 1, @truncate(u8, divisor >> 8)); - // Send the properties to use - arch.outb(port_int + LCR, lcrValue(CHAR_LEN, SINGLE_STOP_BIT, PARITY_BIT, 0) catch |e| { - panic(@errorReturnTrace(), "Failed to setup serial properties: {}", .{e}); - }); - // Stop initialisation - arch.outb(port_int + 1, 0); - - if (build_options.rt_test) runtimeTests(); -} - -test "lcrValue computes the correct value" { - // Check valid combinations - inline for ([_]u8{ 0, 5, 6, 7, 8 }) |char_len| { - inline for ([_]bool{ true, false }) |stop_bit| { - inline for ([_]bool{ true, false }) |parity_bit| { - inline for ([_]u1{ 0, 1 }) |msb| { - const val = try lcrValue(char_len, stop_bit, parity_bit, msb); - const expected = char_len & 0x3 | - @boolToInt(stop_bit) << 2 | - @boolToInt(parity_bit) << 3 | - @intCast(u8, msb) << 7; - testing.expectEqual(val, expected); - } - } + /// + /// Write a slice of bytes to the serial stream. + /// + /// Arguments: + /// str: []const u8 - The bytes to send. + /// + pub fn writeBytes(self: *const @This(), bytes: []const u8) void { + for (bytes) |byte| { + self.write(byte); } } +}; - // Check invalid char lengths - testing.expectError(SerialError.InvalidCharacterLength, lcrValue(4, false, false, 0)); - testing.expectError(SerialError.InvalidCharacterLength, lcrValue(9, false, false, 0)); -} - -test "baudDivisor" { - // Check invalid baudrates - inline for ([_]u32{ 0, BAUD_MAX + 1 }) |baud| { - testing.expectError(SerialError.InvalidBaudRate, baudDivisor(baud)); - } - - // Check valid baudrates - var baud: u32 = 1; - while (baud <= BAUD_MAX) : (baud += 1) { - const val = try baudDivisor(baud); - const expected = @truncate(u16, BAUD_MAX / baud); - testing.expectEqual(val, expected); - } +/// +/// Initialise the serial interface. The details of how this is done depends on the architecture. +/// +/// Return: Serial +/// The serial interface constructed by the architecture +/// +pub fn init() Serial { + const serial = arch.initSerial(); + if (build_options.rt_test) runtimeTests(serial); + return serial; } /// /// Run all the runtime tests /// -fn runtimeTests() void { - rt_writeByte(); - rt_writeBytes(); +pub fn runtimeTests(serial: Serial) void { + rt_writeByte(serial); + rt_writeBytes(serial); } /// /// Test writing a byte and a new line separately /// -fn rt_writeByte() void { - write('c', Port.COM1); - write('\n', Port.COM1); +fn rt_writeByte(serial: Serial) void { + serial.write('c'); + serial.write('\n'); } /// /// Test writing a series of bytes /// -fn rt_writeBytes() void { - writeBytes(&[_]u8{ '1', '2', '3', '\n' }, Port.COM1); +fn rt_writeBytes(serial: Serial) void { + serial.writeBytes(&[_]u8{ '1', '2', '3', '\n' }); } diff --git a/test/mock/kernel/arch_mock.zig b/test/mock/kernel/arch_mock.zig index d899887..0565a20 100644 --- a/test/mock/kernel/arch_mock.zig +++ b/test/mock/kernel/arch_mock.zig @@ -6,6 +6,7 @@ const gdt = @import("gdt_mock.zig"); const idt = @import("idt_mock.zig"); const vmm = @import("vmm_mock.zig"); const paging = @import("paging_mock.zig"); +const Serial = @import("../../../src/kernel/serial.zig").Serial; const mock_framework = @import("mock_framework.zig"); pub const initTest = mock_framework.initTest; @@ -101,6 +102,10 @@ pub fn haltNoInterrupts() noreturn { while (true) {} } +pub fn initSerial() Serial { + return .{ .write = undefined }; +} + pub fn initMem(payload: BootPayload) std.mem.Allocator.Error!mem.MemProfile { return MemProfile{ .vaddr_end = @ptrCast([*]u8, &KERNEL_VADDR_END),