cancel
Showing results for 
Search instead for 
Did you mean: 

Bare metal timer-triggered multi-channel ADC conversion using DMA (STM32H745)

Hansel
Senior

Try as I might, I cannot get timer-triggered ADC conversions of three channels working using the DMA. I've managed to get it to run for a single channel and using ADC triggered interrupts (no DMA). While adapting my code to work with DMA, I leaned on the code shown here. I've also read this knowledge base article regarding the DMA not working in relation with memory/caching.

I think the configurations for the timer and the ADC are correct because if I remove the following line, my ADC overrun interrupt routine gets triggered:

ADC->CFGR = ADC->CFGR | ADC_CFGR_AUTDLY;

To work around the issue described in the knowledge base article, I turn off cacheability of the memory that I want to address via the DMA as follows:

void MPUConfig(void) {
 
	// Refer to application note AN4838
 
	MPU_Region_InitTypeDef MPU_InitStruct;
 
	/* Disable MPU */
	HAL_MPU_Disable();
 
	// Configure first 64kB of SRAM1 as non-cacheable
	MPU_InitStruct.Enable = MPU_REGION_ENABLE;
	MPU_InitStruct.BaseAddress = 0x30000000;   // SRAM1
	MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
	MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
	MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
	MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
	MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
	MPU_InitStruct.Number = MPU_REGION_NUMBER0;
	MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
	MPU_InitStruct.SubRegionDisable = 0x00;
	MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
	HAL_MPU_ConfigRegion(&MPU_InitStruct);
 
	/* Enable MPU */
	HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

5 REPLIES 5
Hansel
Senior

As I couldn't put everything in my first post, I continue here.

The configuration of the timer, the ADC and DMA are done with this code (sorry for the ugly formatting, but that's forced by this web page here. If you copy it in a text editor, it'll look nice and neat).

;       /*********************\
	|   Configure timer   |
	\*********************/
 
	// List of TIM6 registers, see Section 40.4 in reference manual RM0399
 
	TIM_TypeDef *TIM;
 
	TIM = TIM6;
 
	// 1.  TURN ON THE CLOCK FOR TIMER 6
    __HAL_RCC_TIM6_CLK_ENABLE();
 
    // 2.  BASE INITIALIZATION
    TIM->CR1  = (TIM->CR1 & ~(TIM_CR1_DIR | TIM_CR1_CMS)) | TIM_COUNTERMODE_UP;  // Configure counter mode
    TIM->CR1  = (TIM->CR1 & ~TIM_CR1_CKD) | TIM_CLOCKDIVISION_DIV1;              // Set clock division
    TIM->CR1  = (TIM->CR1 & ~TIM_CR1_ARPE) | TIM_AUTORELOAD_PRELOAD_ENABLE;      // Enable reload
    TIM->ARR  = 60000;                                                           // Set autoload value for a period of 250 µs
    TIM->PSC  = 0;                                                               // Set prescaler
    TIM->RCR  = 0;                                                               // Set repetition counter
    TIM->EGR  = TIM_EGR_UG;                                                      // Generate update event to reload prescaler and repetition counter
    TIM->SR   = 0;                                                               // Clear status update caused by UG
 
    // 3.  CONFIGURE CLOCK SOURCE FOR INTERNAL CLOCK
    TIM->SMCR = TIM->SMCR & ~(TIM_SMCR_SMS | TIM_SMCR_TS | TIM_SMCR_ETF |        // Reset SMS, TS, ETF, ETPS, ECE, and ETP
    		                  TIM_SMCR_ETPS | TIM_SMCR_ECE | TIM_SMCR_ETP);
 
    // 4.  CONFIGURE TIMER IN MASTER MODE
    TIM->CR2  = (TIM->CR2 & ~TIM_CR2_MMS) | TIM_TRGO_UPDATE;                     // Update event causes trigger output
    TIM->SMCR = (TIM->SMCR & ~TIM_SMCR_MSM) | TIM_MASTERSLAVEMODE_DISABLE;       // Reset master/slave mode
 
 
#ifdef TIMER_DEBUG
    // For debugging purposes:
	TIM->DIER |= TIM_DIER_UIE;  // Call TIM6_DAC_IRQHandler() on interrupt
    HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 10, 0);
    NVIC_EnableIRQ(TIM6_DAC_IRQn);
