classe DMA – acesso ao controlador DMA do RP2040

A classe DMA oferece acesso ao controlador de Acesso Direto à Memória (DMA) do RP2040, fornecendo a capacidade de mover dados entre blocos de memória e/ou registradores de IO. O controlador DMA tem suas próprias conexões de barramento mestre de leitura e escrita, separadas, na malha do barramento, e cada canal DMA pode independentemente ler dados de um endereço e escrevê-los de volta em outro endereço, opcionalmente incrementando um ou ambos os ponteiros, permitindo que ele realize transferências em nome do processador enquanto o processador executa outras tarefas ou entra em um estado de baixo consumo. O controlador DMA do RP2040 tem 12 canais DMA independentes que podem ser executados concorrentemente. Para detalhes completos do sistema DMA do RP2040 veja a seção 2.5 do RP2040 Datasheet.

Exemplos

O uso mais simples do controlador DMA é mover dados de um bloco de memória para outro. Isto pode ser realizado 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

Observe que, embora este exemplo permaneça em um laço ocioso enquanto espera a transferência ser concluída, o programa poderia muito bem realizar algum trabalho útil nesse tempo.

Outro uso, talvez mais comum, do controlador DMA é transferir entre a memória e um periférico de IO. Nesta situação o endereço do registrador de IO não muda a cada transferência, mas o endereço de memória precisa ser incrementado. Também é necessário controlar o ritmo da transferência para não escrever dados antes que possam ser aceitos por um periférico ou lê-los antes que os dados estejam prontos, e isto pode ser controlado com o campo treq_sel do registrador de controle do canal DMA. Os vários campos do registrador de controle de cada canal DMA podem ser empacotados usando o método DMA.pack_ctrl() e desempacotados usando o método estático DMA.unpack_ctrl(). O código para transferir dados de um array de bytes para a FIFO de TX de uma máquina de estados PIO, um byte por vez, é assim:

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

Observe que neste exemplo o valor fornecido para o endereço de escrita é apenas a máquina de estados PIO para a qual estamos enviando os dados. Isto funciona porque as máquinas de estados PIO apresentam o protocolo de buffer, permitindo acesso direto a seus registradores de FIFO de dados.

Construtor

class rp2.DMA

Reivindica 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 registradores do 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 os dados ou um objeto que fornecerá os dados a serem lidos. 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 deste canal parar. Observe que este é o número de transferências, não o número de bytes. Se as transferências tiverem 2 ou 4 bytes de largura, então a quantidade total de dados movidos (e, portanto, o tamanho do buffer necessário) precisa ser multiplicada de acordo.

  • ctrl: O valor para o registrador de controle do DMA. Este é um valor inteiro que normalmente é empacotado usando o DMA.pack_ctrl().

  • trigger: Opcionalmente, inicia a transferência imediatamente.

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

Retorna o objeto IRQ para este canal DMA e, opcionalmente, o configura.

close() None

Libera a reivindicação sobre o canal DMA subjacente e libera o manipulador de interrupção. O objeto DMA não pode ser usado após esta operação.

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

Empacota os valores fornecidos nos argumentos nomeados nos campos nomeados de um novo valor de registrador de controle. Qualquer campo que não for fornecido será definido com um valor padrão. O padrão será obtido do valor default fornecido ou, se este não for dado, um padrão adequado para o canal atual; definir isto para o valor atual do atributo DMA.ctrl fornece uma maneira fácil de sobrescrever um subconjunto dos campos.

