4
\$\begingroup\$

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.

\$\endgroup\$
3
  • \$\begingroup\$ I can't give you an answer -- but these things tend to be very chip specific. I'd give a pretty high probability that the wfi instruction puts the DMA engine to sleep, or some part of the infrastructure that drives it. ST may have made the sleep mode configurable on that chip -- dredge through the technical reference manual and see if you can find a section on that, and if it says anything about DMA. \$\endgroup\$
    – TimWescott
    Commented Aug 8, 2022 at 14:10
  • \$\begingroup\$ That's what it looks like, indeed. The sleep mode doesn't seem to be configurable (or I'm missing something). But the most baffling part is that putting DMA to sleep while the CPU is in sleep mode makes absolutely no sense. Because this is exactly what the sleep mode is for - to save power while hardware does tasks that don't need CPU intervention. Another thing is that it should have been probably written in the RM in bold that power saving is incompatible with DMA, but it's not. \$\endgroup\$
    – neoxic
    Commented Aug 9, 2022 at 0:19
  • \$\begingroup\$ @TimWescott You were right about the issue being very chip specific. I checked other chips/families and updated the question accordingly. \$\endgroup\$
    – neoxic
    Commented Aug 10, 2022 at 5:23

1 Answer 1

4
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.