Skip to main content
Explorer II
April 16, 2025
Solved

ADC1 not starting conversion with TIM2_TRGO trigger on STM32F407VET6 (bare metal, no HAL)

  • April 16, 2025
  • 1 reply
  • 411 views

I'm working on a bare-metal project using an STM32F407VET6 (generic “black board” from China - https://stm32-base.org/boards/STM32F407VET6-STM32-F4VE-V2.0.html). I'm trying to trigger ADC1 conversions using TIM2_TRGO (timer trigger output), but the ADC only works when using SWSTART. I've done extensive testing and debugging to isolate the problem. Here's what I found:

What works:
ADC1 works with SWSTART: I can read from channel 1 (PA1) manually and see the expected values.

LED debug: PD12 lights up if the ADC reads a value above 1000.

TIM2 generates update events: Confirmed by enabling the TIM2 UIF interrupt and toggling PD12 on each update.

Trigger configuration:

EXTSEL = 4 → TIM2_TRGO

EXTEN = 1 → rising edge

Forced trigger via TIM2->EGR |= TIM_EGR_UG does not trigger the ADC.

VREF+ is connected to 3.3V, and VREF- to GND, with a 100nF decoupling cap.

TIM2_TRGO is confirmed to work via IRQ debug (PD12 blinks).

DMA is configured (when enabled), but never triggers → PD12 never toggles.

 

Debug setup:

I'm using a LED on PD12 to debug, I comment / uncomment the code during the tests.

PA1 is directly connected to a 3.3v source on the board.


PD12: toggles if ADC value > 1000

PD12: toggles in TIM2_IRQHandler() → confirms update event

PD12: toggles in DMA2_Stream0_IRQHandler() → never fires (ADC not triggering)

 

Problem:
ADC1 never starts conversion from external trigger (TRGO), despite all relevant settings and confirmed timer behavior. No conversion ever occurs unless I manually call SWSTART.

 

Am I doing something wrong or is there any undocumented requirement for STM32F4 (F407 specifically) to accept external triggers like TIM2_TRGO?
Are there any known silicon errata or obscure setup steps that could block the trigger path to the ADC?

My code (made with the help of ChatGpt) is the following:

#include "stm32f4xx.h"

void LED_Init(void);
void TIM2_TRGO_Init(void);
void ADC1_Trigger_TIM2_Setup(void);
void DMA2_Stream0_ADC_Config(void);


void LED_Init(void) {
 RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
 GPIOD->MODER |= (1 << (12 * 2));
}

void TIM2_TRGO_Init(void) {
 RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

 TIM2->PSC = 84 - 1; // 1 MHz
 TIM2->ARR = 10000 - 1; // 10ms

 TIM2->EGR |= TIM_EGR_UG; // FORÇA UPDATE ANTES DE CR2
 TIM2->CR2 &= ~TIM_CR2_MMS; // Limpa
 TIM2->CR2 |= TIM_CR2_MMS_1; // MMS = 010 (update event)

 TIM2->CR1 |= TIM_CR1_CEN; // Agora sim liga o timer
 TIM2->DIER |= TIM_DIER_UIE; // Habilita interrupção de update
 NVIC_EnableIRQ(TIM2_IRQn); // NVIC
}

void ADC1_Trigger_TIM2_Setup(void) {
 RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
 GPIOA->MODER |= (3 << (1 * 2)); // PA1 analógico

 ADC1->CR2 = 0;
 ADC1->SQR3 = 1;
 ADC1->SMPR2 |= (7 << 3); // canal 1, 480 ciclos

 ADC1->CR2 |= (4 << ADC_CR2_EXTSEL_Pos); // TIM2_TRGO
 ADC1->CR2 |= (1 << ADC_CR2_EXTEN_Pos); // rising edge
 ADC1->CR2 |= ADC_CR2_ADON;
 NVIC_EnableIRQ(DMA2_Stream0_IRQn); // interrupt
}


int main(void) {
 LED_Init();
 DMA2_Stream0_ADC_Config();
 ADC1_Trigger_TIM2_Setup();
 TIM2_TRGO_Init();

 while (1) {
 if (ADC1->SR & ADC_SR_EOC) {
 uint16_t value = ADC1->DR;

 if (value > 1000)
 GPIOD->ODR |= (1 << 12); // debug: NEVER TOGGLES
 else
 GPIOD->ODR &= ~(1 << 12); // debug: NEVER TOGGLES
 }
 }
}

void TIM2_IRQHandler(void) {
 if (TIM2->SR & TIM_SR_UIF) {
 TIM2->SR &= ~TIM_SR_UIF;

 // DEBUG: Pisca PD12 ao disparar TRGO
 // GPIOD->ODR ^= (1 << 12); // debug: OK
 }
}

void DMA2_Stream0_ADC_Config(void) {
 RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; // Habilita clock do DMA2

 DMA2_Stream0->CR &= ~DMA_SxCR_EN; // Garante que o stream está desligado
 while (DMA2_Stream0->CR & DMA_SxCR_EN); // Espera desativação

 // Limpa flags de interrupção
 DMA2->LIFCR |= DMA_LIFCR_CTCIF0 | DMA_LIFCR_CHTIF0 |
 DMA_LIFCR_CTEIF0 | DMA_LIFCR_CDMEIF0 | DMA_LIFCR_CFEIF0;

 // Configura o endereço do periférico (ADC1_DR)
 DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;

 // Endereço do buffer na memória
 DMA2_Stream0->M0AR = (uint32_t)&adc_buffer[0];

 // Número de transferências
 DMA2_Stream0->NDTR = 1;

 // Configura canal 0 (para ADC1)
 DMA2_Stream0->CR =
 (0 << DMA_SxCR_CHSEL_Pos) | // Canal 0
 (1 << DMA_SxCR_MSIZE_Pos) | // Memória 16 bits
 (1 << DMA_SxCR_PSIZE_Pos) | // Periférico 16 bits
 (1 << DMA_SxCR_MINC_Pos) | // Incrementa memória
 (0 << DMA_SxCR_PINC_Pos) | // Não incrementa periférico
 (0 << DMA_SxCR_CIRC_Pos) | // Sem circular (1 se quiser contínuo)
 (0 << DMA_SxCR_DIR_Pos) | // Periférico -> Memória
 (1 << DMA_SxCR_TCIE_Pos); // Interrupção por transferência completa

 DMA2_Stream0->CR |= DMA_SxCR_EN; // Liga o DMA
}

void DMA2_Stream0_IRQHandler(void) {
 if (DMA2->LISR & DMA_LISR_TCIF0) {
 DMA2->LIFCR |= DMA_LIFCR_CTCIF0;

 // DEBUG: Pisca PD12 quando o DMA completar
 GPIOD->ODR ^= (1 << 12); // debug: NEVER TOGGLES
 }
}

Thanks for the help.

 

    This topic has been closed for replies.
    Best answer by cristianosar

    I've found the problem, the code above was missing 

    DMA2_Stream0_ADC_Config

    but the real problem was me setting the wrong bits of the external trigger:
    changed from:

     
    // ADC1->CR2 |= (4 << ADC_CR2_EXTSEL_Pos); // TIM2_TRGO

    to

    ADC1->CR2 |= (6 << ADC_CR2_EXTSEL_Pos); // TIM2_TRGO
     
    Thanks all for the help. 

    1 reply

    cristianosarAuthorAnswer
    Explorer II
    April 17, 2025

    I've found the problem, the code above was missing 

    DMA2_Stream0_ADC_Config

    but the real problem was me setting the wrong bits of the external trigger:
    changed from:

     
    // ADC1->CR2 |= (4 << ADC_CR2_EXTSEL_Pos); // TIM2_TRGO

    to

    ADC1->CR2 |= (6 << ADC_CR2_EXTSEL_Pos); // TIM2_TRGO
     
    Thanks all for the help.