新闻中心

EEPW首页>嵌入式系统>设计应用> DIY:给单片机写个实时操作系统内核!

DIY:给单片机写个实时操作系统内核!

作者: 时间:2016-11-29 来源:网络 收藏

调度策略:实现了调度,还要继续考虑调度策略,就是什么情况下需要调度哪些任务。调度策略分很多种,有兴趣的可以去看那本《操作系统原理》,在我的源代码里面使用了”抢占式优先级调度+同一优先级下时间片轮询调度“的方法。
所谓抢占式优先级调度是一种实时调度的方法,在实时操作系统中常用,这种方法的原理就是:操作系统在任何时候都要保证拥有最高优先级的那个任务处于运行态,比如此记在运行着优先级为2的任务,因为一些信号到达,优先级为1的那个任务解除了阻塞,处于就绪态,这时操作系统就必须马上停止任务2,切换到任务1,切换的这段时间需要越短越好。
而时间片轮询即是让每个任务都处于平等地位,然后给每个任务相同的时间片,当一个任务的运行时间用完了,操作系统就马上切换给下一个需要执行的任务,这种方法的实时性不高,但它确保了每个任务都有相同的执行时间。
我把这两种方法结合起来,首先设定了8个优先级组,每个优先级组下面都用单向链表把具有相同优先级的任务连接起来。这样的话首先操作系统会查找最高优先级的那组,然后在组里面轮流执行所有任务(和UCOS II相比这种做法更具有灵活性,因为UCOS II只有抢占式调度,这是UCOS II的硬伤。。)。我声明了一个任务结构体称为线程控制块,把关于该任务的所有状态都放在一起:
/**
* @结构体声明
* @名称 : OS_TCB , *pOS_TCB
* @成员 : 1. OS_DataType_ThreadStack *ThreadStackTop
* 线程人工堆栈栈顶指针
* 2. OS_DataType_ThreadStack *ThreadStackBottom
* 线程人工堆栈栈底指针
* 3. OS_DataType_ThreadStackSize ThreadStackSize
* 线程人工堆栈大小
* 4. OS_DataType_ThreadID ThreadID
* 线程ID号
* 5. OS_DataType_ThreadStatus ThreadStatus
* 线程运行状态
* 6. OS_DataType_PSW PSW
* 记录线程的程序状态寄存器
* 7. struct _OS_TCB *Front
* 指向上一个线程控制块的指针
* 8. struct _OS_TCB *Next
* 指向下一人线程控制块的指针
* 9.struct _OS_TCB *CommWaitNext ;
* 指向线程通信控制块的指针
* 10.struct _OS_TCB *TimeWaitNext ;
* 指向延时等待链表的指针
* 11.OS_DataType_PreemptionPriority Priority ;
* 任务优先级
* 12.OS_DataType_TimeDelay TimeDelay ;
* 任务延时时间
* @描述 : 定义线程控制块的成员
* @建立时间 : 2011-11-15
* @最近修改时间: 2011-11-17
*/
typedef struct _OS_TCB{
OS_DataType_ThreadStack *ThreadStackTop ;
OS_DataType_ThreadStack *ThreadStackBottom ;
OS_DataType_ThreadStackSize ThreadStackSize;
OS_DataType_ThreadID ThreadID ;
OS_DataType_ThreadStatus ThreadStatus ;
OS_DataType_PSW PSW ;
struct _OS_TCB *Front ;
struct _OS_TCB *Next ;
#if OS_COMMUNICATION_EN == ON
struct _OS_TCB *CommWaitNext ;
#endif

struct _OS_TCB *TimeWaitNext ;
OS_DataType_PreemptionPriority Priority ;

OS_DataType_TimeDelay TimeDelay ;
}OS_TCB,*pOS_TCB;

首先启动系统的时候需要先创建任务,任务被创建之后才可以得到执行,使用如下函数:
/**
* @名称:线程创建函数
* @输入参数:1.pOS_TCB ThreadControlBlock 线程控制块结构体指针
* 2.void (*Thread)(void*) 线程函数入口地址,接受一个空指针形式的输入参数,无返回参数
* 3.void *Argument 需要传递给线程的参数,空指针形式
* @建立时间 : 2011-11-18
* @最近修改时间: 2011-11-18
*/
void OS_ThreadCreate(pOS_TCB ThreadControlBlock,void (*Thread)(void *),void *Argument)
关于创建任务的大致描述就是:填定线程控制块,把线程控制块链到单向链表中,设置人工堆栈,细节很多,就不一一赘述了。

