classe DMA – accès au contrôleur DMA du RP2040

La classe DMA offre l’accès au contrôleur d’accès direct à la mémoire (DMA) du RP2040, permettant de déplacer des données entre des blocs de mémoire et/ou des registres d’E/S. Le contrôleur DMA possède ses propres connexions maîtres de bus de lecture et d’écriture séparées sur la structure du bus, et chaque canal DMA peut indépendamment lire des données depuis une adresse et les réécrire vers une autre adresse, en incrémentant éventuellement l’un des pointeurs ou les deux, ce qui lui permet d’effectuer des transferts pour le compte du processeur pendant que celui-ci exécute d’autres tâches ou entre dans un état de faible consommation. Le contrôleur DMA du RP2040 dispose de 12 canaux DMA indépendants pouvant s’exécuter simultanément. Pour tous les détails sur le système DMA du RP2040, consultez la section 2.5 de la fiche technique du RP2040.

Exemples

L’utilisation la plus simple du contrôleur DMA consiste à déplacer des données d’un bloc de mémoire vers un autre. Cela peut être réalisé avec le code suivant

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

Notez que, bien que cet exemple reste dans une boucle d’attente pendant qu’il attend la fin du transfert, le programme pourrait tout aussi bien effectuer un travail utile pendant ce temps.

Une autre utilisation du contrôleur DMA, peut-être plus courante, consiste à transférer des données entre la mémoire et un périphérique d’E/S. Dans cette situation, l’adresse du registre d’E/S ne change pas à chaque transfert, mais l’adresse mémoire doit être incrémentée. Il est également nécessaire de contrôler le rythme du transfert afin de ne pas écrire de données avant qu’elles puissent être acceptées par un périphérique, ni les lire avant qu’elles soient prêtes, ce qui peut être contrôlé avec le champ treq_sel du registre de contrôle du canal DMA. Les différents champs du registre de contrôle de chaque canal DMA peuvent être empaquetés à l’aide de la méthode DMA.pack_ctrl() et dépaquetés à l’aide de la méthode statique DMA.unpack_ctrl(). Le code permettant de transférer des données d’un tableau d’octets vers la FIFO TX d’une machine d’état PIO, un octet à la fois, ressemble à ceci

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

Notez que dans cet exemple, la valeur donnée pour l’adresse d’écriture est simplement la machine d’état PIO à laquelle nous envoyons les données. Cela fonctionne parce que les machines d’état PIO présentent le protocole tampon, permettant un accès direct à leurs registres de FIFO de données.

Constructeur

class rp2.DMA

Réserve l’un des canaux du contrôleur DMA pour un usage exclusif.

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

Configure les registres DMA du canal et démarre éventuellement le transfert. Les paramètres sont :

  • read : L’adresse à partir de laquelle le contrôleur DMA commencera à lire les données, ou un objet qui fournira les données à lire. Il peut s’agir d’un entier ou de tout objet qui prend en charge le protocole tampon.

  • write : L’adresse vers laquelle le contrôleur DMA commencera à écrire, ou un objet dans lequel les données seront écrites. Il peut s’agir d’un entier ou de tout objet qui prend en charge le protocole tampon.

  • count : Le nombre de transferts de bus qui s’exécuteront avant que ce canal ne s’arrête. Notez qu’il s’agit du nombre de transferts, et non du nombre d’octets. Si les transferts font 2 ou 4 octets de large, la quantité totale de données déplacées (et donc la taille du tampon requis) doit être multipliée en conséquence.

  • ctrl : La valeur du registre de contrôle DMA. Il s’agit d’une valeur entière qui est généralement empaquetée à l’aide de DMA.pack_ctrl().

  • trigger : Démarre éventuellement le transfert immédiatement.

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

Renvoie l’objet IRQ de ce canal DMA et le configure éventuellement.

close() None

Libère la réservation du canal DMA sous-jacent et libère le gestionnaire d’interruption. L’objet DMA ne peut pas être utilisé après cette opération.

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

Empaquette les valeurs fournies dans les arguments nommés dans les champs nommés d’une nouvelle valeur de registre de contrôle. Tout champ non fourni sera défini sur une valeur par défaut. La valeur par défaut sera soit tirée de la valeur default fournie, soit, si celle-ci n’est pas donnée, une valeur par défaut adaptée au canal actuel ; définir ceci sur la valeur actuelle de l’attribut DMA.ctrl offre un moyen simple de remplacer un sous-ensemble des champs.

Les clés des arguments nommés peuvent être n’importe quelle clé renvoyée par la méthode DMA.unpack_ctrl(). Les valeurs modifiables sont :

  • enable : bool À définir pour activer le canal (par défaut : True).

  • high_pri : bool Rend le trafic de bus de ce canal hautement prioritaire (par défaut : False).

  • size : int Taille de transfert : 0=octet, 1=demi-mot, 2=mot (par défaut : 2).

  • inc_read : bool Incrémente l’adresse de lecture après chaque transfert (par défaut : True).

  • inc_write : bool Incrémente l’adresse d’écriture après chaque transfert (par défaut : True).

  • ring_size : int Si non nul, seuls les ring_size bits de poids faible de l’un des registres d’adresse changeront lorsqu’une adresse est incrémentée, faisant boucler l’adresse à la prochaine limite de 1 << ring_size octets. L’adresse qui est bouclée est contrôlée par l’indicateur ring_sel. Une valeur de zéro désactive le bouclage d’adresse.

  • ring_sel : bool À définir sur False pour que ring_size s’applique à l’adresse de lecture, ou sur True pour qu’il s’applique à l’adresse d’écriture.

  • chain_to : int Le numéro de canal d’un canal à déclencher une fois ce transfert terminé. Définir cette valeur sur le propre numéro de canal de cet objet DMA désactive le chaînage (c’est le comportement par défaut).

  • treq_sel : int Sélectionne un signal de requête de transfert (Transfer Request). Voir la section 2.5.3 de la fiche technique du RP2040 pour plus de détails.

  • irq_quiet : bool Ne génère pas d’interruption à la fin de chaque transfert. Les interruptions seront générées à la place lorsqu’une valeur nulle est écrite dans le registre de déclenchement, ce qui arrêtera une séquence de transferts chaînés (par défaut : True).

  • bswap : bool Si défini sur true, les octets dans les mots ou demi-mots seront inversés avant l’écriture (par défaut : True).

  • sniff_en : bool À définir sur True pour permettre l’accès aux données par le matériel de surveillance (sniff) de la puce (par défaut : False).

  • write_err : bool Définir ceci sur True effacera une erreur d’écriture précédemment signalée.

  • read_err : bool Définir ceci sur True effacera une erreur de lecture précédemment signalée.

Voir la description du registre CH0_CTRL_TRIG dans la section 2.5.7 de la fiche technique du RP2040 pour plus de détails sur tous ces champs.

unpack_ctrl(value: int) dict

Dépaquette la valeur d’un registre de contrôle de canal DMA en un dictionnaire avec des paires clé/valeur pour chacun des champs du registre de contrôle. value est la valeur du registre ctrl à dépaqueter.

Cette méthode renverra les valeurs de toutes les clés qui peuvent être passées à DMA.pack_ctrl. De plus, elle renverra également les indicateurs en lecture seule du registre de contrôle : busy, qui passe à l’état haut lorsqu’un transfert démarre et à l’état bas lorsqu’il se termine, et ahb_err, qui est le OU logique des indicateurs read_err et write_err. Ces valeurs seront ignorées lors de l’empaquetage, de sorte que le dictionnaire créé en dépaquetant un registre de contrôle peut être utilisé directement comme arguments nommés pour l’empaquetage.

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

Obtient ou définit si le canal DMA est actuellement en cours d’exécution.

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

Cet attribut reflète l’adresse à partir de laquelle le prochain transfert de bus lira. Il peut être écrit soit avec un entier, soit avec un objet qui prend en charge le protocole tampon, et cela a un effet immédiat.

write: int

Cet attribut reflète l’adresse vers laquelle le prochain transfert de bus écrira. Il peut être écrit soit avec un entier, soit avec un objet qui prend en charge le protocole tampon, et cela a un effet immédiat.

count: int

La lecture de cet attribut renverra le nombre de transferts de bus restants dans la séquence de transfert actuelle. L’écriture de cet attribut définit le nombre total de transferts de la prochaine séquence de transfert.

ctrl: int

Cet attribut reflète le registre de contrôle du canal DMA. Il est généralement écrit avec un entier empaqueté à l’aide de la méthode DMA.pack_ctrl(). La valeur de registre renvoyée peut être dépaquetée à l’aide de la méthode DMA.unpack_ctrl().

channel: int

Le numéro de canal du canal DMA. Il peut être passé dans l’argument chain_to de DMA.pack_ctrl() sur un autre canal pour permettre le chaînage DMA.

registers: 'memoryview'

Cet attribut est un objet de type tableau qui permet un accès direct aux registres du canal DMA. L’index se fait par mot, plutôt que par octet, de sorte que les index de registre correspondent aux décalages d’adresse de registre divisés par 4. Voir la fiche technique du RP2040 pour les détails des registres.

Chaînage et accès au registre de déclenchement

Le contrôleur DMA du RP2040 offre quelques fonctionnalités avancées permettant à un canal DMA d’initier un transfert sur un autre canal. L’une est l’utilisation de la valeur chain_to dans le registre de contrôle et l’autre est l’écriture dans l’un des registres du canal DMA qui a un effet de déclenchement. Couplée à la capacité d’un canal DMA à écrire directement dans les DMA.registers d’un autre canal, cela permet d’effectuer des transactions complexes sans aucune intervention du processeur.

Vous trouverez ci-dessous un exemple utilisant à la fois le chaînage et le déclenchement par registre pour implémenter le rassemblement de plusieurs blocs de données vers une seule destination. Tous les détails de ces fonctionnalités se trouvent dans la section 2.5 de la fiche technique du RP2040, et le code ci-dessous est une version pythonique de l’exemple de la sous-section 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)

Cet exemple reste en attente pendant qu’il attend la fin du transfert ; il pourrait également définir un gestionnaire d’interruption et revenir immédiatement.