2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-13912-1-1.html
第五十九章 UCOSII 实验 1-任务调度

前面我们所有的例程都是跑的裸机程序(裸奔),从本章开始,我们将分 3 个章节向大家介
绍 UCOSII(实时多任务操作系统内核)的使用。本章,我们将向大家介绍 UCOSII 最基本也是
最重要的应用:任务调度。本章分为如下几个部分:
59.1 UCOSII 简介
59.2 硬件设计
59.3 软件设计
59.4 下载验证
59.1 UCOSII 简介
UCOSII 的前身是 UCOS,最早出自于 1992 年美国嵌入式系统专家 Jean J.Labrosse 在《嵌
入式系统编程》杂志的 5 月和 6 月刊上刊登的文章连载,并把 UCOS 的源码发布在该杂志的
BBS 上。目前最新的版本:UCOSIII 已经出来,但是现在使用最为广泛的还是 UCOSII,本章
我们主要针对 UCOSII 进行介绍。
UCOSII 是一个可以基于 ROM 运行的、可裁减的、抢占式、实时多任务内核,具有高度可
移植性,特别适合于微处理器和控制器,是和很多商业操作系统性能相当的实时操作系统
(RTOS)。为了提供最好的移植性能,UCOSII 最大程度上使用 ANSI C 语言进行开发,并且已
经移植到近 40 多种处理器体系上,涵盖了从 8 位到 64 位各种 CPU(包括 DSP)。
UCOSII 是专门为计算机的嵌入式应用设计的,
绝大部分代码是用 C 语言编写的。CPU 硬
件相关部分是用汇编语言编写的、总量约 200 行的汇编语言部分被压缩到最低限度,为的是便
于移植到任何一种其它的 CPU 上。用户只要有标准的 ANSI 的 C 交叉编译器,有汇编器、连
接器等软件工具,就可以将 UCOSII 嵌人到开发的产品中。UCOSII 具有执行效率高、占用空间
小、实时性能优良和可扩展性强等特点, 最小内核可编译至 2KB 。UCOSII 已经移植到了几
乎所有知名的 CPU 上。
UCOSII 构思巧妙、结构简洁精练、可读性强,同时又具备了实时操作系统的全部功能,
虽然它只是一个内核,但非常适合初次接触嵌入式实时操作系统的朋友,可以说是麻雀虽小,
五脏俱全。UCOSII(V2.92 版本)体系结构如图 59.1.1 所示:
图 59.1.1 UCOSII 体系结构图
(1)、这部分是系统配置文件,用来配置所需的系统功能,比如需要用到的 UCOSII 的模块、
时钟频率等等。
(2)、这部分为用户的应用程序,即使用 UCOSII 完成的应用层代码,文件不一定命名为 app.c,
可以命名为其他的。注意,app_hooks.c 里面是钩子函数的应用层代码,app_cfg.h 是与 APP 配
置有关的,这个是 Micrium 公司提供的模板,不使用的话就可以直接删掉。
(3)、这部分是 UCOSII 的核心源码,它们是与处理器无关的代码,都是由高度可移植的 ANSI
C 编写的。
(4)、Micrium 重写了 stdlib 库中的一些函数,如内存复制,字符串相关函数等。这样做的
目的是为了保证在不同应用程序和编译器之间的可移植性。
(5)、这部分的文件需要根据不同的 CPU 架构去做修改,也就是移植的过程。从这里可看
出移植的真正核心就是这三个文件的修改。
(6)、此部分是 Micrium 官方封装起来的 CPU 相关功能代码,比如打开和关闭中断等。
(7)、板级支持包(BSP),说白了就是外设驱动代码,根据需求用户自行编写,不一定要用
bsp.c 和 bsp.h 这样的文件命名。cpu_bsp.c 是与 cpu 有关的驱动。
(8)、CPU 厂商提供的针对本公司 CPU 所制作的库函数,比如 ST 针对 STM32 提供的 STD
和 HAL 这种库函数。
图 67.1.1 中定时器的作用是为 UCOSII 提供系统时钟节拍,实现任务切换和任务延时等功
能。这个时钟节拍由 OS_TICKS_PER_SEC(在 os_cfg.h 中定义)设置,一般我们设置的系统时钟节拍为 1ms~100ms,具体根据你所用处理器和使用需要来设置。本章,我们利用
STM32F7 的 SYSTICK 定时器来提供 UCOSII 时钟节拍。
关于 UCOSII 在 STM32F7 的详细移植过程,请参考光盘资料:《STM32F7 UCOS 开发手
册.pdf》,教程在光盘根目录,这里我们就不详细介绍了。
UCOSII 早期版本只支持 64 个任务,但是从 2.80 版本开始,支持任务数提高到 255 个,不
过对我们来说一般 64 个任务都是足够多了,一般很难用到这么多个任务。UCOSII 保留了最高
4 个优先级和最低 4 个优先级的总共 8 个任务,用于拓展使用,单实际上,UCOSII 一般只占用
了最低 2 个优先级,分别用于空闲任务(倒数第一)和统计任务(倒数第二),所以剩下给我
们使用的任务最多可达 255-2=253 个(V2.92)。
所谓的任务,其实就是一个死循环函数,该函数实现一定的功能,一个工程可以有很多这
样的任务(最多 255 个),UCOSII 对这些任务进行调度管理,让这些任务可以并发工作(注
意不是同时工作!
!
,并发只是各任务轮流占用 CPU,而不是同时占用,任何时候还是只有 1
个任务能够占用 CPU),这就是 UCOSII 最基本的功能。Ucos 任务的一般格式为:
void MyTask (void pdata){任务准备工作…While(1)//死循环{任务 MyTask 实体代码; OSTimeDlyHMSM(x,x,x,x);//调用任务延时函数,释放 cpu 控制权,}}假如我们新建了 2 个任务为 MyTask 和 YourTask,这里我们先忽略任务优先级的概念,两个
任务死循环中延时时间为 1s。如果某个时刻,任务 MyTask 在执行中,当它执行到延时函数
OSTimeDlyHMSM 的时候,它释放 cpu 控制权,这个时候,任务 YourTask 获得 cpu 控制权开
始执行,任务 YourTask 执行过程中,也会调用延时函数延时 1s 释放 CPU 控制权,这个过程中
任务 A 延时 1s 到达,重新获得 CPU 控制权,重新开始执行死循环中的任务实体代码。如此循
环,现象就是两个任务交替运行,就好像 CPU 在同时做两件事情一样。
疑问来了,如果有很多任务都在等待,那么先执行那个任务呢?如果任务在执行过程中,
想停止之后去执行其他任务是否可行呢?这里就涉及到任务优先级以及任务状态任务控制的一
些知识,我们在后面会有所提到。如果要详细的学习,建议看任哲老师的《ucosII 实时操作系
统》一书。
前面我们学习的所有实验,都是一个大任务(死循环),这样,有些事情就比较不好处理,
比如:音乐播放器实验,在音乐播放的时候,我们还希望显示歌词,如果是 1 个死循环(一个
任务),那么很可能在显示歌词的时候,音频可能出现停顿(尤其是采样率高的时候),这主
要是歌词显示占用太长时间,导致 IIS 数据无法及时填充而停顿。而如果用 UCOSII 来处理,
那么我们可以分 2 个任务,音乐播放一个任务(优先级高),歌词显示一个任务(优先级低)。
这样,由于音乐播放任务的优先级高于歌词显示任务,音乐播放任务可以打断歌词显示任务,
从而及时给 IIS 填充数据,保证音频不断,而显示歌词又能顺利进行。这就是 UCOSII 带来的
好处。
这里有几个 UCOSII 相关的概念需要大家了解一下:任务优先级,任务堆栈,任务控制块,
任务就绪表和任务调度器。
任务优先级,这个概念比较好理解,ucos 中,每个任务都有唯一的一个优先级。优先级是任务的唯一标识。在 UCOSII 中,使用 CPU 的时候,优先级高(数值小)的任务比优先级低的
任务具有优先使用权,即任务就绪表中总是优先级最高的任务获得 CPU 使用权,只有高优先级
的任务让出 CPU 使用权(比如延时)时,低优先级的任务才能获得 CPU 使用权。UCOSII 不支
持多个任务优先级相同,也就是每个任务的优先级必须不一样。
任务堆栈,就是存储器中的连续存储空间。为了满足任务切换和响应中断时保存 CPU 寄存
器中的内容以及任务调用其他函数时的需要,每个任务都有自己的堆栈。在创建任务的时候,
任务堆栈是任务创建的一个重要入口参数。
任务控制块 OS_TCB,用来记录任务堆栈指针,任务当前状态以及任务优先级等任务属性。
UCOSII 的任何任务都是通过任务控制块(TCB)的东西来控制的,一旦任务创建了,任务控
制块 OS_TCB 就会被赋值。每个任务管理块有 3 个最重要的参数:1,任务函数指针;2,任务
堆栈指针;3,任务优先级;任务控制块就是任务在系统里面的身份证(UCOSII 通过优先级识
别任务),任务控制块我们就不再详细介绍了,详细介绍请参考任哲老师的《嵌入式实时操作
系统 UCOSII 原理及应用》一书第二章。
任务就绪表,简而言之就是用来记录系统中所有处于就绪状态的任务。它是一个位图,系
统中每个任务都在这个位图中占据一个进制位,该位置的状态(1 或者 0)就表示任务是否处于
就绪状态。
任务调度的作用一是在任务就绪表中查找优先级最高的就绪任务,二是实现任务的切换。
比如说,当一个任务释放 cpu 控制权后,进行一次任务调度,这个时候任务调度器首先要去任
务就绪表查询优先级最高的就绪任务,查到之后,进行一次任务切换,转而去执行下一个任务。
关于任务调度的详细介绍,请参考《嵌入式实时操作系统 UCOSII 原理及应用》一书第三章相
关内容。
UCOSII 的每个任务都是一个死循环。每个任务都处在以下 5 种状态之一的状态下,这 5
种状态是:睡眠状态、
就绪状态、
运行状态、
等待状态(等待某一事件发生)和中断服务状态。
睡眠状态,任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。
就绪状态,系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务已经准
备好了,但由于该任务的优先级比正在运行的任务的优先级低, 还暂时不能运行,这时任务的
状态叫做就绪状态。
运行状态,该任务获得 CPU 使用权,并正在运行中,此时的任务状态叫做运行状态。
等待状态,正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任
务就会把 CPU 的使用权让给别的任务而使任务进入等待状态。
中断服务状态,一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程
序,这时任务的状态叫做中断服务状态。
UCOSII 任务的 5 个状态转换关系如图 59.1.2 所示:
图 59.1.2 UCOSII 任务状态转换关系
接下来,我们看看在 UCOSII 中,与任务相关的几个函数:
1) 建立任务函数
如果想让 UCOSII 管理用户的任务,必须先建立任务。UCOSII 给我们提供了 2 个建立
任务的函数:OSTaskCreate 和 OSTaskCreateExt,我们一般用 OSTaskCreate 函数来创建任
务。但是,如果某个任务中要使用到 FPU,那么就只能用函数 OSTaskCreateExt 来创建,
因为 OSTaskCreate 函数并没有提供针对 FPU 的处理选项,而函数 OSTaskCreateExt 有。
OSTaskCreate 函数原型为:
OSTaskCreate(void(task)(voidpd),voidpdata,OS_STKptos,INTU prio);
该函数包括 4 个参数:task:是指向任务代码的指针;pdata:是任务开始执行时,传
递给任务的参数的指针;ptos:是分配给任务的堆栈的栈顶指针;prio 是分配给任务的优
先级。
每个任务都有自己的堆栈,堆栈必须申明为 OS_STK 类型,并且由连续的内存空间组
成。可以静态分配堆栈空间,也可以动态分配堆栈空间。OSTaskCreateExt 函数原型为:
INT8U OSTaskCreateExt (void (task)(void p_arg), voidp_arg, OS_STK ptos,INT8Uprio, INT16U id,OS_STK pbos, INT32U stk_size,voidpext, INT16U opt);
该函数的参数就比较多了:task 是指向任务函数的函数指针;p_arg 指向传递给任务函
数的参数;ptos 是分配给任务的堆栈的栈顶指针;prio 是任务优先级;id 是任务 ID 号,范
围是 0~65535;pbos 是任务堆栈的栈底指针;stk_size 是任务堆栈大小;pext 是用户补充的
存储区,作为对 TCB 的补充,不使用的时候设置为 0;opt 是任务选项,有三个可选选项:
OS_TASK_OPT_STK_CHK、OS_TASK_OPT_STK_CLR 和 OS_TASK_OPT_SAVE_FP。
它们分别为:检查任务堆栈、任务堆栈清零和保存浮点(FPU)寄存器。
2) 任务删除函数
所谓的任务删除,其实就是把任务置于睡眠状态,并不是把任务代码给删除了。UCOSII
提供的任务删除函数原型为:
INT8U OSTaskDel(INT8U prio);
其中参数 prio 就是我们要删除的任务的优先级,可见该函数是通过任务优先级来实现
任务删除的。
特别注意:任务不能随便删除,必须在确保被删除任务的资源被释放的前提下才能删
除!
3) 请求任务删除函数
前面提到,必须确保被删除任务的资源被释放的前提下才能将其删除,所以我们通过
向被删除任务发送删除请求,来实现任务释放自身占用资源后再删除。UCOSII 提供的请
求删除任务函数原型为:
INT8U OSTaskDelReq(INT8U prio);
同样还是通过优先级来确定被请求删除任务。
4) 改变任务的优先级函数
UCOSII 在建立任务时,会分配给任务一个优先级,但是这个优先级并不是一成不变的,
而是可以通过调用 UCOSII 提供的函数修改。UCOSII 提供的任务优先级修改函数原型为:
INT8U OSTaskChangePrio(INT8U oldprio,INT8U newprio);
5) 任务挂起函数
任务挂起和任务删除有点类似,但是又有区别,任务挂起只是将被挂起任务的就绪标
志删除,并做任务挂起记录,并没有将任务控制块任务控制块链表里面删除,也不需要释
放其资源,而任务删除则必须先释放被删除任务的资源,并将被删除任务的任务控制块也
给删了。被挂起的任务,在恢复(解挂)后可以继续运行。UCOSII 提供的任务挂起函数
原型为:
INT8U OSTaskSuspend(INT8U prio);
6) 任务恢复函数
有任务挂起函数,就有任务恢复函数,通过该函数将被挂起的任务恢复,让调度器能
够重新调度该函数。UCOSII 提供的任务恢复函数原型为:
INT8U OSTaskResume(INT8U prio);
UCOSII 与任务相关的函数我们就介绍这么多。最后,我们来看看在 STM32F7 上面运行
UCOSII 的步骤:
1) 移植 UCOSII
要想 UCOSII 在 STM32F7 正常运行,当然首先是需要移植 UCOSII,这部分我们已经为
大家做好了(移植过程参考光盘:STM32F7 UCOS 开发手册.pdf)。
这里我们要特别注意一个地方,ALIENTEK 提供的 SYSTEM 文件夹里面的系统函数直
接支持 UCOSII,只需要在 sys.h 文件里面将:SYSTEM_SUPPORT_OS 宏定义改为 1,即
可通过 delay_init 函数初始化 UCOSII 的系统时钟节拍,为 UCOSII 提供时钟节拍。
2) 编写任务函数并设置其堆栈大小和优先级等参数。
编写任务函数,以便 UCOSII 调用。
设置函数堆栈大小,这个需要根据函数的需求来设置,如果任务函数的局部变量多,嵌
套层数多,那么相应的堆栈就得大一些,如果堆栈设置小了,很可能出现的结果就是 CPU
进入 HardFault,遇到这种情况,你就必须把堆栈设置大一点了。另外,有些地方还需要注
意堆栈字节对齐的问题,如果任务运行出现莫名其妙的错误(比如用到 sprintf 出错),请
考虑是不是字节对齐的问题。
设置任务优先级,这个需要大家根据任务的重要性和实时性设置,记住高优先级的任务
有优先使用 CPU 的权利。
3) 初始化 UCOSII,并在 UCOSII 中创建任务
调用 OSInit,初始化 UCOSII,通过调用 OSTaskCreate 函数创建我们的任务。
4) 启动 UCOSII
调用 OSStart,启动 UCOSII。
通过以上 4 个步骤,UCOSII 就开始在 STM32F7 上面运行了,这里还需要注意我们必须对
os_cfg.h 进行部分配置,以满足我们自己的需要。
59.2 硬件设计
本节实验功能简介:本章我们在 UCOSII 里面创建 3 个任务:开始任务、LED0 任务和 LED1
任务,开始任务用于创建其他(LED0 和 LED1)任务,之后挂起;LED0 任务用于控制 DS0
的亮灭,DS0 每秒钟亮 80ms;LED1 任务用于控制 DS1 的亮灭,DS1 亮 300ms,灭 300ms,
依次循环。
所要用到的硬件资源如下:
1) 指示灯 DS0 、DS1
这个我们在前面已经介绍过了。
59.3 软件设计
本章,我们在第六章实验 (实验 1 )的基础上修改,在该工程源码下面加入 UCOSII 文
件夹,存放 UCOSII 源码(我们已经将 UCOSII 源码分为五个文件夹:uC-CPU、uC-LIB、
UCOS_BSP、uCOS-CONFIG 和 uCOS-II)。
打开工程,新建 UCOSII_BSP、UCOSII_CPU、UCOSII_LIB、UCOSII_CORE、UCOSII_PORT
和 UCOSII_CONFIG 六个分组,分别添加 UCOSII 五个文件夹下的源码,并且添加相应的头文
件路径,最后得到工程如图 59.3.1 所示:
图 59.3.1 向分组中添加源码
本章,我们对 os_cfg.h 里面定义 OS_TICKS_PER_SEC 的值为 200,也就是设置 UCOSII
的时钟节拍为 5ms,同时设置 OS_MAX_TASKS 为 20,也就是最多 20 个任务(包括空闲任务
和统计任务在内),其他配置我们就不详细介绍了,请参考本实验源码。
前面提到,我们需要在 sys.h 里面设置 SYSTEM_SUPPORT_UCOS 为 1,以支持 UCOSII,
通过这个设置,我们不仅可以实现利用 delay_init 来初始化 SYSTICK,产生 UCOSII 的系统时
钟节拍,还可以让 delay_us 和 delay_ms 函数在 UCOSII 下能够正常使用(实现原理请参考 5.1
节),这使得我们之前的代码,可以十分方便的移植到 UCOSII 下。虽然 UCOSII 也提供了延时
函数:OSTimeDly 和 OSTimeDLyHMSM,但是这两个函数的最少延时单位只能是 1 个 UCOSII 时钟
节拍,在本章,即 5ms,显然不能实现 us 级的延时,而 us 级的延时在很多时候非常有用:比
如 IIC 模拟时序,DS18B20 等单总线器件操作等。而通过我们提供的 delay_us 和 delay_ms,则
可以方便的提供 us 和 ms 的延时服务,这比 UCOSII 本身提供的延时函数更好用。
在设置 SYSTEM_SUPPORT_UCOS 为 1 之后,UCOSII 的时钟节拍由 SYSTICK 的中断服
务函数提供,该部分代码如下:
//systick 中断服务函数,使用 OS 时用到void SysTick_Handler(void){if(delay_osrunning==1)//OS 开始跑了,才执行正常的调度处理{OSIntEnter();//进入中断OSTimeTick();//调用 ucos 的时钟服务程序OSIntExit();//退出中断,会触发进行中断级任务切换}}
以上代码,其中 OSIntEnter 是进入中断服务函数,用来记录中断嵌套层数(OSIntNesting
增加 1);OSTimeTick 是系统时钟节拍服务函数,在每个时钟节拍了解每个任务的延时状态,
使已经到达延时时限的非挂起任务进入就绪状态;OSIntExit 是退出中断服务函数,该函数可能
触发一次任务切换(当 OSIntNesting==0&&调度器未上锁&&就绪表最高优先级任务!
=被中断
的任务优先级时),否则继续返回原来的任务执行代码(如果 OSIntNesting 不为 0,则减 1)。
事实上,任何中断服务函数,我们都应该加上 OSIntEnter 和 OSIntExit 函数,这是因为
UCOSII 是一个可剥夺型的内核,中断服务子程序运行之后,系统会根据情况进行一次任务调
度去运行优先级别最高的就绪任务,而并不一定接着运行被中断的任务!
最后,我们打开 main.c,输入如下代码:
/////////////////////////UCOSII 任务设置/////////////////////////////////////START 任务#define START_TASK_PRIO10//设置任务优先级#define START_STK_SIZE128//设置任务堆栈大小OS_STK START_TASK_STK[START_STK_SIZE];//任务堆栈void start_task(void pdata);//任务函数//LED0 任务#define LED0_TASK_PRIO7//设置任务优先级#define LED0_STK_SIZE128//设置任务堆栈大小OS_STK LED0_TASK_STK[LED0_STK_SIZE];//任务堆栈void led0_task(void pdata);//任务函数//LED1 任务#define LED1_TASK_PRIO6//设置任务优先级#define LED1_STK_SIZE128//设置任务堆栈大小OS_STK LED1_TASK_STK[LED1_STK_SIZE];//任务堆栈void led1_task(void pdata);//任务函数int main(void){ Cache_Enable();//打开 L1-Cache HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(432,25,2,9); //设置时钟,216Mhzdelay_init(216); //延时初始化uart_init(115200); //串口初始化 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(); //开始任务}//开始任务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); //挂起开始任务}//LED0 任务void led0_task(void pdata){while(1){LED0(0);delay_ms(80);LED0(1);delay_ms(920);};}//LED1 任务void led1_task(void pdata){while(1){LED1(0);delay_ms(300);LED1(1);delay_ms(300);};}
该部分代码我们创建了 3 个任务:start_task、led0_task 和 led1_task,优先级分别是 10、7
和 6,堆栈大小都是 128(注意 OS_STK 为 32 位数据)。我们在 main 函数只创建了 start_task
一个任务,然后在 start_task 再创建另外两个任务,在创建之后将自身(start_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
使用权,从而无法运行。
软件设计部分就为大家介绍到这里。
59.4 下载验证
在代码编译成功之后,我们通过下载代码到水星 STM32 开发板上,可以看到 DS0 一秒钟
闪一次,而 DS1 则以固定的频率闪烁,说明两个任务(led0_task 和 led1_task)都已经正常运行
了,符合我们预期的设计。
59.5 任务删除,挂起和恢复测试
前面我们简单的建立了两个任务,主要是让大家了解 UCOSII 怎么运行以及怎样创建任务。
下面我们在这一节补充一个实验测试任务的删除,挂起和恢复。为了和寄存器版本手册章节保
持一致,我们这里不另起一章。实验代码在我们光盘的“实验 62 UCOSII 实验 1-2-任务创建删
除挂起恢复”中,主函数文件 main.c 源码如下:
前面我们简单的建立了两个任务,主要是让大家了解 UCOSII 怎么运行以及怎样创建任务。下
面我们在这一节补充一个实验测试任务的删除,挂起和恢复。为了和寄存器版本手册章节保持
一致,我们这里不另起一章。实验代码在我们光盘的“实验 55 UCOSII 入门实验 1-2-任务创建
删除挂起恢复”中,主函数文件 main.c 源码如下:
/////////////////////////UCOSII 任务设置/////////////////////////////////////START 任务#define START_TASK_PRIO10//设置任务优先级#define START_STK_SIZE128//设置任务堆栈大小OS_STK START_TASK_STK[START_STK_SIZE];//任务堆栈void start_task(void pdata);//任务函数//LED0 任务#define LED0_TASK_PRIO7//设置任务优先级#define LED0_STK_SIZE128//设置任务堆栈大小OS_STK LED0_TASK_STK[LED0_STK_SIZE];//任务堆栈void led0_task(void pdata);//任务函数//LED1 任务#define LED1_TASK_PRIO6//设置任务优先级#define LED1_STK_SIZE128//设置任务堆栈大小OS_STK LED1_TASK_STK[LED1_STK_SIZE];//任务堆栈void led1_task(void pdata);//任务函数int main(void){ Cache_Enable();//打开 L1-Cache HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(432,25,2,9); //设置时钟,216Mhzdelay_init(216); //延时初始化uart_init(115200); //串口初始化 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(); //开始任务}//开始任务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); //挂起开始任务}//LED0 任务void led0_task(void pdata){while(1){LED0(0);delay_ms(80);LED0(1);delay_ms(920);};}//LED1 任务void led1_task(void pdata){while(1){LED1(0);delay_ms(300);LED1(1);delay_ms(300);};}
该代码在 start_task 中创建了 3 个任务分别为 led_task, beep_task 和 key_task。led_task 是
LED0 和 LED1 每隔 500ms 翻转一次。beep_task 在没有收到删除请求的时候是隔一段时间蜂鸣
器鸣叫一次,key_task 是进行按键扫描。当 KEY_RIGHT 按键按下的时候挂起任务 led_task,这
是 LED0 和 LED1 停止闪烁。当 KEY_LEFT 按键按下的时候,如果 led_task 被挂起则恢复之,
如果没有挂起则没有影响。当 KEY_UP 按键按下的时候删除任务 beep_task。当 KEY1 按键按
下的时候,重新创建任务 beep_task。
我们的测试顺序为:首先下载代码之后可以看到 LED0 和 LED1 不断闪烁,同时蜂鸣器不
断鸣叫。这个时候我们按下 KEY0 之后 led_task 任务被挂起,我们可以看到 LED 不再闪烁。接
着我们按下 KEY2,led_task 任务重新恢复,可以看到 LED 恢复闪烁。然后我们按下 KEY_UP,
任务 beep_task 被删除,所以蜂鸣器不再鸣叫。这个时候我们再按下按键 KEY_DOWN,任务
beep_task 被重新创建,所以蜂鸣器恢复鸣叫。