Vamos a escribir un controlador de UART

La máquina "virt" de QEMU tiene una UART [PL011]https://developer.arm.com/documentation/ddi0183/g), así que vamos a escribir un controlador para ella.

const FLAG_REGISTER_OFFSET: usize = 0x18;
const FR_BUSY: u8 = 1 << 3;
const FR_TXFF: u8 = 1 << 5;

/// Controlador mínimo para un UART PL011.
#[derive(Debug)]
pub struct Uart {
    base_address: *mut u8,
}

impl Uart {
    /// Construye una instancia nueva del controlador de UART para un dispositivo PL011 en la
    /// dirección base proporcionada.
    ///
    /// # Seguridad
    ///
    /// La dirección base debe apuntar a los 8 registros de control MMIO de un 
    /// dispositivo PL011, que debe asignarse al espacio de direcciones del proceso
    /// como memoria del dispositivo y no tener ningún otro alias.
    pub unsafe fn new(base_address: *mut u8) -> Self {
        Self { base_address }
    }

    /// Escribe un solo byte en el UART.
    pub fn write_byte(&self, byte: u8) {
        // Espera hasta que haya espacio en el búfer de TX.
        while self.read_flag_register() & FR_TXFF != 0 {}

        // SAFETY: We know that the base address points to the control
        // registers of a PL011 device which is appropriately mapped.
        unsafe {
            // Escribe en el búfer de TX.
            self.base_address.write_volatile(byte);
        }

        // Espera hasta que el UART esté libre.
        while self.read_flag_register() & FR_BUSY != 0 {}
    }

    fn read_flag_register(&self) -> u8 {
        // SAFETY: We know that the base address points to the control
        // registers of a PL011 device which is appropriately mapped.
        unsafe { self.base_address.add(FLAG_REGISTER_OFFSET).read_volatile() }
    }
}
  • Ten en cuenta que Uart::new no es seguro, mientras que los otros métodos sí lo son. Esto se debe a que mientras que el llamador de Uart::new asegure que se cumplan sus requisitos de seguridad (es decir, que solo haya una instancia del controlador para una UART determinada y que nada más asigne alias a su espacio de direcciones), siempre es más seguro llamar a write_byte más adelante, ya que podemos asumir\ las condiciones previas necesarias.
  • Podríamos haberlo hecho al revés (haciendo que new fuese seguro y write_byte no seguro), pero\sería mucho menos cómodo de usar, ya que cada lugar que llamase a write_byte tendría que pensar en la seguridad
  • Este es un patrón común para escribir envoltorios seguros de código inseguro: mover la carga de la prueba de seguridad de un gran número de lugares a otro más pequeño.