当前版本只实现了轮询调度,还没加上抢占调度,使用下面的函数就可以启动操作系统开始多线程任务!
/**
* @名称 : 实时内核引发函数
* @版本 : V 0.0
* @输入参数 : 无
* @输出参数 : 无
* @描述 : 在主函数中用于启动,调用该函数后不会返回,直接切换到最高优先级任务开始执行
* @建立时间 : 2011-11-15
* @最近修改时间: 2011-11-15
*/
void OS_KernelStart(void)
{
OS_Status = OS_RUNNING ; //把内核状态设置为运行态

//取得第一个需要运行的任务
OS_CurrentThread = OS_TCB_PriorityGroup[pgm_read_byte(ThreadSearchTab + OS_PreemptionPriority)].OS_TCB_Current;
OS_LastThread = NULL ;
//SP指针指向该任务的栈顶
SP = (uint16_t)OS_CurrentThread->ThreadStackTop ;
//使用出栈操作
POP_REG();
//调用RET,调用之后开始执行任务,不会再返回到这里
_asm("RET");
}

怎样实现时间片?答案是用定时器定时,每次定时器产生中断的时候就转换一次任务,时基可以自己确定,一般来说时基越小的话会让CPU花很多时间在切换任务上,降低了效率,时基大的话又使时间粒度变粗,会使一些程序得不到及时的执行。我设定了每10MS中断一次,就是说每一轮中每个线程都有10MS的执行时间。具体算法不再赘述。

内存管理策略
接下来要考虑怎样管理内存了!在PC里面编程的时候,如果需要开辟一个内存空间,我们可以很容易地调用malloc()和free()来完成,但是在单片机里面却行不通,因为要实现这两个函数背后需要完成很多算法支持,从速度和空间上单片机都做不到。
在单片机里面如果你需要开辟内存空间,你只有在编译的时候就先定义好变量,无法动态申请,但是我们可以设计一个简单的内存管理策略来实现这种动态申请!原理就是在编译的时候先向编译器要一块足够大的内存并且声明为静态,然后把这块空间交给内存管理模块来调用,内存管理模块负责分配这块内存,当有任务要向它申请内存的时候它就从里面拿出一块交给任务,而任务要释放的时候就把该内存空间交给内存管理模块来实现。
关于内存管理也有很多种策略,在这里就不一一述说了,我在源代码里面使用了一种简单的随机分配的方法,即有线程申请的时候就从当前内存块的可用空间里拿出一块来,然后在内存头加上一个专用的结构体,把每个内存块都链接起来,这样便于管理。当线程释放内存的时候,就把内存返回到内存空间并跟其他空间的内存块合并起来等待线程再次调用。
/**
* @名称 : 内存块申请函数
* @版本 : V 0.0
* @输入参数 : 1. OS_DataType_MemorySize MemorySize
需要申请内存块的大小
* @输出参数 : 1. void *
若申请成功,则返回可使用内存块首地址,否则返回NULL
* @描述 :
* @建立时间 : 2011-11-16
* @最近修改时间: 2011-11-16
*/
#if OS_MEMORY_EN
void *OS_MemoryMalloc(OS_DataType_MemorySize MemorySize)
{
pOS_MCB pmcb = OS_MCB_Head ;
pOS_MCB pmcb2 ;
MemorySize+=OS_MEMORY_BLOCK_SIZE ;
//进入内存搜索算法
while(1)
{
//检测该内存块是否存在
if(pmcb==NULL)
{
return NULL ;
}
//如果存在则检测该内存块的使用状态
else if( (pmcb->Status==OS_MEMORY_STATUS_IDLE) && (pmcb->Size >= MemorySize) )
{
//如果可用内存块大小刚好等于需要申请的大小
//则立即分配
if(pmcb->Size == MemorySize)
{
pmcb->Status=OS_MEMORY_STATUS_USING ;
OS_MemoryIdleCount -= MemorySize ;
return (OS_DataType_Memory *)pmcb + OS_MEMORY_SIZE ;
}
//若可用内存块大小大于需要申请的大小
//则进行分割操作
else
{
pmcb2=(pOS_MCB)( (OS_DataType_Memory *)pmcb + MemorySize );
pmcb2->Front=pmcb ;
pmcb2->Next=pmcb->Next ;
pmcb2->Status=OS_MEMORY_STATUS_IDLE ;
pmcb2->Size = pmcb->Size - MemorySize ;
pmcb->Status = OS_MEMORY_STATUS_USING ;
pmcb->Size = MemorySize ;
pmcb->Next=pmcb2;
OS_MemoryIdleCount -= MemorySize ;
return (OS_DataType_Memory *)pmcb+OS_MEMORY_BLOCK_SIZE ;
}
}
else
{
pmcb=pmcb->Next;
}
}
}
#endif

