class DMA — доступ к контроллеру DMA в RP2040

Класс DMA предоставляет доступ к контроллеру прямого доступа к памяти (DMA) в RP2040, обеспечивая возможность перемещать данные между блоками памяти и/или регистрами ввода-вывода. Контроллер DMA имеет собственные, отдельные ведущие подключения шины для чтения и записи к шинной матрице, и каждый канал DMA может независимо читать данные с одного адреса и записывать их по другому адресу, при необходимости увеличивая один или оба указателя, что позволяет ему выполнять передачи от имени процессора, пока процессор выполняет другие задачи или находится в состоянии низкого энергопотребления. Контроллер DMA в RP2040 имеет 12 независимых каналов DMA, которые могут работать одновременно. Полное описание системы DMA в RP2040 см. в разделе 2.5 RP2040 Datasheet.

Примеры

Простейшее использование контроллера DMA — перемещение данных из одного блока памяти в другой. Это можно сделать с помощью следующего кода:

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

Обратите внимание, что хотя в этом примере программа находится в холостом цикле, ожидая завершения передачи, она с тем же успехом могла бы в это время выполнять какую-нибудь полезную работу.

Другое, возможно, более распространённое применение контроллера DMA — передача между памятью и периферийным устройством ввода-вывода. В этой ситуации адрес регистра ввода-вывода не меняется при каждой передаче, но адрес памяти необходимо увеличивать. Также необходимо управлять темпом передачи, чтобы не записывать данные до того, как они смогут быть приняты периферийным устройством, и не читать их до того, как данные будут готовы; этим можно управлять с помощью поля treq_sel управляющего регистра канала DMA. Различные поля управляющего регистра для каждого канала DMA можно упаковать с помощью метода DMA.pack_ctrl() и распаковать с помощью статического метода DMA.unpack_ctrl(). Код для передачи данных из массива байтов в TX FIFO конечного автомата PIO по одному байту за раз выглядит так:

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

Обратите внимание, что в этом примере значение, заданное для адреса записи, — это просто конечный автомат PIO, которому мы отправляем данные. Это работает, потому что конечные автоматы PIO реализуют протокол буфера, обеспечивая прямой доступ к регистрам своего FIFO данных.

Конструктор

class rp2.DMA

Захватывает один из каналов контроллера DMA для монопольного использования.

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

Настраивает регистры DMA для канала и, при необходимости, запускает передачу. Параметры:

  • read: Адрес, с которого контроллер DMA начнёт читать данные, или объект, предоставляющий данные для чтения. Это может быть целое число или любой объект, поддерживающий протокол буфера.

  • write: Адрес, по которому контроллер DMA начнёт запись, или объект, в который будут записываться данные. Это может быть целое число или любой объект, поддерживающий протокол буфера.

  • count: Количество передач по шине, которые будут выполнены до остановки этого канала. Обратите внимание, что это количество передач, а не количество байтов. Если ширина передач составляет 2 или 4 байта, то общий объём перемещаемых данных (и, следовательно, размер требуемого буфера) необходимо соответственно умножить.

  • ctrl: Значение для управляющего регистра DMA. Это целочисленное значение, которое обычно упаковывается с помощью DMA.pack_ctrl().

  • trigger: При необходимости немедленно начать передачу.

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

Возвращает объект IRQ для этого канала DMA и, при необходимости, настраивает его.

close() None

Освобождает захват базового канала DMA и освобождает обработчик прерывания. Объект DMA нельзя использовать после этой операции.

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

Упаковывает значения, предоставленные в именованных аргументах, в соответствующие поля нового значения управляющего регистра. Любое непредоставленное поле получит значение по умолчанию. Значение по умолчанию либо берётся из предоставленного значения default, либо, если оно не задано, используется значение по умолчанию, подходящее для текущего канала; установка его в текущее значение атрибута DMA.ctrl предоставляет простой способ переопределить подмножество полей.

