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: Use the SPI traits to represent a single peripheral on an SPI bus #299

Closed
GrantM11235 opened this issue Jul 20, 2021 · 2 comments
Closed

Comments

@GrantM11235
Copy link
Contributor

GrantM11235 commented Jul 20, 2021

What?

Currently, the SPI traits are used to represent an entire SPI bus. I propose using them to represent a single peripheral on an SPI bus.

Why?

Generic device drivers want to be able to communicate with an SPI device or devices, they don't need access to the whole bus. In fact, giving the entire bus to a driver has a number of downsides:

  • The driver must manually toggle it's chip select. This is inconvenient and can lead to problems like Clarify SPI timing behaviour (currently completely broken!) #264.
  • Because the driver must hold the SPI bus struct (or at least a &mut), nothing else can use that bus for the lifetime of the driver. shared-bus exists to work around this problem, but it has limitations (edit: and it is unsound SpiProxy is still unsound, even in a single thread Rahix/shared-bus#23) due to the manual handling of chip selects.
  • A driver that communicates with two SPI devices must decide whether to take one bus (and only work if both devices are on the same bus) or two buses (and only work if the devices are on separate buses).
  • A driver has no way to communicate with two SPI devices on the same bus if they use different bus configurations (polarity, phase, speed) because there is no way to change the configuration of the bus using embedded_hal.

How?

embedded_hal

We don't need to actually change any code in embedded_hal, we just need to mandate that any implementations of the spi traits must hold their chip select pins low for the duration of the transfer.

This is similar to #180/#245, but it would be mandatory, not optional.

Generic device drivers

Generic device drivers can be updated by simply removing any manual toggling of chip select pins.

HALs

This is where it gets interesting...

HAL implementations

Instead of implementing the SPI traits directly on an spi bus, HALs will instead create device structs. Here are some simplified examples:

A device struct which shares it's bus:

pub struct Device {
	bus: Mutex<Spi1>,
	cs_pin: OutputPin,
	config: Config,
}

impl spi::Write<u8> for Device {
	fn write(&mut self, words: &[u8]) {
		self.bus.lock(|bus| {
			bus.reconfigure(self.config);
			self.cs_pin.set_low();
			bus.write(words);
			self.cs_pin.set_high();
		});
	}
}

A device struct with exclusive access to it's bus, which doesn't need a mutex and doesn't need to reconfigure the bus every time:

pub struct ExclusiveDevice<'a> {
	bus: &'a mut Spi1,
	cs_pin: OutputPin,
}

impl<'a'> spi::Write<u8> for ExclusiveDevice<'a> {
	fn write(&mut self, words: &[u8]) {
		self.cs_pin.set_low();
		self.bus.write(words);
		self.cs_pin.set_high();
	}
}

What could this look like?

let spi1 = hal::spi::Spi1::take();

let lcd_spi = spi1.new_device(lcd_cs_pin, MODE_1, MegaHertz(20));

let sensor_spi = spi1.new_device(sensor_cs_pin, MODE_2, MegaHertz(1));

let mut lcd = Lcd::new(lcd_spi, lcd_reset_pin);

let mut sensor = Sensor::new(sensor_spi);

loop {
    let data = sensor.read_data();

    display_data(&mut lcd, data);
}

Unresolved questions

  • What guarantees do we need to provide about the chip select pin? Obviously it needs to be low during a transfer, but do we need to guarantee that it goes high afterwards? What about between Transactional operations?
  • How hard is it to implement this in HALs?
  • How much overhead is introduced by reconfiguring the bus every time a trait method is used? Can it be mitigated with Transactional?
  • How would this work for the nonblocking SPI trait?
@eldruin
Copy link
Member

eldruin commented Jul 21, 2021

Thank you for this proposal. It sure seems interesting.
Just setting expectations here: I might be wrong but I think this is too big to iron out before 1.0.

In my opinion, redefining such behavior without changing code would be difficult to handle across the ecosystem. Driver and HAL writers would only have the embedded-hal version to decide whether they need to handle CS or not. These changes are not obvious when updating and might lead to broken code.
I would rather favor adding a marker trait like ManagedCS.

bors bot added a commit that referenced this issue Feb 23, 2022
351: SPI: split into device/bus, allows sharing buses with automatic CS r=therealprof a=Dirbaio

Because why not?

This separates the concepts of "SPI bus" and "SPI device". I got the idea of having traits for SPI devices from #299. It has an excellent breakdown of the issues of the status quo, so go read that first.

The difference from #299 is that proposes "repurposing" the existing traits to represent devices, while this PR proposes specifying the existing traits represent buses, and introduces a new trait for representing devices on top of that.

- HALs impl the bus traits, just like now. No extra boilerplate.
- Crates like `shared-bus` add the "bus mutexing" logic, to impl `SpiDevice` on top of `SpiBus` (+ `OutputPin` for CS). There's a wide variety of possible impls, all can satisfy the `SpiDevice` requirements:
   - Exclusive single-device, that just owns the entire bus
   - Single-thread, on top of `RefCell`
   - Multithreaded std, using std `Mutex`
   - Multithreaded embedded, using some `AtomicBool` taken flag.
   - Async embedded, using an async mutex (this'd require an async version of SpiDevice).
- Drivers take `T: SpiDevice` (or `SpiDeviceRead`, `SpiDeviceWrite`), then they start a `.transaction()`, then use it to read/write/transfer data. The transaction ends automatically when returning. Example:

```rust
fn read_register(&mut self, addr: u8) -> Result<u8, Error> {
    let mut buf = [0; 1];
    self.spi.transaction(|bus| {
        bus.write(&[addr])?;
        bus.read(&mut buf)?;
    })?;
    Ok(buf[0])
}
```

## No Transactional needed

Previous proposals boiled down to: for each method call (read/write/transfer), it automatically assets CS before, deasserts it after. This means two `write`s are really two transactions, which might not be what you want. To counter this, `Transactional` was added, so that you can do multiple operations in a single CS assert/deassert.

With this proposal, you have an explicit "Transaction" object, so you can easily do multiple reads/writes on it in a single transaction. This means `Transactional` is no longer needed.

The result is more flexible than `Transactional`, even. Take an imaginary device that responds to "read" operations with a status code, and the data only arrives on success. With this proposal you can easily read data then take conditional action.  With `Transactional` there's no way, best you can do is read the data unconditionally, then discard it on error.

It allows for smaller memory usage as well. With `Transactional` you had to allocate buffer space for all operations upfront, with this you can do them as you go.

```rust
fn read_data(&mut self, addr: u8, dest: &mut [u8]) -> Result<(), Error> {
    self.spi.transaction(|bus| {
        bus.write(&[addr])?;

        // Read status code, 0x00 indicates success
        let mut buf = [0; 1];
        bus.read(&mut buf)?;
        if buf[0] != 0x00 {
            return Err(Error::ErrorCode(buf[0]));
        }

        // If the operation is successful, actually read the data, still in the same transaction
        bus.read(dest)
    })
}
```


Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
@Rahix
Copy link

Rahix commented Mar 21, 2022

With #351, something similar is now implemented so this issue can be closed I think...

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

No branches or pull requests

3 participants