Skip to content
  • Date Created: 2025-12-12
  • Last Modified: 2025-12-12

Progress Log: GNSS 1PPS Signal Support Investigation

Task Description

Investigated methods to improve GNSS timestamp accuracy from current ±10ms (centisecond-limited NMEA) to microsecond precision using the 1PPS (Pulse Per Second) signal. Examined TinyGPS++ capabilities and designed alternative approaches for hardware synchronization.

Current Limitation: NMEA GPRMC sentences provide only centisecond precision (0-99, or ±10ms resolution), which limits absolute time accuracy to ±10 milliseconds regardless of satellite fix quality.

Goal: Achieve microsecond-level precision (±100µs or better) for timestamp correlation with cosmic ray detection events across multiple detectors.

Outcome

Finding 1: TinyGPS++ Does NOT Support PPS Signals

  • Scope: TinyGPS++ library is NMEA sentence parser only
  • Supported: Parsing position, time, date, satellites, HDOP from GPRMC/GPGGA sentences
  • Not Supported: Direct PPS signal processing or interrupt handling
  • Source: GitHub Issue #53 confirms this is design limitation, not missing feature

Finding 2: Three Viable Implementation Approaches

  • Architecture: Wire PPS output to dedicated GPIO (e.g., GPIO35)
  • Implementation: Add RISING edge interrupt handler
  • Calculation: timestamp_us = (nmea_seconds * 1000000) + pps_micros_offset
  • Precision: ±100µs typical (depends on RTC crystal oscillator stability)
  • Integration: Minimal changes to existing gnss_manager.cpp
  • Effort: Low (2-3 interrupt handlers, ~100 LOC)

Option B: Replace TinyGPS++ with Dedicated GNSS Library

  • Candidates:
  • u-blox NEO-6/7/8 native binary protocol (UBX)
  • SparkFun GNSS Library (handles PPS natively)
  • ArduinoGNSS (Toshiba positioning support)
  • Precision: ±10µs possible with hardware support
  • Integration: Major refactoring of gnss_manager.cpp (breaks existing API)
  • Effort: High (full module rewrite, ~500 LOC)

Option C: Software-Only Enhancement (Fallback)

  • Method: Combine NMEA seconds + centiseconds with micros() interpolation
  • Precision: ±1ms achievable without hardware
  • Limitations: Dependent on stable system clock, drifts over hours
  • Integration: No hardware changes needed
  • Effort: Low (modify timestamp conversion functions)

Finding 3: GT502MGG GNSS Module Capability

GT502MGG multiband receiver supports:

  • UART Output: NMEA sentences (confirmed working)
  • 1PPS Output: Available on dedicated GPIO pin (per datasheet)
  • Binary Protocol: Limited UBX support (device-dependent)
  • Limitation: Datasheet does not explicitly confirm 1PPS nanosecond precision

Finding 4: Hardware Requirements for Option A

For GPIO interrupt approach:

  • 1PPS PIN: Connect GNSS module 1PPS output → ESP32 GPIO35 (or any GPIO)
  • Level Shifter: Required if GNSS module uses 5V logic (GT502MGG typically 3.3V compatible)
  • Debounce: Not needed (PPS is clean square wave, 100ms pulse width)
  • Interrupt Latency: ESP32 RISING edge interrupt ~200ns typical

Learnings

What's Feasible

  1. 1PPS interrupt handling is straightforward on ESP32 (uses standard attachInterrupt())
  2. Option A (hybrid approach) provides excellent precision/effort tradeoff
  3. Current NMEA-only code is not the limitation; hardware PPS support is available but unused

What's NOT Feasible

  1. TinyGPS++ alone cannot achieve better than centisecond precision
  2. Upgrading to binary protocols requires either new library or custom UBX parsing
  3. Software-only timing cannot exceed ±1ms without significant complexity

