-
Notifications
You must be signed in to change notification settings - Fork 201
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: split into device/bus, allows sharing buses with automatic CS #351
Conversation
r? @eldruin (rust-highfive has picked a reviewer for you, use r? to override) |
dcc96b9
to
37831ac
Compare
hey another interesting option!
🤣
i guess you'd also have a
i believe this is achieved by both ManagedCS proposals, though it is still useful to have a mechanism for representing an arbitrary sequence of operations.
with this approach the CS is effectively scoped to the parent /// Code to do some SPI stuff then wait for 100ms
/// note that the missing `drop` means the delay will occur within the CS assertion rather than between CS assertions
/// which is not super clear from the code
fn something(&mut self) {
let t = spi.transaction();
// do some SPI stuff
// Delay for 100ms to let the device get ready for the next operation
delay_ms(100);
} i prefer the closure because it make it obvious when the end of the transaction is, rather than whenever the compiler chooses to drop the object. the timing of when a mutex is dropped / reopened doesn't typically matter because it's just a software concept, but CS assertion and deassertion very much does. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! this looks pretty cool.
A few questions:
- While I like the idea of closing the transaction when falling out of scope, do you have any plan on how to inform the driver/what to do about a CS pin deasertion error on
drop()
(if not using adone(mut self)
method like @ryankurte suggested)? - Any reason why a similar approach would not work for
I2C
?
I see the point of people leaving the transaction open while doing something else, but given the popularity of this way of holding onto mutexes with guards and the fact that the SPI bus cannot be used for anything else as long as the transaction is not completed, it would be fine for me. The peripheral would just be listening to silence for longer than necessary. Attempting to create a new transaction should result in a compilation error.
Some TODOs:
- Ensure that implementations for
&mut T
do not introduce problems like those highlighted in Draft: alternate ManagedCS trait #350 - Ensure it is not possible to call
transaction
twice before the first one is dropped. - For async, ensure it is not possible to call
transaction
again while a transaction is still ongoing but an operation is being awaited. - Since it is a whole new concept it would be especially important to provide PoC implementations for HAL, drivers and bus mutexing.
- Wait for stable GATs ☕
They're scoped to the parent block, not function. You can also add explicit
Oh, this is a problem indeed. Perhaps the closure is better after all. The analogy with Mutex/RefCell breaks down because dropping Transaction causes I/O side effects which are fallible, while dropping Mutex/RefCell guards doesn't...
It'd work too, I guess? Same for the closure version. i2c has the same problems outlined in #299 so the device/bus split would help a lot there too. Same for the closure version if we decide to go with that. It'd be nice if i2c+spi transactions were consistent. i2c is a bit more picky with the repeated-start stuff etc, that might make it more complicated.
I would not do
These are already enforced by the
Yep... 😴 |
37831ac
to
89e118a
Compare
Pushed a new version!
I also wrote a working end-to-end test case here. Targets the Raspberry Pi Pico RP2040 with the Pico-ResTouch-LCD-2.8. It's the perfect use case because it has an SPI display and an SPI resistive touch sensor on the same SPI bus with separate CS pins.
It all seems to work nicely together! :D Open questions:
|
approach looks pretty good to me! though still a couple of questions... i don't think the i think we can resolve this with better module and per-function documentation wrt. the
agreed that (for now at least) we're going to need to drop the trait aliases... we could replicate this behaviour with marker traits and bounded default implementations, though they conflict with
i am pretty sure this is still necessary. the idea of |
Agreed, will add more docs.
I think "SpiDevice, SpiBus" is the clearest. At least clearer than "Spi, SpiBus". What's "a SPI"? If we do the Device/Bus thing for i2c as well, then there's no more consistency issues: the "bus" concept only applies to i2c and spi. I don't agree SpiDevice is what most people will use:
Blanket impls don't work, yeah... I'll remove them, then add more docs.
I would argue Drivers that want an SpiDevice intentionally without CS shouldn't be a thing. These kinds of drivers should use SpiBus instead:
|
sure, but the number of HALs << the number of drivers << the number of applications. HAL authors are already deep in the technical side, and i would expect orders of magnitude more drivers and applications, so it seems worth weighting our solutions towards that end.
i think we are looking at this from the opposite direction. so your expectation is that HALs will provide because in many cases you will choose whether to use the hw implementation based on the functionality you require, so can't be required to use the hw approach. and building on that what happens if you had a cursed situation like CS via GPIO expander, how would you model this?
this is the whole point of |
89e118a
to
20b533e
Compare
Pushed new version
@ryankurte hopefully the added docs clarifies the confusion. The TLDR is:
Since |
ugh, MSRV is 1.46+ but |
20b533e
to
678a5eb
Compare
alright i think that's clearer. and if your hardware does CS but you don't use it you just ignore it?
would it work to chuck the images here then link to them instead of embedding for now? |
Yep, The HAL would only enable hardware-CS if you ask for it. You can just use the SpiBus part.
👍 |
The "configure bus" closure is super neat! That's a valid SpiDevice impl (allowed by the trait contract) indeed. I think it's best to leave the issue of configuring the bus to external crates, so we don't lock ourselves to a particular solution. One such solution is HALs providing In the future we could add traits for |
…nagement. Co-authored-by: Diego Barrios Romero <eldruin@gmail.com>
e3b3a60
to
3d9f178
Compare
New version
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The convenience methods should be generic over the word type
Co-authored-by: GrantM11235 <GrantM11235@gmail.com>
New version
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still lgtm, shall we get this done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes please! We can incorporate this into riscv-rust/e310x-hal#42 and test things out soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me, thank you everybody for the interesting discussion!
@therealprof ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we're all agreed.
Let's get this merged. 🎉
bors r+
365: spi: allow impls returning early, add flush. r=therealprof a=Dirbaio Fixes #264 Depends on #351 - Document that all methods may return early, before bus is idle. - Add a `flush` method to wait for bus to be idle. The alternative is to document that methods MUST wait for bus idle before returning, but that would be bad for performance IMO. Allowing early return means multiple successive writes can "overlap" so that bytes come out back-to-back without gaps, which is great for workloads with tons of small writes, like SPI displays with `embedded-graphics`. Drivers using `SpiDevice` won't have to worry about this, it'll Just Work since Device impls must flush before deasserting CS. Drivers using SpiBus will have to care if they coordinate SPI activity with other GPIO activity. Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
347: async: add SPI r=eldruin a=Dirbaio This is an exact mirror of the blocking SPI trait (including the proposed changes in #351 ), except the following differences: - `W: 'static` is required everywhere, otherwise complicated lifetime bounds are required in the future GATs. Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
627: Update Rust nightly, embedded-hal 1.0, embedded-hal-async. r=Dirbaio a=Dirbaio Includes the SpiDevice/SpiBus split. rust-embedded/embedded-hal#351 Includes the GAT where clause location change. rust-lang/rust#89122 Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
627: Update Rust nightly, embedded-hal 1.0, embedded-hal-async. r=Dirbaio a=Dirbaio Includes the SpiDevice/SpiBus split. rust-embedded/embedded-hal#351 Includes the GAT where clause location change. rust-lang/rust#89122 Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
461: spi: remove write-only and read-only traits. r=Dirbaio a=Dirbaio When introducing the Bus/Device split in #351, I kept the ability to represent "read-only" and "write-only" buses, with separate `SpiBusRead`, `SpiBusWrite` buses. This didn't have much discussion, as it was the logical consequence of keeping the separation in the already existing traits (`Read`, `Write`, `Transfer`, ...). Later, in #443, when switching from the closure-based API to the operation-slice-based API, this required adding `SpiDeviceRead`, `SpiDeviceWrite` as well, because you can no longer put a bound on the underlying bus with the new API. This means we now have *seven* traits, which we can reduce to *two* if we drop the distinction. So, is the distinction worth it? I've always been on the fence about it, now I'm sort of leaning towards no. First, using write-only or read-only SPI is rare. - write-only SPI: for SPI displays you don't want to read from, or to abuse it to bitbang stuff like ws2812b, - read-only SPI: some ADCs that can't be configured at all, you just clock out bits. Or even weirder abuses, like to build a logic analyzer Second, for it to be useful HALs have to track "read-onlyness / write-onlyness" in the type signature, so a read-only SPI really only implements `SpiBusRead` and not `SpiBus`. HALs already have enough generics. For example, Embassy HALs don't implement the distinction (you can create MOSI-only or MISO-only SPIs, but they all impl the full `SpiBus`, because I didn't want to make the types carry pin information). Third, it makes the API easier to use. Simpler, users only have to import one trait, docs have all the methods in one place. Much less boilerplate to impl the traits (look at how shorter `embedded-hal-bus` gets!). So I think we should remove them. Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
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.
shared-bus
add the "bus mutexing" logic, to implSpiDevice
on top ofSpiBus
(+OutputPin
for CS). There's a wide variety of possible impls, all can satisfy theSpiDevice
requirements:RefCell
Mutex
AtomicBool
taken flag.T: SpiDevice
(orSpiDeviceRead
,SpiDeviceWrite
), then they start a.transaction()
, then use it to read/write/transfer data. The transaction ends automatically when returning. Example: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. WithTransactional
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.