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

Spi api changes #469

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Conversation

pietgeursen
Copy link
Contributor

@pietgeursen pietgeursen commented Oct 13, 2022

There are few things that I discovered I need from the SPI.

This PR is a bit of conversation starter. I'm happy to split it up / close it if it's not useful. But I thought I should at least try and get my work back upstream :)

Spi::set_baudrate is slow

Slow is relative of course, but it takes ~100us at runtime on rp2040. That's a deal breaker for me because I have multiple devices on a single SPI bus and I need to be able to reconfigure the SPI and write to it it every 50us.

I extracted the guts of the calculation into const fn calc_spi_clock_divider_settings_for_baudrate. This returns a SpiClockDividerSettings which can then be passed to pub fn set_baudrate_from_settings(&mut self, settings: &SpiClockDividerSettings) which is what actually writes registers.

This enables you to only do that calculation when you actually need to.

set_baudrate is refactored to use these functions internally.

Spi::set_mode(&mut self, mode: Mode)

I think this is safe to do. It's just a convenience function to update the Modeon an enabled Spiwithout having to disable it and create a new one.

Spi::pub fn complete_transfer(&mut self) -> Result<(), nb::Error<Infallible>>

I found a footgun with the embedded hal spi trait

The rp2040 has fifo of width 8. Imagine you do this:

chip_select.set_low().unwrap();
nb::block!(spi.send(byte1)).unwrap();
nb::block!(spi.send(byte2)).unwrap();
nb::block!(spi.send(byte3)).unwrap();
chip_select.set_high().unwrap();

Assuming the fifo was empty to start with, those sends will never block. The implementation only returns WouldBlock when the transmit buffer is full, not when the transfer is complete. That's arguably the correct thing to do. But my chip select will go high in the middle of the sends.

Now you can do this:

chip_select.set_low().unwrap();
nb::block!(spi.send(byte1)).unwrap();
nb::block!(spi.send(byte2)).unwrap();
nb::block!(spi.send(byte3)).unwrap();
nb::block!(spi.complete_transfer()).unwrap()
chip_select.set_high().unwrap();

rp2040-hal/src/spi.rs Outdated Show resolved Hide resolved
}

/// Wait until all operations have completed and the bus is idle.
pub fn flush(&self) -> Result<(), nb::Error<Infallible>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another remark: The future of nb is a little bit unclear. It might get replaced by an async API.
While I'd say that we should continue supporting the nb traits of embedded-hal for the foreseeable future, I wouldn't add methods using it to the the rp2040-hal impls.

So this method should have the same signature as https://docs.rs/embedded-hal/1.0.0-alpha.9/embedded_hal/spi/trait.SpiBusFlush.html#tymethod.flush, fn flush(&mut self) -> Result<(), Self::Error>.

Copy link
Contributor

@ptpaterson ptpaterson Oct 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self::Error is currently being implemented as Infallable, so the proposed signature change would mean flush would have to be blocking, right?

    fn flush(&self) -> Result<(), Self::Error> {
        while self.device.sspsr.read().bsy().bit() {}
        Ok(())
    }

If that's the case, why not make the inherent impl return () and allow an eh alpha SpiBusFlush implementation to use that. See Fallability discussion in proposed migration guide.

    fn flush(&self) {
        while self.device.sspsr.read().bsy().bit() {}
    }

Or for that matter, if it is helpful to have an inherent non-blocking implementation (I honestly don't know if that's helpful) why not let the inherent impl be non-blocking (keep the current signature with Result<(), nb::Error<Infallible>>) to be consistent with the other inherent impls and the SpiBusFlush trait impl loop over it to make it blocking.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized this PR is based on a relatively old commit -- this needs a rebase.

So if rebased, Spi<Enabled, D, DS> now has an is_busy method which can be used to implement flush. I think it makes sense to only implement flush for enabled SPI anyway.

impl<D: SpiDevice, const DS: u8> Spi<Enabled, D, DS> {
    /* ... */
    fn flush(&self) {
        while self.is_busy {}
    }
}

which would allow later:

    #[cfg(feature = "eh1_0_alpha")]
    impl<D: SpiDevice> eh1::SpiBusFlush for Spi<Enabled, D, $nr> {
        fn flush(&mut self) -> Result<(), Self::Error> {
            self.flush();
            Ok(())
        }
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if rebased, Spi<Enabled, D, DS> now has an is_busy method which can be used to implement flush.

Exactly, and is_busy is functionally equivalent to the non-blocking version of flush, so it's sufficient to have the blocking flush method.

@ithinuel
Copy link
Member

Is there anything else to do on this PR? (rebase/reach compromise/clone?)

Comment on lines +144 to +149
pub struct SpiClockDividerSettings {
/// The prescaler for writing to sspcpsr
pub prescale: u8,
/// The postdiv for writing to sspcr0
pub postdiv: u8,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making fields public is a strong commitment (see rust's future proofing guidelines).

Also, I'm not even sure allowing users to set them to arbitrary values is a good idea since they'd need to be validated before set in the registers (eg only even values are allowed in CPSDVSR).

///
/// If you do change your peripheral clock at runtime you can store the [SpiClockDividerSettings] and only re-calculate it
/// when the peripheral clock frequency changes.
pub const fn calc_spi_clock_divider_settings_for_baudrate(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would make a nice inherent method to SpiClockDividerSettings like

impl SpiClockDividerSettings {
    pub const fn new(peri_frequency: HertzU32, baudrate: HertzU32) -> Self {
        todo!()
    }
}

}
}

/// Clock divider settings
pub struct SpiClockDividerSettings {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should most likely derive:

  • Copy, Clone
  • PartialEq, Eq
  • Debug
  • defmt::Format (when the feature is enabled)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants