CircuitPythonのasyncioを使った協調型マルチタスク3 割り込み編

AdafruitのLearning Guideの非公式日本語訳です。
英語独特の言い回しを可能な限り日本語的な表現に直していますが、不自然に感じる部分も残っていますことを何卒ご容赦ください。

割り込みを制御する

前ページの「タスク間のコミュニケーション」では、LEDを点滅させながら同時にボタンが押された場合の処理を紹介しました。
ボタンが押されるというのは、非同期イベントの一例で、実行中のプログラムの外部にある何かによって引き起こされるイベントで、いつでも起こる可能性があります。

割り込み

マイコンは、非同期イベントを処理するために割り込みと呼ばれるハードウェア機構を提供します。ハードウェア割り込みは、ピンの状態が変化したとき、内部タイマーが作動したとき、I2Cのリード/ライトなどのハードウェア動作が完了したとき、その他多くの理由で発生します。これらのイベントは通常、実行中のプログラムとは非同期ですが、存在しないメモリアドレスにアクセスした場合など、プログラムがエラーを起こしたことを示すために割り込みが使用される場合もあります。

割り込みが発生すると、割り込み機構は割り込みハンドラと呼ばれるルーチンを呼び出します。現在実行中のプログラムは一時的に中断され、優先順位の低い他の割り込みはブロックされます。割込みハンドラルーチンは素早く何かをして戻り、その後通常のプログラムが(通常)再開されます。割込みハンドラは、マルチタスクの基礎編のページで紹介した協調型マルチタスクの一例です。

例えば、外部センサに接続されたピンが変化し、センサが新しいデータを持っていることを示すことがあります。割り込みハンドラ自身がデータを読み込んで記録することもできますが、センサが応答するまでに時間がかかりすぎることがよくあります。そこで、割り込みハンドラで直接センサデータを読み込む代わりに、新しいデータが利用可能であることを示すフラグをセットします。また、そのデータを読み込むためのタスクを後で実行するようにスケジューリングすることもあります。フラグのチェックやタスクの実行は、イベントループの中で行われることが多いです。

実際のハードウェア割り込みは、ハードウェアが生成し処理するため、「ハード割り込み」と呼ばれることが多いです。その割り込みを後からソフトウェアで非同期的に処理することを、「ソフト割り込み」の処理と呼ぶことが多いようです。

ポーリング

割り込みの代わりになるのがポーリングです。何かを何度もチェックし、変化を待つことをポーリングといいます。例えば、DigitalInOut.valueをループの中で何度も監視することができます。このガイドの例では、ある条件をチェックし、asyncio.sleep()を実行するコードが出てきます。このコードはポーリングしていますが、他のコードの実行をブロックしないように制御されています。

countioを使った割り込みの制御

CircuitPythonはcountioというピンの立ち上がりや立ち下がりをカウントするネイティブモジュールを提供しています。内部的には、countioは割り込みや他のハードウェアメカニズムを使ってこれらの遷移をキャッチし、カウントをインクリメントします。

asyncioでcountioを使うと、割り込みをキャッチして、その割り込みに基づいて何かをすることができます。ここでは、countio を使ってタクトスイッチに接続されたピンを監視し、デバイスの割り込みをシミュレートする簡単な例を示します。countioの値はタスクの中でポーリングされていることに注意してください。

import asyncio
import board
import countio

async def catch_interrupt(pin):
    """Print a message when pin goes low."""
    with countio.Counter(pin) as interrupt:
        while True:
            if interrupt.count > 0:
                interrupt.count = 0
                print("interrupted!")
            # Let another task run.
            await asyncio.sleep(0)


async def main():
    interrupt_task = asyncio.create_task(catch_interrupt(board.D3))
    await asyncio.gather(interrupt_task)

asyncio.run(main())

このプログラムにはタスクが1つしかないので、それほど面白いものではありません。しかし、タスク間のコミュニケーションのページで説明されているテクニックを使って、割り込みが起こったことを他のタスクに警告することができます。

keypadを使った割り込みの制御

CircuitPythonのキーパッドモジュールは、ピンの遷移を検出する方法も提供しています。これは実際にはハードウェア割り込みを使用しません。その代わりに、数ミリ秒ごとにピンをポーリングします。

ここでは、遷移の検出だけを簡略化した例を示します。

import asyncio
import board
import keypad

async def catch_pin_transitions(pin):
    """Print a message when pin goes low and when it goes high."""
    with keypad.Keys((pin,), value_when_pressed=False) as keys:
        while True:
            event = keys.events.get()
            if event:
                if event.pressed:
                    print("pin went low")
                elif event.released:
                    print("pin went high")
            await asyncio.sleep(0)

async def main():
    interrupt_task = asyncio.create_task(catch_pin_transitions(board.D3))
    await asyncio.gather(interrupt_task)

asyncio.run(main())

まとめ

  • 非同期でピンの状態遷移をポーリングするタスクを作成し、asyncioでピン割り込みを監視する。
  • countioやkeypad.Keysで非同期でピンの状態遷移を検出する。

Follow me on Twitter