手写mini-RTOS
最近想温习和巩固一下RTOS的部分内容,所以想尝试通过手写一个小的RTOS来巩固一下知识,也打算写出文档来分享和以后温习作用。
数据类型重定义
首先要说明的是,FreeRTOS对所有的数据类型基本都做了一次重定义,定义的文件在portmacro.h这个文件里面,第一次使用需要自行创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #ifndef PORTMACRO_H #define PORTMACRO_H
#include"stdint.h" #include"stddef.h"
#define portCHAR char #define portFLOAT float #define portDOUBLE double #define portLONG long #define portSHORT short #define portSTACK_TYPE uint32_t #define portBASE_TYPE long
typedef portSTACK_TYPE StackType_t; typedef long BaseType_t; typedef unsigned long UBaseType_t;
#if( configUSE_16_BIT_TICKS == 1 ) typedef uint16_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffff #else typedef uint32_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffffffffUL #endif
#endif
|
其中用的比较多的就是 StackType_t ,这个是专门用于定义任务的栈,就是之后每个任务的私有领地。还有 TickType_t 则是心跳计数,可以理解为计时器的一个基本单元。
基础配置项
这个是给用户来做一些基础配置的,在FreeRTOSConfig.h的文件里面
1 2 3 4 5 6
| #ifndef FREERTOS_CONFIG_H #define FREERTOS_CONFIG_H
#define configUSE_16_BIT_TICKS 0
#endif
|
在这里面,我们把 configUSE_16_BIT_TICKS 设置为了0,也就是使用的是32位的mcu,Cortex-M3、M4 和 M7,都是32位的mcu
列表和列表项
在RTOS中,列表项就是指的是链表项,列表就是指的链表,所以这里的所有内容都是关于链表的使用,而在RTOS中,所使用的链表是双向链表,就是每一个链表项都可以访问前一个和后一个链表项,并且有一个尾链表,来表示链表的结尾。每一条链表,都同时有一个链表管理者,这个不会参与到具体的链表里面,而是在外面定义的,里面的一些项会指向这个链表,也就是说,可以通过这个链表管理者,准确找到对应的链表。
数据结构
首先先是链表和链表项的一些基础的定义,这些都是在list.h里面,如果第一次使用就需要创建。
基础链表项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct xLIST_ITEM {
TickType_t xItemValue;
struct xLIST_ITEM *pxNext;
struct xLIST_ITEM *pxPrevious;
void *pvOwner;
void *pvContainer;
};
typedef struct xLIST_ITEM ListItem_t;
|
讲一下各项的基础含义:
pxNext:指向下一个列表项的指针。
pxPrevious:指向前一个列表项的指针,跟pxNext共同组成,是双向链表的核心
xItemValue:列表项的值,在RTOS中,通过这个值来辅助链表项的之间的排序
pvOwner:指向拥有该列表项的父对象(通常是任务控制块TCB)的指针。
pvContainer:这个就是指向链表管理者的指针。
结束链表项:
1 2 3 4 5 6 7
| struct xMINI_LIST_ITEM { TickType_t xItemValue; struct xLIST_ITEM *pxNext; struct xLIST_ITEM *pxPrevious; }; typedef struct xMINI_LIST_ITEM Mini_List_Item_t;
|
含义基本和普通链表项差不多,只是少了一些内容,其中它的 pvOwner 和 pvContainer 被省略了,并且它的 pxPrevious 指向的就是链表的第一个,构成了一个回环。
链表管理者
1 2 3 4 5 6 7
| struct xList { UBaseType_t uxNumberOfItems; ListItem_t *pxIndex; Mini_List_Item_t xListEnd; }; typedef struct xList List_t;
|
讲一下各项的基础含义:
uxNumberOfItems:当前链表中列表项的数量。
pxIndex:指向当前列表项的指针,用于辅助遍历链表,表示之前遍历到的位置。
xListEnd:链表的结束项,它指向的就是这个链表的结束项
列表函数
列表项初始化函数
1 2 3 4
| void vListInitialiseItem( ListItem_t * const pxItem ) { pxItem->pvContainer = NULL; }
|
这个函数比较简单,就简单初始化了一个列表项,把它的管理者设置为了null。
列表管理者初始化函数
1 2 3 4 5 6 7 8 9 10 11 12
| 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; }
|
这个函数就是初始化了列表管理者,也就是正式创建了一个链表,可以看到这个链表只有一个尾节点,尾节点的next和previous都指向它自己。
列表尾部插入函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem ) { ListItem_t * const pxIndex = pxList->pxIndex; pxNewListItem->pxNext = ( ListItem_t * ) &( pxList->xListEnd ); pxNewListItem->pxPrevious = pxIndex->pxPrevious; pxIndex->pxPrevious->pxNext = pxNewListItem; pxIndex->pxPrevious = pxNewListItem; pxNewListItem->pvContainer = ( void * ) pxList; ( pxList->uxNumberOfItems )++; }
|
这个建议自己想象地尝试理解,链表的连接都是通过它们本身人为存进去的next和previous的指针,链表加入新的项必须要注意的就是不能把原来的链表搞断裂了,必须要同时维护好其他的指针。
列表按序插入函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem ) { ListItem_t *pxIterator; BaseType_t xInsertAfterEqual = ( BaseType_t ) 0; const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) { if( xValueOfInsertion == pxIterator->pxNext->xItemValue ) { xInsertAfterEqual = ( BaseType_t ) 1; } } if( xInsertAfterEqual != ( BaseType_t ) 0 ) { pxIterator = pxIterator->pxNext; } pxNewListItem->pxNext = pxIterator->pxNext; pxNewListItem->pxPrevious = pxIterator; pxIterator->pxNext->pxPrevious = pxNewListItem; pxIterator->pxNext = pxNewListItem; pxNewListItem->pvContainer = ( void * ) pxList; ( pxList->uxNumberOfItems )++; }
|
其实原理和尾部插入差不多,但是这里引入了 xItemValue 其实也就是多了一个遍历链表的步骤,遍历整个链表,找到一个大小合适的位置插入链表项。
列表删除函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove ) { List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer; pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext; pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; if( pxList->pxIndex == pxItemToRemove ) { pxList->pxIndex = pxItemToRemove->pxNext; } pxItemToRemove->pvContainer = NULL; return --( pxList->uxNumberOfItems );
}
|
这个与列表按序插入函数大同小异,先找到需要删除的项的管理者,因为链表是双向的,所以直接把前后两个链表项连接起来即可,这里就体现了链表的魅力,不需要手动删除那个位置的数据,而是软删除,把数据的位置抹除。
列表额外小方法
链表的处理还有一些额外的比较简单的小方法,这些东西就不需要单独做为一个函数,而是直接通过宏定义来实现,把这些放到 list.h 里面即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| #define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner )\ ( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) )
#define listGET_LIST_ITEM_OWNER( pxListItem )\ ( ( pxListItem )->pvOwner )
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue )\ ( ( pxListItem )->xItemValue = ( xValue ) )
#define listGET_LIST_ITEM_VALUE( pxListItem )\ ( ( pxListItem )->xItemValue )
#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList )\ ( ( ( pxList )->xListEnd ).pxNext->xItemValue )
#define listGET_HEAD_ENTRY( pxList )\ ( ( ( pxList )->xListEnd ).pxNext )
#define listGET_NEXT( pxListItem )\ ( ( pxListItem )->pxNext )
#define listGET_END_MARKER( pxList )\ ( ( ListItem_t const * ) ( &( ( pxList )->xListEnd ) ) )
#define listLIST_IS_EMPTY( pxList )\ ( ( BaseType_t ) ( ( pxList )->uxNumberOfItems == ( UBaseType_t ) )
#define listCURRENT_LIST_LENGTH( pxList )\ ( ( pxList )->uxNumberOfItems )
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) { List_t * const pxConstList = ( pxList ); ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) { ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; } ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; }
|
任务
任务定义
任务栈
既然是一个任务,就相当于一个程序,那么自然,必须要有相对应的栈,每个任务都有对应的栈,在创建任务的时候必须要事先规定好栈的大小。
1 2 3 4
| #define TASK1_STACK_SIZE 128 StackType_t Task1Stack[TASK1_STACK_SIZE]; #define TASK2_STACK_SIZE 128 StackType_t Task2Stack[TASK2_STACK_SIZE];
|
任务函数
这个可以自己随意定义,要注意,这些任务是无限循环并且不能有返回值。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void delay (uint32_t count) { for (; count!=0; count--); }
void Task1_Entry( void *p_arg )(1) { for ( ;; ) { flag1 = 1; delay( 100 ); flag1 = 0; delay( 100 ); } }
void Task2_Entry( void *p_arg )(2) { for ( ;; ) { flag2 = 1; delay( 100 ); flag2 = 0; delay( 100 ); } }
|
任务控制块
任务控制块(TCB)是用来存储任务信息的结构体,相当于任务的管理者,我们所有的对任务的操作都是通过控制任务管理者来实现的,每个任务都有一个TCB。
这个实际上是放在 task.c 里面的,至于为什么,之后在讲,不过为了别的文件也可以引入,就先放在 FreeRTOS.h 这个文件里面的,如果没有可以新建一个。
1 2 3 4 5 6 7 8
| typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; ListItem_t xStateListItem; StackType_t *pxStack; char pcTaskName[ configMAX_TASK_NAME_LEN ]; } tskTCB; typedef tskTCB TCB_t;
|
简单讲一下各项的含义吧。
pxTopOfStack:指向任务栈的栈顶,当任务切换时,CPU寄存器的值会保存到这个位置。一般来说,栈底是固定的,但是栈顶是动态的,会随着运行过程中而不断变化,所谓的栈溢出,实际上也是栈顶溢出了,跑出了事先规定的边界。
pxStack:指向任务栈的栈底,栈底是固定的,创建任务的时候,就已经固定下来了。
xStateListItem:任务状态列表项,用于将任务添加到各种任务列表中(例如就绪列表、阻塞列表等),这个实际上就指出了任务当前的状态。
pcTaskName:任务名称,一个字符串,用于调试和识别任务。
任务函数指针
这个是定义在 projdefs.h 里面的,如果没有,也需要自己创建一个
1 2 3 4 5 6 7 8 9 10 11 12
| #ifndef PROJDEFS_H #define PROJDEFS_H
typedef void (*TaskFunction_t)( void * );
#define pdFALSE ( ( BaseType_t ) 0 ) #define pdTRUE ( ( BaseType_t ) 1 )
#define pdPASS ( pdTRUE ) #define pdFAIL ( pdFALSE )
#endif
|
这个是函数指针,指向的是一个函数,在这里其实指向的也就是任务函数,其实也就是你的函数名。
任务句柄
这个是在 task.h 中定义的,表示任务句柄,根据c语言的语法来讲,这个是一个万能的指针,是一个4字节的地址,RTOS用这个来专门指向TCB。
1
| typedef void * TaskHandle_t;
|
但是为什么要专门用一个万能指针来指向TCB呢,这个实际上是体现了FreeRTOS作者设计的巧妙,可以很好地保障系统的稳定性,因为 task.h 对外暴露出的接口,就只有一个 TaskHandle_t 但是这是一个万能指针,所以用户就不可能直接修改TCB的内部的值(因为这是一个万能的指针,用户不知道里面的实际内容,但是内核知道),只能通过 task.c 来实现对任务的控制,维护了系统的稳定。
任务函数
任务创建函数
configSUPPORT_STATIC_ALLOCATION 这个宏定义是在 FreeRTOSConfig.h 里面的,也就是允许创建静态函数,让用户可以自定义栈底的位置来创建,而不是RTOS来帮你分配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #if (configSUPPORT_STATIC_ALLOCATION == 1)
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, const char *const pcName, const uint32_t ulStackDepth, void *const pvParameters, StackType_t *const puxStackBuffer, TCB_t *const pxTaskBuffer ) { TCB_t *pxNewTCB; TaskHandle_t xReturn; if ((pxTaskBuffer != NULL) && (puxStackBuffer != NULL)) { pxNewTCB = (TCB_t *)pxTaskBuffer; pxNewTCB->pxStack = (StackType_t *)puxStackBuffer; prvInitialiseNewTask(pxTaskCode, pcName, ulStackDepth, pvParameters, &xReturn, pxNewTCB); } else { xReturn = NULL; }
return xReturn; }
#endif
|
看到这个代码,可能又会有疑问了,前面不是说为了保护系统,RTOS不会把 TCB_t 暴露出来吗,为什么这里又暴露出来了,实际上这是因为这是一个mini-RTOS,并不是真正的官方源码,而官方是怎么做的呢。
1 2 3 4 5 6 7
| 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 ) PRIVILEGED_FUNCTION;
|
这个是官方的做法,可以看到,官方用的是一个 StaticTask_t 这样的东西来代替了,那么这个是什么呢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| typedef struct xSTATIC_TCB { void *pxDummy1; #if ( portUSING_MPU_WRAPPERS == 1 ) xMPU_SETTINGS xDummy2; #endif StaticListItem_t xDummy3[ 2 ]; UBaseType_t uxDummy5; void *pxDummy6; uint8_t ucDummy7[ configMAX_TASK_NAME_LEN ]; #if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) ) void *pxDummy8; #endif #if ( portCRITICAL_NESTING_IN_TCB == 1 ) UBaseType_t uxDummy9; #endif #if ( configUSE_TRACE_FACILITY == 1 ) UBaseType_t uxDummy10[ 2 ]; #endif #if ( configUSE_MUTEXES == 1 ) UBaseType_t uxDummy12[ 2 ]; #endif #if ( configUSE_APPLICATION_TASK_TAG == 1 ) void *pxDummy14; #endif #if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) void *pvDummy15[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ]; #endif #if ( configGENERATE_RUN_TIME_STATS == 1 ) uint32_t ulDummy16; #endif #if ( configUSE_NEWLIB_REENTRANT == 1 ) struct _reent xDummy17; #endif #if ( configUSE_TASK_NOTIFICATIONS == 1 ) uint32_t ulDummy18; uint8_t ucDummy19; #endif #if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) uint8_t uxDummy20; #endif #if( INCLUDE_xTaskAbortDelay == 1 ) uint8_t ucDummy21; #endif #if ( configUSE_POSIX_ERRNO == 1 ) int iDummy22; #endif } StaticTask_t;
|
是不是有点感觉好复杂,看不懂,看不懂就对了,这就是官方为了保护任务不被用户擅自修改,随便设置的占位的内容,这个的数据大小和大致的数据结构和 TCB_t 是一模一样的,但是用户如果访问这个的话,那么就只能得到一堆 ucDummy 像这样的,不知所然的东西。
任务创建内部函数
prvInitialiseNewTask 这个函数是 xTaskCreateStatic 内部调用的,用于初始化新任务,其实本质上也就只是把所有的东西都开辟出一个空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, const char *const pcName, const uint32_t ulStackDepth, void *const pvParameters, TaskHandle_t *const pxCreatedTask, TCB_t *pxNewTCB) { StackType_t *pxTopOfStack; UBaseType_t x; pxTopOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1); pxTopOfStack = (StackType_t *)(((uint32_t)pxTopOfStack) & (~((uint32_t)0x0007))); for (x = (UBaseType_t)0; x < (UBaseType_t)configMAX_TASK_NAME_LEN; x++) { pxNewTCB->pcTaskName[x] = pcName[x]; if (pcName[x] == 0x00) { break; } } pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN - 1] = '\0'; vListInitialiseItem(&(pxNewTCB->xStateListItem)); listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB); pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters); if ((void *)pxCreatedTask != NULL) { *pxCreatedTask = (TaskHandle_t)pxNewTCB; } }
|
也就是为任务的栈、任务控制块等开辟空间,然后还顺便把名字写进去了,而其中的8字节对齐,也是为了保证稳定性,在计算一些浮点运算的时候,保障系统不会崩溃。
任务栈初始化函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #define portINITIAL_XPSR (0x01000000) #define portSTART_ADDRESS_MASK ((StackType_t)0xfffffffeUL) static void prvTaskExitError(void) { for (;;) ; } 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; }
|
这里差不多就是FreeRTOS的难点部分了,这个相当于伪造了一个中断的现场,就是中断的时候,CPU各个寄存器的值。
先大概讲一下CPU的寄存器的内容吧。CPU 寄存器有这么些,R0-R12是通用寄存器,然后就是R13,也就是SP,这个是栈指针,指向CPU当前的栈顶,接下来就是R14,也就是链接寄存器LR,这是当前函数执行完返回后,就会执行这里,所以说把 prvTaskExitError 放到这里,就是为了避免任务可返回,继续就是R15,也就是PC,这里存放的是CPU下一条需要执行的指令,我们把任务函数放到了PC寄存器,那么CPU下一步就自然会执行我们的任务函数了,然后就是xPSR寄存器,表示寄存器当前的状态。
但是为什么我们的任务参数要在中间存进去呢,这是因为CPU可以自动压栈和出栈,但不是所有的寄存器都有这个功能,只有任务参数和任务参数之前的内容,CPU会自动把当前栈指针所指的位置和后面的7个数据(也就是R0,PC,LR等那些数据),全部放入它的几个核心寄存器。
就绪列表初始化
就绪列表实际上就是一个数组,这个数组的元素全部都是链表,而这个链表中的链表项就是一个个的任务,其中 configMAX_PRIORITIES 需要在 FreeRTOSConfig.h 中定义,这个就是优先级的个数,定义为5就好了。
1 2 3 4 5 6 7 8 9 10 11
| List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; void prvInitialiseTaskLists( void ) { UBaseType_t uxPriority; for ( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ) { vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) ); } }
|
其实就绪列表的初始化,也就是遍历并且初始化所有的链表。
调度器
启动调度器函数
1 2 3 4 5 6 7 8 9 10 11 12
| TCB_t *pxCurrentTCB void vTaskStartScheduler( void ) {
pxCurrentTCB = &Task1TCB;
if ( xPortStartScheduler() != pdFALSE ) { } }
|
其实就是手动指定当前的任务,然后开启调度。
启动调度器内部函数
xPortStartScheduler 这个函数是启动调度器的核心,它会设置中断,启动第一个任务,并且进入调度循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20))
#define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 16UL) #define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
BaseType_t xPortStartScheduler( void ) { (1) portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
prvStartFirstTask();(2)
return 0; }
|
大致内容就是通过寄存器配置,把PendSV 和 SysTick 的中断优先级降下来,防止干扰到其他硬件中断,而 configKERNEL_INTERRUPT_PRIORITY 一般设置为15,是最小的中断,这个也是放在 FreeRTOSConfig.h 里面,但是这个也不能直接设置为15,因为CPU取得是这里的高四位,而不是第四位,所以应该这样子写:
1
| #define configKERNEL_INTERRUPT_PRIORITY 240
|
开始第一个任务函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| __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 }
|
svc中断函数
这个函数在官方版本里面是命名的 vPortSVCHandler 但是命名成这个,显然是不能被cpu作为中断调用的,所以所以我们需要在宏定义里面,修改中断的名字。
在 FreeRTOSConfig.h 这个文件里面加入宏定义。
1 2 3
| #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler #define vPortSVCHandler SVC_Handler
|
然后就是中断处理函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| __asm void vPortSVCHandler( void ) { extern pxCurrentTCB; 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 }
|
这个函数相当于就是改了svc中断处理函数,在中断里面,修改完cpu的寄存器,引导cpu去执行别的函数。
任务切换函数
任务切换其实就是找就绪列表中的优先级最高的那个任务,然后进入中断,修改cpu的值,执行即可。
主要是 taskYIELD 这个函数,不过这个函数没有直接写成函数了,直接用宏定义做了处理
1 2 3 4 5 6 7 8 9 10
| #define portNVIC_INT_CTRL_REG (*(( volatile uint32_t *) 0xe000ed04)) #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL ) #define portSY_FULL_READ_WRITE #define portYIELD() { portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; __dsb( portSY_FULL_READ_WRITE ); __isb( portSY_FULL_READ_WRITE ); }
|
这个其实比较简单,就是设置寄存器的值,让系统进入PendSV中断,然后在PendSV中断,再写具体的任务切换内容,而后面的 dsp ,意思是保证内存写入完全, isp 则是同步指令,让CPU以最新的系统状态运行,防止CPU预存指令。
PendSV服务函数
这个是PendSV的中断函数,在这里正式进行任务切换,其中configMAX_SYSCALL_INTERRUPT_PRIORITY 是在 FreeRTOSConfig.h 中定义,这里需要定义为191,原因同样是,高四位有效。当然在这里肯定也还是需要对 xPortPendSVHandler 进行宏定义的,因为这明显不是默认的中断回调函数,但是这个宏定义我们其实在之前就已经完成了,所以这里可以不用再重定义了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| __asm void xPortPendSVHandler( void ) { extern pxCurrentTCB; extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp isb
ldr r3, =pxCurrentTCB ldr r2, [r3] stmdb r0!, {r4-r11} str r0, [r2]
stmdb sp!, {r3, r14} mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 dsb isb bl vTaskSwitchContext mov r0, #0 msr basepri, r0 ldmia sp!, {r3, r14}
ldr r1, [r3] ldr r0, [r1] ldmia r0!, {r4-r11} msr psp, r0 isb bx r14 nop }
|
看注释,应该可以一点点理清了,其实就是一个先保存旧任务的数据,再切换 pxCurrentTCB ,然后把新任务的数据存进去。
当然这里,可能有点人就会有疑问了,为什么代码里面只有软件处理的保存数据,那硬件自动压栈和出栈是什么时候呢。
当一进入中断的瞬间,就自动把当前部分寄存器压栈了,出栈也是,当中断出来,异常结束,硬件会自动读出栈的值,放到寄存器里面,恢复运行。
切换pxCurrentTCB函数
其实就是前面的跳转过来的函数。
1 2 3 4 5 6 7 8 9 10 11 12
| void vTaskSwitchContext( void ) {
if ( pxCurrentTCB == &Task1TCB ) { pxCurrentTCB = &Task2TCB; } else { pxCurrentTCB = &Task1TCB; } }
|
我们这里先比较简单,就只实现了两个任务的调换。
到现在,就已经基本实现了任务的调度。
临界的保护
临界段的保护实质上是对部分全局变量,让这个变量在被一个函数修改的时候,不会被另一个函数再次修改,其实这个的本质也可以归结于对中断的处理。
关中断函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() void vPortRaiseBASEPRI( void ) { uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { msr basepri, ulNewBASEPRI dsb isb } }
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() ulPortRaiseBASEPRI( void ) { uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { mrs ulReturn, basepri msr basepri, ulNewBASEPRI dsb isb } return ulReturn; }
|
首先要讲的是 basepri 寄存器,这个是一个优先级过滤器,当优先级的数值大于这个寄存器的值,就会被屏蔽,优先级低的仍然可以触发。
带返回值和不带返回值的区别就在于,同样是关中断,但是带返回值的关中断会把旧的中断过滤器的值保存下来,并且返回出来。
开中断函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| #define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
void vPortSetBASEPRI( uint32_t ulBASEPRI )(1) { __asm { msr basepri, ulBASEPRI } }
|
在带中断保护的开中断服务中,实际意思其实就是这个时候开中断,可以把之前保存的旧中断的函数保存进去,而不是单纯地传进去一个0。
进入临界段(不带中断保护)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
void vPortEnterCritical( void ) { portDISABLE_INTERRUPTS(); uxCriticalNesting++;
if ( uxCriticalNesting == 1 ) { configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 ); } }
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void ) { uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm { msr basepri, ulNewBASEPRI dsb isb } }
|
这一部分的宏定义挺绕的,并且注意这里重写了一下之前的 vPortRaiseBASEPRI 函数,加了一个 portFORCE_INLINE ,这是强制内联,作用是当这个函数被别的地方调用的时候,正常情况就是跳转到这个位置,来执行这个函数,但是如果写了强制内联,那就是相当于把这个函数又复制了一遍,这样不用跳转,直接执行就可以了,为的就是节省一点点的时间。
其中的 uxCriticalNesting 这个变量,是为了记录关中断的次数,如果A函数关了中断,B函数也关了中断,那么这个中断的计数器会变成2,也只有当A函数退出中断,B函数也退出中断的时候,才会真正地把中断关闭了。
而 if ( uxCriticalNesting == 1 ) 实际上就是当第一次进入这个函数的时候,检验一下是否是在中断即可。
进入临界段(带中断保护)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) { uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm { mrs ulReturn, basepri msr basepri, ulNewBASEPRI dsb isb }
return ulReturn; }
|
这个参考上面的不带中断保护的代码理解即可,本质区别不大。
退出临界段(不带中断保护)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portEXIT_CRITICAL() vPortExitCritical()
void vPortExitCritical( void ) { configASSERT( uxCriticalNesting ); uxCriticalNesting--; if ( uxCriticalNesting == 0 ) { portENABLE_INTERRUPTS(); } }
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) { __asm { msr basepri, ulBASEPRI } }
|
configASSERT( uxCriticalNesting ) 这个是为了保护,防止没有进入临界段就执行了退出临界段的函数,然后就是当 uxCriticalNesting == 0 的时候,就是退出到最后一次临界段了,确认没有程序需要关闭中断了,就执行打开中断的函数。
退出临界段(带中断保护)
1 2 3 4 5 6 7 8 9 10 11 12 13
|
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) { __asm { msr basepri, ulBASEPRI } }
|
结合不带中断保护的内容理解即可。
空闲任务与阻塞延时的实现
空闲任务实际上就是rtos会创建的一个最低优先级的任务,这个任务的作用可以相当于大楼的第一楼,就是当系统没事干的时候,就会开始执行这个函数,回想之前的内容,你可能会想起有个这么的函数 prvTaskExitError 这个的具体位置在 任务栈初始化函数 这里,prvTaskExitError 这个相当于大楼的地基,这个是防止运行的任务return的,这两个是有区别的,空闲任务一般会跑一些系统清理之类的内容。
空闲任务的定义
首先肯定是空闲任务的一些定义,因为这个函数是相对固定的,所以很多东西都写成了宏定义在文件里面,
1 2 3 4
| #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;
|
普通的任务定义,可以放在main.c里面,这里定义完了这个任务的栈,同时还有这个任务的控制块,
空闲任务的创建
然后就是空闲任务的创建,这个的创建是每次启动调度器都必须要创建的一个任务,所以放在了 vTaskStartScheduler() 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize ) { *ppxIdleTaskTCBBuffer=&IdleTaskTCB; *ppxIdleTaskStackBuffer=IdleTaskStack; *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE; }
void vTaskStartScheduler( void ) {
TCB_t *pxIdleTaskTCBBuffer = NULL; StackType_t *pxIdleTaskStackBuffer = NULL; uint32_t ulIdleTaskStackSize; vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); xIdleTaskHandle = xTaskCreateStatic((TaskFunction_t)prvIdleTask, (char *)"IDLE", (uint32_t)ulIdleTaskStackSize , (void *) NULL, (StackType_t *)pxIdleTaskStackBuffer, (TCB_t *)pxIdleTaskTCBBuffer );
vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
pxCurrentTCB = &Task1TCB;
if ( xPortStartScheduler() != pdFALSE ) {
} }
|
这个其实跟普通的任务创建类似,还是一样处理tcb,名字,栈,参数这些,并且顺带塞到就绪列表里面。
阻塞延时
因为rtos的独特的特性,所以可以实现高效利用cpu,绝对不会让cpu空转和空等来进行延时,而是在不该执行这个函数的时候,让cpu执行别的函数,等到延时结束后,再通过中断回调调回来。
首先需要了解的是,FreeRTOS中的延时函数,主要是通过 SysTick 来实现的,我们就是首先通过设置这个中断函数的执行时间,作为基准,比如说我可以把这个函数设置为每10ms执行一次,那么我的延时时间将只能是10,20,30这种。
在这里的实际延时函数和实现过程,和实际的工程代码是有出入,现在这个的版本是比较简单的版本,具体的工程代码的实践是通过再维护一个阻塞列表来实现延时的。
vTaskDelay()函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| void vTaskDelay( const TickType_t xTicksToDelay ) { TCB_t *pxTCB = NULL;
pxTCB = pxCurrentTCB;
pxTCB->xTicksToDelay = xTicksToDelay;
taskYIELD(); }
|
这样子就是一个简单的延时函数,传进去的是你想要延时的次数,具体的延时时间,是需要根据你的 SysTick 函数来确定的,比如说,你的 SysTick 函数的延时是10ms,而你传进来的 xTicksToDelay 为2,那么就相当于延时20ms。
vTaskSwitchContext()函数
这是一个比较简单的版本,和正式的代码区别也特别大,因为这里比较简单,所以只维护了两个函数 Task1 和 Task2 ,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| void vTaskSwitchContext( void ) { if ( pxCurrentTCB == &IdleTaskTCB ) { if ( Task1TCB.xTicksToDelay == 0 ) { pxCurrentTCB = &Task1TCB; } else if ( Task2TCB.xTicksToDelay == 0 ) { pxCurrentTCB = &Task2TCB; } else { return; } }
else { if ( pxCurrentTCB == &Task1TCB ) { if ( Task2TCB.xTicksToDelay == 0 ) { pxCurrentTCB = &Task2TCB; } else if ( pxCurrentTCB->xTicksToDelay != 0 ) { pxCurrentTCB = &IdleTaskTCB; } else { return; } }
else if ( pxCurrentTCB == &Task2TCB ) { if ( Task1TCB.xTicksToDelay == 0 ) { pxCurrentTCB = &Task1TCB; } else if ( pxCurrentTCB->xTicksToDelay != 0 ) { pxCurrentTCB = &IdleTaskTCB; } else { return; } } } }
|
可以结合注释进行理解,其实难度不大,就是实现了一个在三个任务之间轮询,一直找没在延时的任务,如果没找到,就只能执行空闲任务,起码得让 pxCurrentTCB 指向一个任务,不能为null
SysTick中断函数
然后就是最重要的 SysTick 部分。
1 2 3 4 5 6 7 8 9 10 11
| void xPortSysTickHandler( void ) { vPortRaiseBASEPRI();
xTaskIncrementTick();
vPortClearBASEPRIFromISR(); }
|
然后就是 xTaskIncrementTick
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void xTaskIncrementTick( void ) { TCB_t *pxTCB = NULL; BaseType_t i = 0;
const TickType_t xConstTickCount = xTickCount + 1; xTickCount = xConstTickCount;
for (i=0; i<configMAX_PRIORITIES; i++) { pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) ); if (pxTCB->xTicksToDelay > 0) { pxTCB->xTicksToDelay --; } }
portYIELD(); }
|
首先是 xConstTickCount 这是系统时钟,管理着整个系统的时钟,这个也是以 SysTick 的中断时间为基准的,先是维护了这个变量,然后扫描整个就绪列表,找到有任务的列表,取出其中的任务,然后如果这个任务是有延时的,就将延时计数减1。
所以说在这里,整个 SysTick 函数的作用就是把有延时的任务的计数全部减1,不做其他判断了
这个代码也是有一个小坑的,可以看到,在这里,它是直接取出来一个就绪列表的第一项内容,而不是慢慢遍历对应的优先级的就绪列表的全部内容,所以如果说在同一个优先级的就绪列表有两个任务,那么就永远不会执行第二个任务。
SysTick初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *) 0xe000e010 ))
#define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *) 0xe000e014 ))
#ifndef configSYSTICK_CLOCK_HZ #define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
#define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL ) #else #define portNVIC_SYSTICK_CLK_BIT ( 0 ) #endif #define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL ) #define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
void vPortSetupTimerInterrupt( void ) {
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT ); }
BaseType_t xPortStartScheduler( void ) {
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
vPortSetupTimerInterrupt();
prvStartFirstTask();
return 0; }
|
这个其实就是类似于32的寄存器编程, portNVIC_SYSTICK_CTRL_REG 这个地址是设置使能的,portNVIC_SYSTICK_LOAD_REG 这个地址是设置重装寄存器的,也就是倒计时的起点。
xPortStartScheduler 这个函数也是老朋友了,之前就碰到过,不过当时这个函数的实现没有初始化 SysTick ,
1 2
| #define configCPU_CLOCK_HZ (( unsigned long ) 25000000) #define configTICK_RATE_HZ (( TickType_t ) 100)
|
同时还有这两个,第一个是定义了CPU的运行频率的,第二个是定义了每秒开多少次中断。
小结
这个的内容比较琐碎,也不是特别难以理解,了解一下空闲函数的内容,重点就是在于阻塞延时的简单实现,利用SysTick中断来为每个任务来记录时间