ArduinoとRTC(DS3231)で電源を切っても狂わない時計を作る!PCの時刻をPythonで合わせる方法
「Arduinoで時計を作ったのに、電源を抜いたら時刻がリセットされた…」「millis() で時間を数えていたら、思ったよりズレていた」
データロガーや自動水やりのように「何時に何をするか」が大事な工作では、正しい時刻を持ち続けるのが意外と難しいですよね。
そこで登場するのが RTC(リアルタイムクロック)モジュール です。この記事では、定番の DS3231 を Arduino UNO につないで時刻を表示し、さらに PC の時刻を Python から書き込んで秒単位で合わせる ところまで作ります。
- 難易度:★★☆(Arduino への書き込みと、Python の基本がわかればOK)
- 対象:Arduino 入門〜初級の方、Python でシリアル通信を試したい方
- 使うもの:Arduino UNO、DS3231 モジュール(電池ホルダー付き)、ジャンパーワイヤー、PC(Python 3.13 と pyserial 3.5 で確認)
実機がなくても試せるように、Arduino の代わりをする Python プログラムも用意しました。一緒に見ていきましょう!
RTC(リアルタイムクロック)とは?

RTC は、イメージとしては 「電池で動き続ける小さな腕時計」 です。Arduino 本体の電源が切れても、モジュールに付いたボタン電池(CR2032 など)で時を刻み続けます。
Arduino の millis() は「起動してから何ミリ秒たったか」を数えるだけなので、電源を切ればゼロに戻りますし、今日が何日かもわかりません。RTC なら 年・月・日・時・分・秒を、いつでも読み出せます。
DS3231 は、温度によるズレを自動で補正する水晶発振回路を内蔵しているのが特長です。データシートでは精度が ±2ppm(0〜40℃)、±3.5ppm(-40〜85℃) とされています。
±2ppm って、どれくらいズレるの?
ppm は「100万分の1」のことです。つまり ±2ppm は「100万秒で最大2秒ズレる」という意味です。…と言われてもピンときませんよね。Python で1日・30日・1年あたりに直してみましょう👇
# 時計のズレ(ppm)が、1日・30日・1年で何秒になるかを計算する
SECONDS_PER_DAY = 24 * 60 * 60
clocks = [
("DS3231(0〜40℃)", 2), # データシートの精度 ±2ppm
("DS3231(-40〜85℃)", 3.5), # 広い温度範囲では ±3.5ppm
("ズレ20ppmの時計(例)", 20),
("ズレ0.1%の時計(例)", 1000), # 1000ppm = 0.1%
]
for name, ppm in clocks:
per_day = SECONDS_PER_DAY * ppm / 1_000_000 # 1日あたりの最大ズレ(秒)
print(f"■ {name} ±{ppm}ppm")
print(f" 1日: {per_day:.2f}秒 / 30日: {per_day * 30:.1f}秒 / 1年: {per_day * 365 / 60:.1f}分")

■ DS3231(0〜40℃) ±2ppm
1日: 0.17秒 / 30日: 5.2秒 / 1年: 1.1分
■ DS3231(-40〜85℃) ±3.5ppm
1日: 0.30秒 / 30日: 9.1秒 / 1年: 1.8分
■ ズレ20ppmの時計(例) ±20ppm
1日: 1.73秒 / 30日: 51.8秒 / 1年: 10.5分
■ ズレ0.1%の時計(例) ±1000ppm
1日: 86.40秒 / 30日: 2592.0秒 / 1年: 525.6分
ここが重要です👇
- DS3231 は 0〜40℃ の範囲なら、30日たっても最大5秒ほど、1年でも約1分のズレにおさまる計算です
- ズレが 0.1% の時計だと、1日で 86秒もズレます。ppm の小さな違いが、長く動かすほど効いてくるのがわかります
- どんなに精度がよくてもゼロにはならないので、ときどき正しい時刻に合わせ直すしくみがあると安心です(この記事の後半で作ります)
配線とライブラリの準備
DS3231 は I2C という2本の信号線で通信します。Arduino UNO とは、次の4本をつなぐだけです。
| DS3231 のピン | Arduino UNO のピン |
|---|---|
| VCC | 5V |
| GND | GND |
| SDA | A4(または SDA と書かれたピン) |
| SCL | A5(または SCL と書かれたピン) |
- DS3231 の I2C アドレスは 0x68 です。EEPROM(AT24C32)付きのモジュールでは、もう1つのアドレス(0x57 など)も見えますが、それはメモリ側なので気にしなくて大丈夫です
- ⚠️ よく出回っている青い基板のモジュールには、電池を充電する回路が付いているものがあります。充電できない CR2032 を入れて使う場合は、販売ページや説明書の注意書きを必ず確認しましょう
ライブラリは、Arduino IDE の「ライブラリマネージャ」で RTClib(Adafruit 製)を検索してインストールします。関数の一覧は RTClib の公式リポジトリ で確認できます。
まずは時刻と温度をシリアルモニタに表示しよう
最初のスケッチは「RTC から日時を読んで、1秒ごとに表示する」だけのシンプルなものです。
#include <RTClib.h> // ライブラリマネージャで「RTClib」(Adafruit)を入れておく
RTC_DS3231 rtc;
const char* WEEK[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
void setup() {
Serial.begin(9600);
if (!rtc.begin()) {
Serial.println("RTC not found. Check wiring.");
while (true) delay(10); // 見つからなければここで止める
}
if (rtc.lostPower()) {
// 電池切れなどで時刻が消えていたら、コンパイルした日時を仮に入れる
Serial.println("RTC lost power. Set compile time.");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
void loop() {
DateTime now = rtc.now(); // RTCから現在の日時を読む
Serial.print(now.timestamp()); // 例: 2026-10-25T09:00:00
Serial.print(" (");
Serial.print(WEEK[now.dayOfTheWeek()]); // 0=日曜
Serial.print(") ");
Serial.print(rtc.getTemperature()); // チップ内蔵の温度センサー
Serial.println("C");
delay(1000);
}
ポイントをまとめるとこんな感じです。
rtc.begin()が false なら、RTC が見つかっていません。まず配線(特に SDA と SCL の入れ違い)を疑いましょうrtc.lostPower()は「電池切れなどで時計が止まったことがあるか」を教えてくれます。true のときだけ時刻を入れ直します__DATE__と__TIME__は、スケッチをコンパイルした日時です。手軽ですが、書き込みにかかった時間のぶん遅れた時刻が入りますdayOfTheWeek()は 0 が日曜日です。DS3231 には温度センサーも入っていて、getTemperature()で読めます(0.25℃きざみ)
シリアルモニタ(9600bps)を開くと、次のように表示されます(表示例)。
2026-10-25T09:00:00 (Sun) 24.25C
2026-10-25T09:00:01 (Sun) 24.25C
2026-10-25T09:00:02 (Sun) 24.50C
一度時刻が入れば、USB を抜いてしばらく置いても、次に起動したときには正しい時刻から表示が続きます。これが RTC のうれしいところです😊
PC の時刻を Python から書き込めるようにする
コンパイル日時を使う方法だと、どうしても数秒以上ズレてしまいます。そこで、PC の時刻をシリアル通信で送って、RTC に書き込むしくみを作ります。
やりとりのルール(プロトコル)は、次の2つだけにしました。
| PC から送る | Arduino の返事 | 意味 |
|---|---|---|
T1792918953 |
OK 1792918953 |
この時刻に合わせて |
? |
NOW 1792918953 |
今の時刻を教えて |
時刻は「1970年1月1日0時からの秒数」で送ります。数字1つで日時を表せるので、文字列を分解するよりずっと簡単です。Arduino 側のスケッチはこちら👇
#include <RTClib.h>
RTC_DS3231 rtc;
void setup() {
Serial.begin(9600);
if (!rtc.begin()) {
Serial.println("ERR rtc");
while (true) delay(10);
}
}
void loop() {
if (Serial.available() == 0) return; // 何も届いていなければ何もしない
String line = Serial.readStringUntil('\n'); // 1行受け取る
line.trim(); // 末尾の改行などを取る
if (line.startsWith("T")) {
// 例: "T1792918953" → 1970年からの秒数として時刻を書き込む
uint32_t t = line.substring(1).toInt();
rtc.adjust(DateTime(t));
Serial.print("OK ");
Serial.println(rtc.now().unixtime());
} else if (line == "?") {
// 今のRTCの時刻を秒数で返す
Serial.print("NOW ");
Serial.println(rtc.now().unixtime());
} else {
Serial.println("ERR unknown");
}
}
Serial.readStringUntil('\n')で改行まで受け取り、trim()で余分な改行を消していますDateTime(t)に秒数を渡すと日時に変換され、rtc.adjust()で書き込めます。逆にunixtime()で秒数に戻せますtoInt()は32ビットの long を返すので、この方法は2038年1月までの日時で使えます
⚠️ 日本時間のまま秒数にするのがポイント
ここが少しややこしいところです。本来の「1970年からの秒数」は世界標準時(UTC)で数えます。でも RTC にはタイムゾーンの考え方がないので、UTC の秒数をそのまま入れると 表示が9時間ずれます。
そこでこの記事では、日本時間の日時を、そのまま秒数に直して送ることにします。RTC の中身はいつも「日本時間の壁掛け時計」と同じ数字になるので、表示もそのまま使えます。
実機がなくても試せる「にせArduino」を作る
Python 側のスクリプトを書く前に、テスト用の相手を用意しておきましょう。pyserial の serial_for_url() は、COM3 のような本物のポートだけでなく、socket://localhost:5000 のようなネットワークの接続先も同じように開けます。
そこで、さっきのスケッチと同じ受け答えをする Python プログラムを作り、Arduino の代わりにしました。わざと RTC が95秒遅れている状態から始めます。
# 実機の代わりに、スケッチと同じ受け答えをする「にせArduino」
# socket://localhost:5000 で待ち受ける(pyserial から COM ポートと同じように使える)
import calendar
import socket
from datetime import datetime
rtc_offset = -95 # RTC が PC より 95 秒遅れている状態から始める
def rtc_now():
return calendar.timegm(datetime.now().timetuple()) + rtc_offset # PCの時刻の「壁時計の秒数」
def handle(line):
global rtc_offset
if line.startswith("T"):
target = int(line[1:])
rtc_offset += target - rtc_now() # rtc.adjust(DateTime(t)) の代わり
return f"OK {rtc_now()}"
if line == "?":
return f"NOW {rtc_now()}"
return "ERR unknown"
with socket.create_server(("localhost", 5000)) as server:
print("にせArduino: 接続を待っています(localhost:5000)")
conn, _ = server.accept()
with conn, conn.makefile("rw", encoding="ascii", newline="\n") as f:
for raw in f:
reply = handle(raw.strip())
print(f"受信: {raw.strip():<14} → 返信: {reply}")
f.write(reply + "\n")
f.flush()
これを1つ目のターミナルで python fake_arduino.py と実行しておくと、接続を待つ状態になります。
時刻を合わせる Python スクリプト
いよいよ本題の、PC の時刻を RTC に書き込むスクリプトです。最初に今のズレを表示し、--set を付けたときは時刻を書き込んでから、もう一度ズレを確認します。
# PC の時刻を Arduino の RTC(DS3231)に書き込み、ズレを確認するスクリプト
import argparse
import calendar
import time
from datetime import datetime, timedelta
import serial # pip install pyserial
EPOCH = datetime(1970, 1, 1)
def to_seconds(dt):
"""日本時間の日時を、そのまま「1970年からの秒数」に直す(タイムゾーン変換はしない)"""
return calendar.timegm(dt.timetuple())
def to_text(seconds):
return (EPOCH + timedelta(seconds=seconds)).strftime("%Y-%m-%d %H:%M:%S")
def ask(port, command):
"""1行送って、1行受け取る"""
port.write((command + "\n").encode("ascii"))
reply = port.readline().decode("ascii").strip()
if not reply:
raise TimeoutError(f"返事がありません(送信: {command})")
return reply
def show_diff(port):
rtc = int(ask(port, "?").split()[1]) # 返事の例: "NOW 1793001600"
pc = to_seconds(datetime.now())
print(f"PC : {to_text(pc)}")
print(f"RTC: {to_text(rtc)}(ズレ {rtc - pc:+d} 秒)")
def set_time(port):
# 秒の切り替わりまで待ってから送ると、ズレが1秒未満におさまる
now = datetime.now()
time.sleep(1 - now.microsecond / 1_000_000)
target = to_seconds(datetime.now())
reply = ask(port, f"T{target}") # 返事の例: "OK 1793001600"
if not reply.startswith("OK"):
raise RuntimeError(f"書き込みに失敗しました: {reply}")
print(f"書き込み完了: {to_text(target)}")
parser = argparse.ArgumentParser()
parser.add_argument("port", help="例: COM3 / /dev/ttyACM0 / socket://localhost:5000")
parser.add_argument("--set", action="store_true", help="PCの時刻をRTCに書き込む")
args = parser.parse_args()
with serial.serial_for_url(args.port, baudrate=9600, timeout=2) as port:
time.sleep(2) # UNO はポートを開くとリセットされるので、起動を待つ
port.reset_input_buffer() # 起動メッセージなどを捨てる
show_diff(port)
if args.set:
set_time(port)
show_diff(port)

ここが重要です👇
calendar.timegm(dt.timetuple())で、日本時間の日時をタイムゾーン変換なしで秒数にしています- 秒数を日時の文字列に戻すときも
datetime(1970, 1, 1) + timedelta(...)で計算するので、PC のタイムゾーン設定に左右されません - 書き込む直前に 次の秒の切り替わりまで待つので、ズレを1秒未満におさえられます
- 返事が来なかったときは
TimeoutErrorを出して、黙って失敗しないようにしています
2つ目のターミナルで、にせArduino に向けて実行してみました。
PC : 2026-10-25 09:03:19
RTC: 2026-10-25 09:01:44(ズレ -95 秒)
書き込み完了: 2026-10-25 09:03:20
PC : 2026-10-25 09:03:20
RTC: 2026-10-25 09:03:20(ズレ +0 秒)
-95秒あったズレが、書き込み後は +0秒 になりました🚀 にせArduino 側の画面では、実際にやりとりされた文字列が確認できます。
にせArduino: 接続を待っています(localhost:5000)
受信: ? → 返信: NOW 1792918904
受信: T1792919000 → 返信: OK 1792919000
受信: ? → 返信: NOW 1792919000
本物の Arduino で使うときは、ポート名を変えるだけです。
- Windows:
python rtc_sync.py COM3 --set(ポート番号は Arduino IDE の「ツール → ポート」で確認) - Mac / Linux:
python rtc_sync.py /dev/ttyACM0 --setなど --setを付けなければ、ズレを確認するだけです。ときどき実行して、ズレの増え方を見てみるのもおもしろいですよ
よくあるつまずきと対処
最初のコマンドに返事がない
Arduino UNO は、PC がシリアルポートを開いた瞬間に リセットされます。起動中に送ったコマンドは受け取ってもらえないので、スクリプトでは time.sleep(2) で待ってから送っています。「返事がありません」と出るときは、この待ち時間を少し長くしてみてください。
シリアルモニタを開いたままだと接続できない
1つのポートは、同時に1つのプログラムしか使えません。Python から接続するときは、Arduino IDE のシリアルモニタを閉じておきましょう。
時刻が9時間ずれる
UTC の秒数(time.time() など)をそのまま送ると起こります。この記事のスクリプトのように、日本時間の日時から秒数を作れば解決します。
電源を入れ直すたびに時刻が戻る
電池が入っていないか、切れている可能性があります。また、最初のスケッチで lostPower() の判定を外して毎回 adjust() していると、起動のたびにコンパイル日時へ戻ってしまうので注意です。
まとめ
- RTC は 電池で動き続ける小さな時計。DS3231 は温度補正つきで、0〜40℃なら ±2ppm(30日で最大5秒ほど)の精度です
- 配線は I2C の4本だけ。RTClib の
begin()・now()・adjust()・lostPower()を覚えれば時計が作れます - PC の時刻は「日本時間のままの秒数」で送ると、表示がずれずにシンプルです
serial_for_url()を使えば、実機がなくても にせArduino で通信手順をテストできます
時刻が正しく持てるようになると、「毎朝7時に水やり」「1時間ごとに記録」のような工作がぐっと作りやすくなります。「むずかしそう」を「できそう」に変える第一歩として、ぜひ試してみてください😊
こちらも読まれています
📡 Arduinoをもっと深く学ぼう!
Arduino・ラズパイ・ロボットプログラミングを体系的に学びたい方へ。おすすめのUdemyコースや電子部品もまとめています。
Arduinoロボット入門のおすすめコース・部品を見る →📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。




