Measuring pulse duration more accurately with ESP32

Hi everyone,

I would like to measure a pulse duration of 32us (4 clock cycles at 125kHz). Therefor I set an interrupt on the according pin. The interrupt is triggered with the rising and falling edge and store the start & endtime with the funktion micros() in two diffrent variables. The calculation of the duration take place later.

However, I just found out that I have +/- 1 microsecond delay and I would like to avoid this.

I already tried to use the function pulseIn(). Here I found out, that my ESP32 don't like this function in a ISR (make sense to me) but if I use it in my main loop I get in trouble with my UART interface ( Later, the duration should only be printed if the ESP receives a corresponding message via the UART interface). So I guess that's not working for my code...

Hopefully someone have a soulution for my problem. Maybe I should mention that I just started with programming microcontrollers....

I am happy about every tip!!

Vanessa234

One way to a solution would be to provide your code in code tags so we can possibly see where the error is. But if you'll not be posting your code in code tags then the issue is line 12056 of your code.

The ESP32 can detect a 12.5 nanosecond pulse. I'd not use the micros() with an ESP32, I'd use the built in ESP32 micros counter instead.

I'm sorry. That is my whole code the reason therefor ist, that if I just extract the IRAM_ATTR TCPulseISR(), calculatepulse() and my "UART Switch" it works fine...

Maybe I should explain what my code is doing: The ESP is used as a slave in an SPI communication. In the first step, it receives 16 bits from the master and in the next transaction, the ESP sends 18x 16 bits back to the master. Since there have already been timing problems with the Chip Select Signal, a workaround was necessary. With this workaround, a continuous CS signal is then generated so that the ESP can send the 18x 16 bits without any problems. After those transactions the master sends a pulse to the ESP, which I would like to measure...

#include <ESP32SPISlave.h>

ESP32SPISlave slave;

#define RXD2 32 //GPIO32
#define TXD2 33 //GPIO33

static constexpr uint8_t BUFFER_SIZE {37};
uint8_t spi_slave_tx_buf[BUFFER_SIZE];
uint8_t spi_slave_rx_buf[BUFFER_SIZE];
uint8_t spi_slave_rx_backup_buf[BUFFER_SIZE];

uint8_t TCMirrorMSB1 = 0;
uint8_t TCMirrorMSB2 = 0;
uint8_t TCMirrorLSB1 = 0;
uint8_t TCMirrorLSB2 = 0;
uint8_t TMData[37] = { 0x0A, 0x0A, 0x10, 0x10, 0x2A, 0x2A, 0x3F, 0x3F, 0x4A, 0x4A, 0x5F, 0x5F, 0x6A, 0x6A, 0x7F, 0x7F, 0x8A, 0x8A, 0x9F, 0x9F, 0xAA, 0xAA, 0xBF, 0xBF, 0xCA, 0xCA, 0xDF, 0xDF, 0 , 0, 0, 0, 0xEA, 0xEA, 0xFF, 0xFF};

uint8_t messageEGSE[15];
uint8_t message[15];
uint8_t sum = 0;
uint8_t previous = 0;
uint8_t j = 0;

uint8_t counterLADU = 0;


int Direction = 17;
uint8_t DirectionSignal = 1;

bool laduCS = true;
bool help = false;
bool addTR = false;

bool pulse = false;
bool printtime = false;

unsigned long starttime = 0;
unsigned long endtime = 0;
unsigned long pulseduration;
uint8_t durationLSB = 0;
uint8_t durationMSB = 0;
uint8_t TCexecuted = 0;

unsigned long startcycle = 0;
unsigned long endcycle = 0;

// ISR for TC Pulse
void IRAM_ATTR TCPulseISR() {
  pulse = (GPIO.in >> 4) & 1;       //if PIN == HIGH -> pulse = true
  if (pulse == true) {
    starttime = micros();
    TCexecuted = 1;
    printtime = false;
  } else {
    endtime = micros();
    printtime = true;
  }
}


