Freertos 基础实现 freertos 代码结构分析
stm32中,freertos内存分布预览 stm32f103zet6 rtos的内存分布
[ 高地址 (High Address) ] 0x2001 0000 (SRAM 结束/64 KB) | v +------------------------------------------+ | System Stack (MSP) | <--- 栈顶(Top) , 向下生长 ↓ | (用于中断服务 ISR、启动前的 main 函数) | +------------------------------------------+ | (空闲区) | +------------------------------------------+ | FreeRTOS Heap (ucHeap[]) | <--- 这里的管理是源码阅读重点 | +------------------------------------+ | | | [空闲堆内存] (Free Heap Block) | | | +------------------------------------+ | | | Task B Stack (任务B栈) | | | | Task B TCB (任务B控制块) | | | +------------------------------------+ | | | Task A Stack (任务A栈) | | | | Task A TCB (任务A控制块) | | | +------------------------------------+ | +------------------------------------------+ | .bss (未初始化的全局变量) | <--- 包括上面的 ucHeap 数组 +------------------------------------------+ | .data (已初始化的全局变量) | +------------------------------------------+ [ 低地址 (Low Address) ] 0x2000 0000 (SRAM 起始) ----------------- 物理分界线 (SRAM vs Flash) ----------------- [ 高地址 (High Address) ] 0x0808 0000 (Flash 结束/512 KB) | +------------------------------------------+ | .rodata (只读常量) | +------------------------------------------+ | .text (程序代码/FreeRTOS源码) | +------------------------------------------+ | Vector Table (中断向量表) | +------------------------------------------+ [ 低地址 (Low Address) ] 0x0800 0000 (Flash 起始)
(前置知识),CM3中断 rtos本质上是要依赖mcu的中断来实现的,所以有必要先复习一下cm3内核的中断相关的知识了。
中断 是 CPU 的一种常见特性,中断一般由硬件产生,当中断发生后,会中断 CPU 当前正 在执行的程序而跳转到中断对应的服务程序种去执行
ARM Cortex-M 内核的 MCU 具有一个用于中断管理的嵌套向量中断控制器(NVIC ,全称:Nested vectored interrupt controller)。
STM32的NVIC 最多支持256个中断源 (16个系统中断 + 240个外部中断)
(zet6只用到了10个系统中断 + 60个外部中断)
1. 外部中断优先级配置
NVIC这个外设,在芯片定义层的结构体定义:我们主要使用IP,来设置外部中断优先级,一共240个字节,刚好对应240个外部中断
typedef struct { __IOM uint32_t ISER[8U ]; uint32_t RESERVED0[24U ]; __IOM uint32_t ICER[8U ]; uint32_t RSERVED1[24U ]; __IOM uint32_t ISPR[8U ]; uint32_t RESERVED2[24U ]; __IOM uint32_t ICPR[8U ]; uint32_t RESERVED3[24U ]; __IOM uint32_t IABR[8U ]; uint32_t RESERVED4[56U ]; __IOM uint8_t IP[240U ]; uint32_t RESERVED5[644U ]; __OM uint32_t STIR; } NVIC_Type;
IP的每一个字节,对应一种外部中断的优先级,但是8位只用到了高4位 (里面又可以再细分为抢占优先级+子优先级 ),一般设置成抢占式4位,子优先级不用,这样最简单。
2. 系统中断优先级配置
系统中断优先级配置,由独立的SHPR1、SHPR2、SHPR3来进行配置,不是通过NVIC(因为不是嵌套嘛)。 里面,比较重要的就是PendSV中断和 SysTick 中断,SVCall 中断优先级
3. 三个中断屏蔽寄存器
PRIMASK
屏蔽除 NMI 和 HardFault 外的所有异常和中断,
FAULTMASK
屏蔽除 NMI 外的所有异常和中断
BASEPRI
中断优先级低于 BASEPRI 阈值的中断就都会被屏蔽掉
我们主要关注的是BASEPRI这个
除了NMI 和 hardfault, reset这些中断是无法设置优先级的外
剩下的所有中断(系统中断(由CM3自己受理) + 外部中断(由st的NVIC受理)),他们对外都是一致的, 也就是都可以被上面3个屏蔽寄存器屏蔽。
也就意味着freertos可以通过设置systick中断+pendSV中断(系统中断)的优先级为15(最低), 同时设置阈值5-15为rtos可控制中断,在进入临界区的时候,就可以关中断+关闭任务调度了。
总结 :
中断的“两个家族”
在 Cortex-M3 内核中,所有的中断统称为“异常(Exception)”,但管理上分为两派:
系统异常 (System Exceptions): 由 ARM 内核定义(如 SVC, PendSV, SysTick),由 SCB 寄存器管理。
外部中断 (External Interrupts/IRQs): 由芯片厂家(如 ST)定义(如 UART, Timer, DMA),由 NVIC 寄存器管理。
统一规则: 无论属于哪个家族,优先级逻辑是通用的——数字越小,优先级越高。对外地位等价
核心机制:
BASEPRI 与“围栏”BASEPRI 寄存器: 是 FreeRTOS 实现临界区保护的“秘密武器”。
工作原理 : 当 RTOS 进入临界区,会将 BASEPRI 设为 0x50。此时,硬件会自动屏蔽掉所有优先级 $\ge 5$ 的中断。
系统心脏: SysTick 和 PendSV 必须被设为最低优先级 (15) 。这样它们永远不会打断硬件中断。它们在临界区内会被一起屏蔽 ,保证任务切换时内核数据(如就绪列表)的绝对安全。
API 调用禁区: 如果在 0-4 级中断里调用了 API,会破坏 RTOS 内部链表的原子性,导致系统崩溃(通常卡在 configASSERT)。
A. freertos 中断配置 根据freertos的框图架构,可以看出,freertos,利用NVIC和SHPR寄存器来配置内核中断+外部中断的优先级,
pendSV 中断和systick中断优先级配置
在port.c中
#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL ) #define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL ) BaseType_t xPortStartScheduler ( void ) { portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; }
开关中断的接口
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() #define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
freertos进出临界区api分两套 :
普通任务进出临界区:
可嵌套
无需备份之前的basepri
中断进出临界区:
不可以嵌套
需要备份之前的basepri
B. freertos 任务 在传统的裸机开发中,一般是一个while大循环,然后里面顺序的执行函数(后台),当中断来临,这时候进入中断服务程序(前台)。
但是这在大型嵌入式系统设计中,实时性严重不足
多任务系统 的多个任务可以“同时”运行,是从宏观的角度而言的,对于单核 的 CPU 而言,CPU 在同一时刻只能够处理一个任务
多任务系统的任务也是具有优先级的 ,高优先 级的任务可以像中断的抢占一样,抢占低优先级任务的 CPU 使用权
任务调度 则分为抢占式调度+时间片轮询
任务的状态
运行态(正在占用cpu)
就绪态(排队等待执行)(当前有同或更高优先级的任务)
阻塞态(延时一段时间vTaskDelay();等待外部事件发生(超时时间))
挂起态(通过函数 vTaskSuspend()和函数 vTaskResums()进入和退出挂起态)
任务优先级
每一个任务都被分配一个0~(configMAX_PRIORITIES-1)的任务优先级,宏 configMAX_PRIORITIES 在 FreeRTOSConfig.h文件中定义
宏 configMAX_PRIORITIES 的值不能超过 32,原因是freertos里面有选择是否用硬件计算前导0指令,最大支持32位
任务优先级高低 与其对应的优先级数值,是成正比的
任务task 在rtos中,task由TCB和栈空间组成。
TCB结构体如下:
typedef struct tskTaskControlBlock { volatile StackType_t * pxTopOfStack; #if ( portUSING_MPU_WRAPPERS == 1 ) xMPU_SETTINGS xMPUSettings; #endif ListItem_t xStateListItem; ListItem_t xEventListItem; UBaseType_t uxPriority; StackType_t * pxStack; char pcTaskName[ configMAX_TASK_NAME_LEN ]; #if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) ) StackType_t * pxEndOfStack; #endif #if ( portCRITICAL_NESTING_IN_TCB == 1 ) UBaseType_t uxCriticalNesting; #endif #if ( configUSE_TRACE_FACILITY == 1 ) UBaseType_t uxTCBNumber; UBaseType_t uxTaskNumber; #endif #if ( configUSE_MUTEXES == 1 ) UBaseType_t uxBasePriority; UBaseType_t uxMutexesHeld; #endif #if ( configUSE_APPLICATION_TASK_TAG == 1 ) TaskHookFunction_t pxTaskTag; #endif #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS]; #endif #if ( configGENERATE_RUN_TIME_STATS == 1 ) configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; #endif #if ( configUSE_NEWLIB_REENTRANT == 1 ) struct _reent xNewLib_reent ; #endif #if ( configUSE_TASK_NOTIFICATIONS == 1 ) volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ]; #endif #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) uint8_t ucStaticallyAllocated; #endif #if ( INCLUDE_xTaskAbortDelay == 1 ) uint8_t ucDelayAborted; #endif #if ( configUSE_POSIX_ERRNO == 1 ) int iTaskErrno; #endif } tskTCB; typedef tskTCB TCB_t;typedef struct tskTaskControlBlock * TaskHandle_t ;
可以理解为,一个TCB(任务控制块),就是一个任务的本体。 一个任务TCB,里面包含了比如任务栈空间栈顶的指针,优先级,临界区的嵌套次数等等。
可以看到所谓的任务句柄 ,实际上就是TCB内存块的指针 。
而栈空间 ,是和一个任务在运行过程中,和函数的局部变量,函数调用的现场和返回地址有关的,所以是在创建过程 中开辟的一段内存。
task创建 通过静态创建,可以很清晰的看出一个任务的内存分布和内部结构。
1. 静态创建
#if ( configSUPPORT_STATIC_ALLOCATION == 1 ) TaskHandle_t xTaskCreateStatic ( TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * const pvParameters, UBaseType_t uxPriority, StackType_t * const puxStackBuffer, StaticTask_t * const pxTaskBuffer ) { TCB_t * pxNewTCB; TaskHandle_t xReturn; configASSERT( puxStackBuffer != NULL ); configASSERT( pxTaskBuffer != NULL ); #if ( configASSERT_DEFINED == 1 ) { volatile size_t xSize = sizeof ( StaticTask_t ); configASSERT( xSize == sizeof ( TCB_t ) ); ( void ) xSize; } #endif if ( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) ) { pxNewTCB = ( TCB_t * ) pxTaskBuffer; pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer; #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) pxNewTCB->ucStaticallyAllocated = tskSTATICALLY_ALLOCATED_STACK_AND_TCB; } #endif prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, &xReturn, pxNewTCB, NULL ); prvAddNewTaskToReadyList( pxNewTCB ); } else { xReturn = NULL ; } return xReturn; } #endif
注意:如果是选择静态创建,那么之后所有的任务创建都只能使用静态,包括空闲任务IDLE task, 和软件定时器服务任务 可以看开启调度器的逻辑:
void vTaskStartScheduler ( void ) { BaseType_t xReturn; #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxIdleTaskTCBBuffer = NULL ; StackType_t * pxIdleTaskStackBuffer = NULL ; uint32_t ulIdleTaskStackSize; vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); xIdleTaskHandle = xTaskCreateStatic( prvIdleTask, configIDLE_TASK_NAME, ulIdleTaskStackSize, ( void * ) NULL , portPRIVILEGE_BIT, pxIdleTaskStackBuffer, pxIdleTaskTCBBuffer ); if ( xIdleTaskHandle != NULL ) { xReturn = pdPASS; } else { xReturn = pdFAIL; } } #else { xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL , portPRIVILEGE_BIT, &xIdleTaskHandle ); } #endif #if ( configUSE_TIMERS == 1 ) { if ( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif
调度器如果启用了定时器xTimerCreateTimerTask(),则需要创建定时器任务
BaseType_t xTimerCreateTimerTask ( void ) { BaseType_t xReturn = pdFAIL; prvCheckForValidListAndQueue(); if ( xTimerQueue != NULL ) { #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxTimerTaskTCBBuffer = NULL ; StackType_t * pxTimerTaskStackBuffer = NULL ; uint32_t ulTimerTaskStackSize; vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize ); xTimerTaskHandle = xTaskCreateStatic( prvTimerTask, configTIMER_SERVICE_TASK_NAME, ulTimerTaskStackSize, NULL , ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT, pxTimerTaskStackBuffer, pxTimerTaskTCBBuffer ); if ( xTimerTaskHandle != NULL ) { xReturn = pdPASS; } } #else { xReturn = xTaskCreate( prvTimerTask, configTIMER_SERVICE_TASK_NAME, configTIMER_TASK_STACK_DEPTH, NULL , ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT, &xTimerTaskHandle ); } #endif } else { mtCOVERAGE_TEST_MARKER(); } configASSERT( xReturn ); return xReturn; }
2. 动态创建
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) BaseType_t xTaskCreate ( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask ) { TCB_t * pxNewTCB; BaseType_t xReturn; #if ( portSTACK_GROWTH > 0 ) { } #else { StackType_t * pxStack; pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof ( StackType_t ) ) ); if ( pxStack != NULL ) { pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof ( TCB_t ) ); if ( pxNewTCB != NULL ) { pxNewTCB->pxStack = pxStack; } else { vPortFreeStack( pxStack ); } } else { pxNewTCB = NULL ; } } #endif if ( pxNewTCB != NULL ) { #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) { pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB; } #endif 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
3. 删除任务
#if ( INCLUDE_vTaskDelete == 1 ) void vTaskDelete ( TaskHandle_t xTaskToDelete ) { TCB_t * pxTCB; taskENTER_CRITICAL(); { pxTCB = prvGetTCBFromHandle( xTaskToDelete ); if ( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) { 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++; if ( pxTCB == pxCurrentTCB ) { vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) ); ++uxDeletedTasksWaitingCleanUp; traceTASK_DELETE( pxTCB ); portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending ); } else { --uxCurrentNumberOfTasks; traceTASK_DELETE( pxTCB ); prvResetNextTaskUnblockTime(); } } taskEXIT_CRITICAL(); if ( pxTCB != pxCurrentTCB ) { prvDeleteTCB( pxTCB ); } if ( xSchedulerRunning != pdFALSE ) { if ( pxTCB == pxCurrentTCB ) { configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } } #endif
任务挂起和恢复则需要结合任务列表一起分析,见下
任务链表与任务节点 FreeRTOS 中的列表和列表项就是数据结构中的链表 和节点 。
链表
FreeRTOS 中的链表 是一个双向链表 ,具体定义如下:
typedef struct xLIST { listFIRST_LIST_INTEGRITY_CHECK_VALUE volatile UBaseType_t uxNumberOfItems; ListItem_t * configLIST_VOLATILE pxIndex; MiniListItem_t xListEnd; listSECOND_LIST_INTEGRITY_CHECK_VALUE } List_t;
这里面的两个校验值,只是用来检查链表的数据是否遭到破坏
成员变量 xListEnd 是一个迷你链表项(详见 7.1.3 小节),链表中迷你链表项的值一般被设置为最大值,用于将链表中的所有链表项按升序排序时,排在最末尾;同时 xListEnd 也用于挂载其他插入到链表中的链表项。
所以xListEnd 仅仅只是作为链表的末尾节点存在,不计入链表节点的个数。
ListItem_t, 也就是用于遍历链表的pxIndex的指针类型,才是链表节点的真是类型。
freertos中链表的结构示意图如下:
节点 :
freertos中链表节点的定义
struct xLIST_ITEM { listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE configLIST_VOLATILE TickType_t xItemValue; struct xLIST_ITEM * configLIST_VOLATILE pxNext ; struct xLIST_ITEM * configLIST_VOLATILE pxPrevious ; void * pvOwner; struct xLIST * configLIST_VOLATILE pxContainer ; listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE }; typedef struct xLIST_ITEM ListItem_t ;
可以看到,每个链表节点,都拥有一个前向指针和一个后向指针,也就是双向。
xItemValue 为列表项的值,这个值多用于按升序对链表中的节点进行排序。
每个链表节点,都知道,自己是属于哪一个链表的。
pxOwner 用于指向包含列表项的对象(通常是任务控制块),这样就可以把任务TCB和链表节点挂钩
节点结构的示意图如下:
迷你链表项
前面在链表的组成结构中,我们发现,每个链表都持有一个迷你链表项。
仅用于标记列表的末尾 和挂载其他插入 列表中的列表项,用户是用不到迷你列表项的
定义如下:
struct xMINI_LIST_ITEM { listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE configLIST_VOLATILE TickType_t xItemValue; struct xLIST_ITEM * configLIST_VOLATILE pxNext ; struct xLIST_ITEM * configLIST_VOLATILE pxPrevious ; }; typedef struct xMINI_LIST_ITEM MiniListItem_t ;
可以看出:
迷你列表项支持检测数据完整性
xItemValue 为列表项的值,用于升序排序到最后
pxNext 和 pxPrevious用于指向上一个和下一个链表节点
只用于标记列表的末尾和挂载其他插入列表中的列表项,因此不需要成员变量 pxOwner 和 pxContainer
以上总结了,freertos中,链表和节点的组成部分
链表,节点的使用 1. 链表的初始化 :
就是写初始化List_t的内容,因为是空的,多数都是0,指向最后的迷你节点
void vListInitialise ( List_t * const pxList) { pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); pxList->xListEnd.xItemValue = portMAX_DELAY; pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); pxList->uxNumberOfItems = ( UBaseType_t ) 0U ; listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList ); }
2. 链表节点的初始化
可以看到,也仅仅只是初始化一个空的节点,既没有指向TCB内容,也没有归属那个链表
void vListInitialiseItem ( ListItem_t * const pxItem) { pxItem->pxContainer = NULL ; listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem ); listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem ); }
3. vListInsertEnd插入到end
是一种无序的插入方法
void vListInsertEnd ( List_t * const pxList, ListItem_t * const pxNewListItem) { ListItem_t * const pxIndex = pxList->pxIndex; listTEST_LIST_INTEGRITY( pxList ); listTEST_LIST_ITEM_INTEGRITY( pxNewListItem ); pxNewListItem->pxNext = pxIndex; pxNewListItem->pxPrevious = pxIndex->pxPrevious; mtCOVERAGE_TEST_DELAY(); pxIndex->pxPrevious->pxNext = pxNewListItem; pxIndex->pxPrevious = pxNewListItem; pxNewListItem->pxContainer = pxList; ( pxList->uxNumberOfItems )++; }
将待插入的列表项插入到列表 pxIndex 指向列表项的前面 ,要注意的是,pxIndex 不一定指向 xListEnd,而是有可能指向列表中任意一个列表项
示意图如下:
可以直接想象成整个链表节点,构成一个圈 :里面每个节点两两相连,可以前向,后向移动。用miniEND节点表示末尾
4. vListInsert()
将节点,按照节点值,升序插入到列表
void vListInsert ( List_t * const pxList, ListItem_t * const pxNewListItem) { ListItem_t * pxIterator; const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; listTEST_LIST_INTEGRITY( pxList ); listTEST_LIST_ITEM_INTEGRITY( pxNewListItem ); if ( xValueOfInsertion == portMAX_DELAY ) { pxIterator = pxList->xListEnd.pxPrevious; } else { for ( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) { } } pxNewListItem->pxNext = pxIterator->pxNext; pxNewListItem->pxNext->pxPrevious = pxNewListItem; pxNewListItem->pxPrevious = pxIterator; pxIterator->pxNext = pxNewListItem; pxNewListItem->pxContainer = pxList; ( pxList->uxNumberOfItems )++; }
可以看出:在将待插入列表项插入列表之前,会前遍历列表 ,找到待插入列表项需要插入的位置
为什么值的类型是 TickType_t?
这是由 FreeRTOS 的核心应用场景决定的。 在 RTOS 中,需要用到“升序排列链表”最主要的场景是**“延时任务列表”(Delayed Task List)**。
为了快速查找: 当多个任务都在延时(阻塞)时,内核需要知道“谁该第一个醒来”。
排序的基准是时间: 如果链表按“唤醒时间点”升序排列,那么表头(第一个节点)永远是那个最先到期的任务。
类型一致性: 系统时钟(Tick Count)的类型是 TickType_t,为了能够直接存储和比较时间点,避免溢出或类型转换错误,xItemValue 必须使用相同的类型。
5. 链表移除节点
这个就很简单了,不用细说了
6. 链表的操作
这里已经提供给我们了,有很多方便的宏,可以帮助我们直接操作链表和节点
freertos的启动流程 上面已经分析了freertos的中断设置,任务,链表节点,下面来看一下freertos的启动流程来
讲解 FreeRTOS 系统启动到第一个任务开始运行的一整个流程
1. 裸机 这里就和普通的裸机程序一样,你要先初始化好所有需要的硬件,比如HAL的init,可以配置好基础的systick中断,定时器的初始化,lcd屏幕的初始化,key的初始化等。
之后就是创建好任务xTaskCreate,在堆中开辟好你的任务的TCB+Stack内存。 之后就是启动任务调度器,接下来,调度器便会开始任务调度。
调度器除非被关掉,否则不会再回到裸机的主程序中了。
2. 开启任务调度器 开启任务调度器的代码如下:
void vTaskStartScheduler ( void ) { BaseType_t xReturn; #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { } #else { xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL , portPRIVILEGE_BIT, &xIdleTaskHandle ); } #endif #if ( configUSE_TIMERS == 1 ) { if ( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif if ( xReturn == pdPASS ) { #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT { freertos_tasks_c_additions_init(); } #endif portDISABLE_INTERRUPTS(); #if ( configUSE_NEWLIB_REENTRANT == 1 ) { _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } #endif xNextTaskUnblockTime = portMAX_DELAY; xSchedulerRunning = pdTRUE; xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); traceTASK_SWITCHED_IN(); if ( xPortStartScheduler() != pdFALSE ) { } else { } } else { configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY ); } ( void ) xIdleTaskHandle; ( void ) uxTopUsedPriority; }
所以,task中启动调度器的核心就是另外创建两个task:IDLE + (软件定时器任务,如果需要)
之后就是xPortStartScheduler来真正设置systick中断,来进行调度。
总结一下 ,启动调度器里面的工作:
创建空闲任务
创建定时器服务任务
关闭中断,防止 SysTick 中断在任务调度器开启之前或过程中,产生中断。(在开始运行第一个任务时,重新打开中断)
初始化一些全局变量,并将任务调度器的运行标志设置为已运行
单开一个时基定时器,用于任务运行时间统计功能(如果不用就不需要)
调用函数 xPortStartScheduler()
下面看一下,实际进行任务调度的xPortStartScheduler()的实现:
BaseType_t xPortStartScheduler ( void ) { #if ( configASSERT_DEFINED == 1 ) { } #endif portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; vPortSetupTimerInterrupt(); uxCriticalNesting = 0 ; prvStartFirstTask(); return 0 ; }
总结一下 port里面,开启调度器的工作内容:
断言配置的检查
配置 PendSV 和 SysTick 的中断优先级为最低优先级
根据FreeRTOSConfig.h 文件中配置的systick中断周期,重新设置systick中断, 然后启动 SysTick 计数和中断
初始化临界区嵌套计数器为 0
prvStartFirstTask()启动第一个任务
3. 启动第一个任务 下面看一下是如何启动第一个任务的prvStartFirstTask()
__asm void prvStartFirstTask ( void ) { PRESERVE8 ldr r0, =0xE000ED08 ldr r0, [ r0 ] ldr r0, [ r0 ] msr msp, r0 cpsie i cpsie f dsb isb svc 0 nop nop }
这里实际开启第一个任务,已经完全由汇编来实现了。
保证8字节对齐 ,这是ARM 官方定义了一套标准,叫做 AAPCS,在调用公共接口(即函数切换、中断进入)时,堆栈指针 SP 必须保持 8 字节对齐,c程序在编译阶段,就会默认进行8字节对齐。
读取0xE000ED08(VTOR)取值,存放的就是 MSP 的初始地址,在启动调度器前,彻底回收启动阶段占用的栈空间,让 MSP 重新回到最干净的状态
实际Cotex-M提供了两个栈空间,MSP(主栈指针,系统内核使用,比如中断) 和 PSP(进程栈指针,任务栈使用),当使用不同的堆栈指针时,SP 会等于当前使用的堆栈指针
0xE000ED08 是 VTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址 。
一般来说向量表是从其实地址 0x00000000 开始的
ARM Corten-M 提供了 VTOR 对向量表进行重定向。而向量表是用来保存中断异常的入口函数地址,即栈顶地址的
__Vectors DCD __initial_sp ; 栈底指针 DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler DCD MemManage_Handler ; MPU Fault Handler
所以这个操作,相当于丢弃了程序之前保存在栈中的数据,因此也就无法返回,因为刷掉了内核栈空间
重新使能全局中断,因为在taskstartSchedule中,曾经关掉了可控的所有中断
使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断
既然,SVC是一个触发中断,那么CM3内核在接收到SVC 0的指令后,会跳转执行SVC中断服务程序:
DCD SVC_Handler ; SVCall Handler
原本他是要跳转到SVC_Handler程序的,这本来定义在裸机的中断.c文件中,但是我们在移植freertos时,就已经把他屏蔽掉了,换成了我们自己的SVC中断处理程序 vPortSVCHandler()
__asm void vPortSVCHandler ( void ) { PRESERVE8 ldr r3, = pxCurrentTCB ldr r1, [ r3 ] ldr r0, [ r1 ] ldmia r0 !, { r4 - r11 } msr psp, r0 isb mov r0, # 0 msr basepri, r0 orr r14, # 0xd bx r14 }
这里就是跳转到当前需要执行的第一个任务来执行:
pxCurrentTCB 是一个全局变量,根据他的第一个成员变量,可以得到这个任务栈的地址,在前面创建 start_task 任务、空闲任务、定时器处理任务时自动根据任务的优先级高低进行赋值的
这里总结一下 : 中断优先级(0-15,越小越大,所有pendSV, systick都是最小) 任务优先级(0-31,越大越大) start_task 任务 优先级 1 IDLE任务 优先级 0 定时器处理任务 优先级 31
因此:因此当进入 SVC 中断时,pxCurrentTCB 就是指向了定时器处理任务的任务控制块。
将任务栈中的内容出栈到 CPU 寄存器中,任务栈中的内容在调用任务创建函数的时候已经初始化了
然后再设置 PSP 指针,那么,这么一来,任务的运行环境就准备好了
开中断
跳转任务的函数中去执行
关于最后一句r14的寄存器是如何实现跳转执行的 ,这里面有一些技术细节:
进入中断后 r14 为 EXC_RETURN 的具体应用就是,通过判断EXC_RETURN 的 bit4 是否为 0,来判断任务是否使用了浮点单元。最后通过 bx r14 指令,跳转到任务的任务函数中执行 ,执行此指令,CPU 会自动从 PSP 指向的栈中出栈 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,并且如果 EXC_RETURN 的bit4 为 0(使用了浮点单元),那么 CPU 还会自动恢复浮点寄存器
4. 任务状态列表 以上,我们就分析了,freertos的启动流程 ,如何一步一步,从
裸机初始化硬件,到
创建任务,在堆区自己开辟出任务内存区域,然后
调用task启动调度器函数
创建IDLE任务
创建定时器任务(如有)
port启动调度器
清楚MSP裸机的栈空间
bx r14, 跳转第一个任务
下面,我需要看一下,任务task,在freertos中是如何被编排调度的呢,前面我们说了,freertos中的任务状态:
运行态
就绪态
阻塞态
挂起态
那任务是如何体现自己当前是哪个态的呢?
答案:除了运行态 ,任务状态的其他三种 任务都有其对应的任务状态链表
FreeRTOS 使用这些任务状态列表来管理处于不同状态的任务
task.c中对任务状态链表的定义如下:
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; PRIVILEGED_DATA static List_t xDelayedTaskList1; PRIVILEGED_DATA static List_t xDelayedTaskList2; PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; PRIVILEGED_DATA static List_t xPendingReadyList;
可以看到,
就绪态任务链表,是一个链表数组,数组大小=最大任务优先级
就是说,每种优先级的任务,全部处在一个链表中
阻塞态任务列表,阻塞态任务列表一共有两个,分别为是阻塞态任务列表 1 和阻塞态任务列表 2,并且该有两个阻塞态任务列表指针。
这么做的么的是为了解决任务阻塞时间溢出的问题(超时) ,这个会在后续讲解阻塞相关的内容时,具体分析
挂起态任务列表,被挂起的任务就会被添加到挂起态任务列表中
5. 任务的操作 与 任务状态 下面,要结合任务的创建,删除等任务操作,来看看,如何利用这些链表,来实现任务在不同状态的切换
xTaskCreate() BaseType_t xTaskCreate ( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask) { prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL ); prvAddNewTaskToReadyList( pxNewTCB ); xReturn = pdPASS; }
prvInitialiseNewTask() 初始化TCB 那么核心就是prvInitialiseNewTask()他如何初始化一个任务TCB的,新创建的任务,是什么状态的?
static void prvInitialiseNewTask ( TaskFunction_t pxTaskCode, const char * const pcName, 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 ( portUSING_MPU_WRAPPERS == 1 ) #endif #if ( tskSET_NEW_STACKS_TO_KNOWN_VALUE == 1 ) { ( void ) memset ( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof ( StackType_t ) ); } #endif #if ( portSTACK_GROWTH < 0 ) { pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] ); pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) ); configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) ); #if ( configRECORD_STACK_HIGH_ADDRESS == 1 ) { pxNewTCB->pxEndOfStack = pxTopOfStack; } #endif } #else { } #endif if ( pcName != NULL ) { for ( x = ( UBaseType_t ) 0 ; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ) { pxNewTCB->pcTaskName[ x ] = pcName[ x ]; if ( pcName[ x ] == ( char ) 0x00 ) { break ; } else { mtCOVERAGE_TEST_MARKER(); } } pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0' ; } else { pxNewTCB->pcTaskName[ 0 ] = 0x00 ; } configASSERT( uxPriority < configMAX_PRIORITIES ); if ( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ) { uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U ; } else { mtCOVERAGE_TEST_MARKER(); } pxNewTCB->uxPriority = uxPriority; #if ( configUSE_MUTEXES == 1 ) { pxNewTCB->uxBasePriority = uxPriority; pxNewTCB->uxMutexesHeld = 0 ; } #endif vListInitialiseItem( &( pxNewTCB->xStateListItem ) ); vListInitialiseItem( &( pxNewTCB->xEventListItem ) ); listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB ); listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB ); #if ( portCRITICAL_NESTING_IN_TCB == 1 ) { pxNewTCB->uxCriticalNesting = ( UBaseType_t ) 0U ; } #endif #if ( configUSE_APPLICATION_TASK_TAG == 1 ) { pxNewTCB->pxTaskTag = NULL ; } #endif #if ( configGENERATE_RUN_TIME_STATS == 1 ) { pxNewTCB->ulRunTimeCounter = ( configRUN_TIME_COUNTER_TYPE ) 0 ; } #endif #if ( portUSING_MPU_WRAPPERS == 1 ) { vPortStoreTaskMPUSettings( &( pxNewTCB->xMPUSettings ), xRegions, pxNewTCB->pxStack, ulStackDepth ); } #else { ( void ) xRegions; } #endif #if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 ) { memset ( ( void * ) &( pxNewTCB->pvThreadLocalStoragePointers[ 0 ] ), 0x00 , sizeof ( pxNewTCB->pvThreadLocalStoragePointers ) ); } #endif #if ( configUSE_TASK_NOTIFICATIONS == 1 ) { memset ( ( void * ) &( pxNewTCB->ulNotifiedValue[ 0 ] ), 0x00 , sizeof ( pxNewTCB->ulNotifiedValue ) ); memset ( ( void * ) &( pxNewTCB->ucNotifyState[ 0 ] ), 0x00 , sizeof ( pxNewTCB->ucNotifyState ) ); } #endif #if ( configUSE_NEWLIB_REENTRANT == 1 ) { _REENT_INIT_PTR( ( &( pxNewTCB->xNewLib_reent ) ) ); } #endif #if ( INCLUDE_xTaskAbortDelay == 1 ) { pxNewTCB->ucDelayAborted = pdFALSE; } #endif #if ( portUSING_MPU_WRAPPERS == 1 ) { } #else { #if ( portHAS_STACK_OVERFLOW_CHECKING == 1 ) { #if ( portSTACK_GROWTH < 0 ) { pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxNewTCB->pxStack, pxTaskCode, pvParameters ); } #else { } #endif } #else { pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); } #endif } #endif if ( pxCreatedTask != NULL ) { *pxCreatedTask = ( TaskHandle_t ) pxNewTCB; } else { mtCOVERAGE_TEST_MARKER(); } }
以上就是函数 prvInitialiseNewTask()的具体代码,可以看到函数 prvInitialiseNewTask()就是初始化了任务控制块TCB中的成员变量 ,其中比较重要的操作就是调用函数 pxPortInitialiseStack()初始化了任务栈。
那么pxPortInitialiseStack()他是如何初始化任务栈 的呢?
pxPortInitialiseStack() 初始化任务栈
这里的初始化栈,就是要在任务切换到这个task的时候,从栈中弹出要执行的环境,就是用以恢复该任务的上下文 包括 xPSR 寄存器的初始值、任务的函数地址(PC 寄存器)、任务错误退出函数地址(LR 寄存器)、任务函数的传入参数(R0 寄存器)以及为 R1~R12 寄存器预留空间,若使用了浮点单元,那么还会有 EXC_RETURN 的值。 同时该函数会返回更新后的栈顶指针
注意 : 针 对 ARM Cortex-M3 和针对 ARM Cortex-M4 和 ARM Cortex-M7 内 核 的 函 数 pxPortInitialiseStack()稍有不同,原因在于 ARM Cortex-M4 和 ARM Cortex-M7 内核具有浮点单 元,因此在任务栈中还需保存浮点寄存器的值
StackType_t * pxPortInitialiseStack ( StackType_t * pxTopOfStack, TaskFunction_t pxCode, void * pvParameters ) { pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError; pxTopOfStack -= 5 ; *pxTopOfStack = ( StackType_t ) pvParameters; pxTopOfStack -= 8 ; return pxTopOfStack; }
如果是M4、M7的内核,还有什么浮点数相关的地方,也要预留栈的位置
最终,初始化任务栈构造出来的栈内容如下所示
prvAddNewTaskToReadyList() 加入就绪队列 prvAddNewTaskToReadList()用于将新建的任务添加到就绪态任务列表中
函数同样定义在task.c中
static void prvAddNewTaskToReadyList ( TCB_t * pxNewTCB ) { taskENTER_CRITICAL(); { uxCurrentNumberOfTasks++; if ( pxCurrentTCB == NULL ) { pxCurrentTCB = pxNewTCB; if ( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 ) { prvInitialiseTaskLists(); } else { mtCOVERAGE_TEST_MARKER(); } } else { if ( xSchedulerRunning == pdFALSE ) { if ( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ) { pxCurrentTCB = pxNewTCB; } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } uxTaskNumber++; #if ( configUSE_TRACE_FACILITY == 1 ) { pxNewTCB->uxTCBNumber = uxTaskNumber; } #endif traceTASK_CREATE( pxNewTCB ); prvAddTaskToReadyList( pxNewTCB ); portSETUP_TCB( pxNewTCB ); } taskEXIT_CRITICAL(); if ( xSchedulerRunning != pdFALSE ) { if ( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority ) { taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } }
总之,prvAddTaskToReadyList()主要就完成两件事:
记录任务优先级(FreeRTOS 会以位图的方式记录就绪态任务列表中就绪态任务的优先级,这样能够提 高切换任务时的效率)
根据任务调度器的运行状态,决 定 是 否 进 行 任 务 切 换,taskYIELD_IF_USING_PREEMPTION()进行任务切换,关于任务切换,后面讨论
vTaskDelete() 删除任务 task.c中定义
void vTaskDelete ( TaskHandle_t xTaskToDelete ) { TCB_t * pxTCB; taskENTER_CRITICAL(); { pxTCB = prvGetTCBFromHandle( xTaskToDelete ); if ( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) { 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++; if ( pxTCB == pxCurrentTCB ) { vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) ); ++uxDeletedTasksWaitingCleanUp; traceTASK_DELETE( pxTCB ); portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending ); } else { --uxCurrentNumberOfTasks; traceTASK_DELETE( pxTCB ); prvResetNextTaskUnblockTime(); } } taskEXIT_CRITICAL(); if ( pxTCB != pxCurrentTCB ) { prvDeleteTCB( pxTCB ); } if ( xSchedulerRunning != pdFALSE ) { if ( pxTCB == pxCurrentTCB ) { configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } }
从上面可以看出,vTaskDelete()删除任务时,需要考虑两种情况,当前要删除的任务:
是正在执行的任务(需要将当前任务添加到任务待删除列表中,空闲任务会处理这个任务待删除列表,将待删除的任务统一删除,后面空闲任务详细分析)
不是正在执行的任务(直接删除prvDeleteTCB())
prvDeleteTCB() 具体删除任务 static void prvDeleteTCB ( TCB_t * pxTCB ) { #if ( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) && ( portUSING_MPU_WRAPPERS == 0 ) ) { vPortFreeStack( pxTCB->pxStack ); vPortFree( pxTCB ); } #elif ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) { if ( pxTCB->ucStaticallyAllocated == tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB ) { vPortFreeStack( pxTCB->pxStack ); vPortFree( pxTCB ); } else if ( pxTCB->ucStaticallyAllocated == tskSTATICALLY_ALLOCATED_STACK_ONLY ) { vPortFree( pxTCB ); } else { configASSERT( pxTCB->ucStaticallyAllocated == tskSTATICALLY_ALLOCATED_STACK_AND_TCB ); mtCOVERAGE_TEST_MARKER(); } } #endif }
vTaskSuspend() 挂起任务 task.c中定义
void vTaskSuspend ( TaskHandle_t xTaskToSuspend ) { TCB_t * pxTCB; taskENTER_CRITICAL(); { pxTCB = prvGetTCBFromHandle( xTaskToSuspend ); traceTASK_SUSPEND( pxTCB ); if ( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) { 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->xStateListItem ) ); #if ( configUSE_TASK_NOTIFICATIONS == 1 ) { BaseType_t x; for ( x = 0 ; x < configTASK_NOTIFICATION_ARRAY_ENTRIES; x++ ) { if ( pxTCB->ucNotifyState[ x ] == taskWAITING_NOTIFICATION ) { pxTCB->ucNotifyState[ x ] = taskNOT_WAITING_NOTIFICATION; } } } #endif } taskEXIT_CRITICAL(); if ( xSchedulerRunning != pdFALSE ) { taskENTER_CRITICAL(); { prvResetNextTaskUnblockTime(); } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } if ( pxTCB == pxCurrentTCB ) { if ( xSchedulerRunning != pdFALSE ) { configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); } else { if ( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) == uxCurrentNumberOfTasks ) { pxCurrentTCB = NULL ; } else { vTaskSwitchContext(); } } } else { mtCOVERAGE_TEST_MARKER(); } }
A. 关于更新下一个任务的阻塞超时时间,以防被挂起的任务就是下一个阻塞超时的任务 的问题: 先要知道:
在 FreeRTOS 中,如果有多个任务进入了阻塞态(例如调用了 vTaskDelay),它们会被放入“延时列表(Delayed Task List)”。这个列表是按唤醒时间升序排列的。
常规做法:在每一次系统节拍中断(Tick Interrupt)中,扫描整个延时列表,看看有没有任务到期。但这太浪费 CPU 时间了。
FreeRTOS 的优化:内核维护了一个变量 xNextTaskUnblockTime,它记录了延时列表中最快要唤醒的那一个任务的时间点 。
带来的好处:在 Tick 中断里,内核只需要简单判断一下:if (xTickCount < xNextTaskUnblockTime)。如果当前时间还没到,说明列表里所有任务都没到期,中断可以立即退出。
B. 为什么要重置?(情景模拟)
假设现在有三个任务在延时列表中: 任务 A:唤醒时间点为 100 任务 B:唤醒时间点为 150 任务 C:唤醒时间点为 200 此时,全局变量 xNextTaskUnblockTime = 100 。 如果你现在调用 vTaskSuspend(任务 A): 任务 A 会从“延时列表”中被移除,搬移到“挂起列表(Suspended List)”。 危机出现:此时延时列表中最快要唤醒的任务变成了任务 B(时间点 150 )。 结果:如果此时不更新 xNextTaskUnblockTime,它的值依然是 100 。 如果不更新会发生什么? 当系统时间(Tick Count)到达 100 时,Tick 中断触发,内核发现 xTickCount >= xNextTaskUnblockTime,于是满心欢喜地去延时列表找任务 A,结果发现列表里第一个任务是 B(150 ),A 已经不见了。这意味着内核在时间点 100 做了一次无意义的唤醒检查。
C. 关于pxCurrentTCB
1. 什么是 pxCurrentTCB?pxCurrentTCB 是 FreeRTOS 内核中最重要的全局指针,它永远指向当前正在占用 CPU 的那个任务的任务控制块(TCB)。 运行态的标志: 在任何时刻,CPU 正在执行哪行代码,那个代码所属任务的 TCB 地址就存在 pxCurrentTCB 里。 切换的基准: 调度器进行上下文切换时,本质就是把旧任务的现场保存到旧的 pxCurrentTCB 指向的栈里,然后让 pxCurrentTCB 指向新任务,再从新栈里恢复现场。
D. 如果是自挂,且在运行时,portYIELD_WITHIN_API()的实际作用是什么?
在 Cortex-M 架构中,PendSV(Pended Service Call)是一个非常特殊的系统异常。 "Pend" (悬挂)普通的硬件中断(如串口接收)是“立刻执行”的,但 PendSV 是可以“延迟执行”的。 "手动挂起" : 你代码中的 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 实际上是向 CPU 发出了一个信号:“我这里有个换人的活儿要干,先在挂号处登记一下(Pending)。”等待时机: "CPU 不会立刻跳进 PendSV" 。它会先看一眼:“现在还有没有比 PendSV 优先级更高的中断在跑?” 如果有,CPU 先把紧急的中断跑完。 如果没有,或者等所有紧急中断都退出了,CPU 才会进入 "PendSV_Handler" 。 现在的逻辑(使用 PendSV): 任务自挂: 任务 A 调用 vTaskSuspend()。 登记: vPortYield() 登记了一个 PendSV 请求。 安全检查: CPU 检查发现当前没有其他中断,或者等到其他中断都跑完了。 换人: 进入最低优先级的 PendSV_Handler,此时它是系统里唯一在跑的异常,可以安全地保存任务 A 的现场,切换 pxCurrentTCB 到任务 B,并恢复任务 B 的现场。
所以本质上,任务的切换 ,本质上是依赖pendSV中断(最低优先级)来实现 的。
而实际在触发pendSV中断前,用vPortYield()登记的时候,里面有两条汇编指令:
__dsb( portSY_FULL_READ_WRITE ); __isb( portSY_FULL_READ_WRITE );
总结一下自挂的逻辑 :
发起请求: vTaskSuspend() 发现你要挂起自己,它把你从就绪链表挪到挂起链表。
呼叫后台: 调用 vPortYield(),往 ICSR 寄存器里写一个比特位,触发 PendSV 登记。
等待调度: 只要没有其他更高优先级的中断,CPU 就会立刻跳进 PendSV_Handler(这是由 FreeRTOS 写的汇编函数)。
现场搬运:
把 CPU 现在的 $R4-R11$ 寄存器压入任务 A 的堆栈。
把新的栈指针存入任务 A 的 TCB。
找到 pxCurrentTCB 指向的新任务(比如任务 B)。
从任务 B 的堆栈里弹出 $R4-R11$ 到 CPU 寄存器。
重生: 异常返回,CPU 此时拿的是任务 B 的寄存器和堆栈,任务 B 开始运行。
vPortYield() 只是“登记”,真正的魔术全在汇编函数 xPortPendSVHandler 里面。
补充:pendSV 保存当前任务现场操作 1. CM3内核寄存器 :
Cortex-M3 内核总共有 16 个主要寄存器:
通用寄存器:$R0$ - $R12$
栈指针 (SP):$R13$
链接寄存器 (LR):$R14$
程序计数器 (PC):$R15$
状态寄存器:$xPSR$
当 PendSV 中断触发时,硬件和软件会分工合作,共同完成这 16 个寄存器的保护
2. pendSV中断触发,硬件自动工作 :
硬件自动压栈 (Hardware Stacking)当异常发生的一瞬间,CPU 硬件会自动将以下 8 个寄存器压入当前任务的栈中:
$xPSR$
$PC$ ($R15$)
$LR$ ($R14$)
$R12$
$R3$
$R2$
$R1$
$R0$
为什么是这几个? 根据 ARM 的 AAPCS 标准,这些被称为“调用者保存(Caller-saved)”寄存器。硬件自动保存它们是为了保证中断退出后,C 语言环境的函数逻辑不会乱套。
3. pendSV中断触发,软件手动工作 :
剩下的寄存器硬件管不着,必须由 FreeRTOS 在汇编(xPortPendSVHandler)中手动保存:
$R4, R5, R6, R7, R8, R9, R10, R11$
这些被称为“被调用者保存(Callee-saved)”寄存器。因为硬件不负责它们,所以 FreeRTOS 必须手动用一条汇编指令 stmdb sp!, {r4-r11} 把它们排队送入栈中。
注意这里的SP仍然使用的是PSP,所以保存的是A的任务栈中。以上当前任务的PC指针,各种环境都已经保存了。
4. CM3的双栈机制
以上可以得知,在pendSV中断中,处理压栈,此时SP指向的是当前任务栈(使用的是PSP)
在 Cortex-M3 中,R13 寄存器(即 SP)实际上有两个物理备份:
MSP (Main Stack Pointer): 主堆栈指针。用于 OS 内核、中断服务程序(ISR)。
PSP (Process Stack Pointer): 进程堆栈指针。专门用于任务(Task)。
补充:pendSV中断实现任务切换的详细过程 我们把这个过程拆解为三个清晰的阶段:
1. 第一阶段:硬件自动“序幕” (Hardware Entry)
当 PendSV 被悬起(Pending)且 CPU 决定执行它时,硬件会在执行汇编第一行代码前 完成以下动作:
判定当前栈指针 :CPU 发现当前任务正在使用$PSP$ 。
硬件压栈 (Hardware Stacking) :CPU 自动向 $PSP$ 指向的内存压入 8 个寄存器($xPSR, PC, LR, R12, R3, R2, R1, R0$)。此时 $PSP$ 的值会自动减小。
模式与 SP 切换 :
CPU 从线程模式 进入处理者模式 (Handler Mode)。
关键: 硬件自动将当前使用的 SP 从 PSP 切换到 MSP。
PC 跳转 :CPU 从向量表中取 PendSV_Handler 的地址并赋值给 PC。
[!IMPORTANT]结论: 当你在 PendSV_Handler 汇编里看到的第一行指令执行时,此时的 SP 已经变成 MSP 了,而刚才压了一半寄存器的 PSP 正孤零零地指在任务栈的某个位置。
2. 第二阶段:软件手动“核心” (Software/Assembly)
现在进入了 FreeRTOS 的汇编代码(xPortPendSVHandler)。因为硬件只存了“一半”现场,剩下的由软件补齐:
取出 PSP :由于现在的 SP 是 MSP,软件第一步必须执行 mrs r0, psp,把那个任务栈的地址($PSP$)抢救回寄存器 R0 中。
软件压栈 (Software Stacking) :利用刚才取回的$R0$(即 $PSP$),手动执行汇编指令将 $R4$ 至 $R11$压入任务栈。
指令:stmdb r0!, {r4-r11}( 指针会继续向下移动)。
保存 SP 到 TCB :此时 $R0$ 指向的是整个 16 个寄存器“全家福”的底部。
动作:将 $R0$ 的值写入 pxCurrentTCB->pxTopOfStack。至此,旧任务的灵魂被完整封印在它自己的栈里了。
寻找新任务 :调用 vTaskSwitchContext。这个 C 函数会更新 pxCurrentTCB 指向最高优先级的就绪任务。
重点在于调用了函数 taskSELETE_HIGHEST_PRIORITY_TASK() 更新pxCurrentTCB 指向优先级最高的就绪态任务
恢复新任务 SP :从新的 pxCurrentTCB->pxTopOfStack 中取出值存入 。
软件出栈 :手动弹出新任务栈里的 至 。
更新 PSP :将恢复后的 (指向剩下 8 个寄存器的地方)重新写回 寄存器。
3. 第三阶段:硬件自动“尾声” (Hardware Exit)
汇编代码的最后一行通常是 bx lr(此时 是一个特殊的跳转值 0xFFFFFFFD)。
触发硬件返回 :当硬件看到 $LR$ 是这个特殊值时,它知道异常结束了。
自动切换回 PSP :硬件根据返回指令,自动把 $SP$ 从 $MSP$ 切回 $PSP$。
硬件出栈 (Hardware Unstacking) :硬件自动从 $PSP$ 中弹出剩下的 8 个寄存器(($R0-xPSR$)。)。
恢复运行 :此时 $PC$ 被恢复为新任务上次离开时的位置,新任务正式复活。
4. 梳理对比表
为了防止混淆,请看这个精确的先后顺序:
顺序
执行者
动作
使用的 SP
1
硬件
压入 $R0-xPSR$
$PSP$
2
硬件
切到 $MSP$,跳转到 Handler 地址
$MSP$
3
软件
手动压入 $R4-R11$
通过 $R0$ 间接操作 $PSP$
4
软件
更新 pxCurrentTCB (寻找新任务)
$MSP$
5
软件
手动弹出新任务的 $R4-R11$
通过 $R0$ 间接操作 $PSP$
6
硬件
切回 $PSP$,弹出 $R0-xPSR$
$PSP$
你已经打通了任务切换的“任督二脉”。现在你可能会好奇:“既然任务切换靠 PendSV,那如果我在代码里写一个死循环而不调用任何 API(不触发 Yield),FreeRTOS 还能切换任务吗?”
这就涉及到了 SysTick 这种“强制打断”的机制。你想聊聊 SysTick 是如何通过“强行挂号” PendSV 来实现时间片轮转的吗?
恢复任务 恢复任务和挂起类似,比较简单。都需要判断
确保有待恢复的任务
确保恢复的不是当前正在运行的任务
进入临界区
从 挂起态任务列表中,移除该节点
添加到就绪态任务链表中
判断优先级,是否需要进行任务抢占切换
退出临界区
6. 空闲任务 空闲任务 主要用于处理待删除任务列表 和低功耗
task.c中定义:
static portTASK_FUNCTION ( prvIdleTask, pvParameters ) { ( void ) pvParameters; portALLOCATE_SECURE_CONTEXT( configMINIMAL_SECURE_STACK_SIZE ); for ( ; ; ) { prvCheckTasksWaitingTermination(); #if ( configUSE_PREEMPTION == 0 ) { taskYIELD(); } #endif #if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) { if ( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 ) { taskYIELD(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif #if ( configUSE_IDLE_HOOK == 1 ) { extern void vApplicationIdleHook ( void ) ; vApplicationIdleHook(); } #endif #if ( configUSE_TICKLESS_IDLE != 0 ) { TickType_t xExpectedIdleTime; xExpectedIdleTime = prvGetExpectedIdleTime(); if ( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) { vTaskSuspendAll(); { configASSERT( xNextTaskUnblockTime >= xTickCount ); xExpectedIdleTime = prvGetExpectedIdleTime(); configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING( xExpectedIdleTime ); if ( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) { traceLOW_POWER_IDLE_BEGIN(); portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ); traceLOW_POWER_IDLE_END(); } else { mtCOVERAGE_TEST_MARKER(); } } ( void ) xTaskResumeAll(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif } }
任务切换 PendSV(Pended Service Call,可挂起服务调用)
PendSV的中断优先级是可以编程的
PendSV 的中断由将中断控制状态寄存器(ICSR)中 PENDSVSET 为置一触发
PendSV 与 SVC 不同,PendSV 的中断是非实时的, 即 PendSV 的中断可以在更高优先级的中断中触发,但是在更高优先级中断结束后才执行
利用 PendSV 的这个可挂起特性,在设计 RTOS 时,可以将 PendSV 的中断优先级设置为最低的中断优先级, 任务切换时,就需要用到 PendSV 的这个特性
下面看一下任务切换的基本概念:
在典型的 RTOS 中,任务的处理时间被分为多个时间片 ,OS 内核 的执行可以有两种触发方式
一种是通过在应用任务中通过 SVC 指令触发,
例如在应用任务在等待某个时间发生而需要停止的时候,那么就可以通过 SVC 指令来触发 OS 内核的执行,以切换到其他任务;
第二种方式是,SysTick 周期性的中断,来触发 OS 内核的执行。
图演示了只有两个任务的 RTOS 中,两个任务交替执行的过程:
在操作系统中,任务调度器决定是否切换任务。图 9.1.1 中的任务及切换都是在 SysTick 中 断中完成的,SysTick 的每一次中断都会切换到其他任务
如果一个中断请求(IRQ)在 SysTick 中断产生之前产生,那么 SysTick 就可能抢占该中断 请求,这就会导致该中断请求被延迟处理,这在实时操作系统中是不允许的 ,因为这将会影响 到实时操作系统的实时性
并且,当 SysTick 完成任务的上下文切换,准备返回任务中运行时,由于存在中断请求, ARM Cortex-M 不允许返回线程模式,因此,将会产生用法错误异常(Usage Fault)。
在一些 RTOS 的设计中,会通过判断是否存在中断请求,来决定是否进行任务切换。虽然 可以通过检查 xPSR 或 NVIC 中的中断活跃寄存器来判断是否存在中断请求,但是这样可能会 影响系统的性能,甚至可能出现中断源在 SysTick 中断前后不断产生中断请求,导致系统无法 进行任务切换的情况。
PendSV 通过延迟执行任务切换,直到处理完所有的中断请求,以解决上述问题。为了达到 这样的效果,必须将 PendSV 的中断优先级设置为最低的中断优先等级。如果操作系统决定切 换任务,那么就将 PendSV 设置为挂起状态,并在 PendSV 的中断服务函数中执行任务切换
SVC中断,pendSV中断,systick中断 1. SVC中断: 作用单一:它主要用于启动第一个任务。
它的使命:当你调用 vTaskStartScheduler() 启动调度器时,最终会触发 svc 0。
执行过程:
vPortSVCHandler 汇编函数被激活。
它去获取最高优先级任务的栈顶指针(pxCurrentTCB->pxTopOfStack)。
它手动恢复该任务的寄存器环境。
最后通过修改 CONTROL 寄存器,让 CPU 切换到使用 PSP,并PC跳转到任务入口。
为什么只用一次?:一旦第一个任务跑起来了,后续的任务切换全部交给 PendSV 处理。SVC 在 FreeRTOS 的标准移植层里基本就“光荣退休”了。
2. systick中断 这个中断的优先级,依然是最低优先级,作用就是系统节拍计数+维护延迟阻塞任务列表到就绪列表
它的核心职责:
维持时间轴:每次中断,全局变量 xTickCount 加 1。
唤醒任务:检查“延时列表”里有没有任务的时间到了。如果有任务该醒了,就把它从延时列表挪回“就绪列表”。
强制打断(时间片轮转):如果配置了 configUSE_PREEMPTION(抢占式),SysTick 会检查当前任务是否跑得太久了,或者是否有更高优先级的任务刚醒过来。
它与 PendSV 的联动: 如果 SysTick 发现需要换人,它不会自己去换。它会顺手悬起(Pending)一个 PendSV 中断。
形象理解:SysTick 发现时间到了,在黑板上写下“该换人了”,然后退出。因为 PendSV 也是最低优先级,CPU 接着就会自动进入 PendSV 去执行真正的搬运工作。
场景1:systick中断,判断抢占式调度
——“有更重要的人醒了”
即便当前任务没有运行太久,只要有更高优先级的任务“醒了”,SysTick 就会强制打断当前任务。
关键点:由于任务 B(3)的优先级比当前运行的任务 A(2)高,SysTick 会立刻标记需要进行切换(悬起 PendSV)。
结果:这被称为基于优先级的抢占。它不是因为任务 A 跑太久了,而是因为 B 更紧急。
2. 场景二:时间片轮转(针对相同优先级) ——“大家级别一样,换个班吧”
逻辑:如果当前优先级列表里不止一个任务,SysTick 会请求一次任务切换(悬起 PendSV)。
结果:这样在下一次中断后,任务 D 就能跑 1ms,然后换回任务 C。这就是同优先级的时间片轮转。
在 同优先级时间片轮转(Time Slicing)的默认配置下,每个任务执行一个 SysTick 周期(通常是 1ms),然后就会把 CPU 转交给下一个同优先级的任务
[!IMPORTANT] 一般也推荐使用 SysTick 作为 RTOS 的时钟节拍,当然啦,用户也可以用其他的硬件定时器为 RTOS 提供时钟节拍。
systick中断处理函数定义在delay.c中,但是内部的实现,在rtos的源码中提供了相应的回调办法:xPortSysTickHandler
systick_handler() ->xPortSysTickHandler ->xTaskIncrementTick()
3. pendSV中断 这个上面有解析过,负责具体的任务切换。
以上就是freertos的基本实现,后面就是一些使用,比如队列,信号量,互斥量,事件组,任务通知,延迟,定时器等,这些都比较简单。下一篇会简单介绍。