STM32 tutorial: Efficiently receive UART data using DMA

U(S)ART peripheral can work very good by using RXNE (Receive Not Empty) for each byte separatelly. In this case, every received byte is manipulated by CPU by jumping to appropriate UART interrupt service routine. To allow CPU to do fully other job when we receive UART data at high speed we can use DMA (Direct Memory Access) to offload CPU. We can think of DMA as co-processor who can only transfer data between different memories, in our case between peripheral data register UART and temporary DMA buffer we assign to it.

In general, before you start DMA, you have to assign number of bytes DMA should transfer before you say Stop, I’m done with transfer. This event is later called Transfer Complete (TC). But we know, in general, UART can receive data at any time. By UART specifications, we don’t know when and how many of bytes will arrive.

Problem

We receive each 5 minutes between 10 and 20 bytes but we don’t know exact number of bytes at a time. We have to set DMA number of bytes to receive before transfer complete notification is met to read the data with CPU. We can set DMA receive to 10 bytes and after again 10 bytes, but if we receive 14 bytes, then we miss 4 bytes. Actually, they will be in buffer but we won’t be notified that DMA has 4 bytes in memory. In this case, we will spend 5 minutes before another packet with data bytes arrives to first flush old data together with 6 bytes of new data. This can lead to Timeouts if our high-level protocol is packet based with command->response approach.

Solution

We can use very useful feature in UART peripheral, called IDLE line detection. Idle line is detected on RX line when there is no received byte for more than 1 byte time length. So, if we receive 10 bytes one after another (no delay), IDLE line is detected after 11th bytes should be received but is not.

We are able to force DMA to call transfer complete interrupt when we disable DMA stream by hand, thus disabling enable bit in stream control register. Then, DMA will make an interrupt if they are enabled and we can read number of bytes received already.

 

If we now do our example again by receiving between 10 and 20 bytes (we receive 14 in this example), we have two options:

  • Set DMA to receive 10 bytes and this will happen
    • When 10th byte is received, DMA will force transfer complete interrupt since all bytes were received (NO IDLE LINE by USART)
    • We will transfer 10 bytes to upper layer buffer and after start DMA receive again for 10 bytes
    • We will only receive 4 more bytes but now IDLE LINE will be detected and we will force DMA to stop at this point. Again, interrupt will be called and we can read data from buffer filled by DMA
  • Set DMA to receive 20 bytes and we will only receive 14 bytes at this time but we will receive IDLE LINE detection and therefore we will force DMA Transfer Complete interrupt.

Source code

To ease and to show what I was talking about, you can find example below. It was tested on STM32F4xx but the concept will work on other STM32 families too.

  • Tested on Nucleo-F411 and Nucleo-F401 with 9600, 115200 and 921600 bauds
  • USART2 is used at pins PA2 and PA3
  • It uses Standard Peripheral Drivers for principe, can easily be ported to HAL
  • Register access is used inside interrupts to manipulate data as fast as possible
  • It uses high level buffer for storing received data and uses DMA RX buffer of smaller size for temporary receive data
  • Code is documented inline

Above code was written using Standard Peripheral Drivers while code below is the same, just written in new LL (Low-Layer) which are already part of STM32Cube package for STM32 microcontrollers and are compatible to be used with HAL drivers together in single project.

Project was initially generated using CubeMX software but later modified to use LL drivers where necessary.

 

tilz0R

Owner of this site. Also electronic enthusiasts, web developer, 3D printer fan, handball player and more. Big fan of STM32F4 devices. In anticipation of the new Discovery board for STM32F7 lines.

You may also like...

Read before commenting!

Before you make a new comment, make sure you agree with things listed below:

  • - Read post to make sure if it is already posted what you are asking for,
  • - Make sure you have the latest version of libraries used in your project,
  • - Make a clean and grammatically correct written message,
  • - Report as many details as possible, including what have you done so far,
  • - Do NOT post any code here. Use Pastebin,
  • - Do NOT post any error codes here. Use Pastebin,
  • - Specify STM32Fxxx family and used Discovery/EVAL/Nucleo or custom made board,
  • - Make sure your clock is set correct for PLL,
  • - If you are using my HAL drivers, please check this post how to start.
Comment will be deleted on breaking these rules without notification!
  • me

    doesn’t work with STM321x as the TC flag isn’t set when you disable the DMA. Secondly there seems to be an idle int after every TX as well, so that needs trapping out to ensure it doesn’t force the rxdma to be aborted.

    • Matthias Lipinsky

      Seems that I am facing the same problem with not raising the TC interrupt after disabling the DMA at a STM32F107VCT6. Is there any solution known?

      TIA

        • Matthias Lipinsky

          The only new information I found at your link was this:
          “I don’t use the ‘F1xx. I looked into RM0008 and – contrary to ‘F4xx – there is no mention that the Transfer Complete interrupt should be thrown upon channel disable, so it appears it works as expected and you have to cope with this in your Software”
          So, in the Manual of the used STM32F107VCT6 I could neither find this mention, that this Interrupt is thrown upon disabling. So, that means, that not every STM32 cpu supports that?

          • Actually, I suggest you fully read the comments. The 3rd example is the best one where you disable option for possible race conditions and lost bytes.

          • Attila

            Hi! Your STM Community link is currently down. Could you please share that 3rd example here for STM32F1xx devices? Thank you!

  • me

    ignore the ‘secondly’ part- that was something else!

  • sameena shaikh

    Hi Tilen,
    How can i receive different data types through uart. I don’t know the no of bytes. . I am receiving data from vectornav through uart to stm32f407 board. The data to be received is in this format.

    $VNYMR,-168.471,-000.723,-179.360,-00.4097,-00.0397,+00.0803,-00.142,-00.068,+10.387,+00.015401,+00.018077,+00.345140*6E
    $VNYMR,-169.199,-000.806,-179.314,-00.4108,-00.0317,+00.0832,-00.084,-00.118,+10.381,+00.024314,+00.061842,+00.724389*64
    $VNYMR,-170.592,-000.813,-179.262,-00.4142,-00.0181,+00.0824,-00.381,+00.543,+10.543,+00.030802,+00.039011,+00.543873*66
    $VNYMR,-171.556,-001.319,-179.189,-00.4118,-00.0147,+00.0788,-00.159,-00.112,+10.346,+00.011041,+00.488687,+00.845943*65

    These are kind of nmea data format

    • What do you mean different data types? Data over UART are bytes only. Post describes how you can do it using DMA.

  • Dawid Kopyś

    Hi,
    I just want to let you know that you should consider adding “volatile” keyword to size_t Write, Read; declaration since you are using those variables in interrupt routines.
    The code you wrote wasn’t working for me till I changed the declaration by adding mentioned “volatile”.
    Anyways, great job man, I would like to be as good embedded programmer as you are someday.
    Cheers.

    • Absolutelly I agree here. It was just to show the concept.

  • Dawid Kopyś

    Hi again, everything works fine on my Nucleo-144 board with STM32F429ZI but it does not for my custom board with STM32F405RGT6 CPU.
    I have changed all necesarry the settings like choosing appropriate USART, appropriate RX and TX pins etc.
    The program still ends up in Infinite Loop in WWDG_IRQHandler (why?).
    I will add that I did not turn on the Watchdog and the program reaches both Interrupt handlers.
    Any suggestions?
    Thanks in advance for your time, keep up the good work.
    #EDIT: The board is working fine with other programs.

    • Cannot say much from this info, but are you sure you have different startup file for F405?

      Are you also sure that dma triggers this irq? To me it sounds like WWDG simply triggers irq.

      • Dawid Kopyś

        Yes, I am sure I have different startup file, I have already checked it around 20 times.
        The code does not enter this Infinite Loop till I trigger USART interrupt by entering a character into terminal. I do not enable the WWDG at all, I have also tried disabling clock for the WWDG but the program gets stuck in the same Infinte Loop as before.
        I am giving up with this I think, there is no point in wasting any more of my or yours time.
        Thanks for trying to help me.
        Cheers.

  • Grigori Timonen

    This approach doesn’t seem to work properly for STM32F1xx.
    I have a device with both F4 and F1. Disabling a DMA stream in the UART IDLE interrupt handler generates a TC interrupt for DMA on F4 MCU, but doesn’t lead to a TC interrupt on F1 MCU. In the idle interrupt handler I can see NDTR register has a proper number of remaining bytes. DMA interrupt is working properly, when I request to transfer only few bytes and send this number of bytes over UART. Has anybody faced same issue?

    As a workaround we can try to move bytes from DMA buffer to task buffer in the IDLE interrupt handler.

    • That’s true. I’ve updated the main article on community.st.com which I wrote some time ago to give you even better solution for all STM32 families.

      • Grigori Timonen

        In my case I have a system with sleeping tasks. They are executed on demand. I’d like to receive data without any polling. The Idea is to receive data into task buffer and then to set execution pending flag in the task scheduler. This should happen in IRQ handler. Your approach requires a continuous polling which I would like to avoid.
        I found NDTR register and DMA buffer contain right data already in UART IDLE interrupt call. So I disable DMA, transfer data from DMA buffer to taks buffer, set huart->RxState to ready instead of busy and call HAL_UART_Receive_DMA() for the next portion of data. This all happens in IDLE interrupt handler. Also I left same old DMA data handling the TC handler. It is supposed to work in case we transfer same amount of data than DMA buffer size.
        This approach _initially_ seems to work.

  • pavithra

    i want to receive gsm response on uart2 of stm32f103c8t6

  • Deniz Can Çığşar

    You do not have to disable DMA, or transfer the data collected by DMA. Just use half transfer and a circular buffer.. DMA stays enabled..