void IRAM_ATTR laduCSISR() {

  laduCS = false;

  if (DirectionSignal == HIGH) {
    if (help == false) {
      GPIO.out_w1tc = 1 << 5;
      help = true;
    } else {
      GPIO.out_w1ts = 1 << 5;
      help = false;
      addTR = true;
    }
  }

  if (DirectionSignal == LOW) {
    if (counterLADU < 36) {
      counterLADU = counterLADU + 1;
      GPIO.out_w1tc = 1 << 5; // PIN 5 = 0
    }
    if (counterLADU > 35) {
      counterLADU = 0;
      GPIO.out_w1ts = 1 << 5; //PIN 5 = 1
      laduCS = true;      //otherwise laduCS is not updatet due to position of pin query in ISR
    }
  } //End LOW
} // End ISR

void resetMessage() {
  memset(message, 0, 14);
  message[0] = 0x24;
  message[2] = 0xE;
  message[4] = 1;
  message[13] = 0x0D;
}

//Build the Checksum of the Message
void checksum() {
  previous = message[0] ^ message[1];
  for (uint8_t i = 2; i < 11; i++) {
    sum = previous ^ message[i];
    previous = sum;
  }
  message[11] = (sum & 0xF0) >> 4;
  message[12] = (sum & 0x0F);
}

//Send received data via UART
void sendTC() {
  message[6] = 0;
  message[7] = (spi_slave_rx_backup_buf[0] & 0xF0) >> 4;
  message[8] = (spi_slave_rx_backup_buf[0] & 0x0F);
  message[9] = (spi_slave_rx_backup_buf[1] & 0xF0) >> 4;
  message[10] = (spi_slave_rx_backup_buf[1] & 0x0F);
  checksum();

  Serial2.write(message[0]);
  for (uint8_t i = 1; i < 13; i++) {
    Serial2.print(message[i], HEX);
  }
  Serial2.write(message[13]);
}

//calculate and transmit pulse duration via UART
void calculatepulse() {
  if (printtime == true) {
    pulseduration = endtime - starttime;
    durationLSB = (pulseduration & 0x00FF);
    durationMSB = (pulseduration & 0xFF00) >> 8;
    printtime = false;
  }
//  message[6] = 1;
//  message[7] = (durationMSB & 0xF0) >> 4;
//  message[8] = (durationMSB & 0x0F);
//  message[9] = (durationLSB & 0xF0) >> 4;
//  message[10] = (durationLSB & 0x0F);
//  checksum();
//  //Serial2.write(message, 12);
//  Serial2.write(message[0]);
//  for (uint8_t i = 1; i < 13; i++) {
//    Serial2.print(message[i], HEX);
//  }
//  Serial2.write("\r");
    Serial.println(pulseduration);
}

//send TCexecuted Flag via UART
void sendFlag() {
  message[6] = 2;
  message[10] = TCexecuted;
  checksum();
  //Serial2.write(message, 12);
  Serial2.write(message[0]);
  for (uint8_t i = 1; i < 13; i++) {
    Serial2.print(message[i], HEX);
  }
  Serial2.write("\r");
}

//Send TM Data
void sendTMData() {
  for (uint8_t k = 0; k < 36; k = k + 2) {
    message[5] = ((3 + j) & 0xF0) >> 4;
    message[6] = ((3 + j) & 0x0F);
    message[7] = (TMData[k] & 0xF0) >> 4;
    message[8] = (TMData[k] & 0x0F);
    message[9] = (TMData[k + 1] & 0xF0) >> 4;
    message[10] = (TMData[k + 1] & 0x0F);
    checksum();
    // Serial2.write(message, 12);
    Serial2.write(message[0]);
    for (uint8_t i = 1; i < 13; i++) {
      Serial2.print(message[i], HEX);
    }
    Serial2.write("\r");
    resetMessage();
    j++;
  }
  j = 0;
}

void sendTMDataN(uint8_t caseNumber, uint8_t n) {
  message[5] = (caseNumber & 0xF0) >> 4;
  message[6] = (caseNumber & 0x0F);
  message[7] = (TMData[n] & 0xF0) >> 4;
  message[8] = (TMData[n] & 0x0F);
  message[9] = (TMData[n + 1] & 0xF0) >> 4;
  message[10] = (TMData[n + 1] & 0x0F);
  checksum();
  //Serial2.write(message, 12);
  Serial2.write(message[0]);
  for (uint8_t i = 1; i < 13; i++) {
    Serial2.print(message[i], HEX);
  }
  Serial2.write("\r");
  resetMessage();
}

void setup() {
  Serial.begin(115200);
  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
  delay(2000);

  // begin() after setting
  // HSPI = CS: 15, CLK: 14, MOSI: 13, MISO: 12
  slave.setDataMode(SPI_MODE2);   //CPOL = 1, CPHA = 0
  slave.setQueueSize(1);          // transaction queue size
  slave.begin();                  // default SPI is HSPI


  // clear buffers
  memset(spi_slave_tx_buf, 0, BUFFER_SIZE);
  memset(spi_slave_rx_buf, 0, BUFFER_SIZE);
  memset(spi_slave_rx_backup_buf, 0, BUFFER_SIZE);

  //clear and define UART Messages
  memset(messageEGSE, 0, 14);
  memset(message, 0, 14);
  message[0] = 0x24;
  message[2] = 0xE;
  message[4] = 1;
  message[13] = 0x0D;

  //Direction signal
  pinMode(Direction, INPUT);

  pinMode(16, INPUT);
  attachInterrupt(16, laduCSISR, CHANGE);

  pinMode(5, OUTPUT);
  GPIO.out_w1ts = 1 << 5;      //CS active LOW, set CS HIGH

  // TC Pulse
  pinMode(4, INPUT);
  attachInterrupt(4, TCPulseISR, CHANGE);

}


void loop() {

  //Send every Second: Backupbuf, pulseduration, TMData
//  endcycle = millis();
//  if (endcycle - startcycle > 2000) { //1s
//    sendTC();
//    resetMessage();
//    calculatepulse();
//    resetMessage();
//    sendTMData();
//    sendTMDataN(16, 26);
//    startcycle = millis();
//  }

  DirectionSignal = (GPIO.in >> 17) & 1; //pin query
  while (laduCS == false) {            //as long as LaduCS is low
    DirectionSignal = (GPIO.in >> 17) & 1; //pin query

    //If Direction HIGH, send no Data to Master. Store Recieved Data in spi_slave_rx_backup_buf
    if (DirectionSignal == HIGH) {
      if (addTR == true) {
        TCexecuted = 0;
        memcpy(spi_slave_rx_backup_buf, spi_slave_rx_buf, 2);

        //Define TC Mirror first(MSB) and second (LSB) Byte with Identifier
        TCMirrorMSB1 = 0b11101000 | ((spi_slave_rx_backup_buf[0] & 0b11000000) >> 6);
        TCMirrorMSB2 = 0b00000001 | ((spi_slave_rx_backup_buf[0] & 0b00111111) << 2);
        TCMirrorLSB1 = 0b11100100 | ((spi_slave_rx_backup_buf[1] & 0b11000000) >> 6);
        TCMirrorLSB2 = 0b00000001 | ((spi_slave_rx_backup_buf[1] & 0b00111111) << 2);


        //Init Mirror
        TMData[28] = TCMirrorMSB1; //15 TCMirror MSB
        TMData[29] = TCMirrorMSB2;
        TMData[30] = TCMirrorLSB1; //16 TCMirror LSB
        TMData[31] = TCMirrorLSB2;

        for (uint8_t i = 0; i < 36; i++) {
          spi_slave_tx_buf[i] = TMData[i];
        }

        slave.queue(spi_slave_rx_buf, spi_slave_tx_buf, BUFFER_SIZE); //Add Transaction to queue

        addTR = false;
        laduCS = true;
      }// End Add Transaction
    }// End Direction HIGH

    // If Direction LOW, compare Data in spi_slave_rx_backup_buf and send Data to Master
    if (DirectionSignal == LOW) {

      if (slave.remained() == 0) { //is there no transaction in queue?

        slave.queue(spi_slave_rx_buf, spi_slave_tx_buf, BUFFER_SIZE); //add transaction
      } //end if remained
    }// end if LOW


    // if transaction has completed from master,
    // available() returns size of results of transaction,
    // and buffer is automatically updated
  }//end while LaduCS is low

  while (slave.available()) {
    slave.queue(spi_slave_rx_buf, spi_slave_tx_buf, BUFFER_SIZE); //add transaction
    slave.pop();
  }//end while available
 
  
  while (Serial2.available()) {

    Serial2.readBytes(messageEGSE, 14);
    switch (messageEGSE[6]) {
      case 0: sendTC();
              resetMessage(); break;
      case 1: calculatepulse();
              resetMessage(); break;
      case 2: sendFlag();
        resetMessage(); break;
      case 3: sendTMDataN(3, 0); break;
      case 4: sendTMDataN(4, 2); break;
      case 5: sendTMDataN(5, 4); break;
      case 6: sendTMDataN(6, 6); break;
      case 7: sendTMDataN(7, 8); break;
      case 8: sendTMDataN(8, 10); break;
      case 9: sendTMDataN(9, 12); break;
      case 10: sendTMDataN(10, 14); break;
      case 11: sendTMDataN(11, 16); break;
      case 12: sendTMDataN(12, 18); break;
      case 13: sendTMDataN(13, 20); break;
      case 14: sendTMDataN(14, 22); break;
      case 15: sendTMDataN(15, 24); break;
      case 16: sendTMDataN(16, 26); break;
      case 17: sendTMDataN(17, 28); break;
      case 18: sendTMDataN(18, 30); break;
      case 19: sendTMDataN(19, 32); break;
      case 20: sendTMDataN(20, 34); break;
      default: Serial.println("Error in cmd");
    }
  }

} //end loop

And in order to be able to test more easily, I have commented out some parts

Has the OP considered using freeRTOS for better multitasking performance?

The code in the ISR is not all that cpu intensive, I'd keep the code in the ISR.

Here is an example of my use of the ESP32's microsecond timer, esp_timer_get_time(). Every Arduino ESP32 core functions you can bypass will save a little bit of processing time. A call to the Arduino ESP32 core micros() function goes through the Arduino IDE core layer.

Next with loop() contain

void fDoMoistureDetector( 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 (;;)
  {
    //read AD values every 100mS.
    if ( (esp_timer_get_time() - TimeADreading) >= TimeForADreading )
    {
      xEventGroupSetBits( eg, evtADCreading );
      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 )
          {
            WaterPump0_on();
            log_i( "pump on " );
            pumpOn = !pumpOn;
            PumpOnTime = esp_timer_get_time();
          }
        }
        //xSemaphoreGive( sema_RemainingMoisture );
      } else {
        log_i( "water level bad " );
        WaterPump0_off();
        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
        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
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete( NULL );
}// end fDoMoistureDetector()
////

And including BaseType_t xHigherPriorityTaskWoken; should let the OS know something else is afoot, tend to it first.


void IRAM_ATTR onTimer()
{
  BaseType_t xHigherPriorityTaskWoken;
  xEventGroupSetBitsFromISR(eg, OneMinuteGroup, &xHigherPriorityTaskWoken);
} // void IRAM_ATTR onTimer()

Those are my suggestions for better performance with my strongest suggestion is to use freeRTOS for better ESP32 performance.

Remember the ESP32 starts up with an OS that runs the Arduino ESP32 core, bypassing the Arduino core does improve performance and using freeRTOS takes advantage of the OS.

1 Like

I just included your two tips in my code, but I still have the us delay. Unfortunately, there will be no way around freeRTOS...

Nevertheless, thank you very much!

Has the OP considered commenting out all the code but the code necessary to just detect the pulse in to see if the delay goes away. If it does then add in one code block at a time till the culprit is fund out? The ESP32 has a 12.5 nano second timing resolution.

This is not surprising because micros () has a resolution of 1 microsecond. For greater accuracy, you may need something from the following link.

https://sub.nanona.fi/esp8266/timing-and-ticks.html

As an FYI esp_timer_get_time() has a resolution of 12.5 nano seconds.

Ok. I didn't understand it before.

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