DS3231 リアルタイム クロックを使用して Arduino に割り込むことについて学んだコツ
Arduino ハードウェアのタイマーやカウンターを使用せずに、Arduino がスケッチ内の特別なコード セグメントを実行する正確な時間を決定したいと考えています。 Arduino IDE を使用してコードを作成するスキルと経験があり、 delay()
などのコード ステートメントの使用を避けたいと考えています。
代わりに、非常に高精度の DS3231 リアルタイム クロック モジュールを外部割り込みのソースとして接続するとします。 Arduino の電源が一時的に失われた場合でも、DS3231 がバッテリーを使用して正確な時間を維持できることが重要になる場合があります。
最後に、Arduino オンライン リファレンス: https://www.arduino.cc/reference/en/libraries/ds3231/ で参照されている Andrew Wickert による DS3231.h ライブラリの使用方法を学習します。これには、努力して習得する価値のあるいくつかのトリックが含まれています。このライブラリは、ライブラリ マネージャー ([ツール] > [ライブラリの管理...]) を使用して Arduino IDE にインポートできます。
一歩ずつ進んでください。このチュートリアルでは、次の手順を説明します。
特別なコードを指定した間隔で複数回実行したい場合は、コードでステップ 3 を再度実行し、新しいアラームを設定できます。このチュートリアルに付属するサンプル スケッチは、Arduino を 10 秒間隔で繰り返し中断します。
このチュートリアルは、次のような「公式」リファレンスから引用しています。
attachInterrupt()
関数の Arduino リファレンス: https://www.arduino.cc/reference/en/ language/functions/external-interrupts/attachinterrupt/。 この例の特別なコードは簡単です。DS3231 から現在時刻を出力するだけです。実際の例では、植物に水を与えたり、温度測定を記録したりするなど、何か役立つことを行う可能性があります。タスクが何であれ、コードは独自の特別なブロックに入り、DS3231 アラームが Arduino に割り込んだ場合にのみ実行されるようにする必要があります。
コードを短い関数に分割し、各関数が 1 つのタスクまたは 1 つの関連タスク セットのみを処理することが良い方法だと私は考えています。関数の名前は何でも構いません。なぜ関数が何をするのかを説明させないのでしょうか?この例の特別なコードを実行する関数の一部を次に示します。
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
}
5 対のピンの間にワイヤを配線します。各ペアは 1 つの電気的目的を実行し、Arduino のピンを DS3231 の対応するピンと一致させます。ゆっくりと各ペアを接続し、両端をチェックして各ワイヤが適切な場所に接続されていることを確認します。この表には、Arduino Uno から DS3231 に接続されるペアが左から右に順番にリストされています。
目的 | DS3231 ピン | Arduino ピン |
---|---|---|
アラーム | SQW | 3* |
SCL | SCL | SCL** |
SDA | SDA | SDA** |
5ボルトの電源 | VCC | 5V |
地面 | GND | GND |
先ほども言いましたが、これらのつながりを作るには時間をかけてください。多くの場合、ゆっくりと確実に何かを正しく完了するのが最も早い方法です。
DS3231「オブジェクト」(名前が付いたソフトウェア ツールボックスのようなもの)を使用して DS3231 モジュールと通信します。 DS3231 ライブラリは、ボックス内で多くの関数を定義しています (ツールと考えてください)。関数を使用する場合は、ツールボックスの名前、ドット、ツールの名前の順に書きます。サンプル スケッチでは、この目的のために「クロック」変数を作成します。これで、スケッチは「時計」ボックス内のツールにアクセスできるようになります。すべてのツールは、前述した DS3231.h ファイルで宣言されています。
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
「時計」オブジェクトのツールを使用してアラームを設定します。ただし、最初にアラーム時間を計算する必要があります。
この手順では、事前に DS3231 に実際の時間が設定されていることを前提としています。サンプル スケッチには、必要に応じて時計の時刻を設定するために使用できるコードが含まれています。コメントを囲むコメント区切り文字 /* と */ を削除するだけです。
このチュートリアルのサンプル スケッチでは、現在の時刻に間隔 (秒数) を追加することで、将来のアラーム時刻を計算します。この例では 10 秒追加します。 1 分だと 60 秒追加されます。 1 時間だと 3,600 秒追加されます。 1 日は 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 オブジェクトは秒数に基づいて作成されますが、年、月、日、時、分、秒を表すツールがツールボックスに用意されています。次に説明するように、必要な値のソースとしてalarmTime オブジェクトを使用して、DS3231 にアラーム時間を設定します。
たとえば、DS3231 モジュールによって報告された currentTime が、2021 年 10 月 27 日水曜日の午前 10 時 42 分を 7 秒過ぎたとします。上で計算された AlarmTime は、同日の 10 秒後の 10:42:17 になります。
DS3231 では、アラーム #1 (A1) とアラーム #2 (A2) の 2 つの異なるアラームを使用できます。どちらのアラームも日付と時刻を 1 分単位で指定できます。違いは、A1 はさらに秒単位で指定できることです。 DS3231 ライブラリには、アラームの時刻を設定し、その時刻を読み取るための独自の関数のペアが各アラームにあります。これらの関数はすべて、DS3231 オブジェクト (たとえば、「クロック」という名前を付けたオブジェクト) を通じてアクセスされます。
クロック.setA1Time()、クロック.getA1Time()、クロック.setA2Time()、およびクロック.getA2Time()
DS3231.h ファイルからの引用にリストされているように、setA1Time() 関数は 8 つのパラメーターを取ります。
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
DS3231.h ヘッダー ファイルをよく読むと、パラメーターについて説明できます。ここに続くのは、それらを私自身の言葉で自分自身に再説明する試みです。読者が私のバージョンとヘッダー ファイルの間に矛盾を見つけた場合は、ヘッダーが正しいものとみなしてください。
最初の 5 つのパラメータのタイプは「バイト」です。 cppreference Web サイトでは、次のようにバイト タイプを定義しています https://en.cppreference.com/w/cpp/types/byte:
std::byte は、C++ 言語定義で指定されているバイトの概念を実装する個別型です。
char や unsigned char と同様に、他のオブジェクト (オブジェクト表現) が占有する生のメモリにアクセスするために使用できますが、これらの型とは異なり、文字型でも算術型でもありません。バイトは単なるビットの集合であり、バイトに対して定義されている唯一の演算子はビット単位の演算子です。
この特定の状況では、日時のバイト型変数を符号なし整数であるかのように考えることができます。 0 ~ 255 の整数値を保持できます。 注意: コード作成者は、意味のない値を避ける必要があります。たとえば、値 102 は、これらのパラメータのいずれにとっても意味がありません。適切な値を提供するのは、コード作成者の仕事です。
前のステップで作成したalarmTime、つまり月の27日、午前10時42分過ぎ17秒で続けてみましょう。以下のリストは、これらの値を関数に指定する方法を示しています。人間が読みやすくし、コメントのためのスペースを確保するために、各パラメーターを個別の行にリストします。ここの例は不完全です。日付と時刻のバイト型の値のみを示しています。この関数には、以下で説明するようにさらに多くのパラメータが必要であり、ここに示されている形式では実行されません。
ところで、「クロック」変数と「アラームタイム」変数はオブジェクト、つまりソフトウェア ツールボックスであることに注意してください。ご覧のとおり、それぞれのツールボックス内のツールを使用して、オブジェクトに含まれる情報にアクセスします。
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()
関数の最後の 3 つのパラメータはブール値、つまり true/false 値です。これらは、アラーム設定を評価する方法に関する詳細情報を 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 のアラームを有効にするために追加のアクションを実行する必要があります。読者には、たとえ何らかの理由で必要性が低いと思われるステップがあったとしても、一貫して 3 つのステップの順序に従うことをお勧めします。コードにエラーが発生する必要がある場合は、確実性を考慮してエラーを発生させてください。
アラーム 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 の場合は、パラメータを 2 に変更するだけです。例: checkIfAlarm(2); // clear A2 flag bit in register 0Fh
。
@flowmeter によってこのリポジトリで説明されている問題では、DS3231 がアラームを送信する前に両方のアラーム フラグをクリアする必要があることが強調されています。確実にするには、アラームを 1 つだけ使用している場合でも、checkIfAlarm() を 2 回 (アラームごとに 1 回) 呼び出すことを検討してください。
checkIfAlarm(1);
checkIfAlarm(2);
なぜコード作成者は、現在信号を送信していないと思われるアラームを「チェック」することを選択するのでしょうか?その理由は、 checkIfAlarm()
関数には明らかではない副作用があるためです。アラームフラグビットをクリアします。 checkIfAlarm()
関数を使用します。これは、DS3231 ライブラリ内で必要な操作を実行する唯一の関数だからです。
考えてみてください。以下で説明する理由により、Arduino 割り込み検出ハードウェアでは、アラームが発生する前に DS3231 の SQW ピンの電圧が HIGH である必要があります。アラーム イベントにより、DS3231 内部の 2 つのことが変わります。
これらのアラーム フラグ ビットのいずれか 1 つがセットされている限り、SQW ピンは LOW のままになります。 DS3231 内のアラーム フラグ ビットが SQW ピンを LOW に保持している限り、Arduino はそれ以上アラームを感知できません。 DS3231 の SQW アラーム ピンの電圧を HIGH に戻すには、アラーム フラグ ビットを両方ともクリアする必要があります。 DS3231 データシートの 14 ページの「ステータス レジスタ (0Fh)」のビット 1 および 0 の説明を参照してください。
各アラームは DS3231 内に独自のアラーム フラグ ビットを持っています。アラーム フラグ ビットのいずれか 1 つで SQW ピンを LOW に保持できます。 DS3231 は、自らの意思でアラーム フラグ ビットをクリアしません。アラーム発生後にアラーム フラグ ビットをクリアするのはコード作成者の仕事です。
メインループでは時間を測定する必要はありません。アラームが発生したかどうかを確認するには、フラグをチェックするだけで済みます。スケッチ例では、このフラグは「alarmEventFlag」という名前のブール変数です。
if (alarmEventFlag == true) {
// run the special code
}
ほとんどの場合、フラグはfalseになり、ループは特別なコードをスキップします。スケッチではどのようにフラグが設定されますか? 3 つのステップ:
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
関数がすべてをまとめます。次の例では、指定されたデジタル ピンで "FALLING" 信号を検出するとすぐにrtcISR()
関数を実行するように Arduino ハードウェアに指示します。attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
深い奥深い理由から、割り込みのピン番号を指定するときは、常に特別な関数、 digitalPinToInterrupt()
を使用してください。なぜその関数が必要なのかを読者が理解するための演習として残しておきます。
FALLING信号とは何ですか?これは、Arduino のデジタル ピンによって検出される、HIGH から LOW への電圧の変化を意味します。電圧変化はどこから来るのでしょうか?これは、DS3231 モジュールのアラーム ピンから発生します。このピンには SQW というラベルが付けられており、ほとんどの場合、VCC 電源レベル (つまり、Uno では 5 ボルト) に近い HIGH 電圧が放出されます。アラームが発生すると、DS3231 は SQW ピンの電圧を LOW に変更します。 Arduino は SQW ピンからの電圧を感知し、その変化を認識します。 Arduino に FALLING を認識するように指示します。これは、そのイベントがアラームごとに 1 回だけ発生するのに対して、LOW レベルが持続し、Arduino が混乱して多くの割り込みをトリガーする可能性があるためです。
どのピンが電圧の FALLING 変化を感知できますか? Unos の場合は、ピン 2 または 3 のいずれかを選択できます。Leonardo の場合は、ピン 0、1、または 7 のいずれかを選択できます。(はい、知っています。Leonardo はピン 2 と 3 の割り込みも感知します。ただし、これらはLeonardo の I2C ピン。つまり、DS3231 モジュールがそれらを使用することになります。Leonardo の割り込みにはピン 7 から始めます。) サンプル スケッチでは dataPin を定義しています。変数を作成し、次のように Uno で実行するためにその値を 3 に初期化します。
int dataPin = 3;
サイクルを繰り返したい場合は、特別なコードで新しいアラーム時刻を設定することもできます。ステップ 3 の説明に従って、新しいアラーム時刻を計算することから始め、そこから一連のステップに従います。
このチュートリアルで説明した方法で、DS3231 モジュールに正しく接続された Arduino Uno 上でサンプル スケッチを実行すると、サンプル スケッチが以下の図のような出力を生成すると予想されます。表示される時間が異なっていても驚かないでください。とにかく、自分の時間を使って仕事をする必要があります。