Critical Design Decision

  • Code Isolation: PPS handler should be separate module (pps_sync.h/cpp) to avoid coupling with GNSS parser
  • Configuration: Make PPS optional at compile-time (ENABLE_PPS=1) to maintain backward compatibility
  • API: Expose pps_get_timestamp_us() alongside gnss_get_unix_timestamp_ms()

Next Steps

  1. Add GPIO interrupt handler in new pps_sync.cpp module
  2. Modify gnss_manager.cpp to use PPS timestamp when available
  3. Test with actual GT502MGG hardware 1PPS output
  4. Verify precision against reference source (external atomic clock or NTP)

Phase 2: Integration (Conditional)

  • If Phase 1 achieves ±100µs: Merge into main firmware
  • If Phase 1 fails: Fall back to Option C (software interpolation)
  • Document PPS wiring in docs/hardware/gnss-1pps-setup.md (TBD)

Phase 3: Multi-Detector Synchronization (Future)

  • Implement time synchronization across multiple detector units using shared 1PPS reference
  • Consider NTP fallback for detectors without local 1PPS source

Configuration (Proposed)

# Enable 1PPS support (default: 0)
PLATFORMIO_BUILD_FLAGS="-DENABLE_PPS=1" task build

# With both GNSS and PPS
PLATFORMIO_BUILD_FLAGS="-DENABLE_GNSS=1 -DENABLE_PPS=1" task build

Files to Modify (Phase 1)

  1. New: include/pps_sync.h - PPS signal handler interface
  2. New: src/pps_sync.cpp - Interrupt handler and timestamp management
  3. Modify: include/gnss_manager.h - Add PPS integration point
  4. Modify: src/gnss_manager.cpp - Call PPS handler in timestamp conversion
  5. Modify: platformio.ini - Add ENABLE_PPS build flag
  6. Modify: include/config.h - Define PPS pin and default settings

Technical Deep Dive: 1PPS Signal & Implementation Details

What is 1PPS (Pulse Per Second)?

1PPS (Pulse Per Second) はGPS/GNSS受信機から出力される極度に高精度な時刻同期信号

物理的特性

項目 仕様値
ロジックレベル TTL / 3.3V
周波数 正確に 1 Hz(毎秒1回)
パルス幅 8~100 マイクロ秒
精度 12 ピコ秒~100 ナノ秒
ジッター <100ns(良質なGNSS受信機)
上昇時間 ~100 ナノ秒

なぜ1PPSが必要か?

  1. ハードウェアベースの精度: NMEA文は可変遅延(50-100ms)で通信されるが、1PPS信号は受信機の内部クロック(衛星の原子時計と同期)から直接生成される
  2. NMEAvs1PPSの精度比較:
  3. NMEA文のみ: ±10~100ミリ秒(セン蒂秒単位に限定)
  4. 1PPSのみ: ±100ナノ秒~1マイクロ秒(超高精度)
  5. 組み合わせ: NMEA(秒単位の絶対時刻)+1PPS(マイクロ秒単位のタイミング)=±5マイクロ秒の完全なタイムスタンプ

精度の実現値比較

実装方式 精度 用途
NMEA文のみ(現在) ±10ms 位置情報追跡向け
NMEA + 1PPS割り込み(推奨) ±5µs 高精度タイムスタンプ
ポーリング方式 ±100µs~ms 非推奨(実装が簡単だがジッター大)
高精度GNSSライブラリ ±10ns 必要な場合のみ

改善倍率: 1PPS併用で 2000倍の精度向上 (±10ms → ±5µs)


How 1PPS Works on ESP32(ESP32での使用方法)

一般的なフロー

GNSS受信機 1PPS出力
    ↓ (GPIO4に接続)
ESP32 GPIO
    ↓ (ハードウェア検出)
割り込みハンドラ実行
    ↓ (2~5マイクロ秒で実行)
タイムスタンプ取得 esp_timer_get_time()
    ↓
グローバル変数に保存
    ↓
メインループで使用

割り込みハンドラ実装例

