Some guidance on timers and interrupts

Hi all,

I’m looking for some general guidance on timers (I think) and interrupts on an ESP32.

I have a project that will be measuring a frequency generated by a wind sensor and periodically sending that frequency and a couple of voltage measurements off to a somewhere else via a canbus controller.

The frequency being measured is pretty low, in the range of 1Hz to maybe 100 Hz.

The updates need to be displayed about once every second or two.

I feel like the correct approach is to use a timer to periodically check the current frequency and voltage but I cannot quite get my head around how all this works when I have interrupts constantly calculating the frequency.

Should I maybe use the timer to enable the interrupts, measure the frequency, disable the interrupts and then go on to grab the voltages before sending the results off on the canbus? Or do I leave the interrupts active and just use a timer to go and see what the currently recorded frequency is at?

Sorry if I mulched the language a bit here, but I’m still on a pretty steep bit of the learning curve.

Maybe there are some good examples people can point me to?
Some good descriptions of timersperhaps? I’ve read a lot on interrupts and I’ve also read the latest timer library documents, but I’m still a bit unclear on the approach.

Matt

Thank you, Millis certainly looks the way to go. (Still pondering how to handle the 50 day rollover event, but I’m sure that won’t be a problem…)

I was planning to use a pin interrupt function but now I’ve looked at millis I think I can just stick to taking a measurement at intervals. It would certainly make debugging easier.

There would be several approaches. You can just check the voltages in the loop and try to detect when the signal is at it max or min. Or better use an interrupt function, the timer or the counter.

You can attach an interrupt function to the pin. When it changes from low to high the function will be triggered. Then you check the microseconds, compare it with the previous_time (would be the amplitude), and store the current time as previous_time.
You should check if the transition from low to high of your signal works for you. Otherwise you would need a comparator to convert the sine wave into a square wave. You could need that anyway depending of the signal.

The counter will count itself the changes in the pin during a period. Then you read the count and divide it by the time elapsed since it started. And reset the counter.

The interrupts and the timer and counter run in the background. So, your loop should only check the results stored in the global variables when needed.

ESP32 simple program measuring square wave frequency

// ESP32 square wave determine pulses/sec and pulse width in microseconds

#define RXPIN 16

// pulse/sec *2 and average pulse width time
volatile long pulses = 0, average = 0;

// interrupt handler - note pulse width in microseconds
void IRAM_ATTR handleInterrupt2() {
  static long lastTimer = 0;
  long timer = micros();
  // add pulse width time in microseconds to average and increment pulse counter
  if (lastTimer) average += (timer - lastTimer);
  lastTimer = timer;    // note current time
  pulses++;             // count changes/second
}

void setup() {
  Serial.begin(115200);
  pinMode(RXPIN, INPUT_PULLDOWN);
  attachInterrupt(digitalPinToInterrupt(RXPIN), handleInterrupt2, CHANGE);
}

// display pulse/second and calculate pulse width in microseconds
void loop() {
  static long printer = millis();
  // after 10 seconds print average pulse width time
  if (millis() - printer > 10000) {
    Serial.printf("frequency %ldHz pulse width = %.2fuSec  \n",
                  pulses / 20, ((float)average / pulses));  // prin results
    average = pulses = 0;                                   // reset initialse values
    printer = millis();
  }
}

serial output for square wave 50KHz and 100KHz


frequency 50000Hz pulse width = 10.00uSec  
frequency 50000Hz pulse width = 10.00uSec  
frequency 50001Hz pulse width = 10.00uSec  

frequency 99401Hz pulse width = 5.03uSec  
frequency 99395Hz pulse width = 5.03uSec  
frequency 99397Hz pulse width = 5.03uSec

How may quantities are there that you wish to measure and store within two seconds using 3.3V ESP32 Dev Module? For example:
1. Frequency (must be a unipolar 3.3V level square wave).
2. Voltage-1 (range: 0V - 3.0V)
3. Voltage-2 (range: 0V - 3.0V)
4. ........

If you use unsigned long variables for everything related to the millis() function the integer math makes ure that the 49,7 days rollover will be handled correct

here is a demo-code that demonstrates the rollover and shows that even if a rollover happens everything is calculated correctly
if you use this condition

if( millis() - StartOfTimingPeriod >= interval) {
}
unsigned long start;
unsigned long simMillis;

void setup() {
  Serial.begin(115200);
  Serial.println("Setup-Start");
  // 4 before max 4294967295
  simMillis     = 4294967291; 
  start = simMillis;

  for (byte i = 0; i <= 10; i++) {
    Serial.print("simMillis=");
    Serial.print(simMillis);

    Serial.print("  start=");
    Serial.println(start);

    Serial.println("simMillis - start");
    Serial.print(simMillis);
    Serial.print(" - ");
    Serial.print(start);
    Serial.print(" = ");
    Serial.println(simMillis - start);
    Serial.println();
    simMillis++;  
  }
}


void loop() {
}

Wow, terrific responses here, thank you all.

I’ve got a bit to read now for sure.

Just a clarifications, not essential but may be of interest:

I’ve used a voltage comparator to turn the incoming signal from the sensor into a nice clean square wave. This was mainly to address the very low voltage variation from the signal. A LM393 chip (I think) did the job.

I’m super interested in the comment from Gromit1 about how the interrupt routine can run in the background. If that’s the case then combining some of the great code examples here will produce a very good result.

Thank you all again, I’ll try to get some coding and testing time this week to put these ideas into practice and report back on my results.

Matt

Haven't read it in detail, but apparently the MCPWM Hardware can do that.

Yes, simplifying this is how it works. You have already solved the most important topic with the comparator. The code examples and ideas in the comments should work fine.

If you attach an interrupt function to an event, e.g. the falling edge of your square wave, every time that it happens the MCU will interrupt what it was doing, execute the function, and come back silently to the point where it was.
From the point of view of the main program nothing has happened. The function was executed in the background.

Yes, but you know that because you are the programmer, not the 'main program' :wink:
And that's why I said 'simplifiying'.

Fully agree. In this case with so low frequencies it's not necessary.

Actually my first suggestion some posts above, was just to check the voltage in the loop and try find the max of the wave. And now with a square wave it's even easier.

But in this case the interrupt approach is also quite simple. And it's good to know the concept and how it works.

I like the idea of using interrupts because it allows me to keep a constant running average from the wind sensor and simply interrogate that average periodically.

The wind sensor will produce very variable results so periodic samples might end up very skewed without some good smoothing and a long sample duration.

If I know it right
micros() delivers UNsigned long
so you should use unsigned longs for everything related to the timing based on micros()

I have some questions about the details of your code
declaring a variable inside a function without static means the variable is initialised new each time the function is entered

adding the attribute "static" means keep the content of this variable alive if function is left.

Now you do

static unsigned long lastTimer = 0;

what happends with the assigning of value zero?

is it only done on first call after powerup?
is it done with each call to function handleInterrupt2()?

which would be counteracting on the attribute "static"

you are correct - variables should be unsigned long - I was measuring high frequencies and being lazy!

on the first call to handleInterrupt2() the variable lastTimer is initialized to 0 - it is then updated inside the function and retains its updated value on the next call
without the static it would be initialzed to 0 on every call of the function
could have used a global variable but as lastTimer is only used inside handleInterrupt2() I prefer to use a static local variable

updated code fixing unsigned long and changed printed value of frequency to a float to enable printing of frequencies less than 1Hz

// ESP32 square wave determine pulses/sec and pulse width in microseconds

// samples averaged over 10 seconds therefore lowest frequency to measure is 0.1Hz - if less pulses = 0

#define RXPIN 16

// pulse count and average pulse width time
volatile unsigned long pulses = 0, average = 0;

// interrupt handler - note pulse width in microseconds
void IRAM_ATTR handleInterrupt2() {
  static unsigned long lastTimer = 0;
  unsigned long timer = micros();
  // add pulse width time in microseconds to average and increment pulse counter
  if (lastTimer) average += (timer - lastTimer);
  lastTimer = timer;    // note current time
  pulses++;             // count changes/second
}

void setup() {
  Serial.begin(115200);
  pinMode(RXPIN, INPUT_PULLDOWN);
  attachInterrupt(digitalPinToInterrupt(RXPIN), handleInterrupt2, CHANGE);
}

// display pulse/second and calculate pulse width in microseconds
void loop() {
  static unsigned long printer = millis();
  // after 10 seconds print average pulse width time
  if (millis() - printer > 10000) {
    Serial.printf("frequency %.1fHz pulse width = %.2fuSec  \n",
                  ((float)pulses / 20), ((float)average / pulses));  // print results
    average = pulses = 0;                                   // reset initialse values
    printer += 10000;
  }
}

serial output at low frequencies

frequency 0.1Hz pulse width = 5000230.00uSec  
frequency 1.0Hz pulse width = 500019.00uSec  
frequency 10.0Hz pulse width = 50001.87uSec  
frequency 100.0Hz pulse width = 5000.19uSec  
frequency 1000.0Hz pulse width = 500.02uSec  

frequency and pulse width displayed OK

Not sure, but you can use the timer in capture mode:

Based on the posted reference link, the code below sends pulses out Pin 27 with a random period between 1 and 2 seconds (I chose a slow pulse rate so the print outs wouldn’t be crazy, but the timer hardware capture can handle much higher frequencies). The pulses are received on Pin 33 and the time interval between the current pulse and the previous one is printed.

