第七十四章 UCOSII实验1-任务调度
前面我们所有的例程都是跑裸机程序,简称裸跑,从本章开始,我们将分为3个章节向大家介绍UCOSII(实时多任务操作系统内核)的使用。本章,我们将向大家介绍UCOSII最基本也是最重要的应用:任务调度。
本章分为如下几个小节:
74.1 嵌入式实时操作系统介绍
74.2 硬件设计
74.3 程序设计
74.4 下载验证
74.1 嵌入式实时操作系统介绍
74.1.1 裸机系统和多任务系统的区别
在嵌入式设备的开发过程中,我们使用的是两种程序,一是裸机程序,前面所有的实验章节使用的都是裸机程序;二是多任务程序,即接下来的三章都是多任务程序。很多人会有疑问用得好好的裸机程序,为什么要用多任务程序呢?
裸机程序最大的特点,就是主函数会有一个大循环,大循环中就会有很多个小任务的实现。任务间是按照顺序进行执行的,换句话来说它们执行等级是一样的,下一个任务想要执行必须等上一个任务执行完成才能进行。这个运行着的大循环我们称之为后台程序。中断是可以打断系统当前的,让后台任务优先被执行,等执行完后,再回到后台被打断处继续执行后台程序,中断处理程序称之为前台程序。这种使用前后台裸机程序的叫做前后台系统,如下图所示:

图74.1.1.1前后台系统
这样的前后台系统在实时性处理方面存在缺陷,例如Task1是重要任务,需要能够得到及时的响应,但是在执行Task3的时候,产生中断。现在的情况是执行Task1条件满足,理想的处理方式就是Task1立刻被执行,但是前后台程序中做不到,因为任务是被顺序执行的,即使Task1十万火急,也必须要等待Task3处理完毕才能被执行。
前面的情况对于要求实时性比较强的产品来说,是不允许的。所以出现了多任务程序,这种使用多任务程序的系统,叫做嵌入式实时操作系统。它把任务分为不同的优先级,当运行条件被满足时,高优先级任务可以打断低优先级任务优先运行,从而极大地提高了系统的实时性。嵌入式实时操作系统执行任务示意图如图74.1.1.2所示:

图74.1.1.2嵌入式实时操作系统执行任务示意图
嵌入式实时操作系统与前后台系统相比,优势明显体现在实时性方面,同时它在多任务管理、任务间通信、内存管理、定时器管理、设备管理等方面也提供了一套完整的机制,极大程度上便利了嵌入式应用程序的开发、管理和维护。
74.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.93.01版本,它的体系结构如图74.1.2.1所示。

图74.1.2.1 UCOSII体系结构图
该版本比早期的UCOSII(如V2.52)多了很多功能,比如软件定时器,支持任务数最大达到255个等,而且修正了很多已知BUG。
在UCOSII的移植,我们只需要修改:cpu_cfg.h、lib_cfg.h和os_cfg.h、app_cfg.h四个文件即可,其中cpu_cfg.h该文件主要是配置CPU相关的一些宏定义,如时间戳、前导置零指令相关等内容;lib_cfg.h,该文件主要用于配置 µC/LIB 组件,如使能内存库中优化的内存相关操作函数 ;os_cfg.h该文件是系统的配置文件,用于配置系统默认的功能, 比如消息队列、信号量、事件标志位等;app_cfg.h该文件用于配置软件定时器的任务优先级等。
图74.1.2.1中定时器的作用是为UCOSII提供系统时钟节拍,实现任务切换和任务延时等功能。这个时钟节拍由OS_TICKS_PER_SEC(在os_cfg.h中定义)设置,一般我们设置UCOSII的系统时钟节拍为1ms~100ms,具体根据你所用处理器和使用需要来设置。本章,我们利用STM32H7R7的SYSTICK定时器来提供UCOSII时钟节拍。
74.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个状态转换关系如图74.1.3.1所示:

