如果这是你首次去深入了解一个操作系统,我还是希望你可以学习到最基本的操作系统是如何工作的。FreeRTOS是相对简单的,特别是相比Windows,linux,或者OS X而言,不过所有操作系统都有着相同的概念和目标,所以不论学习哪个操作系统都是有启发和有趣的。
3.1 什么是“嵌入式”和“实时”?
“嵌入式”和“实时”对于不同的人来说代表不同的理解,所以让我们像FreeRTOS用户那样来定义它们。
嵌入式系统就是一个专门设计用来做一些简单事情的计算机系统,就像是电视遥控器,车载GPS,电子手表,或者起搏器这类。嵌入式系统比通用计算机系统显著的区别在于更小和更慢,通常也更便宜。一个典型的低端嵌入式系统可能有一个运行速度为25MHz的8位CPU,几KB的内存,和也许32KB的闪存。一个高端的嵌入式系统可能有一个运行速度为750MHz的32位CPU,一个GB左右的内存,和几个GB的闪存。
实时系统是设计去完成一定时间内的事,它们保证这些事是在应该做的时候去做。
心脏起搏器是实时嵌入式系统的一个极好例子。起搏器必须在正确的时间收缩心肌,以挽救你的生命;它不能够太忙而没有及时响应。心脏起搏器以及其他的实时嵌入式系统都必须精心设计,以便在任何时刻都能及时执行它们的任务。
3.2 架构概述
FreeRTOS是一个相对较小的应用程序。最小化的FreeRTOS内核仅包括3个(.c)文件和少数头文件,总共不到9000行代码,还包括了注释和空行。一个典型的编译后(二进制)代码映像小于10KB。
FreeRTOS的代码可以分解为三个主要区块:任务,通讯,和硬件接口。
●任务:大约有一半的FreeRTOS的核心代码用来处理多数操作系统首要关注的问题:任务。任务是给定优先级的用户定义的C函数。task.c和task.h完成了所有有关创建,调度,和维护任务的繁重工作。
●通讯:任务很重要,不过任务间可以互相通讯则更为重要!它给我们带来FreeRTOS的第二项任务:通讯。大约40%的FreeRTOS核心代码是用来处理通讯的。queue.c和queue.h是负责处理FreeRTOS的通讯的。任务和中断使用队列互相发送数据,并且使用信号灯和互斥来发送临界资源的使用情况。
●硬件接口:接近9000行的代码拼凑起基本的FreeRTOS,是硬件无关的;相同的代码都能够运行,不论FreeRTOS是运行在不起眼的8051,还是最新、最炫的ARM内核上。大约有6%的FreeRTOS的核心代码,在硬件无关的FreeRTOS内核与硬件相关的代码间扮演着垫片的角色。我们将在下个部分讨论硬件相关的代码。
硬件注意事项
硬件无关的FreeRTOS层在硬件相关层之上。硬件相关层声明了你选择什么样的芯片架构。
图3.1显示了FreeRTOS的各层。
FreeRTOS包含所有你需要用来启动很运行系统的硬件无关以及硬件相关的代码。它支持许多编译器(CodeWarrior,GCC,IAR等)也支持许多处理器架构(ARM7,ARM Cortex-M3,PICs各系列,Silicon Labs 8051, x86等)。请参阅FreeRTOS网站,可以看到处理器和编译器支持列表。
FreeRTOS是高可配置设计。FreeRTOS可以被编译成为适合单CPU,极简RTOS,只之支持少数任务的操作系统,也可以被编译成为适合多核功能强大的结合了TCP/IP,文件系统,和USB的怪兽。
配置选项可以通过设置不同的#defines,在FreeRTOSConfig.h文件里选择。时钟速度,堆大小,互斥,和API子集,连同其他许多选项,都可以在这个文件中配置。这里是几个例子,设置了任务优先级的最大数量,CPU的频率,系统节拍器的频率,最小的堆栈大小和总的堆大小:
#define configMAX_PRIORITIES ( ( unsigned portBASE_TYPE ) 5 )
#define configCPU_CLOCK_HZ ( 12000000UL )
#define configTICK_RATE_HZ ( ( portTickType ) 1000 )
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 100 )
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 4 * 1024 ) )
对于不同的交叉编译器和CPU架构,硬件相关代码分布在多个文件中。举例来说,如果你使用ARM Cortex-M3芯片,IAR编译器工作,那么硬件相关的代码就存在FreeRTOS/Source/portable/IAR/ARM_CM3/目录下。portmacro.h文件声明了所有硬件特定功能,port.c和portasm.s 包含了所有实际硬件相关的代码。硬件无关的头文件portable.h在编译的时候用#include's引入正确的portmacro.h文件。FreeRTOS使用#define'd调用在portmacro.h中声明的硬件特定功能。#define portENTER_CRITICAL() vPortEnterCritical()
vPortEnterCritical()实际上是在FreeRTOS/Source/portable/IAR/ARM_CM3/port.c中定义的。这个port.c文件是一个硬件相关的文件,同时包含了对IAR编译器和Cortex-M3芯片认识的代码文件。vPortEnterCritical()函数利用这些硬件特定的知识进入临界区,又返回到与硬件无关的task.c中。#define portBASE_TYPE long // Basic integer variable type
#define portSTACK_TYPE unsigned long // Pointers to memory locations
typedef unsigned portLONG portTickType; // The system timer tick type
这样使用数据类型的方法,和函数透过小层的#defines,看上去略微有点复杂,不过它允许了FreeRTOS能够被重新编译在一个完全不同的系统架构上,仅仅只需要通过修改这些硬件相关的文件。同时,如果你想要让FreeRTOS运行在一个当前尚未被支持的架构上,你只仅仅需要去实现硬件相关的功能,这要比在FreeRTOS上去实现硬件无关的部分,要少得多。3.3. 调度任务:快速概述
任务优先级和就绪列表
所有任务都有一个用户指定优先级,从0(最低优先级)到 configMAX_PRIORITIES-1的编译时间值(最高优先级)。例如,如果configMAX_PRIORITIES设置为5,当FreeRTOS使用5个优先等级时:0(最低优先级),1,2,3,和4(最高优先级)。
FreeRTOS使用一个“就绪列表”来跟踪所有已经准备好运行的任务。它像一个任务列表数组来实现就绪列表,如下所示:
static xList pxReadyTasksLists[ configMAX_PRIORITIES ]; /* Prioritised ready tasks. */
pxReadyTasksLists[0]是所有准备好的优先级为0的任务列表,pxReadyTasksLists[1]是所有准备好的优先级为1的任务列表,以此类推,直到pxReadyTasksLists[configMAX_PRIORITIES-1]。/* Find the highest-priority queue that contains ready tasks. */
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )
{
configASSERT( uxTopReadyPriority );
--uxTopReadyPriority;
}
/* listGET_OWNER_OF_NEXT_ENTRY walks through the list, so the tasks of the same
priority get an equal share of the processor time. */
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) );图3.2是一个大致的就绪列表看起来像什么的图。这个例子有三个优先级,有一个优先级为0的任务,没有优先级为1的任务,和三个优先级为2的任务。这张图是准确的,但不完整的;它的少掉一些细节,我们稍后将补充。
图3.2:FreeRTOS的就绪列表的基本视图
现在我们有了大致概述的方式,让我们去深究它的细节。我们将着眼于三个主要FreeRTOS的数据结构:任务,列表和队列。
3.4. 任务
所有操作系统的主要工作是运行和协调用户任务。像多数操作系统一样,FreeRTOS中的基本工作单元是任务。FreeRTOS的使用任务控制块(TCB)来表示每个任务。
任务控制块(TCB)
TCB的在tasks.c定义是这样的:
typedef struct tskTaskControlBlock
{
volatile portSTACK_TYPE *pxTopOfStack; /* Points to the location of
the last item placed on
the tasks stack. THIS
MUST BE THE FIRST MEMBER
OF THE STRUCT. */
xListItem xGenericListItem; /* List item used to place
the TCB in ready and
blocked queues. */
xListItem xEventListItem; /* List item used to place
the TCB in event lists.*/
unsigned portBASE_TYPE uxPriority; /* The priority of the task
where 0 is the lowest
priority. */
portSTACK_TYPE *pxStack; /* Points to the start of
the stack. */
signed char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* Descriptive name given
to the task when created.
Facilitates debugging
only. */
#if ( portSTACK_GROWTH > 0 )
portSTACK_TYPE *pxEndOfStack; /* Used for stack overflow
checking on architectures
where the stack grows up
from low memory. */
#endif
#if ( configUSE_MUTEXES == 1 )
unsigned portBASE_TYPE uxBasePriority; /* The priority last
assigned to the task -
used by the priority
inheritance mechanism. */
#endif
} tskTCB;
TCB在pxStack里存储堆栈的起始地址,以及在pxTopOfStack里存储当前堆栈的顶部。如果堆栈“向上”增长到更高的地址,它还在pxEndOfStack存储堆栈的结束的指针来检查堆栈溢出。如果堆栈“向下”增长到更低的地址,那么通过比较当前堆栈的顶部与pxStack中的堆内存起始位置来检查溢出。unsigned int *pxPortInitialiseStack( unsigned int *pxTopOfStack,
pdTASK_CODE pxCode,
void *pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on
entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( portSTACK_TYPE ) pxCode; /* PC */
pxTopOfStack--;
*pxTopOfStack = 0; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( portSTACK_TYPE ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
当一个任务被中断的时候,ARM Cortex-M3处理器就压寄存器入堆栈。pxPortInitialiseStack()修改堆栈使之看来像是即便任务实际上还未开始运行,寄存器就已经被压入了。已知的值被存储到堆栈,赋给ARM寄存器xPSR,PC,LR,和R0。剩余的寄存器R1-R12获得由栈顶指针递减分配给它们的寄存器空间,但没有具体的数据存储在这些寄存器堆栈内。ARM架构告诉我们那些寄存器在复位的时候未被定义,所以一个(非弱智的)程序将不依赖于已知的值。 这个FreeRTOS的列表是一个有着几个有趣的补充的标准循环双链表。下面就是列表元素:
struct xLIST_ITEM
{
portTickType xItemValue; /* The value being listed. In most cases
this is used to sort the list in
descending order. */
volatile struct xLIST_ITEM * pxNext; /* Pointer to the next xListItem in the
list. */
volatile struct xLIST_ITEM * pxPrevious; /* Pointer to the previous xListItem in
the list. */
void * pvOwner; /* Pointer to the object (normally a TCB)
that contains the list item. There is
therefore a two-way link between the
object containing the list item and
the list item itself. */
void * pvContainer; /* Pointer to the list in which this list
item is placed (if any). */
};
每个元素持有一个数字,xItemValue,这通常是一个被跟踪的任务优先级或者是一个调度事件的计时器值。列表保存从高到低的优先级指令,这意味着最高的优先级xItemValue(最大数)在列表的最前端,而最低的优先级xItemValue(最小数)在列表的末尾。
pvContainer指向自己所在的这个列表。若列表项处于一个特定列表它被用作快速终止。任意列表元素可以被置于一个列表,如下所定义:
typedef struct xLIST
{
volatile unsigned portBASE_TYPE uxNumberOfItems;
volatile xListItem * pxIndex; /* Used to walk through the list. Points to
the last item returned by a call to
pvListGetOwnerOfNextEntry (). */
volatile xMiniListItem xListEnd; /* List item that contains the maximum
possible item value, meaning it is always
at the end of the list and is therefore
used as a marker. */
} xList;
列表的大小任何时候都是被存储在uxNumberOfItems中,用于快速列表大小操作。所有的列表都被初始化为容纳一个元素:xListEnd元素。xListEnd.xItemValue是一个定点值,当portTickType是16位数时,它等于xItemValue变量的最大值:0xffff,portTickType是32位数时为0xffffffff。其他的列表元素也可以使用相同的值;插入算法保证了xListEnd总是列表项中最后一个值。
for (listPtr = listStart; listPtr != NULL; listPtr = listPtr->next) {
// Do something with listPtr here...
}
并且返回pxIndex。(当然它也会正确检测列尾环绕。)这种,当执行遍历的时候使用pxIndex,由列表自己负责跟踪“在哪里”的方法,使FreeRTOS可以休息而不用关心这方面的事。
typedef struct QueueDefinition
{
signed char *pcHead; /* Points to the beginning of the queue
storage area. */
signed char *pcTail; /* Points to the byte at the end of the
queue storage area. One more byte is
allocated than necessary to store the
queue items; this is used as a marker. */
signed char *pcWriteTo; /* Points to the free next place in the
storage area. */
signed char *pcReadFrom; /* Points to the last place that a queued
item was read from. */
xList xTasksWaitingToSend; /* List of tasks that are blocked waiting
to post onto this queue. Stored in
priority order. */
xList xTasksWaitingToReceive; /* List of tasks that are blocked waiting
to read from this queue. Stored in
priority order. */
volatile unsigned portBASE_TYPE uxMessagesWaiting; /* The number of items currently
in the queue. */
unsigned portBASE_TYPE uxLength; /* The length of the queue
defined as the number of
items it will hold, not the
number of bytes. */
unsigned portBASE_TYPE uxItemSize; /* The size of each items that
the queue will hold. */
} xQUEUE;
这是一个颇为标准的队列,不但包括了头部和尾部指针,而且指针指向我们刚刚读过或者写过的位置。