第44回 構造体

今回はメロディーデータの形式を変更します。

メロディーデータの問題点

前回の記事では2次元配列を使用して、メロディーデータを音階データと音の長さデータの組のようなデータ形式にしました。

uint16_t alarm_melody[][2] = {
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {SI_4, 400},
  {SO_5, 800}};

このように書くと、音の長さの変更もしやすいですし、データも読みやすくなります。

ところで、、、

メロディーデータをこのような2次元配列にした場合、スケッチを読むときにちょっとだけ問題があります。

例えば、最初の音を鳴らす場合は以下のようにスケッチに書くことになります。

tone( alarm_melody[0][0] );

また、この音を一定の長さで鳴らしたままにするため、以下のようにdelay関数を使って時間待ちをします。

delay( alarm_melody[1][0] );

スケッチを自分で作った場合はこれで問題ないですが、初めてこのスケッチを見た人は、この2次元配列の部分がちょっとわかづらそうって感じがしませんか?

具体的には、音階データは「alarm_melody[0][0]」、音の長さデータは「alarm_melody[1][0]」になっていて、「0」が音階データ、「1」が音の長さデータにしていますが、そもそも「0」「1」というのは自分で決めただけなので、他の人が見たらわかりづらいですよね。

例えば、以下のように#define定義すればもう少しわかりやすくなりそうです。

#define ONKAI 0
#define NAGASA 1

tone( alarm_melody[ONKAI][0] );
delay( alarm_melody[NAGASA][0] );

このように書くと、音階データなのか音の長さデータなのか、数字で書くよりはわかりやすくなりますが、相変わらず#defineで0と1を指定していますので、ちょっとわかりづらい気もします。

C/C++言語ではこのような時のために他の仕組みが用意されていますので、その仕組みを使ってスケッチを変更してみます。

 

構造体

キッチンタイマーで扱うメロディーデータは、「音階データ」とその「音の長さデータ」で、これら2つのデータは密接な関係があります。

メロディーデータに限らず、このように密接に関係しているデータを扱うケースは他にもたくさんあります。

例えば、ArduinoボードにLEDとスピーカー、スイッチを接続して、どれかのスイッチを押したときに、LED
とスピーカーが反応するような動作を考えます。

例えば、あるスイッチを押すと、

「12番端子に接続してあるLEDを点灯させて、ド(262Hz)の音を鳴らす」

という動作にしたり、また、違うスイッチを押すと、

「10番端子に接続してあるLEDを点灯させて、ソ(392Hz)の音を鳴らす」

という動作にする場合を考えてみます。この場合、「12と262」「10と392」のようにLED端子番号と周波数の数字はまとめて組にして扱いたいですよね。

このようにデータを一つの「組」として扱うために「構造体」という仕組みが用意されています。

構造体とは、複数の変数を組にして、その組に対して名前をつける仕組みです。これだけの説明ですとちょっとわかりづらいので、アラーム音の例で考えてみます。

アラーム音の一つ一つのデータは、「音階」と「音の長さ」のデータが組になっていますよね。これを変数で別々に扱う場合は例えば以下のように宣言します。

uint16_t onkai;
uint16_t nagasa;

このように2行続けて書くと、「なんとなく組になってるよなぁ」って感じですよね。

でも次のように、この2行以外にも変数の宣言がたくさんあったら、この2つの変数が組になっている、と理解するのはだんだん難しくなってきます。

uint16_t onkai;
uint16_t nagasa;
uint8_t  count;
uint8_t  oto_bangou;

そこで、スケッチを見ただけで誰もが「この『onkai』と『nagasa』の2つの変数は組になっているんだ」と理解できるようになるといいですよね。

「構造体」とは、複数の変数を誰が見ても組になっていることがわかるようにする仕組みです。イメージとしては以下のような感じです。

構造体のイメージ

構造体の宣言は、変数の組に名前をつけてまとめる、というようなものです。

それでは、最初に構造体の宣言方法から確認しましょう。

構造体宣言

さて、、、この説明だけではさっぱりわかりませんよね。ということで、具体例で確認します。先ほどのイメージのように、uint16_t型の「onkai」と「nagasa」という2つの変数をまとめて組にして「oto」という名前を付ける場合を例にします。「音」は「音階」と「長さ」から作られている、というイメージでしょうか。

この場合、構造体の宣言は以下のように書きます。

struct oto {
  uint16_t onkai;
  uint16_t nagasa;
};

