- DS3231 RTCモジュールをPico 2 Wで使う|この記事で試したこと
- DS3231とは?温度補正RTCの仕組みとDS3231SN/DS3231Mの違い
- 用意するもの(DS3231モジュール・Raspberry Pi Pico 2 W ほか)
- ZS-042の充電回路とCR2032の注意点(201抵抗を外す)
- DS3231×Pico 2 Wの回路図(I2C・SQW配線)
- MicroPythonでDS3231の時刻を読む(I2Cスキャン・OSFフラグ)
- DS3231の時刻合わせ:NTPとPCの時刻を比べる
- DS3231の電池バックアップを確認(CR2032だけで時刻は保たれるか)
- DS3231のアラーム設定(アラーム1/2・SQW割り込み)
- AT24C32(EEPROM)の読み書き:addrsize=16とページ書き込み
- DS3231の精度を実測:26時間で-0.95ppm
- DS3231が動かない・時刻が戻るときのよくある質問(FAQ)
- まとめ
DS3231 RTCモジュールをPico 2 Wで使う|この記事で試したこと
こんにちは。ニンジンです🥕
Pico 2 WはWi-Fiがあるので、NTPで時刻を取ってくれば時計は作れます。ただ、電源を入れ直したときにWi-Fiにつながらないと、時刻がわかりません。停電の後や、ルーターが止まっているときがそうです。
そこで使うのが、電源が切れても時刻を刻み続けるRTC(リアルタイムクロック)モジュールです。今回は定番のDS3231モジュールを、Raspberry Pi Pico 2 WとMicroPythonで動かしてみました。
時刻合わせ、アラーム、電池バックアップ、モジュールに載っているEEPROM(AT24C32)まで、一通り実機で確認しています。最後に、NTPと比べて精度も測りました。
この記事の結論
- DS3231SNの実測の精度は -0.95ppm(1日あたり約82ms遅れる)。仕様の±2ppm以内でした
- CR2032だけで、USBを抜いた2時間20分後も時刻が合っていました
- CR2032付きで届いても、充電回路が付いていることがあります。 使う前に確認が必要です
- 時刻合わせは、NTPなら誤差約1ms。PCの時刻から合わせる方法は、合わせるたびに数百msずれました
DS3231とは?温度補正RTCの仕組みとDS3231SN/DS3231Mの違い
RTC(リアルタイムクロック)とは
RTCは、時刻を数え続けることだけに特化したICです。マイコンの電源が切れても、ボタン電池から電気をもらって時計を動かし続けます。マイコンは起動したときにRTCから時刻を読めば、すぐに正しい時刻がわかります。
DS3231の特徴
DS3231は、Analog Devices(旧Maxim)のRTCです。安いRTCとの一番の違いは、温度でズレないように補正する仕組みを持っていることです。
水晶の振動は温度によって少しだけ速さが変わり、それが時計のズレになります。DS3231は温度センサーを内蔵していて、64秒ごとに温度を測り、そのぶん発振の速さを補正しています。
ほかにも、次の機能があります。
- I2Cで接続(アドレスは0x68で固定)
- アラーム2つ(アラーム1、アラーム2)
- SQWピンからの矩形波出力(1Hz、1.024kHz、4.096kHz、8.192kHz)
- 温度センサー(0.25℃刻みで読める)
DS3231SNとDS3231Mの違い
DS3231には、大きく分けて2つの種類があります。チップの刻印で見分けられます。
| 型番 | 発振のしくみ | 精度(仕様) |
|---|---|---|
| DS3231SN | 水晶(温度補正付き) | ±2ppm(0〜40℃) |
| DS3231M | MEMS(シリコンの振動子) | ±5ppm(-45〜85℃) |
±2ppmは、1日あたり最大約0.17秒、1年で約1分のズレです。±5ppmなら、その2.5倍になります。
今回届いたモジュールのチップには、「DS3231SN」と刻印されていました。
ZS-042モジュールの構成
Amazonなどでよく売られている青い基板は、一般に「ZS-042」と呼ばれています(今回届いた基板の印字は「B411」でした)。DS3231のほかに、次の部品が載っています。
| 部品 | 役割 |
|---|---|
| DS3231SN | RTC本体(I2Cアドレス 0x68) |
| AT24C32 | 4KBのEEPROM(I2Cアドレス 0x57) |
| 「472」の部品×2 | 4.7kΩの抵抗が4本入った部品。I2Cなどのプルアップ用 |
| POWER LED | 電源が入ると点灯 |
| ダイオード+「201」抵抗 | CR2032を充電する回路(後で外します) |
| A1〜A3のパッド | EEPROMのI2Cアドレスを変えるためのもの |
I2Cのプルアップ抵抗が基板に載っているので、外付けの抵抗はいりません。
用意するもの(DS3231モジュール・Raspberry Pi Pico 2 W ほか)
今回使った部品です。Picoまわりは、これまでのPico記事と同じ定番パーツです。






このほか、充電回路の確認にテスターを使いました。ZS-042以外の基板では、充電回路の有無や部品の配置が違うことがあります。
Pico 2WHはmicro USBです。充電専用のケーブルだとPCがPicoを認識しないので、データ通信対応のケーブルを使ってください。
※リンクはアフィリエイトを含みます。価格・在庫・同梱内容は変わることがあるので、購入前に各商品ページをご確認ください。
ZS-042の充電回路とCR2032の注意点(201抵抗を外す)
CR2032付きで届いても、充電回路があることがある
ZS-042には、電池を充電する回路が付いています。VCC → 200Ωの抵抗 → ダイオード → 電池、という経路です。
ところが、今回もCR2032が付属していました。CR2032は充電できない電池なので、本来は充電式のLIR2032と組み合わせる設計です。
今回届いた基板にも、CR2032が入った状態で、充電回路がそのまま付いていました。
3.3Vなら大丈夫?5Vだと?
充電回路が電池にかける電圧は、おおよそ「VCC − ダイオードの電圧」です。ダイオードの電圧は流れる電流によって変わり、電流がほとんど流れないときは小さくなります。
| VCC | 充電回路が電池にかける電圧 | CR2032(約3V)にとって |
|---|---|---|
| 3.3V(Pico) | 約2.7〜3.1V(実測は3.096V) | 電池とほぼ同じ。電池が減ってくると、わずかに電流が流れる可能性がある |
| 5V(Arduinoなど) | 約4.4V | 電池に電流が流れ込む。危険 |
3.3Vならすぐに壊れることはなさそうですが、電池に電流が流れる経路が残っていることに変わりはありません。5Vのマイコンで使い回すことも考えて、今回は充電回路を切り離しました。
201抵抗を外して、テスターで確認
外したのは、右上の201抵抗1個だけです。はんだごてで外せます。
確認は、PicoをUSBにつないで、3V3(OUT)とGNDの2本だけをモジュールのVCCとGNDにつないで行いました。安定化電源がなくても、Picoを電源代わりにできます。
- CR2032を外す
- テスターの黒をGND、赤を電池ホルダーの+端子に当てる
- 抵抗を外す前と後で、電圧を比べる
| 状態 | 電池ホルダーの+端子の電圧 |
|---|---|
| 201抵抗を外す前 | 3.096V |
| 201抵抗を外した後 | 0.316V |
外した後も0Vにはなりませんでした。どこにもつながっていない端子なので、ICの中のわずかな漏れ電流などで0.3V程度が出ていると考えられます。VCCからの経路は切れているので、電池に電流が流れ込むことはありません。
データシートでは、電池を使わない場合はVBAT(電池の端子)をGNDにつなぐよう書かれています。充電回路を外したら、電池は入れたまま使うのがおすすめです。
DS3231×Pico 2 Wの回路図(I2C・SQW配線)
| DS3231 | Pico 2 W |
|---|---|
| VCC | 36番 3V3(OUT) |
| GND | 28番 GND(どのGNDでも可) |
| SDA | 6番 GP4 |
| SCL | 7番 GP5 |
| SQW | 20番 GP15 |
| 32K | 未接続 |
LEDは、21番 GP16 → 330Ω → LED → 23番 GND の順につなぎます。
ポイントは次の3つです。
- VCCは3.3Vにつなぐ:DS3231自体は5Vでも動きます。ただ、モジュールのプルアップ抵抗はVCCにつながっているので、5Vにつなぐと、SDAやSCLに5VがかかってPicoのピンを傷めます
- SQWはLEDに直結せず、Picoで受ける:SQWは、1Hzの出力とアラームの割り込みの両方に使うピンです。Picoで受けてLEDを光らせれば、配線を変えずに両方を試せます
- SQWはオープンドレイン:自分でHighを出せない出力です。基板にプルアップ抵抗が載っていますが、念のためPico側でも内部プルアップを有効にしています
MicroPythonでDS3231の時刻を読む(I2Cスキャン・OSFフラグ)
準備:プログラムのダウンロードとPicoへの保存
開発環境はThonnyを使いました。ThonnyのインストールとPico 2 WへのMicroPythonの書き込みは、こちらの記事で画面付きで解説しています。
この記事のプログラムは、まとめてダウンロードできます。
| ファイル | 内容 |
|---|---|
ds3231.py | DS3231を読み書きするための自作ドライバ |
netutil.py | Wi-Fi接続とNTPの問い合わせ |
config.py | Wi-FiのSSIDとパスワード |
step1_scan.py〜step7_eeprom.py | 記事の各手順で実行するプログラム |
check_offset.py | DS3231とNTPのズレを測る |
main.py | 長時間のズレ記録用(ウォッチドッグ付き) |
このうち ds3231.py、netutil.py、config.py の3つは、ほかのプログラムから読み込んで使います。Thonnyで開いて「名前を付けて保存」→「Raspberry Pi Pico」を選び、Pico本体に同じ名前で保存しておきます。
config.py は、お手持ちの環境のSSIDとパスワードに書き換えてください。" の中身だけを書き換えます。" は消さないでください。
WIFI_SSID = "your-ssid"
WIFI_PASSWORD = "your-password"
NTP_HOST = "ntp.nict.jp"
JST_OFFSET = 9 * 3600 # DS3231には日本時間で書き込む
Pico 2 WのWi-Fiは2.4GHz帯だけに対応しています。Pico WのWi-Fi接続の基本は、こちらの記事でも扱っています。
USBを抜き差しした後に実行したら、ModuleNotFoundError: No module named 'machine' というエラーが出ました。Thonnyの右下を見ると「ローカル Python 3」になっていて、PicoではなくPCのPythonで実行していたのが原因です。右下をクリックして「MicroPython (Raspberry Pi Pico)」を選び直せば直ります。
I2Cスキャンで接続を確認
まずは、配線が合っているかを確認します。
# step1_scan.py(抜粋) I2Cにつながっている機器を探す
from machine import I2C, Pin
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
found = i2c.scan()
print("見つかったアドレス:", [hex(a) for a in found])見つかったアドレス: ['0x57', '0x68']
0x68がDS3231、0x57がEEPROMです。2つとも見えていれば、配線はOKです。
時刻を読む
# step2_read.py DS3231の時刻と温度を1秒ごとに表示する
import time
from machine import I2C, Pin
from ds3231 import DS3231
WDAY = "月火水木金土日"
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
ds = DS3231(i2c)
if ds.osf():
print("OSF=1: 発振が一度止まっています。時刻は信用できません(step3で合わせてください)")
else:
print("OSF=0: 電源を切っている間も時計は動き続けていました")
while True:
y, mo, d, h, mi, s, wd = ds.datetime()
print("{}/{:02d}/{:02d}({}) {:02d}:{:02d}:{:02d} 温度 {:.2f}℃".format(
y, mo, d, WDAY[wd], h, mi, s, ds.temperature()))
time.sleep(1)届いてから一度も時刻を合わせていない状態で実行すると、こうなりました。
OSF=1: 発振が一度止まっています。時刻は信用できません(step3で合わせてください)
2000/01/01(月) 00:19:59 温度 26.25℃
2000/01/01(月) 00:20:00 温度 26.25℃
ここから、DS3231の性質が2つわかります。
- 電源が完全に切れると、2000年1月1日0時0分0秒から数え直す:00:19:59は、最後に電源が入ってから約20分経ったという意味です
- 曜日は計算されない:実際の2000年1月1日は土曜日ですが、「月」と出ています。DS3231の曜日は1〜7を順に数えるだけのカウンターで、初期値の1をこのコードでは月曜として表示しているためです。曜日は、時刻を書き込むときに自分で正しい値を入れる必要があります
OSFフラグとは
OSF(Oscillator Stop Flag)は、「発振が一度でも止まったら1になる」フラグです。電池がない状態で電源が切れると1になり、時刻を書き込むと0に戻るようにしています。
起動したときにOSFを見れば、今の時刻を信用して良いかを判断できます。
DS3231の時刻合わせ:NTPとPCの時刻を比べる
時刻合わせは2つの方法を試しました。
| 方法 | 合わせた直後のズレ | 必要なもの |
|---|---|---|
| Wi-Fi(NTP)から合わせる | 約-1ms | Wi-Fi |
| PCの時刻から合わせる | +260ms、-93ms(毎回変わる) | PC+Thonny |
ズレは、ダウンロードファイルに入っている check_offset.py でNTPと比べて測りました。
Wi-Fi(NTP)で合わせる
Pico 2 WならWi-Fiが使えるので、NTPサーバー(今回は ntp.nict.jp)から時刻を取ってきます。
工夫したのは、秒が切り替わる瞬間を待ってから書き込むことです。DS3231は、秒のレジスタを書き込んだ瞬間に、1秒未満のカウントもリセットされます。そのため、ちょうど秒が切り替わるタイミングで書けば、1秒未満のズレもほぼなくせます。
# step3_ntp_set.py NTPの時刻をDS3231に書き込む(秒の切り替わりに合わせて書く)
import time
from time import ticks_us, ticks_diff
from machine import I2C, Pin
from ds3231 import DS3231
from config import JST_OFFSET
from netutil import wifi_connect, ntp_time_us
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
ds = DS3231(i2c)
wifi_connect()
epoch_us, tick_at, rtt = ntp_time_us()
print("NTP往復時間: {:.1f} ms".format(rtt / 1000))
# 今のUTC(µs)を求め、次のちょうど1秒の瞬間まで待つ
now_us = epoch_us + ticks_diff(ticks_us(), tick_at)
next_sec = now_us // 1_000_000 + 1
t = time.gmtime(next_sec + JST_OFFSET)
time.sleep_us(next_sec * 1_000_000 - now_us)
ds.set_datetime(t[0], t[1], t[2], t[3], t[4], t[5], t[6])
print("書き込みました:", ds.datetime())
print("OSF:", ds.osf())Wi-Fi接続OK: 192.168.61.8
NTP往復時間: 14.1 ms
書き込みました: (2026, 10, 2, 13, 30, 34, 4)
OSF: False
MicroPythonの標準の ntptime モジュールは、秒単位でしか時刻を返しません。ここでは1秒未満まで使いたかったので、NTPの問い合わせを自分で書いています(netutil.py)。
合わせた直後に check_offset.py で測ると、ズレは約-1msでした。
Wi-Fiなしで、PCの時刻から合わせる
Wi-Fiがない環境では、PCの時刻を使う方法があります。
ThonnyはPicoに接続したとき、Pico内部の時計をPCの時刻に合わせてくれます。その時刻を、DS3231に書き写すだけです。
# step3b_set_from_pc.py(抜粋) Wi-Fiを使わず、PCの時刻をDS3231に書き込む
import time
from machine import I2C, Pin
from ds3231 import DS3231
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
ds = DS3231(i2c)
now = time.localtime()
if now[0] < 2024:
print("Picoの時計が合っていません。Thonnyの設定を確認してください")
raise SystemExit
# Pico内部の時計の秒が切り替わる瞬間を待つ
s0 = time.localtime()[5]
while time.localtime()[5] == s0:
pass
t = time.localtime()
ds.set_datetime(t[0], t[1], t[2], t[3], t[4], t[5], t[6])
print("PCの時刻を書き込みました")
# ダウンロード版は、このあと書き込んだ時刻を5秒間表示するPCの時刻を書き込みました
DS3231: 2026/10/06(火) 14:08:11 OSF=0
DS3231: 2026/10/06(火) 14:08:12 OSF=0Thonnyの「ツール」→「オプション」→「インタプリタ」で、次の2つにチェックが入っている必要があります(今回の環境では、両方とも最初から入っていました)。
- デバイスのリアルタイム クロックを同期する:オフだと、Picoの時計が合いません
- リアルタイム クロックで現地時間を使用する:オフだと、日本時間ではなくUTC(9時間前)が書き込まれます
手軽ですが、精度はNTPに負けます。
- 2回試すと、ズレは+260msと-93msで、毎回変わった
- PCの時計そのもののズレなら、毎回ほぼ同じ値になるはず
- そのため、Thonnyが秒単位でPicoの時計を合わせていることが主な原因だと考えている(1秒未満の部分が、毎回違う値で切り捨てられる)
- 普通の時計なら十分。ミリ秒単位で合わせたいならNTP
DS3231の電池バックアップを確認(CR2032だけで時刻は保たれるか)
NTPで時刻を合わせた後、USBを抜いて、電池だけの状態にしました。5分のつもりが、別の作業をしていて約2時間20分放置してしまいました。USBを挿し直して step2_read.py を実行した結果です。
OSF=0: 電源を切っている間も時計は動き続けていました
2026/10/02(金) 15:57:35 温度 25.75℃OSF=1: 発振が一度止まっています。時刻は信用できません(step3で合わせてください)
2000/01/01(月) 00:00:19 温度 26.50℃
| 条件 | 結果 |
|---|---|
| CR2032あり(USBを抜いて約2時間20分) | 時刻はスマホと一致、OSF=0 |
| CR2032なし(電池を外して電源を切った) | 2000/01/01に戻った、OSF=1 |
DS3231のアラーム設定(アラーム1/2・SQW割り込み)
アラームの仕組み
DS3231には、アラームが2つあります。
- アラーム1:秒まで指定できる
- アラーム2:分までしか指定できない(秒は00に固定)
アラームが来てから次のアラームまでの流れは、次のとおりです。
- 設定した時刻になると、ステータスレジスタのA1F(アラーム1のフラグ)が1になる
- 割り込みを有効にしておくと、SQWピンがLowになる
- PicoはSQWの立ち下がりで割り込みを受ける
- プログラムでA1Fを0に戻すと、SQWがHighに戻る
フラグを0に戻すまで、SQWはLowのままです。戻し忘れると、次のアラームで割り込みが来ません。
アラームの設定パターン
アラームは、「どこが一致したら鳴らすか」を、各レジスタの一番上のビット(マスクビット)で選びます。
アラーム1
| 鳴るタイミング | 例 |
|---|---|
| 毎秒 | — |
| 毎分、指定した秒 | 毎分42秒(今回試したもの) |
| 毎時、指定した分と秒 | 毎時15分00秒 |
| 毎日、指定した時・分・秒 | 毎日7時30分00秒 |
| 毎月、指定した日・時・分・秒 | 毎月1日 0時00分00秒 |
| 毎週、指定した曜日・時・分・秒 | 毎週月曜 9時00分00秒 |
アラーム2
| 鳴るタイミング | 例 |
|---|---|
| 毎分(00秒) | — |
| 毎時、指定した分 | 毎時30分 |
| 毎日、指定した時・分 | 毎日22時00分 |
| 毎月・毎週の指定 | アラーム1と同じ |
今回のドライバには、「毎分、指定した秒」の設定だけを入れています。ほかのパターンは、マスクビットの組み合わせを変えれば設定できます。
実例:アラーム1で毎分決まった秒にLEDを点滅
# step5_alarm.py アラーム1で毎分決まった秒にLEDを点滅
import time
from machine import I2C, Pin
from ds3231 import DS3231
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
ds = DS3231(i2c)
now_sec = ds.datetime()[5]
alarm_sec = (now_sec + 10) % 60 # 10秒後の秒に合わせる(以後は毎分その秒)
ds.set_alarm1_every_minute(alarm_sec)
print("アラーム設定: 毎分 {} 秒".format(alarm_sec))
sqw = Pin(15, Pin.IN, Pin.PULL_UP)
led = Pin(16, Pin.OUT)
fired = False
def on_alarm(pin):
global fired
fired = True # 割り込みの中では最小限の処理だけ
sqw.irq(trigger=Pin.IRQ_FALLING, handler=on_alarm)
while True:
if fired:
fired = False
print("アラーム!", ds.datetime()[3:6])
for _ in range(5):
led.on(); time.sleep_ms(100)
led.off(); time.sleep_ms(100)
ds.clear_alarm1() # フラグを戻さないとSQWはLowのまま
time.sleep_ms(50)アラーム設定: 毎分 42 秒
アラーム! (16, 41, 42)
アラーム! (16, 42, 42)
アラーム! (16, 43, 42)3分続けて、毎分42秒ちょうどにLEDが5回点滅しました。
おまけ:SQWピンから1Hzの矩形波を出す
アラームを使わないときは、SQWピンから矩形波を出せます。1Hzに設定して、立ち上がりと立ち下がりの両方で割り込みを受け、LEDを点滅させました。
# step4_sqw_led.py(抜粋)
ds.disable_alarm1()
ds.sqw_1hz()
sqw = Pin(15, Pin.IN, Pin.PULL_UP)
led = Pin(16, Pin.OUT)
def on_edge(pin):
led.value(not pin.value()) # SQWがLowの間LEDを点ける
sqw.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_edge)
割り込みは1秒に2回ずつ入り、LEDは0.5秒点灯・0.5秒消灯をくり返しました。撮った動画からLEDの明るさを0.1秒ごとに測っても、きっちり1秒周期でした。
AT24C32(EEPROM)の読み書き:addrsize=16とページ書き込み
ZS-042には、AT24C32というEEPROMも載っています。電源を切っても消えないメモリで、容量は32Kbit=4KBです。設定値や、最後に動いた時刻などを保存しておくのに使えます。
I2Cアドレスは0x57
AT24C32のアドレスは、A0〜A2の3本のピンで決まります。ZS-042では、これらがプルアップされているので、何もしなければ0x57です。基板のA1〜A3のパッドをはんだでつなぐと、アドレスを変えられます。
読み書きのポイント
- メモリ番地は2バイト:MicroPythonでは
addrsize=16を指定します - 書き込みは32バイトのページ単位:ページの区切りをまたいで一度に書くと、ページの先頭に戻って上書きされてしまいます。区切りごとに分けて書きます
- 1ページ書くごとに、最大10ms待つ:内部の書き込みが終わるのを待ちます
# step7_eeprom.py EEPROMに文字を書いて読み戻す
import time
from machine import I2C, Pin
from ds3231 import DS3231
EEPROM_ADDR = 0x57
PAGE = 32
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
ds = DS3231(i2c)
def ee_read(addr, n):
return i2c.readfrom_mem(EEPROM_ADDR, addr, n, addrsize=16)
def ee_write(addr, data):
"""ページの区切りごとに分けて書き込む"""
i = 0
while i < len(data):
n = min(PAGE - (addr % PAGE), len(data) - i)
i2c.writeto_mem(EEPROM_ADDR, addr, data[i:i + n], addrsize=16)
time.sleep_ms(10)
addr += n
i += n
def load_message():
"""先頭1バイト=長さ、その後ろにUTF-8の文字列"""
n = ee_read(0, 1)[0]
if n == 0xFF or n == 0: # 未使用のEEPROMは0xFFで埋まっている
return None
return ee_read(1, n).decode()
def save_message(text):
b = text.encode()
ee_write(0, bytes([len(b)]) + b)
prev = load_message()
print("前回の内容:", prev if prev else "(まだ何も書かれていません)")
y, mo, d, h, mi, s, _ = ds.datetime()
msg = "最後に書き込んだ時刻は {}/{:02d}/{:02d} {:02d}:{:02d}:{:02d} です".format(y, mo, d, h, mi, s)
save_message(msg)
print("書き込み:", msg, "({}バイト)".format(len(msg.encode()) + 1))
back = load_message()
print("読み戻し:", back)
print("一致" if back == msg else "不一致!")1回目の実行結果です。届いたときのEEPROMは空でした。
前回の内容: (まだ何も書かれていません)
書き込み: 最後に書き込んだ時刻は 2026/10/06 14:11:33 です (61バイト)
読み戻し: 最後に書き込んだ時刻は 2026/10/06 14:11:33 です
一致USBを抜いて挿し直し、2回目を実行すると、1回目に書いた内容が残っていました。
前回の内容: 最後に書き込んだ時刻は 2026/10/06 14:11:33 です
書き込み: 最後に書き込んだ時刻は 2026/10/06 14:13:30 です (61バイト)
読み戻し: 最後に書き込んだ時刻は 2026/10/06 14:13:30 です
一致
日本語はUTF-8で1文字3バイトなので、この文字列は61バイトあります。32バイトのページの区切りをまたいでも、正しく書けています。
DS3231の精度を実測:26時間で-0.95ppm
測り方
DS3231は秒単位でしか時刻を読めません。そこで、次の方法でミリ秒単位のズレを測りました。
- DS3231の秒レジスタを繰り返し読み、秒が切り替わった瞬間を記録する
- NTPサーバーに問い合わせて、その瞬間の正確な時刻を求める
- 2つの差がズレ
NTPの問い合わせには、行きと帰りの通信時間(往復時間)がかかります。往復のちょうど真ん中の時刻をサーバーの時刻とみなして計算しています。往復時間が長いと、行きと帰りの偏りで誤差が大きくなるので、グラフにするときに往復時間が14msを超えた測定は除外しました(307回中12回)。
5分(300秒)おきに測って、CSVに記録していきました。長時間動かすので、固まったときに自動で再起動するウォッチドッグ付きの main.py を使っています。Picoに main.py という名前で保存すれば、PCにつながなくても、USBアダプターにつなぐだけで記録を続けられます。
ダウンロードファイルには、ウォッチドッグなどを省いたシンプルな版の step6_drift_log.py(同じく5分おき)も入れています。しくみを追うならこちらが読みやすいです。
結果:-0.95ppm(1日あたり約82ms)
約26時間、307回測った結果です。
| 項目 | 値 |
|---|---|
| ズレの速さ | -0.95ppm(DS3231が少しずつ遅れる方向) |
| 1日あたり | 約82ms |
| 1か月あたり | 約2.5秒 |
| 近似直線からのバラツキ | ほとんどが±1ms以内(大きく外れた点でも3ms程度) |
| DS3231SNの仕様 | ±2ppm(0〜40℃) |
点がほぼ一直線に並んでいて、ズレは一定の速さでたまっていきます。仕様の±2ppmの半分以下に収まりました。
温度による違い
3時間ごとに区切って傾きを計算すると、室温が25℃前後の昼は約-1.0ppm、20℃前後の夜は約-0.91ppmでした。温度補正付きでも、温度によってわずかに差が出るようです。ただし差は0.1ppm程度なので、「傾向が見える」程度の話です。
想定外の実験:Wi-Fiが2日半止まっていた
実は、最初に週末を使って測ろうとしたときは、失敗しています。電源設備の点検でUPSを止めて帰ったら、UPSにつながっていたWi-Fiルーターも止まっていました。CSVには、金曜の3行しか残っていませんでした。
それでも、DS3231は時計を刻み続けていました。月曜に測った結果です。
| 期間 | Wi-Fi | DS3231のズレ |
|---|---|---|
| 金曜17:11 → 月曜08:51(約2日半) | ルーター停止 | -218ms(-0.95ppm) |
ズレの速さは、26時間の測定とそろっています。Wi-Fiがなくても時刻がほとんどズレなかったのは、RTCを付けているからこそです。
DS3231が動かない・時刻が戻るときのよくある質問(FAQ)
QDS3231の時刻が2000年1月1日に戻ってしまうのはなぜですか?
A電池がない状態(または電池切れ)で電源が切れると、DS3231は2000年1月1日0時0分0秒から数え直します。このときOSFフラグが1になるので、起動時にOSFを見れば時刻を信用してよいか判断できます。CR2032を入れたうえで、NTPかPCの時刻で合わせ直してください。
QI2Cスキャンで0x68が見つかりません。
ASDA(GP4)・SCL(GP5)・VCC(3V3(OUT))・GNDの配線を確認してください。No module named 'machine' と出る場合は配線ではなく、Thonnyの右下が「ローカル Python 3」になっています。「MicroPython (Raspberry Pi Pico)」を選び直すと直ります。
QDS3231のアラームが1回しか鳴りません。
Aアラームのフラグ(アラーム1ならA1F)を0に戻すまで、SQWピンはLowのままです。立ち下がりの割り込みが次に来ないので、アラームを処理したら clear_alarm1() のようにフラグを戻してください。
QCR2032付きのZS-042モジュールは、そのまま使って大丈夫ですか?
AZS-042には電池を充電する回路が付いています。Picoの3.3Vならすぐに壊れることはなさそうですが、電池に電流が流れる経路は残っています。5Vのマイコンでは電池に電流が流れ込むので危険です。今回は201抵抗を外して充電回路を切り離しました。
QDS3231の精度は実際どのくらいですか?
A今回のDS3231SNをNTPと比べて約26時間測ったところ、-0.95ppm(1日あたり約82ms、1か月あたり約2.5秒の遅れ)でした。仕様の±2ppm(0〜40℃)の半分以下です。
まとめ
- DS3231SNの精度は実測で-0.95ppm(1日あたり約82ms)。仕様の±2ppm以内
- CR2032付きで届いても、充電回路が付いていることがある。3.3Vでもわずかに電流が流れる可能性があり、5Vでは確実に危険
- 時刻合わせは、NTPなら約1ms。PCの時刻からだと、合わせるたびに数百msずれる
- アラームは、フラグを0に戻すまでSQWがLowのまま
- AT24C32は、2バイトの番地指定と32バイトのページに注意すれば、MicroPythonから普通に読み書きできる
Pico 2 Wで時計やデータロガーを作るなら、電源を入れ直してもすぐに正しい時刻がわかるように、DS3231を付けておくのがおすすめです。
この記事のプログラム(再掲)
📦 rtc001-code.zip をダウンロードFollow me on X
本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕
@ninjin_py_vba をフォロー













コメント