图74.1.3.1 UCOSII任务转换关系
74.1.4 任务调度
UCOSII的任务调度思想是:“近似每时每刻让优先级最高的就绪任务处于运行状态”。在具体做法上,它在系统或者用户任务调用系统函数及执行中断服务程序结束时来调用调度器,以确定应该运行的任务并运行它。
在多任务系统中,令CPU中止当前正在运行的任务转而去运行另一个任务的工作叫任务切换,而按照某种规则进行任务切换的工作叫做任务调度。
在UCOSII中,任务调度是由任务调度器来完成。任务调度器的主要工作就有两个,①在任务就绪表中查找具有最高优先级别的就绪任务,②实现任务切换。
74.2 硬件设计
1. 例程功能
创建了3个任务start_task、led0_task和led1_task。其中start_task是创建其他2个任务(led0和led1)的。当start_task创建完其他2个任务后就会挂起。led0_task和led1_task这两个任务分别是让LED0,LED1闪烁。LED0每秒钟亮80ms;LED1每秒钟亮300ms。
2. 硬件资源
1)LED灯
LED0 :LED0 – PD14
LED1: LED1 – PC0
3. 原理图
本章用到的硬件LED灯:LED0和LED1。电路在开发板上已经连接好了,所以在硬件上不需要动任何东西,直接下载代码就可以测试使用。其连接原理图如图74.2.1所示:

图74.2.1 LED与STM32H7R7连接原理图
74.3 程序设计
74.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)
l 函数描述:
用于创建一个任务
l 函数形参:
函数OSTaskCreateExt具有9个形参,如表74.3.1.1所示:

