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 task
  • StartTask1: Tên task entry function
  • osPriorityNormal: Khởi tạo priority của task function
  • 0: Số instance có thể có của task
  • 128: 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 task
  • NULL: 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()osThreadGetPriority() để lấy ID và Priority
  • osThreadYield() để đưa quyền điều khiển cho task kế tiếp
  • osDelay() dùng để dừng task trong thời gian định sẵn
  • osThreadTerminate() để xóa task
  • osThreadSuspend() & 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]