Set the starting time for ESP32 hardware timers (RTC)

Hello everyone,

I am trying to implement an algorithm for time synchronization in ESP 32 using LoRawAN.
For that I get correct value of time in microseconds from the LoRa Gateway.

My idea is to start the hardware timer from the time value I get from the gateway.
Is there anyway through which we can start the timer from a known value, and it keeps on automatically increasing as time passes there on, with the resolution of time set in the timer initialized.

I read about using timers with ESP32 in Arduino framework, but could not find any api or implementation, where they initialize a timer with value calculated in the code and then providing it as input to start the timer from there as explained in previous paragraph.

After that I would like to get the timer value to check the synchronization of time with the gateway time. Any ideas in this direction will be also a great help.

Many Thanks,
Abhishek

Because that time is the time since startup and program code shouldn't be able to modify this.
Just keep the difference between the time you got and the time returned and always add the difference to the value you get by the next call.

BTW, I have doubts that a value in microseconds make sense if transferred by LoRa.

Using the ESP32's HRT would work.


esp_timer_handle_t oneshot_timer; //variable to store the hardware timer handle


void IRAM_ATTR oneshot_timer_callback( void* arg )
{
  BaseType_t xHigherPriorityTaskWoken;
  xEventGroupSetBitsFromISR( eg, evtDoParticleRead, &xHigherPriorityTaskWoken );
} //void IRAM_ATTR oneshot_timer_callback( void* arg )
//////

voiding setup()
{
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html?highlight=hardware%20timer High Resoultion Timer API
  esp_timer_create_args_t oneshot_timer_args = {}; // initialize High Resoulition Timer (HRT) configuration structure
  oneshot_timer_args.callback = &oneshot_timer_callback; // configure for callback, name of callback function
  esp_timer_create( &oneshot_timer_args, &oneshot_timer ); // assign configuration to the HRT, receive timer handle
  //

}

void fDoParticleDetector( void * parameter )
{
  /*
    ug/m3     AQI                 Lvl AQ (Air Quality)
    (air Quality Index)
    0-35     0-50                1   Excellent
    35-75    51-100              2   Average
    75-115   101-150             3   Light pollution
    115-150  151-200             4   moderate
    150-250  201-300             5   heavy
    250-500  >=300               6   serious
  */
  float ADbits = 4095.0f;
  float uPvolts = 3.3f;
  float adcValue = 0.0f;
  float dustDensity = 0.0f;
  float Voc = 0.6f; // Set the typical output voltage, when there is zero dust.
  const float K = 0.5f; // Use the typical sensitivity in units of V per 100ug/m3.
  xEventGroupWaitBits (eg, evtWaitForBME, pdTRUE, pdTRUE, portMAX_DELAY );
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 100; //delay for mS
  for (;;)
  {
    //enable sensor led
    gpio_set_level( GPIO_NUM_4, HIGH ); // set gpio 4 to high to turn on sensor internal led for measurement
    esp_timer_start_once( oneshot_timer, 280 ); // trigger one shot timer for a 280us timeout, warm up time.
    xEventGroupWaitBits (eg, evtDoParticleRead, pdTRUE, pdTRUE, portMAX_DELAY ); // event will be triggered by the timer expiring, wait here for the 280uS
    adcValue = float( adc1_get_raw(ADC1_CHANNEL_0) ); //take a raw ADC reading from the dust sensor
    gpio_set_level( GPIO_NUM_4, LOW );//Shut off the sensor LED
    adcValue = ( adcValue * uPvolts ) / ADbits; //calculate voltage
    dustDensity = (adcValue / K) * 100.0; //convert volts to dust density
    if ( dustDensity < 0.0f )
    {
      dustDensity = 0.00f; // make negative values a 0
    }
    if ( xSemaphoreTake( sema_PublishPM, 0 ) == pdTRUE )  // don't wait for semaphore to be available
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY );
      //log_i( "ADC volts %f Dust Density = %ug / m3 ", adcValue, dustDensity ); // print the calculated voltage and dustdensity
      MQTTclient.publish( topicInsidePM, String(dustDensity).c_str() );
      xSemaphoreGive( sema_MQTT_KeepAlive );
      x_eData.PM2 = dustDensity;
    }
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
    //log_i( " high watermark % d",  uxTaskGetStackHighWaterMark( NULL ) );
  }
  vTaskDelete( NULL );
}

Setting up the HRT in setup() and using the HRT. As you might see the HRT can be retriggered with a new value.

I agree with your logic, but the need for my project is to have a RTC, always in sync with the LoRa Gateway timer counter, thus making synchronization possible. If I wait for the next call to complete and update the value, it will only be in synchronization at that particular instant.

I am calculating the offset between the gateway and lora node timer counter, but the doubt is that, if there is any way i can initialize a timer from there. because then I would be able to drift correction and speed control using the presecaler or frequency divider.

Why do you think you have to wait for the next call? You get a time value from your LoRa gateway and you store the difference you get to the millis() value you get at that moment. Every time you want to get the current time value you use millis() and add the stored difference.

I'm quite sure you wont get a time synchronization by LoRa precise enough to to make a drift correction to the internal timer which is based on crystal. At least not if you get that time synchronization more than once a day.

Unfortunately I misunderstood your suggestions before. You are correct to say that I add the offset in millis() to get synchronization. Rather I amusing micros() for more precision.

This works when I implement it on one node, but say suppose there are more then one node in the network for time synchronization. To solve and verify this, I produce a pulse every 5 seconds to see if the tick synchronization and verify time sync implementation. I fire an impulse at specific point in time using modulo(%) function, to get the right instant of time to start firing the impulse.

But the implementation is not accurate and reliable. I get variable latencies between tick, from 10ms to 22ms. Which is very high for tick synchronization. Please refer to my below code.

#pragma once

#ifndef PULSE_TRIG_OUT_H
#define PULSE_TRIG_OUT_H

#include <Arduino.h>
#include "lmic.h"
#include "hal/hal.h"
#include "timeControl.h"

/********************************************************************************************************************************************/

// forward declarations
void blink_led();
void pulsePinSetup();

// Decalare variables

const int pulseInterval = 5000000; // interval at which to blink LED (microseconds) 5 seconds
unsigned long previousMicros = 0;  // will store last time LED was updated
bool pulseStart = false;
bool whileStart;
unsigned long synctime;

/********************************************************************************************************************************************/

void pulsePinSetup()
{

  pinMode(13, OUTPUT);

}


// function to get right instant for starting the impulse after synchronisation.
// the time interval between tick is set in pulse interval parameter
void blink_led(signed long sync_time, bool status)
{

  unsigned long currentMicros = micros() + sync_time; // "sync time" is the offset measured between node local clock and LoRa gateway clock
  

  while ((status && (currentMicros - previousMicros) % pulseInterval <= 1e-1))
  {
    pulseStart = true;
    break;
  }

  if (pulseStart && (currentMicros - previousMicros) >= pulseInterval)
  {
    // Serial.println("LED HIGH");
    digitalWrite(13, HIGH);
    digitalWrite(13, LOW);

    previousMicros = currentMicros;
  }
}

/********************************************************************************************************************************************/

#endif // BLINK_LED_H

Thank you @Idahowalker ! I am trying to implement this logic. Surely learning new approaches with this idea :slight_smile:

Of course you can use micros() but you'll never get to that precision in synchronization anyway.

Can you explain what you expect that code to do?

BTW,

(currentMicros - previousMicros) % pulseInterval <= 1e-1

is identical to

(currentMicros - previousMicros) % pulseInterval == 0

Module operation is integer arithmetic and won't be able to produce an output that is smaller than 1 but not equal 0.

Do you think you get the variable latencies there? I'm still convinced that you cannot expect a higher precision from using LoRa, although I'm not an expert in this radio technology.

And if one wants to measure pulses on the ESP32 one could give the PCNT API (Pulse Counter) a try.

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

Works under the Arduino IDE.

Lets say there are 2 LoRa ESP32 nodes. They both get time from Gateway to synchronize. After adding the offset in both the nodes, I want to produce impulse from the ESP32 nodes. Such that the impulse (or tick), from both the nodes are synchronized. Meaning the on both the ESP32 nodes, the time when a Digital Pin goes high/down is same. This is one of the application i am trying to develop.

But the problem there will be at what time they should start impulse, so that it they are synchronized. Because it will be good if the synchronization remains independent of the power-on of ESP32 (which is starting point for the micros(), to start counting.)

To solve this lets assume a standard wall clock, where we want to give an output impulse, on 5th min, 10th min, 15 min, 20min........ We can see they are all exactly divisible by 5 (== pulseInterval, in the code). So after adding the offset, the code will check when the the value of time, which leaves 0 remainder or a value close to it. This make the impulses to synchronize at the same time between two ESP32 nodes. (I am hoping to get accuracy below 100 micro seconds) :slight_smile: :sweat_smile:

Maybe the graphical representation, in the current implementation will help to be more clear.

The latencies changes when compared between separate nodes synchronized with the Gateway time. But perhaps this reference could help you to understand more better, for what I am trying to implementLongShoT: long-range synchronization of time

That code doesn't do that.

It's about the same as this:

if ((status && (currentMicros - previousMicros) % pulseInterval <= 1e-1))
  {
    pulseStart = true;
  }

The break statement will end the while loop immediately, which is good because else that would be an endless loop (nothing the condition will change).

Very interesting and amazing what they did to get an extremely precise time distribution. Unfortunately there is no code in the document and no reference to it. Do you have access to that code?

So would you agree that logic with "if" is correct to synchronize the pulses in different nodes independent of their switch on time ? or Are there any other ideas I can try out ?

Unfortunately I don't have access to their code implementation. But If my implementation works, I will surely make it open source :slight_smile:

No, as I noted earlier there are other errors in that code part.
The posted code is just an excerpt. If you have just that code it won't do anything.

In the posted code you probably have to inline the currentMicros variable (into the while condition), change from micros() to millis() and change the module calculation to a simple >.
But that's a lot of guessing because I haven't seen the surrounding code.

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