タイマー割込みをストプウォッチの動作で紹介「ATOM LITE + OLED液晶」

「ATOM LITE」に液晶表示を接続したストップウォッチの製作

タイマー割込みを使用したストップウオッチでタイマー割り込みの動作を紹介します。

正確な時間で処理を行うというのはひと手間必要で、このためにタイマー割込みを使用します。
非常に動作が複雑ですが一つづつ詳しく紹介していきたいと思います。

今回は「ATOM LITE」に「液晶表示器SH1107」を接続して数値の表示を行ないました。

「ATOM LITE」は超小型(25×25×10mm)で多機能!はんだ付けも不要で基板剥き出しではなくコンパクトなケースに入っているので初心者にも扱いやすいマイコンボードです。

「ATOM LITE」については以下のリンクで詳しく紹介しています。

ATOM LITE プログラミング初心者におすすめ超小型で高機能!
マイコンボードはRaspberry Pi、Arduino、M5Stack等がありますが、一通りやってみてそれぞれの良さはあるものの「最初に何を?」と聞かれたらATOM LITEが一番お手軽♪プログラミング初心者におすすめ

OLED液晶「SH1107」の使い方については以下のリンクで詳しく紹介しています。

有機EL表示器OLED(SH1107)の使い方。M5GFXライブラリ使用。
OLED SH1107の使い方紹介。1.3インチ 128 x 64のディスプレイユニットで、I2C通信でM5GFX等のライブラリを使うことで簡単に表示できます。
「開発環境の準備」がまだの方は → こちら

スポンサーリンク

1.液晶表示器を使ってできること

外付けした液晶には文字や数字、線や円などの図形をプログラムで指定して表示させることができます。

液晶表示器のない「ATOM LITE」では、内部の変数の値を確認したい時は「シリアル出力」を使用してパソコンのモニタ上で確認していましたが、液晶表示器があればパソコンがなくても確認することができます。

文字や数字の表示だけでなく、取得した「アナログデータ」や「カウント数」をグラフにして表示することもできます。

WiFi接続をする時には、通信状態の確認や接続先の「SSID」「IPアドレス」を確認するのにも便利です。


スポンサーリンク

2.「ストップウォッチ」の概略動作について

同じ処理を繰り返し実行するということはプログラムの得意とするところですが、メインのプログラムの処理は「if文」の条件や「for文」の繰り返し回数の違いで1回の処理時間が異なることがあります。

メインのプログラム内で単純に時間をカウントすると、処理時間の違いから安定した時間カウントを行うことができません。
このため正確に一定時間間隔で処理するための機能として「タイマー割込み」という機能があります。

「タイマー割込み」とはマイコンの動作クロック(処理速度を決めている周波数)を利用して、メインの処理とは別に常に時間をカウントさせることで、設定した時間ごとに設定したプログラムを実行させることができるものです。

今回はこの「タイマー割込み」機能を使用して、0.01秒単位のカウントを処理して、カウントした結果を液晶画面に表示するようにしています。


下画像が今回作ったストップウォッチです。
液晶表示器に付属の「Groveコネクタ」配線で直接「ATOM LITE」本体と接続できるので配線も楽です。

「ATOM LITE」に液晶表示を接続したストップウォッチの製作

「ATOM LITE」に電源を接続すると液晶が表示され、本体LEDが白色に点灯します。

正面のボタンを押すと本体のLEDが青色に変わり、時間カウントが開始されます。
時間カウント中にもう一度正面のボタンを押すと時間カウントは停止し本体のLEDは白色に変わります。

本体横の小さいリセットボタンを押すと時間カウントはリセットされます。
時間カウントは「99’00″00」(99分)で自動で停止します。

再度0からカウントする時はリセットボタンを押してリセットしてください。

スポンサーリンク

3.必要なもの(部品リスト)

今回使用した部品は以下になります。


4.プログラムの書込み(コピペ)

プログラムは以下に準備しましたので「コピペ」して書き込んでみましょう。

今回は液晶表示を制御するために「M5GFX」ライブラリを使用しています。
このライブラリを使用することで簡単に液晶を制御できます。作っていただいた方に感謝しつつ使わせていただきましょう。

ライブラリの追加方法は以下のリンクで「FastLED」ライブラリの追加で紹介しています。
「M5GFX」も同様に追加しておきましょう。

ATOM LITE の初期設定。プログラミング初心者におすすめ
Platform IOを使用した ATOM LITE の初期設定です。ファイルを作成、必要なライブラリの準備、初期設定方法を紹介。実際に「Lチカ」プログラムを「コピペ」で書き込み動作確認まで行います。

下のコードを「コピペ」して書き込んでください。
※コピーは下コード(黒枠)内の右上角にある小さなアイコンのクリックでもできます。

#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行目から文字や数字、線などを座標を指定して表示させています。

液晶表示を指定するコマンドはたくさんありますが、今回使用したものを抜粋して紹介します。

初期設定終了後や画面の表示内容をすべて消去したい時は以下を実行します。
display.clearDisplay() ;             //表示クリア

63行目~66行目は文字や数値を表示させるコマンドです。

文字や数値を表示させる時は以下のように「フォント」「文字サイズ」「座標」「内容」を以下のように設定します。

この液晶画面の座標の最大値は (x, y) = (128, 64) です。
文字の座標の基準位置は特に指定しない限り左上です。
display.setFont ( &fonts:: フォント名 ) ; //フォント設定
display.setTextSize( 1) ;                     //文字サイズ(倍率)
display.setCursor( x,  y) ;                   //x,y座標(ピクセル)を指定
display.printf( ” 内容 ”) ;                     //表示内容指定

68行目は線を描くコマンドです。
座標を指定して線を描きますが、線の指定方法には以下のように幾つかあります。

display.drawFastHLine(x, y, 長さ, TFT_); //x,y座標から横線
display.drawFastVLine(x, y, 長さ, TFT_); //x,y座標から縦線
display.drawLine(x1, y1, x2, y2, TFT_); //x1,y1座標からx2,y2座標までの直線

69行目~71行目は三角を描くコマンドで、時間を区切る記号「’」「”」を三角で表現しています。
三角も座標を指定して下のように指定します。

display.drawTriangle(x1, y1, x2, y2, x3, y3, TFT_);
 //x1,y1座標、x2,y2座標、x3,y3座標を結ぶ三角
※塗り潰し無しはdisplay.fillTriangleで指定
液晶表示のコマンドは他にもたくさんあります。
ここでは紹介しきれないのでまた別記事で紹介したいと思います。

6.まとめ

タイマー割り込みの動作について紹介しました。

割り込みを使用しなくても時間をカウントするプログラムは、メイン処理の繰り返し時間を利用することでもできますが、正確な時間で処理し続けることは困難です。

また、メイン処理の中にループを作って時間カウントすることもできますが、この場合はこのループから抜けるまで他の処理を実行できません。この場合は他の処理を割込みで処理する必要があります。

メインプログラム内でいろいろな処理を実行しながら正確な時間で処理を行いたい場合は「タイマー割込み」を使っていきましょう。

AtomS3の便利な使い方、日本語表示、並列処理、PWM、カウントダウンタイマ
M5GFXライブラリを使用した日本語表示やスプライト表示、マルチタスク(並列処理)による時間カウントやPWM制御での外付けブザーの使い方等について詳しく紹介します

コメント

タイトルとURLをコピーしました