cancel
Showing results for 
Search instead for 
Did you mean: 

How to setup DMA to GPIO transfer using HAL in the CubeIDE? My goal is to transfer the contents of an integer array (8 bits per value) to 8 GPIO pins.

Broyildiz
Associate II

Using the CubeMX perspective in the CubeIDE (latest version) I configured the DMA2 to work with Timer1 in the memory to peripheral mode.

I am using the STM32 F4 Discovery board (STM32F407VG)

I need a Hello World on how to push one value of an array to the GPIO pins. The GPIO pins used are GPIOE pins 8 through 15.

If the array value is 1 (0xFF), the GPIO pins should have the values "1 1 1 1 1 1 1 1"

Configuration Tim1:

  • clock source: internal
  • counter settings:
  • prescaler: 0
  • period: 11
  • no clock division
  • auto-reload preload disabled

Configuration DMA:

  • DMA peripheral DMA2
  • Stream 5
  • DMA request: TIM1_UP
  • Direction: Memory to Peripheral
  • Priority Very High

The following code was used after initialization of the peripherals (initialization was done using auto generated code):

  1. /*The array filled with zeros*/
  2. #define BUFLEN (512)
  3. uint16_t dmabuf[BUFLEN];
  4.  
  5. HAL_DMA_Init(&hdma_tim1_up);
  6. HAL_DMA_Start_IT(&hdma_tim1_up, (uint32_t)dmabuf, (uint32_t)&GPIOE->ODR, BUFLEN);
  7. /* Enable the TIM Update DMA request */
  8. __HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE);
  9. /* Enable the Peripheral */
  10. __HAL_TIM_ENABLE(&htim1);

The GPIO pins are set high at initialization. When this code runs they should be set low, because the values in the array are all zeros (did not include that code for simplicity).

  

When the code runs, it does execute the functions. It does not throw an error, I checked it with the status return value of the functions. It jumps into the DMA2_Stream5_IRQHandler afterwards, so I'm guessing it did something. The GPIO pins are however never set low.

  

What am I doing wrong?

How do I get the values from an array onto some GPIO pins using the DMA2 peripheral?

Thanks in advance!

1 ACCEPTED SOLUTION

Accepted Solutions

Your help has been amazing @berendi​ .

Below is the code that works and its explanation.

It takes the values inside the array (8 bits per value) and maps them onto the 8 GPIO pins.

#define BUFLEN (512)
uint8_t dmabuf[BUFLEN];
int i;
for(i=0; i<BUFLEN; i++) dmabuf[i] = 0xAB;
 
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_DMA2EN;
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
 
//Set GPIOE8-15 as output
GPIOE->MODER = (GPIOE->MODER & 0x0000FFFF) | 0x55550000;
 
//Set up DMA2 Stream 5.
DMA2_Stream5->NDTR = BUFLEN;
DMA2_Stream5->M0AR = &dmabuf;
GPIOE->ODR = 0x0000;
DMA2_Stream5->PAR = (((uint32_t)0x40021000) | ((uint32_t)0x00000014) | ((uint32_t)0x00000001));
//	DMA2_Stream5->PAR = (uint32_t)&GPIOE->ODR;
DMA2_Stream5->CR =
	  (6u << DMA_SxCR_CHSEL_Pos) | // select channel 6, see DMA request mapping in the reference manual
        // DMA_SxCR_MSIZE_0           | // 0b01:memory data size 16 bit
        // DMA_SxCR_PSIZE_0           | // 0b01:peripheral data size 16 bit
        DMA_SxCR_MINC              | // memory increment
        DMA_SxCR_DIR_0             | // 0b01: memory to peripheral
        DMA_SxCR_EN                | // enable channel
        0;
 
//Now set up the timer
TIM1->ARR = 11 - 1;        // Set to the required period - 1
//TIM1->BDTR = TIM_BDTR_MOE; // required on advanced timers
TIM1->DIER = TIM_DIER_UDE; // enable DMA on timer update (overflow)
TIM1->CR1 = TIM_CR1_CEN;   // start the timer

Code explanation:

Lines 1-4: Define and fill an array. In this case it is filled with the value 0xAB. This way I could verify that each individual GPIO pin had its own value and not some default value.

