clase DMA – acceso al controlador DMA del RP2040

La clase DMA ofrece acceso al controlador de Acceso Directo a Memoria (DMA) del RP2040, proporcionando la capacidad de mover datos entre bloques de memoria y/o registros de E/S. El controlador DMA tiene sus propias conexiones maestras de bus de lectura y escritura, separadas, hacia el entramado de bus, y cada canal DMA puede leer datos de forma independiente desde una dirección y escribirlos en otra dirección, incrementando opcionalmente uno o ambos punteros, lo que le permite realizar transferencias en nombre del procesador mientras este lleva a cabo otras tareas o entra en un estado de bajo consumo. El controlador DMA del RP2040 tiene 12 canales DMA independientes que pueden ejecutarse de forma concurrente. Para conocer todos los detalles del sistema DMA del RP2040, consulte la sección 2.5 de la Hoja de datos del RP2040.

Ejemplos

El uso más simple del controlador DMA es mover datos de un bloque de memoria a otro. Esto se puede lograr con el siguiente código:

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

Tenga en cuenta que, aunque este ejemplo permanece en un bucle inactivo mientras espera a que se complete la transferencia, el programa podría igualmente realizar algún trabajo útil durante este tiempo.

Otro uso del controlador DMA, quizás más común, es transferir entre la memoria y un periférico de E/S. En esta situación, la dirección del registro de E/S no cambia en cada transferencia, pero la dirección de memoria necesita incrementarse. También es necesario controlar el ritmo de la transferencia para no escribir datos antes de que un periférico pueda aceptarlos ni leerlos antes de que los datos estén listos, y esto se puede controlar con el campo treq_sel del registro de control del canal DMA. Los distintos campos del registro de control de cada canal DMA se pueden empaquetar usando el método DMA.pack_ctrl() y desempaquetar usando el método estático DMA.unpack_ctrl(). El código para transferir datos de un array de bytes a la FIFO de TX de una máquina de estados PIO, un byte a la vez, tiene este aspecto:

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

Tenga en cuenta que en este ejemplo el valor dado para la dirección de escritura es simplemente la máquina de estados PIO a la que estamos enviando los datos. Esto funciona porque las máquinas de estados PIO presentan el protocolo de búfer, lo que permite el acceso directo a sus registros FIFO de datos.

Constructor

class rp2.DMA

Reclama uno de los canales del controlador DMA para uso exclusivo.

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

Configura los registros DMA del canal y opcionalmente inicia la transferencia. Los parámetros son:

  • read: La dirección desde la que el controlador DMA comenzará a leer datos, o un objeto que proporcionará los datos a leer. Puede ser un entero o cualquier objeto que admita el protocolo de búfer.

  • write: La dirección en la que el controlador DMA comenzará a escribir, o un objeto en el que se escribirán los datos. Puede ser un entero o cualquier objeto que admita el protocolo de búfer.

  • count: El número de transferencias de bus que se ejecutarán antes de que este canal se detenga. Tenga en cuenta que este es el número de transferencias, no el número de bytes. Si las transferencias son de 2 o 4 bytes de ancho, entonces la cantidad total de datos movidos (y, por tanto, el tamaño del búfer requerido) debe multiplicarse en consecuencia.

  • ctrl: El valor para el registro de control DMA. Este es un valor entero que normalmente se empaqueta usando DMA.pack_ctrl().

  • trigger: Opcionalmente, inicia la transferencia de inmediato.

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

Devuelve el objeto IRQ para este canal DMA y opcionalmente lo configura.

close() None

Libera la reclamación sobre el canal DMA subyacente y libera el manejador de interrupción. El objeto DMA no se puede usar después de esta operación.

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

Empaqueta los valores proporcionados en los argumentos de palabra clave en los campos con nombre de un nuevo valor de registro de control. Cualquier campo que no se proporcione se establecerá en un valor predeterminado. El valor predeterminado se tomará del valor default proporcionado, o, si no se da, un valor predeterminado adecuado para el canal actual; establecerlo al valor actual del atributo DMA.ctrl proporciona una forma sencilla de sobrescribir un subconjunto de los campos.

Las claves para los argumentos de palabra clave pueden ser cualquier clave devuelta por el método DMA.unpack_ctrl(). Los valores escribibles son:

  • enable: bool Establecer para habilitar el canal (predeterminado: True).

  • high_pri: bool Hace que el tráfico de bus de este canal sea de alta prioridad (predeterminado: False).

  • size: int Tamaño de transferencia: 0=byte, 1=media palabra, 2=palabra (predeterminado: 2).

  • inc_read: bool Incrementa la dirección de lectura después de cada transferencia (predeterminado: True).

  • inc_write: bool Incrementa la dirección de escritura después de cada transferencia (predeterminado: True).

  • ring_size: int Si es distinto de cero, solo los ring_size bits inferiores de un registro de dirección cambiarán cuando se incremente una dirección, haciendo que la dirección dé la vuelta en el siguiente límite de 1 << ring_size bytes. Qué dirección da la vuelta se controla con el indicador ring_sel. Un valor cero deshabilita el envolvimiento de direcciones.

  • ring_sel: bool Establecer en False para que ring_size se aplique a la dirección de lectura o en True para aplicarlo a la dirección de escritura.

  • chain_to: int El número de canal de un canal que se activará después de que se complete esta transferencia. Establecer este valor al propio número de canal de este objeto DMA deshabilita el encadenamiento (este es el valor predeterminado).

  • treq_sel: int Selecciona una señal de Solicitud de Transferencia. Consulte la sección 2.5.3 de la hoja de datos del RP2040 para más detalles.

  • irq_quiet: bool No generar una interrupción al final de cada transferencia. En su lugar, las interrupciones se generarán cuando se escriba un valor cero en el registro de activación, lo que detendrá una secuencia de transferencias encadenadas (predeterminado: True).

  • bswap: bool Si se establece en true, los bytes en palabras o medias palabras se invertirán antes de escribirlos (predeterminado: True).

  • sniff_en: bool Establecer en True para permitir que el hardware de inspección (sniff) del chip acceda a los datos (predeterminado: False).

  • write_err: bool Establecer esto en True borrará un error de escritura reportado previamente.

  • read_err: bool Establecer esto en True borrará un error de lectura reportado previamente.

Consulte la descripción del registro CH0_CTRL_TRIG en la sección 2.5.7 de la hoja de datos del RP2040 para conocer los detalles de todos estos campos.

unpack_ctrl(value: int) dict

Desempaqueta un valor para un registro de control de canal DMA en un diccionario con pares clave/valor para cada uno de los campos del registro de control. value es el valor del registro ctrl que se va a desempaquetar.

Este método devolverá valores para todas las claves que se pueden pasar a DMA.pack_ctrl. Además, también devolverá los indicadores de solo lectura del registro de control: busy, que se pone en alto cuando comienza una transferencia y en bajo cuando termina, y ahb_err, que es el OR lógico de los indicadores read_err y write_err. Estos valores se ignorarán al empaquetar, de modo que el diccionario creado al desempaquetar un registro de control se puede usar directamente como argumentos de palabra clave para empaquetar.

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

Obtiene o establece si el canal DMA se está ejecutando actualmente.

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

Este atributo refleja la dirección desde la que leerá la siguiente transferencia de bus. Se puede escribir con un entero o con un objeto que admita el protocolo de búfer, y hacerlo tiene efecto inmediato.

write: int

Este atributo refleja la dirección en la que escribirá la siguiente transferencia de bus. Se puede escribir con un entero o con un objeto que admita el protocolo de búfer, y hacerlo tiene efecto inmediato.

count: int

Leer este atributo devolverá el número de transferencias de bus restantes en la secuencia de transferencia actual. Escribir este atributo establece el número total de transferencias para la siguiente secuencia de transferencia.

ctrl: int

Este atributo refleja el registro de control del canal DMA. Normalmente se escribe con un entero empaquetado usando el método DMA.pack_ctrl(). El valor de registro devuelto se puede desempaquetar usando el método DMA.unpack_ctrl().

channel: int

El número de canal del canal DMA. Este puede pasarse en el argumento chain_to de DMA.pack_ctrl() de otro canal para permitir el encadenamiento de DMA.

registers: 'memoryview'

Este atributo es un objeto similar a un array que permite el acceso directo a los registros del canal DMA. El índice es por palabra, en lugar de por byte, de modo que los índices de registro son los desplazamientos de dirección del registro divididos por 4. Consulte la hoja de datos del RP2040 para conocer los detalles de los registros.

Acceso a registros de encadenamiento y activación

El controlador DMA del RP2040 ofrece un par de características avanzadas para permitir que un canal DMA inicie una transferencia en otro canal. Una es el uso del valor chain_to en el registro de control y la otra es escribir en uno de los registros del canal DMA que tiene un efecto de activación. Cuando se combina con la capacidad de que un canal DMA escriba directamente en los DMA.registers de otro canal, esto permite realizar transacciones complejas sin ninguna intervención de la CPU.

A continuación se muestra un ejemplo del uso tanto del encadenamiento como de la activación por registro para implementar la recopilación de múltiples bloques de datos en un único destino. Todos los detalles de estas características se pueden encontrar en la sección 2.5 de la hoja de datos del RP2040, y el código siguiente es una versión pythónica del ejemplo de la subsección 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)

Este ejemplo permanece inactivo mientras espera a que se complete la transferencia; alternativamente, podría establecer un manejador de interrupción y retornar de inmediato.