หลักการทำงานของ DMA
โดยปกติแล้ว เวลาที่เราต้องย้ายข้อมูล เช่น:
- อ่านข้อมูลจาก USART แล้วบันทึกใน RAM
- หรือ อ่านข้อมูลจาก ADC แล้วเก็บใน Buffer
ถ้าไม่มี DMA: CPU จะต้องอ่านข้อมูลทีละ Byte/Word แล้วเขียนไปยังเป้าหมายเอง → กินเวลาของ CPU มาก
แต่ถ้ามี DMA: CPU แค่ตั้งค่าครั้งเดียว → แล้วปล่อยให้ DMA ทำงานแทน → CPU ว่างไปทำงานอย่างอื่นได้ทันที
ประเภทการโอนข้อมูลของ DMA
- Peripheral-to-Memory
ตัวอย่างเช่น: ADC อ่านค่ามา → เขียนลง RAM อัตโนมัติ - Memory-to-Peripheral
ตัวอย่างเช่น: ส่งข้อมูลใน RAM ออก USART โดยอัตโนมัติ - Memory-to-Memory
ตัวอย่างเช่น: คัดลอกข้อมูลจากที่อยู่หนึ่งใน RAM ไปอีกที่หนึ่งใน RAM โดยตรง
จุดเด่นของการใช้ DMA
- ลดภาระ CPU: เพราะ CPU ไม่ต้องย้ายข้อมูลเอง
- โอนข้อมูลเร็วขึ้น: เพราะ DMA ทำการย้ายแบบต่อเนื่องในฮาร์ดแวร์
- เหมาะกับงานที่ต้องโอนข้อมูลปริมาณมากหรือต่อเนื่อง เช่น:
- Streaming ข้อมูล
- การอ่านเซ็นเซอร์อย่างต่อเนื่อง
- การสื่อสารข้อมูลจำนวนมากผ่าน USART/SPI/I2C
สรุปเปรียบเทียบ
หัวข้อ | ไม่มี DMA (CPU ทำเอง) | มี DMA |
---|---|---|
ภาระ CPU | สูง | ต่ำ |
ความเร็วโอนข้อมูล | ปานกลาง | สูงมาก |
การตั้งค่า | ง่าย แต่ทำงานหนัก | ต้องตั้งค่าระบบล่วงหน้า |
การใช้ในงานจริง | งานง่าย ๆ | งานต้องการความเร็ว |
สรุปการใช้งาน DMA (Direct Memory Access) และขั้นตอนการเขียนโปรแกรมใช้งานบน CH32V003
1. ข้อมูลพื้นฐานของ DMA บน CH32V003
- มี DMA Controller จำนวน 1 ชุด
- รองรับ 7 Channels
- รองรับรูปแบบการโอนข้อมูล:
- Memory-to-Memory
- Peripheral-to-Memory
- Memory-to-Peripheral
- รองรับ Ring Buffer (โหมดวนซ้ำ)
- Peripheral ที่รองรับ DMA ได้แก่:
- USART
- I2C
- SPI
- ADC
- TIMx (Timer)
เป้าหมาย คือให้ CPU ไม่ต้องทำการย้ายข้อมูลเอง → ลดภาระงานของ CPU และเพิ่มประสิทธิภาพของระบบ
2. ขั้นตอนการเขียนโปรแกรมใช้งาน DMA
(1) เปิดใช้งาน Clock ของ DMA
ต้องเปิด Clock ให้โมดูล DMA ก่อนใช้งาน โดยใช้ฟังก์ชัน SDK เช่น RCC_AHBPeriphClockCmd()
(2) ตั้งค่าโครงสร้างข้อมูล DMA_InitTypeDef
ใช้งานโครงสร้าง DMA_InitTypeDef
เพื่อกำหนดพารามิเตอร์ต่าง ๆ เช่น:
- ที่อยู่ต้นทาง (Source Address)
- ที่อยู่ปลายทาง (Destination Address)
- ทิศทางการโอน (Direction)
- ขนาดข้อมูลแต่ละครั้ง (Data Size)
- จำนวนข้อมูลทั้งหมด (Transfer Size)
- โหมด (Normal/ Circular)
- การเพิ่ม Address (Increment Mode)
ตัวอย่างโครงสร้าง
DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->DATAR); // Peripheral source/destination DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)RxBuffer; // RAM source/destination DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // ทิศทาง DMA_InitStructure.DMA_BufferSize = 64; // ขนาดข้อมูล DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // Peripheral ไม่ต้องเพิ่ม Address DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // RAM ต้องเพิ่ม Address DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &DMA_InitStructure);
(3) เปิดใช้งาน DMA Channel
เมื่อตั้งค่าพร้อมแล้ว เปิด DMA Channel ด้วยฟังก์ชัน DMA_Cmd()
เช่น:
DMA_Cmd(DMA1_Channel1, ENABLE);
(4) จัดการ Interrupt (ถ้าต้องการ)
หากต้องการใช้งาน DMA Interrupt เช่น เมื่อโอนข้อมูลเสร็จสิ้น ให้:
- เปิดใช้งาน Interrupt
- เขียนฟังก์ชัน ISR (Interrupt Service Routine)
ตัวอย่างเปิด Interrupt:
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
ใน ISR:
void DMA1_Channel1_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); // ใส่โค้ดหลังโอนข้อมูลเสร็จ } }
3. สรุปขั้นตอนแบบลำดับ
- เปิด Clock ให้ DMA
- เตรียมข้อมูลที่ต้องโอน (Buffer / Peripheral)
- กำหนดค่า DMA_InitTypeDef
- เรียก
DMA_Init()
เพื่อลงทะเบียนการตั้งค่า - เปิดใช้งาน DMA Channel (
DMA_Cmd(ENABLE)
) - (ถ้าจำเป็น) เปิด DMA Interrupt และเขียนฟังก์ชัน IRQ Handler
- เริ่มการโอนข้อมูล (Peripheral จะ trigger DMA ตามเงื่อนไข)
หมายเหตุพิเศษ
- ต้องแน่ใจว่า Peripheral ที่เกี่ยวข้อง (USART/SPI/ADC ฯลฯ) เปิด DMA Request ก่อน เช่น
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
- ต้องระวังเรื่อง Size Alignment เช่น Peripheral/Memory ขนาดไม่ตรงกัน อาจทำให้ DMA Error ได้