Difference between revisions of "STM32 Timer PWM Input Capture"
(19 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | [[Category:C]] [[Category:STM32 Development]] [[Category:STM32CubeMX]] [[Category:STM32CubeIde]] [[Category:Embedded]] [[Category:STM32]] {{metadesc|Decode PWM Input}} | + | [[Category:C]] [[Category:STM32 Development]] [[Category:STM32CubeMX]] [[Category:STM32CubeIde]] [[Category:Embedded]] [[Category:STM32]] {{metadesc|Decode PWM Input using timers}} |
− | The timers in [[STM32]] [[MCU]]s are incredible powerful. We have used them before to generate [[STM32 Pulse Width Modulation|PWM Signals]] and to [[STM32 Rotary Encoder|decode signals from rotary encoders]]. | + | The timers in [[STM32]] [[MCU]]s are incredible powerful. We have used them before to generate [[STM32 Pulse Width Modulation|PWM Signals]] and to [[STM32 Rotary Encoder|decode signals from rotary encoders]]. In this example we will use them to determine frequency an duty cycle of an external [[PWM]] signal. |
+ | |||
+ | For the example we will be using a [[Black Pill]] development board and we will be using the same [[MCU]] to both generate and to decode the [[PWM]] signal. | ||
+ | |||
+ | The source of the complete example is [https://github.com/lbthomsen/blackpill/tree/master/pwm_ic here]. | ||
+ | |||
+ | == Wiring == | ||
+ | |||
+ | Since we are using the same board to generate and to decode the [[PWM]] signal, we need a jumper wire to connect the PWM Output to the PWM Input. The pinout resulting from the setup in the [[#STM32CubeMX Setup|next section]] looks like this: | ||
+ | |||
+ | [[File:PWM Input Capture example - pinout.png|600px]] | ||
+ | |||
+ | So, we will need to run a jumper wire from PB6 (the PWM output pin) to PA5 (the PWM input capture pin) - like this: | ||
+ | |||
+ | [[File:PWM Input Capture example - wiring.jpg|600px]] | ||
+ | |||
+ | In this photo, a [[ST-Link]] device with serial port, and an oscilloscope probe has also been connected. | ||
+ | |||
+ | == STM32CubeMX Setup == | ||
+ | |||
+ | We can now begin configuring the [[STM32]] in [[STM32CubeMX]]. As usual the clock is critical. | ||
+ | |||
+ | [[File:PWM Input Capture example - clock settings.png|1000px]] | ||
+ | |||
+ | The 25 MHz crystal oscillator on the [[Black Pill]] '''can''' generate a 100 MHz clock, but even though USB is not needed in this example, the 48 MHz frequency needed for USB is not possible when the clock is set to 100 MHz. I therefore often use 96 MHz, as this can be used for USB as well. | ||
+ | |||
+ | The most important frequency is the APB1 timer clock. In this example that will be running at the full 96 MHz. | ||
+ | |||
+ | We will be using Timer 4 to generate the [[PWM]] signal. The setup of that timer looks like this: | ||
+ | |||
+ | [[File:PWM Input Capture example - Timer 4 configuration.png|400px]] | ||
+ | |||
+ | By default we set the prescaler to 47 and the counter period to 1000. This will result in a frequency of 96 MHz / 48 / 1000 - 2 kHz. The pulse has been configured to 900 resulting in a duty cycle of 90 %. | ||
+ | |||
+ | According to the [https://www.st.com/resource/en/datasheet/stm32f411ce.pdf datasheet] of the [[STM32F411]] [[MCU]] on the [[Black Pill]] board, the following timers are available: | ||
+ | |||
+ | [[File:STM32F411 Timers.png|600px]] | ||
+ | |||
+ | Since Timer 2 has a 32 bit counter, we will be using that for the input capture (lower frequencies can be measured): | ||
+ | |||
+ | [[File:PWM Input Capture example - Timer 2 confriguration.png|400px]] | ||
+ | |||
+ | A few things are important here. First of all, no prescaler is set, so the counter resolution will be the full 96 MHz internal clock. Second, the Combined PWM Input on Channel 1. Notice that both Channel 1 and Channel 2 are "greyed out". This is because Channel 1 is used a the primary channel, triggering on the rising edge, while Channel 2 counter is triggered on the falling edge (making PWM duty cycle calculation possible). | ||
+ | |||
+ | Finally we need to ensure that the global interrupt on Timer 2 is enabled: | ||
+ | |||
+ | [[File:PWM Input Capture example - timer 2 interrupt.png|400px]] | ||
+ | |||
+ | == Code == | ||
+ | |||
+ | In our code, the first step is to get the PWM output going: | ||
+ | |||
+ | <pre> | ||
+ | HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1); // Output PWM Generation | ||
+ | </pre> | ||
+ | |||
+ | The values of this timer - the prescaler and the pulse count - can be adjusted later. | ||
+ | |||
+ | Next, we need to enable our input capture timer. This is where it gets a bit interesting: | ||
+ | |||
+ | <pre> | ||
+ | HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // Primary channel - rising edge | ||
+ | HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_2); // Secondary channel - falling edge | ||
+ | </pre> | ||
+ | |||
+ | We start Timer2, channel 1 in Interrupt mode, but we also start channel 2. The result is, both counters start counting on the rising edge. On the falling edge, the channel 2 counter is stopped, while the channel 1 counter continues. Finally on the next rising edge both counters can be examined. We can handle that in the interrupt callback: | ||
+ | |||
+ | <pre> | ||
+ | void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { | ||
+ | if (htim->Instance == TIM2) { | ||
+ | uint32_t cl = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); | ||
+ | uint32_t ch = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); | ||
+ | |||
+ | freq = (float) TIMER_CLOCK_FREQ / (cl + 1); | ||
+ | duty = (float) 100 * ch / cl; | ||
+ | } | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | The global variables freq and duty will be updated continously. | ||
+ | |||
+ | To be able to test multiple frequencies the following array is defined: | ||
+ | |||
+ | <pre> | ||
+ | uint32_t pwm_vals[][2] = { | ||
+ | {9599, 100}, | ||
+ | {9599, 500}, | ||
+ | {9599, 900}, | ||
+ | {959, 100}, | ||
+ | {959, 500}, | ||
+ | {959, 900}, | ||
+ | {95, 100}, | ||
+ | {95, 500}, | ||
+ | {95, 900}, | ||
+ | {47, 100}, | ||
+ | {47, 500}, | ||
+ | {47, 900}, | ||
+ | }; | ||
+ | </pre> | ||
+ | |||
+ | This is simply prescaler and duty counter values. In our program main loop we got the following: | ||
+ | |||
+ | <pre> | ||
+ | while (1) { | ||
+ | |||
+ | now = HAL_GetTick(); | ||
+ | |||
+ | if (now - last_print >= 1000) { | ||
+ | DBG("Tick %4lu freq = %4.1f Hz duty = %2.1f %%", now / 1000, freq, duty); | ||
+ | |||
+ | last_print = now; | ||
+ | } | ||
+ | |||
+ | if (now - last_change >= 2000) { | ||
+ | |||
+ | __HAL_TIM_SET_PRESCALER(&htim4, pwm_vals[pwm_vals_count][0]); | ||
+ | __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, pwm_vals[pwm_vals_count][1]); | ||
+ | |||
+ | ++pwm_vals_count; | ||
+ | if (pwm_vals_count >= sizeof(pwm_vals) / sizeof(pwm_vals[0])) { | ||
+ | pwm_vals_count = 0; | ||
+ | } | ||
+ | |||
+ | last_change = now; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | In short, every second we print out the values of float and duty and every 2 seconds we change the values according to the previously defined array. | ||
+ | |||
+ | When running, the following output is generated: | ||
+ | |||
+ | <pre> | ||
+ | Tick 1 freq = 2000.0 Hz duty = 90.0 % | ||
+ | Tick 2 freq = 2000.0 Hz duty = 90.0 % | ||
+ | Tick 3 freq = 10.0 Hz duty = 10.0 % | ||
+ | Tick 4 freq = 10.0 Hz duty = 10.0 % | ||
+ | Tick 5 freq = 10.0 Hz duty = 50.0 % | ||
+ | Tick 6 freq = 10.0 Hz duty = 50.0 % | ||
+ | Tick 7 freq = 10.0 Hz duty = 90.0 % | ||
+ | Tick 8 freq = 10.0 Hz duty = 90.0 % | ||
+ | Tick 9 freq = 100.0 Hz duty = 10.0 % | ||
+ | Tick 10 freq = 100.0 Hz duty = 10.0 % | ||
+ | Tick 11 freq = 100.0 Hz duty = 50.0 % | ||
+ | Tick 12 freq = 100.0 Hz duty = 50.0 % | ||
+ | Tick 13 freq = 100.0 Hz duty = 90.0 % | ||
+ | Tick 14 freq = 100.0 Hz duty = 90.0 % | ||
+ | Tick 15 freq = 1000.0 Hz duty = 10.0 % | ||
+ | Tick 16 freq = 1000.0 Hz duty = 10.0 % | ||
+ | Tick 17 freq = 1000.0 Hz duty = 50.0 % | ||
+ | Tick 18 freq = 1000.0 Hz duty = 50.0 % | ||
+ | Tick 19 freq = 1000.0 Hz duty = 90.0 % | ||
+ | Tick 20 freq = 1000.0 Hz duty = 90.0 % | ||
+ | Tick 21 freq = 2000.0 Hz duty = 10.0 % | ||
+ | Tick 22 freq = 2000.0 Hz duty = 10.0 % | ||
+ | Tick 23 freq = 2000.0 Hz duty = 50.0 % | ||
+ | Tick 24 freq = 2000.0 Hz duty = 50.0 % | ||
+ | Tick 25 freq = 2000.0 Hz duty = 90.0 % | ||
+ | Tick 26 freq = 2000.0 Hz duty = 90.0 % | ||
+ | Tick 27 freq = 10.0 Hz duty = 10.0 % | ||
+ | Tick 28 freq = 10.0 Hz duty = 10.0 % | ||
+ | </pre> | ||
+ | |||
+ | Which is exactly what we expected. | ||
== Miscellaneous Links == | == Miscellaneous Links == | ||
− | * [https://github.com/lbthomsen/blackpill/tree/master/pwm_ic PWM Input Capture Example for [[Black Pill | + | * [https://github.com/lbthomsen/blackpill/tree/master/pwm_ic PWM Input Capture Example] for [[Black Pill]] |
Latest revision as of 02:06, 18 July 2022
The timers in STM32 MCUs are incredible powerful. We have used them before to generate PWM Signals and to decode signals from rotary encoders. In this example we will use them to determine frequency an duty cycle of an external PWM signal.
For the example we will be using a Black Pill development board and we will be using the same MCU to both generate and to decode the PWM signal.
The source of the complete example is here.
Wiring
Since we are using the same board to generate and to decode the PWM signal, we need a jumper wire to connect the PWM Output to the PWM Input. The pinout resulting from the setup in the next section looks like this:
So, we will need to run a jumper wire from PB6 (the PWM output pin) to PA5 (the PWM input capture pin) - like this:
In this photo, a ST-Link device with serial port, and an oscilloscope probe has also been connected.
STM32CubeMX Setup
We can now begin configuring the STM32 in STM32CubeMX. As usual the clock is critical.
The 25 MHz crystal oscillator on the Black Pill can generate a 100 MHz clock, but even though USB is not needed in this example, the 48 MHz frequency needed for USB is not possible when the clock is set to 100 MHz. I therefore often use 96 MHz, as this can be used for USB as well.
The most important frequency is the APB1 timer clock. In this example that will be running at the full 96 MHz.
We will be using Timer 4 to generate the PWM signal. The setup of that timer looks like this:
By default we set the prescaler to 47 and the counter period to 1000. This will result in a frequency of 96 MHz / 48 / 1000 - 2 kHz. The pulse has been configured to 900 resulting in a duty cycle of 90 %.
According to the datasheet of the STM32F411 MCU on the Black Pill board, the following timers are available:
Since Timer 2 has a 32 bit counter, we will be using that for the input capture (lower frequencies can be measured):
A few things are important here. First of all, no prescaler is set, so the counter resolution will be the full 96 MHz internal clock. Second, the Combined PWM Input on Channel 1. Notice that both Channel 1 and Channel 2 are "greyed out". This is because Channel 1 is used a the primary channel, triggering on the rising edge, while Channel 2 counter is triggered on the falling edge (making PWM duty cycle calculation possible).
Finally we need to ensure that the global interrupt on Timer 2 is enabled:
Code
In our code, the first step is to get the PWM output going:
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1); // Output PWM Generation
The values of this timer - the prescaler and the pulse count - can be adjusted later.
Next, we need to enable our input capture timer. This is where it gets a bit interesting:
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); // Primary channel - rising edge HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_2); // Secondary channel - falling edge
We start Timer2, channel 1 in Interrupt mode, but we also start channel 2. The result is, both counters start counting on the rising edge. On the falling edge, the channel 2 counter is stopped, while the channel 1 counter continues. Finally on the next rising edge both counters can be examined. We can handle that in the interrupt callback:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { uint32_t cl = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint32_t ch = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); freq = (float) TIMER_CLOCK_FREQ / (cl + 1); duty = (float) 100 * ch / cl; } }
The global variables freq and duty will be updated continously.
To be able to test multiple frequencies the following array is defined:
uint32_t pwm_vals[][2] = { {9599, 100}, {9599, 500}, {9599, 900}, {959, 100}, {959, 500}, {959, 900}, {95, 100}, {95, 500}, {95, 900}, {47, 100}, {47, 500}, {47, 900}, };
This is simply prescaler and duty counter values. In our program main loop we got the following:
while (1) { now = HAL_GetTick(); if (now - last_print >= 1000) { DBG("Tick %4lu freq = %4.1f Hz duty = %2.1f %%", now / 1000, freq, duty); last_print = now; } if (now - last_change >= 2000) { __HAL_TIM_SET_PRESCALER(&htim4, pwm_vals[pwm_vals_count][0]); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, pwm_vals[pwm_vals_count][1]); ++pwm_vals_count; if (pwm_vals_count >= sizeof(pwm_vals) / sizeof(pwm_vals[0])) { pwm_vals_count = 0; } last_change = now; } }
In short, every second we print out the values of float and duty and every 2 seconds we change the values according to the previously defined array.
When running, the following output is generated:
Tick 1 freq = 2000.0 Hz duty = 90.0 % Tick 2 freq = 2000.0 Hz duty = 90.0 % Tick 3 freq = 10.0 Hz duty = 10.0 % Tick 4 freq = 10.0 Hz duty = 10.0 % Tick 5 freq = 10.0 Hz duty = 50.0 % Tick 6 freq = 10.0 Hz duty = 50.0 % Tick 7 freq = 10.0 Hz duty = 90.0 % Tick 8 freq = 10.0 Hz duty = 90.0 % Tick 9 freq = 100.0 Hz duty = 10.0 % Tick 10 freq = 100.0 Hz duty = 10.0 % Tick 11 freq = 100.0 Hz duty = 50.0 % Tick 12 freq = 100.0 Hz duty = 50.0 % Tick 13 freq = 100.0 Hz duty = 90.0 % Tick 14 freq = 100.0 Hz duty = 90.0 % Tick 15 freq = 1000.0 Hz duty = 10.0 % Tick 16 freq = 1000.0 Hz duty = 10.0 % Tick 17 freq = 1000.0 Hz duty = 50.0 % Tick 18 freq = 1000.0 Hz duty = 50.0 % Tick 19 freq = 1000.0 Hz duty = 90.0 % Tick 20 freq = 1000.0 Hz duty = 90.0 % Tick 21 freq = 2000.0 Hz duty = 10.0 % Tick 22 freq = 2000.0 Hz duty = 10.0 % Tick 23 freq = 2000.0 Hz duty = 50.0 % Tick 24 freq = 2000.0 Hz duty = 50.0 % Tick 25 freq = 2000.0 Hz duty = 90.0 % Tick 26 freq = 2000.0 Hz duty = 90.0 % Tick 27 freq = 10.0 Hz duty = 10.0 % Tick 28 freq = 10.0 Hz duty = 10.0 %
Which is exactly what we expected.