Lines 6-7: Enable the clocks for GPIO group E, DMA2 and Timer1.

Line 10:

  1. Take the register value of the port mode register (GPIOE->MODER). This register contains which pin is an input, output, etc.
  2. Do an AND operation on the contents of the register with 0x0000FFFF. This way the first 16 bits are cleared (set to 0) and the last 16 bits are set to one.
  3. Do an OR operation on the previous value with 0x55550000. This way the bits we previously cleared are now set (1) and the other set bits are set to 0.

Line 13: Set the Number of Data Register to the amount of data to be send. In this case it is the whole array so it is BUFLEN.

Line 14: Set the Memory 0 Address Register to the address of the array. This address contains the starting point of the data to be sent.

Line 15: As @Community member​  pointed out this register just needs to be set to 0. I tested the code and it still works.

This was the tricky part. In line 15 we set the Output Data Register to 0xFF000000. The values in this register will tell the DMA which pins should receive the output. By setting the values in the register to 0xFF000000 we only select the pins 8-15.

Lines 16-17: The Peripheral Address Register holds the address of the GPIOE->ODR register (which we just configured) . We can set the address in a couple of ways, both are essentially the same:

  1. In line 16 the address is inputted manually. I just happen to know the values. If you want to find the values you will have to dig deep into the Reference Manual.
  2. In line 17 the same is achieved. The address (using the & sign) of the GPIOE->ODR is given to the DMA. (This is the easy/recommended way)

Lines 18-25:

  1. This is where the DMA itself is configured.
  2. In line 19 the DMA channel is selected (by bit shifting)
  3. Lines 20 and 21 are disabled. this way the corresponding register values (Memory Data Size and Peripheral Data Size) are set to 8 bit values.
  4. Line 22 instructs the DMA to send the values of the DMA one by one.
  5. Line 23 instructs the DMA to send the data from the memory to the peripheral (to the GPIO pins).
  6. Line 24 Enables the DMA. This is the last step, because the previous settings could only be done if this part of the register was not enabled (0)
  7. Line 25: I do not know what it does.

Lines 28: Set the period Timer1

Line 29: According to @Community member​  this is only needed when you want to output a waveform using this timer. I removed this line, tested the code and it still works.

Line 30: When Timer1 counts up to its maximum value, send the next DMA value.

Line 31: Start Timer1

Thank you very much @berendi​ .

When I finish the project, i'll probably upload a video to YouTube explaining the VGA signal and i'll post the link here if I do.

View solution in original post

11 REPLIES 11
berendi
Principal

I could never figure it out in HAL, it might depend on the CubeMX / HAL versions, or on some hard to find setting. It is poorly documented.

Using the registers as documented in the reference manual usually works.

RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_DMA2EN;
RCC->APB2EN |= RCC->APB2_TIM1EN;

Set GPIOE8-15 as output

GPIOE->MODER = (GPIOE->MODER & 0x0000FFFF) | 0x55550000;

Set up DMA2 Stream 5.

DMA2_Stream5->NDTR = BUFLEN;
DMA2_Stream5->M0AR = &dmabuf;
DMA2_Stream5->PAR = (uint32_t)&GPIOE->ODR;
DMA2_Stream5->CR =
    (6u << DMA_SxCR_CHSEL_Pos) | // select channel 6, see DMA request mapping in the reference manual
    DMA_SxCR_MSIZE_0           | // 0b01:memory data size 16 bit
    DMA_SxCR_PSIZE_0           | // 0b01:peripheral data size 16 bit
    DMA_SxCR_MINC              | // memory increment
    DMA_SxCR_DIR_0             | // 0b01: memory to peripheral
    DMA_SxCR_EN                | // enable channel
    0;

Now set up the timer

TIM1->ARR = 11 - 1;        // Set to the required period - 1
TIM1->BDTR = TIM_BDTR_MOE; // required on advanced timers
TIM1->DIER = TIM_DIER_UDE; // enable DMA on timer update (overflow)
TIM1->CR1 = TIM_CR1_CEN;   // start the timer

I haven't tested it, but it should work of the F4 series.

The code looks very promising, sadly there is an error.

RCC->APB2EN |= RCC->APB2_TIM1EN;

