Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] The value of new_typed vs always type erasing drivers #2572

Open
MabezDev opened this issue Nov 20, 2024 · 6 comments
Open

[RFC] The value of new_typed vs always type erasing drivers #2572

MabezDev opened this issue Nov 20, 2024 · 6 comments
Labels
1.0-blocker RFC Request for comment

Comments

@MabezDev
Copy link
Member

For our drivers we have the option of opting into the new_typed method to create a driver without type erasure, i.e Spi<SPI2, Blocking>.

I'm wondering what use cases this opens up (if any) and whether we should always type erase.

I believe having the concrete types helps at least with DMA channels on PDMA devices, as we can (at compile time) check a user isn't passing an invalid DMA channel for the peripheral. We still have a runtime check for the case where they pass AnyDmaChannel, but there is merit to a compile time check.

One downside is that our compiler allocated "inference slot" (the last generic param can be inferred by rustc automatically, but any others can't) it taken by the instance type. This means if we ever want the compiler to infer a generic parameter we can't.

@MabezDev MabezDev added RFC Request for comment 1.0-blocker labels Nov 20, 2024
@bugadani
Copy link
Contributor

bugadani commented Nov 20, 2024

I believe having the concrete types helps at least with DMA channels on PDMA devices, as we can (at compile time) check a user isn't passing an invalid DMA channel for the peripheral. We still have a runtime check for the case where they pass AnyDmaChannel, but there is merit to a compile time check.

I'd like to point out that there is only a single place where this makes any difference:

  • On ESP32-S2, checking for DMA_SPI2 vs DMA_SPI3.
  • In all other cases, DMA channels are either
    • cross-compatible (ESP32 and SPI DMA, UDMA)
    • or there is only a single peripheral (ESP32-S2 I2S)
    • or a single shared DMA controller (ESP32-S2 UDMA, CryptoDMA).

@bugadani
Copy link
Contributor

One place where optional type retention makes sense is GPIOs, where the extra instance dispatch is probably comparable to the IO speed. Are we okay with treating GPIOs as an exception here?

@bugadani
Copy link
Contributor

bugadani commented Nov 20, 2024

Thirdly, we can opt back into type checking DMA channels if we approach constructors differently. Instead of Spi::new().with_dma() we can provide SpiDma::new() that checks, then erases. There are advantages to this approach, namely that we don't forget out the pin setters from the API, which are currently not available on SpiDma.

This would make it harder to implement SpiDma -> Spi conversions if we ever get to supporting releasing just the DMA channel. I'm not sure whether this is a valuable enough capability, though.

@MabezDev
Copy link
Member Author

One place where optional type retention makes sense is GPIOs, where the extra instance dispatch is probably comparable to the IO speed. Are we okay with treating GPIOs as an exception here?

I would be okay with that. It would be nice to maybe do a small benchmark on this to see the difference.

@MabezDev
Copy link
Member Author

Following on from the discussion here: #2573 (comment):

I was looking at our drivers, and future chips, namely the P4. Fortunately I don't think we'll have any issues with our current driver set, with the possible exception of USB, the P4 has one full speed usb and one high speed usb, and the driver is currently not instanced. It's not clear to me how much the two will share, so maybe it's not an issue in this case.

I think this leads us towards type erase by default though, because it only takes a new chip to ruin this. We could work around this later down the line with cfgs etc but I'd rather avoid the issue entirely.

@bugadani
Copy link
Contributor

bugadani commented Nov 20, 2024

I would be okay with that. It would be nice to maybe do a small benchmark on this to see the difference.

My LA isn't the best, but on an ESP32:

Typed

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{
    delay::Delay,
    gpio::{GpioPin, Input, Level, Output, Pin, Pull},
    prelude::*,
};

#[entry]
fn main() -> ! {
    let peripherals = esp_hal::init(esp_hal::Config::default());

    // Set LED GPIOs as an output:
    let led = Output::new_typed(peripherals.GPIO2, Level::Low);

    toggle_pins(led)
}

fn toggle_pins(mut pin: Output<GpioPin<2>>) -> ! {
    loop {
        pin.toggle();
    }
}

image

Erased

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{
    delay::Delay,
    gpio::{Input, Level, Output, Pin, Pull},
    prelude::*,
};

#[entry]
fn main() -> ! {
    let peripherals = esp_hal::init(esp_hal::Config::default());

    // Set LED GPIOs as an output:
    let led = Output::new(peripherals.GPIO2, Level::Low);

    toggle_pins(led)
}

fn toggle_pins(mut pin: Output) -> ! {
    loop {
        pin.toggle();
    }
}

image

There seems to be a ~3x difference in this one experiment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.0-blocker RFC Request for comment
Projects
Status: Todo
Development

No branches or pull requests

2 participants