klasa DMA – dostęp do kontrolera DMA układu RP2040

Klasa DMA oferuje dostęp do kontrolera bezpośredniego dostępu do pamięci (DMA) układu RP2040, umożliwiając przenoszenie danych między blokami pamięci i/lub rejestrami IO. Kontroler DMA ma własne, oddzielne połączenia magistrali nadrzędnej do odczytu i zapisu w strukturze magistrali, a każdy kanał DMA może niezależnie odczytywać dane z jednego adresu i zapisywać je pod inny adres, opcjonalnie inkrementując jeden lub oba wskaźniki, co pozwala mu wykonywać transfery w imieniu procesora, podczas gdy procesor realizuje inne zadania lub przechodzi w stan niskiego poboru mocy. Kontroler DMA układu RP2040 ma 12 niezależnych kanałów DMA, które mogą działać współbieżnie. Pełne szczegóły dotyczące systemu DMA układu RP2040 znajdują się w sekcji 2.5 dokumentu RP2040 Datasheet.

Przykłady

Najprostszym zastosowaniem kontrolera DMA jest przeniesienie danych z jednego bloku pamięci do drugiego. Można to osiągnąć za pomocą następującego kodu:

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

Należy zauważyć, że choć w tym przykładzie program znajduje się w pętli bezczynności, czekając na zakończenie transferu, mógłby równie dobrze wykonywać w tym czasie jakąś użyteczną pracę.

Innym, być może częstszym zastosowaniem kontrolera DMA jest transfer pomiędzy pamięcią a urządzeniem peryferyjnym IO. W takiej sytuacji adres rejestru IO nie zmienia się dla każdego transferu, ale adres pamięci musi być inkrementowany. Konieczne jest również kontrolowanie tempa transferu, aby nie zapisywać danych, zanim mogą zostać przyjęte przez urządzenie peryferyjne, ani nie odczytywać ich, zanim dane będą gotowe; można to kontrolować za pomocą pola treq_sel rejestru kontrolnego kanału DMA. Poszczególne pola rejestru kontrolnego każdego kanału DMA można spakować metodą DMA.pack_ctrl() i rozpakować statyczną metodą DMA.unpack_ctrl(). Kod przenoszący dane z tablicy bajtów do FIFO TX maszyny stanów PIO, po jednym bajcie naraz, wygląda następująco:

# 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
)

Należy zauważyć, że w tym przykładzie wartością podaną jako adres zapisu jest po prostu maszyna stanów PIO, do której wysyłamy dane. Działa to dlatego, że maszyny stanów PIO udostępniają protokół buforowy, umożliwiając bezpośredni dostęp do swoich rejestrów FIFO danych.

Konstruktor

class rp2.DMA

Rezerwuje jeden z kanałów kontrolera DMA do wyłącznego użytku.

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

Konfiguruje rejestry DMA dla kanału i opcjonalnie rozpoczyna transfer. Parametry to:

  • read: Adres, od którego kontroler DMA zacznie odczytywać dane, lub obiekt dostarczający dane do odczytu. Może to być liczba całkowita lub dowolny obiekt obsługujący protokół buforowy.

  • write: Adres, pod który kontroler DMA zacznie zapisywać dane, lub obiekt, do którego dane będą zapisywane. Może to być liczba całkowita lub dowolny obiekt obsługujący protokół buforowy.

  • count: Liczba transferów magistrali, które wykonają się, zanim ten kanał się zatrzyma. Należy zauważyć, że jest to liczba transferów, a nie liczba bajtów. Jeśli transfery mają szerokość 2 lub 4 bajtów, całkowita ilość przenoszonych danych (a tym samym rozmiar wymaganego bufora) musi zostać odpowiednio przemnożona.

  • ctrl: Wartość rejestru kontrolnego DMA. Jest to wartość całkowita, którą zwykle pakuje się za pomocą DMA.pack_ctrl().

  • trigger: Opcjonalnie rozpoczyna transfer natychmiast.

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

Zwraca obiekt IRQ dla tego kanału DMA i opcjonalnie go konfiguruje.

close() None

Zwalnia rezerwację bazowego kanału DMA i uwalnia procedurę obsługi przerwań. Obiekt DMA nie może być używany po tej operacji.

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

Pakuje wartości podane w argumentach nazwanych do nazwanych pól nowej wartości rejestru kontrolnego. Każde pole, które nie zostanie podane, zostanie ustawione na wartość domyślną. Wartość domyślna zostanie pobrana z podanej wartości default, a jeśli jej nie podano, zostanie użyta wartość domyślna odpowiednia dla bieżącego kanału; ustawienie jej na bieżącą wartość atrybutu DMA.ctrl stanowi łatwy sposób na nadpisanie podzbioru pól.

Kluczami dla argumentów nazwanych mogą być dowolne klucze zwracane przez metodę DMA.unpack_ctrl(). Wartości zapisywalne to:

  • enable: bool Ustaw, aby włączyć kanał (domyślnie: True).

  • high_pri: bool Nadaje ruchowi magistrali tego kanału wysoki priorytet (domyślnie: False).

  • size: int Rozmiar transferu: 0=bajt, 1=półsłowo, 2=słowo (domyślnie: 2).

  • inc_read: bool Inkrementuje adres odczytu po każdym transferze (domyślnie: True).

  • inc_write: bool Inkrementuje adres zapisu po każdym transferze (domyślnie: True).

  • ring_size: int Jeśli wartość jest niezerowa, podczas inkrementowania adresu zmieniają się tylko najniższe bity ring_size jednego rejestru adresowego, co powoduje zawijanie adresu na następnej granicy 1 << ring_size bajtów. To, który adres jest zawijany, kontroluje flaga ring_sel. Wartość zero wyłącza zawijanie adresu.

  • ring_sel: bool Ustaw na False, aby ring_size dotyczyło adresu odczytu, lub na True, aby dotyczyło adresu zapisu.

  • chain_to: int Numer kanału, który ma zostać wyzwolony po zakończeniu tego transferu. Ustawienie tej wartości na numer kanału tego samego obiektu DMA wyłącza łańcuchowanie (jest to wartość domyślna).

  • treq_sel: int Wybiera sygnał żądania transferu (Transfer Request). Szczegóły znajdują się w sekcji 2.5.3 karty katalogowej RP2040.

  • irq_quiet: bool Nie generuje przerwania na końcu każdego transferu. Zamiast tego przerwania będą generowane, gdy do rejestru wyzwalającego zostanie zapisana wartość zero, co zatrzyma sekwencję połączonych łańcuchowo transferów (domyślnie: True).

  • bswap: bool Jeśli ustawione na true, bajty w słowach lub półsłowach zostaną odwrócone przed zapisem (domyślnie: True).

  • sniff_en: bool Ustaw na True, aby umożliwić dostęp do danych przez sprzęt podsłuchujący (sniff) układu (domyślnie: False).

  • write_err: bool Ustawienie na True wyczyści wcześniej zgłoszony błąd zapisu.

  • read_err: bool Ustawienie na True wyczyści wcześniej zgłoszony błąd odczytu.

Szczegóły wszystkich tych pól znajdują się w opisie rejestru CH0_CTRL_TRIG w sekcji 2.5.7 karty katalogowej RP2040.

unpack_ctrl(value: int) dict

Rozpakowuje wartość rejestru kontrolnego kanału DMA do słownika z parami klucz/wartość dla każdego z pól rejestru kontrolnego. value to wartość rejestru ctrl do rozpakowania.

Ta metoda zwróci wartości dla wszystkich kluczy, które można przekazać do DMA.pack_ctrl. Dodatkowo zwróci również flagi tylko do odczytu z rejestru kontrolnego: busy, która przyjmuje stan wysoki, gdy transfer się rozpoczyna, i niski, gdy się kończy, oraz ahb_err, która jest logiczną sumą (OR) flag read_err i write_err. Wartości te będą ignorowane podczas pakowania, dzięki czemu słownik utworzony przez rozpakowanie rejestru kontrolnego może być bezpośrednio użyty jako argumenty nazwane do pakowania.

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

Pobiera lub ustawia informację o tym, czy kanał DMA jest obecnie uruchomiony.

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

Ten atrybut odzwierciedla adres, z którego będzie czytał następny transfer magistrali. Może być zapisany liczbą całkowitą lub obiektem obsługującym protokół buforowy, co odnosi natychmiastowy skutek.

write: int

Ten atrybut odzwierciedla adres, pod który będzie zapisywał następny transfer magistrali. Może być zapisany liczbą całkowitą lub obiektem obsługującym protokół buforowy, co odnosi natychmiastowy skutek.

count: int

Odczyt tego atrybutu zwróci liczbę pozostałych transferów magistrali w bieżącej sekwencji transferu. Zapis tego atrybutu ustawia całkowitą liczbę transferów dla następnej sekwencji transferu.

ctrl: int

Ten atrybut odzwierciedla rejestr kontrolny kanału DMA. Jest zwykle zapisywany liczbą całkowitą spakowaną metodą DMA.pack_ctrl(). Zwróconą wartość rejestru można rozpakować metodą DMA.unpack_ctrl().

channel: int

Numer kanału DMA. Można go przekazać w argumencie chain_to metody DMA.pack_ctrl() na innym kanale, aby umożliwić łańcuchowanie DMA.

registers: 'memoryview'

Ten atrybut jest obiektem przypominającym tablicę, który umożliwia bezpośredni dostęp do rejestrów kanału DMA. Indeksowanie odbywa się słowami, a nie bajtami, więc indeksy rejestrów to przesunięcia adresów rejestrów podzielone przez 4. Szczegóły rejestrów znajdują się w karcie katalogowej RP2040.

Łańcuchowanie i dostęp do rejestrów wyzwalających

Kontroler DMA w układzie RP2040 oferuje kilka zaawansowanych funkcji pozwalających jednemu kanałowi DMA zainicjować transfer na innym kanale. Jedną z nich jest użycie wartości chain_to w rejestrze kontrolnym, a drugą jest zapis do jednego z rejestrów kanału DMA, który ma efekt wyzwalający. W połączeniu z możliwością bezpośredniego zapisu przez jeden kanał DMA do DMA.registers innego kanału, pozwala to na wykonywanie złożonych transakcji bez żadnej interwencji procesora.

Poniżej znajduje się przykład użycia zarówno łańcuchowania, jak i wyzwalania przez rejestr w celu zaimplementowania zbierania wielu bloków danych do jednego miejsca docelowego. Pełne szczegóły tych funkcji można znaleźć w sekcji 2.5 karty katalogowej RP2040, a poniższy kod jest pythoniczną wersją przykładu z podsekcji 2.5.6.2.

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)

Ten przykład pozostaje bezczynny, czekając na zakończenie transferu; alternatywnie mógłby ustawić procedurę obsługi przerwań i natychmiast powrócić.