cancel
Showing results for 
Search instead for 
Did you mean: 

Custom Signal generation using PWM and DMA

ST AME Support NF
ST Employee

0. Introduction

The purpose of this article is to explain how to generate a custom signal, a sine wave in this case, using an STM32 Microcontroller’s DMA controller and a PWM output from a timer. Although this example uses the NUCLEO-H745ZI-Q, the same steps can be used for other STM32H7 based boards. In this article, the following are used to generate the custom signal: - Timer in PWM output mode with the signal on CH1 - DMA controller

Note, other techniques can be used to generate various types of signals such as a DAC (Digital to Analog Controller) or even GPIO bit-banging. For this example case, generating a sine wave using a Timer and DMA is the optimal method and is the one described in this article.


1. Theory

The idea is to generate a digital signal from a timer output (PWM mode) on an STM32 and use an external RC filter to convert it to an analog sine wave. For simplicity, a basic low pass filter is used as shown below for the conversion. While this article focuses on the basics of generating the digital signal, please consult other resources for details on RC filters.

1683.png

We will generate a custom signal like the one in red shown below using the STM32 Timer PWM output channel:

1685.png
Then using a RC filter connected to the output of Timer PWM output channel, we will get our desired Sine Wave:
1687.png

We will be using the center aligned timer mode to generate a custom signal using PWM and DMA. This can be done with one method of combining the STM32H7 timer with the internal DMA feature. This method will use the DMA to transfer the continuous data of a sine wave output to the timer CCR register.  The timer CCR register controls the width of the PWM duty cycle. This will produce the resultant gate drive signal at the timer output pin. When using the DMA hardware to transfer the continuous data to the timer, the Cortex-M core is free to process other application functions in parallel. We can generate any custom waveform by feeding the data of that signal to the timer CCR register via DMA. In this exercise, we will be generating a sine wave.

 

2. Pre-requisites

NUCLEO-H745ZI-Q:

1690.jpg

Micro USB Cable

  • The cable connects a host computer to the Nucleo board for power and debugging/programming of the STM32 on the Nucleo board.

Oscilloscope

  • This is to look at the signal from the STM32 I/Os to check the generated signals.

Software Tools

  • STM32CubeIDE

1691.png
 

3. Steps

In this section we will go through the steps to generate a custom signal using the STM32CubeIDE and configuring the STM32H7 used on the NUCLEO-H745ZI-Q board.

  1. Open STM32CubeIDE and create a project using the NUCLEO-H745ZI-Q board

  2. Clock Configuration: 480/240 MHz

We are running the STM32H7 at its maximum speed: 480 MHz for the System Clock and 240 MHz for the peripheral clocks.


1692.png
 

  1. Timer 4 configuration
  •  Enable TIM4

  • In configuration mode, select Cortex-M7 and select PWM Generation CH1 for Channel 1:

1693.png
 

  • Add the following configuration

In this section we select the frequency of the PWM signals that we will generate.
Please change the counter period as shown highlighted in red below.
Also configure the PWM Generation Channel 1 as shown in red below.
Note: While the PWM is configured to have an initial pulse width of one count, the DMA will be responsible for writing the values from the table to generate the desired pulse train.

1695.png

  1. DMA configuration

We use DMA in this case, as this is the most optimal method for generating the signal accurately. In this case, the CPU is not used and can take care of other tasks.


1697.png
 

  1. Generate Code

Save the STM32CubeIDE project and this action will generate the code.

  1. Add code for sine wave

For this example, it is a sine wave, but other data can be used to generate a sine wave with a different frequency.
If you would like to learn more about where the data for the sine wave comes from, you can find more details in the following two-part video:
Hands-On with STM32 Timers: Custom Signal Generation using PWM and DMA , Part 1 of 2 - YouTube
Hands-On with STM32 Timers: Custom Signal Generation using PWM and DMA, Part 2 of 2 - YouTube


In main.c in the CM7 project:

...

/* USER CODE BEGIN PV */
#define CCRValue_BufferSize     37

ALIGN_32BYTES (uint32_t DiscontinuousSineCCRValue_Buffer[CCRValue_BufferSize]) =
{
  14999, 17603, 20128, 22498, 24640, 26488, 27988, 29093, 29770,
  29998, 29770, 29093, 27988, 26488, 24640, 22498, 20128, 17603,
  14999, 12394, 9869, 7499, 5357, 3509, 2009, 904, 227, 1, 227,
  904, 2009, 3509, 5357, 7499, 9869, 12394, 14999
};
/* USER CODE END PV */
  1. Enable D & I (Data and Instruction) cache and flush D cache

The two caches in the STM32 are enabled to increase overall performance. Also, the data cache must be flushed to properly update the contents of the SRAM used by the DMA.
Add following code in main.c for the CM7 project:

  /* USER CODE BEGIN 1 */
  /* Enable I-Cache---------------------------------------------------------*/
  SCB_EnableICache();

  /* Enable D-Cache---------------------------------------------------------*/
  SCB_EnableDCache();
  /* USER CODE END 1 */
…
  /* USER CODE BEGIN Init */

  /* Clean Data Cache to update the content of the SRAM to be used by the DMA */
  SCB_CleanDCache_by_Addr((uint32_t *) DiscontinuousSineCCRValue_Buffer, CCRValue_BufferSize );

  /* USER CODE END Init */
  1. Start function to transfer data from RAM to timer

Add the following code in main.c for the CM7 project:

/* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start_DMA(&htim4, TIM_CHANNEL_1, DiscontinuousSineCCRValue_Buffer, CCRValue_BufferSize);
  /* USER CODE END 2 */
  1. Compile and flash the code to your Nucleo board

  2. Run the code by resetting the board using the black reset button B2 on the Nucleo board

4. Results on the Oscilloscope

Output of the custom PWM signal from TIM4 CH1 (GPIO Pin PD12) is available on the Nucleo board on connector CN10, pin #21 (see the image below).

1698.png

Oscilloscope capture on PD12:

1700.png

When adding the external RC filter to the PWM output, the signal is converted to a sine wave as shown in the following oscilloscope capture:

1702.png

5. Links

Video

Below are the links to a two-part video that describes the steps covered in this article.
Hands-On with STM32 Timers: Custom Signal Generation using PWM and DMA , Part 1 of 2 - YouTube
Hands-On with STM32 Timers: Custom Signal Generation using PWM and DMA, Part 2 of 2 - YouTube

Comments
LJi
Associate

I have more questions about this DMA timer update operation.

  1. Can I generate the second PWM with channel 2 of the TIM4 and start them at the same time?
  2. Does your example work with Cortex-M4?
  3. If the anser is yes to above question, can I use the same timer setting with STM32F4?

Thanks

PGood.1
Associate II

A simple project on a stm32G030C8Tx

A bit of an issue with a single transfer if you set up CubeIDE for a Normal Mode in the DMA settings then the PWM is left running at the last value in your DMA memory array in this example the 

PGood1_1-1711740393536.png

 

 So if I genererate a CubeIDE project with 

Channel1  PWM Generation CH1

Prescaler 10

Counter Mode Up

Clock period 255

Internal Clock Division No Devision

Repetition counter 0

auto-reload preload 0

DMA Settings 

TIM!_CH1          DMA1 Channel 1       Memory To Peripheral         Low

DMA Request Settings

Mode Normal

Increment Address Memory and Not Peripheral

Data Width 

Periperial Word

Memory Byte

PGood1_0-1711740049388.png

 

/* USER CODE BEGIN 2 */
//TIM1->CCR1 = 4;
//HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

uint8_t pwmData[10];

pwmData[0] = 25;
pwmData[1] = 50;
pwmData[2] = 75;
pwmData[3] = 100;
pwmData[4] = 125;
pwmData[5] = 150;
pwmData[6] = 175;
pwmData[7] = 200;
pwmData[8] = 225;
pwmData[9] = 250;

HAL_TIM_PWM_Start_DMA(&htim1, TIM_CHANNEL_1, (uint32_t *)pwmData, 10);

 

 

 

 

 

Jackie3
Associate

Hello all,

In Step 3, 4. The DMA data width configuration is sometimes important. The 32 bit "word" is uncommon for most timers of STM32. I have spent almost a week troubleshooting a similar project. Finally found the problem was here. After changing both widths to "half word", everything works well.

BTW, I am using STM32H5xx, the GPDMA is even more complicated with different ioc GUI configurations.

mkrug
Associate III

Dear all,

Some comments from my side. All related to a STM32L4 using the TIM2 timer - but I guess it's the same for all STM32 with TIM1/2/3/4/5 - or better say general timers. I also looked only in the DMA supported PWM generation. IT or none support at all might be different - but I will not recommend them because they are very time consuming with respect to CPU cycles.

  1. I doubt that the PWM signal is really working if it is configured like in the initial example and also according to the comments made about it. You will miss the first PWM cycle if you do so because the values of shadowed register CCRx is not loaded into the active register. I assume many programmers did not notice that because it's fast and the CCRx is set to 0 after reset. So if (like usual) the output is active high there is no pulse is on the output pin and propably not realised that there is a severe problem. But it still is not correct.
  2. Using the HAL_TIM_PWM_DMA() function will lead to half tranfer interrupt that is in all the examples not really necessary. So you better disable it.
  3. To overcome the above mentioned problem you need to fill the CCRx register (hence, writing to the shadow register) before starting the PWM function and also set the UG bit to load it into the active register. It then might look like:

    uint32_t duty_cycle[] = {1000, 2000, 3000,1};

    htim2.Instance->CCR2 = duty_cycle[0];

    HAL_TIM_GenerateEvent(&htim2,TIM_EVENTSOURCE_UPDATE);

    HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_2, &duty_cycle[1], 3);

    __HAL_DMA_DISABLE_IT(htim2.hdma[TIM_DMA_ID_CC2],DMA_IT_HT);

  4. But please be careful. If you do so, the HAL functions raise a problem by doing one DMA move too much (that's why I add a fourth data element to my duty_cylce[] array). The reason is, that the DMA move is triggered at the compare event. So with the last 'desired' compare event one more DMA move is triggered before the ISR will abort the DMA move. According to the examples in the previous posts there is a read access to data that is located after the duty cycle array. I guess in most cases this will not harm anything but it's still not correct and in case you have cache or a MPU active you might run into trouble. 
  5. The standard HAL treatment at the end of the PWM generation using DMA will reliable stop the DMA transfer (after doing one move too much) but also will stop the timer before ending the actual PWM period. One more time I guess this will be not a big problem for most of the applications if they use active high during the duty cycle. However, it is still not a correct behaviour.
  6. Not really part of the PWM issue: I doubt if the cache clearance is necessary as shown in the first example. As long as no other source is writing to your duty cycle array you should be save. From my experience the problem starts if more than one source is writing to the same data and the cache might hold an outdated version of it.

 

Conclusion: To be honest, I haven't one. At least with the standard HAL function my code given in 3.) is the best I found so far - and it still has its problems. I assume you need to dig deeper with the LL_ functions or even do you own register setting to come up with a really perfect solution that is not doing more DMA moves as necessary and don't stop the timer before the last PWM period expired. If I have time I will add more to this if I find a better solution.
The internet (and also this forum) is full of examples that ignore my points. So please be careful if you have really a sensitive (functional safety) application.

Best Regards
Markus

Version history
Last update:
‎2025-03-17 5:23 AM
Updated by: