The word measurement is a big word in this context. In fact all we are going to do is count the number of rising and/or falling edges of a signal for a defined time. Depending on the input signal there are various ways to do that. If you are not into counting, you should read Syed Zain Nasir’s article on measuring the high and low time of an input signal.

As always in life the easiest way is the worst: Programm a loop in which you repeatedly poll the input signal each millisecond and increment your counter if the input is high. On a good days this might even work.

int measure_frequency(int myInputPin, int num_samples=100)
{
int my_counter = 0;
for(int i = 0; i < num_samples; i++)
{
if(digialRead(myInputPin) == HIGH)
{
my_counter++;
delay(1);
}
}
return my_counter *10;
}

But this approach definitely has major issues. First of all, a changing frequency does not affect this algorithm in any regards. Neither is the time base stable. Remember the unit of frequency is Hz, which is 1/s. So time really is important when measuring frequencies.

A better solution is using an interrupt, which gets triggered every time the input signal is HIGH.

#define inputPin 3;

volatile double frequency = 0;

void IRAM_ATTR incFrequencyCounter()
{
frequency++;
} void setup() { pinMode(inputPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(inputPin),
incFrequencyCounter,
RAISING); } void loop() { Serial.println(frequency);
delay(1000);
frequency = 0; }

 

Please note that not all pins of a Arduino are usable for interrupts. Please see Arduino reference for further information on your board. There is no such limitation on ESP32. You can use just every input pin.

This solution is much better, but it still has some issues. First of all the frequency is limited to some kHz on Arduino due to its capabilities in computing interrupts. An ESP32 is much faster. Nevertheless this approach is very limited and I will show you a better way. Second, if you put millis() around the above mentioned code you will see that the execution time is not constant and as I already stated, time is an important factor when measuring frequencies.

The ESP32 gives you very neat tools on the one hand to count very fast and on the other hand to get an exact timing. The first tool is a pulse counter (PCNT) which is a software module designed for counting rising or falling edges of an input signal.

The ESP32 comes with 8 pulse counter units, each of them with two channels. Each unit channel can be configured separately with the help of struct pcnt_config_t available in header file driver/pcnt.h. The table below shows its public members.

Member nameTypeComment
pulse_gpio_numintGPIO of input signal
ctrl_gpio_numintGPIO of control signal. Setting this to 0V or 3.3V affects the counting mode
lctrl_modepcnt_ctrl_mode_tDefines what to do if ctrl_gpio_num = 0V:
PCNT_MODE_KEEP = 0 (keep counter mode)
PCNT_MODE_REVERSE = 1 (invert counter mode)
PCNT_MODE_DISABLE = 2 (keep counter value)
hctrl_modepcnt_ctrl_mode_tDefines what to do if ctrl_gpio_num = 3.3V (values see above)
pos_modepcnt_count_mode_tDefines what to do on a rising edge of the input signal:
PCNT_COUNT_DIS = 0 (counter value won’t change)
PCNT_COUNT_INC = 1 (increment counter)
PCNT_COUNT_DEC = 2 (decrement counter)
neg_modepcnt_count_mode_tDefines what to do on a falling edge of the input signal (values see above)
counter_h_limint16_tThe upper limit of the counter. After reaching this value the counter starts at 0. Though it’s a uin16_t the max value is 20.000
counter_l_limint16_tThe lower limit of the counter. After reaching this value the counter starts at 0. Though it’s a uin16_t the max value is -20.000
unitpcnt_unit_tPCNT unit number
channelpcnt_channel_tPCNT channel number

The below shown configuration can be used if you would like to count an input signal attached to ESP32 on GPIO 18 and control GPIO 19. PCNT shall count up to 20.000 on rising and falling edges of the input. ctrl_gpio_num shall be grounded. We increment on rising and falling edges just to make sure we do not miss an edge. Just keep in mind to divide the result by two.

#include "driver/pcnt.h"
pcnt_config_t pcnt_config;

pcnt_config.pulse_gpio_num = 18;
pcnt_config.ctrl_gpio_num = 19;
pcnt_config.counter_h_lim = 20000;
pcnt_config.counter_l_lim = 0;
pcnt_config.channel = PCNT_CHANNEL_0;
pcnt_config.unit = PCNT_UNIT_0;
pcnt_config.pos_mode = PCNT_COUNT_INC;
pcnt_config.neg_mode = PCNT_COUNT_INC;
pcnt_config.lctrl_mode = PCNT_MODE_KEEP;
pcnt_config.hctrl_mode = PCNT_MODE_DISABLE;

Once you have a valid configuration, you need to config the PCNT by calling:

pcnt_unit_config(pcnt_config);

You can tell PCNT what to do on various events. Events can be, e.g. reaching the upper/lower limit or you can set thresholds which trigger an event if reached. The below given snippet shows how to activate the event of reaching the upper limit and how to pause and clear the counter.

pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_H_LIM); pcnt_counter_pause(PCNT_UNIT_0); 
pcnt_counter_clear(PCNT_UNIT_0);

With this code we have a counter, which counts up to the set upper limit. What we need next is an event handler functions which counts the loops of the counter (zero to upper limit). For the sake of simplicity we assume that we have two global variables: One uint8_t which is the number of overflows and a MUX to protect us from multi-thread errors. As the maximum of uint8_t is 255, we are able to count up to 255* 20.000 = 5.1 million. If you are interested in higher frequencies you should use uint16_t as overflow counter. The maximum input frequency that can be handled by ESP32 is 40 MHz. Why 40 MHz? The pulse counter are sampled with the clock of the APB_CLK, which has a frequency of 80 MHz. You should have at least two samples per clock cycle, that makes 40 MHz maximum for the input signal.

portMUX_TYPE timer_mux = portMUX_INITIALIZER_UNLOCKED;
uint8_t overflow_counter = 0;

void IRAM_ATTR pcnt_event_handler(void* arg){
portENTER_CRITICAL_ISR(&timer_mux);
overflow_counter++;
PCNT.int_clr.val = BIT(PCNT_UNIT_0);
portEXIT_CRITICAL_ISR(&timer_mux);
}

All this function does is increment the overflow counter if the counter reached the upper limit. Next step is to introduce the event handler in our main function and enable interrupts for this PCNT unit.

pcnt_isr_register(pcnt_event_handler, NULL, 0, NULL);
pcnt_intr_enable(PCNT_UNIT_0);

So far we achieved to have a precise counter. What we now need is a tool which gives us a precise time: A timer. But before we can configure the timer, we need a callback function, which returns the PCNT counter value.

volatile double frequency = 0;
void pcnt_get_counter(void* p)
{
uint16_t result = 0;
pcnt_counter_pause(PCNT_UNIT_0);
pcnt_get_counter_value(PCNT_UNIT_0, (int16_t*) &result);
frequency = result + (overflow_counter*20000);
overflow_counter = 0;
pcnt_counter_clear(PCNT_UNIT_0);
pcnt_counter_resume(PCNT_UNIT_0);
}

Now, the timer. We need a variable of type esp_timer_create_args_t for passing arguments to the timer, a mutex and a timer handle of type esp_timer_handle_t.


esp_timer_create_args_t timer_args;
esp_timer_handle_t timer_handle;
portMUX_TYPE timer_mux;

timer_mux = portMUX_INITIALIZER_UNLOCKED;

timer_args.name = "one shot timer";
timer_args.callback = pcnt_get_counter;
timer_args.arg = (void*) this;

if(esp_timer_create(&timer_args, &timer_handle) != ESP_OK)
{
ESP_LOGE(TAG, "timer create");
}

All that is left is clearing the PCNT and trigger the timer. Once the timer has finished we can read the frequency from variable frequency which was set in the timer callback function, hence it is declared volatile:

pcnt_counter_clear(pcnt_config->unit); esp_timer_start_once(timer_handle, sensor_sample_time);