Skip to content

進捗ログ: 自己測定モードの設計の検討

タスク概要

宇宙線測定モードに加えて、内部タイマー割り込みを使用した自己測定モード(chrono_detector)の設計を検討しました。

このモードは以下の項目を測定します。

  • デッドタイム: イベントの不感時間
  • ジッター: タイミングの分散(標準偏差)
  • タイムスタンプ精度: micros()の精度とドリフト
  • 割り込みレイテンシー: GPIO割り込みハンドラーの応答時間
  • クロック安定性: 周波数誤差(ppm単位)
  • 電源ノイズ: ADC未使用チャンネルのRMS電圧とピークtoピーク

現在のコードベースを分析し、実装戦略を提案しました。

用語定義(用語の統一)

システム全体で使用する用語を以下のように統一しました:

Build mode(ビルド環境)

  • production: 本番環境ビルド(最適化O2、テキストコマンド無効)
  • development: 開発環境ビルド(テキストコマンド有効、タイムスタンプ有効)
  • 設定方法: platformio.ini の [env:...] セクション選択
  • 変更可能性: コンパイル時のみ

Detection mode(検出報告の動作)

  • normal: 即座報告(検出があれば直ちに報告、間隔制限なし)
  • periodic: 周期報告(検出があっても一定間隔でしか報告しない)
  • 設定方法: DEBUG_DETECTION_MODE コンパイル時フラグ
  • 変更可能性: コンパイル時のみ

Trigger mode(イベント入力源)

  • cosmic: 宇宙線検出(GPIO割り込みで物理信号を検出)
  • chrono: 自己測定(esp_timerで擬似イベントを生成)
  • 設定方法: 実行時変数 detector_mode
  • 変更可能性: ランタイム動的切り替え可能

モード組み合わせの例

  • 本番環境で宇宙線即座検出:Build=production, Detection=normal, Trigger=cosmic
  • 開発環境で自己測定テスト:Build=development, Detection=periodic, Trigger=chrono

設計概要

アーキテクチャ: 新規モジュール(chrono_detector.h/cpp)を追加して、デュアル Trigger mode 動作を実現します。

  • MODE_COSMIC: 既存の宇宙線検出(GPIO割り込みで物理信号を検出)
  • MODE_CHRONO: 内部タイマー駆動の時間精度測定(esp_timer で擬似イベント生成)

構造体: chrono_result_t

typedef struct {
  // デッドタイム測定(ISR不感時間)
  uint32_t expected_event_count;   // 期待イベント数
  uint32_t actual_event_count;     // 実測イベント数
  uint32_t lost_event_count;       // 喪失したイベント数
  uint32_t test_interval_us;       // テスト用タイマー間隔(マイクロ秒)
  float deadtime_estimate_us;      // デッドタイム推定値(μs)

  // ジッター測定
  uint32_t jitter_stddev_us;       // イベント間隔の標準偏差
  uint32_t jitter_min_us;          // 最小イベント間隔
  uint32_t jitter_max_us;          // 最大イベント間隔

  // タイムスタンプ精度
  uint64_t reference_clock_cycles; // 参照クロックサイクル数
  float timestamp_rms_error_us;    // タイムスタンプRMS誤差

  // 割り込みレイテンシー
  uint32_t int_latency_avg_us;     // 割り込み平均レイテンシー
  uint32_t int_latency_max_us;     // 割り込み最大レイテンシー

  // クロック安定性
  float frequency_error_ppm;       // 周波数誤差(ppm単位)

  // 電源ノイズ
  float power_noise_rms_mv;        // ノイズRMS電圧
  float power_noise_pp_mv;         // ノイズピーク・ツー・ピーク電圧

  uint32_t sample_count;           // 採集サンプル数
  uint32_t test_duration_ms;       // テスト実行期間(ミリ秒)
} chrono_result_t;

実装戦略

タイマーソースのオプション

  1. esp_timer(マイクロ秒精度、推奨)
  2. IDF標準API: esp_timer_create()esp_timer_get_time()
  3. 高分解能クロックとの統合が容易

  4. ハードウェアタイマー(超低ジッター代替案)

  5. IDF timer.h: timer_init()timer_set_alarm_value()
  6. より直接的な制御、やや複雑

データの収集:

  • 環形(=リング)バッファー(1024サンプル)でメモリ効率化(約12KB)
  • サンプル構造: {uint64_t timestamp_us, uint32_t gpio_state}
  • 設定可能なテスト期間(デフォルト10秒)とトリガー間隔(デフォルト100ms)

測定方法:

  • デッドタイム(ISR不感時間)
  • 方法:タイマー間隔を段階的に短縮しながらテスト
  • 期待イベント数:expected_count = (test_duration_ms × 1000) / interval_us
  • 実測イベント数:actual_count = 受信したイベント数
  • 喪失イベント数:lost = expected_count - actual_count
  • デッドタイム推定:イベント喪失が開始される臨界間隔がデッドタイムの目安
  • 例:1μs間隔では全イベント受信、5μs間隔で喪失開始 → デッドタイムは~5μs程度

  • ジッター(タイミング分散)

  • イベント間隔のばらつきを測定
  • 計算:各イベント間隔と平均値の偏差の標準偏差
  • タイマー周期安定性の指標

  • タイムスタンプ精度

  • micros()の精度を検証
  • esp_timerクロックサイクルとの比較
  • タイムスタンプドリフト測定

  • 割り込みレイテンシー

  • タイマー設定時刻と実際の割り込み発生時刻の差
  • ISRハンドラー応答時間を測定
  • 最大値・平均値を記録

  • クロック安定性

  • 周波数誤差をppm単位で測定
  • 計算:freq_error_ppm = ((expected - actual) / expected) × 1e6
  • 長時間テストでドリフトを検出

  • 電源ノイズ

  • 未使用ADCチャンネルのノイズ測定
  • RMS電圧とピーク・ツー・ピーク電圧を記録
  • 注:浮遊チャンネルのため、ボードノイズフロアの参考値として扱う

統合ポイント:

  1. main.cpp: モード選択ロジック
enum {
   MODE_COSMIC = 0,
   MODE_CHRONO = 1
};
volatile uint8_t detector_mode = MODE_COSMIC;
  1. text_command_handler.cpp: 新規コマンド
  2. CHRONO <duration_ms> <interval_ms> - テスト開始
  3. CHRONO_STATUS - 実行状態確認
  4. CHRONO_RESULTS - 結果表示

  5. config.h: オプションのチューニングパラメーター

  6. CHRONO_MAX_SAMPLES(デフォルト1024)
  7. CHRONO_DEFAULT_DURATION_MS(デフォルト10000)
  8. CHRONO_DEFAULT_INTERVAL_MS(デフォルト100)

CHRONOオプションと宇宙線検出オプションの分離

CHRONOモード用の設定パラメーターは、既存の宇宙線検出オプションと独立した設定ツリーを形成します。 以下のオプションは兼用できません:

パラメーター 用途 モード 理由
DETECT_POLL_COUNT GPIO検出強度(サンプル数) 宇宙線のみ CHRONOはesp_timerを使用し、GPIO検出を行わない
DEBUG_DETECTION_MODE 検出動作制御(本物 vs 周期) 宇宙線のみ CHRONOは完全に独立した測定システムで、常にタイマー駆動
CHRONO_DEFAULT_INTERVAL_MS イベント生成周期 CHRONOのみ esp_timerのトリガー間隔。宇宙線モードには不要
CHRONO_DEFAULT_DURATION_MS テスト実行期間 CHRONOのみ タイマー駆動テスト。宇宙線モードには不要
CHRONO_MAX_SAMPLES リングバッファーサイズ CHRONOのみ 共通バッファー構造体だが、宇宙線検出ではこのサイズを使用しない

設計原則

  1. イベント入力源の差異
  2. 宇宙線モード:GPIO割り込み(物理信号)
  3. CHRONOモード:esp_timer割り込み(ソフトウェア生成)

  4. 概念的独立性

  5. CHRONOは「内部タイマー駆動テスト」という独立した機能
  6. 宇宙線検出の設定はCHRONOの動作に影響しない
  7. 両モードは実行時に動的に切り替え可能(コンパイル時フラグではない)

  8. 保守性

  9. 設定を分離することで、各モードのパラメーター変更が他方に波及しない
  10. 新しい検出方法追加時も既存設定が保護される

メモリ&フラッシュ分析

  • RAM: 1024サンプル × 12バイト = 12KB(320KBの3.75%)
  • Flash: 約2-3KBの新規コード + esp_timerライブラリオーバーヘッド(すでにIDF内に含有)
  • 永続ストレージ不要(結果はRAM内のみ)

出力形式(人間が読みやすい形式)

