在51上写一个“OS”原型

自己在51单片机上实现任务调度器的记录过程,下面的文本内容,完整的图文文档传送到了文库。传送门

闲来无聊,便有了想写操作系统的念头。之前也用过ucso、rtt、raw-os,虽然没怎么深入应用,但对操作系统也有些认识。好奇心的驱使,终于在国庆这段时间里实现了这个“OS”。于是,便有了本文,用来记录自己实现一个OS的过程。当然,这个OS,可不像上面说的几个rtos那样,这个OS只是实现了任务调度功能,还不能算真正意义的OS,甚至编码上看起来很丑陋。由于51单片机相对简单,尽管资源上比较有限,但还是选择了51作为本次的主角。

本文的目标很简单。有两个任务:一个让led闪烁;一个让数码管显示0~9;两个任务每50ms切换一次。

由于涉及到汇编,所以在开始写这个OS之前,特意学习了一下51的汇编。因为有一定基础,学起来也不是很难。只是开始会有点不习惯。

搞定了汇编后,就可以真正的来写这个“OS”了,这里使用了KEIL和proteus来验证。其实可以只使用KEIL,不过配合proteus用起来形象很多。

首先,来复习几个刚开始学习单片机会接触到的内容。单片机中有个PC寄存器(16Bit)。它指向了下一条要被执行的指令。最简单的程序,可能PC就只是递增而已。如果程序中出现跳转指令或者CALL指令,则PC的变化可能就不规则了。当程序运行到某个地方,调用了一个子函数。假设这个地方为A(PC指向A的下一条指令),函数的入口为B。那么当子函数返回(RET)后,程序应该从A的下一条指令开始运行。

CALL指令的结果就是PC的值等于函数的入口地址值(B)。这样一CALL,PC就变了,那么A就不见了。如果在PC值变成B时,没有把A的下一条指令地址保存起来,则程序就再也回去不了。所以便有了个叫栈的东西用来保存这个地址,由寄存器SP所指示栈顶。

从这张图可以看出来,程序运行时,寄存器和RAM的某一时刻情况。此时,程序正准备调用第一个task_reg子函数。这个时候:

1)         SP的值为0xd0;

2)         第二个task_reg子函数的入口地址为0x01B1;

3)         SP所指向的RAM里面的内容为 00 9B 01 00 …

这张图是调用子函数后,寄存器和RAM的变化情况。重点看SP发生的变化:

1)         SP的值为0xd2;

2)         SP所指向的RAM里面的内容为 00 B1 01 …

很明显的看出来,CALL指令将会使CPU对PC中的执行地址进行压栈PUSH。而当函数返回(RET)时,CPU会将栈中保存的地址弹出POP。保证了程序可以准确运行。另外,SP指向的是栈顶,压栈前SP会加一(向上增长)。我们可以总结成下面这种很常见的图。

弄清楚这么一个机制之后,就可以开始来实现我们这个简单的OS了。在开始动手之前,先约定几个概念。

task(任务):为实现某种功能的一个函数,这个函数实际上不会return;其真正去实现某种功能的动作是在一个死循环中;

任务栈:用来存放task各种信息(task被切换前PC的值)的一种数据结构,是task私有的;

系统时钟:用来告诉调度器到时间切换task了,使用定时器0来作为这个OS的系统时钟;

调度器:用于切换task的一个程序块,当一个系统时钟到来时,保存当前PC值到当前task的栈中。再将SP指向另一个task的栈,并POP上次运行到的程序地址到PC。

具体过程:

task与栈:task实质上就是一个函数,栈则是一个数组。具体代码如下:

这里需要说明一下,task中依然使用了传统的delay延时,似乎与使用OS的初衷矛盾。其实这里为了使proteus显示效果好看点。在最开始的设计里,我们只要看到每一次系统时钟到来时,task发生切换就行了。等设计深入后,我们再来让我们的OS看起来更像OS。同时,在最初的设计里,忽略切换task的时候对R0~R7的PUSH POP。

接着,我们需要对task和栈做一个绑定。也就是把函数的地址压入栈中。51的地址是16bit,也就是两个字节,但是Rn寄存器却是一个字节,所以我们任务栈的也是unsigned char类型。在把PC压栈时,需要考虑大小端问题。从keil仿真可以看出来,先压入低字节。出栈则先出高字节

绑定函数的代码如下:

> 8; //高字节
tab[0] = ((int)task) & 0xff;
if(index != 16)
list_tab[index++] = tab;
}
" v:shapes="圆角矩形_x0020_46">

Pfunc 是一个函数指针,其指向的是一个不带参数、不带返回值的函数。而这个指针刚好就是这个函数的入口地址。然后把这个地址存入task自己的栈。这样就实现了“绑定”。把栈指针SP指向task的私有栈,就能得到task上次被切换出去时的各种信息了。

绑定好后,就可以启动OS了,启动task代码如下:

这段代码的作用就是把栈指针SP指向task表中第一个task的栈顶。list_tab是一个存放task栈地址的数组。在使用task_reg函数绑定task和其任务栈的同时,把任务栈的地址保存到这个数组中来。list_tab、task_stack、task之间关系大概是这样子的:

到此,就差调度器还没有实现了。对于CPU来讲,栈只有一个,它就是SP所指向的一块内存空间。在每一次CPU要调用(CALL)子函数的时候,CPU都会把PC值压入栈中,退出(RET/RETI)子函数时把栈中存储的PC值送回去。调度器要做的就是压入PC值到当前task的栈里面去,然后改变SP所指向的内存空间,最后退出(RET/RETI)。从而实现任务调度的效果。因为是要定时的改变任务,所以我们要用到定时器,这里选用定时器0,因为定时器1串口要用到。具体代码由C语言和汇编语言组成。具体如下:

> 8; //重置定时器
TL0 = tick & 0xff;
TF0 = 0;
return list_tab[task_now] + 1; //返回栈的地址
}
" v:shapes="圆角矩形_x0020_53">

到这里,我们这个运行于51单片机上面的“OS”就完成了。但这并不是一个真正的OS,这只是一个初具OS样子的一个OS原型。在这个OS的设计过程中,最终目的很简单,就是实现两个任务切换,让整体看起来很像两个任务同时运行,而忽略了很多东西。比如经常提到的“现场保护”。另外,task_switch函数在切换task的时候,总是取出栈的固定位置的内容,也就是说task_switch函数并不知道栈顶在哪里!最明显的就是,task中有多级函数嵌套,但task_switch只是找到最外一层函数。

说白了,这个“OS”要变成OS还需要改进。

继续完善

尽管OS跑起来了,但跑起来却很奇怪。上面也说到了,“现场保护”。用个具体的例子来说明一下:

在led和数码管两个task中都调用了delay函数。从Keil的反汇编可以看出来,delay函数用到了R5、R6、R7三个寄存器。也就是说led和数码管两个task中的delay函数,是各自拥有R5、R6、R7的。每次切换task前,必须把当前task的这几个值保存到各自的栈里面去。同时把数码管task中delay函数的R5、R6、R7从栈里面恢复到CPU的R寄存器中去。另外,ACC、PSW、DPL、DPH也要保存起来。总共有13个寄存器(13B),加上task运行地址(2B)和task中函数嵌套产生的压栈地址(每嵌套一个2B)。每个任务栈至少也要15B了。而51内部RAM才256(暂不考虑那些增强型51,类似与cc2530、cc2540那种几个K的RAM),也就难怪会有51不适合跑OS了。需要注意的是,要确保任务栈足够大,否则可能会破坏其他任务栈里面的内容(假设任务栈的地址是相邻的)。

如果画个图来表示,大概应该是这个样子的了:task1调用A运行到here的时候,产生了一个任务调度,这时需要先把CPU寄存器里面的内容保存到task1栈里面去。接着把task2栈里面的内容还原到CPU寄存器里面去。这里假设task2上次被剥夺CPU使用权的时候也调用了A。

考虑了“现场保护”后,问题又来了。原先设计的task_switch,return的结果始终都是task中next的值!而task_switch的作用就是用来改变SP的。如果我们在改变SP之前,把现在的SP存起来,那么下次return的值才是真正的栈顶。于是,调度器的代码应该是这样的了:

> 8;
TL0 = tick & 0xff;
return list_tab[task_now];
}
" v:shapes="圆角矩形_x0020_70">

这里要注意的是,调用task_switch的时候,这条语句的地址也会被压入任务栈里面去,但是这个地址并不需要保存。因为这个函数return后就会执行这一句,没必要再把它保存起来了。

加入的“现场保护”后,考虑这样一个现象。想想之前“绑定”的实现。当启动OS的时候,第一次进行任务调度时,当SP指向另一个task后,会执行13次POP指令。那么RETI的结果是啥?没错,不能进入task2。因为之前“绑定”后,栈的长度是2,但却执行了13次POP。真正的入口老早就被POP掉了。于是,又得改!

> 8;
tab[0] = ((int)task) ;
if(list_tab[index] != 16)
list_tab[index++] = tab + 14;
}
" v:shapes="圆角矩形_x0020_72">

对应的,启动list_tab中首个任务的代码也得改。因为首个任务启动前,没有执行POP。

到这里,我们这个OS才看起来像是一个OS。好吧,其实这只是个任务调度器。那怎么才更像一个OS呢?起码要不delay去掉,换成sleep吧。还有,得加个idle吧。还有什么信号量,互斥量,邮箱,and so on。当然,这种资源有限的51单片机,加这些东西有点压力吧。还有,你真的像自己写一个完整的OS吗?如果你的回答是肯定的话,那我的这文章就帮不了你啦。不过,我希望这文章有助于你了解一点点OS。当然,文章可能存在表达不严谨的地方。

