組み込みシステム・API・データベースでのUnixタイムスタンプ
RTC、NTP、単調時計、JSON、SQLを使うセンサー時刻の設計を、ESP32の同期例とともに解説します。
有用なセンサーデータには、測定値、時刻の基準、不確かさが必要です。Unix時刻は相互運用を助けますが、正確さや順序を保証しません。Unixタイムスタンプ / Epoch変換で記録を確認し、UnixタイムスタンプとEpoch時刻を理解するで表現の基礎を確認できます。
RTCとシステム時計
ハードウェアRTCは、CPUの休止中も暦やカウンターを保持し、バックアップ電源を使う場合があります。自動的にUTCや同期済みになるわけではありません。システム時計はハードウェアの時間基準とエポックを対応付けます。電源喪失後はRTCの有効性を確認し、初期値を実測日時として扱わないでください。
STM32、ESP32、組み込みLinux
STM32のRTC、バックアップ領域、秒未満レジスターは系列で異なるため、対象のリファレンスマニュアルとST AN4759を確認します。全系列共通のUnix時刻レジスターはありません。ESP-IDFは設定に応じRTCと高分解能タイマーを組み合わせます。LinuxにはCLOCK_REALTIMEと単調時計があります。CPUの幅だけでなくlibcとABIも確認してください。
NTPとSNTPによる同期
NTPは時計の差と通信遅延を推定し、SNTPはクライアント処理を簡素化します。通信経路の非対称性は誤差として残ります。NTPのエポックと時代番号はUnix秒とは違うため、変換はプロトコル実装に任せます。同期状態、最終同期からの時間、不確かさを保存します。時刻のステップ補正は突然の跳躍を生み、緩やかな補正は一時的に時計の進み方を変えます。
暦時計と単調時計
暦時計はUTC日時を示し、補正によって前後に跳ぶことがあります。単調時計は任意の原点からの経過時間を測るもので、UTC日時を直接表しません。再起動時にリセットされる場合があり、休止中の扱いも時計の種類に依存します。タイムアウトには単調時計、装置間の照合には別のUTC基準を使います。
センサーの取得時刻を記録する
測定を定義するADCサンプルや割り込みの近くで時刻を取得します。送信時刻にはバッファー、無線待機、再送が含まれ、サーバー受信時刻とは別のフィールドにします。高速のブロック取得では開始時刻、周期、連番と発振器のドリフトを考慮します。サンプリングの解説も参考になります。
イベントログとテレメトリー
装置識別子、起動識別子、連番を付けると、後方補正でUTC値が重複しても別の記録を保持できます。実測時刻と再構成した時刻を区別します。不確かさの区間が重なる装置間では、UTC順の並べ替えだけで因果関係は保証できません。
単位と誤差予算
| タイムスタンプの単位 | 1 s | 1700000000 s |
|---|---|---|
| 秒 (s) | 1 | 1700000000 |
| ミリ秒 (ms) | 1000 | 1700000000000 |
| マイクロ秒 (µs) | 1000000 | 1700000000000000 |
| ナノ秒 (ns) | 1000000000 | 1700000000000000000 |
秒は粗い予定、ミリ秒は一般的なテレメトリー、マイクロ秒は対応する取得処理、ナノ秒は対応するハードウェアに適します。分解能、繰り返し性、正確さは別の特性です。自由走行で20 ppmの誤差があると名目上の24時間で約1.728秒ずれ、温度や経年変化で変動します。
REST APIとJSON
{
"timestamp_ns": "1700000000123456789",
"captured_at": "2023-11-14T22:13:20.123456789Z",
"time_quality": "synced",
"uncertainty_ms": 20,
"boot_id": "device-session-42",
"sequence": 17
}
仕様にエポック、単位、符号、範囲を明記します。binary64を使うクライアントには、大きい値を十進文字列で送ります。BigIntはJSON.stringifyへ直接渡せません。timestamp_nsという名前は単位を明示します。未同期のUTCは架空の1970年の日付ではなく、品質情報を添えたnullとします。
ISO 8601とRFC 3339
Zまたは数値オフセットと、小数部の桁数を決めます。2023-11-14T22:13:20Zと2023-11-14T23:13:20+01:00は同じ瞬間です。末尾に地域情報がない日時ではUTCは確定しません。オフセットだけではEurope/Parisの将来の規則を保存できません。
SQLへの保存
CREATE TABLE sensor_measurement (
device_id text NOT NULL,
boot_id text NOT NULL,
sequence bigint NOT NULL,
captured_at timestamptz,
monotonic_us bigint NOT NULL,
time_quality text NOT NULL,
temperature_c double precision NOT NULL,
PRIMARY KEY (device_id, boot_id, sequence)
);
INSERT INTO sensor_measurement VALUES ('sensor-1', 'boot-42', 17,
TIMESTAMPTZ '2023-11-14T22:13:20.123456Z', 9812370, 'synced', 23.625);
この例はPostgreSQL用です。timestamptzは瞬間を保存し、セッションの地域で表示しますが、元のIANA識別子は保持しません。必要なら別に保存します。精度はマイクロ秒なので、正確なナノ秒には別の整数や十進数フィールドが必要です。他のSQLエンジンは規則が異なります。時刻インデックスと装置・起動・連番による識別は別の役割です。
UTCと現地の予定
イベントの瞬間はUTCで保存します。パリで毎日08:00という予定は、現地時刻とIANA地域が必要です。夏時間で対応するUTCが変わるためです。存在しない時刻と2回ある時刻の扱いも決めます。Unix秒を86400足しても、翌日の現地の同じ時刻になるとは限りません。
time_tの限界
sizeof(void*)からtime_tの幅を推測しないでください。符号付き32ビット秒のフィールドは、他の部分が64ビットでも2038年に到達します。ESP-IDFはv5.0以後に符号付き64ビットtime_tを使いますが、旧ファームウェアや通信形式は狭いままかもしれません。printf、SQL列、ブートローダー、RTCドライバー、OTA移行を確認します。
ESP32の例:NTP同期したセンサー記録
ESP-IDF 5.x/6.x用の次のコードは、esp-netifの初期化とWi-FiまたはEthernet接続後に1回呼びます。基板のセンサードライバーが有限の温度値をlog_sensorへ渡します。最初の同期は最大15秒待ち、取得ごとにJSONを出力します。
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "esp_netif_sntp.h"
#include "esp_timer.h"
#include "esp_random.h"
static bool clock_synced;
static int64_t last_sync_us;
static uint32_t boot_id;
static uint64_t sequence;
void ntp_after_network_ready(void) {
boot_id = esp_random();
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
esp_netif_sntp_init(&config);
clock_synced = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(15000)) == ESP_OK;
if (clock_synced) last_sync_us = esp_timer_get_time();
}
void log_sensor(double temperature_c) {
int64_t monotonic_us = esp_timer_get_time();
struct timeval tv;
bool valid = clock_synced && monotonic_us - last_sync_us < INT64_C(3600000000)
&& gettimeofday(&tv, NULL) == 0;
printf("{\"boot_id\":\"%08" PRIx32 "\",\"sequence\":%" PRIu64
",\"monotonic_us\":\"%" PRId64 "\",\"timestamp_us\":",
boot_id, sequence++, monotonic_us);
if (valid) {
int64_t utc_us = (int64_t)tv.tv_sec * INT64_C(1000000) + tv.tv_usec;
printf("\"%" PRId64 "\"", utc_us);
} else printf("null");
printf(",\"time_quality\":\"%s\",\"temperature_c\":%.3f}\n",
valid ? "synced" : "unsynced", temperature_c);
}
timestamp_usとmonotonic_usは十進文字列です。未同期ならUTCはnullでも、取得順序は保持します。1時間の有効期限は例の方針であり、精度保証ではありません。後続の自動同期で信頼状態を更新する処理は省略しています。製品では同期通知からlast_sync_usを更新し、共有状態の保護、再接続、不確かさを実装します。32ビットの乱数起動IDは例示用です。衝突が問題になる用途では永続カウンターなど十分な識別方式が必要です。
再起動、ドリフト、同期喪失
再起動は新しい単調時系列を作ります。起動IDを保持し、別の起動のカウンターを無条件につなげないでください。ネットワーク喪失中は、発振器の誤差予算と最終同期からの時間に応じて不確かさを増やします。バックアップRTCも品質状態を明示して使います。大きな補正を観測可能にし、過去の測定を黙って書き換えない設計にします。
設計と検証の要点
- すべての境界でエポック、単位、符号を定義する。
- イベントはUTC、期限は単調時計を使う。
- 連番、起動ID、同期品質を保持する。
- 未接続起動、RTC電池切れ、後方補正、夏時間、整数範囲を試験する。
- 将来の現地予定に使う地域データを更新する。
技術FAQ
NTP同期に成功すればマイクロ秒の正確さが得られますか?
通信の非対称性、実行待ち、取得遅延は残ります。全経路を測定し、不確かさを示してください。
単調カウンターだけでUTCを求められますか?
UTCとの対応、ドリフトモデル、同期の古さ、不確かさが必要です。別の再起動へ自動的には引き継げません。
現地時刻で保存するべきですか?
イベントはUTCで保存し、現地の規則や元の文脈が必要なら地域を別に保持して、表示時に変換します。
ツールと一次資料
Unixタイムスタンプ / Epoch変換 · UnixタイムスタンプとEpoch時刻を理解する · 発振器の許容差。
- IETF · RFC 3339
- IETF · RFC 5905 (NTP)
- IETF · RFC 8259 (JSON)
- Python · PEP 418
- Espressif · ESP-IDF
- STMicroelectronics · AN4759
- PostgreSQL · SQL