Frequency measurement on STM32

In this short article I want to tell you about various methods for measuring the frequency of a square wave signal using an STM32 microcontroller.

While working on one of the pieces of hardware, it became necessary to organize several pins that would measure the frequency of the input signal. After trying several different options, I decided that it was no good for the examples to gather dust on the back of the D drive and that they were worth sharing with the community. I hope someone in a similar situation will find this material useful. The material is primarily intended for beginners.

Our stand today: JDS6600 frequency generator and BluePill

Our stand today: JDS6600 frequency generator and BluePill

Initial conditions: input signal frequency from 0 to 10 kHz. Microcontroller STM32F103C8T6, the well-known bluepill board. HAL library. The source of the signal, the frequency of which will be measured, will be a two-channel frequency generator to check its functionality. JDS6600. I will use the CH340G module (USB to UART Converter) to transfer data to the terminal (terminal v1.9b) for clarity. I will send the value received after processing to it.

Let’s consider the first method – frequency measurement using a timer.

Connection: The frequency generator is directly connected to the microcontroller pins PA0 And PA2. The ground findings are combined.

Project setup: Let’s go to STM32IDE and create a project for our controller. After setting up the clock source, enable UART to send values:

UART setup

UART setup

The clock frequency was set to 64 MHz.

In the example, I will use Timer 2. Let’s configure the timer to process two signals at once:

Channel 1 is the main (direct) channel, and channel 2 is the indirect (indirect). The indirect channel does not have a separate output. Similarly for channels 3 and 4. Frequency acquisition occurs as follows – the first channel reacts to the leading edge, and we record the start time of the new period. Afterwards, the second channel records the falling edge, and we record the end time of the pulse. Then the first channel records the beginning of a new period / the end of the previous one. Having these three time points, you can calculate the period length and duty cycle. Timer 2 divisor and period value:

The prescaler value determines the step at which the timer will count. The smaller the step, the more accurate the result, but then you may encounter the fact that the captured signal will not fit into the duration of one timer period. With a timer clock frequency of 64 MHz and a prescaler = 64 (the prescaler in the program is set to 1 less), we find that the duration of one timer tick is equal to a microsecond. The period (Counter periode) determines to what value the timer will count until it overflows. In this problem, it is better to leave it at its maximum value.

The Input Filter is used to eliminate interference and noise that may occur at the timer input. It allows you to filter the input signal and take into account only stable and long-term changes in the signal, ignoring short-term unwanted interference. If the input signal is stable and not subject to strong noise, you can set the filter value to the minimum or even turn it off. However, when working with noisy or unstable input signals, it may be necessary to increase the filter value to ensure reliable and stable operation of the timer. I set the Input Filter to maximum. Let’s enable the timer interrupt:

We write the code: First, let’s create global variables to store durations:

volatile uint16_t start1 = 0, end_imp1 = 0, end_per1 = 0, start2 = 0, end_imp2 = 0, end_per2 = 0;

where the variable start will store the value of the beginning of the period, end_imp will store the value of the end of the impulse, end_ per will store the value of the end of the period. 1 and 2, respectively, CH1+CH2 and CH3+CH4.

A picture to help you understand what will be stored in each variable.

A picture to help you understand what will be stored in each variable.

volatile uint16_t period1 = 0, fill_factor1 = 0, long_imp1 = 0, period2 = 0, fill_factor2 = 0, long_imp2 = 0;
volatile uint16_t freq1 = 0, freq2 = 0;
volatile uint8_t flag_IC = 0;

Here we will store the calculated variables and set a flag indicating that a signal has arrived. Next, let’s turn on all the channels of our timer and create an array for sending via UART:

  /* USER CODE BEGIN 2 */
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2);
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_3);
	HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_4);

After the capture interrupt fires, the program will go to the callback (this function that is called after a certain event/interrupt) function, which is described below:

/* USER CODE BEGIN 4 */
/*----------------------------------------------------------------------------*/
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == TIM2)
	{
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
		{
			start1 = end_per1;
			end_per1 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
			end_imp1 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_2);
		}
		if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_3)
		{
			start2 = end_per2;
			end_per2 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_3);
			end_imp2 = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_4);
		}
		flag_IC = 1;
	}
}

It sets the flag “flag_IC”, by which we will determine the arrival of new data. In a separate function we will calculate the value of the period and duty cycle:

