I'm looking to get my STM32F303 discovery board to continuously read from a temperature sensor using the ADC and DMA transfers to a memory location, but during the interrupt callback, the memory location appears to only have been written to once and is never updated subsequently. In the callback, I also confirm that the value in the ADC's data register is changing.
I'm at a loss as to what the issue here is and could use some pointers in the right direction. I'm using the Knurling app template, RTIC (Real-Time Interrupt-driven Concurrency) and the stm32f3xx-hal crate for this project.
Here's my current source code
#![no_main]
#![no_std]
use cortex_m::{asm, peripheral::NVIC};
use stm32f3xx_hal::{pac::{self, Interrupt}, rcc::Clocks};
use stm32f3xx_hal::prelude::*;
use adc_app as _; // global logger + panicking behavior + memory layout
#[rtic::app(device = pac, peripherals = true)]
mod app {
use super::*;
#[shared]
struct Shared {}
#[local]
struct Local {
adc1: &'static mut pac::adc1::RegisterBlock,
dma1: &'static mut pac::dma1::RegisterBlock,
adc_buffer: [u16; ADC_BUFFER_LENGTH],
}
const ADC1_ADDRESS: u32 = 0x5000_0000;
const ADC1_DR_ADDRESS_OFFSET: u32 = 0x40;
const ADC_BUFFER_LENGTH: usize = 4;
const CLOCK_HZ: u32 = 12_000_000;
const MAX_ADVREGEN_STARTUP_US: u32 = 10;
const SAMPLE_HZ: u32 = 8_000;
const ARR_PERIOD: u32 = CLOCK_HZ / SAMPLE_HZ; // number of periods in SAMPLE_HZ required to auto-reload register (arr)
#[init]
fn init(ctx: init::Context) -> (Shared, Local, init::Monotonics) {
let dp = ctx.device;
init_rcc(&dp.RCC);
let mut flash = dp.FLASH.constrain();
let mut rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.sysclk(CLOCK_HZ.Hz().into()).freeze(&mut flash.acr);
let mut gpioa = dp.GPIOA.split(&mut rcc.ahb);
gpioa.pa0.into_analog(&mut gpioa.moder, &mut gpioa.pupdr);
init_tim2(&dp.TIM2);
init_adc(&dp.ADC1, &dp.ADC1_2, &clocks);
let adc_buffer = init_dma(&dp.DMA1);
(
Shared {},
Local {
adc1: unsafe { &mut *(pac::ADC1::ptr() as *mut _) },
dma1: unsafe { &mut *(pac::DMA1::ptr() as *mut _) },
adc_buffer,
},
init::Monotonics(),
)
}
#[idle]
fn idle(_: idle::Context) -> ! {
loop {}
}
#[task(binds = DMA1_CH1, local = [adc1, dma1, adc_buffer])]
fn dma1_ch1(ctx: dma1_ch1::Context) {
let dma1_ch1::LocalResources { adc1, adc_buffer, dma1, .. } = ctx.local;
let isr = dma1.isr.read();
dma1.ifcr.write(|w| w
.chtif1().clear()
.ctcif1().clear()
.cteif1().clear()
);
let bits = adc1.dr.read().rdata().bits();
// handle interrupt events
if isr.htif1().is_half() {
defmt::info!("interrupt: half: {:?}: {}", adc_buffer, bits);
} else if isr.tcif1().is_complete() {
defmt::info!("interrupt: full: {:?}: {}", adc_buffer, bits);
} else if isr.teif1().is_error() {
// handle dma error
defmt::error!("interrupt: dma transfer error");
} else {
// handle unknown interrupt
defmt::error!("interrupt: unknown error");
}
}
// Initialize all rcc configurations
fn init_rcc(rcc: &pac::RCC) {
// set the division factor of the AHB clock to be SYSCLK / 1
rcc.cfgr.modify(|_, w| w.hpre().div1());
// configure AHB clock
rcc.ahbenr.modify(|_, w| w
.adc12en().enabled() // enable the ADC1/2 clock
.dma1en().enabled() // enable the DMA clock
);
// enable the APB1 clock to the TIM2 timer
rcc.apb1enr.modify(|_, w| w.tim2en().enabled());
}
// Initialize the TIM2 timer
fn init_tim2(tim2: &pac::TIM2) {
// trigger interrupt when counter reaches arr value
tim2.cr2.write(|w| w.mms().update());
// timer period
tim2.arr.write(|w| w.arr().bits(ARR_PERIOD));
// enable TIM2
tim2.cr1.modify(|_, w| w.cen().enabled());
}
// Configure ADC1 on channel 1 to initiate conversion, where
// the timing is connected to the DMA clock TIM2.
//
// T_ADC = num conversions in sequence * (num clock cycles in SMPL + num resolution bits + 0.5) * T_ADC_CLK
// T_External_trigger >= (T_ADC / T_ADC_CLK + 1) * T_ADC_CLK
//
// See section 15.3.10 Constraints when writing the ADC control bits
fn init_adc(adc1: &pac::ADC1, adc1_2: &pac::ADC1_2, clocks: &Clocks) {
// disable ADC prior to configuration / callibration
adc1.cr.modify(|_, w| w.aden().clear_bit());
// ADC clock will be derived from the AHB clock divided by 4
adc1_2.ccr.modify(|_, w| w.ckmode().sync_div4());
// enable the ADC voltage regulator
adc1.cr.modify(|_, w| w.advregen().intermediate());
adc1.cr.modify(|_, w| w.advregen().enabled());
// wait for maximum possible necessary start up time (10us) of ADC voltaget regulator
asm::delay(MAX_ADVREGEN_STARTUP_US * 1_000_000 / clocks.sysclk().0);
// set ADC channel 1 (difsel_10 is intentional) to be in single-ended input mode (uses V_REF- as reference voltage)
adc1.difsel.modify(|_, w| w.difsel_10().clear_bit());
adc1.cr.modify(|_, w| w.adcaldif().single_ended());
// start ADC calibration
adc1.cr.modify(|_, w| w.adcal().calibration());
// wait for ADC calibration to finish
while adc1.cr.read().adcal().is_calibration() {}
// wait 8 * 4 clock cycles of the AHB
asm::delay(8 * 4);
// enable ADC
adc1.cr.modify(|_, w| w.aden().enable());
// wait until ADC is ready
while adc1.isr.read().adrdy().is_not_ready() {}
// set number of conversions per sequence to 1 and the conversion to be from channel 1 (sq1)
adc1.sqr1.modify(|_, w| unsafe { w
.l().bits(0) // set number of conversions per sequence to 1
.sq1().bits(1) // set first conversion to occur on channel 1
});
// set the ADC sampling time to be longer than the required time for a correct sample to
// be taken given an expected external source output impedance
adc1.smpr1.modify(|_, w| w.smp1().bits(0));
adc1.cfgr.modify(|_, w| w
.cont().continuous() // set mode to continuous conversion
.discen().disabled() // disable discontinous mode
.dmacfg().circular() // enable DMA to operate in cicular mode (suitable for streaming input data)
.dmaen().enabled() // enable DMA (transfers will be triggered on each conversion)
.exten().rising_edge() // set conversions to occur on rising edges
.extsel().tim2_trgo() // set conversion to be triggered by TIM2_TRGO event
.res().bits12() // set bit resolution to 12
);
// --- Start continuous ADC conversion ---
// start ADC conversion
adc1.cr.modify(|_, w| w.adstart().start());
}
// Initialize the DMA1 controller
fn init_dma(dma1: &pac::DMA1) -> [u16; ADC_BUFFER_LENGTH] {
let adc_buffer = [0x00_u16; ADC_BUFFER_LENGTH];
// set the source address: buffer address
dma1.ch1.mar.modify(|_, w| unsafe { w.ma().bits(adc_buffer.as_ptr() as u32) });
// set the destintaion address: ADC1 12-bit right-aligned data holding register (ADCx_DR)
dma1.ch1.par.modify(|_, w| unsafe { w.pa().bits(ADC1_ADDRESS + ADC1_DR_ADDRESS_OFFSET) });
// set the number of items to be transferred (item size is configured below)
dma1.ch1.ndtr.modify(|_, w| w.ndt().bits(ADC_BUFFER_LENGTH as u16));
// configure DMA channel to be used as a double buffer
// i.e. while half the buffer is transferred, the other
// half is being filled
dma1.ch1.cr.modify(|_, w| w
.mem2mem().disabled() // channel is memory to peripheral so mem2mem is disabled
.pl().high() // set the channel priority (pl) to high
.msize().bits16() // set memory word size to 16 bits
.psize().bits16() // set peripheral word size to 16 bits
.minc().enabled() // increment memory address after every transfer
.pinc().disabled() // disable peripheral increment mode
.circ().enabled() // dma mode is circular
.dir().from_peripheral() // transfer data from peripheral to memory
.htie().enabled() // enable half transfer interrupts
.tcie().enabled() // enable transfer complete interrupts
.teie().enabled() // enable transfer error interrupts
);
// enable DMA interrupt
unsafe {
NVIC::unmask(Interrupt::DMA1_CH1);
}
// enable DMA channel
dma1.ch1.cr.modify(|_, w| w.en().enabled());
defmt::info!("dma setup: {:?}", adc_buffer);
asm::delay(400);
adc_buffer
}
}
Here's some sample output from this run
0 INFO dma setup: [1044, 1046, 1040, 1041]
└─ main::app::init_dma @ src/bin/main.rs:226
1 INFO interrupt: half: [1039, 1043, 1042, 1045]: 1040
└─ main::app::dma1_ch1 @ src/bin/main.rs:81
2 INFO interrupt: half: [1039, 1043, 1042, 1045]: 1042
└─ main::app::dma1_ch1 @ src/bin/main.rs:81
3 INFO interrupt: half: [1039, 1043, 1042, 1045]: 1041
└─ main::app::dma1_ch1 @ src/bin/main.rs:81
4 INFO interrupt: half: [1039, 1043, 1042, 1045]: 1042