cancel
Showing results for 
Search instead for 
Did you mean: 

Nucleo-G431KB help with DMA GPIO Output

etheory
Senior

Hi there!

I've just started working with STM32CubeIDE and after a bit of a vertical learning curve I'm slowly getting to grips with most aspects of it.

I want to DMA to GPIO and there doesn't appear to be any way to do this through the .ioc GUI interface. GPIO ports can't be targeted as a peripheral by the DMA interface, and when you select a MemToMem option in the DMA section, the code that is generated by STM32CubeIDE doesn't have any user sections, and is hence effectively not able to be edited by the user.

The other issue is that the MemToMem DMA option doesn't expose the most useful options like "circular" mode (it only exposes "normal") and doesn't give the option to provide any triggers either.

Have I hit the limits of what STM32CubeIDE is able to do already? or is there some way to achieve this through the STM32CubeIDE interface that I haven't yet seen?

BTW FWIW I'm using the very very latest release of STM32CubeIDE.

Thanks,

Luke

1 ACCEPTED SOLUTION

Accepted Solutions
berendi
Principal

> it's quite frustrating that the interface gets SO CLOSE, but doesn't go the whole way.

Yes, there are reasons why I'm not using it.

> I hope STM intends on extending the functionality rather than leaving it as an intro program to get people into the ecosystem.

I think they should rather fix the supposedly existing functionality first. It's a lot more frustrating when even the basic clock setup is messed up so bad that the MCU locks up solid.

> If someone could please take a look and let me know what I'm doing wrong that would be great.

Sorry, I can't decipher HAL code. If you post the relevant peripheral registers (RCC, DMA, DMAMUX, TIM8, GPIOA), I might be able to notice if something is missing.

> I've stepped through with the debugger and everything gets set up as you'd expect. ​The DMA addresses and modes are correct, everything gets initialized etc. yet nothing seems to happen.

Is the timer running, i.e. CNT value changing? Are there any flags set in the timer or DMA status registers? Can you read back the register values you've set?

> Nothing in the datasheet indicates that this shouldn't work.

Neither the datasheet nor the reference manual documents what HAL functions do, and how are they supposed to be used. The HAL documentation describes a few common use cases, the rest should be treated as undocumented, i.e. even if a functionality seems to work now, it might stop working anytime.

Anyway, here is what I think should work, but I don't have a G4 to test it. I've omitted GPIO pin setup, set them to outputs in GPIOA->MODER.

1. Make sure that all peripherals used (GPIO, TIM, DMA and DMAMUX) are enabled in RCC.

RCC->AHB1ENR |= RCC_AHB1ENR_DMAMUX1EN | RCC_AHB1ENR_DMA1EN;
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
RCC->APB2ENR |= RCC_APB2ENR_TIM8EN;

2. Set up a DMA channel to copy from the array of memory to BSRR.

DMA1_Channel2->CMAR = gpio_data;
DMA1_Channel2->CPAR = &GPIOA->BSRR;
DMA1_Channel2->CNDTR = GPIO_DATA_LENGTH;
DMA1_Channel2->CCR =
    DMA_CCR_MSIZE_1 | // memory data size 32 bits
    DMA_CCR_PSIZE_1 | // peripheral data size 32 bits
    DMA_CCR_CIRC    | // circular mode
    DMA_CCR_MINC    | // increment memory pointer
    DMA_CCR_DIR     | // direction memory to peripheral
    DMA_CCR_EN      | // enable channel
    0);

3. Connect the timer update DMA request to the DMA channel.

DMAMUX1_Channel2->CCR = (DMA_REQUEST_TIM8_UP << DMAMUX_CxCR_DMAREQ_ID_Pos);

4. Set the timer prescaler, period, enable DMA and start.

TIM8->PSC = 1000 - 1;
TIM8->EGR = TIM_EGR_UG; // required to load the prescaler
TIM8->ARR = 17000 - 1;
TIM8->BDTR = TIM_BDTR_MOE; // required on advanced timers
TIM8->SR = 0; // clear status after TIM_EGR_UG
TIM8->DIER = TIM_DIER_UDE; // enable DMA on timer update (overflow)
TIM8->CR1 = TIM_CR1_CEN; /// start the timer

After each step, check that the register values are actually written, that they don't contain anything but the reset values in the datasheet modified with what you've written. Step through the code and check status registers at each step.

View solution in original post

24 REPLIES 24
berendi
Principal

The code generator and its parameters are very poorly documented, so it's hard to even know what it can do, let alone how to do it. If something doesn't work at the first try, then usually the fastest way to solve it is figuring out from the reference manual how do to it using the register interface, which is extensively documented, and does not change unexpectedly at each release.

Find out which DMA controller can do memory to memory transfers at all. There are some STM32 series where one of the bus master ports of DMA1 is not connected to the bus matrix (the main switchboard of the system bus), so only DMA2 can be used for this purpose. See the system architecture diagram at the beginning of chapter 2 in the reference manual.

Each peripheral (with few exceptions) must be enabled in RCC before using it. Find out which bit in the RCC clock enable registers is responsible for the DMA controller, search for DMA1EN or DMA2EN in the RCC chapter of the reference manual. On my STM32L476 board it is RCC->AHB1ENR, so I begin the program with

RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;

The peripheral needs a few cycles to start up, read back the register to pass the time.

RCC->AHB1ENR;

Declare some arrays to copy

#define N 20
uint32_t src[N] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
uint32_t dst[N];

Pick a DMA channel to use. Confusingly enough, they are called streams instead on some series. The reference manual should tell you which way it is. I have channels.

Setting up the source, destination, and number of items to copy is straightforward. It does not matter which address is the source and which is the destination, the direction can be set later.

DMA2_Channel1->CMAR = (uint32_t)src;
DMA2_Channel1->CPAR = (uint32_t)dst;
DMA2_Channel1->CNDTR = N;

Set the channel parameters

	DMA2_Channel1->CCR =
			DMA_CCR_MEM2MEM | // memory to memory mode
			DMA_CCR_MSIZE_1 | // data size in memory is 32 bits
			DMA_CCR_PSIZE_1 | // data size in the peripheral channel is 32 bits
			DMA_CCR_MINC    | // increment the memory pointer
			DMA_CCR_PINC    | // increment the peripheral pinter
			DMA_CCR_DIR     | // direction is memory to peripheral
			DMA_CCR_EN      | // enable the channel
			0;

As the enable bit is set, you can expect dst to contain a copy of src. A well behaving program should of course check the status to see when the transfer is complete

	while(!(DMA2->ISR & DMA_ISR_TCIF1))
		;

clear the flags

	DMA2->IFCR = DMA_IFCR_CGIF1;

and disable the channel

	DMA2_Channel1->CCR = 0;

otherwise the the channel cannot be reprogrammed for the next transfer.

Now do it backwards, reusing the pointers, but with a twist. Clear src[] to be able to see that it is actually filled.

	memset(src, 0, N * sizeof(uint32_t));

Set the number of data items again, because it went to zero during the previous transfer

	DMA2_Channel1->CNDTR = N;

Set up the channel to copy in the opposite direction

	DMA2_Channel1->CCR =
			DMA_CCR_MEM2MEM    | // memory to memory mode
			// DMA_CCR_MSIZE_1 | // data size in memory is 8 bits
			DMA_CCR_PSIZE_1    | // data size in the peripheral channel is 32 bits
			DMA_CCR_MINC       | // increment the memory pointer
			DMA_CCR_PINC       | // increment the peripheral pinter
			// DMA_CCR_DIR     | // direction is peripheral to memory
			DMA_CCR_EN         | // enable the channel
			0;

And wait for the transfer to complete. Of course the program can attend to other stuff while the transfer is ongoing, but expect reduced speed because DMA blocks access to the memory area.

	while(!(DMA2->ISR & DMA_ISR_TCIF1))
		;

Exercise 1: The result in src will be different. Can you find out why? Hint: read the section about programmable data width in the reference manual.

Exercise 2: Instead of copying, fill an array using DMA with a single value, i.e. what memset() does.

etheory
Senior

@berendi​ this is an amazing answer, thank you SO MUCH! It's greatly appreciated.

I'm impressed at how responsive this forum is.

Luckily I'm using the Nucleo-STM32G431KB, so everything is able to connect to everything on this MCU's AHB bus.

I don't have restrictions about which DMA I can use it appears:

0690X00000DXsSgQAL.jpg

I'll have to spend some time reading through the rest of your answer as it is quite extensive, but I did have a LITTLE bit of luck last night trying the following:

1.) Add DMA to TIM6 (which is also controlling a DAC simultaneously):0690X00000DXsclQAD.jpg

2.) Change the setting for the timer DMA in the auto-generated code to use DMA_MEMORY_TO_MEMORY rather than DMA_MEMORY_TO_PERIPHERAL:

0690X00000DXskVQAT.jpg

3.) Start the transfer writing to GPIOA->BSRR:0690X00000DXss0QAD.jpg

And this frankenstein solution, which obvious gets nuked when you regenerate using a new .ioc due to modifying a generated function, ALMOST works.

I get one of the GPIOA pins changing, but at approx 3 times the rate I would expect.

So I think going back to custom functions is the way to go as you've outlined above.

Thanks again.

P.S. it's quite frustrating that the interface gets SO CLOSE, but doesn't go the whole way.

Simply enabling:

1.) Circular DMA mode to the memtomem option.

2.) User-code sections for memtomem transfers selected from the interface.

Would allow so much more flexibility from the interface. I hope STM intends on extending the functionality rather than leaving it as an intro program to get people into the ecosystem. Since even for seasoned programmers a little extra functionality would make it infinitely more usable (and as you mention, more useful and complete documentation).

etheory
Senior

After reading the STM32G4 ​RM0440 guide I found that circular mode doesn't work for memory to memory DMA, which is a shame since I want to output address data for a multiplexer simultaneously to outputting DAC data. Circular mode would have been perfect since the GPIO address sequence is always the same. I'll just have to find another way.

You probably don't want M2M DMA. It differes from M2P and P2M transfers only in that it does ignores any external "trigger" (request) and self-triggers transfers if NDTR is not zero. This is why it does not allow circular transfers - it would flood the buses and probably kill other DMA transfers (pending priority etc.)

Simply use any of M2P or P2M triggered e.g. by timer, and then set the GPIO address as the destination address, i.e. if you chose M->P, set GPIO as peripheral address.

As berendi already hinted, Cube/CubeMX is inevitably constrained only to the "normal" cases, so as soon as you want anything less usual, it starts to get in your way.

JW

etheory
Senior

OK, I'm stumped.

I've attached my project.

The basic setup of the project is as follows:

TIM8 set up as the timer:

0690X00000DYN17QAH.jpg

DMA is set up to be triggered by TIM8 UP, in circular memory to peripheral mode:

0690X00000DYN1CQAX.jpg

The code is then extremely simple:

Start the timer (FWIW I've also tried HAL_TIM_BASE_Start and it makes no difference, still doesn't work):

if (HAL_TIM_Base_Start_IT(&htim8) != HAL_OK)
  {
    Error_Handler();
  }

Start the circular DMA transfer (Also tried HAL_DMA_Start_IT and it made no difference, still didn't work):

if (HAL_DMA_Start(&hdma_tim8_up, (uint32_t)gpio_data, (uint32_t)&GPIOA->BSRR, GPIO_DATA_LENGTH) != HAL_OK)
  {
    Error_Handler();
  }

Where we pass in the auto-generated DMA handle (hdma_tim8_up) and the data pointer (gpio_data) to the BSRR register for GPIOA output.

But it doesn't work.

I've tried this with TIM2, TIM6 and TIM8 across all DMA's (1 and 2) and Channels (1 through 6).

I've stepped through with the debugger and everything gets set up as you'd expect. ​The DMA addresses and modes are correct, everything gets initialized etc. yet nothing seems to happen.

If someone could please take a look and let me know what I'm doing wrong that would be great.

Nothing in the datasheet indicates that this shouldn't work.

I've read all the relevant sections of R0440, and the connectivity suggests this should be fine.

In the example project, to switch back to basic HAL_Delay and polling, to show that my data is correct, just un-comment:

//#define SIMPLER_WORKING_EXAMPLE

And it will automatically switch back to a simpler code-example that just sets BSRR directly, and it works (obviously).

If anyone can help it would be greatly appreciated. I've looked at all the examples people have shown online, and they simply don't work.

berendi
Principal

> it's quite frustrating that the interface gets SO CLOSE, but doesn't go the whole way.

Yes, there are reasons why I'm not using it.

> I hope STM intends on extending the functionality rather than leaving it as an intro program to get people into the ecosystem.

I think they should rather fix the supposedly existing functionality first. It's a lot more frustrating when even the basic clock setup is messed up so bad that the MCU locks up solid.

> If someone could please take a look and let me know what I'm doing wrong that would be great.

Sorry, I can't decipher HAL code. If you post the relevant peripheral registers (RCC, DMA, DMAMUX, TIM8, GPIOA), I might be able to notice if something is missing.

> I've stepped through with the debugger and everything gets set up as you'd expect. ​The DMA addresses and modes are correct, everything gets initialized etc. yet nothing seems to happen.

Is the timer running, i.e. CNT value changing? Are there any flags set in the timer or DMA status registers? Can you read back the register values you've set?

> Nothing in the datasheet indicates that this shouldn't work.

Neither the datasheet nor the reference manual documents what HAL functions do, and how are they supposed to be used. The HAL documentation describes a few common use cases, the rest should be treated as undocumented, i.e. even if a functionality seems to work now, it might stop working anytime.

Anyway, here is what I think should work, but I don't have a G4 to test it. I've omitted GPIO pin setup, set them to outputs in GPIOA->MODER.

1. Make sure that all peripherals used (GPIO, TIM, DMA and DMAMUX) are enabled in RCC.

RCC->AHB1ENR |= RCC_AHB1ENR_DMAMUX1EN | RCC_AHB1ENR_DMA1EN;
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
RCC->APB2ENR |= RCC_APB2ENR_TIM8EN;

2. Set up a DMA channel to copy from the array of memory to BSRR.

DMA1_Channel2->CMAR = gpio_data;
DMA1_Channel2->CPAR = &GPIOA->BSRR;
DMA1_Channel2->CNDTR = GPIO_DATA_LENGTH;
DMA1_Channel2->CCR =
    DMA_CCR_MSIZE_1 | // memory data size 32 bits
    DMA_CCR_PSIZE_1 | // peripheral data size 32 bits
    DMA_CCR_CIRC    | // circular mode
    DMA_CCR_MINC    | // increment memory pointer
    DMA_CCR_DIR     | // direction memory to peripheral
    DMA_CCR_EN      | // enable channel
    0);

3. Connect the timer update DMA request to the DMA channel.

DMAMUX1_Channel2->CCR = (DMA_REQUEST_TIM8_UP << DMAMUX_CxCR_DMAREQ_ID_Pos);

4. Set the timer prescaler, period, enable DMA and start.

TIM8->PSC = 1000 - 1;
TIM8->EGR = TIM_EGR_UG; // required to load the prescaler
TIM8->ARR = 17000 - 1;
TIM8->BDTR = TIM_BDTR_MOE; // required on advanced timers
TIM8->SR = 0; // clear status after TIM_EGR_UG
TIM8->DIER = TIM_DIER_UDE; // enable DMA on timer update (overflow)
TIM8->CR1 = TIM_CR1_CEN; /// start the timer

After each step, check that the register values are actually written, that they don't contain anything but the reset values in the datasheet modified with what you've written. Step through the code and check status registers at each step.

Thanks again @berendi​! I'll give this a go tonight.

It'll be good to get in some experience moving away from the HAL as well.

Thanks again @berendi​, I just did exactly what you said,

I stepped through the code, validated that all of the registers are getting set correctly, which they are, and it still doesn't work.

I am guess either there is something about the connectivity not in the reference manual that prevents the timer/dma/gpio combinations I'm using from working on the G4, or there is some other software or hardware fault at play.

Has anyone managed to get this working on a G4?

I might also write directly to STM hardware support and ask if I'm missing something else.

Let me check the register values. RCC, DMAMUX, DMA, TIM8, GPIO, post all of them