ข้ามไปยังเนื้อหา

DMA

เมื่อจบบทเรียนนี้ คุณจะ

  1. อธิบายว่า DMA ช่วยลดภาระ CPU ในการย้ายข้อมูลอย่างไร และเมื่อใดไม่คุ้ม
  2. ระบุความเสี่ยงของบัฟเฟอร์ที่ DMA กับ CPU ใช้ร่วมกัน เช่น cache และการอ่านก่อนการย้ายเสร็จ

ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5) แล็บของบทนี้เป็นการอ่านโค้ดและค่าตั้งจริงของ SDK เพราะตัวอย่างในแคตตาล็อกของ SDK ที่ commit นี้ยังไม่มีตัวอย่าง DMA โดยตรง

ทวนจากบทก่อนหน้าสองข้อ

  1. บัฟเฟอร์วงแหวนในบทเรียน 1.3 ต้องเขียนข้อมูลก่อนแล้วจึงเลื่อน head ทำไมลำดับนี้สำคัญเมื่อผู้เขียนเป็นอีกบริบทหนึ่ง
  2. ISR ที่ดีในบทเรียน 4.1 ทำงานอะไรบ้าง และส่งอะไรต่อให้ task

เปิด examples/10_dma_cache_sim.c แบบจำลองนี้มีหน่วยความจำจริงที่ “DMA” เขียน และ cache ที่ CPU อ่านผ่าน เหมือน Cortex-M55 ที่มี data cache ทายก่อนรัน ว่าบรรทัด frame 2, stale cache: จะพิมพ์ค่าอะไร

Terminal window
gcc -std=c11 -Wall -Wextra -o dma_cache_sim examples/10_dma_cache_sim.c
./dma_cache_sim

DMA เขียน BB ครบแล้ว แต่ CPU ยังเห็น AA เพราะอ่านจากสำเนาใน cache จนกว่าจะ invalidate และบรรทัด frame 3, read too early: แสดงบั๊กอีกแบบ CPU อ่านตอน DMA เขียนไปครึ่งเดียว ได้ CC ครึ่งหนึ่งกับ BB อีกครึ่ง บั๊กทั้งสองแบบไม่ทำให้โปรแกรมล้ม มันแค่ให้ข้อมูลผิดอย่างเงียบ ๆ

DMA (Direct Memory Access) คือฮาร์ดแวร์ที่ย้ายข้อมูลระหว่างรีจิสเตอร์ของอุปกรณ์กับหน่วยความจำ หรือระหว่างหน่วยความจำด้วยกัน โดยไม่ต้องให้ CPU คัดลอกทีละไบต์ CPU ตั้ง descriptor ครั้งเดียว (ต้นทาง ปลายทาง ขนาดของแต่ละชิ้น จำนวนชิ้น และสัญญาณที่สั่งให้ย้าย) แล้วไปทำอย่างอื่น DMA ย้ายเองทุกครั้งที่อุปกรณ์ส่งสัญญาณ trigger และแจ้งด้วย interrupt เมื่อครบ เอกสารของ PDL สรุปไว้ว่า “The DMA channel can be used in any project to transfer data without CPU intervention basing on a hardware trigger signal from another component.” (cy_dma.h @ release-v3.24.0)

PDL สำหรับชิปนี้มีไดรเวอร์ DMA สองชนิด คือ DataWire (Cy_DMA_* ใน cy_dma.h) และ AXI DMAC (Cy_AXIDMAC_* ใน cy_axidmac.h) ลำดับการตั้งตามเอกสาร: Cy_DMA_Descriptor_Init() แล้ว Cy_DMA_Channel_Init() แล้ว Cy_DMA_Enable() กับ Cy_DMA_Channel_Enable() และเอกสารเตือนว่า “Even if a DMA channel is enabled, it is not operational until the DMA block is enabled”

ตัวอย่างการตัดสินใจเรื่อง DMA ที่มีใน SDK เอง

งาน ทางที่ SDK ใช้ เหตุผล
รับภาพจากกล้องทีละบรรทัด DMA ย้ายค่าจากพอร์ต GPIO ลงบัฟเฟอร์บรรทัด แล้ว AXI DMAC ย้ายบรรทัดไปรวมเป็นภาพ (ไดรเวอร์กล้อง) ข้อมูลมาเร็วกว่าที่ CPU จะรับทีละไบต์ด้วย interrupt ได้
อ่านเขียน FIFO ของเรดาร์ผ่าน SPI Cy_SCB_SPI_Transfer() แบบ interrupt ปริมาณพอที่ ISR ของ SPI จัดการได้
ทดสอบ SPI 8 ไบต์บน header bit-bang ด้วย Cy_GPIO_Write() (ตัวอย่าง Header I/O Test บน Developer Hub) งานสั้นมาก ครั้งคราว การตั้ง DMA ไม่คุ้ม

DMA ไม่คุ้ม เมื่อข้อมูลน้อยจนเวลาตั้ง descriptor มากกว่าเวลาคัดลอกเอง เมื่อข้อมูลต้องถูกประมวลผลทีละไบต์ระหว่างทางอยู่แล้ว เมื่อช่อง DMA มีน้อยและอุปกรณ์อื่นต้องการมากกว่า และเมื่อค่าใช้จ่ายของการดูแล cache (หัวข้อถัดไป) กับความซับซ้อนของโค้ดมากกว่าเวลา CPU ที่ประหยัดได้

DMA กับ CPU ทำงานพร้อมกันจริง บัฟเฟอร์ที่ DMA กำลังเขียนอยู่ห้ามให้ CPU อ่าน วิธีมาตรฐานคือ ping-pong มีสองก้อน DMA เขียนก้อนหนึ่ง ขณะ CPU ประมวลผลอีกก้อน และสลับกันเมื่อ DMA แจ้งว่าเสร็จ ไดรเวอร์กล้องใน SDK ทำแบบนี้ ISR ของสัญญาณ HREF ตั้งปลายทางของ DMA เป็น line_buffer[row_buffer_flag] แล้วสลับ row_buffer_flag ส่วน ISR ของ VSYNC สลับ frame buffer แล้วตั้ง *_frame_ready = true ให้ฝั่งที่ใช้ภาพรู้ว่าพร้อม (mtb_dvp_camera_ov7675.c บรรทัด 627-670) ถ้า CPU ช้ากว่า DMA ต้องมีนโยบายชัดว่าจะทิ้งข้อมูลก้อนไหน และ นับ ทุกครั้งที่ทิ้ง (overrun) แบบที่แบบฝึกของบทนี้ทำ

Cortex-M55 มี data cache CPU อ่านเขียนผ่านสำเนาใน cache ส่วน DMA อ่านเขียนหน่วยความจำจริงโดยไม่ผ่าน cache ความจริงสองชุดนี้ต้องทำให้ตรงกันเอง

  • CPU เขียน แล้ว DMA อ่าน (เช่น descriptor หรือข้อมูลที่จะส่งออก) ต้อง clean คือเขียนสำเนาใน cache ลงหน่วยความจำก่อนสั่ง DMA
  • DMA เขียน แล้ว CPU อ่าน ต้อง invalidate คือทิ้งสำเนาเก่าใน cache ก่อนอ่าน ไม่อย่างนั้นได้ค่าเก่าแบบในตัวอย่าง
  • หรือ วางบัฟเฟอร์ไว้ในหน่วยความจำที่ไม่ผ่าน cache แล้วไม่ต้องทำทั้งสองอย่าง

ไดรเวอร์กล้องใน SDK ใช้ครบทั้งสามแบบ และเขียนเหตุผลไว้ทุกจุด

Cy_DMA_Descriptor_SetDstAddress(&CYBSP_DMA_DVP_CAM_CONTROLLER_Descriptor_0,
(uint8_t*)line_buffer[row_buffer_flag]);
#if defined (__DCACHE_PRESENT) && (__DCACHE_PRESENT != 0)
/* line_buffer is in .cy_sharedmem (non-cacheable) — no invalidate needed.
* DMA descriptor is in cached .cy_socmem_data — clean IS needed. */
SCB_CleanDCache_by_Addr((uint32_t*)&CYBSP_DMA_DVP_CAM_CONTROLLER_Descriptor_0,
sizeof(CYBSP_DMA_DVP_CAM_CONTROLLER_Descriptor_0));
#endif
Cy_DMA_Channel_Enable(CYBSP_DMA_DVP_CAM_CONTROLLER_HW,
CYBSP_DMA_DVP_CAM_CONTROLLER_CHANNEL);
row_buffer_flag = !row_buffer_flag;

ที่มา: mtb_dvp_camera_ov7675.c บรรทัด 637-648 (Apache-2.0, Copyright 2025 Infineon Technologies AG ดัดแปลงโดยทีม TESA ใน tesaiot-pse84-devkit-sdk)

ห้าจุดที่ควรสังเกต: บัฟเฟอร์บรรทัดถูกวางด้วย __attribute__((section(".cy_sharedmem"))) ซึ่งคอมเมนต์บอกว่าไม่ผ่าน cache จึงไม่ต้อง invalidate, descriptor อยู่ในหน่วยความจำที่ผ่าน cache จึงต้อง SCB_CleanDCache_by_Addr() (ฟังก์ชันของ CMSIS-Core ที่ PDL ใช้) ทุกครั้งที่ CPU แก้มัน ก่อนเปิด channel, การ clean ถูกครอบด้วย #if defined (__DCACHE_PRESENT) โค้ดเดียวกันจึงถูกทั้งคอร์ที่มีและไม่มี data cache, เอกสาร cy_dma.h ของ PDL เขียนหลักเดียวกันไว้สำหรับชิปที่มีคอร์ CM7 (“D cache needs to be cleaned before DMA transfer and should be invalidated after DMA transfer”) และ cache ทำงานเป็น line (เอกสาร PDL ระบุ 32 ไบต์สำหรับ CM7) บัฟเฟอร์ของ DMA จึงควรตรงขอบและยาวเต็ม line เพื่อไม่ให้การ clean หรือ invalidate ไปโดนตัวแปรข้าง ๆ

examples/10_dma_cache_sim.c ทำงานเป็นสามท่า

  • ท่าที่ 1 DMA เติมบัฟเฟอร์ครบ CPU อ่านครั้งแรก cache ว่างอยู่จึงได้ของจริง
  • ท่าที่ 2 DMA เติมรอบสอง CPU อ่านโดยไม่ invalidate ได้สำเนาเก่า แล้ว invalidate แล้วอ่านใหม่ได้ของจริง
  • ท่าที่ 3 invalidate ถูกแล้ว แต่อ่านเร็วเกินไป ได้ข้อมูลครึ่งเก่าครึ่งใหม่ จนกว่าจะรอให้ DMA เสร็จ

ลองแก้แล้วทายก่อนรัน

  1. ลบ cache_invalidate(&b); บรรทัดแรกในท่าที่ 3 ออก บรรทัด read too early จะเห็นอะไร และบั๊กสองแบบซ้อนกันทำให้วินิจฉัยยากขึ้นอย่างไร
  2. ทำให้ cpu_read() ไม่ผ่าน cache เลย (เหมือนวางบัฟเฟอร์ไว้ในหน่วยความจำที่ไม่ผ่าน cache) บั๊กแบบไหนหายไป แบบไหนยังอยู่
  3. เพิ่มฟังก์ชัน cpu_write() กับ cache_clean() แล้วจำลองกรณี CPU เตรียมข้อมูลให้ DMA ส่งออกโดยลืม clean

เปิด practice/10_ping_pong.c มีช่องให้เติม 6 จุด มากที่สุดในโมดูลนี้ เพราะนี่คือบทปิดท้ายของโมดูล ตัวจัดการ ping-pong ต้องไม่ให้ DMA ทับก้อนของ CPU นับ overrun เมื่อ CPU ช้า และ invalidate ก่อนคืนก้อนให้ CPU อ่าน

Terminal window
gcc -std=c11 -Wall -Wextra -o ping_pong practice/10_ping_pong.c && ./ping_pong

test ตั้งใจให้ CPU อ่านก้อนแรกไว้หนึ่งครั้งก่อนเริ่ม cache จึงมีสำเนาเก่าค้างอยู่ ถ้าคุณลืม invalidate test ข้อที่อ่านค่า 0x11 จะล้ม

ลองเองก่อนอย่างน้อย 15 นาที แล้วเปิด solution/10_ping_pong.c เราทดลองลบบรรทัด invalidate ออกจากเฉลย แล้วรัน test ได้ FAIL ... expected 17 got 0 คือ test จับบั๊ก cache ได้จริง คอมเมนต์ในเฉลยบอกทางเลือกบนบอร์ดจริงสองทาง invalidate ด้วยฟังก์ชันของ CMSIS-Core ตามขนาดบัฟเฟอร์ หรือวางบัฟเฟอร์ในหน่วยความจำที่ไม่ผ่าน cache แบบไดรเวอร์กล้อง

ตอบคำถาม 4 ข้อใน quiz.yaml (บนเว็บไซต์อยู่ท้ายหน้านี้) ครอบคลุมเป้าหมายทั้งสองข้อ ตอบถูกตั้งแต่ 3 ข้อขึ้นไปถือว่าจบบทเรียน

งาน: อ่านค่าตั้ง DMA จริงใน BSP ของ SDK และไดรเวอร์กล้อง แล้วตอบด้วยหลักฐานว่าข้อมูลไหลอย่างไร

  1. เปิด cycfg_dmas.c ของ BSP (หรือเปิด design.modus ของแม่แบบด้วย Device Configurator ของ ModusToolbox) แล้วจดค่าของ descriptor CYBSP_DMA_DVP_CAM_CONTROLLER_Descriptor_0_config: dataSize descriptorType xCount yCount triggerInType interruptType
  2. คำนวณว่า descriptor หนึ่งตัวย้ายกี่ชิ้น และกี่ไบต์ต่อหนึ่งรอบ แล้วอธิบายว่า CY_DMA_1ELEMENT ใน triggerInType แปลว่า trigger หนึ่งครั้งย้ายเท่าไร (ความหมายของค่า enum เหล่านี้อยู่ใน cy_dma.h)
  3. ใน cycfg_dmas.h หาว่า channel ของกล้องใช้ DMA block ไหน channel เท่าไร และ IRQ ชื่ออะไร BSP ยังตั้ง channel ไหนไว้อีกบ้าง
  4. ในไดรเวอร์กล้อง หาทุกบรรทัดที่เรียก SCB_CleanDCache_by_Addr() แล้วเขียนตารางว่าแต่ละครั้ง clean อะไร และทำไมบัฟเฟอร์ข้อมูลไม่ต้อง invalidate
  5. ตอบคำถามออกแบบ: ถ้าย้าย line_buffer ไปไว้ใน RAM ปกติที่ผ่าน cache ต้องเพิ่มอะไรในโค้ด ที่จุดไหน และจะรู้ได้อย่างไรว่าลืม (อาการที่จะเห็นบนภาพ)

