2024年10月19日 星期六

MicroPython 學習筆記 : 即時時鐘 machine.RTC 用法

ESP8266/ESP32 模組都有內建 RTC 時鐘 (real time clock) 功能, 主要是用在低功耗深度睡眠時的時間計時與喚醒. 不過 ESP8266 的 RTC 功能較有限, 它沒有獨立的時鐘來源, 須從 NTP 伺服器取得網路時間來同步其 RTC 時鐘. ESP32 則具備完整的 RTC 功能, 它具備獨立時鐘源 (須提供 32.768 KHz 晶振以保持高精度), 可在低功耗模式下保持系統時間與透過計時器喚醒, 且具有慢速 (深眠) 與快速 (淺眠) 各 8KB RTC 記憶體可在進入睡眠模式時儲存喚醒所需的資料. 

ESP8266 使用 32 位元計時器來計時, 每 7 小時 45 分鐘左右會因為發生溢位導致時間錯誤, 故必須在此時限內呼叫 time.localtime() 或 time.time() 一次來更新時鐘. ESP32 因為使用 64 位元計時器, 提供了極長的計時範圍, 因此並無像 ESP8266 那樣的溢位問題, 基本上無須週期性地校正時間. 不過由於計時器精度導致的時間飄移, 通常還是需要定期透過 NTP 伺服器來校正時鐘. 

注意, ESP8266/ESP32 的 RTC 時鐘會在關機後消失, 重開機時又從 2000/1/1 開始計時, 因此開機後通常都需要倚賴 NTP 伺服器提供網路時鐘來設定內部時鐘, 而且預設使用內部 RC 震盪電路做為時鐘源, 其精度較差, 每小時會飄移約 180 秒. ESP32 雖可在指定腳位接上晶體震盪器做為外部時鐘源, 但每 10 小時也會飄移約 0.72 秒, 因此都需要周期性查詢 NTP 伺服器來更正時鐘. 


1. 設定 RTC 時鐘 : 

MicroPython 內建的 machine 模組中有一個用來存取 RTC 時鐘的 RTC 類別, 使用時先呼叫其建構式 RTC() 建立一個 RTC 物件, 當開發板重啟動時它預設會寫入 2000/1/1 0 時的初始日期時間, 然後 RTC 裡的計時器就會開始計時 (單位是微秒), 例如 : 

MicroPython v1.23.0 on 2024-06-02; Generic ESP32 module with ESP32
Type "help()" for more information.  
>>> from machine import RTC   
>>> rtc=RTC()    
>>> type(rtc)    
<class 'RTC'>    

用 dir() 檢視物件內容 : 

>>> dir(rtc)     
['__class__', 'datetime', 'init', 'memory']   

呼叫 datetime() 方法不傳入參數為 getter 方法, 會傳回 RTC 目前的即時日期時間 : 

>>> rtc.datetime()   
(2000, 1, 1, 5, 0, 0, 39, 767046)     # (年, 月, 日, 星期, 時, 分, 秒, 微秒)

前三個元素是日月年, 第四個元素是星期 (星期一=0), 第 5~7 個元素是時分秒, 最後一個是微秒. 如果呼叫 datetime() 方法時傳入一個日期時間 tuple 為 setter, 可以設定起始時間 (周與微秒可一律設為 0, 函式會自動設定) :

>>> rtc.datetime((2024, 10, 18, 0, 17, 02, 33, 0))   
>>> rtc.datetime()   
(2024, 10, 18, 4, 17, 2, 37, 848969)    

time 模組的 localtime() 函式也是去 RTC 讀取即時時鐘 :

>>> import time    
>>> time.localtime()     
(2024, 10, 18, 17, 3, 0, 4, 292)    #  (年, 月, 日, 時, 分, 秒, 星期, 天數)

如果要讓 RTC 紀錄目前本地時間, 可以在開發板一開機時先連上網路, 然後利用 MicroPython 內建的 ntptime 模組從 NTP 伺服器取得 UTC 的時戳後加上時差秒數 (台灣是 UTC+8, 要加 28800 秒) 傳給 time.localtime() 即可設定 RTC :

>>> import xtools   
>>> import config   
>>> ip=xtools.connect_wifi(config.SSID, config.PASSWORD)    
Connecting to network...
network config: ('192.168.192.189', '255.255.255.0', '192.168.192.92', '192.168.192.92')

>>> import ntptime    
>>> ntptime.time()    # 傳回 UTC 的時戳秒數, 注意, 若 NTP 無回應會出現例外
782558499 

把這個 UTC 時戳傳給 time.localtime() 會傳回 UTC 時間的日期時間 tuple (年, 月, 日, 時, 分, 秒, 星期, 天數), 但台灣時間為 UTC+8 小時, 因此加上 28800 秒就可以得到台灣時間的時戳秒數 : 

>>> t=time.localtime(ntptime.time() + 28800)    
>>> t   
(2024, 10, 18, 17, 22, 6, 4, 292)

我們可以從傳回的元組 (年, 月, 日, 時, 分, 秒, 星期, 天數) 中的前 6 個元素取得日期時間, 利用 RTC 物件的 datetime() 方法來設定 RTC 時鐘 : 

>>> t[0:3] + (0,) + t[3:6] + (0,)  
(2024, 10, 18, 0, 17, 22, 6, 0) 

此處 t[0:3] 是從 localtime() 傳回的 tuple 中取出前三個元素 (日期), t[3:6] 是取出後三個元素 (時間), 而 (0, ) 則是給 tuple 添加一個元素 0 (這是 Python 元組添加單一元素的固定語法), 分別是星期幾及微秒數, 預設給 0 即可. 

