- 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¶
Option A: GPIO Interrupt + TinyGPS++ Hybrid (Recommended)¶
- 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¶
- 1PPS interrupt handling is straightforward on ESP32 (uses standard
attachInterrupt()) - Option A (hybrid approach) provides excellent precision/effort tradeoff
- Current NMEA-only code is not the limitation; hardware PPS support is available but unused
What's NOT Feasible¶
- TinyGPS++ alone cannot achieve better than centisecond precision
- Upgrading to binary protocols requires either new library or custom UBX parsing
- 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()alongsidegnss_get_unix_timestamp_ms()
Next Steps¶
Phase 1: Prototype (Recommended)¶
- Add GPIO interrupt handler in new
pps_sync.cppmodule - Modify
gnss_manager.cppto use PPS timestamp when available - Test with actual GT502MGG hardware 1PPS output
- 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)¶
- New:
include/pps_sync.h- PPS signal handler interface - New:
src/pps_sync.cpp- Interrupt handler and timestamp management - Modify:
include/gnss_manager.h- Add PPS integration point - Modify:
src/gnss_manager.cpp- Call PPS handler in timestamp conversion - Modify:
platformio.ini- Add ENABLE_PPS build flag - 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が必要か?¶
- ハードウェアベースの精度: NMEA文は可変遅延(50-100ms)で通信されるが、1PPS信号は受信機の内部クロック(衛星の原子時計と同期)から直接生成される
- NMEAvs1PPSの精度比較:
- NMEA文のみ: ±10~100ミリ秒(セン蒂秒単位に限定)
- 1PPSのみ: ±100ナノ秒~1マイクロ秒(超高精度)
- 組み合わせ: 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待つ → パルスを見落とす可能性大!
}
問題点:
- ループ遅延: 10msの遅延 > パルス幅(100µs)→ パルスを完全に見落とす
- 変動する取得時刻: パルス幅(0-100µs)の中でいつ読むか不確定 → ±50µsの誤差
- ジッター: ループ実行時間が変動(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¶
特性:
- GitHub: sparkfun/SparkFun_u-blox_GNSS_v3
- 1PPS対応: ✅ ネイティブ対応(設定関数あり)
- 対応モジュール: u-blox NEO-6/7/8/M10 など
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割り込みハンドラ
理由:
- TinyGPS++は既に設計に組み込まれている
- 割り込みハンドラは ~100行で実装可能
- 任意のGNSS受信機で動作(GT502MGG互換性保証)
- モジュール化可能(
pps_sync.cppで分離) - 精度: ±5µs(SparkFunと同等)
- 実装難度: 低(数時間で完成)
⚠️ 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テスト)¶
検証項目¶
- PPS信号の物理確認
- オシロスコープで1PPS出力を観察
-
パルス幅、周期、ジッターを測定
-
ESP32での捕捉テスト
- シリアルモニタでPPS受信タイムスタンプを表示
-
連続1時間監視して統計精度を測定
-
NMEA + PPS統合テスト
- 検出イベント時のマイクロ秒タイムスタンプを出力
-
NTP標準時刻源と比較
-
複数検出器間の同期テスト(将来)
- 複数ESP32に同じ1PPS信号を接続
- イベントタイムスタンプの時間差を測定