六十一章 UCOSII实验1-任务调度
前面我们所有的例程都是跑裸机程序,简称裸跑,从本章开始,我们将分3个章节向大家介绍UCOSII(实时多任务操作系统内核)的使用。本章,我们将向大家介绍UCOSII最基本也是最重要的应用:任务调度。
本章分为如下几个小节:
61.1嵌入式实时操作系统介绍
61.2 硬件设计
61.3 程序设计
61.4 下载验证
61.1 嵌入式实时操作系统介绍
61.1.1 裸机系统和多任务系统的区别
在嵌入式设备的开发过程中,我们使用的是两种程序,一是裸机程序,前面所有的实验章节使用的都是使用裸机程序;二是多任务程序,即接下来的三章都是多任务程序。很多人会有疑问用得好好的裸机程序,为什么要用多任务程序呢?
裸机程序最大的特点,就是主函数中会有一个大循环,大循环中就会有很多个小任务的实现。任务间是按照顺序进行执行的,换句话来说它们执行等级是一样的,下一个任务想要执行必须等上一个任务执行完成才能进行。这个运行着的大循环我们称之为后台程序。中断是可以打断系统当前的后台任务优先被执行,等待执行完后,再回到原来后台被打断处继续执行后台程序,中断处理程序称之前台程序。这种使用前后台裸机程序的叫做前后台系统,如下图所示:
图61.1.1.1 前后台系统
这样的前后台系统在实时性处理方便存在缺陷,例如Task1是重要任务,需要能够得到及时的响应,但是在执行Task3的时候,产生中断。现在的情况是执行Task1条件满足,理想的处理方式就是Task1需要立刻被执行,但是前后台程序中做不到,因为任务是被顺序执行的,即使Task1十万火急,也必须要等待Task3处理完毕才能被执行。
前面的情况对于要求实时性比较强的产品来说,是不允许的。所以出现了多任务程序,这种使用多任务程序的系统,叫做嵌入式实时操作系统。它把任务分为不同的优先级,当运行条件被满足时,高优先级任务可以打断低优先级任务优先运行,从而极大地提高了系统的实时性。嵌入式实时操作系统执行任务示意图如图61.1.1.2所示:
图61.1.1.2 嵌入式实时操作系统执行任务示意图
嵌入式实时操作系统相比前后台系统明显体现在实时性方面,同时它在多任务管理、任务间通信、内存管理、定时器管理、设备管理等方面也提供了一套完整的机制,极大程度上便利了嵌入式应用程序的开发、管理和维护。
61.1.2 UCOSII介绍
现在市面上有许多实时操作系统,国外的实时操作系统就有FreeRTOS,UCOS和RTX,国内的实时操作系统就有RT_Thread、LiteOS等。其中FreeRTOS使用率世界最高,UCOS发展历史最悠久。在这里我们主要是对UCOSII进行学习。
UCOSII,全称是Micro Control Operation System Two,是由Micrium公司提供,是一个可移植、可固化、可裁剪的、占先式多任务实时内核,它适用于多种微处理器,微控制器和数字处理芯片。早在1992年就由美国嵌入式专家Jean J.Labrosse在《嵌入式系统编程》杂志中提出,并公布源码。UCOSII只是一个实时操作系统内核,它仅仅包含了任务调度,任务管理,时间管理,内存管理和任务间的通信和同步等基本功能。没有提供输入输出管理,文件系统,网络等额外的服务。该实时系统十分适合初次接触嵌入式实时操作系统的朋友。
本章实验中我们使用的是UCOSII V2.91版本,它的体系结构如图61.1.2.1所示。该版本比早期的UCOSII(如V2.52)多了很多功能,比如软件定时器,支持任务数最大达到255个等,而且修正了很多已知BUG。
图61.1.2.1 UCOSII体系结构图
从上图可以看出,UCOSII的移植,我们只需要修改:os_cpu.h、os_cpu_a.asm和os_cpu.c三个文件即可,其中os_cpu.h是进行数据类型的定义以及处理器相关代码和几个函数原型;os_cpu_a.asm,是移植过程中需要汇编完成的一些函数,主要就是任务切换函数;os_cpu.c定义一些用户HOOK函数。
图中定时器的作用是为UCOSII提供系统时钟节拍,实现任务切换和任务延时等功能。这个时钟节拍由OS_TICKS_PER_SEC(在os_cfg.h中定义)设置,一般我们设置UCOSII的系统时钟节拍为1ms~100ms,具体根据你所用处理器和使用需要来设置。本章,我们利用STM32的SYSTICK定时器来提供UCOSII时钟节拍。
61.1.3 任务定义
在前面也说到有任务,在前面的多任务系统中,我们根据功能的不同,把整个系统分成一个个独立且不无法返回的函数,这些函数称为任务。而UCOSII就是一个能对这些任务的运行进行管理和调度的多任务操作系统。UCOSII最大支持的任务数达到了255个,但是对于我们来说一般64个任务已经足够。
任务类型有两种:一种是系统任务,另一种是用户任务。由系统提供的任务叫系统任务,由用户编写的任务叫用户任务。系统任务是为应用程序提供某种服务或为系统本身服务的。UCOSII具有2个系统任务,即空闲任务和统计任务,占用最低2个优先级。空闲任务是UCOSII优先级最低的任务,当所有其他任务均没有使用CPU时,空闲任务就会占用CPU。统计任务是UCOSII优先级倒数第二低的任务,用于统计CPU的使用率和各个任务的堆栈使用情况。
相对于系统任务而言,我们开发者用得多的就是用户任务。用户任务需要注意的是:用户任务对应的函数是一个带有无限循环体的函数,没有返回值;每一个用户任务具有唯一的优先级号。
实时操作系统为了更好的调度任务,给每一个任务都定义了一个任务控制块TCB(Task Control Block)。这个任务控制块就相当于任务在系统里的身份证,存放着任务的所有信息,比如任务函数指针,任务堆栈指针,任务优先级等。
由于CPU只有一个,所以一个时刻只会有一个任务占用CPU处于运行状态,而其他任务只能处于其他状态。UCOSII系统中的任务具有5种,系统运行起来的时候,每一个任务都处在以下5种状态之一的状态下,这5种状态分别是睡眠状态、就绪状态、运行状态、等待状态和中断服务状态。
睡眠状态,任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。
就绪状态,系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务已经准备好了,但由于该任务的优先级比正在运行的任务的优先级低,还暂时不能运行,这时任务的状态叫做就绪状态。
运行状态,该任务获得CPU 使用权,并正在运行中,此时的任务状态叫做运行状态。
等待状态,正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会把CPU 的使用权让给别的任务而使任务进入等待状态。
中断服务状态,一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态叫做中断服务状态。
UCOSII任务的5个状态转换关系如图61.1.3.1所示:
图61.1.3.1 UCOSII任务转换关系
61.1.4 任务调度
UCOSII的任务调度思想是:“近似每时每刻让优先级最高的就绪任务处于运行状态”。在具体做法上,它在系统或者用户任务调用系统函数及执行中断服务程序结束时来调用调度器,以确定应该运行的任务并运行它。
在多任务系统中,令CPU中止当前正在运行的任务转而去运行另一个任务的工作叫任务切换,而按照某种规则进行任务切换的工作叫做任务调度。
在UCOSII中,任务调度是由任务调度器来完成。任务调度器的主要工作就有两个,①在任务就绪表中查找具有最高优先级别的就绪任务 ②实现任务切换
61.2 硬件设计
1. 例程功能
本实验我们在UCOSII里面创建3个任务:开始任务、LED0任务和LED1任务,开始任务用于创建其他(LED0和LED1)任务,之后挂起;LED0任务用于控制LED0的亮灭,LED0每秒钟亮80ms;LED1任务用于控制LED1的亮灭,LED1亮300ms,灭300ms,依次循环。
2. 硬件资源
1)RGB灯
RED :LED0 - PB4
GREEN :LED1 - PE6
61.3 程序设计
61.3.1 UCOSII驱动函数
在这里主要对本实验用到的UCOSII驱动函数进行介绍。
1. OSTaskCreateExt函数
创建任务函数,该函数是OSTaskCreate函数的扩展,并提供了一些附加功能。OSTaskCreateExt函数创建任务更加灵活,不过会增加一些额外的开销。其声明如下:
INT8U OSTaskCreateExt (void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT8U prio,
INT16U id,
OS_STK *pbos,
INT32U stk_size,
void *pext,
INT16U opt)
- 函数描述:用于创建一个任务
- 函数形参:OSTaskCreatEx函数的9个形参介绍,如表61.3.1.1所示:
参数 | 描述 | |
(*task)(void *p_arg) | 指向任务的指针 | |
*p_arg | 传递给任务的参数 | |
*ptos | 指向任务堆栈栈顶的指针 | |
prio | 任务的优先级 | |
id | 任务的标识 | |
*pbos | 任务堆栈栈底的指针 | |
stk_size | 任务堆栈的容量 | |
*pext | 指向附加数据域的指针 | |
opt | 用于设定操作选项 | |
OS_OPT_TASK_NONE | 表示没有任何选项。 | |
OS_OPT_TASK_STK_CHK | 指定是否允许检测该任务的堆栈。 | |
OS_OPT_TASK_STK_CLR | 指定是否清除该任务的堆栈。 | |
OS_OPT_TASK_SAVE_FP | 指定是否存储浮点寄存器,CPU需要有浮点运。 |
表61.3.1.1 OSTaskCreatEx函数的9个形参介绍
- 函数返回值:OS_ERR_NONE:函数调用成功
OS_ERR_PRIO_EXIST:具有该优先级的任务已经存在
OS_ERR_PRIO_INVALID:参数指定的优先级大于最大优先级
OS_ERR_TASK_CREATE_ISR:在ISR中创建任务
OS_ERR_ILLEGAL_CREATE_RUN_TIME:尝试在安全关键操作启动后创建任务 - 注意事项:1、任务必须被创建在多任务开始之前或者运行的任务中2、任务不能由ISR创建3、任务必须在死循环中,并且不能返回2. OSTaskSuspend函数 任务挂起函数,其声明如下:
INT8U OSTaskSuspend (INT8U prio)
- 函数描述:用于将任务挂起
- 函数形参:Prio:要挂起任务的优先级。
- 函数返回值:OS_ERR_NONE:函数调用成功OS_ERR_TASK_SUSPEND_IDLE:挂起空闲任务OS_ERR_PRIO_INVALID:参数指定的优先级大于最大优先级OS_ERR_TASK_SUSPEND_PRIO:需要挂起的任务不存在OS_ERR_TASK_NOT_EXITS:任务被分配到一个互斥执行3. OSTaskDel函数 删除任务函数,其声明如下:
INT8U OSTaskDel (INT8U prio)
- 函数描述:用于删除任务
- 函数形参:Prio:要删除任务的优先级。如果任务不知道自己优先级,还可以传递参数OS_PRIO_SELF。被删除的任务将回到休眠状态。
- 函数返回值:OS_ERR_NONE:函数调用成功OS_ERR_TASK_DEL_IDLE:删除空闲任务OS_ERR_PRIO_INVALID:参数指定的优先级大于最大优先级OS_ERR_TASK_DEL:任务被分配给互斥量执行OS_ERR_TASK_NOT_EXIST:要删除的任务不存在OS_ERR_TASK_DEL_ISR:在中断处理函数中删除任务4.OSInit函数UCOSII系统初始化函数,其声明如下:
void OSInit (void)
- 函数描述:用于初始化UCOSII内部
- 函数形参:无
- 函数返回值:无5. OSStart函数多任务启动函数,其声明如下:
void OSStart (void)
- 函数描述:用于用于启动多任务
- 函数形参:无
- 函数返回值:无
- 注意事项:
多任务的的启动是通过调用OSStart实现的,而在启动UCOSII之前至少需要建立一个应用任务。
61.3.2 程序流程图
图61.3.2.1 UCOSII任务调度实验
61.3.3 程序解析
在STM32上运行UCOSII的步骤:
- 移植UCOSII要使得UCOSII在STM32上正常运行,首先需要移植UCOSII,这部分我们已经为大家做好了。
这里我们需要注意的一个地方,SYSTEM文件夹里面的系统函数直接支持UCOSII,只需要在sys.h文件里将:SYSTEM_SUPPORT_UCOS宏定义改为1,即可通过delay_init函数初始化UCOSII的系统时钟节拍,为UCOSII提供时钟节拍。 - 编写任务函数并设置其堆栈大小和优先级等参数编写任务函数,以便UCOSII调用。
设置函数堆栈大小,这个需要根据函数的需求来设置,如果任务函数的局部变量多,嵌套层数多,那么对应的堆栈就得大一些,如果堆栈设置小了,很可能出现的结果就是CPU进入HardFault,遇到这种情况,你就必须把堆栈设置大一点了。另外,有些地方还需要注意堆栈字节对齐的问题,如果任务运行出现莫名其妙的错误(比如用到sprintf出错),请考虑是不是字节对齐的问题。
设置任务优先级,这个需要大家根据任务的重要性和实时性设置,记住高优先级的任务有优先使用CPU的权力。 - 初始化UCOSII,并在UCOSII中创建任务调用OSInit,初始化UCOSII,通过调用OSTaskCreate函数创建我们的任务。
- 启动UCOSII
调用OSStart,启动UCOSII。
通过以上4个步骤,UCOSII就开始在STM32上面运行了,这里还需要注意我们必须对os_cfg.h进行部分配置,以满足我们的需求。
main.c代码
在main.c文件下,除了main函数之外,还有UCOSII任务的一些配置以及3个任务函数。我们先看一下UCOSII任务的一些宏定义,如下代码所示:
/* UCOSII任务设置 */
/* START 任务 配置
包括: 任务优先级 堆栈大小 等
*/
#define START_TASK_PRIO 10 /* 开始任务的优先级设置为最低 */
#define START_STK_SIZE 128 /* 堆栈大小 */
OS_STK START_TASK_STK[START_STK_SIZE]; /* 任务堆栈 */
void start_task(void *pdata); /* 任务函数 */
/* LED0 任务 配置
包括: 任务优先级 堆栈大小 等
*/
#define LED0_TASK_PRIO 7 /* 开始任务的优先级设置为最低 */
#define LED0_STK_SIZE 128 /* 堆栈大小 */
OS_STK LED0_TASK_STK[LED0_STK_SIZE]; /* 任务堆栈 */
void led0_task(void *pdata); /* 任务函数 */
/* LED1 任务 配置
包括: 任务优先级 堆栈大小 等
*/
#define LED1_TASK_PRIO 6 /* 开始任务的优先级设置为最低 */
#define LED1_STK_SIZE 128 /* 堆栈大小 */
OS_STK LED1_TASK_STK[LED0_STK_SIZE]; /* 任务堆栈 */
void led1_task(void *pdata); /* 任务函数 */
上面就是对创建这START_TASK、LED0_TASK和LED1_TASK三个任务的参数进行配置,例如优先级、堆栈大小和任务函数。
下面看一下main主函数的代码:
int main(void)
{
sys_cache_enable(); /* 打开L1-Cache */
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(240, 2, 2, 4); /* 设置时钟, 480Mhz */
delay_init(480); /* 延时初始化 */
led_init(); /* 初始化LED */
OSInit(); /* UCOS初始化 */
OSTaskCreateExt((void(*)(void *) )start_task, /* 任务函数 */
(void * )0, /* 传递给任务函数的参数 */
/* 任务堆栈栈顶 */
(OS_STK * )&START_TASK_STK[START_STK_SIZE - 1],
(INT8U )START_TASK_PRIO, /* 任务优先级 */
(INT16U )START_TASK_PRIO,/* 任务ID,这里设置为和优先级一样 */
(OS_STK * )&START_TASK_STK[0], /* 任务堆栈栈底 */
(INT32U )START_STK_SIZE, /* 任务堆栈大小 */
(void * )0, /* 用户补充的存储区 */
(INT16U )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR
| OS_TASK_OPT_SAVE_FP);
/* 任务选项,为了保险起见,所有任务都保存浮点寄存器的值 */
OSStart(); /* 开始任务 */
}
在main函数里,我们按照前面说的在STM32运行UCOSII的步骤进行操作,可以看到先对UCOS进行初始化,再创建start_task任务,最后开始任务。
按照前面所说的,led0_task和led1_task是在start_task中创建,下面让我们看一下那三个任务的代码:
/**
开始任务
无
无
*/
void start_task(void *pdata)
{
OS_CPU_SR cpu_sr = 0;
pdata = pdata;
OSStatInit(); /* 开启统计任务 */
OS_ENTER_CRITICAL(); /* 进入临界区(关闭中断) */
/* LED0任务 */
OSTaskCreateExt((void(*)(void *) )led0_task,
(void * )0,
(OS_STK * )&LED0_TASK_STK[LED0_STK_SIZE - 1],
(INT8U )LED0_TASK_PRIO,
(INT16U )LED0_TASK_PRIO,
(OS_STK * )&LED0_TASK_STK[0],
(INT32U )LED0_STK_SIZE,
(void * )0,
(INT16U )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR
| OS_TASK_OPT_SAVE_FP);
/* LED1任务 */
OSTaskCreateExt((void(*)(void *) )led1_task,
(void * )0,
(OS_STK * )&LED1_TASK_STK[LED1_STK_SIZE - 1],
(INT8U )LED1_TASK_PRIO,
(INT16U )LED1_TASK_PRIO,
(OS_STK * )&LED1_TASK_STK[0],
(INT32U )LED1_STK_SIZE,
(void * )0,
(INT16U )OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR
| OS_TASK_OPT_SAVE_FP);
OS_EXIT_CRITICAL(); /* 退出临界区(开中断) */
OSTaskSuspend(START_TASK_PRIO); /* 挂起开始任务 */
}
/**
任务
传入参数(未用到)
无
*/
void led0_task(void *pdata)
{
while (1)
{
LED0(0);
delay_ms(80);
LED0(1);
delay_ms(920);
}
}
/**
任务
传入参数(未用到)
无
*/
void led1_task(void *pdata)
{
while (1)
{
LED1(0);
delay_ms(300);
LED1(1);
delay_ms(300);
}
}
从上面的代码可以看到start_task函数中的确是创建了led0_task和led1_task两个任务,创建这两个任务后,将自己挂起。
我们单独创建start_task的目的是为了提供一个单一任务,实现应用程序开始之前的准备工作,比如外设初始化,创建任务,初始化统计任务,以及后面讲到的创建信号量、创建邮箱、创建消息队列、创建信号量集等。
在应用程序中经常有一些代码段必须不受任何干扰地连续运行,这样的代码段叫做临界段(或临界区)。因此,为了使临界段在运行时不受中断所打断,在临界段代码前必须用关中断指令使CPU屏蔽中断请求,而在临界段代码后必须用开中断指令接触屏蔽使得CPU可以响应中断请求。UCOSII提供OS_ENTER_CRITICAL 和OS_EXIT_CRITICAL 两个宏来实现,这两个宏需要我们在移植UCOSII的时候实现,本章我们采用方法3(即OS_CRITICAL_METHOD 为3)来实现这两个宏。因为临界段代码不能被中断打断,将严重影响系统的实时性,所以临界段代码越短越好!
在start_task 任务中,我们在创建led0_task 和led1_task 的时候,不希望中断打断,故使用了临界区。其他两个任务,就十分简单了,我们就不细说了,注意我们这里使用的延时函数还是delay_ms,而不是直接使用的 OSTimeDly。
另外,一个任务里面一般是必须有延时函数的,以释放 CPU 使用权,否则可能导致低优先级的任务因高优先级的任务不释放 CPU 使用权而一直无法得到 CPU 使用权,从而无法运行。
61.4 下载验证
将程序下载到开发板后,可以看到LED0一秒钟闪一次,而LED1则以固定的频率闪烁。说明两个任务(led0_task和led1_task)都已经正常运行,符合我们预期的设计。