第42回 列挙型

今回もさらに関数を変更します。

時間表示関数の引数について

現在のスケッチでは、時間表示用LEDの制御用に以下の関数を作成しています。

void controlTimeLed(uint8_t nokori_jikan) {
  
  // 残り時間に応じた処理
  switch(nokori_jikan) {

    // 残り時間十分
    case 3:
      digitalWrite(LED_MIDORI, HIGH);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    LOW);
      break;
         
    // 残り時間半分以下
    case 2:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
      digitalWrite(LED_AKA,    LOW);
      break;

    // もうすぐ設定時刻
    case 1:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    HIGH);
      break;
  }

}

この関数はこれで問題なさそうですが、よくよく考えるとちょっとおかしなところがあるんです。あまり意識していなかったと思いますが、そのおかしなところとは、引数の数字です。

この関数ではswitch文を使用して、引数の残り時間に応じて場合分けをして処理をしています。その場合分けは以下のようにしています。

残り時間 caseの値
十分 3
半分以下 2
もうすぐ 1

おかしなところ、というのはcaseで判断するときの数字です。残り時間が十分なときは「3」、半分以下は「2」、もうすぐは「1」としていますが、そもそもこの3、2、1というのは意味がある数字なのでしょうか。

例えば、残り時間が十分な場合は「2」、半分以下は「1」、もうすぐは「0」としてもいいのではないでしょうか。さらには、「3、2、1」や「2、1、0」ではなく「11、12、33」という3つの数字で判断することもできるので、特に「3、2、1」でなければならない、ということはありません。

例えば、caseで判断する値を適当な3つの数字にして、以下のようにした場合、

void controlTimeLed(uint8_t nokori_jikan) {
  
  // 残り時間に応じた処理
  switch(nokori_jikan) {

    // 残り時間十分
    case 78:
      digitalWrite(LED_MIDORI, HIGH);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    LOW);
      break;
         
    // 残り時間半分以下
    case 17:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
      digitalWrite(LED_AKA,    LOW);
      break;

    // もうすぐ設定時刻
    case 199:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    HIGH);
      break;
  }

}

関数を呼ぶときに、「残り時間十分」のときは

controlTimeLed(78);

と書けば問題なく動作します。

このように考えると、最初のスケッチで書いたswitch文のcaseの値の「3、2、1」という数字は、それ自体には特に意味がない、ということになります。でも数字で書いてしまうと「何かこの数字には意味があるのかな」と思ってしまい、スケッチが読みづらくなってしまいます。

例えば初めて見たスケッチに、先ほどのようなswitch文にcaseの値が「78」「17」「199」と書いてあったら、なぜこの数字なんだろう、と考え込んでしまうのではないでしょうか。

このように考えると、残り時間表示LED制御用の関数のcaseの値は、単に残り時間を区別するための記号のようなものだった、ということになります。

そこで、もう少し見やすいスケッチを検討してみましょう。

 

#defineを使ってみる

switch文に「case 3:」などと書いてあるとわかりづらいわけですから、この数字の部分を#defineで定義するのはどうでしょうか。具体的には、定数を以下のように#define定義します。

#define JYUUBUN 3  // 残り時間十分
#define HANBUN  2  // 残り時間半分以下
#define MOUSUGU 1  // もうすぐ時間

関数はこの定義を使用して以下のように書きます。

void controlTimeLed(uint8_t nokori_jikan) {
  
  // 残り時間に応じた処理
  switch(nokori_jikan) {

    // 残り時間十分
    case JYUUBUN:
      digitalWrite(LED_MIDORI, HIGH);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    LOW);
      break;
         
    // 残り時間半分以下
    case HANBUN:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
      digitalWrite(LED_AKA,    LOW);
      break;

    // もうすぐ設定時刻
    case MOUSUGU:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    HIGH);
      break;
  }

}

この関数を呼び出す場合も以下のように書けば数字は出てきません。

controlTimeLed(JYUUBUN);

このように書くとスケッチ本体では謎の数字がない状態になります。