หลักฐานที่เก็บไว้ใน portfolio: ตารางค่าตั้งของ descriptor พร้อมการคำนวณ ตาราง clean ทุกจุด และคำตอบข้อ 5 (หลักสูตรนี้ไม่ได้ขอให้รันไดรเวอร์กล้องบนบอร์ด และไม่ได้ยืนยันว่าแม่แบบที่ build ตามค่าเริ่มต้นคอมไพล์ไดรเวอร์นี้เข้าไปด้วย บทนี้ใช้มันเป็นโค้ดสำหรับอ่าน)

  • อ่านหัวไฟล์ของ cy_axidmac.h แล้วเทียบกับ DataWire DMA ว่าต่างกันอย่างไร และไดรเวอร์กล้องใช้แต่ละตัวทำอะไร
  • โจทย์ท้าทาย: ขยายแบบฝึกเป็นสามก้อน (triple buffering) แล้วดูว่าจำนวน overrun ในสถานการณ์เดียวกันลดลงไหม แลกกับหน่วยความจำเท่าไร
  • เชื่อมกับบทถัดไป: UART, I2C และ SPI ในโมดูล 5 ต่างก็ใช้ DMA ได้เมื่อข้อมูลมาก ลองหาว่า BSP ตั้ง CYBSP_DMA_TX_SPI_CONTROLLER ไว้ให้ SPI ตัวไหน

