MicroPython を用いて Raspberry Pi Pico の PIO と DMA を使う方法についての覚書。それぞれの情報は入手できるが、TX_FIFO/RX_FIFO 両方に DMA する例がなかったので書き残す。
本内容は MicroPython のドキュメント並びに RP2040 の仕様書から抜粋した内容となっている。
HW 上達成すべきこと
PIOx_BASE + (T|R)XFy のアドレスに読み書きを行うこと。
以下内容より PIO Address x= (0|1) と
(T|R)XFy のアドレス y= (0|1|2|3) を組み合わせて、PIO の TX_FIFO/RX_FIFO にアクセスする。
PIOx_BASE
PIOx_BASE は、PIO の Base Address。 2.2. Address Map にて以下定義がなされている。
PIO0_BASE=0x50200000PIO1_BASE=0x50300000
(T|R)XFy
(T|R)XFy は、PIO の TX_FIFO/RX_FIFO のレジスタ。Chapter 3. PIO 3.7. List of Registers にて以下定義がなされている。
| Offset | Name | 
|---|---|
| 0x010 | TXF0 | 
| 0x014 | TXF1 | 
| 0x018 | TXF2 | 
| 0x01C | TXF3 | 
| 0x020 | RXF0 | 
| 0x024 | RXF1 | 
| 0x028 | RXF2 | 
| 0x02C | RXF3 | 
FIFO の空きに関連する情報は Offset 0x008 FDEBUG レジスタの TXSTALL, TXOVER, RXUNDER, RXSTALL 及び FLEVEL の (T|R)Xyから確認できる。
DMA における FIFO Status の確認
DMA が一方的にデータ転送しても FIFO にデータがないのに読み出したり、あるのに書き込んで溢れてしまう。これを防ぐため、ペリフェラル(ここでは PIO)のペースでデータ転送を行えるようなしくみがあり、 Data Request (DREQ) と呼ばれる設定を行う。
2.5.3.1 System DREQ Table の設定があり、PIO に関連する内容は以下のようになっている。
DREQ# = PIO# * 4 + (4 if RX else 0) + SM#
| PIO | SM | PORT | DREQ# | Description | 
|---|---|---|---|---|
| 0 | 0 | TX | 0 | PIO0 SM0 TX DREQ | 
| 0 | 0 | TX | 1 | PIO0 SM1 TX DREQ | 
| 0 | 0 | TX | 2 | PIO0 SM2 TX DREQ | 
| 0 | 0 | TX | 3 | PIO0 SM3 TX DREQ | 
| 0 | 0 | RX | 4 | PIO0 SM0 RX DREQ | 
| 0 | 0 | RX | 5 | PIO0 SM1 RX DREQ | 
| 0 | 0 | RX | 6 | PIO0 SM2 RX DREQ | 
| 0 | 0 | RX | 7 | PIO0 SM3 RX DREQ | 
| 1 | 0 | TX | 8 | PIO1 SM0 TX DREQ | 
| 1 | 0 | TX | 9 | PIO1 SM1 TX DREQ | 
| 1 | 0 | TX | 10 | PIO1 SM2 TX DREQ | 
| 1 | 0 | TX | 11 | PIO1 SM3 TX DREQ | 
| 1 | 0 | RX | 12 | PIO1 SM0 RX DREQ | 
| 1 | 0 | RX | 13 | PIO1 SM1 RX DREQ | 
| 1 | 0 | RX | 14 | PIO1 SM2 RX DREQ | 
| 1 | 0 | RX | 15 | PIO1 SM3 RX DREQ | 
MicroPython での実装
DMA class, PIO class, StateMachine class を使って、PIO の TX_FIFO/RX_FIFO に DMA 転送を行う。
入力されたデータを Echo する PIO プログラムを実行し、TX_FIFO と RX_FIFO の両方に DMA 転送を行う例を示す。
# 送受信するデータ
tx_data = array.array("I", [0x12345678, 0x9ABCDEF0, 0xDEADBEEF, 0xFEEDFACE]) # test data
rx_data = array.array("I", [0] * len(tx_data))
# PIO プログラムの定義
@rp2.asm_pio(
    # out, sideset, shift方向, autopush等の設定がここで行える
)
def pio_echoback():
    # PIO asm本体。ここではTX_FIFOの内容をRX_FIFOにそのまま返す
    wrap_target()
    pull(block)
    out(x, 32)
    in_(x, 32)
    push(block)
    wrap()
# PIO0 State Machine 0を起動
sm0 = rp2.StateMachine(0)
sm0.init(
    prog=pio_echoback,
    freq=125_000_000,  # 125MHz
    # pin設定, 周波数設定等の設定が行える
)
sm0.active(1)
# Setup TX DMA
tx_dma0 = rp2.DMA()
tx_dma0_ctrl = tx_dma0.pack_ctrl(
    size=2,  # 0=1byte, 1=2byte, 2=4byte転送
    inc_read=True,
    inc_write=False,  # PIO0 TXF0は場所固定なのでincrementしない
    treq_sel=0,  # PIO0 SM0 TX DREQ
)
tx_dma0.config(
    read=tx_data,  # 転送元データをそのまま渡す
    write=sm0,  # state machine をそのまま渡す
    count=len(
        tx_data
    ),  # 転送byte数ではなく、pack_ctrlのsizeで指定した単位での転送数なので注意
    ctrl=tx_dma0_ctrl,
    trigger=True,  # 開始
)
# Setup RX DMA
rx_dma0 = rp2.DMA()
rx_dma0_ctrl = rx_dma0.pack_ctrl(
    size=2,  # 0=1byte, 1=2byte, 2=4byte転送
    inc_read=False,  # PIO0 RXF0は場所固定なのでincrementしない
    inc_write=True,
    treq_sel=4,  # PIO0 SM0 RX DREQ
)
rx_dma0.config(
    read=sm0,  # state machine をそのまま渡す
    write=rx_data,  # 転送先データをそのまま渡す
    count=len(
        tx_data
    ),  # 転送byte数ではなく、pack_ctrlのsizeで指定した単位での転送数なので注意
    ctrl=rx_dma0_ctrl,
    trigger=True,
)
# 転送完了待ち
while rx_dma0.active():
    pass
# 結果確認
for i in range(len(rx_data)):
    print(f"TX Data[{i}]: {tx_data[i]:#010x}, RX Data[{i}]: {rx_data[i]:#010x}")
    assert tx_data[i] == rx_data[i], (
        f"Data mismatch at index {i}: {tx_data[i]:#010x} != {rx_data[i]:#010x}"
    )
# 終了処理
sm0.active(0)
tx_dma0.close()
rx_dma0.close()
出力
TX Data[0]: 0x12345678, RX Data[0]: 0x12345678
TX Data[1]: 0x9abcdef0, RX Data[1]: 0x9abcdef0
TX Data[2]: 0xdeadbeef, RX Data[2]: 0xdeadbeef
TX Data[3]: 0xfeedface, RX Data[3]: 0xfeedface
Tips
byteswap したい
pio program 中でエンディアンが入れ替わってしまうケースなどを想定。BSWAP option があり、これは DMA の設定で行える
# Setup RX DMA
rx_dma0 = rp2.DMA()
rx_dma0_ctrl = rx_dma0.pack_ctrl(
    size=2,  # 0=1byte, 1=2byte, 2=4byte転送
    inc_read=False,  # PIO0 RXF0は場所固定なのでincrementしない
    inc_write=True,
    bswap=True,  # 受信データはBig Endianなので、バイトオーダーを反転
    treq_sel=4,  # PIO0 SM0 RX DREQ
)
実行結果
TX Data[0]: 0x12345678, RX Data[0]: 0x78563412
Traceback (most recent call last):
  File "<stdin>", line 444, in <module>
AssertionError: Data mismatch at index 0: 0x12345678 != 0x78563412
1byte ずつ転送したい
size=0 を指定することで、1byte ずつ転送できる。転送カウント数に注意
# Setup TX DMA
tx_dma0 = rp2.DMA()
tx_dma0_ctrl = tx_dma0.pack_ctrl(
    size=0,  # 0=1byte, 1=2byte, 2=4byte転送
    inc_read=True,
    inc_write=False,  # PIO0 TXF0は場所固定なのでincrementしない
    treq_sel=0,  # PIO0 SM0 TX DREQ
)
tx_dma0.config(
    read=tx_data,  # 転送元データをそのまま渡す
    write=sm0,  # state machine をそのまま渡す
    count=len(tx_data) * 4,  # 転送byte数ではなく、pack_ctrlのsizeで指定した単位での転送数なので注意
    ctrl=tx_dma0_ctrl,
    trigger=True,  # 開始
)
# RX側も同様に修正
結果
TX Data[0]: 0x12345678, RX Data[0]: 0x12345678
TX Data[1]: 0x9abcdef0, RX Data[1]: 0x9abcdef0
TX Data[2]: 0xdeadbeef, RX Data[2]: 0xdeadbeef
TX Data[3]: 0xfeedface, RX Data[3]: 0xfeedface
sniff したい、chain したい、...
DMA.pack_ctrl() や園周辺を読むと概ねやりたいことは記載があるはず。先に示した例を下に改造・改良していくことを推奨。
終わりに
DMA.config の read, write に Peripheral が来るケースでの指定がいまいち読み取りづらかったので書き残した。

