main/console: console abstraction and print macros
authorKit Rhett Aultman <kit@kitaultman.com>
Sun, 30 Jun 2024 05:00:43 +0000 (01:00 -0400)
committerKit Rhett Aultman <kit@kitaultman.com>
Sun, 30 Jun 2024 05:00:43 +0000 (01:00 -0400)
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
Cargo.toml
src/console.rs [new file with mode: 0644]
src/main.rs

index c0b5a87f18bea4fb33e33d7fd50893449229d991..75e1067292c5566d3e670f5c1ed693f9577bd5d9 100644 (file)
@@ -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"
index 56aec2c65641dbbe9f8e0f62312cd2f36e92acb8..f88d624c0e1fc69d1e0174bbc6f517217167ac8d 100644 (file)
@@ -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 (file)
index 0000000..3644555
--- /dev/null
@@ -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<Option<Console>> = 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)*));
+}
index da4170be56b1a49edc1aef18a9a08f2bb32d2717..3e9bc536d03e987c918a7f683d21c53c0c3b2bba 100644 (file)
@@ -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 {}
 }