/*----------------------------------------------------------------------------*/
void CALC_FREQ (void)
{
	// длительность периода в импульсах (1 импульс = 1 мкс)
	if (end_per1 > start1) 
	{
		period1 = end_per1 - start1;
		long_imp1 = end_imp1 - start1;
		if (period1 > 0)
		{
			freq1 = 1000000 / period1;
			fill_factor1 = (long_imp1 * 100) / period1;
		}
	}
	if (end_per2 > start2)
	{
		period2 = end_per2 - start2;
		long_imp2 = end_imp2 - start2;
		if (period2 > 0)
		{
			freq2 = 1000000 / period2;
			fill_factor2 = (long_imp2 * 100) / period2;
		}
	}
}

The “if (end_per1 > start1)” condition is necessary in order not to calculate pulses that fall on the timer overflow, when the beginning of the period was in one timer cycle and the end in another. But calculation of such a case can also be realized.

In the main loop, we monitor the value of the flag, as soon as it becomes equal to one, we calculate the value of the frequency and duty cycle, then we send the data via UART to the terminal and reset the flag value:

if(flag_IC == 1)
	{
		CALC_FREQ();
		snprintf(buff, 80, "Freq 1 = %d fill_factor 1 = %d  Freq 2 = %d fill_factor 2 = %d\r\n", freq1, fill_factor1, freq2, fill_factor2);
		HAL_UART_Transmit_IT(&huart1, buff, strlen(buff));
		HAL_Delay(500);
		flag_IC = 0;
	}

I run the frequency generator with these settings:

We send two rectangular signals with a frequency of 1 kHz and 850 Hz. Now let’s see what came to the terminal:

In the terminal we set the speed to 115200 and get the result. Now let’s check the same code, but for a dynamic signal. I rotated the frequency value on the first channel from 1 kHz to 8 kHz and back in 1 kHz steps and this is what I got:

In principle it works, but in real systems the frequency can change much faster than I can turn the knob, so the test with a dynamic signal can correctly be considered simplified.

Advantages and disadvantages:

+

It is possible to measure the duty cycle of the pulse.

Timers are required, one timer for two inputs. A small controller for a large number of signals may simply not be enough, since many other tasks also require free timers.

Unlike other methods, it can calculate the signal frequency in 1 period (with the filter disabled).

Limiting the frequency capture range. In this example, the timer counts up to 65 ms, and if the signal period is greater than this value, for example 100 ms (10 Hz), then the timer will not be able to capture it without additional software tricks.

If the input signal is lost, the frequency value will not reset to zero on its own. You can check whether the frequency value is changing, and if not, then reset the frequency value to zero, or set some flag when it hits the callback.

Now consider the option of measuring the frequency value using EXTI pin.

The EXTI (External Interrupt) pins on STM32 microcontrollers are designed to handle external interrupts from various sources. EXTI allows you to react to changes in the state of external signals and generate interrupts to handle these events. The operating logic is quite simple – every time a pulse arrives, we will increment the value of the pulses by interruption, and once a second we will look at how many pulses have arrived. This is how we get the frequency we need.

Connection: the frequency generator is connected to pins PA0 and PA1.

Project setup: In the controller settings, we enable pins PA0 and PA1 as EXTI pins. We also enable interruption for them. The interrupt will be triggered on a rising edge of the signal.

We also need to enable a timer that will count down one second, in the interruption of which we will record our input frequency:

We write the code:

/* USER CODE BEGIN PV */
volatile uint32_t num_imp1 = 0, num_imp2 = 0;
uint32_t freq1 = 0, freq2 = 0;
volatile uint8_t  flag_timer = 0;

Let’s create variables. The first two will be incremented every time an impulse arrives. Every second, when the timer is interrupted, a flag will be displayed, when changed, we will fix the frequency value and reset the pulse counters. This is what the callback for EXTI pins will look like:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if (GPIO_Pin == GPIO_PIN_0)
	  num_imp1++;
  if (GPIO_Pin == GPIO_PIN_1)
	  num_imp2++;
} 

And like this on the timer:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
	flag_timer = 1;
	freq1 = num_imp1;
	freq2 = num_imp2;
	num_imp1 = 0;
	num_imp2 = 0;
}

The value of the number of pulses per period is reset every second. Now we send the frequency value via UART:

if (flag_timer == 1)
{
	snprintf(buff, 80, "Freq 1 = %d Freq 2 = %d\r\n", freq1, freq2);
	HAL_UART_Transmit_IT(&huart1, buff, strlen(buff));
	flag_timer = 0;
}

Let’s apply 1 kHz to the first channel and 5 kHz to the second:

Here is the result of capturing a dynamic signal (channel ch2 is static):

Great, everything works. Now about advantages and disadvantages:

+

Quite a simple method

Data arrives quite rarely – in this case, once per second

Only one timer needed

By default, we immediately get the average result

And the last method I considered is capture with ETR2. When setting the timer clock source, you can select ETR2 (External Trigger Input 2), and then the timer will use an external source as a clock source. The measured signal will be supplied to this pin.

We implement a frequency meter of the second kind. The basic principle of operation of a frequency meter of the second type is to compare the frequency of the incoming signal with the frequency generated inside the device; in our case, the duration of the period of the external signal will be measured in the number of ticks of another timer.

We start timer 2, which simply counts pulses with the highest possible frequency. When the first pulse arrives at ETR2, timer 4 starts. When, say, the hundredth pulse arrives at timer 2, we record how many pulses timer 4 counted. We divide this value by 100 and get the period of the captured signal. Picture for a little better understanding:

Project setup: Let’s set up timer 2, to which the input signal will be supplied. It counts up to the 100th pulse, and then the count is reset and a new period begins. It is at this moment that we will record the amount of time counted per 100 periods of the input signal.

Setting Timer 4 to count down time:

Both timers must have an interrupt enabled. Let’s move on to the code: create global variables:

/* USER CODE BEGIN PV */
volatile uint16_t num_per_tim = 0;    // кол-во прошедших периодов таймера
volatile uint16_t time_tim = 0;       // сюда будем записывать время с таймера, когда при-шел 10 импульс
volatile uint16_t per_tim = 0;        // сюда будем записывать кол-во периодов таймера
volatile uint8_t  flag_data = 0;      // по этому флагу будем отслеживать момент, когда пора считать значение частоты

We create a callback for data processing. The point is that timer 4 counts at a frequency greater than the input signal frequency and will overflow many times. The number of timer 4 overflows will be stored in the num_per_tim variable, so in the callback of timer 4 it is necessary to increment the timer period counter each time. And the program will enter the callback of timer 2 when all 100 pulses have arrived and the spent number of pulses of timer 4 will be recorded.

/* USER CODE BEGIN 4 */
/*----------------------------------------------------------------------------*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	// если таймер досчитал до конца, не получив все 100 импульсов на вход Timer 2 ETR2, то увеличиваем счетчик кол-ва периодов
	if (htim->Instance == TIM4)
		num_per_tim++;
	if (htim->Instance == TIM2)
	{
		time_tim = __HAL_TIM_GET_COUNTER(&htim4);
		per_tim  = num_per_tim;
		num_per_tim = 0;                                   // сброс счетчика периодов
		TIM4->CNT = 0;                                     // сброс счета таймера
		flag_data = 1;
	}
}

Now, by tracking the state of flag_data, you can read the frequency:

if (flag_data == 1)
{
	float freq = 0;
	float period = time_tim + per_tim * 65536 + 1;             // период в тиках таймера
	if (period != 0)                                           // на всякий исключим деление на ноль
	{
		period /= 100;                                         // длительность одного периода
		freq = 72000000 / period;
	}
	else freq = 0;
	snprintf(buff, 80, "Freq = %.3f\r\n", freq);
	HAL_UART_Transmit_IT(&huart1, buff, strlen(buff));
	flag_data = 0;
	HAL_Delay(500);
}

First, we calculate the total time spent, and then divide it by 100 and voila, the frequency value is ready. Let’s apply a signal with a frequency of 10 kHz to pin PA0:

Theoretically, this method should have the largest input signal frequency range. Let’s try 100 kHz:

Well, 1 MHz:

The last result has a large error, but nevertheless it works.

Advantages and disadvantages:

+

Greater range relative to others

You need a whole timer with ETR output for each individual timer for counting

Of course, not all possible methods of measuring the frequency of an input signal are described here, but only a few that I encountered. Some of them were tested only on a prototype and in real tasks they may behave differently from what I got.

For myself, I chose the option with EXTI pins, since the device does not require high speed and high accuracy, but it does require many capture channels. I liked the first method less than all the others, because in the other options we immediately get a certain average value, which suited me much better.

Link to projects

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *