Skip to content

Conversation

usbalbin
Copy link
Member

@usbalbin usbalbin commented Aug 27, 2025

Fixes #35

  • Copy paste from H7 hal
  • Demacrofy most of the code
  • Configure pins and ensure it compiles for
    • H503
    • H523
    • H533
    • H562
    • H563
    • H573
  • Add examples
  • Test
  • Clean up
  • Clean up some more?
  • Remove references to H7 hal

Comment on lines 26 to 28
adc:
- adc
-
Copy link
Member Author

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

Copy link
Member

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?

Copy link
Member Author

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"]
Copy link
Member Author

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.

Copy link
Member

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
Comment on lines 55 to 62
let mut adc1 = adc::Adc::new(
dp.ADC1,
4.MHz(),
&mut delay,
ccdr.peripheral.ADC,
&ccdr.clocks,
&pwrcfg,
);
Copy link
Member Author

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

Copy link
Member

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?

Copy link
Member Author

@usbalbin usbalbin Aug 29, 2025

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

@usbalbin usbalbin marked this pull request as ready for review September 7, 2025 16:21
@usbalbin
Copy link
Member Author

usbalbin commented Sep 7, 2025

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 configure_channel from the g4 hal to prepare for that but I think the rest of the DMA stuff can wait for another PR.

Comment on lines +193 to +211
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> {
Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

@astapleton astapleton left a 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:

  1. 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.
  2. 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.
  3. 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.
  4. I think the API can be made more ergonomic, similar to the GPIO/GPDMA APIs. See the comments about that.
  5. 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 a Adc<ADC1, Disabled> or Adc<ADC1, PoweredDown>?

Copy link
Member

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 {
Copy link
Member

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?

Comment on lines +260 to +262
sample_time: AdcSampleTime,
resolution: Resolution,
current_channel: Option<u8>,
Copy link
Member

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> {
Copy link
Member

@astapleton astapleton Sep 9, 2025

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(
Copy link
Member

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)),+ $(,)*) => {
Copy link
Member

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 {
Copy link
Member

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.

Copy link
Member Author

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>

Copy link
Member

Choose a reason for hiding this comment

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

Ah right, yeah :/

Comment on lines +79 to +89
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));
}
Copy link
Member

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);
Copy link
Member

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?

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough :)

Comment on lines +193 to +211
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> {
Copy link
Member

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.

@usbalbin
Copy link
Member Author

Thank you for the comments :)

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

Successfully merging this pull request may close these issues.

Feature request - ADC
2 participants