Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions embassy-stm32/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- chore: Updated stm32-metapac and stm32-data dependencies
- feat: stm32/adc/v3: allow DMA reads to loop through enable channels
- fix: Fix XSPI not disabling alternate bytes when they were previously enabled
- feat: stm32/adc/v3: added support for Continuous DMA configuration
- fix: Fix stm32h7rs init when using external flash via XSPI
- feat: Add Adc::new_with_clock() to configure analog clock
- feat: Add GPDMA linked-list + ringbuffer support ([#3923](https://github.com/embassy-rs/embassy/pull/3923))
Expand Down
180 changes: 180 additions & 0 deletions embassy-stm32/src/adc/ringbuffered_v3.rs
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>();
}
}
138 changes: 138 additions & 0 deletions embassy-stm32/src/adc/v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, getting a fast as possible buffer was the point.
this is intended for audio applications where you want a constant stream of samples

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.
Expand Down
51 changes: 51 additions & 0 deletions examples/stm32l4/src/bin/adc_dma.rs
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);
}
}
}
}