第六十三章 UCOSII实验3-消息队列、信号量集和软件定时器
[mw_shl_code=c,true]1.硬件平台:正点原子探索者STM32F407开发板
2.软件平台:MDK5.1
3.固件库版本:V1.4.0[/mw_shl_code]
上一章,我们学习了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[],该数组中的元素都是一些指向消息的指针。
图63.1.1 消息队列的数据结构
队列控制块(OS_Q)的结构定义如下:
typedef struct os_q
{
struct
os_q *OSQPtr;
void
**OSQStart;
void
**OSQEnd;
void **OSQIn;
void
**OSQOut;
INT16U OSQSize;
INT16U OSQEntries;
} OS_Q;
该结构体中各参数的含义如表63.1.1所示:
参数
|
说明
|
OSQPtr
|
指向下一个空的队列控制块
|
OSQSize
|
数组的长度
|
OSQEntres
|
已存放消息指针的元素数目
|
OSQStart
|
指向消息指针数组的起始地址
|
OSQEnd
|
指向消息指针数组结束单元的下一个单元。它使得数组构
成了一个循环的缓冲区
|
OSQIn
|
指向插入一条消息的位置。当它移动到与OSQEnd相等时,
被调整到指向数组的起始单元
|
OSQOut
|
指向被取出消息的位置。当它移动到与OSQEnd相等时,被
调整到指向数组的起始单元
|
表63.1.1 队列控制块各参数含义
其中,可以移动的指针为OSQIn和OSQOut,而指针OSQStart和OSQEnd只是一个标志(常指针)。当可移动的指针OSQIn或OSQOut移动到数组末尾,也就是与OSQEnd相等时,可移动的指针将会被调整到数组的起始位置OSQStart。也就是说,从效果上来看,指针OSQEnd与OSQStart等值。于是,这个由消息指针构成的数组就头尾衔接起来形成了一个如图63.1.2所示的循环的队列。
图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所示
图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所示:
常数
|
信号有效状态
|
等待任务的就绪条件
|
WAIT_CLR_ALL或
WAIT_CLR_AND
|
0
|
信号全部有效(全0)
|
WAIT_CLR_ANY或
WAIT_CLR_OR
|
0
|
信号有一个或一个以上有效(有0)
|
WAIT_SET_ALL或
WAIT_SET_AND
|
1
|
信号全部有效(全1)
|
WAIT_SET_ANY或
WAIT_SET_OR
|
1
|
信号有一个或一个以上有效(有1)
|
表63.1.2 OSFlagNodeWaitType可选值及其意义
OSFlagFlags、OSFlagNodeFlags、OSFlagNodeWaitType三者的关系如图63.1.4所示:
图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所示:
图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所示。
图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,输入如下代码:
/////////////////////////UCOSII任务设置///////////////////////////////////
//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(182,10,cpuusage/5,3,16,0); //显示CPU使用率
cpuusage=0;
tcnt=0;
}
cpuusage+=OSCPUUsage;
tcnt++;
LCD_ShowxNum(182,30,my_mem_perused(SRAMIN),3,16,0); //显示内存使用率
LCD_ShowxNum(182,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,err;
static
u8 msg_cnt=0; //msg编号
p=mymalloc(SRAMIN,13); //申请13个字节的内存
if(p)
{
sprintf((char*)p,"ALIENTEK
%03d",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 S0
KEY1 SW KEY2:CLR");
LCD_DrawLine(0,70,lcddev.width,70);
LCD_DrawLine(130,0,130,70);
LCD_DrawLine(0,120,lcddev.width,120);
LCD_DrawLine(0,220,lcddev.width,220);
LCD_DrawLine(130,120,130,lcddev.height);
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(150,10,200,16,16,"CPU: %");
LCD_ShowString(150,30,200,16,16,"MEM: %");
LCD_ShowString(150,50,200,16,16," Q
:000");
delay_ms(300);
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口波特率为115200
LED_Init(); //初始化LED
LCD_Init(); //初始化LCD
BEEP_Init(); //蜂鸣器初始化
KEY_Init(); //按键初始化
TPAD_Init(8); //初始化TPAD
my_mem_init(SRAMIN); //初始化内部内存池
tp_dev.init(); //初始化触摸屏
ucos_load_main_ui();
//加载主界面
OSInit(); //初始化UCOSII
OSTaskCreate(start_task,(void *)0,(OS_STK
*)&START_TASK_STK[START_STK_SIZE
-1],START_TASK_PRIO );//创建起始任务
OSStart(); //启动UCOSII
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//画水平线
//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(); //初始化统计任务.这里会延时1秒钟左右
OS_ENTER_CRITICAL(); //进入临界区(无法被中断打断)
OSTaskCreate(led_task,(void *)0,(OS_STK*)&LED_TASK_STK[LED_STK_SIZE-1],
LED_TASK_PRIO);
OSTaskCreate(touch_task,(void
*)0,(OS_STK*)&TOUCH_TASK_STK[TOUCH_STK_
SIZE-1],TOUCH_TASK_PRIO);
OSTaskCreate(qmsgshow_task,(void
*)0,(OS_STK*)&QMSGSHOW_TASK_STK[
QMSGSHOW_STK_SIZE-1],QMSGSHOW_TASK_PRIO);
OSTaskCreate(main_task,(void
*)0,(OS_STK*)&MAIN_TASK_STK[MAIN_STK_SIZE
-1],MAIN_TASK_PRIO);
OSTaskCreate(flags_task,(void
*)0,(OS_STK*)&FLAGS_TASK_STK[FLAGS_STK_
SIZE-1],FLAGS_TASK_PRIO);
OSTaskCreate(key_task,(void
*)0,(OS_STK*)&KEY_TASK_STK[KEY_STK_SIZE-1],
KEY_TASK_PRIO);
OSTaskSuspend(START_TASK_PRIO); //挂起起始任务.
OS_EXIT_CRITICAL(); //退出临界区(可以被中断打断)
}
//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(5);
}
}
//队列消息显示任务
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,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");//提示关闭了
}
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所示:
图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区域显示按键信息。
正点原子探索者STM32F407开发板购买地址:http://item.taobao.com/item.htm?id=41855882779
|