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
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);
}
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));
DMA writes that word → all three actions happen atomically.
Using LL (register-level) instead of HAL (outline)
- Configure GPIOx as output; enable clocks.
- Configure TIMx PSC/ARR; set TIMx->DIER |= TIM_DIER_UDE (Update DMA enable).
- 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=
- 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)