割り込みハンドラの作成

適切なハードウェア上で MicroPython は、Python で割り込みハンドラを書く能力を提供します。割り込みハンドラ(割り込みサービスルーチン - ISR とも呼ばれます)は、コールバック関数として定義されています。これらはタイマトリガやピンの電圧変化などのイベントに応答して実行されます。このような事象は、プログラムコードの実行中の任意の時点で発生する可能性があります。これは MicroPython 言語に特有の重要な結果をもたらします。その他のものは、リアルタイムイベントに応答することができるすべてのシステムに共通です。このドキュメントでは、まず言語特有の問題について説明し、それに続いてリアルタイムプログラミングの概要を説明します。

ここの紹介では、「遅い」または「できるだけ早く」のような曖昧な用語を使っています。速度はアプリケーションに依存するため、これは意図的なものです。ISR の許容可能な期間は、割り込みが発生する速度、メインプログラムの性質、および他の同時発生イベントの存在に依存します。

MicroPython の問題

緊急例外バッファ

ISR でエラーが発生した場合、MicroPython はエラーレポートのための特別なバッファが作成されていないかぎりエラーレポートを生成できません。次のコードが割り込みを使用するプログラムに含まれていると、デバッグが簡単になります。

import micropython
micropython.alloc_emergency_exception_buf(100)

この緊急例外バッファには1つの例外スタックトレースしか保持できません。つまり、ヒープがロックされている間に2つ目の例外が発生した場合、その2つ目の例外のスタックトレースが元のものに置き換わることになります。このせいで、バッファを後で表示した場合に、例外メッセージが混乱させてしまうかもしれません。

簡素性

さまざまな理由から、ISR コードをできるだけ短く簡単に保つことが重要です。割り込みを引き起こしたイベントの直後に行われなければならないことだけをすべきです。延期できる操作はメインプログラムのループに委ねられるべきです。通常、ISRは割り込みを発生させたハードウェアデバイスを処理し、次の割り込みが発生する準備ができます。割り込みが発生したことを示す共有データを更新することでメインループと通信し、復帰します。ISR は、できるだけ迅速にメインループに制御を戻す必要があります。これは特定の MicroPython の問題ではありませんので、 以降 で詳しく説明します。

ISR とメインプログラムとの間の通信

通常、ISR はメインプログラムと通信する必要があります。これを行う最も簡単な方法は、グローバルとして宣言されているか、クラスを介して共有されている1つ以上の共有データオブジェクトを経由することです(以降を参照)。これを行う際には、さまざまな制限や危険があり、詳しくは以降で説明します。データの共有には、整数、 bytesbytearray オブジェクトが、さまざまなデータ型を格納できる配列(array モジュールから)とともによく使われます。

コールバックとしてのオブジェクトメソッドの使用

MicroPython は基盤コードとのインスタンス変数共有を ISR で可能とする強力な手法をサポートしています。また、デバイスドライバーを実装するクラスが複数のデバイスインスタンスをサポートできるようにします。次の例では2つの LED を異なる速度で点滅させます。

import pyb, micropython
micropython.alloc_emergency_exception_buf(100)
class Foo(object):
    def __init__(self, timer, led):
        self.led = led
        timer.callback(self.cb)
    def cb(self, tim):
        self.led.toggle()

red = Foo(pyb.Timer(4, freq=1), pyb.LED(1))
green = Foo(pyb.Timer(2, freq=0.8), pyb.LED(2))

この例では、 red インスタンスはタイマー 4 を LED 1 に関連付けます。タイマー4 の割り込みが発生すると、LED 1 の状態を変えるよう red.cb() を呼び出します。 green インスタンスも同様に動作です: 実行中にタイマー 2 の割り込みが発生すると、LED 2 の点灯/消灯を切り替えるために green.cb() を呼び出します。インスタンスメソッドの利用には2つの利点があります。1つは、単一のクラスのコードを複数のハードウェアインスタンス間で共有できることです。もう1つは、バインドされたのがメソッドであると、コールバック関数の第1引数が self となることです。これにより、コールバックはインスタンスデータにアクセスし、連続する呼び出し間で状態を保存できます。たとえば、上記のクラス self.count がコンストラクターで変数をゼロに設定していた場合、 cb() はカウンターをインクリメントできます。 redgreen インスタンスは、各 LED が状態を変化させた回数を個別にカウントしていきます。

