Sự khác nhau khi sử dụng Polling, Interrupt và DMA trong giao tiếp với ngoại vi UART

Nguyen_bui

Trứng gà
Khi giao tiếp với ngoại vi UART, STM32 cung cấp cho người dùng 2 thiết lập cơ bản chính: UART Polling và UART Interrupt. Ngoài ra STM32 cũng cung cấp cho người dùng chức năng DMA giúp tối ưu hóa hiệu suất cho CPU và ta hoàn toàn có thể áp dụng DMA trong giao tiếp với UART
Trong bài này, chúng ta sẽ áp dụng cả 3 chế độ để so sánh những nhược điểm và ưu điểm của chúng qua 1 ví dụ cụ thể : gửi chuỗi “pif\r\n” qua UART.

1.Polling mode:

Trong file main.c

C:
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  while(1)
  {
    char test[] = "pif\r\n";
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);
    HAL_UART_Transmit(&huart1, (uint8_t*)test, sizeof(test), HAL_MAX_DELAY);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_SET);
    HAL_Delay(2);
  }
}
Giải thích một chút về code:
  • Đầu tiên khởi tạo biến test chứa chuỗi cần gửi qua giao tiếp UART, sau đó hàm HAL_GPIO_WritePin() sẽ kéo chân A12 xuống mức 0.
  • HàmHAL_UART_Transmit() có chức năng gửi chuỗi trong biến test qua ngoại vi UART , sau đó toàn bộ các lệnh sau HAL_UART_Transmit() đều phải đợi quá trình truyền hoàn thành xong-> Có nghĩa nếu giả sử quá trình truyền bị lỗi cộng với việc để tham số Timeout HAL_MAX_DELAY thì cả chương trình đằng sau sẽ không bao giờ được thực hiện. :9cool_too_sad:
Kết quả đo được sau khi generate code:

Polling_UART_official.PNG


Nhận xét:
  • Kết quả đo được từ Logic analyzer cho thấy tin nhắn “pif\r\n” được gửi đến thành công, tuy nhiên trong suốt khoảng thời gian 0.5342 ms, CPU đã phải thường trực công việc chuyển dữ liệu từ ngoại vi UART đến bộ nhớ nội. Điều này rất lãng phí nguồn tài nguyên và lãng phí thời gian của CPU trong khi ta có thể tận dụng khoảng thời gian này cho nhiều mục đích khác.
  • Polling mode là cách cài đặt đơn giản nhất khi giao tiếp với ngoại vi UART tuy nhiên ta sẽ ít thấy cách cài đặt này được sử dụng trong thực tế vì sự kém hiệu quả trong tối ưu hóa nguồn tài nguyên. :5cool_sweat:
2. Interrupt Mode

Trong file main.c
C:
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  while(1)
  {
    char test[] = "pif\r\n";
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_12,GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,GPIO_PIN_SET);
    HAL_UART_Transmit_IT(&huart1, (uint8_t*)test, sizeof(test));
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_12,GPIO_PIN_SET);
    HAL_Delay(2);
  }

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart1)
{
    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
}
Giải thích code trong file main.c:
  • Trong hàm main, chúng ta set up chân GPIOA12 như là một tín hiệu để xác định trong bao lâu thì hàm HAL_UART_Transmit_IT()sẽ return, vậy nên trước khi truyền ta kéo chân A12 lên mức cao, sau đó sau khi truyền xong ta sẽ kéo chân A12 lên mức thấp (chú ý là các chân GPIO sẽ đều có trở kéo lên).
  • Chân GPIOB15 sẽ đóng vai trò như là một tín hiệu để thông báo byte cuối cùng đã được gửi. Sau khi thực hiện truyền chuỗi hoàn tất, hàm HAL_UART_TxCpltCallback() sẽ được gọi và chân B15 sẽ được kéo lên mức cao.
  • Chú ý là với UART Interrupt, hàmHAL_UART_Transmit_IT() sẽ không có tham số Timeout vì CPU không phải đợi quá trình truyền của UART hoàn tất.

Trong file stm32f1xx_it.c
C:
void USART1_IRQHandler(void)
{
  HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,GPIO_PIN_RESET);
  HAL_UART_IRQHandler(&huart1);
  HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,GPIO_PIN_SET);
}
Giải thích code trong file stm32f1xx_it.c
  • Trong hàm USART1_IRQHandler(), hàmHAL_IRQhandler() sẽ được gọi trong đó chân GPIOA8 sẽ giúp chúng ta quan sát tốt hơn khi nào hàm USART1_IRQHandler() được gọi.
  • Hàm USART1_IRQHandler() sẽ được gọi trước mỗi byte data sẵn sàng để truyền đi và sau khi quá trình truyền toàn bộ data hoàn thành (sau đó chương trình sẽ nhảy vào hàm HAL_UART_TxCpltCallback()trong file main.c).
