当前位置:首页 > 单片机 > 单片机
[导读]任务:实现并行多任务,没有考虑中断问题。我会在“51操作系统学习笔记(二)”考虑中断问题,研究一个复杂一点的操作系统。51单片机上用操作系统,虽说没有太大的实际用处。但是,对于学习嵌入式操作系统,毕竟是最

任务:实现并行多任务,没有考虑中断问题。我会在“51操作系统学习笔记(二)”考虑中断问题,研究一个复杂一点的操作系统。

51单片机上用操作系统,虽说没有太大的实际用处。但是,对于学习嵌入式操作系统,毕竟是最容易入门的。

开源的small rtos51和uc os,对初学者来说,还是有一定难度。

在网上找了一个最容易入手的,简单的,51操作系统入门资料。

这个资料其实算不上完整的操作系统,但它实现了简单的并行多任务。

从这里,也可以理解堆栈的应用。

该程序很巧妙的是用ret来实现程序的跳转。

ret实现从子程序返回主程序,sp的内容弹出到pc.

子程序结束时会调用ret,本文利用这一点进行跳转。

程序的核心流程是:

在主程序的最后一句os_start(0)里,sp被赋值为任务一的程序地址,执行return时实现ret,sp的内容赋给pc,即pc的值就是任务一的程序地址,所以马上执行任务一。

任务一中的task_switch()会把sp的值赋给任务一的指针(task_sp[0]=sp,保留任务一的断点,下次执行任务一时从改断点运行),然后把任务二的程序地址赋给sp,task_switch()结束时会调用ret,sp的值会赋给pc,即pc为任务二的程序地址,所以马上执行任务二。

同理执行任务三。

然后又执行任务一。

.....

如此不断循环。

全部程序如下:详细讲解见http://www.ourdev.cn/bbs/bbs_content_all.jsp?bbs_sn=1398508

//下面是全部程序,大家可以把它拷贝到keil中,编译链接.

//在proteus中仿真,p1,p2,p3分别接led灯(p1,p2,p3低电平灯亮),会发现三个led轮流亮灭,这表示三个任务都在模拟并行执行

/*

简单的多任务操作系统

其实只有个任务调度切换,把说它是OS有点牵强,但它对于一些简单的开发应用来说,简单也许就是最好的.尽情的扩展它吧.别忘了把你的成果分享给大家.

这是一个最简单的OS,一切以运行效率为重,经测试,切换一次任务仅20个机器周期,也就是在标准51(工作于12M晶振)上20uS.
而为速度作出的牺牲是,为了给每个任务都分配一个私有堆栈,而占用了较多的内存.作为补偿,多任务更容易安排程序逻辑,从而可以节省一些用于控制的变量.
任务槽越多,占用内存越多,但任务也越好安排,以实际需求合理安排任务数目.一般来说,4个已足够.况且可以拿一个槽出来作为活动槽,换入换入一些临时任务.

task_load(函数名,任务槽号)
装载任务

os_start(任务槽号)
启动任务表.参数必须指向一个装载了的任务,否则系统会崩溃.

task_switch()
切换到其它任务

.编写任务函数注意事项:
KEIL C编译器是假定用户使用单任务环境,所以在变量的使用上都未对多任务进行处理,编写任务时应注意变量覆盖和代码重入问题.

1.覆盖:编译器为了节省内存,会给两个没用调用关系的函数分配同一内存地址作为变量空间.这在单任务下是很合理的,但对于多任务来说,两个进程会互相干扰对方.
解决的方法是:凡作用域内会跨越task_switch()的变量,都使用static前辍,保证其地址空间分配时的唯一性.

2.重入:重入并不是多任务下独有的问题,在单任务时,函数递归同样会导致重入,即,一个函数的不同实例(或者叫作"复本")之间的变量覆盖问题.
解决的方法是:使用reentrant函数后辍(例如:void function1() reentrant{...}).当然,根本的办法还是避免重入,因为重入会带来巨大的目标代码量,并极大降低运行效率.

3.额外提醒一句,在本例中,任务函数必须为一个死循环.退出函数会导致系统崩溃.


.任务函数如果是用汇编写成或内嵌汇编,切换任务时应该注意什么问题?

由于KEIL C编译器在处理函数调用时的约定规则为"子函数有可能修改任务寄存器",因此编译器在调用前已释放所有寄存器,子函数无需考虑保护任何寄存器.
这对于写惯汇编的人来说有点不习惯: 汇编习惯于在子程序中保护寄存器.
请注意一条原则:凡是需要跨越task_switch()的寄存器,全部需要保护(例如入栈).根本解决办法还是,不要让寄存器跨越任务切换函数task_switch()
事实上这里要补充一下,正如前所说,由于编译器存在变量地址覆盖优化,因此凡是非静态变量都不得跨越task_switch().


任务函数的书写:
void 函数名(void){//任务函数必须定义为无参数型
while(1){//任务函数不得返回,必须为死循环
//....这里写任务处理代码

task_switch();//每执行一段时间任务,就释放CPU一下,让别的任务有机会运行.
}
}


任务装载:
task_load(函数名,任务槽号)

装载函数的动作可发生在任意时候,但通常是在main()中.要注意的是,在本例中由于没考虑任务换出,
所以在执行os_start()前必须将所有任务槽装满.之后可以随意更换任务槽中的任务.

启动任务调度器:
os_start(任务槽号)

调用该宏后,将从参数指定的任务槽开始执行任务调度.本例为每切换一次任务需额外开销20个机器周期,用于迁移堆栈.
*/


#include


/*============================以下为任务管理器代码============================*/

#define MAX_TASKS 3//任务槽个数.在本例中并未考虑任务换入换出,所以实际运行的任务有多少个,就定义多少个任务槽,不可多定义或少定义

//任务的栈指针
unsigned char idata task_sp[MAX_TASKS];

#define MAX_TASK_DEP 12 //最大栈深.最低不得少于2个,保守值为12.
//预估方法:以2为基数,每增加一层函数调用,加2字节.如果其间可能发生中断,则还要再加上中断需要的栈深.
//减小栈深的方法:1.尽量少嵌套子程序 2.调子程序前关中断.
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];//任务堆栈.

unsigned char task_id;//当前活动任务号


//任务切换函数(任务调度器)
void task_switch(){
task_sp[task_id] = SP;

if(++task_id == MAX_TASKS)
task_id = 0;

SP = task_sp[task_id];
}

//任务装入函数.将指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.
void task_load(unsigned int fn, unsigned char tid){
task_sp[tid] = task_stack[tid] + 1; //
task_stack[tid][0] = (unsigned int)fn & 0xff; //低8位
task_stack[tid][1] = (unsigned int)fn >> 8; //高8位
}

//从指定的任务开始运行任务调度.调用该宏后,将永不返回.
#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}


/*============================以下为测试代码============================*/


unsigned char stra[3], strb[3];//用于内存块复制测试的数组.


//测试任务:复制内存块.每复制一个字节释放CPU一次
void task1(){
//每复制一个字节释放CPU一次,控制循环的变量必须考虑覆盖
static unsigned char i;//如果将这个变量前的static去掉,会发生什么事?
i = 0;

while(1){//任务必须为死循环,不得退出函数,否则系统会崩溃
P1=0xff;//p1的灯灭
P2=0x00;//p2的灯亮
stra[i] = strb[i];
if(++i == sizeof(stra))
i = 0;

//变量i在这里跨越了task_switch(),因此它必须定义为静态(static),否则它将会被其它进程修改,因为在另一个进程里也会用到该变量所占用的地址.
task_switch();//释放CPU一会儿,让其它进程有机会运行.如果去掉该行,则别的进程永远不会被调用到
}
}

//测试任务:复制内存块.每复制一个字节释放CPU一次.
void task2(){
//每复制一个字节释放CPU一次,控制循环的变量必须考虑覆盖
static unsigned char i;//如果将这个变量前的static去掉,将会发生覆盖问题.task1()和task2()会被编译器分配到同一个内存地址上,当两个任务同时运行时,i的值就会被两个任务改来改去
i = 0;

while(1){//任务必须为死循环,不得退出函数,否则系统会崩溃
P2=0xff;//p2的灯灭
P3=0x00;//p3的灯亮
stra[i] = strb[i];
if(++i == sizeof(stra))
i = 0;

//变量i在这里跨越了task_switch(),因此它必须定义为静态(static),否则它将会被其它进程修改,因为在另一个进程里也会用到该变量所占用的地址.
task_switch();//释放CPU一会儿,让其它进程有机会运行.如果去掉该行,则别的进程永远不会被调用到
}
}

//测试任务:复制内存块.复制完所有字节后释放CPU一次.
void task3(){
//复制全部字节后才释放CPU,控制循环的变量不须考虑覆盖
unsigned char i;//这个变量前不需要加static,因为在它的作用域内并没有释放过CPU

while(1){//任务必须为死循环,不得退出函数,否则系统会崩溃

P3=0xff;//p3的灯灭
P1=0x00;//p1的灯亮
i = sizeof(stra);
do{
stra[i-1] = strb[i-1];
}while(--i);

//变量i在这里已完成它的使命,所以无需定义为静态.你甚至可以定义为寄存器型(regiter)
task_switch();//释放CPU一会儿,让其它进程有机会运行.如果去掉该行,则别的进程永远不会被调用到
}
}

void main(){
//在这个示例里并没有考虑任务的换入换出,所以任务槽必须全部用完,否则系统会崩溃.
//这里装载了三个任务,因此在定义MAX_TASKS时也必须定义为3
task_load(task1, 0);//将task1函数装入0号槽
task_load(task2, 1);//将task2函数装入1号槽
task_load(task3, 2);//将task3函数装入2号槽

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

Windows Embedded Compact 7(简称WinCE)是一种专为嵌入式系统设计的操作系统,具有体积小、效率高、可定制性强的特点。在WinCE中设置自动运行软件,通常是为了满足设备在启动后自动执行特定任务的...

关键字: 嵌入式系统 软件 操作系统

今天,小编将在这篇文章中为大家带来Windows 11系统的有关报道,通过阅读这篇文章,大家可以对Windows 11系统具备清晰的认识,主要内容如下。

关键字: Windows 操作系统

全新随插即用方案简化虚拟化实时IIoT平台的设置

关键字: 计算机模块 IIoT 操作系统

目前,HarmonyOS NEXT星河预览版已经正式面向开发者开放申请,面向鸿蒙原生应用及元服务开发者提供的集成开发环境——DevEco Studio也迎来功能更细化的4.1版本。

关键字: HarmonyOS 操作系统

华为P40是一款备受关注的高端智能手机,搭载了华为自研的鸿蒙操作系统。鸿蒙系统作为华为自主研发的操作系统,具有高度的可定制性和扩展性,能够为用户带来全新的使用体验。本文将详细介绍华为P40鸿蒙系统的升级方法,帮助用户更好...

关键字: 华为P40 智能手机 操作系统

安装Linux操作系统并不复杂,下面是一个大致的步骤指南,以帮助您完成安装。1. 下载Linux发行版:首先,您需要从Linux发行版官方网站下载最新的ISO镜像文件。

关键字: Linux 操作系统 ISO镜像

计算机是由一堆硬件组成的,为了有限的控制这些硬件资源,于是就有了操作系统的产生,操作系统是软件子系统的一部分,是硬件基础上的第一层软件。

关键字: Linux 操作系统 计算机

Linux操作系统是一套免费使用和自由传播的类Unix操作系统,通常被称为GNU/Linux。它是由林纳斯·托瓦兹在1991年首次发布的,并基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。Lin...

关键字: Linux 操作系统

华为鸿蒙系统作为华为推出的全新一代操作系统,自发布以来备受关注。本文将对华为鸿蒙系统的实际体验进行详细评测,旨在帮助读者了解该系统的优缺点。

关键字: 华为 鸿蒙系统 操作系统

随着华为鸿蒙OS系统的发布,越来越多的人开始关注这一全新的操作系统。鸿蒙OS系统的界面设计作为用户体验的重要组成部分,也备受关注。本文将详细介绍鸿蒙操作系统界面的设计理念、特点以及与其他系统的对比。

关键字: 华为鸿蒙 操作系统 界面设计
关闭
关闭