Getting PWM to work on STM32F4 using ST's HAL libraries

I'm late to the party, but I found myself in a similar situation and have the real answer, or at least the answer that I've personally verified to work with my own STM32 board and the STM32CubeMx drivers.

  1. Make sure you initialize (zero out) your stack-allocated structs. e.g. memset(&sConfigOC, 0, sizeof(sConfigOC)); Local variables are not automatically initialized and you will have unexpected/unintended configuration because the HAL makes very basic assumptions about the peripheral state based on these handle variables. It is particularly maddening when the behaviour changes from compile to compile or from function call to function call as the stack memory changes.
  2. Don't call HAL_TIM_PWM_MspInit() manually. It is automatically called when you call HAL_TIM_PWM_Init(), but you must make sure your handle variables are properly initialized (see #1 above).
  3. You don't need to call HAL_TIMEx_MasterConfigSynchronization() if you're not changing from the default (non chained/synchronized) configuration. It won't hurt anything, but it's also not necessary.
  4. Your MspInit() function is good, except you should also enable the clock of the GPIOC peripheral.

And the magic that is preventing it from working:

  1. You MUST call HAL_TIM_PWM_Start() after every call to HAL_TIM_PWM_ConfigChannel() -- the first thing HAL_TIM_PWM_ConfigChannel() does is to disable the timer outputs (the CCxE bits in TIMx_CCER). HAL_TIM_PWM_ConfigChannel() does not re-enable the timer outputs!

Since the only HAL way of manually updating the PWM duty cycle is through HAL_TIM_PWM_ConfigChannel(), you must always call HAL_TIM_PWM_Start() or you will not have a PWM output. I think this is done because the normal way of working with PWM is to use the _IT() or _DMA() variants of HAL_TIM_PWM_Start(), and thus you are updating the duty cycle "in the background" so to speak.

Some of the other answers (including the one you had originally marked as accepted) are wrong:

  1. Do NOT call HAL_TIM_Base_Start(), nor HAL_TIM_Base_Init(), nor any of the other non-PWM calls; First, the HAL will not call your PWM_MspInit() function because the peripheral handle will no longer be in reset state, but more importantly, the PWM (and OC, and other variants) all call the low level internal functions properly without you manually doing it. The entire point of the HAL is to not have to worry so much about the details, but you've got to have a good handle on what the HAL is doing and what it's expecting. The full source is available and is fairly well documented. You just need to read it.
  2. You do not have to call HAL_TIMEx_PWMN_Start() or any of the other TIMEx functions unless you're using the complimentary outputs. Again, this is fairly well documented in the HAL source.

When using Cube Mx, the generated code initializes the timer peripheral but does not start it running. The solution is as suggested by others: add the Start functions.

HAL_TIM_Base_Start(&htim3); 
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_ALL); 

Place this code in the "USER CODE" section after the generated code that calls MX_TIM3_Init().


I'm using STCubeMX and the generated HAL initialization files. Process verified on my F302 Nucleo Board. I set up advanced Timer 1 (TIM1) with a normal and complementary output, no dead time.

Here's how I configured PWM in CubeMX:

  1. In pinout view, I selected two pins as the TIM1_CH & TIM1_CHN pins. On the left hand pane, set TIM1 channel 1 as "PWM Generation CH1 CH1N".
  2. In the configuration tab, I put the following setting (TIM1 clk is 64MHz) CubeMX TIM1 Config Settings
  3. After code is generated, we still need to start the PWM. This is done by calling the following functions in the user code region before while(1), within main():

    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); //starts PWM on CH1 pin HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); //starts PWM on CH1N pin

  4. To change the config settings, use the following macros provided in stm32f3xx_hal_tim.h. HAL_TIM_PWM_ConfigChannel() is not the only way to update PWM setting manually as said by @akohlsmith.

    __HAL_TIM_GET_AUTORELOAD(&htim1); //gets the Period set for PWm __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, dutyCycle); //sets the PWM duty cycle (Capture Compare Value)

No need to again call HAL_TIM_PWM_Start(). These macros change the setting during run time.