2017年8月11日 星期五

*0*RTC(即時時鐘)模組*0* 如何更新 DS3231 RTC 模組的時間與大型數字時鐘製作

網頁最後修改時間:2017/08/11 

RTC ( Real-Time Clock, 即時時鐘) 常用於需要提供時間戳記的應用上,在一些不易取得市電供應且耗電要求低的地方,就會需要這樣的一個可用鈕扣電池驅動的 RTC 模組提供時間紀錄,只要根據實際需要選擇年、日/月、(12/24格式)小時:分鐘和秒並加以組建,就是一條時間戳記。

而在這篇網頁中,DS3231 (+AT24C32, 32KBytes EEPROM) RTC 模組會在程式編譯上傳的同時被更新年份、日期和時間,配合整合型 LCD (I2C模式) 和大型數字顯示方式,分別在 LCD 上以四個不同頁面分別以年、日/月、(12/24格式)小時:分鐘和完整格式的方式顯示,藉由這種方式讓使用者了解 RTC 模組的基本使用方法,請看影片

當然,DS3231 RTC 模組不只是一個時鐘而已,還有可程式的方波輸出功能、兩組日曆鬧鐘可以設定和內建精度 ±3°C 的數位溫度感測器可以使用,另外此模組也外掛了一顆 32KBytes 的 EEPROM 晶片可用來儲存資料,有用到時千萬別忘了 !

相關更詳細的資料請參閱晶片手冊 (或賣場的附件資料),或上網搜尋!
時間戳記對於資料的紀錄很重要,之後在其他地方也會用到,所以關於 DS3231 RTC 模組其他的功能就不多講了,但不管如何,RTC 模組時間的設定是一個重點,一定要會!

接線:

下面是使用 Arduino Nano 做為微控制器的接線圖,不過同樣也適用於其他的 Arduino 開發板,因為是使用 I2C 通訊,最多只是接腳編號不一樣而已,換一下就能用!

另外有一點需要注意,DS3231 RTC 模組適用於 DC 3V3 與 DC 5V 的系統,雖然這裡選用的是 DC 5V 系統,但是只要將微控制器 (例如 ESP8266 開發板) 與整合型 LCD 更換為 3V3 版本的,就是適用於 DC 3V3 的系統。
DS3231 RTC Module + 整合型 LCD 參考接線圖
下圖是使用 Arduino Nano 擴充底板 連接 DS3231 (+AT24C32, 32KBytes EEPROM) RTC 模組整合型 LCD (I2C模式) 的實際接線照片
實際接線與程式執行情形 - 顯示年份於整合型 LCD 螢幕上
只要準備夠多的杜邦線 (母對母),配合 Arduino Nano 擴充底板與周邊模組,上面多到爆的 Vcc 和 GND 與額外拉出的 UART 與 I2C 接腳,實際佈線連接時非常的方便且快速,可節省蠻多時間!

Arduino 函式庫:

接線完成之後,先來認識等一下會用到的 DS3231 暫存器。基本上,只有位址 0x00 ~ 0x06 會用到,分別代表秒、分鐘、小時、日、月和年,可讀取或是寫入 (變更) 時間,是 BCD 碼表示法
DS3231 暫存器位址與說明, 來源:maxim integrated, ds3231 datasheets
請注意,這篇網頁所選用的函式庫與雲端資料夾上面的不同,位址 0x00 ~ 0x06 必須一次寫入不能分開設定;簡言之,年、月、日、小時、分鐘、秒不能單獨設定!
如何更新 RTC 模組的時間?

RTC 模組要更新時間可以在任何時間不準確的時候來做,但是對於微控制器來說不是那麼的方便 (我是這樣認為) ? 方便且又實用的方法,就是在編譯的時候順便變更時間,而使用的方 法就是檢查 DS3231 暫存器地址 0x0F 裡的第 7 個字元 (bit) OSF ( Oscillator Stop Flag, 振盪器停止旗標 ),OSF 只有在下面幾種情況時被設置為 1:
  • 第一次通電 (或是只裝上鈕扣電池時)
  • VCC 和 VBAT 上的電壓不足以支持振盪器動作
  • 在電池備份模式,地址 0x0E 的 /EOSC 位元被關閉
  • 晶體的外部影響 ( 雜訊、漏電流 ... 等 )
利用上面說到的第一點,假設拿到的 DS3231 RTC 模組並沒有裝上鈕扣電池,下載並安裝好上面下載的函式庫,開啟一個空白的 Sketch 並預先上傳到 Arduino 開發板,移除之前已燒錄的韌體 ( 只要沒有用到 RTC 函式庫的程式都可以用來預先上傳,為什麼 ? 下面會談到 ! )。

在 Arduino IDE 選單 "File/Examples/RTClib" 開啟 "ds3231" 草稿檔 ( Sketch ),接著設定好 "Tools" 選單下面的 "Board""Processor""Port" 各選項

對照下面程式碼,修改 ds3231.ino 裡 setup() 函式跟下面一樣;修改之後另存新檔為 ds3231_timesync

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void setup () {

  Serial.begin(9600);

  delay(3000); // wait for console opening

  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, lets set the time!");
    // following line sets the RTC to the date & time this sketch was compiled
    DateTime timeupdate = DateTime( F( __DATE__ ), F( __TIME__ ) ) + TimeSpan( /*days*/0, /*hours*/0, /*minutes*/0, /*seconds*/20 );
    // This line sets the RTC with an explicit date & time, for example to set
    // January 21, 2014 at 3am you would call:
    rtc.adjust( timeupdate );
  }
}

line 15 是最重要的一行,timeupdate 是最終要寫入到 RTC 模組裡面 (暫存器位址 0x00 ~ 0x06 ) 設定時間的,包含下面兩個部分的加總:
  • DateTime( F( __DATE__ ), F( __TIME__ ) ) 
    這一部分所產生出來的時間是程式編譯的時間 (2017/08/10 23:04:10)
  • TimeSpan( /*days*/0, /*hours*/0, /*minutes*/0, /*seconds*/20 )
    這一部分我添加上去,用來補償編譯至上傳完成之後執行 RTC 模組時間更新的時間差。如果沒有加上這一段的話,則最後在 Serial Monitor 上面的時間會與作業系統的時間出現多十幾秒以上的時間差。
TimeSpan 的設定應該只需要設定 "senonds",其他欄位只要設定為 0 即可。那麼要設定為多少呢 ? 這取決於電腦速度會有不同的編譯時間,不過使用 Arduino IDE 編譯時並不會幫我們計算編譯至完成程式上傳的時間,所以必須自己算;在按下編譯按鈕的同時開始計時,完成編譯停止計時,然後將這時間填入到 TimeSpan 的欄位中進行第一次更新。
程式編譯至上傳結束的時間計時
時間更新完成之後,打開 Serial Monitor 比較輸出的時間與系統之間的時間差,再次對 TimeSpan 欄位做修正,就可以得到跟系統時間不到 1 秒鐘差異的時間。
DS3231 RTC 模組時間校對之後與系統時間的比對
而在這設定之中最重要的步驟就是讓每次程式上傳的時候,RTC 模組的 OSF 位元都必須是處在 1 的情況才能更新成功,否則即便程式重新上傳也不會正常更新,這部分上面已經有說過。

所以歸納之後可得到步驟流程如下:
首先,先確認 Arduino 開發板通電 (這裡指的是將 USB 插上電腦,通電與通訊),RTC 模組鈕扣電池拔掉也沒通電
  1. 打開 Arduino IDE (建議使用 v1.8.3) 並 ( "File/New" )  開啟一個新的草稿檔
  2. Arduino 開發板通電後,設定好 "Tools" 選單下面的 "Board""Processor" 和 "Port" 各選項,編譯並上傳程式 *
  3. RTC 模組通電,這時 OSF 會被設定為 1
  4. 打開上面剛剛修改過的 ds3231_timesync 程式,編譯之後上傳
  5. 打開 Serial Monitor ( 設定為 9600 bps ) 對照輸出的時間和系統時間之間的差距 **
  6. RTC 模組時間差異可接受,直接裝上鈕扣電池,完成 RTC 模組時間設定;否則,下一步驟
  7. 將步驟 5 的時間差,回填修改到 setup() 函式裡 TimeSpan 的引數
  8. 將 RTC 模組斷電,跳至步驟 1
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

*     由於修改 RTC 模組時間的程式會重複上傳幾次,若上傳之前就存在之前的程式,那麼是無法在正常情況下進行修改的;因為在上傳程式的時候,即便 OSF 先被設置為 1,由於 Arduino 開發板是在通電的情況,程式還未上傳就已被就程式修改過了,新程式是來不及更新時間的。
**    編譯的時間跟電腦效能有很大關係,所以為了減少每一次編譯相同程式的時間,不要開一堆耗能的程式。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*


大型數字時鐘製作?

這個數字時鐘使用到自訂字元的方式顯示大型數字,這些大型數字字元的介紹與說明,在部落格的這個網頁中 { 單晶片 + Arduino + 樹莓派 } 整合型 LCD ( @ I2C 模式 ) 的漂亮數字顯示 ( 自訂字型或圖案 ) 有詳細說明,在這裡我們將這些常用的程式碼組合成一個 Arduino 標頭檔案來用,有該零件雲端資料夾的,可以在 codes/Arduino/RTC 模組大型數字標頭檔 資料夾中下載 IICLCDLargeNumber.h 這個檔案來用。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 程式碼:
這邊的程式碼使用 ds3231_timesync 這個程式進行修改,因此同樣具備修改 RTC 模組時間的功能,同時加上了對於整合型 LCD 的支援,可顯示影片中展示的時間各項資訊。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <Wire.h>
#include <RTClib.h>

/** 整合型 LCD @ IIC Mode */
#include "IICLCDLargeNumber.h"

/** DS3231 */
RTC_DS3231 rtc;

/** time update */
unsigned long previousMills = 0;
const long interval = 1000;         // milliseconds
int idx = 0;
char buffer[16];

