imxrt-rs/imxrt-hal

ADC implementation

Walther opened this issue ยท 8 comments

embedded-hal specifies ADC traits here

It seems that this crate does not implement those yet. This would be useful in order to e.g. read analog values from the pins of a Teensy 4 board.


Would love to try and help with this, if someone could point me to the right direction & provide a bit of mentoring ๐Ÿ™‚

Hi Walther!

ADC would be a great addition to the HAL. In looking at the embedded-hal interface its pretty limited in how it allows the ADC to operate. Still its a good start for a one shot software driven conversion I suppose! Hopefully they have improvements in mind.

I highly recommend keeping a copy of the enormous ref manual handy. You might need to signup for a NXP account to download it.

To implement the Channel trait I might define a macro for such a thing in a new adc.rs

macro_rules! adc_channel {
   ($adc: ident, $channel: ident, $pin: ident, $alt_mode: ident) => {
        impl Channel<$adc_instance> for $pin<$alt> {
        }
   }
}

Implementing OneShot I'd likely do something similar, though it will require creating an ADC type as well. Something like the following...

/// However many instances needed here
pub struct ADC1;
pub struct ADC2;

/// ADC Type to impl OneShot on.
pub struct ADC<Instance> {
   _marker: PhantomType<Instance>;
}

macro_rules! adc_one_shot {
   ($adc: ident, $pin: ident, $alt_mode: ident) {
       impl<WORD> OneShot<ADC<$adc>, WORD, $pin<$alt_mode>> for ADC<$adc> {
       where
       WORD: From<u16>,
       {
          fn read(&mut self, pin: &mut $pin<$alt_mode>) {
              ///configure pin to be usable for ADC, perhaps *saving configuration*?
             /// configure adc for one shot
             /// wait for oneshot to complete
             /// restore gpio configuration?
          }
       }
   }
}

Lastly you could tied it all together in a adc macro that provides both the channel and OneShot impls, hopefully avoiding a lot of extra typing.

Notably the ADC type would need to likely start off in an Unlocked state like all the other HAL peripherals do, not providing its useful functionality until clock() is called on it. See imxrt-hal/src/pwm.rs for some inspiration.

Any updates on this? I'd be happy to try and get something working but I'm new to both embedded and rust so it might be an interesting time.

Haven't seen any updates on my end. Happy to help with either the embedded or the Rust parts.

The CONTRIBUTING guide gives some tips to get started, and points to resources you might want to have on hand, like the processor reference manual.

I might avoid the macro at this point, I don't know if its necessary with the way we are doing things now. Instead use generics if possible, as they are easier to document.

Sorry for the lack of updates!
I ended up thoroughly confused by the significant use of macros in this crate, not having personally implemented / used any before. I would need to learn writing those first with a simpler project / tutorial, read more about them in the book, etc.

I still have the devices and have some audio projects in mind, so I would be happy to help with testing if you do get something implemented!

Got some bare basic test code working, I'm thinking about how to do the interface. I'm not familiar with the conventions for interfaces in these crates, so any references/help is appreciated. First, there's a LOT of global register settings (ADHSC, ADLPC, ADLSMP, ADSTS, and the clock dividing vs asynchronous clock, see pg. 3354 in the RT1060 manual). Do we want to expose each of these settings individually or do more what like the teensy core library and others do and hide those settings behind the user's choice for resolution and/or sampling/conversion speed. Or both? The iomuxc crate is also a bit of a mystery for me, and I'm not sure how to get channels from pins (eg. pad AD_B1_02 is channel 7, for the input select of the ADCx_HC0 register).

Do we want to expose each of these settings individually or do more what like the teensy core library and others do and hide those settings behind the user's choice for resolution and/or sampling/conversion speed.

I'll vote for the simpler API that lets users configure the driver by resolutions and conversion speeds. An API using resolutions might be easier to use for newcomers to this chip. If we find a need for a power-user API in the future, we should be able to expose those methods in a forward-compatible manner. If you'd like to tackle both as part of this effort, go for it!

Some other HAL peripheral drivers let users select clocks (kinda; most clock selections are only a single option). So, if you're thinking about letting users select the IPG clock, IPG div 2 clock, or ADACK, there's examples for how we might do it. That being said, the ADC driver implementation might be simpler by committing to just ADACK. The data sheets note ADACK clock speeds; I couldn't find it in the reference manual.

The iomuxc crate is also a bit of a mystery for me, and I'm not sure how to get channels from pins

I might be able to get you started. The goal of the IOMUXC crate is to specify which pads support which functions. There are tables in the reference manual (like Table 10-1, Table 66-2, iMXRT1060, Rev 2) that indicate the ADC-compatible pins. We just need a way to express that in Rust.

This commit in my imxrt-rs fork, branch proto/adc, prototypes one way to express the ADC-compatible pins. It should handle the fact that some pads might be used for both ADC1 and ADC2. But, it's otherwise untested, and has a few TODOs. Feel free to steal it, ignore it, or ask questions right on that commit.

From that IOMUXC implementation, the pads express their supported ADC module:

use imxrt_hal::iomuxc::adc::{ADC, Pin}

fn use_adc_pin_for_something<ADCx: ADC, P: Pin<ADCx>>(pin: P) {
    // ADCx is either ADC1, or ADC2
    // The INx number is available using the associated type, P::Input:
   let input_channel: usize = P::Input::USIZE;
}

Ahh ok that makes sense, thank you! I'll see if I can get a prototype up for you guys to pick over.