Skip to content

20 Nhận dữ liệu UART bằng DMA và bằng ngắt ​

Khởi tạo UART ​

Cấu hình GPIO ​

Đầu tiên, ta cần cấu hình hai chân PA9 và PA10 của GPIOA để sử dụng cho USART1:

  • PA9 → chân TX (truyền dữ liệu)
  • PA10 → chân RX (nhận dữ liệu)

Các thiết lập cấu hình như sau:

  • Chế độ: Chân đa chức năng (Alternate Function)
  • Tốc độ: 100 MHz (cao)
  • Kiểu output: Push-pull (đẩy kéo)
  • Chân RX (PA10) cần bật pull-up để giữ mức ổn định khi không có tín hiệu
c
GPIO_InitTypeDef GPIO_InitStructure; // Cấu trúc cấu hình GPIO

// Bật clock cho GPIOA
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

// Gán chức năng chân cho USART1
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);   // PA9 làm chân TX (USART1_TX)
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);  // PA10 làm chân RX (USART1_RX)

// Cấu hình các tham số GPIO
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;       // Sử dụng cả hai chân PA9 và PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;                  // Chế độ chức năng thay thế (Alternate Function)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;            // Tốc độ cao (100MHz)
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;                // Kiểu xuất push-pull
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;                  // Kéo lên (pull-up)
GPIO_Init(GPIOA, &GPIO_InitStructure);                        // Áp dụng cấu hình cho GPIOA

Cấu hình USART1 ​

Thiết lập các thông số hoạt động cho USART1 bao gồm:

  • Tốc độ baud: dùng biến __baud để dễ thay đổi
  • Độ dài dữ liệu: 8 bit
  • Số bit dừng: 1 bit
  • Kiểm tra chẵn lẻ (parity): không dùng
  • Flow control: không dùng phần cứng
  • Chế độ: vừa truyền vừa nhận (TX + RX)
c
USART_InitTypeDef USART_InitStructure; // Cấu trúc cấu hình USART

// Bật clock cho USART1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

// Cấu hình các tham số của USART1
USART_InitStructure.USART_BaudRate = __baud;                       // Tốc độ baud (được truyền vào từ tham số)
USART_InitStructure.USART_WordLength = USART_WordLength_8b;        // Độ dài dữ liệu: 8 bit
USART_InitStructure.USART_StopBits = USART_StopBits_1;             // 1 bit dừng
USART_InitStructure.USART_Parity = USART_Parity_No;                // Không dùng kiểm tra chẵn lẻ
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // Không dùng flow control
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;    // Kích hoạt cả nhận và truyền

// Áp dụng cấu hình cho USART1
USART_Init(USART1, &USART_InitStructure);

Lưu ý: Biến __baud là tham số đầu vào của hàm, giúp bạn dễ dàng thay đổi tốc độ baud trong chương trình.

Cấu hình ngắt (Interrupt Configuration) ​

Cấu hình ưu tiên và kích hoạt ngắt cho:

  • USART1 (ngắt khi phát hiện chế độ nhàn rỗi – idle)
  • DMA2_Stream5 (DMA nhận)
  • DMA2_Stream7 (DMA truyền)
NVIC_InitTypeDef NVIC_InitStructure;  // Cấu trúc cấu hình ngắt

// Cấu hình ngắt USART1
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;                 // Kênh ngắt cho USART1
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;        // Mức ưu tiên ngắt (preemptive) = 2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;               // Mức ưu tiên phụ (sub) = 1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                  // Bật kênh ngắt
NVIC_Init(&NVIC_InitStructure);                                  // Áp dụng cấu hình

// Cấu hình ngắt cho DMA nhận (Stream5)
NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream5_IRQn;          // Kênh ngắt cho DMA2_Stream5 (USART1_RX)
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;        // Ưu tiên = 2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;               // Ưu tiên phụ = 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// Cấu hình ngắt cho DMA truyền (Stream7)
NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;          // Kênh ngắt cho DMA2_Stream7 (USART1_TX)
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;        // Ưu tiên = 2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;               // Ưu tiên phụ = 3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// Cấu hình ngắt cho USART1 ở chế độ nhàn rỗi (idle line interrupt)
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);       // Bật ngắt phát hiện "đường truyền rảnh"

// Kích hoạt chế độ DMA cho USART1
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);       // Bật DMA cho truyền nhận (RX)
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);       // Bật DMA cho truyền gửi (TX)

Giải thích nhanh:

  • USART_IT_IDLE giúp phát hiện kết thúc một frame dữ liệu → cực kỳ hữu ích khi dùng với DMA để xử lý một khối dữ liệu lớn
  • DMA2_Stream5 thường dùng cho USART1_RX
  • DMA2_Stream7 thường dùng cho USART1_TX
  • Ngắt DMA giúp xác định khi DMA hoàn thành quá trình truyền/nhận

Cấu hình DMA truyền nhận cho USART1 ​

Cấu hình bao gồm:

  • Bật clock DMA
  • Cấu hình DMA2_Stream5 → dùng cho USART1_RX
  • Cấu hình DMA2_Stream7 → dùng cho USART1_TX
  • Thiết lập kênh, địa chỉ, chế độ truyền, ưu tiên, burst, v.v
c
DMA_InitTypeDef DMA_InitStructure;

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); // Bật clock cho DMA2

DMA_DeInit(DMA2_Stream5); // Reset lại cấu hình stream trước đó

DMA_InitStructure.DMA_Channel = DMA_Channel_4;  // USART1 sử dụng DMA Channel 4
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // Địa chỉ thanh ghi dữ liệu USART1
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)DMA_USART1_RX_BUF; // Bộ đệm đích trong RAM
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; // Ngoại vi → RAM
DMA_InitStructure.DMA_BufferSize = USART_MAX_LEN; // Số byte cần nhận
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // Địa chỉ ngoại vi cố định
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // RAM tăng địa chỉ sau mỗi byte
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // Chế độ truyền đơn
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; // Ưu tiên cao
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; // Không dùng FIFO
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

DMA_Init(DMA2_Stream5, &DMA_InitStructure);
DMA_Cmd(DMA2_Stream5, ENABLE); // Bật DMA RX

Trình phục vụ ngắt USART1 (USART1_IRQHandler) ​

Mục tiêu: Khi ngắt IDLE (đường truyền rảnh) xảy ra, nghĩa là đã nhận xong một frame dữ liệu, ta sẽ:

  • Tắt DMA để xử lý dữ liệu
  • Tính độ dài dữ liệu đã nhận
  • Gửi lại dữ liệu vừa nhận (chức năng echo)
  • Khởi động lại DMA để chuẩn bị cho lần nhận tiếp theo
c
void USART1_IRQHandler(void)  // Trình xử lý ngắt USART1
{
    // 1. Nếu phát hiện ngắt Idle (USART nhận xong 1 frame)
    if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
    {
        usart1_recv_end_flag = 1;  // Đánh dấu đã nhận xong 1 frame

        DMA_Cmd(DMA2_Stream5, DISABLE);  // Tạm thời tắt DMA RX để xử lý dữ liệu

        // Tính số byte đã nhận được
        usart1_rx_len = USART_MAX_LEN - DMA_GetCurrDataCounter(DMA2_Stream5);

        // Xóa cờ DMA truyền xong
        DMA_ClearFlag(DMA2_Stream5, DMA_FLAG_TCIF5);

        // Gửi lại (echo) dữ liệu vừa nhận bằng DMA truyền
        DMA_USART1_Send(DMA_USART1_RX_BUF, usart1_rx_len);

        // Reset lại bộ đếm DMA để chuẩn bị nhận frame tiếp theo
        DMA_SetCurrDataCounter(DMA2_Stream5, USART_MAX_LEN);
        DMA_Cmd(DMA2_Stream5, ENABLE);  // Bật lại DMA nhận

        USART_ReceiveData(USART1); // Đọc 1 byte để xóa cờ ngắt idle
    }

    // 2. Kiểm tra ngắt TXE (gửi xong)
    if (USART_GetFlagStatus(USART1, USART_IT_TXE) == RESET)
    {
        USART_ITConfig(USART1, USART_IT_TC, DISABLE); // Tắt ngắt truyền xong
        usart1_rx_len = 0;  // Reset độ dài dữ liệu đã gửi
    }
}

