操作系统的核心就是任务创建,删除,挂起,恢复和调度。要了解OS的调度,需要对CPU内核相关寄存器有一定的了解,本文会根据《Cortex-M4权威指南》进行针对性的讲解。
(后面还要继续分析MPU, SVC, PendSV,中断优先级问题,空闲函数)
任务创建可以分为静态创建和动态创建(其中会涉及到MPU内存保护单元,这里不做讲述,后面会根据《Cortex-M4权威指南》的MPU相关章节做详细说明),分别使用xTaskCreateStatic()和xTaskCreate()函数来创建Task。
/* Tasks.c */
// 此段代码去掉了一些条件编译和Assert代码
#if(configSUPPORT_STATIC_ALLOCATION == 1) // 静态创建的任务无非就是要自己来申请栈和TCB的内存
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, // Task的函数
const char *const pcName, // Task的名字 指针和指针指向的数据都是不可修改的
const uint32_t ulStackDepth, // Task栈的深度
void* const pvParameters, // Task参数指针,指针是不可修改的
UBaseType_t uxPriority, // Task的优先级
StackType_t* const puxStackBuffer, // Task栈地指针,指针不可修改
StaticTask_t* const pxTaskBuffer) // 在FreeRTOS.h中定义和TCB一样大小的结构体,是为了让用户层不接触到内核的结构体
{
TCB_t* pxNewTCB;
TaskHandle_t xReturn;
if((puxStackBuffer != NULL) && (pxTaskBuffer != NULL)) // 确保Task栈和TCB指针都不能为NULL,在调用这个函数之前,必须已经定义pxTaskBuffer和puxStackBuffer,并申请了内存
{
pxNewTCB = (TCB_t*)pxTaskBuffer;
pxNewTCB->pxStack = (StackType_t*)puxStackBuffer;
#if(tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0) //如果定义了宏tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE == 1就证明了既可以动态创建Task也可以静态创建Task
{
pxNewTCB->ucStaticallyAllocated = tskSTATICALLY_ALLOCATION_STACK_AND_TCB; // 如果是静态创建的Task就确保不要释放相关内存,删除任务的时候和动态申请的操作不同
}
#endif
prvInitialiseNewTask(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, &xReturn, pxNewTCB, NULL); // 核心函数初始化新的任务
prvAddNewTaskToReadyList(pxNewTCB);
}
else
{
xReturn = NULL;
}
return xReturn;
}
#endif
// 此函数没有打开MPU
static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, // Task的函数
const char *const pcName, // Task的名字 指针和指针指向的数据都是不可修改的
const uint32_t ulStackDepth, // Task栈的深度
void* const pvParameters, // Task参数指针,指针是不可修改的
UBaseType_t uxPriority, // Task的优先级
TaskHandle_t* const pxCreatedTask, // 用于返回TaskHandle
TCB_t* pxNewTCB) // 用于返回TCB的指针
{
StackType_t* pxTopOfStack; // 定义临时栈顶指针
UBaseType_t x;
#if( tskSET_NEW_STACKS_TO_KNOWN_VALUE == 1)
{
(void)memset(pxNewTCB->pxStack, (int)tskSTACK_FILL_BYTE, (size_t)ulStackDepth * sizeof(StackType_t)); // 往Task栈里面写入固定的值,0xa5U
}
#endif
// 根据栈的生长方向确定栈顶的指针
#if(portSTACK_GROWTH < 0) // Cortex-M系列都是-1,小于零
{
pxTopOfStack = &(pxNewTCB->pxStack[ulStackDepth - (uint32_t)1]);
pxTopOfStack = (StackType_t*)(((uint32_t*)(pxTopOfStack)) & (~(uint32_t)(0x7))); // 向下做8字节对齐 cortex-M系列
#if(configRECORD_STACK_HIGH_ADDRESS == 1)
{
pxNewTCB->pxEndOfStack = pxTopOfStack; // 记录最高地址
}
#endif
}
#else
{
pxTopOfStack = pxNewTCB->pxStack; // 栈顶指针就是栈的起始地址
pxNewTCB->pxEndOfStack = &(pxNewTCB->pxStack[ulStackDepth - 1]);
}
#endif
// 保存task的名字
for(x = (UBaseType_t)0; x < (UBaseType_t)configMAX_TASK_NAME_LEN; x++)
{
pxNewTCB->pcTaskName[x] = pcName[x];
if (pcName[x] == char(0x00)) // 如果名字的长度小于最大的长度,‘\0’后面就是0x00这个时候就break出循环
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN - 1] = '\0'; // 如果名字大于最大的长度 把最后一个字符强制设置成‘\0’,如果名字小于最大长度,就会有两个‘\0’,但是没有影响
if (uxPriority >= (UBaseType_t)configMAX_PRIORITIES) // 如果大于等于最高优先级就要减1
{
uxPriority = (UBaseType_t)configMAX_PRIORITIES - (UBaseType_t)1;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxNewTCB->uxPriority = uxPriority; // 把优先级计入TCB中
#if (configUSE_MUTEXES == 1) // 用于优先级继承
{
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
}
#endif
vListInitialiseItem(&(pxNewTCB->xStateListItem)); // 初始化状态节点,主要用于调度(ready,blocked,suspended)
vListInitialiseItem(&(pxNewTCB->xEventListItem)); // 初始化事件节点
listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB); // 状态节点的节点owner设置为自己的TCB
// 事件列表的优先级值最大,优先级高,但是在事件列表的节点是优先级越小越放在前面,越早执行,因此此处xEventListItem的优先级应该是最大优先级减去本身的优先级
listSET_LIST_ITEM_VALUE(&(pxNewTCB->xEventListItem), configMAX_PRIORITIES - uxPriority);
listSET_LIST_ITEM_OWNER(&(pxNewTCB->xEventListItem), pxNewTCB); // 把xEventListItem的owner设成自己的TCB
// 记录中断嵌套
#if (configCRITICAL_NESTING_IN_TCB == 1)
{
pxNewTCB->uxCriticalNesting = (UBaseType_t)0U;
}
#endif
// 记录应用TASK的tag
#if (configUSE_APPLICATION_TASK_TAG == 1)
{
pxNewTCB->pxTaskTag = NULL;
}
#endif
// 记录Task运行的时间
#if (configGENERATE_RUN_TIME_STATS == 1)
{
pxNewTCB->ulRunTimeCounter == 0UL;
}
#endif
// 根据具体芯片初始化Task stack, 最后返回栈顶指针
pxNewTCB->pxNewTCB = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
if (pxCreateTask != NULL)
{
*pxCreatedTask = (TaskHandle_t)pxNewTCB; // pxCreateTask Task句柄是指向TCB指针的指针
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
下面是任务栈的结构图,对应与代码进行分析
栈顶,假设一开始栈顶就是8字节对齐 | 高地址 &pxStack[ulStackDepth - 1] |
---|---|
xPSR = 01000000 bit24必须置1 | 0x20001000 后三位清零 8bytes对齐 自动加载到CPU寄存器的内容 |
R15(PC)任务入口地址 : task entry | ·····自动 |
R14(LR) : prvTaskExitError | ·····自动 |
R12 : 0 | ·····自动 |
R3 : 0 | ·····自动 |
R2 : 0 | ·····自动 |
R1 : 0 | ·····自动 |
R0 : 任务形参parameter | 自动加载到CPU寄存器的内容 |
EXC_RETURN : 0xfffffffd | 需手动加载到CPU寄存器的内容 |
R11 : 0 | ·····手动 |
R10 : 0 | ·····手动 |
R9 : 0 | ·····手动 |
R8 : 0 | ·····手动 |
R7 : 0 | ·····手动 |
R6 : 0 | ·····手动 |
R5 : 0 | ·····手动 |
R4 : 0 | <----- pxTopOfStack 需手动加载到CPU寄存器的内容 |
·········· | |
空闲堆栈 | |
·········· | 低地址 &pxStack[0] 0x20000000 |
堆栈是用来在进行上下文切换的时候保存现场,一般新建好一个堆栈以后会对其先进行初始化处理,即对Cortex-M内核的某些寄存器赋初值。这些初始值就保存在任务堆栈中,保存的顺序按照上图顺序进行排列。(运行的时候会将这些值,给到内核寄存器中,任务栈中的R0-R15只是用作与内核寄存器保持对应关系)
// Port.c
StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack, TaskFunction pxCode, void *pvParameters)
{
pxTopOfStack--; // 前四个字节没赋值
*pxTopOfStack = portINITIAL_XPSR; // xPSR的值设置为0x01000000, xPSR是Cortex-M4的一个内核寄存器,叫程序状态寄存器,bit24设置为1的时候,表示处于Thumb状态
pxTopOfStack--;
*pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK; // 任务函数的入口地址会保存到PC寄存器中 portSTART_ADDRESS_MASK = 0xfffffffe
pxTopOfStack--;
*pxTopOfStack = (StackType_t)prvTaskExitError; // 链接寄存器,任务的返回地址,通常任务是不会返回的,如果返回了就跳转到prvTaskExitError, 该函数是个无限循环
pxTopOfStack -= 5; // 偏移到R0 parameter的寄存器中,跳过的R12, R3, R2, R1默认初始化为0
*pxTopOfStack = (StackType_t)pvParameters; // R0寄存器里面存的是parameter的指针
pxTopOfStack--;
*pxTopOfStack = portINITIAL_EXC_RETURN; // 保存EXC_RETURN的值,用于退出SVC或PendSV中断的时候处理器应该处于什么状态。处理器进入异常或中断服务程序(ISR)时,链接寄存器R14(LR)的数值会被更新为EXC_RETURN数值,之后该数值会在异常处理结束时触发异常返回。这里人为设置为0xfffffffd, 表示退出异常以后CPU进入线程模式并且使用进程栈
pxTopOfStack -= 8; // 跳过R11-R4寄存器,此时栈顶的指针在R4上。
return pxTopOfStack;
}
任务创建完成之后,就会把TCB添加到就绪列表中,FreeRTOS使用不同的列表表示任务的不同状态,在文件task.c中定义了多个列表来完成不同的功能,具体的列表如下:
// task.c
static List_t pxReadyTasksLists[configMAX_PRIORITIES]; // 根据优先级创建ready列表
static List_t xDelayedTaskList1;
static List_t xDelayedTaskList2;
static List_t* volatile pxDelayedTaskList;
static List_t* volatile pxOverflowDelayedTaskList;
static List_t xPendingReadyList;
将TCB插入到对应的Ready列表中:
// task.c
static void prvAddNewTaskToReadyList(TCB_t* pxNewTCB)
{
taskENTER_CRITICAL(); // 进入临界区,列表更新的时候不允许有中断过来
{
uxCurrentNumberOfTask++; // 全局变量 当前的任务数加一
if (pxCurrentTCB == NULL) // 当前没有任务在运行
{
pxCurrentTCB = pxNewTCB;
if (uxCurrentNumberOfTask == 1) // 证明只有一个任务,就是马上要插入的任务
{
prvInitialiseTaskLists(); // 初始化各种列表
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
if (xSchedulerRunning == pdFALSE)
{
if (pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority)
{
pxCurrentTCB = pxNewTCB; // 当调度器没有开始调度,如果要插入的任务给到pxCurrentTCB中
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
} // 上面代码就是要改变pxCurrentTCB
uxTaskNumber++;
#if (configUSE_TRACE_FACILITY == 1)
{
pxNewTCB->uxTCBNumber = uxTaskNumber;
}
#endif
prvAddTaskToReadyList(pxNewTCB); // pxCurrentTCB无论是否改变,都要有插入就绪列表的实际操作, 是个宏
portSETUP_TCB(pxNewTCB);
}
taskEXIT_CRITICAL();
if (xSchedulerRunning == pdTRUE) // 插入完成后,如果已经开始调度了
{
// 如果创建的任务比当前的Task有更高的优先级,就会马上跑起来
if (pxCurrentTCB->uxPriority < pxNewTCB->uxPriority)
{
taskYELD_IF_USING_PREEMPTION(); // 完成一次任务切换,后面讲解调度的时候会讲到
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
上面代码插入列表的函数,prvAddTaskToReadyList()
#define prvAddTaskToReadyList(pxTCB)\
taskRECORD_READY_PRIORITY((pxTCB)->uxPriority);\ // 根据优先级设置全局变量uxTopReadyPriority
vListInsertEnd(&(pxReadyTasksLists[(pxTCB)->uxPriority]), &((pxTCB)->xStateListItem));
#if (INCLUDE_vTaskDelete == 1)
void vTaskDelete(TaskHandle_t xTaskToDelete)
{
TCB_t* pxTCB;
// 进入临界区
taskENTER_CRITICAL();
{
pxTCB = prvGetTCBFromHandle(xTaskToDelete); // 如果参数为NULL,就把当前的Task删除掉
if (uxListRemove(&(pxTCB->xStateListItem) == 0) // 此处把TCB节点从链表中删除,如果删除了节点之后,链表空了
{
taskRESET_READY_PRIORITY(pxTCB->uxPriority); // 改变当前的最高优先级
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if (listList_ITEM_CONTAINER(&(pxTCB->xEventListItem) != NULL) // 事件列表节点的所属链表不为空
{
(void)uxListRemove(&(pxTCB->xEventListItem)); // 把节点从链表中删除
}
else
{
mtCOVERAGE_TEST_MARKER();
}
uxTaskNumber++; // 内核debugger用的全局变量
if (pxTCB == pxCurrentTCB)
{
vListInsertEnd(&xTasksWaitingTermination, &(pxTCB->xStateListItem)); // 把状态节点插入到待删除的列表中 等到idle任务的时候会清空内存资源
++uxDeletedTasksWaitingCleanUp; // 需要清理的资源+1, 用于idle任务的时候清空资源
portPRE_TASK_DELETE_HOOK(pxTCB, &xYieldPending); // 用户定义的钩子函数 for windows simulator
}
else // 如果要删除的不是当前的任务
{
--uxCurrentNumberOfTasks; // 当前的任务数-1
prvDeleteTCB(pxTCB); // 回收TCB的资源
prvResetNextTaskUnblockTime(); // 重置下一个非阻塞的时间
}
}
taskExit_CRITICAL();
if(xSchedulerRunning != pdFALSE) // 如果调度器正在调度
{
if(pxTCB == pxCurrentTCB)
{
portYIELD_WITHIN_API(); // 执行一次任务切换
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
#endif
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
{
TCB_t pxTCB;
taskENTER_CRITICAL();
{
// 和删除任务的函数一样
pxTCB = prvGetTCBFromHandle(xTaskToSuspend); // 如果参数为NULL,就把当前的Task删除掉
if (uxListRemove(&(pxTCB->xStateListItem) == 0) // 此处把TCB节点从链表中删除,如果删除了节点之后,链表空了
{
taskRESET_READY_PRIORITY(pxTCB->uxPriority); // 改变当前的最高优先级
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if (listList_ITEM_CONTAINER(&(pxTCB->xEventListItem) != NULL) // 查看任务是不是在等待某个事件(如信号量,队列等),如果任务还在等待某个事件的话,就将它从相应的事件列表中删除
{
(void)uxListRemove(&(pxTCB->xEventListItem)); // 把节点从链表中删除
}
else
{
mtCOVERAGE_TEST_MARKER();
}
vListInsertEnd(&xSuspendedTaskList, &(pxTCB->xEventListItem)); // 将状态节点插入到挂起列表的最后
#if (configUSE_TASK_NOTIFICATIONS == 1)
{
if (pxTCB->ucNotifyState == taskWAITING_NOTIFICATION) // 此任务已经被挂起,所以通知的状态由等待通知->不等待通知
{
pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
}
#endif
}
taskEXIT_CRITICAL();
if (xSchedulerRunning != pdFALSE)
{
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime(); // 重新计算一下还要多长时间执行下一个任务,也就是下一个任务的解锁时间。防止有任务的解锁时间参考了刚刚被挂起的那个任务
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if (pxTCB == pxCurrentTCB)
{
if (xSchedulerRunning != pdFALSE)
{
portYIELD_WITHIN_API(); // 如果正在调度,强行进行一次任务转换
}
else
{
if (listCURRENT_LIST_LENGTH(&xSuspendedTaskList) == uxCurrentNumberOfTasks)
{
pxCurrentTCB = NULL; // 如果所有的任务都挂起了
}
else
{
vTaskSwitchContext(); // 获取下一个要执行的任务
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
任务恢复函数有两个vTaskResume()和xTaskResumeFromISR(),一个是用在任务中的,一个是用在中断中的,但是处理过程都相似。具体函数分析见下面:
// Task.c
#if ( INCLUDE_vTaskSuspend == 1 )
void vTaskResume( TaskHandle_t xTaskToResume )
{
TCB_t * const pxTCB = xTaskToResume;
/* 要恢复任务的任务肯定不能是当前的任务,同时参数也不能为NULL */
if( ( pxTCB != pxCurrentTCB ) && ( pxTCB != NULL ) )
{
taskENTER_CRITICAL(); // 进入临界区
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) // 判断是不是被挂起了
{
traceTASK_RESUME( pxTCB );
/* 把状态节点从suspend的列表中移除 */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB ); // 把TCB加入到ready列表中
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
taskYIELD_IF_USING_PREEMPTION(); // 强制进行一次任务切换
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_vTaskSuspend */
调度的过程会涉及到很多的汇编语言,这些汇编语言可以参考《Cortex-M4权威指南》的第五章‘指令集’。开启任务调度的函数是vTaskStartScheduler():
void vTaskStartScheduler()
{
BaseType_t xReturn;
#if (configSUPPORT_STATIC_ALLOCTION == 1) // 静态创建空闲任务
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL; // 空闲任务TCB的buffer指针
StaticType_t *pxIdleTaskStackBuffer = NULL; // 空闲任务栈的指针
uint32_t ulIdleTaskStackSize; // 空闲任务栈的大小
vApplicationGetIdleTaskMemory(&pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize) // 应用层使用USER RAM来为Idle任务申请内存
xIdleTaskHandle = xTaskCreateStatic(prvIdleTask, // 空闲任务函数,后面会单独讲解这个函数
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
(void*)NULL,
portPRIVILEGE_BIT, // 优先级,根据是否使用MPU是有所不同的
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer);
if (xIdleTaskHandle != NULL)
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else // 动态创建空闲任务
{
xReturn = xTaskCreate(prvIdleTask, // 空闲任务函数,后面会单独讲解这个函数
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE, // 最小栈
(void*)NULL,
portPRIVILEGE_BIT, // 优先级,根据是否使用MPU是有所不同
xIdleTaskHandle);
}
#endif
#if (configUSE_TIMERS == 1)
{
if (xReturn == pdPASS)
{
xReturn = xTimerCreateTimerTask(); // 如果配置相关宏,就创建Timer
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
if (xReturn == pdPASS)
{
portDISABLE_INTERRUPTS(); // 关闭中断,之后会在SVC中断服务函数vPortSVCHandler()中会重新打开中断
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = (TickType_t)configINITIAL_TICK_COUNT; // 初始的Tickcount 应该 = 0
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); // 当宏configGENERATE_RUN_TIME_STATS为1的时候使能时间统计功能,此时需要用户实现宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器
if (xPortStartScheduler() != pdFALSE)
{
// 调度器启动成功,是不会有返回值的,不会执行到这里
}
else
{
// 只有调用了xTaskEndScheduler()函数之后,才会进入到这里
}
}
else
{
configASSERT(xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY); // 程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建空闲任务或者定时器任务的时候没有足够的内存
}
(void)xIdleTaskHandle; // 如果宏INCLUDE_xTaskGetIdleTaskHandle定义为0的话编译器就会提示xIdleTaskHandle未使用
}
调度的核心就是xPortStartScheduler(),这个函数在port.c的文件中,与MUC的架构息息相关,下面结合MCU的架构分析一下这个函数。
// FreeRTOSConfig.h
#define configKERNEL_INTERRUPT_PRIORITY 255
// port.c
#define portNVIC_PENDSV_PRI ((( uint32_t ) configKERNEL_INTERRUPT_PRIORITY) << 16UL)
#define portNVIC_SYSTICK_PRI ((( uint32_t ) configKERNEL_INTERRUPT_PRIORITY) << 24UL)
#define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20))
#define portASPEN_AND_LSPEN_BITS (0x3UL << 30UL)
#define portFPCCR ((volatile uint32_t *) 0xe000ef34)
中断优先级相关寄存器,用来改变中断的优先级 P178->《Cortex-M4权威指南》
浮点上下文控制寄存器->FPCCR, 浮点上下文控制寄存器(FPCCR)用于控制惰性压栈特性等异常处理行为 P293->《Cortex-M4权威指南》
ASPEN和LSPEN两个比特位的组合:
BaseType_t xPortStartScheduler(void)
{
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; // 设置PendSV中断为最低优先级
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; // 设置systic为最低优先级
vPortSetupTimerInterrupt(); // 配置systic中断周期,这里中断时被禁止的
uxCriticalNesting = 0; // 初始化中断嵌套计数器
prvEnableVFP(); // 使能 FPU
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; // 设置FPCCR的bit31和bit30为1,这样S0~S15和FPSCR寄存器在异常入口和退出时的状态自动保存,并且使用惰性压栈以保证中断等待
prvStartFirstTask(); // 开启第一个任务
/* Should not get here! */
return 0; // 程序不应该走到这里 除非调用end scheduler
}
使能FPU,CP11和CP10的值要求必须一致。
__asm void prvEnableVFP(void)
{
PRESERVE8 //栈的8字节对齐
ldr.w r0, =0xE000ED88 ;把0xE000ED88写入到r0 这是个地址CPACR .w强制汇编器在ARMv6T2及以上的处理器上把Thumb代码中LDR伪指令编码为32位长
ldr r1, [r0] ;将CPACR寄存器的内容给到r1
orr r1, r1, #(0xf << 20) ;r1=r1|(0xf << 20)
str r1, [r0] ;将r1的值在写会r0地址指示的内存,即将CPACR的CP10和CP11写入到
bx r14 ;跳转到链接寄存器
nop
}
为什么要有systick定时器:
systick主要有四个寄存器:
使能systick计数器后,当前值寄存器会在每个处理器时钟上升沿减小1。若计数为0的时候,它会从重加载寄存器中加载数值,并继续周而复始的工作。
// FreeRTOSConfig.h
#define configCPU_CLOCK_HZ 48000000 // CPU时钟 即系统时钟SystemCoreClock
#define configTICK_RATE_HZ (1000)
#define configUSE_TICKLESS_IDLE 1
// Port.c
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
/* 确保systic与CPU有相同的频率. */
#define portNVIC_SYSTICK_CLK_BIT (1UL << 2UL)
#else
/* The way the SysTick is clocked is not modified in case it is not the same
as the core. */
#define portNVIC_SYSTICK_CLK_BIT (0)
#endif
/* A fiddle factor to estimate the number of SysTick counts that would have
occurred while the SysTick counter is stopped during tickless idle
calculations. */
#define portMISSED_COUNTS_FACTOR (45UL)
/* The systick is a 24-bit counter. */
#define portMAX_24_BIT_NUMBER ( 0xffffffUL )
/* Constants required to manipulate the core. Registers first... */
#define portNVIC_SYSTICK_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) )
#define portNVIC_SYSTICK_CURRENT_VALUE_REG ( * ( ( volatile uint32_t * ) 0xe000e018 ) )
#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
/* ...then bits in the registers. */
#define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
#define portNVIC_SYSTICK_COUNT_FLAG_BIT ( 1UL << 16UL )
#if (configOVERRIDE_DEFAULT_TICK_CONFIGURATION == 0)
void vPortSetupTimerInterrupt(void)
{
// 计算配置systic中断所需要的常量
#if (configUSE_TICKLESS_IDLE == 1) // 定义了这个宏,在不调度,不用到systic的时候,就停止systic
{
ulTimerCountsForOneTick = (configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ); // 震荡多少次会产生中断,需要填入重载寄存器中
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
portNVIC_SYSTICK_CTRL_REG = 0UL; // 控制寄存器清零
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL; // 当前值寄存器清零
portNVIC_SYSTICK_CTRL_REG = (configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ) - 1UL; // 要求必须要-1
portNVIC_SYSTICK_CTRL_REG = (portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT); // 把控制寄存器的bit0 bit1 bit2清零
}
#endif
0xE000ED08寄存器是VTOR 向量表偏移寄存器,通过这个寄存器可以重新定义向量表,比如在STM32F767的ST官方库中就会通过函数SystemInit()来设置VTOR寄存器。向量表和向量表的重定向请参照>《Cortex-M4权威指南》-7.5向量表和向量表重定向。 不管你定义用那个向量表,VTOR里面的值始终是正在使用的向量表的地址
启动第一个任务是汇编来写的,具体代码如下:
开关中断指令
存储器屏障指令,详情参照《Cortex-M4权威指南》5.6.13章节
__asm void prvStartFirstTask()
{
PRESERVE8
ldr r0, =0xE000ED08 ;把VTOR的地址给到R1
ldr r0, [r0] ;将r0地址的内容给到r0,其实得到的就是向量表的地址
ldr r0,[r0] ;把向量表中第一个内容给到r0, 即r0得到MSP初始值
msr msp, r0 ;处理器内传送数据的指令,将数据从r0复制到msp寄存器中,即msp得到了向量表中msp的初始值,相当于复位msp
mov r0, #0 ;将r0寄存器清0
msr control, r0 ;将r0寄存器里的值复制到control特殊寄存器中,即将control清0
cpsie i ;使能中断 清除PRIMASK
cpsie f ;使能中断 清除FAULTMASK
dsb ;存储器屏障指令
isb
svc 0 ;调用SVC指令触发SVC中断,SVC也叫做请求管理调用,SVC和PendSV异常对于OS的设计来说非常重要。SVC异常由SVC指令触发
;在FreeRTOS中仅仅使用SVC异常来启动第一个任务,后面的程序就再也用不到SVC了。
nop
nop
}
上面的代码通过svc 0指令出发了SVC中断,下面会分析SVC中断函数中的内容,会面会针对SVC和PendSV做更深入的分析。
#if WORKAROUND_PMU_CM001 == 1
#define xPortPendSVHandler PendSV_Handler_Veneer
#else
#define xPortPendSVHandler PendSV_Handler
#endif
#define vPortSVCHandler SVC_Handler // 把svc_handler改名字
#define xPortSysTickHandler SysTick_Handler
__asm void vPortSVCHandler(void)
{
PRESERVE8
ldr r3, =pxCurrentTCB ;获取当前TCB的指针的地址,其实就是pxCurrentTCB这个变量的地址
ldr r1, [r3] ;将r3地址里面的内容给r1, r1获取到当前TCB的指针
ldr r0, [r1] ;TCB结构体的第一个字段就是栈顶指针,r0获取TCB的栈顶指针
ldmia r0!, {r4-r11, r14} ;r0里面现在存的是栈顶的指针,将栈中的内容恢复到r4-r11, r14中,r0中的地址最后只到了参数那个字段
msr psp, r0 ;将r0里面的数据给到psp,psp里面的值就是r0里面的值,即参数那个字段 psp = r0
isb ;存储器屏障指令
mov r0, #0 ;将r0寄存器清0
msr basepri, r0 ;将basepri清0,打开中断
bx r14 ;跳转到链接寄存器LR r14
}
执行此行代码以后硬件自动恢复寄存器R0-R3、R12、LR、PC和xPSR的值,堆栈是用进程栈PSP,然后执行寄存器PC中保存的任务函数。至此,FreeRTOS的任务调度正式开始运作。
空闲任务是FreeRTOS系统自动创建的,不需要用户手动创建。空闲任务的作用:
(1)判断系统是否有任务删除,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块内存。
(2)运行用户设置的空闲任务钩子函数。
(3)判读是否开启低功耗tickless模式,如果开启的话还需要做相应的处理