You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Andre Richter 2e72a8408f
Rework driver subsystem
This update significantly decouples the generic kernel code
from the BSP code.
Prior to this patch, the BSP had way too much business logic
that should have always been the generic kernel's concern.
2 years ago
..
.vscode Fix rust-analyzer targets 2 years ago
src Rework driver subsystem 2 years ago
tests Refactor Tests and Makefile 3 years ago
Cargo.lock Bump dependencies 2 years ago
Cargo.toml Bump dependencies 2 years ago
Makefile Add Cargo.toml to dependencies 2 years ago
README.md Rework driver subsystem 2 years ago
build.rs Linker: Use absolute library path 2 years ago

README.md

Tutorial 07 - Timestamps

tl;dr

  • We add abstractions for timer hardware, and implement them for the ARM architectural timer in _arch/aarch64.
  • The new timer functions are used to annotate UART prints with timestamps, and to get rid of the cycle-based delays in the GPIO device driver, which boosts accuracy.
  • A warn!() macro is added.

Test it

Check it out via chainboot (added in previous tutorial):

$ make chainboot
[...]
Minipush 1.0

[MP] ⏳ Waiting for /dev/ttyUSB0
[MP] ✅ Serial connected
[MP] 🔌 Please power the target now

 __  __ _      _ _                 _
|  \/  (_)_ _ (_) |   ___  __ _ __| |
| |\/| | | ' \| | |__/ _ \/ _` / _` |
|_|  |_|_|_||_|_|____\___/\__,_\__,_|

           Raspberry Pi 3

[ML] Requesting binary
[MP] ⏩ Pushing 12 KiB =========================================🦀 100% 0 KiB/s Time: 00:00:00
[ML] Loaded! Executing the payload now

[    0.143123] mingo version 0.7.0
[    0.143323] Booting on: Raspberry Pi 3
[    0.143778] Architectural timer resolution: 52 ns
[    0.144352] Drivers loaded:
[    0.144688]       1. BCM PL011 UART
[    0.145110]       2. BCM GPIO
[W   0.145469] Spin duration smaller than architecturally supported, skipping
[    0.146313] Spinning for 1 second
[    1.146715] Spinning for 1 second
[    2.146938] Spinning for 1 second

Diff to previous


diff -uNr 06_uart_chainloader/Cargo.toml 07_timestamps/Cargo.toml
--- 06_uart_chainloader/Cargo.toml
+++ 07_timestamps/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "mingo"
-version = "0.6.0"
+version = "0.7.0"
 authors = ["Andre Richter <andre.o.richter@gmail.com>"]
 edition = "2021"

Binary files 06_uart_chainloader/demo_payload_rpi3.img and 07_timestamps/demo_payload_rpi3.img differ
Binary files 06_uart_chainloader/demo_payload_rpi4.img and 07_timestamps/demo_payload_rpi4.img differ

diff -uNr 06_uart_chainloader/Makefile 07_timestamps/Makefile
--- 06_uart_chainloader/Makefile
+++ 07_timestamps/Makefile
@@ -23,29 +23,27 @@
 QEMU_MISSING_STRING = "This board is not yet supported for QEMU."

 ifeq ($(BSP),rpi3)
-    TARGET                 = aarch64-unknown-none-softfloat
-    KERNEL_BIN             = kernel8.img
-    QEMU_BINARY            = qemu-system-aarch64
-    QEMU_MACHINE_TYPE      = raspi3
-    QEMU_RELEASE_ARGS      = -serial stdio -display none
-    OBJDUMP_BINARY         = aarch64-none-elf-objdump
-    NM_BINARY              = aarch64-none-elf-nm
-    READELF_BINARY         = aarch64-none-elf-readelf
-    LD_SCRIPT_PATH         = $(shell pwd)/src/bsp/raspberrypi
-    RUSTC_MISC_ARGS        = -C target-cpu=cortex-a53
-    CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi3.img
+    TARGET            = aarch64-unknown-none-softfloat
+    KERNEL_BIN        = kernel8.img
+    QEMU_BINARY       = qemu-system-aarch64
+    QEMU_MACHINE_TYPE = raspi3
+    QEMU_RELEASE_ARGS = -serial stdio -display none
+    OBJDUMP_BINARY    = aarch64-none-elf-objdump
+    NM_BINARY         = aarch64-none-elf-nm
+    READELF_BINARY    = aarch64-none-elf-readelf
+    LD_SCRIPT_PATH    = $(shell pwd)/src/bsp/raspberrypi
+    RUSTC_MISC_ARGS   = -C target-cpu=cortex-a53
 else ifeq ($(BSP),rpi4)
