class DMA – RP2040의 DMA 컨트롤러에 대한 접근

DMA 클래스는 RP2040의 직접 메모리 접근(Direct Memory Access, DMA) 컨트롤러에 대한 접근을 제공하여 메모리 블록 및/또는 IO 레지스터 간에 데이터를 이동하는 기능을 제공합니다. DMA 컨트롤러는 버스 패브릭에 자체적인 별도의 읽기 및 쓰기 버스 마스터 연결을 가지고 있으며, 각 DMA 채널은 한 주소에서 데이터를 독립적으로 읽어 다른 주소에 다시 쓸 수 있고, 선택적으로 한쪽 또는 양쪽 포인터를 증가시킬 수 있어, 프로세서가 다른 작업을 수행하거나 저전력 상태에 진입하는 동안 프로세서를 대신하여 전송을 수행할 수 있습니다. RP2040의 DMA 컨트롤러에는 동시에 실행할 수 있는 12개의 독립적인 DMA 채널이 있습니다. RP2040의 DMA 시스템에 대한 자세한 내용은 RP2040 Datasheet의 2.5절을 참조하세요.

예제

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 컨트롤러의 또 다른, 아마도 더 일반적인 사용법은 메모리와 IO 주변장치 간에 전송하는 것입니다. 이 상황에서는 IO 레지스터의 주소가 각 전송마다 변하지 않지만 메모리 주소는 증가해야 합니다. 또한 주변장치가 데이터를 수락하기 전에 데이터를 쓰거나 데이터가 준비되기 전에 읽지 않도록 전송 속도를 제어해야 하며, 이는 DMA 채널 제어 레지스터의 treq_sel 필드로 제어할 수 있습니다. 각 DMA 채널의 제어 레지스터의 다양한 필드는 DMA.pack_ctrl() 메서드를 사용하여 패킹하고 DMA.unpack_ctrl() 정적 메서드를 사용하여 언패킹할 수 있습니다. 바이트 배열에서 PIO 상태 머신의 TX FIFO로 한 번에 한 바이트씩 데이터를 전송하는 코드는 다음과 같습니다:

# 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

이 DMA 채널의 IRQ 객체를 반환하고 선택적으로 구성합니다.

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 0이 아닌 경우, 주소가 증가할 때 한 주소 레지스터의 하위 ring_size 비트만 변경되어 다음 1 << ring_size 바이트 경계에서 주소가 래핑됩니다. 어느 주소가 래핑되는지는 ring_sel 플래그로 제어됩니다. 값이 0이면 주소 래핑이 비활성화됩니다.

  • ring_sel: bool ring_size를 읽기 주소에 적용하려면 False로, 쓰기 주소에 적용하려면 True로 설정합니다.

  • chain_to: int 이 전송이 완료된 후 트리거할 채널의 채널 번호입니다. 이 값을 이 DMA 객체 자체의 채널 번호로 설정하면 체이닝이 비활성화됩니다(이것이 기본값입니다).

  • treq_sel: int 전송 요청(Transfer Request) 신호를 선택합니다. 자세한 내용은 RP2040 데이터시트의 2.5.3절을 참조하세요.

  • irq_quiet: bool 각 전송이 끝날 때 인터럽트를 생성하지 않습니다. 대신 트리거 레지스터에 0 값이 쓰여질 때 인터럽트가 생성되며, 이는 체인된 전송 시퀀스를 중단합니다(기본값: True).

  • bswap: bool true로 설정하면 워드 또는 하프 워드의 바이트가 쓰기 전에 역순으로 바뀝니다(기본값: True).

  • sniff_en: bool 칩의 스니프 하드웨어가 데이터에 접근할 수 있도록 하려면 True로 설정합니다(기본값: False).

  • write_err: bool 이를 True로 설정하면 이전에 보고된 쓰기 오류가 지워집니다.

  • read_err: bool 이를 True로 설정하면 이전에 보고된 읽기 오류가 지워집니다.

이 모든 필드에 대한 자세한 내용은 RP2040 데이터시트의 2.5.7절에 있는 CH0_CTRL_TRIG 레지스터 설명을 참조하세요.

unpack_ctrl(value: int) dict

DMA 채널 제어 레지스터의 값을 제어 레지스터의 각 필드에 대한 키/값 쌍을 가진 딕셔너리로 언패킹합니다. value는 언패킹할 ctrl 레지스터 값입니다.

이 메서드는 DMA.pack_ctrl에 전달할 수 있는 모든 키에 대한 값을 반환합니다. 또한 제어 레지스터의 읽기 전용 플래그도 반환합니다: 전송이 시작될 때 high가 되고 끝날 때 low가 되는 busy, 그리고 read_errwrite_err 플래그의 논리 OR인 ahb_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 채널의 채널 번호입니다. 이는 다른 채널의 DMA.pack_ctrl()chain_to 인수에 전달하여 DMA 체이닝을 허용할 수 있습니다.

registers: 'memoryview'

이 속성은 DMA 채널의 레지스터에 직접 접근할 수 있게 하는 배열형 객체입니다. 인덱스는 바이트 단위가 아니라 워드 단위이므로, 레지스터 인덱스는 레지스터 주소 오프셋을 4로 나눈 값입니다. 레지스터 세부 정보는 RP2040 데이터시트를 참조하세요.

체이닝 및 트리거 레지스터 접근

RP2040의 DMA 컨트롤러는 한 DMA 채널이 다른 채널에서 전송을 시작할 수 있게 하는 몇 가지 고급 기능을 제공합니다. 하나는 제어 레지스터의 chain_to 값을 사용하는 것이고, 다른 하나는 트리거 효과가 있는 DMA 채널 레지스터 중 하나에 쓰는 것입니다. 한 DMA 채널이 다른 채널의 DMA.registers에 직접 쓸 수 있는 기능과 결합하면, CPU 개입 없이 복잡한 트랜잭션을 수행할 수 있습니다.

아래는 체이닝과 레지스터 트리거링을 모두 사용하여 여러 데이터 블록을 단일 대상으로 수집하는 것을 구현하는 예제입니다. 이러한 기능에 대한 자세한 내용은 RP2040 데이터시트의 2.5절에서 찾을 수 있으며, 아래 코드는 2.5.6.2 하위 절의 예제를 Python 방식으로 작성한 버전입니다.

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)

이 예제는 전송이 완료되기를 기다리는 동안 유휴 상태로 있습니다. 대안으로 인터럽트 핸들러를 설정하고 즉시 반환할 수도 있습니다.