内存释放函数:
/**
* @名称 : 内存块释放函数
* @版本 : V 0.0
* @输入参数 : 1. OS_DataType_MemorySize MemorySize
需要申请内存块的大小
* @输出参数 : 1. void *
若申请成功,则返回可使用内存块首地址,否则返回NULL
* @描述 :
* @建立时间 : 2011-11-16
* @最近修改时间: 2011-11-16
*/
#if OS_MEMORY_EN
void OS_MemoryFree(void *MCB)
{
pOS_MCB pmcb = (pOS_MCB)( (OS_DataType_Memory *)MCB - OS_MEMORY_BLOCK_SIZE );
//将当前内存块设置为空闲状态
pmcb->Status=OS_MEMORY_STATUS_IDLE ;
OS_MemoryIdleCount += pmcb->Size ;
//如果存在上一块内存块,则进入判断
if(pmcb->Front!=NULL)
{
//如果上一块内存块处于空闲状态,则进行合并操作
if(pmcb->Front->Status == OS_MEMORY_STATUS_IDLE)
{
pmcb->Front->Size += pmcb->Size ;
pmcb->Front->Next = pmcb->Next ;
pmcb=pmcb->Front ;
OS_MemoryIdleCount += pmcb->Size ;
}
}
//如果存在下一块内存块,则进入判断
if(pmcb->Next!=NULL)
{
//如果下一块内存块处于空闲状态,则进行合并操作
if(pmcb->Next->Status==OS_MEMORY_STATUS_IDLE)
{
pmcb->Size += pmcb->Next->Size ;
pmcb->Next = pmcb->Next->Next ;
OS_MemoryIdleCount += pmcb->Size ;
}
}
}
#endif

这种分配策略虽然实现简单,但是缺点就是容易产生内存碎片,即随着时间推移,可用内存会越来越碎片化,最后导致想要申请足够大的内存块都没办法。。。

/********************************************************************************/
至此,一个简单的单片机使用的操作系统模型就算完成了,应用在AVR单片机中,下面进入测试阶段:
因为还没有完成线程通信模块还抢占式算法,所以目前只能执行轮询多任务操作。我写了一个测试程序,就是创建三个流水灯程序 (是不是觉得写个操作系统就用来跑流水灯太浪费了,哈哈),让它们同时闪,在PROTEUS中仿真查看
在AVR STUDIO5开发环境中编写,代码如下:
#include "includes.h"
#include "OS_core.h"

#define STACK_SIZE 80 //定义每个任务的人工堆栈大小

//定义三个任务各自的人工堆栈
uint8_t Test1Stack[STACK_SIZE];
uint8_t Test2Stack[STACK_SIZE];
uint8_t Test3Stack[STACK_SIZE];

//定义三个任务各自的线程控制块
OS_TCB Task1;
OS_TCB Task2;
OS_TCB Task3;

//线程1让PB口闪烁
void Test1(void *p)
{
uint8_t i;
DDRB=0XFF;
PORTB=0xff;
SREG|=0X80;
while(1)
{
for(i=0;i<8;i++)PORTB=1< }
}

评论


技术专区