快捷搜索:  汽车  科技

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)63.3 软件设计63.2 硬件设计上一章,我们学习了 UCOSII 的信号量和邮箱的使用,本章,我们将学习消息队列、信号量集和软件定时器的使用。本章分为如下几个部分:63.1 UCOSII 消息队列、信号量集和软件定时器简介

1)实验平台:alientek 阿波罗 STM32F767 开发板

2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(1)

第六十三章 UCOSII 实验 3-消息队列、信号量集和软件定时

上一章,我们学习了 UCOSII 的信号量和邮箱的使用,本章,我们将学习消息队列、信号

量集和软件定时器的使用。本章分为如下几个部分:

63.1 UCOSII 消息队列、信号量集和软件定时器简介

63.2 硬件设计

63.3 软件设计

63.4 下载验证

63.1 UCOSII 消息队列、信号量集和软件定时器简介

上一章,我们介绍了信号量和邮箱的使用,本章我们介绍比较复杂消息队列、信号量集以

及软件定时器的使用。

消息队列

使用消息队列可以在任务之间传递多条消息。消息队列由三个部分组成:事件控制块、消

息队列和消息。当把事件控制块成员 OSEventType 的值置为 OS_EVENT_TYPE_Q 时,该事件

控制块描述的就是一个消息队列。

消息队列的数据结构如图 63.1.1 所示。从图中可以看到,消息队列相当于一个共用一个任

务等待列表的消息邮箱数组,事件控制块成员 OSEventPtr 指向了一个叫做队列控制块(OS_Q)

的结构,该结构管理了一个数组 MsgTbl[],该数组中的元素都是一些指向消息的指针。

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(2)

图 63.1.1 消息队列的数据结构

队列控制块(OS_Q)的结构定义如下:

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(3)

表 63.1.1 队列控制块各参数含义

其中,可以移动的指针为 OSQIn 和 OSQOut,而指针 OSQStart 和 OSQEnd 只是一个标志

(常指针)。当可移动的指针 OSQIn 或 OSQOut 移动到数组末尾,也就是与 OSQEnd 相等时,

可移动的指针将会被调整到数组的起始位置 OSQStart。也就是说,从效果上来看,指针 OSQEnd

与 OSQStart 等值。于是,这个由消息指针构成的数组就头尾衔接起来形成了一个如图 63.1.2

所示的循环的队列。

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(4)

图 63.1.2 消息指针数组构成的环形数据缓冲区

在UCOSII初始化时,系统将按文件os_cfg.h中的配置常数OS_MAX_QS定义OS_MAX_QS

个队列控制块,并用队列控制块中的指针 OSQPtr 将所有队列控制块链接为链表。由于这时还

没有使用它们,故这个链表叫做空队列控制块链表。

接下来我们看看在 UCOSII 中,与消息队列相关的几个函数(未全部列出,下同)。

1) 创建消息队列函数

创建一个消息队列首先需要定义一指针数组,然后把各个消息数据缓冲区的首地址存

入这个数组中,然后再调用函数 OSQCreate 来创建消息队列。创建消息队列函数 OSQCreate

的原型为:

OS_EVENT *OSQCreate(void**start INT16U size);

其中,start 为存放消息缓冲区指针数组的地址,size 为该数组大小。该函数的返回值

为消息队列指针。

2) 请求消息队列函数

请求消息队列的目的是为了从消息队列中获取消息。任务请求消息队列需要调用函数

OSQPend,该函数原型为:

void*OSQPend(OS_EVENT*pevent INT16U timeout INT8U *err);

其中,pevent 为所请求的消息队列的指针,timeout 为任务等待时限,err 为错误信息。

3) 向消息队列发送消息函数

任务可以通过调用函数 OSQPost 或 OSQPostFront 两个函数来向消息队列发送消息。

函数 OSQPost 以 FIFO(先进先出)的方式组织消息队列,函数 OSQPostFront 以 LIFO(后

进先出)的方式组织消息队列。这两个函数的原型分别为:

INT8U OSQPost(OS_EVENT*pevent void *msg); INT8U OSQPostFront (OS_EVENT*pevent void*msg);

其中,pevent 为消息队列的指针,msg 为待发消息的指针。

消息队列还有其他一些函数,这里我们就不介绍了,感兴趣的朋友可以参考《嵌入式实时

操作系统 UCOSII 原理及应用》第五章,关于队列更详细的介绍,也请参考该书。

信号量集

在实际应用中,任务常常需要与多个事件同步,即要根据多个信号量组合作用的结果来决

定任务的运行方式。UCOSII 为了实现多个信号量组合的功能定义了一种特殊的数据结构——

信号量集。

信号量集所能管理的信号量都是一些二值信号,所有信号量集实质上是一种可以对多个输

入的逻辑信号进行基本逻辑运算的组合逻辑,其示意图如图 63.1.3 所示

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(5)

图 63.1.3 信号量集示意图

不同于信号量、消息邮箱、消息队列等事件,UCOSII 不使用事件控制块来描述信号量集,

而使用了一个叫做标志组的结构 OS_FLAG_GRP 来描述。OS_FLAG_GRP 结构如下:

typedef struct { INT8U OSFlagType; //识别是否为信号量集的标志 void *OSFlagWaitList; //指向等待任务链表的指针 OS_flags OSFlagFlags; //所有信号列表 }OS_FLAG_GRP;

成员 OSFlagWaitList 是一个指针,当一个信号量集被创建后,这个指针指向了这个信号量

集的等待任务链表。

与其他前面介绍过的事件不同,信号量集用一个双向链表来组织等待任务,每一个等待任

务都是该链表中的一个节点(Node)。标志组 OS_FLAG_GRP 的成员 OSFlagWaitList 就指向了

信号量集的这个等待任务链表。等待任务链表节点 OS_FLAG_NODE 的结构如下:

typedef struct { void *OSFlagNodeNext; //指向下一个节点的指针 void *OSFlagNodePrev; //指向前一个节点的指针 void *OSFlagNodeTCB; //指向对应任务控制块的指针 void *OSFlagNodeFlagGrp; //反向指向信号量集的指针 OS_FLAGS OSFlagNodeFlags; //信号过滤器 INT8U OSFlagNodeWaitType; //定义逻辑运算关系的数据 } OS_FLAG_NODE;

其中 OSFlagNodeWaitType 是定义逻辑运算关系的一个常数(根据需要设置),其可选值和

对应的逻辑关系如表 63.1.2 所示:

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(6)

表 63.1.2 OSFlagNodeWaitType 可选值及其意义

OSFlagFlags、OSFlagNodeFlags、OSFlagNodeWaitType 三者的关系如图 63.1.4 所示:

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(7)

图 63.1.4 标志组与等待任务共同完成信号量集的逻辑运算及控制

图中为了方便说明,我们将 OSFlagFlags 定义为 8 位,但是 UCOSII 支持 8 位/16 位/32 位

定义,这个通过修改 OS_FLAGS 的类型来确定(UCOSII 默认设置 OS_FLAGS 为 16 位)。

上图清楚的表达了信号量集各成员的关系:OSFlagFlags 为信号量表,通过发送信号量集的

任务设置;OSFlagNodeFlags 为信号滤波器,由请求信号量集的任务设置,用于选择性的挑选

OSFlagFlags 中的部分(或全部)位作为有效信号;OSFlagNodeWaitType 定义有效信号的逻辑

运算关系,也是由请求信号量集的任务设置,用于选择有效信号的组合方式(0/1? 与/或?)。

举个简单的例子,假设请求信号量集的任务设置 OSFlagNodeFlags 的值为 0X0F,设置

OSFlagNodeWaitType 的值为 WAIT_SET_ANY,那么只要 OSFlagFlags 的低四位的任何一位为

1,请求信号量集的任务将得到有效的请求,从而执行相关操作,如果低四位都为 0,那么请求

信号量集的任务将得到无效的请求。

接下来我们看看在 UCOSII 中,与信号量集相关的几个函数。

1) 创建信号量集函数

任务可以通过调用函数 OSFlagCreate 来创建一个信号量集。函数 OSFlagCreate 的原

型为:

OS_FLAG_GRP *OSFlagCreate (OS_FLAGS flags INT8U *err );

其中,flags 为信号量的初始值(即 OSFlagFlags 的值),err 为错误信息,返回值为该

信号量集的标志组的指针,应用程序根据这个指针对信号量集进行相应的操作。

2) 请求信号量集函数

任务可以通过调用函数 OSFlagPend 请求一个信号量集,函数 OSFlagPend 的原型为:

OS_FLAGS OSFlagPend(OS_FLAG_GRP*pgrp OS_FLAGS flags INT8U wait_type INT16U timeout INT8U *err);

其中,pgrp 为所请求的信号量集指针,flags 为滤波器(即 OSFlagNodeFlags 的值),

wait_type 为逻辑运算类型(即 OSFlagNodeWaitType 的值),timeout 为等待时限,err 为错

误信息。

3) 向信号量集发送信号函数

任务可以通过调用函数 OSFlagPost 向信号量集发信号,函数 OSFlagPost 的原型为:

OS_FLAGS OSFlagPost (OS_FLAG_GRP *pgrp OS_FLAGS flags INT8U opt INT8U *err);

其中,pgrp 为所请求的信号量集指针,flags 为选择所要发送的信号,opt 为信号有效选项,err 为错误信息。

所谓任务向信号量集发信号,就是对信号量集标志组中的信号进行置“1”(置位)或

置“0”(复位)的操作。至于对信号量集中的哪些信号进行操作,用函数中的参数 flags

来指定;对指定的信号是置“1”还是置“0”,用函数中的参数 opt 来指定(opt =

OS_FLAG_SET 为置“1”操作;opt = OS_FLAG_CLR 为置“0”操作)。

信号量集就介绍到这,更详细的介绍,请参考《嵌入式实时操作系统 UCOSII 原理及应用》

第六章。

软件定时器

UCOSII 从 V2.83 版本以后,加入了软件定时器,这使得 UCOSII 的功能更加完善,在其上

的应用程序开发与移植也更加方便。在实时操作系统中一个好的软件定时器实现要求有较高的

精度、较小的处理器开销,且占用较少的存储器资源。

通过前面的学习,我们知道 UCOSII 通过 OSTimTick 函数对时钟节拍进行加 1 操作,同时

遍历任务控制块,以判断任务延时是否到时。软件定时器同样由 OSTimTick 提供时钟,但是软

件定时器的时钟还受 OS_TMR_CFG_TICKS_PER_SEC 设置的控制,也就是在 UCOSII 的时钟

节拍上面再做了一次“分频”,软件定时器的最快时钟节拍就等于 UCOSII 的系统时钟节拍。这

也决定了软件定时器的精度。

软件定时器定义了一个单独的计数器 OSTmrTime,用于软件定时器的计时,UCOSII 并不

