第11回 変数のスコープ

スイッチが押されたらLEDをピカッと点灯するスケッチをうまく動くようにします。

第10回から第12回までの内容は、少し難しくなっています。
すべて理解できなくても、第13回以降の記事がわからなくなる、ということはありません。理解できるところまでチャレンジしてみましょう!
もし難しく感じたら、とりあえず第12回の記事まではスキップして、第13回の記事からお読みいただければと思います。

目次

スケッチがうまく動かない原因

前回の記事で作成したスケッチは、アルゴリズムとしては問題ないはずなのにうまく動作しませんでした。

今回の記事では、先にその原因を解明します。

ただ、言葉で解説しているとよくわからなくなってしまいますので、サンプルのスケッチの動作を見て、その原因を把握していこうと思います。


次のスケッチは、loop関数の中でcountという変数を毎回プラス1しながら、その内容をシリアルモニタに表示する、というものです。

/*
 *   変数の振る舞いについて確認するスケッチ
 *     loop関数で変数countを毎回プラス1しながらシリアルモニタに表示
 */

void setup() {
  
  // シリアルモニタ初期化
  Serial.begin(9600);

  // シリアルモニタ待ち
  while(!Serial){
  }

}


void loop() {

  // 「count」という変数を宣言する
  uint8_t count;

  // countの数字を1プラスする
  count++;

  // countの内容をシリアルモニタに表示する
  Serial.println(count);

  // 1秒待つ
  delay(1000);
  
}

このスケッチを実行すると、スケッチには次のように表示されました。(数字は1でないケースもあります)

1
1
1
(このあと同じ数字が続く)

loop関数の中では、変数count++で毎回プラス1されるようにしたつもりです。

ポイントは、同じcountという変数を毎回プラス1しているのに実際のcountの値は増えない、という点です。

なぜこのようになるのか、次にその理由を解明していきます。

変数のスコープ

もしかしたら、上のスケッチがうまく動かない理由はすぐにわかった方もいらっしゃるかもしれませんね。

話は基礎編パート1の第27回までさかのぼります。その記事の内容は「変数のスコープ」というものでした。

かなり前の話ですので、もう一度「変数のスコープ」の意味を確認しておきたいと思います。

変数が使える範囲

変数は、宣言する位置によって使える範囲が決められている、というルールがありました。

その使える範囲のことを「変数のスコープ」「変数の有効範囲」と呼んでいます。

例えば、次のようにloop関数の始めに変数を宣言した場合、変数のスコープは「変数を宣言した直後から、その変数が属している{}の範囲内の}まで」でした…

って、なんだかややこしいですよね。そこで具体例として次のスケッチで確認しておきましょう。

このスケッチでは、変数countloop関数の始めで宣言しています。

このcountが使える範囲(countのスコープ)は、「変数を宣言した直後から、この変数が属する}まで」です。

つまり、上のスケッチではcountを宣言した直後からloop関数の最後の}までが、この変数の使える範囲になります。

変数の寿命

ところで、この「変数のスコープ」に関して、もう一つ重要な性質があります。

先ほど、スケッチ上で変数の使える範囲について説明しました。

今度は、Arduino Micro内部でこの変数をどのように扱っているかという裏の話です。


このシリーズ記事では、「変数」を「メモ用紙」に例えて説明してきました。

例えばuint8_t count;と宣言すると、countという名前で、0〜255まで書くことのできるサイズのメモ用紙が用意される、というイメージでしたよね。(ただ宣言するだけですと、次のように適当な数字が書かれている状態になります)

変数のイメージ

ところで、このメモ用紙には寿命があるんです。

この「寿命」に関して、Arduino Microが裏でどのようにこの変数、つまりメモ用紙を処理しているか確認していきます。


Arduino Microは、uint8_t count;と宣言したときにこのメモ用紙を用意します。

ところが、変数のスコープ最後の}のところでArduino Microはせっかく用意したメモ用紙を捨ててしまうんです!(もったいないですね!)

変数を宣言していろいろな計算をしても、}のところできれいさっぱり捨てられてしまいます。

スケッチがうまく動かなかった理由

最初のスケッチでloop関数で毎回countをプラス1しているのに数字が増えなかったのは、これが原因です。

スケッチでは、次のloop関数の4行目で変数を用意して、7行目でプラス1したにもかかわらず、最後の15行目の}で変数countは捨てられてしまっていたためなんです。

void loop() {

  // 「count」という変数を宣言する
  uint8_t count;

  // countの数字を1プラスする
  count++;

  // countの内容をシリアルモニタに表示する
  Serial.println(count);

  // 1秒待つ
  delay(1000);
  
}

せっかく7行目でcountを1増やしても、最後の}で変数が捨てられてしまいます。

そのため、loop関数は何度も繰り返しますが、毎回新しいメモ用紙が用意されるため、シリアルモニタには「1」ばかりが表示されていたわけです。


Arduino Microは内部でこのように変数を扱っているため、変数の「寿命」があることがわかりました。

この変数の寿命は「変数の記憶域期間」と呼ばれることがあります。

なんだかすごく難しい専門用語って感じですね。個人的な経験では、周りの人でこの用語を使うことはあまりいないので、頭の片隅に置いておいていただければと思います。

グローバル変数の寿命

今まで説明したように{}で囲まれた領域内で宣言した変数は、いずれ}に出会うので寿命があることがわかりました。

{}で囲まれた領域内で宣言した変数は「ローカル変数」とも呼ばれていましたよね。つまりローカル変数には寿命があるわけです。

ところで、「ローカル変数」に対して「グローバル変数」があります。

今度はグローバル変数の寿命について見ていきます。


グローバル変数は{}の範囲外で宣言されますので、どこのブロックにも属していません。

先ほど、ローカル変数は}で捨てられる、と説明しましたが、グローバル変数は}に出会うことはありません。

つまり、グローバル変数は捨てられてしまうタイミングがないので、寿命は永遠ということになります。

Arduino Microは電源を切ると完全に動作が止まりますので「永遠」は言い過ぎですが、実際にはスケッチが動作開始してから、スケッチの動作停止までです。

Arduino Microは、電源を入れるとスケッチの動作を開始して、電源を切るまでずっとスケッチが動作します。

つまり、グローバル変数の寿命は、Arduino Microの電源を入れてから電源を切るまでになります。


変数のスコープについて理解が深まりましたので、前回のスケッチが動作しなかった理由を考えて正しく動作するようにしてみましょう!

前回記事のスケッチを正しく動作するように変更しよう!

正しく動作しなかった原因

前回のプログラムがうまく動作しなかったのは、次回のスイッチ状態チェック用に保存しておいたzenkaiの変数が、そのスコープの最後、つまりloop関数の最後で捨てられてしまっていたためです。

せっかく変数に保存したのに、その直後に捨てられてしまっていました!

これでは次回のloopの処理のとき用にzenkaiに値を保存しておく、という処理はうまくいきませんよね…

正しく動作するスケッチ

では、どのように解決すればよいのでしょうか?

原因は変数zenkailoop関数の最後に捨てられてしまうことにありましたので、loop関数の処理が終わっても捨てられない変数にする必要があります。

つまり変数zenkaiをグローバル変数にすればよいわけです。

ということで、変数zenkaiをグローバル変数に変更しましょう。

次のスケッチは変数zeinkaiをグローバル変数に変更したものです。「グローバル変数に変更」といっても、宣言の位置を20行目に移動しただけですが…

/*
 *  スイッチ制御プログラム改良版
 *    スイッチを押すとピカッとLEDを光らせる
 */

// 青色LEDのピン接続番号
#define LED_BLUE  12

// 左側スイッチのピン接続番号
#define SWITCH_HIDARI  A5

// スイッチの状態
#define OFF 1
#define ON  0

// 点灯時間(単位:ms)
#define TENTOU_JIKAN 50

// 前回のスイッチ状態保存用の変数
uint8_t zenkai;

void setup() {
  pinMode(LED_BLUE, OUTPUT);             // LEDのピンを出力に設定
  pinMode(SWITCH_HIDARI, INPUT_PULLUP);  // スイッチのピンを入力(プルアップ)に設定

  digitalWrite(LED_BLUE, LOW);  // 最初はLEDをOFFに設定しておく
}


void loop() {

  uint8_t konkai;  // 今回のスイッチ状態を記録する変数
  
  // 今回のスイッチ状態を読み取る
  konkai = digitalRead(SWITCH_HIDARI);

  // スイッチ状態が、前回OFF、今回ONの時、スイッチが押されたと判断する
  if( (zenkai == OFF) && (konkai == ON) ) {
    digitalWrite(LED_BLUE, HIGH);  // LEDを点灯
    delay(TENTOU_JIKAN);           // 一定時間待つ
    digitalWrite(LED_BLUE, LOW);   // LEDを消す
  }

  // 今回の値を前回の値として保存
  zenkai = konkai;
  
}

これでうまく動くようになりました。

手元にブレッドボード回路がありましたら動作確認してみてください!スイッチを押した瞬間、LEDがピカっと光ると思います。

このようにスイッチが押されたことを検知できれば、あとは「LEDをピカッと1回光らせる」という処理を「1文字のデータをPCに送る」という処理に変更すれば、キーボードが作れそうです!

と、今回の記事は無事にここで終わり、ということになりそうですが、実は上のスケッチにはちょっと変なところがあるんです。

さらに深掘りしましょう!もう少しお付き合いください!

グローバル変数の初期化

すでにお気づきの方もいらっしゃるかもしれませんが、グローバル変数として宣言したzenkaiは次のようにただ宣言しただけです。

uint8_t zenkai;  // グローバル変数宣言

今まで、このように変数を宣言すると(=初期化せずに変数を宣言すると)、変数には適当な数字が代入されている、と説明してきました。

実はこの説明はローカル変数のときの話だったんです。

グローバル変数は、上の例のように初期化せずに宣言しても「裏で自動的に0に初期化」してくれるんです。


ということは、グローバル変数で上のように宣言したzenkaiは最初0が代入された状態になります。