#include "Arduino.h"
#include "driver/mcpwm.h"
#include "soc/rtc.h"
#include "hal/mcpwm_hal.h"

bool captureIsr(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_channel, const cap_event_data_t *edata,
                void *user_data);

void processCaptures(void *pvParameters);
void sendPulses(void *pvParameters);

constexpr uint32_t cap0IntEn = 1UL << 27;
constexpr uint8_t pulseOutPin = 27;
QueueHandle_t timerCaptureQueue;

void setup() {
  constexpr gpio_num_t capturePin = GPIO_NUM_33;
  constexpr size_t timerCaptureQueueSize = 10;
  pinMode(pulseOutPin, OUTPUT);
  digitalWrite(pulseOutPin, HIGH);

  timerCaptureQueue = xQueueCreate(timerCaptureQueueSize, sizeof(uint32_t));
  assert((timerCaptureQueue != 0) && "Failed to Create timerCaptureQueue");

  BaseType_t returnCode = xTaskCreatePinnedToCore(processCaptures, "Process Captures", 1600, NULL, 4, NULL, CONFIG_ARDUINO_RUNNING_CORE);
  assert((returnCode == pdTRUE) && "Failed to Create Process Captures");

  Serial.begin(115200);
  delay(2000);
  Serial.println("Starting");

  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_CAP_0, capturePin);
  gpio_pullup_en(capturePin);

  mcpwm_config_t pwm_config;
  pwm_config.frequency = 1000;    // Unused in this example
  pwm_config.cmpr_a = 50.0;       // Unused in this example
  pwm_config.cmpr_b = 50.0;       // Unused in this example
  pwm_config.counter_mode = MCPWM_UP_COUNTER; // Unused in this example
  pwm_config.duty_mode = MCPWM_DUTY_MODE_0; // Unused in this example
  esp_err_t result = mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);
  assert((result != ESP_FAIL) && "MCPWM Initialization Failed");

  mcpwm_capture_config_t captureConfig {
    MCPWM_NEG_EDGE,
    1,
    captureIsr,
    NULL
  };

  result = mcpwm_capture_enable_channel(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, &captureConfig);
  assert((result != ESP_FAIL) && "MCPWM Capture Enable Failed");

  returnCode = xTaskCreatePinnedToCore(sendPulses, "Send Pulses", 1600, NULL, 2, NULL, CONFIG_ARDUINO_RUNNING_CORE);
  assert((returnCode == pdTRUE) && "Failed to Create Send Pulses");
}

void loop() {
  vTaskDelete(NULL);
}

bool IRAM_ATTR captureIsr(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_channel, const cap_event_data_t *edata,
                          void *user_data) {
  if (mcpwm != MCPWM_UNIT_0) {
    return false;
  }
  uint32_t mcpwmItrStatus = MCPWM0.int_st.val;
  MCPWM0.int_clr.val = mcpwmItrStatus;
  if ((cap_channel != MCPWM_SELECT_CAP0) || (edata->cap_edge != MCPWM_NEG_EDGE)) {
    return false;
  }

  BaseType_t pxHigherPriorityTaskWoken = pdFALSE;
  uint32_t timerValue = mcpwm_capture_signal_get_value(MCPWM_UNIT_0, MCPWM_SELECT_CAP0);
  xQueueSendToBackFromISR(timerCaptureQueue, &timerValue, &pxHigherPriorityTaskWoken);

  if (pxHigherPriorityTaskWoken == pdTRUE) {
    return true;
  }
  return false;
}


void sendPulses(void *pvParameters) {
  TickType_t xLastWakeTime = xTaskGetTickCount();
  TickType_t waitTime = 1000;

  while (1) {
    vTaskDelayUntil(&xLastWakeTime, waitTime);
    digitalWrite(pulseOutPin, LOW);
    digitalWrite(pulseOutPin, HIGH);
    waitTime = 1000 + random(0, 1000);
  }
}

void processCaptures(void *pvParameters) {
  uint32_t lastTimerCapture;
  uint32_t scale = 10000000000ULL / rtc_clk_apb_freq_get();
  xQueueReceive(timerCaptureQueue, &lastTimerCapture, portMAX_DELAY);  // wait for first capture

  while (1) {
    uint32_t currentTimerCapture;
    xQueueReceive(timerCaptureQueue, &currentTimerCapture, portMAX_DELAY);
    uint64_t deltaTimer = currentTimerCapture - lastTimerCapture;
    lastTimerCapture = currentTimerCapture;
    uint32_t microSeconds = scale * deltaTimer / 10000;
    Serial.printf("Detected Edge, deltaT = %u us\n", microSeconds);
  }
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.