Let’s write a UART driver

The QEMU ‘virt’ machine has a PL011 UART, so let’s write a driver for that.

  1. const FLAG_REGISTER_OFFSET: usize = 0x18;
  2. const FR_BUSY: u8 = 1 << 3;
  3. const FR_TXFF: u8 = 1 << 5;
  4. /// Minimal driver for a PL011 UART.
  5. #[derive(Debug)]
  6. pub struct Uart {
  7.     base_address: *mut u8,
  8. }
  9. impl Uart {
  10.     /// Constructs a new instance of the UART driver for a PL011 device at the
  11.     /// given base address.
  12.     ///
  13.     /// # Safety
  14.     ///
  15.     /// The given base address must point to the 8 MMIO control registers of a
  16.     /// PL011 device, which must be mapped into the address space of the process
  17.     /// as device memory and not have any other aliases.
  18.     pub unsafe fn new(base_address: *mut u8) -> Self {
  19.         Self { base_address }
  20.     }
  21.     /// Writes a single byte to the UART.
  22.     pub fn write_byte(&self, byte: u8) {
  23.         // Wait until there is room in the TX buffer.
  24.         while self.read_flag_register() & FR_TXFF != 0 {}
  25.         // SAFETY: We know that the base address points to the control
  26.         // registers of a PL011 device which is appropriately mapped.
  27.         unsafe {
  28.             // Write to the TX buffer.
  29.             self.base_address.write_volatile(byte);
  30.         }
  31.         // Wait until the UART is no longer busy.
  32.         while self.read_flag_register() & FR_BUSY != 0 {}
  33.     }
  34.     fn read_flag_register(&self) -> u8 {
  35.         // SAFETY: We know that the base address points to the control
  36.         // registers of a PL011 device which is appropriately mapped.
  37.         unsafe { self.base_address.add(FLAG_REGISTER_OFFSET).read_volatile() }
  38.     }
  39. }
  • Note that Uart::new is unsafe while the other methods are safe. This is because as long as the caller of Uart::new guarantees that its safety requirements are met (i.e. that there is only ever one instance of the driver for a given UART, and nothing else aliasing its address space), then it is always safe to call write_byte later because we can assume the necessary preconditions.
  • We could have done it the other way around (making new safe but write_byte unsafe), but that would be much less convenient to use as every place that calls write_byte would need to reason about the safety
  • This is a common pattern for writing safe wrappers of unsafe code: moving the burden of proof for soundness from a large number of places to a smaller number of places.