STM32: Problem with DMA while CPU is in sleep

dmastm32

I'm completely baffled as to how to make DMA and sleep mode work together on an STM32F0. It appears when the CPU enters sleep mode via the wfi instruction, DMA stops working. If a busy loop is used instead of wfi, everything works as it should.

The following is a piece of code that illustrates the problem. It uses USART1_TX on pin B6 for logging. TIM1_CH1 output is on pin A8.

#include <stdio.h>
#include <errno.h>
#include <libopencm3/cm3/scb.h>
#include <libopencm3/cm3/nvic.h>
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/stm32/dma.h>
#include <libopencm3/stm32/usart.h>

int _write(int fd, char *buf, int len);
int _write(int fd, char *buf, int len) { // STDOUT -> USART1_TX (blocking)
    if (fd != 1) {
        errno = EIO;
        return -1;
    }
    for (int i = 0; i < len; ++i) usart_send_blocking(USART1, buf[i]);
    return len;
}

void tim1_cc_isr(void) {
    TIM1_SR = ~TIM_SR_CC1IF;
    // Dummy interrupt that wakes up the CPU
}

void main(void) {
#ifdef STM32F1
    RCC_AHBENR = RCC_AHBENR_DMA1EN;
    RCC_APB2ENR = RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_TIM1EN | RCC_APB2ENR_USART1EN;
    GPIOA_CRH   |= 0x000000aa; // A8 (TIM1_CH1), A9 (USART1_TX)
#else
    RCC_AHBENR = RCC_AHBENR_DMAEN | RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOBEN;
    RCC_APB2ENR = RCC_APB2ENR_SYSCFGCOMPEN | RCC_APB2ENR_TIM1EN | RCC_APB2ENR_USART1EN;
#ifdef STM32F030
    GPIOA_AFRL  |= 0x00000100; // A2 (USART1_TX)
    GPIOA_AFRH  |= 0x00000002; // A8 (TIM1_CH1)
    GPIOA_MODER |= 0x00020020; // A2 (USART1_TX), A8 (TIM1_CH1)
#else
    GPIOA_AFRH  |= 0x00000002; // A8 (TIM1_CH1)
    GPIOA_MODER |= 0x00020000; // A8 (TIM1_CH1)
    GPIOB_MODER |= 0x00002000; // B6 (USART1_TX)
#endif
#endif
    nvic_enable_irq(NVIC_TIM1_CC_IRQ);

    TIM1_PSC = 99;
    TIM1_ARR = 9999;
    TIM1_EGR = TIM_EGR_UG;
    TIM1_CR1 = TIM_CR1_CEN;
    TIM1_BDTR = TIM_BDTR_MOE;
    TIM1_CCMR1 = TIM_CCMR1_OC1PE | TIM_CCMR1_OC1M_PWM1;
    TIM1_CCER = TIM_CCER_CC1E;
    TIM1_DIER = TIM_DIER_CC1IE | TIM_DIER_UDE;

    int buf[8] = {1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888};
    DMA1_CPAR(5) = (uint32_t)&TIM1_CCR1;
    DMA1_CMAR(5) = (uint32_t)&buf;
    DMA1_CNDTR(5) = 8;
    DMA1_CCR(5) = DMA_CCR_EN | DMA_CCR_DIR | DMA_CCR_CIRC | DMA_CCR_MINC | DMA_CCR_PSIZE_32BIT | DMA_CCR_MSIZE_32BIT;

    usart_set_baudrate(USART1, 115200);
    usart_set_mode(USART1, USART_MODE_TX);
    usart_enable(USART1);

    for (;;) {
        __asm__("wfi");
        // while (TIM1_CNT);
        printf("%d\n", (int)TIM1_CCR1);
    }
}

The code in a nutshell:

  • TIM1 is free running.
  • DMA channel 5 is configured to constantly update TIM_CCR1 from a circular buffer.
  • The CPU is in sleep mode.
  • A DMA transaction is triggered when TIM1 overflows.
  • The CPU wakes up via a dummy interrupt, prints the contents of TIM_CCR1 and goes back to sleep.

Unexpected output (TIM_CCR1 is not updated):

0
0
0
0
0
0
...

If I comment out the wfi instruction and uncomment the busy waiting, I get the expected results (TIM_CCR1 is updated each time):

1111
2222
3333
4444
5555
6666
7777
8888
1111
2222
3333
4444
5555
6666
7777
8888
...

So basically, I don't understand how I can configure a DMA channel to transfer data to/from a timer while the CPU is in sleep mode. What am I missing?

EDIT:

It appears the problem is heavily chip specific. I updated the code to be able to check other families. It looks like only the STM32F072 is prone to the unexpected results.

Checked MCUs:

  • STM32F030 – ok
  • STM32F051 – ok
  • STM32F072 – FAIL
  • STM32F103 – ok

It's strange to see the STM32F051 to pass because that's the one I came from in the first place. The code snippet works on it though.

If someone could explain why the STM32F072 is so special, it would be great.

Best Answer

Answering my own question. The problem with DMA not working in Sleep mode stems from the fact that DMA needs SRAM and possibly FLASH clocked in Sleep mode. Hence SRAMEN and possibly FLITF in RCC_AHBENR must be set. Both bits are set by default after a reset, but in my code, I simply forgot to set them explicitly in an assignment.

So this problem is NOT chip specific, and different results observed are solely due to subtle differences in run/sleep/run transitions and corner conditions of SRAM clocking between them. Overall, the code has the aforementioned bug that manifests itself as "glitching" DMA.