1)实验平台:正点原子开拓者FPGA 开发板
2)摘自《开拓者 Nios II开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/index.html
第十七章uC/OSII任务管理与时间管理
在前面的章节中,我们介绍了uCOSII系统以及uCOSII系统的创建方法。本章我们来学习
uCOSII系统中两个重要的概念,uCOSII系统的任务管理与时间管理。本章包括以下几个部分:
17.1 任务管理和时间管理简介
17.2 实验任务
17.3 硬件设计
17.4 软件设计
17.5 下载验证
任务管理和时间管理简介
任务管理
uCOSII系统的主要工作就是对任务进行管理和调度,那么什么是任务呢?
生活中我们处理一个大问题的时候通常都是将这个问题“分而治之”,把大问题分成多个
小问题,小问题被逐步的解决掉,大问题也就随之解决了。那么这些小问题就可以看成是很多
个小任务。
在我们设计复杂、大型程序的时候也是一样的,将这些负责的程序分割成许多个简单的小
程序,这些小程序就是单个的任务,所有的小任务和谐的工作,最终完成复杂的功能。在操作
系统中这些小任务可以并发执行,从而提高CPU的使用效率。
uCOSII就是一个可剥夺的多任务系统,我们使用uCOSII的一个重要的原因就是它的多任务
处理能力。在uCOSII中任务就是程序实体,uCOSII能够管理和调度这些小任务(程序)。
uCOSII中的任务由三部分组成:任务堆栈、任务控制块和任务程序代码(任务函数),如
图 17.1.1所示:
图 17.1.1 任务的组成
任务控制块:用来记录任务的堆栈指针、当前状态、优先级别等一些与任务管理有关的属
性的表就叫做任务控制块。任务控制块相当于一个任务的身份证,系统就是通过任务控制块来
感知和管理任务的,没有任务控制块的任务不能被系统承认和管理。
任务堆栈:在任务切换和响应中断时,保存CPU寄存器中的内容,以及任务调用其他函数
时需要保存的局部变量,每一个任务都有一个独立的任务堆栈。
任务程序代码:指任务的执行程序。
任务如何才能切换回上一个任务并且还能接着从上次被中断的地方开始运行?恢复现场
即可,现场就是CPU的内部各个寄存器。因此在创建一个新任务时,必须把系统启动这个任务
时所需的CPU各个寄存器初始值事先存放在任务堆栈中。这样当任务获得CPU使用权时,就把任
务堆栈的内容复制到CPU的各个寄存器,从而可以任务顺利地启动并运行。
把任务初始数据存放到任务堆栈的工作就叫做任务堆栈的初始化,用户一般不会直接操作
堆栈初始化函数,任务堆栈初始化函数由任务创建函数OSTaskCreateExt调用。
接下来我们介绍下uCOSII系统的任务状态。uCOSII的每个任务都是一个死循环,每个任务
都处在以下5种状态之一的状态下,这5种状态是:睡眠状态、就绪状态、运行状态、等待状态
(等待某一事件发生)和中断服务状态。
睡眠状态(DORMANT):任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。
可以通过调用OSTaskDel()函数让一个任务进入睡眠状态。也可以通过使用OSTaskCreate()或
OSTaskCreateExt()函数来建立任务,使得任务进入就绪状态,准备运行。
就绪状态(READY):系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,
任务已经准备好了,但由于该任务的优先级比正在运行的任务的优先级低,还暂时不能运行,
这时任务的状态叫做就绪状态。
运行状态(RUNNING):该任务获得CPU使用权,并正在运行中,此时的任务状态叫做运行
状态。
等待状态(WAITING):正在运行的任务,需要等待一段时间或需要等待一个事件发生再
运行时,该任务就会把CPU的使用权让给别的任务而使任务进入等待状态。
中断服务状态(ISR):一个正在运行的任务一旦响应中断申请就会中止运行而去执行中
断服务程序,这时任务的状态叫做中断服务状态。
uCOSII任务的5个状态转换关系如图 17.1.2所示:
图 17.1.2 uCOSII任务状态转换关系
uCOSII的各个状态可以通过函数进行切换,下面是具体包含函数的状态转换图:
图 17.1.3 包含函数的状态转换图
在多任务系统中,令CPU中止当前正在运行的任务转而去运行另一个任务的工作叫做任务切换,而按某种规则进行任务切换的工作就叫做任务调度。uCOSII的任务调度策略非常简单,
它只需要检查等待列表中各任务的优先级,谁的优先级高就运行谁。
这种简单的任务调度策略,虽然可以节省嵌入式系统的资源,但同时也会带来一些问题,
比如说:如果一个高优先级的任务一直占用CPU,那么低优先级的任务将一直等待,因此我们
应该合理地分配任务的优先级。
当有多个任务处于就绪状态时,系统需要在这些任务中选择一个来运行。就绪任务有多个,
CPU却有一个,所以CPU需要一个规则来进行选择。uCOSII采用优先级抢占规则。系统中的每一
个任务根据其重要性都配有一个唯一的优先级(uCOSII中,每一个优先级只能有一个任务),优
先级高的任务先得到执行,优先级低的任务后执行。(数值越小优先级越高)。
uCOSII中的任务分为两种:系统任务和用户任务;系统任务是指由系统提供并为系统管理
服务的任务;我们一般可以不用管它。用户任务是指为了解决应用问题由用户编写的任务。
uCOSII预定义了两个系统任务:空闲任务和统计任务。
1、空闲任务:uCOSII必须创建的任务,此任务由uCOSII自动创建,不需要用户手动创建。
2、统计任务:可选任务,用来统计CPU使用率和各个任务的堆栈使用量。此任务是可选任
务,由宏OS_TASK_STAT_EN控制是否使用此任务。
接下来,我们看看在uCOSII中,与任务相关的几个函数:
1)建立任务函数(OSTaskCreateExt)
如果想让uCOSII管理用户的任务,必须先建立任务。uCOSII给我们提供了2个建立任务的
函数:OSTaskCreate和OSTaskCreateExt。建立任务函数已经在前一章节进行了讲解,详细讲
解请查看前一章节的内容。
2)删除任务函数(OSTaskDel)
所谓的任务删除,其实就是把任务置于睡眠状态,并不是把任务代码给删除了。uCOSII提
供的任务删除函数原型为:INT8U OSTaskDel(INT8U prio),其中参数prio就是我们要删除的
任务的优先级,可见该函数是通过任务优先级来实现任务删除的。
特别注意:任务不能随便删除,必须在确保被删除任务的资源被释放的前提下才能删除!
3)请求任务删除函数(OSTaskDelReq)
前面提到,必须确保被删除任务的资源被释放的前提下才能将其删除,所以我们通过向被
删除任务发送删除请求,来实现任务释放自身占用资源后再删除。uCOSII提供的请求删除任务函数原型为:INT8U OSTaskDelReq(INT8U prio),同样还是通过优先级来确定被请求删除任务。
4)改变任务的优先级函数(OSTaskChangePrio)
uCOSII在建立任务时,会分配给任务一个优先级,但是这个优先级并不是一成不变的,而
是可以通过调用uCOSII提供的函数修改。uCOSII提供的任务优先级修改函数原型为:INT8U
OSTaskChangePrio(INT8U oldprio,INT8U newprio)。
5)任务挂起函数
任务挂起和任务删除有点类似,但是又有区别,任务挂起只是将被挂起任务的就绪标志删
除,并做任务挂起记录,并没有将任务控制块任务控制块链表里面删除,也不需要释放其资源,
而任务删除则必须先释放被删除任务的资源,并将被删除任务的任务控制块也给删了。被挂起
的任务,在恢复(解挂)后可以继续运行。uCOSII提供的任务挂起函数原型为:INT8U
OSTaskSuspend(INT8U prio)。
6)任务恢复函数
有任务挂起函数,就有任务恢复函数,通过该函数将被挂起的任务恢复,让调度器能够重
新调度该函数。uCOSII提供的任务恢复函数原型为:INT8U OSTaskResume(INT8U prio)。
更加详细的任务函数讲解,请参考邵贝贝老师的《嵌入式实时操作系统uCOS-II》一书的
第四章。
时间管理
uCOSII中的任务是一个无限循环并且还是一个抢占式内核,为了使高优先级的任务不至于
独占CPU,可以给其他优先级较低任务获取CPU使用权的机会,uCOSII中除空闲任务外的所有任
务必须在合适的位置调用系统提供的延时函数,让当前的任务暂停运行一段时间并进行一个任
务切换。
uCOSII规定:除了空闲任务之外的所有任务,必须在任务中合适的位置调用系统提供的任
务延时函数。
延时函数使当前任务的运行延时一段时间并进行一次任务调度,以让出 CPU的使用权。该
函数是以系统时钟节拍为基准的,如果系统时钟节拍延长或缩短了,任务的实际延时时间也跟
着延长或缩短。
在这里我们主要介绍五个与时钟节拍相关的系统服务:
1)任务延时函数OSTimeDly()
使用该函数后,任务可以延时一段时间(时间的长短由时钟节拍数决),也就是该任务会
被挂起,并且开始执行处于就绪态中优先级最高的任务。当延时期满或者有别的任务使用
OSTimeDlyResume()取消了延时,该任务就会立刻进入就绪状态。
调用该延时函数时,使用的节拍数处于1到65535之间。当设置的节拍数为0时,表示不延
时;每次时钟节拍发生的时候,OSTCBDly(任务延时挂起的时间)的值都会被减去一,当该值
为0的时候,内核就会把任务放入就绪队列。
2)按时分秒延时函数OSTimeDlyHMSM()
使用OSTimeDlyHMSM()这个函数后,就可以以小时(H)、分(M)、秒(S)和毫秒(m)的形式来
延时了,使用起来非常简单,其工作原理与OSTimeDly()一致,推荐使用OSTimeDlyHMSM()函数。
3)让延时期的任务结束延时的函数OSTimeDlyResume()
用户可以不用等待延时结束,通过使用OSTimeDlyResume()和要恢复的任务的优先级,来
取消延时,进入就绪状态。
4)得到系统时间函数OSTimeGet()
用户调用OSStart()初始化多任务的时候,会有一个32位的计数器开始累加。随着时钟节
拍的发生,计数器累计数到最大值的时候,会从零开始重新计数。使用OSTimeGet()能够获得
该计数器的当前值。
5)修改系统时间函数OSTimeSet()
使用OSTimeSet()可以改变上面提到的计数器中的值。
实验任务
本节通过一个实例,让大家学习和掌握uCOSII系统的任务管理和时间管理功能。
硬件设计
Qsys系统搭建的步骤以及使用的IP核和“创建第一个uC/OSII系统”章节中完全一致,顶
层模块代码除了模块名不一致外,其余也完全一致。如果大家有不明白的地方,可以参考“创
建第一个uC/OSII系统”章节实验,这里不再赘述。
软件设计
uCOSII系统的创建可以参考“创建第一个uC/OSII系统”章节中的软件设计部分。创建完
uCOSII系统之后,只需修改生成的代码,代码修改如下:
1 #include <stdio.h>
2 #include "includes.h"
3
4 void time_task(void* pdata);
5 void CPUInfo_task(void* pdata);
6
7 /* Definition of Task Stacks */
8 #define TASK_STACKSIZE 2048
9 OS_STK start_stk[TASK_STACKSIZE];
10 OS_STK time_stk[TASK_STACKSIZE];
11 OS_STK CPUInfo_stk[TASK_STACKSIZE];
12
13 /* Definition of Task Priorities */
14
15 #define START_PRIORITY 100
16 #define TIME_PRIORITY 101
17 #define CPUINFO_PRIORITY 102
18
19 //开始任务
20 void start_task(void* pdata)
21 {
22 OSTaskCreateExt(time_task,
23 NULL,
24 (void *)&time_stk[TASK_STACKSIZE-1],
25 TIME_PRIORITY,
26 TIME_PRIORITY,
27 time_stk,
28 TASK_STACKSIZE,
29 NULL,
30 0);
31
32
33 OSTaskCreateExt(CPUInfo_task,
34 NULL,
35 (void *)&CPUInfo_stk[TASK_STACKSIZE-1],
36 CPUINFO_PRIORITY,
37 CPUINFO_PRIORITY,
38 CPUInfo_stk,
39 TASK_STACKSIZE,
40 NULL,
41 0);
42
43 OSTaskDel(START_PRIORITY);
44 }
45
46 /* 获取系统时间 */
47 void time_task(void* pdata)
48 {
49 while (1)
50 {
51 printf("The System Time is %d",OSTimeGet());
52 OSTimeDlyHMSM(0, 0, 0, 500);
53 }
54 }
55 /* 获取CPU使用率 */
56 void CPUInfo_task(void* pdata)
57 {
58 while (1)
59 {
60 printf("The CPU Usage is %d",OSCPUUsage);
61 OSTimeDlyHMSM(0, 0, 1, 0);
62 }
63 }
64 /* 创建开始任务 */
65 int main(void)
66 {
67 OSTaskCreateExt(start_task,
68 NULL,
69 (void *)&start_stk[TASK_STACKSIZE-1],
70 START_PRIORITY,
71 START_PRIORITY,
72 start_stk,
73 TASK_STACKSIZE,
74 NULL,
75 0);
76
77 OSStart();
78 return 0;
79 }
首先main函数创建了一个开始任务(start_task),然后启动uCOSII系统。我们在前面说
过,任务是一个无限的循环,由于开始任务只需要运行一次,因此开始任务不需要写成while(1)
的形式。
除了在main函数创建任务外,在任务中也可以创建任务,如代码的第19行至第44行所示。
开始任务(start_task)实现的功能是创建了获取系统时间任务(time_task)和获取CPU使用
率任务(CPUInfo_task)。获取系统时间的函数为OSTimeGet,获取CPU使用率的函数为
OSCPUUsage,并通过printf函数打印出来;OSTimeDlyHMSM函数用于延时,并交出CPU的使用权,
以便于其它准备就绪的任务获取CPU的使用权。
下载验证
接下来编译工程,稍等片刻后console界面会显示Build Finished,即编译成功,此时就
可以下载程序了。开发板连接电源线和下载器,并打开电源开关。
首先下载sof文件,然后下载elf文件。程序下载完成,就会看到Nios II Console界面如
下图所示:
图 17.5.1 Nios II Console界面
由打印结果可知,系统时间每500ms打印一次,CPU的使用率每一秒钟打印一次。需要说明
的是,打印的系统时间不一定从0开始的,可能会有些偏差,这个是正常的。