一个超级简单的多任务操作系统[转载]

最近在某校上着一门51单片机课程,学生新学单片机,同时新学C语言,因而软硬件基础暂时都还很薄弱,难以一下子掌握过于复杂的问题,因而不容易编写出结构良好、运行稳定的单片机程序,这可把我愁坏了。

在教完定时器的内容之后,我终于忍不住了,针对学生的特殊情况,用了整整一下午,特别设计了一个超级简单的多任务操作系统,并用了2次课的时间教会学生,终于使学生能够编写稍有复杂度的程序了,效果很不错。

现在,我把这个超级简单的多任务操作系统(暂命名为EventOS)分享出来,给初学者们参考。

1. EventOS操作系统简介:

  • 这是一个事件驱动型操作系统,也称为”事件-响应”式操作系统
  • 它只是一个框架,没有OS源码,也不需要OS源码
  • 通过摸拟单片机的中断响应模型而工作
  • 属于非抢占式伪操作系统,但实时性可以达到硬件的极限
  • 简单到不需要提供OS源码,懂中断就能写能用
  • 暂命名为EventOS

    2. 单片机的中断响应模型

    单片机的中断响应模型很简单:每个中断源被触发后,都会向单片机的相应中断标志位发送”置1″信号。当单片机的硬件检测到该”置1″信号后,就会自动触发及运行相应的中断服务程序(前提是开中断)。而该中断服务程序一旦运行,单片机就会通过硬件自动或通过软件手动将该中断标志位及时清0,以便该中断标志位能够接收下一次的”置1″信号。如图1所示。


    图1 单片机的中断响应模型

    3. EventOS的任务响应模型

    EventOS模拟了单片机的中断响应模型:每个事件源被触发后,都会向操作系统的相应事件标志位发送”置1″信号。当操作系统检测到”置1″信号后,就会自动触发及运行相应的任务程序。而该任务程序一旦运行,操作系统就会自动或通过任务程序手动将该事件标志位及时清0,以便该事件标志位能够接收下一次的”置1″信号。如图2所示:


    图2 EventOS的任务响应模型

    但是为了尽可能地简化概念、逻辑,以及代码的设计,减轻学生的学习负担,EventOS对该模型进行了进一步的简化:将中断标志位的检测过程交给具体的任务程序,而非操作系统。当任务检测到自己的事件标志位”置1″后,任务自己去清除该标志位,并主动响应该事件。换言之,EventOS放弃了抢占式任务切换能力,操作系统无法主动触发及运行任务程序,而只能由任务自己查询到事件标志位之后,由任务主动响应之。简化后的任务响应模型如下图所示:


    图3 简化后的任务响应模型

    5. EventOS的基本框架

    呃。。。不就是一个大循环嘛,史上最简单的操作系统,最适合零基础的新手了。

    //教学中使用的是STC89C52RC单片机,晶振为12MHz,时钟为1MHz
    

    #include <STC89C5xRC.H>
    									

     

    //定义一个全局变量,用于管理事件标志位(可管理8个)
    								

    //8个标志位不够用时,可以多定义几个全局变量,或扩展event的位宽
    								

    unsigned
    								char event =
    										0x00;
    

    //请在这里添加注释,说明每个标置位是干哈的,例如:
    								

    //bit0...
    

    //bit1...
    

     

    void
    								main(void) {
    

    
    								//初始化
    								

    
    							//......
    

     

    
    								//轮流执行while(1)中的任务
    									

    
    							while(1) {
    

            task0();//0号任务
    								

            task1();//1号任务
    								

            task2();//2号任务
    								

        }
    

    }
    						

    6. 任务(task)的基本框架

    任务本质上是一个由事件驱动的响应型函数:仅当该任务的事件标置位”置1″时,该任务的具体业务代码才会真的执行。注意,单片机自带的各种中断,它们的中断服务程序本身也可以理解成EventOS的”任务”。只不过这些任务比较特殊,它们的事件标志位(即中断标志位)由硬件分配,而不必在event中分配;同时,它们的响应时机也由硬件触发,而不必在while(1)大循环中调用;它们可应用于实时性极强的场合,从而使EventOS的实时性达到单片机的硬件极限——在这一点上,EventOS的实时性,比其它一切实时和非实时、抢占式和非抢占式操作系统都要高。

    void
    								task0(void) {
    

    
    								//仅当检测到事件标志位"1",才执行代码
    									

    
    							if(event &
    							1
    									<<
    									0) {
    

    
    								//及时清0事件标志位,使标志位能接收新的事件
    									

            event &=
    							~(1
    									<<
    									0);
    

    
    								//然后执行具体的业务...
    

    
    								//这里是具体的任务代码
    								

        }
    

    }
    						

    7. 如何向任务发送事件?

    很简单,只要将全局变量event的相应标志位(称为事件标志位)”置1″即可。该事件的响应者(目标任务)会自己检测和清除该标志位,并响应该事件,而无需事件的发起者操心。

    //发送0号事件
    								

    event |=
    								1
    										<<
    										0;
    						

    EventOS应用示例

    示例1 八位段式数码管显示

    电路如图4所示。


    图4 截图自LY-51S V3.0

    J2的1、2脚分别由单片机的P0.0、P0.1引脚控制,分别作为数码管的位数据、段数据的锁存引脚。J3的1~8脚分别由单片机的P2.0~P2.7引脚控制,作为数码管的位数据、段数据的数据引脚。

    数码管的硬件驱动原理:段锁存器输出的图像数据,会同步连接到八位数码管的段引脚上。此时的八位数码管,谁得到了位信号,谁就能显示图像数据。如果都得到了位信号,就都能显示图像数据——但每位数码管显示的图像数据是一样的,这并非我们的期望。若希望每位数码管位分别显示不同的图像数据,可采用分时复用的方法,逐位和循环向八位数码管传送相应的位信号和图像数据,当循环(也叫扫描)速度足够快时,人眼就会误以为八位数码管是”同时点亮”的,因则能看到八数码管的各位稳守地显示不同图像。

    根据有关资料,当扫描频率高于25Hz时,人眼感应看不到数码管闪烁。如果你相信这些有关资料,眼睛就废了。实测60Hz时,免强无闪感,但快速晃动数码管时,依然能感受到闪感,眼睛累。扫描频率越高,能够容许的无闪感晃动速度就越快。当扫描频率高于120Hz时,即使以极快的速度晃动数码管,数码管也不会有闪感。故将数码管的扫描频率定为125Hz,即扫描周期为8ms,位周期为1ms。因而,需要通过定时器实现一个1ms的定时。对于12T的STC89C52RC单片机,在12MHz晶振、1MHz时钟下,时钟周期为1us,则1ms需要定时1000个时钟周期。

    数码管的软件驱动原理:首先通过定时器循环定时约1ms;然后在1ms中断中发送0号事件。编写一个任务程序,当它检测到0号事件时,立即清除该事件的标志位,然后执行按键扫描任务。该任务每次执行时,都切换一位数码来显示内容,每执行八次,就完成了一轮数码管位的显示,耗时约8ms,每秒能够完成约125次扫描。

    1ms定时中断:1ms定时时间到了之后,向事件标志管理器(event)发送0号事件。

    void
    								T0_1ms_isr(void)  interrupt   1
    													

    {
    

    
    								//重新装载,约定时1ms1000个时钟周期)
    									

        TH0 = (unsigned
    									char)((65536
    													-
    													1000) >>
    															8);
    

        TL0 = (unsigned
    									char)(65536
    													-
    													1000);
    

     

    
    								//1ms定时时间到了,发送一个"1ms事件的标志位"
    

    
    								//假定0号事件是"1ms事件标志位"
    

        event |=
    							1
    									<<
    									0;
    

    }
    

     

    //定时器0初始化,工作于方式116位计数方式
    								

    void
    								T0_init(void) {
    

        TH0 = (unsigned
    									char)((65536
    													-
    													1000) >>
    															8);
    

        TL0 = (unsigned
    									char)(65536
    													-
    													1000);
    

        TMOD |=
    							1
    									<<
    									0;
    

        ET0 =
    							1;
    

        TR0 =
    							1;
    

    }
    						

    数码管扫描任务

    需要使用seg.h文件

    //给定数值(下标),查找图像数据
    								

    unsigned
    								char segMap[10] = {
    

        SEG_IMAGE_0,
    

        SEG_IMAGE_1,
    

        SEG_IMAGE_2,
    

        SEG_IMAGE_3,
    

        SEG_IMAGE_4,
    

        SEG_IMAGE_5,
    

        SEG_IMAGE_6,
    

        SEG_IMAGE_7
    

        SEG_IMAGE_8,
    

        SEG_IMAGE_9,
    

    };
    

     

    //数码管的显示缓冲区,默认显示20211125
    

    unsigned
    								char segBuffer[8] = {2, 0, 2, 1, 1, 1, 2, 5};
    

     

    //数码管扫描任务
    								

    //仅在0号事件标志位"1"时执行具体业务
    								

    //需要在while(1)主循环中调用本任务
    								

    void
    								task_seg_scanf(void) {
    

    
    							static
    							unsigned
    									char cursor =
    											0;
    

     

    
    							if(event &
    							1
    									<<
    									0) {
    

    
    								//及时将0号标志位清0
    

            event &=
    							~(1
    									<<
    									0);
    

     

    
    								//以下是具体业务代码,每隔约1ms执行一次
    									

    
    								//清空上一位数码管的图像数据
    								

    
    								//可以消除残影
    								

            P2 =
    							0x00;
    

    
    								//送锁存信号
    								

            P0 |=
    							1
    									<<
    									0;
    

    
    								//恢复锁存引脚
    								

            P0 &=
    							~(1
    									<<
    									0);
    

     

    
    								//写当前数码管的位数据
    								

    
    								//共阴数码管,位信号为低电平有效
    								

            P2 =
    							~(1
    									<< cursor);
    

    
    								//同上,只是换了写法
    								

            P0 ^=
    							1
    									<<
    									0;
    

            P0 ^=
    							1
    									<<
    									0;
    

     

    
    								//写当前数码管的图像数据
    								

            P2 = segMap[segBuffer[cursor]];
    

    
    								//同上,只是换了写法
    								

            P0 ^=
    							1
    									<<
    									0;
    

            P0 ^=
    							1
    									<<
    									0;
    

     

    
    								//使游标在0~7之间循环
    									

    
    							if(++cursor ==
    							8) {
    

                cursor =
    							0;
    

            }
    

        }
    

    }
    

     

    //数码管初始化
    								

    void
    								seg_init(void) {
    

    
    								//确保锁存引脚P0.0P0.1为低电平态
    									

        P0 &=
    							~0x03;
    

    }
    						

    代码解析:cursor为当前需要刷新的数码管的编号(游标),segBuffer[cursor]表示缓冲区中的cursor号数码管的数值,segMap[segBuffer[cursor]]是根据该数值找到对应的数码管图像数据,P2 = segMap[segBuffer[cursor]]; 是把该图像数据赋值给P2,即送到数码管的段引脚。

    完整代码:

    //教学中使用的是STC89C52RC单片机,晶振为12MHz,时钟为1MHz
    

    #include <STC89C5xRC.H>
    									

    #include "seg.h"
    									

     

    //定义一个全局变量,用于管理事件标志位(可管理8个)
    								

    //8个标志位不够用时,可以多定义几个全局变量,或扩展event的位宽
    								

    unsigned
    								char event =
    										0x00;
    

    //请在这里添加注释,说明每个标置位是干哈的,例如:
    								

    //bit0:事件"1ms定时时间到"的标志位
    								

     

    //给定数值(下标),查找图像数据
    								

    unsigned
    								char segMap[10] = {
    

        SEG_IMAGE_0,
    

        SEG_IMAGE_1,
    

        SEG_IMAGE_2,
    

        SEG_IMAGE_3,
    

        SEG_IMAGE_4,
    

        SEG_IMAGE_5,
    

        SEG_IMAGE_6,
    

        SEG_IMAGE_7
    

        SEG_IMAGE_8,
    

        SEG_IMAGE_9,
    

    };
    

     

    //数码管的显示缓冲区,默认显示20211125
    

    unsigned
    								char segBuffer[8] = {2, 0, 2, 1, 1, 1, 2, 5};
    

     

    //1ms定时中断服务程序(中断号为1
    								

    void
    								T0_1ms_isr(void)  interrupt   1
    													

    {
    

    
    								//重新装载,约定时1ms
    

        TH0 = (unsigned
    									char)((65536
    													-
    													1000) >>
    															8);
    

        TL0 = (unsigned
    									char)(65536
    													-
    													1000);
    

     

    
    								//1ms定时时间到了,发送一个"1ms事件的标志位"
    

    
    								//假定0号事件是"1ms事件标志位"
    

        event |=
    							1
    									<<
    									0;
    

    }
    

     

    //定时器0初始化,工作于方式116位计数方式
    								

    void
    								T0_init(void) {
    

        TH0 = (unsigned
    									char)((65536
    													-
    													1000) >>
    															8);
    

        TL0 = (unsigned
    									char)(65536
    													-
    													1000);
    

        TMOD |=
    							1
    									<<
    									0;
    

        ET0 =
    							1;
    

        TR0 =
    							1;
    

    }
    

     

    //数码管扫描任务
    								

    //仅在0号事件标志位"1"时执行具体业务
    								

    void
    								task_seg_scanf(void) {
    

    
    							static
    							unsigned
    									char cursor =
    											0;
    

     

    
    							if(event &
    							1
    									<<
    									0) {
    

    
    								//及时将0号标志位清0
    

            event &=
    							~(1
    									<<
    									0);
    

     

    
    								//以下是具体业务代码,每隔约1ms执行一次
    									

    
    								//清空上一位数码管的图像数据
    								

    
    								//可以消除残影
    								

            P2 =
    							0x00;
    

    
    								//送锁存信号
    								

            P0 |=
    							1
    									<<
    									0;
    

    
    								//恢复锁存引脚
    								

            P0 &=
    							~(1
    									<<
    									0);
    

     

    
    								//写当前数码管的位数据
    								

    
    								//共阴数码管,位信号为低电平有效
    								

            P2 =
    							~(1
    									<< cursor);
    

    
    								//同上,只是换了写法
    								

            P0 ^=
    							1
    									<<
    									0;
    

            P0 ^=
    							1
    									<<
    									0;
    

     

    
    								//写当前数码管的图像数据
    								

            P2 = segMap[segBuffer[cursor]];
    

    
    								//同上,只是换了写法
    								

            P0 ^=
    							1
    									<<
    									0;
    

            P0 ^=
    							1
    									<<
    									0;
    

     

    
    								//使游标在0~7之间循环
    									

    
    							if(++cursor ==
    							8) {
    

                cursor =
    							0;
    

            }
    

        }
    

    }
    

     

    //数码管初始化
    								

    void
    								seg_init(void) {
    

    
    								//确保锁存引脚P0.0P0.1为低电平态
    									

        P0 &=
    							~0x03;
    

    }
    

     

    void
    								main(void) {
    

    
    								//初始化
    								

        seg_init();
    

        T0_init();
    

    
    								//开总中断
    								

        EA =
    							1;
    

     

    
    								//轮流执行while(1)中的任务
    									

    
    							while(1) {
    

            task_seg_scanf();
    

        }
    

    }
    						

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注