Course contentsShow
← Rust
Lesson 1493 of 1,91012. Unsafe Rust and Low-Level AccessFree lesson

Serial Communication Drivers

You will write direct register access code for standard hardware communication interfaces.

In this lesson, you will learn how to bypass OS abstractions and directly interact with memory-mapped UART (Universal Asynchronous Receiver-Transmitter) registers to send data.

The Anatomy of a Hardware Register

When working in no_std environments, "talking" to hardware is just writing to specific memory addresses. A UART driver essentially translates your high-level bytes into toggles on physical pins. To do this, we use the core::ptr module to perform volatile read/write operations, which prevents the Rust compiler from optimizing away what it thinks are "redundant" memory accesses.

Direct Register Access

Imagine a simple UART controller mapped to a base memory address. You typically have a Data Register (to hold the byte) and a Status Register (to check if the hardware is ready to transmit). Because this is hardware, these registers must be marked volatile.

use core::ptr::{read_volatile, write_volatile};

pub struct UartDriver {
    base_addr: *mut u8,
}

impl UartDriver {
    pub const fn new(addr: usize) -> Self {
        Self { base_addr: addr as *mut u8 }
    }

    pub unsafe fn transmit(&self, byte: u8) {
        // Offset 0: Status Register (check if bit 5 is set for 'ready')
        while (read_volatile(self.base_addr.add(5)) & 0x20) == 0 {
            core::hint::spin_loop();
        }
        
        // Offset 0: Data Register (write the byte)
        write_volatile(self.base_addr, byte);
    }
}

Safety and Invariants

In the code above, the unsafe keyword is mandatory because we are performing direct memory access. As an advanced Rustacean, you must ensure two things:

  1. Memory Map Alignment: The base_addr must correctly point to the specific hardware address defined in your device's datasheet (referencing our previous study on memory map layouts).
  2. Volatile Semantics: We use read_volatile and write_volatile because hardware status bits change independently of the CPU. If we used standard pointer dereferencing, the compiler might cache the status bit value, resulting in an infinite loop because it would never "see" the hardware change to "ready."

By wrapping these raw pointers in a struct, you create a safe abstraction layer that hides the unsafe code from the rest of your application, fulfilling the core goal of embedded driver design.

Key Takeaway: Hardware interaction requires volatile pointer access to ensure the compiler respects the real-time, non-deterministic nature of peripheral status registers.