RaspberryPi & ESP32 [38] I2C接続のDS3231 RTCモジュール

I2C接続のRTCモジュールです。アマゾンで4つで1485円で買いました。

I2Cなので、液晶ディスプレイと同じピンから接続できます。アドレスを確認してみると、0x68でした。これが表示されないなら、I2Cが有効になっているか確認してください。

pi@w2-x4-44:~/work/PPW $ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --  

+5V 3.3Vの両方に対応しています。

DS3231を認識させるために、/boot/config.txt に dtoverlay=i2c-rtc,ds3231 と追記してリブートします。なんか、いろいろやるのかなと思いましたが、それだけです。一度、インターネットに接続してRTCに現時刻を記録させればインターネット接続が途切れても時刻を刻みます。

 5番 + ーー>Vcc    

4番 D -->SDA

  3番 C -->SDC    

  2番 NC        

  1番 ー -->GND

DS3231のチップにはSQW(Square Wave)がありますが、このモジュールはそれが配線されておらず、NCになっています。

Raspberry PiでI2C RTC(DS3231)を使い、NTP同期後にRTCへ書き戻す構成

■RTCをconfig.txtで有効化
/boot/config.txt を編集してRTCオーバーレイを追加する。
sudo vi /boot/config.txt
末尾に追加:
dtoverlay=i2c-rtc,ds3231
保存後再起動。
sudo reboot
■RTCが認識されているか確認
i2cdetectでデバイス確認(通常0x68)。
sudo i2cdetect -y 1
デバイスファイル確認。
ls /dev/rtc*
RTCが表示されればカーネル認識はOK。
■起動時RTCが使われるか確認
ネットワークを切断して再起動し、dateが現在時刻に近ければRTCから自動読み込みされている。
sudo hwclock -r でRTC内容も確認可能。
fake-hwclockが有効な場合はRTCより優先されるため無効化する。
sudo systemctl disable fake-hwclock
sudo apt purge fake-hwclock
■chrony同期後にRTCへ書き戻すスクリプト

#!/bin/bash

# 許容できるオフセット(秒単位)
THRESHOLD=0.05

echo "Waiting for chrony to synchronize..."

while true; do
    # `chronyc tracking` の出力から `System time offset` を取得
    OFFSET=$(chronyc tracking | grep "System time offset" | awk '{print $4}')

    # 絶対値に変換
    OFFSET_ABS=$(echo $OFFSET | awk '{ print ($1 < 0) ? -$1 : $1 }')

    # 許容範囲内なら RTC に書き込む
    if (( $(echo "$OFFSET_ABS < $THRESHOLD" | bc -l) )); then
        echo "Chrony is synchronized. Writing to RTC..."
        sudo hwclock -w
        exit 0
    fi

    # 5秒ごとに再チェック
    sleep 5
done

■root cronで実行(起動時+日次更新)
RTC書き込みはroot権限が必要なためroot crontabに登録する。
sudo crontab -e
以下を追加:
@reboot /bin/bash /home/pi/work/XING/timesync.sh >> /var/log/timesync.log 2>&1
0 17 * * * /bin/bash /home/pi/work/XING/timesync.sh >> /var/log/timesync.log 2>&1
これにより
起動直後:RTC時刻で起動
ネット同期後:RTCへ書き戻し
日次17時:RTC再補正
という安定構成になる。
■ポイント
スクリプトはpi所有のままで問題ない
root cronからbashで実行すれば実行権限や所有者変更は不要
RTCは定期的に書き戻すことで長期ズレを防げる
■まとめ
RTC追加後は「起動時はRTC」「同期後はRTCへ書き戻す」という双方向同期が重要。cronでのワンショット同期を入れておくと現場運用で安定する。

同じアマゾンで、VKLSVAN 2個 DS3231 AT24C32 RTCモジュールというのがありますが、これはSQWと32Kの出力があります。

