class DMA -- RP2040 の DMA コントローラへのアクセス

DMA クラスは RP2040 のダイレクトメモリアクセス(DMA)コントローラへのアクセスを提供し、メモリブロック間や IO レジスタ間でデータを移動する機能を提供します。DMA コントローラはバスファブリックに対して独自の独立した読み取り用および書き込み用のバスマスタ接続を持ち、各 DMA チャネルは独立して1つのアドレスからデータを読み取り、別のアドレスに書き戻すことができ、一方または両方のポインタをオプションでインクリメントできます。これにより、プロセッサが他のタスクを実行したり低電力状態に入ったりしている間に、プロセッサに代わって転送を実行できます。RP2040 の DMA コントローラには、同時に動作可能な12個の独立した DMA チャネルがあります。RP2040 の DMA システムの詳細については、RP2040 データシート のセクション2.5を参照してください。

DMA コントローラの最も単純な使い方は、あるメモリブロックから別のメモリブロックへデータを移動することです。これは以下のコードで実現できます:

a = bytearray(32*1024)
b = bytearray(32*1024)
d = rp2.DMA()
c = d.pack_ctrl()  # Just use the default control value.
# The count is in 'transfers', which defaults to four-byte words, so divide length by 4
d.config(read=a, write=b, count=len(a)//4, ctrl=c, trigger=True)
# Wait for completion
while d.active():
    pass

この例では、転送の完了を待つ間アイドルループに入っていますが、プログラムはこの時間を使って代わりに何か有用な処理を行うこともできることに注意してください。

DMA コントローラのもう1つの、おそらくより一般的な使い方は、メモリと IO ペリフェラルの間で転送することです。この状況では、IO レジスタのアドレスは各転送ごとに変化しませんが、メモリアドレスはインクリメントする必要があります。また、ペリフェラルが受け入れる前にデータを書き込んだり、データが準備できる前に読み取ったりしないように、転送のペースを制御する必要があります。これは DMA チャネルの制御レジスタの treq_sel フィールドで制御できます。各 DMA チャネルの制御レジスタの各種フィールドは DMA.pack_ctrl() メソッドを使用してパックでき、DMA.unpack_ctrl() 静的メソッドを使用してアンパックできます。バイト配列から PIO ステートマシンの TX FIFO へ1バイトずつデータを転送するコードは次のようになります:

# pio_num is index of the PIO block being used, sm_num is the state machine in that block.
# my_state_machine is an rp2.PIO() instance.
DATA_REQUEST_INDEX = (pio_num << 3) + sm_num

src_data = bytearray(1024)
d = rp2.DMA()

# Transfer bytes, rather than words, don't increment the write address and pace the transfer.
c = d.pack_ctrl(size=0, inc_write=False, treq_sel=DATA_REQUEST_INDEX)

d.config(
    read=src_data,
    write=my_state_machine,
    count=len(src_data),
    ctrl=c,
    trigger=True
)

この例では、書き込みアドレスとして与えられている値が、データを送信している PIO ステートマシンそのものであることに注意してください。これが機能するのは、PIO ステートマシンがバッファプロトコルを提供しており、そのデータ FIFO レジスタへの直接アクセスを可能にしているためです。

コンストラクタ

class rp2.DMA

DMA コントローラのチャネルの1つを排他使用のために確保します。

config(read: 'int | _AnyReadableBuf | None' = None, write: 'int | _AnyWritableBuf | None' = None, count: int | None = None, ctrl: int | None = None, trigger: bool = False) None

チャネルの DMA レジスタを設定し、オプションで転送を開始します。パラメータは次のとおりです:

  • read: DMA コントローラが読み取りを開始するアドレス、または読み取るデータを提供するオブジェクトです。整数またはバッファプロトコルをサポートする任意のオブジェクトを指定できます。

  • write: DMA コントローラが書き込みを開始するアドレス、またはデータが書き込まれるオブジェクトです。整数またはバッファプロトコルをサポートする任意のオブジェクトを指定できます。

  • count: このチャネルが停止するまでに実行されるバス転送の回数です。これは転送の回数であり、バイト数ではないことに注意してください。転送幅が2バイトまたは4バイトの場合、移動されるデータの総量(したがって必要なバッファのサイズ)はそれに応じて掛け合わせる必要があります。

  • ctrl: DMA 制御レジスタの値です。これは通常 DMA.pack_ctrl() を使用してパックされる整数値です。

  • trigger: オプションで、転送をただちに開始します。

irq(handler: Callable[[DMA], None] | None = None, hard: bool = False) Callable

この DMA チャネルの IRQ オブジェクトを返し、オプションでそれを設定します。

close() None

基となる DMA チャネルの確保を解放し、割り込みハンドラを解放します。この操作の後、DMA オブジェクトは使用できなくなります。

pack_ctrl(default: int | None = None, **kwargs) int

キーワード引数で指定された値を、新しい制御レジスタ値の名前付きフィールドにパックします。指定されなかったフィールドはデフォルト値に設定されます。デフォルトは、指定された default 値から取られるか、それが指定されていない場合は現在のチャネルに適したデフォルトになります。これを DMA.ctrl 属性の現在の値に設定すると、フィールドのサブセットを簡単にオーバーライドできます。

キーワード引数のキーには、DMA.unpack_ctrl() メソッドが返す任意のキーを指定できます。書き込み可能な値は次のとおりです:

  • enable: bool チャネルを有効にするために設定します(デフォルト: True)。

  • high_pri: bool このチャネルのバストラフィックを高優先度にします(デフォルト: False)。

  • size: int 転送サイズ: 0=バイト、1=ハーフワード、2=ワード(デフォルト: 2)。

  • inc_read: bool 各転送の後に読み取りアドレスをインクリメントします(デフォルト: True)。

  • inc_write: bool 各転送の後に書き込みアドレスをインクリメントします(デフォルト: True)。

  • ring_size: int 0以外の場合、アドレスがインクリメントされる際に一方のアドレスレジスタの下位 ring_size ビットのみが変化し、次の 1 << ring_size バイト境界でアドレスがラップします。どちらのアドレスがラップするかは ring_sel フラグで制御されます。値が0の場合はアドレスのラップが無効になります。

  • ring_sel: bool ring_size を読み取りアドレスに適用するには False に、書き込みアドレスに適用するには True に設定します。

  • chain_to: int この転送が完了した後にトリガするチャネルのチャネル番号です。この値をこの DMA オブジェクト自身のチャネル番号に設定すると、チェーンが無効になります(これがデフォルトです)。

  • treq_sel: int 転送リクエスト信号を選択します。詳細については RP2040 データシートのセクション2.5.3を参照してください。

  • irq_quiet: bool 各転送の終了時に割り込みを生成しません。代わりに、トリガレジスタに0の値が書き込まれたときに割り込みが生成され、これによってチェーンされた一連の転送が停止します(デフォルト: True)。

  • bswap: bool true に設定すると、書き込み前にワードまたはハーフワード内のバイトが反転されます(デフォルト: True)。

  • sniff_en: bool チップの sniff ハードウェアによるデータアクセスを許可するには True に設定します(デフォルト: False)。

  • write_err: bool これを True に設定すると、以前に報告された書き込みエラーがクリアされます。

  • read_err: bool これを True に設定すると、以前に報告された読み取りエラーがクリアされます。

これらすべてのフィールドの詳細については、RP2040 データシートのセクション2.5.7の CH0_CTRL_TRIG レジスタの説明を参照してください。

unpack_ctrl(value: int) dict

DMA チャネル制御レジスタの値をアンパックし、制御レジスタの各フィールドのキー/値ペアを持つ辞書にします。value はアンパックする ctrl レジスタ値です。

このメソッドは、DMA.pack_ctrl に渡すことができるすべてのキーの値を返します。さらに、制御レジスタ内の読み取り専用フラグも返します。busy は転送が開始すると high になり終了すると low になります。ahb_errread_err フラグと write_err フラグの論理 OR です。これらの値はパック時には無視されるため、制御レジスタをアンパックして作成された辞書を、そのままパック用のキーワード引数として使用できます。

active(value: bool | None = None, /) bool

DMA チャネルが現在実行中かどうかを取得または設定します。

>>> sm.active()
0
>>> sm.active(1)
>>> while sm.active():
...     pass
read: int

この属性は、次のバス転送が読み取るアドレスを反映します。整数またはバッファプロトコルをサポートするオブジェクトのいずれかで書き込むことができ、書き込むとただちに反映されます。

write: int

この属性は、次のバス転送が書き込むアドレスを反映します。整数またはバッファプロトコルをサポートするオブジェクトのいずれかで書き込むことができ、書き込むとただちに反映されます。

count: int

この属性を読み取ると、現在の転送シーケンスにおける残りのバス転送の回数が返されます。この属性に書き込むと、の転送シーケンスの転送の総回数が設定されます。

ctrl: int

この属性は DMA チャネル制御レジスタを反映します。通常は DMA.pack_ctrl() メソッドを使用してパックされた整数で書き込まれます。返されたレジスタ値は DMA.unpack_ctrl() メソッドを使用してアンパックできます。

channel: int

DMA チャネルのチャネル番号です。これは別のチャネルの DMA.pack_ctrl()chain_to 引数に渡すことで、DMA チェーンを可能にできます。

registers: 'memoryview'

この属性は、DMA チャネルのレジスタへの直接アクセスを可能にする配列ライクなオブジェクトです。インデックスはバイト単位ではなくワード単位であるため、レジスタのインデックスはレジスタアドレスのオフセットを4で割った値になります。レジスタの詳細については RP2040 データシートを参照してください。

チェーンとトリガレジスタへのアクセス

RP2040 の DMA コントローラは、1つの DMA チャネルが別のチャネルの転送を開始できるようにするいくつかの高度な機能を提供します。1つは制御レジスタの chain_to 値の使用であり、もう1つはトリガ効果を持つ DMA チャネルのレジスタの1つへの書き込みです。1つの DMA チャネルが別のチャネルの DMA.registers に直接書き込める機能と組み合わせると、CPU の介入なしに複雑なトランザクションを実行できます。

以下は、チェーンとレジスタトリガの両方を使用して、複数のデータブロックを単一の宛先に集約する処理を実装する例です。これらの機能の詳細については RP2040 データシートのセクション2.5を参照してください。以下のコードはサブセクション2.5.6.2の例を Python 的に書き直したものです。

from rp2 import DMA
from uctypes import addressof
from array import array

def gather_strings(string_list, buf):
    # We use two DMA channels. The first sends lengths and source addresses from the gather
    # list to the registers of the second. The second copies the data itself.
    gather_dma = DMA()
    buffer_dma = DMA()

    # Pack up length/address pairs to be sent to the registers.
    gather_list = array("I")

    for s in string_list:
        gather_list.append(len(s))
        gather_list.append(addressof(s))

    gather_list.append(0)
    gather_list.append(0)

    # When writing to the registers of the second DMA channel, we need to wrap the
    # write address on an 8-byte (1<<3 bytes) boundary. We write to the ``TRANS_COUNT``
    # and ``READ_ADD_TRIG`` registers in the last register alias (registers 14 and 15).
    gather_ctrl = gather_dma.pack_ctrl(ring_size=3, ring_sel=True)
    gather_dma.config(
        read=gather_list, write=buffer_dma.registers[14:16],
        count=2, ctrl=gather_ctrl
    )

    # When copying the data, the transfer size is single bytes, and when completed we need
    # to chain back to the start another gather DMA transaction.
    buffer_ctrl = buffer_dma.pack_ctrl(size=0, chain_to=gather_dma.channel)
    # The read and count values will be set by the other DMA channel.
    buffer_dma.config(write=buf, ctrl=buffer_ctrl)

    # Set the transfer in motion.
    gather_dma.active(1)

    # Wait until all the register values have been sent
    end_address = addressof(gather_list) + 4 * len(gather_list)
    while gather_dma.read != end_address:
        pass

input = ["This is ", "a ", "test", " of the scatter", " gather", " process"]
output = bytearray(64)

print(output)
gather_strings(input, output)
print(output)

この例は転送の完了を待つ間アイドル状態になります。代わりに割り込みハンドラを設定してただちにリターンすることもできます。