Python オブジェクトの作成

ISR は Python オブジェクトのインスタンスを作成できません。これは、MicroPython がヒープと呼ばれる空きメモリブロックのストアからオブジェクトのメモリを割り当てる必要があるためです。これは、ヒープ割り当てが再入可能ではないため、割り込みハンドラでは許可されません。言い換えれば、メインプログラムが割り当てを実行する途中で割り込みが発生する可能性があります。インタープリタがヒープの整合性を維持するために ISR コード内でのメモリ割り当てを許可しません。

この結果、ISRでは浮動小数点演算を使えません。これは、float が Python オブジェクトであるためです。同様に、ISR はアイテムをリストに追加することはできません。実際には、どのコード構成がメモリ割り当てを試み、エラーメッセージを出すのかを正確に判断するのは難しいかもしれません: これが ISR コードを短く簡潔に保つようにするもう一つの理由です。

この問題を回避する1つの方法は、ISR が事前に割り当てられたバッファを使用することです。たとえば、クラスコンストラクターは bytearray インスタンスとブール値フラグを作成します。ISR メソッドは、バッファ内の場所にデータを割り当て、フラグを設定します。メモリ割り当ては、ISR 内ではなく、オブジェクトがインスタンス化されたときにメインプログラムコードで行うようにします。

MicroPython ライブラリの I/O メソッドは、通常、事前に割り当てられたバッファを使用するオプションを提供します。たとえば pyb.i2c.recv() は、第1引数として可変バッファを受け入れることができます。これは ISR 内で使えます。

クラスやグローバルを使わずにオブジェクトを作成する方法は次のとおりです。

def set_volume(t, buf=bytearray(3)):
    buf[0] = 0xa5
    buf[1] = t >> 4
    buf[2] = 0x5a
    return buf

コンパイラは、関数が最初にロードされたとき(普通は関数が定義されているモジュールが import されたとき)、デフォルトの buf 引数をインスタンス化します。

オブジェクトのインスタンス作成は、バインドされたメソッドへの参照が作成されたときに発生します。これは ISR がバインドされたメソッドを関数に渡すことができないことを意味します。1つの解決策は、クラスコンストラクターでバインドされたメソッドへの参照を作成し、その参照をISRに渡すことです。たとえば次のようにします:

class Foo():
    def __init__(self):
        self.bar_ref = self.bar  # ここで割り当て発生
        self.x = 0.1
        tim = pyb.Timer(4)
        tim.init(freq=2)
        tim.callback(self.cb)

    def bar(self, _):
        self.x *= 1.2
        print(self.x)

    def cb(self, t):
        # self.bar を渡す割り当て発生
        micropython.schedule(self.bar_ref, 0)

他の手法は、コンストラクター内でメソッドを定義してインスタンス化するか、 Foo.bar() を引数 self と渡すかです。

Python オブジェクトの使用について

Python が動作する仕組みから、オブジェクトに対するさらなる制限が生じます。import 文が実行されると、その Python コードがバイトコードにコンパイルされ、1行のコードを、典型的には複数のバイトコードにマッピングします。コードが実行されると、インタープリタは各バイトコードを読み取り、それを一連のマシンコード命令として実行します。マシンコード命令の間にいつでも割り込みが発生することがあるとすれば、Pythonコードの元の行は部分的にしか実行されない可能性があります。その結果、メインループで変更された集合、リスト、辞書などの Python オブジェクトは、割り込みが発生した瞬間に内部整合性が欠落することがあります。

典型的なパターンは次のとおりです。オブジェクトの一部が更新されている状況で、ごくまれに ISR が実行されるパターンです。このような状況で ISR がオブジェクトを読み取ろうとすると、クラッシュが発生します。このような問題は、通常、まれにしか発生しないため、診断が難しい場合があります。この問題を回避する方法は、後の クリティカルセクション で説明します。

