-
Notifications
You must be signed in to change notification settings - Fork 1.2k
stm32/adc/v3: added support for DMA based adc sampling #4583
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
Changes from 18 commits
14a047a
0b8da5a
1350700
e5d4ef4
756ec7f
75484f4
6b8d375
a79a566
e48dc90
1db247f
acc3c4b
f701fc4
63d0172
5046a8a
0a97a1d
cd91fe3
04a36e9
cb0175a
0754972
8215864
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| use core::marker::PhantomData; | ||
| use core::sync::atomic::{Ordering, compiler_fence}; | ||
|
|
||
| use embassy_hal_internal::Peri; | ||
|
|
||
| use crate::adc::{Instance, RxDma}; | ||
| use crate::dma::{ReadableRingBuffer, TransferOptions}; | ||
| use crate::rcc; | ||
|
|
||
| #[cfg_attr(feature = "defmt", derive(defmt::Format))] | ||
| pub struct OverrunError; | ||
|
|
||
| pub struct RingBufferedAdc<'d, T: Instance> { | ||
| pub _phantom: PhantomData<T>, | ||
| pub ring_buf: ReadableRingBuffer<'d, u16>, | ||
| } | ||
|
|
||
| impl<'d, T: Instance> RingBufferedAdc<'d, T> { | ||
| pub fn new(dma: Peri<'d, impl RxDma<T>>, dma_buf: &'d mut [u16]) -> Self { | ||
| //dma side setup | ||
| let opts = TransferOptions { | ||
| half_transfer_ir: true, | ||
| circular: true, | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| // Safety: we forget the struct before this function returns. | ||
| let request = dma.request(); | ||
|
|
||
| let ring_buf = | ||
| unsafe { ReadableRingBuffer::new(dma, request, T::regs().dr().as_ptr() as *mut u16, dma_buf, opts) }; | ||
|
|
||
| Self { | ||
| _phantom: PhantomData, | ||
| ring_buf, | ||
| } | ||
| } | ||
|
|
||
| #[inline] | ||
| fn start_continuous_sampling(&mut self) { | ||
| // Start adc conversion | ||
| T::regs().cr().modify(|reg| { | ||
| reg.set_adstart(true); | ||
| }); | ||
| self.ring_buf.start(); | ||
| } | ||
|
|
||
| #[inline] | ||
| pub fn stop_continuous_sampling(&mut self) { | ||
| // Stop adc conversion | ||
| if T::regs().cr().read().adstart() && !T::regs().cr().read().addis() { | ||
| T::regs().cr().modify(|reg| { | ||
| reg.set_adstp(true); | ||
| }); | ||
| while T::regs().cr().read().adstart() {} | ||
| } | ||
| } | ||
| pub fn disable_adc(&mut self) { | ||
| self.stop_continuous_sampling(); | ||
| self.ring_buf.clear(); | ||
| self.ring_buf.request_pause(); | ||
| } | ||
|
|
||
| pub fn teardown_adc(&mut self) { | ||
| self.disable_adc(); | ||
|
|
||
| //disable dma control | ||
| #[cfg(not(any(adc_g0, adc_u0)))] | ||
| T::regs().cfgr().modify(|reg| { | ||
| reg.set_dmaen(false); | ||
| }); | ||
| #[cfg(any(adc_g0, adc_u0))] | ||
| T::regs().cfgr1().modify(|reg| { | ||
| reg.set_dmaen(false); | ||
| }); | ||
|
|
||
| //TODO: do we need to cleanup the DMA request here? | ||
|
|
||
| compiler_fence(Ordering::SeqCst); | ||
| } | ||
|
|
||
| /// Reads measurements from the DMA ring buffer. | ||
| /// | ||
| /// This method fills the provided `measurements` array with ADC readings from the DMA buffer. | ||
| /// The length of the `measurements` array should be exactly half of the DMA buffer length. Because interrupts are only generated if half or full DMA transfer completes. | ||
| /// | ||
| /// Each call to `read` will populate the `measurements` array in the same order as the channels defined with `sequence`. | ||
| /// There will be many sequences worth of measurements in this array because it only returns if at least half of the DMA buffer is filled. | ||
| /// For example if 2 channels are sampled `measurements` contain: `[sq0 sq1 sq0 sq1 sq0 sq1 ..]`. | ||
| /// | ||
| /// Note that the ADC Datarate can be very fast, it is suggested to use DMA mode inside tightly running tasks | ||
| /// Otherwise, you'll see constant Overrun errors occuring, this means that you're sampling too quickly for the task to handle, and you may need to increase the buffer size. | ||
| /// Example: | ||
| /// ```rust,ignore | ||
| /// const DMA_BUF_LEN: usize = 120; | ||
| /// use embassy_stm32::adc::{Adc, AdcChannel} | ||
| /// | ||
| /// let mut adc = Adc::new(p.ADC1); | ||
| /// let mut adc_pin0 = p.PA0.degrade_adc(); | ||
| /// let mut adc_pin1 = p.PA1.degrade_adc(); | ||
| /// let adc_dma_buf = [0u16; DMA_BUF_LEN]; | ||
| /// | ||
| /// let mut ring_buffered_adc: RingBufferedAdc<embassy_stm32::peripherals::ADC1> = adc.into_ring_buffered( | ||
| /// p.DMA2_CH0, | ||
| /// adc_dma_buf, [ | ||
| /// (&mut *adc_pin0, SampleTime::CYCLES160_5), | ||
| /// (&mut *adc_pin1, SampleTime::CYCLES160_5), | ||
| /// ].into_iter()); | ||
| /// | ||
| /// | ||
| /// let mut measurements = [0u16; DMA_BUF_LEN / 2]; | ||
| /// loop { | ||
| /// match ring_buffered_adc.read(&mut measurements).await { | ||
| /// Ok(_) => { | ||
| /// defmt::info!("adc1: {}", measurements); | ||
| /// } | ||
| /// Err(e) => { | ||
| /// defmt::warn!("Error: {:?}", e); | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// ``` | ||
| /// | ||
| /// | ||
| /// [`teardown_adc`]: #method.teardown_adc | ||
| /// [`start_continuous_sampling`]: #method.start_continuous_sampling | ||
| pub async fn read(&mut self, measurements: &mut [u16]) -> Result<usize, OverrunError> { | ||
| assert_eq!( | ||
| self.ring_buf.capacity() / 2, | ||
| measurements.len(), | ||
| "Buffer size must be half the size of the ring buffer" | ||
| ); | ||
|
|
||
| let r = T::regs(); | ||
|
|
||
| // Start background receive if it was not already started | ||
| if !r.cr().read().adstart() { | ||
| self.start_continuous_sampling(); | ||
| } | ||
|
|
||
| self.ring_buf.read_exact(measurements).await.map_err(|_| OverrunError) | ||
| } | ||
|
|
||
| /// Read bytes that are readily available in the ring buffer. | ||
| /// If no bytes are currently available in the buffer the call waits until the some | ||
| /// bytes are available (at least one byte and at most half the buffer size) | ||
| /// | ||
| /// Background receive is started if `start_continuous_sampling()` has not been previously called. | ||
| /// | ||
| /// Receive in the background is terminated if an error is returned. | ||
| /// It must then manually be started again by calling `start_continuous_sampling()` or by re-calling `blocking_read()`. | ||
| pub fn blocking_read(&mut self, buf: &mut [u16]) -> Result<usize, OverrunError> { | ||
| let r = T::regs(); | ||
|
|
||
| // Start background receive if it was not already started | ||
| if !r.cr().read().adstart() { | ||
| self.start_continuous_sampling(); | ||
| } | ||
|
|
||
| loop { | ||
| match self.ring_buf.read(buf) { | ||
| Ok((0, _)) => {} | ||
| Ok((len, _)) => { | ||
| return Ok(len); | ||
| } | ||
| Err(_) => { | ||
| self.stop_continuous_sampling(); | ||
| return Err(OverrunError); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<T: Instance> Drop for RingBufferedAdc<'_, T> { | ||
| fn drop(&mut self) { | ||
| self.teardown_adc(); | ||
| rcc::disable::<T>(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,13 @@ pub use pac::adc::vals::{Ovsr, Ovss, Presc}; | |
| use super::{ | ||
| Adc, AdcChannel, AnyAdcChannel, Instance, Resolution, RxDma, SampleTime, SealedAdcChannel, blocking_delay_us, | ||
| }; | ||
|
|
||
| #[cfg(any(adc_v3, adc_g0, adc_u0))] | ||
| mod ringbuffered_v3; | ||
|
|
||
| #[cfg(any(adc_v3, adc_g0, adc_u0))] | ||
| use ringbuffered_v3::RingBufferedAdc; | ||
|
|
||
| use crate::dma::Transfer; | ||
| use crate::{Peri, pac, rcc}; | ||
|
|
||
|
|
@@ -559,6 +566,137 @@ impl<'d, T: Instance> Adc<'d, T> { | |
| }); | ||
| } | ||
|
|
||
| /// Configures the ADC to use a DMA ring buffer for continuous data acquisition. | ||
| /// | ||
| /// The `dma_buf` should be large enough to prevent DMA buffer overrun. | ||
| /// The length of the `dma_buf` should be a multiple of the ADC channel count. | ||
| /// For example, if 3 channels are measured, its length can be 3 * 40 = 120 measurements. | ||
| /// | ||
| /// `read` method is used to read out measurements from the DMA ring buffer, and its buffer should be exactly half of the `dma_buf` length. | ||
| /// It is critical to call `read` frequently to prevent DMA buffer overrun. | ||
| /// | ||
| /// [`read`]: #method.read | ||
| #[cfg(any(adc_v3, adc_g0, adc_u0))] | ||
| pub fn into_ring_buffered<'a>( | ||
| &mut self, | ||
| dma: Peri<'a, impl RxDma<T>>, | ||
| dma_buf: &'a mut [u16], | ||
| sequence: impl ExactSizeIterator<Item = (&'a mut AnyAdcChannel<T>, SampleTime)>, | ||
| ) -> RingBufferedAdc<'a, T> { | ||
| assert!(!dma_buf.is_empty() && dma_buf.len() <= 0xFFFF); | ||
| assert!(sequence.len() != 0, "Asynchronous read sequence cannot be empty"); | ||
| assert!( | ||
| sequence.len() <= 16, | ||
| "Asynchronous read sequence cannot be more than 16 in length" | ||
| ); | ||
| // reset conversions and enable the adc | ||
| Self::cancel_conversions(); | ||
| self.enable(); | ||
|
|
||
| //adc side setup | ||
|
|
||
| // Set sequence length | ||
| #[cfg(not(any(adc_g0, adc_u0)))] | ||
| T::regs().sqr1().modify(|w| { | ||
| w.set_l(sequence.len() as u8 - 1); | ||
| }); | ||
|
|
||
| #[cfg(adc_g0)] | ||
| { | ||
| let mut sample_times = Vec::<SampleTime, SAMPLE_TIMES_CAPACITY>::new(); | ||
|
|
||
| T::regs().chselr().write(|chselr| { | ||
| T::regs().smpr().write(|smpr| { | ||
| for (channel, sample_time) in sequence { | ||
| chselr.set_chsel(channel.channel.into(), true); | ||
| if let Some(i) = sample_times.iter().position(|&t| t == sample_time) { | ||
| smpr.set_smpsel(channel.channel.into(), (i as u8).into()); | ||
| } else { | ||
| smpr.set_sample_time(sample_times.len(), sample_time); | ||
| if let Err(_) = sample_times.push(sample_time) { | ||
| panic!( | ||
| "Implementation is limited to {} unique sample times among all channels.", | ||
| SAMPLE_TIMES_CAPACITY | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| }); | ||
| } | ||
| #[cfg(not(adc_g0))] | ||
| { | ||
| #[cfg(adc_u0)] | ||
| let mut channel_mask = 0; | ||
|
|
||
| // Configure channels and ranks | ||
| for (_i, (channel, sample_time)) in sequence.enumerate() { | ||
| Self::configure_channel(channel, sample_time); | ||
|
|
||
| // Each channel is sampled according to sequence | ||
| #[cfg(not(any(adc_g0, adc_u0)))] | ||
| match _i { | ||
| 0..=3 => { | ||
| T::regs().sqr1().modify(|w| { | ||
| w.set_sq(_i, channel.channel()); | ||
| }); | ||
| } | ||
| 4..=8 => { | ||
| T::regs().sqr2().modify(|w| { | ||
| w.set_sq(_i - 4, channel.channel()); | ||
| }); | ||
| } | ||
| 9..=13 => { | ||
| T::regs().sqr3().modify(|w| { | ||
| w.set_sq(_i - 9, channel.channel()); | ||
| }); | ||
| } | ||
| 14..=15 => { | ||
| T::regs().sqr4().modify(|w| { | ||
| w.set_sq(_i - 14, channel.channel()); | ||
| }); | ||
| } | ||
| _ => unreachable!(), | ||
| } | ||
|
|
||
| #[cfg(adc_u0)] | ||
| { | ||
| channel_mask |= 1 << channel.channel(); | ||
| } | ||
| } | ||
|
|
||
| // On G0 and U0 enabled channels are sampled from 0 to last channel. | ||
| // It is possible to add up to 8 sequences if CHSELRMOD = 1. | ||
| // However for supporting more than 8 channels alternative CHSELRMOD = 0 approach is used. | ||
| #[cfg(adc_u0)] | ||
| T::regs().chselr().modify(|reg| { | ||
| reg.set_chsel(channel_mask); | ||
| }); | ||
| } | ||
| // Set continuous mode with Circular dma. | ||
| // Clear overrun flag before starting transfer. | ||
| T::regs().isr().modify(|reg| { | ||
| reg.set_ovr(true); | ||
| }); | ||
|
|
||
| #[cfg(not(any(adc_g0, adc_u0)))] | ||
| T::regs().cfgr().modify(|reg| { | ||
| reg.set_discen(false); | ||
| reg.set_cont(true); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using continuous and circular mode means you sample as fast as possible for the ADC. This means that the DMA buffer fills up really quickly and care must be taken when setting up the task that reads from the buffer. As an alternative approach I think you should consider using triggered measurements to control the rate at which new samples are taken. I have started working on this for the g4 series #4786 but it would be good to develop similar APIs for all versions in the long run. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, getting a fast as possible buffer was the point. in my case I needed to sample 4 analog inputs as fast as possible and only pass to a different task samples that pass some threshold. as such, by setting the sequence to the highest sample time I was able to get super fast samples without needing to retrigger the ADC, essentially acting as a clock source for the audio pipeline while the device I used, stm32l412kb does not support changing the clock to the ADC, other devices do. alternatively you can also drive the conversion starts from a timer, as such this will ensure the ADC is always ready to provide samples and the relevant task will be awaken every time the DMA buffer is half full. |
||
| reg.set_dmacfg(Dmacfg::CIRCULAR); | ||
| reg.set_dmaen(true); | ||
| }); | ||
| #[cfg(any(adc_g0, adc_u0))] | ||
| T::regs().cfgr1().modify(|reg| { | ||
| reg.set_discen(false); | ||
| reg.set_cont(true); | ||
| reg.set_dmacfg(Dmacfg::CIRCULAR); | ||
| reg.set_dmaen(true); | ||
| }); | ||
|
|
||
| RingBufferedAdc::new(dma, dma_buf) | ||
| } | ||
|
|
||
| #[cfg(not(adc_g0))] | ||
| fn configure_channel(channel: &mut impl AdcChannel<T>, sample_time: SampleTime) { | ||
| // RM0492, RM0481, etc. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| #![no_std] | ||
| #![no_main] | ||
|
|
||
| use defmt::*; | ||
| use embassy_executor::Spawner; | ||
| use embassy_stm32::Config; | ||
| use embassy_stm32::adc::{Adc, AdcChannel, SampleTime}; | ||
| use {defmt_rtt as _, panic_probe as _}; | ||
|
|
||
| const DMA_BUF_LEN: usize = 512; | ||
|
|
||
| #[embassy_executor::main] | ||
| async fn main(_spawner: Spawner) { | ||
| info!("Hello World!"); | ||
|
|
||
| let mut config = Config::default(); | ||
| { | ||
| use embassy_stm32::rcc::*; | ||
| config.rcc.mux.adcsel = mux::Adcsel::SYS; | ||
| } | ||
| let p = embassy_stm32::init(config); | ||
|
|
||
| let mut adc = Adc::new(p.ADC1); | ||
| let mut adc_pin0 = p.PA0.degrade_adc(); | ||
| let mut adc_pin1 = p.PA1.degrade_adc(); | ||
| let mut adc_dma_buf = [0u16; DMA_BUF_LEN]; | ||
| let mut measurements = [0u16; DMA_BUF_LEN / 2]; | ||
| let mut ring_buffered_adc = adc.into_ring_buffered( | ||
| p.DMA1_CH1, | ||
| &mut adc_dma_buf, | ||
| [ | ||
| (&mut adc_pin0, SampleTime::CYCLES640_5), | ||
| (&mut adc_pin1, SampleTime::CYCLES640_5), | ||
| ] | ||
| .into_iter(), | ||
| ); | ||
|
|
||
| info!("starting measurement loop"); | ||
| loop { | ||
| match ring_buffered_adc.read(&mut measurements).await { | ||
| Ok(_) => { | ||
| //note: originally there was a print here showing all the samples, | ||
| //but even that takes too much time and would cause adc overruns | ||
| info!("adc1 first 10 samples: {}", measurements[0..10]); | ||
| } | ||
| Err(e) => { | ||
| warn!("Error: {:?}", e); | ||
| } | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.