#define PPS_PIN GPIO_NUM_4

// 割り込みハンドラ(必ず IRAM_ATTR属性を付ける)
void IRAM_ATTR pps_isr_handler() {
  // ステップ1: 即座にタイムスタンプ取得(最初に実行)
  g_pps_timestamp_us = esp_timer_get_time();  // 1~2マイクロ秒のオーバーヘッド

  // ステップ2: フラグを立てる
  g_pps_received = true;
}

// セットアップ
void pps_init() {
  pinMode(PPS_PIN, INPUT);
  // RISINGエッジ検出: 1PPS信号の立ち上がり(UTC秒の開始)
  attachInterrupt(digitalPinToInterrupt(PPS_PIN), pps_isr_handler, RISING);
}

なぜIRAM_ATTRが必要か?

  • IRAM_ATTR: 割り込みハンドラコードを内部RAMに置く属性
  • 理由: フラッシュメモリからの読み込み遅延(3~5µs)を回避
  • 効果: ISR遅延を約3倍短縮(5µs → 2µs)

実測値:ESP32割り込み遅延

条件 遅延時間
最小(空のISR) ~2 マイクロ秒
タイムスタンプ取得時 ~2-5 マイクロ秒
WiFi有効時 ~5-16 マイクロ秒
WiFi+スケジューラ干渉時 ~10 ミリ秒(稀)

: WiFiを一時的に無効化できれば、ほぼ常に ±2-5µs を実現可能


なぜ割り込みが必要なのか?(Polling vs Interrupt比較)

❌ ポーリング方式がダメな理由

// ANTI-PATTERN: こういう書き方はダメ
void loop() {
  if (digitalRead(PPS_PIN) == HIGH) {
    timestamp = micros();  // パルス幅の途中で取得される
  }
  delay(10);  // 10ms待つ → パルスを見落とす可能性大!
}

問題点:

  1. ループ遅延: 10msの遅延 > パルス幅(100µs)→ パルスを完全に見落とす
  2. 変動する取得時刻: パルス幅(0-100µs)の中でいつ読むか不確定 → ±50µsの誤差
  3. ジッター: ループ実行時間が変動(100µs~ms)→ タイミング精度が大幅低下

✅ 割り込み方式が優れている理由

方面 ポーリング 割り込み
応答時間 0~10ms(不確定) 2µs(決定的)
ジッター ±100µs ±5µs
パルス見落とし 高リスク ゼロ(100%検出)
精度 ±500µs~±10ms ±5µs
改善倍率 1000~2000倍

実測データ:コミュニティプロジェクト

ESP32-GPS-msec-ts (GitHub: coniferconifer) での検証結果:

ポーリングのみ:    ±10~50 ミリ秒
割り込み方式:      ±4~32 マイクロ秒

改善: 250~3000倍

定量分析:最悪ケース

10msのループ遅延を想定した場合:

  • ポーリング方式:
  • ループ遅延: 10,000 µs
  • パルス幅: 100 µs
  • 最悪誤差: ±5,000 µs = ±5 ミリ秒
  • 割り込み方式:
  • ISR遅延: 2-5 µs
  • 検出ジッター: ±5 µs
  • 最悪誤差: ±5 マイクロ秒

精度改善: 1000倍


NMEA解析と1PPS読み取りができる外部ライブラリ

ライブラリ比較表

ライブラリ NMEA解析 1PPS対応 推奨度 備考
TinyGPS++ ✅ 優秀 ❌ なし 推奨 別途割り込みコード必須(~100行)
SparkFun u-blox v3 ✅ あり ✅ あり ⚠️ 条件付き GT502MGG互換性を確認必須
ArduinoGNSS ✅ あり ⚠️ 不明 ❌ 非推奨 ESP32向けでない
MicroNMEA ✅ 軽量 ❌ なし ❌ 非推奨 TinyGPS++の方が機能豊富

1. TinyGPS++ ➜ 推奨

