class DMA – accesso al controller DMA del RP2040

La classe DMA offre l’accesso al controller DMA (Direct Memory Access) del RP2040, fornendo la possibilità di spostare dati tra blocchi di memoria e/o registri IO. Il controller DMA ha le proprie connessioni master di bus separate per la lettura e la scrittura verso la struttura del bus, e ogni canale DMA può leggere in modo indipendente i dati da un indirizzo e riscriverli a un altro indirizzo, incrementando opzionalmente uno o entrambi i puntatori, consentendogli di eseguire trasferimenti per conto del processore mentre questo svolge altre attività o entra in uno stato a basso consumo. Il controller DMA del RP2040 dispone di 12 canali DMA indipendenti che possono essere eseguiti contemporaneamente. Per tutti i dettagli sul sistema DMA del RP2040 si veda la sezione 2.5 del Datasheet del RP2040.

Esempi

L’uso più semplice del controller DMA è spostare dati da un blocco di memoria a un altro. Questo può essere realizzato con il codice seguente:

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

Si noti che, sebbene questo esempio resti in un ciclo di inattività mentre attende il completamento del trasferimento, in questo lasso di tempo il programma potrebbe altrettanto bene svolgere qualche lavoro utile.

Un altro uso, forse più comune, del controller DMA è il trasferimento tra la memoria e una periferica IO. In questa situazione l’indirizzo del registro IO non cambia ad ogni trasferimento, ma l’indirizzo di memoria deve essere incrementato. È inoltre necessario controllare il ritmo del trasferimento, in modo da non scrivere dati prima che possano essere accettati da una periferica né leggerli prima che i dati siano pronti, e questo può essere controllato con il campo treq_sel del registro di controllo del canale DMA. I vari campi del registro di controllo di ciascun canale DMA possono essere impacchettati usando il metodo DMA.pack_ctrl() e spacchettati usando il metodo statico DMA.unpack_ctrl(). Il codice per trasferire dati da un array di byte alla FIFO TX di una macchina a stati PIO, un byte alla volta, ha questo aspetto:

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

Si noti che in questo esempio il valore fornito per l’indirizzo di scrittura è semplicemente la macchina a stati PIO a cui stiamo inviando i dati. Questo funziona perché le macchine a stati PIO presentano il protocollo buffer, consentendo l’accesso diretto ai loro registri FIFO dei dati.

Costruttore

class rp2.DMA

Riserva uno dei canali del controller DMA per uso esclusivo.

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

Configura i registri DMA per il canale e opzionalmente avvia il trasferimento. I parametri sono:

  • read: L’indirizzo da cui il controller DMA inizierà a leggere i dati oppure un oggetto che fornirà i dati da leggere. Può essere un intero o qualsiasi oggetto che supporta il protocollo buffer.

  • write: L’indirizzo in cui il controller DMA inizierà a scrivere oppure un oggetto in cui verranno scritti i dati. Può essere un intero o qualsiasi oggetto che supporta il protocollo buffer.

  • count: Il numero di trasferimenti di bus che verranno eseguiti prima che questo canale si fermi. Si noti che questo è il numero di trasferimenti, non il numero di byte. Se i trasferimenti sono larghi 2 o 4 byte, la quantità totale di dati spostati (e quindi la dimensione del buffer richiesto) deve essere moltiplicata di conseguenza.

  • ctrl: Il valore per il registro di controllo DMA. Questo è un valore intero che viene tipicamente impacchettato usando DMA.pack_ctrl().

  • trigger: Avvia opzionalmente il trasferimento immediatamente.

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

Restituisce l’oggetto IRQ per questo canale DMA e opzionalmente lo configura.

close() None

Rilascia la prenotazione sul canale DMA sottostante e libera il gestore dell’interrupt. L’oggetto DMA non può essere usato dopo questa operazione.

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

Impacchetta i valori forniti negli argomenti keyword nei campi denominati di un nuovo valore del registro di controllo. Qualsiasi campo non fornito sarà impostato a un valore predefinito. Il valore predefinito sarà preso dal valore default fornito oppure, se questo non viene dato, da un valore predefinito adatto al canale corrente; impostarlo al valore corrente dell’attributo DMA.ctrl fornisce un modo semplice per sovrascrivere un sottoinsieme dei campi.

Le chiavi per gli argomenti keyword possono essere qualsiasi chiave restituita dal metodo DMA.unpack_ctrl(). I valori scrivibili sono:

  • enable: bool Impostare per abilitare il canale (predefinito: True).

  • high_pri: bool Rende il traffico di bus di questo canale ad alta priorità (predefinito: False).

  • size: int Dimensione del trasferimento: 0=byte, 1=mezza parola, 2=parola (predefinito: 2).

  • inc_read: bool Incrementa l’indirizzo di lettura dopo ogni trasferimento (predefinito: True).

  • inc_write: bool Incrementa l’indirizzo di scrittura dopo ogni trasferimento (predefinito: True).

  • ring_size: int Se diverso da zero, solo i ring_size bit inferiori di un registro di indirizzo cambieranno quando un indirizzo viene incrementato, facendo sì che l’indirizzo ricominci al successivo limite di 1 << ring_size byte. Quale indirizzo viene fatto ricominciare è controllato dal flag ring_sel. Un valore zero disabilita il wrapping dell’indirizzo.

  • ring_sel: bool Impostare a False per far applicare ring_size all’indirizzo di lettura oppure a True per applicarlo all’indirizzo di scrittura.

  • chain_to: int Il numero del canale da attivare al completamento di questo trasferimento. Impostando questo valore al numero di canale di questo stesso oggetto DMA si disabilita il concatenamento (questa è l’impostazione predefinita).

  • treq_sel: int Seleziona un segnale di richiesta di trasferimento (Transfer Request). Si vedano i dettagli nella sezione 2.5.3 del datasheet del RP2040.

  • irq_quiet: bool Non genera un interrupt alla fine di ogni trasferimento. Gli interrupt verranno invece generati quando un valore zero viene scritto nel registro trigger, il che interromperà una sequenza di trasferimenti concatenati (predefinito: True).

  • bswap: bool Se impostato a true, i byte nelle parole o nelle mezze parole verranno invertiti prima della scrittura (predefinito: True).

  • sniff_en: bool Impostare a True per consentire l’accesso ai dati da parte dell’hardware di sniff del chip (predefinito: False).

  • write_err: bool Impostare questo a True cancellerà un errore di scrittura precedentemente segnalato.

  • read_err: bool Impostare questo a True cancellerà un errore di lettura precedentemente segnalato.

Si veda la descrizione del registro CH0_CTRL_TRIG nella sezione 2.5.7 del datasheet del RP2040 per i dettagli di tutti questi campi.

unpack_ctrl(value: int) dict

Spacchetta un valore per un registro di controllo del canale DMA in un dizionario con coppie chiave/valore per ciascuno dei campi del registro di controllo. value è il valore del registro ctrl da spacchettare.

Questo metodo restituirà i valori per tutte le chiavi che possono essere passate a DMA.pack_ctrl. Inoltre, restituirà anche i flag di sola lettura nel registro di controllo: busy, che diventa alto quando un trasferimento inizia e basso quando finisce, e ahb_err, che è l’OR logico dei flag read_err e write_err. Questi valori verranno ignorati durante l’impacchettamento, in modo che il dizionario creato spacchettando un registro di controllo possa essere usato direttamente come argomenti keyword per l’impacchettamento.

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

Ottiene o imposta se il canale DMA è attualmente in esecuzione.

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

Questo attributo riflette l’indirizzo da cui leggerà il prossimo trasferimento di bus. Può essere scritto con un intero oppure con un oggetto che supporta il protocollo buffer, e farlo ha effetto immediato.

write: int

Questo attributo riflette l’indirizzo in cui scriverà il prossimo trasferimento di bus. Può essere scritto con un intero oppure con un oggetto che supporta il protocollo buffer, e farlo ha effetto immediato.

count: int

La lettura di questo attributo restituirà il numero di trasferimenti di bus rimanenti nella sequenza di trasferimento corrente. La scrittura di questo attributo imposta il numero totale di trasferimenti per la prossima sequenza di trasferimento.

ctrl: int

Questo attributo riflette il registro di controllo del canale DMA. Viene tipicamente scritto con un intero impacchettato usando il metodo DMA.pack_ctrl(). Il valore del registro restituito può essere spacchettato usando il metodo DMA.unpack_ctrl().

channel: int

Il numero del canale DMA. Questo può essere passato nell’argomento chain_to di DMA.pack_ctrl() su un altro canale per consentire il concatenamento DMA.

registers: 'memoryview'

Questo attributo è un oggetto di tipo array che consente l’accesso diretto ai registri del canale DMA. L’indice è per parola, anziché per byte, quindi gli indici dei registri sono gli offset degli indirizzi dei registri divisi per 4. Si veda il datasheet del RP2040 per i dettagli sui registri.

Concatenamento e accesso al registro trigger

Il controller DMA del RP2040 offre un paio di funzionalità avanzate per consentire a un canale DMA di avviare un trasferimento su un altro canale. Una è l’uso del valore chain_to nel registro di controllo e l’altra è la scrittura in uno dei registri del canale DMA che ha un effetto di trigger. Quando combinato con la possibilità di far scrivere a un canale DMA direttamente nei DMA.registers di un altro canale, questo consente di eseguire transazioni complesse senza alcun intervento della CPU.

Di seguito è riportato un esempio di utilizzo sia del concatenamento sia del triggering dei registri per implementare la raccolta di più blocchi di dati in un’unica destinazione. Tutti i dettagli di queste funzionalità sono reperibili nella sezione 2.5 del datasheet del RP2040 e il codice seguente è una versione Pythonic dell’esempio nella sottosezione 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)

Questo esempio resta inattivo mentre attende il completamento del trasferimento; in alternativa potrebbe impostare un gestore di interrupt e ritornare immediatamente.