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 進入深度睡眠時儲存資料以備喚醒時使用, 這部分放在低功耗睡眠測試時再來測試.
沒有留言:
張貼留言