const arch = @import("arch.zig").internals; const panic = @import("panic.zig").panic; const testing = @import("std").testing; const 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, }; /// Errors thrown by serial functions 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); } /// /// 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 (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); } } } } // 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); } } /// /// Run all the runtime tests /// fn runtimeTests() void { rt_writeByte(); rt_writeBytes(); } /// /// Test writing a byte and a new line separately /// fn rt_writeByte() void { write('c', Port.COM1); write('\n', Port.COM1); } /// /// Test writing a series of bytes /// fn rt_writeBytes() void { writeBytes(&[_]u8{ '1', '2', '3', '\n' }, Port.COM1); }