こんにちは、「メカのりまき」です。

この記事では、外部割り込みについて説明した後、前回の記事で途中まで制作したLEDを使ったゲームの説明の続きをしたいと思います。

この記事は前回の記事の続きとなっております。

関数が良く分からないという方や、後半説明するLEDを使ったゲームを制作したいという方は、前回の記事を先にご覧になることをお勧めします。

本記事で使用するもの

本記事で私が使用した物は以下の通りです。

  • 「Arduino IDE2.0.4」をインストールしたPC(Windows11)
  • USBケーブル
  • Arduino Uno R3(ELEGOO UNO R3でも可)
  • ブレッドボード
  • LED
  • 抵抗器
  • パッシブブザー
  • タクトスイッチ
  • ジャンパーワイヤー

「Arduino Uno R3」の互換ボードであれば、「ELEGOO UNO R3」以外のボードでも、本記事と同様な手順で動作可能だと思われますが、動作検証はしていないので、その点はご了承ください。

以下のリンクのスターターキットを購入した場合、上記で述べた使用するものは、PC以外すべて入手することができます。

Arduinoをまだ購入していないという方やArduinoで動かすことができるたくさんのパーツが欲しいという方は、購入を検討してみてください。

回路について

本記事では、前回の記事で制作した、以下のような回路を使用して説明を行います。

右往左往ゲームの回路

ゲームの製作は行わず、外部割込みの使用方法だけを知りたいという方は、以下のような回路でも問題ありません。

外部割り込みの使用方法だけ知りたいときの回路図

外部割込みの説明

外部割り込みは、ピンの電圧がある条件を満たしたときに、現在の処理を中断して、指定した関数を実行するという機能です。

外部割り込みについての説明をした図

外部割り込みに使用できるピンはボードごとに決められており、「Arduino Uno R3」では、2番ピンと3番ピンが対応しています。

この外部割り込みを利用すると、スイッチが押された瞬間に、指定した関数を実行するといったことができるようになります。

外部割り込みの例

では、早速ですが、以下のスケッチを書き込んでください。

スケッチの書き込みが終わりましたら、次はシリアルモニタを開き、2番ピンに繋いだスイッチを押してみてください。

スケッチと回路が正しければ、スイッチが押し込まれた瞬間に「0」か「1」の数字が出力されるのが確認できます。

スイッチが押し込まれると0か1の数字が出力される

ここで、「0」か「1」の数字が出力されない場合は、スケッチと回路が誤っていないか確認をしてみてください。

スケッチの解説

本記事では、外部割り込みの部分についてのみ、説明したいと思います。

スイッチの使い方が分からないという方は、以下のリンク先の記事で説明していますので、是非、ご覧になってください。

では、スケッチの説明をしたいと思います。

まず、2行目の変数宣言を行う際に、「volatile」という新しい単語が出てきます。

「volatile」は、割り込み処理の中と外で共通の変数を使う場合に記述します。

「volatile」が何をしているかについては、次の節で説明したいと思います。

ひとまず、割り込み処理の中と外で共通の変数を使う場合に、必要なものと覚えておいてください。

次に、13行目の以下の文について説明します。

「attachInterrupt()」は外部割り込みの設定を行うための関数で、引数の構成は以下のようになっています。

attachInterrupt(割り込み番号,実行したい関数,割り込みの発生条件)

まず、上記の「割り込み番号」について説明したいと思います。

スケッチの13行目の文を見ると割り込み番号を指定をする部分に、「digitalPinToInterrupt()」という関数が使われています。

これは、ピン番号を割り込み番号に変更するための関数です。

実は割り込み番号と外部割り込みに利用するピンの番号は異なっており、「Arduino Uno R3」の場合、割り込み番号「0」が2番ピン、割り込み番号「1」が3番ピンに対応しています。

この番号の対応は、ボードの種類によって、異なるため、割り込み番号を直接記述する場合は、どのピンがどの割り込み番号に対応しているのかを確認する必要があります。

しかし、「digitalPinToInterrupt()」を利用すれば、ボードの種類に関係なく、ピン番号で指定することができるので、基本的には「digitalPinToInterrupt()」を使うことをおすすめします。

次に、「実行したい関数」についてです。

ここで指定する関数は、値を返さない「void型」で、さらに引数を持たないことが条件になります。

実行したい関数の条件

また、指定した関数の中に割り込み処理を利用するような関数を入れていた場合、うまく機能することができない場合があります。

例えば「delay()」は割り込み処理を利用した関数であるため、指定した関数の中に入れていても、うまく機能することができません。

また、今回「Serial.println()」を実行させるようにしていますが、「void loop()」内でシリアルモニタへ表示させる処理を行っていた場合、シリアルモニタへ表示させる処理が競合してしまい表示がうまくいかないことがあるので、「void loop()」と「割り込み処理」で同時に使用されないように注意が必要になります。

最後に「割り込みの発生条件」についてです。

「Arduino Uno R3」で利用できる発生条件は以下の4つです。

記述 条件
LOW ピンの電圧がLOWのとき
CHANGE ピンの電圧が変化したとき
RISING ピンの電圧がLOWからHIGHに変化したとき
FALLING ピンの電圧がHIGHからLOWに変化したとき

ここで指定した条件を、指定したピンが満たした場合、割り込み処理が実行されます。

上記の説明をまとめると、スケッチの13行目の文は、2番ピンの電圧が「HIGH」から「LOW」に変わった瞬間に、「Pushed()」という関数を実行するように設定しているということになります。

ここで、スケッチ全体の流れをまとめると以下のようになります。

外部割り込みを使ったスケッチの図

「void loop()」では、「Pushed_Flag」の「true」と「false」を1秒ごとに切り替える処理が行われています。

ここで、スイッチが押された瞬間、2番ピンの電圧が「HIGH」から「LOW」に変わるので、「Pushed_Flag」の値を表示させる割り込み処理が発生します。

よって、スイッチが押された瞬間に「0」か「1」が表示されるという処理になっていたということになります。

少し話は変わりますが、上記のスケッチの動作確認で時々、1回しかスイッチを押していないのに、処理が何度も実行されることがあります。

これは、スイッチが押し込まれたときの振動でスイッチのONとOFFが切り替わる、チャタリングという現象が発生していることが原因です。

チャタリングの説明

スイッチの判定を正確に行うには、別途対策を行う必要がありますが、今回の記事では、チャタリング対策なしで、割り込み処理の説明をさせていただきます。

volatileの説明

スケッチを「Arduino」に書き込む際には、スケッチを機械の言葉に翻訳する「コンパイル」というものが行われており、その「コンパイル」を行うプログラムのことを「コンパイラ」と呼びます。

「コンパイラ」は私たちが書いたスケッチを「コンパイル」する際に、無駄な処理を省いたり、順番を少し入れ替えたりして、より速く処理が行われるように最適化を行うことがあります。

最適化は、処理を速くしてくれるので、基本的には良いことなのですが、割り込み処理を利用する場合、最適化が悪影響を及ぼすことがあります。

先程のスケッチの2行目の「volatile」をなくした以下のようなスケッチを書き込んで、スイッチを何度か押してみてください。

先程のスケッチと異なり、スイッチを何度押しても、「1」という数字のみが出力されてしまいます。

スイッチを何度押しても1と出力される

上記のように、割り込みで実行する関数の中と外で共通の変数(上記の場合「Pushed_Flag」)を使用していた場合、最適化により、変数が思っていたように変化しておらず、こちらが意図していないような処理が行われることがあります。

そのような場合は、共通で使用する変数の型の前に「volatile」と記述することにより、解決することができます。

volatileを付けるべき変数の説明

「volatile」は型修飾子と呼ばれているものの1つです。

型修飾子は変数の扱い方を、「コンパイラ」に詳しく伝える役割を持っており、「volatile」は、変数の内容が複数の関数からアクセスされ、値が常に変化するような変数であるということを「コンパイラ」に伝えています。

すると、「コンパイラ」は、その変数に対しての最適化を抑制して、変化に応じた新しい値をそれぞれの関数で扱うことができるようにコンパイルを行います。

「volatile」を付けていなくても、こちらが意図したように割り込み処理を行うこともありますが、上記のように、コンパイルの最適化で、こちらが意図していないような割り込み処理になってしまうこともあるため、割り込みで実行する関数の中と外で共通の変数を使用する場合は、「volatile」を付けることをおすすめします。

volatileの注意点