でもzenkaiに代入する値はスイッチの状態で、#defineで次のように定義していましたよね。

// スイッチの状態
#define OFF 1
#define ON  0

最初はスイッチOFF(のはず)ですので、zenkaiの値は最初は1になります。

そこで、完成版のスケッチとしてはzenkaiは次のように1(OFF)で初期化することにしました。(20行目)

/*
 *  スイッチ制御プログラム改良版
 *    スイッチを押すとピカッとLEDを光らせる
 */

// 青色LEDのピン接続番号
#define LED_BLUE  12

// 左側スイッチのピン接続番号
#define SWITCH_HIDARI  A5

// スイッチの状態
#define OFF 1
#define ON  0

// 点灯時間(単位:ms)
#define TENTOU_JIKAN 50

// 前回のスイッチ状態保存用の変数
uint8_t zenkai = OFF;

void setup() {
  pinMode(LED_BLUE, OUTPUT);             // LEDのピンを出力に設定
  pinMode(SWITCH_HIDARI, INPUT_PULLUP);  // スイッチのピンを入力(プルアップ)に設定

  digitalWrite(LED_BLUE, LOW);  // 最初はLEDをOFFに設定しておく
}


void loop() {

  uint8_t konkai;  // 今回のスイッチ状態を記録する変数
  
  // 今回のスイッチ状態を読み取る
  konkai = digitalRead(SWITCH_HIDARI);

  // スイッチ状態が、前回OFF、今回ONの時、スイッチが押されたと判断する
  if( (zenkai == OFF) && (konkai == ON) ) {
    digitalWrite(LED_BLUE, HIGH);  // LEDを点灯
    delay(TENTOU_JIKAN);           // 一定時間待つ
    digitalWrite(LED_BLUE, LOW);   // LEDを消す
  }

  // 今回の値を前回の値として保存
  zenkai = konkai;
  
}

これでやっと完成しました!

グローバル変数の初期化

グローバル変数は宣言時に初期化しなくても0で自動的に初期化されます。

そのため、0で初期化されたグローバル変数を使いたい場合、次のようなスケッチで全く問題ありません。

int switch;  // グローバル変数の場合、0で初期化される

ただ、このような場合でも次のようにスケッチを書くケースを見かけることがあります。

int siwtch = 0;  // グローバル変数を0で初期化した宣言の書き方

これは、「0で初期化しています」ということを明示的にスケッチ上で伝えるためです。

スケッチを見ているといろいろな書き方がありますので、このような背景を頭の片隅に入れておいていただければと思います。

ミニチャレンジ課題

今回もミニチャレンジ課題に挑戦してみましょう。

課題

ミニチャレンジ課題

作成したスケッチは、スイッチを押した瞬間にLEDが50ms短く1回だけ点灯します。
今度は、スイッチを押したタイミングではなく、スイッチを離したタイミングでLEDが50msピカッと光るスケッチを作ってみてください。

解答例

「スイッチを離したタイミング」は次のイラストのように考えると、検知したいタイミングは「前回のスイッチ状態がON」かつ「今回のスイッチ状態がOFF」という条件が成立するときです。

つまりifの条件部分は次のように書けば良いことになります。

  if( (zenkai == ON) && (konkai == OFF) ) 

先ほど完成させたスケッチのこの部分を変更すれば良いので、次のようなスケッチにしてみました。

/*
 *  Arduino入門基礎編パート2 第11回
 *    ミニチャレンジ課題解答例
 */

// 青色LEDのピン接続番号
#define LED_BLUE  12

// 左側スイッチのピン接続番号
#define SWITCH_HIDARI  A5

// スイッチの状態
#define OFF 1
#define ON  0

// 点灯時間(単位:ms)
#define TENTOU_JIKAN 50

// 前回のスイッチ状態保存用の変数
uint8_t zenkai = OFF;

void setup() {
  pinMode(LED_BLUE, OUTPUT);             // LEDのピンを出力に設定
  pinMode(SWITCH_HIDARI, INPUT_PULLUP);  // スイッチのピンを入力(プルアップ)に設定

  digitalWrite(LED_BLUE, LOW);  // 最初はLEDをOFFに設定しておく
}


void loop() {

  uint8_t konkai;  // 今回のスイッチ状態を記録する変数
  
  // 今回のスイッチ状態を読み取る
  konkai = digitalRead(SWITCH_HIDARI);

  // スイッチ状態が、前回OFF、今回ONの時、スイッチが押されたと判断する
  if( (zenkai == ON) && (konkai == OFF) ) {
    digitalWrite(LED_BLUE, HIGH);  // LEDを点灯
    delay(TENTOU_JIKAN);           // 一定時間待つ
    digitalWrite(LED_BLUE, LOW);   // LEDを消す
  }

  // 今回の値を前回の値として保存
  zenkai = konkai;
  
}

更新履歴

日付内容
2021.9.11新規投稿
2025.2.9説明内容簡略化
グローバル変数初期化の解説を訂正
ミニチャレンジ課題解答例追加
通知の設定
通知タイミング
guest
0 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
目次