嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 【重学51单片机】四、硬件定时器实现分时轮询调度算法

【重学51单片机】四、硬件定时器实现分时轮询调度算法

【说在前面的话】

众所周知,一个简单的嵌入式设备都不是仅仅有一个功能。而每一个功能就相当于程序中的一个驱动(任务),比如一个时钟,就有屏幕、按键、时钟芯片等。因此,要实现一个时钟,就需要实现屏幕显示任务、按键设置任务、和时间计算任务。而在上一篇中我们已经用状态机简单实现了多任务,不过他的缺点就是延时不够精确,所以我们今天就用硬件定时器来精确延时,并实现一种基于分时轮询调度算法的多任务调度器。

我们先简单介绍一下分时轮询,这可是在像51单片机这种小资源单片机中很常用编程结构。所谓分时,就是把多个任务按照时间节点进行划分(也就是这个任务每秒执行一次,那个任务10ms执行一次……)。这样,多个任务就按照时间节点分开了。

那怎么知道到了时间节点该运行哪个任务呢?

这时,就需要使用一个定时器,比如定时1ms,也就是每毫秒询问一下,该任务到没到设定的时间,如果时间到就去执行,每个任务挨个询问一遍,这就是轮询,是不是很简单。

小提示:不过这种分时轮询的程序结构也是有一些小缺点的,就是对一些实时性要求高的任务可能导致处理不及时,这时就需要抢占式的调度器来完成了,不过我们今天不做论述。

【硬件定时器的使用】

好,知道了分时轮询,那我们就用51单片机的硬件定时器,来实现一个精确延时1ms的定时器中断程序。

这里我们也简单讲一下中断的概念。所谓中断是指CPU正在执行某段程序时,外部或内部发生了随机事件,请求CPU去处理。此时,CPU会暂停正在执行的程序转而去执行中断服务程序。如下图

