PR

DS3231の使い方|ラズパイPico 2 Wで時刻合わせ・アラーム・電池バックアップ【回路図付き】

ハードウェア制御 / 電子工作
PICO MODULE LAB / DS3231 RTC
  1. DS3231 RTCモジュールをPico 2 Wで使う|この記事で試したこと
  2. DS3231とは?温度補正RTCの仕組みとDS3231SN/DS3231Mの違い
    1. RTC(リアルタイムクロック)とは
    2. DS3231の特徴
    3. DS3231SNとDS3231Mの違い
    4. ZS-042モジュールの構成
  3. 用意するもの(DS3231モジュール・Raspberry Pi Pico 2 W ほか)
  4. ZS-042の充電回路とCR2032の注意点(201抵抗を外す)
    1. CR2032付きで届いても、充電回路があることがある
    2. 3.3Vなら大丈夫?5Vだと?
    3. 201抵抗を外して、テスターで確認
  5. DS3231×Pico 2 Wの回路図(I2C・SQW配線)
  6. MicroPythonでDS3231の時刻を読む(I2Cスキャン・OSFフラグ)
    1. 準備:プログラムのダウンロードとPicoへの保存
    2. I2Cスキャンで接続を確認
    3. 時刻を読む
    4. OSFフラグとは
  7. DS3231の時刻合わせ:NTPとPCの時刻を比べる
    1. Wi-Fi(NTP)で合わせる
    2. Wi-Fiなしで、PCの時刻から合わせる
  8. DS3231の電池バックアップを確認(CR2032だけで時刻は保たれるか)
  9. DS3231のアラーム設定(アラーム1/2・SQW割り込み)
    1. アラームの仕組み
    2. アラームの設定パターン
    3. 実例:アラーム1で毎分決まった秒にLEDを点滅
    4. おまけ:SQWピンから1Hzの矩形波を出す
  10. AT24C32(EEPROM)の読み書き:addrsize=16とページ書き込み
    1. I2Cアドレスは0x57
    2. 読み書きのポイント
  11. DS3231の精度を実測:26時間で-0.95ppm
    1. 測り方
    2. 結果:-0.95ppm(1日あたり約82ms)
    3. 温度による違い
    4. 想定外の実験:Wi-Fiが2日半止まっていた
  12. DS3231が動かない・時刻が戻るときのよくある質問(FAQ)
  13. まとめ
スポンサーリンク

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℃)
DS3231MMEMS(シリコンの振動子)±5ppm(-45〜85℃)

±2ppmは、1日あたり最大約0.17秒、1年で約1分のズレです。±5ppmなら、その2.5倍になります。

今回届いたモジュールのチップには、「DS3231SN」と刻印されていました。

チップの刻印。上がDS3231SN、下がEEPROMの24C32
▲ チップの刻印。上がDS3231SN、下がEEPROMの24C32

ZS-042モジュールの構成

Amazonなどでよく売られている青い基板は、一般に「ZS-042」と呼ばれています(今回届いた基板の印字は「B411」でした)。DS3231のほかに、次の部品が載っています。

部品役割
DS3231SNRTC本体(I2Cアドレス 0x68)
AT24C324KBのEEPROM(I2Cアドレス 0x57)
「472」の部品×24.7kΩの抵抗が4本入った部品。I2Cなどのプルアップ用
POWER LED電源が入ると点灯
ダイオード+「201」抵抗CR2032を充電する回路(後で外します)
A1〜A3のパッドEEPROMのI2Cアドレスを変えるためのもの

I2Cのプルアップ抵抗が基板に載っているので、外付けの抵抗はいりません。


スポンサーリンク

用意するもの(DS3231モジュール・Raspberry Pi Pico 2 W ほか)

今回使った部品です。Picoまわりは、これまでのPico記事と同じ定番パーツです。

小型DS3231 AT24C32 I2C リアルタイムクロックモジュール(ボタン電池付き・A145)
必須

CR2032付きで届く。使う前に充電回路を確認

小型DS3231 AT24C32 I2C リアルタイムクロックモジュール(ボタン電池付き・A145)

この記事の主役。DS3231SNとEEPROM(AT24C32)が載ったZS-042タイプの基板です。今回届いた基板には充電回路が付いていたので、201抵抗を外して使いました。

I2C 0x68 / 0x57CR2032付き
ヘッダー ハンダ付け済み Raspberry Pi Pico 2WH
必須

ピンヘッダー実装済みで、届いたその日からブレッドボードに挿せる

ヘッダー ハンダ付け済み Raspberry Pi Pico 2WH

今回のマイコン。無線付き(W)・ピンヘッダー実装済み(H)モデルで、Wi-FiでNTPから時刻を取るのにも使います(筆者はPico 2 Wで動作確認)。

無線+ヘッダー実装済みMicroPython
Freenove Raspberry Pi Pico 1/2 W H WH用ブレイクアウトボード
あると便利

ピン名が印字されていて、配線を確認しやすい

Freenove Raspberry Pi Pico 1/2 W H WH用ブレイクアウトボード

Picoを挿すだけで、全ピンがネジ端子とピン名の印字付きで引き出されます。ピンを端から数える手間が減ります。

ネジ端子ピン名印字
ブレッドボード 400穴
必須

はんだ付けなしで回路を組み替えられる基本の作業台

ブレッドボード 400穴

DS3231モジュールとLED・抵抗を挿すのに使用。今回の回路は400穴サイズで十分収まります。

はんだ付け不要
ELEGOO 120pcs 多色デュポンワイヤー
必須

オス-オス・オス-メス・メス-メスがそろって、どんな配線にも使える

ELEGOO 120pcs 多色デュポンワイヤー

モジュール・Pico・ブレッドボードの間の配線に使用。SDA・SCL・SQWで色を分けておくと、後から見直すときに楽です。

3種類×40本多色
サムコス 発光ダイオード 3mm/5mm 5色 200個入り(収納ケース付き)
動作確認用

アラームとSQWの1Hzを、目で見て確かめられる

サムコス 発光ダイオード 3mm/5mm 5色 200個入り(収納ケース付き)

SQWピンの1Hz出力と、アラームが来たことの確認に、赤のLEDを1個使いました。

3mm / 5mm5色200個入り
GOODCHI 金属皮膜抵抗器 抵抗セット(10Ω〜1MΩ・30種類)
動作確認用

330Ωなど、回路に合わせてすぐ選べる

GOODCHI 金属皮膜抵抗器 抵抗セット(10Ω〜1MΩ・30種類)

LEDの電流制限用の330Ωに使用。セットで持っておくと、ほかの工作でも困りません。

30種類×各20本収納ケース付き

このほか、充電回路の確認にテスターを使いました。ZS-042以外の基板では、充電回路の有無や部品の配置が違うことがあります。

USBケーブルについて

Pico 2WHはmicro USBです。充電専用のケーブルだとPCがPicoを認識しないので、データ通信対応のケーブルを使ってください。

※リンクはアフィリエイトを含みます。価格・在庫・同梱内容は変わることがあるので、購入前に各商品ページをご確認ください。


スポンサーリンク

ZS-042の充電回路とCR2032の注意点(201抵抗を外す)

CR2032付きで届いても、充電回路があることがある

ZS-042には、電池を充電する回路が付いています。VCC → 200Ωの抵抗 → ダイオード → 電池、という経路です。

ところが、今回もCR2032が付属していました。CR2032は充電できない電池なので、本来は充電式のLIR2032と組み合わせる設計です。

今回届いた基板にも、CR2032が入った状態で、充電回路がそのまま付いていました。

DS3231の右隣の赤いガラス管がダイオード、その右上が201抵抗

▲ DS3231の右隣の赤いガラス管がダイオード、その右上が201抵抗

右上の抵抗は、写真の向きだと「10Z」と読めます。基板を逆さにすると「201」=200Ωです。

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を電源代わりにできます。

Picoを電源代わりにして、VCCとGNDの2本だけつなぐ
▲ Picoを電源代わりにして、VCCとGNDの2本だけつなぐ
Pico側は3V3(OUT)とGND
▲ Pico側は3V3(OUT)とGND
モジュール側はVCCとGND
▲ モジュール側はVCCとGND
  1. CR2032を外す
  2. テスターの黒をGND、赤を電池ホルダーの+端子に当てる
  3. 抵抗を外す前と後で、電圧を比べる
テスターの黒はクリップでGNDピンに
▲ テスターの黒はクリップでGNDピンに
赤は電池ホルダーの+端子に当てる
▲ 赤は電池ホルダーの+端子に当てる
状態電池ホルダーの+端子の電圧
201抵抗を外す前3.096V
201抵抗を外した後0.316V
外す前:3.096V
▲ 外す前:3.096V
201抵抗を外した基板
▲ 201抵抗を外した基板
外した後:0.316V
▲ 外した後:0.316V

外した後も0Vにはなりませんでした。どこにもつながっていない端子なので、ICの中のわずかな漏れ電流などで0.3V程度が出ていると考えられます。VCCからの経路は切れているので、電池に電流が流れ込むことはありません。

注意

