我學到的使用 DS3231 實時時鐘中斷 Arduino 的技巧
您想要確定 Arduino 在草圖中運行特殊程式碼段的精確時間,而不使用 Arduino 硬體中的計時器和計數器。您具有使用 Arduino IDE 編寫程式碼的一些技能和經驗,並且您希望避免使用諸如delay()
之類的程式碼語句。
相反,您希望連接一個非常精確的 DS3231 即時時脈模組作為外部中斷來源。即使 Arduino 暫時斷電,DS3231 也可以使用電池來保持準確的時間,這對您來說可能很重要。
最後,您想了解如何使用 Andrew Wickert 的 DS3231.h 庫,該庫在 Arduino 線上參考中引用:https://www.arduino.cc/reference/en/libraries/ds3231/。它包含一些值得努力掌握的技巧。您可以使用庫管理員(工具 > 管理庫...)將此庫匯入到 Arduino IDE 中。
一步一步來。本教學示範了以下步驟:
如果您希望特殊代碼按照您指定的時間間隔運行多次,您的程式碼可以再次執行步驟 3 並設定新的警報。本教學附帶的範例草圖以 10 秒的間隔重複中斷 Arduino。
本教學取自「官方」參考資料,包括:
attachInterrupt()
函數的Arduino參考:https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/。 本例中的特殊代碼很簡單:它只列印 DS3231 的當前時間。現實生活中的範例可能會做一些有用的事情,例如給植物澆水或記錄溫度測量結果。無論什麼任務,程式碼都應該進入自己的特殊區塊,僅在 DS3231 警報中斷 Arduino 時才運行。
我認為將程式碼分解為短函數是一種很好的做法,其中每個函數只處理一個任務或一組相關任務。函數的名稱可以是任何名稱;為什麼不讓它描述這個函數的作用呢?這是我的函數的一部分,用於運行本範例中的特殊程式碼。
void runTheSpecialCode() {
// get the current time
// using the DateTime and RTClib classes
// defined in DS3231.h
DateTime dt = RTClib::now();
// print the current time
Serial.print(dt.hour()); Serial.print(":");
if (dt.minute() < 10) Serial.print("0");
Serial.print(dt.minute()); Serial.print(":");
if (dt.second() < 10) Serial.print("0");
Serial.println(dt.second());
// There will be more to do here, as you will see.
// This is enough, for now, to illustrate the idea:
// put special code in its own, special function
}
您將在五對引腳之間佈線。每對執行一個電氣用途,並將 Arduino 上的引腳與 DS3231 上的相應引腳相匹配。慢慢地,連接每一對,然後檢查兩端,確保每條電線都在它應該的位置。該表按從左到右從 Arduino Uno 連接到 DS3231 的順序列出了這些對。
目的 | DS3231 腳 | Arduino 腳 |
---|---|---|
警報 | 量子阱 | 3* |
SCL | SCL | SCL** |
SDA | SDA | SDA** |
5伏特電源 | 電壓控制電路 | 5V |
地面 | 接地 | 接地 |
就像我說的,慢慢來建立這些連結。緩慢而確定通常是正確完成任何事情的最快方法。
我們將透過 DS3231「物件」(一種帶有名稱的軟體工具箱)與 DS3231 模組進行通訊。 DS3231 庫在盒子內部定義了許多功能——將它們視為工具。當我們想要使用一個函數時,我們寫下工具箱的名稱,後面跟著一個點,然後是工具的名稱。範例草圖為此目的創建了一個“時鐘”變數。然後草圖就可以存取「時鐘」框中的工具了。如上所述,所有工具均在 DS3231.h 檔案中聲明。
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
我們將使用“時鐘”物件中的工具來設定鬧鐘。但首先我們需要計算鬧鐘時間。
此步驟假設您之前已在 DS3231 上設定了實際時間。範例草圖包含可用於設定時鐘時間的程式碼(如果您需要)。只需刪除其周圍的註釋分隔符號 /* 和 */ 即可。
本教程中的範例草圖透過將間隔(以秒數表示)添加到當前時間來計算未來的鬧鐘時間。該範例增加了十秒。一分鐘就會增加 60 秒。一小時將增加 3,600 秒。一天,86,400 秒。等等。
DS3231 庫有一個隱藏的“技巧”,可以輕鬆添加以秒為單位的時間。您不會在 DS3231 庫的 README 頁面上的可用函數中找到此技巧。透過查看 DS3231.h 檔案也不是完全顯而易見。一些細節有待在 DS3231.cpp 程式碼檔案中找到。以下是執行計算的步驟。
now()
函數,需要以特殊方式存取函數。步驟 4 和步驟 5 可以合併。
const Uint32_t interval = 10; // number of seconds to add
DateTime currentTime; // default declaration
currentTime = RTClib::now(); // RTClib is defined in DS3231.h
uint32_t currentSeconds = currentTime.unixtime(); // express the date in seconds
DateTime alarmTime(currentSeconds + interval); // add 10 seconds and create a new date
儘管alarmTime物件是基於秒數創建的,但它在其工具箱中提供了工具來表達其年、月、日、小時、分鐘和秒。我們在 DS3231 上設定鬧鐘時間,如下所述,使用 AlarmTime 物件作為我們需要的值的來源。
例如,假設 DS3231 模組報告的 currentTime 為 2021 年 10 月 27 日星期三上午 10:42 7 秒。
DS3231 提供兩種不同的警報:警報#1 (A1) 和警報#2 (A2)。兩個警報都可以指定為日期和時間,小至一分鐘。不同之處在於 A1 可以進一步指定為秒。每個鬧鐘在 DS3231 庫中都有自己的一對函數,用於設定鬧鐘時間和讀取該時間。這些功能都是透過 DS3231 物件存取的,例如我們命名為「clock」的物件:
時鐘.setA1Time()、時鐘.getA1Time()、時鐘.setA2Time() 和時鐘.getA2Time()
setA1Time() 函數採用八個參數,如 DS3231.h 檔案中的參考所列:
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
仔細閱讀DS3231.h頭檔可以解釋這些參數。接下來是我嘗試用自己的話向自己重新解釋它們。如果讀者發現我的版本和頭檔之間有任何差異,請假設頭檔是正確的。
前五個參數的類型為“byte”。 cppreference 網站以此方式定義位元組類型 https://en.cppreference.com/w/cpp/types/byte:
std::byte 是一種獨特的類型,它實作了 C++ 語言定義中指定的位元組概念。
與 char 和 unsigned char 一樣,它可用於存取其他物件(物件表示)所佔用的原始內存,但與這些類型不同的是,它不是字元類型,也不是算術類型。位元組只是位元的集合,為其定義的唯一運算子是位元運算子。
在這種特殊情況下,我們可以將日期和時間的位元組類型變數視為無符號整數。它們可以保存 0 到 255 之間的整數值。例如,值 102 對於這些參數中的任何一個都沒有意義。作為程式碼編寫者,您的工作就是提供合理的值。
讓我們繼續在上一步中創建的alarmTime:該月的第27天,上午10點42分17秒。下面的清單顯示如何將這些值提供給函數。我將每個參數列出在自己的行中,以使它們更容易被人類閱讀並為註釋留出空間。這裡的例子並不完整;它僅演示日期和時間的位元組類型值。此函數需要更多參數,如下所述,並且不會以此處顯示的形式運行。
順便說一下,請注意「clock」和「alarmTime」變數是對象,也就是說,它們是軟體工具箱。如您所見,我們使用對應工具箱內的工具來存取物件包含的資訊。
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
// ... the remaining parameters are explained below
);
下一個位元組類型參數名為 AlarmBits,實際上只是位元的集合。這些位元的名稱如 DS3231 資料表(第 11 頁)所定義。
位元 7 | 位元 6 | 位元 5 | 位元 4 | 位元 3 | 位元 2 | 位元 1 | 位元 0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | 鎝 | A1M4 | A1M3 | A1M2 | A1M1 |
這些位元一起形成一個「掩碼」或模式,告訴 DS3231 何時以及以何種頻率發出警報訊號。資料表第 12 頁的表格給出了不同位元集合的意義。根據該表,本教程中的範例草圖使用以下位元集合:
-- | -- | -- | 鎝 | A1M4 | A1M3 | A1M2 | A1M1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
這種位的排列可以在代碼中明確表達: 0x00001110
。它告訴 DS3231 在「秒匹配」時發出警報訊號,即當警報設定的「秒」值與當前時間的「秒」值相符時。
setA1Time()
函數的最後三個參數是布林值或真/假值。它們告訴 DS3231 有關如何評估警報設定的更多資訊。以下程式碼片段顯示了對 setA1Time() 的完整調用,繼續上面開始的範例:
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
0x00001110, // AlarmBits = signal when the seconds match
false, // A1Dy false = A1Day means the date in the month;
// true = A1Day means the day of the week
false, // A1h12 false = A1Hour in range 0..23;
// true = A1Hour in range 1..12 AM or PM
false // A1PM false = A1Hour is a.m.;
// true = A1Hour is p.m.
);
在範例草圖中,我們將鬧鐘設定為每 10 秒中斷一次,只有 A1Second 和 AlarmBits 參數很重要。但是,當我們呼叫setA1Time()
函數時,我們需要提供所有這些。提供正確的值並不比提供垃圾值更困難;我們不妨照顧他們。
setA2Time()
函數的工作方式類似,但沒有秒數參數。花一些時間查看庫中 DS3231.h 檔案的第 119 行到 145 行以及資料表中的第 11-12 頁。耐心地閱讀這些參考資料,直到您在其中找到設定鬧鐘時間所需的資訊。
設定時間後,程式必須採取額外的操作來啟用 DS3231 中的警報。我鼓勵讀者始終遵循三步驟順序,即使某些步驟因某些原因有時看起來不太必要。如果你的程式碼一定會出錯,那就讓它在確定性方面出錯。
對於警報 A1,DS3231 庫中的指令為:
turnOffAlarm(1); // clear the A1 enable bit in register 0Eh
checkIfAlarm(1); // clear the A1 alarm flag bit in register 0Fh
turnOnAlarm(1); // set the A1 enable bit in register 0Eh
對於警報A2,只需將參數改為checkIfAlarm(2); // clear A2 flag bit in register 0Fh
即可。 checkIfAlarm(2); // clear A2 flag bit in register 0Fh
。
@flowmeter 在此儲存庫中描述的一個問題強調,必須先清除兩個警報標誌,然後 DS3231 才能發出警報信號。為了確定這一點,請考慮呼叫 checkIfAlarm() 兩次,每個警報呼叫一次,即使您只使用其中一個警報:
checkIfAlarm(1);
checkIfAlarm(2);
為什麼程式碼編寫者會選擇「檢查」他們認為目前沒有發送訊號的警報?原因是checkIfAlarm()
函數有一個不明顯的副作用。它清除警報標誌位。我們使用函數checkIfAlarm()
,因為它是 DS3231 函式庫中唯一執行必要操作的函數。
想一想。由於以下將要解釋的原因,Arduino 中斷感應硬體需要在警報發生之前 DS3231 的 SQW 引腳上的電壓為高電平。警報事件改變了 DS3231 內部的兩件事:
只要這些警報標誌位之一保持置位,SQW 引腳就會保持低電平。只要 DS3231 內部的警報標誌位元將 SQW 接腳保持為低電平,Arduino 就無法再偵測到任何警報。為了使 DS3231 恢復其 SQW 警報引腳上的高電壓,必須清除警報標誌位元。請參閱 DS3231 資料表第 14 頁「狀態暫存器 (0Fh)」中對位元 1 和 0 的討論。
每個警報在 DS3231 內部都有自己的警報標誌位。任一警報標誌位元都可以將 SQW 接腳保持為低電位。 DS3231 不會主動清除警報標誌位。警報發生後清除警報標誌位是代碼編寫者的工作。
您的主循環不需要測量時間。只需要檢查一個標誌就可以知道是否發生了警報。在範例草圖中,此標誌是一個名為「alarmEventFlag」的布林變數:
if (alarmEventFlag == true) {
// run the special code
}
大多數時候,標誌將為false ,循環將跳過特殊代碼。草圖是如何立flag的?三個步驟:
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
函數將所有這些整合在一起。以下範例告訴 Arduino 硬體在指定數位引腳上偵測到「FALLING」訊號時立即執行rtcISR()
函數。attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
出於深刻而神秘的原因,在指定中斷的引腳號時,請始終使用特殊函數digitalPinToInterrupt()
。我把它當作一個練習,讓讀者發現我們為什麼需要這個函數。
什麼是下降訊號?這意味著電壓從高電平變為低電平的變化,由 Arduino 的數位引腳檢測到。電壓變化從何而來?它源自於 DS3231 模組的警報引腳。此引腳標記為 SQW,大部分時間它會發出高電壓,接近 VCC 電源電平(即 Uno 上的 5 伏特)。警報導致 DS3231 將 SQW 引腳上的電壓變更為低電平。 Arduino 感測來自 SQW 引腳的電壓並注意到變化。我們告訴 Arduino 注意“FALLING”,因為每個警報只發生一次該事件,而低電平可能會持續存在並使 Arduino 感到困惑,從而觸發許多中斷。
哪一個接腳可以感應到電壓的下降變化?對於Unos,您可以選擇引腳2 或3 之一。到中斷。上運行:
int dataPin = 3;
如果您想重複循環,特殊程式碼還可以設定新的鬧鐘時間。首先計算新的鬧鐘時間,如步驟 3 所述,然後按照步驟順序進行操作。
當範例草圖在正確連接到 DS3231 模組的 Arduino Uno 上運行時(按照我在本教程中描述的方式),我希望範例草圖能夠產生類似於下圖的輸出。如果顯示的時間不同,請不要感到驚訝。無論如何,你應該按照自己的時間工作。