Two problems:

  1. there is no struct member RCC->APB2EN
    1. CubeIDE does find: RCC->APB2ENR
  2. There is also no RCC->APB2_TIM1EN

Except for this one line it does compile.

You can look up in the reference manual how it is actually called.

I looked it up in the Referance Manual

RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;

It did not work.

I am using the auto generated HAL code to set the clock configuration before any of this code runs.

I do not think that that is the issue, because I setup the clocks using pure RCC code and even then it did not work.

(Thanks for your help so far by the way)

The code so far:

#define BUFLEN (512)
  uint16_t dmabuf[BUFLEN];
  int i;
  for(i=0; i<BUFLEN; i++) dmabuf[i] = 0xFF;
 
  RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_DMA2EN;
  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
 
 
  //Set GPIOE8-15 as output
  GPIOE->MODER = (GPIOE->MODER & 0x0000FFFF) | 0x55550000;
  //Set up DMA2 Stream 5.
  DMA2_Stream5->NDTR = BUFLEN;
  DMA2_Stream5->M0AR = &dmabuf;
  DMA2_Stream5->PAR = (uint32_t)&GPIOE->ODR;
  DMA2_Stream5->CR =
      (6u << DMA_SxCR_CHSEL_Pos) | // select channel 6, see DMA request mapping in the reference manual
      DMA_SxCR_MSIZE_0           | // 0b01:memory data size 16 bit
      DMA_SxCR_PSIZE_0           | // 0b01:peripheral data size 16 bit
      DMA_SxCR_MINC              | // memory increment
      DMA_SxCR_DIR_0             | // 0b01: memory to peripheral
      DMA_SxCR_EN                | // enable channel
      0;
  //Now set up the timer
  TIM1->ARR = 11 - 1;        // Set to the required period - 1
  TIM1->BDTR = TIM_BDTR_MOE; // required on advanced timers
  TIM1->DIER = TIM_DIER_UDE; // enable DMA on timer update (overflow)
  TIM1->CR1 = TIM_CR1_CEN;   // start the timer

Now I have tried it (with your fix) on some real hardware, it works. Timer and DMA are running, and it copies the contents of dmabuf into GPIOE->ODR.

I have just pasted it into the main() function of an empty project. Quite empty, I mean, I have kept only Reset_Handler(), SystemInit(), and the header files.

Try clearing the DMA and timer flag registers before enabling them, or find out where and how HAL is messing them up.

You are correct, it does push the values to the GPIO pins!

But to the wrong pins. The debugger shows me that the values are pushed to GPIO pins 0-7, not to 8-15.

The pins 8-15 are configured as output pins. I tried to change the address of

DMA2_Stream5->PAR = (uint32_t)&GPIOE->ODR;

but that did not help.

Would you mind sharing your code with me (or help me overcome this issue) @berendi​ ?

By the way, the reason I need those specific GPIO pins is because the VGA connector is connected to them (on the pcb to which the Discovery board is connected).

In school we have code that runs in the old Coocox IDE that is able to drive a VGA screen. I'm remaking the code in the CubeIDE for the whole class to use.

As you have defined dmabuf as an array of uint16_t values, I've assumed that you'd like to copy all 16 bits to ODR.

To copy 8 bit values, you have to

  • Change the DMA cortol register to transfer 8 bits instead of 16 bits
  • Adjust the address in the PAR to point to the byte address representing bits 8-15.

Read the DMA chapter in the reference manual.

Your help has been amazing @berendi​ .

Below is the code that works and its explanation.

It takes the values inside the array (8 bits per value) and maps them onto the 8 GPIO pins.

#define BUFLEN (512)
uint8_t dmabuf[BUFLEN];
int i;
for(i=0; i<BUFLEN; i++) dmabuf[i] = 0xAB;
 
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_DMA2EN;
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
 
//Set GPIOE8-15 as output
GPIOE->MODER = (GPIOE->MODER & 0x0000FFFF) | 0x55550000;
 
//Set up DMA2 Stream 5.
DMA2_Stream5->NDTR = BUFLEN;
DMA2_Stream5->M0AR = &dmabuf;
GPIOE->ODR = 0x0000;
DMA2_Stream5->PAR = (((uint32_t)0x40021000) | ((uint32_t)0x00000014) | ((uint32_t)0x00000001));
//	DMA2_Stream5->PAR = (uint32_t)&GPIOE->ODR;
DMA2_Stream5->CR =
	  (6u << DMA_SxCR_CHSEL_Pos) | // select channel 6, see DMA request mapping in the reference manual
        // DMA_SxCR_MSIZE_0           | // 0b01:memory data size 16 bit
        // DMA_SxCR_PSIZE_0           | // 0b01:peripheral data size 16 bit
        DMA_SxCR_MINC              | // memory increment
        DMA_SxCR_DIR_0             | // 0b01: memory to peripheral
        DMA_SxCR_EN                | // enable channel
        0;
 
//Now set up the timer
TIM1->ARR = 11 - 1;        // Set to the required period - 1
//TIM1->BDTR = TIM_BDTR_MOE; // required on advanced timers
TIM1->DIER = TIM_DIER_UDE; // enable DMA on timer update (overflow)
TIM1->CR1 = TIM_CR1_CEN;   // start the timer

Code explanation:

Lines 1-4: Define and fill an array. In this case it is filled with the value 0xAB. This way I could verify that each individual GPIO pin had its own value and not some default value.

Lines 6-7: Enable the clocks for GPIO group E, DMA2 and Timer1.

Line 10:

  1. Take the register value of the port mode register (GPIOE->MODER). This register contains which pin is an input, output, etc.
  2. Do an AND operation on the contents of the register with 0x0000FFFF. This way the first 16 bits are cleared (set to 0) and the last 16 bits are set to one.
  3. Do an OR operation on the previous value with 0x55550000. This way the bits we previously cleared are now set (1) and the other set bits are set to 0.

Line 13: Set the Number of Data Register to the amount of data to be send. In this case it is the whole array so it is BUFLEN.

Line 14: Set the Memory 0 Address Register to the address of the array. This address contains the starting point of the data to be sent.

Line 15: As @Community member​  pointed out this register just needs to be set to 0. I tested the code and it still works.

This was the tricky part. In line 15 we set the Output Data Register to 0xFF000000. The values in this register will tell the DMA which pins should receive the output. By setting the values in the register to 0xFF000000 we only select the pins 8-15.

Lines 16-17: The Peripheral Address Register holds the address of the GPIOE->ODR register (which we just configured) . We can set the address in a couple of ways, both are essentially the same:

  1. In line 16 the address is inputted manually. I just happen to know the values. If you want to find the values you will have to dig deep into the Reference Manual.
  2. In line 17 the same is achieved. The address (using the & sign) of the GPIOE->ODR is given to the DMA. (This is the easy/recommended way)

Lines 18-25:

  1. This is where the DMA itself is configured.
  2. In line 19 the DMA channel is selected (by bit shifting)
  3. Lines 20 and 21 are disabled. this way the corresponding register values (Memory Data Size and Peripheral Data Size) are set to 8 bit values.
  4. Line 22 instructs the DMA to send the values of the DMA one by one.
  5. Line 23 instructs the DMA to send the data from the memory to the peripheral (to the GPIO pins).
  6. Line 24 Enables the DMA. This is the last step, because the previous settings could only be done if this part of the register was not enabled (0)
  7. Line 25: I do not know what it does.

Lines 28: Set the period Timer1

Line 29: According to @Community member​  this is only needed when you want to output a waveform using this timer. I removed this line, tested the code and it still works.

Line 30: When Timer1 counts up to its maximum value, send the next DMA value.

Line 31: Start Timer1

Thank you very much @berendi​ .

When I finish the project, i'll probably upload a video to YouTube explaining the VGA signal and i'll post the link here if I do.

GPIOE->ODR = 0xFF000000;

> Line 15: This was the tricky part. In line 15 we set the Output Data Register to 0xFF000000. The values in this register will tell the DMA which pins should receive

> the output. By setting the values in the register to 0xFF000000 we only select the pins 8-15.

What?

No, ODR is an output register, and it's 16 bit wide, so you simply set it to 0x0000 by this line.

> TIM1->BDTR = TIM_BDTR_MOE; // required on advanced timers

You don't need this unless you want output waveforms on the timer's pins.

JW