進捗ログ: 自己測定モードの設計の検討¶
タスク概要¶
宇宙線測定モードに加えて、内部タイマー割り込みを使用した自己測定モード(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;
実装戦略¶
タイマーソースのオプション:
- esp_timer(マイクロ秒精度、推奨)
- IDF標準API:
esp_timer_create()、esp_timer_get_time() -
高分解能クロックとの統合が容易
-
ハードウェアタイマー(超低ジッター代替案)
- IDF
timer.h:timer_init()、timer_set_alarm_value() - より直接的な制御、やや複雑
データの収集:
- 環形(=リング)バッファー(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電圧とピーク・ツー・ピーク電圧を記録
- 注:浮遊チャンネルのため、ボードノイズフロアの参考値として扱う
統合ポイント:
- main.cpp: モード選択ロジック
enum {
MODE_COSMIC = 0,
MODE_CHRONO = 1
};
volatile uint8_t detector_mode = MODE_COSMIC;
- text_command_handler.cpp: 新規コマンド
CHRONO <duration_ms> <interval_ms>- テスト開始CHRONO_STATUS- 実行状態確認-
CHRONO_RESULTS- 結果表示 -
config.h: オプションのチューニングパラメーター
CHRONO_MAX_SAMPLES(デフォルト1024)CHRONO_DEFAULT_DURATION_MS(デフォルト10000)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のみ | 共通バッファー構造体だが、宇宙線検出ではこのサイズを使用しない |
設計原則:
- イベント入力源の差異:
- 宇宙線モード:GPIO割り込み(物理信号)
-
CHRONOモード:esp_timer割り込み(ソフトウェア生成)
-
概念的独立性:
- CHRONOは「内部タイマー駆動テスト」という独立した機能
- 宇宙線検出の設定はCHRONOの動作に影響しない
-
両モードは実行時に動的に切り替え可能(コンパイル時フラグではない)
-
保守性:
- 設定を分離することで、各モードのパラメーター変更が他方に波及しない
- 新しい検出方法追加時も既存設定が保護される
メモリ&フラッシュ分析¶
- 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 ===
学習事項¶
重要な知見¶
- 既存コードベースは機能追加に適した構造
- モジュール型設計(cosmic_detector、serial_communication、bme280_sensor等)により独立した追加が容易
config.h経由の設定によりコード変更なしでビルド時チューニング可能-
テキストコマンドインフラ(
text_command_handler.h)がすでに整備されている -
タイマー駆動アプローチでGPIO割り込み信頼性を検証
esp_timerをシグナルソースとして使用し、宇宙線検出と同じGPIO ISRインフラをテスト- 高分解能タイムスタンプでレイテンシー測定が可能
-
環形バッファー方式で統計データ取得時にRAMフットプリントを最小化
-
2段階測定戦略
- 段階1(必須): デッドタイム+ジッター+基本的なタイムスタンプ精度
- 段階2(発展): クロック安定性+電源ノイズ+詳細なレイテンシー分析
-
段階的実装とテストが可能
-
電源ノイズ測定の制限
- ADC「浮遊」チャンネルはノイズ隔離を保証しない
- 代替案: 専用ノイズ測定ピン(外付け抵抗分圧器)の使用、またはADC入力インピーダンス効果を受け入れる
- 初期版: 未使用ADCチャンネルをボードノイズフロアのプロキシとしてサンプリング
設計判断¶
- esp_timerを選択: ハードウェアタイマーより良いIDF統合、シンプルなAPI、マイクロ秒レベル測定に十分な精度
- 環形バッファーをストリーミング上で選択: シリアルI/Oオーバーヘッド削減、オフライン分析が可能
- 設定可能な期間/間隔: 1秒の簡易チェックから60秒以上の長期安定性テストまで対応
- 実行時のモード選択: コンパイル時プロトコル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実装(必須メトリクス)
chrono_detector.h/cppでデッドタイムとジッター測定を実装- タイマー割り込みハンドラーを統合
CHRONOとCHRONO_STATUSテキストコマンドを追加-
シリアルコマンドで手動テスト
-
ビルドシステムへの統合
config.hにオプションENABLE_CHRONOフラグを追加platformio.iniを更新してesp_timerヘッダーを含有-
すべてのプロファイル(dev、debug、prod)でのコンパイルを検証
-
検証とテスト
- 既知の間隔時間での結果を手動検証
- タイムスタンプ精度を外部参照(例:オシロスコープ測定)と比較
-
ヒストグラム可視化でレイテンシー分布を特性評価
-
段階2拡張(必要に応じて)
- 詳細出力用
CHRONO_RESULTSコマンドを追加 - クロック安定性と電源ノイズ測定を実装
- 結果CSVエクスポート用の分析ツールを作成