=== CHRONO RESULTS ===
Duration: 10.00s, Samples: 100
Dead Time: min=99.5ms, max=100.5ms, avg=100.0ms
Jitter: stddev=0.23ms, min=99.2ms, max=100.8ms
Timestamp Accuracy: RMS=2.4μs
Interrupt Latency: avg=12.3μs, max=45.2μs
Clock Stability: ±2.3 ppm
Power Noise: RMS=18.5mV, P-P=156mV
Sample Count: 100 / Test Duration: 10000ms
=== END ===

学習事項

重要な知見

  1. 既存コードベースは機能追加に適した構造
  2. モジュール型設計(cosmic_detector、serial_communication、bme280_sensor等)により独立した追加が容易
  3. config.h経由の設定によりコード変更なしでビルド時チューニング可能
  4. テキストコマンドインフラ(text_command_handler.h)がすでに整備されている

  5. タイマー駆動アプローチでGPIO割り込み信頼性を検証

  6. esp_timerをシグナルソースとして使用し、宇宙線検出と同じGPIO ISRインフラをテスト
  7. 高分解能タイムスタンプでレイテンシー測定が可能
  8. 環形バッファー方式で統計データ取得時にRAMフットプリントを最小化

  9. 2段階測定戦略

  10. 段階1(必須): デッドタイム+ジッター+基本的なタイムスタンプ精度
  11. 段階2(発展): クロック安定性+電源ノイズ+詳細なレイテンシー分析
  12. 段階的実装とテストが可能

  13. 電源ノイズ測定の制限

  14. ADC「浮遊」チャンネルはノイズ隔離を保証しない
  15. 代替案: 専用ノイズ測定ピン(外付け抵抗分圧器)の使用、またはADC入力インピーダンス効果を受け入れる
  16. 初期版: 未使用ADCチャンネルをボードノイズフロアのプロキシとしてサンプリング

設計判断

  1. esp_timerを選択: ハードウェアタイマーより良いIDF統合、シンプルなAPI、マイクロ秒レベル測定に十分な精度
  2. 環形バッファーをストリーミング上で選択: シリアルI/Oオーバーヘッド削減、オフライン分析が可能
  3. 設定可能な期間/間隔: 1秒の簡易チェックから60秒以上の長期安定性テストまで対応
  4. 実行時のモード選択: コンパイル時プロトコルmutex(ENABLE_TEXT_COMMANDSと異なり)を回避し、動的切り替えが可能

統合設計方針(イベント入力源の共通化)

宇宙線測定モードと自己測定モードを統合し、イベント処理と解析部分を共通化する設計を採用:

[イベント入力源] ---> [共通ISR] ---> [共通バッファ/タイムスタンプ] ---> [解析loop()]

イベント入力源の切り替え:

  • 宇宙線モード: GPIO割り込み(GPIO0ボタン、またはGPIO12/19/27の検出信号)
  • 自己測定モード: esp_timerによる擬似イベント(設定可能な周期)

モード選択(フラグまたはモード変数):

enum Mode {
  MODE_COSMIC,     // 宇宙線測定モード
  MODE_CHRONO      // 時間精度測定モード
};

volatile Mode detector_mode = MODE_COSMIC;  // 実行時に切り替え可能

共通ISR本体と統合ハンドラー:

// 共通イベント処理関数(ISRから呼び出し)
void IRAM_ATTR pushEvent(uint64_t timestamp_us) {
  // Δt(イベント間隔)計算
  uint64_t delta_t_us = timestamp_us - last_event_time_us;
  last_event_time_us = timestamp_us;

  // リングバッファーに格納(タイムスタンプと間隔を記録)
  event_buffer[buffer_index] = {
    timestamp_us: timestamp_us,
    delta_t_us: delta_t_us,
    source: detector_mode  // COSMIC or CHRONO
  };

  buffer_index = (buffer_index + 1) % BUFFER_SIZE;
  event_count++;
}

// GPIO割り込みハンドラー(宇宙線モード)
void IRAM_ATTR onGPIOEvent() {
  pushEvent(micros());
}

// タイマー割り込みハンドラー(自己測定モード)
void IRAM_ATTR onTimerEvent(void* arg) {
  pushEvent(micros());
}

// 割り込み設定(実行時に切り替え可能)
void setupEventSource(Mode mode) {
  if (mode == MODE_COSMIC) {
    attachInterrupt(DETECT_PIN, onGPIOEvent, RISING);
  } else if (mode == MODE_CHRONO) {
    esp_timer_create_args_t timer_args = {
      .callback = onTimerEvent,
      .arg = NULL,
      .dispatch_method = ESP_TIMER_TASK,
      .name = "chrono_timer"
    };
    esp_timer_create(&timer_args, &chrono_timer_handle);
    esp_timer_start_periodic(chrono_timer_handle, CHRONO_INTERVAL_US);
  }
}

共通解析ルーチン(main loop):

// イベントバッファーから新規イベントを確認
bool hasNewEvent() {
  return (buffer_read_index != buffer_index);
}

// バッファからイベント間隔を取得
uint64_t getDeltaTime() {
  uint64_t delta_t = event_buffer[buffer_read_index].delta_t_us;
  buffer_read_index = (buffer_read_index + 1) % BUFFER_SIZE;
  return delta_t;
}

// デッドタイム統計を更新
void updateDeadtime(uint64_t delta_t_us) {
  deadtime_min_us = min(deadtime_min_us, delta_t_us);
  deadtime_max_us = max(deadtime_max_us, delta_t_us);
  deadtime_avg_us = (deadtime_avg_us * sample_count + delta_t_us) / (sample_count + 1);
  sample_count++;
}

// ジッター統計を更新
void updateJitter(uint64_t delta_t_us) {
  // 平均からの偏差を記録
  int64_t deviation = delta_t_us - deadtime_avg_us;
  interval_variance += (deviation * deviation);

  // 標準偏差を計算(定期的に)
  if (sample_count % 100 == 0) {
    jitter_stddev_us = sqrt(interval_variance / sample_count);
  }
}

// 最大レート(最小間隔から)をチェック
void checkMaxRate(uint64_t delta_t_us) {
  max_event_rate_hz = 1e6 / deadtime_min_us;  // 最大可能周波数

  // イベント間隔が予想より短い場合は警告
  if (detector_mode == MODE_CHRONO && delta_t_us < (CHRONO_INTERVAL_US * 0.95)) {
    // タイマー周波数がずれている可能性
    frequency_error_ppm = ((float)CHRONO_INTERVAL_US - delta_t_us) / CHRONO_INTERVAL_US * 1e6;
  }
}

// main loop の解析処理
void loop() {
  // 既存のシリアル/センサー処理...

  // 共通イベント解析(モード依存しない)
  while (hasNewEvent()) {
    uint64_t delta_t_us = getDeltaTime();

    updateDeadtime(delta_t_us);
    updateJitter(delta_t_us);
    checkMaxRate(delta_t_us);

    // 統計情報を定期的に出力
    if (sample_count % LOG_INTERVAL == 0) {
      printStatistics();
    }
  }
}

// 統計情報を出力
void printStatistics() {
  Serial.print("Dead Time: min=");
  Serial.print(deadtime_min_us);
  Serial.print("us, max=");
  Serial.print(deadtime_max_us);
  Serial.print("us, avg=");
  Serial.print(deadtime_avg_us);
  Serial.print("us | Jitter(σ)=");
  Serial.print(jitter_stddev_us);
  Serial.print("us | Max Rate=");
  Serial.print(max_event_rate_hz);
  Serial.print("Hz | Freq Error=");
  Serial.print(frequency_error_ppm);
  Serial.println("ppm");
}

メリット:

  • コード重複削減: ISRと解析ロジックが1つ
  • モード切り替え容易: イベント入力源を変更するだけで動作切り替え
  • 統計精度向上: 同じバッファー・タイムスタンプ機構を使用
  • デバッグ容易: 両モードで同じメトリクス出力形式
  • 検証容易: 宇宙線モードの信頼性を自己測定モードで定量化

次のステップ

  1. 段階1実装(必須メトリクス)
  2. chrono_detector.h/cppでデッドタイムとジッター測定を実装
  3. タイマー割り込みハンドラーを統合
  4. CHRONOCHRONO_STATUSテキストコマンドを追加
  5. シリアルコマンドで手動テスト

  6. ビルドシステムへの統合

  7. config.hにオプションENABLE_CHRONOフラグを追加
  8. platformio.iniを更新してesp_timerヘッダーを含有
  9. すべてのプロファイル(dev、debug、prod)でのコンパイルを検証

  10. 検証とテスト

  11. 既知の間隔時間での結果を手動検証
  12. タイムスタンプ精度を外部参照(例:オシロスコープ測定)と比較
  13. ヒストグラム可視化でレイテンシー分布を特性評価

  14. 段階2拡張(必要に応じて)

  15. 詳細出力用CHRONO_RESULTSコマンドを追加
  16. クロック安定性と電源ノイズ測定を実装
  17. 結果CSVエクスポート用の分析ツールを作成