ゆるく楽しむプログラミング

RTC(Real-Time Clock) DS1307

スペックシートに記載されている通りに接続します。
SQW/OUTにはLEDをつないでおきます。

 CRYSTAL:32.768kHz
 Rpu:5KΩ ※4.7KΩが良いが手元にないので









I2C_Scannerで動作確認
I2Cアドレス 0x68 で応答あり、動作している事を確認
※クリスタルがなくても応答は返ってくるので、この時点では正常動作か否かは不明


DS1307のコマンド(時刻設定/読み取り)

DS1307のレジスタ
アドレス BIT7 BIT6 BIT5 BIT4 BIT3 BIT2 BIT1 BIT0 機能 値の範囲
00h 0:RTC動作
1:RTC停止
秒(10の位) 1~5 秒(1の位) 0~9 00~59
01h 分(10の位) 0~5 分(1の位) 0~9  00~59 
02h  0に固定  0:24時間表示 時(10の位) 0~2  時(1の位) 0~9   時   00~24 
1:12時間表示 0:AM
1:PM 
 時(10の位) 0~1 1~12+AM/PM
03h 0に固定 0に固定  曜日 1:日 ~ 7:土  曜日 1~7
04h 0に固定 日(10の位) 0~3 日(1の位) 0~9  1~31 
05h 0に固定 月(10の位) 0~1 月(1の位) 0~9  1~12 
06h 年(10の位) 0~9  年(1の位) 0~9  年  00~99 
07h SQWの出力
※備考1 
0に固定 0:SQWE停止
1:SQWE出力
0に固定 RS1
※備考2 
RS2
※備考 
コントロール   
08h~3Fh    RAM
56バイト
00~FF 
※備考1: SQWE=0(方形波を出力しない)場合の出力レベル 0=LOW、1=HIGH
      参考回路の場合、LEDをプルアップ接続しているので LOWにするとLEDが点灯する

※備考2:SQWEの出力周波数
RS1 RS2 出力周波数
0  1Hz
0  4.096kHz
1 0  8.192kHz
1 1  32.768kHz


このレジスタに次の手順で読み書きを行います

書き込み(時刻設定)手順


読み込み手順
読み込み開始位置を指定する機能がないので、書き込み開始を指定してから読み込みを行います。
すると、書き込み開始位置からデータを読みだすことができます。



-- 時刻読み取りスケッチ --
初期時刻として 21/12/31 23:59:00 を設定しています。
※無駄に長いのは、ほぼ表示用の書式変換のせい
※バックアップバッテリーを付けていないので電源を切ると初期化されます

#include <Wire.h>
#define RTC_ADDRESS 0x68

// 2進化10進数(BCD)を10進数に変換
byte BCDtoDec(byte value) {
  return ((value >> 4) * 10) + (value & 0x0F) ;
}

// RTCのレジスタテーブル(8byte)
int RegTblPr[8];
int RegTbl[8];

// 状態変位用変数
uint8_t yer ;     // 年
uint8_t mon ;     // 月
uint8_t day ;     // 日
uint8_t sec ;     // 秒
uint8_t mnt ;     // 分
uint8_t how ;     // 時
uint8_t weekday ; // 曜日(1~7)

char strBuff[80] ;

void setup() {
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);

  Wire.begin(); // マスタとしてI2Cバスに接続する

  Wire.beginTransmission(RTC_ADDRESS);    // I2Cスレーブに対して送信処理を開始する
 
  Wire.write(0x00);  // レジスタのアドレスを指定する(0-63の内、先頭から)
  
  // 2進化10進数(BCD)で指定します。(03以外)
  // Wire.write(B10000000);  // [00] RTC HALT
  Wire.write(0x40);  // [00]Seconds(00秒)
  Wire.write(0x59);  // [01]Minutes(m分)
  Wire.write(B01100000 | 0x11);  // [02]Hours(h時)
  Wire.write(0x1);  // [03]WDay 1:日 2:月 3:火 4:水 5:木 6:金 7:土
  Wire.write(0x31); // [04]Date(31日)
  Wire.write(0x12); // [05]Month(12月)
  Wire.write(0x21); // [06]Years(2021年)
  Wire.write(B00010000); // [07] Start SQW Output
 
  // I2Cスレーブへの送信完了
  Wire.endTransmission();

}

void loop() {

  // レジスタのアドレスを先頭にする
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x00);
  Wire.endTransmission();
 
  // I2Cスレーブに8byteのレジスタデータを要求する
  Wire.requestFrom(RTC_ADDRESS, 8);
 
  // 8byteのデータを取得する
  int i;
  bool flagDisplay = false ;
  for (i = 0; i < 8; i++) {
    while (Wire.available() == 0 ) {}
    RegTbl[i] = Wire.read();
    if (RegTblPr[i] != RegTbl[i]) flagDisplay = true ;
    RegTblPr[i] = RegTbl[i];
  }
  if (flagDisplay) {
    // 現在日時 : グローバル変数にセット
    bool flagPM = ((RegTbl[2]&B00100000) == 0) ? false :true ;
    yer = BCDtoDec(RegTbl[6]) ;
    mon = BCDtoDec(RegTbl[5]) ;
    day = BCDtoDec(RegTbl[4]) ;
    weekday = BCDtoDec(RegTbl[3] & 0x07) ;
    how = BCDtoDec(RegTbl[2] & 0x1F) ;
    mnt = BCDtoDec(RegTbl[1] & 0x7F) ;
    sec = BCDtoDec(RegTbl[0] & 0x7F) ;
  
    sprintf(strBuff,"%02d/%02d/%02d (%s)%02d:%02d:%02d",yer,mon,day,((flagPM)?"PM":"AM"),how,mnt,sec);
    Serial.println(strBuff);
  }

  delay(250) ;
}

実行結果
 ちゃんと時刻が変化しています。


RAM領域の使用

56バイトなので使い道に悩みますが、不揮発性記憶領域として使用できます。

ArtduinoのWireクラスの制限で一度に読み書き出来るのは32バイトまでです。
書き込み時にも読み込み時にもエラーチェックを入れた方が良いです。

-- RAMに0~56の数字を書き込んで、読み出すテスト --

#include <Wire.h>
#define RTC_ADDRESS 0x68

// 2進化10進数(BCD)を10進数に変換
byte BCDtoDec(byte value) {
  return ((value >> 4) * 10) + (value & 0x0F) ;
}

char strBuff[80] ;
char strBuff2[80] ;

void setup() {
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);

  Wire.begin(); // マスタとしてI2Cバスに接続する
  // 書き込み
  Wire.beginTransmission(RTC_ADDRESS);    // I2Cスレーブに対して送信処理を開始する
  Wire.write(0x08);  // RAMの先頭アドレス
  Serial.print("書き込み:");
  for (int i=0;i<56;i++) {
    uint8_t ch = i ;
    if (Wire.write(ch) == 0) {
      Wire.endTransmission();
      Wire.beginTransmission(RTC_ADDRESS);
      Wire.write(0x08+i);  // 続きの書き込アドレスを指定する
      Wire.write(ch);
    }
    sprintf(strBuff,"%d ",i) ;
    Serial.print(strBuff);
  }
  Serial.println("");

  // I2Cスレーブへの送信完了
  Wire.endTransmission();

  // 読み出し
  Wire.beginTransmission(RTC_ADDRESS);    // I2Cスレーブに対して送信処理を開始する
  Wire.write(0x08);  // RAMの先頭アドレス
  Wire.endTransmission();

  // I2Cスレーブに56byteのレジスタデータを要求する
  int readLen = 56 ;
  Serial.print("読み込み:");
  while(readLen>0) {
    int len = Wire.requestFrom(RTC_ADDRESS, readLen); // 読み込めたデータ長さを取得する
    for (int j=0;j<len;j++) {
      int d = Wire.read();
      sprintf(strBuff,"%d ",d) ;
      Serial.print(strBuff);
    }
    readLen -= len ; // 残りバイト数::一度に読み込めるのは最大32バイトなので、全部読むまでループする
  }
  Serial.println("");
}

