-
Notifications
You must be signed in to change notification settings - Fork 234
Use associated Word
types
#295
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
Conversation
To me, it made a lot of sense to have the impl spi::Write for SpiDriver<u8> {
type Word = u8;
}
impl spi::Write for SpiDriver<u16> {
type Word = u16;
}
impl spi::Write for SpiDriver<u32> {
type Word = u32;
} To me, this would be a very odd API because most people will need to use At that point, I'm questioning whether the |
My reasoning for making this change is that although there is plenty of hardware and HAL drivers that support multiple word sizes, I don't know of any that support multiple word sizes at the same time. Doing so would require either
I believe that
Because of this, I believe that it makes sense to use an associated type. This change has very little effect on driver structs that are generic over word type, for example impl<WORD> spi::Write<WORD> for SpiDriver<WORD> {
/* ... */
} simply becomes impl<WORD> spi::Write for SpiDriver<WORD> {
type Word = WORD;
/* ... */
} When I brought this up in the last meeting, @Dirbaio said:
To be honest, I'm not sure if I have a good answer for this. I would argue, however, that generic device drivers should not be written to require |
Just for reference, (A) does exist, take the MAX32666's SPI as an example (UG Section 14). But as I wrote at the end of my previous comment, I am not sure whether the Now for other communication interfaces I am less certain. Especially USART which can accomodate 9-bit characters certainly needs some way to represent those... |
Uhm, Section 14 talks about QSPI which is a completely different beast. I'm not sure how to best address this, but supporting multiple word sizes simultaneously makes a ton of sense if you have a number of SPI devices connected which require different word sizes. Indeed this is often a mode of the SPI peripheral so I'd expect that if a MCU would support that it would (a) need to keep track of the current mode and (b) need to be able to switch to a different mode when such a transfer is requested. It seems to me that having a typestate on the impl (i.e. being generic over the wordsize) would make the most sense. |
Since the current system can only support 8/16/32/64 bit words anyway, though, you can always use multiple 8-bit transfers to perform a larger SPI transfer, there's no difference from the SPI device's point of view. It's marginally less efficient for the microcontroller, but that's it. A 24-bit transfer can't be represented in the current system but can be done as three u8 transfers just fine. Plus, the current system doesn't give any way to control endianness, which can be different between different SPI devices.
I think I like this idea too now. It's simplest and as far as I can tell supports the exact same set of SPI devices (i.e. those that take a multiple of 8 bit word sizes). That said, how some HALs implement this today is by making the SPI driver generic over a word size as well, and then the type of the driver is inferred from the type of the call to let data = [1u16, 2, 3, 4];
let driver = Spi::new();
driver.write(&data); and |
Which (for this MCU) supports
This was exactly my line of thinking. We're making it harder on ourselves than we need to here; the |
... and thinking about user convenience, we could provide defaulted methods for other word-sizes: pub trait Write {
fn write(&mut self, buf: &[u8]);
fn write_u16_be(&mut self, buf: &[u16]) { /* ... */ }
fn write_u16_le(&mut self, buf: &[u16]) { /* ... */ }
fn write_u32_be(&mut self, buf: &[u32]) { /* ... */ }
fn write_u32_le(&mut self, buf: &[u32]) { /* ... */ }
} maybe with fn write_be<I: ...>(&mut self, buf: &[I]) { ... }
fn write_le<I: ...>(&mut self, buf: &[I]) { ... } |
See page 317, you must configure the number of bits per word in the QSPIn_CTRL2 register, which supports any number of bits between 1 and 16 (inclusive).
If the HAL driver struct is generic over the word size, that means that any concrete instance of the struct can only support one word size
The current traits, as well as this PR, can in theory support any word size. It just requires HAL authors and generic device driver authors to agree on what types to use to support these other word sizes. They could use arbintrary for example. Some more thoughts in no particular order:I think there is some confusion about the word size of spi interfaces versus the data sent over an spi interface. For example, an ILI9341 LCD in 4-line spi mode uses an 8 bit interface. Many of its commands involve sending It is possible to emulate a 16 bit interface on an 8 bit interface by splitting each An ILI9341 LCD in 3-line spi mode uses a 9 bit interface where the first/most significant bit of every word indicates whether the remaining 8 bits are a command or data. It is not possible to emulate a 9 bit interface on an 8 bit interface unless it is acceptable to add some padding words. If a generic device driver communicates with two spi devices, it should take two different spi interface structs. If both devices are on the same bus, the user can use two To further complicate things, I would like to point out that this PR also affects uart serial, where a 16 bit read or write is not equivalent to two 8 bit transfers. |
Oh, yea, this is a really good point. Of course "native" 16-bit SPI doesn't have an endianness as such because it's thinking about entire words and uses whatever bit order (typically MSbit first as you said) the SPI interface is configured for. So it definitely makes life much harder for a driver that wants 16-bit words to use an 8-bit SPI interface.
Yea, true. We could even define or re-export some suitable types in embedded-hal. |
True. I think I got myself totally lost in transaction size and missed that just like USART we also have non-byte-aligned character widths. In that case, disregard my previous comments - we do need a way to represent this. Sorry for the noise.
I think this is extremely imporant. As @adamgreig said, I would also suggest that we either define our own wrapper types or re-export relevant ones from another crate like the Now, back to the point of this PR, I'm still not convinced of having the character size associated with the entire peripheral. SPI is a bus with possibly many devices connected, which each need their own "configuration". This can mean different character sizes as well. So I would argue that the character size is a parameter of a transfer, not the entire peripheral. Specifically I do not agree with this:
We want a single electrical connection to the connected peripheral so from our side it is one SPI bus, not multiple. The difference is just that some transfers use different configuration. For me, the conclusion is that the generic driver gets one bus-handle (for example shared via pub trait Write<W> {
fn write(&mut self, mode: spi::Mode, buf: &[W]);
} and maybe even further: pub trait Write<W, CS> {
fn write(&mut self, mode: spi::Mode, cs_pin: Option<&mut CS>, buf: &[W]);
} (which would solve race-conditions when the CS pin is managed externally on a shared SPI bus and also allow for hardware CS signal management) In contrast, looking at USART, I specifically think the above does not apply. Here it is more sensible to do what your PR is suggesting as the most common case will certainly be one where the char size stays constant for all uses of the peripheral. |
Thinking about this has led me to open #299 |
I feel like the focus, that many (especially) serial devices support 9 bits instead of 8 and therefor we have to be generic over the word size, is trying to be a bit too generic. 9 bit for serial devices (edit: I mean UART in this example) needs to be supported by the hardware to be able to send a parity bit over the line without having to sacrifice the bandwidth of the data. I can't think of any serial interface where 9 bits are required and the receiving device does also support 9 data bits, but maybe I'm wrong on that part? This feels like a special case, which is not that important, when talking about generic interfaces (like this crate describes). |
Note that this is not a parity bit:
|
If it is the MSB, then this should be translatable to a 2 byte spi transaction, where the first byte represents the What my point is: We should really take the interface simple on the This is necessarily not true and sometimes makes the implementation to specific / complicated. IMHO |
This would not work, the lcd would see this as sending one 9-bit word and then sending the first 7 bits of the next word |
You mean the first significant byte would be interpreted as 8 bits and only the first bit of the second byte would be recognized? That's a good point.
Even though this does make sense on the hardware side, it makes it hard for the developer (for any implementation in- and outside the rust embedded ecosystem I guess) to work with that device, when any slight abstraction comes into play, because it is usually described with basic primitive types. I haven't looked into the datasheet of the ILI9341 LCD, but I hope this is not as a hard constraint, as you make it to be. I mean, of course, it is desirable to have such a tool in place to write a driver for this device via But how would you even describe such an interface with primitive types? It doesn't matter if pub trait Write<Word> {} or pub trait Write {
/// Word type
type Word;
} is used, when What I could imagine is support for a "variable bitwidth" type, where you can choose in runtime how many bits are actually sent on the wire with the SPI. Something like: // This example has to definitely be more fleshed out.
// I don't know if a generic `Word` would make sense in this context
// and if not, which base type to choose, as `u8` does not cover enough usecases at all (9bit does not work)
pub trait Write<Word> {
/// ...
fn write_with_bitwidth(&mut self, buffer: &[Word], bitwidth: u8) -> Result<(), Self::Error>;
} Or introducing custom bitwidth types, but this seems like a lot of work for the implementation, as these are distinct types Either way, I think having an associated type for |
The operation mode I described is supported by the chip with the 16-bit bus interface. |
A sidenote here: The |
Which endianness do drivers expect for So either way I don't really see the point of the If you really need it you can still define a special |
thanks for the PR! it seems to me that moving to associated types doesn't have substantial benefit to outweigh the inability to represent multiple word sizes or the increased complexity of genericising over associated types. it's also worth consideration i think that the existing approach (which may have room for improvement), does work for a bunch of folks / use cases, and a proposed change would need to demonstrably still achieve / improve on this. as such, i propose we close this PR and open issues to address any specific points from the discussion here. any thoughts @rust-embedded/hal ? |
No description provided.