1. Task function
在 FreeRTOS 裡, 每條thread都稱作 task, 實作成C function, 回傳值是void, 並且parameter為void pointer, 像這樣
void ATaskFunction( void *pvParameters );
每個task通常會一直執行不會停 (所以應該有個loop在裡面), FreeRTOS 不准有"return", 也不准執行到最後一行
如果我們不需要某個task, 我們應該刪除這個task
底下是 task function可能的實作
void ATaskFunction( void *pvParameters )
{
/* 如同一般的function來宣告變數 */
int iVariableExample = 0;
/* 實作一個 infinite loop */
for( ;; )
{
/* task實際上要做的事 */
}
/* 如果task在loop中break出來, 那麼task必需在到function end之前刪除,
比如說用 vTaskDelete( NULL ), 裡面的NULL表示是刪目前的task */
vTaskDelete( NULL );
}
task function 可以拿來建多個task, 每個task都是獨立的instance, 有自己的stack
2. Task state
我們先簡單的把task分成Running / Not Running
當 task 從 Not Running 變成 Running, 稱作 task in
相反地稱作 task out
3. Create Task
建task用到 xTaskCreate() API, 它的prototype如下
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
const signed portCHAR * const pcName,
unsigned portSHORT usStackDepth,
void *pvParameters,
unsigned portBASE_TYPE uxPriority,
xTaskHandle *pxCreatedTask
);
( task.h )
底下是參數的說明:
- pvTaskCode : 放task function的function pointer
- pcName : task的名字, 在FreeRTOS不會用到它, 這只是用於debug用途, 名字長度限制為configMAX_TASK_NAME_LEN
- usStackDepth : 每個 task 被放進 stack 的時候, usStackDepth表示它需要的最大stack space, usStackDepth以byte為單位, 比如說, stack size = 32 bit, usStackDepth = 100, 則會用到100*4=400 bytes stack space, 如果task只是設計成idle task, 那麼可以用這個值 configMINIMAL_STACK_SIZE
- pvParameters : task function的參數, 型態為 (void *)
- uxPriority : task 的優先權, 0最低, (configMAX_PRIORITIES – 1)最高
- pxCreatedTask : 這個task的reference, 用於動態調整這個task的priority, 或是刪掉這個task
可能的回傳值 :
- pdTRUE : 表示成功
- errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY : 表示RAM不夠了
底下是sample code:
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* busy delay */
}
}
}
void vTask2( void *pvParameters )
{
const char *pcTaskName = "Task 2 is running\r\n";
volatile unsigned long ul;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* busy delay */
}
}
}
int main( void )
{
/* 建tasks1, 通常需要檢查回傳值, 這裡先略過 */
xTaskCreate( vTask1, /* task function pointer, 通常用function name就可以 */
"Task 1", /* Task name */
1000, /* Stack depth - 一般的例子不會用到這麼多 */
NULL, /* 不帶parameter */
1, /* priority */
NULL ); /* 不需要task handle */
/* task2跟task1是差不多的 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* 啟動scheduler, 讓tasks開始執行 */
vTaskStartScheduler();
/* 正常來說不會執行到這邊, 因為scheduler會執行tasks,
執行到這邊可能是 heap memory 不夠 */
for( ;; );
}
實際執行結果會看到task1 & task2兩個不停地task in/out
前個例子是把兩個task在main裡面create, 我們也可以把task放在task裡面create
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;
/* 執行到這邊表示Scheduler已經開始執行task1 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* busy delay */
}
}
}
因為task1 & task2很像, 所以改寫成一個就好
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile unsigned long ul;
/* 把要印出來的字串經由parameter帶進來, 再轉成 (char *) */
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
/* 把perameter帶進來的字串印出來 */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* busy delay*/
}
}
}
/* 要帶給task的字串 */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;
int main( void )
{
/* Create task 1 */
xTaskCreate( vTaskFunction, /* function pointer */
"Task 1", /* task name */
1000, /* stack depth */
(void*)pcTextForTask1, /* 參數, 把要印的字串轉成(void *) */
1, /* priority */
NULL ); /* task handle */
/* Create task2, 參數不一樣 */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
/* Start scheduler */
vTaskStartScheduler();
for( ;; );
}
4. Task priority
priority可以動態的改, 最高不會超過configMAX_PRIORITIES (defined in FreeRTOSConfig.h)
我們可以改configMAX_PRIORITIES, 但改的愈高也就耗掉更多的RAM, 所以通常會保留default值
為了選出下一個要執行的task, scheduler應該要在task執行的片段最後面執行
而 tick interrupt 就是讓scheduler達到這個目的
time slice的常度定義在 configTICK_RATE_HZ (FreeRTOSConfig.h)
舉例來說, 如果 configTICK_RATE_HZ =100Hz, 那麼time slice = 10ms
要注意的是, FreeRTOS的時間是以tick當單位, 就連 portTICK_RATE_MS 也是以tick當單位
每次選task, 都會選priority最高的task, 如果有一樣的priority則會輪流
如果我們把剛剛的例子改一下priority的部份
int main( void )
{
/* task1 的 priority 是 1 */
xTaskCreate( vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL );
/* task2 的 priority 是 2 */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
vTaskStartScheduler();
return 0;
}
執行結果會看到:
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
.....
結果Task 1因為priority比Task 2, 所以每次選都選不到它造成starvation
5. Not running state
為了避免starvation造成某些task永遠跑不到, 可以使用event driven
這意味著在選擇task的時候, scheduler沒有選priority最高的task
5.1 Blocked State
當task在等某個event的時候, 稱它在 Blocked State
task等的event分兩種 :
- Temporal event : 在等delay或是確切的時間
- Synchronization event : 這個 event 由其它 task 或 interrupt 產生
task 也可以同時等這兩種, 比如說, 在10s內等待data
5.2 Suspended State
當 task 在 suspended state 的時候, 就不會被scheduler選上
進suspend : vTaskSuspend()
離開suspend : vTaskResume() or xTaskResumeFromISR()
但大多時候不太會用到suspend
5.3 Ready State
當 task為 Not running, 並且不是Blocked/Suspended State, 就稱作 Ready State
5.4 State Transition Diagram
5.4 vTaskDelay()
之前的例子, 用的是busy delay, 所以Task 2實際上並沒有suspend
我們可以用 vTaskDelay() 來改寫這個 delay 讓 Task 2 進 suspend
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
vPrintString( pcTaskName );
/* 等250 ms, 參數是ticks, 可以利用portTICK_RATE_MS把milliseconds轉成ticks */
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
實際結果是:
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
.....
因為有足夠的時間, 所以雖然Task2的priority比較高, 但它執行完就suspend, 讓Task 1在Task 2 suspend的期間成為唯一的task可以執行, 然後suspend
5.5 vTaskDelayUntil()
vTaskDelayUntil() 跟vTaskDelay()很像, 差別是vTaskDelayUntil()決定多少個tick之後要被離開blocked state, 適合用在需要定時執行的task
這是它的prototype
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
其中它的參數:
- pxPreviousWakeTime : 這個pointer會更新上一次離開block state的時間
- xTimeIncrement : 週期, 單位為ticks, 一樣可以用 portTICK_RATE_MS 轉換成ms
我們再改寫剛剛的例子
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
portTickType xLastWakeTime;
pcTaskName = ( char * ) pvParameters;
/* xLastWakeTime 需要初始化成目前的tick count */
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
vPrintString( pcTaskName );
/* 自 xLastWakeTime 的時間等 250ms 離開 block state */
vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
}
}
6. Idle task
Processor通常都需要執行些什麼, 如果task都進suspend, 那裡它會執行idle task
idle task 是在 scheduler 一開始的時候產生, 擁有priority 0, 做的事情就跟busy loop差不多
6.1 Idle Task Hook Function
我們也可以加 callback function 在 idle task 裡
通常用於:
- 執行低優先權的工作, 或只需要背景處理的工作
- 計算剩下的processing capacity
- 進low power mode
6.2 Limitation of Idle Task
不可以block / suspend
如果 callback function 呼叫 vTaskDelete() 來刪除某個task, 那麼會馬上跳回這個task, 讓這個task能夠釋放已用的資源
6.3 Usage of callback of Idle Task
callback function的prototype如下
void vApplicationIdleHook( void );
使用的時候要注意 USE_IDLE_HOOK 的config要設成1 (FreeRTOSConfig.h)
底下是sample code:
/* 儲存 ilde task 裡的 loop 次數 */
unsigned long ulIdleCycleCount = 0UL;
/* Idle hook function只能叫做 vApplicationIdleHook() */
void vApplicationIdleHook( void )
{
ulIdleCycleCount++;
}
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
pcTaskName = ( char * ) pvParameters;
for( ;; )
{
/* 印出 task name 以及 ulIdleCycleCount */
vPrintStringAndNumber( pcTaskName, ulIdleCycleCount );
/* Delay 250 ms*/
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
執行結果像這樣:
Task 2 is running
uIdleCycleCOunt = 0
Task 1 is running
uIdleCycleCOunt = 0
Task 2 is running
uIdleCycleCOunt = 3869504
Task 1 is running
uIdleCycleCOunt = 3869504
Task 2 is running
uIdleCycleCOunt = 8564623
Task 1 is running
uIdleCycleCOunt = 8564623
......
7. Changing task priority
更改priority :
void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );
pxTask 是 task handle, uxNewPriority 是新的 priority
取得priority :
unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );
底下是 sample code
/* 拿來存task2 handle */
xTaskHandle xTask2Handle;
void vTask1( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
vPrintString( "Task1 is running\r\n" );
/* 把 Task 2 的 priority 設成 Task 1 多 1 */
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
}
}
void vTask2( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
vPrintString( "Task2 is running\r\n" );
/* 把 Task 2 的 priority 降 2 */
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
int main( void )
{
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
vTaskStartScheduler();
for( ;; );
}
因為Task 2的priority在一開始比Task 1少1,
所以這個sample裡, Scheduler會讓Task 1執行 (並把Task 2的priority設成比Task 1大1)
在下一輪, Scheduler發現Task 2的priority比Task 1大, 就執行Task 2 (並降priority比Task 1小1)
執行結果就是Task 1/2輪流地執行
8. Deleting Task
用 vTaskDelete() API 來刪掉task, 被刪的task就再也不會進到 Running state
而 idle task 負責釋放被刪掉的task的資源, 注意不可大量用 vTaskDelete() 造成 idle task starve
並且要注意只有 kernel 分配給 task 的資源會自動釋放, 但如果是 task 主動要的資源則要在刪除前釋放
底下是 prototype
void vTaskDelete( xTaskHandle pxTaskToDelete );
底下是sample code
/* 存 Task 2 handle */
xTaskHandle xTask2Handle;
void vTask2( void *pvParameters )
{
vPrintString( "Task2 is running and about to delete itself\r\n" );
vTaskDelete( xTask2Handle );
}
void vTask1( void *pvParameters )
{
const portTickType xDelay100ms = 100 / portTICK_RATE_MS;
for( ;; )
{
vPrintString( "Task1 is running\r\n" );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
vTaskDelay( xDelay100ms );
}
}
int main( void )
{
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
vTaskStartScheduler();
for( ;; );
}
Task 1會create Task 2, Task 2在執行完後就會自刪, 所以執行結果就是Task 1/2輪流執行