void loop() {
}



RTCユニット HW-111(以外にもDS1307を使用した多くのRTCユニットに当てはまる)

【重要】【注意】
 HW-111は充電池LIR2032の使用を想定しており、ボタン電池は充電されます。
 一次電池のCR2032は使えません。
 ※火災の危険がありますのでCR2032は使用しないでください。

HW-111(以外にもDS1307を使用した多くのRTCユニット)にCR2032を使用する場合

バックアップ電池を充電する必要がない場合 (CR2032を使用する場合)
パターンを切断して充電されないようにします。
ダイオードを外すのが分かりやすくて簡単です。


セットでついている4KByte EEPROM(AT24C32)と温度センサー(DS18B20)

DS1307を使用したRTCユニット HW-111の回路図を見ると、4KByte EEPROM(AT24C32)と温度センサー(DS18B20)が付いているのが分かります。

Amazonで5個セット699円のRTCユニットには
DS18B20は実装されていませんでした。
※基板にはHW-111の型番が印刷されています。
※DS18B20は1個320円くらい


EEPROM(AT24C32)の使い方

i2c_scannerでチェックすると、0x68と0x50の二つのアドレスが返ってきます。
AT24C32のデータシートを見るとAT24C32は0x50~0x57のアドレスを取るようですので0x50がEEPROMのI2Cアドレスのようです。
※複数のアドレスを設定できるのは、複数のEEPROMを接続できるようにするためだそうです。
※WH-111の回路図ではWPはGNDに繋がっているので、書き込み禁止機能は使えないようです。
The 32K/64K is internally organized as 256 pages of 32 bytes each.
Random word addressing requires a 12/13 bit data word address
とスペックシートには書かれていますので、内部は32バイトのページで区切られているようです。
AT24C32の場合は32バイトx128ページで構成されます。

使い方

データ書き込み(1バイト)


データ書き込み(複数バイト)
書き込みアドレスは下位5ビットのみインクリメントされ、上位ビットはインクリメントされません。
ページの境界を超えた書き込みは出来ないと言う事です。
アドレス[B00011111]の次はアドレス[B00000000]に対して書き込みが行われます。
アドレス[B00111111]の次はアドレス[B00100000]に対して書き込みが行われます。


データの読み込み(1バイト)
読み込み開始アドレスを指定するコマンドはないのでダミー書き込みを行ってから読み込みを行います。
※連続して読み込む場合、2バイト目以降はダミー書き込みを行わなくても、読み込むたびに読み込みアドレスがインクリメントされます。



データの読み込み(複数バイト)
読み込み開始アドレスを指定するコマンドはないのでダミー書き込みを行ってから読み込みを行います。
読み込み時にはページ境界は問題にならないようです。


※書き込み後にI2Cバスが解放されるまで最大10msのタイムラグがあります。
※書き込み/読み込み、書き込み/書き込みを連続して行うと開始処理に失敗する場合がありますのでエラー処理を入れた方が良いです。

-- LIST --
ページ境界を超える書き込みと読み出しのテスト

#include <Wire.h>
#define ROM_ADDRESS 0x50  // AT42C32のアドレス

char strBuff[80] ;  // シリアルモイニターに表示する文字列作成用の文字列バッファ

// コレを関数化しないと流石にメンドクサイので切り出しておきます
// I2Cバスが使用可能になるのを最大500m秒待ちます。
// 500m秒待って使用可能にならなかった時のことは考えてません。
void writeReadyCheck() {
  int errCount = 0 ;
  while(errCount < 100) {
    Wire.beginTransmission(ROM_ADDRESS);// I2Cスレーブに対して送信処理を開始する
    int ready = Wire.endTransmission(); // トランザクションが正常に終了する事を持ってI2Cバスが使用可能と判断
    if (ready == 0) {
      break ;
    }
    if (errCount > 100) break ;
    errCount ++ ;
    delay(5) ;
    break;
  }
}

