-
Notifications
You must be signed in to change notification settings - Fork 224
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
Comments
Thank you for this proposal. It sure seems interesting. 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 |
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>
With #351, something similar is now implemented so this issue can be closed I think... |
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:
&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.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:
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:
What could this look like?
Unresolved questions
Transactional
operations?Transactional
?The text was updated successfully, but these errors were encountered: