class DMA -- 存取 RP2040 的 DMA 控制器¶
DMA 類別提供對 RP2040 的直接記憶體存取(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 物件,並可選擇性地設定它。
- 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以允許晶片的探測(sniff)硬體存取資料(預設值: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旗標的邏輯 OR)。這些值在封裝時會被忽略,因此解封裝控制暫存器所建立的字典可直接用作封裝時的關鍵字引數。
- 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)而非位元組(byte)為單位,因此暫存器索引即為暫存器位址偏移量除以 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)
此範例在等待傳輸完成時處於閒置狀態;或者,它也可以設定中斷處理常式並立即回傳。