「volatile」を付けた変数を利用すれば、変化に応じた新しい値を扱うことができるという説明をしましたが、その場合、変化の途中で割り込み処理が起こり、値がおかしくなる可能性があることに注意する必要があります。

以下のスケッチを書き込んで、スイッチを何度もすばやく連打してみてください。

すると、時々「4649」という数字が表示されるのが確認できると思います。

(ごく稀にですが、「4848」と表示される場合もあります。)

スイッチを何度か押すと、時々4649と出力される

スケッチの解説

スケッチ全体の流れは以下のようになっています。

4649と出力されるスケッチの図

ここで、シリアルモニタに数字を表示させる条件を見ると、「value」の値が「1833」でも「4848」でもないときとなっています。

「value」にはループ処理内で「1833」、割り込み処理内で「4848」を入れていますが、それ以外の数字を入れるような処理は行っていません。

よって、シリアルモニタに表示させる処理は行われないと考えられますが、実際には「4649」という数字が出力されてしまっています。

その理由について、順を追って説明したいと思います。

まず、「1833」、「4848」、「4649」をそれぞれ2進数で表すと以下のようになります。

4848 1001011110000 1833 11100101001 4649 1001000101001

この2進数は、数値を「0」と「1」の2種類の数字で表したものです。

私たちが普段使うのは10進数と呼ばれるもので、「0」から「9」の10種類の数字を使って、数値を表し、「9」の次は桁が上がって「10」となります。

2進数の場合は、「1」の次に桁が上がり、「10」という表記になります。

ここで、10進数と2進数の関係を図にまとめると以下のようになります。

2進数と10進数の対応

コンピュータは処理を行う際、2進数を使っていて、1桁分のデータを1ビット([bit])と呼びます。

ビットについての説明

1回の処理で扱えるビット数はマイコンごとに決まっており、「Arduino Uno R3」が1回の処理で扱えるのは8ビットとなっています。

そして、「Arduino Uno R3」の場合、「int型」のデータはで16ビットとして扱われます。

int型のビット数

つまり、「int型」のデータは2回に分けて、読み書きがおこなわれているということになります。

int型の読み書きについての説明

ここで、運悪く「value=1833」の途中で、割り込み処理が発生した場合、以下のような流れで処理が行われます。

4649と表示される仕組み

よって、「4649」という数字が出力されることがあるということになります。

ちなみに、ごく稀に「4848」が表示されるのは、「value=4649」で条件が満たされた後に、割り込み処理で「value=4848」になる可能性があるからです。

割り込み禁止の使い方

上記のように、割り込み処理で8ビット以上の変数(「int型」や「float型」など)を共有して扱う場合は、データを入れる前と後で、割り込み処理の禁止と許可をすると、安全にデータのやり取りを行うことができます。

また、「if文」の条件が正しいか判断しているときや、条件が満たされた後に、割り込みが発生する可能性はあるので必要に応じて割り込み禁止と許可をします。

以下のスケッチを書き込んでスイッチを何度か押してみてください。

今度は、スイッチを押しても何も表示されないのが確認できると思います。

スケッチの解説

今回追加したのは、19、39行目の「noInterrupts()」と25、31行目の「interrupts()」という関数です。

「noInterrupts()」を実行すると、すべての割り込み処理が禁止されます。

そして、「interrupts()」を実行すると、割り込み処理が再び行えるようになります。

上記のように、割り込み処理で8ビット以上の変数を扱う場合や、割り込みが入っては困るような大事な処理がある場合は、「noInterrupts()」と「interrupts()」を使うことをおすすめします。

上記のif文の場合、条件が正しいか判断しているときに値が変化したとしても、条件が満たされることは無いと思われますが、if文の「1833」と「4848」を入れ替えた場合は、条件が満たされる場合があるので、割り込み禁止と許可を追加しています。

割り込み禁止中の判定

割り込み禁止中も、外部割り込みの判定は行われているので、注意が必要です。

以下のスケッチを書き込んでください。

シリアルモニタの左側に「Interrupt: 」と表示されている時に、スイッチを押すと、右側の数字が変化するのが確認できます。

Interrupt:と表示されているときにスイッチを押すと数字が変化する

一方、シリアルモニタの左側に「No_Interrupt: 」と表示されている時に、スイッチを押しても、右側の数字は変化しないことが確認できます。

