手写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/* PORTMACRO_H */

其中用的比较多的就是 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 // !FREERTOS_CONFIG_H

在这里面,我们把 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;          //列表项的其中一个项

讲一下各项的基础含义:

  1. pxNext:指向下一个列表项的指针。
  2. pxPrevious:指向前一个列表项的指针,跟pxNext共同组成,是双向链表的核心
  3. xItemValue:列表项的值,在RTOS中,通过这个值来辅助链表项的之间的排序
  4. pvOwner:指向拥有该列表项的父对象(通常是任务控制块TCB)的指针。
  5. 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;       //这是列表的管理者,不参与列表,不是链表中的一员

讲一下各项的基础含义:

  1. uxNumberOfItems:当前链表中列表项的数量。
  2. pxIndex:指向当前列表项的指针,用于辅助遍历链表,表示之前遍历到的位置。
  3. 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;
    /* 将最后一个节点的pxNext和pxPrevious指针均指向节点自身,表示链表为空 */
    pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
    pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
    /* 初始化链表节点计数器的值为0,表示链表为空 */
    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;
    /* 将新节点的pxNext指针指向链表的最后一个节点 */
    pxNewListItem->pxNext = ( ListItem_t * ) &( pxList->xListEnd );
    /* 将新节点的pxPrevious指针指向链表的最后一个节点的前一个节点 */
    pxNewListItem->pxPrevious = pxIndex->pxPrevious;
    /* 将链表的最后一个节点的前一个节点的pxNext指针指向新节点 */
    pxIndex->pxPrevious->pxNext = pxNewListItem;
    /* 将链表的最后一个节点的pxPrevious指针指向新节点 */
    pxIndex->pxPrevious = pxNewListItem;
    /* 将新节点的pvContainer成员变量设置为链表地址,表示该节点已经加入到链表中 */
    pxNewListItem->pvContainer = ( void * ) pxList;
    /* 链表中的节点计数器加1 */
    ( 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;
    /* 将新节点的pvContainer成员变量设置为链表地址,表示该节点已经加入到链表中 */
    pxNewListItem->pvContainer = ( void * ) pxList;
    /* 链表中的节点计数器加1 */
    ( 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;
    /* 将要删除的节点的前一个节点的pxNext指针指向要删除的节点的下一个节点 */
    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
    /* 将要删除的节点的下一个节点的pxPrevious指针指向要删除的节点的前一个节点 */
    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
    if( pxList->pxIndex == pxItemToRemove )
    {
        /* 如果要删除的节点是链表索引指针所指向的节点,则将链表索引指针指向要删除的节点的下一个节点 */
        pxList->pxIndex = pxItemToRemove->pxNext;
    }
    /* 将要删除的节点的pvContainer成员变量设置为NULL,表示该节点已经不在链表中 */
    pxItemToRemove->pvContainer = NULL;
    /* 链表中的节点计数器减1,并返回当前链表中的节点数量 */
    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 )

/* 获取链表第一个节点的OWNER,即TCB */
#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--);
}
/* 任务1 */
void Task1_Entry( void *p_arg )(1)
{
for ( ;; )
{
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
}
}
/* 任务2 */
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;

简单讲一下各项的含义吧。

  1. pxTopOfStack:指向任务栈的栈顶,当任务切换时,CPU寄存器的值会保存到这个位置。一般来说,栈底是固定的,但是栈顶是动态的,会随着运行过程中而不断变化,所谓的栈溢出,实际上也是栈顶溢出了,跑出了事先规定的边界。
  2. pxStack:指向任务栈的栈底,栈底是固定的,创建任务的时候,就已经固定下来了。
  3. xStateListItem:任务状态列表项,用于将任务添加到各种任务列表中(例如就绪列表、阻塞列表等),这个实际上就指出了任务当前的状态。
  4. 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/* PROJDEFS_H */

这个是函数指针,指向的是一个函数,在这里其实指向的也就是任务函数,其实也就是你的函数名。

任务句柄

这个是在 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;
    }

    /* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */
    return xReturn;
}

#endif /* configSUPPORT_STATIC_ALLOCATION */

看到这个代码,可能又会有疑问了,前面不是说为了保护系统,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);
    /* 向下做8字节对齐 */
    pxTopOfStack = (StackType_t *)(((uint32_t)pxTopOfStack) & (~((uint32_t)0x0007)));
    /* 将任务的名字存储在TCB中 */
    for (x = (UBaseType_t)0; x < (UBaseType_t)configMAX_TASK_NAME_LEN; x++)
    {
        pxNewTCB->pcTaskName[x] = pcName[x];
        if (pcName[x] == 0x00)
        {
            break;
        }
    }
    /* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */
    pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN - 1] = '\0';
    /* 初始化TCB中的xStateListItem节点 */
    vListInitialiseItem(&(pxNewTCB->xStateListItem));
    /* 设置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)   //这是ARM Cortex-M 系列处理器的硬性规定,就是xPSR的第24位必须为1,否则会进hardfault
#define portSTART_ADDRESS_MASK ((StackType_t)0xfffffffeUL)//地址对齐掩码
static void prvTaskExitError(void)
{
    /* 函数停止在这里 */
    for (;;)
        ;
    //这个函数的存在是为了防止任务函数有return,如果有return,就会进入这个位置
}
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack,
                                   TaskFunction_t pxCode,
                                   void *pvParameters)
{
    /* 异常发生时,自动加载到CPU寄存器的内容 */
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_XPSR;
    pxTopOfStack--;
    *pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;//填入任务函数的入口地址,并且用掩码保证对齐
    pxTopOfStack--;
    *pxTopOfStack = (StackType_t)prvTaskExitError;//把安全防线填入LR寄存器
    pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为0 */
    *pxTopOfStack = (StackType_t)pvParameters;//把任务参数放到R0寄存器
    pxTopOfStack -= 8; //为其他寄存器留下空间
    /* 返回栈顶指针,此时pxTopOfStack指向空闲栈 */
    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 )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */(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 //告诉汇编器,这里必须要8字节对齐

ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]

/* 设置主栈指针msp的值 */
msr msp, r0//相当于把cpu的运行拨回了初始状态

/* 使能全局中断 */
cpsie i
cpsie f
dsb
isb

//开始触发svc中断,开始运行RTOS
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] //把当前运行的任务的栈顶指针放到r0

ldmia r0!, {r4-r11} //软件恢复寄存器
msr psp, r0 //把psp放到r0,也就是把psp放到当前栈顶
isb
mov r0, #0 //赋值r0为0
msr basepri, r0 //设置不屏蔽任何中断
orr r14, #0xd //设置返回,返回线程模式,并且使用psp作为栈
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()
{
    /* 触发PendSV,产生上下文切换 */
    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] //把新的栈顶写会r2

stmdb sp!, {r3, r14} //把r3和r14弹出去,保护好,r3保存的是pxCurrentTCB的地址,而r14保存的是LR的值,这个值是一直恒定的
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0 //屏蔽一定范围内的中断
dsb
isb
bl vTaskSwitchContext //在这里面切换pxCurrentTCB为新的任务
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14} //再把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
/* ==========进入临界段,不带中断保护版本,不能嵌套=============== */
/* 在task.h中定义 */
#define taskENTER_CRITICAL() portENTER_CRITICAL()

/* 在portmacro.h中定义 */
#define portENTER_CRITICAL() vPortEnterCritical()

/* 在port.c中定义 */
void vPortEnterCritical( void )//进入临界段的函数
{
portDISABLE_INTERRUPTS();//其实就是vPortRaiseBASEPRI函数,关中断
uxCriticalNesting++;

if ( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );//判断是否处于中断,因为这个的版本是不带返回值的,所以不能在中断中调用
}
}

/* 在portmacro.h中定义 */
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()