void setup() {
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);
  Wire.begin(); // マスタとしてI2Cバスに接続する

  // 書き込み
  int wAddr = 0;    // 0x0000 から書き込み
  uint8_t adrH , adrL ; // アドレス
  int wBytes = 64 ; // 64バイト書き込み
  uint8_t wdata = 1;    // 書き込むデータ

  // 書き込み
  writeReadyCheck() ;

  while (wBytes > 0) {
    // 書き込み開始アドレスを上位バイトと下位バイトに分解
    adrH = (wAddr & 0x0F00) >> 8 ;
    adrL = wAddr & 0xFF ;
    // 書き込み開始アドレス送信
    Wire.beginTransmission(ROM_ADDRESS);    // I2Cスレーブに対して送信処理を開始する
    Wire.write(adrH);
    Wire.write(adrL);

    sprintf(strBuff,"\nWRITE[%04X] ",wAddr) ;
    Serial.print(strBuff);

    // データ送信
    while (wBytes > 0) {
      if (Wire.write(wdata) == 0) {
        // Wireクラスの制限で連続して書き込めるのは32バイト、33バイト目の書き込みは必ず失敗するので
        // 失敗したらトランザクションを一旦閉じて、再度、書き込みアドレスの指定から始める
        Wire.endTransmission();
        writeReadyCheck() ;
        // 書き込み開始アドレスを上位バイトと下位バイトに分解
        adrH = (wAddr & 0x0F00) >> 8 ;
        adrL = wAddr & 0xFF ;
        // 書き込み開始アドレス送信
        Wire.beginTransmission(ROM_ADDRESS);    // I2Cスレーブに対して送信処理を開始する
        Wire.write(adrH);
        Wire.write(adrL);
        break ;
      }
      wBytes -- ;
      wAddr ++ ;
      sprintf(strBuff,"%02d ",wdata) ;
      Serial.print(strBuff);
      wdata ++ ;

      if ((wAddr & B00011111) == 0) {
        // ページ境界を超えられないので、ページ境界に達したら
        // トランザクションを一旦閉じて、再度、書き込みアドレスの指定から始める
        Serial.print(" ページ境界");
        Wire.endTransmission();
        writeReadyCheck() ;
        // 書き込み開始アドレスを上位バイトと下位バイトに分解
        adrH = (wAddr & 0x0F00) >> 8 ;
        adrL = wAddr & 0xFF ;
        // 書き込み開始アドレス送信
        Wire.beginTransmission(ROM_ADDRESS);    // I2Cスレーブに対して送信処理を開始する
        Wire.write(adrH);
        Wire.write(adrL);
        break ;
      }
    }
  }
  Wire.endTransmission();
  Serial.println("");
  
  writeReadyCheck() ;
  
  // 読み込み
  int rAddr = 0;    // 0x0000 から読み込み
  int rBytes = 64 ; // 64バイト読み込み

  // ダミー書き込み
  // 書き込み開始アドレスを上位バイトと下位バイトに分解
  adrH = (rAddr & 0x0F00) >> 8 ;
  adrL = rAddr & 0xFF ;
  // 書き込み開始アドレス送信
  Wire.beginTransmission(ROM_ADDRESS);    // I2Cスレーブに対して送信処理を開始する
  Wire.write(adrH);
  Wire.write(adrL);
  Wire.endTransmission();

  // 読み込み開始
  while(rBytes>0) {
    // Wireクラスの制限で一度に読み込めるのは32バイトまでなので
    // 読み込むバイト数-読み込んだバイト数がゼロになるまで繰り返す
    int len = Wire.requestFrom(ROM_ADDRESS, rBytes);
    sprintf(strBuff,"\nREAD[%04X] ",rAddr) ;
    Serial.print(strBuff);
    for (int j=0;j<len;j++) {
      int d = Wire.read();
      sprintf(strBuff,"%02d ",d) ;
      Serial.print(strBuff);
    }
    rBytes -= len ;
    rAddr += len ;
  }
  Serial.println("");
}

void loop() {
}

実行結果


解説動画