Today I’m going to look at producing a PWM pulse of fixed frequency and duty cycle.
You will need to choose a PWM-compatable pin. I chose PA11. The pins will have a timer and channel associated with them, according to the following diagram:

Look at the purple boxes. So, for example, PC9 corresponds with timer 1, channel 4. PA11 corresponds to timer 1, channel 4.
You also need to associate the timer with the pin. For that you need the “alternate function”, which you can get from the following table (anything other than the F411 will likely be different):
AF0 system AF1 TIM1/TIM2 AF2 TIM3..5 AF3 TIM8..11 AF4 I2C1..3 AF5 SPI1/SPI2 AF6 SPI3 AF7 USART1..3 AF8 USART4..6 AF9 CAN1..2, TIM12..14 AF10 OTG_FS, OTG_HS AF11 ETH AF12 FSMC, SDIO, OTH_HS AF13 DCMI AF14 AF15 EVENTOUT
Choose a frequency. I chose 500Hz. I set a prescaler for timer 1 via the line:
htim1.Init.Prescaler = HAL_RCC_GetSysClockFreq() / 1000000 - 1;
This has the effect of scaling the prescaler so that timer 1 is at 1MHz. We actually want a PWM frequency of 500Hz. This is calculated according to the formula
f_pwm = f_clk / (arr+1)/(psc+1)
where f_clk is the clock that drives the timer module, ARR is the prescaler, and ARR is the Auto Reload Register value. Substitute in the value of the f_clk and the prescaler in the formula above, and rearrange is to to obtain:
arr = 1,000,000/f_pwm -1
it is a period, equivalent to the piece of code that reads:
htim1.Init.Period = 1000000 / freq - 1;
The pulse length, i.e. the value at which the PWM is turned off, will be based on the duty cycle:
sConfigOC.Pulse = 1000000 / freq * duty_cycle_pc/100 - 1;
There’s a bunch of other stuff to do like setting up the pin, enabling the relevant RCCs, and the timer. Putting it altogether, the complete code looks like this:
#include "stm32f4xx_hal.h"
TIM_HandleTypeDef htim1;
static void MX_TIM1_Init(uint32_t freq, uint32_t duty_cycle_pc)
{
htim1.Instance = TIM1;
htim1.Init.Prescaler = HAL_RCC_GetSysClockFreq() / 1000000 - 1;
htim1.Init.Period = 1000000 / freq - 1;
HAL_TIM_Base_Init(&htim1);
HAL_TIM_PWM_Init(&htim1);
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 1000000 / freq * duty_cycle_pc/100 - 1;
HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_4);
/**TIM1 GPIO Configuration: PA11 ------> TIM1_CH4 */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
int main(void)
{
HAL_Init();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_TIM1_CLK_ENABLE();
HAL_NVIC_EnableIRQ(TIM1_CC_IRQn);
MX_TIM1_Init(500, 25);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
while (1)
;
}
That’s 36 lines of code, so not too bad. To reiterate, this will produce a 500Hz PWM signal on pin PA11 with a duty cycle of 25%.
Update 1: If you’re using CubeMX, the configuration steps are (IIRC):
Pinout View: PA11 : TIM1_CH4
Timers: TIM 1:
- Timers: TIM1:
- Mode:
- Clock source: Internal clock
- Channel 4: PWM Generation CH4
- Parameter settings:
- Counter settings:
- Prescaler: HAL_RCC_GetSysClockFreq()/1000000 -1 (No check)
- Counter Period: 1000000/500-1
- auto-reload preload: enable
- PWM Generation Channel 4:
- Mode: PWM mode 1
- Pulse: 1000000/500/4 -1
- Counter settings:
- NVIC Settings: TIM1 capture compare interrupt: Enabled
- Mode: