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
Ayu
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.