No_Interrupt:と表示されているときにスイッチを押しても数字は変化しない

スケッチの解説

上記のスケッチは、左側に割り込みが許可されているかを、右側にbool型の変数の値を表示させ続ける処理を行っており、スイッチを押して割り込み処理が行われると、変数の値が切り替わるという仕組みになっています。

スケッチ全体の流れは以下の図のようになります。

割り込み禁止中の判定について説明しているスケッチの図

ここで、割り込みを禁止している際に、スイッチを押したときの処理に注目してください。

割り込み禁止の状態で、スイッチを押しても、数字の変化はありませんが、割り込みが許可された瞬間に遅れて数字が変化することが確認できます。

割り込み禁止時にスイッチを押しても数値の変化はないが、割り込みが許可されたとき遅れて数字が変化する

これは、割り込みを禁止にしていても、外部割り込みの判定が行われていて、割り込みが許可された瞬間に処理が実行されることを意味しています。

割り込み禁止中の判定についての説明

上記のように、割り込み禁止中に外部割り込みの条件を満たした場合、回数はカウントされませんが、割り込みを許可したタイミングで、処理が実行されるので注意が必要となります。

イメージとしては以下のような感じです。

割り込み禁止のイメージ

外部割り込み禁止の使い方

「noInterrupts()」で、割り込み禁止を行った場合、すべての割り込み処理が行えなくなります。

以下のスケッチを書き込んでください。

上記のスケッチは、シリアルモニタへの出力の後に、「delay()」を使っていますが、割り込み禁止中の「delay()」は機能しておらず、「No_Interrupt: ~」の部分だけ、一気に出力されるのが確認できます。

No_Interrupt:は一気に出力される

外部割込みの処理は禁止にしたいけれど、他の割り込み処理は許可したいという場合は、「noInterrupts()」の代わりに「detachInterrupt()」という関数を利用します。

以下のスケッチを書き込んでください。

先ほどのスケッチと異なり、「delay()」が正常に処理されているのが確認できると思います。

スケッチの解説

「noInterrupts()」の代わりに以下の文により、割り込みを禁止にしています。

上記の「detachInterrupt()」は指定した割り込み番号に、対応する割り込み処理を禁止して、実行する関数の登録を解除するという文です。

指定するのは割り込み番号であるため、「attachInterrupt()」と同様に「digitalPinToInterrupt()」を使って、対応するピンの指定を行っています。

割り込みを許可するときは、実行する関数の登録を解除されてしまっているため、「attachInterrupt()」を使い、外部割り込みを再設定して割り込みを行えるようにします。

「noInterrupts()」ときと同様、割り込み禁止中も、外部割り込みの判定は行われているので注意が必要です。

外部割り込みについての説明は以上となります。

LEDを使ったゲームを作る(後編)

前回の記事に引き続き、LEDを使ったゲームの作り方を説明していきたいと思います。

ステージを作る

前回の記事では、8つのパターンでLEDが右に向かって点灯するところまでを作成しました。

前回のスケッチ

今回はこれをゲームらしくするために、まず、ステージとステージクリアについての処理を追加したいと思います。

ステージを追加する

以下のスケッチを書き込んでください。

黄色でマークされている部分が前回のスケッチから追加した部分です。

上記のスケッチを書き込むと、1回目のパターンでLEDが点灯した後、クリアしたことを知らせる音が鳴るのが確認できます。

また、2回目のパターンでLEDが点灯した後、失敗を知らせる音が鳴り、その後は同じパターンを繰り返し続けるのが確認できます。

スケッチの解説

今回のスケッチでは3つの変数を追加し、ステージとステージクリアの管理を行うようにしています。

まずは、「Stage」という変数について説明したいと思います。

この変数は、現在どのステージなのかを確認するために使用します。

「void loop()」ではこの変数と「if文」を組み合わせることにより、ステージごとに異なるパターンでLEDを点灯させています。

ステージに応じたパターンでLEDを点灯させる仕組み

続いて、「Clear_Flag」という変数について説明します。

この「Clear_Flag」はステージをクリアにするかどうかを判断するために使用しており、クリアにしたいときは「true」それ以外のときは「false」になるようにしています。

新しく作成した「Judge()」という関数では、「Clear_Flag」が「true」の場合、クリアを知らせる音を鳴らした後「Stage」の値を「1」増やし、最後に「Clear_Flag」を「false」にします。

