第37回 プリプロセッサ指示子 #include

タイトル画像

今回はスケッチを整理します。

目次

スケッチ、長くないですか?

前回、アラーム音をメロディーに変えたのはいいですが、スケッチがなんだかずいぶん長くなってしまいましたよね。

特に最初の#defineが長くなっています。肝心のスケッチ本体であるsetup()やloop()がずいぶんと下の方に行ってしまい、スケッチを開くとしばらく#defineばかりでなんとも見づらいスケッチ、という状況です。

今回の記事では、この長い#define部分をなんとかしてみましょう。

ヘッダ

今まで作成したスケッチを見てみます。

スケッチの構成を見ると、最初にコメント部分、次に#define部分、それらが終わるとやっとsetup()とloop()がでてきます。

Arduinoのスケッチに限らず、プログラム本体が始まるまでの部分を「ヘッダ」と呼んでいます。Arduinoの場合はsetup()が始まるまでの部分がヘッダになります。

ヘッダ部

それにしても、前回追加した以下の音階の#define定義部分はなんだか無駄に長いですよね。

// 音の周波数
#define DO_3   131
#define DOS_3  139
#define RE_3   147
#define RES_3  156
#define MI_3   165
#define FA_3   175
#define FAS_3  185
#define SO_3   196
#define SOS_3  208
#define RA_3   220
#define RAS_3  233
#define SI_3   247

#define DO_4   262
#define DOS_4  277
#define RE_4   294
#define RES_4  311
#define MI_4   330
#define FA_4   349
#define FAS_4  370
#define SO_4   392
#define SOS_4  415
#define RA_4   440
#define RAS_4  466
#define SI_4   494

#define DO_5   523
#define DOS_5  554
#define RE_5   587
#define RES_5  622
#define MI_5   659
#define FA_5   698
#define FAS_5  740
#define SO_5   784
#define SOS_5  831
#define RA_5   880
#define RAS_5  932
#define SI_5   988

よくよく考えてみると、この音階の定義の部分って、、、

  • 音階によって周波数が決まっているわけだから、変更することはないし、
  • この音階の定義部分は、他に何かメロディーを演奏するスケッチを作った時に流用したいし、
  • 正直なところ邪魔なので、どこか目につかないところで定義してほしい

って感じですよね。

例えば、ヘッダ部のはじめの方にある「#define BYOU_LED 12」は青色LEDの接続端子番号を定義しています。将来、青色LEDの接続端子を変更した場合、この定義を変更すればスケッチ側の対応はすぐに終わります。また、「#define TIMER_JIKAN 10」はタイマー時間を定義しています。スケッチを作成している間はこのタイマー時間をいろいろ変更できるので#define定義してあるのは便利です。

このように以前定義していた#defineは参照したり変更したりするのでスケッチのヘッダ部分に書いてある方が便利です。

でも音階の#define定義は、各音階の周波数の定義が変わらない限り変更することもないですよね。さらに、他のスケッチでも使うかもしれません。

C/C++言語では、このような時のために便利な機能があります。最初に概要を説明します。

ヘッダの外部ファイル化

このように普段はそれほど参照しないし、他でも流用するようなヘッダ部分は、以下のように外部にファイル化して、

外部ファイル化

そのファイルをスケッチで指定して取り込んでもらうにすれば便利ですよね。

外部ファイルのインクルード

C/C++言語では、このような仕組みが用意されています。

#include

先ほどのようにヘッダ部分をファイル化したものを「ヘッダファイル」と呼んでいます。

スケッチでヘッダファイルを取り込んでもらう場合、以下のように「#include」(インクルード)という命令を使用します。

#include(1)

「include」は日本語で、「含める」「取り込む」などの意味です。

また、このようにヘッダファイルを取り込むことを「ヘッダファイルをインクルードする」と呼んでいます。

なお、このようにインクルードするファイル名の拡張子は通常「h」を使用します。hはheader(ヘッダ)のhです。例えば先ほどの音階の#define部分をファイルにした場合、「onkai.h」などのファイル名にします。

別ファイルにした「onkai.h」をスケッチで取り込む場合、スケッチの先頭部分に以下のように書きます。

#include "onkai.h"

Arduino IDEは、この#includeを処理するときに、「onkai.h」というファイルをキッチンタイマーと同じフォルダに探しにいきます。このファイル名のファイルがあれば、そのファイルを取り込んで処理します。

#includeはちょっとややこしいのですが、ファイル名をダブルクォーテーション( ” )で囲んだ場合、そのスケッチと同じフォルダを探しにいきます。またヘッダファイルがスケッチとは違うフォルダにある場合、フォルダパスも含めて指定します。

なお、ヘッダファイルは自分で作成したもの以外にもArduinoIDEで最初から用意されているものを使用することもあります。このようにシステム側で用意されているヘッダファイルをインクルードする場合は以下のようにファイル名を不等号記号(< >)で囲みます。

#include(2)

このような< >を使用してヘッダファイルを指定する方法は、基礎編のパート2で出てきます。そのときに#includeの役割についてさらに詳しく説明します。

それでは、スケッチの音階#define部分をヘッダファイル化してインクルードするように変更します。

音階のヘッダファイル化

それでは音階#define部分をヘッダファイルにしてみます。

キッチンタイマーのスケッチに新しくファイルを追加する場合、以下のようにウインドウ右上の三角ボタンをクリックします。

新規タブ追加

三角ボタンをクリックすると以下のようにメニューが表示されますので、一番上の「新規タブ」を選択します。

新規タブメニュー

「新規タブ」を選択すると、ウインドウの下の方に新しいタブ名を入力する欄が表示されます。

新規タブ名入力

このタブ名のところに「onkai.h」と入力してOKボタンをクリックすると、新しくonkai.hというファイルが作成されます。

新規タブ作成

なお、ファイル名を間違ってしまった場合は、先ほどの三角メニューに「名前を変更」という選択肢がありますので、それを選んでファイル名を修正します。

それでは、元の「kitchen_timer」のスケッチから、音階の#define定義部分をコピーして新しく作成した「onkai.h」にコピーしましょう。

onkai.h

この状態ですとまだ内容が保存されていませんので、ファイルメニューから保存を選択して保存します。また、元の「kitchen_timer」にある音階定義の部分は必要ありませんので削除します。

これで音階の#define定義部分をヘッダファイルにできましたので、kitchen_timerのスケッチでこのファイルをインクルードします。インクルードは通常スケッチの最初の方に書きます。

ここまでの作業で完成したスケッチを以下に示します。

「kitchen_timer」のスケッチ

/*
 * キッチンタイマー
 * 
 * 内容: スイッチ、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: アラーム音をメロディーに変更
 *               音階定義をファイル化
 */

// 音階定義ファイルのインクルード
#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 // 通常のアラーム音周波数



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を点灯
  digitalWrite(LED_MIDORI, HIGH);

}


void loop() {
  // for文で回数を数えるために使用する変数
  uint8_t count;

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

    // 残り時間表現用LEDの制御
    if( count == KIIRO_JIKAN ) {
      digitalWrite(LED_MIDORI, LOW);
      digitalWrite(LED_KIIRO,  HIGH);
    }

    if( count == AKA_JIKAN ){
      digitalWrite(LED_KIIRO, LOW);
      digitalWrite(LED_AKA, HIGH);
    }

    // 残り時間に応じて処理を変える
    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++) {

    // ド
    tone(SPEAKER, DO_4);
    delay(200);
    // ミ
    tone(SPEAKER, MI_4);
    delay(200);
    // ソ
    tone(SPEAKER, SO_4);
    delay(200);
    // ミ
    tone(SPEAKER, MI_4);
    delay(200);
    // ド
    tone(SPEAKER, DO_4);
    delay(200);
    // ミ
    tone(SPEAKER, MI_4);
    delay(200);
    // ソ
    tone(SPEAKER, SO_4);
    delay(200);
    // ミ
    tone(SPEAKER, MI_4);
    delay(200);
    // ド
    tone(SPEAKER, DO_4);
    delay(200);
    // ミ
    tone(SPEAKER, MI_4);
    delay(200);
    // ソ
    tone(SPEAKER, SO_4);
    delay(200);
    // シ
    tone(SPEAKER, SI_4);
    delay(200);
    // 1オクターブ高いド
    tone(SPEAKER, DO_5);
    delay(200);
    
    // 音を消す
    noTone(SPEAKER);

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

  }

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

}

「onkai.h」のスケッチ

// 音の周波数
#define DO_3   131
#define DOS_3  139
#define RE_3   147
#define RES_3  156
#define MI_3   165
#define FA_3   175
#define FAS_3  185
#define SO_3   196
#define SOS_3  208
#define RA_3   220
#define RAS_3  233
#define SI_3   247

#define DO_4   262
#define DOS_4  277
#define RE_4   294
#define RES_4  311
#define MI_4   330
#define FA_4   349
#define FAS_4  370
#define SO_4   392
#define SOS_4  415
#define RA_4   440
#define RAS_4  466
#define SI_4   494

#define DO_5   523
#define DOS_5  554
#define RE_5   587
#define RES_5  622
#define MI_5   659
#define FA_5   698
#define FAS_5  740
#define SO_5   784
#define SOS_5  831
#define RA_5   880
#define RAS_5  932
#define SI_5   988

スケッチができたらArduinoボードに送って動作を確認してみましょう。

プリプロセッサ指示子

ところで先ほど「#include命令」と説明しましたが、実はこれ「Arduinoボードに対する命令」ではないんです。#includeは「プリプロセッサ指示子」と呼ばれています。

ここで今回のスケッチがどのように処理されてArduinoボードに送られているか、少し詳しくみてみます。

プリプロセッサ

Arduino IDEはスケッチをArduinoボードに送るときスケッチをArduinoボード用に変換しますが、その前にスケッチの中に#includeがあればその部分を#includeで指定したファイルの中身で置き換えます。

このようにArduino IDEは、スケッチをArduinoボード用に変換する前に、スケッチの前処理をしているんです。この前処理は「プリプロセッサ」というツールが行なっています。「プリプロセッサ」は英語で「pre-processor」で、「pre」は「前」、「processor」は「処理」という意味です。

#includeはプリプロセッサに指示をする文字ですので「プリプロセッサ指示子」と呼ばれています。また「プリプロセッサ指令」などと呼ばれることもあります。

実は#defineもプリプロセッサ指示子だったんです。

ヘッダファイル

ところで、今回は音階の#define定義を「onkai.h」という別ファイルにしてkitchen_timerでインクルードしましたが、「kitchen_timer」のスケッチにはまだ#defineがたくさん残っていますよね。

kitchen_timerに残っている#defineは変更することがあるのでこのままにしますが、一般的には#defineなどヘッダ部が長い場合、ヘッダファイルにすることが多いです。

kitchen_timerのスケッチでは、#defineの部分を全てヘッダファイルに持っていき、kitchen_timerのスケッチにはsetup()とloop()を書くようにします。

今回はkitchen_timerスケッチに#defineを書きますが、ネットで他の人が書いたスケッチでは、スケッチとは別にヘッダファイルにヘッダ部が書かれている場合もあります。その場合、原則的なルールがありますので説明します。

プログラムの拡張子は、Arduinoスケッチの場合は「ino」、C言語の場合は「c」、C++言語の場合は「cpp」です。例えばC言語の「sample.c」というプログラムファイルがある場合、このプログラムのヘッダファイルを作る場合は「sample.h」というようにプログラム名と同じ名前でヘッダファイルを作成します。

今回のkitchen_timerスケッチのヘッダ部分をヘッダファイルにする場合は、ヘッダファイルのファイル名は「kitchen_timer.h」とします。

なお、ヘッダファイルは複数使用する場合も多くあります。その場合は、ヘッダファイル自体を複数用意して、本体のスケッチには#includeを複数書いてそれらのヘッダファイルを読み込みます。

今回のキッチンタイマーのスケッチの場合、以下のようなファイル構成にすることもできます。

ファイル 内容
onkai.h 音階の#defineが書かれたヘッダファイル
kitchen_timer.h キッチンタイマーのLED接続端子番号などの#defineが書かれたヘッダファイル
kitchen_timer.ino キッチンタイマーのスケッチ本体。このスケッチには#defineは書かれておらず、上の2つのヘッダファイルを#includeにより読み込む

スケッチをフォルダに入れる理由

かなり前の記事になりますのですっかり忘れてしまっていると思いますが、第10回の記事で、スケッチを作成するとスケッチのファイルが1つなのに、それをフォルダに入れていることが謎でした。

スケッチファイルは1つしかないので、わざわざフォルダに入れる必要はないのに、なんとも無駄なことをしているように見えていましたよね。

今回の記事で作成したスケッチを開いた状態で、「スケッチ」メニューから「スケッチのフォルダを表示」を選択してみてください。このメニューを選択すると、kitchen_timerのスケッチが保存されているフォルダが表示されます。

このフォルダの中身を見ると、「kitchen_timer.ino」というスケッチ本体と一緒に、今回の記事で作成したヘッダファイル「onkai.h」が保存されていますよね。

ということで、スケッチをフォルダで保存するのは、このようにスケッチ以外に関連するファイルが複数出てくるためなんです。

今回は「onkai.h」というヘッダファイル1つを追加しただけですが、スケッチの規模が大きくなってくると、スケッチ自体を複数のファイルに分けたり、ヘッダファイルを複数使用したりというケースが出てきます。そのような時のためにファイルがバラバラにならないようにスケッチは始めからフォルダで管理されています。

更新履歴

日付 内容
2019.11.4 新規投稿
2021.8.27 新サイトデザイン対応
2022.2.17 複数ヘッダファイルの説明追加
通知の設定
通知タイミング
guest
0 コメント
本文中にフィードバック
全てのコメントを見る
目次