Measure time between pulses with ESP32 and interrupt

Hello All,

I'm trying to measure the time between two pulses with a ESP32 Wroom with interrupts. I have seen some examples but all use the TomerOne.h library and that one I cant use with an ESP32. So I get a bit stuck on how to fix this. Previously I did the same with a MEGA with some help here on the forum but with the MEGA I used a pin read to see which pin had changed and computed the time between change. But now I would like to do it with interrupts, could someone help me a bit to figure out how I can do this? I created one interrupt (eventually I need two) I will use it for speed measurement of a motor.

#include <CommandHandler.h>

#define PWM_A 14
#define PWM_A_Chan 0
#define AI_1 12
#define AI_2 13
#define PWM_B 25
#define PWM_B_Chan 1
#define BI_1 26
#define BI_2 27
#define PWM_Res 8
#define PWM_Freq 1000

int PWM_DutyCycle = 100;
float diskslots = 20.00;                                //Total slots on motor disk
const int RightMotorSpeedSens = 18;                    //Right motor Interrupt pin for speed sensing 
unsigned int RightSpeedCount = 0;                       //Pulse counter for right motor

CommandHandler<> SerialCmds;

void IRAM_ATTR isr()
{
  RightSpeedCount++;
}

void setup() {
 
  Serial.begin(115200);
  Serial.println(F("Program starts....."));
  
  SerialCmds.AddCommand(F("MotorSpeed"), Cmd_DriveForwards);          //Command to communicate with Megunolink
  pinMode(AI_1, OUTPUT);                                              //A motor setup input channels
  pinMode(AI_2, OUTPUT);
  pinMode(BI_1, OUTPUT);                                              //B motor setup input channels
  pinMode(BI_2, OUTPUT);
  pinMode(RightMotorSpeedSens, INPUT_PULLUP);

  ledcAttachPin(PWM_A, PWM_A_Chan);                                   //Setup A motor PWM channel
  ledcAttachPin(PWM_B, PWM_B_Chan);                                   //Setup B motor PWM channel
  ledcSetup(PWM_A_Chan, PWM_Freq, PWM_Res);
  ledcSetup(PWM_B_Chan, PWM_Freq, PWM_Res);

  attachInterrupt(digitalPinToInterrupt(RightMotorSpeedSens), isr, RISING);

}

void loop() {
  SerialCmds.Process();                                             //Monitor serial commands
}

void Cmd_DriveForwards(CommandParameter& p)                         //Function for driving forward
{
  int DutyCycle = p.NextParameterAsInteger();  
  digitalWrite(AI_1, LOW);
  digitalWrite(AI_2, HIGH);
  digitalWrite(BI_1, LOW);
  digitalWrite(BI_2, HIGH);
  ledcWrite(PWM_A_Chan, DutyCycle);
  ledcWrite(PWM_B_Chan, DutyCycle);
  Serial.println(DutyCycle);
}

The idea is record the current time when a rising edge occurs when the following rising edge occurs I could compute the difference and than I could compute the speed of the motor in RPM or m/s.

I hope someone could pont me out in the good direction, thanks in advance.

assume others have more experience with this.

looks like you're trying to compute the frequency of events -- # of interrupts over some period of time -- and from that frequency control adjust some PWM value to maintain some speed.

but i don't the the count incremented in the interrupt being used anywhere, nor do i see any time being measured (e.g. using millis()).

and after seeing a similar thread on using PWM to control motor speed i think the PID algorithm is needed because the rate of change of the PWM signal probably needs to be accounted for when attempting to maintain a constant speed

From what I remember now, millis can't be used inside an ISR.

But you can use micros if the interval between pulses is not so long.

Here an example:

#if defined(ESP8266)
void ICACHE_RAM_ATTR falling();
void ICACHE_RAM_ATTR rising();
#define PIN D8  // PIN where the PWM singal arrives
#else
#define PIN 2  // PIN where the PWM singal arrives
#endif

// global variables
volatile unsigned int pulse_width = 0; 
volatile unsigned int prev_time = 0; 
 
void rising() {
  attachInterrupt(digitalPinToInterrupt(PIN), &falling, FALLING);  // when PIN goes LOW, call falling()
  prev_time = micros();
}
 
void falling() {
  attachInterrupt(digitalPinToInterrupt(PIN), &rising, RISING);  // when PIN goes HIGH, call rising()
  pulse_width = micros() - prev_time;
  Serial.println(pulse_width);
}
 
void setup() {
  pinMode(PIN, INPUT_PULLUP);

  Serial.begin(115200);
  attachInterrupt(digitalPinToInterrupt(PIN), &rising, RISING);  // when PIN goes HIGH, call rising()
  sei();  // enable interrupts
}
 
void loop() { }

No I did not state any time yet because I did not know how to fix it with an interrupt. But you are right with what I want to accomplish :wink:

Thanks for your example I'll dive into it seems like this will help. Millis() wont do the job I need microsecond the time difference between pulses will be very small.

One thing I noticed from your code is that you use Serial.print(). But I thought this is not "allowed" with interrupts as it is a void function.
You measure the pulse width between rising and faling edges. I could measure between two rising edges but could I store those values in an array for example in the interrupt function?

Well it can... it just doesn't increment inside an interrupt. Depending on how you intend to use it this may/may not be a problem.

1 Like

Something to experiment with ...

A little bit more explanation would be appreciated.

Note that I'm using a infrared sensor that counts pulses, I need to measure the time between those pulses in order to compute the speed of the motor. The picture below shows what I'm using.

Looking to the code It might be what I need but I dont understand it fully. And I hate it to copy paste without understanding what I'm doing :smiley:

Yes, measure the time between raising and falling then between falling and rising after you will have the time between two raising.

You can create global variables to use outside of interrupt.

I don't know any problem between interrupt and serial.print.

  • Copy: Ctrl-C, Paste: Ctrl-V (couldn't resist)
  • The example basically measures the μs from rising edge to rising edge of the pulses
  • Click on the PWM breakout to change the frequency to match your IR sensor

unless instead you count the # of pulses over some period of time

in other words, you make periodic (every 100ms) adjustments instead of adjustments after each pulse

Yes your right about that!

Nevertheless I just added the code as suggested before post#7 and this works decent. I wont get a real stable value yet. And need to fix it so I can use this for two motors. But I'm on my way to nice results.

The ESP32 has a Pulse Counter module built in and does not require CPU time once it programmed to do its thing.

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/pcnt.html

Hmm that sounds interesting this means I could go a complete other direction with good mesurements. I'll have a look in to this and see if I can make it work.

i think i have the same robot platform you show in the photo with ~2in wheels and a block disc with ~20 openings for use as an encoder (usually with 2 optical sensors)

not sure you need such fine resolution (usec/msec) for likely speeds

   ft/s  rev/s  tic/s ms/tic
      1    0.2    3.2  314.2
      2    0.3    6.4  157.1
      3    0.5    9.5  104.7
      4    0.6   12.7   78.5
      5    0.8   15.9   62.8
      6    1.0   19.1   52.4
      7    1.1   22.3   44.9
      8    1.3   25.5   39.3
      9    1.4   28.6   34.9

i believe for such a platform, controlling the amount of rotation is more important than speed. the amount of rotation will determine how straight a platform moves and how to turn by incrementally moving each wheel a specific # of tics

The ESP32 cycle counters is a pico second cycle counter from which all other cycle counts are derived from. Using millis() on a ESP32 incurs the overhead of the ESP32's Arduino Core.

Instead of using millis() on a ESP32, I use esp_timer_get_time(); which returns uSeconds and overflows after 200+ years.

Example of use of esp_timer_get_time();.


void fDoMoistureDetector1( void * parameter )
{
  //wait for a mqtt connection
  while ( !MQTTclient.connected() )
  {
    vTaskDelay( 250 );
  }
  int      TimeToPublish = 5000000; //5000000uS
  int      TimeForADreading = 100 * 1000; // 100mS
  uint64_t TimePastPublish = esp_timer_get_time(); // used by publish
  uint64_t TimeADreading   = esp_timer_get_time();
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 10; //delay for 10mS
  float    RemainingMoisture = 100.0f; //prevents pump turn on during start up
  bool     pumpOn = false;
  uint64_t PumpOnTime = esp_timer_get_time();
  int      PumpRunTime = 11000000;
  uint64_t PumpOffWait = esp_timer_get_time();
  uint64_t PumpOffWaitFor = 60000000; //one minute
  float    lowMoisture = 23.0f;
  float    highMoisture = 40.0f;
  for (;;)
  {
    xSemaphoreTake( sema_WaterCactus, portMAX_DELAY );
    //read AD values every 100mS.
    if ( (esp_timer_get_time() - TimeADreading) >= TimeForADreading )
    {
      xEventGroupSetBits( eg, evtADCreading0 );
      TimeADreading = esp_timer_get_time();
    }
    xQueueReceive(xQ_RM, &RemainingMoisture, 0 ); //receive queue stuff no waiting
    //read gpio 0 is water level good. Yes: OK to run pump : no pump off.   remaining moisture good, denergize water pump otherwise energize water pump.
    if ( RemainingMoisture >= highMoisture )
    {
      WaterPump0_off();
    }
    if ( !pumpOn )
    {
      log_i( "not pump on ");
      if ( gpio_get_level( GPIO_NUM_0 ) )
      {
        if ( RemainingMoisture <= lowMoisture )
        {
          //has one minute passed since last pump energize, if so then allow motor to run
          if ( (esp_timer_get_time() - PumpOffWait) >= PumpOffWaitFor )
          {
            gpio_set_level( GPIO_NUM_5, HIGH); //open valve
            WaterPump0_on();
            log_i( "pump on " );
            pumpOn = !pumpOn;
            PumpOnTime = esp_timer_get_time();
          }
        }
        //xSemaphoreGive( sema_RemainingMoisture );
      } else {
        log_i( "water level bad " );
        WaterPump0_off();
        gpio_set_level( GPIO_NUM_5, LOW); //denergize/close valve
        PumpOffWait = esp_timer_get_time();
      }
    } else {
      /*
         pump goes on runs for X seconds then turn off, then wait PumpOffWaitTime before being allowed to energize again
      */
      if ( (esp_timer_get_time() - PumpOnTime) >= PumpRunTime )
      {
        log_i( "pump off " );
        WaterPump0_off(); // after 5 seconds turn pump off
        gpio_set_level( GPIO_NUM_5, LOW); //denergize/close valve
        pumpOn = !pumpOn;
        PumpOffWait = esp_timer_get_time();
      }
    }
    // publish to MQTT every 5000000uS
    if ( (esp_timer_get_time() - TimePastPublish) >= TimeToPublish )
    {
      xQueueOverwrite( xQ_RemainingMoistureMQTT, (void *) &RemainingMoisture );// data for mqtt publish
      TimePastPublish = esp_timer_get_time(); // get next publish time
    }
    xSemaphoreGive( sema_WaterCactus );
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete( NULL );
}// end fDoMoistureDetector1()

esp_timer_get_time(); works just like millis().

1 Like

A bit late reply sorry about that but I was not at home during the week.

Controlling the speed or rotation is basically the same. I could choos for speed control or rpm control. Important is that both wheels are the same.

The previous suggested code from @dlloyd seemed to work. But Im not there yet, the best IMHO is to take readings and start to take a moving average on them. This to filter out "weird" values. Besides of that I need the reading of two sensors and im not quite sure how to do that with interrupts.

So thats basically my goal for this weekend :slight_smile:

Using the code as @dlloyd suggested we have this:

#include <LiquidCrystal_I2C.h>

const byte   slots = 1; // no of slots in a rotating object
volatile unsigned long us, prevPulseUs, pulseUs, prevPulseUsCopy, pulseUsCopy;
volatile bool ready;
unsigned long prevMs, now, pulsePeriod, rpm;
const unsigned long slotUs = 60000000 / slots;

void IRAM_ATTR isr() {
  us = micros();
  if ((us - pulseUs) > 100) {  // debounce interval, also determines max rpm
    prevPulseUs = pulseUs;
    pulseUs = us;
  }
}

bool timeOut(unsigned long ms) {
  now = millis();
  if ((now - prevMs) >= ms) {
    prevMs = now;
    return true;
  }
  return false;
}

LiquidCrystal_I2C lcd (0x27, 16, 2);

void setup() {
  pinMode(5, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(19), isr, RISING);
  lcd.init();
  lcd.backlight();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("___TACHOMETER___");
  lcd.setCursor(0, 1);
  lcd.print("RPM: ");
}

void loop() {
  if (timeOut(250)) {  // 4 Hz update frequency
    cli(); //stop interrupts
    prevPulseUsCopy = prevPulseUs;
    pulseUsCopy = pulseUs;
    sei(); //allow interrupts
    pulsePeriod = pulseUsCopy - prevPulseUsCopy;
    if (pulsePeriod) rpm = slotUs / pulsePeriod;
    lcd.setCursor(5, 1);
    lcd.print(rpm);
    lcd.print("    ");
  }
  //delay(5);
}

I do not use a LCD, but for the sake of keeping the code complete I post all here.

Could someone explain me for example what this statement does?

if (pulsePeriod) rpm = slotUs / pulsePeriod;

I understand it is a If statement, but an If statement does something when it is "True" but here I don't see what should be true?

The example was quickly thrown together and didn't deal with max or min rpm limits.
This update is somewhat improved. Future update could detect when there's no interrupts happening (0 rpm) and use float rpm for improved resolution.