このように、組にしたい変数を「 { 」と「 } 」が囲み、その前に「struct 組の名前」と書いて、最後に「 ; 」をつければOKです。

スケッチに構造体宣言がこのように書かれていた場合、以下のような意味で理解すればOKです。

構造体宣言の読み方

構造体で使う専門用語も覚えておきましょう。構造体の名前のことを「タグ名」、組にする変数のことを「メンバ」や「メンバ変数」などと呼んでいます。

このように宣言したタグ名が「oto」という構造体ですが、すぐに使えるわけではないんです。

初めて構造体を習得するとき、ここが一番つまづきやすいところですので、しっかりと理解するようにしてください。

先ほど宣言した構造体(タグ名がotoの構造体)は、変数そのものではなく、変数型のようなものなんです。ん? ちょっとわかりづらいですよね。そこで、もう一度「変数」がどのようなものだったかを振り返ってみましょう。

例えば「uint8_t」というのは「0〜255」までの数字が書けるメモ帳の「タイプ」を指定するものでしたよね。この「uint8_t」というのは変数そのものではなく、変数の「型」でした。実際に変数を使うときは以下のように変数型を指定して、変数名を書きます。

uint8_t count;

このようにスケッチに書くと、uint8_t型の変数「count」が用意されます。

先ほど宣言した構造体は、「uint8_t」と同じような型を表すものなんです。そこで構造体を使用するときは、構造体のタグ名を指定して、実際に使用する構造体を作る必要があります。

キッチンタイマーの設定時間になったら演奏するアラームですので「alarm_melody」という名前にします。

このとき以下のように宣言すると、「alarm_melody」は先ほど宣言した構造体になります。

struct oto alarm_melody;

このように宣言した「alarm_melody」構造体は、2つのメンバ変数「onkai」と「nagasa」を持っています。

さて、このように宣言した「alarm_melody」のメンバ変数「onkai」と「nagasa」に値を代入する場合はどうすればいいのでしょうか。

構造体への値の代入は以下のようにします。

alarm_melody.onkai = 262;
alarm_melody.nagasa = 200;

このように構造体のメンバ変数への代入は構造体の名前に「 . 」(ピリオド)をつけて、メンバ変数を書きます。

また構造体のメンバ変数を知るには、代入した時と同じように構造体の名前に「 . 」をつけてメンバ変数を書きます。

構造体の基本的な使い方は以上になります。

 

構造体の配列

さて実際のメロディーは、複数の音の集まりですので構造体ではなく、構造体の配列を使用することになります。そこで、このセクションでは構造体の配列の使い方を習得しましょう。

構造体の配列は、普通の配列と同じように宣言します。キッチンタイマーのメロディーデータは音の数が13ですので、この場合は以下のように宣言します。

struct oto alarm_melody[13];

このように宣言した構造体配列のメンバ変数は以下のように使用します。

alarm_melody[0].onkai = 262;
alarm_melody[0].nagasa = 200;

また、構造体配列の初期化は、2次元配列の時と同様に以下のようにかけばOKです。配列の添字は初期化の場合は数が明確になっていますので省略することができます。

struct oto alarm_melody[] = {
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {SI_4, 400},
  {DO_5, 800}
};

 

メロディーデータを構造体にする

という気ことで、構造体とその配列について一通り説明しましたが、かなりややこしかったですよね。これまでの知識をもとにスケッチを構造体の配列に変更しましたので、じっくり読み解いて構造体とその配列の使い方をしっかり習得するようにしてください。

/*
 * キッチンタイマー
 * 
 * 内容: スイッチ、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: 残り時間を列挙型で宣言
 *   2019.12. 7: メロディーの長さを変えられるように変更
 *   2019.12.21: メロディーデータを構造体の配列に変更
 */

// 音階定義ファイルのインクルード
#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 // アラームメロディーの音の数

// 音データの構造体
struct oto {
  uint16_t onkai;
  uint16_t nagasa;
};

// 音データの構造体配列
struct oto alarm_melody[] = {
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {MI_4, 400},
  {DO_4, 200},
  {MI_4, 200},
  {SO_4, 400},
  {SI_4, 400},
  {DO_5, 800}
};

// 残り時間の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][0]);
      //delay(alarm_melody[oto_bangou][1]);
      tone(SPEAKER, alarm_melody[oto_bangou].onkai);
      delay(alarm_melody[oto_bangou].nagasa);
    }
    
    // 音を消す
    noTone(SPEAKER);

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

  }

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

}

 

 

更新履歴

日付 内容
2019.12.21 新規投稿