ES1 PRJ2, PRJV & PRJD: STM32F05x microcontroller

Lesson 7: DMA

By Hugo Arends

© 2017 HAN University of Applied Sciences, version 1.0


Direct Memory Access

Direct Memory Access (DMA) is used in order to provide high-speed data transfer between peripherals and memory as well as memory to memory. Data can be moved quickly by DMA without any CPU actions. This keeps CPU resources free for other operations.[1]

Each DMA transfer basically consists of four operations. If, for example, data is transferred from memory to a peripheral (USART) the operations are:

  1. The peripheral initiates an update request indicating it is ready to receive new data.
  2. Set the current load address.
  3. Store data
  1. Set the current store store address
  2. Store data from the load address to the store address
  1. Post-decrement of the countdown counter, which holds the number of transactions that have still to be performed.

The following simplified block diagram shows these steps:

The DMA controller features five independently configurable channels. Each channel can be triggered by a request from a peripheral (TIMx, ADC, DAC, SPI, I2C, and USARTx) or by a trigger from software. The hardware requests from the peripherals are logically ORed before entering the DMA controller. This means that on one channel, only one request must be enabled at a time. The following diagram shows how the requests are mapped to the channels.

Copyright STMicroelectronics (2012). Retrieved from RM0091.

This lesson has four examples to show how DMA can be used. There is one μVision project with four different DMA example targets:

RAM to USART

The goal of this example is to transfer 1 kB of data with DMA from a RAM buffer to the Tx output of the USART. To run the program a level shifter (such as the FT232RL breakout board) must be connected to the Rx and Tx pins of USART1.

After the USART has been set up, as we have seen in lesson 3, DMA must be initialized. The following code is used to achieve this:

    // --------------------------------------------------------------------

    //  Transfer 1 kB with DMA

    // --------------------------------------------------------------------

    // Enable DMA1 peripheral

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    // De-initialize DMA1 Channel 2

    DMA_DeInit(DMA1_Channel2);

    // DMA channel Tx of USART Configuration

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->TDR);

    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)TxBuffer;

    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

    DMA_InitStructure.DMA_BufferSize = (uint16_t)1024;

    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;

    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;

    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;

    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;

    DMA_InitStructure.DMA_Priority = DMA_Priority_High;

    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

    DMA_Init(DMA1_Channel2, &DMA_InitStructure);

   

    // Enable USART DMA interface

    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);

   

    // Clear the Transmission Complete flag

    USART_ClearFlag(USART1, USART_FLAG_TC);

   

    // Enable DMA1 USART Tx Channel

    DMA_Cmd(DMA1_Channel2, ENABLE);

First we make sure the DMA1 peripheral clock is enabled and that DMA channel 2 is de-initialized (registers are set to their default values). Channel 2 is used, because this is the channel that is mapped to USART1_Tx.

Then the DMA controller is initialized by filling the DMA_TypeDef structure parameters. The parameters are described in the comments of the DMA_InitTypeDef in the file stm32f0xx_dma.h:

/**

  * @brief  DMA Init structures definition

  */

typedef struct

{

  uint32_t DMA_PeripheralBaseAddr;

  /*!< Specifies the peripheral base address for DMAy Channelx.

   */

  uint32_t DMA_MemoryBaseAddr;    

  /*!< Specifies the memory base address for DMAy Channelx.

   */

  uint32_t DMA_DIR;                

  /*!< Specifies if the peripheral is the source or destination.

       This parameter can be a value of @ref DMA_data_transfer_direction

   */

  uint32_t DMA_BufferSize;        

  /*!< Specifies the buffer size, in data unit, of the specified Channel.

       The data unit is equal to the configuration set in

       DMA_PeripheralDataSize or DMA_MemoryDataSize members depending in

       the transfer direction

   */

  uint32_t DMA_PeripheralInc;    

  /*!< Specifies whether the Peripheral address register is incremented or

       not.

       This parameter can be a value of @ref

       DMA_peripheral_incremented_mode

   */

  uint32_t DMA_MemoryInc;          

  /*!< Specifies whether the memory address register is incremented or not.

       This parameter can be a value of @ref DMA_memory_incremented_mode

   */

  uint32_t DMA_PeripheralDataSize;

  /*!< Specifies the Peripheral data width.

       This parameter can be a value of @ref DMA_peripheral_data_size

   */

  uint32_t DMA_MemoryDataSize;    

  /*!< Specifies the Memory data width.

       This parameter can be a value of @ref DMA_memory_data_size

   */

  uint32_t DMA_Mode;

  /*!< Specifies the operation mode of the DMAy Channelx.

       This parameter can be a value of @ref DMA_circular_normal_mode

       @note: The circular buffer mode cannot be used if the

              memory-to-memory data transfer is configured on the selected

              Channel

   */

  uint32_t DMA_Priority;

  /*!< Specifies the software priority for the DMAy Channelx.

       This parameter can be a value of @ref DMA_priority_level

   */

  uint32_t DMA_M2M;

  /*!< Specifies if the DMAy Channelx will be used in memory-to-memory

       transfer.

       This parameter can be a value of @ref DMA_memory_to_memory                  

   */

}DMA_InitTypeDef;

Some additional comments:

After initializing the DMA controller, USART1 DMA interface- and USART1 DMA Transmit Request must be enabled. This is done with the USART_DMACmd() function.

After making sure the transmission complete flag is cleared, DMA1 USART Tx channel (= channel 2) is enabled and the data will be transferred.

In the example code, after the DMA transfer has been started, a delay of 0.04 sec. is implemented. After this delay the transmit buffer is changed in 5 places. Only the last three changes will be visible in the output window, because DMA has already transferred approximately 460 bytes .

Assignment

Each channel has several flags that can be checked by software (where x is 1 to 5 for the corresponding channel):

In the example code the following line of code is comment:

  //while(DMA_GetFlagStatus(DMA1_FLAG_TC2) == RESET){;}

Uncomment this line. What do you expect to see in the terminal window?

USART to RAM

The goal of this example is to transfer 10 bytes of data with DMA from USART Rx to a RAM buffer. To run the program a suitable level shifter (such as the FT232RL breakout board) must be connected to USART1 Rx and Tx pins.

The initialisation of the DMA controller and USART1 DMA is very similar to the previous example:

    // --------------------------------------------------------------------

    //  Transfer 10 bytes with DMA

    // --------------------------------------------------------------------

    // Enable DMA1 peripheral

    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    // De-initialize DMA1 Channel 3    

    DMA_DeInit(DMA1_Channel3);

    // DMA channel Rx of USART Configuration

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->RDR);

    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RxBuffer;

    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;

    DMA_InitStructure.DMA_BufferSize = (uint16_t)10;

    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;

    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;

    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;

    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;

    DMA_InitStructure.DMA_Priority = DMA_Priority_High;

    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

    DMA_Init(DMA1_Channel3, &DMA_InitStructure);

   

    // Enable USART DMA interface

    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

   

    // Enable DMA1 USART Rx Channel

    DMA_Cmd(DMA1_Channel3, ENABLE);

   

    // Wait for the USART DMA Rx transfer to complete

    while(DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET){;}

We must now use channel 3, because this channel is mapped to USART1_Rx.

The 10 bytes are placed in a Receive buffer. After all bytes have been received, the Transfer Complete flag of channel 3 will be set. In the example program the received data is displayed in the terminal program.

Assignment

Change this example so it:

Flash to RAM

The goal of this example is to transfer one hundred 32-bit integers from the Flash memory to a RAM buffer. We use an interrupt to get notified if the transfer is complete.

To achieve this, the following global variables will be used:

// Global flag to indicate end of DMA transfer. This flag is set in the

// interrupt handler and tested in the main loop.

// 0 = reset

// 1 = set

extern volatile uint32_t DMA_EndOfTransfer;

// Initialize the flash buffer. Declaring it ‘const’ makes the compiler

// place the code in Flash memory. Start the debugger to see the exact

// address.

