タイマー割込みを使用したストップウオッチでタイマー割り込みの動作を紹介します。
正確な時間で処理を行うというのはひと手間必要で、このためにタイマー割込みを使用します。
非常に動作が複雑ですが一つづつ詳しく紹介していきたいと思います。
今回は「ATOM LITE」に「液晶表示器SH1107」を接続して数値の表示を行ないました。
「ATOM LITE」については以下のリンクで詳しく紹介しています。
OLED液晶「SH1107」の使い方については以下のリンクで詳しく紹介しています。
1.液晶表示器を使ってできること
2.「ストップウォッチ」の概略動作について
3.必要なもの(部品リスト)
4.サンプルプログラム(コピペ)
5.プログラムの詳細
・タイマー割込み
・液晶表示
6.まとめ
1.液晶表示器を使ってできること
外付けした液晶には文字や数字、線や円などの図形をプログラムで指定して表示させることができます。
液晶表示器のない「ATOM LITE」では、内部の変数の値を確認したい時は「シリアル出力」を使用してパソコンのモニタ上で確認していましたが、液晶表示器があればパソコンがなくても確認することができます。
文字や数字の表示だけでなく、取得した「アナログデータ」や「カウント数」をグラフにして表示することもできます。
WiFi接続をする時には、通信状態の確認や接続先の「SSID」「IPアドレス」を確認するのにも便利です。
2.「ストップウォッチ」の概略動作について
同じ処理を繰り返し実行するということはプログラムの得意とするところですが、メインのプログラムの処理は「if文」の条件や「for文」の繰り返し回数の違いで1回の処理時間が異なることがあります。
メインのプログラム内で単純に時間をカウントすると、処理時間の違いから安定した時間カウントを行うことができません。
このため正確に一定時間間隔で処理するための機能として「タイマー割込み」という機能があります。
今回はこの「タイマー割込み」機能を使用して、0.01秒単位のカウントを処理して、カウントした結果を液晶画面に表示するようにしています。
下画像が今回作ったストップウォッチです。
液晶表示器に付属の「Groveコネクタ」配線で直接「ATOM LITE」本体と接続できるので配線も楽です。
「ATOM LITE」に電源を接続すると液晶が表示され、本体LEDが白色に点灯します。
正面のボタンを押すと本体のLEDが青色に変わり、時間カウントが開始されます。
時間カウント中にもう一度正面のボタンを押すと時間カウントは停止し本体のLEDは白色に変わります。
本体横の小さいリセットボタンを押すと時間カウントはリセットされます。
時間カウントは「99’00″00」(99分)で自動で停止します。
再度0からカウントする時はリセットボタンを押してリセットしてください。
3.必要なもの(部品リスト)
今回使用した部品は以下になります。
4.プログラムの書込み(コピペ)
プログラムは以下に準備しましたので「コピペ」して書き込んでみましょう。
ライブラリの追加方法は以下のリンクで「FastLED」ライブラリの追加で紹介しています。
「M5GFX」も同様に追加しておきましょう。
下のコードを「コピペ」して書き込んでください。
※コピーは下コード(黒枠)内の右上角にある小さなアイコンのクリックでもできます。
#include <M5Atom.h>
#include <M5UnitOLED.h> //M5GFXライブラリ使用
// OLED設定(M5GFX)
M5UnitOLED display(26, 32, 400000); //任意の端子でI2Cを使用する場合(SDA, SCL, FREQ)
// FastLED(CRGB構造体)設定
CRGB dispColor(uint8_t r, uint8_t g, uint8_t b) {
return (CRGB)((r << 8) | (g << 16) | b);
}
// グローバル変数宣言
int start = 0; //カウント開始フラグ
int sec = 0; //秒カウント用
int minute = 0; //分カウント用
// 割込み内で使用する変数宣言(volatileで保持領域を固定して宣言)
volatile int count = 0; //10msタイマカウンタ用
volatile int sec_up = 0; //1秒経過フラグ用
// タイマ割込み設定
hw_timer_t * tim0 = NULL; //タイマー0の割り込みtim0で定義
volatile SemaphoreHandle_t timerSemaphore; //セマフォの宣言(割込み発生の確認用)
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; //排他制御の利用を宣言
// tim0のタイマー割り込みハンドラ。この処理はsetupより先に書く
// タイマ割込み発生時に実行される処理 (IRAM_ATTRで宣言してRAM上に配置)
void IRAM_ATTR onTimer() {
portENTER_CRITICAL_ISR(&timerMux); //排他制御で以下を実行(割込み禁止)
if(start == 1) { //カウント開始フラグが1なら
count++; //10msタイマカウンタ +1
if (count >= 100) { //100回=1秒経過していたら
sec_up = 1; //1秒経過フラグをON (メイン処理でリセットする)
count = 0; //10msタイマカウンタをリセット
}
}
portEXIT_CRITICAL_ISR(&timerMux); //排他制御終了(割り込み許可)
xSemaphoreGiveFromISR(timerSemaphore, NULL); //セマフォを開放
}
//-------------------------------------------------
// 初期設定
//-------------------------------------------------
void setup() {
M5.begin(false, false, true); //Serial,POWER,LED
// タイマ割込み設定
timerSemaphore = xSemaphoreCreateBinary(); //バイナリセマフォを作成(0か1のバイナリ)
tim0 = timerBegin(0, 80, true); //タイマー0を80MHz/80(1us)動作カウントアップでtim0に設定
timerAttachInterrupt(tim0, &onTimer, true); //tim0割込みが発生した時に実行する処理を指定「onTime」
timerAlarmWrite(tim0, 10000, true); //tim0割込み発生周期を10ms(1us × 10000)に設定
timerAlarmEnable(tim0); //タイマー0割込みを有効化
// OLED液晶表示設定(M5GFXライブラリ使用)
display.init(); //表示器の初期化
display.setRotation(1); //表示方向(コネクタ位置基準):0下、1右、2上、3左
display.setColorDepth(1); //モノクロ
display.setTextWrap(false); //自動改行をしない
// OLED液晶初期画面
display.clearDisplay(); //表示クリア
display.setFont(&fonts::lgfxJapanGothicP_16); //フォント設定(ゴシック体)
display.setTextSize(1); //文字サイズ)
display.setCursor(0, 0); //座標を指定(1行目)
display.printf("ストップウォッチ"); //タイトル
display.drawFastHLine(0, 20, 128, TFT_WHITE); //横線
display.fillTriangle(50, 24, 52, 24, 50, 28, TFT_WHITE); //三角
display.fillTriangle(103, 24, 105, 24, 103, 28, TFT_WHITE); //三角
display.fillTriangle(108, 24, 110, 24, 108, 28, TFT_WHITE); //三角
display.setFont(&fonts::Font7); //フォント設定(7セグ)
display.setTextSize(0.8); //文字サイズ
display.setCursor(0, 26); //座標を指定(2行目、時間)
display.printf("%02d", minute); //時間カウント値
display.setCursor(52, 26); //座標を指定(2行目、分)
display.printf("%02d", sec); //分カウント値
M5.dis.drawpix(0, dispColor(20, 20, 20)); //本体LED(白)
}
//-------------------------------------------------
// メイン
//-------------------------------------------------
void loop() {
M5.update(); //ボタン状態更新
// 本体スイッチ処理
if (M5.Btn.wasPressed()) { //本体ボタンが押されていれば
start = !start; //カウント開始フラグ反転
if(start == 1) { //カウント開始フラグが1なら
M5.dis.drawpix(0, dispColor(0, 0, 200)); //本体LED(青)
} else { //カウント開始フラグが0なら
M5.dis.drawpix(0, dispColor(20, 20, 20)); //本体LED(白)
}
}
// 分:秒カウント処理(割込み処理が完了したら実行)
if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE) { //セマフォが解放されたら
if(sec_up == 1) { //1秒カウントフラグが1なら
sec++; //秒カウント+1
portENTER_CRITICAL(&timerMux); //排他制御で以下を実行(割込み禁止)
sec_up = 0; //1秒カウントフラグクリア
portEXIT_CRITICAL(&timerMux); //排他制御終了(割り込み許可)
if(sec == 60) { //秒カウントが60なら
minute++; //分カウント+1
sec = 0; //秒カウントリセット
if(minute == 99) //分カウントが100なら
start = !start; //カウント終了
}
display.setTextSize(0.8); //文字サイズ
display.setCursor(0, 26); //座標を指定(2行目、時間)
display.printf("%02d", minute); //時間カウント値
display.setCursor(52, 26); //座標を指定(2行目、分)
display.printf("%02d", sec); //分カウント値
}
}
// OLED表示
display.setCursor(105, 45); //座標を指定(10ms表示位置)
display.setTextSize(0.4); //文字サイズ
display.printf("%02d", count); //10msカウント
//遅延時間(メイン処理時間が10msより少し早くなるように設定)
delay(7);
}
5.プログラムの詳細
サンプルプログラムの動作について「タイマー割り込み」と「液晶表示」でそれぞれ詳しく紹介します。
・タイマー割り込み
プログラム内で0.01秒のカウントを処理している「タイマー割込み」動作について紹介していきます。
17行目~39行目がタイマー割込みの設定です。
割込み処理内で書き換えられる変数は「volatile」を付けて宣言します。
// 割込み内で使用する変数宣言(volatileで保持領域を固定して宣言)
volatile int count = 0; //10msタイマカウンタ用
volatile int sec_up = 0; //1秒経過フラグ用
21行目~24行目の「タイマ割込み設定」は決まった書き方なので毎回このまま記入します。
(22行目の「tim0」は自由に決められます。変更する場合は以降全て変更必要)
// タイマ割込み設定
hw_timer_t * tim0 = NULL; //タイマー0の割り込みtim0で定義
volatile SemaphoreHandle_t timerSemaphore; //セマフォの宣言(割込み発生の確認用)
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; //排他制御の利用を宣言
26行目~39行目はタイマー割込みが発生する度に実行される処理です。
この処理は初期設定より先に書き関数「onTimer」も「IRAM_ATTR」を付けて宣言しRAM領域に配置します。
割込み処理の中でプログラムを実行する時は、他の割込みを受けつけないように割込み禁止(29行目)にしてから実行し、実行したら割込みを許可(37行目)します。
ここでは0.01秒のカウントを実行し、100回カウント(1秒)完了で「sec_up」フラグを1にして、これを確認することでメイン処理内で1秒カウントを+1させます。
// tim0のタイマー割り込みハンドラ。この処理はsetupより先に書く
// タイマ割込み発生時に実行される処理 (IRAM_ATTRで宣言してRAM上に配置)
void IRAM_ATTR onTimer() {
portENTER_CRITICAL_ISR(&timerMux); //排他制御で以下を実行(割込み禁止)
if(start == 1) { //カウント開始フラグが1なら
count++; //10msタイマカウンタ +1
if (count >= 100) { //100回=1秒経過していたら
sec_up = 1; //1秒経過フラグをON (メイン処理でリセットする)
count = 0; //10msタイマカウンタをリセット
}
}
portEXIT_CRITICAL_ISR(&timerMux); //排他制御終了(割り込み許可)
38行目は割込み処理が完了したことを知らせるための「セマフォ」と呼ばれるものです。
この「セマフォ」を確認することでメイン処理内で割込み完了後に実行させたい処理を実行します。
xSemaphoreGiveFromISR(timerSemaphore, NULL); //セマフォを開放
47行目~52行目はタイマー割込みの初期設定です。
まずは「セマフォ」の使用を設定しています。
次に「タイマー0」を「1μs」の「アップカウンタ」に設定し、「10000回カウント」で割り込みを発生させることで「10ms(0.01秒)」ごとに「onTimer」関数を実行するように設定しています。
// タイマ割込み設定
timerSemaphore = xSemaphoreCreateBinary(); //バイナリセマフォを作成(0か1のバイナリ)
tim0 = timerBegin(0, 80, true); //タイマー0を80MHz/80(1us)動作カウントアップでtim0に設定
timerAttachInterrupt(tim0, &onTimer, true); //tim0割込みが発生した時に実行する処理を指定「onTime」
timerAlarmWrite(tim0, 10000, true); //tim0割込み発生周期を10ms(1us × 10000)に設定
timerAlarmEnable(tim0); //タイマー0割込みを有効化
96行目~108行目は割込み完了後にメイン処理内で実行する関数です。
97行目で割込み処理が終了(セマフォ解放)したかを確認して以下を実行します。
割込み処理内で変更される変数(ここでは「sec_up」)をメイン内で処理する時は、処理途中で割込みが発生しないように割り込みを禁止(100行目)して、処理が終わったら割込みを許可(102行目)します。
割込み処理内で0.01秒カウントが100回完了(sec_up = 1)していたら1秒カウント「sec」を+1し「sec_up」を0リセットします。
1秒カウント「sec」が60回になったら1分カウント「minute」を+1します。
1分カウント「minute」が99でカウント終了「start」反転(1から0へ)してカウント停止します。
// 分:秒カウント処理(割込み処理が完了したら実行)
if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE) { //セマフォが解放されたら
if(sec_up == 1) { //1秒カウントフラグが1なら
sec++; //秒カウント+1
portENTER_CRITICAL(&timerMux); //排他制御で以下を実行(割込み禁止)
sec_up = 0; //1秒カウントフラグクリア
portEXIT_CRITICAL(&timerMux); //排他制御終了(割り込み許可)
if(sec == 60) { //秒カウントが60なら
minute++; //分カウント+1
sec = 0; //秒カウントリセット
if(minute == 99) //分カウントが100なら
start = !start; //カウント終了
}
・液晶表示
54行目~78行目は「液晶表示器」の初期設定と「初期画面」の表示です。
54行目~58行目が「液晶表示器」の初期設定ですが、今回使用した液晶では「画面の表示向き」以外は毎回同じ内容になります。
// OLED液晶表示設定(M5GFXライブラリ使用)
display.init(); //表示器の初期化
display.setRotation(1); //表示方向(コネクタ位置基準):0下、1右、2上、3左
display.setColorDepth(1); //モノクロ
display.setTextWrap(false); //自動改行をしない
60行目から文字や数字、線などを座標を指定して表示させています。
液晶表示を指定するコマンドはたくさんありますが、今回使用したものを抜粋して紹介します。
63行目~66行目は文字や数値を表示させるコマンドです。
文字や数値を表示させる時は以下のように「フォント」「文字サイズ」「座標」「内容」を以下のように設定します。
display.setTextSize( 1) ; //文字サイズ(倍率)
display.setCursor( x, y) ; //x,y座標(ピクセル)を指定
display.printf( ” 内容 ”) ; //表示内容指定
68行目は線を描くコマンドです。
座標を指定して線を描きますが、線の指定方法には以下のように幾つかあります。
display.drawFastVLine(x, y, 長さ, TFT_色); //x,y座標から縦線
display.drawLine(x1, y1, x2, y2, TFT_色); //x1,y1座標からx2,y2座標までの直線
69行目~71行目は三角を描くコマンドで、時間を区切る記号「’」「”」を三角で表現しています。
三角も座標を指定して下のように指定します。
//x1,y1座標、x2,y2座標、x3,y3座標を結ぶ三角
※塗り潰し無しはdisplay.fillTriangleで指定
6.まとめ
タイマー割り込みの動作について紹介しました。
割り込みを使用しなくても時間をカウントするプログラムは、メイン処理の繰り返し時間を利用することでもできますが、正確な時間で処理し続けることは困難です。
また、メイン処理の中にループを作って時間カウントすることもできますが、この場合はこのループから抜けるまで他の処理を実行できません。この場合は他の処理を割込みで処理する必要があります。
メインプログラム内でいろいろな処理を実行しながら正確な時間で処理を行いたい場合は「タイマー割込み」を使っていきましょう。
コメント