บทถัดไปเข้าสู่โมดูล 5: บทเรียน 5.1 UART

  • บั๊กของ cache ไม่เกิดบนคอมพิวเตอร์ที่คุณ test เกิดแค่บนชิปที่มี data cache คุณจะเขียน test หรือ checklist แบบไหนให้จับได้ก่อนถึงมือผู้ใช้
  • งานไหนในโปรเจกต์ของคุณที่ CPU กำลังคัดลอกข้อมูลทีละไบต์ และถ้าย้ายไปให้ DMA จะได้เวลา CPU กลับมาเท่าไร

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. ข้อใดอธิบายการทำงานของ DMA ได้ถูกต้องที่สุด (เป้าหมายข้อ 1)

    1. CPU คัดลอกข้อมูลเร็วขึ้นเพราะใช้คำสั่งพิเศษ
    2. CPU ตั้ง descriptor ครั้งเดียว แล้ว DMA ย้ายข้อมูลเองทุกครั้งที่อุปกรณ์ส่ง trigger และแจ้งด้วย interrupt เมื่อครบ
    3. DMA ประมวลผลข้อมูลแทน CPU เช่นกรองสัญญาณ
    4. DMA ทำงานได้ทันทีที่เปิด channel โดยไม่ต้องเปิด DMA block
    ดูเฉลย

    คำตอบ: B. CPU ตั้ง descriptor ครั้งเดียว แล้ว DMA ย้ายข้อมูลเองทุกครั้งที่อุปกรณ์ส่ง trigger และแจ้งด้วย interrupt เมื่อครบ

    DMA ย้ายข้อมูล ไม่ได้ประมวลผล CPU ว่างไปทำงานอื่นระหว่างนั้น และเอกสาร PDL เตือนว่า channel ที่เปิดแล้วยังไม่ทำงานจนกว่าจะเรียก Cy_DMA_Enable() ให้ DMA block

  2. กรณีใดที่ DMA มักไม่คุ้ม (เลือกได้หลายข้อ) (เป้าหมายข้อ 1)

    1. ส่ง 8 ไบต์ครั้งเดียวตอนทดสอบบอร์ด
    2. รับภาพจากกล้องทีละบรรทัดต่อเนื่อง
    3. ข้อมูลต้องถูกตรวจและแปลงทีละไบต์ระหว่างทางอยู่แล้ว
    4. ช่อง DMA เหลือน้อยและอุปกรณ์อื่นที่ข้อมูลมากกว่ากำลังต้องการ
    ดูเฉลย

    คำตอบ: A. ส่ง 8 ไบต์ครั้งเดียวตอนทดสอบบอร์ด · C. ข้อมูลต้องถูกตรวจและแปลงทีละไบต์ระหว่างทางอยู่แล้ว · D. ช่อง DMA เหลือน้อยและอุปกรณ์อื่นที่ข้อมูลมากกว่ากำลังต้องการ

    งานเล็กและครั้งคราวเสียเวลาตั้ง descriptor มากกว่าคัดลอกเอง งานที่ต้องประมวลผลทีละไบต์ได้ประโยชน์น้อย และช่อง DMA เป็นทรัพยากรจำกัด ส่วนภาพจากกล้องคือกรณีที่ DMA คุ้มที่สุด ไดรเวอร์กล้องของ SDK จึงใช้ DMA

  3. บน CM55 ที่มี data cache DMA เพิ่งเขียนบัฟเฟอร์ใน RAM ปกติที่ผ่าน cache เสร็จ CPU ต้องทำอะไรก่อนอ่าน (เป้าหมายข้อ 2)

    1. clean cache ของบัฟเฟอร์นั้น
    2. invalidate cache ของบัฟเฟอร์นั้น เพื่อทิ้งสำเนาเก่าแล้วอ่านจากหน่วยความจำจริง
    3. ไม่ต้องทำอะไร cache รู้เองว่า DMA เขียน
    4. ปิด interrupt ทั้งหมด
    ดูเฉลย

    คำตอบ: B. invalidate cache ของบัฟเฟอร์นั้น เพื่อทิ้งสำเนาเก่าแล้วอ่านจากหน่วยความจำจริง

    DMA เขียนหน่วยความจำจริงโดยไม่ผ่าน cache สำเนาใน cache จึงอาจเก่า ต้อง invalidate ก่อนอ่าน ส่วน clean ใช้เมื่อ CPU เขียนแล้ว DMA จะอ่าน อีกทางคือวางบัฟเฟอร์ในหน่วยความจำที่ไม่ผ่าน cache แบบ line_buffer ของไดรเวอร์กล้อง

  4. ไดรเวอร์กล้องใน SDK ป้องกันความเสี่ยงของบัฟเฟอร์ร่วมอย่างไร (เลือกได้หลายข้อ) (เป้าหมายข้อ 2)

    1. ใช้บัฟเฟอร์สองก้อนสลับกันด้วย row_buffer_flag ให้ DMA กับฝั่งที่อ่านไม่ใช้ก้อนเดียวกันพร้อมกัน
    2. วาง line_buffer ใน .cy_sharedmem ที่ไม่ผ่าน cache จึงไม่ต้อง invalidate
    3. clean cache ของ descriptor ด้วย SCB_CleanDCache_by_Addr() ทุกครั้งที่ CPU แก้ก่อนเปิด channel
    4. ให้ CPU อ่านบัฟเฟอร์ระหว่างที่ DMA เขียนเพื่อประหยัดเวลา
    ดูเฉลย

    คำตอบ: A. ใช้บัฟเฟอร์สองก้อนสลับกันด้วย row_buffer_flag ให้ DMA กับฝั่งที่อ่านไม่ใช้ก้อนเดียวกันพร้อมกัน · B. วาง line_buffer ใน .cy_sharedmem ที่ไม่ผ่าน cache จึงไม่ต้อง invalidate · C. clean cache ของ descriptor ด้วย SCB_CleanDCache_by_Addr() ทุกครั้งที่ CPU แก้ก่อนเปิด channel

    ping-pong กันการอ่านก่อนเสร็จ หน่วยความจำที่ไม่ผ่าน cache ตัดปัญหา invalidate และ descriptor ที่อยู่ในหน่วยความจำผ่าน cache ต้อง clean ก่อน DMA อ่าน คอมเมนต์ในไฟล์เขียนเหตุผลของแต่ละจุดไว้

อ้างอิงบทเรียนนี้

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

"DMA" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ข้อความอ้างอิงภาษาอังกฤษ: "DMA" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0

ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/embedded-c-foundations/m04-peripherals/l04-dma/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).

วิธีอ้างอิง TESA ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA