-
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: group into read-only, write-only and read-write traits. #323
Conversation
r? @ryankurte (rust-highfive has picked a reviewer for you, use r? to override) |
huh, definitely an interesting concept. i like the idea of grouping, it's a neat solution to offer default impls for approaches like for example, SPI bindings like: where
T: Read<u8, Error=E> + Write<u8, Error=E> + Transactional<u8, Error=E> would become: where
T: WriteRead<u8, Error=E> it also raises the question for other traits, obviously most of these are orthogonal (and we've already done this for things like stateful pins) but for example, is there any reason to have separate |
Yeah I think i2c should be a single trait. I don't think "write-only i2c" exists, right? |
The bound would be even simpler, just where T: WriteRead The |
not afaik
i think you're still going to need the error bound at a driver level to effectively wrap bus / driver errors anyway (for driver methods, like this seems like an improvement to me, any other votes @rust-embedded/hal ? |
You still need it in error enums, but you don't need it in the driver struct itself: enum MyDriverError<SpiError, PinError> {
Spi(SpiError),
Pin(PinError),
}
struct MyDriver<S, P> { spi: S, pin: P }
impl<S: spi::ReadWrite, P: OutputPin> MyDriver<S, P> {
fn do_something(&mut self) -> Result<(), MyDriverError<S::Error, P::Error> {
...
}
} |
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.
I disagree with a few statements in the problem description.
For example, a good driver which needs transactional
would encourage HAL impls to support it.
I see the ecosystem development to be much more of a hand-in-hand thing.
I should note that we used to have 20-line-ish blanket impls for Transactional
, so supporting it given Write
and Transfer
is pretty trivial.
Nevertheless, I think the solution is a good idea, including the default method impls and forcing the error types to match.
The default impls here would clash with ManagedCS
, though.
Also, being such a big change and touching traits that Just Work ™️, I think some HALs should be adapted as proof, just in case we are missing something.
src/spi/blocking.rs
Outdated
/// typically `0x00`, `0xFF`, or configurable. | ||
fn read_batch(&mut self, words: &mut [&mut [W]]) -> Result<(), Self::Error> { | ||
for buf in words { | ||
self.read(buf)?; |
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.
This default impl would not work for a Read + ManagedCS
impl as read()
would assert/deassert CS for each slice.
src/spi/blocking.rs
Outdated
/// Writes all slices in `words` to the slave as part of a single SPI transaction, ignoring all the incoming words | ||
fn write_batch(&mut self, words: &[&[W]]) -> Result<(), Self::Error> { | ||
for buf in words { | ||
self.write(buf)?; |
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.
Same thing with Write+ManagedCS
src/spi/blocking.rs
Outdated
WI: IntoIterator<Item = W>, | ||
{ | ||
for word in words { | ||
self.write(&[word])?; |
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.
same.
src/spi/blocking.rs
Outdated
Operation::Read(words) => self.read(words)?, | ||
Operation::Write(words) => self.write(words)?, | ||
Operation::Transfer(read, write) => self.transfer(read, write)?, | ||
Operation::TransferInplace(words) => self.transfer_inplace(words)?, |
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.
For completeness: same
A driver author writes a driver because they need it for a project they're working on now. The author has two options:
Option 2 is way way easier. The author wants their driver to work now. Why would they choose option 1? |
This means ManagedCs impls MUST override all the spi methods, not using any default impl. If they don't, they're wrong. It's not a conflict. |
I agree option 2 can be easier but I, for one, did implement re. |
Agreed, it's kind of a footgun. Maybe we could not have default method impls? Adding a trait method with a default impl is backwards-compatible, but arguably not in this case because it would cause ManagedCs impls that were previously correct to become incorrect? Maybe a different ManagedCs design could avoid these advantages? A closure-based one? Not sure if it's worth it, the existing design is super nice and simple, anything else wlil be more complex. |
There are devices where a write-only I²C would be enough to use them, because they can't be read anyway. |
I think the best solution is to remove the default method implementations for now. If we can find a solution to the chipselect problem (and I am hopeful that we can), we should be able to add the default method implementations back as a non-breaking change. |
3b4b548
to
039bd36
Compare
Removed the default method impls. |
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.
Looking good, thanks!
Left to do would be the adaption of a HAL (ideally 2) to make sure we are not missing anything, since this touches traits that currently work fine, like I said earlier.
I'm fine with this. But instead of deleted code, I'd want to see implementation examples in docs. |
I don't think that it is necessary to include implementation examples in the docs, it would just be extra clutter for users of the traits to sift through. I think that HAL authors will have no trouble implementing the traits using the existing documentation. |
Good point, that this documentation is not really relevant for the user. The documentation of the trait should gear towards the user of the trait just as well as the implementor. We could either add a |
I've split the "unify error types" to #331, also including unifying across |
Without Arguably this is only a problem for Read, since Write has write_iter though, which you can use to chain multiple buffers. I don't see an easy way to make a I have no strong opinion, I'm fine with removing them, I just thought they'd be handy. |
i suspect for these cases you could use i have a weak preference for eliding them to minimise the API surface but, would be okay either way. |
|
331: spi: enforce all traits have the same Error type. r=ryankurte a=Dirbaio Previously discussed in #323, split off to its own PR now. This PR enforces all SPI trait impls for a given type use the same Error associated type, by moving it to its own ErrorType trait. ## How breaking is this? I believe this is not very breaking in practice (it won't make upgrading existing code to 1.0 harder), because: - HALs already use the same error type for all methods (I haven't seen one that doesn't) - Drivers already often add bounds to enforce the errors are the same. [Example](https://github.com/rust-iot/rust-radio-sx127x/blob/8188c20c89603dbda9592c40a8e3fc5b37769a00/src/lib.rs#L122). Without these bounds, propagating errors up would be more annoying, drivers would need even more generic types `SpiWriteError, SpiReadError, SpiTransferError, SpiTransactionalError`... ## Why is this good? Traits being able to have different Error types is IMO a case of "bad freedom" for the HALs. Today's traits don't stop HALs from implementing them with different error types, but this would cause them to be incompatible with drivers with these bounds. If traits force error types to be the same, the problem is gone. ## What about other traits? I believe this should be also applied to the other traits. I propose discussing this in the context of SPI here, and if the consensus is it's good I'll send PRs doing the same for the other traits. Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
Rebased. Also, renamed ping @eldruin @therealprof @ryankurte thoughts, now that #331 is merged? |
Rebased for What's the final decision regarding |
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.
Without read_batch/write_batch it's impossible to read/write multiple buffers in a single transaction for impls that impl only Write or only Read. This will be a problem when combined with ManagedCs: it would force users to fall back to manual CS management.
is this a thing in practice? while many peripherals might be unidirectional i can't think of an SPI controller i have used that only supports write or read.
i can't find the latest in my mailbox but iirc the latest approach to ManagedCS
looks like some kind of closure, so you'll be able to call .with_cs( |s| { s.read(..)?; s.read(..)? })
to chain operations without using transactional
.
(@GrantM11235 do you have a branch / PR for the work you were doing on this?)
What's the final decision regarding read_transaction, write_transaction? Do we keep them or not?
i really don't know, gut feeling is they're not needed / demonstrated, but i can see they'd be more annoying to add later. @eldruin any opinions?
} | ||
} | ||
/// Writes all slices in `words` to the slave as part of a single SPI transaction, ignoring all the incoming words | ||
fn write_transaction(&mut self, words: &[&[Word]]) -> Result<(), Self::Error>; |
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.
in transaction
we call them operations
rather than words
, here they're not exactly operations but also not really words. non critical, but, are we happy with this / is there any clearer name?
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.
buffers
?
All hardware supports read+write, but the user might want a write-only SPI for example, with MOSI but no MISO. HALs might want to have a separate type for this that impls Write but not Read, to avoid misuse. Drivers will then want to require only
Hmm, then maybe they aren't as necessary, yeah... Doesn't the same argument apply to the (read-write) |
I like the |
After further discussion with @ryankurte, we would favor removing the
As pointed out above, any SPI controller supports read and write in practice, so using the transactional interface would be possible. |
Do we want to support readonly, writeonly, and "readwrite but in readonly/writeonly mode because it's missing pins" SPI peripherals? If the answer is yes, then we should have the Read, Write traits be "first class citizens", complete with with their If the answer is no (it seems you're arguing for no), then we should merge all SPI traits in one, like #339. (and then not have Having separate traits but without separate Shall I merge all 3 traits? :D |
I think merging all traits into one (1.) makes sense except there is a general problem with implementing
Opinions @rust-embedded/hal? |
What's the problem with Transactional? AFAICT all hardware that can impl read/write/transfer should be able to impl transactional. |
Saying about |
I don't think making it a single trait fixes the original issue raised in #289. Also, the arguments against the Default I raised in #289 (comment) still apply IMO |
AFAIK there is no problem with // I just wrote this down, please ignore trivial mistakes
pub trait Spi {
fn write(&mut self, data: &[Word]) -> Result<(), Self::Error> {
self.transaction(&[Operation::Write(data)])
}
fn read(&mut self, data: &mut [Word]) -> Result<(), Self::Error> {
self.transaction(&[Operation::Read(data)])
}
fn transaction(&mut self, ops: &[Operation]) -> Result<(), Self::Error>;
} |
yep, though it is also useful of itself for DMA-ing a set of transactions on platforms that support this.
one of the side effects of the newer option for
i still prefer this, though in practice i have never only used |
There's another use case for readonly/writeonly SPI: limited DMA channels. STM32 needs one DMA channel for read, another for write. If the user only wants to write it's very rude to force them to waste one DMA channel for reading. Channels are very scarce on low-end chips (smallest G0, L0 only have 5!). Also, in chips without DMAMUX, channels have no/limited remappability, so the wasted channel could cause a conflict even if there are other free channels.
This applies more to async, rather than blocking, but I'd like async+blocking to be consistent on this. Given this, I no longer think unifying SPI into a single trait is a good idea... IMO the answer to "Do we want to support readonly, writeonly SPI peripherals?" is Yes. On If HALs are going to have write-only/read-only SPI, you can't use |
In any case, I believe that it is better to solve the problem of missing implementations in organizational way. |
Thanks for that important argument @Dirbaio. Let's keep |
339: I2c unify r=eldruin a=Dirbaio Depends on #336 Equivalent of #323 but for I2c. I think for i2c unifying everything in a single trait makes the most sense. The i2c bus is specified to be bidirectional, I believe no hardware out there can "only write" or "only read" (and writing requires *reading* ACK bits anyway!). Co-authored-by: Dario Nieuwenhuis <dirbaio@dirbaio.net>
Closing in favor of #351 which incorporates all the ideas from here. |
This PR depends on #331
The problem
Currently, embedded-hal has independent traits for every single method. The rationale is to allow HALs to only implement what they support. For example, a HAL could allow setting up an SPI with a MOSI pin but no MISO pin. This would be a "write-only" SPI, so the resulting struct would implement
write
andwrite_iter
, but not other methods likeread
ortransfer
. This is awesome, because it enforces at compile time that you can't pass a write-only SPI to a driver that needs a read-write SPI.However, splitting one trait per method is way too granular. It leaves too much room for variability in HAL implementations. I've done a quick survey of which HAL implements which traits, here's the result:
As you can see, all HALs implement
write
andtransfer
, but only about half implementwrite_iter
and almost none implementstransactional
.The result is drivers can't reliably rely on
Transactional
orWriteIter
being present, so they stick toTransfer
andWrite
to ensure maximum HAL compatibility. This in turn means HALs have little incentive to implementTransactional
andWriteIter
since no drivers use them. This causes a chicken-and-egg situation, which in the end makesTransactional
andWriteIter
useless for drivers in practice.(Too much freedom for HALs causing traits to not be useful for drivers is a common theme in e-h. For example #312 and #201 are the same. IMO e-h should be slightly more opinionated in general. Just enough opinionatedness to force HAL impls to be consistent to be useful for drivers.)
The solution
The solution is to split the traits by hardware capability, not by method.
write
, it's capable ofwrite_iter
. It can write bytes, after all. There's no reason to allow a HAL to implementwrite
but notwrite_iter
.transfer
, there's no reason it can'ttransfer_inplace
,transactional
,read
,write
, etc. If it can read and write, it should be able to do all the kinds of reads/writes we have.So, I propose we split the traits like this:
Read
: read-only SPI.Write
: write-only SPI. It supports all the forms of writing:write
andwrite_iter
.ReadWrite
: read-write SPI. It requiresRead
andWrite
, and supports all the forms of bidirectional transfers:transfer
,transfer_inplace
,transactional
.This way HALs keep the "good" freedom to declare their SPI is readonly/writeonly, but they no longer have the "bad" freedom of only implementing the basic traits making the advanced traits useless.
Other traits
I believe this should be also applied to the other traits. I propose discussing this in the context of SPI here, and if the consensus is it's good I'll send PRs doing the same for the other traits.