-
Notifications
You must be signed in to change notification settings - Fork 6
Adc #70
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
base: master
Are you sure you want to change the base?
Adc #70
Conversation
.github/workflows/ci.yml
Outdated
adc: | ||
- 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.
Not sure how we want to test this... This duplicates the number of tests to run
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.
Does the ADC need to be feature gated?
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 that depends on how we want to do with the extra dependencies.
Cargo.toml
Outdated
@@ -67,17 +67,22 @@ defmt = [ | |||
"stm32h5/defmt", | |||
] | |||
|
|||
adc = ["dep:embedded-hal-02", "dep:nb"] |
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.
We need embedded_hal_02 for the adc::Channel
trait. However we could probably avoid nb
if we provide our own blocking Adc::convert
method for those that do not need to be generic using the embedded_hal_02::adc::Oneshot
trait.
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.
What's the advantage of using that trait if embedded-hal
has dropped it in 1.0?
examples/adc.rs
Outdated
let mut adc1 = adc::Adc::new( | ||
dp.ADC1, | ||
4.MHz(), | ||
&mut delay, | ||
ccdr.peripheral.ADC, | ||
&ccdr.clocks, | ||
&pwrcfg, | ||
); |
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 am not too happy with the api for setting up the adc. I think I would prefer something more like the G4 hal
let adc12_common = dp.ADC12_COMMON.claim(Default::default(), &mut rcc);
let mut adc = adc12_common.claim_and_configure(
dp.ADC2,
stm32g4xx_hal::adc::config::AdcConfig::default(),
&mut delay,
);
Here settings are divided into the individual config and the common config. Config structs are used with sensible defaults for those that do not care instead of individual arguments for frequency etc/or setter methods that have to be used after the fact
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.
We can't use extension traits like we use for other peripherals?
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.
Probably, I just have not really changed the api from H7 yet. But yes that would probably be more consistent with the rest of the H5 hal.
The H503 only has one adc so it does not have the ADCC common register block as a separate block so that would probably not work with the above from G4
This still needs some testing, me or @Wratox will hopefully get to that in the coming days. In the meantime what do you think of this @astapleton? There are still a lot that can be done to get this to work with DMA. I have taken |
pub fn claim_and_configure<ADC: Instance>( | ||
&self, | ||
adc: ADC, | ||
delay: &mut impl DelayNs, | ||
resolution: Resolution, | ||
) -> Adc<ADC, Disabled> { | ||
Self::setup_adc(adc, delay, resolution) | ||
} | ||
|
||
/// Initialise ADC | ||
/// | ||
/// Sets all configurable parameters to one-shot defaults, | ||
/// performs a boot-time calibration. | ||
#[cfg(feature = "rm0492")] | ||
pub fn claim_and_configure( | ||
self, | ||
delay: &mut impl DelayNs, | ||
resolution: Resolution, | ||
) -> Adc<ADC1, Disabled> { |
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.
To make it as consistent as possible the user has to create an AdcCommon
for the H503 just like the other devices, even though it does not have a separate adc common ADCC
block(it is part of ADC1
which is the one and only ADC for h503).
Because of this, claim_and_configure
looks a bit different here. For H503 the ADC1
(acting as the ADCC) was consumed to create the AdcCommon
so the user can not pass that as the adc
again as for the other devices. Instead self is consumed as opposed to the other devices which just borrows it, to prevent the user from creating multiple instances.
Do you have any other suggestions?
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 thinking we can do something similar to how the GPIO driver does it:
- define an associated type on the ADCC extension trait defines types that must be passed in to the
claim
method. For H503 it's a()
, but it would be(ADC1, ADC2)
for the other processors - define an associated type that indicates a struct that owns all ADC peripherals and is returned by the claim method
- define an extension trait for the ADC peripherals themselves that enables them.
Something like:
pub trait AdccExt { // AdcCommonExt renamed to reflect similar naming conventions for other peripherals
type Parts;
type Peripherals;
fn claim(
self,
peripherals: Self::Peripherals,
f_adc: Hertz,
prec: rec::Adc,
clocks: &CoreClocks,
pwrcfg: &pwr::PowerConfiguration,
) -> Self::Parts;
}
trait AdcExt<ADC: Instance>: Sized {
fn enable(self, config: AdcConfig) -> Adc<ADC>;
}
impl<ADC: Instance> AdcExt<ADC> for ADC {
fn enable(self, config: AdcConfig) -> Adc<ADC> {
// This is essentially the AdcCommon::setup_adc method without being bound to the AdcCommon struct
setup_adc(self, config)
}
}
#[cfg(feature = "rm0492")]
mod rm0492 {
use crate::stm32::ADC1;
use super::*;
pub struct AdcPeriphs {
adc1: ADC1,
}
impl AdccExt for ADC1 {
type Parts = AdcPeriphs;
type Peripherals = ();
fn claim(
self,
peripherals: (),
f_adc: Hertz,
prec: rec::Adc,
clocks: &CoreClocks,
pwrcfg: &pwr::PowerConfiguration,
) -> Self::Parts {
// Check adc_ker_ck_input
kernel_clk_unwrap(&prec, clocks);
// Enable AHB clock
let prec = prec.enable().reset();
// This is AdcCommon::configure_clock without being bound to the AdcCommon struct
let _ = configure_clock(&self, f_adc, prec, clocks, pwrcfg); // ADC12_COMMON
AdcPeriphs { adc1: self }
}
}
}
#[cfg(feature = "rm0492")]
pub use rm0492::*
#[cfg(feature = "rm0481")]
mod rm0481 {
use crate::stm32::{ADC1, ADC2, ADCC};
use super::*;
pub struct AdcPeriphs {
adc1: ADC1,
adc2: ADC2,
}
impl AdccExt for ADCC {
type Parts = AdcPeriphs;
type Peripherals = (ADC1, ADC2);
fn claim(
self,
(adc1, adc2): (ADC1, ADC2),
f_adc: Hertz,
prec: rec::Adc,
clocks: &CoreClocks,
pwrcfg: &pwr::PowerConfiguration,
) -> Self::Parts {
// Check adc_ker_ck_input
kernel_clk_unwrap(&prec, clocks);
let prec = prec.enable().reset();
let _ = configure_clock(&self, f_adc, prec, clocks, pwrcfg); // ADC12_COMMON
AdcPeriphs { adc1, adc2 }
}
}
}
#[cfg(feature = "rm0481")]
pub use rm0481::*
It would then be used as follows:
On H503:
let adc_periphs = dp.ADC1.claim((), 4.MHz(), ccdr.peripheral.ADC, &ccdr.clocks, &pwrcfg);
let adc1 = adc_periphs.adc1.enable(config);
On the other parts:
let adc_periphs = dp.ADCC.claim((dp.ADC1, dp.ADC2), 4.MHz(), ccdr.peripheral.ADC, &ccdr.clocks, &pwrcfg);
let adc1 = adc_periphs.adc1.enable(config);
let adc2 = adc_periphs.adc2.enable(config);
The nice thing about this is that only the claim method changes between processor families for end users, all the differences between the families are encapsulated in these trait implementations (which in turn can be encapsulated in the h5
module), and the extension method for the ADC peripherals is a bit more ergonomic.
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.
As I see it, we need a couple of things
- A way to ensure that there are no races configuring
Temperature
,Vrefint
in the common block - Ensure the clocks are configured before allowing the user to try to setup or use the ADCs
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 PR! Thanks for taking the time to demacrofy the H7 version of this driver.
I have some concerns about this implementation so far and I think there are some improvements that can be made to the structure of the driver:
- There are a lot of calls to
ADCC::steal()
,ADC1::steal()
etc. This gives me concerns about the soundness of the implementation, and I think it would be best to minimize these if possible. I made some suggestions in the comments; hopefully they are helpful. - I'm also concerned about the soundness of starting a conversion and then allowing the GPIOs to be reconfigured after the conversion has been started (see
configure_channel
). I think taking ownership of the channel would be an improvement. - I find it hard to reason about code when there are a lot of instructions conditionally compiled, and much prefer to move those processor/feature-dependent differences into submodules, and/or abstract over them with traits. I'd recommend doing so with the conditional compilation directives in the top-level driver file.
- I think the API can be made more ergonomic, similar to the GPIO/GPDMA APIs. See the comments about that.
- Can we avoid the typestate type parameter and only return an enabled
Adc
value when initializing? Does it make sense in terms of how the ADC is generally used to take control of aAdc<ADC1, Disabled>
orAdc<ADC1, PoweredDown>
?
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.
Let's keep the module naming scheme consistent: ie: put this in the src/ root and name it adc.rs
|
||
/// The place in the sequence a given channel should be captured | ||
#[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] | ||
pub enum Sequence { |
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 enum doesn't seem all that useful? Why not just use a usize and check the bounds of the parameter when it's used to configure the channel?
sample_time: AdcSampleTime, | ||
resolution: Resolution, | ||
current_channel: Option<u8>, |
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.
It doesn't look like these values (sample_time
, resolution
, and current_channel
) need to be stored in the Adc
type. In fact, they prevent the struct from being zero-sized for little or no benefit. Let's rather remove them and instead pass in a configuration struct when one is needed.
impl ED for Enabled {} | ||
impl ED for Disabled {} | ||
|
||
pub struct Adc<ADC, ED> { |
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.
Can we eliminate the typestate ED
here? It's quite an annoying (IMO) pattern to use for something that needs to be held for the lifetime of a program. By the looks of things it's mostly used before the Adc is enabled. Can we instead not either:
a) only return an Adc
that is enabled, and disable it when dropping the Adc
struct, either through a method that consumes self
or a drop implementation, or
b) bound interactions such that the Adc is only enabled in a blocking call, or such that we return some sort of state object that keeps it enabled while it is held or active (ie. until it is passed back to the Adc
instance)
} | ||
|
||
/// No clock checks | ||
fn configure_clock_unchecked( |
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 doesn't need to be a struct method
} | ||
|
||
macro_rules! adc_internal { | ||
([$INT_ADC:ident, $INT_ADC_COMMON:ident]; $($input:ty => ($chan:expr, $en:ident)),+ $(,)*) => { |
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.
$INT_ADC_COMMON
is not used?
($ADC:ident, $($input:ty => $chan:expr),+ $(,)*) => { | ||
$( | ||
#[cfg(feature = "eh-02")] | ||
impl embedded_hal_02::adc::Channel<$ADC> for $input { |
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.
instead of implementing this for every input, how about providing a blanket implementation for every T that implements 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 tried that, but since (as I understand it) we dont own the trait and can therefore not implement it for all T's where T: AdcChannel<some 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.
Ah right, yeah :/
pub fn enable(_adc: &mut super::Adc<ADC2, super::Disabled>) { | ||
let adc2 = unsafe { ADC2::steal() }; | ||
|
||
adc2.or().modify(|_, w| w.op0().bit(true)); | ||
} | ||
|
||
pub fn disable(_adc: &mut super::Adc<ADC2, super::Disabled>) { | ||
let adc2 = unsafe { ADC2::steal() }; | ||
|
||
adc2.or().modify(|_, w| w.op0().bit(false)); | ||
} |
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.
Some of these enable
/disable
methods take in an Adc
struct, and some take in the AdcCommon
struct, but the parameters are not used. I think we should try to eliminate this inconsistency, and make use of the parameter to avoid "stealing" the peripheral. Perhaps a trait that provides a method to enable a particular channel, or provides a reference to the register block struct would both use the parameter, and avoid "stealing" the peripheral.
self.rb | ||
.cr() | ||
.modify(|_, w| w.deeppwd().clear_bit().advregen().set_bit()); | ||
delay.delay_us(10); |
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.
It's kind of annoying to have to create and pass in a Delay struct here for a one off delay like this (it can't be used/owned elsewhere). Can we not calculate the amount of cycles to wait from the HCLK and use cortex_m::asm::delay
in here to perform the delay?
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.
[...] it can't be used/owned elsewhere [...]
Why not? It is only passed in by mut ref
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 might enforce ordering constraints on system initialization that might be inconvenient (need to initialize the ADC before another system component that wants to take ownership of the Delay implementation), and if we could do the delay transparently to the consumer (even if we just chose a static amount of cycles that is longer with a slower clock speed) that would make for a better API.
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.
Fair enough :)
pub fn claim_and_configure<ADC: Instance>( | ||
&self, | ||
adc: ADC, | ||
delay: &mut impl DelayNs, | ||
resolution: Resolution, | ||
) -> Adc<ADC, Disabled> { | ||
Self::setup_adc(adc, delay, resolution) | ||
} | ||
|
||
/// Initialise ADC | ||
/// | ||
/// Sets all configurable parameters to one-shot defaults, | ||
/// performs a boot-time calibration. | ||
#[cfg(feature = "rm0492")] | ||
pub fn claim_and_configure( | ||
self, | ||
delay: &mut impl DelayNs, | ||
resolution: Resolution, | ||
) -> Adc<ADC1, Disabled> { |
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 thinking we can do something similar to how the GPIO driver does it:
- define an associated type on the ADCC extension trait defines types that must be passed in to the
claim
method. For H503 it's a()
, but it would be(ADC1, ADC2)
for the other processors - define an associated type that indicates a struct that owns all ADC peripherals and is returned by the claim method
- define an extension trait for the ADC peripherals themselves that enables them.
Something like:
pub trait AdccExt { // AdcCommonExt renamed to reflect similar naming conventions for other peripherals
type Parts;
type Peripherals;
fn claim(
self,
peripherals: Self::Peripherals,
f_adc: Hertz,
prec: rec::Adc,
clocks: &CoreClocks,
pwrcfg: &pwr::PowerConfiguration,
) -> Self::Parts;
}
trait AdcExt<ADC: Instance>: Sized {
fn enable(self, config: AdcConfig) -> Adc<ADC>;
}
impl<ADC: Instance> AdcExt<ADC> for ADC {
fn enable(self, config: AdcConfig) -> Adc<ADC> {
// This is essentially the AdcCommon::setup_adc method without being bound to the AdcCommon struct
setup_adc(self, config)
}
}
#[cfg(feature = "rm0492")]
mod rm0492 {
use crate::stm32::ADC1;
use super::*;
pub struct AdcPeriphs {
adc1: ADC1,
}
impl AdccExt for ADC1 {
type Parts = AdcPeriphs;
type Peripherals = ();
fn claim(
self,
peripherals: (),
f_adc: Hertz,
prec: rec::Adc,
clocks: &CoreClocks,
pwrcfg: &pwr::PowerConfiguration,
) -> Self::Parts {
// Check adc_ker_ck_input
kernel_clk_unwrap(&prec, clocks);
// Enable AHB clock
let prec = prec.enable().reset();
// This is AdcCommon::configure_clock without being bound to the AdcCommon struct
let _ = configure_clock(&self, f_adc, prec, clocks, pwrcfg); // ADC12_COMMON
AdcPeriphs { adc1: self }
}
}
}
#[cfg(feature = "rm0492")]
pub use rm0492::*
#[cfg(feature = "rm0481")]
mod rm0481 {
use crate::stm32::{ADC1, ADC2, ADCC};
use super::*;
pub struct AdcPeriphs {
adc1: ADC1,
adc2: ADC2,
}
impl AdccExt for ADCC {
type Parts = AdcPeriphs;
type Peripherals = (ADC1, ADC2);
fn claim(
self,
(adc1, adc2): (ADC1, ADC2),
f_adc: Hertz,
prec: rec::Adc,
clocks: &CoreClocks,
pwrcfg: &pwr::PowerConfiguration,
) -> Self::Parts {
// Check adc_ker_ck_input
kernel_clk_unwrap(&prec, clocks);
let prec = prec.enable().reset();
let _ = configure_clock(&self, f_adc, prec, clocks, pwrcfg); // ADC12_COMMON
AdcPeriphs { adc1, adc2 }
}
}
}
#[cfg(feature = "rm0481")]
pub use rm0481::*
It would then be used as follows:
On H503:
let adc_periphs = dp.ADC1.claim((), 4.MHz(), ccdr.peripheral.ADC, &ccdr.clocks, &pwrcfg);
let adc1 = adc_periphs.adc1.enable(config);
On the other parts:
let adc_periphs = dp.ADCC.claim((dp.ADC1, dp.ADC2), 4.MHz(), ccdr.peripheral.ADC, &ccdr.clocks, &pwrcfg);
let adc1 = adc_periphs.adc1.enable(config);
let adc2 = adc_periphs.adc2.enable(config);
The nice thing about this is that only the claim method changes between processor families for end users, all the differences between the families are encapsulated in these trait implementations (which in turn can be encapsulated in the h5
module), and the extension method for the ADC peripherals is a bit more ergonomic.
Thank you for the comments :) |
Fixes #35