STM32 + HAL + FreeRTOS Part VIII: PWM and complimentary PWM

I need to drive a Peltier element for thermal stabilization. Typically they are driven by a linear source and it's even recommended to do it that way. I did use MAX1968 as a bipolar driver, but that has a few drawbacks - hardware PID controller is fixed so there's very little room for thermal load variations; limited current/voltage. So I decided to try out driving an H-bridge with PWM and LC filter. While looking at PWM module in STM32F429 I noticed an interesting "complimentary" feature. I tested it out and here we go.

Advanced timers on F429 (TIM1 and TIM8) apart from being wider, also have "complimentary" feature, where a single TIM channel can drive two GPIOs, one in being an opposite of another.

But first things first. Let's try to set up a simple PWM first. As usual there are a few basic steps one has to perform:

  1. Configure the timer
  2. Configure PWM output of the timer
  3. Feed clock to the timer
  4. Clock up GPIOs
  5. Configure those as outputs
  6. Enable timer on particular channel

 Due to TIM1 outputs on my trusty Discovery board being used by other things, I decided to go with TIM8. So, all the configuration go into tim.c:

void MX_TIM8_Init(void) {
	TIM_ClockConfigTypeDef sClockSourceConfig = {0};
	TIM_MasterConfigTypeDef sMasterConfig = {0};
	TIM_OC_InitTypeDef sConfigOC = {0};
	TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};

	// All PWM channel outputs are based on this setup for single timer
	// If we look at stm32f4xx_hal_rcc_ex.h file section for __HAL_RCC_TIM8_CLK_ENABLE(), we see that it's running off APB2 clock which is running at 72MHz

	htim8.Instance = TIM8;
	htim8.Init.CounterMode = TIM_COUNTERMODE_UP;
	// prescaler is 16-bit, dividing by 720 will give us 100kHz ticks
	htim8.Init.Prescaler = 720;
	// now we should be able to simply select the length we want in units of 0.01 milliseconds
	htim8.Init.Period = 100;
	htim8.Init.RepetitionCounter = 0;
	// This is not actual timer clock division, but used only for dead-time calculation
	// With Divider 72MHz/4 one DTS (Deat Time Step) is 1/18MHz or 5.6ns
	htim8.Init.ClockDivision = TIM_CLOCKDIVISION_DIV4;
	htim8.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

	// Try to init
	if (HAL_TIM_Base_Init(&htim8) != HAL_OK) {

	// Clock straight from internal clock.
	// This can be used to chain clocks to get super-long timer periods
	// much longer than maximum settings of a single timer would allow.
	sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
	if (HAL_TIM_ConfigClockSource(&htim8, &sClockSourceConfig) != HAL_OK) {
	if (HAL_TIM_PWM_Init(&htim8) != HAL_OK) {

	// These calls are optional, since peripheral should be in this state by default
	sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
	sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;

	if (HAL_TIMEx_MasterConfigSynchronization(&htim8, &sMasterConfig) != HAL_OK) {

	// Now set up actual PWM params
	// We want a simple PWM on TIM8 Channel 3
	// In Mode1 channel is active (of selected Polarity) as long as TIMx_CNT<TIMx_CCR1
	// In Mode2 channel is inactive (of selected Polarity) as long as TIMx_CNT<TIMx_CCR1
	sConfigOC.OCMode = TIM_OCMODE_PWM1;
	// Pulse length in clock ticks should give us 65ms
	sConfigOC.Pulse = 65;
	if (HAL_TIM_PWM_ConfigChannel(&htim8, &sConfigOC, TIM_CHANNEL_3) != HAL_OK) {

	// configure pins


What we are doing here is telling TIM8 to:

  • count upwards from 0 until  "Period" value is reached
  • setting up TIM8 clock frequency to 72MHz / 7200 = 100kHz or 10us
  • setting up single Period duration to 100 * 10us = 1ms. This is our PWM frequency where both high and low periods are summed
  • setting up Dead Time Step period (needed later)
  • feeding the timer its clock from APB2
  • setting "active" period "Pulse" value to 65 * 10us = 650 us. This is the duration when output will be of OCPolarity, in our case LOW
  • telling that Active is LOW, while Idle is HIGH. This can also be inverted by selecting PWM Mode2

MspPostInit() function just sets up GPIOs:

void HAL_TIM_MspPostInit(TIM_HandleTypeDef* timHandle) {
	GPIO_InitTypeDef GPIO_InitStruct = {0};
	if(timHandle->Instance==TIM8) {
		// enable port C clocks in case they are not enabled yet
		/**TIM8 GPIO Configuration
		PC7     ------> TIM8_CH2
		PC8     ------> TIM8_CH3
		PB0		------> TIM8_CH2N

		// CH 3 / 2 / 1
		GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_7;
		GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
		GPIO_InitStruct.Pull = GPIO_NOPULL;
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
		GPIO_InitStruct.Alternate = GPIO_AF3_TIM8;
		HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

		// CH 2N
		GPIO_InitStruct.Pin = GPIO_PIN_0;
		HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

		// CH 1N
		GPIO_InitStruct.Pin = GPIO_PIN_5;
		HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

Then we need to enable the PWM by calling     HAL_TIM_PWM_Start(&htim8, TIM_CHANNEL_3);

Simple enough.

Now complementary will be on the same timer, but using Channel 2 (PC7 and PB0 as inverse). We'll add to the same MX function before MspPostInit() call:

	// we'll settle for 500 us on 500 us off with 50 us dead time (both pulled the same way)
	// We want complimentary (CHx = a, CHxN = !a) PWM on TIM8 Channel 2
	sConfigOC.OCMode = TIM_OCMODE_PWM1;
	// Pulse length in clock ticks should give us 65ms
	sConfigOC.Pulse = 50;
	if (HAL_TIM_PWM_ConfigChannel(&htim8, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) {

	sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_ENABLE;
	sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
	sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
	// In counts of DTS periods (in this case 400us), max value 0xFF
	// t_DTS with 72MHz APB clock and predivider 4 to 18MHz period is ~56 ns
	// From TIMx_BDTR reg description
	// DTG[7:5]=0xx => DT=DTG[7:0]x t_dtg with t_dtg = t_DTS = 56 ns
	// DTG[7:5]=10x => DT=(64+DTG[5:0])x t_dtg with t_dtg = 2x t_DTS = 112 ns
	// DTG[7:5]=110 => DT=(32+DTG[4:0])x t_dtg with t_dtg = 8x t_DTS = 448 ns
	// DTG[7:5]=111 => DT=(32+DTG[4:0])x t_dtg with t_dtg = 16x t_DTS = 896 ns
	// so max that we can get is (32+31)(0.896) = 56.448 us
	// let's try to get 50us
	// 50000 = (32+x) * 16 * 56
	// x = 50000/16/46 - 32 = 23.8036
	// with x = 24, DT should be (32+24) * 16 * 56ns = 50 176 ns
	sBreakDeadTimeConfig.DeadTime = (0b11100000 | 24);
	sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
	sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
	sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;
	if (HAL_TIMEx_ConfigBreakDeadTime(&htim8, &sBreakDeadTimeConfig) != HAL_OK) {

Again we set up:

  • PWM mode 1 - to have active while counting
  • Polarities same with idle states opposite, just to see what's happening
  • Dead time is inserted to prevent transistor shoot-trough in H-bridge connection. FETs tend to have large input capacitances which means that it takes time to close the gate. If one FET hasn't discharged before other one opens, we get power supply short to ground through 2 FETs. To avoid this we need to give FETs some time to close up, before opening another one. This is called Dead time. DT calculations are a bit involved, but I described it in code comments.
Now we just need to turn them on. A weird quirk with using complimentary outputs with HAL, is that you need to start them separately:
void start_TIM8(void) {
	HAL_TIM_PWM_Start(&htim8, TIM_CHANNEL_3);
	HAL_TIMEx_PWMN_Start(&htim8, TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(&htim8, TIM_CHANNEL_2);
And to verify it, we can look at the scope capture. 
  • Channel 2 is our PWM Channel 3, which is inverted 1 ms long with 650 us being low and 350 us being high.
  • Channels 3 and 4 are our complimentary outputs. As you can see, they are HIGH ~450 us, LOW are 550 us with 50 us overlap when both are low. This overlap is our dead time.

As usual, code in GitHub. I refactored it a bit, to have features in separate branches. This will live in "pwm" branch

No comments:

Post a Comment