DMA trong STM32: Hướng Dẫn Cấu Hình Direct Memory Access

Trong lập trình nhúng trên vi điều khiển STM32, một trong những cơ chế quan trọng giúp tối ưu hiệu năng hệ thống là DMA – Direct Memory Access. DMA cho phép truyền dữ liệu tốc độ cao giữa bộ nhớ và ngoại vi mà không cần sự can thiệp của CPU, giải phóng tài nguyên xử lý cho các tác vụ quan trọng hơn. Bài viết này sẽ hướng dẫn toàn diện về DMA trong STM32F1: từ nguyên lý hoạt động, sơ đồ khối, đến cách cấu hình chi tiết.

Hình 1. DMA truyền dữ liệu trực tiếp giữa I/O và Memory, CPU không tham gia vào quá trình này

DMA là gì?

DMA (Direct Memory Access) là cơ chế phần cứng cho phép truyền dữ liệu với tốc độ cao từ thiết bị ngoại vi đến bộ nhớ (và ngược lại), cũng như từ bộ nhớ đến bộ nhớ, mà không thông qua data bus của CPU.

Trong kiến trúc thông thường, mỗi khi ngoại vi (ví dụ UART) nhận được một byte dữ liệu, CPU phải dừng công việc hiện tại, đọc dữ liệu từ thanh ghi ngoại vi, ghi vào bộ nhớ rồi tiếp tục. Với số lượng lớn dữ liệu, điều này tiêu tốn rất nhiều chu kỳ CPU và dễ gây mất dữ liệu nếu CPU đang bận xử lý việc khác.

DMA giải quyết vấn đề này bằng cách:

  • Truyền dữ liệu song song với CPU – CPU không bị chặn trong suốt quá trình truyền.
  • Tự động lưu dữ liệu vào vùng nhớ được chỉ định – người dùng chỉ cần đọc từ vùng nhớ đó khi cần.
  • Tránh mất dữ liệu – dữ liệu từ ngoại vi được ghi liên tục vào buffer mà không phụ thuộc vào trạng thái CPU.
  • DMA có thể điều khiển luồng dữ liệu từ SRAM ↔ Peripheral (UART, SPI, I2C…) theo cả hai chiều.

Nhờ DMA, tài nguyên CPU được giải phóng hoàn toàn để xử lý các tác vụ tính toán, điều khiển logic, hoặc phản hồi thời gian thực – đặc biệt quan trọng trong các ứng dụng automotive embedded như xử lý tín hiệu CAN, truyền nhận dữ liệu LIN tốc độ cao.

Sơ đồ bộ DMA trong STM32F1

STM32F1 tích hợp 2 bộ DMA (DMA1 và DMA2), mỗi bộ có nhiều kênh (channel) độc lập. Mỗi kênh được kết nối cố định với một hoặc nhiều ngoại vi cụ thể.

Hình 2. Sơ đồ khối DMA trong STM32 – DMA1 có 7 kênh, DMA2 có 5 kênh, kết nối với Bus Matrix

Đặc điểm chính của DMA trong STM32F1

  • Các Channel đều có thể được cấu hình riêng biệt và độc lập với nhau.
  • Mỗi Channel được kết nối dành riêng cho tín hiệu DMA từ các thiết bị ngoại vi hoặc tín hiệu từ bên trong MCU.
  • Có 4 mức ưu tiên (priority) có thể lập trình cho mỗi Channel: Very High, High, Medium, Low.
  • Kích thước data linh hoạt: 1 Byte2 Byte (Half Word) hoặc 4 Byte (Word).
  • Hỗ trợ chế độ lặp liên tục (Circular mode) – dữ liệu được truyền vòng như Ring buffer.
  • 5 cờ báo ngắt: DMA Half Transfer, DMA Transfer Complete, DMA Transfer Error, DMA FIFO Error, Direct Mode Error.
  • Quyền truy cập tới: Flash, SRAM, APB1, APB2, AHB.
  • Số lượng data có thể lập trình được lên tới 65.535 đơn vị.
  • Đối với DMA2, mỗi luồng hỗ trợ truyền Memory-to-Memory (bộ nhớ đến bộ nhớ).

Bảng phân kênh DMA1 – STM32F103

Bảng dưới đây là bảng phân kênh DMA1 trong STM32F103, cho thấy ngoại vi nào sử dụng kênh nào:

Hình 3. Bảng phân kênh DMA1 – mỗi ngoại vi được gán cố định vào một kênh DMA cụ thể

Ví dụ: nếu muốn dùng DMA với USART1_RX, bạn phải dùng DMA1 Channel 5; còn USART1_TX dùng DMA1 Channel 4. Việc chọn sai channel sẽ khiến DMA không hoạt động.

Hai chế độ hoạt động của DMA

DMA trong STM32 có 2 chế độ hoạt động chính:

Chế độMô tảỨng dụng phù hợp
Normal modeDMA truyền đủ lượng dữ liệu đã khai báo rồi tự dừng. Muốn truyền tiếp phải khởi động lại (re-enable).Gửi một gói dữ liệu cố định, flash một buffer ra UART một lần.
Circular modeKhi truyền đủ lượng dữ liệu, DMA tự quay về đầu buffer và tiếp tục (cơ chế Ring buffer). Không cần khởi động lại.Thu thập dữ liệu ADC liên tục, nhận dữ liệu UART streaming.

Cấu hình DMA trong STM32F1

Việc cấu hình DMA bao gồm 3 bước chính: cấp xung clock, cấu hình ngoại vi tương ứng, và cấu hình các tham số DMA.

Hình 4. Sơ đồ khối STM32F103xx – DMA được cấp xung từ AHB và kết nối thông qua Bus Matrix

Bước 1: Cấp xung clock cho DMA

Khác với các ngoại vi thường dùng APB1/APB2, DMA được cấp xung từ AHB (Advanced High-performance Bus). Cả DMA1 và DMA2 đều lấy xung từ bus AHB.

/* Cấp xung clock cho DMA1 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

/* Nếu dùng DMA2 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);

Bước 2: Cấu hình ngoại vi

DMA truyền nhận dữ liệu từ ngoại vi, nên trước hết cần cấu hình ngoại vi tương ứng (ví dụ: UART, SPI, ADC…) và bật tính năng DMA của ngoại vi đó.

/* Ví dụ bật DMA cho USART1 TX */
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);

/* Hoặc cho USART1 RX */
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);

Bước 3: Cấu hình struct DMA_InitTypeDef

Toàn bộ tham số DMA được cấu hình qua struct DMA_InitTypeDef. Dưới đây là mô tả chi tiết từng trường:

Tham sốMô tảVí dụ giá trị
DMA_PeripheralBaseAddrĐịa chỉ thanh ghi data của ngoại vi – nơi DMA lấy/gửi data(uint32_t)&USART1->DR
DMA_MemoryBaseAddrĐịa chỉ vùng nhớ (buffer) cần ghi/đọc data(uint32_t)rxBuffer
DMA_DIRHướng truyền: từ ngoại vi → bộ nhớ hoặc bộ nhớ → ngoại viDMA_DIR_PeripheralSRC
DMA_BufferSizeKích thước buffer – số lượng data muốn gửi/nhận64
DMA_PeripheralIncĐịa chỉ ngoại vi có tự tăng sau mỗi lần truyền không? Thường là DISABLE vì thanh ghi DR cố định.DMA_PeripheralInc_Disable
DMA_MemoryIncĐịa chỉ bộ nhớ có tự tăng sau mỗi lần truyền không? Thường là ENABLE để ghi vào các phần tử liên tiếp của buffer.DMA_MemoryInc_Enable
DMA_PeripheralDataSizeĐộ lớn data phía ngoại vi: Byte, HalfWord hoặc WordDMA_PeripheralDataSize_Byte
DMA_MemoryDataSizeĐộ lớn data phía bộ nhớ: Byte, HalfWord hoặc WordDMA_MemoryDataSize_Byte
DMA_ModeChế độ hoạt động: Normal hoặc CircularDMA_Mode_Circular
DMA_PriorityMức ưu tiên kênh DMA: Low, Medium, High, VeryHighDMA_Priority_High
DMA_M2MBật chế độ Memory-to-Memory (chỉ DMA2 hỗ trợ)DMA_M2M_Disable

Ví dụ hoàn chỉnh: DMA nhận UART (USART1 RX)