とは言っても、スケッチのヘッダ部分では#define定義に相変わらず数字があります。この数字は「3、2、1」を使用していますが、最初の説明のように以下のように定義してもスケッチは全く問題なく動作しますよね。

#define JYUUBUN 12  // 残り時間十分
#define HANBUN  34  // 残り時間半分以下
#define MOUSUGU 56  // もうすぐ時間

ということで、#defineを使用するとスケッチは読みやすくなるものの、まだ数字を使用していますので、初めてスケッチを見た人は「この数字に何か意味があるのかな」と思ってしまう可能性があります。

 

列挙型

プログラミングでは、このように「単に何かを区別するための記号」を使うケースがよくあります。この「区別するための記号」のことを「識別子」と呼んでいます。「識別」は区別すること、「子」は記号の意味です。(以前「演算子」とかで「子」は出てきましたよね)

C/C++言語では、このように識別子を使いたい場合に便利な仕組みが用意されています。「列挙型」という仕組みです。

enum宣言

「enum」は「enumeration」の略で、日本語では「列挙」という意味です。「列挙」とは平たくいうと「並べる」というような意味でしょうか。

さて、、、関数の時もそうでしたがこの説明だけではさっぱりわかりませんよね。ということで、具体的な例から理解してみることにします。

キッチンタイマーの関数では、switch文で3種類のケースを場合分けして処理をしています。残り時間が「十分」「半分以下」「もうすぐ」の3種類です。つまり、識別子が3種類欲しい、という状況です。

このような場合、スケッチには以下のように書きます。

enum nokori {jyuubun, hanbun, mousugu};

このように書くと、Arduino IDEは以下のように理解してくれます。

enum宣言の例

列挙型の名前(タグ名)と要素名は自分で好きな文字列にすることができます。使える文字は英数字と一部の記号です。

このように列挙型を宣言すると、スケッチをArduinoボードに送るときに、Arduino IDEは裏で、列挙型の要素「jyuubun」「hanbun」「mousugu」を、それぞれ以下の数字に置き換えてくれます。

enum宣言の数字割り当て

#define定義と似てますよね。#defineと違うところは、スケッチ上で数字が見えない点です。このenum宣言は、以下の#define定義と同じようなもの、という理解で問題ありません。

#define jyuubun 0
#define hanbun  1
#define mousugu 2

次に使い方ですが、#defineで定義した場合と同じです。

enum nokori {jyuubun, hanbun, mousugu};

と書いた場合、スケッチの中で「jyuubun」「hanbun」「mousugu」を使用することができます。

enumは、結局は#defineと一緒です。決定的な違いは、#defineの場合はスケッチを書く人が数字か何かで定義する必要がありますが、列挙型の場合はArduino IDEが裏で勝手に数字を定義してくれる、という点です。

先ほどの例では要素数は3つでしたが、必要な分、要素を書いていけば、Arduino IDEは最初の要素を0として順番に番号を割り当ててくれます。

列挙型についてもう少し知識をつけておきましょう。

 

列挙型の変数

列挙型は#defineと同じ働きをしてくれますが、#defineとは異なり変数を宣言することもできます。

変数を宣言するときは「型」が必要でしたよね。

uint16_t hensuu;

このように「hensuu」という変数を定義した場合、hensuuには0〜65535の数字を代入することができます。

変数と同じような感じで列挙型の変数を宣言することができます。

列挙型変数宣言

さて、、、この説明もこれだけではさっぱりわかりませんので、以下に例をみてみましょう。

enum nokori {jyuubun, hanbun, mousugu};

このようにenumで列挙型を宣言して、

enum nokori nokori_jikan;

このように「enum タグ名 変数名;」と宣言すると列挙型の変数を宣言することができます。このように宣言した変数には、列挙型の要素を代入することができます。

nokori_jikan = jyuubun;

この代入式では変数「nokori_jikan」に「jyuubun」が代入されます。実際には裏では「jyuubun」は0で定義されますので、nokori_jikanには0が代入されます。とは言っても列挙型は基本的には識別子ですので、スケッチ上では「nokori_jikan」に「jyuubun」が代入された、と理解すれば問題ありません。

 