As chaves para os argumentos nomeados podem ser qualquer chave retornada pelo método DMA.unpack_ctrl(). Os valores graváveis são:

  • enable: bool Defina para habilitar o canal (padrão: True).

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

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

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

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

  • ring_size: int Se for diferente de zero, apenas os ring_size bits inferiores de um dos registradores de endereço mudarão quando um endereço for incrementado, fazendo com que o endereço dê a volta no próximo limite de 1 << ring_size bytes. Qual endereço dá a volta é controlado pela flag ring_sel. Um valor zero desabilita o retorno do endereço.

  • ring_sel: bool Defina como False para que o ring_size seja aplicado ao endereço de leitura ou True para aplicá-lo ao endereço de escrita.

  • chain_to: int O número do canal de um canal a ser disparado após a conclusão desta transferência. Definir este valor para o número do próprio canal deste objeto DMA desabilita o encadeamento (este é o padrão).

  • treq_sel: int Seleciona um sinal de Requisição de Transferência. Veja a seção 2.5.3 no datasheet do RP2040 para detalhes.

  • irq_quiet: bool Não gera interrupção ao final de cada transferência. As interrupções serão geradas, em vez disso, quando um valor zero for escrito no registrador de disparo, o que interromperá uma sequência de transferências encadeadas (padrão: True).

  • bswap: bool Se definido como verdadeiro, os bytes em palavras ou meias palavras serão invertidos antes da escrita (padrão: True).

  • sniff_en: bool Defina como True para permitir que os dados sejam acessados pelo hardware de sniff do chip (padrão: False).

  • write_err: bool Definir isto como True limpará um erro de escrita reportado anteriormente.

  • read_err: bool Definir isto como True limpará um erro de leitura reportado anteriormente.

Veja a descrição do registrador CH0_CTRL_TRIG na seção 2.5.7 do datasheet do RP2040 para detalhes de todos esses campos.

unpack_ctrl(value: int) dict

Desempacota um valor de um registrador de controle de canal DMA em um dicionário com pares chave/valor para cada um dos campos do registrador de controle. value é o valor do registrador ctrl a ser desempacotado.

Este método retornará valores para todas as chaves que podem ser passadas a DMA.pack_ctrl. Além disso, também retornará as flags somente leitura do registrador de controle: busy, que vai para alto quando uma transferência começa e para baixo quando ela termina, e ahb_err, que é o OU lógico das flags read_err e write_err. Esses valores serão ignorados ao empacotar, de modo que o dicionário criado ao desempacotar um registrador de controle pode ser usado diretamente como os argumentos nomeados para o empacotamento.

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

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

>>> 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á. Ele pode ser escrito com um inteiro ou com um objeto que suporte o protocolo de buffer, e fazer isso tem efeito imediato.

write: int

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

count: int

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

ctrl: int

Este atributo reflete o registrador de controle do canal DMA. Ele normalmente é escrito com um inteiro empacotado usando o método DMA.pack_ctrl(). O valor de registrador retornado pode ser desempacotado usando o método DMA.unpack_ctrl().

channel: int

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

registers: 'memoryview'

Este atributo é um objeto semelhante a um array que permite acesso direto aos registradores do canal DMA. O índice é por palavra, em vez de por byte, de modo que os índices de registrador são os deslocamentos de endereço de registrador divididos por 4. Veja o datasheet do RP2040 para detalhes dos registradores.

Acesso a encadeamento e registrador de disparo

O controlador DMA no RP2040 oferece alguns recursos avançados para permitir que um canal DMA inicie uma transferência em outro canal. Um é o uso do valor chain_to no registrador de controle e o outro é a escrita em um dos registradores do canal DMA que tem um efeito de disparo. Quando combinado com a capacidade de ter um canal DMA escrevendo diretamente nos DMA.registers de outro canal, isto permite que transações complexas sejam realizadas sem qualquer intervenção da CPU.

Abaixo está um exemplo do uso tanto de encadeamento quanto de disparo por registrador para implementar a coleta de múltiplos blocos de dados em um único destino. Detalhes completos desses recursos podem ser encontrados na seção 2.5 do datasheet do RP2040, e o código abaixo é uma versão Pythonic do exemplo da subseçã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 permanece ocioso enquanto espera a transferência ser concluída; alternativamente, ele poderia definir um manipulador de interrupção e retornar imediatamente.