割り込みハンドラの記述¶
対応するハードウェアでは、MicroPython は Python で割り込みハンドラを記述する機能を提供します。割り込みハンドラ(割り込みサービスルーチン、ISR とも呼ばれます)は、コールバック関数として定義されます。これらは、タイマートリガーやピンの電圧変化などのイベントに応答して実行されます。このようなイベントは、プログラムコードの実行中の任意の時点で発生する可能性があります。このことは、重大な結果をもたらします。その一部は MicroPython 言語に固有のものであり、その他はリアルタイムイベントに応答できるすべてのシステムに共通するものです。このドキュメントでは、まず言語固有の問題を取り上げ、続いてリアルタイムプログラミングに不慣れな方のための簡単な入門を行います。
この入門では、「遅い」や「できるだけ速く」といった曖昧な用語を用います。これは意図的なものであり、速度はアプリケーションに依存するためです。ISR の許容される実行時間は、割り込みの発生頻度、メインプログラムの性質、および他の並行イベントの有無に依存します。
ヒントと推奨されるプラクティス¶
ここでは以下で詳述するポイントをまとめ、割り込みハンドラコードに関する主な推奨事項を列挙します。
コードはできるだけ短く、シンプルに保ちます。
メモリ割り当てを避けます。リストへの追加や辞書への挿入、浮動小数点演算を行わないようにします。
上記の制約を回避するために
micropython.scheduleの使用を検討します。ISR が複数のバイトを返す場合は、事前に割り当てた
bytearrayを使用します。複数の整数を ISR とメインプログラムの間で共有する場合は、配列(array.array)を検討します。メインプログラムと ISR の間でデータを共有する場合は、メインプログラムでデータにアクセスする前に割り込みを無効化し、直後に再度有効化することを検討します(クリティカルセクションを参照)。
緊急例外バッファを割り当てます(以下を参照)。
MicroPython における問題¶
緊急例外バッファ¶
ISR でエラーが発生した場合、MicroPython はその目的のために特別なバッファが作成されていない限り、エラーレポートを生成できません。割り込みを使用するすべてのプログラムに以下のコードを含めると、デバッグが簡単になります。
import micropython
micropython.alloc_emergency_exception_buf(100)
緊急例外バッファには、1 つの例外スタックトレースしか保持できません。これは、ヒープがロックされている間に例外の処理中に 2 番目の例外がスローされた場合、その 2 番目の例外がクリーンに処理されたとしても、その 2 番目の例外のスタックトレースが元のものを置き換えることを意味します。これにより、後でバッファが出力されたときに、紛らわしい例外メッセージが表示される可能性があります。
シンプルさ¶
さまざまな理由から、ISR コードはできるだけ短く、シンプルに保つことが重要です。ISR は、それを引き起こしたイベントの直後に行わなければならないことだけを行うべきです。延期できる操作は、メインプログラムループに委ねるべきです。通常、ISR は割り込みを引き起こしたハードウェアデバイスを扱い、次の割り込みが発生できるように準備します。割り込みが発生したことを示すために共有データを更新することでメインループと通信し、そして戻ります。ISR は、できるだけ速くメインループに制御を返すべきです。これは MicroPython 固有の問題ではないため、以下 で詳しく説明します。
ISR とメインプログラムの間の通信¶
通常、ISR はメインプログラムと通信する必要があります。これを行う最も簡単な手段は、グローバルとして宣言するか、クラスを介して共有する 1 つ以上の共有データオブジェクトを使用することです(以下を参照)。これを行う際にはさまざまな制限と危険性があり、以下で詳しく説明します。この目的には、整数、bytes、bytearray オブジェクトに加えて、さまざまなデータ型を格納できる配列(array モジュール由来)がよく使用されます。
コールバックとしてのオブジェクトメソッドの使用¶
MicroPython は、ISR が基盤となるコードとインスタンス変数を共有できるようにする、この強力な手法をサポートしています。また、デバイスドライバを実装するクラスが複数のデバイスインスタンスをサポートできるようにもします。次の例では、2 つの LED が異なるレートで点滅します。
import machine
import micropython
micropython.alloc_emergency_exception_buf(100)
class Foo(object):
def __init__(self, freq, led):
self.led = led
self.timer = machine.Timer(-1, freq=freq, callback=self.cb, hard=True)
def cb(self, tim):
self.led.toggle()
red = Foo(1, machine.LED("LED_RED"))
green = Foo(0.8, machine.LED("LED_GREEN"))
この例では、red インスタンスが 1 Hz の仮想タイマーから赤色 LED を駆動します。タイマーが発火するたびに red.cb() が呼び出され、赤色 LED が切り替わります。green インスタンスも同様に、0.8 Hz のタイマーで緑色 LED を切り替えます。インスタンスメソッドの使用には 2 つの利点があります。第一に、単一のクラスによって複数のハードウェアインスタンス間でコードを共有できます。第二に、バインドされたメソッドであるため、コールバック関数の最初の引数は self になります。これにより、コールバックはインスタンスデータにアクセスし、連続する呼び出しの間で状態を保存できます。たとえば、上記のクラスがコンストラクタでゼロに設定された変数 self.count を持っていた場合、cb() はカウンタをインクリメントできます。red と green のインスタンスは、それぞれの LED が状態を変えた回数を独立してカウントすることになります。
Python オブジェクトの作成¶
ISR は Python オブジェクトのインスタンスを作成できません。これは、MicroPython が heap と呼ばれる空きメモリブロックのストアからオブジェクトのためにメモリを割り当てる必要があるためです。ヒープの割り当ては再入可能ではないため、割り込みハンドラではこれが許可されません。言い換えると、メインプログラムが割り当ての途中である時点で割り込みが発生する可能性があります。ヒープの整合性を維持するために、インタプリタは ISR コードでのメモリ割り当てを禁止します。
この結果として、ISR は浮動小数点演算を使用できません。これは、浮動小数点数が Python オブジェクトであるためです。同様に、ISR はリストに項目を追加することもできません。実際には、どのコード構造がメモリ割り当てを試みてエラーメッセージを引き起こすかを正確に判断するのは難しい場合があります。これもまた、ISR コードを短くシンプルに保つべきもう一つの理由です。
この問題を回避する一つの方法は、ISR が事前に割り当てられたバッファを使用することです。たとえば、クラスのコンストラクタが bytearray インスタンスとブール値のフラグを作成します。ISR メソッドはバッファ内の位置にデータを割り当て、フラグを設定します。メモリ割り当ては、ISR 内ではなく、オブジェクトがインスタンス化されるときにメインプログラムコード内で発生します。
MicroPython のライブラリの I/O メソッドは通常、事前に割り当てられたバッファを使用するオプションを提供します。たとえば machine.I2C.readfrom_into() は、呼び出し側が提供する可変バッファに読み込みます。これにより ISR での使用が可能になります。
クラスやグローバルを使用せずにオブジェクトを作成する手段は以下のとおりです。
def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
コンパイラは、関数が初めてロードされたとき(通常はそれが含まれるモジュールがインポートされたとき)に、デフォルトの buf 引数をインスタンス化します。
オブジェクト作成の一例は、バインドされたメソッドへの参照が作成されるときに発生します。これは、ISR がバインドされたメソッドを関数に渡せないことを意味します。一つの解決策は、クラスのコンストラクタでバインドされたメソッドへの参照を作成し、ISR でその参照を渡すことです。例:
class Foo():
def __init__(self):
self.bar_ref = self.bar # Allocation occurs here
self.x = 0.1
self.tim = machine.Timer(-1, freq=2, callback=self.cb, hard=True)
def bar(self, _):
self.x *= 1.2
print(self.x)
def cb(self, t):
# Passing self.bar would cause allocation.
micropython.schedule(self.bar_ref, 0)
他の手法としては、コンストラクタでメソッドを定義してインスタンス化するか、引数 self を付けて Foo.bar() を渡す方法があります。
Python オブジェクトの使用¶
オブジェクトに関するさらなる制限は、Python の動作方法から生じます。import 文が実行されると、Python コードは bytecode にコンパイルされ、通常 1 行のコードが複数のバイトコードにマッピングされます。コードが実行されると、インタプリタは各バイトコードを読み取り、一連のマシンコード命令として実行します。割り込みはマシンコード命令間の任意の時点で発生する可能性があるため、元の Python コードの行は部分的にしか実行されていない場合があります。その結果、メインループで変更されるセット、リスト、辞書などの Python オブジェクトは、割り込みが発生した瞬間に内部の整合性を欠いている可能性があります。
典型的な結果は次のとおりです。まれに、オブジェクトが部分的に更新された正確な瞬間に ISR が実行されることがあります。ISR がそのオブジェクトを読み取ろうとすると、クラッシュが発生します。このような問題は通常まれにランダムな場面で発生するため、診断が難しい場合があります。この問題を回避する方法があり、以下の クリティカルセクション で説明します。
オブジェクトの変更とは何を構成するのかを明確にすることが重要です。配列や bytearray の内容を変更することは安全です。これは、バイトやワードが割り込み不可能な単一のマシンコード命令として書き込まれるためです。リアルタイムプログラミングの言葉で言えば、その書き込みはアトミックです。辞書の項目を更新する場合も同様です。項目は整数またはオブジェクトへのポインタであるマシンワードだからです。ユーザー定義オブジェクトは、配列や bytearray をインスタンス化する場合があります。メインループと ISR の両方がこれらの内容を変更することは有効です。
危険性は、オブジェクトの構造が変更されたときに生じ、特に辞書の場合に顕著です。キーの追加や削除はリハッシュを引き起こす可能性があります。リハッシュの進行中にハード ISR が実行され、項目にアクセスしようとすると、クラッシュが発生する可能性があります。内部的には、グローバルは辞書として実装されています。したがって、メインプログラムは、ハード割り込みを生成するプロセスを開始する前に、必要なすべてのグローバルを作成すべきです。アプリケーションコードは、グローバルの削除も避けるべきです。
MicroPython は任意精度の整数をサポートしています。230 -1 から -230 の間の値は、単一のマシンワードに格納されます。それより大きい値は Python オブジェクトとして格納されます。したがって、長整数への変更はアトミックとは見なせません。ISR での長整数の使用は安全ではありません。これは、変数の値が変化する際にメモリ割り当てが試みられる可能性があるためです。
浮動小数点の制限の克服¶
一般に、ISR コードでは浮動小数点数の使用を避けるのが最善です。ハードウェアデバイスは通常整数を扱い、浮動小数点数への変換は通常メインループで行われます。しかし、浮動小数点を必要とする DSP アルゴリズムがいくつか存在します。ハードウェア浮動小数点を備えたプラットフォーム(STM32 ベースの OpenMV Cam など)では、インライン ARM Thumb アセンブラを使用してこの制限を回避できます。これは、プロセッサが浮動小数点値をマシンワードに格納するためです。値は浮動小数点数の配列を介して ISR とメインプログラムコードの間で共有できます。
micropython.schedule の使用¶
この関数を使用すると、ISR は「ごく近いうちに」実行されるコールバックをスケジュールできます。コールバックは実行のためにキューに入れられ、ヒープがロックされていない時点で実行されます。したがって、Python オブジェクトを作成したり、浮動小数点数を使用したりできます。コールバックは、メインプログラムが Python オブジェクトの更新を完了した時点で実行されることも保証されているため、コールバックが部分的に更新されたオブジェクトに遭遇することはありません。
典型的な用途は、センサーハードウェアの処理です。ISR はハードウェアからデータを取得し、さらなる割り込みを発行できるようにします。その後、データを処理するためのコールバックをスケジュールします。
スケジュールされたコールバックは、以下に概説する割り込みハンドラ設計の原則に従うべきです。これは、メインプログラムループをプリエンプトするあらゆるコードで発生し得る、I/O 動作や共有データの変更から生じる問題を回避するためです。
実行時間は、割り込みが発生し得る頻度との関係で考慮する必要があります。前のコールバックの実行中に割り込みが発生すると、コールバックのさらなるインスタンスが実行のためにキューに入れられます。これは現在のインスタンスが完了した後に実行されます。したがって、高い割り込み繰り返しレートが持続すると、キューが際限なく増大し、最終的に RuntimeError で失敗するリスクが生じます。
schedule() に渡すコールバックがバインドされたメソッドである場合は、「Python オブジェクトの作成」の注記を参照してください。
例外¶
ISR が例外を発生させても、その例外はメインループには伝播しません。例外が ISR コードで処理されない限り、割り込みは無効化されます。
asyncio とのインターフェース¶
ISR が実行されると、asyncio スケジューラをプリエンプトする可能性があります。ISR が asyncio 操作を実行すると、スケジューラの動作が妨げられる可能性があります。これは、割り込みがハードかソフトかにかかわらず適用され、ISR が micropython.schedule を介して別の関数に実行を渡した場合にも適用されます。特に、ISR のコンテキストでタスクを作成またはキャンセルすることは無効です。asyncio と安全にやり取りする方法は、asyncio.ThreadSafeFlag による同期を行うコルーチンを実装することです。次のフラグメントは、割り込みに応答してタスクを作成する例を示しています。
tsf = asyncio.ThreadSafeFlag()
def isr(_): # Interrupt handler
tsf.set()
async def foo():
while True:
await tsf.wait()
asyncio.create_task(bar())
この例では、ISR の実行と foo() の実行の間に可変量のレイテンシが生じます。これは協調的スケジューリングに固有のものです。最大レイテンシはアプリケーションとプラットフォームに依存しますが、通常は数十 ms 程度で測定されます。
一般的な問題¶
これはリアルタイムプログラミングの主題に関する簡単な入門にすぎません。初心者の方は、リアルタイムプログラムにおける設計上の誤りが、特に診断の難しい障害につながる可能性があることに注意してください。これは、それらがまれにしか発生せず、本質的にランダムな間隔で発生するためです。最初の設計を正しく行い、問題が発生する前に予測することが極めて重要です。割り込みハンドラとメインプログラムの両方を、以下の問題を理解した上で設計する必要があります。
割り込みハンドラの設計¶
上述のように、ISR はできるだけシンプルになるよう設計すべきです。ISR は常に、短く予測可能な時間内に戻るべきです。これは、ISR が実行されている間はメインループが実行されないため重要です。必然的に、メインループはコード内のランダムな箇所で実行の一時停止を経験します。このような一時停止は、特にその持続時間が長かったり変動したりする場合、診断の難しいバグの原因となり得ます。ISR の実行時間の影響を理解するためには、割り込み優先度の基本的な把握が必要です。
割り込みは優先度スキームに従って構成されています。ISR コード自体が、より高い優先度の割り込みによって中断される可能性があります。これは、2 つの割り込みがデータを共有する場合に影響を及ぼします(以下のクリティカルセクションを参照)。そのような割り込みが発生すると、ISR コードに遅延が挿入されます。ISR の実行中に低い優先度の割り込みが発生すると、それは ISR が完了するまで遅延されます。遅延が長すぎると、低い優先度の割り込みが失敗する可能性があります。遅い ISR に関するさらなる問題は、その実行中に同じ種類の 2 番目の割り込みが発生する場合です。2 番目の割り込みは、1 番目の終了時に処理されます。しかし、入ってくる割り込みのレートが ISR がそれらを処理する能力を一貫して超える場合、結果は望ましいものにはなりません。
したがって、ループ構造は避けるか最小限にすべきです。割り込みを発生させるデバイス以外のデバイスへの I/O は通常避けるべきです。ディスクアクセス、print 文、UART アクセスなどの I/O は比較的遅く、その持続時間は変動する可能性があります。ここでのさらなる問題は、ファイルシステム関数が再入可能ではないことです。ISR とメインプログラムでファイルシステム I/O を使用することは危険です。決定的に重要なこととして、ISR コードはイベントを待機すべきではありません。ピンや LED の切り替えなど、コードが予測可能な時間内に戻ることが保証できる場合は、I/O は許容されます。割り込みを発生させるデバイスに I2C や SPI 経由でアクセスすることが必要な場合もありますが、そのようなアクセスにかかる時間は計算または測定し、アプリケーションへの影響を評価すべきです。
通常、ISR とメインループの間でデータを共有する必要があります。これは、グローバル変数を介して、あるいはクラス変数やインスタンス変数を介して行うことができます。変数は通常、整数型やブール型、または整数やバイトの配列です(事前に割り当てられた整数配列は、リストよりも高速なアクセスを提供します)。ISR によって複数の値が変更される場合、メインプログラムが一部の値にはアクセスしたが、すべてにはアクセスしていない時点で割り込みが発生するケースを考慮する必要があります。これは不整合につながる可能性があります。
次の設計を考えてみましょう。ISR は入ってくるデータを bytearray に格納し、その後、受信したバイト数を処理準備が整った総バイト数を表す整数に加算します。メインプログラムはバイト数を読み取り、バイトを処理し、その後、処理準備が整ったバイト数をクリアします。これは、メインプログラムがバイト数を読み取った直後に割り込みが発生するまでは正常に動作します。ISR は追加されたデータをバッファに入れ、受信数を更新しますが、メインプログラムはすでに数を読み取っているため、元々受信したデータを処理します。新しく到着したバイトは失われます。
この危険性を回避するさまざまな方法があり、最も簡単なのはサーキュラーバッファを使用することです。本質的にスレッドセーフな構造を使用できない場合の他の方法については、以下で説明します。
再入可能性¶
関数やメソッドがメインプログラムと 1 つ以上の ISR の間、または複数の ISR の間で共有される場合、潜在的な危険性が生じる可能性があります。ここでの問題は、関数自体が中断され、その関数のさらなるインスタンスが実行される可能性があることです。これが発生する場合、関数は再入可能になるよう設計されなければなりません。これをどのように行うかは、このチュートリアルの範囲を超えた高度なトピックです。
クリティカルセクション¶
クリティカルセクションのコードの一例は、ISR によって影響を受け得る複数の変数にアクセスするものです。個々の変数へのアクセスの間に割り込みがたまたま発生すると、それらの値は不整合になります。これは、競合状態として知られる危険性の一例です。ISR とメインプログラムループが変数の変更を競い合うのです。不整合を回避するには、クリティカルセクションの間 ISR が値を変更しないことを保証する手段を講じる必要があります。これを実現する一つの方法は、セクションの開始前に machine.disable_irq() を発行し、終了時に machine.enable_irq() を発行することです。このアプローチの例を以下に示します。
import machine
import micropython
import array
import random
import time
micropython.alloc_emergency_exception_buf(100)
class BoundsException(Exception):
pass
ARRAYSIZE = const(20)
index = 0
data = array.array('i', [0] * ARRAYSIZE)
def callback1(t):
global data, index
for x in range(5):
data[index] = random.getrandbits(30) # simulate input
index += 1
if index >= ARRAYSIZE:
raise BoundsException('Array bounds exceeded')
tim = machine.Timer(-1, freq=100, callback=callback1, hard=True)
for loop in range(1000):
if index > 0:
irq_state = machine.disable_irq() # Start of critical section
for x in range(index):
print(data[x])
index = 0
machine.enable_irq(irq_state) # End of critical section
print('loop {}'.format(loop))
time.sleep_ms(1)
tim.deinit()
クリティカルセクションは、単一行のコードと単一の変数で構成される場合があります。次のコードフラグメントを考えてみましょう。
count = 0
def cb(): # An interrupt callback
count += 1
def main():
# Code to set up the interrupt callback omitted
while True:
count += 1
この例は、微妙なバグの原因を示しています。メインループ内の count += 1 という行には、リードモディファイライトとして知られる特定の競合状態の危険性が伴います。これはリアルタイムシステムにおけるバグの典型的な原因です。メインループでは、MicroPython は count の値を読み取り、それに 1 を加え、書き戻します。まれに、読み取りの後、書き込みの前に割り込みが発生します。割り込みは count を変更しますが、その変更は ISR が戻ったときにメインループによって上書きされます。実際のシステムでは、これがまれで予測不可能な障害につながる可能性があります。
上述のように、Python の組み込み型のインスタンスがメインコードで変更され、そのインスタンスが ISR でアクセスされる場合は注意が必要です。変更を実行するコードは、ISR が実行されたときにインスタンスが有効な状態にあることを保証するために、クリティカルセクションと見なすべきです。
データセットが異なる ISR 間で共有される場合は、特に注意が必要です。ここでの危険性は、低い優先度の割り込みが共有データを部分的に更新した時点で、より高い優先度の割り込みが発生する可能性があることです。この状況への対処は、この入門の範囲を超えた高度なトピックですが、以下で説明するミューテックスオブジェクトが場合によっては使用できることだけは記しておきます。
クリティカルセクションの間、割り込みを無効化することは、通常かつ最も簡単な進め方ですが、これは問題を引き起こす可能性のあるものだけでなく、すべての割り込みを無効化してしまいます。一般に、割り込みを長時間無効化することは望ましくありません。タイマー割り込みの場合、コールバックが発生する時刻に変動をもたらします。デバイス割り込みの場合、デバイスのサービスが遅れすぎて、データの損失やデバイスハードウェアでのオーバーランエラーにつながる可能性があります。ISR と同様に、メインコード内のクリティカルセクションは、短く予測可能な持続時間を持つべきです。
割り込みが無効化される時間を劇的に短縮するクリティカルセクションへの対処アプローチは、ミューテックス(相互排他という概念に由来する名前)と呼ばれるオブジェクトを使用することです。メインプログラムはクリティカルセクションを実行する前にミューテックスをロックし、終了時にロックを解除します。ISR はミューテックスがロックされているかどうかをテストします。ロックされている場合、ISR はクリティカルセクションを回避して戻ります。設計上の課題は、クリティカルな変数へのアクセスが拒否された場合に ISR が何を行うべきかを定義することです。ミューテックスの簡単な例は こちら にあります。ミューテックスのコードは割り込みを無効化しますが、それはわずか 8 個のマシン命令の間だけであることに注意してください。このアプローチの利点は、他の割り込みがほとんど影響を受けないことです。
割り込みと REPL¶
タイマーに関連付けられたものなどの割り込みハンドラは、プログラムが終了した後も実行を続けることがあります。これは、コールバックを発生させるオブジェクトがスコープ外になったと予想していた場合に、予期しない結果を生む可能性があります。たとえば OpenMV Cam では:
def bar():
foo = machine.Timer(-1, freq=4, callback=lambda t: print('.', end=''), hard=True)
bar()
これは、タイマーが明示的に無効化されるか、ボードが Ctrl-D でリセットされるまで実行を続けます。