正点原子stm32mini板型号(正点原子STM32Mini板资料连载)
正点原子stm32mini板型号(正点原子STM32Mini板资料连载)40.4 下载验证40.3 软件设计最重要的应用:任务调度。本章分为如下几个部分:40.1 UCOSII 简介40.2 硬件设计
1)实验平台:正点原子STM32mini开发板
2)摘自《正点原子STM32 不完全手册(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子
第四十章 UCOSII 实验 1-任务调度
前面我们所有的例程都是跑的裸机程序(裸奔),从本章开始,我们将分 3 个章节向大家介
绍 UCOSII(实时多任务操作系统内核)的使用。本章,我们将向大家介绍 UCOSII 最基本也是
最重要的应用:任务调度。本章分为如下几个部分:
40.1 UCOSII 简介
40.2 硬件设计
40.3 软件设计
40.4 下载验证
40.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.91 版本)体系结构如图 40.1.1 所示:
图 40.1.1 UCOSII 体系结构图
注意本章我们使用的是:UCOSII V2.91 版本,该版本 UCOSII 比早期的 UCOSII(如 V2.52)
多了很多功能(比如多了软件定时器,支持任务数最大达到 255 个等),而且修正了很多已知
BUG。不过,有两个文件:os_dbg_r.c 和 os_dbg.c,我们没有在上图列出,也不将其加入到我
们的工程中,这两个主要用于对 UCOS 内核进行调试支持,比较少用到。
从上图可以看出,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 时钟节拍。
关于UCOSII在STM32的详细移植,请参考光盘资料(《UCOSII在STM32的移植详解.pdf》),
这里我们就不详细介绍了。
UCOSII 早期版本只支持 64 个任务,但是从 2.80 版本开始,支持任务数提高到 255 个,不
过对我们来说一般 64 个任务都是足够多了,一般很难用到这么多个任务。UCOSII 保留了最高
4 个优先级和最低 4 个优先级的总共 8 个任务,用于拓展使用,单实际上,UCOSII 一般只占用
了最低 2 个优先级,分别用于空闲任务(倒数第一)和统计任务(倒数第二),所以剩下给我
们使用的任务最多可达 255-2=253 个(V2.91)。
所谓的任务,其实就是一个死循环函数,该函数实现一定的功能,一个工程可以有很多这
样的任务(最多 255 个),UCOSII 对这些任务进行调度管理,让这些任务可以并发工作(注
意不是同时工作!!,并发只是各任务轮流占用 CPU,而不是同时占用,任何时候还是只有 1
个任务能够占用 CPU),这就是 UCOSII 最基本的功能。
前面我们学习的所有实验,都是一个大任务(死循环),这样,有些事情就比较不好处理,
比如:MP3 实验,在 MP3 播放的时候,我们还希望显示歌词,如果是一个死循环(一个任务),
那么很可能在显示歌词的时候,MP3 声音出现停顿(尤其是高码率的时候),这主要是歌词显
示占用太长时间,导致 VS1053 由于不能及时得到数据而停顿。而如果用 UCOSII 来处理,那
么我们可以分 2 个任务,MP3 播放一个任务(优先级高),歌词显示一个任务(优先级低)。
这样,由于 MP3 任务的优先级高于歌词显示任务,MP3 任务可以打断歌词显示任务,从而及
时给 VS1053 提供数据,保证音频不断,而显示歌词又能顺利进行。这就是 UCOSII 带来的好
处。
UCOSII 的任何任务都是通过一个叫任务控制块(TCB)的东西来控制的,每个任务管理
块有 3 个最重要的参数:1,任务函数指针;2,任务堆栈指针;3,任务优先级;任务控制块就
是任务在系统里面的身份证(UCOSII 通过优先级识别任务),任务控制块我们就不再详细介
绍了,详细介绍请参考任哲老师的《嵌入式实时操作系统 UCOSII 原理及应用》一书第二章。
在 UCOSII 中,使用 CPU 的时候,优先级高(数值小)的任务比优先级低的任务具有优先
使用权,即任务就绪表中总是优先级最高的任务获得 CPU 使用权,只有高优先级的任务让出
CPU 使用权(比如延时)时,低优先级的任务才能获得 CPU 使用权。UCOSII 不支持多个任务
优先级相同,也就是每个任务的优先级必须不一样。
任务的调度其实就是 CPU 运行环境的切换,即:PC 指针、SP 指针和寄存器组等内容的存
取过程,关于任务调度的详细介绍,请参考《嵌入式实时操作系统 UCOSII 原理及应用》一书
第三章相关内容。
UCOSII 的每个任务都是一个死循环。每个任务都处在以下 5 种状态之一的状态下,这 5
种状态是:睡眠状态、
就绪状态、
运行状态、
等待状态(等待某一事件发生)和中断服务状态。
睡眠状态,任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。
就绪状态,系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务已经准
备好了,但由于该任务的优先级比正在运行的任务的优先级低, 还暂时不能运行,这时任务的
状态叫做就绪状态。
运行状态,该任务获得 CPU 使用权,并正在运行中,此时的任务状态叫做运行状态。
等待状态,正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任
务就会把 CPU 的使用权让给别的任务而使任务进入等待状态。
中断服务状态,一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程
序,这时任务的状态叫做中断服务状态。
UCOSII 任务的 5 个状态转换关系如图 40.1.2 所示:
图 40.1.2 UCOSII 任务状态转换关系
接下来,我们看看在 UCOSII 中,与任务相关的几个函数:
1) 建立任务函数
如果想让 UCOSII 管理用户的任务,必须先建立任务。UCOSII 提供了我们 2 个建立任
务的函数:OSTaskCreat 和 OSTaskCreatExt,我们一般用 OSTaskCreat 函数来创建任务,
该函数原型为:OSTaskCreate(void(*task)(void*pd) void*pdata OS_STK*ptos INTU prio)。该
函数包括 4 个参数:task:是指向任务代码的指针;pdata:是任务开始执行时,传递给任
务的参数的指针;ptos:是分配给任务的堆栈的栈顶指针;prio 是分配给任务的优先级。
每个任务都有自己的堆栈,堆栈必须申明为 OS_STK 类型,并且由连续的内存空间组
成。可以静态分配堆栈空间,也可以动态分配堆栈空间。
OSTaskCreatExt 也可以用来创建任务,详细介绍请参考《嵌入式实时操作系统 UCOSII
原理及应用》3.5.2 节。
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与任务相关的函数我们就介绍这么多。最后,我们来看看在STM32上面运行UCOSII的步骤:
1) 移植 UCOSII
要想 UCOSII 在 STM32 正常运行,当然首先是需要移植 UCOSII,这部分我们已经为大
家做好了(参考光盘源码,想自己移植的,请参考光盘 UCOSII 资料)。
这里我们要特别注意一个地方,ALIENTEK 提供的 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 进行部分配置,以满足我们自己的需要。
40.2 硬件设计
本节实验功能简介:本章我们在 UCOSII 里面创建 3 个任务:开始任务、LED0 任务和 LED1
任务,开始任务用于创建其他(LED0 和 LED1)任务,之后挂起;LED0 任务用于控制 DS0
的亮灭,DS0 每秒钟亮 80ms;LED1 任务用于控制 DS1 的亮灭,DS1 亮 300ms,灭 300ms,依次循环。
所要用到的硬件资源如下:
1) 指示灯 DS0 、DS1
40.3 软件设计
本章,我们在第六章实验 (实验 1 )的基础上修改,在该工程源码下面加入 UCOSII 文
件夹,存放UCOSII源码(我们已经将UCOSII源码分为三个文件夹:CORE、PORT和CONFIG)。
打开工程,新建 UCOSII-CORE、UCOSII-PORT 和 UCOSII-CONFIG 三个分组,分别添加
UCOSII 三个文件夹下的源码,并将这三个文件夹加入头文件包含路径,最后得到工程如图
40.3.1 所示:
图 40.3.1 添加 UCOSII 源码后的工程
UCOSII-CORE 分组下面是 UCOSII 的核心源码,我们不需要做任何变动。
UCOSII-PORT 分组下面是我们移植 UCOSII 要修改的 3 个代码,这个在移植的时候完成。
UCOSII-CONFIG 分组下面是 UCOSII 的配置部分,主要由用户根据自己的需要对 UCOSII进行裁剪或其他设置。
本章,我们对 os_cfg.h 里面定义 OS_TICKS_PER_SEC 的值为 200,也就是设置 UCOSII
的时钟节拍为 5ms,同时设置 OS_MAX_TASKS 为 10,也就是最多 10 个任务(包括空闲任务和统计任务在内),其他配置我们就不详细介绍了,请参考本实验源码。前面提到,我们需要在 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 中断服务函数 使用 ucos 时用到
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,输入如下代码:
//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[LED1_STK_SIZE];
//任务函数
void led1_task(void *pdata);
int main(void)
{
HAL_Init();
//初始化 HAL 库
Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟 72M
delay_init(72);
//初始化延时函数
LED_Init();
//初始化 LED
OSInit();
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;
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); //挂起开始任务
}
//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 使用权,从而无法运行。软件设计部分就为大家介绍到这里。
40.4 下载验证
在代码编译成功之后,我们通过下载代码到 MiniSTM32 开发板上,可以看到 DS0 一秒钟
闪一次,而 DS1 则以固定的频率闪烁,说明两个任务(led0_task 和 led1_task)都已经正常运行
了,符合我们预期的设计。