Close

“Drifting pulses”

A project log for Sub-microsecond absolute timestamping

Timestamping GPIO events with GNSS high-resolution time service

ayuAyu 12/09/2025 at 10:440 Comments

2025-12-06

Capturing the PPS signal is as straightforward as setting up an any-edge interrupt on a GPIO pin and recording the system clock on ISR entry. To determine the specific edge (rising or falling), it suffices to simply read the pin's level.

#define PIN_PPS 7   // Pin number
static QueueHandle_t pps_queue;

void IRAM_ATTR pps_isr(void *_unused)
{
  unsigned t = esp_cpu_get_cycle_count();
  t = t * 2 + gpio_get_level(PIN_PPS);
    // Pack the timer value with the pin's level
  xQueueSendFromISR(pps_queue, &t, NULL);
}

void app_main(void)
{
  // ...

  pps_queue = xQueueCreate(10, sizeof(unsigned));

  gpio_config(&(gpio_config_t){
    .pin_bit_mask = (1ull << PIN_PPS),
    .mode = GPIO_MODE_INPUT,
    .intr_type = GPIO_INTR_ANYEDGE,
  });
  gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
  gpio_isr_handler_add(PIN_PPS, pps_isr, NULL);

  unsigned t;
  while (xQueueReceive(pps_queue, &t, 0)) {
    // Unpack timestamp and pin level
    printf("PPS: %9u %u\n", t / 2, t % 2);
  }

  // NOTE: NMEA message dumping omitted
}

Here is an excerpt from the log, each rising edge annotated with its difference from the previous rising edge (period):

PPS:  45414689 1
PPS:  45495902 0
PPS:  60057157 1   # diff: 14642468
PPS:  60095860 0
PPS:  74170535 1   # diff: 14113378
PPS:  74209238 0
PPS:  88283927 1   # diff: 14113392
PPS:  88322630 0
PPS: 102402459 1   # diff: 14118532
PPS: 102441712 0
PPS: 116518092 1   # diff: 14115633
PPS: 116557363 0
PPS: 130767654 1   # diff: 14249562
PPS: 130806907 0
PPS: 145020104 1   # diff: 14252450
PPS: 145059357 0
PPS: 159274266 1   # diff: 14254162
PPS: 159313519 0
PPS: 173528508 1   # diff: 14254242
PPS: 173567761 0

The system clock is 160 MHz. The ±1~2% variation in the period seems unexpectedly large; the calculated time of 90 ms is even stranger. After a few fiddling attempts, I noticed that this recorded period changes with other parts of the code. Thus, it is highly likely that the esp_cpu_get_cycle_count() counter freezes when the core is sleeping.

As of v5.5, ESP-IDF does not provide an accessible interface to read the system timer counter; the only public interface esp_timer_get_time() has a resolution of 1 us. Ideally, we need something finer to reach a more confident conclusion.

If we look into the esp_timer component, there is a private subroutine esp_timer_impl_get_counter_reg() (source) that has what we want. Replacing the timer call with this clears up all confusion:

PPS:  13370588 1
PPS:  14970609 0
PPS:  29370808 1  # diff: 16000220
PPS:  30970830 0
PPS:  45371029 1  # diff: 16000221
PPS:  46971051 0
PPS:  61371250 1  # diff: 16000221
PPS:  62971272 0
PPS:  77371472 1  # diff: 16000222
PPS:  78971493 0
PPS:  93371693 1  # diff: 16000221
PPS:  94971714 0
PPS: 109371914 1  # diff: 16000221
PPS: 110971936 0
PPS: 125372135 1  # diff: 16000221
PPS: 126972157 0
PPS: 141372357 1  # diff: 16000222

The period is 16000221 cycles (one second), consistent down to ±1 cycle. This stayed stable over the few minutes during testing. This means that, in the optimistic estimate, we might have a temporal accuracy on the order of 100, even 10 ns! Will that be real?

Corresponding commit: ba5d348

Discussions