然後呼叫 RTC 物件的 datetime() 並傳入此元組設定 RTC 時鐘為台灣時間 :

>>> rtc.datetime(t[0:3] + (0,) + t[3:6] + (0,))  # 設定 RTC 時鐘

這時查看 RTC 時間會發現已被更新為目前的本地時間 :

>>> rtc.datetime()    
(2024, 10, 18, 4, 17, 22, 21, 689026) 

ntptime 模組還有一個函式 settime() 可以用來設定 RTC 時鐘 :

>>> ntptime.settime()    

但是 ntptime.settime() 會在查詢 NTP 伺服器後直接將 RTC 設為 UTC 時間, RTC 時鐘儲存 UTC 時間會在使用時很麻煩, 每次都還要加 28800 秒才會得到台灣時間, 還不如上面像那樣用 ntptime.time() 取得 UTC 後加 28800 再去設定 RTC 時鐘, 讓 RTC 時鐘直接儲存台灣時間在使用上更方便. 

除了按 Reset 外, 如果要將 RTC 時間重設回預設的 2000/1/1 日 0 時可這樣做 : 

>>> rtc.datetime((2000, 1, 1, 0, 0, 0, 0, 0))    
>>> rtc.datetime()     
(2000, 1, 1, 5, 0, 0, 9, 358765)   
>>> time.localtime()      
(2000, 1, 1, 0, 0, 16, 5, 1)    

或者也可以用較低階的初始化方法 init(), 通常用在系統一開機或 Reset 之後對 RTC 硬體與計時器進行初始化設定, 而 datetime() 雖然也可以設定時鐘, 但 datetime() 較高階, 它不會對 RTC 硬體做初始化, 只會設定計時器而已. init() 的傳入參數與 datetime() 的一樣, 為 (年, 月, 日, 星期, 時, 分, 秒, 微秒) 格式的 tuple, 例如 : 

>>> rtc.init((2000, 1, 1, 0, 0, 0, 0, 0))   
>>> rtc.datetime()     
(2000, 1, 1, 5, 0, 0, 27, 989084)     # (年, 月, 日, 星期, 時, 分, 秒, 微秒) 

參考 : 



2. 修改 xtools 函式庫的 tw_now() 函式 :    

這兩天在測試 SSD1306 時發現呼叫 xtools.tw_now() 出現時間異常, 若查詢 NTP 失敗, 有時傳回比正確時間落後 8 小時的時間, 檢查原始碼發現應該是呼叫 ntptime.settime() 直接將 UTC 時戳寫入 RTC 時鐘所致 : 

def tw_now():
    try:
        print('Querying NTP server and set RTC time ... ', end='')
        ntptime.settime()   
        print('OK.')
        delta=28800
    except:
        print('Failed.')
        delta=0
    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + delta)
    t='%s-%s-%s %s:%s:%s' % \
    (Y, pad_zero(M), pad_zero(D), pad_zero(H), pad_zero(m), pad_zero(S))
    return t

如上所述, 直接將 UTC 時戳寫入 RTC 時鐘的缺點是每次呼叫 time.localtiome() 時還要加 28800 秒才會得到台灣時間; 其次是上面 tw_now() 原實作方式當查詢 NTP 失敗時推測 RTC 應該保持原計時才對, 但實測卻傳回少 8 小時的 UTC 時間, 為了解決此問題將 tw_now() 改寫如下 :

def tw_now():
    try: # 從 NTP 取得 UTC 時戳加 8 為台灣時間, 若成功設定 RTC
        print('Querying NTP server and set RTC time ... ', end='')
        utc=ntptime.time() # 取得 UTC 時戳
        print('OK.')
        t=time.localtime(utc + 28800) # 傳回台灣時間的元組
        rtc=RTC() # RTC 物件
        rtc.datetime(t[0:3] + (0,) + t[3:6] + (0,)) # 設定 RTC
    except:  # 查詢 NTP 失敗不設定 RTC 
        print('Failed.')
    return strftime()  # 傳回目前之日期時間字串 YYYY-mm-dd HH:MM:SS

用 while 迴圈測試功能正常 :

>>> import time   
>>> while True:   
    xtools.tw_now()
    time.sleep(60)
    
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:41:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:42:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:43:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:44:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:45:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:46:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:47:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:48:26'
Querying NTP server and set RTC time ... OK.
'2024-10-19 12:49:26'
... (略) ...
Querying NTP server and set RTC time ... OK.
'2024-10-19 13:01:28'
Querying NTP server and set RTC time ... OK.
'2024-10-19 13:02:28'
Querying NTP server and set RTC time ... OK.
'2024-10-19 13:03:28'
Querying NTP server and set RTC time ... Failed.
'2024-10-19 13:04:29'
Querying NTP server and set RTC time ... OK.
'2024-10-19 13:05:29'
... (略) ...
Querying NTP server and set RTC time ... OK.
'2024-10-19 15:35:45'
Querying NTP server and set RTC time ... OK.
'2024-10-19 15:36:45'
Querying NTP server and set RTC time ... OK.
'2024-10-19 15:37:45'
Querying NTP server and set RTC time ... Failed.
'2024-10-19 15:38:46'
Querying NTP server and set RTC time ... OK.
'2024-10-19 15:39:46'

可見查詢失敗時就會傳回 RTC 時鐘的時間. 

RTC 還提供記憶體讓 CPU 進入深度睡眠時儲存資料以備喚醒時使用, 這部分放在低功耗睡眠測試時再來測試. 

沒有留言 :