Frank's Random Wanderings

STM32F2xx DMA Controllers Part 2

The discussion around the STM32F2xx and STM32F4xx DMA controllers is becoming a bit too large for a single posting, so here we are with Part 2!

Clearing a DMA Interrupt

In theory this is very simple – in practice not so much. The documentation for the “DMA low interrupt flag clear register” (DMA_LIFCR) and the “DMA high interrupt flag clear register” (DMA_HIFCR) shows bits, such as CTCIFx: Stream x clear transfer complete interrupt flag (x = 3..0). The documentation says to write a “1” to the bit to clear the interrupt flag. That is correct.

However, I was finding that in certain circumstances the interrupt kept on reocurring, immediately, even though I was clearing out the interrupt.

DMA Interrupt Re-entering Reason #1

There is a well-known reason for this. The Cortex-M3 / M4 is a pipelined processor. If you clear the interrupt at the very bottom of your interrupt service routine code, immediately before you exit your interrupt routine, the clearing of the interrupt might not have propagated through the processor before your interrupt routine exited, meaning that the interrupt will be immediately triggered again.

It’s important to ensure the interrupt clear has actually happened before you exit your interrupt code. The easy way to do this is to clear the interrupt at the very top of your interrupt handler, before you do anything else. Then the time taken by executing your interrupt handler code allows lots of time for the interrupt clear to propagate. If this isn’t possible, or if your interrupt handler is extremely short, write to the interrupt clear register, then do a while() loop on the interrupt flag register to wait until the clear happens. Note that if you do this, make sure you put a timeout of some description in your while() loop to prevent a hangup. The interrupt clear should normally take effect very quickly.

DMA Interrupt Re-entering Reason #2

My interrupt re-entering was not caused by #1 above, and I spent a frustrating day hunting down the reason, finally discovering reason #2.

My interrupt handler code looked generally like this:

void DMA2_Stream1_IRQHandler (void)
{
  DMA2->LIFCR = (uint32_t)0x00000F40;      // clear DMA IRQ flags

  ... do a bunch of stuff ....

  // setup the DMA for a new transfer - DMA currently on so turn it off first
  DMA2->S1CR = (uint32_t)0x02025410;      // need to turn off DMA first to change settings
  while (DMA2->S1CR & DMA_CR_EN);         // wait until DMA is actually off

  DMA2->S1M0AR = ...     
  DMA2->S1NDTR = ...     
  ...
  DMA2->S1CR = (uint32_t)0x02065510;      // everything setup but not enabled     
  DMA2->LIFCR = (uint32_t)0x00000F40;     // clear any pending (old) DMA2 Stream 1 interrupts
  DMA2->S1CR = (uint32_t)0x02065511;      // everything setup and enabled   
}

You can see two interrupt clears taking place. One at the top of the interrupt handler (as it should be), and a second clear shortly before I re-enable the DMA controller, to ensure nothing is pending when the DMA is started up again.

Remember too, that the STM32F2xx / STM32F4xx DMA won’t even start if it has a pending interrupt (see STM32F2xx DMA Controllers Part 1) so clearing the interrupt flags is not optional – it must be done (even if you’re not using interrupts).

This second clear is not necessarily essential – it’s just for my own peace of mind, to be sure the DMA will start up correctly.

The reason the interrupt immediately re-enters is here:

// setup the DMA for a new transfer – DMA currently on so turn it off first
DMA2->S1CR = (uint32_t)0x02025410; // need to turn off DMA first to change settings

In this example the DMA is currently enabled, due to it being in a double-buffer / circular mode. I turn off the DMA. This results in the DMA TCIF flag being set. Again, so to speak – the TCIF was set earlier which triggered this interrupt, I cleared the flag at the top of the interrupt handler, and now that I’m turning off the DMA the TCIF flag has become set a second time. In the DMA configuration register bit 4, “TCIE: Transfer complete interrupt enable” is set, meaning that when the TCIF flag becomes set, it’s passed on to the NVIC (the Cortex-M3 / M4 Nested Vectored Interrupt Controller).

Hence at this point, even though the STM32F2xx / STM32F4xx DMA has been turned off, the simple act of turning it off has caused a new DMA interrupt to be pending in the NVIC. Because this particular code is in an interrupt service routine, this pending interrupt hasn’t executed yet, but it will the moment we exit our interrupt handler. This is the source of the endlessly reoccuring DMA interrupt.

There are a few possible solutions to this problem. Here’s one:

void DMA2_Stream1_IRQHandler (void)
{
  DMA2->LIFCR = (uint32_t)0x00000F40;      // clear DMA IRQ flags

  ... do a bunch of stuff ....

  // setup the DMA for a new transfer - DMA currently on so turn it off first
  DMA2->S1CR = (uint32_t)0x02025410;      // need to turn off DMA first to change settings
  while (DMA2->S1CR & DMA_CR_EN);         // wait until DMA is actually off

  NVIC_ClearPendingIRQ (DMA2_Stream1_IRQn);    // clear pending DMA IRQ from the NVIC

  DMA2->S1M0AR = ...     
  DMA2->S1NDTR = ...     
  ...
  DMA2->S1CR = (uint32_t)0x02065510;      // everything setup but not enabled     
  DMA2->LIFCR = (uint32_t)0x00000F40;     // clear any pending (old) DMA2 Stream 1 interrupts
  DMA2->S1CR = (uint32_t)0x02065511;      // everything setup and enabled   
}