const uint32_t FlashBuffer[BUFFER_SIZE] = {0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9,

                                           0,1,2,3,4,5,6,7,8,9};

// Zero initialize the entire RAM buffer.

uint32_t RamBuffer[BUFFER_SIZE] = {0};

Initialization of the DMA controller is very similar to the previous examples.

  // ----------------------------------------------------------------------

  //  Transfer 100 bytes with DMA

  // ----------------------------------------------------------------------

  // Enable DMA1 peripheral

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

 

  // De-initialize DMA1 Channel 1  

  DMA_DeInit(DMA1_Channel1);

 

  // DMA channel Configuration

  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)FlashBuffer;

  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RamBuffer;

  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;

  DMA_InitStructure.DMA_BufferSize = (uint16_t)BUFFER_SIZE;

  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;

  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;

  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;

  DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;

  DMA_InitStructure.DMA_Priority = DMA_Priority_High;

  DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;

  DMA_Init(DMA1_Channel1, &DMA_InitStructure);

 

  // Enable DMA1 Channel1 Transfer Complete interrupt

  DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);

 

  // Enable DMA1 channel1 IRQ Channel

  NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;

  NVIC_InitStructure.NVIC_IRQChannelPriority = 0;

  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

  NVIC_Init(&NVIC_InitStructure);    

  // Enable DMA1 Channel 1

  DMA_Cmd(DMA1_Channel1, ENABLE);

Either channel can be used for a memory-to-memory transfer, we pick channel 1. For source and destination the address must be incremented and the data size is set to word (32-bit). Normal mode is selected and the Memory-To-Memory (M2M) is enabled.

We want an interrupt to be generated when the transfer is complete, so with the function DMA_ITConfig() the DMA_IT_TC is enabled for channel 1. Then also channel 1 IRQ in the NVIC must be enabled.

Finally, the DMA is enabled with the function DMA_Cmd().

In the example program the global variable DMA_EndOfTransfer is polled after enabling the DMA transfer. The flag is set in the interrupt handler (see file stm32f0xx_it.c). Then the contents of the two buffers is compared.

Flash to DAC

The goal of this example is to generate a sine wave with a frequency of 1 kHz on the DAC output pin PA4 like depicted in the picture below:

The sine wave will be implemented using a lookup table of 32 steps:

const uint16_t sine12bit_lut[32] = {

    2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056,

    4095, 4056, 3939, 3750, 3495, 3185, 2831, 2447,

    2047, 1647, 1263,  909,  599,  344,  155,   38,

       0,   38,  155,  344,  599,  909, 1263, 1647};

If we want the sine wave to have a frequency of 1 kHz, the period equals 1 ms. So each of the 32 steps takes 1 ms / 32 = 31.25 us. A timer is very suitable to realize this timing.

The following simplified block diagram shows the interaction between the three peripherals: DAC, TIM3 and DMA.

The timer TIM3 triggers the DAC each 31.25 us. This causes the DHR to be written to the DOR. When this is done, the DAC signals the DMA with an Update request that a new value can be written to the DHR. The DMA sets the load address, loads the data, sets the store address and stores the data. The store address has a fixed value and the load address will be incremented automatically. The DMA controller will do this in a circular mode, so when the 32 values are transferred, the DMA controller will automatically restart at the beginning of the lookup table.

This example uses three functions, each to set up one of the peripherals. In the main routine these functions are called and nothing is done in the main loop, except for blinking an LED.

The first function sets up the DMA controller:

/**

  * @brief  Configures DMA1 channel3

  * @param  None

  * @retval None

  */

void DMA_Setup(void)

{

  DMA_InitTypeDef DMA_InitStructure;

  // ----------------------------------------------------------------------

  //  Setup for continues (circular) transfer of 32 half words (16-bits)

  // ----------------------------------------------------------------------

  // Enable DMA1 peripheral

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

 

  // De-initialize DMA1 Channel 3  

  DMA_DeInit(DMA1_Channel3);

 

  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(DAC->DHR12R1);

  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)sine12bit_lut;

  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

  DMA_InitStructure.DMA_BufferSize = (uint16_t)32;

  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;

  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;

  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;

  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;

  DMA_InitStructure.DMA_Priority = DMA_Priority_High;

  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

  DMA_Init(DMA1_Channel3, &DMA_InitStructure);

 

  // Enable DMA1 Channel 3 (DAC)

  DMA_Cmd(DMA1_Channel3, ENABLE);

}

Channel 3 of the DMA must be configured, because the DAC is used. The DAC is set as the destination and the data size is set to half word (16-bits). Notice that the datatype of the lookup table also is 16-bits. By enabling circular mode we ensure the DMA will restart from the beginning of the lookup table after the last data has been transmitted.

Then channel 1 of the DAC is initialized:

/**

  * @brief  Configures DAC channel 1

  * @param  None

  * @retval None

  */

void DAC_Setup(void)

{

  GPIO_InitTypeDef GPIO_InitStructure;

  DAC_InitTypeDef  DAC_InitStructure;

 

  //(+) Enable DAC APB1 clock to get write access to DAC registers

  //    using RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE)

  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);

  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);

 

  //(+) Configure DAC_OUT1 (DAC_OUT1: PA4) in analog mode

  //    using GPIO_Init() function  

  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_4;

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;

  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

  GPIO_Init(GPIOA, &GPIO_InitStructure);

 

  //(+) Configure the DAC channel using DAC_Init()

  DAC_InitStructure.DAC_Trigger = DAC_Trigger_T3_TRGO;

  DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable;

  DAC_Init(DAC_Channel_1, &DAC_InitStructure);

 

  //(+) Enable the DAC channel using DAC_Cmd()

  DAC_Cmd(DAC_Channel_1, ENABLE);

 

  // Enable DMA for DAC Channel1

  DAC_DMACmd(DAC_Channel_1, ENABLE);

}

The DAC is initialized as we have seen in lesson 5. We make sure the DAC is triggered by timer 3 trigger output (DAC_Trigger_T3_TRGO).

To make DAC channel one generate DMA request we simply need to enable the DMA for DAC channel 1 with the DAC_DMACmd() function.

Finally the timer needs to be initialized.

/**

  * @brief  The desired sine frequency is 1 kHz. This means this timer

  *         must trigger TIM_TRGOSource_Update each 0.001 / 32 = 31.25 us.

  *        

  *         This is accomplished by making the timer count as fast as

  *         possible (no prescaler = SystemCoreClock) and using this

  *         frequency to calculate the period:

  *         period = SystemCoreClock * time between triggers

  * @param  None

  * @retval None

  */

void TIM_Setup(void)

{

  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

  //(#) Enable TIM clock using

  //    RCC_APBxPeriphClockCmd(RCC_APBxPeriph_TIMx, ENABLE) function.

  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

 

  //(#) Fill the TIM_TimeBaseInitStruct with the desired parameters.

  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);

  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

  TIM_TimeBaseStructure.TIM_Period = (SystemCoreClock * 0.00003125) - 1;

  TIM_TimeBaseStructure.TIM_Prescaler = 0;

 

  //(#) Call TIM_TimeBaseInit(TIMx, &TIM_TimeBaseInitStruct) to configure

  //    the Time Base unit with the corresponding configuration.

  TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

  // TIM3 TRGO selection

  TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update);

  //(#) Call the TIM_Cmd(ENABLE) function to enable the TIM counter.

  TIM_Cmd(TIM3, ENABLE);

}

We need to generate a trigger event (TIM_TRGOSource_Update) each 31.25 us. This can simply be done with the Timebase mode (see lesson 4).  If we make the timer count as fast as possible (no prescaler), then the autoreload value (period) must be equal to (SystemCoreClock x 31.25 us) - 1.

Assignment

Change this example so that it generates a sawtooth wave with a frequency of 50Hz and a resolution of 128 steps.


 hugo.arends@han.nl More lessons? Click here!


[1] Ref.: STMicroelectronics (2012), Reference Manual RM0091, [Online publication]