Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information