DEV Community

Hedy
Hedy

Posted on

How to use DMA to BSRR to set/reset leds on STM32 microcontorller?

You can drive LEDs entirely from DMA by streaming 32-bit “commands” into the GPIOx->BSRR register. Each DMA write atomically sets and/or resets pins—no CPU toggling, no read-modify-write.

Core idea

BSRR (bit set/reset register):

  • Bits [15:0] = set pins to 1
  • Bits [31:16] = reset pins to 0

So one 32-bit word can, in the same write, turn some LEDs on and others off.

Example for PA5 (active-high LED):

#define BSRR_SET(pin)   (1u << (pin))          // lower half
#define BSRR_RST(pin)   (1u << ((pin) + 16))   // upper half
uint32_t on  = BSRR_SET(5);     // set PA5 = LED ON
uint32_t off = BSRR_RST(5);     // reset PA5 = LED OFF
Enter fullscreen mode Exit fullscreen mode

What you need

  • A DMA stream configured Memory→Peripheral, destination = &GPIOA->BSRR (32-bit).
  • A trigger to clock out items (usually a timer update DMA request for a steady blink rate; could be any peripheral DMA request).
  • A buffer of 32-bit BSRR words (your LED pattern).

This works across STM32 families (F0/F1/F3/F4/L4/G4/H7/U5, etc.). Always pick the correct DMA request mapping for your timer in the reference manual.

Minimal HAL example (Nucleo-64, LED on PA5)

This streams a simple ON/OFF pattern at a fixed rate using TIM2 update as the DMA trigger. Adjust stream/channel to your MCU’s mapping.

// --- Globals ---
TIM_HandleTypeDef   htim2;
DMA_HandleTypeDef   hdma_tim2_up;

#define LED_PIN 5
static uint32_t bsrr_pattern[] = {
  (1u << LED_PIN),           // ON  (set PA5)
  (1u << (LED_PIN + 16)),    // OFF (reset PA5)
};

// --- GPIO init (PA5 as output) ---
static void MX_GPIOA_Init(void) {
  __HAL_RCC_GPIOA_CLK_ENABLE();
  GPIO_InitTypeDef g = {0};
  g.Pin = GPIO_PIN_5;
  g.Mode = GPIO_MODE_OUTPUT_PP;
  g.Pull = GPIO_NOPULL;
  g.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &g);
}

// --- Timer init (e.g., 5 Hz blink clock) ---
static void MX_TIM2_Init(void) {
  __HAL_RCC_TIM2_CLK_ENABLE();
  htim2.Instance = TIM2;
  htim2.Init.Prescaler     = 8399;  // @84MHz -> 10 kHz
  htim2.Init.CounterMode   = TIM_COUNTERMODE_UP;
  htim2.Init.Period        = 9999;  // 10 kHz/10000 = 1 Hz update
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_Base_Init(&htim2);
}

// --- DMA init (TIM2_UP request -> write to GPIOA->BSRR) ---
static void MX_DMA_Init(void) {
  __HAL_RCC_DMA1_CLK_ENABLE();
  // Choose the proper stream/channel for TIM2_UP per your device RM.
  // Example (varies by MCU!): DMA1 Stream1 Channel3 for TIM2_UP.
  hdma_tim2_up.Instance = DMA1_Stream1;
  hdma_tim2_up.Init.Channel             = DMA_CHANNEL_3;
  hdma_tim2_up.Init.Direction           = DMA_MEMORY_TO_PERIPH;
  hdma_tim2_up.Init.PeriphInc           = DMA_PINC_DISABLE;
  hdma_tim2_up.Init.MemInc              = DMA_MINC_ENABLE;
  hdma_tim2_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;  // 32-bit BSRR
  hdma_tim2_up.Init.MemDataAlignment    = DMA_MDATAALIGN_WORD;
  hdma_tim2_up.Init.Mode                = DMA_CIRCULAR;         // loop pattern
  hdma_tim2_up.Init.Priority            = DMA_PRIORITY_LOW;
  hdma_tim2_up.Init.FIFOMode            = DMA_FIFOMODE_DISABLE;
  HAL_DMA_Init(&hdma_tim2_up);

  // Link DMA to TIM2 update
  __HAL_LINKDMA(&htim2, hdma[TIM_DMA_ID_UPDATE], hdma_tim2_up);
}

// --- Start: stream BSRR words at each TIM2 update ---
void LEDs_DMA_BSRR_Start(void) {
  // Program DMA with src buffer and dest = BSRR
  HAL_DMA_Start(&hdma_tim2_up,
                (uint32_t)bsrr_pattern,
                (uint32_t)&GPIOA->BSRR,
                sizeof(bsrr_pattern)/sizeof(bsrr_pattern[0]));

  // Enable TIM2 Update DMA request
  __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_UPDATE);

  // Run the timer (generates DMA requests)
  HAL_TIM_Base_Start(&htim2);
}

Enter fullscreen mode Exit fullscreen mode

What it does: every TIM2 update event fires one DMA request → DMA writes the next 32-bit word to GPIOA->BSRR → LED toggles. With DMA_CIRCULAR, the pattern loops forever (ON, OFF, ON, OFF…).

Change PSC/ARR to adjust the update rate; add repeated ON/OFF entries in bsrr_pattern to control duty cycle (e.g., 9×ON, 1×OFF = 90% duty blink without PWM).

Multi-LED patterns (set/reset several pins at once)

Build each word as a combination of sets and resets:

// Example on port B: set PB0, PB3; reset PB1
uint32_t word = (1u<<0) | (1u<<3) | (1u<<(1+16));
Enter fullscreen mode Exit fullscreen mode

DMA writes that word → all three actions happen atomically.

Using LL (register-level) instead of HAL (outline)

  1. Configure GPIOx as output; enable clocks.
  2. Configure TIMx PSC/ARR; set TIMx->DIER |= TIM_DIER_UDE (Update DMA enable).
  3. Configure DMA stream:
  • SxPAR = (uint32_t)&GPIOx->BSRR;
  • SxM0AR = (uint32_t)pattern;
  • SxNDTR = pattern_len;
  • SxCR: DIR=Mem2Periph, MSIZE=PSIZE=32-bit, MINC=1, PINC=0, CIRC=1, CHSEL=
  1. Enable DMA stream; TIMx->CR1 |= TIM_CR1_CEN;

Tips, pitfalls & variants

  • Use 32-bit transfers (word). BSRR is 32-bit; byte/halfword writes won’t do what you expect.
  • Don’t write ODR via DMA for toggling—ODR is not atomic (read-modify-write hazards). BSRR is designed for this.
  • Active-low LEDs? Swap ON/OFF words (RESET = LED ON).
  • Glitch-free updates: need to change the pattern while running? Use double buffer (HAL_DMAEx_MultiBufferStart) so you can edit the inactive buffer.
  • Custom pacing: on DMAMUX devices (STM32G4/H7/U5), you can use a Request Generator to pace DMA from a timer, EXTI, or even software events.
  • One-shot events: set NDTR=1 and enable DMA request from your chosen peripheral (e.g., ADC EOC) to “blink on event” by pushing a single BSRR word.

Quick checklist

  • GPIO pin configured as output.
  • Peripheral address = &GPIOx->BSRR, word aligned.
  • DMA Mem→Periph, word size, MINC=ON, PINC=OFF.
  • A valid DMA request source selected (TIMx_UP is common).
  • Circular mode for repeating patterns; Normal for one-shot.

Top comments (0)