队列(邮箱:先进先出的线性表)
定义
队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息(而这个消息可以是任意类型的数据),任务能够从队列里面读取消息,也能够向队列发送消息。
队列可以被多个任务读、也可以被多个任务写,但要防止同一时刻被多个任务访问(多进程/多线程(多任务)对共享变量的并发访问)
特点
• 一般情况下队列消息是先进先出方式排队,也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据。
• 队列传输数据时有两种方法:1、直接拷贝数据;2、拷贝数据的地址,然后根据地址读取数据。 第二种方法适合传输大数据比如一个大数组, 或者一个结构体变量。
• 队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。 因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。
• 读/写队列均支持阻塞机制
(以读队列为例:在任务从队列读取消息时,可以指定一个阻塞超时时间。如果队列不为空则会读取队列中第一个消息(通过拷贝的方式: memcpy函数),如果队列为空,则看我们自己设置阻塞时间(1.阻塞时间为0:即刻返回队列空错误。2.阻塞时间不为0:假设阻塞时间为20ms,刚开始队列为空,则任务进入阻塞,如果在20ms内有消息入队了即该任务会被唤醒然后读取消息,如果在20ms内还没有消息则任务就不会再等待直接从阻塞态中唤醒,返回队列空错误。3.阻塞时间为最大:任务死等进入阻塞态,直到完成读取队列的消息。 写队列过程基本一致,不过一个的队列空一个是队列满。)
•当在中断中读写队列时,如果队列空或满,不会进行阻塞,直接返回队列空或队列满错误,因为中断要的就是快进快出。
队列的结构体
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
int8_t * pcHead; /*< 指向队列存储区域的开始。*/
int8_t * pcWriteTo; /*< 指向存储区域的下一个空闲位置。 */
/* 当用于队列时,使用联合体中的 xQueue 当用于信号量时,使用联合体中的 xSemaphore */
union
{
QueuePointers_t xQueue;
SemaphoreData_t xSemaphore;
} u;
List_t xTasksWaitingToSend; /*< 因为等待入队而阻塞的任务列表。 按优先级顺序存储。*/
List_t xTasksWaitingToReceive; /*< 因为等待出队而阻塞的任务列表。按优先级顺序存储。 */
volatile UBaseType_t uxMessagesWaiting; /*< 当前队列的队列项数目。 */
UBaseType_t uxLength; /*< 队列的总队列项数。 */
UBaseType_t uxItemSize; /*< 队列将保存的每个队列项的大小(单位为字节)。*/
volatile int8_t cRxLock; /*< 存储队列锁定时,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED。 */
volatile int8_t cTxLock; /*< 存储队列锁定时,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED。 */
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果队列使用的内存是静态分配的,则设置为 pdTRUE,以确保不尝试释放内存。*/
#endif
/* 此宏用于使能启用队列集 */
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition * pxQueueSetContainer; /* 指向队列所在队列集 */
#endif
/* 此宏用于使能可视化跟踪调试 */
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
/* 队列的类型
0: 队列或队列集
1: 互斥信号量
2: 计数型信号量
3: 二值信号量
4: 可递归信号量
*/
uint8_t ucQueueType;
#endif
} xQUEUE;
/* 重定义成 Queue_t */
typedef xQUEUE Queue_t;
接下来分析一下各个结构体成员:
1.pcHead:指向队列消息存储区起始位置,即第一个消息空间。
2. pcWriteTo :指向队列消息存储区下一个可用消息空间。(一般就是从pcWriteTo 位置入队消息即尾插)。
3.一个联合体变量: 当用于队列时使用xQueue结构体变量,当用于信号量 时使用xSemaphore结构体变量,详情看下图。
这里只讲队列:
pcTail:队列存储区域的结束地址,与pcHead一样一个指向开始地址一个指向结束地址,他们只是一个一头一尾的标识,在入队出队的时候他们并不会改变。
pcReadFrom:最后一次读取队列的位置。(后面源码会详细解析,有个Read肯定与读队列(出队)有关)
4.xTasksWaitingToSend :发送消息阻塞列表,看英文意思也知道:等待发送的任务,也就是队列已满,任务想要发送消息到队列(入队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为满了,xTasksWaitingToSend 列表中优先级高的就会先被唤醒)。
5.xTasksWaitingToReceive:等待消息阻塞列表,看英文意思也知道:等待接收的任务,也就是队列已空,任务想要从队列中读取消息(出队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为空了,xTasksWaitingToReceive列表中优先级高的就会先被唤醒)。
其实这两条阻塞列表就是队列的核心之一,当然这里只是讲了个大概,更多细节看源码分析。
6.uxMessagesWaiting:用于记录当前消息队列的消息个数,如果消息队列被用于信号量的时候,这个值就表示有效信号量个数。
7.uxLength:表示队列的长度,表示一共能存放多少消息。
8.uxItemSize:表示单个消息的大小(单位为字节)。
9.cRxLock:队列上锁后,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
10.cTxLock:队列上锁后,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
队列为什么要上锁,上锁之后又为啥要记录入队出队个消息个数?关于这些问题后面会有解答。
11.ucQueueType:队列的类型,前面就说了那些信号量都是基于队列的。
创建/删除函数
创建队列:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
xQueueCreate函数有两个参数uxQueueLength,uxItemSize。
uxQueueLength:队列能够存储的最大消息数目,即队列长度。
uxItemSize:队列中消息的大小,以字节为单位 float是四个字节
返回值:
如果创建成功则返回一个队列句柄(就是队列结构体的地址),用于访问创建的队列。 如果创建不成功则返回NULL,可能原因是创建队列需要的 RAM 无法分配成功。
队列创建函数
队列创建与之前的任务创建都分为动态与静态创建,所谓静态创建就是队列所需要的内存需要自己来分配,而动态创建则由FreeRTOS动态分配。在队列这里就不讲静态创建了因为也基本不用,要支持动态创建需要将宏
configSUPPORT_DYNAMIC_ALLOCATION 定义为1,队列所需要的内存分为两部分:1.队列结构体变量 2.队列的存储区域(环形缓存区),动态创建时会自动分配这两块内存,而且是连续的。
xQueueCreate()其实是一个宏,真正来创建队列的函数是xQueueGenericCreate()
发现xQueueGenericCreate()比xQueueCreate()多了一个参数ucQueueType队列类型,Generic代表的就是通用的创建函数,既可以创建队列,也可以创建各种信号量,就取决于ucQueueType这个参数。
xQueueCreate()则代入的是queueQUEUE_TYPE_BASE这个参数,表示创建队列。
源码分析:
代码分析:
1.参数检查是否合法
2.计算队列环形存储空间需要的字节大小,环形存储区大小=消息的总个数*每个消息的大小(字节)。
3.为队列申请内存空间,队列所需内存就是队列结构体的大小+环形存储区的大小,然后一起分配出空间(前面作为队列结构体)。
4.当内存申请成功,获取存储区的起始地址,标记队列为动态申请,最后去调用prvInitialiseNewQueue()函数初始化队列(那些队列结构体成员的初始化)。
5.调用prvInitialiseNewQueue()函数进行队列的初始化:
prvInitialiseNewQueue()函数的源码分析:
prvInitialiseNewQueue()函数前面比较简单就初始化pcHead、uxLength、uxItemSize直接看代码注释即可,最后调用了xQueueGenericReset()函数去重置队列(这里会有更多的结构体成员的初始化)
xQueueGenericReset()函数原形:
xQueueGenericReset()函数源码分析:
1.初始化各个队列结构体变量
pcTail:pcHead存储区起始位置加上存储区的大小(总消息个数*一个消息大小),指向存储区结束位置。
uxMessagesWaiting:刚创建的队列里面无消息,即队列为空,则消息数量为0。
pcWriteTo:下一个入队的位置(从尾部入队),这里的尾部与pcTail是不一样的pcTail是始终指向存储区结束位置,而pcWriteTo是从队首开始第一个空位,所谓的尾部入队。
pcReadFrom:最后一次读取的位置,最开始是在队尾第一个消息的位置,后面出队源码分析的时候再来讲为什么在这个位置。
cRxLock,cTxLock设置为queueUNLOCKED等于-1,表示一开始队列不会上锁。(后面再来讲关于队列上锁)。
到这里队列结构体成员变量基本初始化完毕, 假设我们申请了4个队列项,每个队列项占用32字节存储空间(即uxLength=4、uxItemSize=32),则队列内存分布如下图所示。
后面讲解入队出队的源码时,会查看这张图,主要是看出入队时pcWriteTo、pcReadFrom指针的变化。
2.xQueueGenericReset()函数xNewQueue参数来区别新旧队列,有些人问我们创建队列不就是新队列嘛为啥区分,答:人家这个函数又不是只能重置新队列,这个函数也可以在其他地方调用重置已经创建的旧队列。
那么问题来了重置新队列与重置旧队列有啥区别?
1.重置旧队列:
也就是xNewQueue参数== pdFALSE,我们知道前面的重置操作是一样的,前面已经把队列置空了,记住这是前提条件,所以:
1)如果旧队列之前有任务在等待写队列,此时我们就需要去唤醒阻塞链表优先级最高的一个任务,添加到就绪链表因为前面的重置操作已经让队列为空则允许写队列。
2)如果旧队列之前有任务在等待读队列,则不需要管他让任务继续阻塞,因为重置的队列还是空的不允许读队列。
唤醒任务函数xTaskRemoveFromEventList()源码解析:
唤醒任务xTaskRemoveFromEventList()函数:
1.首先将任务从事件列表(xTasksWaitingToSend或xTasksWaitingToReceive)中移除,该任务是列表中优先级最高的。因为事件列表是按优先级大小排序的(优先级最高的排第一个)。
为什么在任务插入事件列表是按照优先级从大到小排序?
看下图就明白了:
2.将任务从事件列表移除后,此时任务并没有被真正唤醒,因为任务还被挂入了延时列表中(如果不清楚后面入队出队函数会讲)。
3.先需要判断任务调度器是否被挂起(关于任务调度器的知识请参考->FreeRTOS-任务管理)
1.当任务调度器未被挂起时,此时可以操作就绪链表,则将任务从延时链表中移除,然后将任务添加到就绪链表,此时任务就被真正被唤醒。
2.当任务调度器挂起时,则无法访问就绪列表(因为都无法进行任务切换了),所以将任务先挂起到xPendingReadyList链表中,等到任务调度器解除挂起(调用xTaskResumeAll()函数)在该函数中任务会真正被唤醒(从延时链表中移除,然后将任务添加到就绪链表)。
当任务调度器被挂起的时候,就绪链表不能被访问,所以在调度器挂起期间有(某个在等待同步事件的任务等到了该事件就应该从阻塞态变成就绪态)但是调度器挂起无法挂起就绪链表则先将任务挂起到xPendingReadyList链表中等到调度器恢复时,再将xPendingReadyList链表任务一一添加到就绪链表中
恢复调度器函数xTaskResumeAll()
- 2.重置新队列:
新队列的话没有阻塞在事件列表中的任务,直接就初始化两个事件阻塞列表就行。
自此创建队列函数xQueueCreate()就全部讲完,基本上创建一个队列就是为队列分配内存,然后初始化队列结构体的成员变量。
删除队列:
void vQueueDelete( TaskHandle_t pxQueueToDelete );
发送函数
BaseType_t xQueueSend( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
xQueue:要写入的队列
pvItemToQueue:要写入的消息(数据的地址)
xTicksToWait:阻塞超时时间/等待时间(当队列为满,是否需要进行阻塞等待)
返回值:
1.pdTRUE:写入成功
2.errQUEUE_FULL:队列满,写入失败
接收函数
BaseType_t xQueueReceive( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait );
xQueue:要写入的队列
pvItemToQueue:要写入的消息(数据的地址)
xTicksToWait:阻塞超时时间(当队列为满,是否需要进行阻塞等待)
返回值:
1.pdTRUE:写入成功
2.errQUEUE_FULL:队列满,写入失败
发送/接收中断版本
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void *pvBuffer, BaseType_t *pxHigherPriorityTaskWoken );
队列任务版本与中断版本的区别
1、中断中是不能进入阻塞态
2、中断中不能立马切换任务
3、中断是快进快出,执行的代码越少越好
信号量
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,其实信号量主要的功能:就是实现任务之间的同步与互斥,实现的方式主要就是依靠队列 (信号量是特殊的队列)的任务阻塞机制。
既然队列也可以实现同步与互斥那为什么还要信号量?
答:信号量相比队列更节省空间,因为实现同步与互斥不需要传递数据,只需要传递信息。 队列既可以传递信息也可以传递信号
种类
- 计数信号量
• 二值信号量(值为0或1)
• 互斥信号量
• 递归互斥型信号量
任务的同步:
在多任务里,每个任务一定是顺序执行的,它们各自独立, 以不可预知的速度向前推进,但有时候我们希望多个任务能密切合 作,以实现一个共同的任务。
所谓同步,就是多任务在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为任务同步。
同步解决的是任务协同工作的问题
任务的互斥:
多任务执行共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区(criticalsection),它是执行共享资源的代码片段,一定不能给多任务同时执行。
所以我们希望这段代码是互斥(mutualexclusion)的,也就说执行临界区(criticalsection)代码段的只能有一个任务,其他任务阻塞等待,达到排队效果。
互斥解决的是多任务对临界区访问的问题
二值信号量
定义
只有两个值的计数信号量 。
应用场景
二值信号量用于同步:在多任务系统中,经常会使用二值信号量来实现任务之间或者任务与中断之间的同步,比如,某个任务需要等待一个标记,那么任务可以在轮询中查询这个标记有没有被置位,则任务在等待的过程也会消耗CPU的资源, 使用信号量的方式就可以完美实现同步,即保证了正确性,有保证了效率(让任务进 入阻塞态)。
二值信号量用于互斥:二值信号量一般不用于任务之间的互斥(任务之间互斥的访问一个临界资源,同一时间只能一个任务可以使用),因为它有优先级反转的缺点,解决互斥的方式就是使用互斥信号量(具有优先级继承的机制能减少优先级反转的影响) 。
API接口
计数信号量
定义:
计数值信号量是长度大于0的队列,计数值信号量常用于事件计数、资源管理,其实如果限定计数信号量计数值最大值只能为1则就等同于二值信号量。
应用场景
事件计数:在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的资源数加 1:生产),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1:消费),这种场景下,计数型信号量的资源数一般在创建时设置为 0。
资源管理:在这种场合下,计数型信号量的资源数代表着共享资源的可用数量,一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。
API接口
互斥信号量
定义:
二值信号量/计数值信号量其实也可以实现互斥,但存在优先级反转的缺点,而解决方法就是优先级继承,优先级继承就是互斥信号量的特性,则互斥信号量的本质就是具有优先级继承的二值信号量。
优先级反转
简单来说就是低优先级的任务霸占CPU资源,导致高优先级任务无法运行的情况。导致这种严重优先级反转的问题的根本原因在于持有锁的低优先级任务因为优先级低,而得不到执行,得不到执行的话,就无法解锁,无法解锁就导致高优先级的任务获取锁会失败,从而导致高优先级任务一直在阻塞状态。
解决优先级反转:优先级继承
解决方式就是优先级继承,在高优先级任务进入阻塞之前将低优先级任务的优先级提升至与高优先级一致,这样等高优先级任务进入阻塞之后,低优先级任务就能继承高优先级任务的优先级,这样低优先级任务就能尽快执行(从而解锁,让高优先级能够获取锁)。
优先级继承:暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。
消除优先级反转带来的影响了吗?
没有。因为优先级反转概念是指一个低优先级的任务持有一 个被高优先级任务所需要的共享资源,也就是说当因为任务A上锁之后,然后任务C来获取锁失败而进入阻塞态,在高优先级任务进入阻塞态的时候已经是优先级反转了,而优先级继承只是让任务A 尽快执行(尽快解锁),让高优先级任务C尽快解除阻塞态,所以优先级继承只能是缩短优先级反转的时间。 (尽可能地降低,并没有消除)
API接口
应用场景
一般用于保护共享临界资源,从而实现独占式访问,而可以降低优先级反转带来的影响。
注意: 1.优先级继承并不是完全能避免优先级反转只能是减低其影响(前面已经解释过) 2.互斥信号量不能用于中断服务函数中,原因如下:
(1) 互斥信号量有任务优先级继承的机制,但是中断不是任务,没有任务优先 级,所以互斥信号量只能用与任务中,不能用于中断服务函数。
(2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态
递归互斥信号量
引入
互斥信号量的缺陷:1.并没有实现谁持有锁就由谁释放
2.不能递归上锁,会导致死锁
而递归互斥信号量则完美实现了这两点,而且递归互斥信号量也是基于互斥信号量的,所以一样具有优先级继承的机制,无中断
API接口
实验:
添加触摸屏部分touch.c
在LCDconf.c中添加红字部分
//自己写
void LCD_X_Config(void)
{
GUI_DEVICE_CreateAndLink(&GUIDRV_Template_API, GUICC_M565, 0, 0); /* 创建显示驱动器件 */
//添加LCD的分辨率
LCD_SetSizeEx(0, XSIZE_PHYS, YSIZE_PHYS); //实际
LCD_SetVSizeEx(0, XSIZE_PHYS, YSIZE_PHYS); //虚拟的
//触摸屏的XY的分辨率和最大值
GUI_TOUCH_SetOrientation(GUI_SWAP_XY);
GUI_TOUCH_Calibrate(GUI_COORD_X, 0, 240 - 1, 140, 3881);
GUI_TOUCH_Calibrate(GUI_COORD_Y, 0, 320 - 1, 170, 3742);
}
这段代码主要负责配置LCD显示器和触摸屏的相关设置,以便于在嵌入式系统中正确显示图像和处理触摸输入。下面是详细的解释:
函数概述
`LCD_X_Config(void)` 函数是用户自定义的配置函数,用于初始化LCD显示设备及触摸屏的设置。它包含了以下几个关键步骤:
1. 创建显示驱动器件
- `GUI_DEVICE_CreateAndLink(&GUIDRV_Template_API, GUICC_M565, 0, 0);`
这行代码创建并链接了一个图形设备(通常是LCD显示器)到GUI系统中。这里使用的是`GUIDRV_Template_API`作为显示驱动的接口,`GUICC_M565`指定了色彩模式为16位(每像素5位红、6位绿、5位蓝),后两个0是驱动特定的参数,可能代表默认设置或无额外参数。
2. 设置LCD显示尺寸
- `LCD_SetSizeEx(0, XSIZE_PHYS, YSIZE_PHYS);`
- `LCD_SetVSizeEx(0, XSIZE_PHYS, YSIZE_PHYS);`
这两行分别设置了物理尺寸(实际显示区域的大小)和虚拟尺寸(逻辑显示区域的大小)为相同的值(`XSIZE_PHYS`和`YSIZE_PHYS`),意味着虚拟显示区域与实际显示区域完全匹配,没有缩放或滚动。索引0通常指的是默认的显示层。
3. 触摸屏配置
1. GUI_TOUCH_SetOrientation(GUI_SWAP_XY):这行代码是用来设置触摸屏坐标系统的方向。`GUI_SWAP_XY`宏意味着它会交换X轴和Y轴的方向。换句话说,原本横向滑动变成了纵向滑动,纵向滑动变成了横向滑动。这是在触摸屏坐标与显示屏坐标不匹配,或者需要根据设备物理布局调整触摸响应时非常有用。
2. `GUI_TOUCH_Calibrate(GUI_COORD_X, 0, 240 - 1, 140, 3881):这行代码是用来校准触摸屏的X轴。`GUI_TOUCH_Calibrate`函数通常接受四个参数:要校准的坐标轴(在这里是X轴,由`GUI_COORD_X`指定),以及两个点的坐标值。这里的点 `(0, 140)` 和 `(240 - 1, 3881)` 分别代表了触摸屏上的两个已知点的原始坐标值。通过这两个点,EMWIN可以计算出一个转换矩阵,使得触摸输入能够准确映射到屏幕的坐标系统上。注意,`(240 - 1)` 表示宽度的最后一个像素位置,这是因为坐标往往是从0开始的。
3. `GUI_TOUCH_Calibrate(GUI_COORD_Y, 0, 320 - 1, 170, 3742):同样,这行代码是用来校准触摸屏的Y轴。原理和X轴的校准相同,只是这次应用于垂直方向。点 `(0, 170)` 和 `(320 - 1, 3742)` 是用户在Y轴上选择的两个参考点,用于确定触摸输入在Y轴上的线性映射关系。`(320 - 1)` 是屏幕高度的最后一个像素位置。
如果在GUI_Conf中设置GUI_SUPPORT_TOUCH为1,则添加GUI_X_Touch_Analog.c保证四个函数的完整性且自己添加
int GUI_TOUCH_X_MeasureX(void) {
int32_t xvalue;
uint16_t x,y;
tp_dev.scan(0);
if (tp_dev.sta & TP_PRES_DOWN) /* 触摸屏被按下 */
{
/* 电阻屏出现触摸没有那么顺畅的问题,请把touch.c文件下的TP_ERR_RANGE宏定义设置大一点 */
TP_Read_XY2(&x, &y);
return (4095 - x);
}
return 0;
}
int GUI_TOUCH_X_MeasureY(void)
{
int32_t yvalue;
uint16_t x,y;
/* 解决快速按下的鼠标乱屏问题 */
tp_dev.scan(0);
if (tp_dev.sta & TP_PRES_DOWN) /* 触摸屏被按下 */
{
/* 电阻屏出现触摸没有那么顺畅的问题,请把touch.c文件下的TP_ERR_RANGE宏定义设置大一点 */
TP_Read_XY2(&x, &y);
return y;
}
return 0;
}
这段代码是用于处理电阻式触摸屏的读取和坐标转换功能的,主要目的是为了与EMWIN图形库集成,实现在触摸屏上获取触控坐标的功能。具体分析如下:
GUI_TOUCH_X_MeasureX(void)
- **功能**: 此函数用于测量并返回触摸屏上触点的X坐标。
- **过程**:
1. 首先,调用`tp_dev.scan(0);`来扫描触摸屏状态,检查是否有触摸事件发生。
2. 判断`tp_dev.sta & TP_PRES_DOWN`条件是否成立,该条件检查触摸屏是否被按下。如果触摸屏被按下,则继续执行。
3. 调用`TP_Read_XY2(&x, &y);`读取当前触点的X、Y坐标。这里返回的是原始的触摸坐标。
4. 因为坐标可能是从一边倒计数的,所以对X坐标进行反转处理,即返回`(4095 - x)`作为最终的X坐标。这通常是因为某些触摸屏的坐标系原点在屏幕的右下角,而EMWIN或其他图形库可能期望原点在左上角。
5. 如果没有触摸事件发生,则返回0。
GUI_TOUCH_X_MeasureY(void)
- **功能**: 此函数用于测量并返回触摸屏上触点的Y坐标,过程类似上述X坐标的测量,但Y坐标直接使用读取到的值,未进行特殊转换。
- **过程**:
1. 同样,通过`tp_dev.scan(0);`检查触摸屏状态。
2. 判断触摸屏是否被按下。
3. 使用`TP_Read_XY2(&x, &y);`读取触点坐标。
4. 直接返回Y坐标值`y`,因为在这个例子中没有进行Y坐标的特别转换。
5. 若无触摸事件,同样返回0。
这两段代码是电阻式触摸屏驱动与EMWIN图形库接口的重要组成部分,确保了触摸输入能够被正确读取并转换为EMWIN可以理解的坐标格式,进而响应用户的触摸操作。
main函数
GUI_TOUCH_Exec();调用上面添加部分
#include "main.h"
extern WM_HWIN CreateWindow(void);
TaskHandle_t DHT11Task_Handler; /* 任务句柄 */
TaskHandle_t LCDTask_Handler; /* 任务句柄 */
TaskHandle_t TouchTask_Handler; /* 任务句柄 */
char show_buffer[100];
DHT11_t gDht11;
LED_t gLed[3];
WM_HWIN hItemH, hItemT;
QueueHandle_t gDHT112EmWin_Queue;
xSemaphoreHandle gDHT112EmWin_Queue_Mutex;
/*****************************************************
* @brief Task_DHT11
* @param pvParameters : 传入参数(未用到)
* @retval 无
*
******************************************************/
void Task_DHT11(void *pvParameters)
{
//float占用四个字节
float dht11_data[2] = {0};
/* 初始化DHT11 */
int ret = DHT11_Init(&gDht11, GPIOA, GPIO_Pin_6);
printf("ret: %d\r\n", ret);
//初始化队列
gDHT112EmWin_Queue = xQueueCreate(5, sizeof(float) * 2); //8个字节
if (!gDHT112EmWin_Queue) {
printf("create emwin queue fail\r\n");
}
gDHT112EmWin_Queue_Mutex = xSemaphoreCreateMutex();
if (!gDHT112EmWin_Queue_Mutex) {
printf("create emwin queue mutex fail\r\n");
}
while (1) {
/* 每秒读取一次温湿度传感器,并通过串口发送 */
DHT11_ReadData(&gDht11);
dht11_data[0] = gDht11.temperature;
dht11_data[1] = gDht11.humidity;
xSemaphoreTake(gDHT112EmWin_Queue_Mutex, portMAX_DELAY); //上锁
xQueueSend(gDHT112EmWin_Queue, dht11_data, portMAX_DELAY); //向队列中发送数据
xSemaphoreGive(gDHT112EmWin_Queue_Mutex); //解锁
printf("T: %0.2f H: %0.2f\r\n", gDht11.temperature, gDht11.humidity);
vTaskDelay(1000);
}
}
/*****************************************************
* @brief Task_LCD
* @param pvParameters : 传入参数(未用到)
* @retval 无
*
******************************************************/
void Task_LCD(void *pvParameters)
{
int i = 0;
float dht11_data[2] = {0};
float temperature; //温度值
float humidity; //湿度
CreateWindow();
while (1) {
xSemaphoreTake(gDHT112EmWin_Queue_Mutex, portMAX_DELAY); //上锁
//判断队列是否空?判断是否有数据项
BaseType_t wait_cnt = uxQueueMessagesWaiting(gDHT112EmWin_Queue); //uxQueueMessagesWaiting() 被用来查询 queue 现在包含的数据项数
if (wait_cnt <= 0){
xSemaphoreGive(gDHT112EmWin_Queue_Mutex); //解锁
vTaskDelay(10);
continue; //跳出本次循环到while的第一句开始再次执行
}
xQueueReceive(gDHT112EmWin_Queue, dht11_data, portMAX_DELAY); //从队列中接受数据
xSemaphoreGive(gDHT112EmWin_Queue_Mutex); //解锁
temperature = dht11_data[0];
humidity = dht11_data[1];
memset(show_buffer, 0, sizeof(show_buffer));
sprintf((char *)show_buffer, "%0.2f", temperature);
EDIT_SetText(hItemT, show_buffer);
memset(show_buffer, 0, sizeof(show_buffer));
sprintf((char *)show_buffer, "%0.2f", humidity);
EDIT_SetText(hItemH, show_buffer);//EDIT_SetText() 设置文本
GUI_Delay(50);
}
}
void Task_Touch(void *pvParameters)
{
pvParameters = pvParameters;
GUI_Init(); //任务三的优先级最高,GUI初始化之后,任务1/2直接调用
while (1)
{
GUI_TOUCH_Exec();调用的是GUI_X_Touch_Analog.c检测X,Y坐标的函数
GUI_Delay(5);
}
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //中断分组配置
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_CRC, ENABLE); //开启CRC时钟
Debug_Init(115200);
Delay_Init();
LED_Init(&gLed[0], GPIOE, GPIO_Pin_5);
LED_Init(&gLed[1], GPIOE, GPIO_Pin_6);
LED_Init(&gLed[2], GPIOC, GPIO_Pin_13);
ILI9341_Init();
tp_dev.init(); /* 触摸屏初始化 */
xTaskCreate((TaskFunction_t )Task_DHT11, /* 任务函数 */
(const char* )"Task_DHT11", /* 任务名称 */
(uint16_t )512, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )2, /* 任务优先级 */
(TaskHandle_t* )&DHT11Task_Handler); /* 任务句柄 */
xTaskCreate((TaskFunction_t )Task_Touch, /* 任务函数 */
(const char* )"Task_Touch", /* 任务名称 */
(uint16_t )512, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )3, /* 任务优先级 */
(TaskHandle_t* )&TouchTask_Handler); /* 任务句柄 */
xTaskCreate((TaskFunction_t )Task_LCD, /* 任务函数 */
(const char* )"Task_LCD", /* 任务名称 */
(uint16_t )512, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )1, /* 任务优先级 */
(TaskHandle_t* )&LCDTask_Handler); /* 任务句柄 */
vTaskStartScheduler();
while(1)
{
}
}