void setup() {
    Serial.begin( 9600 );

    /** DS3231 RTC Module */
    rtc.begin();                    // Wire.begin() 在此初始化了
    if (rtc.lostPower()) {
        Serial.println("RTC lost power, lets set the time!");
        // following line sets the RTC to the date & time this sketch was compiled
        DateTime timeupdate = DateTime( F( __DATE__ ), F( __TIME__ ) ) + TimeSpan( /*days*/0, /*hours*/0, /*minutes*/0, /*seconds*/13 );
        // 預先編譯得知編譯時間之後,補上所花費的時間 (second)    
        rtc.adjust( timeupdate );
        // This line sets the RTC with an explicit date & time, for example to set
        // January 21, 2014 at 3am you would call:
        // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
    }

    /** 整合型 LCD */
    initLCD();
    delay(100);
    clearLCD();
    delay(100);
    // 自訂字元寫入
    writeCGRAM( &CGRAM_block[0],  3, 1 );
    writeCGRAM( &CGRAM_block[24], 3, 4 );
    displayCharOnLCD( 1, 1, "**DS3231 ready**", 16 );
    delay( 1000 );
    displayCharOnLCD( 2, 1, "**IIC LCD ready*", 16 );
    delay( 900 );
    clearLCD();
    delay( 100 );
}

void loop() {

    // time update very second
    unsigned long currentMillis = millis();
    if( currentMillis - previousMills >= interval ){

        previousMills = currentMillis;

        DateTime now = rtc.now();
        ++idx;
        if( idx == 17 ) {
            clearLCD();     // 清除螢幕
            idx = 1;
        }

        switch( idx ) {
            case 1:
                // 2017
                sprintf( buffer, "%4d", now.year() );
                displayNumeric( buffer, 4 );
            break;
            case 3:
                // 08/09
                sprintf( buffer, "%02d%02d", now.month(), now.day() );
                displayCharOnLCD( 1, 9, "/", 1 );
                displayCharOnLCD( 2, 9, "/", 1 );
                displayNumeric( buffer, 4 );
            break;
            case 5:
                // 15:51
                sprintf( buffer, "%02d:%02d", now.hour(), now.minute() );
                displayNumeric( buffer, 5 );
            break;
            default: {
                if( ( idx > 7 ) && ( idx < 16 ) ) {
                    // ***2017/08/09*** 
                    sprintf( buffer, "***%4d/%02d/%02d***", now.year(), now.month(), now.day() );
                    displayCharOnLCD( 1, 1, buffer, 16 );
                    // ****15:51:38****
                    sprintf( buffer, "****%02d:%02d:%02d****", now.hour(), now.minute(), now.second() );
                    displayCharOnLCD( 2, 1, buffer, 16 );
                }
            }
        }
    }
}

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 優化:加入判斷 I2C 裝置存在的程式碼:
眼尖的使用者不知道看到沒有 ? 在 setup() 函式中 rtc.begin() 原本的 if 判斷式,整個被移除掉了,為什麼呢 ? 因為這個判斷式決不會產生作用,因為回傳都是 true,有跟沒有一樣 !

所以接下來,就要加入這個檢測 I2C 裝置的功能到上面的程式中,它可以用來輔助確認接線是否正確以及檢測裝置是否存在。

在上面程式的最下方,加入下面的這一個新增的函式

1
2
3
4
5
6
bool checki2cdevice( uint8_t i2caddr ) {
    // 呼叫此函示之前, Wire 必須先初始化
    Wire.beginTransmission( i2caddr );
    if( Wire.endTransmission() == 0 ) return true;
    return false;
}

接著到 setup() 裡面,對照下面修改裡面的程式碼 (就是插入兩行 //----- 之間的程式碼 ),重新編譯上傳即可 ( RTC 模組不需要斷電 )

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    /** DS3231 RTC Module */
    rtc.begin();                    // Wire.begin() 在此初始化了
    //---------------------
    delay(10);                      // wait wire to stable
    if( !checki2cdevice( DS3231_ADDRESS ) )  { 
        Serial.println( "DS3231 RTC Module not found!" );   // 沒有 delay(10), 這裡輸出不了!
        while(1) delay(1);
    }
    if( !checki2cdevice( IIC_ADDR_LCD1 ) )   {
        Serial.println( "LCD not found!" );
        while(1) delay(1);
    }
    //---------------------
    if (rtc.lostPower()) {
        // ...省略...
    }

之後,只要整合型 LCD 或是 DS3231 RTC 模組沒有裝好,就會在 Serial Monitor 輸出找不到該裝置的訊息。所以之後若是出現問題,別忘了打開 Serial Monitor 看看!

*********************************************************************************
相關模組可至露天賣場訂購:
*********************************************************************************

結論:
在本篇網頁中,提供了設定 RTC 模組時間的步驟,以及時間設定與取出做為數位時鐘顯示的程式碼,展示了 DS3231 RTC 模組基本的操作,並且在最後提供了檢測 I2C 裝置的方法,在電源取得不易且沒有網路或是任何資源可取得時間的環境下,就可選擇 RTC 模組來做為保持時間運行的裝置。

<< 部落格相關網頁 >>

沒有留言:

張貼留言