Đối với những lập trình viên đã và đang làm việc với vi điều khiển STM32 chắc hẳn từng đọc qua các lưu ý khi viết một chương trình phục vụ ngắt, trong đó có một điểm là cẩn thận khi sử dụng hàm HAL_Delay ở các chương trình này. Vậy việc gọi HAL_Delay trong các chương trình phục vụ ngắt sẽ tác động đến luồng thực thi của vi điều khiển như thế nào?
Để có thể làm rõ vấn đề trên, chúng ta hãy cùng tìm hiểu về system timer, ngắt SysTick và hàm HAL_Delay trong thư viện HAL do hãng STMicroelectronic cung cấp.
1. Giới thiệu về system timer, ngắt SysTick
– Vi xử lý lõi ARM Cortex M có một bộ đếm thời gian 24-bit hay còn gọi là 24-bit system timer. Đây là một bộ đếm xuống, nhiệm vụ của system timer là đếm từ giá trị reload được nạp tại thanh ghi SysTick Reload Value Register (SYST_RVR) về 0. Sau đó vi xử lý thực hiện nạp lại giá trị reload và tiếp tục đếm xuống ở xung clock tiếp theo.
– Với chức năng này của system timer, chúng ta có thể cấu hình và kích hoạt để tạo ra sự kiện ngắt SysTick định kỳ mỗi khi bộ đếm đếm ngược về giá trị 0.
– Khi các bạn mới thực hiện tạo code từ phần mềm STM32CubeMX để chuyển đổi các cấu hình đã thực hiện thành mã chương trình, thì mặc định ngắt SysTick đã được bật và ngắt này sẽ định kỳ xảy ra mỗi một mili giây một lần.
– Khi có một tín hiệu yêu cầu ngắt SysTick xảy ra, chương trình phục vụ ngắt SysTick_Handler sẽ được thực hiện và biến đếm thời gian sẽ được tăng thêm 1 đơn vị tại hàm HAL_IncTick.
2. Ứng dụng SysTick trong một số hàm thư viện HAL
Trong thư viện HAL, một biến toàn cục có kiểu dữ liệu số nguyên không dấu 32-bit có tên là uwTick thể hiện giá trị đếm hiện tại của bộ system timer. Các hàm thư viện HAL có liên quan đến SysTick sẽ thực hiện đọc/ghi vào biến uwTick này. Ở đây mình sẽ giới thiệu đến các bạn một số hàm có liên quan được sử dụng:
– HAL_IncTick(): Trong hàm HAL_IncTick, biến uwTick sẽ được mặc định tăng lên một đơn vị khi hàm được gọi (giá trị uwTickFreq = 1).
– HAL_GetTick(): Hàm này thực hiện trả về giá trị đếm của bộ system timer hiện tại ở đơn vị mili giây. Trong hầu hết trường hợp, HAL_GetTick() được sử dụng để thực hiện xử lý thời gian chờ (TimeOut).
– HAL_Delay(): Đây là hàm thực thi delay tính bằng mili giây, sử dụng system timer. Hàm HAL_Delay có thể được gọi ra và sử dụng tại bất kỳ thời điểm nào mà bạn muốn vi điều khiển thực hiện công việc chờ trong khoảng thời gian x mili giây.
- Đầu tiên, vi điều khiển thực hiện hàm HAL_GetTick để lấy giá trị đếm ban đầu và gán vào biến tickstart
- Ở vòng lặp while, vi điều khiển tiếp tục gọi HAL_GetTick() để lấy giá trị đếm ở thời điểm hiện tại sau đó trừ cho giá trị đếm ban đầu tickstart. Nếu kết quả này nhỏ hơn giá trị wait do người dùng nhập vào thì vi điều khiển tiếp tục thực hiện vòng lặp while, ngược lại khi thời gian hiện tại so với thời gian ban đầu lớn hơn hoặc bằng thời gian chờ người dùng mong muốn thì điều kiện while không còn thỏa mãn, vi điều khiển đã thực hiện xong hàm HAL_Delay.
3. Tại sao một số bạn sử dụng HAL_Delay ở chương trình phục vụ ngắt thì vi điều khiển bị treo?
– Để trả lời cho câu hỏi trên, chúng ta cùng phân tích hình ảnh minh họa luồng thực thi của vi điều khiển ở trên. Giả sử vi xử lý sẽ xử lý 2 tín hiệu yêu cầu ngắt: một đến từ system timer và một đến từ ngoại vi bất kỳ, 2 tín hiệu này có cùng độ ưu tiên hoặc độ ưu tiên của tín hiệu yêu cầu ngắt Systick nhỏ hơn độ ưu tiên của tín hiệu yêu cầu ngắt còn lại. (tìm hiểu thêm phân tích về độ ưu tiên ngắt tại đây)
– Cứ mỗi 1 mili giây thì vi xử lý sẽ thực hiện hàm SysTick_Handler một lần và giá trị uwTick sẽ được tăng thêm một đơn vị. Giả sử có một tín hiệu yêu cầu ngắt đến từ một ngoại vi bên ngoài và chương trình phục vụ ngắt của ngoại vi này (Peripheral_Handler) có gọi hàm HAL_Delay, lúc này vi xử lý thực hiện các câu lệnh trong Peripheral_Handler cho đến khi gặp câu lệnh HAL_Delay(x); //delay x mili giay. Vì tín hiệu yêu cầu ngắt của ngoại vi này đang được xử lý nên tín hiệu yêu cầu ngắt Systick đến sau sẽ được đưa vào trạng thái chờ (pending), hàm SysTick_Handler chưa được thực hiện dẫn đến giá trị uwTick không đổi, vi xử lý sẽ thực hiện lặp vô tận trong câu lệnh while của hàm HAL_Delay, điều này dẫn đến chương trình bị treo tại vòng lặp while này.
– Chúng ta cùng xem bên trong hàm HAL_Delay để hiểu lý do vì sao vi xử lý thực hiện lặp vô tận trong câu lệnh while:
- Đầu tiên, biến tickstart chứa giá trị uwTick hiện tại được trả về từ hàm HAL_GetTick.
- Vì SysTick_Handler chưa được thực hiện dẫn đến giá trị uwTick không đổi, giá trị trả về của HAL_GetTick trong điều kiện while bằng giá trị của tickstart ban đầu, dẫn đến kết quả của HAL_GetTick() – tickstart luôn bé hơn wait, dẫn đến vi điều khiển thực hiện lặp vô hạn trong vòng lặp while này.
4. Thực hành để kiểm chứng
– Trong ví dụ này mình sử dụng ngắt ngoài (EXTI) đến từ chân PA0 của vi điều khiển STM32F103C8T6 như hình dưới, độ ưu tiên của EXTI (EXTI line 0 interrupt) và SysTick (Time base: System tick timer) bằng nhau:
– Ở file main.c, mình khai báo biến maincnt và thực hiện tăng maincnt lên 1 đơn vị mỗi 500 mili giây. Mục đích của biến maincnt này là để xác định vi xử lý đang hoạt động bình thường hay đã bị treo
– Tại hàm xử lý ngắt ngoài HAL_GPIO_EXTI_Callback mình thực hiện delay 1000 mili giây và thực hiện đặt breakpoint tại HAL_Delay để kiểm tra vi xử lý có thực hiện HAL_GPIO_EXTI_Callback khi có tín hiệu yêu cầu ngắt đến từ chân PA0 hay không
– Sau đó các bạn tìm đến hàm SysTick_Handler và đặt breakpoint tại câu lệnh HAL_IncTick()
– Biên dịch chương trình và thực hiện debug:
- Sau khi thực hiện chạy chương trình các bạn có thể thấy vi điều khiển đã dừng lại tại điểm breakpoint đã đặt trong hàm SysTick_Handler. Điều này chứng tỏ ngắt System Tick đang được thực thi.
- Các bạn có thể tiếp tục ấn Run thêm vài lần nữa để kiểm tra, sau đó bỏ breakpoint và nhấn Run để vi điều khiển thực thi luồng chương trình như bình thường, giá trị biến maincnt được tăng lên liên tục.
– Tạo tín hiệu ngắt trên chân PA0 để vi điều khiển đi đến hàm HAL_GPIO_EXTI_Callback
– Nhấn Run để tiếp tục chương trình. Lúc này các bạn quan sát biến maincnt không còn tăng nữa, chứng tỏ vi xử lý đã bị treo trong hàm HAL_GPIO_EXTI_Callback. Nhấn vào biểu tượng Stop chúng ta sẽ thấy vi điều khiển đang dừng lại tại HAL_GetTick trong câu lệnh while:
– Vậy làm thế nào để có thể sử dụng HAL_Delay trong các chương trình phục ngắt?
5. Giải pháp
Nếu các bạn muốn sử dụng HAL_Delay trong các chương trình xử lý ngắt thì các bạn phải thực hiện điều chỉnh độ ưu tiên của ngắt SysTick cao hơn so với các ngắt ngoại vi đó. Ví dụ:
Lưu ý: Các bạn nên xem xét sử dụng hàm HAL_Delay trong các chương trình phục vụ ngắt vào các trường hợp cần thiết vì các chương trình phục vụ ngắt nên được xử lý tức thời và càng ngắn gọn càng tốt, tránh ảnh hưởng đến các ngắt đến sau, không đáp ứng được tính realtime của hệ thống dẫn đến bỏ lỡ sự kiện hoặc mất dữ liệu.
Hi vọng bài viết có thể giúp này các bạn hiểu và tránh được sai sót khi áp dụng vào các dự án. Chúc các bạn thành công!
TAPIT ARM R&D
[HỌC ONLINE: LẬP TRÌNH VI ĐIỀU KHIỂN STM32, VI XỬ LÝ ARM CORTEX – M]
Xem thêm Tổng hợp các bài hướng dẫn Lập trình vi điều khiển STM32 tại đây.
Xem thêm Tổng hợp hướng dẫn Internet of Things với NodeMCU ESP8266 và ESP32 tại đây.