オブジェクトの修正内容を明確にしておくことは重要です。辞書などの組み込み型の変更は問題があります。array または bytearray の内容を変更することは問題ありません。これは、バイトまたはワードが中断されない単一のマシンコード命令で書き込まれるからです。リアルタイムプログラミングの用語で、書き込みはアトミックです。ユーザ定義オブジェクトは、整数、array、bytearray をインスタンス化することがあります。メインループと ISR の両方がこれらの内容を変更することは有効です。

MicroPython は任意の精度の整数をサポートします。 2**30 -1 と -2**30 の間の値は、単一の機械語で格納されます。大きな値は Python オブジェクトとして格納されます。したがって、長い整数への変更はアトミックとはみなされません。変数の値が変わるとメモリ割り当てが試みられる可能性があるため、ISR での長い整数の使用は安全ではありません。

浮動小数点の制限を克服する

通常、ISR コードでは浮動小数点数を使わないでください。ハードウェアデバイスは整数を扱い、浮動小数点への変換はメインループで行うようにしてください。しかし、浮動小数点を必要とするいくつかの DSP アルゴリズムがあります。ハードウェア浮動小数点(Pyboardなど)のプラットフォームでは、インライン ARM Thumb アセンブラを使用してこの制限を回避できます。これは、プロセッサが浮動小数点値を機械語に格納するためです。浮動小数点数の配列を使用して ISR とメインプログラムコード間で値を共有できます。

micropython.schedule の利用

この機能により、ISR は実行のコールバックを「非常に早く」スケジュールできます。コールバックは実行のためにキューに入れられ、ヒープがロックされていないときに実行されます。したがって、Python オブジェクトを作成して浮動小数点数を使用することができます。コールバックは、メインプログラムが Python オブジェクトの更新を完了した時点で実行されることも保証されているため、コールバックは部分的に更新されたオブジェクトに遭遇しません。

典型的な使用法は、センサのハードウェアを扱うことです。ISR はハードウェアからデータを取得し、さらに割り込みを発行することができます。その後、データを処理するコールバックをスケジュールします。

スケジュールされたコールバックは、以下に説明する割り込みハンドラの設計の原則にしたがう必要があります。これは、I/O アクティビティおよびメインプログラムループを先取りするコードで発生する可能性のある共有データの変更に起因する問題を回避するためです。

実行時間は、割り込みが発生する頻度に関連して考慮する必要があります。直前のコールバックの実行中に割り込みが発生すると、コールバックのさらなるインスタンスが実行のためにキューに入れられます。現在のインスタンスが完了した後に実行されます。したがって、継続的な高い割り込みの繰り返し率は、制約のないキューの増加と最終的な失敗のリスクを持ち RuntimeError ます。

schedule() に渡されるコールバックが結合メソッド(bound method)の場合は、「Pythonオブジェクトの作成」の注意を考慮してください。

例外

ISR が例外を発生させると、メインループには伝播しません。例外が ISR コードによって処理されない限り、割り込みは無効になります。

uasyncio とのインタフェース

ISR が実行されると uasyncio スケジューラーを先取りすることができます。ISR が uasyncio オペレーションを実行すると、スケジューラの動作が中断される可能性があります。これは割り込みがハードかソフトかに関係なく、また ISR が micropython.schedule を介して他の関数に実行を移した場合にも当てはまります。特に、タスクの作成やキャンセルは、ISR のコンテキストでは無効です。 uasyncio と安全にやりとりする方法は、 uasyncio.ThreadSafeFlag によって同期が行われるコルーチンを実装することです。次の図は、割り込みに応答してタスクを生成しているところです:

tsf = uasyncio.ThreadSafeFlag()

def isr(_):  # Interrupt handler
    tsf.set()

async def foo():
    while True:
        await tsf.wait()
        uasyncio.create_task(bar())

