class DMA -- 访问 RP2040 的 DMA 控制器¶
DMA 类提供对 RP2040 的直接内存访问(Direct Memory Access,DMA)控制器的访问,能够在内存块和/或 IO 寄存器之间移动数据。DMA 控制器在总线网络上拥有自己独立的读写总线主控连接,每个 DMA 通道都可以独立地从一个地址读取数据并将其写回到另一个地址,可选地递增其中一个或两个指针,从而能够在处理器执行其他任务或进入低功耗状态时代表处理器执行传输。RP2040 的 DMA 控制器有 12 个可并发运行的独立 DMA 通道。有关 RP2040 DMA 系统的完整细节,请参阅 RP2040 数据手册 的 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 对象,并可选地对其进行配置。
- 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)信号。详见 RP2040 数据手册的 2.5.3 节。irq_quiet:
bool不在每次传输结束时生成中断。中断将改为在向触发寄存器写入零值时生成,这会停止一系列链接传输(默认值: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的键的值。此外,它还将返回控制寄存器中的只读标志:busy,在传输开始时变高、结束时变低;以及ahb_err,它是read_err和write_err标志的逻辑或。这些值在打包时会被忽略,因此解包控制寄存器所创建的字典可以直接用作打包的关键字参数。
- active(value: bool | None = None, /) bool¶
获取或设置 DMA 通道当前是否正在运行。
>>> sm.active() 0 >>> sm.active(1) >>> while sm.active(): ... pass
- ctrl: int¶
此属性反映 DMA 通道控制寄存器。通常使用
DMA.pack_ctrl()方法打包的整数对其进行写入。返回的寄存器值可以使用DMA.unpack_ctrl()方法解包。
- channel: int¶
DMA 通道的通道号。可以将其传递给另一个通道上
DMA.pack_ctrl()的chain_to参数,以实现 DMA 链接。
- registers: 'memoryview'¶
此属性是一个类数组对象,允许直接访问 DMA 通道的寄存器。索引按字(word)而非字节进行,因此寄存器索引是寄存器地址偏移量除以 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)
本示例在等待传输完成时处于空闲状态;或者,它也可以设置一个中断处理程序并立即返回。