Kết quả đo được sau khi generate code:

Capture.PNG


Nhận xét:

  • Có thể thấy khác với Polling mode, sau khi hàm Transmit_IT được thực thi thì các lệnh ở dưới liền được thực hiện, có nghĩa là CPU không phải đợi quá truyền thực hiện xong và vì thế có thể tranh thủ làm các tác vụ khác.
  • Thực tế khi so sánh tỉ lệ thời gian khi CPU thực hiện ngắt với thời gian truyền 1 kí tự, có thể thấy rằng tác vụ ngắt của CPU chỉ tốn 3.4 us để thực hiện so với 43.4 us- thời gian truyền của 1 byte (bằng xấp xỉ 7.8%).
portion_update.PNG

  • Tuy nhiên có 1 điểm cần lưu ý : không nên sử dụng UART Interrupt khi gửi nhiều chuỗi data 1 cách liên tiếp với tốc độ truyền quá nhanh (trên 38400) vì thời gian của quá trình truyền lớn hơn nhiều so với thời gian hàm HAL_UART_Transmit_IT() chạy -> gây "nghẽn" và dữ liệu cần truyền có thể không nhận được như mong muốn.

3. UART DMA

3.1 DMA là gì ?

  • DMA (Direct Memory Access) là một đơn vị phần cứng (hardware unit) trong STM32 với khả năng cho phép các ngoại vi của STM32 truy cập vào vùng nhớ nội của lõi CPU mà không cần phải thông qua CPU. Vì vậy khi áp dụng cơ chế DMA với các ứng dụng ngoại vi, việc vận chuyển Data từ ngoại vi đến vùng nhớ nội của MCU sẽ được DMA đảm nhiệm. Khi đó CPU có thể thực hiện các tác vụ khác.
Capture.PNG


3.2 Áp dụng cơ chế DMA vào ví dụ trên
Trong file main.c
:
C:
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_DMA_Init();

  char test[] = "pif\r\n";
  HAL_GPIO_WritePin(GPIOA,GPIO_PIN_12,GPIO_PIN_RESET);
  HAL_UART_Transmit_DMA(&huart1,(uint8_t*)test,sizeof(test));
}

void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart1)
{
    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart1)
{
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_12,GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_12,GPIO_PIN_SET);
}
Giải thích code trong file main.c:
  • Chế độ DMA sẽ bao gồm 2 mode: Circular và Normal; mode Circular sẽ truyền chuỗi nhiều lần liên tục còn mode Normal sẽ chỉ truyền chuỗi duy nhất 1 lần, vì trong ví dụ này ta sẽ sử dụng mode Circular nên có thể tập lệnh của chúng ta không cần nằm trong vòng lặp.
  • Sau khi lệnh HA:_UART_Transmit_DMA() được thực thi, chuỗi "pif\r\n" sẽ được DMA phân phối từ vùng nhớ nội đến ngoại vi và tất nhiên CPU không hề biết quá trình truyền đến đâu cho đến khi 2 lần ngắt xảy ra, đó là ngắt khi DMA hoàn thành 1 nửa quá trình truyền và sau khi quá trình truyền hoàn tất.
  • Hàm HAL_UART_TxHalfCpltCallback() sẽ được gọi sau khi lần ngắt đầu tiên xảy ra; tương tự hàm HAL_UART_TxCpltCallback() sẽ được gọi khi quá trình truyền hoàn tất.
Trong file stm32f1xx_it.c:
C:
void DMA1_Channel4_IRQHandler(void)
{
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,GPIO_PIN_RESET);
    HAL_DMA_IRQHandler(&hdma_usart1_tx);
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,GPIO_PIN_SET);
}
Giải thích code trong file stm32f1xx_it.c:
  • HàmDMA1_Channel4_IRQHandle() sẽ là hàm xử lí ngắt trong chương trình chính, ở đây HAL_DMA_IRQHandler sẽ nhảy vào các hàm Callback được định nghĩa ở file main.c và thực thi các tác vụ nằm trong các hàm Callback đó.
Kết quả đo được sau khi generate code:
DMA_UART.PNG




Nhận xét:

  • Rõ ràng ta có thể thấy số lần chương trình phải ngắt đã giảm đi một cách đáng kể, đặc biệt là thời gian ngắt thực thi sẽ không thay đổi theo số lượng data cần gửi. Nghĩa là càng nhiều data được gửi đi (ví dụ 200 kí tự thay vì 3 kí tự) thì ta càng được lợi thêm.
DMA_interrupt.PNG


  • Trong trường hợp ví dụ của chúng ta, tổng thời gian ngắt của cả 1 quá trình truyền sẽ là ~5us x 2 = 10us, so với 0.5208ms. Vậy là thời gian tác vụ ngắt của CPU sẽ chỉ bằng khoảng 1.92 % thời gian của toàn bộ quá trình truyền.
 

Attachments

Last edited:
Top