在 OSTimTick 中进行软件定时器的到时判断与处理,而是创建了一个高于应用程序中所有其他

任务优先级的定时器管理任务 OSTmr_Task,在这个任务中进行定时器的到时判断和处理。时

钟节拍函数通过信号量给这个高优先级任务发信号。这种方法缩短了中断服务程序的执行时间,

但也使得定时器到时处理函数的响应受到中断退出时恢复现场和任务切换的影响。软件定时器

功能实现代码存放在 tmr.c 文件中,移植时只需在 os_cfg.h 文件中使能定时器和设定定时器

的相关参数。

UCOSII 中软件定时器的实现方法是,将定时器按定时时间分组,使得每次时钟节拍到来

时只对部分定时器进行比较操作,缩短了每次处理的时间。但这就需要动态地维护一个定时器

组。定时器组的维护只是在每次定时器到时时才发生,而且定时器从组中移除和再插入操作不

需要排序。这是一种比较高效的算法,减少了维护所需的操作时间。

UCOSII 软件定时器实现了 3 类链表的维护:

OS_EXT OS_TMR OSTmrTbl[OS_TMR_CFG_MAX]; //定时器控制块数组 OS_EXT OS_TMR *OSTmrFreeList;

//空闲定时器控制块链表指针

OS_EXT OS_TMR_WHEEL OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE];//定时器轮

其中 OS_TMR 为定时器控制块,定时器控制块是软件定时器管理的基本单元,包含软件定

时器的名称、定时时间、在链表中的位置、使用状态、使用方式,以及到时回调函数及其参数

等基本信息。

OSTmrTbl[OS_TMR_CFG_MAX];:以数组的形式静态分配定时器控制块所需的 RAM 空间,

并存储所有已建立的定时器控制块,OS_TMR_CFG_MAX 为最大软件定时器的个数。

OSTmrFreeLiSt:为空闲定时器控制块链表头指针。空闲态的定时器控制块(OS_TMR)中,

OSTmrnext 和 OSTmrPrev 两个指针分别指向空闲控制块的前一个和后一个,组织了空闲控制块

双向链表。建立定时器时,从这个链表中搜索空闲定时器控制块。

OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]:该数组的每个元素都是已开启定时器的

一个分组,元素中记录了指向该分组中第一个定时器控制块的指针,以及定时器控制块的个数。

运行态的定时器控制块(OS_TMR)中,OSTmrnext 和 OSTmrPrev 两个指针同样也组织了所在分

组中定时器控制块的双向链表。软件定时器管理所需的数据结构示意图如图 63.1.5 所示:

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(8)

图 63.1.5 软件定时器管理所需的数据结构示意图

OS_TMR_CFG_WHEEL_SIZE 定义了 OSTmrWheelTbl 的大小,同时这个值也是定时器分

组的依据。按照定时器到时值与 OS_TMR_CFG_WHEEL_SIZE 相除的余数进行分组:不同余数

的定时器放在不同分组中;相同余数的定时器处在同一组中,由双向链表连接。这样,余数值

为 0~OS_TMR_CFG_WHEEL_SIZE-1 的不同定时器控制块,正好分别对应了数组元素

OSTmr-WheelTbl[0]~OSTmrWheelTbl[OS_TMR_CFGWHEEL_SIZE-1]的不同分组。每次时钟节

拍到来时,时钟数 OSTmrTime 值加 1,然后也进行求余操作,只有余数相同的那组定时器才有

可能到时,所以只对该组定时器进行判断。这种方法比循环判断所有定时器更高效。随着时钟

数的累加,处理的分组也由 0~OS_TMR_CFG_WHE EL_SIZE-1 循环。这里,我们推荐

OS_TMR_CFG_WHEEL_SIZE 的取值为 2 的 N 次方,以便采用移位操作计算余数,缩短处理时

间。

信号量唤醒定时器管理任务,计算出当前所要处理的分组后,程序遍历该分组中的所有控

制块,将当前 OSTmrTime 值与定时器控制块中的到时值(OSTmrMatch)相比较。若相等(即到

时),则调用该定时器到时回调函数;若不相等,则判断该组中下一个定时器控制块。如此操作,

直到该分组链表的结尾。软件定时器管理任务的流程如图 63.1.6 所示。

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(9)

图 63.1.6 软件定时器管理任务流程

当运行完软件定时器的到时处理函数之后,需要进行该定时器控制块在链表中的移除和再

插入操作。插入前需要重新计算定时器下次到时时所处的分组。计算公式如下:

定时器下次到时的 OSTmrTime 值(OSTmrMatch)=定时器定时值 当前 OSTmrTime 值

新分组=定时器下次到时的 OSTmrTime 值(OSTmrMatch)%OS_TMR_CFG_WHEEL_SIZE

接下来我们看看在 UCOSII 中,与软件定时器相关的几个函数。

1) 创建软件定时器函数

创建软件定时器通过函数 OSTmrCreate 实现,该函数原型为:

OS_TMR *OSTmrCreate (INT32U dly INT32U period INT8U opt OS_TMR_CALLBACK callback void *callback_arg INT8U *pname INT8U *perr);

dly,用于初始化定时时间,对单次定时(ONE-SHOT 模式)的软件定时器来说,这

就是该定时器的定时时间,而对于周期定时(PERIODIC 模式)的软件定时器来说,这是

该定时器第一次定时的时间,从第二次开始定时时间变为 period。

period,在周期定时(PERIODIC 模式),该值为软件定时器的周期溢出时间。

opt,用于设置软件定时器工作模式。可以设置的值为:OS_TMR_OPT_ONE_SHOT

或 OS_TMR_OPT_PERIODIC,如果设置为前者,说明是一个单次定时器;设置为后者则

表示是周期定时器。

callback,为软件定时器的回调函数,当软件定时器的定时时间到达时,会调用该函数。

callback_arg,回调函数的参数。

pname,为软件定时器的名字。

perr,为错误信息。

软件定时器的回调函数有固定的格式,我们必须按照这个格式编写,软件定时器的回

调函数格式为:void (*OS_TMR_CALLBACK)(void *ptmr void *parg)。其中,函数名我们

可以自己随意设置,而 ptmr 这个参数,软件定时器用来传递当前定时器的控制块指针,所

以我们一般设置其类型为 OS_TMR*类型,第二个参数(parg)为回调函数的参数,这个就

可以根据自己需要设置了,你也可以不用,但是必须有这个参数。

2) 开启软件定时器函数

任务可以通过调用函数 OSTmrStart 开启某个软件定时器,该函数的原型为:BOOLEAN OSTmrStart (OS_TMR *ptmr INT8U *perr);

其中 ptmr 为要开启的软件定时器指针,perr 为错误信息。

3) 停止软件定时器函数

任务可以通过调用函数 OSTmrStop 停止某个软件定时器,该函数的原型为:

OSTmrStop (OS_TMR *ptmr INT8U opt void *callback_arg INT8U *perr);

其中 ptmr 为要停止的软件定时器指针。

opt 为停止选项,可以设置的值及其对应的意义为:

OS_TMR_OPT_NONE,直接停止,不做任何其他处理

OS_TMR_OPT_CALLBACK,停止,用初始化的参数执行一次回调函数

OS_TMR_OPT_CALLBACK_ARG,停止,用新的参数执行一次回调函数

callback_arg,新的回调函数参数。

perr,错误信息。

软件定时器我们就介绍到这。

63.2 硬件设计

本节实验功能简介:本章我们在 UCOSII 里面创建 7 个任务:开始任务、LED 任务、触摸

屏任务、队列消息显示任务、信号量集任务、按键扫描任务和主任务,开始任务用于创建邮箱、

消息队列、信号量集以及其他任务,之后挂起;触摸屏任务用于在屏幕上画图,测试 CPU 使

用率;队列消息显示任务请求消息队列,在得到消息后显示收到的消息数据;信号量集任务用

于测试信号量集,采用 OS_FLAG_WAIT_SET_ANY 的方法,任何按键按下(包括 TPAD),

该任务都会控制蜂鸣器发出“滴”的一声;按键扫描任务用于按键扫描,优先级最高,将得到

的键值通过消息邮箱发送出去;主任务创建 3 个软件定时器(定时器 1,100ms 溢出一次,显

示 CPU 和内存使用率;定时 2,200ms 溢出一次,在固定区域不停的显示不同颜色;定时

3, 100ms 溢出一次,用于自动发送消息到消息队列),并通过查询消息邮箱获得键值,根据

键值执行 DS1 控制、控制软件定时器 3 的开关、触摸区域清屏、触摸屏校和软件定时器 2 的

开关控制等。

所要用到的硬件资源如下:

1) 指示灯 DS0 、DS1

2) 4 个机械按键(KEY0/KEY1/KEY2/KEY_UP)

3) TPAD 触摸按键

4) 蜂鸣器

5) TFTLCD 模块

这些,我们在前面的学习中都已经介绍过了。

63.3 软件设计

本章,我们在第四十二章实验 (实验 37 )的基础上修改,首先,是 UCOSII 代码的添加,

具体方法同第 61 章一模一样,本章就不再详细介绍了。另外由于我们创建了 7 个任务,加上统

计任务、空闲任务和软件定时器任务,总共 10 个任务,如果你还想添加其他任务,请把

OS_MAX_TASKS 的值适当改大。

另外,我们还需要在 os_cfg.h 里面修改软件定时器管理部分的宏定义,修改如下:

#define OS_TMR_EN 1u //使能软件定时器功能 #define OS_TMR_CFG_MAX 16u //最大软件定时器个数 #define OS_TMR_CFG_NAME_EN 1u //使能软件定时器命名 #define OS_TMR_CFG_WHEEL_SIZE 8u //软件定时器轮大小 #define OS_TMR_CFG_TICKS_PER_SEC 100u //软件定时器的时钟节拍(10ms) #define OS_TASK_TMR_PRIO 0u //软件定时器的优先级 设置为最高 这样我们就使能 UCOSII 的软件定时器功能了,并且设置最大软件定时器个数为 16,定时 器轮大小为 8,软件定时器时钟节拍为 10ms(即定时器的最少溢出时间为 10ms)。 最后,我们只需要修改 main.c 文件了,打开 main.c,输入如下代码: //START 任务 //设置任务优先级 #define START_TASK_PRIO 10 //开始任务的优先级设置为最低 //设置任务堆栈大小 #define START_STK_SIZE 64 //任务堆栈 OS_STK START_TASK_STK[START_STK_SIZE]; //任务函数 void start_task(void *pdata); //LED 任务 //设置任务优先级 #define LED_TASK_PRIO 7 //设置任务堆栈大小 #define LED_STK_SIZE 64 //任务堆栈 OS_STK LED_TASK_STK[LED_STK_SIZE]; //任务函数 void led_task(void *pdata); //触摸屏任务 //设置任务优先级 #define TOUCH_TASK_PRIO 6 //设置任务堆栈大小 #define TOUCH_STK_SIZE 128 //任务堆栈 OS_STK TOUCH_TASK_STK[TOUCH_STK_SIZE]; //任务函数 void touch_task(void *pdata); //队列消息显示任务 //设置任务优先级 #define QMSGSHOW_TASK_PRIO 5 //设置任务堆栈大小 #define QMSGSHOW_STK_SIZE 128 //任务堆栈 OS_STK QMSGSHOW_TASK_STK[QMSGSHOW_STK_SIZE]; //任务函数 void qmsgshow_task(void *pdata); //主任务 //设置任务优先级 #define MAIN_TASK_PRIO 4 //设置任务堆栈大小 #define MAIN_STK_SIZE 128 //任务堆栈 OS_STK MAIN_TASK_STK[MAIN_STK_SIZE]; //任务函数 void main_task(void *pdata); //信号量集任务 //设置任务优先级 #define FLAGS_TASK_PRIO 3 //设置任务堆栈大小 #define FLAGS_STK_SIZE 128 //任务堆栈 OS_STK FLAGS_TASK_STK[FLAGS_STK_SIZE]; //任务函数 void flags_task(void *pdata); //按键扫描任务 //设置任务优先级 #define KEY_TASK_PRIO 2 //设置任务堆栈大小 #define KEY_STK_SIZE 128 //任务堆栈 OS_STK KEY_TASK_STK[KEY_STK_SIZE]; //任务函数 void key_task(void *pdata); ////////////////////////////////////////////////////////////////////////////// OS_EVENT * msg_key; //按键邮箱事件块 OS_EVENT * q_msg; //消息队列 OS_TMR * tmr1; //软件定时器 1 OS_TMR * tmr2; //软件定时器 2 OS_TMR * tmr3; //软件定时器 3 OS_FLAG_GRP * flags_key;//按键信号量集 void * MsgGrp[256]; //消息队列存储地址 最大支持 256 个消息 //软件定时器 1 的回调函数 //每 100ms 执行一次 用于显示 CPU 使用率和内存使用率 void tmr1_callback(OS_TMR *ptmr void *p_arg) { static u16 cpuusage=0; static u8 tcnt=0; POINT_COLOR=BLUE; if(tcnt==5) { LCD_ShowxNum(202 10 cpuusage/5 3 16 0); //显示 CPU 使用率 cpuusage=0; tcnt=0; } cpuusage =OSCPUUsage; tcnt ; LCD_ShowxNum(202 30 my_mem_perused(SRAMIN) 3 16 0);//显示内存使用率 LCD_ShowxNum(202 50 ((OS_Q*)(q_msg->OSEventPtr))->OSQEntries 3 16 0X80); //显示队列当前的大小 } //软件定时器 2 的回调函数 void tmr2_callback(OS_TMR *ptmr void *p_arg) { static u8 sta=0; switch(sta) { case 0: LCD_Fill(131 221 lcddev.width-1 lcddev.height-1 RED); break; case 1: LCD_Fill(131 221 lcddev.width-1 lcddev.height-1 GREEN); break; case 2: LCD_Fill(131 221 lcddev.width-1 lcddev.height-1 BLUE); break; case 3: LCD_Fill(131 221 lcddev.width-1 lcddev.height-1 MAGENTA); break; case 4: LCD_Fill(131 221 lcddev.width-1 lcddev.height-1 GBLUE); break; case 5: LCD_Fill(131 221 lcddev.width-1 lcddev.height-1 YELLOW); break; case 6: LCD_Fill(131 221 lcddev.width-1 lcddev.height-1 BRRED); break; } sta ; if(sta>6)sta=0; } //软件定时器 3 的回调函数 void tmr3_callback(OS_TMR *ptmr void *p_arg) { u8* p; u8 err; static u8 msg_cnt=0; //msg 编号 p=mymalloc(SRAMIN 13); //申请 13 个字节的内存 if(p) { sprintf((char*)p "ALIENTEK d" msg_cnt); msg_cnt ; err=OSQPost(q_msg p); //发送队列 if(err!=OS_ERR_NONE) //发送失败 { myfree(SRAMIN p); //释放内存 OSTmrStop(tmr3 OS_TMR_OPT_NONE 0 &err); //关闭软件定时器 3 } } } //加载主界面 void ucos_load_main_ui(void) { LCD_Clear(WHITE); //清屏 POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(10 10 200 16 16 "Explorer STM32"); LCD_ShowString(10 30 200 16 16 "UCOSII TEST3"); LCD_ShowString(10 50 200 16 16 "ATOM@ALIENTEK"); LCD_ShowString(10 75 240 16 16 "TPAD:TMR2 SW KEY_UP:ADJUST"); LCD_ShowString(10 95 240 16 16 "KEY0:DS0 KEY1:Q SW KEY2:CLR"); LCD_DrawLine(0 70 lcddev.width-1 70); LCD_DrawLine(150 0 150 70); LCD_DrawLine(0 120 lcddev.width-1 120); LCD_DrawLine(0 220 lcddev.width-1 220); LCD_DrawLine(130 120 130 lcddev.height-1); LCD_ShowString(5 125 240 16 16 "QUEUE MSG");//队列消息 LCD_ShowString(5 150 240 16 16 "Message:"); LCD_ShowString(5 130 125 240 16 16 "FLAGS");//信号量集 LCD_ShowString(5 225 240 16 16 "TOUCH"); //触摸屏 LCD_ShowString(5 130 225 240 16 16 "TMR2"); //队列消息 POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(170 10 200 16 16 "CPU: %"); LCD_ShowString(170 30 200 16 16 "MEM: %"); LCD_ShowString(170 50 200 16 16 " Q :000"); delay_ms(300); } int main(void) { HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(336 8 2 7); //设置时钟 168Mhz delay_init(168); //初始化延时函数 uart_init(115200); //初始化 USART LED_Init(); //初始化 LED KEY_Init(); //初始化按键 BEEP_Init(); //初始化蜂鸣器 LCD_Init(); //初始化 LCD TPAD_Init(8); //初始化触摸按键 tp_dev.init(); //初始化触摸屏 my_mem_init(SRAMIN); //初始化内部内存池 ucos_load_main_ui(); //加载主界面 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(); //开始任务 } /////////////////////////////////////////////////////////////////////////////////////////////////// //画水平线 //x0 y0:坐标 //len:线长度 //color:颜色 void gui_draw_hline(u16 x0 u16 y0 u16 len u16 color) { if(len==0)return; LCD_Fill(x0 y0 x0 len-1 y0 color); } //画实心圆 //x0 y0:坐标 //r:半径 //color:颜色 void gui_fill_circle(u16 x0 u16 y0 u16 r u16 color) { u32 i; u32 imax = ((u32)r*707)/1000 1; u32 sqmax = (u32)r*(u32)r (u32)r/2; u32 x=r; gui_draw_hline(x0-r y0 2*r color); for (i=1;i<=imax;i ) { if ((i*i x*x)>sqmax)// draw lines from outside { if (x>imax) { gui_draw_hline (x0-i 1 y0 x 2*(i-1) color); gui_draw_hline (x0-i 1 y0-x 2*(i-1) color); } x--; } // draw lines from inside (center) gui_draw_hline(x0-x y0 i 2*x color); gui_draw_hline(x0-x y0-i 2*x color); } } //两个数之差的绝对值 //x1 x2:需取差值的两个数 //返回值:|x1-x2| u16 my_abs(u16 x1 u16 x2) { if(x1>x2)return x1-x2; else return x2-x1; } //画一条粗线 //(x1 y1) (x2 y2):线条的起始坐标 //size:线条的粗细程度 //color:线条的颜色 void lcd_draw_bline(u16 x1 u16 y1 u16 x2 u16 y2 u8 size u16 color) { u16 t; int xerr=0 yerr=0 delta_x delta_y distance; int incx incy uRow uCol; if(x1<size|| x2<size||y1<size|| y2<size)return; delta_x=x2-x1; //计算坐标增量 delta_y=y2-y1; uRow=x1; uCol=y1; if(delta_x>0)incx=1; //设置单步方向 else if(delta_x==0)incx=0;//垂直线 else {incx=-1;delta_x=-delta_x;} if(delta_y>0)incy=1; else if(delta_y==0)incy=0;//水平线 else{incy=-1;delta_y=-delta_y;} if( delta_x>delta_y)distance=delta_x; //选取基本增量坐标轴 else distance=delta_y; for(t=0;t<=distance 1;t )//画线输出 { gui_fill_circle(uRow uCol size color);//画点 xerr =delta_x ; yerr =delta_y ; if(xerr>distance) { xerr-=distance; uRow =incx; } if(yerr>distance) { yerr-=distance; uCol =incy; } } } /////////////////////////////////////////////////////////////////////////////////////////////////// //开始任务 void start_task(void *pdata) { OS_CPU_SR cpu_sr=0; u8 err; pdata=pdata; msg_key=OSMboxCreate((void*)0); //创建消息邮箱 q_msg=OSQCreate(&MsgGrp[0] 256); //创建消息队列 flags_key=OSFlagCreate(0 &err); //创建信号量集 OSStatInit(); //开启统计任务 OS_ENTER_CRITICAL(); //进入临界区(关闭中断) //LED 任务 OSTaskCreateExt((void(*)(void*) )led_task (void* )0 (OS_STK* )&LED_TASK_STK[LED_STK_SIZE-1] (INT8U )LED_TASK_PRIO (INT16U )LED_TASK_PRIO (OS_STK* )&LED_TASK_STK[0] (INT32U )LED_STK_SIZE (void* )0 (INT16U )OS_TASK_OPT_STK_CHK| OS_TASK_OPT_STK_CLR|OS_TASK_OPT_SAVE_FP); //触摸任务 OSTaskCreateExt((void(*)(void*) )touch_task (void* )0 (OS_STK* )&TOUCH_TASK_STK[TOUCH_STK_SIZE-1] (INT8U )TOUCH_TASK_PRIO (INT16U )TOUCH_TASK_PRIO (OS_STK* )&TOUCH_TASK_STK[0] (INT32U )TOUCH_STK_SIZE (void* )0 (INT16U )OS_TASK_OPT_STK_CHK| OS_TASK_OPT_STK_CLR|OS_TASK_OPT_SAVE_FP); //消息队列显示任务 OSTaskCreateExt((void(*)(void*) )qmsgshow_task (void* )0 (OS_STK* )&QMSGSHOW_TASK_STK[QMSGSHOW_STK_SIZE-1] (INT8U )QMSGSHOW_TASK_PRIO (INT16U )QMSGSHOW_TASK_PRIO (OS_STK* )&QMSGSHOW_TASK_STK[0] (INT32U )QMSGSHOW_STK_SIZE (void* )0 (INT16U )OS_TASK_OPT_STK_CHK| OS_TASK_OPT_STK_CLR|OS_TASK_OPT_SAVE_FP); //主任务 OSTaskCreateExt((void(*)(void*) )main_task (void* )0 (OS_STK* )&MAIN_TASK_STK[MAIN_STK_SIZE-1] (INT8U )MAIN_TASK_PRIO (INT16U )MAIN_TASK_PRIO (OS_STK* )&MAIN_TASK_STK[0] (INT32U )MAIN_STK_SIZE (void* )0 (INT16U )OS_TASK_OPT_STK_CHK| OS_TASK_OPT_STK_CLR|OS_TASK_OPT_SAVE_FP); //信号量集任务 OSTaskCreateExt((void(*)(void*) )flags_task (void* )0 (OS_STK* )&FLAGS_TASK_STK[FLAGS_STK_SIZE-1] (INT8U )FLAGS_TASK_PRIO (INT16U )FLAGS_TASK_PRIO (OS_STK* )&FLAGS_TASK_STK[0] (INT32U )FLAGS_STK_SIZE (void* )0 (INT16U )OS_TASK_OPT_STK_CHK| OS_TASK_OPT_STK_CLR|OS_TASK_OPT_SAVE_FP); //按键任务 OSTaskCreateExt((void(*)(void*) )key_task (void* )0 (OS_STK* )&KEY_TASK_STK[KEY_STK_SIZE-1] (INT8U )KEY_TASK_PRIO (INT16U )KEY_TASK_PRIO (OS_STK* )&KEY_TASK_STK[0] (INT32U )KEY_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); //挂起开始任务 } //LED 任务 void led_task(void *pdata) { u8 t; while(1) { t ; delay_ms(10); if(t==8)LED0=1; //LED0 灭 if(t==100) //LED0 亮 { t=0; LED0=0; } } } //触摸屏任务 void touch_task(void *pdata) { u32 cpu_sr; u16 lastpos[2]; //最后一次的数据 while(1) { tp_dev.scan(0); if(tp_dev.sta&TP_PRES_DOWN) //触摸屏被按下 { if(tp_dev.x[0]<(130-1)&&tp_dev.y[0]<lcddev.height&&tp_dev.y[0]>(220 1)) { if(lastpos[0]==0XFFFF) { lastpos[0]=tp_dev.x[0]; lastpos[1]=tp_dev.y[0]; } OS_ENTER_CRITICAL(); //进入临界段 防止其他任务 打断 LCD 操作 导致液晶乱序. lcd_draw_bline(lastpos[0] lastpos[1] tp_dev.x[0] tp_dev.y[0] 2 RED);//画线 OS_EXIT_CRITICAL(); lastpos[0]=tp_dev.x[0]; lastpos[1]=tp_dev.y[0]; } }else { lastpos[0]=0XFFFF; delay_ms(10); //没有按键按下的时候 } } } //队列消息显示任务 void qmsgshow_task(void *pdata) { u8 *p; u8 err; while(1) { p=OSQPend(q_msg 0 &err);//请求消息队列 LCD_ShowString(5 170 240 16 16 p);//显示消息 myfree(SRAMIN p); delay_ms(500); } } //主任务 void main_task(void *pdata) { u32 key=0; u8 err; u8 tmr2sta=1; //软件定时器 2 开关状态 u8 tmr3sta=0; //软件定时器 3 开关状态 u8 flagsclrt=0;//信号量集显示清零倒计时 tmr1=OSTmrCreate(10 10 OS_TMR_OPT_PERIODIC (OS_TMR_CALLBACK) tmr1_callback 0 "tmr1" &err); //100ms 执行一次 tmr2=OSTmrCreate(10 20 OS_TMR_OPT_PERIODIC (OS_TMR_CALLBACK) tmr2_callback 0 "tmr2" &err); //200ms 执行一次 tmr3=OSTmrCreate(10 10 OS_TMR_OPT_PERIODIC (OS_TMR_CALLBACK) tmr3_callback 0 "tmr3" &err); //100ms 执行一次 OSTmrStart(tmr1 &err);//启动软件定时器 1 OSTmrStart(tmr2 &err);//启动软件定时器 2 while(1) { key=(u32)OSMboxPend(msg_key 10 &err); if(key) { flagsclrt=51;//500ms 后清除 OSFlagPost(flags_key 1<<(key-1) OS_FLAG_SET &err); //设置对应的信号量为 1 } if(flagsclrt)//倒计时 { flagsclrt--; if(flagsclrt==1)LCD_Fill(140 162 239 162 16 WHITE);//清除显示 } switch(key) { case 1://控制 DS1 LED1=!LED1; break; case 2://控制软件定时器 3 tmr3sta=!tmr3sta; if(tmr3sta)OSTmrStart(tmr3 &err); else OSTmrStop(tmr3 OS_TMR_OPT_NONE 0 &err);//关闭软件定时器 3 break; case 3://清除 LCD_Fill(0 221 129 lcddev.height-1 WHITE); break; case 4://校准 OSTaskSuspend(TOUCH_TASK_PRIO); //挂起触摸屏任务 OSTaskSuspend(QMSGSHOW_TASK_PRIO);//挂起队列信息显示任务 OSTmrStop(tmr1 OS_TMR_OPT_NONE 0 &err);//关闭软件定时器 1 if(tmr2sta)OSTmrStop(tmr2 OS_TMR_OPT_NONE 0 &err); //关闭软件定时器 2 if((tp_dev.touchtype&0X80)==0)TP_Adjust(); OSTmrStart(tmr1 &err); //重新开启软件定时器 1 if(tmr2sta)OSTmrStart(tmr2 &err); //重新开启软件定时器 2 OSTaskResume(TOUCH_TASK_PRIO); //解挂 OSTaskResume(QMSGSHOW_TASK_PRIO); //解挂 ucos_load_main_ui(); //重新加载主界面 break; case 5://软件定时器 2 开关 tmr2sta=!tmr2sta; if(tmr2sta)OSTmrStart(tmr2 &err); //开启软件定时器 2 else { OSTmrStop(tmr2 OS_TMR_OPT_NONE 0 &err);//关闭软件定时器 2 LCD_ShowString(148 262 240 16 16 "TMR2 STOP"); //提示定时器 2 关闭了 } break; } delay_ms(10); } } //信号量集处理任务 void flags_task(void *pdata) { u16 flags; u8 err; while(1) { flags=OSFlagPend(flags_key 0X001F OS_FLAG_WAIT_SET_ANY 0 &err); //等待信号量 if(flags&0X0001)LCD_ShowString(140 162 240 16 16 "KEY0 DOWN "); if(flags&0X0002)LCD_ShowString(140 162 240 16 16 "KEY1 DOWN "); if(flags&0X0004)LCD_ShowString(140 162 240 16 16 "KEY2 DOWN "); if(flags&0X0008)LCD_ShowString(140 162 240 16 16 "KEY_UP DOWN"); if(flags&0X0010)LCD_ShowString(140 162 240 16 16 "TPAD DOWN "); BEEP=1; delay_ms(50); BEEP=0; OSFlagPost(flags_key 0X001F OS_FLAG_CLR &err);//全部信号量清零 } } //按键扫描任务 void key_task(void *pdata) { u8 key; while(1) { key=KEY_Scan(0); if(key==0) { if(TPAD_Scan(0))key=5; } if(key)OSMboxPost(msg_key (void*)key);//发送消息 delay_ms(10); } }