Ключами для именованных аргументов могут быть любые ключи, возвращаемые методом DMA.unpack_ctrl(). Записываемые значения:

  • enable: bool Установите для включения канала (по умолчанию: True).

  • high_pri: bool Сделать трафик шины этого канала высокоприоритетным (по умолчанию: False).

  • size: int Размер передачи: 0=байт, 1=полуслово, 2=слово (по умолчанию: 2).

  • inc_read: bool Увеличивать адрес чтения после каждой передачи (по умолчанию: True).

  • inc_write: bool Увеличивать адрес записи после каждой передачи (по умолчанию: True).

  • ring_size: int Если не равно нулю, то при увеличении адреса будут меняться только младшие ring_size битов одного из адресных регистров, в результате чего адрес будет циклически переноситься на следующей границе 1 << ring_size байт. То, какой именно адрес переносится, управляется флагом ring_sel. Нулевое значение отключает перенос адреса.

  • ring_sel: bool Установите False, чтобы ring_size применялся к адресу чтения, или True, чтобы применялся к адресу записи.

  • chain_to: int Номер канала, который нужно запустить после завершения этой передачи. Установка этого значения в номер собственного канала данного объекта DMA отключает цепочку (это значение по умолчанию).

  • treq_sel: int Выбор сигнала запроса передачи (Transfer Request). Подробности см. в разделе 2.5.3 даташита RP2040.

  • irq_quiet: bool Не генерировать прерывание в конце каждой передачи. Вместо этого прерывания будут генерироваться при записи нулевого значения в регистр запуска, что остановит последовательность связанных в цепочку передач (по умолчанию: True).

  • bswap: bool Если установлено в true, байты в словах или полусловах будут переставлены перед записью (по умолчанию: True).

  • sniff_en: bool Установите True, чтобы разрешить доступ к данным аппаратному модулю sniff чипа (по умолчанию: False).

  • write_err: bool Установка значения True сбросит ранее зарегистрированную ошибку записи.

  • read_err: bool Установка значения True сбросит ранее зарегистрированную ошибку чтения.

Описание всех этих полей см. в описании регистра CH0_CTRL_TRIG в разделе 2.5.7 даташита RP2040.

unpack_ctrl(value: int) dict

Распаковывает значение управляющего регистра канала DMA в словарь с парами ключ/значение для каждого из полей управляющего регистра. value — это значение регистра ctrl для распаковки.

Этот метод возвращает значения для всех ключей, которые можно передать в DMA.pack_ctrl. Кроме того, он также возвращает флаги управляющего регистра, доступные только для чтения: busy, который устанавливается в высокий уровень при начале передачи и в низкий по её окончании, и ahb_err, который является логическим ИЛИ флагов read_err и write_err. Эти значения будут игнорироваться при упаковке, так что словарь, созданный распаковкой управляющего регистра, можно напрямую использовать в качестве именованных аргументов для упаковки.

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

Получает или устанавливает, выполняется ли канал DMA в данный момент.

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

Этот атрибут отражает адрес, с которого будет читать следующая передача по шине. В него можно записать либо целое число, либо объект, поддерживающий протокол буфера, и это сразу же вступает в силу.

write: int

Этот атрибут отражает адрес, по которому будет писать следующая передача по шине. В него можно записать либо целое число, либо объект, поддерживающий протокол буфера, и это сразу же вступает в силу.

count: int

Чтение этого атрибута возвращает количество оставшихся передач по шине в текущей последовательности передач. Запись в этот атрибут задаёт общее количество передач для следующей последовательности передач.

ctrl: int

Этот атрибут отражает управляющий регистр канала DMA. В него обычно записывается целое число, упакованное с помощью метода DMA.pack_ctrl(). Возвращаемое значение регистра можно распаковать с помощью метода DMA.unpack_ctrl().

channel: int

Номер канала DMA. Его можно передать в аргументе chain_to метода DMA.pack_ctrl() другого канала, чтобы обеспечить связывание DMA в цепочку.

registers: 'memoryview'

Этот атрибут является объектом, похожим на массив, который обеспечивает прямой доступ к регистрам канала DMA. Индексация производится по словам, а не по байтам, поэтому индексы регистров — это смещения адресов регистров, делённые на 4. Подробности о регистрах см. в даташите RP2040.

Связывание в цепочку и доступ к регистрам запуска

Контроллер DMA в RP2040 предлагает несколько продвинутых возможностей, позволяющих одному каналу DMA инициировать передачу на другом канале. Одна из них — использование значения chain_to в управляющем регистре, а другая — запись в один из регистров канала DMA, имеющий эффект запуска. В сочетании с возможностью одного канала DMA записывать напрямую в DMA.registers другого канала это позволяет выполнять сложные транзакции без какого-либо вмешательства ЦП.

Ниже приведён пример использования как связывания в цепочку, так и запуска через регистры для реализации сбора нескольких блоков данных в единое место назначения. Полное описание этих возможностей можно найти в разделе 2.5 даташита RP2040, а приведённый ниже код является питоническим вариантом примера из подраздела 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)

Этот пример простаивает в ожидании завершения передачи; в качестве альтернативы он мог бы установить обработчик прерывания и немедленно вернуться.