#endif
 
 
 
	/*******************\
	|   Configure pins  |
	\*******************/
 
    __HAL_RCC_GPIOH_CLK_ENABLE();
 
     GPIO_InitTypeDef GPIO_InitStruct = {0};
     GPIO_InitStruct.Mode             = GPIO_MODE_ANALOG;      // Set input analog
     GPIO_InitStruct.Pull             = GPIO_NOPULL;           // No pull-up or pull-down
 
     GPIO_InitStruct.Pin              = GPIO_PIN_2;            // PH2 --> ADC3, INP13
     HAL_GPIO_Init(GPIOH, &GPIO_InitStruct);
     GPIO_InitStruct.Pin              = GPIO_PIN_3;            // PH3 --> ADC3, INP14
     HAL_GPIO_Init(GPIOH, &GPIO_InitStruct);
     GPIO_InitStruct.Pin              = GPIO_PIN_4;            // PH3 --> ADC3, INP15
     HAL_GPIO_Init(GPIOH, &GPIO_InitStruct);
 
 
 
	/*******************\
	|   Configure ADC   |
	\*******************/
 
#define ADC_ISR_LDORDY (1UL << 12)
 
	ADC_TypeDef *ADC;
	ADC_Common_TypeDef *ADCCommon;
	ADC = ADC3;
	ADCCommon = ADC3_COMMON;
 
	// Turn on the clock for the ADC and for the pin
    __HAL_RCC_ADC3_CLK_ENABLE();
 
    // Configure interrupt for ADC overruns
    ADC3->IER |= ADC_IER_OVRIE;
    NVIC_EnableIRQ(ADC3_IRQn);
 
 
    // Set common clock
    ADCCommon->CCR = (ADCCommon->CCR & ~(ADC_CCR_CKMODE | ADC_CCR_PRESC)) | ADC_CCR_CKMODE; // Set clock to hclk/4
 
	// RM0399, Section 26.4.6: By default, the ADC is in deep power-down mode where its supply is internally switched off to reduce
	// the leakage currents (the reset state of bit DEEPPWD is 1 in the ADC_CR register). To start ADC operations, it is first
	// required to exit deep power-down mode by clearing bit DEEPPWD.
	if(ADC->CR & (ADC_CR_DEEPPWD)) { // Check if in deep power-down mode
		ADC->CR = ADC->CR & ~ADC_CR_DEEPPWD; // Clear deep power-down bit
	}
 
	// RM0399, Section 26.4.6: Next it is required to enable the ADC internal voltage regulator by setting the bit
	// ADVREGEN=1 in the ADC_CR register.
	if(!(ADC->CR & ADC_CR_ADVREGEN)) { // Check if voltage regulator is already on
		ADC->CR = ADC->CR | ADC_CR_ADVREGEN; // Enable ADC voltage regulator
	}
 
	// Instead of above instructions, check if LDO is ready
	while(!(ADC->ISR & ADC_ISR_LDORDY));
 
    // Run calibration (offset and linearity) for single-ended mode
    // RM0399, Section 26.4.8: Software procedure to calibrate the ADC
    ADC->CR = (ADC->CR & ~ADC_CR_ADCALDIF) | ADC_CR_ADCALLIN;            // ADCALDIF = 0, ADCALLIN = 1;
    ADC->CR = ADC->CR | ADC_CR_ADCAL;                                    // Run calibration
    while(ADC->CR & ADC_CR_ADCAL) {}                                     // Wait for calibration to finish
 
    // Set boost mode to 0x11 as the ADC is expected to run at 50 MHz
    ADC->CR = ADC->CR | ADC_CR_BOOST_1 | ADC_CR_BOOST_0;
 
    // Enable ADC according to Section 26.4.9
    ADC->ISR = ADC->ISR | ADC_ISR_ADRDY;                                 // Clear ADRDY by writing a 1
    ADC->CR = ADC->CR | ADC_CR_ADEN;                                     // Enable ADC
    while(!(ADC->ISR & ADC_ISR_ADRDY));                                  // Wait until ADC is ready
    // Now that the ADC is enabled, control bits can be written (as long as no conversion is running).
 
    // Not sure if this is needed:
	// ADC3->ISR |= ADC_ISR_ADRDY;
    // ADC3->CR &= ~ADC_CR_ADSTART;
 
 
    // Configure channel scanning
    ADC->PCSEL = ADC->PCSEL | ADC_PCSEL_PCSEL_13;                        // Select channel 13 (INP13 of ADC3)
    ADC->PCSEL = ADC->PCSEL | ADC_PCSEL_PCSEL_14;                        // Select channel 14 (INP14 of ADC3)
    ADC->PCSEL = ADC->PCSEL | ADC_PCSEL_PCSEL_15;                        // Select channel 15 (INP15 of ADC3)
    ADC->SQR1  = ADC->SQR1  | (2 << 0);                                  // Number of channels in sequencer:  3
    ADC->SQR1  = ADC->SQR1  | (13 << 6);                                 // First channel to be converted:   13
    ADC->SQR1  = ADC->SQR1  | (14 << 12);                                // Second channel to be converted:  14
    ADC->SQR1  = ADC->SQR1  | (15 << 18);                                // Third channel to be converted:   15
 
    // Configure sampling times
    ADC->SMPR2 = (ADC->SMPR2 & ~(0b111 << 9)) | (0b000 << 9);            // 1.5 ADC clock cycles for channel 13
    ADC->SMPR2 = (ADC->SMPR2 & ~(0b111 << 12)) | (0b000 << 12);          // 1.5 ADC clock cycles for channel 14
    ADC->SMPR2 = (ADC->SMPR2 & ~(0b111 << 15)) | (0b000 << 15);          // 1.5 ADC clock cycles for channel 15
 
    // Miscellaneous settings
    ADC->CFGR  = ADC->CFGR | ADC_CFGR_EXTEN_0;                           // Turn on hardware trigger, rising edge
    ADC->CFGR  = ADC->CFGR | ADC_CFGR_AUTDLY;                            // Not sure if/why this is needed.
    ADC->CFGR  = ADC->CFGR & ~ADC_CFGR_CONT;                             // Turn off continuous mode
    ADC->CFGR  = ADC->CFGR | (13 << ADC_CFGR_EXTSEL_Pos);                // Use adc_ext_trg13 as shown in Table 212 of RM0399
    ADC->CFGR  = ADC->CFGR | ADC_CFGR_DMNGT_1 | ADC_CFGR_DMNGT_0;        // Set DMA circular mode (?)
 
 
 
 
	/*******************\
	|   Configure DMA   |
	\*******************/
 
    uint16_t *val = (uint16_t*) 0x30000000;   // This sits in SRAM1
 
    __HAL_RCC_D2SRAM1_CLK_ENABLE();                                      // Enable SRAM1
    __HAL_RCC_DMA1_CLK_ENABLE();                                         // Turn on the clock for the DMA
 
 
    // See Chapter 16.3.19 in RM0399 for the order of settings to be done
 
    DMA1_Stream0->CR &= ~DMA_SxCR_EN;                                    // Turn off DMA controller
    while(DMA1_Stream0->CR & DMA_SxCR_EN) {}                             // Wait until DMA is turned off
 
    DMA1_Stream0->CR |= DMA_SxCR_TEIE | DMA_SxCR_TCIE;                   // Enable interrupts for transfer errors and transfer completion
    NVIC_EnableIRQ(DMA1_Stream0_IRQn);
 
    DMA1_Stream0->PAR = (uint32_t)  &ADC3->DR;                           // ADC data register as peripheral input
    DMA1_Stream0->M0AR = (uint32_t) val;                                 // Absolute memory location for output --> SRAM1
 
    DMA1_Stream0->NDTR = 3;                                              // Configure for three items to be transfered
 
    DMAMUX1_Channel0->CCR = 115;                                         // ADC3 DMA (See Table 125 in RM0399)
 
    DMA1_Stream0->CR |= DMA_SxCR_PL_0 | DMA_SxCR_PL_1;                   // Highest priority setting
 
    DMA1_Stream0->CR |= DMA_SxCR_CIRC;                                   // Set up for circular mode
    DMA1_Stream0->CR |= ~DMA_SxCR_PSIZE_1 | DMA_SxCR_PSIZE_0;            // 2 bytes peripheral data size
    DMA1_Stream0->CR |= ~DMA_SxCR_PINC;                                  // No memory increments
    DMA1_Stream0->CR |= ~DMA_SxCR_MSIZE_1 | DMA_SxCR_MSIZE_0;            // 2 bytes memory size
    DMA1_Stream0->CR |= DMA_SxCR_MINC;                                   // Memory increments
 
 
 
	/*****************************\
	|   Start the process chain   |
	\*****************************/
 
    // ENABLE DMA
    DMA1_Stream0->CR |= DMA_SxCR_EN;
 
    // START ADC
    ADC->CR = ADC->CR | ADC_CR_ADSTART;
 
    // ENABLE TIMER
	TIM->CR1 = TIM->CR1 | TIM_CR1_CEN;