本章 main.c 的代码有点多,因为我们创建了 7 个任务,3 个软件定时器及其回调函数,所

以,整个代码有点多,我们创建的 7 个任务为:start_task、led_task、touch_task、qmsgshow_task 、

flags_task 、main_task 和 key_task,优先级分别是 10 和 7~2,堆栈大小除了 start_task 和 led_task

是 64,其他都是 128。

我们还创建了 3 个软件定时器 tmr1、tmr2 和 tmr3,tmr1 用于显示 CPU 使用率和内存使用

率,每 100ms 执行一次;tmr2 用于在 LCD 的右下角区域不停的显示各种颜色,每 200ms 执行

一次;tmr3 用于定时向队列发送消息,每 100ms 发送一次。

本章,我们依旧使用消息邮箱 msg_key 在按键任务和主任务之间传递键值数据,我们创建

信号量集 flags_key,在主任务里面将按键键值通过信号量集传递给信号量集处理任务 flags_task,

实现按键信息的显示以及发出按键提示音。

本章,我们还创建了一个大小为 256 的消息队列 q_msg,通过软件定时器 tmr3 的回调函数

向消息队列发送消息,然后在消息队列显示任务 qmsgshow_task 里面请求消息队列,并在 LCD

上面显示得到的消息。消息队列还用到了动态内存管理。

