如何编写一个最简单的嵌入式操作系统(1)简单任务调度
2016-12-14 14:25
267 查看
转载于http://blog.csdn.net/zds9204/article/details/18867239
放寒假了终于有时间学习一下嵌入式操作系统的知识。一直想做嵌入式底层开发,但以前没有接触过这方面的知识,现在一边学习一边写博客,与大家分享一下自己的学习历程。
一直认为能够自己编写一个操作系统,才是真正的学会了操作系统的知识。所以选择了陈旭武的《轻松自编小型嵌入式操作系统》。但是看了一部分后觉得,书中用拼音命名变量的习惯,已及作者编写的操作系统极高的内存占用率实在是让人无力吐槽了。所以这本书平时翻一翻还可以,作为入门教材就不向大家推荐了。博客内容也借鉴了书中比较优秀的一部分内容,说在前面。
以现代观点而言,一个标准个人电脑的OS应该提供以下的功能:
进程管理(Processing management)
内存管理(Memory management)
文件系统(File system)
网络通讯(Networking)
安全机制(Security)
用户界面(User interface)
驱动程序(Device drivers)
但一个最简易的嵌入式操作系统,所包含的可以少很多。最简单的操作系统,通常都是围绕着进程管理展开的。所以,现在可以尝试下一个最简单的“操作系统”,只能做简单地进行人工任务调度。为了简单起见,使用最简单的AT89S52运行程序:内存小的数的清字节数,外设只有几个IO,结构简单,很方便操作系统的编写。
相信大家都很熟悉,用单片机裸跑,程序一般都写成如下一个大的while死循环:
[cpp] view
plain copy
void main (void)
{
while (1) /* repeat forever */
{
do_something();
}
}
或者又像:
[cpp] view
plain copy
void main (void)
{
while (1) /* repeat forever */
{
do_something1();
do_something2(); //Catch data input
do_something3();
.
.
.
}
}
这里每一个函数完成一个独立的操作或者任务,这些函数(也可以叫任务)以一定的顺序执行,一个接着一个。这里的任务切换,单纯就是执行完一个,再执行另一个。不断循环。
但是,一旦增加更多的任务,那么执行的顺序就变成了一个问题。在以上的例子中,一旦函数do_something1()运行了太长的时间,那么主循环就需要很长的时间才可以执行到do_something2()。如果do_something2()是接收输入数据的函数,数据就很有可能丢失。当然,我们也可以在循环中插入更多的do_something2()函数调用,或者把do_something1()拆分成几个比较小的部分。但这就比较考验编程者功力了,如果任务太多,编写程序将成为一个相当复杂的问题。
这时,一个帮助你分配各个任务运行时间的操作系统就很有必要了。在操作系统中,任务一般形如:
[cpp] view
plain copy
void check_serial_io_task (void) _task_ 1
{
/* This task checks for serial I/O */
}
void process_serial_cmds_task (void) _task_ 2
{
/* This task processes serial commands */
}
void check_kbd_io_task (void) _task_ 3
{
/* This task checks for keyboard I/O */
}
任务之间的切换已经交给操作系统完成了,熟悉的main函数和while(1)一般已经隐去不见了。
[cpp] view
plain copy
<pre code_snippet_id="175088" snippet_file_name="blog_20140129_4_6370941"></pre>
<pre></pre>
任务切换,看起来很玄,实际上说白了,就是改变程序指针PC的值。前边写的_task_ 1,_task_ 2,编译以后,都存储在ROM中。把PC指向这段ROM,他就执行了,想切换另一个任务,就用PC指向那个任务。就这么简单。这样说,是不是就是PC=一个地址就可以了?不行,因为绝大多数单片机,是不允许给PC寄存器直接赋值的。那样写,编译器会报错的。一般操作系统,都用以下方法改变PC的值:
[cpp] view
plain copy
unsigned char Task_Stack1[3];
Task_Stack1[1] = (uint16) Task_1;
Task_Stack1[2] = (uint16) Task_1 >> 8;
SP = Task_Stack1+2;
}//编译成RET
PC的值不能直接改变,但是可以变通,通过其他方式改变PC的值。一个函数执行完毕,总是要改变PC的。这是,PC是如何改变的呢?函数执行前,PC被压入了堆栈中。函数结束,要调用的是RET指令,也就是PC出栈。压在堆栈中的原始PC值,这时从堆栈中弹出,程序又回到了原来的位置。这里就是模仿这一过程:模拟一个堆栈的结构,把要执行的函数入口地址(C语言中的函数名)装入其中,把SP指向这个自己创建的堆栈栈顶。一个RET指令,就将[SP]和[SP-1]弹到PC中了。就这样,PC改变到了要执行的函数入口地址,开始执行目标函数。(AT89s52的PC为16位,压到堆栈中是两个字节)
[cpp] view
plain copy
typedef unsigned char uint8;
typedef unsigned int uint16;
#include <reg52.h>
sbit led0 = P0^0;
sbit led1 = P0^1;
sbit led2 = P0^2;
uint8 Cur_TaskID; //当前运行的任务号
uint8 Task_Stack0[10]; //0号任务的堆栈
uint8 Task_Stack1[10];
uint8 Task_Stack2[10];
uint8 Task_StackSP[3]; //3个堆栈的栈顶指针
//Task_StackSP[0] -> Task_Stack0
//Task_StackSP[1] -> Task_Stack1
//Task_StackSP[2] -> Task_Stack2
void Task_0(); //任务0
void Task_1(); //任务1
void Task_2(); //任务2
void Task_Scheduling(uint8 Task_ID); //任务调度
void main (void)
{
Task_Stack0[1] = (uint16) Task_0; <span style="white-space:pre"> </span>//按照小端模式,任务函数入口地址装入任务堆栈
Task_Stack0[2] = (uint16) Task_0 >> 8;
Task_Stack1[1] = (uint16) Task_1;
Task_Stack1[2] = (uint16) Task_1 >> 8;
Task_Stack2[1] = (uint16) Task_2;
Task_Stack2[2] = (uint16) Task_2 >> 8;
Task_StackSP[0] = Task_Stack0;
Task_StackSP[0] += 2; //刚入栈两个元素。这里取得栈顶地址,即Task_Stack0[2]
Task_StackSP[1] = Task_Stack1;
Task_StackSP[1] += 2;
Task_StackSP[2] = Task_Stack2;
Task_StackSP[2] += 2;
Cur_TaskID = 0;
SP = Task_StackSP[0]; //SP取得0号任务的栈顶地址
}//利用main的返回指令RET,使PC取得0号任务入口地址
//任务调度函数
void Task_Scheduling(uint8 Task_ID)
{
Task_StackSP[Cur_TaskID] = SP;
Cur_TaskID = Task_ID;
SP = Task_StackSP[Cur_TaskID];
}
//0号任务函数
void Task_0()
{
while(1)
{
led0 = 0;
Task_Scheduling(1);
}
}
//1号任务函数
void Task_1()
{
while(1)
{
led1 = 0;
Task_Scheduling(2);
}
}
//2号任务函数
void Task_2()
{
while(1)
{
led2 = 0;
Task_Scheduling(0);
}
}
代码要做的,就是3个任务的顺序执行。任务调度函数Task_Scheduling的思想也即如前面所述。在Keil中可以运行代码,可以看到,程序在3个任务中顺序执行了。
写在前面:
放寒假了终于有时间学习一下嵌入式操作系统的知识。一直想做嵌入式底层开发,但以前没有接触过这方面的知识,现在一边学习一边写博客,与大家分享一下自己的学习历程。一直认为能够自己编写一个操作系统,才是真正的学会了操作系统的知识。所以选择了陈旭武的《轻松自编小型嵌入式操作系统》。但是看了一部分后觉得,书中用拼音命名变量的习惯,已及作者编写的操作系统极高的内存占用率实在是让人无力吐槽了。所以这本书平时翻一翻还可以,作为入门教材就不向大家推荐了。博客内容也借鉴了书中比较优秀的一部分内容,说在前面。
最简单的任务调度
以现代观点而言,一个标准个人电脑的OS应该提供以下的功能:进程管理(Processing management)
内存管理(Memory management)
文件系统(File system)
网络通讯(Networking)
安全机制(Security)
用户界面(User interface)
驱动程序(Device drivers)
但一个最简易的嵌入式操作系统,所包含的可以少很多。最简单的操作系统,通常都是围绕着进程管理展开的。所以,现在可以尝试下一个最简单的“操作系统”,只能做简单地进行人工任务调度。为了简单起见,使用最简单的AT89S52运行程序:内存小的数的清字节数,外设只有几个IO,结构简单,很方便操作系统的编写。
1.裸跑的任务和操作系统中的任务
相信大家都很熟悉,用单片机裸跑,程序一般都写成如下一个大的while死循环:[cpp] view
plain copy
void main (void)
{
while (1) /* repeat forever */
{
do_something();
}
}
或者又像:
[cpp] view
plain copy
void main (void)
{
while (1) /* repeat forever */
{
do_something1();
do_something2(); //Catch data input
do_something3();
.
.
.
}
}
这里每一个函数完成一个独立的操作或者任务,这些函数(也可以叫任务)以一定的顺序执行,一个接着一个。这里的任务切换,单纯就是执行完一个,再执行另一个。不断循环。
但是,一旦增加更多的任务,那么执行的顺序就变成了一个问题。在以上的例子中,一旦函数do_something1()运行了太长的时间,那么主循环就需要很长的时间才可以执行到do_something2()。如果do_something2()是接收输入数据的函数,数据就很有可能丢失。当然,我们也可以在循环中插入更多的do_something2()函数调用,或者把do_something1()拆分成几个比较小的部分。但这就比较考验编程者功力了,如果任务太多,编写程序将成为一个相当复杂的问题。
这时,一个帮助你分配各个任务运行时间的操作系统就很有必要了。在操作系统中,任务一般形如:
[cpp] view
plain copy
void check_serial_io_task (void) _task_ 1
{
/* This task checks for serial I/O */
}
void process_serial_cmds_task (void) _task_ 2
{
/* This task processes serial commands */
}
void check_kbd_io_task (void) _task_ 3
{
/* This task checks for keyboard I/O */
}
任务之间的切换已经交给操作系统完成了,熟悉的main函数和while(1)一般已经隐去不见了。
[cpp] view
plain copy
<pre code_snippet_id="175088" snippet_file_name="blog_20140129_4_6370941"></pre>
<pre></pre>
2.如何做任务切换
还是说单片机裸跑,裸跑时,把C语言文件编译成汇编,可以看到,是用CALL指令去调一个任务函数,执行完毕后,用RET退出。但是这样的方法用在切换频繁的操作系统中,就无疑不适合了,因为我们无法做到预知什么时候退出,即调用RET。任务切换,看起来很玄,实际上说白了,就是改变程序指针PC的值。前边写的_task_ 1,_task_ 2,编译以后,都存储在ROM中。把PC指向这段ROM,他就执行了,想切换另一个任务,就用PC指向那个任务。就这么简单。这样说,是不是就是PC=一个地址就可以了?不行,因为绝大多数单片机,是不允许给PC寄存器直接赋值的。那样写,编译器会报错的。一般操作系统,都用以下方法改变PC的值:
[cpp] view
plain copy
unsigned char Task_Stack1[3];
Task_Stack1[1] = (uint16) Task_1;
Task_Stack1[2] = (uint16) Task_1 >> 8;
SP = Task_Stack1+2;
}//编译成RET
PC的值不能直接改变,但是可以变通,通过其他方式改变PC的值。一个函数执行完毕,总是要改变PC的。这是,PC是如何改变的呢?函数执行前,PC被压入了堆栈中。函数结束,要调用的是RET指令,也就是PC出栈。压在堆栈中的原始PC值,这时从堆栈中弹出,程序又回到了原来的位置。这里就是模仿这一过程:模拟一个堆栈的结构,把要执行的函数入口地址(C语言中的函数名)装入其中,把SP指向这个自己创建的堆栈栈顶。一个RET指令,就将[SP]和[SP-1]弹到PC中了。就这样,PC改变到了要执行的函数入口地址,开始执行目标函数。(AT89s52的PC为16位,压到堆栈中是两个字节)
3.一个最简单的人工调度系统
应用上面的思想,写一个最简单的3任务人工调度系统。代码如下:[cpp] view
plain copy
typedef unsigned char uint8;
typedef unsigned int uint16;
#include <reg52.h>
sbit led0 = P0^0;
sbit led1 = P0^1;
sbit led2 = P0^2;
uint8 Cur_TaskID; //当前运行的任务号
uint8 Task_Stack0[10]; //0号任务的堆栈
uint8 Task_Stack1[10];
uint8 Task_Stack2[10];
uint8 Task_StackSP[3]; //3个堆栈的栈顶指针
//Task_StackSP[0] -> Task_Stack0
//Task_StackSP[1] -> Task_Stack1
//Task_StackSP[2] -> Task_Stack2
void Task_0(); //任务0
void Task_1(); //任务1
void Task_2(); //任务2
void Task_Scheduling(uint8 Task_ID); //任务调度
void main (void)
{
Task_Stack0[1] = (uint16) Task_0; <span style="white-space:pre"> </span>//按照小端模式,任务函数入口地址装入任务堆栈
Task_Stack0[2] = (uint16) Task_0 >> 8;
Task_Stack1[1] = (uint16) Task_1;
Task_Stack1[2] = (uint16) Task_1 >> 8;
Task_Stack2[1] = (uint16) Task_2;
Task_Stack2[2] = (uint16) Task_2 >> 8;
Task_StackSP[0] = Task_Stack0;
Task_StackSP[0] += 2; //刚入栈两个元素。这里取得栈顶地址,即Task_Stack0[2]
Task_StackSP[1] = Task_Stack1;
Task_StackSP[1] += 2;
Task_StackSP[2] = Task_Stack2;
Task_StackSP[2] += 2;
Cur_TaskID = 0;
SP = Task_StackSP[0]; //SP取得0号任务的栈顶地址
}//利用main的返回指令RET,使PC取得0号任务入口地址
//任务调度函数
void Task_Scheduling(uint8 Task_ID)
{
Task_StackSP[Cur_TaskID] = SP;
Cur_TaskID = Task_ID;
SP = Task_StackSP[Cur_TaskID];
}
//0号任务函数
void Task_0()
{
while(1)
{
led0 = 0;
Task_Scheduling(1);
}
}
//1号任务函数
void Task_1()
{
while(1)
{
led1 = 0;
Task_Scheduling(2);
}
}
//2号任务函数
void Task_2()
{
while(1)
{
led2 = 0;
Task_Scheduling(0);
}
}
代码要做的,就是3个任务的顺序执行。任务调度函数Task_Scheduling的思想也即如前面所述。在Keil中可以运行代码,可以看到,程序在3个任务中顺序执行了。
相关文章推荐
- 如何编写一个最简单的嵌入式操作系统(1)简单任务调度
- 如何编写一个简单的嵌入式操作系统 (2)时间片轮转
- 如何编写一个简单的嵌入式操作系统 (2)时间片轮转
- Linux操作系统的简单指令及如何使用vim编写一个程序,然后使用gcc查看【预处理】、【编译】、【汇编】、【链接】各阶段文件的内容。
- 教你如何使用java语言编写一个简单的SqlHelper类
- 如何编写并运行你写的简单“操作系统”
- 浅谈操作系统是如何工作的及简单的进程调度的linux实现
- 如何使用libgdx编写一个简单的游戏(三)— 人性化
- 如何使用libgdx编写一个简单的游戏(一)— 雏形
- 如何用Python编写一个简单的爬虫
- 简单思考如何编写描述一个模块
- 实现一个最简单的嵌入式操作系统
- 实现一个最简单的嵌入式操作系统
- 如何用FFmpeg编写一个简单播放器详细步骤介绍(转载)
- 如何快速理解一个全新的嵌入式操作系统(续)
- 如何写一个最简单的操作系统
- 如何编写一个简单的 taglib
- ARM嵌入式编程(无操作系统、基于MDK)之最简单的程序:点亮一个LED灯
- 如何使用Python为Hadoop编写一个简单的MapReduce程序
- 实现一个最简单的嵌入式操作系统