-    TARGET                 = aarch64-unknown-none-softfloat
-    KERNEL_BIN             = kernel8.img
-    QEMU_BINARY            = qemu-system-aarch64
-    QEMU_MACHINE_TYPE      =
-    QEMU_RELEASE_ARGS      = -serial stdio -display none
-    OBJDUMP_BINARY         = aarch64-none-elf-objdump
-    NM_BINARY              = aarch64-none-elf-nm
-    READELF_BINARY         = aarch64-none-elf-readelf
-    LD_SCRIPT_PATH         = $(shell pwd)/src/bsp/raspberrypi
-    RUSTC_MISC_ARGS        = -C target-cpu=cortex-a72
-    CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi4.img
+    TARGET            = aarch64-unknown-none-softfloat
+    KERNEL_BIN        = kernel8.img
+    QEMU_BINARY       = qemu-system-aarch64
+    QEMU_MACHINE_TYPE =
+    QEMU_RELEASE_ARGS = -serial stdio -display none
+    OBJDUMP_BINARY    = aarch64-none-elf-objdump
+    NM_BINARY         = aarch64-none-elf-nm
+    READELF_BINARY    = aarch64-none-elf-readelf
+    LD_SCRIPT_PATH    = $(shell pwd)/src/bsp/raspberrypi
+    RUSTC_MISC_ARGS   = -C target-cpu=cortex-a72
 endif

 # Export for build.rs.
@@ -91,7 +89,7 @@
     -O binary

 EXEC_QEMU          = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE)
-EXEC_TEST_MINIPUSH = ruby tests/chainboot_test.rb
+EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb
 EXEC_MINIPUSH      = ruby ../common/serial/minipush.rb

 ##------------------------------------------------------------------------------
@@ -161,7 +159,7 @@
 ##------------------------------------------------------------------------------
 ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board.

-qemu qemuasm:
+qemu:
 	$(call color_header, "$(QEMU_MISSING_STRING)")

 else # QEMU is supported.
@@ -170,17 +168,13 @@
 	$(call color_header, "Launching QEMU")
 	@$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN)

-qemuasm: $(KERNEL_BIN)
-	$(call color_header, "Launching QEMU with ASM output")
-	@$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -d in_asm
-
 endif

 ##------------------------------------------------------------------------------
 ## Push the kernel to the real HW target
 ##------------------------------------------------------------------------------
 chainboot: $(KERNEL_BIN)
-	@$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(CHAINBOOT_DEMO_PAYLOAD)
+	@$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN)

 ##------------------------------------------------------------------------------
 ## Run clippy
@@ -237,8 +231,7 @@
 ##------------------------------------------------------------------------------
 test_boot: $(KERNEL_BIN)
 	$(call color_header, "Boot test - $(BSP)")
-	@$(DOCKER_TEST) $(EXEC_TEST_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \
-		-kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD)
+	@$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN)

 test: test_boot


diff -uNr 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s 07_timestamps/src/_arch/aarch64/cpu/boot.s
--- 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s
+++ 07_timestamps/src/_arch/aarch64/cpu/boot.s
@@ -18,17 +18,6 @@
 	add	\register, \register, #:lo12:\symbol
 .endm

-// Load the address of a symbol into a register, absolute.
-//
-// # Resources
-//
-// - https://sourceware.org/binutils/docs-2.36/as/AArch64_002dRelocations.html
-.macro ADR_ABS register, symbol
-	movz	\register, #:abs_g2:\symbol
-	movk	\register, #:abs_g1_nc:\symbol
-	movk	\register, #:abs_g0_nc:\symbol
-.endm
-
 //--------------------------------------------------------------------------------------------------
 // Public Code
 //--------------------------------------------------------------------------------------------------
@@ -48,35 +37,31 @@
 	// If execution reaches here, it is the boot core.

 	// Initialize DRAM.
-	ADR_ABS	x0, __bss_start
-	ADR_ABS x1, __bss_end_exclusive
+	ADR_REL	x0, __bss_start
+	ADR_REL x1, __bss_end_exclusive

 .L_bss_init_loop:
 	cmp	x0, x1
-	b.eq	.L_relocate_binary
+	b.eq	.L_prepare_rust
 	stp	xzr, xzr, [x0], #16
 	b	.L_bss_init_loop

-	// Next, relocate the binary.
-.L_relocate_binary:
-	ADR_REL	x0, __binary_nonzero_start         // The address the binary got loaded to.
-	ADR_ABS	x1, __binary_nonzero_start         // The address the binary was linked to.
-	ADR_ABS	x2, __binary_nonzero_end_exclusive
-
-.L_copy_loop:
-	ldr	x3, [x0], #8
-	str	x3, [x1], #8
-	cmp	x1, x2
-	b.lo	.L_copy_loop
-
 	// Prepare the jump to Rust code.
