Pythonロボティクスコース レッスン 34
テーマ.9-1 割込み機能を利用した並行処理
便利な割込み処理について学習しよう!
チャプター1
テーマ9を通して学ぶこと
コンピュータが複数のプログラムや1つのプログラム内で複数の命令の流れを並行して処理することを「並行処理」といいます。並行処理の技術があることで、例えばパソコンは、同時に複数のソフトウェアを立ち上げて実行することができます。実際に、レッスンでもWebブラウザとプログラムエディタ(Mu)の2つのソフトウェアを同時に立ち上げています。そこで、テーマ9では「割込み処理」や「マルチスレッド」などの並行処理を実現するための様々な技術について学習します。
チャプター2
このレッスンで学ぶこと
このレッスンでは、「割込み処理」について新たに学習します。割込みは、他の命令を処理している途中で、新たに緊急性の高い命令が与えられた場合、それを優先して処理する仕組みです。割込み処理を理解するためには、まずコンピュータの仕組みから知る必要があります。そのため、今回は少し説明が長くなりますが、そこから解説をしていきます。しっかりと順番に読み進めていき、最後の課題で割込み処理を応用したゲームの制作に取り組みましょう。
チャプター3
コンピュータの仕組み
割込み処理を理解するためには、Pythonの文法だけでなく、コンピュータの「ハードウェア」の知識も身に着けておかなければいけません。そこで、このチャプターでは、Studuino:bitのような小型のコンピュータに着目して、その仕組みを解説していきます。
3. 1 ロボットの制御に使われる小型のコンピュータ
家電製品やロボットを制御する目的で使われる小型のコンピュータは、「マイクロコンピュータ」と呼ばれています。(※または、省略されて「マイコン」とも呼ばれます。)様々なソフトウェアがインストールされ、多用途なパソコンとは違い、マイコンは用途が限られています。そのため、処理能力の高さよりも小型でかつ低消費電力であることが求められます。
また、1個のマイコンのチップには、コンピュータの中心となる計算処理や周辺装置(※)の制御処理を行う「CPU(Central Processing Unit/中央処理装置)」の他に、プログラムやデータを記録しておくための「メモリ」や、外部の機器からの電気的な入力を受けたり、反対に外部の機器への出力を行ったりするための「汎用入出力端子(GPIO:General Purpose Input/Output)」など、いくつかの装置(ハードウェア)が組み込まれています。
※ 周辺装置にはモーターやセンサーだけでなく、キーボードやモニター、ハードディスクドライブ(HDD)やSDカードなどが含まれます。
そして、下の図はStuduino:bitに搭載されている「ESP32」という名前のマイコンです。
このマイコンの中には次の部品が組み込まれています。
【 ESP32のハードウェア構成 】
部品名 | 役割 |
---|---|
CPU | コンピュータの脳ともいえる演算処理装置 |
内部メモリ(記憶装置) | データやプログラムを記憶するための装置 |
タイマ | 時間の計測や一定時間が経過したことを通知する機能をもった電子回路 |
ウォッチドッグタイマ | ウォッチドッグとは「番犬」を表す言葉で、コンピュータが正常に動作しているかどうかを定期的に監視し、異常があればリセットをかけるための特別なタイマ |
クロック発振回路 | クロックと呼ばれる信号を作成するための電子回路。クロックは一定の周波数で与えられる信号で、コンピュータはクロックをペースメーカーとしてタイミングを取りながら、あらゆる制御を行っています。 |
A-D変換器 | センサなどから得られるアナログ(Analog)な信号をコンピュータが処理できるデジタル(Digital)な信号へ変換する電子回路 |
D-A変換器 | コンピュータが出力するデジタル(Digital)な信号をモーターなどのアクチュエータを制御するためのアナログ(Analog)な信号へ変換する電子回路 |
汎用入出力端子(GPIO) | センサなど外部の機器からの電気的な信号の入力やモーターなどのアクチュエータを制御するための信号の出力を行うための端子 |
通信用インターフェース | USBケーブルを介してパソコンなどと通信を行ったり、SDカードへのデータの読み書きを行ったりするための制御を可能にする装置 |
MicroPythonでは、これらの部品を直接制御するためのモジュールが提供されています。次のチャプターでは、その一例を見ていきましょう。
チャプター4
MicroPython独自の機能
Pythonの実行環境は、ある程度のデータ容量をもち、高速な処理が行えるパソコンなどの汎用的なコンピュータ上で動作することを前提として作成されています。一方で、みなさんがレッスンで使用しているのは、マイコンで動作することを前提としてPythonを小型化した「MicroPython」の実行環境です。MicroPythonは小型軽量化されただけでなく、前のチャプターで紹介したマイコンの部品を制御するための機能を提供しています。これらの機能は、主にmachine
モジュールにまとめられています。
MicroPythonにはmachine
モジュール以外にも、ネットワーク通信を行うnetwork
モジュールやBluetooth通信を行うbluetooth
モジュールなどが、オリジナルのPythonにはない固有の機能として提供されています。
4. 1 machineモジュールとは
machine
モジュールには、上で紹介した部品の制御用に複数の関数とクラスが定義されています。すべての関数やクラスについての詳細な説明はここでは行いません。興味のある人は、以下のMicroPython公式ドキュメントで確認してください。
※ 公式ドキュメントの解説には専門用語が多数含まれています。理解するには一定以上のハードウェアやソフトウェアに関する知識が必要となります。これらをすべて理解していなくてもレッスンを進めていくことはできますので、難しいと感じる場合は飛ばしてください。
【 machineモジュールで定義されている関数の一例 】
関数名 | 処理内容 |
---|---|
reset() | 外付けのリセットボタンを押すのと同じように、マイコンの状態をリセットします。 |
disable_irq() | 割込みの要求(Interrupt Request)を受付けない設定にします。 |
enable_irq() | 割込みの要求を受け付ける設定にします。 |
freq() | CPUのクロック周波数を返します。 |
※「割込み」については、次のチャプターで詳しく説明します。
※ 「disable」は無効にする、「enable」は有効にするという意味がそれぞれあります。
※ 英語で周波数のことを「frequency」といいます。
【 machineモジュールで定義されているクラスの一例 】
クラス名 | 役割 |
---|---|
Pin | 汎用入出力端子(Input/Output pin)の制御を行います。 |
ADC | センサーなどから得られたアナログな信号をコンピュータが処理できるデジタルな信号へ変換します。(Analog-to-digital converter) |
RTC | 日時の経過を追いかけるリアルタイムクロック(Real Time Clock)機能を提供します。 |
Timer | タイマを利用して、指定した周期(1秒ごとや10秒ごとなど)で特定の処理を実行する機能を提供します。 |
※ 英語で変換器のことを「converter」といいます。
このレッスンで学習する「割込み処理」では、この中からPin
クラスとTimer
クラスを使用します。では、次のチャプターで実際にそれぞれのクラスを使った割込み処理のプログラムを書いてみましょう。
チャプター5
割込み処理
ある処理の実行途中に、別の処理を受け付けることを「割込み処理」といいます。このとき元の処理は一時的に中断され、割込みのあった処理を終えたあとで、実行が再開されます。
5. 1 割込み処理はなぜ必要なのか?
例えば、飲食店などで食器を洗って乾燥させ、棚に片付けるまでの仕事を考えたとき、各工程に人やロボット、専用の機械などがついていれば、同時並行で作業を進めることができます。このように作業を分担することで、作業全体を早く終わらせることができます。
処理能力の高いコンピュータでは、複数の演算処理装置が搭載されているため、これと同じように作業を分担でき、複数の処理を同時並行で行うことができます。一方で、マイコンのように処理能力がそれほど高くないコンピュータではこれができません。つまり、上の例でいうと作業者が1人(または機械やロボットが1台)しかいないため、ひとつの作業を終えてから次の作業を行うことになり、全体で見るとそれだけ多くの時間が掛かかってしまいます。
では、もしこの例で食器を洗っている途中に、急ぎ皿を使う必要が出てきた場合はどのように対処すべきでしょうか。恐らく、食器を洗うのを一時中断して、先に乾燥させて片付けるという行動を取るでしょう。
マイコンの割込み処理は、まさにこれと同じことを行います。このように、コンピュータ上で複数の処理を並行して切り替えながら行うことを「マルチタスク」といいます。割込み処理は、マイコンがマルチタスクを行うための手段として広く用いられています。
※ 割込み処理はマイコンだけでなく、パソコンなど汎用的なコンピュータ上でも行われています。
5. 2 割り込み処理の種類
コンピュータにおける割込み処理は大きく分けると、「ソフトウェア割込み」と「ハードウェア割込み」に分かれます。
- ソフトウェア割込み(内部割込み)
CPUの内部に要因がある割込み処理。プログラムの実行中に発生するエラーによって、CPUからの命令として要求されるものなどがあります。 - ハードウェア割込み(外部割込み)
CPUの外部に要因がある割込み処理。例えば、キーボードが押下されたり、周辺機器(スキャナーやネットワーク機器、カメラなど)からのデータ入力があった場合に、割込み要求用に用意された入出力端子の電圧を変化させることで要求が行われます。
一般的に、「割込み」と言われるときは「ハードウェア割込み」を指すことが多いようです。ここからは、例としても分かりやすい、ハードウェア割込みを実践していきましょう。
チャプター6
ハードウェア割込みの実践
ここからは、「タイマ割込み」と「外部端子割込み」の2種類のハードウェアを使った割込み処理について、実際にプログラムを書きながら学習します。
6. 1 タイマ割込み
マイコンの「タイマ」には、時間経過を計測したり、一定の時間が経過したことを通知したりする機能があります。このうち、通知機能を利用して指定した時間が経過したときや、一定時間おきに割込みを要求することを「タイマ割込み」といいます。
MicroPythonでは、machine
モジュール内のTimer
クラスを使用してタイマ割込みを設定できます。例として、「1秒おきにブザー音(C4)の再生と停止を切り替える処理」をタイマ割込みを利用して書いてみましょう。
■ タイマ割込みのプログラムの作成
タイマ割込みは、Timer
クラスのインスタンスを作成して、init()
メソッドを使って設定します。このinit()
メソッドには次の3つの引数を渡します。
Timer.init(*, mode=Timer.PERIODIC, period=-1, callback=None)
※「*
」以降の引数は、キーワード引数として渡す必要があります。
引数名 | 内容 |
---|---|
mode | period に指定した時間が経過したときに一回だけ割込みを行う場合はTimer.ONE_SHOT を指定し、period の時間が経過するたびに割込みを行う場合はTimer.PERIODIC を指定します。 |
preriod | 割込みを行うまでに経過を待つ時間(単位:ミリ秒)を指定します。 |
callback | 割込みで行う処理をまとめた関数の名前を指定します。 |
※英語で「期間」のことを「period」といいます。
では、順番にコードを書いていきましょう。まずは、先頭で使用するクラスとオブジェクトをインポートします。
from machine import Timer # タイマ割込み用のクラス
from pystubit.board import buzzer # ブザー制御用のオブジェクト
次に、割込ませる処理を関数として定義します。今回割込ませる処理は「ブザー音(C4)の再生と停止を切り替える処理」です。割込みの関数も、グローバル変数を共有しているため、この関数が切り替えスイッチとして機能するように、グローバル変数に再生と停止の状態を記録するコードを書きましょう。
追加【4行目、6行目~13行目】
from machine import Timer
from pystubit.board import buzzer
is_playing = False # ブザー音の再生と停止状態を表すブール値
def toggle_buzzer(t): # 割込みで呼び出される関数には、呼び出し元のタイマオブジェクトが引数として渡されます
global is_playing # 内容を更新するため、グローバル宣言が必要
if is_playing: # 現在再生中なら停止
buzzer.off()
else: # 現在停止中なら再生
buzzer.on("C4")
is_playing = not is_playing # not演算子でTrueとFalseを切り替える
※ 切り替えスイッチのように、2つの状態を交互に切り替えるための機構を「toggle(トグル)」といいます。
ここで注意したいのは、割込みで呼び出す関数に引数が1つ必要という点です。この引数は、タイマオブジェクトがinit()
メソッドから呼び出すときに、タイマオブジェクト自身を渡すために使います。
では、最後にinit()
メソッドを実行して、タイマ割込みを設定しましょう。
【 サンプルコード 6-1-1 】
追加【16行目、18行目】
from machine import Timer
from pystubit.board import buzzer
is_playing = False
def toggle_buzzer(t):
global is_playing
if is_playing:
buzzer.off()
else:
buzzer.on("C4")
is_playing = not is_playing
mytimer = Timer(1) # 整数値でIDを指定してオブジェクトを作成。IDは重ならなければ、自由に指定できる。
# 今回は定期的に割込みを要求するため、引数modeに「Timer.PERIODIC」を指定する
mytimer.init(mode=Timer.PERIODIC, period=1000, callback=toggle_buzzer)
では、完成したプログラムを実行して動作を確認しましょう。
■ 割込み処理を行う上での注意点
割込みで行われる処理はできるだけ最小限の命令に留めておかなければいけません。もし割込みで多くの命令を実行することになると、他の処理の実行に大幅な遅れが出る恐れがあります。
例えば、上の【 サンプルコード 6-1-1 】では、関数toggle_buzzer()
で行うことを最小限にしていたため、以下のようにLEDディスプレイに繰り返しメッセージを表示する処理を並行して行う場合でも、ほとんど遅れが気になりません。
【 サンプルコード 6-1-2 】
追加・変更【2行目、18行目、19行目】
from machine import Timer
from pystubit.board import buzzer, display # LEDディスプレイ制御用のオブジェクトを追加
is_playing = False
def toggle_buzzer(t):
global is_playing
if is_playing:
buzzer.off()
else:
buzzer.on("C4")
is_playing = not is_playing
mytimer = Timer(1)
mytimer.init(mode=Timer.PERIODIC, period=1000, callback=toggle_buzzer)
while True: # 繰り返しメッセージをスクロール表示する処理
display.scroll("Hello Python!")
このプログラムを実行すると、スクロールを特に妨げることなく、ブザー音の再生・停止が制御できていることが分かります。
しかし、もしtime
モジュールのsleep_ms()
関数を使用して同じような命令を書いた場合、ブザー音が鳴っている間は、LEDディスプレイへのスクロール表示が止まってしまいます。実際に以下のサンプルコードを実行して、違いを確認してみましょう。
【 サンプルコード 6-1-3 】
追加・変更【1行目、6行目~10行目、14行目】
import time # timeモジュールのインポート
from machine import Timer
from pystubit.board import buzzer, display
# timeモジュールのsleep_ms()関数を使用した1秒間ブザーから音を鳴らす処理
def toggle_buzzer(t):
buzzer.on("C4")
time.sleep_ms(1000)
buzzer.off()
mytimer = Timer(1)
# 割込みの間隔を、2000ミリ秒に変更(period=2000)
mytimer.init(mode=Timer.PERIODIC, period=2000, callback=toggle_buzzer)
while True:
display.scroll("Hello Python!")
このような結果となってしまうのは、割込み処理の実行中はメインの処理(スクロール表示)が中断されており、time
モジュールのsleep_ms()
関数で停止している時間だけメイン処理の実行が遅れてしまうためです。
そのため、割込み処理を行う場合は、time
モジュールのsleep_ms()
関数のような遅延処理の使用を極力避けて、別の方法を選択するようにしてください。
6. 2 外部端子割込み
ほとんどのマイコンには、外部のセンサからの電気的な信号(電圧の変化)の入力やモーターなどのアクチュエーターを制御するための信号の出力を行うために、「汎用入出力端子/GPIO(General Purpose Input/Output)」が備わっています。
下の図は、Studuino:bitで使用しているマイコン(ESP32)の各汎用入出力端子の位置を示しています。この端子を通じて、LEDディスプレイの制御や、ロボット拡張ユニットにつないだセンサやモーターの制御を行っています。
Studuino:bitでは、各入出力端子を次の用途で利用しています。
【 ESP32の汎用入出力端子とStuduino:bitでの用途の比較表 】
ESP32の汎用入出力端子名 | Studuino:bitでの用途 |
---|---|
GPIO1 | USBケーブルなどを介したシリアル通信用(TXD:送信出力) |
GPIO2 | LEDディスプレイの電源のON/OFF制御用 |
GPIO3 | USBケーブルなどを介したシリアル通信用(RXD:受信入力) |
GPIO4 | LEDディスプレイの点灯パターンの制御用 |
GPIO5 | モーションセンサ(加速度、ジャイロ、磁気) |
GPIO6 | 内部のフラッシュメモリとの通信(読み書き)で使用 |
GPIO7 | 内部のフラッシュメモリとの通信(読み書き)で使用 |
GPIO8 | 内部のフラッシュメモリとの通信(読み書き)で使用 |
GPIO9 | 内部のフラッシュメモリとの通信(読み書き)で使用 |
GPIO10 | 内部のフラッシュメモリとの通信(読み書き)で使用 |
GPIO11 | 内部のフラッシュメモリとの通信(読み書き)で使用 |
GPIO12 | Wi-Fi、Bluetooth通信確認用LED(ボタンB下のLED) |
GPIO13 | ロボット拡張ユニットの「P16」 |
GPIO14 | 電源ランプ用LED(ボタンA下のLED) |
GPIO15 | ボタンA |
GPIO16 | PSRAMの読み書き用 |
GPIO17 | PSRAMの読み書き用 |
GPIO18 | ロボット拡張ユニットの「P13」 |
GPIO19 | ロボット拡張ユニットの「P14」 |
GPIO21 | I2C通信用(SDA) |
GPIO22 | I2C通信用(SCL) |
GPIO23 | ロボット拡張ユニットの「P15」 |
GPIO25 | ブザー |
GPIO27 | ボタンB |
GPIO32 | ロボット拡張ユニットの「P0」 |
GPIO33 | ロボット拡張ユニットの「P1」 |
GPIO36 | ロボット拡張ユニットの「P2」 |
GPIO39 | ロボット拡張ユニットの「P3」 |
「外部端子割込み」は、これら汎用入出力端子で電圧の変化を信号として検出したときに行う割込み処理です。電圧の変化は、センサからの入力が行われたときや、LEDやブザー、モーターなどを制御するときに起こります。
例えば、GPIO15に接続されているボタンAや、GPIO27に接続されているボタンBを押したりはなしたりすると、汎用入出力端子では次のような電圧の変化が起きます。
では練習として、ボタンAが接続されているGPIO15の端子に電圧の変化が起きると、特定の処理を割込むプログラムを作成してみましょう。
■ 外部端子割込みを行うプログラムの作成
汎用入出力端子の制御に関する機能は、machine
モジュールのPin
クラスにまとめられています。このPin
クラスには、端子の電圧変化によって行われる割込み処理を設定するためのirq()
メソッドが用意されています。
※irq
は「Interrupt Request」を省略した表記で、「Interrupt」には割込みという意味があります。
irq()
メソッドを呼び出すときは、次の2つの引数を指定する必要があります。
Pin.irq(handler=None, trigger=(Pin.IRQ_FALLING | Pin.IRQ_RISING))
引数名 | 内容 |
---|---|
handler | trigger に指定した電圧の変化が起きたときに、割込みを行う処理を関数名で指定します。 |
trigger | どのような変化が起きたときに割込みを行うのかを指定します。 |
引数trigger
に指定できる変化として、次の2つがPin
クラスに定数として定義されています。
Pin.IRQ_FALLING
電圧が下がる変化(立ち下がり)Pin.IRQ_RISING
電圧が上がる変化(立ち上がり)
また、2つの変化を両方とも引数trigger
に指定したいときは「|
」のビット演算子を使ってつなぎます。ビット演算については、テーマ10で詳しく説明します。
# 電圧が下がったときと、電圧が上がったときの両方で割込みを設定するとき trigger=(Pin.IRQ_FALLING | Pin.IRQ_RISING)
では、コードを書いていきましょう。これから作成するプログラムは、「ボタンAを押すと割込みが入り、ブザー音(C4)の再生/停止が切り替わる」という動作を行います。
まずは先頭で必要なクラスとオブジェクトをインポートしましょう。
from machine import Pin # 汎用入出力端子の制御用クラス
from pystubit.board import buzzer # ブザー制御用のオブジェクト
ブザー音の再生/停止を切り替える割込みの処理は、タイマ割込みの学習で書いた【 サンプルコード 6-1-1 】と同じ関数を使います。ただし、こちらは外部端子割込みで呼び出されるため、Timer
クラスのオブジェクトではなく、呼び出し元のPin
クラスのオブジェクトを引数として1つ受け取ります。以下のようにコードを追加しましょう。
追加【4行目~13行目】
from machine import Pin
from pystubit.board import buzzer
is_playing = False # 以下の関数は【 サンプルコード 6-1-1 】とほぼ同じ
def toggle_buzzer(pin): # 引数として呼び出し元の`Pin`クラスのオブジェクトを受け取る
global is_playing
if is_playing:
buzzer.off()
else:
buzzer.on("C4")
is_playing = not is_playing
次に、外部端子割込み処理を設定するコードを書いていきます。
まずは、ボタンAを接続している汎用入出力端子を指定して、Pin
クラスのインスタンスを作成します。Pin
クラスのコンストラクタ(__init__()
メソッド)には、2つの引数を渡します。1つめが、使用する汎用入出力端子の番号で、2つめがその用途です。この用途は次の定数で指定します。
Pin.IN
端子を入力用に設定します。ボタンやセンサを接続する場合はこちらを指定します。pin.OUT
端子を出力用に設定します。LEDやブザー、モーターなどを接続する場合はこちらを指定します。
ボタンAはマイコンの「15番」の汎用入出力端子に接続されていて、ボタンは入力を受ける部品であるため、次のようにコードを書きます。
追加【14行目】
from machine import Pin
from pystubit.board import buzzer
is_playing = False
def toggle_buzzer(pin):
global is_playing
if is_playing:
buzzer.off()
else:
buzzer.on("C4")
is_playing = not is_playing
pin_button_a = Pin(15, Pin.IN) # Pinクラスのオブジェクトを作成
そして、作成したオブジェクトからirq()
メソッドを呼び出して、割込みを設定します。割込みを行うタイミングは、ボタンを押したとき、すなわち電圧が下がるPin.IRQ_FALLING
を指定します。
【 サンプルコード 6-2-1 】
追加【16行目】
from machine import Pin
from pystubit.board import buzzer
is_playing = False
def toggle_buzzer(pin):
global is_playing
if is_playing:
buzzer.off()
else:
buzzer.on("C4")
is_playing = not is_playing
pin_button_a = Pin(15, Pin.IN)
pin_button_a.irq(handler=toggle_buzzer, trigger=Pin.IRQ_FALLING) # 割込みを設定
このプログラムを実行して動作を確認しましょう。動作が確認できたら、今度は15行目で引数trigger
にPin.IRQ_RISING
に変えてもう一度実行してみましょう。ブザー音の再生/停止が切り替わるタイミングがボタンAをはなしたときに変わるはずです。
変更【16行目】
pin_button_a = Pin(15, Pin.IN)
pin_button_a.irq(handler=toggle_buzzer, trigger=Pin.IRQ_RISING) # ボタンをはなしたときに割込みが行われる
6. 3 割込み処理を利用しないときとの比較
ここまでで書いてきた割込み処理のサンプルコードと同じ動作は、割込み処理を使わなくても実現できます。既にみなさんはその方法を知っていて、これまで何度もコードを書いてきました。
例えば、ボタンAを押すたびにブザー音の再生・停止が切り替わるプログラムは次のように書くことができます。
from pystubit.board import buzzer, button_a
is_playing = False # 5行目~12行目は同じ関数
def toggle_buzzer(): # 引数は不要
global is_playing
if is_playing:
buzzer.off()
else:
buzzer.on("C4")
is_playing = not is_playing
while True: # 繰り返しボタンAが押されたかどうかを確認
if button_a.was_pressed():
toggle_buzzer()
このプログラムは、「ボタンが押される」や「センサの値が変化した」などのイベントが発生しているかどうかを定期的に監視する処理を行っています。このように特定のイベントが発生したときに、それに対応した処理を行う仕組みを「ポーリング」といい、割込み処理とよく比較して説明されます。
つまり、テーマ8までのレッスンで作成してきたセンサを使うプログラムの多くは、ポーリングを行っていたことになります。
■ 割込み処理と比較したポーリングの長所と短所
ポーリングの基本はwhile
文を使ってループ処理を書くだけなので、プログラムの構造としては比較的簡単です。また、割込み処理はそもそもハードウェアが対応していなければ使えませんが、ポーリングはソフトウェアだけで実現でき、ハードウェアに依存しない点が長所といえます。
しかし、ポーリングの場合はある処理で待ち時間が出てしまうと、他のイベントの発生を確認することができません。そのため、下のプログラムではLEDディスプレイへのスクロール表示が終わるまで、ボタンAが押されたかどうかが確認できず、好きなタイミングでブザー音の再生/停止を切り替えることができません。
from pystubit.board import buzzer, display, button_a
is_playing = False
def toggle_buzzer():
global is_playing
if is_playing:
buzzer.off()
else:
buzzer.on("C4")
is_playing = not is_playing
while True:
# スクロール表示が終わるまで、ボタンが押されたかどうかを確認できない
display.scroll("Hello Python!")
if button_a.was_pressed():
toggle_buzzer()
割込み処理とポーリングはどちらもマイコンのプログラム開発ではよく使われる手法です。それぞれの長所と短所を踏まえた上で使い分けるように心がけましょう。
チャプター7
課題:割込み処理を利用したゲームの制作
ここからは課題として、学習した2種類の割込み処理を応用し、LEDディスプレイを使用した簡単なゲームを製作してみましょう。
7. 1 課題で制作するゲームの紹介
まずは、この課題で制作するゲームのプレイ動画を見てみましょう。
このゲームでは、緑色のドットで表わされたキャタクターをボタンで操作します。キャラクターに向かって壁(赤色のドット)が迫ってきますので、それをタイミングよくボタンを押してジャンプして避けます。ジャンプのタイミングがずれてしまうと失敗となり、20回連続で避けることができればゲームクリアとなります。
■ ゲーム内の処理
このゲームのプログラムでは、以下の処理を割込みで行います。
処理の内容 | 割込みの種類 | 割込みのタイミング |
---|---|---|
壁の移動 | タイマ割込み | 200ミリ秒おき |
キャラクターのジャンプ | 外部端子割込み | ボタンAの押下 |
キャラクターの着地 | タイマ割込み | ジャンプの500ミリ秒後に1回のみ |
これらの割込み処理とは別で、「LEDディスプレイの表示の更新」や「キャラクターと壁との衝突の検出」などをメインのループ処理内で行います。
7. 2 プログラムの作成手順
では順番に、このゲームに必要な処理を書いていきましょう。
■ 壁の移動処理
最初、壁はLEDディスプレイの右外に位置しています。そこから、250ミリ秒が経過するたびに1マス分左に移動します。LEDディスプレイの左端に移動したあとは、再び右外に戻ります。
上のような動きをするため、y座標は固定でx座標のみが時間の経過によって変化します。では、この処理をmoving()
関数として以下のようにまとめましょう。
wall_pos_x = 5 # 壁の現在の位置(x座標)、グローバル変数として定義
def moving():
global wall_pos_x # 関数の内部で値を更新するためグローバル宣言が必要
if wall_pos_x > 0: # LEDディスプレイの左端以外に位置している場合
wall_pos_x -= 1 # 左側へ1マス分移動(x座標の値を-1)
else: # 位置がLEDディスプレイの左端の場合
wall_pos_x = 5 # LEDディスプレイの右外に移動
この関数は、タイマ割込みで呼び出します。そこで、あらかじめ専用のTimer
オブジェクトを用意しておきましょう。また、タイマ割込みで呼び出された関数は、呼び出し元のTimer
オブジェクトを引数として受け取るため、moving()
関数に引数を1つ追加します。
追加・変更【1行目、5行目、8行目】
from machine import Timer # Timerクラスのインポート
wall_pos_x = 5
timer_moving = Timer(1) # 壁の移動用タイマ
def moving(t): # 呼び出し元のTimerオブジェクトを受け取るために引数を追加
global wall_pos_x
if wall_pos_x > 0:
wall_pos_x -= 1
else:
wall_pos_x = 5
そして、壁を避けた回数もこの関数内で数えます。ここでは、壁の位置を右外へ戻すときに回数を増やすようにしてみましょう。
追加・変更【3行目、10行目、16行目】
from machine import Timer
count = 0 # 壁を避けた回数のカウンタ
wall_pos_x = 5
timer_moving = Timer(1)
def moving(t):
global count, wall_pos_x # countもこの関数内で値を更新するためグローバル宣言が必要
if wall_pos_x > 0:
wall_pos_x -= 1
else:
wall_pos_x = 5
count += 1 # 壁を避けた回数を1つ増やす
■ キャラクターのジャンプ処理/着地処理
次にキャラクターがジャンプする処理と、ジャンプ後に一定の時間が経つと着地する処理を書いていきます。
ここで注意したいのが、ジャンプの処理と着地の処理で割込み処理の種類がちがうということです。ジャンプの処理は、ボタンAが押されたときに外部端子割込みで要求します。一方で着地の処理は、ジャンプの処理が行われたあとから500ミリ秒が経過したときにタイマ割込みで1回だけ要求します。そして、着地処理のタイマ割込みはジャンプの処理の中で設定する必要がある点にも注意してください。
では、順を追ってコードを書いていきましょう。
始めに、外部端子割込みに必要なPin
クラスを新たにインポートし、ボタンAに割り当てられている汎用入出力端子の番号を指定してオブジェクトを作成します。
追加・変更【1行目、7行目】
from machine import Timer, Pin # Pinクラスのインポート
count = 0
wall_pos_x = 5
timer_moving = Timer(1)
pin_button_a = Pin(15, Pin.IN) # ボタンAに対応したPinオブジェクト
続けて、キャラクターがジャンプする処理をjumping()
関数にまとめます。このjumping()
関数は、Pin
オブジェクトの割込み要求で呼び出されると、引数として呼び出し元のPin
オブジェクトを必ず受け取ります。そのため、引数を1つ宣言する必要があります。
追加・変更【5行目、21行目~24行目】
from machine import Timer, Pin
count = 0
wall_pos_x = 5
character_pos_y = 4 # キャラクターのy座標の位置
timer_moving = Timer(1)
pin_button_a = Pin(15, Pin.IN)
def moving(t):
global wall_pos_x, count
if wall_pos_x > 0:
wall_pos_x -= 1
else:
wall_pos_x = 5
count += 1
# キャラクターがジャンプする処理
def jumping(pin): # 引数は呼び出し元のPinオブジェクトを受け取るために必須
global character_pos_y # グローバル変数を書き換えるため、グローバル宣言が必要
character_pos_y = 1 # ジャンプ後のキャラクタのy座標は「1」
次に着地の処理をlanding()
関数にまとめます。この関数はタイマ割込みで呼び出されるため、呼び出し元のTimer
オブジェクトを引数として受け取る必要があります。
追加・変更【27行目~30行目】
def jumping(pin):
global character_pos_y
character_pos_y = 1
# キャラクターが着地する処理
def landing(t): # 引数は呼び出し元のTimerオブジェクトを受け取るために必須
global character_pos_y
character_pos_y = 4 # 着地後のキャラクターのy座標は「4」
これで、着地の処理としてlanding()
関数が定義できたので、ジャンプの処理のjumping()
関数の中で、500ミリ秒後にタイマ割込みを設定します。専用のTimer
オブジェクトを新たに作成し、init()
メソッドを実行しましょう。この割込みは1回だけ行うため、mode
引数にはTimer.ONE_SHOT
を指定します。
追加・変更【8行目、28行目】
from machine import Timer, Pin
count = 0
wall_pos_x = 5
character_pos_y = 4
timer_moving = Timer(1)
timer_landing = Timer(2) # キャラクターの着地処理用タイマ、2つめなので番号は2を指定
pin_button_a = Pin(15, Pin.IN)
def moving(t):
global wall_pos_x, count
if wall_pos_x > 0:
wall_pos_x -= 1
else:
wall_pos_x = 5
count += 1
def jumping(pin):
global character_pos_y, is_jumping
character_pos_y = 1
is_jumping = True
# 着地する処理の実行を500ミリ秒後にタイマ割込みで行う
timer_landing.init(mode=Timer.ONE_SHOT, period=500, callback=landing)
これで、ジャンプと着地の処理が定義できましたが、このコードには1つだけ問題があります。それは、ジャンプ中にボタンを押すと、jumping()
関数が再度実行され、タイマ割込みが再設定されてしまうという点です。そこで、ジャンプ中かどうかを判断するためのフラグを用意し、ジャンプ中は一連の処理を行わないようにするコードを追加します。
追加・変更【11行目、24行目、26・27行目、30行目、35行目、38行目】
from machine import Timer, Pin
count = 0
wall_pos_x = 5
character_pos_y = 4
timer_moving = Timer(1)
timer_landing = Timer(2)
pin_button_a = Pin(15, Pin.IN)
is_jumping = False # プレイヤーがジャンプ中かどうかの判別用フラグ
def moving(t):
global wall_pos_x, count
if wall_pos_x > 0:
wall_pos_x -= 1
else:
wall_pos_x = 5
count += 1
def jumping(pin):
global character_pos_y, is_jumping # グローバル宣言の追加
if is_jumping: # ジャンプ中は以降の処理を行わない
return # ここで関数の処理を終える
character_pos_y = 1
is_jumping = True # ジャンプ中のフラグを立てる
timer_landing.init(mode=Timer.ONE_SHOT, period=500, callback=landing)
def landing(t):
global character_pos_y, is_jumping # グローバル宣言の追加
character_pos_y = 4
is_jumping = False # 着地後はフラグを降ろす
■ 衝突の検出処理
ゲームの途中でキャラクターと壁が衝突した場合は失敗になります。この衝突の判定を行う処理をdetect_collision()
関数としてまとめます。衝突の判定はいたってシンプルで、キャラクターと壁が次の位置にある場合のみ、衝突していると判断します。
コードを書くと以下のようになります。
追加・変更【39行目~43行目】
def landing(t):
global character_pos_y, is_jumping
character_pos_y = 4
is_jumping = False
# 衝突の検出
def detect_collision():
if wall_pos_x == 1 and character_pos_y == 4:
return True # 衝突している場合はTrueを返す
else:
return False # 衝突していない場合はFalseを返す
■ LEDディスプレイの表示の更新処理
ここまでで書いたコードで、壁とキャラクターの座標の変化と衝突の判定を行うことができました。次は、壁とキャラクターをその座標でLEDディスプレイに表示します。
壁とキャラクターの座標は時間の経過や、プレイヤーがボタンを押すことで刻々と変化します。そこで、LEDディスプレイの表示も一定時間おきに更新するようにします。ここで定義するupdate_display()
関数は、その表示の更新を1回だけ行います。
追加・変更【2行目、14行目、50行目~62行目】
from machine import Timer, Pin
from pystubit.board import display, Image # ディスプレイのオブジェクトとイメージのクラスをインポート
count = 0
wall_pos_x = 5
character_pos_y = 4
timer_moving = Timer(1)
timer_landing = Timer(2)
pin_button_a = Pin(15, Pin.IN)
is_jumping = False
img = Image(5, 5) # 空の5マス×5マスのイメージオブジェクトを作成
def detect_collision():
if wall_pos_x == 1 and character_pos_y == 4:
return True
else:
return False
# LEDディスプレイの表示を更新する処理
def update_display():
# イメージのリセット
for x in range(5):
for y in range(5):
img.set_pixel(x, y, 0) # 各LEDを消灯に設定
# 壁をイメージへ追加
x = wall_pos_x # ローカル変数に代入
if x <= 4: # LEDディスプレイの右外(x=5)以外の場合は壁を表示
for y in range(2, 5): # y座標の位置は2~4で固定
img.set_pixel_color(x, y, color=(31, 0, 0)) # 壁は赤色
# キャラクターをイメージへ追加、x座標は1で固定
img.set_pixel_color(1, character_pos_y, color=(0, 31, 0)) # キャラクターは緑色
display.show(img, delay=0) # 遅延なしでイメージの表示
■ ゲームの終了処理
ゲームの終了時には、「設定した割込み処理の解除」と「結果の表示」を行います。
Timer
オブジェクトは割込み設定の解除用に引数不要のdeinit()
メソッドが用意されています。一方でPin
オブジェクトには解除用のメソッドがないため、代わりにもう一度irp()
メソッドを実行し、そのときにhandler
引数にNone
オブジェクトを指定します。
そして、ゲームを成功したかどうかは、変数count
の数値で判断できます。この回数が20回に達していれば成功で、達していなければ失敗です。この結果はLEDディスプレイにスクロール表示します。
以上の処理はquit_game()
関数にまとめます。以下のようにコードを書きましょう。
追加・変更【3行目、62行目~73行目】
from machine import Timer, Pin
from pystubit.board import display, Image
import time # timeモジュールのインポート
def update_display():
for x in range(5):
for y in range(5):
img.set_pixel(x, y, 0)
x = wall_pos_x
if x >= 0 and x <= 4:
for y in range(2, 5):
img.set_pixel_color(x, y, color=(31, 0, 0))
img.set_pixel_color(1, character_pos_y, color=(0, 31, 0))
display.show(img, delay=0)
# ゲームを終了するときの処理
def quit_game():
# 割込み処理の設定解除
timer_moving.deinit()
timer_landing.deinit()
pin_button_a.irq(handler=None) # 引数triggerは省略できる
# 結果の表示
time.sleep_ms(1000) # 結果の表示まで少し間を空ける
if count < 20: # 失敗の場合、赤色で結果を表示
display.scroll("Failed.", delay=50, color=(31, 0, 0))
else: # 成功の場合、緑色で結果を表示
display.scroll("Success!", delay=50, color=(0, 31, 0))
■ ゲームの状態をリセットする処理
ゲームを何度も繰り返し挑戦できるようにするには、前に挑戦したときの状態から元の状態へリセットする必要があります。そのための処理をinit_game()
関数にまとめましょう。
追加・変更【74行目~80行目】
def quit_game():
timer_moving.deinit()
timer_landing.deinit()
pin_button_a.irq(handler=None)
time.sleep_ms(1000)
if count < 20:
display.scroll("Failed.", delay=50, color=(31, 0, 0))
else:
display.scroll("Success!", delay=50, color=(0, 31, 0))
# ゲームの状態をリセット
def init_game():
global count, wall_pos_x, character_pos_y, is_jumping
count = 0
wall_pos_x = 5
character_pos_y = 4
is_jumping = False
■ メインのループ処理
最後にメインのループ処理を定義します。この処理はstart_game()
関数としてまとめ、「ゲームの状態のリセット」➡「ゲーム開始のカウントダウン」➡「割込み処理の設定」➡「LEDディスプレイの表示更新/衝突の検出のループ」➡「ゲームの終了」を順番に実行します。
追加・変更【84行目~101行目】
def init_game():
global count, wall_pos_x, character_pos_y, is_jumping
count = 0
wall_pos_x = 5
character_pos_y = 4
is_jumping = False
# メインのループ処理
def start_game():
init_game() # 状態をリセット
# ゲーム開始までのカウントダウン
for num in range(3, -1, -1): # 3, 2, 1, 0 を順番に表示
display.show(str(num), delay=1000)
# 割込み処理の設定
timer_moving.init(mode=Timer.PERIODIC, period=200, callback=moving)
pin_button_a.irq(handler=jumping, trigger=Pin.IRQ_FALLING)
# 表示の更新と衝突検出のループ。壁を避けた回数が20回に達してもループを抜ける。
while not detect_collision() and count < 20:
time.sleep_ms(33) # 1秒間に約30回の頻度で表示を更新
update_display()
# 上のループを抜けると結果を表示して、ゲームを終了する
quit_game()
これで、プログラムの完成です。プログラムの実行後、ターミナルからstart_game()
関数を呼び出して、遊んでみましょう。
>>> start_game()
もし、実行時に発生するエラーが改善できない場合は、以下のコードと見比べて誤りがないかを確認しましょう。
【 サンプルコード 7-2-1 】
from machine import Timer, Pin
from pystubit.board import display, Image
import time
count = 0
wall_pos_x = 5
character_pos_y = 4
timer_moving = Timer(1)
timer_landing = Timer(2)
pin_button_a = Pin(15, Pin.IN)
is_jumping = False
img = Image(5, 5)
def moving(t):
global count, wall_pos_x
if wall_pos_x > 0:
wall_pos_x -= 1
else:
wall_pos_x = 5
count += 1
def jumping(pin):
global character_pos_y, is_jumping
if is_jumping:
return
character_pos_y = 1
is_jumping = True
timer_landing.init(mode=Timer.ONE_SHOT, period=500, callback=landing)
def landing(t):
global character_pos_y, is_jumping
character_pos_y = 4
is_jumping = False
def detect_collision():
if wall_pos_x == 1 and character_pos_y == 4:
return True
else:
return False
def update_display():
for x in range(5):
for y in range(5):
img.set_pixel(x, y, 0)
x = wall_pos_x
if x <= 4:
for y in range(2, 5):
img.set_pixel_color(x, y, color=(31, 0, 0))
img.set_pixel_color(1, character_pos_y, color=(0, 31, 0))
display.show(img, delay=0)
def quit_game():
timer_moving.deinit()
timer_landing.deinit()
pin_button_a.irq(handler=None)
time.sleep_ms(1000)
if count < 20:
display.scroll("Failed.", delay=50, color=(31, 0, 0))
else:
display.scroll("Success!", delay=50, color=(0, 31, 0))
def init_game():
global count, wall_pos_x, character_pos_y, is_jumping
count = 0
wall_pos_x = 5
character_pos_y = 4
is_jumping = False
def start_game():
init_game()
for num in range(3, -1, -1):
display.show(str(num), delay=1000)
timer_moving.init(mode=Timer.PERIODIC, period=200, callback=moving)
pin_button_a.irq(handler=jumping, trigger=Pin.IRQ_FALLING)
while not detect_collision() and count < 20:
time.sleep_ms(33)
update_display()
quit_game()
チャプター8
おわりに
8. 1 このレッスンのまとめ
このレッスンでは、コンピュータがマルチタスクを行うための方法として、新たに「割込み処理」について学習しました。また、割込み処理にはいくつかの種類があることを知り、その中から「タイマ割込み」と「外部端子割込み」について、実際にコードを書きながら、仕組みとその使い方を確認しました。
レッスンの前半では、マイコンの仕組みを簡単に紹介しましたが、ものづくりにおいては、ソフトウェだけでなく、ハードウェアも同じくらい重要な技術であり、将来ソフトウェアエンジニアとして働きたいという人でもハードウェアの知識を身に着けたことが必ず役に立ちます。もう一度レッスンを読み返しながら、知らない用語は検索で調べるなどして、理解を深めていきましょう。
8. 2 次のレッスンについて
次回のレッスンでも引き続き、並行処理の技術について学習します。