class DMA – Zugriff auf den DMA-Controller des RP2040

Die Klasse DMA bietet Zugriff auf den Direct-Memory-Access-Controller (DMA) des RP2040 und ermöglicht es, Daten zwischen Speicherblöcken und/oder IO-Registern zu verschieben. Der DMA-Controller verfügt über eigene, separate Lese- und Schreib-Busmaster-Verbindungen zur Bus-Fabric, und jeder DMA-Kanal kann unabhängig Daten von einer Adresse lesen und sie an eine andere Adresse zurückschreiben, wobei optional einer oder beide Zeiger inkrementiert werden, sodass er Übertragungen im Namen des Prozessors durchführen kann, während der Prozessor andere Aufgaben erledigt oder in einen Energiesparzustand wechselt. Der DMA-Controller des RP2040 verfügt über 12 unabhängige DMA-Kanäle, die gleichzeitig laufen können. Vollständige Details zum DMA-System des RP2040 finden Sie in Abschnitt 2.5 des RP2040-Datenblatts.

Beispiele

Die einfachste Verwendung des DMA-Controllers besteht darin, Daten von einem Speicherblock in einen anderen zu verschieben. Dies kann mit dem folgenden Code erreicht werden:

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

Beachten Sie, dass dieses Beispiel zwar in einer Leerlaufschleife verharrt, während es auf den Abschluss der Übertragung wartet, das Programm aber in dieser Zeit ebenso gut nützliche Arbeit verrichten könnte.

Eine andere, vielleicht häufigere Verwendung des DMA-Controllers ist die Übertragung zwischen Speicher und einem IO-Peripheriegerät. In dieser Situation ändert sich die Adresse des IO-Registers bei jeder Übertragung nicht, aber die Speicheradresse muss inkrementiert werden. Es ist außerdem notwendig, das Tempo der Übertragung zu steuern, um keine Daten zu schreiben, bevor sie von einem Peripheriegerät angenommen werden können, oder sie zu lesen, bevor die Daten bereit sind; dies kann mit dem Feld treq_sel des Steuerregisters des DMA-Kanals gesteuert werden. Die verschiedenen Felder des Steuerregisters für jeden DMA-Kanal können mit der Methode DMA.pack_ctrl() gepackt und mit der statischen Methode DMA.unpack_ctrl() entpackt werden. Code, um Daten aus einem Byte-Array byteweise in die TX-FIFO einer PIO-State-Machine zu übertragen, sieht so aus:

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

Beachten Sie, dass in diesem Beispiel der für die Schreibadresse angegebene Wert einfach die PIO-State-Machine ist, an die wir die Daten senden. Dies funktioniert, weil PIO-State-Machines das Buffer-Protokoll bereitstellen und so direkten Zugriff auf ihre Daten-FIFO-Register ermöglichen.

Konstruktor

class rp2.DMA

Beansprucht einen der DMA-Controller-Kanäle zur exklusiven Nutzung.

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

Konfiguriert die DMA-Register für den Kanal und startet optional die Übertragung. Die Parameter sind:

  • read: Die Adresse, von der der DMA-Controller mit dem Lesen von Daten beginnt, oder ein Objekt, das die zu lesenden Daten bereitstellt. Es kann eine Ganzzahl oder ein beliebiges Objekt sein, das das Buffer-Protokoll unterstützt.

  • write: Die Adresse, an die der DMA-Controller mit dem Schreiben beginnt, oder ein Objekt, in das Daten geschrieben werden. Es kann eine Ganzzahl oder ein beliebiges Objekt sein, das das Buffer-Protokoll unterstützt.

  • count: Die Anzahl der Busübertragungen, die ausgeführt werden, bevor dieser Kanal stoppt. Beachten Sie, dass dies die Anzahl der Übertragungen ist, nicht die Anzahl der Bytes. Wenn die Übertragungen 2 oder 4 Byte breit sind, muss die Gesamtmenge der verschobenen Daten (und damit die Größe des benötigten Puffers) entsprechend multipliziert werden.

  • ctrl: Der Wert für das DMA-Steuerregister. Dies ist ein Ganzzahlwert, der typischerweise mit DMA.pack_ctrl() gepackt wird.

  • trigger: Startet die Übertragung optional sofort.

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