表74.3.1.1 OSTaskCreateExt函数参数描述
l 函数返回值:
OS_ERR_NONE:函数调用成功
OS_ERR_PRIO_EXIST:具有该优先级的任务已经存在
OS_ERR_PRIO_INVALID:参数指定的优先级大于最大优先级
OS_ERR_TASK_CREATE_ISR:在ISR中创建任务
OS_ERR_ILLEGAL_CREATE_RUN_TIME:尝试在安全关键操作启动后创建任务
l 注意事项:
1,任务必须被创建在多任务之前或者运行的任务中
2,任务不能由ISR创建
3,任务必须在死循环中,并且不能返回
2.OSTaskSuspend函数
任务挂起函数,其声明如下:
INT8U OSTaskSuspend (INT8U prio)
l 函数描述:
用于将任务挂起
l 函数形参:
prio:要挂起任务的优先级
l 函数返回值:
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)
l 函数描述:
用于删除任务
l 函数形参:
prio:要删除任务的优先级。如果任务不知道自己优先级,还可以传递参数OS_PRIO_SELF。
l 函数返回值:
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)
l 函数描述:
用于初始化UCOSII内部
l 函数形参:
无
l 函数返回值:
无
5.OSStart函数
多任务启动函数,其声明如下:
void OSStart (void)
l 函数描述:
用于启动多任务
l 函数形参:
无
l 函数返回值:
无
l 注意事项:
多任务的启动是通过调用OSStart实现的,而在启动UCOSII之前至少需要建立一个应用任务。
74.3.2 程序解析
在STM32上运行UCOSII的步骤:
1、移植UCOSII
要使得UCOSII在STM32上正常运行,首先需要移植UCOSII,这部分我们已经为大家做好了。(要学习更详细的UCOS移植方法,可以参考我们关于UCOS的专门教程)
2、编写任务函数并设置其堆栈大小和优先级等参数
编写任务函数,以便UCOSII调用。
设置函数堆栈大小,这个需要根据函数的需求来设置,如果任务函数的局部变量多,嵌套层数多,那么对应的堆栈就得大一些,如果堆栈设置小了,很可能出现的结果就是CPU进入HardFault,遇到这种情况,你就必须把堆栈设置大一点了。另外,有些地方还需要注意堆栈字节对齐的问题,如果任务运行出现莫名其妙的错误(比如用到sprintf出错),请考虑是不是字节对齐的问题。
设置任务优先级,这个需要大家根据任务的重要性和实时性设置,记住高优先级的任务有优先使用CPU的权力。
3、初始化UCOSII,并在UCOSII中创建任务
调用OSInit,初始化UCOSII,通过调用OSTaskCreate函数创建我们的任务。
4、启动UCOSII
调用OSStart,启动UCOSII。
通过以上4个步骤,UCOSII就开始在STM32上面运行了,这里还需要注意我们必须对os_cfg.h进行配置,以满足我们的需求。
1. uc-os2_demo.c代码
这里是UCOSII任务的一些配置以及3个任务函数,如下代码所示:
/* START 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define START_TASK_PRIO 10 /* 开始任务的优先级设置为10 */
#define START_STK_SIZE 128 /* 堆栈大小 */
OS_STK START_TASK_STK[START_STK_SIZE]; /* 任务堆栈 */
void start_task(void *p_arg); /* 任务函数 */
/* LED0 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define LED0_TASK_PRIO 7 /* 任务优先级设置为7 */
#define LED0_STK_SIZE 128 /* 堆栈大小 */
OS_STK LED0_TASK_STK[LED0_STK_SIZE]; /* 任务堆栈 */
void led0_task(void *p_arg); /* 任务函数 */
/* LED1 任务 配置
* 包括: 任务优先级 堆栈大小 等
*/
#define LED1_TASK_PRIO 6 /* 任务优先级设置为6 */
#define LED1_STK_SIZE 128 /* 堆栈大小 */
OS_STK LED1_TASK_STK[LED0_STK_SIZE]; /* 任务堆栈 */
void led1_task(void *p_arg); /* 任务函数 */
/**
* @brief start_task
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void start_task(void *p_arg)
{
OS_CPU_SR cpu_sr = 0;
CPU_INT32U cnts;
OSStatInit(); /* 开启统计任务 */
/* 根据配置的节拍频率配置SysTick */
cnts = (CPU_INT32U)(HAL_RCC_GetSysClockFreq() / OS_TICKS_PER_SEC);
OS_CPU_SysTickInit(cnts);
OS_ENTER_CRITICAL(); /* 进入临界区(关闭中断) */
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); /* 挂起开始任务 */
}
/**
* @brief LED0任务
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void led0_task(void *p_arg)
{
while (1)
{
LED0(0);
OSTimeDly(80);
LED0(1);
OSTimeDly(920);
};
}
/**
* @brief LED1任务
* @param p_arg : 传入参数(未用到)
* @retval 无
*/
void led1_task(void *p_arg)
{
while (1)
{
LED1(0);
OSTimeDly(300);
LED1(1);
OSTimeDly(300);
};
}
该代码我们创建了3个任务:start_task、led0_task和led1_task,优先级分别是10、7和6,堆栈大小都是128(注意OS_STK为32位数据)。这里我们单独创建start_task是为了提供一个单一任务,实现应用程序开始运行之前的准备工作(比如:外设初始化、创建信号量、创建邮箱、创建消息队列、创建信号量集、创建任务、初始化统计任务等等)。
在应用程序中经常有一些代码段必须不受任何干扰地连续运行,这样的代码段叫做临界段(或临界区)。因此,为了使临界段在运行时不受中断所打断,在临界段代码前必须用关中断指令使CPU屏蔽中断请求,而在临界段代码后必须用开中断指令解除屏蔽使得CPU可以响应中断请求。UCOSII提供OS_ENTER_CRITICAL和OS_EXIT_CRITICAL两个宏来实现,这两个宏需要我们在移植UCOSII的时候实现,本章我们采用方法3(即OS_CRITICAL_METHOD为3)来实现这两个宏。因为临界段代码不能被中断打断,将严重影响系统的实时性,所以临界段代码越短越好!
在start_task任务中,我们在创建led0_task和led1_task的时候,不希望中断打断,故使用了临界区。其他两个任务,就十分简单了,我们就不细说了。
另外,一个任务里面一般是必须有延时函数或者其他可以引发任务切换的函数,以释放CPU使用权,否则可能导致低优先级的任务因高优先级的任务不释放CPU使用权而一直无法得到CPU使用权,从而无法运行。
2. main.c代码
我们在main函数只调用了uc_os2_demo函数,如下所示:
int main(void)
{
sys_mpu_config(); /* 配置MPU */
sys_cache_enable(); /* 使能Cache */
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(300, 6, 2); /* 配置时钟,600MHz */
delay_init(600); /* 初始化延时 */
usart_init(115200); /* 初始化串口 */
led_init(); /* 初始化LED */
uc_os2_demo(); /* 运行uC/OS-II例程 */
}
软件设计部分就为大家介绍到这里。
74.4 下载验证
将程序下载到开发板后,可以看到LED0一秒钟闪一次,而LED1则以固定的频率闪烁说明两个任务(led0_task和led1_task)都已经正常运行,符合我们预期的设计。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3099244/ ,如需转载,请注明出处,否则将追究法律责任。