From 08da45cef8b6e400e666a3177d25db8fc1ba575b Mon Sep 17 00:00:00 2001 From: Kit Rhett Aultman Date: Sun, 30 Jun 2024 01:00:43 -0400 Subject: [PATCH] main/console: console abstraction and print macros This commit does a bit of a cleanup and re-org of the UART code so that code doesn't need to create and carry around a reference to the UART to print to the console. This is now available through print!() and println!() As with the prior commit, this was heavily informed by Meyer Zinn's excellent blog posts on bare-metal Rust, specifically this one: https://meyerzinn.tech/posts/2023/03/08/p1-printing-and-allocating/ The idea here is to basically create a singleton Console object and then only access it behind the print!() and println!() macros. Since the macros are designed to be used anywhere, they hide the interactions with a global singleton which performs the debug outputs. Even though I likely shouldn't have, I also did a rustfmt to clean up syntax here. Ah, well. --- Cargo.lock | 32 ++++++++++++++++++++ Cargo.toml | 1 + src/console.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 67 +++++++++++++++++++++--------------------- 4 files changed, 146 insertions(+), 34 deletions(-) create mode 100644 src/console.rs diff --git a/Cargo.lock b/Cargo.lock index c0b5a87..75e1067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "bit_field" version = "0.10.2" @@ -14,6 +20,16 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "raw-cpuid" version = "10.7.0" @@ -27,6 +43,7 @@ dependencies = [ name = "riscv" version = "0.1.0" dependencies = [ + "spinning_top", "uart_16550", ] @@ -36,6 +53,21 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "uart_16550" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 56aec2c..f88d624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" target = "riscv64gc-unknown-none-elf" [dependencies] +spinning_top = "0.3.0" uart_16550 = "0.3.0" # We need to turn off panic unwinding diff --git a/src/console.rs b/src/console.rs new file mode 100644 index 0000000..3644555 --- /dev/null +++ b/src/console.rs @@ -0,0 +1,80 @@ +use spinning_top::Spinlock; +use uart_16550::MmioSerialPort; + +/* + * The goal here is to produce a console logging singleton + * that can then be accessed through print! and println! + * macros. Singletons are kinda frowned on in Rust, but + * this can still be done. It requires some kind of sync + * mutex (I use spinning_top because the stdlib Mutex is + * not available), hiding initialization state (this uses + * an Option because the static initializaer code needs + * to be some kind of constant expression), and an AP + * that mutates the state but doesn't leak it (here, it's + * output-only macros). + */ + +pub struct Console { + serial_port: MmioSerialPort, +} + +impl Console { + pub unsafe fn new(base: usize) -> Self { + let serial_port = MmioSerialPort::new(base); + Console { serial_port } + } + + pub fn write(&mut self, data: u8) { + self.serial_port.send(data); + } +} + +// Write trait is used in the macros below; this basicallt +// is a simple function for writing a string slice out to +// the console. + +impl core::fmt::Write for Console { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for byte in s.bytes() { + self.serial_port.send(byte); + } + Ok(()) + } +} + +static SERIAL_BASE: usize = 0x1000_0000; + +// I'm not actually sure Spinlock is necessary here, because I saw that the +// uart_16550 code already uses atomic_ptr, which sure sounds like it has +// synchronization features. + +pub static CONSOLE: Spinlock> = Spinlock::new(None); + +// Since we can't use something like lazy_static!, we're left with having +// to explicitly call the init first. This should be done only once +// at early initialization time +pub fn init_console() { + let mut console = CONSOLE.lock(); + *console = Some(unsafe { Console::new(SERIAL_BASE) }) +} + +// These, like a lot of the code in here, came courtesy of Meyer Zinn's +// baremetal Rust tutorials: https://meyerzinn.tech/posts/2023/03/08/p1-printing-and-allocating/ +// I don't know how to use macros that well yet! + +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => ({ + use core::fmt::Write; + $crate::console::CONSOLE.lock().as_mut().map(|writer| { + writer.write_fmt(format_args!($($arg)*)).unwrap() + }); + }); +} + +/// println prints a formatted string to the [CONSOLE] with a trailing newline character. +#[macro_export] +macro_rules! println { + ($fmt:expr) => ($crate::print!(concat!($fmt, "\n"))); + ($fmt:expr, $($arg:tt)*) => ($crate::print!(concat!($fmt, "\n"), $($arg)*)); +} diff --git a/src/main.rs b/src/main.rs index da4170b..3e9bc53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ #![no_main] #![feature(naked_functions)] // enable functions that are pure inline assembly +mod console; + use core::panic::PanicInfo; // cfg not test is set here because the analyzer and the test system @@ -9,7 +11,7 @@ use core::panic::PanicInfo; #[cfg(not(test))] #[panic_handler] fn on_panic(_info: &PanicInfo) -> ! { - loop {} + loop {} } /* @@ -25,44 +27,41 @@ fn on_panic(_info: &PanicInfo) -> ! { #[no_mangle] // C-style linkage needs an unmangled function #[link_section = ".text.init"] unsafe extern "C" fn _start() -> ! { - use core::arch::asm; - asm!( - // The linker will try to emit the `la` pseudo-instruction when it believes - // the referenced variable will be within a 12-bit offset of the _global_pointer, - // so we must initialize the `gp` register to this at runtime. - // (see the linker script file for more details) - ".option push", - ".option norelax", - "la gp, _global_pointer", - ".option pop", - - // set the stack pointer - "la sp, _stack_top", + use core::arch::asm; + asm!( + // The linker will try to emit the `la` pseudo-instruction when it believes + // the referenced variable will be within a 12-bit offset of the _global_pointer, + // so we must initialize the `gp` register to this at runtime. + // (see the linker script file for more details) + ".option push", + ".option norelax", + "la gp, _global_pointer", + ".option pop", + + // set the stack pointer + "la sp, _stack_top", - // clear the BSS section - "la t0, _bss_start", - "la t1, _bss_end", - "bgeu t0, t1, 2f", -"1:", - "sb zero, 0(t0)", - "addi t0, t0, 1", - "bne t0, t1, 1b", -"2:", + // clear the BSS section + "la t0, _bss_start", + "la t1, _bss_end", + "bgeu t0, t1, 2f", + "1:", + "sb zero, 0(t0)", + "addi t0, t0, 1", + "bne t0, t1, 1b", + "2:", - // "tail-call" to {entry} (call without saving a return address) - "tail {entry}", - entry = sym entry, // {entry} refers to the function [entry] below - options(noreturn) // we must handle "returning" from assembly - ); + // "tail-call" to {entry} (call without saving a return address) + "tail {entry}", + entry = sym entry, // {entry} refers to the function [entry] below + options(noreturn) // we must handle "returning" from assembly + ); } #[no_mangle] // Again, being mindful of the C calling convention extern "C" fn entry() -> ! { - use uart_16550::MmioSerialPort; - const SERIAL_BASE: usize = 0x1000_0000; - let mut serial_port = unsafe { MmioSerialPort::new(SERIAL_BASE) }; - for byte in "KIT -- Hello World!\n".bytes() { - serial_port.send(byte) - } + use crate::console; + console::init_console(); + println!("KIT-- hello works from println!"); loop {} } -- 2.34.1