RPi Pico PIO + DMA 転送覚書 (MicroPython 版)

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 = 0x50200000
  • PIO1_BASE = 0x50300000

(T|R)XFy

(T|R)XFy は、PIO の TX_FIFO/RX_FIFO のレジスタ。Chapter 3. PIO 3.7. List of Registers にて以下定義がなされている。

OffsetName
0x010TXF0
0x014TXF1
0x018TXF2
0x01CTXF3
0x020RXF0
0x024RXF1
0x028RXF2
0x02CRXF3

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#

PIOSMPORTDREQ#Description
00TX0PIO0 SM0 TX DREQ
00TX1PIO0 SM1 TX DREQ
00TX2PIO0 SM2 TX DREQ
00TX3PIO0 SM3 TX DREQ
00RX4PIO0 SM0 RX DREQ
00RX5PIO0 SM1 RX DREQ
00RX6PIO0 SM2 RX DREQ
00RX7PIO0 SM3 RX DREQ
10TX8PIO1 SM0 TX DREQ
10TX9PIO1 SM1 TX DREQ
10TX10PIO1 SM2 TX DREQ
10TX11PIO1 SM3 TX DREQ
10RX12PIO1 SM0 RX DREQ
10RX13PIO1 SM1 RX DREQ
10RX14PIO1 SM2 RX DREQ
10RX15PIO1 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() や園周辺を読むと概ねやりたいことは記載があるはず。先に示した例を下に改造・改良していくことを推奨。

MicroPython library - rp2.DMA

終わりに

DMA.configread, write に Peripheral が来るケースでの指定がいまいち読み取りづらかったので書き残した。

Reference

Docker で riscv-tests をビルドする

(ビルドが長くて) 地味に時間がかかったので、今後時間を溶かさないように書き残す。

手順

Dockerfile で riscv-gnu-toolchain 環境を用意

これはほぼ公式手順を Dockerfile に書き起こすだけで良い。ARCH, ABI などは好みのものに変更する。

FROM ubuntu:24.04

WORKDIR /app

# riscv-gnu-toolchainのリポジトリを取得
RUN apt update && apt install -y git
RUN git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

ENV RISCV=/opt/riscv
ENV ARCH=rv32ima
ENV ABI=ilp32
ENV PATH=$RISCV/bin:$PATH
ENV TOOLCHAIN_CONFIGURE="--prefix=$RISCV --with-arch=$ARCH --with-abi=$ABI --enable-multilib --enable-qemu-system"
ENV DEBIAN_FRONTEND=noninteractive

# 必要なパッケージをインストール
RUN apt install -y \
    autoconf \
    automake \
    autotools-dev \
    curl \
    python3 \
    python3-pip \
    python3-tomli \
    libmpc-dev \
    libmpfr-dev \
    libgmp-dev \
    gawk \
    build-essential \
    bison \
    flex \
    texinfo \
    gperf \
    libtool \
    patchutils \
    bc \
    zlib1g-dev \
    libexpat-dev \
    ninja-build \
    git \
    cmake \
    libglib2.0-dev \
    libslirp-dev

# riscv-gnu-toolchainのインストール
RUN cd riscv-gnu-toolchain && \
    ./configure $TOOLCHAIN_CONFIGURE && \
    make -j$(nproc)

CMD ["bash"]

上記 Dockerfile を使って riscv-tests をビルド

multi-stage build で riscv-tests を行っても良かったが、単にビルドするだけであること、自分が実装した別ソースをビルドすることなどを考え先の Image を利用するだけに留めることにした。

docker compose を用意し、成果物 Volume のマウント並びにビルドのワンライナーを用意する。 実装してある内容は riscv-tests の README 通りだが、今回は rv32 を対象にしているので XLEN を設定していること、ISA テスト用に make 対象を isa に絞っている。

services:
  build-riscv-tests:
    build:
      context: .
    volumes:
      - ./riscv-tests:/app/riscv-tests:rw
    command: >
      bash -c "
      cd /app/riscv-tests && 
      git clone --recursive https://github.com/riscv/riscv-tests && 
      cd riscv-tests && 
      autoconf && 
      ./configure --prefix=$RISCV/target && 
      make XLEN=32 isa
      "

使い方

docker compose run build-riscv-tests

その他

riscv-gnu-toolchain のその他のツールを使いたい

docker compose run build-riscv-tests bash で入った環境であれば、他のツールも使える。

$ docker compose run build-riscv-tests bash
root@460560b02bb5:/app# riscv32-unknown-elf-readelf -e -W riscv-tests/isa/rv32ui-p-addi
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           RISC-V
  Version:                           0x1
  Entry point address:               0x80000000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          9524 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         7
  Section header string table index: 6

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text.init        PROGBITS        80000000 001000 00047c 00  AX  0   0 64
  [ 2] .tohost           PROGBITS        80001000 002000 000048 00  WA  0   0 64
  [ 3] .riscv.attributes RISCV_ATTRIBUTES 00000000 002048 000063 00      0   0  1
  [ 4] .symtab           SYMTAB          00000000 0020ac 0002b0 10      5  37  4
  [ 5] .strtab           STRTAB          00000000 00235c 000198 00      0   0  1
  [ 6] .shstrtab         STRTAB          00000000 0024f4 000040 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), p (processor specific)

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  RISCV_ATTRIBUT 0x002048 0x00000000 0x00000000 0x00063 0x00000 R   0x1
  LOAD           0x001000 0x80000000 0x80000000 0x0047c 0x0047c R E 0x1000
  LOAD           0x002000 0x80001000 0x80001000 0x00048 0x00048 RW  0x1000

 Section to Segment mapping:
  Segment Sections...
   00     .riscv.attributes
   01     .text.init
   02     .tohost

RV64/32 の選択

  • Dockerfile の ARCHABI を変更する
  • make 時の XLEN を変更する
    • riscv-tests のデフォルトは XLEN=64 を使おうとしているので、rv32 向けの toolchain だと gcc などが見つからずエラーになる

Linker script の修正

riscv-tests/env が更に submodule になっており、ここを修正することで対応できる。 例えば p/link.ld は以下の具合

OUTPUT_ARCH( "riscv" )
ENTRY(_start)

SECTIONS
{
  . = 0x80000000;
  .text.init : { *(.text.init) }
  . = ALIGN(0x1000);
  .tohost : { *(.tohost) }
  . = ALIGN(0x1000);
  .text : { *(.text) }
  . = ALIGN(0x1000);
  .data : { *(.data) }
  .bss : { *(.bss) }
  _end = .;
}

この他にも riscv_test.h にテストの各マクロの定義があるので、動作確認向けに変更することもできる。 例えば Pass/Fail は、Pass の場合

//-----------------------------------------------------------------------
// Pass/Fail Macro
//-----------------------------------------------------------------------

#define RVTEST_PASS                                                     \
        fence;                                                          \
        li TESTNUM, 1;                                                  \
        li a7, 93;                                                      \
        li a0, 0;                                                       \
        ecall

#define TESTNUM gp
#define RVTEST_FAIL                                                     \
        fence;                                                          \
1:      beqz TESTNUM, 1b;                                               \
        sll TESTNUM, TESTNUM, 1;                                        \
        or TESTNUM, TESTNUM, 1;                                         \
        li a7, 93;                                                      \
        addi a0, TESTNUM, 0;                                            \
        ecall

disassemble が見たい

付属ツールで disasm 出力しても良かったが、make 時に一緒に生成されていた。 <elf binary>.dump がそれにあたる。

rv32ui-p-addi.dump

rv32ui-p-addi:     file format elf32-littleriscv


Disassembly of section .text.init:

80000000 <_start>:
80000000:	0500006f          	j	80000050 <reset_vector>

80000004 <trap_vector>:
80000004:	34202f73          	csrr	t5,mcause
80000008:	00800f93          	li	t6,8
8000000c:	03ff0863          	beq	t5,t6,8000003c <write_tohost>

...


800003fc <test_25>:
800003fc:	01900193          	li	gp,25
80000400:	02100093          	li	ra,33
80000404:	03208013          	addi	zero,ra,50
80000408:	00000393          	li	t2,0
8000040c:	00701463          	bne	zero,t2,80000414 <fail>
80000410:	02301063          	bne	zero,gp,80000430 <pass>

80000414 <fail>:
80000414:	0ff0000f          	fence
80000418:	00018063          	beqz	gp,80000418 <fail+0x4>
8000041c:	00119193          	slli	gp,gp,0x1
80000420:	0011e193          	ori	gp,gp,1
80000424:	05d00893          	li	a7,93
80000428:	00018513          	mv	a0,gp
8000042c:	00000073          	ecall

80000430 <pass>:
80000430:	0ff0000f          	fence
80000434:	00100193          	li	gp,1
80000438:	05d00893          	li	a7,93
8000043c:	00000513          	li	a0,0
80000440:	00000073          	ecall
80000444:	c0001073          	unimp
80000448:	0000                	.insn	2, 0x
8000044a:	0000                	.insn	2, 0x
8000044c:	0000                	.insn	2, 0x
8000044e:	0000                	.insn	2, 0x
80000450:	0000                	.insn	2, 0x
80000452:	0000                	.insn	2, 0x
80000454:	0000                	.insn	2, 0x
80000456:	0000                	.insn	2, 0x
80000458:	0000                	.insn	2, 0x
8000045a:	0000                	.insn	2, 0x
8000045c:	0000                	.insn	2, 0x
8000045e:	0000                	.insn	2, 0x
80000460:	0000                	.insn	2, 0x
80000462:	0000                	.insn	2, 0x
80000464:	0000                	.insn	2, 0x
80000466:	0000                	.insn	2, 0x
80000468:	0000                	.insn	2, 0x
8000046a:	0000                	.insn	2, 0x
8000046c:	0000                	.insn	2, 0x
8000046e:	0000                	.insn	2, 0x
80000470:	0000                	.insn	2, 0x
80000472:	0000                	.insn	2, 0x
80000474:	0000                	.insn	2, 0x
80000476:	0000                	.insn	2, 0x
80000478:	0000                	.insn	2, 0x
8000047a:	0000                	.insn	2, 0x

LiteX を Ubuntu 24.04 on WSL2 で使用し、 Demo Applicationを動作させる

LiteX を Windows 環境で使用したく、試していたときの備忘録。以下の通り無事 demo app の動作確認までたどり着けたので書き残す。

movie gif


手順まとめ

時系列順に実行した内容のまとめを先に記載する。細かいトラブルシューティングは後半に記載。

環境構築

基本手順は公式ドキュメントの Quick Start Guideに従い、WSL2 上にインストールした Ubuntu 24.04 で行う。 要点は以下。

  • virtualenv を作成し、Python 仮想環境中で実行する
  • virtualenv 中で user install を実行するとエラーになるため、 --user オプションを使用しない
  • litex の repo ごと取得した状態で litex_setup.py を実行しない
# verilator導入
$ sudo apt install verilator

# litexの取得
$ wget https://raw.githubusercontent.com/enjoy-digital/litex/master/litex_setup.py
$ chmod +x litex_setup.py

# venv導入
$ sudo apt install python3-venv
# 仮想環境作成 + activate
$ python3 -m venv .venv
$ source .venv/bin/activate

# litex_setup.py の実行
$ python3 ./litex_setup.py --init --install --config=full

# RSC-V toolchain の導入 (binutils-riscv* 等が導入されるので管理者権限必要)
$ pip3 install meson ninja
$ sudo ./litex_setup.py --gcc=riscv

実機無しでのテスト

ハマりポイントは特になし。verilator があれば動くはず。

Read more  ↩︎

VRChatで動くボードゲームを作る

掲題の通り、VRChat 上で動作するボードゲームを先日公開した。自分自身の備忘録また同様の制作を行いたく情報を探している人向けの制作記録として機能することを願って作業記録を残すことにする。

ss


作るもの

Knucklebones(ナックルボーン)と呼ばれるボードゲームを作ることにした。本家はCult of the Lamb 作中に登場するゲームであり、今回の制作は非公式ファンアートの位置づけである。

サイコロを振って並べるという単純明快なルールでありながら、駆け引き要素がよくできたゲームである。(高得点を狙うほど、相手から一網打尽にされるリスクが上がっていく) もちろん、Knucklebones だけでなくゲーム自体の完成度もすばらしいのでぜひ遊んでみてほしい。

制作手順

おおよそ以下の手順で進めた。あくまでいち参考として見ていただけるとありがたい。

  1. ゲームシステム試作
  2. モデリング
  3. VCC 配布可能な Unity Project 作成
  4. モデルの Unity Setup
  5. ゲームロジック 実装
  6. 動作確認
  7. サンプルワールド作成
  8. 配布準備
Read more  ↩︎