有了中断的概念,我们就看看STC89C52单片机定时器中断程序怎么实现。我们今天要使用的是硬件定时器T2(16位可自动重装),因此我们主要讲一下定时器T2。首先打开数据手册,他一共有6个寄存器,如下图

  • 第一个寄存器T2CON是T2的控制寄存器,如下图

  • TF2为定时器T2溢出中断标志位。也就是说定时器的定时时间到,此位会置1,且该位必须由软件清零。

  • TR2位定时器2启动/停止控制位。当TR2为1时,T2开始计数;当TR2为0时,T2停止计数

  • C/T2为定时器、计数器选择位。也就是当C/T2=0时为定时器(今天使用定时器功能),C/T2=1时为计数器

  • EXEN2为T2外部触发允许控制位。当EXEN2=0时,外部T2EX的跳变对定时器T2无效;当EXEN2=1时,T2EX引脚的负跳变可产生捕获(这次用不到这个功能

  • CP/RL2为捕获/重装选择位。当CP/RL2=0时,定时器T2工作在可重装方式(今天使用这个方式);当CP/RL2=1时,且EXEN2也为1,T2工作在捕获方式。

    好了,这个寄存器就讲这么多,其他今天用不到的大家可以自己看数据手册(* ̄︶ ̄)

接下来再讲一下T2MOD寄存器,如下

  • T2OE为时钟输出控制位。当T2OE=0时,禁止T2作为时钟输出(今天这里需要禁止输出);当T2OE=1时,允许T2作为时钟输出

  • DCEN为递增、递减计数使能位。T2可设置成递增或递减模式,当DCEN=0时,T2(默认)为递增计数(今天使用递增计数

剩下4个寄存器就简单了,他们两两是一对,也就是给16位定时器装初值的(即不同的值代表不同的定时时间)。

那我要定时1ms,初值应该装多少呢?

这个就需要我们计算了。下面以12M晶振为例来计算如下

晶振12M = 12 000 000
1s = 1000ms
STC89C52是12T单片机,也就是12个晶振周期执行一条指令

定时器计数加1需要的时间为

1000/12000000*12=0.001ms=1us

定时器T2是16位的,计数范围为0~65535,定时范围为0~65.535ms
所以定时1ms,需要计数1000

定时器设置为递增,所以初始值为65535-1000=64535

即TH2=64535>>8;  TL2=64535&0xff;

由于我们需要定时器溢出产生中断后可以自动重装,所以RCAP2H = TH2;RCAP2L = TL2;

好,下面我们就开始编写定时器T2的初始化程序,让它定时1ms,如下

#define T1MS 64535void timer2_init(){ //初始化为自动重装且递增计数 T2CON = 0x00; T2MOD = 0x00; //装初值 RCAP2L = TL2 = T1MS; //initial timer2 low byte  RCAP2H = TH2 = T1MS >> 8;       //initial timer2 high byte  TR2 = 1;//T2启动计数}

有了初始化函数,那怎么编写定时器2的中断函数呢?

这个就需要查看数据手册里的中断号了,如下

定时器2使用的中断号为5,使用中断程序就可以这样写,如下

int count = 1000; void tm2_isr() interrupt 5{ TF2 = 0;//清除中断标志位    if (count-- == 0){//1秒     count = 1000; LED1 = ~LED1;//每秒翻转一次LED }}
  • tm2_isr函数名我们可以自己随便取,但是后面的interrupt 5是固定的,也就是加了interrupt 5的函数就是定时器2的中断服务函数了

  • 注意:在中断函数中要记得手动清除中断标志位TF2

  • 我们定义了一个count变量,初始值为1000,每次进入中断函数进行减减(也就是1ms减一次),减到0后让LED灯进行反转。所以我们会看到LED小灯隔1秒闪一次。

哈哈,是不是觉得一个简单的定时器中断服务程序就编写好了,

然而,并没有

因为在初始化中我们还没有打开定时器2的中断,此时需要查看数据手册的中断允许寄存器IE,如下

  • EA为单片机的总中断允许控制位。EA=1,单片机开放中断;EA= 0,屏蔽所有的中断申请

  • ET2为定时器T2的溢出中断允许位。ET2=1,允许T2中断;ET2=0,禁止T2中断

知道了这个,下面把初始化程序再修改一下,如下

void timer2_init(){ //初始化为自动重装且递增计数 T2CON = 0x00; T2MOD = 0x00; //装初值 RCAP2L = TL2 = T1MS; //initial timer2 low byte RCAP2H = TH2 = T1MS >> 8; //initial timer2 high byte TR2 = 1;//T2启动计数 ET2 = 1;//打开T2中断 EA = 1;//打开全局中断}

定时器T2的中断函数就讲完了,接下来就进入今天的主题,实现一个基于分时轮询调度算法的多任务调度器。

【分时轮询调度器】

在实现这个调度器之前,我们需要用C语言中的结构体来定义一个任务,方便调度器调用。定义任务结构体就需要有任务函数,这里我们需要用到C语言中的函数指针。C语言中的函数指针长这样,如下

void (*fun)(void);
  • 是不是有点变态,变量名居然在中间。不过也简单,只要先写出函数原型,然后把函数名用小括号括起来,在里面再加个*号就可以了(* ̄︶ ̄)

当然,你也可以用typedef来定义一个函数指针,如下

typedef void fun_t(void);fun_t* fun;
  • 这样是不是就和定义普通的指针变量一样了(* ̄︶ ̄)

当然,函数指针的讲解也可以看看下面这篇文章,里面还讲了函数指针的套娃形式哦(* ̄︶ ̄)


有了函数指针,我们就开始定义这个结构体,如下

typedef void fun_t(void);typedef struct { char statue;//运行状态 int cycle;//运行周期 int count;//计数变量 fun_t *run;//任务函数}task_t;
  • statue表示运行状态,statue=1,表示运行此任务;statue=0为不运行此任务

  • cycle为运行周期,比如数码管驱动任务每2ms执行一次,则cycle = 2

  • count为计数变量,在定时器中断函数中加加,也就是每毫秒增加一次,达到cycle的值时使statue置1

  • run就是任务函数了,statue=1时执行。

有了任务的结构体,那我们就用他定义两个任务,一个数码管驱动任务,一个时钟显示任务,如下

#define TASK_NUM 2task_t tasks[TASK_NUM];void tasks_init(){  //数码管驱动任务 tasks[0].statue = 0; tasks[0].cycle = 2; tasks[0].run = &smg_display; //时钟显示任务 tasks[1].statue = 0; tasks[1].cycle = 1000;  tasks[1].run = &smg_clock;}
  • 数码管驱动每2ms调用一次,时钟显示1000ms调用一次

接下来我们就让计数值在中断函数中加加,如下

void tm2_isr() interrupt 5{ char i; TF2 = 0;//清除中断标志位 for(i = 0; i < TASK_NUM; i++){ tasks[i].count++;//计数值加1 if(tasks[i].count >= tasks[i].cycle){      //计数值大于周期清零 tasks[i].count = 0; //此任务转为运行状态 tasks[i].statue = 1; } } }
  • 用for循环查询计数值是否大于等于周期,如果是则把运行标志置1

最后,就该在main函数中运行多任务了,如下

void task_progress(){ char i = 0; for(i = 0; i < TASK_NUM; i++){ //判断任务是否是可运行状态 if(1 == tasks[i].statue){ tasks[i].statue = 0; (*tasks[i].run)();//运行该任务 } } }void main(){ timer2_init(); tasks_init();  while(1){        task_progress();     }}
  • task_progress函数中,用for循环判断任务是否是可运行状态,如果是则把状态标志清零,然后运行该任务。

  • (*tasks[i].run)()这个就是用函数指针来调用任务函数

好了,到这里,我们的分时轮询调度器就编写完成了,是不是很简单(* ̄︶ ̄)。当然,我们上一篇写的数码管驱动程序是不能直接拿来用的,需要简单修改,如下

void smg_display(void){  static unsigned char i=0;      SMG_A_DP_PORT=0x00;//消影  P2 = (P2 & 0xf8) | i;//位选 P0= data_display[i];//段选 i++; if(i >= 8){ i = 0; } }
  • 因为每2ms就调用一次这个任务函数,所以把之前的软件延时给去掉了,这样就更简洁了(* ̄︶ ̄)

时钟显示任务我就不在这里修改了,相信大家也会了。不过有需要这个源码的可以在公众号回复【51单片机】来获取。

接下来就是今天的彩蛋环节

彩蛋环节依然是对上面的代码进行优化,使其变得更简洁和优雅。

首先,我们的调度器在添加任务时只能手动在初始化函数中添加,那能不能和其他嵌入式操作系统一样可以动态添加任务呢?

答案是肯定的。

要实现动态添加任务,我们需要再定义一个结构体,如下

#define TASK_NUM_MAX 3typedef struct{ char taskNum;//当前有几个任务  task_t tasks[TASK_NUM_MAX];//任务数组}task_manage_t;
  • taskNum用来表示任务数组tasks中有多少个任务

  • tasks数组最多可以添加3个任务,当然,你也可以修改TASK_NUM_MAX宏来增加任务数

接下来再写一个添加任务的函数,如下

task_manage_t task_manage;void add_task(int cycle,fun_t *run){ if(task_manage.taskNum < TASK_NUM_MAX){ task_manage.tasks[task_manage.taskNum].statue = 0; task_manage.tasks[task_manage.taskNum].cycle = cycle; task_manage.tasks[task_manage.taskNum].run = run; task_manage.taskNum++; } }
  • 此函数有两个参数,第一个cycle就是任务运行周期,第二个参数run就是任务函数

  • 函数中首先判断任务数是不是达到最大值,如果没达到,则添加任务到任务数组(否则添加失败)

  • 每添加一个任务,task_manage.taskNum都要增加1

有了它,初始化任务函数就可以这样写了,如下

void tasks_init(){ task_manage.taskNum = 0; add_task(2,&smg_display);  add_task(1000,&smg_clock);}
  • 记得首先要把task_manage.taskNum的值清零哦,然后添加任务就可以了

  • 怎么样,是不是有点嵌入式操作系统的样子啦

到这里,我们的分时轮询调度器就讲完了,当然还有一些不足之处,还请大家为我指出来。我也建了一个单片机学习交流群,欢迎大家加入,一起学习(* ̄︶ ̄)

原创不易,如果你喜欢我的公众号、觉得我的文章对你有所启发,

请务必“点赞、收藏、转发”,这对我很重要,谢谢!

欢迎订阅    嵌入式小书虫