/* 在portmacro.h中定义 */
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
/* ==========进入临界段,带中断保护版本,可以嵌套=============== */
/* 在task.h中定义 */
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()

/* 在portmacro.h中定义 */
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()

/* 在portmacro.h中定义 */
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
/* ==========退出临界段,不带中断保护版本,不能嵌套=============== */
/* 在task.h中定义 */
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()

/* 在portmacro.h中定义 */
#define portEXIT_CRITICAL() vPortExitCritical()
/* 在port.c中定义 */
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if ( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
/* 在portmacro.h中定义 */
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
/* 在portmacro.h中定义 */
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
/* ==========退出临界段,带中断保护版本,可以嵌套=============== */
/* 在task.h中定义 */
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
/* 在portmacro.h中定义 */
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
/* 在portmacro.h中定义 */
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;
/* 获取空闲任务的内存:任务栈和任务TCB */
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;

/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;

/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;

/* 任务切换 */
taskYIELD();
}

这样子就是一个简单的延时函数,传进去的是你想要延时的次数,具体的延时时间,是需要根据你的 SysTick 函数来确定的,比如说,你的 SysTick 函数的延时是10ms,而你传进来的 xTicksToDelay 为2,那么就相当于延时20ms。

vTaskSwitchContext()函数

这是一个比较简单的版本,和正式的代码区别也特别大,因为这里比较简单,所以只维护了两个函数 Task1Task2

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 )
{
/* --- 情况 1:当前任务是【空闲任务】 --- */
if ( pxCurrentTCB == &IdleTaskTCB )
{
/* 尝试执行任务 1 或者任务 2,看看它们的延时时间是否结束 */
if ( Task1TCB.xTicksToDelay == 0 )
{
pxCurrentTCB = &Task1TCB;
}
else if ( Task2TCB.xTicksToDelay == 0 )
{
pxCurrentTCB = &Task2TCB;
}
else
{
/* 任务延时均没有到期则返回,继续执行空闲任务 */
return;
}
}

/* --- 情况 2:当前任务是【正式任务】(Task1 或 Task2) --- */
else
{
/* --- 如果当前任务是任务 1 --- */
if ( pxCurrentTCB == &Task1TCB )
{
/* 检查另外一个任务(任务 2)是否醒了 */
if ( Task2TCB.xTicksToDelay == 0 )
{
pxCurrentTCB = &Task2TCB;
}
/* 如果任务 2 没醒,判断下当前任务(任务 1)是否应该进入延时状态 */
else if ( pxCurrentTCB->xTicksToDelay != 0 )
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
/* 任务 2 在延时,任务 1 没延时,所以不切换,继续跑任务 1 */
return;
}
}

/* --- 如果当前任务是任务 2 --- */
else if ( pxCurrentTCB == &Task2TCB )
{
/* 检查另外一个任务(任务 1)是否醒了 */
if ( Task1TCB.xTicksToDelay == 0 )
{
pxCurrentTCB = &Task1TCB;
}
/* 如果任务 1 没醒,判断下当前任务(任务 2)是否应该进入延时状态 */
else if ( pxCurrentTCB->xTicksToDelay != 0 )
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
/* 任务 1 在延时,任务 2 没延时,所以不切换,继续跑任务 2 */
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;

/* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;

/* 扫描就绪列表中所有任务的xTicksToDelay,如果不为0,则减1 */
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
/* SysTick 控制寄存器 */
#define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *) 0xe000e010 ))
/* SysTick 重装载寄存器寄存器 */
#define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *) 0xe000e014 ))

/* SysTick 时钟源选择 */
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
/* 确保SysTick的时钟与内核时钟一致 */
#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;

/* 设置系统定时器的时钟等于内核时钟
使能SysTick 定时器中断
使能SysTick 定时器 */
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
portNVIC_SYSTICK_INT_BIT |
portNVIC_SYSTICK_ENABLE_BIT );
}

BaseType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化SysTick */
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中断来为每个任务来记录时间