特性:

  • GitHub: mikalhart/TinyGPSPlus
  • メンテナンス: 活発(2008年から継続)
  • メモリ: ~240 バイト(非常に効率的)
  • 1PPS対応: ❌ なし(GitHub Issue #53で公式確認)

NMEA解析: ✅ GPRMC, GPGGA, GPGSV など完全対応

1PPS統合方法:

#include <TinyGPS++.h>
#include "pps_sync.h"  // 別途実装

TinyGPSPlus gps;

void setup() {
  Serial2.begin(9600);  // GNSS(NMEA出力)
  pps_init(GPIO_NUM_4); // PPS割り込み初期化
}

void loop() {
  // NMEA文を解析
  while (Serial2.available()) {
    gps.encode(Serial2.read());
  }

  // 検出イベント発生時
  if (detection_occurred && gps.time.isValid()) {
    // ステップ1: NMEAから秒単位の絶対時刻を取得
    uint32_t unix_seconds = convert_gps_to_unix(
      gps.date.year(), gps.date.month(), gps.date.day(),
      gps.time.hour(), gps.time.minute(), gps.time.second()
    );

    // ステップ2: 1PPSから秒内のマイクロ秒を取得
    uint64_t pps_us = pps_get_timestamp_us();
    uint32_t us_in_second = pps_us % 1000000;

    // ステップ3: 組み合わせてマイクロ秒精度タイムスタンプ完成
    uint64_t precise_timestamp_us = (unix_seconds * 1000000ULL) + us_in_second;
    // 精度: ±5 マイクロ秒!
  }
}

メリット:

  • ✅ 軽量で安定
  • ✅ どのGNSS受信機でも動作
  • ✅ 既存プロジェクトで実績豊富

デメリット:

  • ❌ 1PPS処理を自分で実装する必要あり(ただし100行程度)
  • ❌ セン蒂秒精度のみ(1PPSなしの場合)

2. SparkFun u-blox GNSS Library v3

特性:

1PPS設定例:

#include <SparkFun_u-blox_GNSS_v3.h>

SFE_UBLOX_GNSS myGNSS;

void setup() {
  // 1PPS出力を設定
  myGNSS.setTimePulseParameters(
    1000000,   // 周期(1秒)
    100000,    // パルス幅(100ms)
    0,         // GNSS時刻にロック
    0x000001F7 // フラグ
  );
}

メリット:

  • ✅ 1PPS処理が組み込まれている
  • ✅ UBX二進プロトコルで直接制御可能
  • ✅ RTKなど高度な機能も利用可能

デメリット:

  • GT502MGGとの互換性が不確定(u-bloxモジュール専用)
  • ❌ ライブラリサイズが大きい(+10KB)
  • ❌ 実装が複雑

GT502MGGについて:

  • データシートでUBXプロトコルのサポート状況を確認が必須
  • 確認前の導入は推奨しない

結論:ライブラリ選択

Kurikintons向けの推奨:

✅ Option A: TinyGPS++ + カスタムPPS割り込みハンドラ

理由:

  1. TinyGPS++は既に設計に組み込まれている
  2. 割り込みハンドラは ~100行で実装可能
  3. 任意のGNSS受信機で動作(GT502MGG互換性保証)
  4. モジュール化可能(pps_sync.cppで分離)
  5. 精度: ±5µs(SparkFunと同等)
  6. 実装難度: (数時間で完成)

⚠️ Option B: SparkFun u-blox v3

  • GT502MGG互換性が確認できれば検討
  • 確認前は使用を避けるべき

実装の具体的手順(Phase 1プロトタイプ)

Step 1: pps_sync.h を作成

#ifndef PPS_SYNC_H
#define PPS_SYNC_H

#include <stdint.h>
#include <stdbool.h>

/**
 * @brief Initialize 1PPS interrupt handler on specified GPIO pin
 * @param pin GPIO pin number (e.g., GPIO_NUM_4)
 */
void pps_init(uint8_t pin);

/**
 * @brief Get microsecond timestamp from last 1PPS edge
 * @return Microseconds since epoch (0 if no valid PPS yet)
 */
uint64_t pps_get_timestamp_us();

/**
 * @brief Check if 1PPS signal is valid (received within 2 seconds)
 * @return true if signal is being received regularly
 */
bool pps_is_valid();

/**
 * @brief Get microseconds offset within current UTC second
 * @return Microseconds (0-999999) within the second
 */
uint32_t pps_get_us_offset();

#endif

Step 2: pps_sync.cpp を実装

#include "pps_sync.h"
#include <Arduino.h>
#include <esp_timer.h>

static volatile uint64_t g_pps_timestamp_us = 0;
static volatile uint64_t g_last_pps_time = 0;
static volatile bool g_pps_received = false;

void IRAM_ATTR pps_isr_handler() {
  // タイムスタンプ取得(ISRの最初で実行)
  g_pps_timestamp_us = esp_timer_get_time();
  g_last_pps_time = millis();
  g_pps_received = true;
}

void pps_init(uint8_t pin) {
  pinMode(pin, INPUT);
  attachInterrupt(digitalPinToInterrupt(pin), pps_isr_handler, RISING);
}

uint64_t pps_get_timestamp_us() {
  return g_pps_timestamp_us;
}

bool pps_is_valid() {
  // 2秒以内にPPS信号を受信していれば有効
  return (millis() - g_last_pps_time) < 2000 && g_pps_received;
}

uint32_t pps_get_us_offset() {
  return g_pps_timestamp_us % 1000000;
}

Step 3: gnss_manager.cpp を修正

最後のgetタイムスタンプ関数に1PPSサポートを追加:

#include "pps_sync.h"

// 既存の関数を修正:1PPSがあれば使用、なければNMEAのみ使用
uint64_t gnss_get_unix_timestamp_us() {
  // NMEA文から秒単位の絶対時刻
  time_t seconds = nmea_to_unix_timestamp_seconds();
  if (seconds == 0) {
    return 0;  // 無効
  }

  // 1PPSが有効なら使用
  if (pps_is_valid()) {
    uint32_t us_offset = pps_get_us_offset();
    return (uint64_t)seconds * 1000000ULL + us_offset;
  }

  // フォールバック: NMEAのセン蒂秒を使用
  uint16_t centiseconds = gps.time.centisecond();
  return (uint64_t)seconds * 1000000ULL + (centiseconds * 10000UL);
}

Step 4: config.h にPPSピン定義を追加

// 1PPS Signal Configuration (v1.17.0+)
#define ENABLE_PPS 1              // 1PPS support enabled
#define PPS_GPIO_PIN GPIO_NUM_4   // ESP32 GPIO4 for 1PPS input

// Timing Precision with PPS
// - Without PPS: ±10 milliseconds (NMEA centisecond limited)
// - With PPS:    ±5 microseconds  (1000× improvement)

Step 5: platformio.ini にビルドフラグを追加

[env:esp32dev-dev]
build_flags =
  -DENABLE_TIMESTAMP=1
  -DENABLE_GNSS=1
  -DENABLE_PPS=1      ; 新規追加

精度検証方法(Phase 2テスト)

検証項目

  1. PPS信号の物理確認
  2. オシロスコープで1PPS出力を観察
  3. パルス幅、周期、ジッターを測定

  4. ESP32での捕捉テスト

  5. シリアルモニタでPPS受信タイムスタンプを表示
  6. 連続1時間監視して統計精度を測定

  7. NMEA + PPS統合テスト

  8. 検出イベント時のマイクロ秒タイムスタンプを出力
  9. NTP標準時刻源と比較

  10. 複数検出器間の同期テスト(将来)

  11. 複数ESP32に同じ1PPS信号を接続
  12. イベントタイムスタンプの時間差を測定