时间: 2024-10-27 03:26:44

在51上写一个“OS”原型的相关文章

写一个xml文件到磁盘的方法

/** * 往磁盘上写一个xml文件 * * <?xml version="1.0" encoding="UTF-8" standalone="true"?> //文档的神明 <persons> //标签tag <person id=“18"> //ID是person标签的一个属性 <name>allen</name> <age>36</age> <

使用python写一个监控mysql的脚本,在zabbix web上自定义模板

##先使用MySQLdb的接口关联数据库. [[email protected] python]# cat check_Mysql_custom.py #!/usr/local/bin/python '''author = chenmingle''' '''Description:get mysql status''' import os import sys try:     import MySQLdb as mysql except Exception, e:     print e   

写一个Windows上的守护进程(5)文件系统重定向

写一个Windows上的守护进程(5)文件系统重定向 在Windows上经常操作文件或注册表的同学可能知道,有“文件系统/注册表重定向”这么一回事.大致来说就是32位程序在64位的Windows上运行时,操作系统会把对System32文件夹的访问重定向到SysWow64下,把对HKEY_LOCAL_MACHINE\SOFTWARE的访问重定向到HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node下.当然不止这些路径和注册表.详情请查看MSDN:https://msdn.

写一个Windows上的守护进程(1)开篇

写一个Windows上的守护进程(1)开篇 最近由于工作需要,要写一个守护进程,主要就是要在被守护进程挂了的时候再把它启起来.说起来这个功能是比较简单的,但是我前一阵子写了好多现在回头看起来比较糟糕的代码,所以就想这次写的顺眼一点.写完后发现,诶,还可以哟.于是就总结总结. 一.大致需求 1. 功能——当被守护进程挂掉后再次启动它 2. 可配置需要守护的进程 二.通盘考虑 1. 为了避免重复造轮子,况且有的轮子可能自己也造不出来,上boost库 2. 为了能够获得较高的权限和能够开机自动启动,将

写一个Windows上的守护进程(4)日志其余

写一个Windows上的守护进程(4)日志其余 这次把和日志相关的其他东西一并说了. 一.vaformat C++日志接口通常有两种形式:流输入形式,printf形式. 我采用printf形式,因为流输入不好控制格式. printf形式要求日志接口支持不定长参数,我没有直接在日志实现类里边支持不定长参数,而是只接受一个字符串参数,可以参见第一篇. 为什么呢? 如果要成为不定长参数,就是这样 bool log_string(const LOG_LEVEL level, const char* fi

写一个Windows上的守护进程(6)Windows服务

写一个Windows上的守护进程(6)Windows服务 守护进程因为要开机启动,还要高权限,所以我就把它做成Windows服务了. 关于Windows服务的官方文档,大家可以看https://msdn.microsoft.com/en-us/library/windows/desktop/ms686953(v=vs.85).aspx. 总的来说,服务的行为区别于普通应用程序的地方有以下几点: 1. 一般来说,服务是运行于System用户下的,当然也可以自己指定.也就是说服务可以在无用户登录的情

写一个Windows上的守护进程(3)句柄的管理

写一个Windows上的守护进程(3)句柄的管理 在Windows中编程,跟HANDLE打交道是家常便饭.为了防止忘记CloseHandle,我都是使用do-while-false手法: void f() { HANDLE h = NULL; do { } while (false); if (h) { CloseHandle(h); h = NULL; } } HANDLE一多,就得写好几段长得一样的清理代码,比较麻烦.仔细一想,这个其实很容易写一个关闭器——在出作用域时自动关闭: class

写一个Windows上的守护进程(8)获取进程路径

写一个Windows上的守护进程(8)获取进程路径 要想守护某个进程,就先得知道这个进程在不在.我们假设要守护的进程只会存在一个实例(这也是绝大部分情形). 我是遍历系统上的所有进程,然后判断他们的路径和要守护的进程是否一致,以此来确定进程是否存在. 遍历进程大家都知道用CreateToolhelp32Snapshot系列API,但是他们最后取得的是进程exe名称,不是全路径,如果仅依靠名称就可以达到目的也就罢了,但是有的时候还是得取到全路径,这样会更靠谱一些. 那么问题来了,如何取到进程全路径

写一个Windows上的守护进程(7)捕获异常并生成dump

写一个Windows上的守护进程(7)捕获异常并生成dump 谁都不能保证自己的代码不出bug.一旦出了bug,最好是崩溃掉,这样很快就能被发现,若是不崩溃,只是业务处理错了,就麻烦了,可能很长时间之后才能被发现. 那么如果崩溃掉,怎么查错呢? 写过Windows驱动的同学应该知道,一旦崩溃,系统会生成dump文件,然后就可以根据dump文件.pdb文件.源码用windbg分析了.应用层的程序同样可以在崩溃的时候生成dump文件,只是没人帮你完成这个步骤,得自己动手. 1. API 这里涉及到的