データシートでは、電池を使わない場合はVBAT(電池の端子)をGNDにつなぐよう書かれています。充電回路を外したら、電池は入れたまま使うのがおすすめです。


スポンサーリンク

DS3231×Pico 2 Wの回路図(I2C・SQW配線)

DS3231とPico 2 Wの結線図
▲ DS3231とPico 2 Wの結線図
DS3231Pico 2 W
VCC36番 3V3(OUT)
GND28番 GND(どのGNDでも可)
SDA6番 GP4
SCL7番 GP5
SQW20番 GP15
32K未接続

LEDは、21番 GP16 → 330Ω → LED → 23番 GND の順につなぎます。

実際の配線の全体
▲ 実際の配線の全体
Pico左側:GP4(SDA)・GP5(SCL)・GP15(SQW)
▲ Pico左側:GP4(SDA)・GP5(SCL)・GP15(SQW)
Pico右側:3V3(OUT)・GND・GP16(LED)
▲ Pico右側:3V3(OUT)・GND・GP16(LED)
ブレッドボード側。モジュールのピンとLEDへの配線
▲ ブレッドボード側。モジュールのピンとLEDへの配線
LEDと330Ωの抵抗
▲ LEDと330Ωの抵抗

ポイントは次の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の書き込みは、こちらの記事で画面付きで解説しています。

🥕 Thonnyの準備はこちら ラズパイPico WとDHT11で温湿度を測定する方法|配線図とMicroPythonコード Thonnyのインストールと、Pico WへのMicroPythonの書き込み(BOOTSEL)を画面付きで解説しています。 →

この記事のプログラムは、まとめてダウンロードできます。

この記事のプログラム一式

📦 rtc001-code.zip をダウンロード

ZIPの中の config.py は、お手持ちの環境のSSIDとパスワードに書き換えてから使ってください。

ファイル内容
ds3231.pyDS3231を読み書きするための自作ドライバ
netutil.pyWi-Fi接続とNTPの問い合わせ
config.pyWi-FiのSSIDとパスワード
step1_scan.py〜step7_eeprom.py記事の各手順で実行するプログラム
check_offset.pyDS3231とNTPのズレを測る
main.py長時間のズレ記録用(ウォッチドッグ付き)

このうち ds3231.py、netutil.py、config.py の3つは、ほかのプログラムから読み込んで使います。Thonnyで開いて「名前を付けて保存」→「Raspberry Pi Pico」を選び、Pico本体に同じ名前で保存しておきます。

「名前を付けて保存」で保存先を聞かれる
▲ 「名前を付けて保存」で保存先を聞かれる
「Raspberry Pi Pico」を選んで同じ名前で保存
▲ 「Raspberry Pi Pico」を選んで同じ名前で保存

config.py は、お手持ちの環境のSSIDとパスワードに書き換えてください。" の中身だけを書き換えます。" は消さないでください。

WIFI_SSID = "your-ssid"
WIFI_PASSWORD = "your-password"

NTP_HOST = "ntp.nict.jp"
JST_OFFSET = 9 * 3600  # DS3231には日本時間で書き込む
config.pyのSSIDとパスワードを書き換える
▲ config.pyのSSIDとパスワードを書き換える

Pico 2 WのWi-Fiは2.4GHz帯だけに対応しています。Pico WのWi-Fi接続の基本は、こちらの記事でも扱っています。

📶 Wi-Fi接続の基本 Pico WからLINEへ通知を送るMicroPythonコード|urequestsでプッシュ送信・日本語エラー400対策 network.WLAN を使ってPico WをWi-Fiにつなぐところから扱っています。 →
つまずきポイント

USBを抜き差しした後に実行したら、ModuleNotFoundError: No module named 'machine' というエラーが出ました。Thonnyの右下を見ると「ローカル Python 3」になっていて、PicoではなくPCのPythonで実行していたのが原因です。右下をクリックして「MicroPython (Raspberry Pi Pico)」を選び直せば直ります。

右下が「ローカル Python 3」になっているとエラーになる
▲ 右下が「ローカル Python 3」になっているとエラーになる

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']
step1_scan.pyの実行結果。ダウンロード版は「0x68: DS3231 OK」なども表示する
▲ step1_scan.pyの実行結果。ダウンロード版は「0x68: DS3231 OK」なども表示する

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)から合わせる約-1msWi-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
step3_ntp_set.pyの実行結果
▲ step3_ntp_set.pyの実行結果

MicroPythonの標準の ntptime モジュールは、秒単位でしか時刻を返しません。ここでは1秒未満まで使いたかったので、NTPの問い合わせを自分で書いています(netutil.py)。

