klasse DMA – toegang tot de DMA-controller van de RP2040

De klasse DMA biedt toegang tot de Direct Memory Access (DMA)-controller van de RP2040 en maakt het mogelijk om gegevens te verplaatsen tussen geheugenblokken en/of IO-registers. De DMA-controller heeft zijn eigen, afzonderlijke lees- en schrijfbusmasterverbindingen met de busstructuur en elk DMA-kanaal kan onafhankelijk gegevens van het ene adres lezen en naar het andere adres terugschrijven, waarbij optioneel een of beide pointers worden verhoogd, zodat het overdrachten kan uitvoeren namens de processor terwijl de processor andere taken uitvoert of een toestand met laag stroomverbruik aanneemt. De DMA-controller van de RP2040 heeft 12 onafhankelijke DMA-kanalen die gelijktijdig kunnen draaien. Voor de volledige details van het DMA-systeem van de RP2040, zie sectie 2.5 van de RP2040 Datasheet.

Voorbeelden

Het eenvoudigste gebruik van de DMA-controller is het verplaatsen van gegevens van het ene geheugenblok naar het andere. Dit kan worden bereikt met de volgende code:

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

Merk op dat hoewel dit voorbeeld in een inactieve lus blijft terwijl het wacht tot de overdracht is voltooid, het programma in deze tijd net zo goed nuttig werk zou kunnen verrichten.

Een ander, wellicht vaker voorkomend gebruik van de DMA-controller is het overdragen tussen geheugen en een IO-randapparaat. In deze situatie verandert het adres van het IO-register niet bij elke overdracht, maar moet het geheugenadres worden verhoogd. Het is ook nodig om het tempo van de overdracht te regelen om geen gegevens te schrijven voordat een randapparaat ze kan accepteren of ze te lezen voordat de gegevens gereed zijn, en dit kan worden geregeld met het veld treq_sel van het controleregister van het DMA-kanaal. De verschillende velden van het controleregister voor elk DMA-kanaal kunnen worden ingepakt met de methode DMA.pack_ctrl() en uitgepakt met de statische methode DMA.unpack_ctrl(). Code om gegevens van een byte-array naar de TX FIFO van een PIO-statemachine over te dragen, een byte tegelijk, ziet er als volgt uit:

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

Merk op dat in dit voorbeeld de waarde die voor het schrijfadres wordt opgegeven gewoon de PIO-statemachine is waarnaar we de gegevens sturen. Dit werkt omdat PIO-statemachines het bufferprotocol aanbieden, waardoor directe toegang tot hun data-FIFO-registers mogelijk is.

Constructor

class rp2.DMA

Claimt een van de kanalen van de DMA-controller voor exclusief gebruik.

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

Configureert de DMA-registers voor het kanaal en start optioneel de overdracht. De parameters zijn:

  • read: Het adres vanwaar de DMA-controller begint met het lezen van gegevens, of een object dat de te lezen gegevens levert. Het kan een geheel getal zijn of elk object dat het bufferprotocol ondersteunt.

  • write: Het adres waarnaar de DMA-controller begint met schrijven, of een object waarin gegevens worden geschreven. Het kan een geheel getal zijn of elk object dat het bufferprotocol ondersteunt.

  • count: Het aantal busoverdrachten dat wordt uitgevoerd voordat dit kanaal stopt. Merk op dat dit het aantal overdrachten is, niet het aantal bytes. Als de overdrachten 2 of 4 bytes breed zijn, moet de totale hoeveelheid verplaatste gegevens (en dus de grootte van de vereiste buffer) dienovereenkomstig worden vermenigvuldigd.

  • ctrl: De waarde voor het DMA-controleregister. Dit is een geheeltallige waarde die doorgaans wordt ingepakt met DMA.pack_ctrl().

  • trigger: Start de overdracht optioneel onmiddellijk.

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

Geeft het IRQ-object voor dit DMA-kanaal terug en configureert het optioneel.

close() None

Geeft de claim op het onderliggende DMA-kanaal vrij en geeft de interrupt-handler vrij. Het DMA-object kan na deze bewerking niet meer worden gebruikt.

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

Pakt de waarden die in de keyword-argumenten zijn opgegeven in de benoemde velden van een nieuwe controleregisterwaarde. Elk veld dat niet wordt opgegeven, wordt op een standaardwaarde ingesteld. De standaardwaarde wordt ofwel ontleend aan de opgegeven default-waarde, of, als die niet is opgegeven, aan een standaardwaarde die geschikt is voor het huidige kanaal; door dit in te stellen op de huidige waarde van het attribuut DMA.ctrl heb je een eenvoudige manier om een deelverzameling van de velden te overschrijven.

De sleutels voor de keyword-argumenten kunnen elke sleutel zijn die door de methode DMA.unpack_ctrl() wordt teruggegeven. De schrijfbare waarden zijn:

  • enable: bool Instellen om het kanaal in te schakelen (standaard: True).

  • high_pri: bool Geeft het busverkeer van dit kanaal hoge prioriteit (standaard: False).

  • size: int Overdrachtsgrootte: 0=byte, 1=half woord, 2=woord (standaard: 2).

  • inc_read: bool Verhoogt het leesadres na elke overdracht (standaard: True).

  • inc_write: bool Verhoogt het schrijfadres na elke overdracht (standaard: True).

  • ring_size: int Als dit niet nul is, veranderen alleen de onderste ring_size bits van een adresregister wanneer een adres wordt verhoogd, waardoor het adres terugloopt bij de volgende 1 << ring_size-bytegrens. Welk adres terugloopt, wordt geregeld door de vlag ring_sel. Een nulwaarde schakelt het teruglopen van adressen uit.

  • ring_sel: bool Stel in op False om de ring_size toe te passen op het leesadres of op True om deze toe te passen op het schrijfadres.

  • chain_to: int Het kanaalnummer van een kanaal dat moet worden getriggerd nadat deze overdracht is voltooid. Door deze waarde in te stellen op het eigen kanaalnummer van dit DMA-object wordt het koppelen uitgeschakeld (dit is de standaardinstelling).

  • treq_sel: int Selecteert een Transfer Request-signaal. Zie sectie 2.5.3 in de RP2040-datasheet voor details.

  • irq_quiet: bool Genereer geen interrupt aan het einde van elke overdracht. In plaats daarvan worden interrupts gegenereerd wanneer een nulwaarde naar het triggerregister wordt geschreven, wat een reeks gekoppelde overdrachten stopt (standaard: True).

  • bswap: bool Indien ingesteld op true, worden bytes in woorden of halve woorden omgekeerd voordat ze worden geschreven (standaard: True).

  • sniff_en: bool Stel in op True om gegevens toegankelijk te maken voor de sniff-hardware van de chip (standaard: False).

  • write_err: bool Door dit op True in te stellen, wordt een eerder gemelde schrijffout gewist.

  • read_err: bool Door dit op True in te stellen, wordt een eerder gemelde leesfout gewist.

Zie de beschrijving van het register CH0_CTRL_TRIG in sectie 2.5.7 van de RP2040-datasheet voor details over al deze velden.

unpack_ctrl(value: int) dict

Pakt een waarde voor een DMA-kanaalcontroleregister uit in een dictionary met sleutel/waarde-paren voor elk van de velden in het controleregister. value is de uit te pakken ctrl-registerwaarde.

Deze methode geeft waarden terug voor alle sleutels die aan DMA.pack_ctrl kunnen worden doorgegeven. Daarnaast geeft het ook de alleen-lezen vlaggen in het controleregister terug: busy, die hoog wordt wanneer een overdracht begint en laag wanneer deze eindigt, en ahb_err, dat de logische OR is van de vlaggen read_err en write_err. Deze waarden worden genegeerd bij het inpakken, zodat de dictionary die wordt gemaakt door het uitpakken van een controleregister direct kan worden gebruikt als de keyword-argumenten voor het inpakken.

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

Haalt op of stelt in of het DMA-kanaal momenteel actief is.

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

Dit attribuut weerspiegelt het adres vanwaar de volgende busoverdracht zal lezen. Het kan worden geschreven met ofwel een geheel getal ofwel een object dat het bufferprotocol ondersteunt, en dit heeft onmiddellijk effect.

write: int

Dit attribuut weerspiegelt het adres waarnaar de volgende busoverdracht zal schrijven. Het kan worden geschreven met ofwel een geheel getal ofwel een object dat het bufferprotocol ondersteunt, en dit heeft onmiddellijk effect.

count: int

Het lezen van dit attribuut geeft het aantal resterende busoverdrachten in de huidige overdrachtsreeks terug. Het schrijven van dit attribuut stelt het totale aantal overdrachten voor de volgende overdrachtsreeks in.

ctrl: int

Dit attribuut weerspiegelt het DMA-kanaalcontroleregister. Het wordt doorgaans geschreven met een geheel getal dat is ingepakt met de methode DMA.pack_ctrl(). De teruggegeven registerwaarde kan worden uitgepakt met de methode DMA.unpack_ctrl().

channel: int

Het kanaalnummer van het DMA-kanaal. Dit kan worden doorgegeven in het argument chain_to van DMA.pack_ctrl() op een ander kanaal om DMA-koppeling mogelijk te maken.

registers: 'memoryview'

Dit attribuut is een array-achtig object dat directe toegang biedt tot de registers van het DMA-kanaal. De index is per woord, niet per byte, dus de registerindices zijn de registeradresoffsets gedeeld door 4. Zie de RP2040-datasheet voor registerdetails.

Koppeling en toegang tot triggerregisters

De DMA-controller in de RP2040 biedt een aantal geavanceerde functies waarmee een DMA-kanaal een overdracht op een ander kanaal kan starten. De ene is het gebruik van de waarde chain_to in het controleregister en de andere is het schrijven naar een van de registers van het DMA-kanaal dat een triggereffect heeft. In combinatie met de mogelijkheid om een DMA-kanaal rechtstreeks naar de DMA.registers van een ander kanaal te laten schrijven, maakt dit het mogelijk om complexe transacties uit te voeren zonder enige CPU-tussenkomst.

Hieronder staat een voorbeeld van het gebruik van zowel koppeling als registertriggering om het verzamelen van meerdere gegevensblokken in een enkele bestemming te implementeren. De volledige details van deze functies zijn te vinden in sectie 2.5 van de RP2040-datasheet en de onderstaande code is een Pythonische versie van het voorbeeld in subsectie 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)

Dit voorbeeld blijft inactief terwijl het wacht tot de overdracht is voltooid; als alternatief zou het een interrupt-handler kunnen instellen en onmiddellijk terugkeren.