在主任务 main_task 里面,我们实现了 63.2 节介绍的功能:KEY0 控制 LED1 亮灭;KEY1

控制软件定时器 tmr3 的开关,间接控制队列信息的发送;KEY2 清除触摸屏输入;KEY_UP 用

于触摸屏校准,在校准的时候,要先挂起触摸屏任务、队列消息显示任务,并停止软件定时器

tmr1 和 tmr2,否则可能对校准时的 LCD 显示造成干扰;TPAD 按键用于控制软件定时器 tmr2

的开关,间接控制屏幕显示。

软件设计部分就为大家介绍到这里。

63.4 下载验证

在代码编译成功之后,我们通过下载代码到探索者 STM32F4 开发板上,可以看到 LCD 显

示界面如图 63.4.1 所示:

正点原子stm32开发板购买建议(正点原子STM32F407探索者开发板资料连载第六十三章)(10)

图 63.4.1 初始界面

从图中可以看出,默认状态下,CPU 使用率为 10%左右。比上一章多出很多,这主要是

key_task 里面增加不停的刷屏(tmr2)操作导致的。

通过按 KEY0,可以控制 DS1 的亮灭;

通过按 KEY1 则可以启动 tmr3 控制消息队列发送,可以在 LCD 上面看到 Q 和 MEM 的值

慢慢变大(说明队列消息在增多,占用内存也随着消息增多而增大),在 QUEUE MSG 区,开

始显示队列消息,再按一次 KEY1 停止 tmr3,此时可以看到 Q 和 MEM 逐渐减小。当 Q 值变

为 0 的时候,QUEUE MSG 也停止显示(队列为空)。

通过 KEY2 按键,清除 TOUCH 区域的输入。

通过 KEY_UP 按键,可以进行触摸屏校准。

通过 TPAD 按键,可以启动/停止 tmr2,从而控制屏幕的刷新。

在 TOUCH 区域,可以输入手写内容。

任何按键按下,蜂鸣器都会发出“滴”的一声,提示按键被按下,同时在 FLAGS 区域显

示按键信息。

猜您喜欢: