Question about strategies of defining peripherals

In the Rust Embedded book and sources of various projects I see people wrap all peripherals into a single struct.

pub struct Peripherals {
    UART0: Option<Uart0>,
    UART1: Option<Uart1>,
    SPI0: Option<Spi0>,
    . . .
}

This struct is then initialized as static somewhere early. As I see it, this could become handy if kernel calls init() for all peripherals.

But what are the true benefits of doing it this way? Why not just refer to each module and take only peripheral you need at a time?

fn main() -> ! {
    let uart = Uart0.take().unwrap();
    // . . . do stuff
    uart.release();
    . . .
}

This looks more reasonable to me. For example, if implementing a UART driver and a serial console on top of it, you don't need to know if other peripherals even exist.

The .take() API is implemented using a static variable, which comes at a cost of some RAM. Having that API for every single peripheral would increase that cost accordingly, so it wasn't done.

Instead, you can move individual peripherals out of the Peripherals struct and move them to where they're needed. Libraries like RTFM can make this somewhat easier, by allowing initialization from an init function at runtime.

Code still doesn't have to know about any other peripherals with this model. You'd simply define, for example, a Serial struct that takes ownership of the UART0, and call a constructor, passing the UART0.

Hi,
well actually I'm defining the peripherals as statics within the crate that is implementing the peripheral. To ensure thread safety those statics are wrapped with a kind of a mutex. However, I'm currently investigating a kind of different approach where I'm creating some sort of a "device manager" where peripherals a baremetal program would like to use could "register" those peripherals. Than I'd pass a reference to this "device manager" around where the peripheral that should be accessed could be requested from. But the API is not yet done and I'm still in a PoC kind of phase. The "drawback" of the current implementation of this approach is that you would need an allocator to dynamically store the references to the peripherals within a Box. This is not an issue for me as I'm mainly targeting RaspberryPiwhere there is "plenty" of memory available.

I decided to try something similar, probably. I implement peripherals as state machines which use atomics to save their "taken" state. Because of this I cannot use them in static contexts (maybe I'm just doing something wrong), so I thought that I will access peripherals through some layer inbetween kernel and hardware and only "take" those which and when I need. Not sure where it will lead me, though and will it be good or not

I'd appreciate to here from your experience from the approach you are going forward with.
The draw back of statics is they can only created with const fn functions. So if they need special "treatment" like initialization that would mutate their state inside the static this would need special wrapping and Mutex kind of locks to ensure thread safe handling of this.

I think - if you prepare a finite list of peripherals you'd like to support this might also be a bit easier to manage how to store and manage the peripherals in your rust code. I hope and can come up with an approach where also other crates could add peripherals that would not have been known upfront when building the initial peripheral handling.

If you'd like to share some details on this statement:

I might be able to share my experience here as well in some more detail :wink:

I'm still doing my first steps and for now it is like

Module platform::peripheral. It contains sub-modues with only basic functions to access and modify peripheral mapped registers.

// platform::peripheral::gpio
pub mod function {
    pub struct Unknown;
    pub struct Input;
    pub struct Output;
    pub struct Alt0;
    pub struct Alt1;
    pub struct Alt2;
    pub struct Alt3;
    pub struct Alt4;
    pub struct Alt5;

    #[repr(u8)]
    pub enum Fsel {
        Input = 0b000,
        Output = 0b001,
        Alt0 = 0b100,
        Alt1 = 0b101,
        Alt2 = 0b110,
        Alt3 = 0b111,
        Alt4 = 0b011,
        Alt5 = 0b010
    }

    pub trait GpioFn { fn fsel(&self) -> u32; }

    impl GpioFn for Input { fn fsel(&self) -> u32 { Fsel::Input as u32 } }
    impl GpioFn for Output { fn fsel(&self) -> u32 { Fsel::Output as u32 } }
    impl GpioFn for Alt0 { fn fsel(&self) -> u32 { Fsel::Alt0 as u32 } }
    impl GpioFn for Alt1 { fn fsel(&self) -> u32 { Fsel::Alt1 as u32 } }
    impl GpioFn for Alt2 { fn fsel(&self) -> u32 { Fsel::Alt2 as u32 } }
    impl GpioFn for Alt3 { fn fsel(&self) -> u32 { Fsel::Alt3 as u32 } }
    impl GpioFn for Alt4 { fn fsel(&self) -> u32 { Fsel::Alt4 as u32 } }
    impl GpioFn for Alt5 { fn fsel(&self) -> u32 { Fsel::Alt5 as u32 } }
}

static TAKEN: AtomicU64 = AtomicU64::new(0);

pub fn take_pin(number: u8) -> Option<Pin<function::Unknown>> {
    let b: u64 = 1 << number as u64;
    if number > 53 || TAKEN.fetch_or(b, Ordering::AcqRel) & b == b {
        None
    } else {
        Some(Pin { number, function: function::Unknown })
    }
}

pub struct Pin<FN> {
    number: u8,
    function: FN
}

impl<FN> Pin<FN> {
    fn into<F: function::GpioFn>(self, function: F) -> Pin<F> {
        unsafe {
            self.fmodify(register::GPFSEL0::as_ptr(), 10, 3, function.fsel());
        }
        Pin { number: self.number, function }

    }

    pub fn into_input(self) -> Pin<function::Input> {
        self.into(function::Input)
    }

    pub fn into_output(self) -> Pin<function::Output> {
        self.into(function::Output)
    }

    pub fn into_alt0(self) -> Pin<function::Alt0> {
        self.into(function::Alt0)
    }

    pub fn into_alt1(self) -> Pin<function::Alt1> {
        self.into(function::Alt1)
    }

    pub fn into_alt2(self) -> Pin<function::Alt2> {
        self.into(function::Alt2)
    }

    pub fn into_alt3(self) -> Pin<function::Alt3> {
        self.into(function::Alt3)
    }

    pub fn into_alt4(self) -> Pin<function::Alt4> {
        self.into(function::Alt4)
    }

    pub fn into_alt5(self) -> Pin<function::Alt5> {
        self.into(function::Alt5)
    }

    pub fn level(&self) -> u32 {
        unsafe {
            let p = register::GPLEV0::as_ptr();
            let a = p.add((self.number / 32) as usize);
            let r = ReadOnly::<u32>::new(a as usize);
            let f = RegisterField::<u32>::new(self.number % 32, 1);
            r.read_field(f)
        }
    }

    unsafe fn fmodify(&self, ptr: *mut u32, fcnt: u8, fsize: u8, val: u32) {
        let a = ptr.add((self.number / fcnt) as usize);
        let r = ReadWrite::<u32>::new(a as usize);
        let f = RegisterField::<u32>::new((self.number % fcnt) * fsize, fsize);
        r.modify_field(f, val);
    }
}

impl Pin<function::Output> {
    pub fn set(&self) {
        unsafe {
            self.fmodify(register::GPSET0::as_ptr(), 32, 1, 1);
        }
    }

    pub fn clear(&self) {
        unsafe {
            self.fmodify(register::GPCLR0::as_ptr(), 32, 1, 1)
        }
    }
}

mod register {
// register difinitions
// I struggled a lot a couple of months ago to implement register abstractions the way I'll like
// and finally got across your idea of modules + constants, but adapted it, so register definitions
// is something similar to what you have in ruspiro-registers
}

Then I have module device. device drivers accessed by kernel to manage devices. Those drivers have access to peripherals they need:

// device::ackled
use crate::platform::peripheral::gpio;

pub struct AckLED {
    pin: gpio::Pin<gpio::function::Output>
}

impl AckLED {
    pub fn init() -> Self {
        Self { pin: gpio::take_pin(42).unwrap().into_output() }
    }

    pub fn on(&self) {
        self.pin.set();
    }

    pub fn off(&self) {
        self.pin.clear();
    }

    pub fn level(&self) -> u32 {
        self.pin.level()
    }
}

Here I will also have shared state of the device.
And finally when something needs a device, it can access it:

//kernel
#[inline(never)]
pub fn main() -> ! {
    use crate::device::ackled;

    let ackled = ackled::AckLED::init();
    ackled.on();
    loop {}
}

Now I'm working on enabling interrupts, then will come shared access to resources. I think I want to implement something as described here.

Hey thanks for sharing your approach.
To enable safe shared access to the peripherals in my implementation I did invent the Singleton struct (see ruspiro-singleton ). What drove me nuts a bit was how to securely disable and re-enable interrupts while performing the actual "lock" (sepecialy if it comes to nested locks and the fact that on the raspberry Pi each core could have interrupts globally enabled/disabled). I finally decided to use simple AtomicBool operations to guard the locking inside the Singleton hoping that the atmic operation itself might not get interrupted, which could lead to a deadlock if the interrupt is handled on the same core as the atomic operation. But this might be a to optimistic for a long-term solution.

Regarding the peripherals I've also moved away from the static representation and also tried to build some sort of HAL around them to share only the traits for those peripherals some other crate would request access usage.

The GPIO Trait looks like this:

/// This trait provides access to the GPIO hardware present in embedded systems.
pub trait HalGpio {
    /// Request a [HalGpioPin] from the GPIO peripheral for further usage. It returns a pin with initially undefined
    /// function and pud settings. If the requested pin is already in use the implementation shall return a respective
    /// [Error].
    fn use_pin(&mut self, id: u32) -> Result<Box<dyn HalGpioPin>, BoxError>;

    /// Release a [GpioPin] that has been in use previously. If the pin to be released has not been in use the
    /// implementation shall return an [Error]. Even though it might not be a real error as such it allows the caller to
    /// properly handle this state.
    fn release_pin(&mut self, id: u32) -> Result<(), BoxError>;

    /// Register an event handler that will be called whenever the given event is detected for the [GpioPin]. The
    /// [HalGpioPin] need to be configured as [HalGpioPinInput]. As the specified events quite likely are triggerd from an
    /// interrupt the implememter of this function need to ensure that the corresponding interrupts are enabled and
    /// activated.
    fn register_event_handler_always(
        &mut self,
        gpio_pin: &dyn HalGpioPinInput,
        event: GpioEvent,
        handler: Box<dyn FnMut() + 'static + Send>,
    );

    /// Register an event handler that will be called only once for the next occurance of the given event is detected
    /// for the [HalGpioPin]. The [HalGpioPin] need to be configured as [HalGpioPinInput]. As the specified events quite
    /// likely are triggerd from an interrupt the implememter of this function need to ensure that the corresponding
    /// interrupts are enabled and activated.
    fn register_event_handler_onetime(
        &mut self,
        gpio_pin: &dyn HalGpioPinInput,
        event: GpioEvent,
        handler: Box<dyn FnOnce() + 'static + Send>,
    );

    /// Unregister an event handler of the given type for the [HalGpioPin].
    fn unregister_event_handler(&mut self, gpio_pin: &dyn HalGpioPin, event: GpioEvent);
}

and the one for the GPIO-Pin:

/// The representation of a generic GPIO PIN
pub trait HalGpioPin {
    /// return the identifier of this [HalGpioPin]
    fn id(&self) -> u32;

    /// re-configure the [HalGpioPin] as an Input pin. This is a stateful operation at the hardware layer
    /// so even if the [HalGpioPin] get's out of scope this setting remains valid
    /// TODO: verify if this is a valid/desired appraoch
    fn into_input(self) -> Box<dyn HalGpioPinInput>;

    /// re-configure the [HalGpioPin] as an Output pin. This is a stateful operation at the hardware layer
    /// so even if the [HalGpioPin] get's out of scope this setting remains valid
    /// TODO: verify if this is a valid/desired appraoch
    fn into_output(self) -> Box<dyn HalGpioPinOutput>;

    /// re-configure the [HalGpioPin] with an alternative function. This is a stateful operation at the hardware layer
    /// so even if the [HalGpioPin] get's out of scope this setting remains valid.
    /// If a specific hardware dow not support the requested alternative function it shall return an [Err]
    /// TODO: verify if this is a valid/desired appraoch
    fn into_altfunc(self: Box<Self>, function: u8) -> Result<Box<dyn HalGpioPinAltFunc>, BoxError>;

    /// Diable the pull-up/down settings for this [HalGpioPin].
    fn disable_pud(&self);
    
    /// Enable the pull-up settings for this [HalGpioPin].
    fn enable_pud_up(&self);

    /// Enable the pull-down settings for this [HalGpioPin].
    fn enable_pud_down(&self);
}

/// The representation of an input GPIOPin
pub trait HalGpioPinInput: HalGpioPin {
    /// Reads the actual level of the [HalGpioPin] and returns [true] if it is high.
    fn is_high(&self) -> bool;

    /// Reads the actual level of the [HalGpioPin] and returns [true] if it is low.
    fn is_low(&self) -> bool { !self.is_high() }
}

/// The representation of an output GPIOPin
pub trait HalGpioPinOutput: HalGpioPin {
    /// Set the output level of the [HalGpioPin] to high
    fn high(&self);

    /// Set the output level of the [HalGpioPin] to low
    fn low(&self);

    /// Toggle the output level of the [HalGpioPin] either from low -> high or from high -> low
    fn toggle(&self);
}

/// The representation of an GPIOPin with alternative function. The meaning of the function is usually specified within
/// the peripheral documentation of the hardware for which this will be implemented.
pub trait HalGpioPinAltFunc: HalGpioPin {}

I also had this nice way to secure the current pin state with generics and some kind of trait bounds, but this does not work as far as I'm aware if you'd like to make a trait object in a call signature like

fn foo(&dyn HalGpioPin) {}

fn bar(Box<dyn HalGpioPin>) {}

This HAL shall allow me to have some device manager that I could use like this:

fn baz(&device_mgr: DeviceMgr) {
    device_mgr.use_device_mut::<HalGpio,_>(|gpio| {
        gpio.use_pin(17).into_output().unwrap().high();
    }
}

What I still was looking for if there is any way for the compiler to check which GPIO-Pins are already "used" to secure this during compile time rather than runtime :slight_smile: but I guess this is not possible...

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.