The idea is to have a look-up-table with a predefined number of sine-wave values. The pulse width of the next pulse is then set to the current value according to that look-up-table during the interrupt service routine. The STM32 HAL provides a callback function that can be implemented, called HAL_TIM_PWM_PulseFinishedCallback. This callback is called during an interrupt service routine. The logic for computing the next index is outsourced to another timer. This timer calls the afformentioned interrupt handler every time a period is done.
First, we have to setup the timers. For that, we will use the STM32 CubeIDE, which provides a nice visual interface to do these things. We setup timer 1, channel 1 to PWM Generation and channel 2 to Output Compare. The Prescaler is set to 1 and the Counter Period is set to PERIOD (Remember to choose "No Check" in order to be able to set a non-hex value).
First, we have to setup the timers. For that, we will use the STM32 CubeIDE, which provides a nice visual interface to do these things. We setup timer 1, channel 1 to PWM Generation and channel 2 to Output Compare. The Prescaler is set to 1 and the Counter Period is set to PERIOD (Remember to choose "No Check" in order to be able to set a non-hex value).
The look-up-table could for example look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #define PERIOD 32 #define DC_OFFSET (PERIOD >> 1) #define PERIOD_FRACTION(X) ((uint16_t) ((X) * (PERIOD >> 1))) const uint16_t sin[] = { DC_OFFSET + 0, DC_OFFSET + PERIOD_FRACTION(0.500), DC_OFFSET + PERIOD_FRACTION(0.866), DC_OFFSET + PERIOD_FRACTION(1), DC_OFFSET + PERIOD_FRACTION(0.866), DC_OFFSET + PERIOD_FRACTION(0.500), DC_OFFSET + 0, DC_OFFSET - PERIOD_FRACTION(0.500), DC_OFFSET - PERIOD_FRACTION(0.866), DC_OFFSET - PERIOD_FRACTION(1), DC_OFFSET - PERIOD_FRACTION(0.866), DC_OFFSET - PERIOD_FRACTION(0.500), }; |
The values used will be presented shortly:
- PERIOD: corresponds to the number of ticks the timer counts before it overflows. Having a lower period enables the generation of higher frequencies.
- DC_OFFSET: As we can only generate pulses with a width greater or equal to 0, we have to add half of the period as dc offset.
- PERIOD_FRACTION: A macro to compute the width of the pulse from a decimal value.
- sin[]: The actual look-up-table with the sine-function samples. The number of samples should be as low as possible in order to keep the code-size small. In addition to that, the number of samples also dictates the maximum frequency that can be generated, as every value must be held for at least one period. Finally, a small value decreases the resolution and the reconstructability of the sinusoidal shape. The table shown is the one that I settled for after a lot of trial and error. One could also obviously get away with only using a quarter period, however that would increase the computation time inside of the ISR, so a time-space-trade-off must be made.
The implemented callback functions look as follows:
As STM32 Timers only have a single set of these callback functions, we need to manually check if we were called by the one timer and channel of interest. Of so, we simply set the pulse for the PWM signal and increase the step.
Note, that this is channel 2, which corresponds to a simple compare timer, which triggers every time a period ends. There are a number of ways to do this, though.
In our main function, we have to start the two channels of the timer:
And that's it for basic SPWM generation. Generating different frequencies can be done by either changing the period length, the prescaler or the number of sine-wave samples.
- PERIOD: corresponds to the number of ticks the timer counts before it overflows. Having a lower period enables the generation of higher frequencies.
- DC_OFFSET: As we can only generate pulses with a width greater or equal to 0, we have to add half of the period as dc offset.
- PERIOD_FRACTION: A macro to compute the width of the pulse from a decimal value.
- sin[]: The actual look-up-table with the sine-function samples. The number of samples should be as low as possible in order to keep the code-size small. In addition to that, the number of samples also dictates the maximum frequency that can be generated, as every value must be held for at least one period. Finally, a small value decreases the resolution and the reconstructability of the sinusoidal shape. The table shown is the one that I settled for after a lot of trial and error. One could also obviously get away with only using a quarter period, however that would increase the computation time inside of the ISR, so a time-space-trade-off must be made.
The implemented callback functions look as follows:
1 2 3 4 5 6 7 8 9 10 11 | void updatePWMPulse(uint16_t value){ htim1.Instance->CCR1 = value; } void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim){ if(htim == &htim1 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2){ updatePWMPulse(sin[step]); step = (step + 1) % num_steps; } } |
As STM32 Timers only have a single set of these callback functions, we need to manually check if we were called by the one timer and channel of interest. Of so, we simply set the pulse for the PWM signal and increase the step.
Note, that this is channel 2, which corresponds to a simple compare timer, which triggers every time a period ends. There are a number of ways to do this, though.
In our main function, we have to start the two channels of the timer:
1 2 | HAL_TIM_PWM_Start_IT(&htim1, TIM_CHANNEL_1); HAL_TIM_OC_Start_IT(&htim1, TIM_CHANNEL_2); |
And that's it for basic SPWM generation. Generating different frequencies can be done by either changing the period length, the prescaler or the number of sine-wave samples.