列挙型による関数の変更

それでは、現在の関数の引数を列挙型に変更します。

まずスケッチのヘッダ部分に、enumを使って列挙型を以下のように宣言します。

enum nokori {jyuubun, hanbun, mousugu};

次に、関数をこの宣言した列挙型に変更します。注意点としては引数の変数は列挙型にします。

void controlTimeLed(enum nokori nokori_jikan) {
  
  // 残り時間に応じた処理
  switch(nokori_jikan) {

    // 残り時間十分
    case jyuubun:
      digitalWrite(LED_MIDORI, HIGH);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    LOW);
      break;
         
    // 残り時間半分以下
    case hanbun:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
      digitalWrite(LED_AKA,    LOW);
      break;

    // もうすぐ設定時刻
    case mousugu:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    HIGH);
      break;
  }

}

関数の引数について補足します。enumは裏で#defineしていることと同じ、という説明をしました。ということは、単に数字ですので、関数の引数を以下のように「uint8_t」としても問題ないのではないでしょうか。

void controlTimeLed(uint8_t nokori_jikan) {

このように書いても問題なく動作します。ただスケッチでは引数を列挙型にしているため、列挙型の変数として宣言する方が望ましいです。

また、この関数を呼び出す場合は、以下のようにenumで宣言した要素を使用します。

controlTimeLed(jyuubun);

 

スケッチ

それでは最後にスケッチとしてまとめます。先ほど説明した変更部分を反映しています。

/*
 * キッチンタイマー
 * 
 * 内容: スイッチ、LED、スピーカーを使ったキッチンタイマー
 * 変更履歴:
 *   2019. 8.11: 新規作成
 *   2019. 8.15: スタートスイッチ処理を追加
 *   2019. 8.17: スイッチ関連の#define追加
 *   2019. 9. 8: 点滅回数カウント追加
 *               最終的に繰り返し処理をfor文で作成
 *   2019. 9.16: スケッチ動作開始時にLEDを点滅
 *   2019.10. 5: アラーム音追加
 *               動作開始時とタイマー時間の時に青色LEDを点灯するように変更
 *   2019.10.13: 残り時間LED制御追加
 *   2019.10.14: 残り時間が5秒以下になったらスピーカーを鳴らす動作を追加
 *   2019.11. 4: アラーム音をメロディーに変更
 *               音階定義をファイル化
 *   2019.11.11: メロディーを配列化
 *   2019.11.23: 残り時間表示LEDの関数化
 *   2019.11.24: 残り時間表示LED関数をswitch文に変更
 *   2019.12. 1: 残り時間を列挙型で宣言
 */

// 音階定義ファイルのインクルード
#include "onkai.h"

// 秒を表現するLED関連(青色LED)
#define BYOU_LED 12 // 秒を表現する青色LEDの端子番号
#define BYOU_ON  50 // 秒を表現するLEDをつけている時間 (単位:ミリ秒)
#define BYOU_OFF 1000 - BYOU_ON // 秒を表現するLEDを消している時間 (単位:ミリ秒)

// 残り時間を表現するLED関連(緑色、黄色、赤色LED)
#define LED_MIDORI 8 // 緑色LEDの端子番号
#define LED_KIIRO  6 // 黄色LEDの端子番号
#define LED_AKA    4 // 赤色LEDの端子番号

// スタートスイッチ関連
#define SWITCH 23    // スイッチを接続している端子番号
#define SWITCH_OFF 1 // スイッチOFFの時のdigitalReadの値
#define SWITCH_ON  0 // スイッチONの時のdigitalReadの値

// タイマー時間設定(単位:秒)
#define TIMER_JIKAN 10

// 残り時間を表現するLEDの制御時間
#define KIIRO_JIKAN TIMER_JIKAN / 3
#define AKA_JIKAN   TIMER_JIKAN / 3 * 2

// アラーム音関連
#define SPEAKER 18  // スピーカーの端子番号
#define ALARM   880 // 通常のアラーム音周波数

// アラームメロディー関連
#define ALARM_ONKAISUU 13 // アラームメロディーの音の数
uint16_t alarm_melody[] = {DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, MI_4, DO_4, MI_4, SO_4, SI_4, DO_5};  // アラームメロディー

// 残り時間のenum型宣言
//            十分    半分以下  もうすぐ
enum nokori {jyuubun, hanbun, mousugu};


// 残り時間表示用LEDの制御関数
//   パラメータ: 残り時間目安
//       jyuubun: 残り時間十分
//       hanbun:  残り時間少ない
//       mousugu: もうすぐ設定時間
//   返り値:
//       なし
//
void controlTimeLed(enum nokori nokori_jikan) {
  
  // 残り時間に応じた処理
  switch(nokori_jikan) {

    // 残り時間十分
    case jyuubun:
      digitalWrite(LED_MIDORI, HIGH);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    LOW);
      break;
         
    // 残り時間半分以下
    case hanbun:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
      digitalWrite(LED_AKA,    LOW);
      break;

    // もうすぐ設定時刻
    case mousugu:
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  LOW);
      digitalWrite(LED_AKA,    HIGH);
      break;
  }

}


