Skip to content
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

Add ADC proxy #24

Merged
merged 3 commits into from
Aug 30, 2021
Merged

Add ADC proxy #24

merged 3 commits into from
Aug 30, 2021

Conversation

geomatsi
Copy link
Contributor

Hi,

This PR adds new proxy type for sharing access to ADC hardware block. The AdcProxy implements OneShot trait so it can be passed to drivers instead of ADC instance. PR also includes some tests for the new proxy type based on embedded-hal-mock.

Regards,
Sergey

@Rahix
Copy link
Owner

Rahix commented Aug 19, 2021

Hi, thanks a lot for the PR! In general this is a good and useful idea. However I see a problem with the OneShot trait, making this not quite as straightforward:

The read() method returns an nb::Result<Word, Self::Error> which means that it should be non-blocking. Users will poll this method repeatedly until it returns the ADC value instead of WouldBlock.

In the context of shared-bus this means we will only lock the mutex for the duration of one non-blocking poll call, not from beginning to end of a conversion. This can lead to very nasty situations: Consider two tasks both trying to read a channel, then yielding to the rest of the system before attempting the poll again. It could end up like this:

        TASK A                 TASK B           Comment
          |                      |
  adc_proxy1.read(Ch1)           |              The ADC HAL starts a conversion on Ch1.
   -> WouldBlock                 |
          |                      |
          |              adc_proxy2.read(Ch2)   A read() for a different channel means the ADC
          |               -> WouldBlock         HAL will cancel the ongoing conversion and
          |                      |              restart converting for Ch2 now.
          |                      |
  adc_proxy1.read(Ch1)           |              Again, a read() for a channel which is not the
   -> WouldBlock                 |              one we're currently converting leads to
          |                      |              cancelling the ongoing conversion and
          |                      |              restarting for Ch1...
          |                      |
          |              adc_proxy2.read(Ch2)   This will now continue forever...
          |               -> WouldBlock
          .                      .
          .                      .
          .                      .

I think shared-bus absolutely must prevent such situations from happening. After all, its entire job is to arbitrate "bus" access between multiple competing users.

Now, all hope is not lost, I see a few possible solutions. First of all, I want to observe that actually a lot of HALs don't even implement OneShot in a non-blocking way! Take a look at the stm32f3xx-hal ADC for example (not that convert_one blocks until the conversion is done) or the stm32f4xx-hal ADC which also blockingly converts a sample.

So what we can do is, just like the HALs, we break the non-blocking contract of the trait and actually just busy-spin until a sample is returned. In practice this would mean changing the proxy implementation to the following:

    fn read(&mut self, pin: &mut Pin) -> nb::Result<Word, Self::Error> {
        self.mutex.lock(|bus| nb::block!(bus.read(pin)))
    }

For the above-mentioned "blocking HALs", this would behave exactly the same, but for the non-blocking HALs which implement OneShot "correctly", it would ensure race-conditions like demonstrated above are impossible because we will keep the ADC mutex locked until a conversion has completed.

Of course this is kind of a hack - I think the better approach here would be to change embedded-hal to have an explicitly blocking OneShot trait instead and then make shared-bus work ontop of that. But as that's not going to happen quickly (and HAL adoption of the new trait is also not going to be fast), I think going with the hacky solution for now is acceptable. We should, however, explicitly document this behavior to at least not negatively surprise anyone...

@Rahix Rahix added the enhancement New feature or request label Aug 19, 2021
Add support for ADC proxy. Note that OneShot trait describes a non-blocking
contract for ADC read operation. However access to shared ADC unit can not
be arbitrated in a completely non-blocking and concurrent way. Any reading
from a channel shall be completed before `shared-bus` can allow  next
reading from the same or another channel. So suggested implementation breaks
the non-blocking contract of the trait and just busy-spins until a sample
is returned.

Apparently this is kind of a hack. Possible better approach here would
be to change embedded-hal to have an explicitly blocking OneShot trait
instead and then make shared-bus work on top of that.

Meanwhile it worth mentioning that a lot of HALs do not implement ADC traits
in a non-blocking way. For instance, quite a few stm32 HALs actually block
until the reading is completed. The list of such HALs includes stm32f0xx-hal,
stm32f1xx-hal, stm32f4xx-hal to name a few.

Signed-off-by: Sergey Matyukevich <geomatsi@gmail.com>
@geomatsi
Copy link
Contributor Author

Hello @Rahix

Thanks for careful review. I force-pushed PR after introducing the changes according to your review comments. One minor change was proper error type mapping of the blocking wait result. ADC blocking behavior has been documented in both documentation block to read function and in commit message.

Regards,
Sergey

@geomatsi
Copy link
Contributor Author

Hi @Rahix

Do you plan to merge this PR in its current form ? Let me know if you have any other concerns.

Regards,
Sergey

Copy link
Owner

@Rahix Rahix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

yes, sorry for the silence. One note for the future: The place where you documented the implementation details (on the read() method inside the trait impl) makes them almost invisible in the documentation. I'll move it to the AdcProxy struct documentation, so you don't have to worry about it :)

Thanks again for the PR!

@Rahix Rahix merged commit 391a5dd into Rahix:main Aug 30, 2021
@geomatsi geomatsi deleted the shared-adc branch August 30, 2021 20:07
@eflukx
Copy link

eflukx commented Apr 29, 2022

Great to see this is already implemented here. Needed this feat, and rust analyzer pointed me out that shared bus is already implementing the OneShot trait! Saves me the hassle implementing 👍

One remark though: maybe call out in this projects' readme.md that an ADC proxy actually exists within this crate, so other people don't have to stumble upon this functionality! 😉

@Rahix
Copy link
Owner

Rahix commented May 1, 2022

Done!

@eflukx
Copy link

eflukx commented May 3, 2022

You're da man! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants