class DMA – acesso ao controlador DMA do RP2040

A classe DMA oferece acesso ao controlador de Acesso Direto à Memória (DMA) do RP2040, disponibilizando a capacidade de mover dados entre blocos de memória e/ou registos de E/S. O controlador DMA tem as suas próprias ligações de bus master de leitura e escrita independentes ao barramento, e cada canal DMA pode ler dados de um endereço e escrevê-los noutro de forma independente, incrementando opcionalmente um ou ambos os ponteiros, permitindo realizar transferências em nome do processador enquanto este executa outras tarefas ou entra num estado de baixo consumo. O controlador DMA do RP2040 tem 12 canais DMA independentes que podem ser executados em simultâneo. Para detalhes completos sobre o sistema DMA do RP2040, consulte a secção 2.5 do RP2040 Datasheet.

Exemplos

A utilização mais simples do controlador DMA é mover dados de um bloco de memória para outro. Isto pode ser feito com o seguinte 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

Note que, embora este exemplo permaneça num ciclo de espera enquanto aguarda a conclusão da transferência, o programa poderia igualmente realizar trabalho útil durante esse tempo.

Outro uso, porventura mais comum do controlador DMA, é a transferência entre memória e um periférico de E/S. Nesta situação, o endereço do registo de E/S não muda em cada transferência, mas o endereço de memória precisa de ser incrementado. É também necessário controlar o ritmo da transferência para não escrever dados antes de o periférico os poder aceitar ou lê-los antes de estarem prontos; isso pode ser controlado com o campo treq_sel do registo de controlo do canal DMA. Os vários campos do registo de controlo de cada canal DMA podem ser agrupados usando o método DMA.pack_ctrl() e desagrupados usando o método estático DMA.unpack_ctrl(). O código para transferir dados de um array de bytes para o TX FIFO de uma máquina de estado PIO, um byte de cada vez, tem o seguinte aspeto:

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

Note que neste exemplo o valor dado para o endereço de escrita é simplesmente a máquina de estado PIO para a qual estamos a enviar os dados. Isto funciona porque as máquinas de estado PIO implementam o protocolo de buffer, permitindo acesso direto aos seus registos FIFO de dados.

Construtor

class rp2.DMA

Reserva um dos canais do 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 os registos DMA para o canal e opcionalmente inicia a transferência. Os parâmetros são:

  • read: O endereço a partir do qual o controlador DMA começará a ler dados, ou um objeto que fornecerá os dados a ler. Pode ser um inteiro ou qualquer objeto que suporte o protocolo de buffer.

  • write: O endereço para o qual o controlador DMA começará a escrever, ou um objeto no qual os dados serão escritos. Pode ser um inteiro ou qualquer objeto que suporte o protocolo de buffer.

  • count: O número de transferências de barramento que serão executadas antes de o canal parar. Note que este é o número de transferências, não o número de bytes. Se as transferências forem de 2 ou 4 bytes de largura, a quantidade total de dados movidos (e, portanto, o tamanho do buffer necessário) deverá ser multiplicada em conformidade.

  • ctrl: O valor para o registo de controlo DMA. É um valor inteiro tipicamente agrupado usando DMA.pack_ctrl().

  • trigger: Opcionalmente, inicia a transferência de imediato.

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

Devolve o objeto IRQ deste canal DMA e opcionalmente configura-o.

close() None

Liberta a reserva sobre o canal DMA subjacente e liberta o gestor de interrupções. O objeto DMA não pode ser utilizado após esta operação.

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

Agrupa os valores fornecidos nos argumentos de palavra-chave nos campos nomeados de um novo valor de registo de controlo. Qualquer campo não fornecido será definido com um valor predefinido. O valor predefinido será retirado do valor default fornecido ou, caso não seja fornecido, um valor predefinido adequado para o canal atual; definir este como o valor atual do atributo DMA.ctrl oferece uma forma fácil de substituir um subconjunto dos campos.

As chaves dos argumentos de palavra-chave podem ser qualquer chave devolvida pelo método DMA.unpack_ctrl(). Os valores graváveis são:

  • enable: bool Definir para ativar o canal (predefinição: True).

  • high_pri: bool Torna o tráfego de barramento deste canal de alta prioridade (predefinição: False).

  • size: int Tamanho da transferência: 0=byte, 1=meia palavra, 2=palavra (predefinição: 2).

  • inc_read: bool Incrementar o endereço de leitura após cada transferência (predefinição: True).

  • inc_write: bool Incrementar o endereço de escrita após cada transferência (predefinição: True).

  • ring_size: int Se não-zero, apenas os ring_size bits inferiores de um registo de endereço serão alterados quando um endereço é incrementado, fazendo o endereço retornar ao início no próximo limite de 1 << ring_size bytes. O endereço que é envolvido é controlado pelo sinalizador ring_sel. Um valor zero desativa o envolvimento de endereço.

  • ring_sel: bool Definir como False para aplicar ring_size ao endereço de leitura ou True para aplicar ao endereço de escrita.

  • chain_to: int O número do canal a acionar após a conclusão desta transferência. Definir este valor como o número do próprio canal deste objeto DMA desativa o encadeamento (este é o valor predefinido).

  • treq_sel: int Seleciona um sinal de Pedido de Transferência. Consulte a secção 2.5.3 do datasheet do RP2040 para detalhes.

  • irq_quiet: bool Não gera interrupção no final de cada transferência. As interrupções serão geradas quando um valor zero for escrito no registo de acionamento, o que irá parar uma sequência de transferências encadeadas (predefinição: True).

  • bswap: bool Se definido como verdadeiro, os bytes em palavras ou meias-palavras serão invertidos antes de escrever (predefinição: True).

  • sniff_en: bool Definir como True para permitir que os dados sejam acedidos pelo hardware de sniff do chip (predefinição: False).

  • write_err: bool Definir como True irá limpar um erro de escrita previamente reportado.

  • read_err: bool Definir como True irá limpar um erro de leitura previamente reportado.

Consulte a descrição do registo CH0_CTRL_TRIG na secção 2.5.7 do datasheet do RP2040 para detalhes de todos estes campos.

unpack_ctrl(value: int) dict

Desagrupa um valor para um registo de controlo de canal DMA num dicionário com pares chave/valor para cada campo do registo de controlo. value é o valor do registo ctrl a desagrupar.

Este método devolverá valores para todas as chaves que podem ser passadas a DMA.pack_ctrl. Além disso, devolverá também os sinalizadores somente de leitura no registo de controlo: busy, que fica alto quando uma transferência começa e baixo quando termina, e ahb_err, que é o OR lógico dos sinalizadores read_err e write_err. Estes valores serão ignorados durante o agrupamento, de modo que o dicionário criado pelo desagrupamento de um registo de controlo pode ser usado diretamente como argumentos de palavra-chave para o agrupamento.

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

Obtém ou define se o canal DMA está atualmente em execução.

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

Este atributo reflete o endereço a partir do qual a próxima transferência de barramento lerá. Pode ser escrito com um inteiro ou com um objeto que suporte o protocolo de buffer, sendo o efeito imediato.

write: int

Este atributo reflete o endereço para o qual a próxima transferência de barramento escreverá. Pode ser escrito com um inteiro ou com um objeto que suporte o protocolo de buffer, sendo o efeito imediato.

count: int

Ler este atributo devolverá o número de transferências de barramento restantes na sequência de transferência atual. Escrever neste atributo define o número total de transferências para a próxima sequência de transferência.

ctrl: int

Este atributo reflete o registo de controlo do canal DMA. Tipicamente é escrito com um inteiro agrupado usando o método DMA.pack_ctrl(). O valor de registo devolvido pode ser desagrupado usando o método DMA.unpack_ctrl().

channel: int

O número do canal DMA. Este pode ser passado no argumento chain_to de DMA.pack_ctrl() noutro canal para permitir o encadeamento de DMA.

registers: 'memoryview'

Este atributo é um objeto semelhante a um array que permite acesso direto aos registos do canal DMA. O índice é por palavra, não por byte, pelo que os índices de registo são os deslocamentos de endereço de registo divididos por 4. Consulte o datasheet do RP2040 para detalhes dos registos.

Encadeamento e acesso ao registo de acionamento

O controlador DMA do RP2040 oferece algumas funcionalidades avançadas para permitir que um canal DMA inicie uma transferência noutro canal. Uma delas é o uso do valor chain_to no registo de controlo e a outra é escrever num dos registos do canal DMA que tem efeito de acionamento. Quando combinado com a capacidade de um canal DMA escrever diretamente nos DMA.registers de outro canal, isto permite que transações complexas sejam realizadas sem qualquer intervenção da CPU.

Em seguida, apresenta-se um exemplo de uso de encadeamento e de acionamento por registo para implementar a recolha de múltiplos blocos de dados num único destino. Os detalhes completos destas funcionalidades podem ser encontrados na secção 2.5 do datasheet do RP2040, e o código seguinte é uma versão Pythónica do exemplo da subsecção 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 exemplo fica em espera enquanto aguarda a conclusão da transferência; em alternativa, poderia definir um gestor de interrupções e retornar imediatamente.