void setup() {
  // 端子の設定
  pinMode(BYOU_LED,   OUTPUT);   // 青色LED接続端子設定
  pinMode(LED_MIDORI, OUTPUT);   // 緑色LED接続端子設定
  pinMode(LED_KIIRO,  OUTPUT);   // 緑色LED接続端子設定
  pinMode(LED_AKA,    OUTPUT);   // 緑色LED接続端子設定
  pinMode(SWITCH, INPUT_PULLUP); // スイッチ接続端子の設定

  // 秒のLED(青色LED)を点灯する
  digitalWrite(BYOU_LED, HIGH);

  // スイッチが押されるまで待つ
  while(digitalRead(SWITCH) == SWITCH_OFF) {
  }

  // タイマー開始時に緑色LEDを点灯
  controlTimeLed(jyuubun); // 「残り時間十分」の表示

}


void loop() {
  
  uint8_t count;       // for文で回数を数えるために使用する変数
  uint8_t oto_bangou;  // for文でメロディーを鳴らすために使用する変数
  

  // TIMER_JIKAN分の回数を数える
  for( count=0; count<TIMER_JIKAN; count++) {

    // 残り時間表現用LEDの制御
    if( count == KIIRO_JIKAN ) {
      controlTimeLed(hanbun); // 「残り時間少ない」の表示
    }

    if( count == AKA_JIKAN ){
      controlTimeLed(mousugu); // 「もうすぐ設定時間」の表示
    }

    // 残り時間に応じて処理を変える
    if( count >= (TIMER_JIKAN - 5) ) {
      // 残り時間が5秒以下になったら、1秒に1回青色LEDを点滅して音を鳴らす
      digitalWrite(BYOU_LED, HIGH);
      tone(SPEAKER, ALARM);
      delay(BYOU_ON);
      digitalWrite(BYOU_LED, LOW);
      noTone(SPEAKER);
      delay(BYOU_OFF);
    } else {
      // そうでなければ、1秒に1回青色LEDを点滅する
      digitalWrite(BYOU_LED, HIGH);
      delay(BYOU_ON);
      digitalWrite(BYOU_LED, LOW);
      delay(BYOU_OFF);
    }
    
  }

  // 時間になったので秒のLED(青色LED)を点灯する
  digitalWrite(BYOU_LED, HIGH);

  // メロディー音を3回鳴らす
  for( count=0; count<3; count++) {

    // メロディーを演奏する
    for( oto_bangou=0; oto_bangou<ALARM_ONKAISUU; oto_bangou++ ) {
      tone(SPEAKER, alarm_melody[oto_bangou]);
      delay(200);
    }
    
    // 音を消す
    noTone(SPEAKER);

    // 1.5秒あける
    delay(1500);

  }

  // 何もしないで待つ
  while( true ) {
  }

}

 

更新履歴

日付 内容
2019.12.1 新規投稿