RTOS Task
Task trong RTOS là gì, nó được sử dụng và khởi tạo như thế nào ? Các API của task là những API nào và cách sử dụng nó ra sao ? tất cả sẽ được làm rõ trong bài viết này.
Tổng quan
Mình xin lấy lại nội dung đã ghi ở bài viết RTOS cơ bản: Một task là một chương trình, chương trình này chạy liên tục trong vòng lặp vô tận và không bao giờ dừng lại. Với mỗi task thì có niềm tin duy nhất là chỉ mình nó đang chạy và có thể sử dụng hết nguồn tài nguyên sẵn có của bộ xử lý (mặc dù là thực tế thì nó vẫn phải chia sẻ nguồn tài nguyên này với các task khác).
Một chương trình thường sẽ có nhiều task khác nhau. Ví dụ như máy bán đồ uống tự động sẽ có các thành task sau
- Task quản lý việc lựa chọn của người dùng
- Task để kiểm tra đúng số tiền người dùng đã trả
- Task để điều khiển động cơ/ cơ cấu cung cấp nước uống.
Kernel sẽ quản lý việc chuyển đổi giữa các task, nó sẽ lưu lại ngữ cảnh của task sắp bị hủy và khôi phục lại ngữ cảnh của task tiếp theo bằng cách:
- Kiểm tra thời gian thực thi đã được định nghĩa trước (time slice được tạo ra bởi ngắt systick)
- Khi có các sự kiện unblocking một task có quyền cao hơn xảy ra (signal, queue, semaphore,…)
- Khi task gọi hàm Yield() để ép Kernel chuyển sang các task khác mà không phải chờ cho hết time slice
Khi khởi động thì kernel sẽ tạo ra một task mặc định gọi là Idle Task.
Task state
Chúng ta cùng xem lại các trạng thái hiện tại của task ở hình bên dưới
Ready: Task đã sẵn sàng để có thể thực thi nhưng chưa được thực thi do có các task khác với độ ưu tiên ngang bằng hoặc hơn đang chạy.
Running: khi task thực sự đang chạy
Blocked(Waiting): Task đang đợi một event tạm hoặc event từ bên ngoài
Suspended: Task không khả dụng để lên lịch (scheduling)
Task switch
Làm thế nào để switch task trong STM32 ?
Kernel sẽ đảm nhiệm nhiệm vụ switch task, nó sẽ lưu lại context(trạng thái) hiện tại của task bị suspend và khôi phục lại context của task đang được tiếp tục. Kernel sẽ thực hiện công việc này trong các trường hợp
- Sau khi có định nghĩa trước về thời gian thực thi (execution time), thời gian time slide được lấy bởi systick interrupt
- Khi có event unblock quyền ưu tiên cao hơn (higher priority) xảy ra như signal,queue, semaphore,..
- Khi task gọi hàm osThreadYield () để thông báo kernel chuyển sang task khác mà không đợi tới kết thúc của time slice
FreeRTOS OS interrupt
Với các core Cortex đã implement một số tính năng giúp hỗ trợ can thiệp trực tiếp vào os hệ thống
2 ngắt chuyên dụng cho os là
PendSV interrupt
- Trong interrupt này là một scheduler
- Quyền ngắt NVIC sẽ ở mức thấp nhất
- Không bị trigger bởi bất kì ngoại vi nào
- Trạng thái chờ xử lý từ các ngắt khác hoặc từ các task muốn kết thúc sớm(non MPU version)
SVC interrupt
- Interrupt sẽ được gọi bởi các tập lệnh SVC
- Được gọi nếu task muốn kết thúc sớm (MPU version)
- Trong ngắt này sẽ set pending state (MPU version)
Stack pointer
2 stack pointer là
Process stack pointer
- Được sử dụng trong các interrupt
- Cấp phát bởi linker trong quá trình compile
Main stack pointer
- Mỗi task sẽ có stack pointer của chính nó
- Trong quá trình switch context thì stack pointer sẽ khởi tạo task chính xác
Systick timer dùng để lập lịch định kỳ (periodically trigger scheduling)
Làm thế nào để tạo task
Bước 1: Định nghĩa task
osThreadDef(Task1, StartTask1, osPriorityNormal, 0, 128);
Trong đó:
Task1
: Tên của taskStartTask1
: Tên task entry functionosPriorityNormal
: Khởi tạo priority của task function0
: Số instance có thể có của task128
: stack size(byte) cần có cho task function
Bước 2: Tạo task và cấp phát bộ nhớ
Task1Handle = osThreadCreate(osThread(Task1), NULL);
Trong đó
osThread(Task1)
: Định nghĩa taskNULL
: Pointer tới argument của task
Task priority
Mức độ ưu tiên của task có thể được thay đổi bằng cách dùng hàm osThreadSetPriority()
Với CMSIS-RTOS thì sẽ có một số priority level như sau
- osPriorityIdle – priority thấp nhất
- osPriorityLow
- osPriorityBelowNormal
- osPriorityNormal – priority mặc định
- osPriorityHigh
- osPriorityRealtime
Task API
Một số API sau đây được dùng để hỗ trợ việc tạo, xóa, lấy ID của task như
osThreadGetID()
vàosThreadGetPriority()
để lấy ID và PriorityosThreadYield()
để đưa quyền điều khiển cho task kế tiếposDelay()
dùng để dừng task trong thời gian định sẵnosThreadTerminate()
để xóa taskosThreadSuspend()
&osThreadResume()
dùng để suspend hoặc resume task
Ta có bảng các hàm API cơ bản của task trong CMSIS RTOS và FreeRTOS
Feature | CMSIS RTOS API | FreeRTOS API |
---|---|---|
Define task attribute | osThreadDef | osThreadDef_t |
Create task | osThreadCreate | xTaskCreate |
Terminate task | osThreadTerminate | vTaskDelete |
Yield task | osThreadYield | taskYield |
Delay task | osDelay | vTaskDelay |
Get task ID | osThreadGetID | xTaskGetCurrentTaskHandle |
Set task priority | osThreadSetPriority | vTaskPrioritySet |
Get task priority | osThreadGetPriority | uxTaskPriorityGet |
Suspend task | osThreadSuspend | vTaskSuspend |
Suspend all task | osThreadSuspendAll | vTaskSuspendAll |
Resume task | osThreadResume | vTaskResume |
Resume all task | osThreadResumeAll | vTaskResumeAll |
Get state of task | osThreadState | eTaskGetState |
List current tasks info | osThreadList | vTaskList |
Delay task until specified time | osDelayUntil | vTaskDelayUntil |
Ví dụ
Để dễ hiểu hơn về cách sử dụng các hàm API này chúng ta đi vào một số ví dụ sau
Nếu bạn là người mới xem bài viết này thì nên xem qua 2 bài viết sau để hiểu rõ hơn về cách tạo project và sử dụng debug
- Dùng SWO debug
- Tạo project FreeRTOS dùng CubeMX
Tạo task và dùng osDelay
Ở ví dụ này ta tạo 2 task với cùng priority dùng cubeMX và sử dụng osDelay.
Ở phần này mình dùng project có sẵn ở bài Debug với SWO để in thông tin từ RTOS, mọi cấu hình ở bài viết SWO mình giữ nguyên, enable thêm FreeRTOS và cấu hình cũng như tạo task.
Mở phần configuration của FreeRTOS ra và tạo 2 task mới với cùng priority bằng cách thêm nút add
Sau đó generate code ra project CubeMX, mở project này lên, quan sát file main.c
Ta thấy những thành phần trong freeRTOS cần phải handle, nó giống với những gì ta cấu hình ở CubeMX
/* Private variables ---------------------------------------------------------*/
osThreadId Task1Handle;
osThreadId Task2Handle;
Task function prototype và tên y như những gì chúng ta đã đặt
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
void StartTask1(void const * argument);
void StartTask2(void const * argument);
Sau khi scheduler khởi động thì chúng ta cần phải tạo các task
/* Create the thread(s) */
/* definition and creation of Task1 */
osThreadDef(Task1, StartTask1, osPriorityHigh, 0, 128);
Task1Handle = osThreadCreate(osThread(Task1), NULL);
/* definition and creation of Task2 */
osThreadDef(Task2, StartTask2, osPriorityIdle, 0, 128);
Task2Handle = osThreadCreate(osThread(Task2), NULL);
Start scheduler bằng hàm
/* Start scheduler */
osKernelStart();
Trong task đầu tiên thì StartTask1 sẽ được gọi, và mặc định task sẽ chạy vòng loop tuần hoàn với for(;;), do đó chúng ta không phải kết thúc task, osDelay sẽ bắt đầu việc switch context
/* StartTask1 function */
void StartTask1(void const *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for (;;)
{
printf("Task 1\n");
osDelay(1000);
}
/* USER CODE END 5 */
}
Tương tự với task 2
/* StartTask2 function */
void StartTask2(void const *argument)
{
/* USER CODE BEGIN StartTask2 */
/* Infinite loop */
for (;;)
{
printf("Task 2\n");
osDelay(1000);
}
/* USER CODE END StartTask2 */
}
Sau khi đã thay đổi thông tin trong hàm main.c thì ta compile và build lại chương trình sau đó chọn debug
Kết quả
Ở trên ta tạo ra 2 task với 2 priority ngang nhau và đều có delay thời gian như nhau, vậy nếu cả 2 delay cùng xảy ra thì FreeRTOS sẽ ở trạng thái idle, các bạn có thể xem hình dưới để dể hình dung
Nếu chúng ta không sử dụng osDelay mà dùng HAL_Delay thì task sẽ có 2 trạng thái là running hoặc ready như hình sau
Set Priority
Ở ví dụ trên thì ta có thể thấy là 2 task với độ ưu tiên ngang nhau thì chưa biết được cái nào sẽ thực hiện trước cái nào thực hiện sau, nên mình sẽ có sự điều chỉnh ở project hiện tại là set một task lên higher priority thông qua CubeMX như hình
Sau khoảng 4 lần gửi text thì ta đưa task vào block state, và do task có priority nên ta phải điều chỉnh lại code như sau
Task 1
/* StartTask1 function */
void StartTask1(void const *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
uint32_t i = 0;
for (;;)
{
for (i = 0; i < 5; i++)
{
printf("Task 1\n");
HAL_Delay(50);
}
osDelay(1000);
}
/* USER CODE END 5 */
}
Task 2
/* StartTask2 function */
void StartTask2(void const *argument)
{
/* USER CODE BEGIN StartTask2 */
/* Infinite loop */
for (;;)
{
printf("Task 2\n");
HAL_Delay(50);
}
/* USER CODE END StartTask2 */
}
Kết quả
Hoạt động của chương trình có thể được minh họa bởi hình sau
Hàm osDelay
ở đây sẽ bắt đầu tính thời gian kể từ khi nó được gọi như hình
Thay đổi priority
Ở ví dụ này thì mình sẽ tận dụng ví dụ ở trên, nhưng có một thay đổi là sẽ dùng chính task2 để thay đổi priority của task1
Ta cấu hình lại FreeRTOS như hình, với task1 mức priority là Realtime, task 2 priority là Normal, một lưu ý khác là phải enable vTaskPriorityGet
và uxTaskPrioritySet
Chương trình chính ta sẽ thay đổi như sau
Task 1
/* USER CODE END Header_StartTask1 */
void StartTask1(void const * argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
osPriority priority;
for (;;)
{
priority = osThreadGetPriority(Task2Handle);
printf("Task 1\n");
osThreadSetPriority(Task2Handle, priority + 1);
HAL_Delay(1000);
}
/* USER CODE END 5 */
}
Task 2
/* USER CODE END Header_StartTask2 */
void StartTask2(void const * argument)
{
/* USER CODE BEGIN StartTask2 */
osPriority priority;
/* Infinite loop */
for (;;)
{
priority = osThreadGetPriority(NULL);
printf("Task 2\n");
osThreadSetPriority(Task2Handle, priority - 2);
}
/* USER CODE END StartTask2 */
}
Kết quả
Hình bên dưới đấy sẽ cho chúng ta thấy priority được thay đổi như thế nào
Tạo và xóa task
Ở ví dụ cuối này thì ta sẽ biết cách để tạo ra một task mới và xóa đi task đó. Vẫn tiếp tục lấy ví dụ từ bài task đầu tiên chỉnh sửa tiếp
Điều chỉnh lại chương trình của task1 và task2
Task1
/* StartTask1 function */
void StartTask1(void const *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for (;;)
{
printf("Create task2\n");
osThreadDef(Task2, StartTask2, osPriorityNormal, 0, 128);
Task2Handle = osThreadCreate(osThread(Task2), NULL);
osDelay(1000);
}
/* USER CODE END 5 */
}
Task2
/* StartTask2 function */
void StartTask2(void const *argument)
{
/* USER CODE BEGIN StartTask2 */
/* Infinite loop */
for (;;)
{
printf("Delete task2\n");
osThreadTerminate(Task2Handle);
}
/* USER CODE END StartTask2 */
}
Kết quả
Hình bên dưới là minh họa cho các thao tác tạo và xóa task
Toàn bộ các ví dụ ở trên các bạn có thể tải tại https://github.com/hocarm/FreeRTOS-STM32F4-Tutorial
Tạm kết
Vậy là ở bài viết này mình đã làm rõ về Task trong RTOS là gì, cách khởi tạo task ? Các API của task và cách sử dụng nó trong các ví dụ tạo task, set priority cho task, tạo và xóa task. Còn các vấn đề về innertask xin hẹn các bạn ở bài viết sau.
Tham khảo
[1]