Khi làm việc với bất kì hệ điều hành nào, việc tạo Task và lập lịch (Scheduling) trên hệ điều hành là một công việc đầu tiên và bắt buộc. Trong bài hôm nay chúng ta sẽ học cách tạo Task trên FreeRTOS với STM32 sử dụng thư viện HAL
Bài đầu tiên về RTOS trong Serie: Học STM32 từ A tới Z
Task là gì
Task là một đơn vị nhỏ nhất của công việc mà hệ điều hành có thể quản lý và thực thi. Task tương tự như một chương trình nhỏ hoặc một tiến trình con trong hệ điều hành máy tính, nhưng với một số đặc điểm cụ thể dành cho các hệ thống nhúng và thời gian thực.
Đặc điểm của Task
- Độc lập: Mỗi task hoạt động một cách độc lập và có không gian ngăn xếp (stack) riêng của mình.
- Độ ưu tiên: Mỗi task có thể được gán một độ ưu tiên, xác định mức độ quan trọng của task đó so với các task khác.
- Thực thi tuần tự: Task có thể chạy, bị tạm dừng, tiếp tục chạy, hoặc bị xóa bởi hệ điều hành.
- Trạng thái: Task có thể ở nhiều trạng thái như sẵn sàng chạy (ready), đang chạy (running), bị chặn (blocked), hoặc bị treo (suspended).
Vai trò của Task trong FreeRTOS
Trong FreeRTOS, task là cách chính để thực hiện đa nhiệm (multitasking). Mỗi task có một chức năng cụ thể và hệ điều hành quản lý việc chuyển đổi giữa các task dựa trên độ ưu tiên và trạng thái của chúng.
Ví dụ, trong một ứng dụng nhúng:
- Một task có thể xử lý việc đọc dữ liệu từ cảm biến.
- Một task khác có thể xử lý giao tiếp với một thiết bị ngoại vi.
- Một task khác nữa có thể xử lý giao tiếp mạng.
Scheduling là gì?
Scheduling (Lập lịch) là quá trình quản lý và quyết định thứ tự thực thi của các tác vụ (tasks) trong hệ thống. Scheduler (bộ lập lịch) là thành phần của RTOS chịu trách nhiệm phân bổ thời gian CPU cho các task dựa trên các thuật toán và tiêu chí nhất định.
Scheduling đóng vai trò quan trọng trong việc:
- Đảm bảo tính đáp ứng thời gian thực: Đảm bảo các task được thực hiện đúng thời gian yêu cầu.
- Tối ưu hóa việc sử dụng tài nguyên: Phân phối tài nguyên hệ thống một cách hiệu quả giữa các task.
- Đảm bảo tính công bằng: Đảm bảo rằng tất cả các task có cơ hội được thực thi.
Các Thuật Toán Scheduling trong FreeRTOS
FreeRTOS hỗ trợ nhiều thuật toán scheduling khác nhau, chủ yếu bao gồm:
- Preemptive Scheduling: Hệ điều hành có thể tạm dừng một task đang chạy để chuyển sang task có độ ưu tiên cao hơn nếu task đó sẵn sàng chạy.
- Cooperative Scheduling: Task hiện tại sẽ chạy cho đến khi nó tự nguyện nhường quyền điều khiển lại cho hệ điều hành bằng cách gọi các hàm như vTaskDelay hoặc taskYIELD.
Cơ chế Scheduling trong FreeRTOS
Trong FreeRTOS, scheduling dựa trên độ ưu tiên của các task. Mỗi task được gán một độ ưu tiên (priority) và scheduler sẽ luôn chọn task có độ ưu tiên cao nhất để thực thi. Nếu có nhiều task cùng độ ưu tiên, chúng sẽ được thực hiện theo cơ chế round-robin (task sẽ thực hiện hết thời gian thực thi sau đó sẽ dừng lại để chuyển task khác)
Trong STM32 chúng ta thường sử dụng thuật toán Preemptive Scheduling để lập lịch cho các Task. Hãy lưu ý về độ ưu tiên của các Task để cấu hình cho phù hợp
Độ ưu tiên của các Task
Dưới đây là một số loại task thường được ưu tiên cao trong các ứng dụng nhúng:
1. Task Xử lý Ngắt (Interrupt Handling Task)
- Mô tả: Task này xử lý các sự kiện ngắt từ phần cứng.
- Lý do ưu tiên: Ngắt thường báo hiệu các sự kiện quan trọng như dữ liệu mới từ cảm biến hoặc thông tin truyền thông.
- Ví dụ: Đọc dữ liệu từ cảm biến, xử lý ngắt từ giao tiếp UART hoặc SPI.
2. Task Xử lý Dữ liệu Thời Gian Thực (Real-Time Data Processing Task)
- Mô tả: Task này xử lý dữ liệu mà yêu cầu đáp ứng trong thời gian thực.
- Lý do ưu tiên: Dữ liệu thời gian thực yêu cầu được xử lý ngay lập tức để đảm bảo hệ thống hoạt động chính xác.
- Ví dụ: Xử lý dữ liệu cảm biến trong hệ thống điều khiển tự động, tính toán điều khiển trong hệ thống nhúng.
3. Task Giao tiếp (Communication Task)
- Mô tả: Task này xử lý giao tiếp giữa các module hoặc thiết bị.
- Lý do ưu tiên: Giao tiếp yêu cầu độ tin cậy và tốc độ để đảm bảo dữ liệu được truyền nhận kịp thời.
- Ví dụ: Task gửi và nhận dữ liệu qua mạng (Ethernet, Wi-Fi), task xử lý giao tiếp CAN bus trong ô tô.
4. Task Điều khiển (Control Task)
- Mô tả: Task này thực hiện các thuật toán điều khiển cho hệ thống.
- Lý do ưu tiên: Các hệ thống điều khiển yêu cầu đáp ứng nhanh và chính xác để duy trì hoạt động ổn định.
- Ví dụ: Điều khiển động cơ, điều khiển nhiệt độ trong hệ thống HVAC.
5. Task Giám sát (Monitoring Task)
- Mô tả: Task này giám sát trạng thái và hiệu suất của hệ thống.
- Lý do ưu tiên: Giám sát liên tục giúp phát hiện và xử lý kịp thời các vấn đề có thể gây hỏng hóc hệ thống.
- Ví dụ: Giám sát điện áp và nhiệt độ, kiểm tra trạng thái hoạt động của hệ thống.
6. Task Bảo trì (Maintenance Task)
- Mô tả: Task này thực hiện các chức năng bảo trì hệ thống định kỳ.
- Lý do ưu tiên: Bảo trì đúng thời gian giúp duy trì hiệu suất và độ tin cậy của hệ thống.
- Ví dụ: Xử lý dọn dẹp bộ nhớ, lưu trữ dữ liệu log.
Tạo Task và Scheduling Task trên STM32 sử dụng Cube MX
Cấu hình FreeRTOS trên CubeMX
Trong phần “Pinout & Configuration”, chuyển đến tab “Middleware” chọn CMSIS_V2
Trong phần Config Parameter hãy cùng tìm hiểu các tham số:
USE_PREEMPTION: Bật cơ chế lập lịch theo độ ưu tiên của Task
TICK_RATE_HZ: Tick rate là số lần ngắt hệ thống (system tick) xảy ra mỗi giây. Trong ví dụ trên, tick rate được thiết lập là 1000 Hz, nghĩa là hệ thống sẽ tạo ra 1000 ngắt tick mỗi giây (mỗi ngắt tick sẽ xảy ra mỗi 1ms).
MINIMAL_STACK_SIZE: Kích thước nhỏ nhất của Stack trên mỗi Task (Mỗi task sẽ sử dụng bộ nhớ Stack khác nhau)
Các thông số khác các bạn có thể để mặc định, ý nghĩa tương ứng với tên.
Tạo Task trên Cube MX
Khi chuyển sang Tab Task and Queues chúng ta sẽ thấy có một task mặc định được tạo sẵn. Để tạo task, ta ấn nút Add, sau đó điền các tham số của Task:
Task Name: Đặt tên của task
Priority: Độ ưu tiên của Task. Chúng ta có nhiều mức ưu tiên đi từ không được ưu tiên tới được ưu tiên như sau:
- Low Priority: Mức ưu tiên nhỏ nhất, thường dùng cho các task không quan trọng
- Bellow Normal, Normal, AbowNormal: dành cho các task có độ ưu tiên bình thường
- High and RealTime: Dành cho các tác vụ cần xử lý ngay lập tức
Stack Size: Là kích thước Stack mà Task sử dụng. Nên tính toán hợp lý với bộ nhớ của Chip nếu có nhiều task
Entry Function: Hàm được call khi Task được gọi
Chúng ta sẽ tạo thêm 2 task là myTask02 với độ ưu tiên Low
Và myTask03 với độ ưu tiên High
Trong Advance Setting, enable USE_NEWLIB_REENTRANT
USE_NEWLIB_REENTRANT là một macro cấu hình sử dụng trong các dự án nhúng để kích hoạt chế độ reentrant (an toàn trong môi trường đa luồng) cho thư viện Newlib. Điều này có nghĩa là các hàm trong thư viện Newlib sẽ sử dụng các phiên bản dữ liệu cục bộ cho từng luồng (thread) thay vì dùng chung dữ liệu toàn cục.
Điều này giúp tránh các vấn đề về tranh chấp tài nguyên và đảm bảo an toàn trong môi trường đa luồng.
Bật bộ UART1 truyền dữ liệu về máy tính, để xem Log task nào được khởi chạy
Trong RCC chúng ta sẽ sử dụng TIM1 làm Clock source vì SysTick có thể được sử dụng cho các nhiệm vụ quan trọng khác ngoài việc tạo tick cho RTOS, như đo thời gian thực thi của các đoạn mã quan trọng hoặc điều khiển các chức năng quan trọng khác.
Vì vậy chúng ta cần giải phóng Systic và thay thế bằng 1 timer bất kì
Sau đó đặt tên project và sinh code
Trường hợp tạo Task có độ ưu tiên khác nhau
Trong bài viết này mình sẽ sử dụng CubeIDE để viết code. Chúng ta mở project vừa tạo ra. CubeMX sẽ tự sinh code
Giải thích các code tự sinh
Phần osThreadAtt là các Attributes (thuộc tính) của Task bao gồm: tên, stacksize và mức ưu tiên
Trong main, chúng ta sẽ thấy các hàm tạo Task và hàm osKernalStart() để bắt đầu khởi chạy RTOS.
Hàm osThreadNew sẽ bao gồm: Tên function được gọi khi Task thực thi, các tham số truyền vào (ở đây là NULL) và phần khai báo các thuộc tính của Task
Thêm chương trình thực thi cho các Task
Kéo xuống phía dưới chúng ta sẽ thấy phần Impliment (thực thi) cho các task. Đây là nơi chúng ta sẽ viết phần thực thi cho các Task của mình
Trong phần thực thi chúng ta In ra giá trị UART chuỗi tương ứng.
Kết quả
Nhấn Project – Build All để build rồi nhấn Run để nạp chương trình vào mạch
Cắm bộ chuyển UART to USB và mở Hercules Terminal để xem kết quả
Chúng ta có thể thấy, Task 3 sẽ được khởi chạy đầu tiên do có độ ưu tiên cao nhất, sau đó đến default task và cuối cùng đến Task2.
Vì mỗi task có thời gian thực thi bằng nhau do hàm osDelay(1000). Vậy nên quá trình này lặp đi lặp lại.
Trường hợp có chung độ ưu tiên
Trong trường hợp này chúng ta sẽ để cả 2 Task có độ ưu tiên bằng nhau osPriorityLow
Khi hai task có cùng độ ưu tiên trong FreeRTOS, việc quản lý và chuyển đổi giữa các task đó sẽ tuân theo cơ chế Round-Robin Scheduling.
Round-Robin Scheduling
Round-Robin Scheduling là một cơ chế phân phối CPU theo vòng tròn giữa các task có cùng độ ưu tiên. Cụ thể:
- Các task có cùng độ ưu tiên sẽ được xếp vào một hàng đợi.
- Mỗi task sẽ được cấp phát CPU trong một khoảng thời gian nhất định gọi là time slice hoặc time quantum.
- Sau khi hết time slice, nếu task vẫn còn công việc phải làm, nó sẽ bị đặt lại vào cuối hàng đợi, và CPU sẽ được cấp phát cho task tiếp theo trong hàng đợi.
Time Slice trong FreeRTOS
Trong FreeRTOS, time slice được xác định bởi tần số tick (tick rate), thường được cấu hình bởi macro configTICK_RATE_HZ trong tệp cấu hình FreeRTOS
Với giá trị này, mỗi tick sẽ xảy ra mỗi 1ms. FreeRTOS sẽ chuyển đổi giữa các task có cùng độ ưu tiên mỗi khi một tick xảy ra.
Ảnh hưởng đến Task2 và Task3
Khi tạo task 2 và task 3 có cùng độ ưu tiên, chúng sẽ được thực thi luân phiên dựa trên cơ chế Round-Robin Scheduling. Giả sử cả hai task đều có độ ưu tiên osPriorityLow, và đều sẵn sàng để chạy, chúng sẽ luân phiên được cấp phát CPU.
Kết quả
defaultTask (ưu tiên trung bình) sẽ in thông báo trước, mỗi giây một lần.
myTask02 và myTask03 (cùng ưu tiên thấp) sẽ luân phiên in thông báo mỗi giây một lần sau khi defaultTask chạy. Task 2 được khởi tạo trước nên sẽ chạy trước Task 3.
Trường hợp các task có thời gian xử lý khác nhau
Chúng ta giữ nguyên độ ưu tiên của Task 2, Task 3 là Low.
Tuy nhiên sửa thời gian đợi của Task 2 là 200ms, Task 3 là 500ms
Kết quả như sau:
Giải thích: Task default có độ ưu tiên Normal nên sẽ chạy đầu tiên, Task 2 và 3 sẽ chạy ngay sau đó.
Kết luận
Việc tạo Task và hiểu cơ chế Scheduling rất quan trọng khi học hệ điều hành RTOS, chúng giúp chúng ta có cái nhìn chính xác về quá trình khởi tạo và chạy các Task.
Để đảm bảo các Task được chạy một cách tối ưu, thông thường người ta sẽ thiết kế Timing diagram cho chương trình của mình. Điều này sẽ giúp các luồng được quản lý chặt chẽ hơn.
Hi vọng sau bài này, các bạn đã hiểu cách tạo Task và lập lịch cho Task, hãy cùng chuyển sang bài tiếp theo trong Serie này nhé.