-
Notifications
You must be signed in to change notification settings - Fork 200
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-related traits #101
ADC-related traits #101
Conversation
src/analog.rs
Outdated
} | ||
|
||
/// ADCs that sample on single channels per request, and do so at the time of the request. | ||
pub trait SingleMode<Word, Pin: AdcChannel> { |
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.
Minor bikeshedding: analog::SingleMode
doesn't seem like the a great name here -- and it doesn't really line up with analog::Continuous
.
Some off-the-cuff ideas:
analog::OnDemand
analog::Once
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 like both suggestions. Another one: OneShot
, which I've seen used in this context.
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.
Yeah, OneShot
sampling is the typical term here. I think it's also important to recognise what is being described here: Is this the acquisition or the trigger mode? At the moment it sounds more like a mix of the two.
Continuous
: Continuous sampling with single value capture (Immediate trigger)Once
: Single sampling with single value capture (Immediate trigger)
Continuous
is only mildly useful as it only saves the reinitialisation cost for the ADC but running an ADC continuously to just capture a value every now and then is rather energy intensive.
Continuous
mode is far more interesting if you can do automatic sampling of many values at a specific rate into a fixed buffer (e.g. audio signal), rolling values into a circular buffer or automatically trigger a recording of a a single or burst of samples at some event (edge/level triggering on the signal).
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 think it's fair to say that I'm trying to describe the trigger mode here since it's closest to how the hardware configuration is realized. I do agree that Continuous gets more interesting when you use it in the ways you describe. I don't see a clear path there, and I feel like there's kind of an implied DMA activity too, traits for which are not yet very well-defined.
src/analog.rs
Outdated
fn stop(&mut self) -> nb::Result<(), Self::Error>; | ||
|
||
/// Read the specified channel | ||
fn read(&mut self, chan: Pin::ID) -> nb::Result<Word, 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.
I'm confused as to how this API ought to work -- maybe I'm missing something.
It seems as though the intended flow would be something like:
fn get_samples() {
let pin = Channel::new(); // actually get a channel though
let adc = Adc::init(); // actually get an ADC handle
adc.add_channel(pin);
adc.start();
for _ in 0..=10 {
let _ = adc.read(pin); // fails borrow check
}
adc.stop();
let rpin = adc.release_channel().unwrap();
}
I'm omitting error checking, etc, and might have made a mistake with this hasty example. The fact remains, however, that unless I'm misinterpreting when and how add_channel()
and read()
must interact, this interface cannot work.
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'm also confused as to how this would work with multiple continuously-sampled channels.
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.
Sorry for the earlier deleted comments - I misunderstood your question.
Note that read
takes a Pin::ID
argument, not the Pin
itself. So the code would look something like:
let pin: gpio::PA0<Analog> = gpioa.PA0.into_analog(); // PA0<Analog> impls AdcChannel<ADC1>
let adc: Adc1<Continuous<...>> = adc::init(...).into_continuous_mode(); // Adc1<Continuous> impls adc::Continuous<...>
adc.add_channel(pin);
adc.start();
for _ in 0..=10 {
let _ = adc.read(PA0::CHANNEL);
}
adc.stop();
let rpin = adc.release_channel(PA0::CHANNEL).unwrap();
src/lib.rs
Outdated
@@ -689,6 +689,7 @@ | |||
extern crate nb; | |||
extern crate void; | |||
|
|||
pub mod analog; |
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.
(Please note that I haven't worked with a DAC so far, so the following concerns may be unfounded. It would be great if someone with more experience in this area could pitch in.)
I'm concerned about the naming, and how well it will suit our needs once we add DAC traits. While analog
lines up nicely with digital
, having a single analog
module means both ADC and DAC will have to share the same namespace, and you're already reserving some attractive names for ADC.
Digital I/O is usually implemented in the same peripheral, and some drivers need to concern themselves with both input and output on the same pin. For that reason, I think it's a good idea to have a single digital
module. The question is, is the same true for ADC and DAC?
If the answer is "no", then I suggest renaming the module to adc
.
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.
My experience lines up with your intuition here -- a rename to adc
would be preferable IMO.
It's even what I started typing when bikeshedding the trait name, before I double checked.
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.
adc
and Once
works for me.
src/analog.rs
Outdated
} | ||
|
||
/// ADCs that sample on single channels per request, and do so at the time of the request. | ||
pub trait SingleMode<Word, Pin: AdcChannel> { |
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 like both suggestions. Another one: OneShot
, which I've seen used in this context.
src/analog.rs
Outdated
} | ||
|
||
/// ADCs that sample on single channels per request, and do so at the time of the request. | ||
pub trait SingleMode<Word, Pin: AdcChannel> { |
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.
Not sure how I feel about the name Pin
. Maybe it Channel
, or just C
would be simpler.
And have you considered moving Pin
to read_channel
? That would make it more convenient to use the trait (one less argument to specify in parameter lists), but I see two potential problems:
- I'm not sure if that would allow an implementation to specify what
Pin
is in awhere
clause. - It would be inconsistent with
Continuous
.
So yeah, not sure if this is a good idea, but I'm interested in knowing if you tried/considered it.
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 mostly kept it there for consistency with the other modes. Nothing's preventing moving Pin
into the various functions. If it's cleaner/easier, I'm not opposed.
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'm not opposed to keeping it where it is. Just wondering out loud.
src/analog.rs
Outdated
fn add_channel(&mut self, pin: Pin) -> nb::Result<(), Self::Error>; | ||
|
||
/// Release the ADC channel/pin | ||
fn release_channel(&mut self) -> nb::Result<Pin, 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.
Presumably you can call add_channel
multiple times. At least the trait won't prevent that. What will release_channel
return then?
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.
Sorry, mistake here. Continuous::release_channel
should indeed take a Pin::ID
argument.
src/analog.rs
Outdated
fn stop(&mut self) -> nb::Result<(), Self::Error>; | ||
|
||
/// Read the specified channel | ||
fn read(&mut self, chan: Pin::ID) -> nb::Result<Word, 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 addition to @austinbes's concerns, I'm confused about the overall purpose of this API. What's the point of starting a continuous conversion, if I then have to poll the read
method anyway?
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.
On such point might be to get a sample rate that's known, and regulated by hardware.
With that said, it would probably be more ergonomic (and more in-line with standard embedded paradigms) to somehow provide a buffer of samples rather than each individual one. I'm not sure the right way to do that in a memory-safe manner, though...
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.
The Continuous::read
method here would likely be non-blocking; For example, in the (iirc) LPC178x, each channel has a specific register and the results of the most recent conversion for that specific channel are stored there. (There's also a register for last conversion result for any channel.)
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.
Also, I contemplated a way to provide access to a buffer, but I couldn't see a way until const generics land because the number of channels would need to be constant in order to know the size of the buffer provided.
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 don't know if it's appropriate here, but I think the canonical way to work around the lack of const generics is generic-array
.
It occurs to me that some ADC operations would be blocking ( |
Why would |
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.
LGTM
I don't suppose they'd have to be, but I imagined them as such because that's how I've used single-ended conversions in the past: request a conversion, wait until done, return the result. I don't think there's anything about these traits that would require one behavior or another - that could be up to the implementor. I also think I should gate this behind unproven, which I haven't done yet, so another commit will be incoming soon. |
I think they absolutely should be non-blocking then. The precedent in this crate so far has been to provide blocking traits only in two cases:
I've had some time to reflect on this pull request now, and I have the following concerns and suggestions:
@thejpster You've already approved this PR without otherwise taking part in the discussion. What's your take on my list of concerns? I'm very interested in more input. |
@hannobraun all good points, thanks for the feedback. I split the concepts of pins and channels because the pin (the physical, mux-able component of the MCU) is distinct from the channel (the voltage source for the ADC conversion). In some cases (e.g. LPC1788) the mapping is 1:1, but in others, you can select between multiple pins. Much as It seems I'll call out separately that I feel very strongly that sample rates should not be part of this API. In every ADC I've ever seen, sample rate is a property of the ADC's core and is determined by some clock input and a prescaler. IOW, I think |
I'll rename Another possibility - move forward with the |
Sorry for the comment spam - one last note. I think implementing these as non-blocking (that is, returning |
Thank you the explanation, @thenewwazoo. Your comments have cleared things up a lot for me. My objections have been reduced to a vague feeling that better traits are possible, but until I dive into some ADCs and try to implement, and improve on, your traits (which, for the foreseeable future, I won't have time for) that's a moot point. I'm still interested in hearing more feedback from others, but as far as I'm concerned, I'm not opposed to merging this (at least for now; can't guarantee I won't come up with more feedback, given enough time :) ).
We can do that, but unless someone comes up with new objections, I don't think it's necessary. There's one more thing I thought about. I think it's part of a larger discussion and shouldn't hold back this pull request, but at least I'd like to note it, while we're all here.
There's precedent for keeping things simple and not enforcing this stuff at compile time (see |
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.
Thanks for the changes, @thenewwazoo. The added documentation improves things a lot!
I found some more stuff to nitpick about. Let me know what you think. Otherwise this is looking really good!
src/adc.rs
Outdated
/// | ||
/// This method takes a `Pin` reference, as it is expected that the ADC will be able to sample | ||
/// whatever channel underlies the pin. | ||
fn read_channel(&mut self, pin: &mut Pin) -> nb::Result<Word, 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.
This is called read_channel
, the method on Continuous
is called read
. I think both are fine names, but it may be better to keep it consistent and call both of them the same.
Given @hannobraun's excellent points regarding the design of |
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.
Thanks for the effort! Looks reasonable to me. I wonder whether it would be useful to have examples of consuming these traits (ie. into structs for drivers) in the documentation too? (on the basis that eventually consumers >> implementers).
@ryankurte I'm happy to drop a link in the doc pointing to my own crate which impls this if that's acceptable; I don't think I'll have time for a few days to work on a minimal-esque example. |
@thenewwazoo even just here for us to look at would be neat ^_^ |
Sure! You can see my impl here for the STM32L0x1. I can't publish yet (it's pending this merge) so you'll have to generate docs yourself if you want to see them rendered. |
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.
looks excellent, thanks!
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.
Thank you, @thenewwazoo, looks good to me!
One last wish: Please squash everything into one commit, then I'll merge.
Waiting anxiously for this! Also worth noting: |
Do I need to do anything more on this? I'm eagerly anticipating its merge. :) |
Sorry, @thenewwazoo, I didn't notice that you pushed a new commit. bors r+ |
101: ADC-related traits r=hannobraun a=thenewwazoo Opening the discussion! One implementation currently exists [here](https://github.com/thenewwazoo/stm32l0x1-context-hal/blob/master/src/adc.rs), and I'll be adding another for the LPC177x/8x HAL soonish. I feel like these traits encompass nearly all the useful "modes" that an ADC operates in; n.b. that IME "continuous" sampling of a single pin looks just like a "scan" with one pin enabled, so I didn't distinguish between the two. I'm not sure if I _should_, but it's a known t.b.d. (obviously the final PR would gate this behind `unproven`, or however is appropriate) Co-authored-by: Brandon Matthews <bmatthews@zipcar.com>
Oh, and thank you for your patience and working with us on this! |
Build succeeded |
Huzzah! |
Now the next question is: when can this be released (i.e. |
Pretty much right now. Consensus in #92 was to release a new patch version right after landing this. Just requires someone from the team finding the time to do it. |
Opening the discussion! One implementation currently exists here, and I'll be adding another for the LPC177x/8x HAL soonish.
I feel like these traits encompass nearly all the useful "modes" that an ADC operates in; n.b. that IME "continuous" sampling of a single pin looks just like a "scan" with one pin enabled, so I didn't distinguish between the two. I'm not sure if I should, but it's a known t.b.d.
(obviously the final PR would gate this behind
unproven
, or however is appropriate)