-
Notifications
You must be signed in to change notification settings - Fork 220
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
ADC OneShot: Clarify asynchronous behavior #123
Comments
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. |
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. |
I agree. Real errors should always result in an |
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 |
(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). |
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. |
|
Out of interest, when is cancel not possible to implement? |
A |
(Also apologies if I'm being overly affected by my knowledge of the stm32F0 ADC, which supports active stopping of in progress conversions) |
@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. |
Fair, the other take would be to just cut off your internal state machine and thus delay the block to the next This is given that |
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? |
You could add a method that only exists in unproven builds I believe. |
Also I have the feeling this is going to cause me to write the non-blocking version of the F0 ADC. sighs |
@chrysn I disagree about the semantics of |
Yeah, my take would be that cancel always ensures that the next read can be invoked and returns a fresh value. I'm not sure if we want to force it to always block there and then or not, but thats just me. |
That said I don't think cancel should be an 'nb' call. I just think that either it should block, or it should setup state such that the blocking happens at the start of the next read call. |
@HarkonenBade I'd rather we don't break existing traits without a good reason. |
So I'd probably go for fn cancel(&mut self) -> (); |
I'd be down for an extension trait, so something like: trait CancellableOneShot<ADC, Word, Pin: Channel<ADC>>: OneShot<ADC, Word, Pin> {
fn cancel(&mut self) -> ();
} |
@HarkonenBade I don't expect a lot of hardware not being able to cancel ongoing sampling, can't remember where I've seen that. It should be clear what to expect though, maybe the application is not even interested in a new reading but wants to disable the ADC instead... |
ad CancellableOneShot, will do. ad blocking: If cancel does not return a nb::Result, I'd fear that some ADCs might not be able to implement CancellableOneShot – but worst case they can't implement CancellableOneShot or need to block in there. |
@chrysn That should be fine, though, no? |
Yeah, as it won't affect me ;-) – more seriously though: Yes, I'd assume so. The most exotic ADC that comes to my mind right now is an SPI-based reader (implementation pending), and that has enough state that a .cancel() can flag the pending transfer as stale or something like that. |
@chrysn If the new trait cannot be implemented then it cannot be implemented. That's why it is going to be a new trait. ;) And even if it could be implemented, the implementer has the options to not do it, e.g. if it is overly expensive or complicated to do. |
@chrysn Why do you think that making cancel blocking would prevent implementation? |
@HarkonenBade Because in my ideal world, no HAL function will block for longer than its CPU / memory access time … no further reasons beyond that. (But given the ADCs at hand, no blocking will happen anyway.) |
I suppose, might be worth doing a |
Ewww. :-D |
Either way that'd need a bit of an update on the read documentation side. What's the currently intended behavior of (not that'd be a particularly clever thing to do given OneShot's documentation, but typewise it's legal) loop {
let left = adc.read(left_pin);
let right = adc.read(right_pin);
if let Ok(x) = left && x < 128 {
...
}
} on a typical MCU ADC? |
My expectation would be that the intended behaviour of that would either cause both left and right to contain 'WouldBlock' or in the best possible case, left would contain the first read and right contain the second value. I'd hope that it would never result in the second read pulling the value that was read from the first. |
WouldBlock on both (or a read if it actually is available immediately) would certainly be the desireable outcomes, but that's not actually trivial to implement. I've been looking around for implementations, but the only one I've found so far is [stm32f0xx]https://github.com/stm32-rs/stm32f0xx-hal/blob/master/src/adc.rs), and that's plainly blocking. |
Basically i'd assume that a non blocking implementation would either refuse the second call with an error, or steamroller the existing call. I'm a biggest fan of the first case where would refuse other conversions until the data had been read for the first read request (or until a cancel was issued). |
(that implementation is mine) |
My intent would be to make a state machine system that is aware of which pin it is converting, and that will refuse conversions on other pins until the active conversion is completed and the data read, or until a cancel is issued. |
If the |
Alternatively, this implementation offers a different error reporting and would return |
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 (Personally I'd have expected something type-state-ish along the lines of trait OneShot<...> {
fn read<'a>(&mut self, channel: &Self::Channel) -> nb::Result<ReadResult<'a, Self>>;
/// Read the latest value of any read operation on this channel
fn fetch(&self) -> nb::Result<...>; // maybe requiring an argument that only a ReadResult could provide
}
struct ReadResult<'a, A>(&'a mut A);
impl<'a, A: OneShot> ReadResult<'a, A> {
fn fetch(&mut self) -> nb::Result<...> {
self.0.fetch()
}
} but that's probably something for much later.) |
@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. |
Some thoughts:
|
Why would you implement the |
Sure, I could just not implement it. The only inconvenience would be if some other library requires a |
Well, you said it is technically not possible. ;) One could always create a blanket implementation which simply waits for the result... |
Sorry my response was not clear enough. dev.read(Channel::A0);
dev.cancel();
block!(dev.read(Channel::A1)); In the code above, with or without the call to |
For me
The latter implies that |
I see. I did not think of 2. |
1. to ensure that the next read will yield a clean result obtained only after called cancel
That's the purpose I'm interested in.
2. to ensure that the hardware is in a consistent and safe state because it will be shutdown or the system is supposed to go into sleep mode
Is that a) something we have a use case for, and b) is
CancellableOneShot the right trait for it? After all, that'd be
something *any* peripheral that can have "running" nb operations could
face that issue, or even be reluctant to be shut down when not "inside"
an nb operation for whatever that'd mean.
|
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. |
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
&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)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?
The text was updated successfully, but these errors were encountered: