[Tự Học FreeRTOS] Chapter 1: Task Management

1.1 Giới Thiệu

Nội dung chương này gồm có:

  • Làm thế nào FreeRTOS cấp phát thời gian xử lý cho mỗi task trong một ứng dụng.
  • Làm thế nào FreeRTOS chọn task được thực thi tại mỗi thời điểm
  • Làm thế nào mối quan hệ giữa độ ưu tiên của mỗi task ảnh hưởng hành vi hệ thống.
  • Các trạng thái mà task hiện hữu.

Đồng thời chương này cũng thảo luận về:

  • Làm sao để thực hiện các task.
  • Làm sao để tạo một hoặc nhiều instance của một task.
  • Làm sao để xài tham số của task.
  • Làm sao để thay đổi độ ưu tiên của một task đã được tạo rồi.
  • Làm sao để xóa một task.
  • Làm sao để thực hiện xử lý định kỳ (periodic processing) bằng một task. (Một chương sau khác sẽ nói về cách làm tương tự bằng software timer).
  • Khi nào idle task sẽ thực thi và nó được xài như thế nào.

1.2 Các Hàm Task (Task Functions)

Task (tác vụ) là những hàm được viết bằng C. Các task phải tuân theo prototype như dưới đây. Prototype này định nghĩa một hàm có một tham số là con trỏ void (a void pointer parameter) và trả về void.

void vATaskFunction( void * pvParameters );

Code Block 1.1. prototype của một hàm task

Mỗi task là một chương trình (program) có quyền riêng của nó. Thông thường, task có một đầu vô, sau đó sẽ chạy vĩnh viễn trong một vòng lặp vô tận và không thoát ra (exit). Code Block 1.2 diễn tả cấu trúc của một task thông thường.

void vATaskFunction( void * pvParameters )
{
    /*
    * Stack-allocated variables can be declared normally when inside a function.
    * Each instance of a task created using this example function will have its
    * own separate instance of lStackVariable allocated on the task's stack.
    */
    long lStackVariable = 0;
    /*
    * In contrast to stack allocated variables, variables declared with the
    `static`
    * keyword are allocated to a specific location in memory by the linker.
    * This means that all tasks calling vATaskFunction will share the same
    * instance of lStaticVariable.
    */
    static long lStaticVariable = 0;
    for( ;; )
    {
    /* The code to implement the task functionality will go here. */
    }
    /*
    * If the task implementation ever exits the above loop, then the task
    * must be deleted before reaching the end of its implementing function.
    * When NULL is passed as a parameter to the vTaskDelete() API function,
    * this indicates that the task to be deleted is the calling (this) task.
    */
    vTaskDelete( NULL );
}

Code Block 1.2. Cấu trúc của một task thông thường

Một FreeRTOS task phải là một hàm không được phép trả về (return). Nó không được chứa lệnh return và không được phép thực thi vượt khỏi phạm vi hàm của nó. Nếu một task không cần xài nữa, nó nên xóa một cách tường minh như được mô tả trong Code block 1.2.

Một định nghĩa (definition) hàm task có thể được xài để tạo ra nhiều task, với mỗi task được tạo ra là một instance thực thi riêng biệt. Mỗi instance đều sở hữu một stack riêng và đồng thời sở hữu bản sao của mọi biến tự động (automatic variable) được định nghĩa bên trong task đó.

1.3 Các Trạng Thái Task Cấp Cao (Top Level Task States)

Một ứng dụng thường có nhiều task. Nếu bộ xử lý (processor) chạy ứng dụng chỉ có một lõi (core) thì chỉ một task được thực thi trong một khoảng thời gian cho phép. Có nghĩa là mỗi task có thể tồn tại ở hai trạng thái: RunningNot Running. Model đơn giản hóa này được xem xét đầu tiên. Sau đó chúng ta sẽ nói về một số trạng thái con (sub-state) của trạng thái Not Running.

Một task ở trạng thái Running khi bộ xử lý đang thực thi code của task. Khi task ở trạng thái Not Running, thì task được tạm dừng (pause) và trạng thái của nó được lưu (save) để nó có thể khôi phục lại (resume) sự thực thi ở lần tiếp theo, khi scheduler quyết định nó được vô (enter) trạng thái Running. Khi task resume sự thực thi, nó tiếp tục tại dòng lệnh mà nó sắp thực thi trước khi thoát khỏi trạng thái Running.

Hình 1.1. Trạng thái task cấp cao và sự chuyển đổi

Sự chuyển đổi task từ trạng thái Not Running sang Running được gọi là “switch in” hay ”swap in”. Ngược lại, quá trình task chuyển từ trạng thái Running sang Not Running được gọi là “switch out” hay “swap out”. FreeRTOS scheduler là entity duy nhất có thể điều khiển chuyển trạng thái một task vào và ra trạng thái Running.

1.4 Tạo Task (Task Creation)

Hàm API xTaskCreate()

Task được tạo bằng hàm API xTashCreate() của FreeRTOS. Đây có vẻ là hàm API phức tạp nhất, không may nó lại là hàm phải học đầu tiên. Task là phần cơ bản quan trọng nhất trong một hệ thống đa tác vụ (multitasking system) nên cần hiểu về nó đầu tiên.

Prototype của hàm API xTaskCreate() như sau.

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
                        const char * const pcName,
                        configSTACK_DEPTH_TYPE usStackDepth,
                        void * pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t * pxCreatedTask );

Code Block 1.3. Cấu trúc của một task thông thường

Tham số và giá trị trả về của xTaskCreate():

  • pvTaskCode

Task là những hàm viết bằng C, những hàm này không bao giờ kết thúc, chúng thường được viết bằng một vòng lặp vô tận. Tham số pvTaskCode chỉ là một pointer trỏ tới hàm viết task (chỉ cần tên của hàm)

  • pcName

Tên mô tả task. FreeRTOS không cần dùng tham số này, thực ra nó giúp việc debug dễ hơn. Định danh task bằng một cái tên mà con người đọc được thì dễ nhận thấy hơn bằng handle của nó (là một con số).

Macro configMAX_TASK_NAME_LEN định nghĩa chiều dài lớn nhất của tên task, bao gồm ký tự kết thúc NULL.  Đưa vô một string dài hơn giá trị lớn nhất sẽ khiến cho string tự động bị cắt mất.

  • usStackDepth

Mỗi task sở hữu một vùng stack riêng biệt được cấp bởi kernel khi task được tạo. Giá trị usStackDepth báo cho kernel cần tạo stack lớn bao nhiêu.

Giá trị này bằng số word mà stack có thể giữ, chứ không phải số byte. Ví dụ, nếu stack rộng 32-bit và usStackDepth là 128, thì xTaskCreate() cấp 512 byte của không gian stack (128 * 4).

configSTACK_DEPTH_TYPE là macro cho phép nhà phát triển chỉ định kiểu dữ liệu để giữ kích thước stack. configSTACK_DEPTH_TYPE mặc định là uint16_t nếu không được định nghĩa, do đó #define configSTACK_DEPTH_TYPE cho unsigned long hoặc size_t trong FreeRTOSConfig.h nếu stack depth nhân với stack width là lớn hơn 65535 (số 16-bit lớn nhất).

Stack của Ilde task có kích thước được định nghĩa bằng macro configMINIMAL_STACK_SIZE. Đây cũng là định nghĩa cho giá trị stack nhỏ nhất. Nếu task của bạn dùng nhiều không gian hơn, bạn nên gán một giá trị lớn hơn.

Không có cách xác định đúng không gian stack cho task dễ dàng. Người ta có thể tính toán, nhưng hầu hết thường chỉ gán đại một giá trị mà họ nghĩ là phù hợp, sau đó chạy thử các chức năng của FreeRTOS để xem không gian có được cấp phát đủ không và RAM không bị lãng phí vô nghĩa.

  • pvParameters

Hàm viết cho task chấp nhận một tham số void pointer (void *). pvParameters là giá trị được truyền (pass) vô task xài tham số đó.

  • uxPriority

Định nghĩa độ ưu tiên mà task sẽ thực thi. Độ ưu tiên có thể gán từ 0, là độ ưu tiên thấp nhất, tới (configMAX_PRIORITIES - 1), là độ ưu tiên cao nhất.

configMAX_PRIORITIES là một macro dành cho người dùng định nghĩa. Không có giới hạn số lượng mức độ ưu tiên khả dĩ, ngoại trừ giới hạn do kiểu dữ liệu được dùng và RAM có sẵn trong vi điều khiển của bạn. Nhưng bạn nên dùng số mức độ ưu tiên thấp nhất để tránh lãng phí RAM.

Nếu truyền giá trị uxPriority lớn hơn (configMAX_PRIORITIES - 1) sẽ khiến độ ưu tiên được gán cho task bị giới hạn một cách âm thầm ở giá trị tối đa hợp lệ.

  • pxCreatedTask

Pointer trỏ tới một vùng lưu một handle của task được tạo. Handle này có thể được dùng bởi các lệnh gọi API tương lai, ví dụ như đổi độ ưu tiên của task hoặc xóa task.

pxCreatedTask là một tham số tùy chọn và được set NULL nếu handle của task không cần thiết.

  • Các giá trị trả về

pdPASS

Task đã được tạo thành công.

pdFAIL

Không có đủ bộ nhớ heap để tạo task.

Ví dụ 1.1 Tạo task

Ví dụ dưới đây mô tả các bước để tạo hai task đơn giản và bắt đầu chạy (start) những task mới này. Các task chỉ đơn giản là in ra một chuỗi ký tự định kỳ, nó xài một vòng lặp null thô lỗ (a crude null loop) để tạo khoảng thời gian trễ (delay). Cả hai task đều được tạo với cùng độ ưu tiên và nội dung giống nhau ngoại trừ chuỗi ký tự được in ra.

void vTask1( void * pvParameters )
{
    /* ulCount is declared volatile to ensure it is not optimized out. */
    volatile unsigned long ulCount;
    for( ;; )
    {
        /* Print out the name of the current task task. */
        vPrintLine( "Task 1 is running" );

        /* Delay for a period. */
        for( ulCount = 0; ulCount < mainDELAY_LOOP_COUNT; ulCount++ )
        {
            /*
            * This loop is just a very crude delay implementation. There is
            * nothing to do in here. Later examples will replace this crude* loop with a proper delay/sleep function.
            */
        }
    }
}

Code Block 1.4. Implementation của task thứ nhất dùng trong Ví dụ 1.1

void vTask2( void * pvParameters )
{
    /* ulCount is declared volatile to ensure it is not optimized out. */
    volatile unsigned long ulCount;

    /* As per most tasks, this task is implemented in an infinite loop. */
    for( ;; )
    {
        /* Print out the name of this task. */
        vPrintLine( "Task 2 is running" );

        /* Delay for a period. */
        for( ulCount = 0; ulCount < mainDELAY_LOOP_COUNT; ulCount++ )
        {
            /*
            * This loop is just a very crude delay implementation. There is
            * nothing to do in here. Later examples will replace this crude
            * loop with a proper delay/sleep function.
            */
        }
    }
}

Code Block 1.5. Implementation của task thứ hai dùng trong Ví dụ 1.1

Hàm main() tạo hai task trước khi bắt đầu chạy scheduler – xem Code Block 1.6 là nội dung của hàm này.

int main( void )
{
    /*
    * Variables declared here may no longer exist after starting the FreeRTOS
    * scheduler. Do not attempt to access variables declared on the stack used
    * by main() from tasks.
    */

    /*
    * Create one of the two tasks. Note that a real application should check
    * the return value of the xTaskCreate() call to ensure the task was
    * created successfully.
    */
    xTaskCreate( vTask1, /* Pointer to the function that implements the task.*/
                "Task 1",/* Text name for the task. */
                1000, /* Stack depth in words. */
                NULL, /* This example does not use the task parameter. */
                1, /* This task will run at priority 1. */
                NULL ); /* This example does not use the task handle. */
    /* Create the other task in exactly the same way and at the same priority.*/
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );

    /* Start the scheduler so the tasks start executing. */
    vTaskStartScheduler();

    /*
    * If all is well main() will not reach here because the scheduler will now
    * be running the created tasks. If main() does reach here then there was
    * not enough heap memory to create either the idle or timer tasks
    * (described later in this book).
    */
    for( ;; );
}

Code Block 1.6. Bắt đầu chạy task của Ví dụ 1.1

Thực thi đoạn code ví dụ cho ra output như Hình 1.2.

C:\Temp>rtosdemo
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running

Hình 1.2. Output khi thực thi Ví dụ 1.1

Kết trên cho thấy mỗi task thay phiên nhau in ra một dòng tin nhắn đầy đủ trước khi task tiếp theo thực thi. Đây là một kết quả mô phỏng chạy trên FreeRTOS Windows. Bộ mô phỏng trên Windows không thật sự là real-time. Ghi ra màn hình console của Windows tương đối lâu hơn vì phải qua một chuỗi lệnh gọi hệ thống Windows. Thực thi code tương tự trên một hệ thống nhúng thật với hàm print nhanh và không chặn (non-blocking) sẽ cho kết quả là mỗi task in ra dòng tin nhắn của nó nhiều lần trước khi bị switch out để cho task khác chạy.

Hình 1.2 cho thấy hai task xuất hiện thực thi đồng thời. Tuy nhiên, cả hai task đều thực thi trên cùng core của vi xử lý, nên thật ra không phải như vậy. Cả hai task đều đi vào và thoát ra trạng thái Running rất nhanh. Chúng đều chạy với độ ưu tiên bằng nhau và do đó chia sẻ khoảng thời gian bằng nhau trên cùng một core vi xử lý. Hình 1.3 dưới đây vẽ chính xác đồ thị thực thi của chúng.

Mũi tên dài dưới đáy Hình 1.3 biểu thị thời gian trôi từ t1 trở đi. Đường thẳng màu biểu thị task nào đang thực thi tại mỗi thời điểm. Ví dụ như task 1 thực thi giữa khoảng thời gian t1 và t2.

Chỉ có duy nhất một task ở trạng thái Running ở bất kỳ thời điểm nào. Do đó, khi một task đi vô trạng thái Running (task được switch in) thì các task còn lại đi vô trạng thái Not Running (task được switch out).

Hình 1.3. Đồ thị quá trình thực thi của hai task trong Ví dụ 1.1

Ví dụ 1.1 tạo hai task từ bên trong hàm main(), trước khi bắt đầu scheduler. Người ta còn có thế tạo task từ bên trong một task khác. Ví dụ, Task 2 có thể được tạo bên trong Task 1, như đoạn code dưới đây.

void vTask1( void * pvParameters )
{
    const char *pcTaskName = "Task 1 is running\r\n";
    volatile unsigned long ul; /* volatile to ensure ul is not optimized away. */

    /*
    * If this task code is executing then the scheduler must already have* been started. Create the other task before entering the infinite loop.
    */
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
    for( ;; )
    {
        /* Print out the name of this task. */
        vPrintLine( pcTaskName );

        /* Delay for a period. */
        for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
        {
            /*
            * This loop is just a very crude delay implementation. There is
            * nothing to do in here. Later examples will replace this crude
            * loop with a proper delay/sleep function.
            */
        }
    }
}

Code Block 1.7. Tạo một task từ bên trong một task khác sau khi scheduler đã bắt đầu

Ví dụ 1.2 Sử dụng tham số của task (task parameter)

Hai task đã tạo ở Ví dụ 1.1 là hầu như giống nhau, chỉ có chuỗi ký tự in ra là khác nhau. Nếu bạn tạo hai instance của một task implementation, và xài task parameter để truyền string vào mỗi instance, thì sẽ bỏ được sự trùng lặp.

Ví dụ 1.2 thay thế hai hàm task dùng ở Ví dụ 1.1 bằng một hàm task gọi là vTaskFunction(), như ở Code Block 1.8. Chú ý cách task parameter được ép kiểu sang một con trỏ tới char để nhận được string mà task sẽ in ra.

void vTaskFunction( void * pvParameters )
{
    char *pcTaskName;
    volatile unsigned long ul; /* volatile to ensure ul is not optimized away. */

    /*
    * The string to print out is passed in via the parameter. Cast this to a
    * character pointer.
    */
    pcTaskName = ( char * ) pvParameters;

    /* As per most tasks, this task is implemented in an infinite loop. */
    for( ;; )
    {
        /* Print out the name of this task. */
        vPrintLine( pcTaskName );

        /* Delay for a period. */
        for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
        {
            /*
            * This loop is just a very crude delay implementation. There is
            * nothing to do in here. Later exercises will replace this crude
            * loop with a proper delay/sleep function.
            */
        }
    }
}

Code Block 1.8. Hàm task dùng để tạo hai task cho Ví dụ 1.2

Code Block 1.9 tạo hai instance của task được implement bằng hàm vTaskFunction(), nó xài task parameter để truyền vô chuỗi ký tự khác nhau cho mỗi task. Cả hai task đều thực thi độc lập dưới sự điều khiển của FreeRTOS scheduler và với stack riêng của chúng, mỗi stack chứa một bản sao riêng biệt của biến pcTaskNameul.

/*
* Define the strings that will be passed in as the task parameters. These are
* defined const and not on the stack used by main() to ensure they remain
* valid when the tasks are executing.
*/
static const char * pcTextForTask1 = "Task 1 is running";
static const char * pcTextForTask2 = "Task 2 is running";

int main( void )
{
    /*
    * Variables declared here may no longer exist after starting the FreeRTOS
    * scheduler. Do not attempt to access variables declared on the stack used
    * by main() from tasks.
    */

    /* Create one of the two tasks. */
    xTaskCreate( vTaskFunction, /* Pointer to the function that
                                   implements the task. */
    "Task 1",                   /* Text name for the task. This is to
                                   facilitate debugging only. */
    1000,                       /* Stack depth - small
                                   microcontrollers
                                   will use much less stack than
                                   this.*/
    ( void * ) pcTextForTask1,  /* Pass the text to be printed into
                                   the task using the task parameter. */
    1,                          /* This task will run at priority 1. */
    NULL );                     /* The task handle is not used in
                                   this example. */

    /*
    * Create the other task in exactly the same way. Note this time that
    * multiple tasks are being created from the SAME task implementation
    * (vTaskFunction). Only the value passed in the parameter is different.
    * Two instances of the same task definition are being created.
    */
    xTaskCreate( vTaskFunction,
    "Task 2",
    1000,
    ( void * ) pcTextForTask2,
    1,
    NULL );

    /* Start the scheduler so the tasks start executing. */
    vTaskStartScheduler();

    /*
    * If all is well main() will not reach here because the scheduler will
    * now be running the created tasks. If main() does reach here then there
    * was not enough heap memory to create either the idle or timer tasks
    * (described later in this book). Chapter 3 provides more information on
    * heap memory management.
    */
    for( ;; )
    {
    }
}

Code Block 1.9. Hàm main() cho Ví dụ 1.2

Output của Ví dụ 1.2 là giống hệt như của Ví dụ 1 ở Hình 1.2.

1.5 Độ Ưu Tiên Task (Task Priorities)

FreeRTOS scheduler luôn luôn đảm bảo task có độ ưu tiên cao nhất (the highest priority task) là task được chọn đi vô trạng thái Running. Những task có độ ưu tiên bằng nhau thì được chuyển vô và ra khỏi trạng thái Running thay phiên nhau.

Tham số uxPriority của hàm API tạo task dùng để cài đặt giá trị độ ưu tiên ban đầu cho task đó. Hàm API vTaskPrioritySet() dùng để thay đổi độ ưu tiên của một task sau khi nó được tạo.

Macro configMAX_PRIORITIES là hằng số cấu hình khi biên dịch (compile-time) cài đặt số mức độ ưu tiên có thể xài. Giá trị số nhỏ biểu thị độ ưu tiên thấp, với độ ưu tiên 0 nghĩa là độ ưu tiên thấp nhất, độ ưu tiên có giá trị từ 0 tới (configMAX_PRIORITIES – 1). Mọi task đều có thể có độ ưu tiên bằng nhau.

FreeRTOS scheduler có implement hai thuật toán dùng để chọn trạng thái Running của task và giá trị configMAX_PRIORITIES lớn nhất có thể cho phép (the maximum allowable value) phụ thuộc theo loại implement được chọn:

1.5.1 Bộ Lập Lịch Phổ Biến (Generic Scheduler)

Generic scheduler được viết bằng C và xài trong tất cả bản port kiến trúc FreeRTOS. FreeRTOS không áp đặt giới hạn trên configMAX_PRIORITIES. Thông thường, lời khuyên là nên tối thiểu configMAX_PRIORITIES vì càng cần nhiều độ ưu tiên thì càng cần nhiều RAM, dẫn đến kết quả là thời gian thực thi trong trường hợp xấu nhất dài hơn.

1.5.2 Bộ Lập Lịch Kiến Trúc Tối Ưu Hóa (Architecture-Optimized Scheduler)

Kiến trúc tối ưu hóa được viết bằng assembly code đặc trưng theo kiến trúc vi xử lý và có hiệu suất cao hơn cách viết bằng C, thời gian thực thi trong trường hợp xấu nhất là giống nhau với mọi giá trị configMAX_PRIORITIES.

Kiến trúc tối ưu hóa áp đặt giá trị cực đại configMAX_PRIORITIES là 32 cho kiến trúc 32-bit và 64 cho kiến trúc 64-bit. Giống như phương pháp generic, lời khuyên là giữ configMAX_PRIORITIES ở giá trị tối thiểu thực tế bởi vì giá trị cao hơn cần nhiều RAM hơn.

Đặt configUSE_PORT_optimized_TASK_SELECTION lên 1 trong FreeRTOSConfig.h để xài kiến trúc tối ưu hóa, hoặc 0 để xài loại phổ biến. Không phải tất cả bản FreeRTOS port đều có thực hiện kiến trúc tối ưu hóa. Những trường hợp đó mặc định macro này là 1 nếu nó không được define. Còn những trường hợp không có, mặc định macro này là 0 nếu nó không được define.

1.6 Đo Thời Gian và Tick Interrupt (Time Measurement and the Tick Interrupt)

‘Time slicing’ (phân chia thời gian) đã được dùng ở những ví dụ trước, là hành vi được theo dõi trong output mà chúng tạo ra. Trong những ví dụ vừa rồi, cả hai task đều được tạo có độ ưu tiên bằng nhau, và chúng đều có thể chạy. Do đó, mỗi task đều thực thi trong một ‘time slice’ (lát cắt thời gian), đi vô trạng thái Running lúc bắt đầu time slice, và thoát khỏi trạng thái Running ở cuối time slice. Ở Hình 1.3, khoảng thời gian giữa t1 và t2 là một time slice.

Scheduler thực thi ở cuối mỗi time slice để chọn lựa task tiếp theo được chạy. Một ngắt chu kỳ, được gọi là ‘tick interrupt’, được dùng cho việc này. Macro configTICK_RATE_HZ dùng để set tần số của tick interrupt và cũng là chiều dài của time slice. Giả sử set configTICK_RATE_HZ giá trị 100 (Hz) nghĩa là mỗi time slice dài 100 mili giây (milliseconds). Khoảng thời gian giữa hai tick interrupt được gọi là ‘tick period’ (chu kỳ tick), cho nên một time slice tương đương một tick period.

Một điều quan trọng cần lưu ý rằng scheduler không chỉ chọn lựa task tiếp theo được chạy tại thời điểm kết thúc time slice. Chúng ta sẽ thảo luận ở những chương sau, khi scheduler cũng có thể chọn lựa task tiếp theo được chạy sau khi task hiện tại đi vô trạng thái Blocked, hoặc khi một ngắt (interrupt) di chuyển một task có độ ưu tiên cao hơn thành trạng thái Ready.

Hình 1.4 là mở rộng cho Hình 1.3 có vẽ thêm sự thực thi của scheduler. Trong Hình 1.4, dòng trên cùng xuất hiện khi scheduler đang thực thi, mũi tên mỏng thể hiện trình tự thực thi chuyển từ một task sang một tick interrupt, sau đó từ tick interrupt trở về một task khác.

Giá trị tối ưu của configTICK_RATE_HZ là tùy theo mỗi ứng dụng. Tuy nhiên 100 thường là giá trị phổ biến.

Hình 1.4. Lưu đồ thực thi mở rộng cho thấy hoạt động của tick interrupt

Các lệnh gọi FreeRTOS API chỉ định thời gian trong mỗi tick period, thường gọi tắt là ‘tick’. Macro pdMS_TO_TICKS() chuyển đổi một khoảng thời gian tính bằng mili giây sang một khoảng thời gian tính bằng tick. Độ phân giải phụ thuộc theo tick frequency được define, và pdMS_TO_TICKS() không xài được nếu tick frequency trên 1KHz (nếu configTICK_RATE_HZ lớn hơn 1000). Code Block 1.10 giới thiệu cách chuyển đổi 200 mili giây thành khoảng thời gian tick tương đương.

/*
* pdMS_TO_TICKS() takes a time in milliseconds as its only parameter,
* and evaluates to the equivalent time in tick periods. This example shows
* xTimeInTicks being set to the number of tick periods that are equivalent
* to 200 milliseconds.
*/
TickType_t xTimeInTicks = pdMS_TO_TICKS( 200 );

Code Block 1.10. Macro pdMS_TO_TICKS() chuyển đổi 200 mili giây thành khoảng thời gian tick tương đương

Xài macro này để chỉ định thời gian tính bằng mili giây thay vì viết trực tiếp bằng tick giúp đảm bảo thời gian trong ứng dụng không bị thay đổi nếu tick frequency bị sửa.

‘tick count’ là tổng số tick interrupt đã xảy ra tính từ lúc scheduler bắt đầu chạy, giả định rằng tick count chưa bị tràn (overflow). Các ứng dụng người dùng không cần quan tâm vấn đề overflow khi chỉ định delay period, bởi vì FreeRTOS quản lý tính nhất quán về thời gian nội bộ.

Phần 1.12 Scheduling Algorithms mô tả các hằng số cấu gây hình ảnh hưởng việc scheduler chọn lựa một task mới được chạy và khi nào một tick interrupt sẽ thực thi.

Ví dụ 1.3 Thí nghiệm với độ ưu tiên

Scheduler sẽ luôn đảm bảo task có độ ưu tiên cao nhất được trở thành task đi vô trạng thái Running. Những ví dụ trước đã tạo hai task có độ ưu tiên giống nhau, nên cả hai đều cùng lần lượt đi vào và ra trạng thái Running. Ví dụ này sẽ nhìn xem chuyện gì xảy ra khi những task có độ ưu tiên khác nhau. Code Block 1.11 diễn tả code tạo hai task, task đầu tiên có độ ưu tiên là 1, task thứ hai có độ ưu tiên là 2. Hàm thực hiện hai task đều không thay đổi. Nó vẫn in một chuỗi theo chu kỳ, xài một vòng lặp null tạo delay.

/*
* Define the strings that will be passed in as the task parameters.
* These are defined const and not on the stack to ensure they remain valid
* when the tasks are executing.
*/
static const char * pcTextForTask1 = "Task 1 is running";
static const char * pcTextForTask2 = "Task 2 is running";

int main( void )
{
    /* Create the first task with a priority of 1. */
    xTaskCreate( vTaskFunction, /* Task Function */
                 "Task 1", /* Task Name */
                 1000, /* Task Stack Depth */
                 ( void * ) pcTextForTask1, /* Task Parameter */
                 1, /* Task Priority */
                 NULL );

    /* Create the second task at a higher priority of 2. */
    xTaskCreate( vTaskFunction, /* Task Function */
                 "Task 2", /* Task Name */
                 1000, /* Task Stack Depth */
                 ( void * ) pcTextForTask2, /* Task Parameter */
                 2, /* Task Priority */
                 NULL );

    /* Start the scheduler so the tasks start executing. */
    vTaskStartScheduler();

    /* Will not reach here. */
    return 0;
}

Code Block 1.11. Tạo hai task có độ ưu tiên khác nhau

Task 2 có độ ưu tiên cao hơn Task 1 đồng thời luôn luôn chạy, nên scheduler luôn chọn Task 2, và Task 1 không bao giờ thực thi. Task 1 được gọi là bị ‘starved’ thời gian xử lý bởi Task 2. Nó không thể in chuỗi ký tự của nó bởi vì nó không bao giờ được vô trạng thái Running. Hình 1.5 là output của Ví dụ 1.3.

C:\Temp>rtosdemo
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running

Hình 1.5. Kết quả chạy hai task có độ ưu tiên khác nhau

Task 2 luôn được chạy vì nó không bao giờ phải chờ bất kỳ cái gì. Nó chạy trong vòng lặp null hoặc in ra terminal.

Hình 1.6. Lưu đồ thực thi khi task có độ ưu tiên cao hơn task còn lại ở Ví dụ 1.3

1.7 Mở Rộng Trạng Thái Not Running

Vậy là chúng ta đã tạo ra những task luôn có nhiệm vụ cần xử lý và không bao giờ phải chờ điều gì, và bởi vì chúng không cần phải chờ điều gì, nên chúng luôn luôn đủ điều kiện vô trạng thái Running. Những task ‘xử lý liên tục’ (continuous processing) kiểu này thì không hiệu quả, chúng chỉ nên được tạo với độ ưu tiên thấp nhất, vì nếu được chạy ở những độ ưu tiên khác, chúng sẽ ngăn cản những task có độ ưu tiên thấp hơn có cơ hội hoạt động (vô trạng thái Running).

Nếu muốn những task này hữu dụng, bạn phải viết lại thành kiểu event-driven. Event-driven task (tác vụ hướng sự kiện) chỉ làm việc khi một sự kiện (event) xảy ra kích hoạt nó thay vì chạy theo luồng tuần tự. Nó không được tự vô trạng thái Running trước thời điểm đó. Event-driven task giúp hệ thống phản ứng nhanh, linh hoạt và xử lý bất đồng bộ. Scheduler luôn luôn chọn task có độ ưu tiên cao nhất được chạy. Nếu một task có độ ưu tiên cao không thể được chọn bởi vì nó đang chờ một sự kiện, thì scheduler phải chọn một task có độ ưu tiên thấp hơn chạy thay thế. Do đó, các event-driven task có thể được tạo với các độ ưu tiên khác nhau ngoại trừ là độ ưu tiên cao nhất vì chúng sẽ giành thời gian xử lý của tất cả task có độ ưu tiên thấp hơn.

1.7.1 Trạng Thái Blocked

Một task chờ đợi một sự kiện gọi là ở trạng thái ‘Blocked’, một trạng thái con của Not Running.

Task có thể đi vô trạng thái Blocked để chờ hai loại sự kiện sau:

  1. Sự kiện tạm thời (temporal event): những sự kiện này xảy ra khi một khoảng thời gian delay kết thúc hoặc đủ một khoảng thời gian cụ thể. Ví dụ, một task có thể vô trạng thái Blocked chờ 10 mili giây trôi qua.
  2. Sự kiện đồng bộ hóa (synchronization event): những sự kiện này bắt nguồn từ task khác hoặc interrupt khác. Ví dụ, một task có thể vô trạng thái Blocked để chờ data có trong hàng đợi (queue). Sự kiện đồng bộ hóa cũng bao gồm nhiều loại sự kiện khác.

FreeRTOS queue, binary semaphore, counting semaphore, mutex, recursive mutex, event group, stream buffer, message buffer, và thông báo task trực tiếp đều có thể tạo sự kiện đồng bộ hóa.

Một task có thể block một sự kiện đồng bộ hóa trong một khoảng thời gian giới hạn (timeout), đồng thời bị block theo cả hai loại sự kiện. Ví dụ, một task có thể chọn chờ data tới queue trong khoảng thời gian tối đa là 10 mili giây. Task sẽ rời trạng thái Blocked nếu tới đến trong vòng 10 mili giây, hoặc đã trôi qua 10 mili giây mà vẫn chưa nhận data.

1.7.2 Trạng Thái Suspended

Suspended cũng là một trạng thái con của Not Running. Task ở trạng thái Suspended không có trong scheduler. Cách duy nhất để vô trạng thái Suspended là gọi hàm API vTaskSuspend(), và cách duy nhất để ra khỏi trạng thái này là gọi hàm API vTaskResume() hoặc xTaskResumeFromISR(). Hầu hết ứng dụng thường không xài trạng thái Suspended.

1.7.3 Trạng Thái Ready

Task không ở trạng thái Not RunningBlocked hoặc Suspended là trạng thái Ready. Chúng có thể chạy, do đó chuẩn bị (ready) chạy, nhưng hiện tại chưa ở trạng thái Running.

1.7.4 Đồ Thị Chuyển Trạng Thái Hoàn Chỉnh

Hình 1.7 mở rộng đồ thị trạng thái ở trên, bổ sung thêm các trạng thái con của Not Running. Những task đã tạo trong các ví dụ trước không xài trạng thái Blocked hay Suspended. Chúng chỉ chuyển trạng thái giữa ReadyRunning như vẽ nét liền đậm trong Hình 1.7.

Hình 1.7. Các trạng thái task đầy đủ

Ví dụ 1.4 Xài trạng thái Blocked để tạo delay

Những task đã tạo ở những ví dụ trước đều delay trong một chu kỳ rồi sau đó in ra chuỗi ký tự của chúng. Delay đã được tạo rất ‘độc ác’ bằng một vòng lặp null (null loop), task poll một biến đếm counter tăng trong mỗi lần lặp tới khi đạt giá trị định sẵn. Ví dụ 1.3 đã mô tả rõ ràng sự bất tiện của phương pháp này. Task có độ ưu tiên cao hơn vẫn duy trì trạng thái Running khi nó thực thi phần vòng lặp null, nó giành toàn bộ thời gian xử lý với task có độ ưu tiên thấp hơn.

Còn nhiều nhược điểm khác của kỹ thuật polling, nổi bật là tính không hiệu quả của nó. Trong khi polling, mặc dù task không làm gì, nhưng nó vẫn xài hết thời gian xử lý, lãng phí chu kỳ xử lý (processor cycles). Ví dụ 1.4 sửa đúng lại bằng cách thay thế cách poll vòng lặp null bằng cách gọi hàm API vTaskDelay(), prototype của nó ở Code Block 1.12. Task được viết mới lại như trong Code Block 1.13. Chú ý rằng hàm API vTaskDelay() chỉ khả dụng khi INCLUDE_vTaskDelay được set thành 1 trong FreeRTOSConfig.h.

vTaskDelay() làm cho task gọi nó đi vô trạng thái Blocked trong một số tick interrupt cố định. Task không tiêu thụ thời gian xử lý (processing time) trong khi ở trạng thái Blocked, task chỉ tiêu thụ thời gian xử lý khi thật sự có công việc cần làm.

void vTaskDelay( TickType_t xTicksToDelay );

Code Block 1.12. Prototype của hàm API vTaskDelay()

Tham số của xTaskDelay():

  • xTicksToDelay

Số tick interrupt mà hàm gọi sẽ duy trì trong trạng thái Blocked trước khi chuyển trạng thái về Ready.

Nếu một task gọi vTaskDelay( 100 ) khi tick đếm là 10,000, thì nó sẽ ngay lập tức vô trạng thái Blocked, và duy trì ở trạng thái Blocked đến khi tick đếm tới 10,100.

Macro pdMS_TO_TICKS() chuyển đổi một khoảng thời gian tính bằng mili giây sang một khoảng thời gian tính bằng tick. Ví dụ, gọi hàm vTaskDelay( pdMS_TO_TICKS( 100 ) ) sẽ cho kết quả là lệnh gọi task duy trì ở trạng thái Blocked trong 100 mili giây.

void vTaskFunction( void * pvParameters )
{
    char * pcTaskName;
    const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );

    /*
    * The string to print out is passed in via the parameter. Cast this to a
    * character pointer.
    */
    pcTaskName = ( char * ) pvParameters;

    /* As per most tasks, this task is implemented in an infinite loop. */
    for( ;; )
    {
        /* Print out the name of this task. */
        vPrintLine( pcTaskName );

        /*
        * Delay for a period. This time a call to vTaskDelay() is used which
        * places the task into the Blocked state until the delay period has
        * expired. The parameter takes a time specified in 'ticks', and the
        * pdMS_TO_TICKS() macro is used (where the xDelay250ms constant is
        * declared) to convert 250 milliseconds into an equivalent time in
        * ticks.*/
        vTaskDelay( xDelay250ms );
    }
}

Code Block 1.13. Source code cho task ví dụ sau khi thay thế delay vòng lặp null bằng lệnh gọi hàm vTaskDelay()

Dù cả hai task được tạo có độ ưu tiên khác nhau, nhưng giờ đây chúng đều có thể chạy được. Output của Ví dụ 1.4Hình 1.8 cho thấy hành vi đúng như mong đợi.

C:\Temp>rtosdemo
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running

Hình 1.8. Output tạo ra khi Ví dụ 1.4 thực thi

Quá trình thực thi được vẽ như Hình 1.9, giải thích tại sao cả hai task đều chạy, ngay cả khi khác độ ưu tiên. Phần thực thi của scheduler đã được lược bỏ cho dễ nhìn.

Idle task được tạo tự động ngay khi scheduler bắt đầu, để đảm bảo luôn có ít nhất một task đang chạy hay ít nhất một task ở trạng thái Ready. Phần 1.8 Idle Task và Idle Task Hook sẽ mô tả về Idle task một cách chi tiết.

Hình 1.9. Lưu đồ thực thi khi các task xài vTaskDelay() thay vì xài vòng lặp null

Chỉ có phần implementation của hai task là thay đổi, còn chức năng của chúng thì không. So sánh Hình 1.9 với Hình 1.4 cho thấy chức năng này đang hoạt động hiệu quả hơn nhiều.

Hình 1.4 cho thấy đoạn thực thi khi các task xài vòng lặp null để tạo delay, do đó nó luôn luôn chạy. Kết quả là chúng luôn xài hết 100% thời gian của bộ xử lý. Hình 1.9 thì cho thấy đoạn thực thi khi các task đi vô trạng thái Blocked hoàn toàn trong khoảng thời gian delay của chúng. Chúng chỉ xài thời gian của bộ xử lý khi chúng thật sự có công việc cần làm (trong trường hợp này thì chỉ đơn giản là in ra dòng tin nhắn), do đó, kết quả là chỉ xài một đoạn nhỏ thời gian của bộ xử lý.

Mỗi lần task rời trạng thái Blocked, chúng thực thi trong một phần nhỏ của tick period trước khi lại quay về trạng thái Blocked. Phần lớn thời gian không có application task nào cần chạy (không có application task nào ở trạng thái Ready), do đó không có application task nào có thể chọn để vô trạng thái Running. Lúc này thì idle task sẽ chạy. Số thời gian xử lý ở idle task là mức đo lường cho hiệu xuất xử lý dự phòng (the spare processing capacity) trong hệ thống. Do đó, RTOS giúp tăng đáng kể hiệu xuất xử lý dự phòng một cách đơn giản chỉ bằng việc cho phép ứng dụng hoàn toàn là event-driven.

Những đường mũi tên in đậm trong Hình 1.10 vẽ sự chuyển đổi của các task trong Ví dụ 1.4, với mỗi task di chuyển qua trạng thái Blocked trước khi trở về trạng thái Ready.

Hình 1.10. Đường mũi tên in đậm biểu thị sự chuyển trạng thái thực hiện bởi các task trong Ví dụ 1.4

1.7.5 Hàm API vTaskDelayUntil()

vTaskDelayUntil() tương tự như vTaskDelay(). Như đã mô tả, tham số của vTaskDelay() chỉ định số tick interrupt xảy ra giữa lần task gọi hàm này đến lúc task này rời khỏi trạng thái Blocked. Thời gian task ở trong trạng thái Blocked được chỉ định bởi tham số của vTaskDelay(), nhưng thời gian lúc task rời khỏi trạng thái Blocked thì phụ thuộc tương đối với thời gian lúc vTaskDelay() đã được gọi.

Thay vào đó các tham số của vTaskDelayUntil() chỉ định chính xác giá trị tick count lúc task gọi sẽ rời khỏi trạng thái Blocked quay về trạng thái Ready. vTaskDelayUntil() là hàm API xài khi cần một khoảng thời gian thực thi cố định (khi bạn muốn task của bạn thực thi theo chu kỳ với một tần số cố định), do thời gian task gọi được unblocked là chính xác, chứ không phụ thuộc theo lúc hàm được gọi như với trường hợp vTaskDelay().

void vTaskDelayUntil( TickType_t * pxPreviousWakeTime,
                      TickType_t xTimeIncrement );

Code Block 1.14. Prototype của hàm vTaskDelayUntil()

Tham số của xTaskDelayUntil():

  • pxPreviousWakeTime

Tham số này được đặt tên dựa trên giả sử rằng vTaskDelayUntil() sẽ được xài để làm cho một task thực thi theo chu kỳ với một tần số cố định. Trong trường hợp này, pxPreviousWakeTime lưu giữ thời gian tại lần cuối task rời khỏi trạng thái Blocked (thức dậy trước đó). Thời điểm này được xem như một điểm tham chiếu để tính toán thời gian ở lần kế tiếp task nên rời khỏi trạng thái Blocked.

Biến được trỏ bởi pxPreviousWakeTime sẽ được cập nhật tự động trong hàm vTaskDelayUntil(). Nó không được thay đổi bởi code của ứng dụng, nhưng phải được khởi tạo bằng tick count hiện tại trước khi gọi lần đầu. Code Block 1.15 mô tả cách khởi tạo biến này.

  • xTimeIncrement

(đang viết tiếp)