Khi tìm hiểu UART trên STM32, các bạn thường tìm hiểu về cách hoạt động của giao tiếp này qua các phương pháp quen thuộc như polling, interrupt hay DMA.
- Phương pháp Polling chỉ phù hợp cho những ứng dụng đơn giản, vi xử lý phải luôn kiểm tra xem có dữ liệu từ UART đến để lưu vào bộ đệm hay không, và có thể nó sẽ gây mất dữ liệu.
- Phương pháp nhận dữ liệu ngắt nhận từng byte có ưu điểm là nhận ngắt tức thời khi có dữ liệu đến, CPU sẽ thực thi ngắt và lưu dữ liệu vào bộ đệm, đồng thời kiểm tra đã nhận đủ dữ liệu chưa và xử lý dữ liệu nhận được. Tuy nhiên, phương pháp này chỉ phù hợp để nhận những chuỗi dữ liệu kích thước ngắn. Nếu kích thước dữ liệu lớn dẫn đến CPU liên tục sinh ra ngắt, làm gián đoạn chương trình và các nhiệm vụ khác.
- Phương pháp DMA là phương pháp hiệu quả hơn vì nó chuyển dữ liệu từ ngoại vi vào bộ nhớ, mà CPU không tham gia vào quá trình này; vì vậy, CPU có thể thực hiện các chức năng khác. Tốc độ giao tiếp bằng DMA cao hơn và có thể lên đến 1Mbps. Tuy nhiên, phương pháp này khi nhận chúng ta phải xác định và biết trước kích thước của dữ liệu.
Một phương pháp tối ưu hơn đó là sử dụng ngắt IDLE kết hợp với DMA. Ngắt IDLE hay còn được gọi là ngắt phát hiện dòng nhàn rỗi, xảy ra khi một khung IDLE được phát hiện. Ưu điểm của nó dùng để nhận được dữ liệu có kích thước bất kỳ không biết trước. Trong bài viết này, chúng ta sẽ cùng nhau thực hành về UART IDLE DMA được xây dựng sẵn trên thư viện HAL.
HƯỚNG DẪN THỰC HÀNH
Bước 1: Tạo mới một project, chọn vi điều khiển, đặt tên cho project.
Bước 2: Tại mục System Core > SYS > Serial Wire.
Bước 3: Cấu hình USART. Mình sử dụng USART2 để thực hiện giao tiếp. Các thông số cấu hình:
- Mode: Asynchronous
- Baudrate: 115200 bits/s
- WordLength: 8 bit, không sử dụng bit Parity
- Stop bit: 1
- Bật ngắt UART lên
- Vào tab DMA Settings để bật DMA lên, chọn chế độ là Normal.
Bước 4: Nhấn vào biểu tượng sinh code.
Trước tiên, các bạn hãy mở và đọc qua mô tả hàm được hỗ trợ bởi thư viện HAL. Vào Divers > STM32Fxxx_HAL_Driver > Src, tìm đến file stm32fxxx_hal_uart_ex.c
Trong bài viết này, mình sẽ tập trung thực hiện bằng phương pháp DMA.
Trước tiên, khai báo một bộ đệm để lưu trữ dữ liệu:
1 2 |
#define RX_SIZE 10 uint8_t rxBuffer[RX_SIZE] = {0}; |
Trong hàm main, chúng ta gọi hàm nhận dữ liệu bằng IDLE DMA. Tuy nhiên, khi sử dụng DMA sẽ sinh ra hai ngắt là HalfReception và EndReception. Chúng ta nên tắt ngắt nhận HalfReception vì nó sẽ gọi hàm RxEventCallback khi nhận được một nữa dữ liệu.
1 2 |
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, RX_SIZE); __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); |
&huart2: là UART2 mà chúng ta đang sử dụng.
rxBuffer: là bộ đệm mà dữ liệu sẽ lưu dữ liệu vào đây.
RX_SIZE: số phần tử sẽ được nhận.
Khi sảy ra sự kiện ngắt, hàm Callback sẽ được gọi và chúng ta có thể biết được kích thước của dữ liệu đã nhận.
1 2 3 4 5 6 |
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, RX_SIZE); __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); size = Size; } |
Mã chương trình
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include "main.h" /* USER CODE BEGIN Includes */ #include "string.h" #include "stdio.h" /* USER CODE END Includes */ UART_HandleTypeDef huart2; DMA_HandleTypeDef hdma_usart2_rx; DMA_HandleTypeDef hdma_usart2_tx; /* USER CODE BEGIN PV */ #define RX_SIZE 10 uint8_t rxBuffer[RX_SIZE] = {0}; uint8_t size = 0; /* USER CODE END PV */ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_DMA_Init(void); static void MX_USART2_UART_Init(void); /* USER CODE BEGIN PFP */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, RX_SIZE); size = Size; } /* USER CODE END PFP */ int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART2_UART_Init(); /* USER CODE BEGIN 2 */ HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, RX_SIZE); __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); /* USER CODE END 2 */ while (1) { } } |
Mở debug, sử dụng Hercules để truyền dữ liệu, quan sát bộ đệm rxBuffer.
Đầu tiên, gửi chuỗi gồm 6 ký tự “abcd\r\n”, ta thấy dữ liệu được lưu từ vị trí 0 đến vị trí 6, giá trị của biến size là 6.
Tiếp theo, gửi chuỗi “123456\r\n” có kích thước lớn hơn chuỗi đầu tiên. Dữ liệu được lưu từ vị trí 0 đến vị trí thứ 7, và giá trị size là 8.
Gửi lại chuỗi thứ nhất, ta thấy dữ liệu lưu từ vị trí 0 đến vị trí thứ 5 và vẫn còn 2 ký tự cũ “\r\n” ở vị trí thứ 6 và thứ 7.
Có thể nhận thấy rằng, dữ liệu luôn được lưu từ vị trí đầu tiên của bộ đệm. Việc này rất tiện cho việc xử lý chuỗi. Không giống như phương pháp DMA, vị trí lưu dữ liệu mới luôn nằm sau vị trí cuối cùng của dữ liệu trước đó.
Tiếp theo, chúng ta gửi một chuỗi có độ dài lớn hơn kích thước bộ đệm “abcd9876543\r\n”.
Phần dữ liệu dư sẽ ghi đè lên dữ liệu cũ từ vị trí đầu tiên của bộ đệm.
Qua bài viết này, hi vọng các bạn có thể thấy được ưu điểm của phương pháp sử dụng ngắt IDLE kết hợp DMA, các chức năng được xây dựng sẵn trong thư viện HAL. Từ đó, phát triển thêm các ứng dụng liên quan đến việc nhận truyền các dữ liệu có kích thước lớn, cải thiện được hiệu suất của thiết bị.
Chúc các bạn thành công!
Cộng tác viên mảng Nhúng tại Cộng đồng Kỹ thuật TAPIT
Hoàng Văn Bình