+.L_prepare_rust:
 	// Set the stack pointer.
-	ADR_ABS	x0, __boot_core_stack_end_exclusive
+	ADR_REL	x0, __boot_core_stack_end_exclusive
 	mov	sp, x0

-	// Jump to the relocated Rust code.
-	ADR_ABS	x1, _start_rust
-	br	x1
+	// Read the CPU's timer counter frequency and store it in ARCH_TIMER_COUNTER_FREQUENCY.
+	// Abort if the frequency read back as 0.
+	ADR_REL	x1, ARCH_TIMER_COUNTER_FREQUENCY // provided by aarch64/time.rs
+	mrs	x2, CNTFRQ_EL0
+	cmp	x2, xzr
+	b.eq	.L_parking_loop
+	str	w2, [x1]
+
+	// Jump to Rust code.
+	b	_start_rust

 	// Infinitely wait for events (aka "park the core").
 .L_parking_loop:

diff -uNr 06_uart_chainloader/src/_arch/aarch64/cpu.rs 07_timestamps/src/_arch/aarch64/cpu.rs
--- 06_uart_chainloader/src/_arch/aarch64/cpu.rs
+++ 07_timestamps/src/_arch/aarch64/cpu.rs
@@ -19,15 +19,6 @@

 pub use asm::nop;

-/// Spin for `n` cycles.
-#[cfg(feature = "bsp_rpi3")]
-#[inline(always)]
-pub fn spin_for_cycles(n: usize) {
-    for _ in 0..n {
-        asm::nop();
-    }
-}
-
 /// Pause execution on the core.
 #[inline(always)]
 pub fn wait_forever() -> ! {

diff -uNr 06_uart_chainloader/src/_arch/aarch64/time.rs 07_timestamps/src/_arch/aarch64/time.rs
--- 06_uart_chainloader/src/_arch/aarch64/time.rs
+++ 07_timestamps/src/_arch/aarch64/time.rs
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2022 Andre Richter <andre.o.richter@gmail.com>
+
+//! Architectural timer primitives.
+//!
+//! # Orientation
+//!
+//! Since arch modules are imported into generic modules using the path attribute, the path of this
+//! file is:
+//!
+//! crate::time::arch_time
+
+use crate::warn;
+use core::{
+    num::{NonZeroU128, NonZeroU32, NonZeroU64},
+    ops::{Add, Div},
+    time::Duration,
+};
+use cortex_a::{asm::barrier, registers::*};
+use tock_registers::interfaces::Readable;
+
+//--------------------------------------------------------------------------------------------------
+// Private Definitions
+//--------------------------------------------------------------------------------------------------
+
+const NANOSEC_PER_SEC: NonZeroU64 = NonZeroU64::new(1_000_000_000).unwrap();
+
+#[derive(Copy, Clone, PartialOrd, PartialEq)]
+struct GenericTimerCounterValue(u64);
+
+//--------------------------------------------------------------------------------------------------
+// Global instances
+//--------------------------------------------------------------------------------------------------
+
+/// Boot assembly code overwrites this value with the value of CNTFRQ_EL0 before any Rust code is
+/// executed. This given value here is just a (safe) dummy.
+#[no_mangle]
+static ARCH_TIMER_COUNTER_FREQUENCY: NonZeroU32 = NonZeroU32::MIN;
+
+//--------------------------------------------------------------------------------------------------
+// Private Code
+//--------------------------------------------------------------------------------------------------
+
+fn arch_timer_counter_frequency() -> NonZeroU32 {
+    // Read volatile is needed here to prevent the compiler from optimizing
+    // ARCH_TIMER_COUNTER_FREQUENCY away.
+    //
+    // This is safe, because all the safety requirements as stated in read_volatile()'s
+    // documentation are fulfilled.
+    unsafe { core::ptr::read_volatile(&ARCH_TIMER_COUNTER_FREQUENCY) }
+}
+
+impl GenericTimerCounterValue {
+    pub const MAX: Self = GenericTimerCounterValue(u64::MAX);
+}
+
+impl Add for GenericTimerCounterValue {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self {
+        GenericTimerCounterValue(self.0.wrapping_add(other.0))
+    }
+}
+
+impl From<GenericTimerCounterValue> for Duration {
+    fn from(counter_value: GenericTimerCounterValue) -> Self {
+        if counter_value.0 == 0 {
+            return Duration::ZERO;
+        }
+
+        let frequency: NonZeroU64 = arch_timer_counter_frequency().into();
+
+        // Div<NonZeroU64> implementation for u64 cannot panic.
+        let secs = counter_value.0.div(frequency);
+
+        // This is safe, because frequency can never be greater than u32::MAX, which means the
+        // largest theoretical value for sub_second_counter_value is (u32::MAX - 1). Therefore,
+        // (sub_second_counter_value * NANOSEC_PER_SEC) cannot overflow an u64.
+        //
+        // The subsequent division ensures the result fits into u32, since the max result is smaller
+        // than NANOSEC_PER_SEC. Therefore, just cast it to u32 using `as`.
+        let sub_second_counter_value = counter_value.0 modulo frequency;
+        let nanos = unsafe { sub_second_counter_value.unchecked_mul(u64::from(NANOSEC_PER_SEC)) }
+            .div(frequency) as u32;
+
+        Duration::new(secs, nanos)
+    }
+}
+
+fn max_duration() -> Duration {
+    Duration::from(GenericTimerCounterValue::MAX)
+}
+
+impl TryFrom<Duration> for GenericTimerCounterValue {
+    type Error = &'static str;
+
+    fn try_from(duration: Duration) -> Result<Self, Self::Error> {
+        if duration < resolution() {
+            return Ok(GenericTimerCounterValue(0));
+        }
+
+        if duration > max_duration() {
+            return Err("Conversion error. Duration too big");
+        }
+
+        let frequency: u128 = u32::from(arch_timer_counter_frequency()) as u128;
+        let duration: u128 = duration.as_nanos();
+
+        // This is safe, because frequency can never be greater than u32::MAX, and
+        // (Duration::MAX.as_nanos() * u32::MAX) < u128::MAX.
+        let counter_value =
+            unsafe { duration.unchecked_mul(frequency) }.div(NonZeroU128::from(NANOSEC_PER_SEC));
+
+        // Since we checked above that we are <= max_duration(), just cast to u64.
+        Ok(GenericTimerCounterValue(counter_value as u64))
+    }
+}
+
+#[inline(always)]
+fn read_cntpct() -> GenericTimerCounterValue {
+    // Prevent that the counter is read ahead of time due to out-of-order execution.
+    barrier::isb(barrier::SY);
+    let cnt = CNTPCT_EL0.get();
+
+    GenericTimerCounterValue(cnt)
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// The timer's resolution.
+pub fn resolution() -> Duration {
+    Duration::from(GenericTimerCounterValue(1))
+}
+
+/// The uptime since power-on of the device.
+///
+/// This includes time consumed by firmware and bootloaders.
+pub fn uptime() -> Duration {
+    read_cntpct().into()
+}
+
+/// Spin for a given duration.
+pub fn spin_for(duration: Duration) {
+    let curr_counter_value = read_cntpct();
+
+    let counter_value_delta: GenericTimerCounterValue = match duration.try_into() {
+        Err(msg) => {
+            warn!("spin_for: {}. Skipping", msg);
+            return;
+        }
+        Ok(val) => val,
+    };
+    let counter_value_target = curr_counter_value + counter_value_delta;
+
+    // Busy wait.
+    //
+    // Read CNTPCT_EL0 directly to avoid the ISB that is part of [`read_cntpct`].
+    while GenericTimerCounterValue(CNTPCT_EL0.get()) < counter_value_target {}
+}

diff -uNr 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
--- 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
+++ 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
@@ -140,25 +140,19 @@
     /// Disable pull-up/down on pins 14 and 15.
     #[cfg(feature = "bsp_rpi3")]
     fn disable_pud_14_15_bcm2837(&mut self) {
-        use crate::cpu;
+        use crate::time;
+        use core::time::Duration;

-        // Make an educated guess for a good delay value (Sequence described in the BCM2837
-        // peripherals PDF).
-        //
-        // - According to Wikipedia, the fastest RPi4 clocks around 1.5 GHz.
-        // - The Linux 2837 GPIO driver waits 1 µs between the steps.
-        //
-        // So lets try to be on the safe side and default to 2000 cycles, which would equal 1 µs
-        // would the CPU be clocked at 2 GHz.
-        const DELAY: usize = 2000;
+        // The Linux 2837 GPIO driver waits 1 µs between the steps.
+        const DELAY: Duration = Duration::from_micros(1);

         self.registers.GPPUD.write(GPPUD::PUD::Off);
-        cpu::spin_for_cycles(DELAY);
+        time::time_manager().spin_for(DELAY);

         self.registers
             .GPPUDCLK0
             .write(GPPUDCLK0::PUDCLK15::AssertClock + GPPUDCLK0::PUDCLK14::AssertClock);
-        cpu::spin_for_cycles(DELAY);
+        time::time_manager().spin_for(DELAY);

         self.registers.GPPUD.write(GPPUD::PUD::Off);
         self.registers.GPPUDCLK0.set(0);

diff -uNr 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
--- 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
+++ 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
@@ -275,7 +275,7 @@
     }

     /// Retrieve a character.
-    fn read_char(&mut self, blocking_mode: BlockingMode) -> Option<char> {
+    fn read_char_converting(&mut self, blocking_mode: BlockingMode) -> Option<char> {
         // If RX FIFO is empty,
         if self.registers.FR.matches_all(FR::RXFE::SET) {
             // immediately return in non-blocking mode.
@@ -290,7 +290,12 @@
         }

         // Read one character.
-        let ret = self.registers.DR.get() as u8 as char;
+        let mut ret = self.registers.DR.get() as u8 as char;
+
+        // Convert carrige return to newline.
+        if ret == '\r' {
+            ret = '\n'
+        }

         // Update statistics.
         self.chars_read += 1;
@@ -376,14 +381,14 @@
 impl console::interface::Read for PL011Uart {
     fn read_char(&self) -> char {
         self.inner
-            .lock(|inner| inner.read_char(BlockingMode::Blocking).unwrap())
+            .lock(|inner| inner.read_char_converting(BlockingMode::Blocking).unwrap())
     }

     fn clear_rx(&self) {
         // Read from the RX FIFO until it is indicating empty.
         while self
             .inner
-            .lock(|inner| inner.read_char(BlockingMode::NonBlocking))
+            .lock(|inner| inner.read_char_converting(BlockingMode::NonBlocking))
             .is_some()
         {}
     }

diff -uNr 06_uart_chainloader/src/bsp/raspberrypi/driver.rs 07_timestamps/src/bsp/raspberrypi/driver.rs
--- 06_uart_chainloader/src/bsp/raspberrypi/driver.rs
+++ 07_timestamps/src/bsp/raspberrypi/driver.rs
@@ -57,6 +57,17 @@
 /// # Safety
 ///
 /// See child function calls.
+///
+/// # Note
+///
+/// Using atomics here relieves us from needing to use `unsafe` for the static variable.
+///
+/// On `AArch64`, which is the only implemented architecture at the time of writing this,
+/// [`AtomicBool::load`] and [`AtomicBool::store`] are lowered to ordinary load and store
+/// instructions. They are therefore safe to use even with MMU + caching deactivated.
+///
+/// [`AtomicBool::load`]: core::sync::atomic::AtomicBool::load
+/// [`AtomicBool::store`]: core::sync::atomic::AtomicBool::store
 pub unsafe fn init() -> Result<(), &'static str> {
     static INIT_DONE: AtomicBool = AtomicBool::new(false);
     if INIT_DONE.load(Ordering::Relaxed) {

diff -uNr 06_uart_chainloader/src/bsp/raspberrypi/kernel.ld 07_timestamps/src/bsp/raspberrypi/kernel.ld
--- 06_uart_chainloader/src/bsp/raspberrypi/kernel.ld
+++ 07_timestamps/src/bsp/raspberrypi/kernel.ld
@@ -3,6 +3,8 @@
  * Copyright (c) 2018-2022 Andre Richter <andre.o.richter@gmail.com>
  */

+__rpi_phys_dram_start_addr = 0;
+
 /* The physical address at which the the kernel binary will be loaded by the Raspberry's firmware */
 __rpi_phys_binary_load_addr = 0x80000;

@@ -26,8 +28,7 @@

 SECTIONS
 {
-    /* Set the link address to 32 MiB */
-    . = 0x2000000;
+    . =  __rpi_phys_dram_start_addr;

     /***********************************************************************************************
     * Boot Core Stack
@@ -44,7 +45,6 @@
     /***********************************************************************************************
     * Code + RO Data + Global Offset Table
     ***********************************************************************************************/
-    __binary_nonzero_start = .;
     .text :
     {
         KEEP(*(.text._start))
@@ -60,10 +60,6 @@
     ***********************************************************************************************/
     .data : { *(.data*) } :segment_data

-    /* Fill up to 8 byte, b/c relocating the binary is done in u64 chunks */
-    . = ALIGN(8);
-    __binary_nonzero_end_exclusive = .;
-
     /* Section is zeroed in pairs of u64. Align start and end to 16 bytes */
     .bss (NOLOAD) : ALIGN(16)
     {

diff -uNr 06_uart_chainloader/src/bsp/raspberrypi/memory.rs 07_timestamps/src/bsp/raspberrypi/memory.rs
--- 06_uart_chainloader/src/bsp/raspberrypi/memory.rs
+++ 07_timestamps/src/bsp/raspberrypi/memory.rs
@@ -11,7 +11,6 @@
 /// The board's physical memory map.
 #[rustfmt::skip]
 pub(super) mod map {
-    pub const BOARD_DEFAULT_LOAD_ADDRESS: usize =        0x8_0000;

     pub const GPIO_OFFSET:         usize = 0x0020_0000;
     pub const UART_OFFSET:         usize = 0x0020_1000;
@@ -36,13 +35,3 @@
         pub const PL011_UART_START: usize = START + UART_OFFSET;
     }
 }
-
-//--------------------------------------------------------------------------------------------------
-// Public Code
-//--------------------------------------------------------------------------------------------------
-
-/// The address on which the Raspberry firmware loads every binary by default.
-#[inline(always)]
-pub fn board_default_load_addr() -> *const u64 {
-    map::BOARD_DEFAULT_LOAD_ADDRESS as _
-}

diff -uNr 06_uart_chainloader/src/cpu.rs 07_timestamps/src/cpu.rs
--- 06_uart_chainloader/src/cpu.rs
+++ 07_timestamps/src/cpu.rs
@@ -14,6 +14,3 @@
 // Architectural Public Reexports
 //--------------------------------------------------------------------------------------------------
 pub use arch_cpu::{nop, wait_forever};
-
-#[cfg(feature = "bsp_rpi3")]
-pub use arch_cpu::spin_for_cycles;

diff -uNr 06_uart_chainloader/src/driver.rs 07_timestamps/src/driver.rs
--- 06_uart_chainloader/src/driver.rs
+++ 07_timestamps/src/driver.rs
@@ -4,7 +4,10 @@

 //! Driver support.

-use crate::synchronization::{interface::Mutex, NullLock};
+use crate::{
+    info,
+    synchronization::{interface::Mutex, NullLock},
+};

 //--------------------------------------------------------------------------------------------------
 // Private Definitions
@@ -151,4 +154,14 @@
             }
         });
     }
+
+    /// Enumerate all registered device drivers.
+    pub fn enumerate(&self) {
+        let mut i: usize = 1;
+        self.for_each_descriptor(|descriptor| {
+            info!("      {}. {}", i, descriptor.device_driver.compatible());
+
+            i += 1;
+        });
+    }
 }

diff -uNr 06_uart_chainloader/src/main.rs 07_timestamps/src/main.rs
--- 06_uart_chainloader/src/main.rs
+++ 07_timestamps/src/main.rs
@@ -108,9 +108,12 @@

 #![allow(clippy::upper_case_acronyms)]
 #![feature(asm_const)]
+#![feature(const_option)]
 #![feature(format_args_nl)]
+#![feature(nonzero_min_max)]
 #![feature(panic_info_message)]
 #![feature(trait_alias)]
+#![feature(unchecked_math)]
 #![no_main]
 #![no_std]

@@ -121,6 +124,7 @@
 mod panic_wait;
 mod print;
 mod synchronization;
+mod time;

 /// Early init code.
 ///
@@ -142,55 +146,30 @@
     kernel_main()
 }

-const MINILOAD_LOGO: &str = r#"
- __  __ _      _ _                 _
-|  \/  (_)_ _ (_) |   ___  __ _ __| |
-| |\/| | | ' \| | |__/ _ \/ _` / _` |
-|_|  |_|_|_||_|_|____\___/\__,_\__,_|
-"#;
-
 /// The main function running after the early init.
 fn kernel_main() -> ! {
-    use console::console;
-
-    println!("{}", MINILOAD_LOGO);
-    println!("{:^37}", bsp::board_name());
-    println!();
-    println!("[ML] Requesting binary");
-    console().flush();
-
-    // Discard any spurious received characters before starting with the loader protocol.
-    console().clear_rx();
-
-    // Notify `Minipush` to send the binary.
-    for _ in 0..3 {
-        console().write_char(3 as char);
-    }
+    use core::time::Duration;

-    // Read the binary's size.
-    let mut size: u32 = u32::from(console().read_char() as u8);
-    size |= u32::from(console().read_char() as u8) << 8;
-    size |= u32::from(console().read_char() as u8) << 16;
-    size |= u32::from(console().read_char() as u8) << 24;
-
-    // Trust it's not too big.
-    console().write_char('O');
-    console().write_char('K');
-
-    let kernel_addr: *mut u8 = bsp::memory::board_default_load_addr() as *mut u8;
-    unsafe {
-        // Read the kernel byte by byte.
-        for i in 0..size {
-            core::ptr::write_volatile(kernel_addr.offset(i as isize), console().read_char() as u8)
-        }
+    info!(
+        "{} version {}",
+        env!("CARGO_PKG_NAME"),
+        env!("CARGO_PKG_VERSION")
+    );
+    info!("Booting on: {}", bsp::board_name());
+
+    info!(
+        "Architectural timer resolution: {} ns",
+        time::time_manager().resolution().as_nanos()
+    );
+
+    info!("Drivers loaded:");
+    driver::driver_manager().enumerate();
+
+    // Test a failing timer case.
+    time::time_manager().spin_for(Duration::from_nanos(1));
+
+    loop {
+        info!("Spinning for 1 second");
+        time::time_manager().spin_for(Duration::from_secs(1));
     }
-
-    println!("[ML] Loaded! Executing the payload now\n");
-    console().flush();
-
-    // Use black magic to create a function pointer.
-    let kernel: fn() -> ! = unsafe { core::mem::transmute(kernel_addr) };
-
-    // Jump to loaded kernel!
-    kernel()
 }

diff -uNr 06_uart_chainloader/src/panic_wait.rs 07_timestamps/src/panic_wait.rs
--- 06_uart_chainloader/src/panic_wait.rs
+++ 07_timestamps/src/panic_wait.rs
@@ -45,15 +45,18 @@
     // Protect against panic infinite loops if any of the following code panics itself.
     panic_prevent_reenter();

+    let timestamp = crate::time::time_manager().uptime();
     let (location, line, column) = match info.location() {
         Some(loc) => (loc.file(), loc.line(), loc.column()),
         _ => ("???", 0, 0),
     };

     println!(
-        "Kernel panic!\n\n\
+        "[  {:>3}.{:06}] Kernel panic!\n\n\
         Panic location:\n      File '{}', line {}, column {}\n\n\
         {}",
+        timestamp.as_secs(),
+        timestamp.subsec_micros(),
         location,
         line,
         column,

diff -uNr 06_uart_chainloader/src/print.rs 07_timestamps/src/print.rs
--- 06_uart_chainloader/src/print.rs
+++ 07_timestamps/src/print.rs
@@ -34,3 +34,51 @@
         $crate::print::_print(format_args_nl!($($arg)*));
     })
 }
+
+/// Prints an info, with a newline.
+#[macro_export]
+macro_rules! info {
+    ($string:expr) => ({
+        let timestamp = $crate::time::time_manager().uptime();
+
+        $crate::print::_print(format_args_nl!(
+            concat!("[  {:>3}.{:06}] ", $string),
+            timestamp.as_secs(),
+            timestamp.subsec_micros(),
+        ));
+    });
+    ($format_string:expr, $($arg:tt)*) => ({
+        let timestamp = $crate::time::time_manager().uptime();
+
+        $crate::print::_print(format_args_nl!(
+            concat!("[  {:>3}.{:06}] ", $format_string),
+            timestamp.as_secs(),
+            timestamp.subsec_micros(),
+            $($arg)*
+        ));
+    })
+}
+
+/// Prints a warning, with a newline.
+#[macro_export]
+macro_rules! warn {
+    ($string:expr) => ({
+        let timestamp = $crate::time::time_manager().uptime();
+
+        $crate::print::_print(format_args_nl!(
+            concat!("[W {:>3}.{:06}] ", $string),
+            timestamp.as_secs(),
+            timestamp.subsec_micros(),
+        ));
+    });
+    ($format_string:expr, $($arg:tt)*) => ({
+        let timestamp = $crate::time::time_manager().uptime();
+
+        $crate::print::_print(format_args_nl!(
+            concat!("[W {:>3}.{:06}] ", $format_string),
+            timestamp.as_secs(),
+            timestamp.subsec_micros(),
+            $($arg)*
+        ));
+    })
+}

diff -uNr 06_uart_chainloader/src/time.rs 07_timestamps/src/time.rs
--- 06_uart_chainloader/src/time.rs
+++ 07_timestamps/src/time.rs
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2020-2022 Andre Richter <andre.o.richter@gmail.com>
+
+//! Timer primitives.
+
+#[cfg(target_arch = "aarch64")]
+#[path = "_arch/aarch64/time.rs"]
+mod arch_time;
+
+use core::time::Duration;
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Provides time management functions.
+pub struct TimeManager;
+
+//--------------------------------------------------------------------------------------------------
+// Global instances
+//--------------------------------------------------------------------------------------------------
+
+static TIME_MANAGER: TimeManager = TimeManager::new();
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// Return a reference to the global TimeManager.
+pub fn time_manager() -> &'static TimeManager {
+    &TIME_MANAGER
+}
+
+impl TimeManager {
+    /// Create an instance.
+    pub const fn new() -> Self {
+        Self
+    }
+
+    /// The timer's resolution.
+    pub fn resolution(&self) -> Duration {
+        arch_time::resolution()
+    }
+
+    /// The uptime since power-on of the device.
+    ///
+    /// This includes time consumed by firmware and bootloaders.
+    pub fn uptime(&self) -> Duration {
+        arch_time::uptime()
+    }
+
+    /// Spin for a given duration.
+    pub fn spin_for(&self, duration: Duration) {
+        arch_time::spin_for(duration)
+    }
+}

diff -uNr 06_uart_chainloader/tests/boot_test_string.rb 07_timestamps/tests/boot_test_string.rb
--- 06_uart_chainloader/tests/boot_test_string.rb
+++ 07_timestamps/tests/boot_test_string.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+EXPECTED_PRINT = 'Spinning for 1 second'

diff -uNr 06_uart_chainloader/tests/chainboot_test.rb 07_timestamps/tests/chainboot_test.rb
--- 06_uart_chainloader/tests/chainboot_test.rb
+++ 07_timestamps/tests/chainboot_test.rb
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-# SPDX-License-Identifier: MIT OR Apache-2.0
-#
-# Copyright (c) 2020-2022 Andre Richter <andre.o.richter@gmail.com>
-
-require_relative '../../common/serial/minipush'
-require_relative '../../common/tests/boot_test'
-require 'pty'
-
-# Match for the last print that 'demo_payload_rpiX.img' produces.
-EXPECTED_PRINT = 'Echoing input now'
-
-# Wait for request to power the target.
-class PowerTargetRequestTest < SubtestBase
-    MINIPUSH_POWER_TARGET_REQUEST = 'Please power the target now'
-
-    def initialize(qemu_cmd, pty_main)
-        super()
-        @qemu_cmd = qemu_cmd
-        @pty_main = pty_main
-    end
-
-    def name
-        'Waiting for request to power target'
-    end
-
-    def run(qemu_out, _qemu_in)
-        expect_or_raise(qemu_out, MINIPUSH_POWER_TARGET_REQUEST)
-
-        # Now is the time to start QEMU with the chainloader binary. QEMU's virtual tty connects to
-        # the MiniPush instance spawned on pty_main, so that the two processes talk to each other.
-        Process.spawn(@qemu_cmd, in: @pty_main, out: @pty_main, err: '/dev/null')
-    end
-end
-
-# Extend BootTest so that it listens on the output of a MiniPush instance, which is itself connected
-# to a QEMU instance instead of a real HW.
-class ChainbootTest < BootTest
-    MINIPUSH = '../common/serial/minipush.rb'
-
-    def initialize(qemu_cmd, payload_path)
-        super(qemu_cmd, EXPECTED_PRINT)
-
-        @test_name = 'Boot test using Minipush'
-
-        @payload_path = payload_path
-    end
-
-    private
-
-    # override
-    def setup
-        pty_main, pty_secondary = PTY.open
-        mp_out, _mp_in = PTY.spawn("ruby #{MINIPUSH} #{pty_secondary.path} #{@payload_path}")
-
-        # The subtests (from this class and the parents) listen on @qemu_out_wrapped. Hence, point
-        # it to MiniPush's output.
-        @qemu_out_wrapped = PTYLoggerWrapper.new(mp_out, "\r\n")
-
-        # Important: Run this subtest before the one in the parent class.
-        @console_subtests.prepend(PowerTargetRequestTest.new(@qemu_cmd, pty_main))
-    end
-
-    # override
-    def finish
-        super()
-        @test_output.map! { |x| x.gsub(/.*\r/, '  ') }
-    end
-end
-
-##--------------------------------------------------------------------------------------------------
-## Execution starts here
-##--------------------------------------------------------------------------------------------------
-payload_path = ARGV.pop
-qemu_cmd = ARGV.join(' ')
-
-ChainbootTest.new(qemu_cmd, payload_path).run

diff -uNr 06_uart_chainloader/update.sh 07_timestamps/update.sh
--- 06_uart_chainloader/update.sh
+++ 07_timestamps/update.sh
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-cd ../05_drivers_gpio_uart
-BSP=rpi4 make
-cp kernel8.img ../06_uart_chainloader/demo_payload_rpi4.img
-make
-cp kernel8.img ../06_uart_chainloader/demo_payload_rpi3.img
-rm kernel8.img