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ả. Task xử lý liên tục (continues processing task) 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:
- 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.
- 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 Running và Blocked 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 Ready và Running 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.4 là Hì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
Tham số này cũng đặt tên theo giả sử rằng vTaskDelayUntil() sẽ được dùng để làm một task thực thi theo chu kỳ với một tần số cố định được xác định bằng giá trị xTimeIncrement.
Ví dụ 1.5 Biến đổi task ví dụ để xài vTaskDelayUntil()
Hai task đã tạo trong ví dụ 1.4 gọi là task chu kỳ (periodic task), nhưng việc xài vTaskDelay() không đảm bảo rằng tần số khi chúng chạy là cố định, do thời điểm task rời khỏi trạng thái Blocked phụ thuộc với lúc chúng gọi hàm vTaskDelay(). Biến đổi bằng cách xài vTaskDelayUntil() thay vì xài vTaskDelay() giải quyết vấn đề tiềm ẩn này.
void vTaskFunction( void * pvParameters )
{
char * pcTaskName;
TickType_t xLastWakeTime;
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/*
* The xLastWakeTime variable needs to be initialized with the current tick
* count. Note that this is the only time the variable is written to
* explicitly. After this xLastWakeTime is automatically updated within
* vTaskDelayUntil().
*/
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/*
* This task should execute every 250 milliseconds exactly. As per
* the vTaskDelay() function, time is measured in ticks, and the
* pdMS_TO_TICKS() macro is used to convert milliseconds into ticks.
* xLastWakeTime is automatically updated within vTaskDelayUntil(), so
* is not explicitly updated by the task.
*/
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
Code Block 1.15. Thực hiện task ví dụ có xài vTaskDelayUntil()
Output của Ví dụ 1.5 giống như của Ví dụ 1.4 trong Hình 1.8.
Ví dụ 1.6 Kết hợp blocking task và non-blocking task
Những ví dụ vừa rồi đã xem xét hành vi của cả polling task và blocking task. Ví dụ này củng cố những gì chúng ta đã đề cập bao gồm hành vi dự kiến của hệ thống và mô tả trình tự thực thi khi hai sơ đồ được kết hợp, như sau:
Hai task được tạo có độ ưu tiên là 1. Những task này không làm gì ngoài print một chuỗi ký tự.
- Những task này không bao giờ gọi hàm API có thể khiến chúng đi vô trạng thái Blocked, cho nên chúng luôn ở trong trạng thái Ready, hoặc Running. Những task như vậy gọi là ‘continuous processing’ task, chúng luôn có việc phải làm (mặc dù không quan trọng, trong trường hợp này). Code Block 1.16 cho thấy source code của các continous processing task.
- Một task thứ ba có độ ưu tiên là 2, cao hơn độ ưu tiên của hai task kia. Task thứ ba cũng chỉ in chuỗi ký tự ra, nhưng nó in theo chu kỳ, nên nó sử dụng hàm API vTaskDelayUntil() để đưa nó vô trạng thái Blocked giữa mỗi vòng lặp in. Code Block 1.17 cho thấy source code của task chu kỳ (periodic task).
void vContinuousProcessingTask( void * pvParameters )
{
char * pcTaskName;
/*
* 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. This task just does this repeatedly
* without ever blocking or delaying.
*/
vPrintLine( pcTaskName );
}
}
Code Block 1.16. Continuous processing task trong Ví dụ 1.6
void vPeriodicTask( void * pvParameters )
{
TickType_t xLastWakeTime;
const TickType_t xDelay3ms = pdMS_TO_TICKS( 3 );
/*
* The xLastWakeTime variable needs to be initialized with the current tick
* count. Note that this is the only time the variable is explicitly
* written to. After this xLastWakeTime is managed automatically by the
* vTaskDelayUntil() API function.
*/
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( "Periodic task is running" );
/*
* The task should execute every 3 milliseconds exactly – see the
* declaration of xDelay3ms in this function.
*/
vTaskDelayUntil( &xLastWakeTime, xDelay3ms );
}
}
Code Block 1.17. Periodic task trong Ví dụ 1.6
Hình 1.11 cho thấy output tạo bởi Ví dụ 1.6, với phần giải thích bằng lưu đồ thực thi trong Hình 1.12.
Continuous task 2 running Continuous task 2 running Periodic task is running Continuous task 1 running Continuous task 1 running Continuous task 1 running Continuous task 1 running Continuous task 1 running Continuous task 2 running Continuous task 2 running Continuous task 2 running Continuous task 2 running Continuous task 2 running Continuous task 1 running Continuous task 1 running Continuous task 1 running Continuous task 1 running
Hình 1.11. Output khi Ví dụ 1.6 thực thi

Hình 1.12. Lưu đồ thực thi của Ví dụ 1.6