คลาส DMA -- การเข้าถึงตัวควบคุม DMA ของ RP2040

คลาส DMA ให้การเข้าถึงตัวควบคุม Direct Memory Access (DMA) ของ RP2040 โดยมีความสามารถในการย้ายข้อมูลระหว่างบล็อกหน่วยความจำและ/หรือรีจิสเตอร์ IO ตัวควบคุม DMA มีการเชื่อมต่อบัสมาสเตอร์อ่านและเขียนแยกต่างหากบนผ้าบัส และแต่ละช่องทาง DMA สามารถอ่านข้อมูลจากที่อยู่หนึ่งและเขียนไปยังที่อยู่อื่นได้อย่างอิสระ โดยเลือกเพิ่มหรือลดพอยน์เตอร์อย่างใดอย่างหนึ่งหรือทั้งสอง ทำให้สามารถดำเนินการถ่ายโอนแทนโปรเซสเซอร์ได้ในขณะที่โปรเซสเซอร์ทำงานอื่นหรืออยู่ในสถานะประหยัดพลังงาน ตัวควบคุม DMA ของ RP2040 มี 12 ช่องทาง DMA อิสระที่ทำงานพร้อมกันได้ สำหรับรายละเอียดทั้งหมดของระบบ DMA ของ RP2040 ดูส่วนที่ 2.5 ของ RP2040 Datasheet

ตัวอย่าง

การใช้งาน DMA controller ที่ง่ายที่สุดคือการย้ายข้อมูลจากบล็อกหน่วยความจำหนึ่งไปยังอีกบล็อกหนึ่ง สามารถทำได้ด้วยโค้ดดังต่อไปนี้:

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 controller อีกแบบที่พบได้บ่อยกว่าคือการถ่ายโอนระหว่างหน่วยความจำและอุปกรณ์ IO ต่อพ่วง ในกรณีนี้ที่อยู่ของรีจิสเตอร์ IO จะไม่เปลี่ยนแปลงในแต่ละการถ่ายโอน แต่ที่อยู่หน่วยความจำต้องถูกเพิ่มขึ้น นอกจากนี้ยังจำเป็นต้องควบคุมความเร็วของการถ่ายโอนเพื่อไม่ให้เขียนข้อมูลก่อนที่อุปกรณ์ต่อพ่วงจะยอมรับได้ หรืออ่านก่อนที่ข้อมูลจะพร้อม และสามารถควบคุมได้ด้วยฟิลด์ treq_sel ของรีจิสเตอร์ควบคุมของช่องทาง DMA ฟิลด์ต่างๆ ของรีจิสเตอร์ควบคุมสำหรับแต่ละช่องทาง DMA สามารถบรรจุด้วยเมธอด DMA.pack_ctrl() และแกะออกด้วยเมธอด static DMA.unpack_ctrl() โค้ดสำหรับถ่ายโอนข้อมูลจาก byte array ไปยัง TX FIFO ของ PIO state machine ทีละหนึ่งไบต์มีลักษณะดังนี้:

# 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 state machine ที่เราส่งข้อมูลไป ซึ่งใช้งานได้เนื่องจาก PIO state machine รองรับโปรโตคอล buffer ทำให้สามารถเข้าถึงรีจิสเตอร์ data FIFO ได้โดยตรง

ตัวสร้าง

class rp2.DMA

จองช่องทาง DMA controller หนึ่งช่องเพื่อใช้งานโดยเฉพาะ

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 controller จะเริ่มอ่านข้อมูล หรือออบเจ็กต์ที่จะให้ข้อมูลที่ต้องการอ่าน อาจเป็นจำนวนเต็มหรือออบเจ็กต์ใดก็ตามที่รองรับโปรโตคอล buffer

  • write: ที่อยู่ที่ DMA controller จะเริ่มเขียน หรือออบเจ็กต์ที่จะเขียนข้อมูลลงไป อาจเป็นจำนวนเต็มหรือออบเจ็กต์ใดก็ตามที่รองรับโปรโตคอล buffer

  • 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 ไม่สร้างอินเทอร์รัปต์เมื่อสิ้นสุดแต่ละการถ่ายโอน อินเทอร์รัปต์จะถูกสร้างแทนเมื่อเขียนค่าศูนย์ไปยังรีจิสเตอร์ trigger ซึ่งจะหยุดลำดับการถ่ายโอนแบบเชื่อมต่อ (ค่าเริ่มต้น: 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 ออกเป็น dictionary ที่มีคู่คีย์/ค่าสำหรับแต่ละฟิลด์ในรีจิสเตอร์ควบคุม value คือค่ารีจิสเตอร์ ctrl ที่ต้องการแกะ

เมธอดนี้จะคืนค่าสำหรับคีย์ทั้งหมดที่สามารถส่งผ่านไปยัง DMA.pack_ctrl นอกจากนี้ยังคืนค่าแฟล็กอ่านอย่างเดียวในรีจิสเตอร์ควบคุม: busy ซึ่งจะสูงเมื่อการถ่ายโอนเริ่มต้นและต่ำเมื่อสิ้นสุด และ ahb_err ซึ่งเป็น OR ทางตรรกะของแฟล็ก read_err และ write_err ค่าเหล่านี้จะถูกละเว้นเมื่อบรรจุ ดังนั้น dictionary ที่สร้างโดยการแกะรีจิสเตอร์ควบคุมสามารถใช้โดยตรงเป็นอาร์กิวเมนต์คีย์เวิร์ดสำหรับการบรรจุได้

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

รับหรือตั้งค่าว่าช่องทาง DMA กำลังทำงานอยู่หรือไม่

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

แอตทริบิวต์นี้สะท้อนที่อยู่ที่การถ่ายโอนบัสครั้งถัดไปจะอ่าน สามารถเขียนด้วยจำนวนเต็มหรือออบเจ็กต์ที่รองรับโปรโตคอล buffer และการทำเช่นนั้นมีผลทันที

write: int

แอตทริบิวต์นี้สะท้อนที่อยู่ที่การถ่ายโอนบัสครั้งถัดไปจะเขียน สามารถเขียนด้วยจำนวนเต็มหรือออบเจ็กต์ที่รองรับโปรโตคอล buffer และการทำเช่นนั้นมีผลทันที

count: int

การอ่านแอตทริบิวต์นี้จะคืนค่าจำนวนการถ่ายโอนบัสที่เหลืออยู่ในลำดับการถ่ายโอน ปัจจุบัน การเขียนแอตทริบิวต์นี้จะตั้งค่าจำนวนการถ่ายโอนทั้งหมดสำหรับลำดับการถ่ายโอน ถัดไป

ctrl: int

แอตทริบิวต์นี้สะท้อนรีจิสเตอร์ควบคุมช่องทาง DMA โดยทั่วไปจะเขียนด้วยจำนวนเต็มที่บรรจุโดยใช้เมธอด DMA.pack_ctrl() ค่ารีจิสเตอร์ที่คืนค่ามาสามารถแกะออกได้โดยใช้เมธอด DMA.unpack_ctrl()

channel: int

หมายเลขช่องทางของช่องทาง DMA สามารถส่งผ่านในอาร์กิวเมนต์ chain_to ของ DMA.pack_ctrl() บนช่องทางอื่นเพื่ออนุญาตให้ DMA chaining

registers: 'memoryview'

แอตทริบิวต์นี้เป็นออบเจ็กต์คล้าย array ที่อนุญาตให้เข้าถึงรีจิสเตอร์ของช่องทาง DMA โดยตรง ดัชนีเป็นแบบคำ ไม่ใช่ไบต์ ดังนั้นดัชนีรีจิสเตอร์จึงเป็น offset ที่อยู่รีจิสเตอร์หารด้วย 4 ดูเอกสารข้อมูล RP2040 สำหรับรายละเอียดรีจิสเตอร์

การเชื่อมต่อและการเข้าถึงรีจิสเตอร์ trigger

DMA controller ใน RP2040 มีฟีเจอร์ขั้นสูงสองอย่างเพื่ออนุญาตให้ช่องทาง DMA หนึ่งเริ่มการถ่ายโอนบนช่องทางอื่น หนึ่งคือการใช้ค่า chain_to ในรีจิสเตอร์ควบคุม และอีกหนึ่งคือการเขียนไปยังรีจิสเตอร์ของช่องทาง DMA หนึ่งที่มีผล trigger เมื่อรวมกับความสามารถให้ช่องทาง DMA หนึ่งเขียนโดยตรงไปยัง DMA.registers ของช่องทางอื่น จะทำให้สามารถดำเนินการธุรกรรมที่ซับซ้อนได้โดยไม่ต้องมีการแทรกแซงของ CPU

ด้านล่างเป็นตัวอย่างการใช้ทั้ง chaining และ register triggering เพื่อรวบรวมข้อมูลหลายบล็อกไปยังปลายทางเดียว รายละเอียดทั้งหมดของฟีเจอร์เหล่านี้สามารถดูได้ในส่วนที่ 2.5 ของ RP2040 data sheet และโค้ดด้านล่างเป็นเวอร์ชัน Pythonic ของตัวอย่างในหัวข้อย่อย 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)

ตัวอย่างนี้วนรอว่างขณะรอให้การถ่ายโอนเสร็จสิ้น หรืออาจตั้งค่าตัวจัดการอินเทอร์รัปต์และคืนค่าทันทีก็ได้