Hansel
Senior

And my interrupt routines look like shown below. When setting a breakpoint inside the stream interrupt handler, it never gets triggered. Can someone shed some light on what I might be doing wrong?

// For debugging purposes
 
void TIM6_DAC_IRQHandler(void) {
	// HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_RESET);
	if(TIM2->SR & TIM_SR_UIF) {
		TIM2->SR &= ~TIM_SR_UIF;
	}
}
 
// Temporary to see if there is an overrun
 
volatile uint16_t adc_flag;
 
void ADC3_IRQHandler(void){
    if(ADC3->ISR & ADC_ISR_OVR){
        ADC3->ISR |= ADC_ISR_OVR;
        adc_flag = 1;
    }
}
 
 
volatile uint16_t dma_flag;
 
void DMA1_Stream0_IRQHandler(void) {
	HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_RESET);
    if(DMA1->LISR & DMA_LISR_TEIF0){
        DMA1->LIFCR |= DMA_LIFCR_CTEIF0;
        dma_flag = 1;
    }
 
    if(DMA1->LISR & DMA_LISR_TCIF0){
        DMA1->LIFCR |= DMA_LIFCR_CTCIF0;
        dma_flag = 2;
    }
}

    DMA1_Stream0->CR |= ~DMA_SxCR_PSIZE_1 | DMA_SxCR_PSIZE_0;            // 2 bytes peripheral data size
    DMA1_Stream0->CR |= ~DMA_SxCR_PINC;                                  // No memory increments
    DMA1_Stream0->CR |= ~DMA_SxCR_MSIZE_1 | DMA_SxCR_MSIZE_0;            // 2 bytes memory size

These lines don't do what you think they do...

You are correct. I don't know why I didn't see this myself. Thanks for the hint. Since the corresponding bits are set to zero on MCU bringup anyways, I just changed this to the following:

DMA1_Stream0->CR |= DMA_SxCR_PSIZE_0;
DMA1_Stream0->CR |= DMA_SxCR_MSIZE_0;

Still not working though. Need to keep debugging...

KiptonM
Lead

I was having the same issue.

It was working with a SW ADC start in the TIM3 interrupt. But I wanted it to start automatically with a HW trigger. In case there was a delay in executing the ISR.

I found this to be helpful.

https://www.youtube.com/watch?v=Yt5cHkmtqlA&t=39s

Once I made the changes he did in the configuration program, my code worked without even having to try his code. (I had a different processor, but made the changes that did the same thing.) I am making it run 10,000 times a second or every 100 us.