この例では、ISR の実行と foo() の実行の間に可変長の待ち時間が発生します。これは協調スケジューリングに固有のものです。最大遅延時間はアプリケーションとプラットフォームに依存しますが、通常数十ミリ秒の単位で測定されるでしょう。

一般的な問題

これは、リアルタイムプログラミングの主題の簡単な紹介にすぎません。初心者は、リアルタイムプログラムの設計ミスは、特に診断が難しい障害につながる可能性があることに注意してください。これは、まれに、本質的にランダムな間隔で発生する可能性があるからです。初期設計の権利を取得し、問題が発生する前に予測することが重要です。割り込みハンドラとメインプログラムは、次の問題を認識して設計する必要があります。

割り込みハンドラの設計

上記のように、ISR はできるだけシンプルに設計する必要があります。ISR からはは常に短期間で予測可能な期間に戻るべきです。これは、ISR が実行されているときにメインループが実行されないため重要です。必然的に、メインループはコード内のランダムなポイントで実行を一時停止します。そのような一時停止は、特にその持続時間が長いか可変である場合に、バグを診断するのが難しい原因となり得る。ISR 実行時間の意味を理解するためには、割り込み優先順位の基本的な把握が必要です。

割込みは、優先順位スキームに従って編成されます。ISR コード自体がより高い優先度の割り込みによって割り込みされる可能性があります。これは、2つの割込みがデータを共有する場合に意味を持ちます(下記のクリティカルセクションを参照)。このような割り込みが発生すると、ISR コードに遅延が挿入されます。ISRの実行中に優先度の低い割り込みが発生すると、ISR が完了するまで遅延します。遅延が長すぎる場合は、優先度の低い割り込みが失敗する可能性があります。遅い ISR のさらなる問題は、実行中に同じタイプの2番目の割り込みが発生する場合です。2番目の割り込みは最初の割り込みの終了時に処理されます。しかし、着信割り込みのレートがISRのサービス能力を一貫して超えている場合、結果は幸いなことではありません。

したがって、ループ構造を避けるか、最小化する必要があります。通常、ディスクアクセス、print ステートメント、UART アクセスなどの I/O は比較的遅く、その継続時間は変動する可能性があります。ここでのさらなる問題は、ファイルシステムの機能が再入可能ではないということです。ISR でファイルシステム I/O を使用すると、メインプログラムは危険です。決定的に ISR コードはイベントを待つべきではありません。ピンまたは LED のトグルなど、予測可能な期間にコードが返されることが保証されている場合は、I/O は許容されます。I2C または SPI を介して割り込みデバイスにアクセスすることが必要な場合がありますが、そのようなアクセスに要する時間を計算または測定し、アプリケーションへの影響を評価する必要があります。

通常、ISR とメインループの間でデータを共有する必要があります。これは、グローバル変数またはクラス変数やインスタンス変数を介して実行できます。変数は、通常、整数またはブール型、または整数またはバイト配列です(事前に割り当てられた整数配列はリストよりも高速なアクセスを提供します)。複数の値が ISR によって変更される場合、メインプログラムがすべての値ではなく一部にアクセスしたときに割り込みが発生する場合を考慮する必要があります。これは矛盾を引き起こす可能性があります。

次の設計を考えてみましょう。ISR は着信データをバイアルに格納し、受信したバイト数を処理可能な総バイト数を表す整数に加算します。メインプログラムはバイト数を読み取り、バイトを処理した後、準備されたバイト数をクリアします。これは、メインプログラムがバイト数を読み取った直後に割り込みが発生するまで機能します。ISR は追加されたデータをバッファに入れ、受信した番号を更新しますが、メインプログラムは既に番号を読み取っているため、最初に受信したデータを処理します。新しく到着したバイトは失われます。

この危険を回避するにはさまざまな方法があります。最も簡単なのは循環バッファを使用することです。固有のスレッド安全性を持つ構造体を使用することができない場合は、他の方法を以下で説明します。

リエントラント性

関数またはメソッドがメインプログラムと1つまたは複数の ISR 間で、または複数の ISR 間で共有される場合、潜在的な危険が生じる可能性があります。ここでの問題は、関数自体が中断され、その関数のさらなるインスタンスが実行されることです。これが発生する場合は、関数はリエントラントになるように設計する必要があります。これがどのように行われるかは、このチュートリアルの範囲を超えた高度なトピックです。

クリティカルセクション

コードのクリティカルセクションの例は、ISR の影響を受ける可能性のある複数の変数にアクセスするコードです。個々の変数へのアクセスの間に割り込みが発生した場合、その値は不一致になります。これは、競合状態として知られるハザードのインスタンスです: ISR とメインプログラムのループ競合が変数を変更します。矛盾を避けるためには、ISR がクリティカルセクションの期間の値を変更しないようにする手段が必要です。これを達成する1つの方法は、セクションの開始前に pyb.disable_irq() を発行し、最後に pyb.enable_irq() を発行することです。このアプローチの例を次に示します:

import pyb, micropython, array
micropython.alloc_emergency_exception_buf(100)

class BoundsException(Exception):
    pass

ARRAYSIZE = const(20)
index = 0
data = array.array('i', 0 for x in range(ARRAYSIZE))

def callback1(t):
    global data, index
    for x in range(5):
        data[index] = pyb.rng() # simulate input
        index += 1
        if index >= ARRAYSIZE:
            raise BoundsException('Array bounds exceeded')

tim4 = pyb.Timer(4, freq=100, callback=callback1)

for loop in range(1000):
    if index > 0:
        irq_state = pyb.disable_irq() # Start of critical section
        for x in range(index):
            print(data[x])
        index = 0
        pyb.enable_irq(irq_state) # End of critical section
        print('loop {}'.format(loop))
    pyb.delay(1)

tim4.callback(None)

クリティカルセクションは、1行のコードと1つの変数で構成できます。以下のコード断片を考えてみましょう。

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 間でデータセットを共有する場合は、特別な注意が必要です。ここでの危険は、プライオリティの低い割り込みが部分的に共有データを更新したときに、より高い優先度の割り込みが発生する可能性があることです。この状況を扱うことは、以下で説明する mutex オブジェクトを使用することがあることに注意する以外にも、この紹介の範囲を超えた高度なトピックです。

クリティカルセクションの間の割り込みの無効化は、通常の最も簡単な方法で行うのですが、問題を引き起こす可能性のある割り込みだけでなく、すべての割り込みを無効にします。一般的に、割り込みを無効にすることは望ましくありません。タイマー割込みの場合は、コールバックが発生する時間が変動してしまいます。デバイス割り込みの場合は、デバイスハードウェアのデータ消失やオーバーランエラーの可能性に対してのデバイスの復旧が遅すぎとなる可能性があります。ISR と同様に、メインコードのクリティカルセクションは、短く、予測可能な期間を持つようにする必要があります。

割り込みを無効する時間を根本的に縮めたクリティカルセクションを扱うには、mutex (mutual exclusion - 相互排除の概念から派生した名前)というオブジェクトを使います。メインプログラムは、クリティカルセクションを実行する前に mutex をロックし、最後にロックを解除します。ISRは、ミューテックスがロックされているかどうかをテストします。そうであれば、クリティカルセクションを避けて戻ります。設計上の課題は、重要な変数へのアクセスが拒否された場合に ISR が行うべきことを定義することです。mutex の簡単な例が ここ にあります。mutex コードは割り込みを無効にしますが、8つのマシン語命令の間のみ有効ですこのアプローチの利点は、他の割り込みにはほとんど影響がないことです。

割り込みと REPL

タイマーに関連付けられているなどの割り込みハンドラは、プログラムの終了後も引き続き実行できます。これにより、コールバックを発生させるオブジェクトがスコープ外になることが予想される場合に、予期しない結果が生じることがあります。次は pyboardの場合 の例です:

def bar():
    foo = pyb.Timer(2, freq=4, callback=lambda t: print('.', end=''))

bar()

これはタイマーが明示的に無効にされるか、ボードが ctrl D でリセットされるまで続きます。