🌊

MicroPythonでRaspberry Pi PicoのPIOを動かしてみる

2022/12/23に公開

はじめに

これはモダン言語によるベアメタル組込み開発 Advent Calendar 2022 19日目の記事です。(遅れてすみません...)

Programable I/O(PIO)とは

Raspberry Pi PicoはRP2040というマイコンを載せていますが、特徴的な機能としてPIOというモジュールを持っています。CPUの動作周波数以下で動作させられ、クロック刻みでI/Oが制御できます。PIOのような機能を持っていないマイコンでは、GPIOをLowからHighにするにも数クロックかかってしまうので、PIOほど即座に精確な信号をGPIOから出力できません。
PIOの用途としては、精確なパルス信号を生成したり、UARTやSPIなどの通信のエミュレートを、CPUを占有せず精確に実行するなどがあります。
PIOはI/O専用の命令を実行するステートマシンを持ってます。命令メモリやレジスタを持つI/Oに特化した小さなCPUのイメージです。PIOステートマシンに対してはFIFO経由でデータの受け渡しを行います。
MicroPythonではPIO用のアセンブリ命令を並べた関数を使用して制御します。

PIOの概要(引用元:https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf 3.2. Programmer's Model)

実行方法

ファームウェアの書き込み

まずMicroPython環境用のファームウェアを書き込みます。

$ wget https://micropython.org/download/rp2-pico/rp2-pico-latest.uf2
# BOOTSELボタンを押しながらUSBを接続
$ dmesg | tail # sdXが接続されたメッセージが出るはず
..
..
[  222.537393]  sdb: sdb1
[  222.641281] sd 3:0:0:0: [sdb] Attached SCSI removable disk
$ sudo mount /dev/sdb1 /media
$ sudo cp rp2-pico-latest.uf2 /media
$ sudo umount

実行環境(コマンドライン)

ThonnyというIDE環境もありますが、ここではコマンドラインで実行できるrshellを使っていきます。
https://github.com/dhylands/rshell

$ python -m pip install rshell
$ sudo adduser -a $USER dialout
$ logout
# 再ログイン
$ export PATH=$PATH:~/.local/bin # 常にパスを通したい場合は.bashrcなどに追記する
$ rshell

replで簡単な動作確認ができます。
自分はPicoで制御したいICの動作確認などに使用したりしてます。

$ rshell
> repl
>>> from machine import Pin
>>> gp0 = Pin(0, Pin.OUT)
>>> gp0.high()

実行

LEDを1秒間隔で点滅させる例を、MicroPythonのGitHubリポジトリから持ってきて動かしてみます。
https://github.com/micropython/micropython/tree/master/examples/rp2

$ git clone https://github.com/micropython/micropython.git
$ cd micropython/examples/rp2

MicroPythonではプログラムの実行方法が2通りあります。
①main.py(MicroPythonではファイル名がmain.pyのものが実行されます)を/pyboardにコピーする

$ rshell
> cp pio_1hz.py /pyboard/main.py
# リセット(USBケーブル抜き差し)

②REPLで実行する。ファイルをREPLで実行する際はrshellをインストールすると一緒に入るpyboardコマンドが便利です。

$ pyboard pio_1hz.py

MicroPythonでは最初にMicroPythonのファームウェアを書き込む時以外は、BOOTSELボタンを押す必要がありません。

作例

少し珍しい方法でUART通信がしたかったので自作してみた作例を載せます。
UARTは一般的にはTXとRXの2線が多いのですが、たまにTXとRXで同じI/Oピンを使用する単線UARTというものがあります。この単線UARTで通信したいと思い、Pico側のTXとRXをショートして通信させようとしましたが、問題が発生しました。
RP2040に内蔵されたUARTモジュールでは2線用で、idle状態の時、TXがHighを出力してしまいます。このため、通信相手がデータ送信時にLowを出力すると、Pico側のHigh出力とぶつかります。結果、通信相手がLowを出力しても電圧が下がりきらずPico側がLowを認識しずらくなる現象がおきました。また、デバイスにとっても出力がぶつかっているとダメージを受ける可能性があるので良くありません。ですので、Pico側のTXをオープンドレイン出力(Low出力はするが、High出力はしない(Hi-Z))として動作させるUART機能をPIOで実装してみました。

uart_open_drain.py
class UARTOpenDrain(UART):
    def __init__(self, *args, sm_id=7, **kwargs):
        super().__init__(*args)
        self.init(sm_id, **kwargs)

    def init(self, sm_id=7, **kwargs):
        super().init(**kwargs)
	# PIOはボーレートの8倍の周波数で動作させる
        self.tx_sm = rp2.StateMachine(
            sm_id,
            self._tx_asm,
            freq=8 * kwargs["baudrate"],
            out_base=kwargs["tx"],
            set_base=kwargs["tx"])
        self.tx_sm.active(1)

    def write(self, data):
        if type(data) is bytes:
            for byte in data:
		# PIOのステートマシンにFIFO経由でデータを渡す、0xFFでxorして反転させているのは、Lowを出力するときは出力モード(pindirsに1を設定)、High(オープンドレインなのでHi-Z)を出力するときは入力モード(pindirに0を設定)にするため
                self.tx_sm.put(byte ^ 0xFF)
        elif type(data) is str:
            for char in data:
                self.tx_sm.put(ord(char) ^ 0xFF) # PIOのステートマシンにFIFO経由でデータを渡す
        else:
            raise TypeError(f"unsupported type {type(data)}")
        return len(data)

    @rp2.asm_pio(out_init=rp2.PIO.OUT_HIGH, set_init=rp2.PIO.OUT_HIGH, out_shiftdir=rp2.PIO.SHIFT_RIGHT)
    def _tx_asm():
        # set(pindirs, 0): 入力モード(Hi-Z)
        # set(pindirs, 1): 出力モード(Low出力のみ)

        set(pindirs, 0)                  # 入力モードに設定
        set(pins, 0)                     # 出力をLowに設定

        wrap_target()
        pull()                           # FIFOにデータが来るまでブロック
        set(pindirs, 1)              [6] # スタートビットのためにLow出力
        set(x, 7)                        # 送ったビット数のカウンタを設定
        # Shift out 8 data bits, 8 execution cycles per bit
        label("bitloop")
        out(pindirs, 1)              [6] # 1ビットずつ出力(ピンの入出力モードを変えるだけ)
        jmp(x_dec, "bitloop")
        set(pindirs, 0)              [6] # ストップビットを出力

    def sendbreak():
        raise NotImplementedError

PIOを使う際に設定したくなる機能

オーバークロック

デフォルトは125MHzです。
私の環境ではUSB給電+250MHzでもPIOは問題なく動作しました。
注意点として、例えばUARTは周波数を設定した後に初期化しないと動作させたいスピードで動作させられません。

machine.freq(250_000_000)
uart = UART(0, baudrate=115200) # 周波数変更後に設定する!

ドライブ能力

残念ながらRP2040のMicroPython実装ではドライブ能力を変更するAPIは見当たりませんでした。C/C++にはあるのですが...
このような場合は、直接制御レジスタを叩く必要があります。2.19.4. Padsにドライブ能力を変更できるという説明があり、Pad control registerがPADS_BANK0_BASE(0x4001c000)以降にマッピングされています(2.19.6.3. Pad Control - User Bank)。[5:4]ビット目で設定できます。
ちなみにRP2040はRead Modify Writeを簡単に行える Atomic Register Access 機能が実装されています。現時点での値を読むことなく変更が可能です。例えば書き込みたいアドレス+0x2000に書くことで1を書きたビットのみ上書きできます

>>> from machine import mem32
>>> print(hex(mem32[0x4001c000+0x40]))
0x56                                     # [5:4]ビットは0b01 => ドライブ能力4mA
>>> mem32[0x4001c000+0x40+0x2000] = 0x30 # GP15のドライブ能力を4mAから12mAに増やす 
>>> print(hex(mem32[0x4001c000+0x40]))
0x76

スルーレート

ドライブ能力と同様に、MicroPythonのAPIには見当たりませんでした。制御レジスタはドライブ能力と同様のPad control registerで、0ビット目で設定できます。

まとめ

MicroPythonでPIOという機能を動かしてみました。
PIOという機能は非常にポテンシャルの高い機能で、使いこなすにはまだまだ理解を深める必要があると感じました。
また、C/C++に馴染みがない方でもMicroPythonで簡単に使えます。REPLが使えるため、プログラムファイルを作らなくても簡単に動作確認できるのは魅力的です。性能についても、CPUで重い計算をさせるのではなく、PIOなどのハードウェアを使用するだけであればMicroPythonのオーバヘッドはほとんど無視できると思います。ただ、MicroPythonでAPI実装されていない機能は自前で制御レジスタにアクセスする必要があるので、自前で細かい制御をやりたくなかったり、より性能を求めるならC/C++などの他言語に移る必要があります。

参考リンク

https://docs.micropython.org/en/latest/library/rp2.html
https://datasheets.raspberrypi.com/pico/pico-datasheet.pdf
https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
C/C++のSDKドキュメントにもPIOの説明があります。
https://datasheets.raspberrypi.com/pico/raspberry-pi-pico-c-sdk.pdf

Discussion