これをI2Cチェッカにかけると、二つのアドレスが見えす。これは、EEPROMが搭載されているモジュールでそのアドレスが見えるようです。

デバイスチップI2C アドレス
RTC(リアルタイムクロック)DS32310x68
EEPROM(不揮発性メモリ)AT24C320x57

1️⃣ DS3231(RTC:リアルタイムクロック)

  • I2C アドレス: 0x68
  • 現在時刻や温度データを管理する IC。
  • 0x68固定アドレス で、変更不可。

2️⃣ AT24C32(EEPROM:不揮発性メモリ)

  • I2C アドレス: 0x57
  • 32Kbit(4KB) のメモリを搭載し、データの保存が可能。
  • 例えば、RTC の設定データやユーザー独自のデータを記録できる。
  • EEPROM のアドレスは 0x500x57 の範囲で設定

ESP32でのアドレスチェックのスケッチとSQWが生きているかを調べるスケッチを上げておきます。

#include <Wire.h>

#define SDA_PIN 23  // 使用する SDA ピン(変更可能)
#define SCL_PIN 19  // 使用する SCL ピン(変更可能)

void setup() {
    Serial.begin(115200);
    Serial.println("\nI2C Scanner");
    
    // I2C 通信を開始(カスタム SDA/SCL の場合は Wire.begin(SDA, SCL);)
    Wire.begin(SDA_PIN, SCL_PIN);

    Serial.println("Scanning for I2C devices...");
}

void loop() {
    byte error, address;
    int nDevices = 0;

    for (address = 1; address < 127; address++) {
        Wire.beginTransmission(address);
        error = Wire.endTransmission();

        if (error == 0) {
            Serial.print("I2C device found at address 0x");
            Serial.print(address, HEX); // 16進数で表示
            Serial.println(" !");
            nDevices++;
        } else if (error == 4) {
            Serial.print("Unknown error at address 0x");
            Serial.println(address, HEX);
        }
    }

    if (nDevices == 0) {
        Serial.println("No I2C devices found\n");
    } else {
        Serial.println("Scan complete\n");
    }

    delay(5000); // 5秒ごとにスキャンを繰り返す
}

#include <Wire.h>
#include "RTClib.h"
#include <Wire.h>

#define DS3231_ADDRESS 0x68  // DS3231のI2Cアドレス
#define SDA_PIN 23       // I2C SDA
#define SCL_PIN 19       // I2C SCL
#define SQW_PIN 18       // SQW ピンを接続する GPIO(変更可能

volatile unsigned long lastTime = 0;
volatile unsigned long pulseInterval = 0;

// 割り込み関数:SQWの立ち下がりを検出
void IRAM_ATTR sqwInterrupt() {
  unsigned long currentTime = micros();
  pulseInterval = currentTime - lastTime;
  lastTime = currentTime;
}

// DS3231のSQW出力を1Hzに設定
void setSQWFrequency(uint8_t mode) {
  Wire.beginTransmission(DS3231_ADDRESS);
  Wire.write(0x0E);  // 制御レジスタのアドレス
  Wire.write(mode);  // 設定値
  Wire.endTransmission();
}

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);  // I2C通信開始

  pinMode(SQW_PIN, INPUT_PULLUP);  // SQWピンを入力モードに設定
  attachInterrupt(digitalPinToInterrupt(SQW_PIN), sqwInterrupt, FALLING);  // 割り込み設定

  // 1Hzの出力に設定
  setSQWFrequency(0x00);
}

void loop() {
  if (pulseInterval > 0) {
    Serial.print("Pulse Interval: ");
    Serial.print(pulseInterval);
    Serial.println(" us");

    float frequency = 1000000.0 / pulseInterval;  // 周波数 (Hz)
    Serial.print("Frequency: ");
    Serial.print(frequency);
    Serial.println(" Hz");

    pulseInterval = 0; // 測定値をリセット
  }
  delay(500);
}