本帖最后由 正点原子运营 于 2023-10-10 16:35 编辑
第六十九章 UCOSII实验1-任务调度
1)实验平台:正点原子探索者STM32F407开发板
2) 章节摘自【正点原子】STM32F407开发指南 V1.1
6)STM32技术交流QQ群:151941872
前面我们所有的例程都是跑裸机程序,简称裸跑,从本章开始,我们将分为3个章节向大家介绍UCOSII(实时多任务操作系统内核)的使用。本章,我们将向大家介绍UCOSII最基本也是最重要的应用:任务调度。 本章分为如下几个小节: 69.1 嵌入式实时操作系统介绍 69.2 硬件设计 69.3 程序设计 69.4 下载验证
69.1 嵌入式实时操作系统介绍
69.1.1 裸机系统和多任务系统的区别在嵌入式设备的开发过程中,我们使用的是两种程序,一是裸机程序,前面所有的实验章节使用的都是裸机程序;二是多任务程序,即接下来的三章都是多任务程序。很多人会有疑问用得好好的裸机程序,为什么要用多任务程序呢?
裸机程序最大的特点,就是主函数会有一个大循环,大循环中就会有很多个小任务的实现。任务间是按照顺序进行执行的,换句话来说它们执行等级是一样的,下一个任务想要执行必须等上一个任务执行完成才能进行。这个运行着的大循环我们称之为后台程序。中断是可以打断系统当前的,让后台任务优先被执行,等执行完后,再回到后台被打断处继续执行后台程序,中断处理程序称之为前台程序。这种使用前后台裸机程序的叫做前后台系统,如图下图所示: 这样的前后台系统在实时性处理方面存在缺陷,例如Task1是重要任务,需要能够得到及时的响应,但是在执行Task3的时候,产生中断。现在的情况是执行Task1条件满足,理想的处理方式就是Task1立刻被执行,但是前后台程序中做不到,因为任务是被顺序执行的,即使Task1十万火急,也必须要等待Task3处理完毕才能被执行。 前面的情况对于要求实时性比较强的产品来说,是不允许的。所以出现了多任务程序,这种使用多任务程序的系统,叫做嵌入式实时操作系统。它把任务分为不同的优先级,当运行条件被满足时,高优先级任务可以打断低优先级任务优先运行,从而极大地提高了系统的实时性。嵌入式实时操作系统执行任务示意图如图69.1.1.2所示: 图69.1.1.2嵌入式实时操作系统执行任务示意图 嵌入式实时操作系统与前后台系统相比,优势明显体现在实时性方面,同时它在多任务管理、任务间通信、内存管理、定时器管理、设备管理等方面也提供了一套完整的机制,极大程度上便利了嵌入式应用程序的开发、管理和维护。
69.1.2 UCOSII介绍 现在市面上有许多实时操作系统,国外的实时操作系统就有FreeRTOS,UCOS和RTX,国内的实时操作系统就有RT_Thread、LiteOS等。其中,FreeRTOS使用率世界最高,UCOS发展历史最悠久。在这里我们主要是对UCOSII进行学习。
UCOSII,全称是MicroControl operation System Two,是由Micrium公司提供,是一个可移植、可固化、可剪裁的占先式多任务实时内核,它适用于多种微处理器,微控制器和数字处理芯片。早在1992年就由美国嵌入式专家Jean J.Labrosse在《嵌入式系统编程》杂志中提出,并公布源码。UCOSII只是一个实时操作系统内核,它仅仅包含了任务调度,任务管理,时间管理,内存管理和任务间的通信和同步等基本功能。没有提供输入输出管理,文件系统,网络等额外的服务。该实时系统十分适合初次接触嵌入式实时操作系统的朋友。
本章实验中我们使用的是UCOSII V2.91版本,它的体系结构如图69.1.2.1所示。 该版本比早期的UCOSII(如V2.52)多了很多功能,比如软件定时器,支持任务数最大达到255个等,而且修正了很多已知BUG。
从图69.1.2.1可以看出,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,具体根据你所用处理器和使用需要来设置。本章,我们利用STM32F4的SYSTICK定时器来提供UCOSII时钟节拍。
69.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个状态转换关系如图69.1.3.1所示: 69.1.4 任务调度 UCOSII的任务调度思想是:“近似每时每刻让优先级最高的就绪任务处于运行状态”。在具体做法上,它在系统或者用户任务调用系统函数及执行中断服务程序结束时来调用调度器,以确定应该运行的任务并运行它。
在多任务系统中,令CPU中止当前正在运行的任务转而去运行另一个任务的工作叫任务切换,而按照某种规则进行任务切换的工作叫做任务调度。
在UCOSII中,任务调度是由任务调度器来完成。任务调度器的主要工作就有两个,①在任务就绪表中查找具有最高优先级别的就绪任务,②实现任务切换。
69.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 – PF9 LED1 – PF10
3. 原理图 本章用到的硬件LED灯:LED0和LED1。电路在开发板上已经连接好了,所以在硬件上不需要动任何东西,直接下载代码就可以测试使用。其连接原理图如图69.2.1所示: 图69.2.1 LED与STM32F407连接原理图 69.3 程序设计
69.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个形参,如表69.3.1.1所示:
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 (INT8Uprio)
复制代码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 (INT8Uprio)
复制代码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系统初始化函数,其声明如下: l 函数描述: 用于初始化UCOSII内部
l 函数形参: 无
l 函数返回值: 无
5.OSStart函数 多任务启动函数,其声明如下: l 函数描述: 用于启动多任务
l 函数形参: 无
l 函数返回值: 无
l 注意事项: 多任务的启动是通过调用OSStart实现的,而在启动UCOSII之前至少需要建立一个应用任务。
69.3.2 程序流程图 我们在main函数中进行外设和UCOS的初始化后,通过创建起始任务,并在起始任务创建LED0和LED1任务。任务初始化完成后,程序不断在LED0和LED1任务间切换。
69.3.3 程序解析 在STM32上运行UCOSII的步骤: 1、移植UCOSII 要使得UCOSII在STM32上正常运行,首先需要移植UCOSII,这部分我们已经为大家做好了。(要学习更详细的UCOS移植方法,可以参考我们关于UCOS的专门教程)
这里我们需要注意的一个地方,SYSTEM文件夹里面的系统函数直接支持UCOSII,只需要在sys.h文件里将:SYSTEM_SUPPORT_UCOS宏定义改为1,即可通过delay_init函数初始化UCOSII的系统时钟节拍,为UCOSII提供时钟节拍。
2、编写任务函数并设置其堆栈大小和优先级等参数 编写任务函数,以便UCOSII调用。
设置函数堆栈大小,这个需要根据函数的需求来设置,如果任务函数的局部变量多,嵌套层数多,那么对应的堆栈就得大一些,如果堆栈设置小了,很可能出现的结果就是CPU进入HardFault,遇到这种情况,你就必须把堆栈设置大一点了。另外,有些地方还需要注意堆栈字节对齐的问题,如果任务运行出现莫名其妙的错误(比如用到sprintf出错),请考虑是不是字节对齐的问题。
设置任务优先级,这个需要大家根据任务的重要性和实时性设置,记住高优先级的任务有优先使用CPU的权力。
3、初始化UCOSII,并在UCOSII中创建任务 调用OSInit,初始化UCOSII,通过调用OSTaskCreate函数创建我们的任务。
4、启动UCOSII 调用OSStart,启动UCOSII。
通过以上4个步骤,UCOSII就开始在STM32上面运行了,这里还需要注意我们必须对os_cfg.h进行配置,以满足我们的需求。
1. main.c代码在main.c文件下,除了main函数之外,还有UCOSII任务的一些配置以及3个任务函数。我们先看一下UCOSII任务的一些宏定义,如下代码所示: - /* UCOSII任务设置 */
- /* START 任务 配置
- * 包括: 任务优先级 堆栈大小 等
- */
- #defineSTART_TASK_PRIO 10 /* 开始任务的优先级设置为最低 */
- #defineSTART_STK_SIZE 128 /* 堆栈大小 */
- OS_STKSTART_TASK_STK[START_STK_SIZE]; /* 任务堆栈 */
- voidstart_task(void *pdata); /* 任务函数 */
- /* LED0 任务 配置
- * 包括: 任务优先级 堆栈大小 等
- */
- #defineLED0_TASK_PRIO 7 /* 开始任务的优先级设置为最低 */
- #defineLED0_STK_SIZE 128 /* 堆栈大小 */
- OS_STKLED0_TASK_STK[LED0_STK_SIZE]; /* 任务堆栈 */
- void led0_task(void *pdata); /* 任务函数 */
- /* LED1 任务 配置
- * 包括: 任务优先级 堆栈大小 等
- */
- #defineLED1_TASK_PRIO 6 /* 开始任务的优先级设置为最低 */
- #defineLED1_STK_SIZE 128 /* 堆栈大小 */
- OS_STKLED1_TASK_STK[LED0_STK_SIZE]; /* 任务堆栈 */
- voidled1_task(void *pdata); /* 任务函数 */
复制代码上面就是对创建这START_TASK、LED0_TASK和LED1_TASK三个任务的参数进行配置,例如优先级、堆栈大小和任务函数。
下面看一下main主函数的代码: - int main(void)
- {
- HAL_Init(); /* 初始化HAL库 */
- sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz*/
- delay_init(168); /* 延时初始化 */
- 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中创建,下面让我们看一下那三个任务的代码: - /**
- * @brief 开始任务
- * @param 无
- * @retval 无
- */
- voidstart_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); /* 挂起开始任务 */
- }
- /**
- * @brief LED0任务
- * @param pdata : 传入参数(未用到)
- * @retval 无
- */
- voidled0_task(void *pdata)
- {
- pdata = pdata;
- while (1)
- {
- LED0(0);
- delay_ms(80);
- LED0(1);
- delay_ms(920);
- }
- }
- /**
- * @brief LED1任务
- * @param pdata : 传入参数(未用到)
- * @retval 无
- */
- voidled1_task(void *pdata)
- {
- pdata = 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使用权,从而无法运行。
软件设计部分就为大家介绍到这里。
69.4 下载验证将程序下载到开发板后,可以看到LED0一秒钟闪一次,而LED1则以固定的频率闪烁说明两个任务(led0_task和led1_task)都已经正常运行,符合我们预期的设计。 |