Gibt das IRQ-Objekt für diesen DMA-Kanal zurück und konfiguriert es optional.

close() None

Gibt den Anspruch auf den zugrunde liegenden DMA-Kanal frei und gibt den Interrupt-Handler frei. Das DMA-Objekt kann nach diesem Vorgang nicht mehr verwendet werden.

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

Packt die in den Schlüsselwortargumenten bereitgestellten Werte in die benannten Felder eines neuen Steuerregisterwerts. Jedes Feld, das nicht bereitgestellt wird, erhält einen Standardwert. Der Standardwert wird entweder dem bereitgestellten default-Wert entnommen oder, falls dieser nicht angegeben ist, ein für den aktuellen Kanal geeigneter Standardwert; das Setzen dieses Werts auf den aktuellen Wert des Attributs DMA.ctrl bietet eine einfache Möglichkeit, eine Teilmenge der Felder zu überschreiben.

Die Schlüssel für die Schlüsselwortargumente können beliebige Schlüssel sein, die von der Methode DMA.unpack_ctrl() zurückgegeben werden. Die beschreibbaren Werte sind:

  • enable: bool Setzen, um den Kanal zu aktivieren (Standard: True).

  • high_pri: bool Macht den Busverkehr dieses Kanals zu hoher Priorität (Standard: False).

  • size: int Übertragungsgröße: 0=Byte, 1=Halbwort, 2=Wort (Standard: 2).

  • inc_read: bool Inkrementiert die Leseadresse nach jeder Übertragung (Standard: True).

  • inc_write: bool Inkrementiert die Schreibadresse nach jeder Übertragung (Standard: True).

  • ring_size: int Wenn ungleich null, ändern sich beim Inkrementieren einer Adresse nur die untersten ring_size Bits eines Adressregisters, wodurch die Adresse an der nächsten 1 << ring_size-Byte-Grenze umbricht. Welche Adresse umbricht, wird durch das Flag ring_sel gesteuert. Ein Wert von null deaktiviert das Adress-Wrapping.

  • ring_sel: bool Auf False setzen, damit ring_size auf die Leseadresse angewendet wird, oder auf True, um es auf die Schreibadresse anzuwenden.

  • chain_to: int Die Kanalnummer eines Kanals, der nach Abschluss dieser Übertragung ausgelöst werden soll. Das Setzen dieses Werts auf die eigene Kanalnummer dieses DMA-Objekts deaktiviert die Verkettung (dies ist der Standard).

  • treq_sel: int Wählt ein Transfer-Request-Signal aus. Details finden Sie in Abschnitt 2.5.3 des RP2040-Datenblatts.

  • irq_quiet: bool Generiert am Ende jeder Übertragung keinen Interrupt. Stattdessen werden Interrupts generiert, wenn ein Wert von null in das Trigger-Register geschrieben wird, was eine Sequenz verketteter Übertragungen anhält (Standard: True).

  • bswap: bool Wenn auf true gesetzt, werden Bytes in Wörtern oder Halbwörtern vor dem Schreiben umgekehrt (Standard: True).

  • sniff_en: bool Auf True setzen, um den Zugriff auf die Daten durch die Sniff-Hardware des Chips zu erlauben (Standard: False).

  • write_err: bool Das Setzen auf True löscht einen zuvor gemeldeten Schreibfehler.

  • read_err: bool Das Setzen auf True löscht einen zuvor gemeldeten Lesefehler.

Eine Beschreibung aller dieser Felder finden Sie in der Beschreibung des Registers CH0_CTRL_TRIG in Abschnitt 2.5.7 des RP2040-Datenblatts.

unpack_ctrl(value: int) dict

Entpackt einen Wert für ein DMA-Kanal-Steuerregister in ein Dictionary mit Schlüssel/Wert-Paaren für jedes der Felder im Steuerregister. value ist der zu entpackende Wert des Registers ctrl.

Diese Methode gibt Werte für alle Schlüssel zurück, die an DMA.pack_ctrl übergeben werden können. Darüber hinaus gibt sie auch die schreibgeschützten Flags im Steuerregister zurück: busy, das hoch wird, wenn eine Übertragung beginnt, und niedrig, wenn sie endet, und ahb_err, das die logische ODER-Verknüpfung der Flags read_err und write_err ist. Diese Werte werden beim Packen ignoriert, sodass das durch Entpacken eines Steuerregisters erstellte Dictionary direkt als Schlüsselwortargumente für das Packen verwendet werden kann.

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

Liest oder setzt, ob der DMA-Kanal gerade läuft.

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

Dieses Attribut spiegelt die Adresse wider, von der die nächste Busübertragung liest. Es kann entweder mit einer Ganzzahl oder einem Objekt beschrieben werden, das das Buffer-Protokoll unterstützt, und dies hat sofortige Wirkung.

write: int

Dieses Attribut spiegelt die Adresse wider, an die die nächste Busübertragung schreibt. Es kann entweder mit einer Ganzzahl oder einem Objekt beschrieben werden, das das Buffer-Protokoll unterstützt, und dies hat sofortige Wirkung.

count: int

Das Lesen dieses Attributs gibt die Anzahl der verbleibenden Busübertragungen in der aktuellen Übertragungssequenz zurück. Das Schreiben dieses Attributs setzt die Gesamtzahl der Übertragungen für die nächste Übertragungssequenz.

ctrl: int

Dieses Attribut spiegelt das DMA-Kanal-Steuerregister wider. Es wird typischerweise mit einer Ganzzahl beschrieben, die mit der Methode DMA.pack_ctrl() gepackt wurde. Der zurückgegebene Registerwert kann mit der Methode DMA.unpack_ctrl() entpackt werden.

channel: int

Die Kanalnummer des DMA-Kanals. Diese kann im Argument chain_to von DMA.pack_ctrl() eines anderen Kanals übergeben werden, um DMA-Verkettung zu ermöglichen.

registers: 'memoryview'

Dieses Attribut ist ein array-ähnliches Objekt, das direkten Zugriff auf die Register des DMA-Kanals ermöglicht. Die Indizierung erfolgt nach Wort statt nach Byte, sodass die Registerindizes die durch 4 geteilten Registeradress-Offsets sind. Details zu den Registern finden Sie im RP2040-Datenblatt.

Verkettung und Zugriff auf das Trigger-Register

Der DMA-Controller im RP2040 bietet einige fortgeschrittene Funktionen, mit denen ein DMA-Kanal eine Übertragung auf einem anderen Kanal initiieren kann. Eine ist die Verwendung des Werts chain_to im Steuerregister und die andere das Schreiben in eines der Register des DMA-Kanals, das einen Trigger-Effekt hat. In Kombination mit der Fähigkeit, dass ein DMA-Kanal direkt in die DMA.registers eines anderen Kanals schreiben kann, ermöglicht dies die Durchführung komplexer Transaktionen ohne jegliches Eingreifen der CPU.

Nachfolgend ein Beispiel für die Verwendung von sowohl Verkettung als auch Register-Triggering, um das Sammeln mehrerer Datenblöcke in einem einzigen Ziel zu implementieren. Vollständige Details zu diesen Funktionen finden Sie in Abschnitt 2.5 des RP2040-Datenblatts, und der Code unten ist eine pythonische Version des Beispiels in Unterabschnitt 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)

Dieses Beispiel verharrt im Leerlauf, während es auf den Abschluss der Übertragung wartet; alternativ könnte es einen Interrupt-Handler setzen und sofort zurückkehren.