Description
The OneShot trait defines returning a nb::Result, but it's not fully clear to me what a a WouldBlock means; could be either of
- Can't do, there's another ADC conversion in progress (should really not happen, b/c it takes a
&mut self
, but maybe it's on a shared bus, or maybe there was a channel switch and some calibration time needs to be accounted for or whatever) - Yes I started it, but the result is not ready yet.
If it can be the latter, that begs the question of whether calling .read() incurs the risk of not actually getting a fresh value but the result an earlier read attempt that was aborted – and a caller can't rely on an instant return to mean "this has been requested earlier" either, for some ADCs might give results instantly.
My gut feeling is that nb::Result functions should be "Retry means I ignored you (and when I do something, I return something)" and not "Retry means I heard you (but I'll ignore any future requests until I've once returned something)" – but I might be wrong there and it doesn't need to be so strict to still give reliable oneshot results. What are other peripherals doing there? SPI doesn't have that issue as it has a send and a read method, maybe OneShot should too?
Activity
HarkonenBade commentedon Jan 31, 2019
My general opinion is that an ADC implementation should reject attempts to 'read' different channels if there is an in progress conversion. I believe the use of nb is because ADC conversions can take a very long time and it may be advantageous to perform them asynchronously. It might be worth adding an explicit 'cancel' call that allows you to end the current conversion. So that in the case you need to force a new read through at the expense of an older one, you can call the cancel method to force the end of the previous read.
HarkonenBade commentedon Jan 31, 2019
I think the risk of returning previous values from cancelled conversions should be avoidable by proper state machine use in the OneShot implementor code. I think that the latter 'conversion started but not yet done' would be the most common reason for returning would block, and I think the former should probably be represented by a separate error.
therealprof commentedon Jan 31, 2019
I agree. Real errors should always result in an
Err()
!=Err(nb:WouldBlock)
.chrysn commentedon Jan 31, 2019
I don't think that a proper state machine can avoid the risk of having stale values, for that state machine doesn't get told when an attempt to read is aborted, and when a new read is started.
Having a
.cancel()
would help a lot; whoever cares about having a fresh read would cancel and then read. (And the analogous .start()/.read() could be implemented in terms of it.)chrysn commentedon Jan 31, 2019
(Thinking on this a bit longer, other errors than "still waiting" are really not the point here, and the first bullet on my original post which @therealprof probably commented on is rather moot).
HarkonenBade commentedon Jan 31, 2019
My assumption would be that the state machine would get informed about cancels, as it would be part of the struct implementing OneShot, and would also then be the interface through which reads are cancelled. I highly agree though that making that explicit in the interface would be very useful.
therealprof commentedon Jan 31, 2019
cancel()
might not always be possible to implement though. A state machine to prevent reading stale values is usually not even necessary because the hardware usually has internally bits indicating whether the sampling has finished, is in progress or resulted in an error.HarkonenBade commentedon Jan 31, 2019
Out of interest, when is cancel not possible to implement?
As from what I can see, if your 'read' method is blocking, then cancel is always a no-op. Otherwise cancel either stops the ADC conversion properly, and halts any active state machines, or it just cuts off the state machine and the next read tramples the existing one.
chrysn commentedon Jan 31, 2019
A
cancel
would, with the ADCs I'm aware of, either set the "start a new sampling now" bit, or WouldBlock until the current reading is completed and then clear the result value. (However we call the method: it doesn't necessarily cancel the ongoing sampling, but ensures that when I .read() next I get a value sampled no earlier than the cancel call).HarkonenBade commentedon Jan 31, 2019
(Also apologies if I'm being overly affected by my knowledge of the stm32F0 ADC, which supports active stopping of in progress conversions)
therealprof commentedon Jan 31, 2019
@HarkonenBade Some hardware does not allow you to cancel ongoing sampling. The only option in that case is to wait as @chrysn said, which is fine but should be documented accordingly.
HarkonenBade commentedon Jan 31, 2019
Fair, the other take would be to just cut off your internal state machine and thus delay the block to the next
read
call (by making it wait until it can start a conversion again).This is given that
cancel
is only really relevant in implementations with non-blockingread
implementations.chrysn commentedon Jan 31, 2019
Sounds like a "let's have a concrete sketch of such an extension" to me. What are the procedures here in terms of unproven and adding a method to a trait? Could I add a method to the trait that, unless unproven is enabled, has a default no-op implementation? Or would all of this need to go into a separate trait?
HarkonenBade commentedon Jan 31, 2019
You could add a method that only exists in unproven builds I believe.
HarkonenBade commentedon Jan 31, 2019
Also I have the feeling this is going to cause me to write the non-blocking version of the F0 ADC. sighs
22 remaining items
eldruin commentedon Jan 31, 2019
Alternatively, this implementation offers a different error reporting and would return
Error::AlreadyInProgress
as in already-in-progress-but-for-different-channel.chrysn commentedon Jan 31, 2019
Mh, I'd have hoped to avoid any such state machines or checking back at the registers. But while that's what it is, then there is #124 to have a proposal to talk about, along with the best example I could come up with that doesn't involve two timers and an interrupt.
As for the behavior when switching channels (whether it's an error or silently switches), is there an intended behavior we agree on? (For the best text to add to
.read()
on this right now would be that "When a read returns WouldBlock, it has to be called to exhaustion with the same channel by the caller; otherwise, the implementation is free to either discard the old request, or to report an error indefinitely.").(Personally I'd have expected something type-state-ish along the lines of
but that's probably something for much later.)
therealprof commentedon Jan 31, 2019
@chrysn Don't forget that a type implementing such a trait is not conjured out of thin air but requires target specific initialisation so the implementer has every possibility of ensuring that sampling requests are mutually exclusive or allow (hardware assisted) parallel use. If useful in any form it would also be possible to implement the trait for a collection of values to be sampled in parallel.
eldruin commentedon Feb 1, 2019
@chrysn Thanks for #124!
Some thoughts:
If I did not miss anything in the datasheet, ADS1x1x devices are an example of devices that do not allow cancelling an ongoing conversion other than by doing a general I2C reset call or power reset. Therefore at least for the time being I would implement the cancel as a no-op, which would already ensure the trait guarantees, albeit not shortening the waiting time if a conversion is ongoing.
Implementations that actually need to do some I2C communication to cancel an ongoing conversion would need a way to report I2C communication errors.
I think the fetch below does not bring much benefit and I would say it would be overly complicated.
I would see remembering the last value read as a user responsibility and not something that a driver must do. Furthermore, nothing would stop you from writing something like that on top of the
OneShot
trait just by remembering the channel inReadResult
and callingread(the_channel)
again onReadResult::fetch()
.In my eyes a
fetch()
method inOneShot
kind of deceives the point ofOneShot
and resembles more the continuous mode.therealprof commentedon Feb 1, 2019
Why would you implement the
CancellableOneShot
trait at all for that device?eldruin commentedon Feb 1, 2019
Sure, I could just not implement it. The only inconvenience would be if some other library requires a
CancellableOneShot
-capable device and the driver does not provide it although it is technically possible. The user could still implement it himself I guess.therealprof commentedon Feb 1, 2019
Well, you said it is technically not possible. ;) One could always create a blanket implementation which simply waits for the result...
eldruin commentedon Feb 1, 2019
Sorry my response was not clear enough.
It is technically possible to implement the
CancellableOneShot
trait meeting its guarantees as currently documented by implementing a no-op given theread()
implementation behavior.The hardware will not actually stop the current measurement, though.
I would not favor a blocking
cancel()
implementation because the same is already possible just withread()
plus no unnecessary busy-wait:In the code above, with or without the call to
cancel()
however this is implemented (blocking or no-op), the result and running time are the same.therealprof commentedon Feb 1, 2019
For me
cancel
has two uses:The latter implies that
CancellableOneShot
is implemented iff the hardware is actually capable of cancelling the operation and that whencancel
returns the operation is concluded.eldruin commentedon Feb 1, 2019
I see. I did not think of 2.
We should definitely document this in #124. Then I could implement a blocking
cancel()
and document why it blocks and why you may not need it.chrysn commentedon Feb 1, 2019
therealprof commentedon Feb 1, 2019
Sane interfaces you mean? I always have a usecase for that. :-D
Yes.
True, and we're gonna get to that. Timers already have cancel and other (potentially long running) operations will eventually get it too.
I don't understand, I'm afraid.