FreeRTOS_分析
FreeRTOS 是一个开源的实时操作系统。可以在很的低内存使用的情况下运行在单片机上,使得单片机可以并发(虽然某一时刻还是只有一个任务运行) 的运行程序。关于一些 FreeRTOS 优缺点的介绍文章很多,这里就不再赘述直接深入代码探究原理。
关于 FreeRTOS 的疑问
在刚接触 FreeRTOS 的时候我是有以下几个问题的。
1. FreeRTOS 是如何建立任务的呢?
2. FreeRTOS 是调度和切换任务的呢?
3. FreeRTOS 是如何保证实时性呢?
这篇就是对第一个问题,FreeRTOS 是如何建立任务从代码上进行分析。
在 FreeRTOS 官方的指南上对创建任务就是调用了 xTaskCreate 函数。下面的图截取与官方的教程,里面对 xTaskCreate 函数的用法进行了展示,并对参数也进行了大体上的介绍。
xTaskCreate 函数分析
这个函数被包含在 FreeRTOS 代码的 task.c 中。这个函数比较长,下面贴出的函数对里面的内容进行了省略。
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */const configSTACK_DEPTH_TYPE usStackDepth,void * const pvParameters,UBaseType_t uxPriority,TaskHandle_t * const pxCreatedTask ){TCB_t * pxNewTCB;BaseType_t xReturn;/* Allocate space for the TCB. Where the memory comes from depends on* the implementation of the port malloc function and whether or not static* allocation is being used. */pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );...pxNewTCB->pxStack = ( StackType_t * ) pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); ...prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );prvAddNewTaskToReadyList( pxNewTCB );xReturn = pdPASS;
从代码中很容易看出,xTaskCreate 函数主要的工作就是 3 步。
第1步 给新的任务申请空间
pvPortMalloc
函数,其作用是给新的任务申请一块任务控制块(TCB, Task Control Block)空间。后面继续使用 pvPortMallocStack
函数为新的任务申请一块栈空间。 其具体的实现在 heapX.c 中 X 可以是1~5。相关的细节可以看一下相关的介绍,这里的2个用于申请空间的函数可以简单的看作是 malloc 函数就可以了。
第2步 为新任务初始化空间
prvInitialiseNewTask
函数也很长,故省略里面很多的内容。专注于实现逻辑,先放弃具体细节。
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */const uint32_t ulStackDepth,void * const pvParameters,UBaseType_t uxPriority,TaskHandle_t * const pxCreatedTask,TCB_t * pxNewTCB,const MemoryRegion_t * const xRegions )
{StackType_t * pxTopOfStack;UBaseType_t x;
...#if ( portSTACK_GROWTH < 0 ){pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) ); /*lint !e923 !e9033 !e9078 MISRA exception. Avoiding casts between pointers and integers is not practical. Size differences accounted for using portPOINTER_SIZE_TYPE type. Checked by assert(). *//* Check the alignment of the calculated top of stack is correct. */configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
...}
.../* Store the task name in the TCB. */if( pcName != NULL ){for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ){pxNewTCB->pcTaskName[ x ] = pcName[ x ];/* Don't copy all configMAX_TASK_NAME_LEN if the string is shorter than* configMAX_TASK_NAME_LEN characters just in case the memory after the* string is not accessible (extremely unlikely). */if( pcName[ x ] == ( char ) 0x00 ){break;}else{mtCOVERAGE_TEST_MARKER();}}/* Ensure the name string is terminated in the case that the string length* was greater or equal to configMAX_TASK_NAME_LEN. */pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';}
...pxNewTCB->uxPriority = uxPriority;vListInitialiseItem( &( pxNewTCB->xStateListItem ) );vListInitialiseItem( &( pxNewTCB->xEventListItem ) );/* Set the pxNewTCB as a link back from the ListItem_t. This is so we can get* back to the containing TCB from a generic item in a list. */listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );/* Event lists are always in priority order. */listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );...pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
...if( pxCreatedTask != NULL ){/* Pass the handle out in an anonymous way. The handle can be used to* change the created task's priority, delete the created task, etc.*/*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;}
...
}
初始化了两个存储在 任务 TCB 中的链表 xStateListItem
& xEventListItem
。从这链表的名字可以能看出来一个是记录这个任务的状态,另一个是记录了任务的事件列表。
再往下的 pxPortInitialiseStack
函数比较关键,这个函数根据不同的芯片架构来为寄存器分配栈空间。下面的函数以 Cortex-m3 为例,具体文件位置为 FreeRTOS\Source\portable\GCC\ARM_CM3\port.c
。
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,TaskFunction_t pxCode,void * pvParameters )
{/* Simulate the stack frame as it would be created by a context switch* interrupt. */pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */*pxTopOfStack = portINITIAL_XPSR; /* xPSR */pxTopOfStack--;*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */pxTopOfStack--;*pxTopOfStack = ( StackType_t ) portTASK_RETURN_ADDRESS; /* LR */pxTopOfStack -= 5; /* R12, R3, R2 and R1. */*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */return pxTopOfStack;
}
在 Cortex-m3 中按照下面的形式在栈上为寄存器规划好存储的位置。
第3步 将新任务加入准备执行队列
prvAddNewTaskToReadyList
函数就是做这个工作的。在加入到 ready 队列后,FreeRTOS 的调度器就可以在上面根据不同的优先级和 Delay 等信息挑选合适的任务转入到 running 模式。
static void prvAddNewTaskToReadyList( TCB_t * pxNewTCB )
{/* Ensure interrupts don't access the task lists while the lists are being* updated. */taskENTER_CRITICAL();{uxCurrentNumberOfTasks++;if( pxCurrentTCB == NULL ){/* There are no other tasks, or all the other tasks are in* the suspended state - make this the current task. */pxCurrentTCB = pxNewTCB;if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 ){prvInitialiseTaskLists();}
...}else{/*看一下新加入的任务的优先级是不是大于当前的任务,如果是的话就把当前任务换成这个新的优先级更高的任务*/if( xSchedulerRunning == pdFALSE ){if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ){pxCurrentTCB = pxNewTCB;}}
...}uxTaskNumber++;prvAddTaskToReadyList( pxNewTCB );}taskEXIT_CRITICAL();
}
prvInitialiseTaskLists
如果是编号为 1 的任务的话,该函数还会额外初始化5个链表。5个链表的作用从注释中也能看出。
PRIVILEGED_DATA static List_t xDelayedTaskList1; /*< Delayed tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList2; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t xPendingReadyList; /*< Tasks that have been readied while the scheduler was suspended. They will be moved to the ready list when the scheduler is resumed. */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; /*< Points to the delayed task list currently being used. */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */
xDelayedTaskList1 链表是用于记录目前的延迟任务。
xDelayedTaskList2 链表是用于记录延迟已经达到当前滴答定时器的溢出数量的任务。
xPendingReadyList 链表使用于记录任务已经准备就绪,这些任务目前处于挂起状态。当调度器恢复后这些任务就会被移动到就绪状态。
pxDelayedTaskList 链表适用于记录当前正在使用的延迟列表。
pxOverflowDelayedTaskList 用于当前用于保存已超出当前tick计数的任务的延迟任务列表。
下面是 prvInitialiseTaskLists 函数的全部代码。
static void prvInitialiseTaskLists( void )
{UBaseType_t uxPriority;for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ){vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );}vListInitialise( &xDelayedTaskList1 );vListInitialise( &xDelayedTaskList2 );vListInitialise( &xPendingReadyList );#if ( INCLUDE_vTaskDelete == 1 ){vListInitialise( &xTasksWaitingTermination );}#endif /* INCLUDE_vTaskDelete */#if ( INCLUDE_vTaskSuspend == 1 ){vListInitialise( &xSuspendedTaskList );}#endif /* INCLUDE_vTaskSuspend *//* Start with pxDelayedTaskList using list1 and the pxOverflowDelayedTaskList* using list2. */pxDelayedTaskList = &xDelayedTaskList1;pxOverflowDelayedTaskList = &xDelayedTaskList2;
}
prvAddNewTaskToReadyList
prvAddNewTaskToReadyList 函数从字面意思就可以看出来,将新的 task 放进 pxReadyTasksLists 链表中。
#define prvAddTaskToReadyList( pxTCB ) \traceMOVED_TASK_TO_READY_STATE( pxTCB ); \taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \listINSERT_END( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
这里面全部使用宏进行实现,以提升速度。因为宏就相当于内联了,在编译的时候就会直接进行替换,省去了调用函数时候的开销。
让当前任务的优先级始终保持为最大。
总结
通过以上的代码分析我们已经知道其实建立一个任务需要三个步骤。
- 给新的任务申请空间
- 为新任务初始化空间
- 将新任务加入准备执行队列
但是对于最开始提到的第二个问题 FreeRTOS 是调度和切换任务的呢?
请看 FreeRTOS从代码层面进行原理分析(2 任务的调度)