Using the NVIC_ClearPendingIRQ() function works in this example because we’re in an interrupt service routine, so the new interrupt is pending but hasn’t had the opportunity to execute yet. Using NVIC_ClearPendingIRQ() in non-interrupt code would not work, because the interrupt will trigger the moment the DMA is turned off. In non-interrupt code (ie your regular main code) you need to prevent the interrupt from reaching the NVIC to begin with. Which would mean making sure bit 4 (TCIE: Transfer complete interrupt enable) in the DMA configuration register is clear (zero) before turning off the DMA. Or disabling the DMA interrupt in the NVIC before turning off the DMA. For example:

NVIC_DisableIRQ (DMA2_Stream1_IRQn);

(I personally haven’t tried this one though, because NVIC_ClearPendingIRQ() worked in my case.)

STM32F DMA interrupts can be a source of great confusion – hopefully this post has clarified things a little.

5 thoughts on “STM32F2xx DMA Controllers Part 2

  1. boretsky vladimir

    Hi FRIENDS!

    I am filling that you really know STM32 DMA issues.
    I have the next problem with STM32F072 processor DMA using.
    Short description:

    I am using DMA and USART1 in the ISO7816 mode for smart card communication.
    Smart Card protocol is half duplex. It is mean that RX and TX lines is the same.
    I opened two DMA. One for transmitted from RAM to USART and second for receive from USART to RAM. This concept working very well if both DMA opened simultaneously. But I need to keep long double buffer, because receiving DMA receive transmitted symbols also.
    Then I tried next solution:
    I keep receiving DMA close till transmission complete.
    The transmit finishing for me is DMA_ISR_TCIF bit set and USART_ISR_TC bit set simultaneously.
    Then I open RXNE interrupt of USART. In this interrupt I open receiving pipe and proceed with receiving information process.
    It is working. But my problem is:
    I keep receive interrupt opened but I can’t receive RXNE interrupt second time.
    I fill that DMA keep all USART’s resources.
    Do you know something about it?

  2. Kenny Koller

    I appreciate the posts on DMA. We’re using the STM32F4 and I just ran in to the interrupt-happens-twice issue. In my case I am using CAN and just had some prototype code that simply cleared the CAN TX interrupt by writing a single bit in a CAN register.

    I tried a DSB and it did not work and after some reading I think this article below cleared it up. The DSB only guarantees that the processor write buffer is drained not write buffers for the APB. In my case APB1 (where CAN1 lives) is running four times slower than the core. So basically the interrupt returns before the APB write buffer has acted on the register.

    The article suggests a dummy read from the peripheral to synchronize.

    http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0321a/BIHEADII.html

  3. Captain

    Stumbling in the dark, blind with broken teeth from ST docs, I find your posts and everything works!

  4. Eric

    The problem with re-entering interrupt reason 1 when the clear interrupt is at the end of interrupt is not cause by the pipeline. With the pipeline, the processor fetch then decode next instructions before executing one, but the execution of next instruction always take place after the previous instruction. If you look at assembly code, the end of interrupt is STR Rx,[Rx] to clear the interrupt and next BX LR to exit interrupt routine. The problem is cause by the write buffer of Cortex-M. For each STR instruction, the core put address and data in a buffer and then really do the store in parallel with the execution of next instruction. So the BX LR to exit is executed BEFORE the store to the DMA register is terminated. So the interrupt is not clear when the core re-enable interrupt priority. Reading DMA register stall the core because bus is busy by the store. There is another better solution. It is DSB instruction. This instruction wait for all the stores are finished before executing next instruction. It is more efficeint than reading DMA register. In compiler there is an intrinsic function __DSB() to generate a DSB instruction.

  5. Bernard

    I’m not sure you got the correct explanation with the pipeline thing: the pipeline feeds data or instruction to the MCU, however the interrupts are signaled by the peripherals and don’t go in the pipeline, as I don’t think that write ops to peripherals are cached, this would make writing drivers impossible. IMHO the multiple interrupt problems happens because as soon as an interrupt is disabled, if a new interrupt condition occurs, then the pending interrupt bit is set. As soon as the interrupt can be processed again (ISR has been exited or interrupt is re-enabled), if the pending bit is set, then the ISR fires again. So yes the solution means clearing the pending interrupt bit, but when to do this is particularly important, I think it must be done before processing all the interrupt conditions in a driver and not later, otherwise you may miss an interrupt.

Leave a Reply

Your email address will not be published. Required fields are marked *