Trình xử lý ngắt DMA khi truyền xong (DMA2_Stream7_IRQHandler) ​

Khi DMA2_Stream7 (dùng để truyền dữ liệu qua USART1) hoàn thành việc gửi dữ liệu, trình phục vụ ngắt sẽ thực hiện:

  • Xóa cờ ngắt truyền hoàn tất
  • Tắt DMA2_Stream7 để chuẩn bị cho lần gửi tiếp theo
  • Bật ngắt truyền hoàn tất USART1 để xử lý hậu kỳ (ví dụ: reset cờ, gửi tiếp...)
c
void DMA2_Stream7_IRQHandler(void)  // Ngắt DMA truyền USART1 TX
{
    if (DMA_GetITStatus(DMA2_Stream7, DMA_IT_TCIF7) != RESET) // Kiểm tra cờ "truyền hoàn tất"
    {
        DMA_ClearITPendingBit(DMA2_Stream7, DMA_IT_TCIF7); // Xóa cờ ngắt

        DMA_Cmd(DMA2_Stream7, DISABLE);  // Tắt DMA truyền

        USART_ITConfig(USART1, USART_IT_TC, ENABLE); // Bật ngắt truyền xong của USART
    }
}

Hàm gửi dữ liệu ​

Chức năng:

  • Sao chép dữ liệu cần gửi vào bộ đệm DMA_USART1_TX_BUF
  • Thiết lập số lượng byte cần truyền
  • Bật DMA2_Stream7 để bắt đầu truyền qua USART1
c
void DMA_USART1_Send(uint8_t *data, uint16_t len)
{
    uint16_t i;

    // 1. Sao chép dữ liệu vào buffer DMA TX
    for (i = 0; i < len; i++)
    {
        DMA_USART1_TX_BUF[i] = data[i];
    }

    // 2. Cấu hình độ dài dữ liệu cần truyền
    DMA_SetCurrDataCounter(DMA2_Stream7, len);

    // 3. Bật DMA truyền để bắt đầu gửi qua USART1
    DMA_Cmd(DMA2_Stream7, ENABLE);
}

Giải thích:

  • data: con trỏ đến dữ liệu cần gửi
  • len: số lượng byte cần truyền
  • DMA_USART1_TX_BUF: buffer cố định đã được cấp phát trước (ví dụ: uint8_t DMA_USART1_TX_BUF[USART_MAX_LEN];)
  • DMA_SetCurrDataCounter(...): thiết lập lại số byte DMA sẽ xử lý trong lần truyền này
  • DMA_Cmd(..., ENABLE): kích hoạt DMA2_Stream7 bắt đầu truyền

Hiện tượng thực nghiệm ​

Mã nguồn của chương này có thể tìm thấy tại:

Đường dẫn trong tài liệu của bo mạch: 立创·梁山派·天空星 STM32F407VET6 开发板资料/第03章软件资料/代码例程/010 串口中断DMA接收二合一


Sau khi nạp chương trình vào bo mạch:

  • Khi bạn gửi dữ liệu từ phần mềm giao tiếp UART,
  • Dữ liệu sẽ được hiển thị lại (echo) trên chính phần mềm đó.

Điều này chứng minh rằng:

  • Dữ liệu đã được DMA nhận, sau đó gửi lại bằng DMA
  • Quá trình này chỉ gây ra một lần ngắt duy nhất (ngắt IDLE) mỗi frame → hiệu suất cao, CPU gần như không bị chiếm dụng

img