Skip to content

Common architecture for .enable() APIs #614

@bradleyharden

Description

@bradleyharden

@jbeaurivage, @vccggorski and @vcchtjader,

I (think?) I had some useful insights recently, while trying to solve #609. I wanted to lay out the problem and my proposed solution, to see if we can come up with a common approach that we all like. That would also help keep the APIs consistent across the HAL.

I'll start with the spi module. Right now, the two main structs look like this:

pub struct Config<P, M, Z>
where
    P: ValidPads,
    M: OpMode,
    Z: Size,
{
    // ...
}
pub struct Spi<C: ValidConfig> { ... }

Users create an spi::Config and use a builder API to configure it, before calling .enable(), which consumes the Config and places it inside the Spi. The AnyKind trait pattern is used (via ValidConfig) to reduce the repetition of type parameters on the Spi struct.

Right now, each call of a builder API function on Config corresponds to a register write. I liked this approach, because it gives users maximum freedom, and it means you don't need to store any of the configuration within Config; it is all stored directly in the registers.

On the other hand, @vccggorski preferred a different approach for the clock::v2 module. There, we define these structs.

pub struct Gclk<G, I>
where
    G: GclkId
    I: GclkSourceId
{
    // ....
}

pub type EnabledGclk<G, I, N> = Enabled<Gclk<G, I,>, N>;

In this API, users create instances of Gclk directly. Gclk also uses a builder API, but the configuration is stored within the struct. None of the builder-API functions actually write the registers. Only when calling .enable() are the registers actually written. Then, the Gclk is packed inside the Enabled struct, which provides compile-time counting of dependent clocks using the N type parameter.

@vccggorski and I have had a few debates about this approach. I don't really like that the configuration data is stored permanently within the Gclk. To me, it seems redundant and less compact than it could be.

However, I have to admit that, on at least one occasion, I have run into a problem with initialization order in the spi module. Moreover, I suspect #609 is also caused by initialization order. From that, I conclude that we should probably store the configuration parameters locally, and perform all the register writes in .enable(), so that we can guarantee the correct initialization order.

So I was left with a problem. I want to store all the configuration options within spi::Config and perform all register writes in the .enable() function, but I also don't want to bloat the size of spi::Spi. I think that is a fairly legitimate concern, too, because the full Config struct ends up looking like this:

pub struct Config<P, M, Z>
where
    P: ValidPads,
    M: OpMode,
    Z: Size,
{
    regs: Registers<P::Sercom>,
    pads: P,
    mode: PhantomData<M>,
    size: Z,
    freq: Hertz,
    cpol: Polarity,
    cpha: Phase,
    bit_order: BitOrder,
    baud: Hertz,
    ibon: bool,
    run_in_standby: bool,
}

You end up carrying around a minimum of something like 9 extra bytes for no real reason.

That's when I realized that the Config struct doesn't have to be stored within Spi. Instead, I can unpack the useful contents of Config in the .enable() call, and then drop the rest. With this approach, Spi ends up looking like this

pub struct Spi<P, M, Z>
where
    P: ValidPads,
    M: OpMode,
    Z: Size,
{
    // ...
}

where its fields are limited to keep it as compact as possible. The only downside is that the type parameters have to be repeated on Spi. But I think that actually ended up simplifying things in this particular case.

At this point, @jbeaurivage might ask how I handle reconfiguring an enabled Spi. The existing API has an Spi::reconfigure function that will temporarily disable the peripheral, call a user-provided closure, and then re-enable the peripheral. The user-provided closure effectively had the signature FnOnce(&mut Config). Users could then call methods of a non-builder API, with signatures like

impl Config {
    pub fn get_setting(&self) -> Setting {}
    pub fn set_setting(&mut self, setting: Setting) {}
}

With the new approach, the Config struct no longer has the non-builder API, because it is never used for anything other than constructing an Spi struct. Instead, I created a separate Reconfig struct that has get_ and set_ methods, and I changed the Spi::reconfigure function to take an FnOnce(Reconfig) closure.

I like this approach, because it provides the best of both worlds. It gives HAL authors complete control over creation and initialization, but it doesn't burden users with an unnecessarily bloated struct.

Moreover, I think the approach could be equally applicable to the clock::v2 API. There, we could provide a gclk::Config struct that acts like spi::Config. It would store all of the configuration options and perform all register writes in .enable(). The gclk::Gclk struct could then be stripped down to only the necessary fields.

At this point, I think @vccggorski might object, because he probably doesn't like the idea of calling gclk::Config::new to create a new Gclk. I also don't really like that, which is why I think we could provide

impl<G, I> Gclk<G, I>
where
    G: GclkId,
    I: GclkSourceId
{
    pub fn config<S>(token: GclkToken<G>, source: S) -> (Config<G, I>, S::Inc)
    where
        S: Source<Id = I>,
    {
    }
}

Existing calls to Gclk::new would instead become calls to Gclk::config. I actually think that's better, because it indicates to the user that the Gclk still needs configuration and is not yet enabled.

What do you all think of this approach? Do you see any problems or have any concerns? If it satisfies everyone, I would like to use this common approach in all of our code. I think that would give the HAL a very consistent feel.

Also, please see #613 for a draft of the new approach with the spi module. I think it's pretty much done, but I still need to update the documentation. I also don't want to go too far with it before I get buy-in from the rest of you.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions