网站建设价格需要多少钱,国人在线做网站,300网站建设,网站是请网络公司制作的请问我该怎样获得并确定网站的所有权?阅读引言#xff1a; 此文将会从一个工程文件#xff0c; 一步一步的分析RTOS的任务调度实现#xff0c; 这里选用FreeRTOS分析#xff0c; 别的也差不多的#xff0c; 可能在细节上有少许不一样。 目录
1#xff0c; 常见嵌入式实时操作系统
2#xff0c; 任务调度的…
阅读引言 此文将会从一个工程文件 一步一步的分析RTOS的任务调度实现 这里选用FreeRTOS分析 别的也差不多的 可能在细节上有少许不一样。 目录
1 常见嵌入式实时操作系统
2 任务调度的本质
3 任务调度分析开始 1 常见嵌入式实时操作系统
操作系统名称特点许可类型适用领域FreeRTOS轻量级、易于配置、广泛使用开源MIT许可各种资源受限的嵌入式系统RT-Thread中文支持良好、组件丰富、高度可扩展开源Apache许可智能家居、穿戴设备等VxWorks高度可靠、网络功能强大、广泛用于军事和航空领域商业许可军事、航空、工业控制uC/OS实时性能高、源码开放、易于理解商业许可工业控制、消费电子QNX安全性高、支持多核、广泛用于汽车电子商业许可汽车电子、医疗设备Zephyr开源、模块化设计、适用于物联网设备开源Apache许可物联网设备mbed OS专为物联网设计、支持多种硬件平台开源Apache许可物联网、智能硬件CMSIS-RTOSARM Cortex微控制器的官方RTOS标准标准ARM Cortex微控制器INTEGRITY确定性性能、广泛用于汽车和航空商业许可汽车、航空、航天ThreadX实时性能高、内存使用效率高、可扩展商业许可工业控制、消费电子 2 任务调度的本质
cpu每隔一定时间执行不同函数的指令 或者更底层一点的说法是完成不同指令段的切换 让cpu分别执行不同指令段的内容 并且cpu在切换不同指令段执行的时候 之前执行的内容 需要被保存 不可能有从头开始把 假设cpu在执行任务1 现在被更高优先级的任务2抢占了 这个时候就需要将任务1的相关内容保存到任务1自己的堆栈里面去。具体保存啥呢 线程是函数吗函数需要保存吗函数在Flash上不会被破坏无需保存 这段内存区域是只读的 函数执行到了哪里需要保存吗需要保存 上下文信息 函数里用到了全局变量全局变量需要保存吗全局变量在内存上还能保存到哪里去全局变量无需保存 函数里用到了局部变量局部变量需要保存吗局部变量在栈里也是在内存里只要避免栈不被破坏即可局部变量无需保存 运算的中间值需要保存吗中间值保存在哪里在CPU寄存器里另一个线程也要用到CPU寄存器所以CPU寄存器需要保存 函数运行了哪里它也是一个CPU寄存器名为PC 汇总CPU寄存器需要保存 保存在哪里保存在线程的栈里 怎么理解CPU寄存器、怎么理解栈
在裸机编程中 不同的函数使用的是同一个栈空间 而装上操作系统之后 不同的任务(线程)之间的栈是独立开的 互不影响 起始函数和线程是很类似的 只是说进程和线程都是针对操作系统提出来的概念。 接下来 让我们看看RTOS是如何实现上下文切换的。 3 任务调度分析开始 接下来 我会从FreeRTOS的一个工程 从启动代码开始 到创建任务 去分析RTOS切换任务是如何实现的。 启动代码就不看了 相信大家都是有基础的 不会的也可以先去看看我之前的文章。前面没啥好说的 直接去freertos_demo函数中看看 在这里呢 创建了一个开始任务 接着就启动了任务调度器。
那咱们先看看创建任务是如何创建的吧 #if ( configSUPPORT_DYNAMIC_ALLOCATION 1 )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;/* If the stack grows down then allocate the stack then the TCB so the stack* does not grow into the TCB. Likewise if the stack grows up then allocate* the TCB then the stack. */#if ( portSTACK_GROWTH 0 ){/* 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 ) );if( pxNewTCB ! NULL ){/* Allocate space for the stack used by the task being created.* The base of the stack memory stored in the TCB so the task can* be deleted later if required. */pxNewTCB-pxStack ( StackType_t * ) pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */if( pxNewTCB-pxStack NULL ){/* Could not allocate the stack. Delete the allocated TCB. */vPortFree( pxNewTCB );pxNewTCB NULL;}}}#else /* portSTACK_GROWTH */{StackType_t * pxStack;/* Allocate space for the stack used by the task being created. */pxStack pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCUs stack and this allocation is the stack. */if( pxStack ! NULL ){/* Allocate space for the TCB. */pxNewTCB ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e9087 !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCUs stack, and the first member of TCB_t is always a pointer to the tasks stack. */if( pxNewTCB ! NULL ){/* Store the stack location in the TCB. */pxNewTCB-pxStack pxStack;}else{/* The stack cannot be used as the TCB was not created. Free* it again. */vPortFreeStack( pxStack );}}else{pxNewTCB NULL;}}#endif /* portSTACK_GROWTH */if( pxNewTCB ! NULL ){#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE ! 0 ) /*lint !e9029 !e731 Macro has been consolidated for readability reasons. */{/* Tasks can be created statically or dynamically, so note this* task was created dynamically in case it is later deleted. */pxNewTCB-ucStaticallyAllocated tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;}#endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );prvAddNewTaskToReadyList( pxNewTCB );xReturn pdPASS;}else{xReturn errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;}return xReturn;}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
/*-----------------------------------------------------------*/
动态创建任务的函数还是非常简单的 简单概述一下 为任务控制块和任务的栈空间分配内存空间 接着假设cpu执行到该任务的的入口指令的位置被打断 将此时CPU寄存器信息保存到任务栈中 接着将此任务挂载到就绪链表中去。 我们看一个东西 ARM架构的保护现场的内存布局图 也就是说cpu的寄存器保存到内容的时候 布局是这样的 接下来就该说说这个函数了函数v TaskStartScheduler 用于启动任务调度器任务调度器启动后FreeRTOS 便会开始进行任务调度除非调用函数x TaskEndScheduler() 停止任务调度器否则不会再返回。函数vTaskStartScheduler 的代码如下所示
void vTaskStartScheduler( void )
{BaseType_t xReturn;/* Add the idle task at the lowest priority. */#if ( configSUPPORT_STATIC_ALLOCATION 1 ){StaticTask_t * pxIdleTaskTCBBuffer NULL;StackType_t * pxIdleTaskStackBuffer NULL;uint32_t ulIdleTaskStackSize;/* The Idle task is created using user provided RAM - obtain the* address of the RAM then create the idle task. */vApplicationGetIdleTaskMemory( pxIdleTaskTCBBuffer, pxIdleTaskStackBuffer, ulIdleTaskStackSize );xIdleTaskHandle xTaskCreateStatic( prvIdleTask,configIDLE_TASK_NAME,ulIdleTaskStackSize,( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */if( xIdleTaskHandle ! NULL ){xReturn pdPASS;}else{xReturn pdFAIL;}}#else /* if ( configSUPPORT_STATIC_ALLOCATION 1 ) */{/* The Idle task is being created using dynamically allocated RAM. */xReturn xTaskCreate( prvIdleTask,configIDLE_TASK_NAME,configMINIMAL_STACK_SIZE,( void * ) NULL,portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */}#endif /* configSUPPORT_STATIC_ALLOCATION */#if ( configUSE_TIMERS 1 ){if( xReturn pdPASS ){xReturn xTimerCreateTimerTask();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_TIMERS */if( xReturn pdPASS ){/* freertos_tasks_c_additions_init() should only be called if the user* definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is* the only macro called by the function. */#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT{freertos_tasks_c_additions_init();}#endif/* Interrupts are turned off here, to ensure a tick does not occur* before or during the call to xPortStartScheduler(). The stacks of* the created tasks contain a status word with interrupts switched on* so interrupts will automatically get re-enabled when the first task* starts to run. */portDISABLE_INTERRUPTS();#if ( configUSE_NEWLIB_REENTRANT 1 ){/* Switch Newlibs _impure_ptr variable to point to the _reent* structure specific to the task that will run first.* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html* for additional information. */_impure_ptr ( pxCurrentTCB-xNewLib_reent );}#endif /* configUSE_NEWLIB_REENTRANT */xNextTaskUnblockTime portMAX_DELAY;xSchedulerRunning pdTRUE;xTickCount ( TickType_t ) configINITIAL_TICK_COUNT;/* If configGENERATE_RUN_TIME_STATS is defined then the following* macro must be defined to configure the timer/counter used to generate* the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS* is set to 0 and the following line fails to build then ensure you do not* have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your* FreeRTOSConfig.h file. */portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();traceTASK_SWITCHED_IN();/* Setting up the timer tick is hardware specific and thus in the* portable interface. */if( xPortStartScheduler() ! pdFALSE ){/* Should not reach here as if the scheduler is running the* function will not return. */}else{/* Should only reach here if a task calls xTaskEndScheduler(). */}}else{/* This line will only be reached if the kernel could not be started,* because there was not enough FreeRTOS heap to create the idle task* or the timer task. */configASSERT( xReturn ! errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );}/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,* meaning xIdleTaskHandle is not used anywhere else. */( void ) xIdleTaskHandle;/* OpenOCD makes use of uxTopUsedPriority for thread debugging. Prevent uxTopUsedPriority* from getting optimized out as it is no longer used by the kernel. */( void ) uxTopUsedPriority;
}
/*-----------------------------------------------------------*/
从上面的代码可以看出函数v TaskStartScheduler() 主要做了六件事情。 1. 创建空闲任务根据是否支持静态内存管理使用静态方式或动态方式创建空闲任务。 2. 创建定时器服务任务创建定时器服务任务需要配置启用软件定时器创建定时器服务任务同样是根据是否配置支持静态内存管理使用静态或动态方式创建定时器服务任务。
3. 关闭中断使用portDISABLE_INTERRUPT() 关闭中断这种方式只关闭受FreeRTOS 管理的中断。关闭中断主要是为了防止SysTick 中断在任务调度器开启之前或过程中产生中断。FreeRTOS 会在开始运行第一个任务时重新打开中断。 4. 初始化一些全局变量并将任务调度器的运行标志设置为已运行。 5. 初始化任务运行时间统计功能的时基定时器任务运行时间统计功能需要一个硬件定时器提供高精度的计数这个硬件定时器就在这里进行配置如果配置不启用任务运行时间统计功能的就无需进行这项硬件定时器的配置。 6. 最后就是调用函数xPortStartScheduler() 。 所以 现在需要了解的函数就是xPortStartScheduler() 走 看看去 函数xPortStartScheduler() 完成启动任务调度器中与硬件架构相关的配置部分以及启动第一个任务具体的代码如下所示
/** See header file for description.*/
BaseType_t xPortStartScheduler( void )
{#if ( configASSERT_DEFINED 1 ){volatile uint32_t ulOriginalPriority;volatile uint8_t * const pucFirstUserPriorityRegister ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 portFIRST_USER_INTERRUPT_NUMBER );volatile uint8_t ucMaxPriorityValue;/* Determine the maximum priority from which ISR safe FreeRTOS API* functions can be called. ISR safe functions are those that end in* FromISR. FreeRTOS maintains separate thread and ISR API functions to* ensure interrupt entry is as fast and simple as possible.** Save the interrupt priority value that is about to be clobbered. */ulOriginalPriority *pucFirstUserPriorityRegister;/* Determine the number of priority bits available. First write to all* possible bits. */*pucFirstUserPriorityRegister portMAX_8_BIT_VALUE;/* Read the value back to see how many bits stuck. */ucMaxPriorityValue *pucFirstUserPriorityRegister;/* The kernel interrupt priority should be set to the lowest* priority. */configASSERT( ucMaxPriorityValue ( configKERNEL_INTERRUPT_PRIORITY ucMaxPriorityValue ) );/* Use the same mask on the maximum system call priority. */ucMaxSysCallPriority configMAX_SYSCALL_INTERRUPT_PRIORITY ucMaxPriorityValue;/* Calculate the maximum acceptable priority group value for the number* of bits read back. */ulMaxPRIGROUPValue portMAX_PRIGROUP_BITS;while( ( ucMaxPriorityValue portTOP_BIT_OF_BYTE ) portTOP_BIT_OF_BYTE ){ulMaxPRIGROUPValue--;ucMaxPriorityValue ( uint8_t ) 0x01;}#ifdef __NVIC_PRIO_BITS{/* Check the CMSIS configuration that defines the number of* priority bits matches the number of priority bits actually queried* from the hardware. */configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) __NVIC_PRIO_BITS );}#endif#ifdef configPRIO_BITS{/* Check the FreeRTOS configuration that defines the number of* priority bits matches the number of priority bits actually queried* from the hardware. */configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) configPRIO_BITS );}#endif/* Shift the priority group value back to its position within the AIRCR* register. */ulMaxPRIGROUPValue portPRIGROUP_SHIFT;ulMaxPRIGROUPValue portPRIORITY_GROUP_MASK;/* Restore the clobbered interrupt priority register to its original* value. */*pucFirstUserPriorityRegister ulOriginalPriority;}#endif /* configASSERT_DEFINED *//* Make PendSV and SysTick the lowest priority interrupts. */portNVIC_SHPR3_REG | portNVIC_PENDSV_PRI;portNVIC_SHPR3_REG | portNVIC_SYSTICK_PRI;/* Start the timer that generates the tick ISR. Interrupts are disabled* here already. */vPortSetupTimerInterrupt();/* Initialise the critical nesting count ready for the first task. */uxCriticalNesting 0;/* Start the first task. */prvStartFirstTask();/* Should not get here! */return 0;
}
/*-----------------------------------------------------------*/ 函数xPort StartScheduler() 的解析如下所示 1. 在启用断言的情况下函数xPortStartScheduler 会检测用户在FreeRTOS Config.h 文件中对中断的相关配置是否有误感兴趣的读者请自行查看这部分的相关代码。 2.配置PendSV 和SysTick 的中断优先级为最低优先级 3. 调用函数v PortSetupTimerInterrupt() 配置SysTickSysTick函数vPortSetupTimerInterrupt 首先会将SysTick 当前计数值清空并根据FreeRTOSConfig .h 文件中配置的configSYSTICK_CLOCK_HZ HZSysTick 时钟源频率和configTICK_RATE_HZ HZ系统时钟节拍频率计算并设置SysTick 的重装载值然后启动SysTick 计数和中断。 4.初始化临界区嵌套计数器为0 。 5. 调用函数prvEnableVFP 使能FPUFPU因为A RM Cortex M3 内核MCU 无FPUFPU此函数仅在ARM Cortex M4/M7 内核MCU 平台上被调用执行改函数后FPU 被开启。 6. 接下来将FPCCR 寄存器的 31:30 置11这样在进出异常时FPU 的相关寄存器就会自动地保存和恢复同样地因为A RM Cortex M3 内核MCU 无FPUFPU此当代码仅在A RM Cortex M4/M7 内核MCU 平台上被调用。 7. 调用函数prvStartFirstTask() 启动第一个任务。 又去看看prvStartFirstTask()函数的内容] 函数prvStartFirstTask() 用于初始化启动第一个任务前的环境主要是重新设置MSP 指针并使能全局中断具体的代码如下所示这里以正点原子的STM 32 F 1 系列开发板为例其他类型的开发板类似
__asm void prvStartFirstTask( void )
{
/* *INDENT-OFF* */PRESERVE8/* Use the NVIC offset register to locate the stack. */ldr r0, 0xE000ED08ldr r0, [ r0 ]ldr r0, [ r0 ]/* Set the msp back to the start of the stack. */msr msp, r0/* Globally enable interrupts. */cpsie icpsie fdsbisb/* Call SVC to start the first task. */svc 0nopnop
/* *INDENT-ON* */
}
/*-----------------------------------------------------------*/
从上面的代码可以看出函数p rvStartFirstTask() 是一段汇编代码解析如下所示 1. 首先是使用了PRESERVE8进行8 字节对齐这是因为栈在任何时候都是需要4 字节对齐的而在调用入口得8 字节对齐在进行C 编程的时候编译器会自动完成的对齐的操作而对于汇编就需要开发者手动进行对齐。 2. 接下来的三行代码是为了获得MSP 指针的初始值那么这里就能够引出两个问题 (1) 什么是MSP 指针 程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时MCU 会自动更新SP 指针使SP 指针指向最后一个入栈的元素那么程序就可以根据SP 指针来从栈中存取信息。对于正点原子的STM 32 F 1 、STM 32 F 4 、STM 32 F 7 和STM 32 H 7 开发板上使用的ARM Cortex M 的MCU 内核来说ARM Cortex M 提供了两个栈空间这两个栈空间的堆栈指针分别是MSP主堆栈指针和PSP进程堆栈指针。在FreeRTOS 中MSP 是给系统栈空间使用的而PSP 是给任务栈使用的也就是说FreeRTOS 任务的栈空间是通过PSP 指向的而在进入中断服务函数时则是使用MSP 指针。当使用不同的堆栈指针时SP 会等于当前使用的堆栈指针。 (2) 为什么是0xE000ED08 0xE000ED08 是VTORVTOR向量表偏移寄存器的地址VTOR 中保存了向量表的偏移地址。一般来说向量表是从其实地址0 x00000000 开始的但是在有情况下可能需要修改或重定向向量表的首地址因此ARM Corten M 提供了VTOR 对向量表进行从定向。而向量表是用来保存中断异常的入口函数地址即栈顶地址的并且向量表中的第一个字保存的就是栈底的地址在start_stm32xxxxxx.s 文件中有如下定义 以上就是向量表只列出前几个的部分内容可以看到向量表的第一个元素就是栈指针的初始值也就是栈底指针。 在了解了这两个问题之后接下来再来看看代码。首先是获取VTOR 的地址接着获取VTOR 的值也就是获取向量表的首地址最后获取向量表中第一个字的数据也就是栈底指针了。
3. 在获取了栈顶指针后将MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序之前保存在栈中的数据因为FreeRTOS 从开启任务调度器到启动第一个任务都是不会返回的是一条不归路因此将栈中的数据丢弃也不会有影响。 4. 重新赋值MSP 后接下来就重新使能全局中断因为之前在函数vTaskStartScheduler 中关闭了受FreeRTOS 的中断。 5. 最后使用SVC 指令并传入系统调用号00触发SVC 中断。 在第一个任务中调用了svc指令 必然会触发svc异常服务 自然的就会调用到svc的服务函数
__asm void vPortSVCHandler( void )
{
/* *INDENT-OFF* */PRESERVE8ldr r3, pxCurrentTCB /* Restore the context. */ldr r1, [ r3 ] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */ldmia r0 !, { r4 - r11 } /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */msr psp, r0 /* Restore the task stack pointer. */isbmov r0, # 0msr basepri, r0orr r14, # 0xdbx r14
/* *INDENT-ON* */
}
从上面代码中可以看出函数v PortSVCHandler() 就是用来跳转到第一个任务函数中去的该函数的具体解析如下 1. 首先通过p xCurrentTCB 获取优先级最高的就绪态任务的任务栈地址优先级最高的就绪态任务就是系统将要运行的任务。p xCurrentTCB 是一个全局变量用于指向系统中优先级最高的就绪态任务的任务控制块在前面创建s tart_task 任务、空闲任务、定时器处理任务时自动根据任务的优先级高低进行赋值的具体的赋值过程在后续分析任务创建函数时会具体分析。 这里举个例子在《FreeRTOS 移植实验》中start_task 任务、空闲任务、定时器处理任务的优先级如下表所示 说了这么多FreeRTOS 对于进入中断后r 14 为EXC_RETURN 的具体应用就是通过判断EXC_RETURN 的bit 4 是否为00来判断任务是否使用了浮点单元。 最后通过bx r14 指令跳转到任务的任务函数中执行执行此指令CPU 会自动从PSP 指向的栈中出栈R 0 、R1 、R2 、R3 、R12 、LR 、PC 、xPSR 寄存器并且如果EXC_RETURN 的b it4 为00使用了浮点单元那么CPU 还会自动恢复浮点寄存器。 好 到此FreeRTOS的第一个任务就算是启动完成了 那后来的任务又是如何进行切换的呢Systick滴答定时器 每隔一个时间片 就会触发一次中断 在其服务函数里面进行的任务切换 具体请接着往下看。
PendSv异常
PendSVPended Service Call可挂起服务调用是一个对RTOS 非常重要的异常。PendSV 的中断优先级是可以编程的用户可以根据实际的需求对其进行配置。PendSV 的中断由将中 断控制状态寄存器ICSR中PENDSVSET 为置一触发中断控制状态寄存器的有关内容请 查看4.1.5 小节《中断控制状态寄存器》。PendSV 与SVC 不同PendSV 的中断是非实时的 即PendSV 的中断可以在更高优先级的中断中触发但是在更高优先级中断结束后才执行。 利用PendSV 的这个可挂起特性在设计RTOS 时可以将PendSV 的中断优先级设置为 最低的中断优先级FreeRTOS 就是这么做的更详细的内容请查看4.3.1 小节《PendSV 和 SysTick 中断优先级》这么一来PendSV 的中断服务函数就会在其他所有中断处理完成后才 执行。任务切换时就需要用到PendSV 的这个特性。 首先来看一下任务切换的一些基本概念在典型的RTOS 中任务的处理时间被分为多 个时间片OS 内核的执行可以有两种触发方式一种是通过在应用任务中通过SVC 指令触发 例如在应用任务在等待某个时间发生而需要停止的时候那么就可以通过SVC 指令来触发OS 内核的执行以切换到其他任务第二种方式是SysTick 周期性的中断来触发OS 内核的执 行。下图演示了只有两个任务的RTOS 中两个任务交替执行的过程 在操作系统中任务调度器决定是否切换任务。图中的任务及切换都是在SysTick 中 断中完成的SysTick 的每一次中断都会切换到其他任务。如果一个中断请求IRQ在SysTick 中断产生之前产生那么SysTick 就可能抢占该中断 请求这就会导致该中断请求被延迟处理这在实时操作系统中是不允许的因为这将会影响 到实时操作系统的实时性如下图所示 并且当SysTick 完成任务的上下文切换准备返回任务中运行时由于存在中断请求 ARM Cortex-M 不允许返回线程模式因此将会产生用法错误异常Usage Fault。 在一些RTOS 的设计中会通过判断是否存在中断请求来决定是否进行任务切换。虽然 可以通过检查xPSR 或NVIC 中的中断活跃寄存器来判断是否存在中断请求但是这样可能会 影响系统的性能甚至可能出现中断源在SysTick 中断前后不断产生中断请求导致系统无法 进行任务切换的情况。 PendSV 通过延迟执行任务切换直到处理完所有的中断请求以解决上述问题。为了达到 这样的效果必须将PendSV 的中断优先级设置为最低的中断优先等级。如果操作系统决定切 换任务那么就将PendSV 设置为挂起状态并在PendSV 的中断服务函数中执行任务切换如 下图所示 1. 任务一触发SVC 中断以进行任务切换例如任务一正等待某个事件发生。 2. 系统内核接收到任务切换请求开始准备任务切换并挂起PendSV 异常。 3. 当退出SVC 中断的时候立刻进入PendSV 异常处理完成任务切换。 4. 当PendSV 异常处理完成返回线程模式开始执行任务二。
5.中断产生并进入中断处理函数。 6. 当运行中断处理函数的时候SysTick 异常用于内核时钟节拍产生。 7. 操作系统执行必要的操作然后挂起PendSV 异常准备进行任务切换。 8.当SysTick 中断处理完成返回继续处理中断。 9. 当中断处理完成立马进入PendSV 异常处理完成任务切换。 10. 当PendSV 异常处理完成返回线程模式继续执行任务一。PendSV在RTOS 的任务切换中起着至关重要的作用FreeRTOS 的任务切换就是在PendSV中完成的。 PendSv中断服务函数
FreeRTOS在PendSV 的中断中完成任务切换PendSV 的中断服务函数由FreeRTOS 编写将PendSV 的中断服务函数定义成函数x PortPendSVHandler() 。针对ARM Cortex M3 和针对ARM Cortex M4 和ARM Cortex M7 内核的函数xPortPendSVHandler 稍有不同其主要原因在于ARM Cortex M4 和ARM Cortex M7 内核具有浮点单元因此在进行任务切换的时候还需考虑是否保护和恢复浮点寄存器的值。针对ARM Cortex M3 内核的函数xPortPendSVHandler ()()具体的代码如下所示
__asm void xPortPendSVHandler( void )
{extern uxCriticalNesting;extern pxCurrentTCB;extern vTaskSwitchContext;/* *INDENT-OFF* */PRESERVE8mrs r0, pspisbldr r3, pxCurrentTCB /* Get the location of the current TCB. */ldr r2, [ r3 ]stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */stmdb sp !, { r3, r14 }mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITYmsr basepri, r0dsbisbbl vTaskSwitchContextmov r0, #0msr basepri, r0ldmia sp !, { r3, r14 }ldr r1, [ r3 ]ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */msr psp, r0isbbx r14nop
/* *INDENT-ON* */
}
/*-----------------------------------------------------------*/
从上面的代码可以看出FreeRTOS 在进行任务切换的时候会将CPU 的运行状态在当前任务在进行任务切换前进行保存保存到任务的任务栈中然后从切换后运行任务的任务栈中恢复切换后运行任务在上一次被切换时保存的CPU 信息。 但是从PendSV 的中断回调函数代码中只看到程序保存和恢复的CPU 信息中的部分寄存器信息R4 寄存器~R11 寄存器这是因为硬件会自动出栈和入栈其他CPU 寄存器的信息。 在任务运行的时候CPU 使用PSP 作为栈空间使用也就是使用运行任务的任务栈。当SysTick 中断SysTick 的中断服务函数会判断是否需要进行任务切换相关内容在后续章节会进行讲解发生时在跳转到SysTick 中断服务函数运行前硬件会自动将除R 4~ R 11 寄存器的其他CPU 寄存器入栈因此就将任务切换前CPU 的部分信息保存到对应任务的任务栈中。当退出PendSV 时会自动从栈空间中恢复这部分CPU 信息以共任务正常运行。 因此在PendSV 中断服务函数中主要要做的事情就是保存硬件不会自动入栈的CPU 信息已经确定写一个要运行的任务并将pxCurrentTCB 指向该任务的任务控制块然后更新PSP 指针为该任务的任务堆栈指针。 FreeRTOS 确定下一个要运行的任务
从上面可以看到在PendSV 的中断服务函数中调用了函数vTaskSwitchContext来确定写一个要运行的任务。 函数vTaskSwitchContext 调用了函数taskSELECT_HIGHEST_PRIORITY_TASK ()()来将p xCurrentTCB 设置为指向优先级最高的就绪态任务。 函数taskSELECT_HIGHEST_PRIORITY_TASK 函数taskSELECT_HIGHEST_PRIORITY_TASK 用于将pcCurrentTCB 设置为优先级最高的就绪态任务因此该函数会使用位图的方式在任务优先级记录中查找优先级最高任务优先等级然后根据这个优先等级到对应的就绪态任务列表在中取任务。 FreeRTOS提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式一种是由纯C 代码实现的这种方式适用于所有运行FreeRTOS 的MCU另外一种方式则是使用了硬件计算前导零的指令因此这种方式并不适用于所有运行FreeRTOS 的MCU而仅适用于具有有相应硬件指令的MCU 。正点原子所有板卡所使用的STM 32 MCU 都支持以上两种方式。具体使用哪种方式用户可以在FreeRTOSConfig.h 文件中进行配置配置方法请查看第三章《FreeRTOS 系统配置》的相关章节。软件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK 是一个宏定义在task .c文件中由定义具体的代码如下所示 依靠特定硬件指令实现的函数taskSELECT_HIGHEST_PRIORITY_TASK 是一个宏定义在t ask.c 文件中有定义具体的代码如下所示 在使用硬件方式实现的函数taskSELECT_HIGHEST_PRIORITY_TASK 中调用了函数portGET_HIGHEST_PRIORITY 来计算任务优先级记录中的最高任务优先级函数portGET_HIGHEST_PRIORITY 实际上是一个宏定义在p ortmacro.h 文件中有定义具体的代码如下所示 PendSV 异常何时触发
PendSV异常用于进行任务切换当需要进行任务切换的时候FreeRTOS 就会触发PendSV 异常以进行任务切换。 FreeRTOS提供了多个用于触发任务切换的宏如下所示 从上面的代码中可以看到这些后实际上最终都是调用了函数portYIELD()函数实际上是一个宏定义在portmacro.h 文件中有定于具体的代码如下所示 上面代码中宏portNVIC_INT_CTRL_REG 和宏p ortNVIC_PENDSVSET_BIT 在p ortmacro.h 文件中有定义具体的代码如下所示 在systick的服务函数里面判断的 判断需要切换任务 往中断状态控制寄存器中写入特定值 触发PendSv中断。
void xPortSysTickHandler( void )
{/* The SysTick runs at the lowest interrupt priority, so when this interrupt* executes all interrupts must be unmasked. There is therefore no need to* save and then restore the interrupt mask value as its value is already* known - therefore the slightly faster vPortRaiseBASEPRI() function is used* in place of portSET_INTERRUPT_MASK_FROM_ISR(). */vPortRaiseBASEPRI();{/* Increment the RTOS tick. */if( xTaskIncrementTick() ! pdFALSE ){/* A context switch is required. Context switching is performed in* the PendSV interrupt. Pend the PendSV interrupt. */portNVIC_INT_CTRL_REG portNVIC_PENDSVSET_BIT;}}vPortClearBASEPRIFromISR();
}
/*-----------------------------------------------------------*/ 既然FreeRTOS 的系统时钟节拍来自SysTick那么FreeRTOS 系统时钟节拍的处理自然就是在SysTick 的中断服务函数中完成的。在前面第2 .1.3 小节《修改SYSTEM 文件》中修改了SysTick 的中断服务函数SysTick 的中断服务函数定义在d elay.c 文件中具体的代码如下所示 从上面的代码可以看出函数xPortSysTickHandler 调用了函数xTaskIncrementTick() 来处理系统时钟节拍。在调用函数x TaskIncrementTick() 前后分别屏蔽了受FreeRTOS 管理的中断和取消中断屏蔽这是因为SysTick 的中断优先级设置为最低的中断优先等级在SysTick 的中断中处理FreeRTOS 的系统时钟节拍时并不希望收到其他中断的影响。在通过函数xTaskIncrementTick( 处理完系统时钟节拍和相关事务后再根据函数xTaskIncrementTick 的返回值决定是否进行任务切换如果进行任务切换就触发PendSV 异常在本次SysTick 中断及其他中断处理完成后就会进入PendSV 的中断服务函数进行任务切换使用PendSV 进行切换任务的相关内容请查看第九章《FreeRTOS 任务切换》。 接下来分析函数xTaskIncrementTick 是如何处理系统时钟节拍及相关事务的函数x TaskIncrementTick() 在t ask.c 文件中有定义具体的代码如下所示
BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB;TickType_t xItemValue;BaseType_t xSwitchRequired pdFALSE;/* Called by the portable layer each time a tick interrupt occurs.* Increments the tick then checks to see if the new tick value will cause any* tasks to be unblocked. */traceTASK_INCREMENT_TICK( xTickCount );if( uxSchedulerSuspended ( UBaseType_t ) pdFALSE ){/* Minor optimisation. The tick count cannot change in this* block. */const TickType_t xConstTickCount xTickCount ( TickType_t ) 1;/* Increment the RTOS tick, switching the delayed and overflowed* delayed lists if it wraps to 0. */xTickCount xConstTickCount;if( xConstTickCount ( TickType_t ) 0U ) /*lint !e774 if does not always evaluate to false as it is looking for an overflow. */{taskSWITCH_DELAYED_LISTS();}else{mtCOVERAGE_TEST_MARKER();}/* See if this tick has made a timeout expire. Tasks are stored in* the queue in the order of their wake time - meaning once one task* has been found whose block time has not expired there is no need to* look any further down the list. */if( xConstTickCount xNextTaskUnblockTime ){for( ; ; ){if( listLIST_IS_EMPTY( pxDelayedTaskList ) ! pdFALSE ){/* The delayed list is empty. Set xNextTaskUnblockTime* to the maximum possible value so it is extremely* unlikely that the* if( xTickCount xNextTaskUnblockTime ) test will pass* next time through. */xNextTaskUnblockTime portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */break;}else{/* The delayed list is not empty, get the value of the* item at the head of the delayed list. This is the time* at which the task at the head of the delayed list must* be removed from the Blocked state. */pxTCB listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */xItemValue listGET_LIST_ITEM_VALUE( ( pxTCB-xStateListItem ) );if( xConstTickCount xItemValue ){/* It is not time to unblock this item yet, but the* item value is the time at which the task at the head* of the blocked list must be removed from the Blocked* state - so record the item value in* xNextTaskUnblockTime. */xNextTaskUnblockTime xItemValue;break; /*lint !e9011 Code structure here is deemed easier to understand with multiple breaks. */}else{mtCOVERAGE_TEST_MARKER();}/* It is time to remove the item from the Blocked state. */listREMOVE_ITEM( ( pxTCB-xStateListItem ) );/* Is the task waiting on an event also? If so remove* it from the event list. */if( listLIST_ITEM_CONTAINER( ( pxTCB-xEventListItem ) ) ! NULL ){listREMOVE_ITEM( ( pxTCB-xEventListItem ) );}else{mtCOVERAGE_TEST_MARKER();}/* Place the unblocked task into the appropriate ready* list. */prvAddTaskToReadyList( pxTCB );/* A task being unblocked cannot cause an immediate* context switch if preemption is turned off. */#if ( configUSE_PREEMPTION 1 ){/* Preemption is on, but a context switch should* only be performed if the unblocked task has a* priority that is equal to or higher than the* currently executing task. */if( pxTCB-uxPriority pxCurrentTCB-uxPriority ){xSwitchRequired pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_PREEMPTION */}}}/* Tasks of equal priority to the currently running task will share* processing time (time slice) if preemption is on, and the application* writer has not explicitly turned time slicing off. */#if ( ( configUSE_PREEMPTION 1 ) ( configUSE_TIME_SLICING 1 ) ){if( listCURRENT_LIST_LENGTH( ( pxReadyTasksLists[ pxCurrentTCB-uxPriority ] ) ) ( UBaseType_t ) 1 ){xSwitchRequired pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* ( ( configUSE_PREEMPTION 1 ) ( configUSE_TIME_SLICING 1 ) ) */#if ( configUSE_TICK_HOOK 1 ){/* Guard against the tick hook being called when the pended tick* count is being unwound (when the scheduler is being unlocked). */if( xPendedTicks ( TickType_t ) 0 ){vApplicationTickHook();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_TICK_HOOK */#if ( configUSE_PREEMPTION 1 ){if( xYieldPending ! pdFALSE ){xSwitchRequired pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_PREEMPTION */}else{xPendedTicks;/* The tick hook gets called at regular intervals, even if the* scheduler is locked. */#if ( configUSE_TICK_HOOK 1 ){vApplicationTickHook();}#endif}return xSwitchRequired;
}
/*-----------------------------------------------------------*/从上面的代码可以看到函数xTaskIncrementTick 处理了系统时钟节拍、阻塞任务列表、时间片调度等。 处理系统时钟节拍就是在每次SysTick 中断发生的时候将全局变量xTickCount 的值加11也就是将系统时钟节拍计数器的值加1 。 处理阻塞任务列表就是判断阻塞态任务列表中是否有阻塞任务超时如果有就将阻塞时间超时的阻塞态任务移到就绪态任务列表中准备执行。同时在系统时钟节拍计数器xTickCount 的加1 溢出后将两个阻塞态任务列表调换这是FreeRTOS 处理系统时钟节拍计数器溢出的一种机制。 处理时间片调度就是在每次系统时钟节拍加1 后切换到另外一个同等优先级的任务中运行要注意的是此函数只是做了需要进行任务切换的标记在函数退出后会统一进行任务切换因此时间片调度导致的任务切换也可能因为有更高优先级的阻塞任务就绪导致任务切换而出现任务切换后运行的任务比任务切换前运行任务的优先级高而非相等优先级。 到此 整个RTOS的任务调度的原理大致就讲完的 希望对有需要的人有所帮助。