FreeRTOS任务源码分析以及程序堆栈与任务堆栈的关系
之前的文章学习了ARM函数调用和返回时的操作,但是对于操作系统下的任务堆栈以及任务切换时堆栈的切换还不太了解,因此,首先分析了一下任务的源码,包括创建任务时,创建堆栈的过程,以及任务调度过程。后来,发现这个分析清楚了,就可以把程序堆栈和任务堆栈也梳理清楚,于是,就继续梳理一下程序堆栈和任务堆栈的关系。
以STM32F4x7_ETH_LwIP_V1.1.1工程为例,使用的版本是FreeRTOSV7.3.0。
STM32F4x7_ETH_LwIP_V1.1.1\Project\FreeRTOS\udptcp_echo_server_netconn\src\main.c中启动任务如下
1 int main(void) 2 { 3 /* Configures the priority grouping: 4 bits pre-emption priority */ 4 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); 5 6 /* Init task */ 7 xTaskCreate(Main_task, (int8_t *)"Main", configMINIMAL_STACK_SIZE * 2, NULL,MAIN_TASK_PRIO, NULL); 8 9 /* Start scheduler */ 10 vTaskStartScheduler(); 11 12 /* We should never get here as control is now taken by the scheduler */ 13 for( ;; ); 14 }
在main中一般都会启动一个主任务或者叫启动任务,然后,开始任务调度,在主任务中,完成其它任务的创建。(为什么要这种模式呢?直接在main中创建所有任务,然后,开始任务调度不可以吗?)
任务控制块TCB,首个成员是任务堆栈顶部地址,第17行表示任务堆栈起始(堆栈像一个桶,桶底是高地址,桶上面是低地址,桶底部为“任务堆栈起始”pxStack,桶里的最后一个数据位置为“任务堆栈顶部地址”pxTopOfStack)。
xGenericListItem是用于将任务串成列表的列表成员,后续该任务加入就绪任务列表还是其他任务列表,都是将该列表成员插入进任务列表。
xEventListItem用于记录该任务是否在等待事件,比如是否向队列发送数据但队列已满、是否从队列读取数据但队列是空的,且设置了等待时间或无限等待。例如,若是向队列发送数据但队列已满,则该任务的xEventListItem会插入该队列的xTasksWaitingToSend列表中;同时将xGenericListItem从就绪任务列表删除,插入到挂起任务队列(若等待时间是无限)或延时任务队列(若等待时间是有限)(该过程由vTaskPlaceOnEventList完成)。若是队列非满了,则会将任务的xEventListItem从xTasksWaitingToSend中移除;同时,将任务的xGenericListItem从挂起任务队列或延时任务队列中移除,并添加到就绪队列中(该过程由xTaskRemoveFromEventList完成)。
1 /* 2 * Task control block. A task control block (TCB) is allocated for each task, 3 * and stores task state information, including a pointer to the task's context 4 * (the task's run time environment, including register values) 5 */ 6 typedef struct tskTaskControlBlock 7 { 8 volatile portSTACK_TYPE *pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */ 9 10 #if ( portUSING_MPU_WRAPPERS == 1 ) 11 xMPU_SETTINGS xMPUSettings; /*< The MPU settings are defined as part of the port layer. THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */ 12 #endif 13 14 xListItem xGenericListItem; /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */ 15 xListItem xEventListItem; /*< Used to reference a task from an event list. */ 16 unsigned portBASE_TYPE uxPriority; /*< The priority of the task. 0 is the lowest priority. */ 17 portSTACK_TYPE *pxStack; /*< Points to the start of the stack. */ 18 signed char pcTaskName[ configMAX_TASK_NAME_LEN ];/*< Descriptive name given to the task when created. Facilitates debugging only. */ 19 20 #if ( portSTACK_GROWTH > 0 ) 21 portSTACK_TYPE *pxEndOfStack; /*< Points to the end of the stack on architectures where the stack grows up from low memory. */ 22 #endif 23 24 #if ( portCRITICAL_NESTING_IN_TCB == 1 ) 25 unsigned portBASE_TYPE uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */ 26 #endif 27 28 #if ( configUSE_TRACE_FACILITY == 1 ) 29 unsigned portBASE_TYPE uxTCBNumber; /*< Stores a number that increments each time a TCB is created. It allows debuggers to determine when a task has been deleted and then recreated. */ 30 unsigned portBASE_TYPE uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */ 31 #endif 32 33 #if ( configUSE_MUTEXES == 1 ) 34 unsigned portBASE_TYPE uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */ 35 #endif 36 37 #if ( configUSE_APPLICATION_TASK_TAG == 1 ) 38 pdTASK_HOOK_CODE pxTaskTag; 39 #endif 40 41 #if ( configGENERATE_RUN_TIME_STATS == 1 ) 42 unsigned long ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */ 43 #endif 44 45 } tskTCB;
任务创建
下面看任务创建函数,xTaskCreate实际调用的是xTaskGenericCreate
E:\project\rtos\STM32F4x7_ETH_LwIP_V1.1.1\Utilities\Third_Party\FreeRTOSV7.3.0\include\task.h
1 #define xTaskCreate( pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask ) xTaskGenericCreate( ( pvTaskCode ), ( pcName ), ( usStackDepth ), ( pvParameters ), ( uxPriority ), ( pxCreatedTask ), ( NULL ), ( NULL ) )
E:\project\rtos\STM32F4x7_ETH_LwIP_V1.1.1\Utilities\Third_Party\FreeRTOSV7.3.0\tasks.c,486
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 signed portBASE_TYPE xTaskGenericCreate( pdTASK_CODE pxTaskCode, const signed char * const pcName, unsigned short usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask, portSTACK_TYPE *puxStackBuffer, const xMemoryRegion * const xRegions ) 2 { 3 signed portBASE_TYPE xReturn; 4 tskTCB * pxNewTCB; 5 6 configASSERT( pxTaskCode ); 7 configASSERT( ( ( uxPriority & ( ~portPRIVILEGE_BIT ) ) < configMAX_PRIORITIES ) ); 8 9 /* Allocate the memory required by the TCB and stack for the new task, 10 checking that the allocation was successful. */ 11 pxNewTCB = prvAllocateTCBAndStack( usStackDepth, puxStackBuffer ); 12 13 if( pxNewTCB != NULL ) 14 { 15 portSTACK_TYPE *pxTopOfStack; 16 17 #if( portUSING_MPU_WRAPPERS == 1 ) 18 /* Should the task be created in privileged mode? */ 19 portBASE_TYPE xRunPrivileged; 20 if( ( uxPriority & portPRIVILEGE_BIT ) != 0U ) 21 { 22 xRunPrivileged = pdTRUE; 23 } 24 else 25 { 26 xRunPrivileged = pdFALSE; 27 } 28 uxPriority &= ~portPRIVILEGE_BIT; 29 #endif /* portUSING_MPU_WRAPPERS == 1 */ 30 31 /* Calculate the top of stack address. This depends on whether the 32 stack grows from high memory to low (as per the 80x86) or visa versa. 33 portSTACK_GROWTH is used to make the result positive or negative as 34 required by the port. */ 35 #if( portSTACK_GROWTH < 0 ) 36 { 37 pxTopOfStack = pxNewTCB->pxStack + ( usStackDepth - ( unsigned short ) 1 ); 38 pxTopOfStack = ( portSTACK_TYPE * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ( portPOINTER_SIZE_TYPE ) ~portBYTE_ALIGNMENT_MASK ) ); 39 40 /* Check the alignment of the calculated top of stack is correct. */ 41 configASSERT( ( ( ( unsigned long ) pxTopOfStack & ( unsigned long ) portBYTE_ALIGNMENT_MASK ) == 0UL ) ); 42 } 43 #else 44 { 45 pxTopOfStack = pxNewTCB->pxStack; 46 47 /* Check the alignment of the stack buffer is correct. */ 48 configASSERT( ( ( ( unsigned long ) pxNewTCB->pxStack & ( unsigned long ) portBYTE_ALIGNMENT_MASK ) == 0UL ) ); 49 50 /* If we want to use stack checking on architectures that use 51 a positive stack growth direction then we also need to store the 52 other extreme of the stack space. */ 53 pxNewTCB->pxEndOfStack = pxNewTCB->pxStack + ( usStackDepth - 1 ); 54 } 55 #endif 56 57 /* Setup the newly allocated TCB with the initial state of the task. */ 58 prvInitialiseTCBVariables( pxNewTCB, pcName, uxPriority, xRegions, usStackDepth ); 59 60 /* Initialize the TCB stack to look as if the task was already running, 61 but had been interrupted by the scheduler. The return address is set 62 to the start of the task function. Once the stack has been initialised 63 the top of stack variable is updated. */ 64 #if( portUSING_MPU_WRAPPERS == 1 ) 65 { 66 pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged ); 67 } 68 #else 69 { 70 pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); 71 } 72 #endif 73 74 /* Check the alignment of the initialised stack. */ 75 portALIGNMENT_ASSERT_pxCurrentTCB( ( ( ( unsigned long ) pxNewTCB->pxTopOfStack & ( unsigned long ) portBYTE_ALIGNMENT_MASK ) == 0UL ) ); 76 77 if( ( void * ) pxCreatedTask != NULL ) 78 { 79 /* Pass the TCB out - in an anonymous way. The calling function/ 80 task can use this as a handle to delete the task later if 81 required.*/ 82 *pxCreatedTask = ( xTaskHandle ) pxNewTCB; 83 } 84 85 /* We are going to manipulate the task queues to add this task to a 86 ready list, so must make sure no interrupts occur. */ 87 taskENTER_CRITICAL(); 88 { 89 uxCurrentNumberOfTasks++; 90 if( pxCurrentTCB == NULL ) 91 { 92 /* There are no other tasks, or all the other tasks are in 93 the suspended state - make this the current task. */ 94 pxCurrentTCB = pxNewTCB; 95 96 if( uxCurrentNumberOfTasks == ( unsigned portBASE_TYPE ) 1 ) 97 { 98 /* This is the first task to be created so do the preliminary 99 initialisation required. We will not recover if this call 100 fails, but we will report the failure. */ 101 prvInitialiseTaskLists(); 102 } 103 } 104 else 105 { 106 /* If the scheduler is not already running, make this task the 107 current task if it is the highest priority task to be created 108 so far. */ 109 if( xSchedulerRunning == pdFALSE ) 110 { 111 if( pxCurrentTCB->uxPriority <= uxPriority ) 112 { 113 pxCurrentTCB = pxNewTCB; 114 } 115 } 116 } 117 118 /* Remember the top priority to make context switching faster. Use 119 the priority in pxNewTCB as this has been capped to a valid value. */ 120 if( pxNewTCB->uxPriority > uxTopUsedPriority ) 121 { 122 uxTopUsedPriority = pxNewTCB->uxPriority; 123 } 124 125 #if ( configUSE_TRACE_FACILITY == 1 ) 126 { 127 /* Add a counter into the TCB for tracing only. */ 128 pxNewTCB->uxTCBNumber = uxTaskNumber; 129 } 130 #endif 131 uxTaskNumber++; 132 133 prvAddTaskToReadyQueue( pxNewTCB ); 134 135 xReturn = pdPASS; 136 portSETUP_TCB( pxNewTCB ); 137 traceTASK_CREATE( pxNewTCB ); 138 } 139 taskEXIT_CRITICAL(); 140 } 141 else 142 { 143 xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY; 144 traceTASK_CREATE_FAILED(); 145 } 146 147 if( xReturn == pdPASS ) 148 { 149 if( xSchedulerRunning != pdFALSE ) 150 { 151 /* If the created task is of a higher priority than the current task 152 then it should run now. */ 153 if( pxCurrentTCB->uxPriority < uxPriority ) 154 { 155 portYIELD_WITHIN_API(); 156 } 157 } 158 } 159 160 return xReturn; 161 }
分配TCB和stack空间
1 /* Allocate the memory required by the TCB and stack for the new task, 2 checking that the allocation was successful. */ 3 pxNewTCB = prvAllocateTCBAndStack( usStackDepth, puxStackBuffer );
第9行先分配TCB的空间,11行,TCB分配成功,再分配堆栈空间。第16行,分配输入参数*4个字节的堆栈,赋值给“任务堆栈起始”pxStack。如果分配成功,那么27行,会初始化堆栈为填充图案,这里为0xa5。
1 /*-----------------------------------------------------------*/ 2 3 static tskTCB *prvAllocateTCBAndStack( unsigned short usStackDepth, portSTACK_TYPE *puxStackBuffer ) 4 { 5 tskTCB *pxNewTCB; 6 7 /* Allocate space for the TCB. Where the memory comes from depends on 8 the implementation of the port malloc function. */ 9 pxNewTCB = ( tskTCB * ) pvPortMalloc( sizeof( tskTCB ) ); 10 11 if( pxNewTCB != NULL ) 12 { 13 /* Allocate space for the stack used by the task being created. 14 The base of the stack memory stored in the TCB so the task can 15 be deleted later if required. */ 16 pxNewTCB->pxStack = ( portSTACK_TYPE * ) pvPortMallocAligned( ( ( ( size_t )usStackDepth ) * sizeof( portSTACK_TYPE ) ), puxStackBuffer ); 17 18 if( pxNewTCB->pxStack == NULL ) 19 { 20 /* Could not allocate the stack. Delete the allocated TCB. */ 21 vPortFree( pxNewTCB ); 22 pxNewTCB = NULL; 23 } 24 else 25 { 26 /* Just to help debugging. */ 27 memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) usStackDepth * sizeof( portSTACK_TYPE ) ); 28 } 29 } 30 31 return pxNewTCB; 32 }
task.c, 204,定义的堆栈填充图案为a5,仅用于检测任务的高地址水印。
1 /* 2 * The value used to fill the stack of a task when the task is created. This 3 * is used purely for checking the high water mark for tasks. 4 */ 5 #define tskSTACK_FILL_BYTE ( 0xa5U )
计算任务堆栈顶部指针
第5行,会根据堆栈生成方向来分别计算,对于arm堆栈是向下生长的,分配的pxStack是低地址,因此,第7行,栈顶就是pxStack+深度-1(-1是因为是full stack,堆栈指针指向最后一个数据)。第8行会进行一下对齐,因为ARM是4字节对齐,因此,该句不会改变地址。
1 /* Calculate the top of stack address. This depends on whether the 2 stack grows from high memory to low (as per the 80x86) or visa versa. 3 portSTACK_GROWTH is used to make the result positive or negative as 4 required by the port. */ 5 #if( portSTACK_GROWTH < 0 ) 6 { 7 pxTopOfStack = pxNewTCB->pxStack + ( usStackDepth - ( unsigned short ) 1 ); 8 pxTopOfStack = ( portSTACK_TYPE * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ( portPOINTER_SIZE_TYPE ) ~portBYTE_ALIGNMENT_MASK ) ); 9 10 /* Check the alignment of the calculated top of stack is correct. */ 11 configASSERT( ( ( ( unsigned long ) pxTopOfStack & ( unsigned long ) portBYTE_ALIGNMENT_MASK ) == 0UL ) ); 12 }
初始化TCB变量
prvInitialiseTCBVariables主要给TCB的变量赋值。重点关注以下几个地方,第3、4行,初始化两个链表的成员,第8、12行设置两个链表的拥有者为TCB(拥有者Owner一般为包含该链表成员的结构体对象),第11行设置xEventListItem的链表成员数值为优先级补数,事件链表永远按优先级排序。
1 static void prvInitialiseTCBVariables( tskTCB *pxTCB, const signed char * const pcName, unsigned portBASE_TYPE uxPriority, const xMemoryRegion * const xRegions, unsigned short usStackDepth ) 2 { 3 vListInitialiseItem( &( pxTCB->xGenericListItem ) ); 4 vListInitialiseItem( &( pxTCB->xEventListItem ) ); 5 6 /* Set the pxTCB as a link back from the xListItem. This is so we can get 7 back to the containing TCB from a generic item in a list. */ 8 listSET_LIST_ITEM_OWNER( &( pxTCB->xGenericListItem ), pxTCB ); 9 10 /* Event lists are always in priority order. */ 11 listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), configMAX_PRIORITIES - ( portTickType ) uxPriority ); 12 listSET_LIST_ITEM_OWNER( &( pxTCB->xEventListItem ), pxTCB ); 13 }
初始化堆栈
注释中说,初始化TCB的堆栈,看起来像任务正在运行,但是被调度器打断。返回地址设置为task函数的起始。堆栈初始化后,堆栈顶部指针变量也会更新。
1 /* Initialize the TCB stack to look as if the task was already running, 2 but had been interrupted by the scheduler. The return address is set 3 to the start of the task function. Once the stack has been initialised 4 the top of stack variable is updated. */ 5 #if( portUSING_MPU_WRAPPERS == 1 ) 6 { 7 pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged ); 8 } 9 #else 10 { 11 pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); 12 } 13 #endif
STM32F4x7_ETH_LwIP_V1.1.1\Utilities\Third_Party\FreeRTOSV7.3.0\portable\RVDS\ARM_CM4F\port.c
第3行注释,模拟栈帧就像是它由一个上下文切换中断创建的。
(后面在prvStartFirstTask——》vPortSVCHandler中,会恢复现场。需要注意的是,这里做了两个现场,一是处理器的stacking,二是OS的入栈。第一个任务在首次恢复时在vPortSVCHandler中,异常进入时处理器会unstacking一部分,OS在vPortSVCHandler再恢复一部分。第一个任务后面的恢复是OS的调度器恢复的,都在xPortPendSVHandler异常处理中,且这里面会先做保存,后作恢复。对于不是第一个任务的其它任务,则都是在xPortPendSVHandler异常处理中恢复。
总结一下:
首次保存 首次恢复 后续恢复
第一个任务 任务创建 vPortSVCHandler xPortPendSVHandler
非第一个任务 任务创建 xPortPendSVHandler xPortPendSVHandler
)
1 portSTACK_TYPE *pxPortInitialiseStack( portSTACK_TYPE *pxTopOfStack, pdTASK_CODE pxCode, void *pvParameters ) 2 { 3 /* Simulate the stack frame as it would be created by a context switch 4 interrupt. */ 5 6 /* Offset added to account for the way the MCU uses the stack on entry/exit 7 of interrupts, and to ensure alignment. */ 8 pxTopOfStack--; 9 10 *pxTopOfStack = portINITIAL_XPSR; /* xPSR */ 11 pxTopOfStack--; 12 *pxTopOfStack = ( portSTACK_TYPE ) pxCode; /* PC */ 13 pxTopOfStack--; 14 *pxTopOfStack = 0; /* LR */ 15 16 /* Save code space by skipping register initialisation. */ 17 pxTopOfStack -= 5; /* R12, R3, R2 and R1. */ 18 *pxTopOfStack = ( portSTACK_TYPE ) pvParameters; /* R0 */ 19 20 /* A save method is being used that requires each task to maintain its 21 own exec return value. */ 22 pxTopOfStack--; 23 *pxTopOfStack = portINITIAL_EXEC_RETURN; 24 25 pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */ 26 27 return pxTopOfStack; 28 }
这里的设置是依据Cortex-M4的堆栈使用规则设置的,如下图(STM32F3与F4系列Cortex M4内核编程手册,p42),寄存器号小的在低地址,所以下图,从上往下地址递减,由于是Descending堆栈,因此,下图中从上往下压栈。(PSR,R15=PC,R14=LR,R12,。。。)
第10行,设置PSR为portINITIAL_XPSR=0x01000000,设置bit24为1,下图中b24(STM32F3与F4系列Cortex M4内核编程手册,p19)是预留的啊,这是怎么回事?
第12行,设置PC为任务函数入口。
第14行,设置LR为0.
第18行,设置R0为输入参数。
第20-23行,设置exec return=0xfffffffd,即返回时使用线程模式Thread和程序堆栈PSP,第25行,给剩余R11~R4寄存器预留位置。这儿的设置在哪儿有说明呢?
将任务添加到任务队列
需要操作任务队列,并将此任务添加到就绪列表,要保证不发生中断。因为就绪列表许多地方使用?那可以用信号量之类的啊?
6行,当前任务为空,10行,设置创建的任务为当前任务;
12行,当前任务数量为1,表示这是创建的第一个任务,因此,初始化任务相关的列表,见后面分析。
20行,当前任务不为空,则27行判断(25行是为什么?),若创建任务优先级大于当前任务,则设置创建的任务为当前任务。
36行,更新最大优先级全局变量。
43行,将创建任务添加到就绪列表中,见后面分析。
1 /* We are going to manipulate the task queues to add this task to a 2 ready list, so must make sure no interrupts occur. */ 3 taskENTER_CRITICAL(); 4 { 5 uxCurrentNumberOfTasks++; 6 if( pxCurrentTCB == NULL ) 7 { 8 /* There are no other tasks, or all the other tasks are in 9 the suspended state - make this the current task. */ 10 pxCurrentTCB = pxNewTCB; 11 12 if( uxCurrentNumberOfTasks == ( unsigned portBASE_TYPE ) 1 ) 13 { 14 /* This is the first task to be created so do the preliminary 15 initialisation required. We will not recover if this call 16 fails, but we will report the failure. */ 17 prvInitialiseTaskLists(); 18 } 19 } 20 else 21 { 22 /* If the scheduler is not already running, make this task the 23 current task if it is the highest priority task to be created 24 so far. */ 25 if( xSchedulerRunning == pdFALSE ) 26 { 27 if( pxCurrentTCB->uxPriority <= uxPriority ) 28 { 29 pxCurrentTCB = pxNewTCB; 30 } 31 } 32 } 33 34 /* Remember the top priority to make context switching faster. Use 35 the priority in pxNewTCB as this has been capped to a valid value. */ 36 if( pxNewTCB->uxPriority > uxTopUsedPriority ) 37 { 38 uxTopUsedPriority = pxNewTCB->uxPriority; 39 } 40 41 uxTaskNumber++; 42 43 prvAddTaskToReadyQueue( pxNewTCB ); 44 45 xReturn = pdPASS; 46 } 47 taskEXIT_CRITICAL();
初始化任务相关的列表
任务相关的列表
(1)pxReadyTasksLists[ configMAX_PRIORITIES ],针对每个优先级都分配一个就绪任务列表,同时,记录最高优先级,这样在切换任务时,直接找最高优先级对应列表,查找会非常快。
(2)延时任务列表,有2个,其中1个用于溢出计数的,two lists are used - one for delays that have overflowed the current tick count。
(3)延时任务列表指针2个。
(4)挂起就绪任务列表,当调度器挂起时,就绪的任务列表,会在调度器继续时,添加到就绪列表。
1 /* Lists for ready and blocked tasks. --------------------*/ 2 PRIVILEGED_DATA static xList pxReadyTasksLists[ configMAX_PRIORITIES ]; /*< Prioritised ready tasks. */ 3 PRIVILEGED_DATA static xList xDelayedTaskList1; /*< Delayed tasks. */ 4 PRIVILEGED_DATA static xList xDelayedTaskList2; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */ 5 PRIVILEGED_DATA static xList * volatile pxDelayedTaskList ; /*< Points to the delayed task list currently being used. */ 6 PRIVILEGED_DATA static xList * volatile pxOverflowDelayedTaskList; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */ 7 PRIVILEGED_DATA static xList xPendingReadyList; /*< Tasks that have been readied while the scheduler was suspended. They will be moved to the ready queue when the scheduler is resumed. */
5行,初始化所有优先级的就绪任务列表;10、11、12初始化2个延时任务列表1个挂起任务列表;16行,等待删除任务列表(自己删除自己的任务会在这上面,会由空闲任务执行实际删除);22行,挂起任务列表;28、29延时任务列表指针赋值。
1 static void prvInitialiseTaskLists( void ) 2 { 3 unsigned portBASE_TYPE uxPriority; 4 5 for( uxPriority = ( unsigned portBASE_TYPE ) 0U; uxPriority < configMAX_PRIORITIES; uxPriority++ ) 6 { 7 vListInitialise( ( xList * ) &( pxReadyTasksLists[ uxPriority ] ) ); 8 } 9 10 vListInitialise( ( xList * ) &xDelayedTaskList1 ); 11 vListInitialise( ( xList * ) &xDelayedTaskList2 ); 12 vListInitialise( ( xList * ) &xPendingReadyList ); 13 14 #if ( INCLUDE_vTaskDelete == 1 ) 15 { 16 vListInitialise( ( xList * ) &xTasksWaitingTermination ); 17 } 18 #endif 19 20 #if ( INCLUDE_vTaskSuspend == 1 ) 21 { 22 vListInitialise( ( xList * ) &xSuspendedTaskList ); 23 } 24 #endif 25 26 /* Start with pxDelayedTaskList using list1 and the pxOverflowDelayedTaskList 27 using list2. */ 28 pxDelayedTaskList = &xDelayedTaskList1; 29 pxOverflowDelayedTaskList = &xDelayedTaskList2; 30 }
添加任务到就绪列表
这是一个宏,根据创建任务的优先级选择相应就绪任务队列,将TCB的xGenericListItem作为列表成员,添加进就绪任务列表。
1 /* 2 * Place the task represented by pxTCB into the appropriate ready queue for 3 * the task. It is inserted at the end of the list. One quirk of this is 4 * that if the task being inserted is at the same priority as the currently 5 * executing task, then it will only be rescheduled after the currently 6 * executing task has been rescheduled. 7 */ 8 #define prvAddTaskToReadyQueue( pxTCB ) \ 9 traceMOVED_TASK_TO_READY_STATE( pxTCB ) \ 10 taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \ 11 vListInsertEnd( ( xList * ) &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xGenericListItem ) )
任务调度
vTaskStartScheduler
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void vTaskStartScheduler( void ) 2 { 3 portBASE_TYPE xReturn; 4 5 /* Add the idle task at the lowest priority. */ 6 #if ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) 7 { 8 /* Create the idle task, storing its handle in xIdleTaskHandle so it can 9 be returned by the xTaskGetIdleTaskHandle() function. */ 10 xReturn = xTaskCreate( prvIdleTask, ( signed char * ) "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle ); 11 } 12 #else 13 { 14 /* Create the idle task without storing its handle. */ 15 xReturn = xTaskCreate( prvIdleTask, ( signed char * ) "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), NULL ); 16 } 17 #endif 18 19 #if ( configUSE_TIMERS == 1 ) 20 { 21 if( xReturn == pdPASS ) 22 { 23 xReturn = xTimerCreateTimerTask(); 24 } 25 } 26 #endif 27 28 if( xReturn == pdPASS ) 29 { 30 /* Interrupts are turned off here, to ensure a tick does not occur 31 before or during the call to xPortStartScheduler(). The stacks of 32 the created tasks contain a status word with interrupts switched on 33 so interrupts will automatically get re-enabled when the first task 34 starts to run. 35 36 STEPPING THROUGH HERE USING A DEBUGGER CAN CAUSE BIG PROBLEMS IF THE 37 DEBUGGER ALLOWS INTERRUPTS TO BE PROCESSED. */ 38 portDISABLE_INTERRUPTS(); 39 40 xSchedulerRunning = pdTRUE; 41 xTickCount = ( portTickType ) 0U; 42 43 /* If configGENERATE_RUN_TIME_STATS is defined then the following 44 macro must be defined to configure the timer/counter used to generate 45 the run time counter time base. */ 46 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); 47 48 /* Setting up the timer tick is hardware specific and thus in the 49 portable interface. */ 50 if( xPortStartScheduler() != pdFALSE ) 51 { 52 /* Should not reach here as if the scheduler is running the 53 function will not return. */ 54 } 55 else 56 { 57 /* Should only reach here if a task calls xTaskEndScheduler(). */ 58 } 59 } 60 61 /* This line will only be reached if the kernel could not be started. */ 62 configASSERT( xReturn ); 63 }
上面函数主要就是调用xPortStartScheduler。
xPortStartScheduler
开启调度器,主要就是设置PendSV和SysTick定时器,使能VFP,启动第一个任务。
1 portBASE_TYPE xPortStartScheduler( void ) 2 { 3 /* Make PendSV, CallSV and SysTick the same priroity as the kernel. */ 4 portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; 5 portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; 6 7 /* Start the timer that generates the tick ISR. Interrupts are disabled 8 here already. */ 9 vPortSetupTimerInterrupt(); 10 11 /* Initialise the critical nesting count ready for the first task. */ 12 uxCriticalNesting = 0; 13 14 /* Ensure the VFP is enabled - it should be anyway. */ 15 prvEnableVFP(); 16 17 /* Lazy save always. */ 18 *( portFPCCR ) |= portASPEN_AND_LSPEN_BITS; 19 20 /* Start the first task. */ 21 prvStartFirstTask(); 22 23 /* Should not get here! */ 24 return 0; 25 }
vPortSetupTimerInterrupt
设置SysTick定时器。
4行,168M/1000-1,每1000分之一秒循环一次,即1ms一个时间片。
5行,设置SysTick定时器时钟来源为Processor Clock(系统主频168MHz)、使能中断、使能定时器。在SysTick定时器中断中,会进行任务切换调度。
1 void vPortSetupTimerInterrupt( void ) 2 { 3 /* Configure SysTick to interrupt at the requested rate. */ 4 portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;; 5 portNVIC_SYSTICK_CTRL_REG = portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT; 6 }
prvEnableVFP
使能FPU协处理器
The REQUIRE8 and PRESERVE8 directives specify that the current file requires or preserves eight-byte alignment of the stack.
LDR (pc-relative) in Thumb-2 You can use the .W width specifier to force LDR to generate a 32-bit instruction in Thumb-2 code. LDR.W always generates a 32-bit instruction, even if the target could be reached using a 16-bit LDR.
1 __asm void prvEnableVFP( void ) 2 { 3 PRESERVE8 4 5 /* The FPU enable bits are in the CPACR. */ 6 ldr.w r0, =0xE000ED88 7 ldr r1, [r0] 8 9 /* Enable CP10 and CP11 coprocessors, then save back. */ 10 orr r1, r1, #( 0xf << 20 ) 11 str r1, [r0] 12 bx r14 13 nop 14 }
prvStartFirstTask
SystemInit设置的0xE000ED08 VTOR=0x0800 0000,即Flash的首地址。
6、7、8行,从VTOR(Vector Table Offset Reg向量表偏移寄存器)获取堆栈地址,存储在Flash的首地址处,0x0800 0004地址处是Reset处理向量。
10行,将获取的堆栈地址赋值给msp。12行,使能中断;14行,调用SVC,System Service异常。
这个函数其实是在任务调度环境设置好之后,初始化任务调度环境,比如,把msp复位,使能中断,触发系统调用异常(在那里才是启动第一个任务)。
1 __asm void prvStartFirstTask( void ) 2 { 3 PRESERVE8 4 5 /* Use the NVIC offset register to locate the stack. */ 6 ldr r0, =0xE000ED08 7 ldr r0, [r0] 8 ldr r0, [r0] 9 /* Set the msp back to the start of the stack. */ 10 msr msp, r0 11 /* Globally enable interrupts. */ 12 cpsie i 13 /* Call SVC to start the first task. */ 14 svc 0 15 nop 16 }
vPortSVCHandler
6行,将当前TCB指针地址加载到r3。
7行,将r3处的数值加载给r1,r1为堆栈顶部变量的地址。
8行,将r1处的数值加载给r0,r0为当前任务堆栈地址。
10行,出栈r4-r11,r14。
11行,将r0赋值给psp。
12、13行,使能中断。(中断禁止是在vTaskStartScheduler中)
14行,退出SVC异常处理。
这里面才是真正的启动第一个任务,比如获取当前任务TCB栈顶指针,将之前保存(在创建任务时,初始化堆栈时会将堆栈设置的像被调度打断了)的寄存器现场恢复出来。主要包括恢复r4-r11和LR(CM3不会恢复LR,因为硬件会自己入栈和出栈),恢复堆栈,打开中断。其它的8个寄存器在异常处理退出时,处理器硬件会自动unstacking,这8个寄存器就包括PC指针(创建任务时压入的PC是任务入口),这8个寄存器最初是在创建任务时,初始化堆栈时,压入堆栈的,模拟好像被调度器打断了。从而,SVC异常退出后,会开始执行第一个任务。
1 __asm void vPortSVCHandler( void ) 2 { 3 PRESERVE8 4 5 /* Get the location of the current TCB. */ 6 ldr r3, =pxCurrentTCB 7 ldr r1, [r3] 8 ldr r0, [r1] 9 /* Pop the core registers. */ 10 ldmia r0!, {r4-r11, r14} 11 msr psp, r0 12 mov r0, #0 13 msr basepri, r0 14 bx r14 15 }
任务切换
时间片到了会触发PendSV异常,主动的yeild也会触发PendSV异常,这里面会将当前任务现场压栈,查找下一个任务,恢复下一个任务现场。
时间片由SysTick实现,vPortSetupTimerInterrupt已经设置1ms一个时间片,1ms到了会触发SysTick中断,在这里实际就是触发PendSV中断,第6行(其中抢占设置为1)。
xPortSysTickHandler
1 void xPortSysTickHandler( void ) 2 { 3 #if configUSE_PREEMPTION == 1 4 { 5 /* If using preemption, also force a context switch. */ 6 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 7 } 8 #endif 9 10 ( void ) portSET_INTERRUPT_MASK_FROM_ISR(); 11 { 12 vTaskIncrementTick(); 13 } 14 portCLEAR_INTERRUPT_MASK_FROM_ISR( 0 ); 15 }
xPortPendSVHandler
1 __asm void xPortPendSVHandler( void ) 2 { 3 extern uxCriticalNesting; 4 extern pxCurrentTCB; 5 extern vTaskSwitchContext; 6 7 PRESERVE8 8 9 mrs r0, psp 10 11 /* Get the location of the current TCB. */ 12 ldr r3, =pxCurrentTCB 13 ldr r2, [r3] 14 15 /* Is the task using the FPU context? If so, push high vfp registers. */ 16 tst r14, #0x10 17 it eq 18 vstmdbeq r0!, {s16-s31} 19 20 /* Save the core registers. */ 21 stmdb r0!, {r4-r11, r14} 22 23 /* Save the new top of stack into the first member of the TCB. */ 24 str r0, [r2] 25 26 stmdb sp!, {r3, r14} 27 mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY 28 msr basepri, r0 29 bl vTaskSwitchContext 30 mov r0, #0 31 msr basepri, r0 32 ldmia sp!, {r3, r14} 33 34 /* The first item in pxCurrentTCB is the task top of stack. */ 35 ldr r1, [r3] 36 ldr r0, [r1] 37 38 /* Pop the core registers. */ 39 ldmia r0!, {r4-r11, r14} 40 41 /* Is the task using the FPU context? If so, pop the high vfp registers 42 too. */ 43 tst r14, #0x10 44 it eq 45 vldmiaeq r0!, {s16-s31} 46 47 msr psp, r0 48 bx r14 49 nop 50 }
第9行,将psp保存在r0,后面会使用r0操作程序堆栈。
第12行,ldr r3, =pxCurrentTCB,将pxCurrentTCB指针的地址加载到r3中,pxCurrentTCB指针的地址为0x2000 0074,通过MAP文件可以确认;pxCurrentTCB指针的数值value为0x20003538,即指针指向的地址为0x20003538某个任务的TCB。
这里需要注意的是ldr r3, =variable仅仅是将variable变量的地址加载到r3,要想获取variable的数值,还需要ldr r3, [r3],即使variable是个int变量。
16~18行,判断是否使用fpu,若使用保存fpu现场信息。
21行,stmdb r0!, {r4-r11, r14},decrease before,将r4~r11和r14(LR,存EXC_RETURN)压入堆栈(LR处理器会存一遍,这里为什么还要存一遍?)
24行,str r0, [r2],将更新后的r0=之前任务的PSP,更新到之前任务的TCB堆栈顶部变量中。
26行,stmdb sp!, {r3, r14},将r3和r14保存在处理模式的主堆栈msp中,r3中放着当前任务指针变量地址(vTaskSwitchContext中会查找下一个调度任务,会更新当前任务指针变量的数值,其实在调用完vTaskSwitchContext后也可以重新将pxCurrentTCB加载到r3中),r14=LR放着返回模式字。
27、28行,在汇编模式下关中断
30、31行,在汇编模式下开中断
29行,执行vTaskSwitchContext,其实这个函数名不太恰当,在该函数里仅仅是找到下一个待调度的任务,叫findnexttask更合适。而切换上下文主要在xPortPendSVHandler中完成。
32行,ldmia sp!, {r3, r14},恢复r3和r14,此时r3中的pxCurrentTCB已经更新为下一个待调度任务了(pxCurrentTCB的数值value已经更新了)
35、36行,获取更新的当前任务TCB的堆栈顶部地址。
39行,恢复新任务的现场,将堆栈中的信息恢复进r4-r11, r14。
43-45,判断是否使用fpu,若是,恢复浮点寄存器。
47行,msr psp, r0,将r0保存在psp中。
48行,bx r14,跳出异常处理,进入新任务。
vTaskSwitchContext
名字虽然叫任务上下文切换,但实际做的就是找到下一个最高优先级的任务,作为下一个调度任务。
1 void vTaskSwitchContext( void ) 2 { 3 if( uxSchedulerSuspended != ( unsigned portBASE_TYPE ) pdFALSE ) 4 { 5 /* The scheduler is currently suspended - do not allow a context 6 switch. */ 7 xMissedYield = pdTRUE; 8 } 9 else 10 { 11 traceTASK_SWITCHED_OUT(); 12 13 taskFIRST_CHECK_FOR_STACK_OVERFLOW(); 14 taskSECOND_CHECK_FOR_STACK_OVERFLOW(); 15 16 taskSELECT_HIGHEST_PRIORITY_TASK(); 17 18 traceTASK_SWITCHED_IN(); 19 } 20 }
taskSELECT_HIGHEST_PRIORITY_TASK
这是一个宏定义,4行从最高优先级开始查看就绪队列是否有节点,12行,获取就绪队列的下一个任务。
1 #define taskSELECT_HIGHEST_PRIORITY_TASK() \ 2 { \ 3 /* Find the highest priority queue that contains ready tasks. */ \ 4 while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) ) \ 5 { \ 6 configASSERT( uxTopReadyPriority ); \ 7 --uxTopReadyPriority; \ 8 } \ 9 \ 10 /* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of \ 11 the same priority get an equal share of the processor time. */ \ 12 listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) ); \ 13 } /* taskSELECT_HIGHEST_PRIORITY_TASK */
任务和异常处理之间的堆栈处理
(STM32F3与F4系列Cortex M4内核编程手册,p41)
When the processor takes an exception, unless the exception is a tail-chained or a late-arriving exception, the processor pushes information onto the current stack. This operation is referred as stacking and the structure of eight data words is referred as stack frame.
下图就是stacking过程压栈的8个寄存器,PSR,R15=PC,R14=LR,R12。下图可以反过来看,但下图正好是下面是低地址,上面是高地址。
这个压栈过程是处理器,也就是硬件完成的,This is achieved through the hardware stacking of registers。
The stack frame includes the return address. This is the address of the next instruction in the interrupted program. This value is restored to the PC at exception return so that the interrupted program resumes.栈帧包括返回地址,即PC。
In parallel to the stacking operation, the processor performs a vector fetch that reads the exception handler start address from the vector table. When stacking is complete, the processor starts executing the exception handler. At the same time, the processor writes an EXC_RETURN value to the LR. This indicates which stack pointer corresponds to the stack frame and what operation mode the was processor was in before the entry occurred.
上面的各种操作的先后顺序,手册写的非常模糊,又是In parallel,又是At the same time的。推测顺序如下:
正在线程模式
|(1)
发生异常
|(2)
查找向量表,执行异常处理函数
在第(2)步,处理器进行了以下动作:
1、stacking,将8个寄存器压入堆栈
2、将EXC_RETURN写入LR
3、查找向量表,执行异常处理函数。
上面第2步是EXC_RETURN写入LR吗?从FreeRTOS,CM4,pxPortInitialiseStack来看,是将EXC_RETURN压栈,即前面有LR,后面有EXC_RETURN,并没有将EXC_RETURN写入LR。
还有R11~R4是什么时候压栈的呢,这个是由OS完成的,一是在创建任务初始化堆栈时,二是在任务切换时xPortPendSVHandler。
FreeRTOS对CM4F压栈的内容如下,下图中绿色背景是处理器硬件压栈的,其它是FreeRTOS压栈的,这是CM4F压栈的内容,对于CM3处理器不会压最后的LR(EXC_RETURN):
重新梳理一遍任务堆栈相关的操作
main——使用线程模式,MSP(复位后处理器默认使用线程模式和MSP)会将r0-r4,lr压入堆栈,占用6*4=24=0x18字节,因此,堆栈变为0x200100f8-0x18=0x200100e0
xTaskCreate——创建任务时,线程模式,MSP,会分配任务堆栈内存,并填充堆栈内容为填充图案a5,堆栈起始赋值给pxStack;堆栈顶部赋值给pxTopOfStack
——prvAllocateTCBAndStack
——pxStack=pvPortMallocAligned
——填充堆栈内容为填充图案a5
——pxTopOfStack = pxNewTCB->pxStack + ( usStackDepth - ( unsigned short ) 1 );
——pxNewTCB->pxTopOfStack = pxPortInitialiseStack
这里初始化堆栈的样子,就像是被调度器打断一样,会将处理器stacking的8个寄存器,以及FreeRTOS约定的r4-r11和r14都压入堆栈,同时更新栈顶指针,形成上面制作的图形状态。
vTaskStartScheduler——线程模式,MSP,将r0-r4,lr压入堆栈,占用6*4=24=0x18字节,因此,堆栈变为0x200100e0-0x18=0x200100c8
——xPortStartScheduler——线程模式,MSP,将r4,lr压入堆栈,占用2*4=8=0x8字节,因此,堆栈变为0x200100c8-0x8=0x200100c0
——prvStartFirstTask,线程模式,MSP,叶子函数,没有用堆栈,但将堆栈复位=0x200100f8。这里主要是在任务调度环境设置好之后,初始化任务调度环境,比如,把msp复位,使能中断,触发系统调用异常(在那里才是启动第一个任务)
vPortSVCHandler,处理模式,MSP,异常进入,处理器硬件stacking共8个reg,4*8=32=0x20,堆栈变为0x200100f8-0x20=0x200100d8
——Exception entry,异常进入时,处理器已经进行了stacking,这里用的是msp
——主体,获取当前任务栈顶,恢复FreeRTOS约定的r4-r11和r14
设置PSP为更新的任务栈顶
打开中断
设置LR=0xFFFF FFFD,返回线程模式,PSP
——Exception return,异常退出时,处理器进行unstacking,将8个寄存器出栈(这些是在创建任务初始化堆栈时设置的,其中PC=任务入口)正式启动第一个任务
————————————————————在这之前用的全是MSP,在这之后才开始用PSP,在异常处理时也用MSP。
Main_task
进入第一个任务执行。
之后,会进入xPortPendSVHandler,在这里面用的还是MSP,要想操作任务堆栈,需要将PSP读进来。但xPortPendSVHandler不会再用MSP了,因为xPortPendSVHandler不会再调用别的函数,这是叶子函数,以后,MSP也都不会再更新了。
关于程序堆栈和任务堆栈
程序堆栈可以看成是MSP,任务堆栈可以看成是PSP。
在创建任务之前,是没有任务堆栈的,只有程序堆栈(暂且叫这个名字,也就是没有OS时的,普通意义上的堆栈),这个堆栈例子工程中定义的是0x200100f8。
2行,在调用时用了BLX,会将PC+4放在LR,同时,在SystemInit会将r4和LR压栈,因为,在SystemInit中也调用了其它函数,也就是SystemInit是非叶子函数,这里使用了堆栈。
3行,调用已经完成,堆栈恢复为0x200100f8,调用__main时,没有用BLX而是用BX,所以,__main是叶子函数,且应该没有用堆栈。调用完__main后,会跳到用户的main函数中(__main无法单步跟踪)。
1 LDR R0, =SystemInit 2 BLX R0 3 LDR R0, =__main 4 BX R0 5 ENDP
main函数开始,使用线程模式,MSP(复位后处理器默认使用线程模式和MSP)会将r0-r4,lr压入堆栈,占用6*4=24=0x18字节,因此,堆栈变为0x200100f8-0x18=0x200100e0(处理器只在进入和离开异常时才进行自动入栈出栈,普通函数调用都在线程模式,不会进行stacking和unstacking)。
关于FreeRTOS的ARM-CM4F移植
GCC下的:FreeRTOSV7.3.0\portable\GCC\ARM_CM4F\port.c
Keil下的:FreeRTOSV7.3.0\portable\RVDS\ARM_CM4F\port.c(Keil和RVDS一样)
其中关于vPortSVCHandler的差异,实际的汇编都是一样的,差别在于编译器如何识别c文件里的汇编代码。
GCC下:
1 void vPortSVCHandler( void ) 2 { 3 __asm volatile ( 4 " ldr r3, pxCurrentTCBConst2 \n" /* Restore the context. */ 5 " ldr r1, [r3] \n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */ 6 " ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */ 7 " ldmia r0!, {r4-r11, r14} \n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */ 8 " msr psp, r0 \n" /* Restore the task stack pointer. */ 9 " mov r0, #0 \n" 10 " msr basepri, r0 \n" 11 " bx r14 \n" 12 " \n" 13 " .align 2 \n" 14 "pxCurrentTCBConst2: .word pxCurrentTCB \n" 15 ); 16 }
RVDS下:
1 __asm void vPortSVCHandler( void ) 2 { 3 PRESERVE8 4 5 /* Get the location of the current TCB. */ 6 ldr r3, =pxCurrentTCB 7 ldr r1, [r3] 8 ldr r0, [r1] 9 /* Pop the core registers. */ 10 ldmia r0!, {r4-r11, r14} 11 msr psp, r0 12 mov r0, #0 13 msr basepri, r0 14 bx r14 15 }
结束