一方、「Clear_Flag」が「true」以外の場合、失敗を知らせる音を鳴らし、「Stage」の値は変更しません。

そうすることにより、ステージをクリアすると次のステージに進むような処理になります。

クリアの判別をする関数の図

今回は、スイッチの判定を行う処理を追加していないため、30行目のように、「Clear_Flag=true」と記述し、強制的にステージをクリアさせています。

最後に、「Last_Stage」という関数についてです。

この変数は、現在のステージが最後のステージであるかを判別するために使用しており、ステージの数を最初に入れています。

なぜ、最後のステージであるかを判別しているかというと、最後のステージをクリアした場合、次のステージがないため、最初のステージに戻す必要があるからです。

ステージ9は用意してないので最初に戻す

よって、111~113行目で、最後のステージをクリアした場合、最初のステージに戻すような処理を行っています。

ステージを最初に戻すスケッチの図

今回のスケッチの処理の流れをざっくりまとめると以下のようになります。

ステージとステージクリアを追加したスケッチの図

スイッチの判定を追加する

上記のスケッチでは、1番目のパターンでLEDを点灯させた後、「Clear_Flag」を「true」にするような文を書くことにより、次のステージへ進ませました。

今度は、右側のスイッチが押されることで、「Clear_Flag」を「true」にするようなスケッチに変更したいと思います。

以下のスケッチを書き込んでください。

黄色でマークされた部分が変更または追加した部分です。

先程のスケッチの30行目で記述していた「Clear_Flag=true;」は削除してください。

上記のスケッチを書き込むとスイッチが押されたときだけ、クリアした音が鳴り、次のステージへ進むのが確認できます。

先程のスケッチと異なり、最後のステージまで進めるようになったので、最後のステージをクリアしたときに、最初のステージに戻ることを確認してみてください。

スケッチの解説

今回追加したのは、2番ピンに対しての外部割り込みの処理です。

138行目から142行目で「Pushed_R」という関数を作成しており、「void setup()」の中で、2番ピンに対してプルアップ付きの入力ピンの設定と「Pushed_R」を実行する外部割り込みの設定を行っています。

「Pushed_R」の内容は、「Clear_Flag」を「true」にするという単純な処理のみです。

「Clear_Flag」は割り込みで実行する関数の中と外で、共通の変数として扱うため「volatile」を付けるように変更を加えています。

また、割り込み処理によって、「tone()関数」の波形が乱されるのを防ぐために、「LED_HIGH()」と「Judge()」が実行されている間の外部割り込みを禁止にしています。

今回のスケッチの処理の流れをざっくりまとめると以下のようになります。

外部割り込みを追加したスケッチの図

判定を細かく設定する

上記のスケッチでは、スイッチを適当なタイミングで押しても、クリアになり、次のステージへ簡単に進ませることができました。

これでは、ゲームとして面白くないため、次は、最後のLEDが点灯したタイミングでスイッチが押せたときだけ、次のステージへ進めるように変更を加えたいと思います。

また、早めにスイッチを押して、失敗したときに、最後のLEDが点灯するまで、待つのは退屈ですし、どこでミスをしたのか分かりづらいので、スイッチが押されたら、判定まで処理をスキップするような変更も加えたいと思います。

以下のスケッチを書き込んでください。

黄色でマークされた部分が追加した部分です。

上記のスケッチを書き込むと、最後のLEDが点灯するタイミングでスイッチを押さないと次のステージへ進めなくなるのが確認できます。

また、スイッチが押されると、LEDの点灯はスキップされることが確認できます。

スケッチの解説

今回のスケッチでは、2つの「bool型」の変数を追加しています。

1つは「Pushed_Flag」という変数で、177行目の文により、スイッチが押されると「true」になり、169行目の文により、ステージクリアの判定が終わると「false」に戻ります。

この変数と「if文」を組み合わせることにより、すでにスイッチが押されている場合、LEDの点灯処理や外部割り込みの処理をスキップさせています。

ボタンが押されたら処理をスキップするようにしたスケッチの図

もう1つは「Last_LED_Flag」という変数です。

この変数は、114~116行目の文により、最後のLEDが点灯する直前に「true」になり、122~124行目の文により、LEDが点灯し終わった後「false」になります。

つまり、「true」になっている期間は、最後のLEDが点灯している間だけとなります。

Last_LED_Flagがtrueの期間

ここで、「Pushed_R」の中身を見ると、if文により、「Last_LED_Flag」が「true」になっているときだけ「Clear_Flag」を「true」にするような処理を行っています。

よって、最後のLEDが点灯している間にスイッチを押したときだけ、ステージクリアになるということになります。

左に向かって点灯するパターンを追加する

次は、すでに記述した関数をコピーしながら、左に向かって点灯するパターンを追加したいと思います。

以下のスケッチを書き込んでください。

黄色でマークされた部分が変更または追加した部分です。

上記のスケッチを書き込むと、左に向かって点灯するパターンが追加されたことが確認できます。

スケッチの解説

今回は左側のスイッチも利用するため、3番ピンに対する割り込みの設定や割り込み禁止の処理を追加しています。

設定のやり方については、2番ピンと同じですが、実行する関数は新たに作成する「Pushed_L()」となっています。

attachInterrupt(digitalPinToInterrupt(3),Pushed_L,FALLING)

左に向かってLEDを点灯させる関数「LED_Beat_L()」や左のスイッチが押されたときに実行する関数「Pushed_L()」は、「LED_Beat_R()」と「Pushed_R()」をコピーしてから、中身を少し変更して作成しています。

「LED_Beat_L()」については、反対方向にLED点灯させていくため、変数「j」が関係している部分のほとんどが変更になるので注意してください。

「void loop()」では偶数のステージの「LED_Beat_R()」を「LED_Beat_L()」に変更して、ステージをクリアするたびに方向が変わるようにしています。

LEDが点灯する向きを交互にする

また、右に向かって点灯しているのか、それとも、左に向かって点灯しているかを判断するために、「Right_Flag」、「Left_Flag」という変数を追加しています。

「Right_Flag」は「LED_Beat_R()」が実行されている間「true」になるようにして、「Left_Flag」は「LED_Beat_L()」が実行されている間「true」になるようにしています。

さらに、「Pushed_R()」は「Right_Flag」が「true」のときのみ中身を実行するようにして、「Pushed_L()」は「Left_Flag」が「true」のときのみ中身を実行するようにしています。

これにより、点灯している方向とは逆のスイッチを押しても反応しないような仕組みとなっています。

ゲームをクリアした際の演出を追加する

上記のスケッチでは、最後のステージをクリアしても、最初のステージに戻るだけで、少し寂しいです。

よって、最後のステージをクリアしたときに、ちょっとした演出を行うように、変更を加えたいと思います。

以下のスケッチを書き込んでください。

黄色でマークされた部分が追加した部分です。

上記のスケッチを書き込んで、最後のステージをクリアするとゲームをクリアしたことを伝える演出が行われることが確認できます。

スケッチの解説

「Game_Clear()」という関数にゲームクリアを知らせる演出についての処理をまとめておき、250行目で関数を呼び出しています。

これにより、最後のステージがクリアされたときに、演出を行ってから、最初のステージに戻る処理が行われています。

ゲームクリアを知らせる演出は、ブザーでそれらしい音を鳴らした後に、偶数番のLEDと奇数番のLEDを交互に点滅させたものとなっています。

余裕のある方は、自分なりの演出を考えてみてください。

ゲームクリアの演出をすぐに確認したい場合は、「Stage」の変数宣言を行っている5行目の文の「1」という数字を、「Last_Stage」の値(上記のスケッチの場合「8」)と同じものに変更すると、最後の1回をクリアするだけで、演出を確認できるようになります。

終わりに

2回の記事に分けて説明した「右往左往ゲーム」の製作はいかがだったでしょうか。

楽しく関数や割り込みの使い方を学んで貰えていれば幸いです。

今回制作したゲームで何か物足りないと感じた方は、スケッチに変更を加えて、色々と改造をしてみてください。

そうすることで、もっと楽しく学ぶことができるはずです。

次回の記事では、タイマー割り込みについて説明した後、今回作ったゲームの回路を再利用した「8分タイマー」の制作について説明したいと思います。

本記事はここまでです。ご清覧ありがとうございました。

広告

楽天モーションウィジェットとGoogleアドセンス広告です。

気になる商品がございましたら、チェックをしてみてください。