Dưới đây là ví dụ cấu hình DMA1 Channel 5 để nhận dữ liệu từ USART1 vào buffer 64 byte theo Circular mode:

#define RX_BUFFER_SIZE  64
uint8_t rxBuffer[RX_BUFFER_SIZE];

void DMA_UART_RX_Init(void)
{
    DMA_InitTypeDef DMA_InitStruct;

    /* Bước 1: Cấp xung AHB cho DMA1 */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

    /* Bước 2: Cấu hình tham số DMA */
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStruct.DMA_MemoryBaseAddr     = (uint32_t)rxBuffer;
    DMA_InitStruct.DMA_DIR                = DMA_DIR_PeripheralSRC;  /* Peripheral → Memory */
    DMA_InitStruct.DMA_BufferSize         = RX_BUFFER_SIZE;
    DMA_InitStruct.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStruct.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
    DMA_InitStruct.DMA_Mode               = DMA_Mode_Circular;  /* Ring buffer */
    DMA_InitStruct.DMA_Priority           = DMA_Priority_High;
    DMA_InitStruct.DMA_M2M                = DMA_M2M_Disable;

    /* Bước 3: Khởi tạo DMA1 Channel 5 (USART1_RX) */
    DMA_Init(DMA1_Channel5, &DMA_InitStruct);

    /* Bước 4: Bật DMA channel */
    DMA_Cmd(DMA1_Channel5, ENABLE);

    /* Bước 5: Bật DMA request phía USART1 */
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
}

Sau khi gọi hàm khởi tạo trên, DMA sẽ tự động nhận dữ liệu từ USART1 vào rxBuffer[] mà không cần CPU xử lý từng byte. Người dùng chỉ cần đọc rxBuffer tại thời điểm thích hợp.

Đọc dữ liệu từ buffer DMA (Circular mode)

Trong Circular mode, DMA liên tục ghi vào buffer theo vòng. Để biết DMA đang ghi đến vị trí nào, dùng hàm:

/* Vị trí hiện tại DMA đang ghi */
uint16_t dmaWritePos = RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);

/* Ví dụ: đọc toàn bộ dữ liệu đã nhận */
for (uint16_t i = 0; i < dmaWritePos; i++) {
    processData(rxBuffer[i]);
}

Lưu ý khi sử dụng DMA

  • Chọn đúng kênh DMA theo bảng mapping của datasheet – mỗi ngoại vi chỉ hỗ trợ một kênh cố định.
  • Khi dùng DMA với bộ nhớ có cache (ví dụ STM32F7/H7), cần flush/invalidate cache trước và sau khi truyền để tránh đọc dữ liệu cũ.
  • Với Normal mode, sau khi DMA hoàn thành, phải gọi DMA_Cmd(..., DISABLE) rồi DMA_Init lại, hoặc dùng ngắt DMA_IT_TC (Transfer Complete) để tái khởi động.
  • Circular mode không cần tái khởi động, nhưng cần cẩn thận với race condition khi CPU đọc buffer trong khi DMA đang ghi.
  • Khi DMA và CPU cùng truy cập một vùng nhớ, nên dùng volatile để tránh compiler tối ưu hóa sai.

Kết luận

DMA (Direct Memory Access) là một trong những tính năng phần cứng quan trọng nhất trong STM32, đặc biệt với các ứng dụng yêu cầu truyền nhận dữ liệu tốc độ cao và liên tục như UART streaming, thu thập ADC, truyền SPI/I2C khối lượng lớn.

Với STM32F1, DMA1 có 7 kênh và DMA2 có 5 kênh, mỗi kênh được gán cố định cho các ngoại vi cụ thể. Việc cấu hình DMA thông qua struct DMA_InitTypeDef khá trực quan, chỉ cần xác định đúng địa chỉ nguồn/đích, kích thước data, hướng truyền và chế độ hoạt động.

Bằng cách tận dụng DMA, CPU được giải phóng hoàn toàn khỏi các tác vụ truyền dữ liệu lặp lại, giúp hệ thống nhúng hoạt động hiệu quả hơn, đáp ứng tốt các yêu cầu thời gian thực trong lập trình automotive embedded.

Leave a Reply