合わせた直後に check_offset.py で測ると、ズレは約-1msでした。

NTPで合わせた直後のズレ
▲ NTPで合わせた直後のズレ

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=0

Thonnyの「ツール」→「オプション」→「インタプリタ」で、次の2つにチェックが入っている必要があります(今回の環境では、両方とも最初から入っていました)。

  • デバイスのリアルタイム クロックを同期する:オフだと、Picoの時計が合いません
  • リアルタイム クロックで現地時間を使用する:オフだと、日本時間ではなくUTC(9時間前)が書き込まれます
Thonnyのオプション画面(インタプリタ)
▲ Thonnyのオプション画面(インタプリタ)

手軽ですが、精度はNTPに負けます。

PCの時刻から合わせた直後のズレ
▲ PCの時刻から合わせた直後のズレ
  • 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℃
約2時間20分後。スマホの時計(15:57)と一致

▲ 約2時間20分後。スマホの時計(15:57)と一致

比較のために、電池を外して電源を切ってから、もう一度起動しました。

OSF=1: 発振が一度止まっています。時刻は信用できません(step3で合わせてください)
2000/01/01(月) 00:00:19  温度 26.50℃
電池なしで起動(16:11)。2000年1月1日に戻った
▲ 電池なしで起動(16:11)。2000年1月1日に戻った
条件結果
CR2032あり(USBを抜いて約2時間20分)時刻はスマホと一致、OSF=0
CR2032なし(電池を外して電源を切った)2000/01/01に戻った、OSF=1

スポンサーリンク

DS3231のアラーム設定(アラーム1/2・SQW割り込み)

アラームの仕組み

DS3231には、アラームが2つあります。

  • アラーム1:秒まで指定できる
  • アラーム2:分までしか指定できない(秒は00に固定)

アラームが来てから次のアラームまでの流れは、次のとおりです。

  1. 設定した時刻になると、ステータスレジスタのA1F(アラーム1のフラグ)が1になる
  2. 割り込みを有効にしておくと、SQWピンがLowになる
  3. PicoはSQWの立ち下がりで割り込みを受ける
  4. プログラムで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回点滅しました。

毎分42秒にLEDが5回点滅
▲ 毎分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回ずつ増えていく
▲ 割り込み回数が1秒に2回ずつ増えていく

割り込みは1秒に2回ずつ入り、LEDは0.5秒点灯・0.5秒消灯をくり返しました。撮った動画からLEDの明るさを0.1秒ごとに測っても、きっちり1秒周期でした。

SQWの1HzでLEDが点滅
▲ SQWの1HzでLEDが点滅

スポンサーリンク

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 です
一致
USBを抜き差しした後の2回目。前回の内容が残っている
▲ USBを抜き差しした後の2回目。前回の内容が残っている

日本語はUTF-8で1文字3バイトなので、この文字列は61バイトあります。32バイトのページの区切りをまたいでも、正しく書けています。


スポンサーリンク

DS3231の精度を実測:26時間で-0.95ppm

測り方

DS3231は秒単位でしか時刻を読めません。そこで、次の方法でミリ秒単位のズレを測りました。

  1. DS3231の秒レジスタを繰り返し読み、秒が切り替わった瞬間を記録する
  2. NTPサーバーに問い合わせて、その瞬間の正確な時刻を求める
  3. 2つの差がズレ

NTPの問い合わせには、行きと帰りの通信時間(往復時間)がかかります。往復のちょうど真ん中の時刻をサーバーの時刻とみなして計算しています。往復時間が長いと、行きと帰りの偏りで誤差が大きくなるので、グラフにするときに往復時間が14msを超えた測定は除外しました(307回中12回)。

5分(300秒)おきに測って、CSVに記録していきました。長時間動かすので、固まったときに自動で再起動するウォッチドッグ付きの main.py を使っています。Picoに main.py という名前で保存すれば、PCにつながなくても、USBアダプターにつなぐだけで記録を続けられます。

Picoに main.py という名前で保存する
▲ Picoに main.py という名前で保存する

ダウンロードファイルには、ウォッチドッグなどを省いたシンプルな版の step6_drift_log.py(同じく5分おき)も入れています。しくみを追うならこちらが読みやすいです。

結果:-0.95ppm(1日あたり約82ms)

約26時間、307回測った結果です。

NTPとの時刻差(上)とDS3231の内蔵温度(下)
▲ NTPとの時刻差(上)とDS3231の内蔵温度(下)
項目値
ズレの速さ-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-FiDS3231のズレ
金曜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 をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket
まとめハブ
ハードウェア制御 / 電子工作
スポンサーリンク

コメント