STM32单片机项目实例:基于TouchGFX的智能手表设计(3)嵌入式程序任务调度的设计
目录
一、嵌入式程序设计
1.1轮询
1.2 前后台(中断+轮询)
1.3 事件驱动与消息
1.3.1 事件驱动的概念
1.4 定时器触发+事件驱动型的任务设计
1.4.1定时器触发
1.4.2 界面事件驱动
一、嵌入式程序设计
大数学家华罗庚先生在《统筹方法》中写到自己泡茶的故事,也就是大家语文课本的《时间统筹法》一文,在这个故事中,时间统筹法主要是用来做时间管理,优化做事情的流程,节约时间。比如:洗开水壶、烧水需要16分钟,洗茶壶、洗茶杯、拿茶叶需要4分钟,这两件事情先做哪个?这是最常见的家务活举例,不同的思维方式产生不同结果。
如果按照线性思维,先去洗开水壶、烧水需要16分钟,再去洗茶壶、洗茶杯和拿茶叶需要4分钟,那一共需要16+4=20分钟;按照时间统筹法,先洗开水壶,把水放在炉子上烧,然后同时去洗茶壶、茶杯、拿茶叶,等水烧好了,茶具也准备好了,这样两件事情一共只需要花费1+15=16分钟,无形中就节约了4分钟的时间。
图 1-1 《时间统筹法》中的家务活举例
用嵌入式系统去看泡茶这件事情,水壶、茶壶、茶杯、茶叶等可以理解为嵌入式系统中的硬件层,洗、拿、烧的动作理解为嵌入式系统中的驱动层,泡茶理解为嵌入式系统中的应用层。
图 1-2 从嵌入式系统的角度看待泡茶故事
泡茶故事中,如按照线性思维去操作,泡茶需要经过洗水壶 -> 烧开水 -> 洗茶壶 -> 洗茶杯 -> 拿茶叶 -> 泡茶总共6个过程,这些过程我们换个词用“状态”去表示,洗水壶状态-> 烧开水状态 -> 洗茶壶状态 -> 洗茶杯状态 -> 拿茶叶状态 -> 泡茶状态,这些状态间的“迁移”依赖于某一时刻发生的有意义的事情(例如洗水壶完成、烧开水完成…),进而发生了状态的迁移,我们称之为“事件”。在状态的“迁移”过程中,我们需要做出其它一些行为,这些行为就是“动作”,例如拿水壶、拿茶壶或者拿茶杯等,“动作”是对事件的响应。对于事件的响应,还依赖于是否满足一定的“条件”才能发生状态间的迁移,并不是有求必应的。泡茶的过程在嵌入式程序设计中可以用嵌入式状态机(FSM)模式进行设计(一款C语言编写的轻量级的函数指针有限状态机编程框架,可实现entry和exit动作),嵌入式状态机是一种基于状态转移的程序设计模式,它通过将程序的执行过程分成一系列状态,以及描述状态转移的规则,实现复杂问题的分步解决。在嵌入式系统中,状态机常用来实现复杂的控制逻辑、事件处理和通信协议等功能,其简单灵活的设计在嵌入式系统应用中得到了广泛的运用。
图 1-3 有限状态机在泡茶故事中的使用
嵌入式系统的应用场景,比泡茶的过程更为复杂。例如硬件方面,处理器的单核与多核、外部设备对响应速度与周期性控制、低功耗等要求等。软件方面裸机与嵌入式系统(RTOS、Linux)开发的不同,以及是否使用中间件(TouchGFX)等。在这些场景下,就需要具备嵌入式程序设计的思想和方法。本文对微控制器裸机任务开发的设计方法进行探讨,主要涉及应用程序的轮询、前后台、优先级与时间片、有限状态机、定时器触发+事件驱动型的任务调度进行讲解。
1.1轮询
对于简单的应用程序,轮询(无限循环)的实现比较简单,在硬件完成初始化后,顺序的完成各种任务。在外设的基础实验中,常采用这种方式。 轮询的伪代码实现方式:
01 int main(void)
02 {
03 /* 硬件相关初始化 */
04 HardwareInit();
05
06 /* 无限循环 */
07 while(1) {
08 /* 任务1 */
09 Task1();
10
11 /* 任务2 */
12 Task2();
13
14 /* 任务3 */
15 Task3();
16 }
17 }
在实际的嵌入式系统中,存在周期性(周期100ms,处理时间10ms)与触发型任务(扫地机器人,悬空检测,实时性),每个任务的执行时间与实时响应要求不同,在采用轮询系统进行程序设计时,很难应对这些场景。
1.2 前后台(中断+轮询)
前后台系统是在轮询的基础上加入了中断。外部事件的记录在中断中操作,对事件的响应在轮询中完成,中断处理过程称之为前台,main函数中的轮询称为后台。如下图所示:
后台的程序顺序执行,如果产生中断,那么中断会打断后台程序的正常执行,转而去执行中断服务程序。如果事件的处理过程比较简单,可以直接在中断服务程序中进行处理;如果事件的处理过程比较复杂,可以在中断中对事件响应进行标记,进而返回后台程序进行处理。轮询的伪代码实现方式:
01 int main(void)
02 {
03 /* 硬件相关初始化 */
04 HardwareInit();
05
06 /* 无限循环 */
07 while(1) {
08 /* 任务1 */
09 if(Task1标志)
10 {
11 Task1();
12 Task1标志为假;
13 }
14
15 /* 任务2 */
16 if(Task2标志)
17 {
18 Task2();
19 Task2标志为假;
20 }
21
22 /* 任务3 */
23 if(Task3标志)
24 {
25 Task3();
26 Task3标志为假;
27 }
28 }
29 }
30 /**
31 ** Task1的中断服务程序
32 **/
33 void Task1_Handler(void)
34 {
35 Task1标志为真;
36 }
37 /**
38 ** Task1的中断服务程序
39 **/
40 void Task2_Handler(void)
41 {
42 Task2标志为真;
43 }
44 /**
45 ** Task1的中断服务程序
46 **/
47 void Task3_Handler(void)
48 {
49 Task3标志为真;
50 }
相较于轮询系统,前后台系统可以确保事件的记录不会丢失,提高了对事件的响应。同时,基于Cortex-M内核的MCU对异常具有可编程的优先级(中断嵌套)、末尾连锁以及延迟到达等功能,这可以大大提高程序的实时响应能力。
采用前后台系统进行程序时,对后台的任务需要进行设计,避免单个任务长时间占有处理器资源。当任务的逻辑比较复杂,任务的拆分难度增加,同时,随着中断事件的增加,整个程序的设计与响应的实时性将会降低。
1.3 事件驱动与消息
嵌入式MCU软件开发中,我们应具备程序分层设计的思想,程序分层设计能够降低软件的复杂度和依赖关系,同时有利于标准化,便于管理各层的程序,提高各层逻辑的复用(软件工程技术中的复用与解耦,复用可以极大提升软件开发效率,使得软件开发可以从 70% 甚至 90% 起步;而解耦可以大幅提升软件的可维护性和可修改性,降低长期维护成本)。
1.3.1 事件驱动的概念
Hello,World!是很多初学者进行嵌入式操作系统编程时的第一个程序。在嵌入式MCU裸机编程中,UART外设要比GPIO外设更为复杂,初学者的第一个程序往往是点亮LED灯(点灯大师),在GPIO的输入操作时,通过按键输入去控制LED,伪代码实现方式如下:
01 int main(void)
02 {
03 /* 硬件相关初始化 */
04 HardwareInit();
05
06 /* 无限循环 */
07 while(1)
08 {
09 /* 按键扫描 */
10 if(Key为低)
11 {
12 delay(100ms);//延时100ms,电平稳定后读取
13 if(Key为低)
14 {
15 LED点亮;
16 }
17 }
18 else
19 {
20 LED熄灭;
21 }
22 /* 其它任务 */
23 ......
24 }
25 }
采用该种方式进行程序结构设计时,按键输入的响应依赖于其它任务的执行时间与任务的数量,若其它任务的执行时间是200ms,则可能造成按键事件的丢失。在学习EXTI部分的知识后,可以采用中断的方式进行按键事件的响应,伪代码实现方式如下:
01 int main(void)
02 {
03 /* 硬件相关初始化 */
04 HardwareInit();
05
06 /* 无限循环 */
07 while(1)
08 {
09 /* 其它任务 */
10 ......
11 }
12 }
13 /******************************************************************
14 *FuncName :EXTIx_IRQHandler
15 *Description :EXTIx中断服务函数
16 *Arguments :void
17 *Return :void
18 *******************************************************************/
19 void EXTIx_IRQHandler ( void )
20 {
21 ......
22 /* 按键扫描 */
23 if(Key为低)
24 {
25 delay(100ms);//延时100ms,电平稳定后读取
26 if(Key为低)
27 {
28 LED点亮;
29 }
30 }
31 else
32 {
33 LED熄灭;
34 }
35 ......
36 }
采用该种方式,提高了系统对按键输入的响应,同时也存在优先级设置的问题,在采用STM32的HAL库开发中,HAL_Delay()延时函数默认采用系统滴答定时器(Systick)的中断产生计时。上述代码能够正常运行的前提是Systick的中断优先级要比外部中断线的优先级高,同时,按键的触发频率也不能太高,这些要求在复杂的系统中,很难得到满足。
注释:T1-T4时间内其它低优先级中断被挂起,降低了系统响应性,同时阻塞了其它任务的执行。T2-T3时间内,处理器的资源被浪费。
对于中断的处理机制,减少中断响应时间是设计的初衷,我们将思路进行调整,当按键按下这个事件发生时,在中断中对事件进行记录,在主循环中对记录的事件进行处理,伪代码实现方式如下:
01 # define FLG_KEY 0x08
02 volatile uint8_t gu8EvntFlgGrp = 0 ; /*事件标志组*/
03 int main(void)
04 {
05 uint8_t pu8FlgTmp = 0 ;
06 /* 硬件相关初始化 */
07 HardwareInit();
08
09 /* 无限循环 */
10 while(1)
11 {
12 pu8FlgTmp = read_envt_flg_grp(); /*读取事件标志组*/
13 //
14 if (pu8FlgTmp) /*是否有事件发生? */
15 {
16 if (pu8FlgTmp & FLG_KEY)
17 {
18 LED点亮;
19 }
20 }
21 else
22 {
23 LED熄灭;
24 ; /* 空闲代码 */
25 }
26
27 }
28 }
29 /******************************************************************
30 *FuncName :read_envt_flg_grp
31 *Description :读取事件标志组 gu8EvntFlgGrp,读取完毕后将其清零。
32 *Arguments :void
33 *Return :void
34 *******************************************************************/
35 uint8_t read_envt_flg_grp ( void )
36 {
37 uint8_t pu8FlgTmp = 0 ;
38 /* 关闭全局中断 */
39 __disable_irq();
40 /* 读取标志组 */
41 pu8FlgTmp = gu8EvntFlgGrp;
42 /* 清零标志组 */
43 gu8EvntFlgGrp = 0 ;
44 /* 开启全局中断 */
45 __enable_irq();
46 //返回值
47 return pu8FlgTmp;
48 }
49 /******************************************************************
50 *FuncName :EXTIx_IRQHandler
51 *Description :EXTIx中断服务函数
52 *Arguments :void
53 *Return :void
54 *******************************************************************/
55 void EXTIx_IRQHandler ( void )
56 {
57 ......
58 /* 关闭全局中断 */
59 __disable_irq();
60 HAL_TIM_Base_Start_IT(&htimx); //开启定时器
61 /* 开启全局中断 */
62 __enable_irq();
63 ......
64 }
65 /******************************************************************
66 *FuncName : TIMx_IRQHandler
67 *Description : TIMx中断服务函数,100ms中断一次
68 *Arguments : void
69 *Return : void
70 *******************************************************************/
71 void TIMx_IRQHandler ( void )
72 {
73 ......
74 if(Key为低)
75 {
76 /* 关闭全局中断 */
77 __disable_irq();
78 gu8EvntFlgGrp |= FLG_KEY; /*设置 KEY 事件标志*/
79 HAL_TIM_Base_Stop(&htimx); //关闭定时器
80 /* 开启全局中断 */
81 __enable_irq();
82 }
83 ......
84 }
注释:T1-T2/ T3-T4快速记录/处理按键触发事件。T2-T3时间内,处理器的资源被充分利用。
上述的按键任务中,按键的按下与释放是一种事件,中断处理流程对事件进行记录,并产生一个消息,主循环中对消息进行执行并销毁,是一种比较简单的事件驱动机制。在嵌入式系统中,事件驱动机制的应用也十分广泛。下面是UART、TIMER、EXTI与KEY等外设的事件检测在各自中断中完成,通过事件驱动机制通知主函数进行处理的完整伪代码。
1 # define FLG_UART 0x01
2 # define FLG_TMR 0x02
3 # define FLG_EXI 0x04
4 # define FLG_KEY 0x08
5
6 volatile uint8_t gu8EvntFlgGrp = 0 ; /*事件标志组*/
7
8 uint8_t read_envt_flg_grp( void );
9 /************************************************************
10 *FuncName :main
11 *Description :主函数
12 *Arguments :void
13 *Return :void
14 *************************************************************/
15 void main ( void )
16 {
17 uint8_t pu8FlgTmp = 0 ;
18 /* 硬件相关初始化 */
19 HardwareInit();
20 /* 无限循环 */
21 while ( 1 )
22 {
23 pu8FlgTmp = read_envt_flg_grp(); /*读取事件标志组*/
24 //
25 if (pu8FlgTmp ) /*是否有事件发生? */
26 {
27 if (pu8FlgTmp & FLG_UART)
28 {
29 action_uart_task(); /* 处理串口事件 */
30 }
31 if (pu8FlgTmp & FLG_TMR)
32 {
33 action_timer_task(); /* 处理定时中断事件 */
34 }
35 if (pu8FlgTmp & FLG_EXI)
36 {
37 action_exti_task(); /* 处理外部中断事件 */
38 }
39 if (pu8FlgTmp & FLG_KEY)
40 {
41 action_key_task(); /* 处理击键事件 */
42 }
43 }
44 else
45 {
46 ; /* 空闲代码 */
47 }
48 }
49 }
50 /******************************************************************
51 *FuncName :read_envt_flg_grp
52 *Description :读取事件标志组 gu8EvntFlgGrp,读取完毕后将其清零。
53 *Arguments :void
54 *Return :void
55 *******************************************************************/
56 uint8_t read_envt_flg_grp ( void )
57 {
58 uint8_t pu8FlgTmp = 0 ;
59 /* 关闭全局中断 */
60 __disable_irq();
61 /* 读取标志组 */
62 pu8FlgTmp = gu8EvntFlgGrp;
63 /* 清零标志组 */
64 gu8EvntFlgGrp = 0 ;
65 /* 开启全局中断 */
66 __enable_irq();
67 //返回值
68 return pu8FlgTmp;
69 }
70 /******************************************************************
71 *FuncName :UARTx_IRQHandler
72 *Description :UARTx中断服务函数
73 *Arguments :void
74 *Return :void
75 *******************************************************************/
76 void UARTx_IRQHandler ( void )
77 {
78 ......
79 push_uart_rcv_buf(new_rcvd_byte); /*新接收的字节存入缓冲区*/
80 /* 关闭全局中断 */
81 __disable_irq();
82 gu8EvntFlgGrp |= FLG_UART; /*设置 UART 事件标志*/
83 /* 开启全局中断 */
84 __enable_irq();
85 ......
86 }
87 /******************************************************************
88 *FuncName : TIMx_IRQHandler
89 *Description : TIMx中断服务函数
90 *Arguments : void
91 *Return : void
92 *******************************************************************/
93 void TIMx_IRQHandler ( void )
94 {
95 uint8_t u8KeyCode = 0 ;
96 ......
97 /* 关闭全局中断 */
98 __disable_irq();
99 gu8EvntFlgGrp |= FLG_TMR; /*设置 TMR 事件标志*/
100 /* 开启全局中断 */
101 __enable_irq();
102 ......
103 u8KeyCode = read_key(); /*读键盘*/
104 if (u8KeyCode) /*有击键操作? */
105 {
106 push_key_buf(u8KeyCode); /*新键值存入缓冲区*/
107 /* 关闭全局中断 */
108 __disable_irq();
109 gu8EvntFlgGrp |= FLG_KEY; /*设置 KEY 事件标志*/
110 /* 开启全局中断 */
111 __enable_irq();
112 }
113 ......
114 }
115 /******************************************************************
116 *FuncName :EXTIx_IRQHandler
117 *Description :EXTIx中断服务函数
118 *Arguments :void
119 *Return :void
120 *******************************************************************/
121 void EXTIx_IRQHandler ( void )
122 {
123 ......
124 /* 关闭全局中断 */
125 __disable_irq();
126 gu8EvntFlgGrp |= FLG_EXI; /*设置 EXI 事件标志*/
127 /* 开启全局中断 */
128 __enable_irq();
129 ......
130 }
以上事件处理代码可以做成标准的框架代码,它能够应对大部分嵌入式裸机编程的情况。同时,事件驱动机制采用这样的方式实现也存几点问题需要注意:
同事件集中爆发时,会丢失后面发生的事件。
不同事件集中爆发,会丢失事件发生的顺序。
事件的优先级与多任务并发执行。
上图中,T1、T2、T3是各个任务的事件处理函数,I1、I2、I3是不同事件触发的IRQ,假定I1、I2、I3分别对应E1、E2、E3事件,当运行T1的事件处理函数时,发生了2次相同的事件,T1事件处理函数被中断2次,I2在执行的时候,连续两次置位了相应的事件标志位。
当T1的事件处理函数完成后,顺序执行T2的事件处理函数,在T2执期间,发生2次不同的事件。T2被中断2次,I1和I3执行并置位相应的事件标志位。
执行T1事件处理函数时,产生的两次E2事件,由于没有缓冲机制,在执行T2事件处理函数时,会丢失对E2事件的处理,也就是我们上面的讲到的:同事件集中爆发时,会丢失后面发生的事件。
执行T2事件处理函数时,产生的E1和E3事件,主循环处理事件的顺序是按照程序预先设定的顺序,一个一个的处理事件,若集中爆发不同事件,对于事件的发生顺序与处理顺序会产生不一致的情况,若系统对于事件的发生顺序敏感,则无法满足。
为了解决事件的丢失与发生顺序,可以在与事件相关的 IRQ中把事件加工成消息,并把它存储在消息缓冲区中,消息缓冲区设计成以“先入先出”方式管理的环形缓冲队列。事件生成的消息总是从队尾入队,管理程序读取消息的时候总是从队头读取,这样,消息在缓冲区中存储的顺序就是事件在时间上发生的顺序,先发生的事件总是能先得到响应。IRQ完成这些工作后立即退出。主循环通过查询消息缓冲区,将存储的消息信息进行分析与执行,最终完成对本次事件的响应。
通过这种方法实现的事件驱动机制能够解决前面提到的那两个问题,即不同事件集中爆发时,无法记录事件发生的前后顺序。同一事件集中爆发时,容易遗漏后面发生的事件。对于第一种情况,消息(事件)在缓冲队列中是以“先入先出”的方式存储的,存储顺序就代表了事件发生的先后顺序。对于第二种情况, 任何被 ISR 捕捉到的事件都会以一个独立的消息实体存入缓冲队列, 即使前后两个是同一个事件, 只要 ISR 反应够快就不会遗漏事件。实际上, ISR 的主要工作就是填写消息实体, 然后将其存入缓冲队列, 做这些工作只占用 CPU 很短的时间。
对于事件的优先级与多任务并发执行需求的场景,建议采用实时操作系统(Real Time Operating System,简称RTOS),RTOS在调度方式上,分为抢占式调度、时间片调度和合作式调度。采用RTOS可以确保在一定的时间内能够执行到所有的任务。
1.4 定时器触发+事件驱动型的任务设计
采用裸机方式开发基于TouchGFX的智能手表项目,了解TouchGFX的相关原理是十分重要的, TouchGFX用户接口遵循Model-View-Presenter(MVP)架构模式,它是Model-View-Controller(MVC)模式的派生模式。 两者都广泛用于构建用户接口应用。MVP模式的主要优势是:
关注点分离:将代码分成不同的部分提供,每部分有自己的任务。 这使得代码更简单、可重复使用性更高且更易于维护。
单元测试:由于UI的逻辑(Presenter)独立于视图(View),因此,单独测试这些部分会容易很多。
MVP中定义了下列三个类:
Model是一种接口,用于定义要在用户界面上显示或有其他形式操作的数据。
View是一种被动接口,用于显示数据(来自Model),并将用户指令(事件)传给Presenter以便根据该数据进行操作。
Presenter的操作取决于Model和View。 它从存储库(Model)检索数据,并将其格式化以便在视图中显示。
在TouchGFX中,从Model类执行与应用非UI部分(这里称为后端系统)的通信。后端系统是从UI接收事件和将事件输入UI的软件组件,例如采集传感器的新测量值。后端系统可作为单独的任务在同一MCU、单独的处理器、云模块或其他硬件上运行。从TouchGFX的角度来看,这并不十分重要,只要它是能够与之通信的组件。
使用的特定通信协议不受TouchGFX管理。它只提供一个在每个TouchGFX嘀嗒时间调用一次的函数,可以在其中处理需要的通信。
屏幕概念
在TouchGFX应用中,可以有任意数量的“屏幕”。 TouchGFX中的屏幕是UI元素(小工具)及其相关业务逻辑的逻辑分组。 屏幕包含两个类:包含该屏幕上显示的所有小工具的View类,以及包含该屏幕的业务逻辑的Presenter类。
可以选择在单个屏幕的背景下实现整个应用(意味着只有一个View和一个Presenter),建议将UI的不相关部分分割成不同屏幕,原因有两个:
1. TouchGFX包含存储器分配方案,可自动分配大多数RAM占用量大的屏幕所需的必要RAM。 只会分配必要的量,此RAM块将在应用中的所有屏幕之间重复使用。
2. 有多个屏幕会使UI代码的维护容易得多。
定义屏幕
关于应如何将应用划分成多个屏幕,并没有具体的规则,但有特定的指南,也许能帮助您决定应当用哪些屏幕构成您的特定应用。 在视觉和功能上无关的UI区域应保存在不同屏幕中。
如果是十分简单的有主温度读出显示屏和配置菜单的恒温器应用,建议创建“主屏幕”用于温度读出,并创建“设置屏幕”用于显示配置菜单。主屏幕的视图将包含用于背景图像的小工具,几个显示温度的文本区和一个用于切换至配置菜单的按钮。 另一方面,用于配置的视图可能包含显示配置选项列表和不同背景图像的小工具。 如果配置菜单能够编辑许多不同类型的设置(日期、名称和键盘、温度、单位等),此屏幕的复杂性将大幅提升。
当前活动屏幕
由于 TouchGFX 为屏幕分配内存的方式(只为最大的 View 和最大的 Presenter 分配),一次只能有一个 View 和一个 Presenter 处于活动状态。 因此,如果您的恒温器应用程序正在显示温度读数,那么配置菜单屏幕不会在任何地方运行,实际上甚至没有分配。
如果从“后端”(所有执行恒温器实际工作的非UI代码)或硬件外设接收到事件,则可以将这些事件传递给当前活动屏幕。
由于一些事件将只与应用中的特定屏幕有关,因此这提供了有效的隔离方式。 例如,只有主屏幕才能处理通知当前温度变化的已接收事件(将更新显示当前温度的文本区),而由于当前温度未显示在配置屏幕上,配置屏幕可以直接丢弃此无关事件。
TouchGFX中的Model-View-Presenter
TouchGFX 遵循 Model-View-Presenter Design Pattern描述的Model-View-Presenter 设计模式。 TouchGFX 屏幕概念通过继承自 TouchGFX 中的 View 和 Presenter 类的类与整个 Model-View-Presenter 架构联系在一起。 因此,在TouchGFX Designer中将新屏幕添加到应用时,会创建新的特定View类和Presenter类以代表该特定屏幕。
TouchGFX应用中MVP类的内容和责任如下:
Model
Model 类是一个永远存在的单类,它有两个用途:
1. 保存UI的状态信息。 在切换屏幕时,View和Presenter的分配会被清除,因此它们不能用于存储在屏幕转换时应当保留的信息。 为此,使用Model保存信息。
2. 作为面向后端系统的接口,向/从当前活动屏幕发送事件。
Model类是自动设置的,具有指向当前活动Presenter的指针。当Model中发生变化时,将变化通知当前活动Presenter。这是通过应用中ModelListener接口中的方法来完成的。
TouchGFX Designer生成的新应用将自动拥有可直接供UI使用的Model类。
View
View类(或者更具体地说,派生自TouchGFX View类的类)包含了该视图中显示的控件作为成员对象。 它还包含setupScreen和tearDownScreen函数,当进入/退出该屏幕时,会自动调用它们。 通常会在setupScreen函数中配置控件。
View还将包含指向相关Presenter的指针。 该指针通过框架自动设置。 使用此指针,您可以将 UI 事件(如按钮单击)传达给 Presenter。
Presenter
Presenter 类(同样,一个派生自 TouchGFX Presenter 类的类)负责当前活动屏幕的业务逻辑。 它将接收来自模型的“后端”事件,以及来自视图的 UI 事件,并决定采取何种行动。 例如,如果从 Model 接收到警报事件,Presenter 可能决定告诉 View 应该显示警报弹出对话框。
基于TouchGFX的智能手表项目的裸机任务采用定时器触发任务+活动屏幕事件驱动的方式,实现方式如下图所示:
采用该框架设计的程序,代码逻辑清晰,任务仅与活动屏幕下的事件有关,缺点是相关任务的设计不能是阻塞的,也存在某一时间段会有多个任务需要顺序执行。该框架下的main函数代码:
01 int main(void)
02 {
03 /* USER CODE BEGIN 1 */
04
05 /* USER CODE END 1 */
06
07 /* MCU Configuration*/
08
09 /* Reset of all peripherals, Initializes the Flash and the Systick. */
10 HAL_Init();
11
12 /* USER CODE BEGIN Init */
13
14 /* USER CODE END Init */
15
16 /* Configure the system clock */
17 SystemClock_Config();
18
19 /* Configure the System Power */
20 SystemPower_Config();
21
22 /* USER CODE BEGIN SysInit */
23
24 /* USER CODE END SysInit */
25
26 /* Initialize all configured peripherals */
27 MX_GPIO_Init();
28 MX_GPDMA1_Init();
29 MX_ICACHE_Init();
30 MX_OCTOSPI1_Init();
31 MX_SPI1_Init();
32 MX_CRC_Init();
33 MX_I2C1_Init();
34 MX_TIM16_Init();
35 MX_TIM17_Init();
36 MX_USART1_UART_Init();
37 MX_RTC_Init();
38 MX_ADC1_Init();
39 MX_UART5_Init();
40 MX_SPI2_Init();
41 MX_TouchGFX_Init();
42 /* USER CODE BEGIN 2 */
43 //ESP8266初始化,HAL库使用USART3
44 ESP8266_Init(&huart5,(uint8_t *)gRX3_BufF,115200);
45 ap3216c_init(); //环境光传感器初始化
46 ILI9341_Init(); //显示屏初始化
47 FT6336_init(); //触摸屏初始化
48 mpu_init_dmp(); //mpu6050 dmp初始化
49 System_Time_init();
50 //NOR Flash初始化
51 OSPI_W25Qxx_Init(); //初始化W25Q128
52 OSPI_W25Qxx_mmap(); //设置为内存映射模式
53 HAL_PWREx_EnableVddA();
54 HAL_PWREx_EnableVddIO2();
55
56 //清空任务列表
57 for(gTaskIndex = 0;gTaskIndex < OS_TASKLISTCNT;gTaskIndex++)
58 g_OSTsakList[gTaskIndex]=NULL;
59
60 //读取ADC值
61 if (HAL_ADC_Start_DMA(&hadc1,(uint32_t *)&gStruADC,ADC_CONVERTED_DATA_BUFFER_SIZE) != HAL_OK)
62 {Error_Handler();}
63 /* USER CODE END 2 */
64
65 /* Infinite loop */
66 /* USER CODE BEGIN WHILE */
67 HAL_TIM_Base_Start_IT(&htim16);//开启定时器16开启,系统任务调度开始
68 HAL_TIM_Base_Start_IT(&htim17);//开启定时器17开启,设备控制任务开始
69 while (1)
70 {
71 /* USER CODE END WHILE */
72
73 MX_TouchGFX_Process();
74 /* USER CODE BEGIN 3 */
75 //执行任务列表中的的任务
76 for(gTaskIndex = 0;gTaskIndex < OS_TASKLISTCNT;gTaskIndex++)
77 {
78 if((*g_OSTsakList[gTaskIndex]) != NULL)
79 {
80 g_OSTsakList[gTaskIndex]();
81 g_OSTsakList[gTaskIndex] = NULL;
82 }
83 }
84 }
85 /* USER CODE END 3 */
86 }
1.4.1定时器触发
通过两个定时器分别产生5ms与200ms的中断,周期性的判断事件的任务标志位,当该事件产生时,将相关事件的任务加入至主任务循环,事件的任务标志组如下:
01 //任务使能标值
02 typedef struct
03 {
04 uint32_t UPDATE_DIAL_EN:1; //表盘页面任务使能
05 uint32_t UPDATE_SIX_AXIS_EN:1; //六轴运动任务使能
06 uint32_t UPDATE_WIFI_RSSI_EN:1; //WiFi联网任务使能
07 uint32_t UPDATE_APP_TASK_EN:1; //APP页面任务使能
08 uint32_t UPDATE_CHIPPAGE:1; //系统信息任务使能
09 uint32_t UPDATE_HEALTHPAGE:1; //健康任务使能
10 uint32_t UPDATE_APPPAGE:1; //应用界面使能
11 uint32_t UPDATE_INFOPAGE:1; //INFO界面使能
12 uint32_t UPDATE_WIFIPAGE:1; //WiFi界面使能
13 uint32_t UPDATE_SETTINGPAGE:1; //Setting界面使能
14 uint32_t UPDATE_MOTORPAGE:1; //Motor界面使能
15 uint32_t UPDATE_BATTERYPAGE:1; //Battery界面使能
16 uint32_t UPDATE_SPORTPAGE:1; //Sport界面使能
17 uint32_t UPDATE_ALIPAYPAGE:1; //Alipay界面使能
18 uint32_t :18;
19 }gTask_MarkEN;
采用STM32的HAL库开发,在定时器回调函数中判断事件的任务标志位,并加入至主循环任务,定时器回调函数如下:
01 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
02 {
03 static uint8_t p_Time16Cnt = 0,p_Time17Cnt = 0;
04 /******************************************************************/
05 //定时器16进行5ms任务中断
06 if (htim->Instance == htim16.Instance)
07 {
08 p_Time16Cnt++;
09 //
10 if(!(p_Time16Cnt % 4)) //20ms(50Hz)进行触发刷新
11 {
12 touchgfx_signalVSynTimer(); //touchgfx用户接口
13 }
14 //五项按键读取
15 if(!(p_Time16Cnt % 20)) //100ms进行一次窗口更新
16 {
17 if(gTaskStateBit.TouchPress == 0) //更新五向键数据
18 {
19 g_OSTsakList[eUPDATE_FIVEKEY] = Update_FiveKey_Value;
20 }
21 }
22 //1000ms运行一次,系统运行指示灯
23 if(!(p_Time16Cnt % 200))
24 {
25 p_Time16Cnt = 0;
26 HAL_GPIO_TogglePin(BLUE_LED_GPIO_Port,BLUE_LED_Pin);
27 }
28 }
29 /******************************************************************/
30 //定时器17进行100ms任务中断
31 if (htim->Instance == htim17.Instance)
32 {
33 p_Time17Cnt++;
34 //周期为200ms任务
35 if(!(p_Time17Cnt % 2)) //200ms进行一次下列代码
36 {
37 if((gTaskEnMark.UPDATE_DIAL_EN || gTaskEnMark.UPDATE_SIX_AXIS_EN)
38 && (gTaskStateBit.TouchPress == 0))
39 {//欧拉角更新
40 g_OSTsakList[eUPDATE_SIX_AXIS] = Update_EulerAngle;
41 }
42 }
43 //周期为300ms任务
44 if(!(p_Time17Cnt % 3)) //300ms进行一次下列代码
45 {
46 if(gTaskEnMark.UPDATE_WIFI_RSSI_EN) //获取wifi连接的RSSI值
47 g_OSTsakList[eUPDATE_WIFI_RSSI] = ESP8266_RSSI_Task;
48 }
49 //周期为500ms任务
50 if(!(p_Time17Cnt % 5)) //500ms进行一次下列代码
51 {
52 if(gTaskEnMark.UPDATE_CHIPPAGE) //系统信息更新
53 g_OSTsakList[eUPDATE_CHIPINFO] = Update_ChipInfo;
54 }
55 //周期为1000ms任务
56 if(!(p_Time17Cnt % 10)) //1s进行一次下列代码
57 {
58 if(gTaskEnMark.UPDATE_DIAL_EN && (gTaskStateBit.TouchPress == 0))
59 { //系统时间更新
60 g_OSTsakList[eUPDATE_TIME] = Update_System_Time;
61 }
62 }
63 //周期为2000ms任务
64 if(!(p_Time17Cnt % 20)) //2s进行一次下列代码
65 {
66 if((gTaskEnMark.UPDATE_DIAL_EN || gTaskEnMark.UPDATE_INFOPAGE)
67 && (gTaskStateBit.TouchPress == 0))
68 {//更新电压、电流、温湿度、光照度
69 g_OSTsakList[eUPDATE_DIAL_INFO] = Update_DialInfo;
70 }
71 }
72 //周期为3000ms任务
73 if(!(p_Time17Cnt % 30)) //3s进行一次下列代码
74 {
75 //心率任务会阻塞主程序
76 if(gTaskEnMark.UPDATE_HEALTHPAGE) //获取健康信息
77 {
78 g_OSTsakList[eUPDATE_HEART_RATE] = Update_HeartRateInfo;
79 }
80 }
81 //周期为10000ms任务
82 if(!(p_Time17Cnt % 100)) //10s进行一次下列代码
83 {
84 p_Time17Cnt = 0;
85 }
86 }
87 /******************************************************************/
88 /* Prevent unused argument(s) compilation warning */
89 UNUSED(htim);
90 }
1.4.2 界面事件驱动
在TouchGFX中,从Model类执行与应用非UI部分(称为后端系统)的通信。后端系统是从UI接收事件和将事件输入UI的软件组件 。在TouchGFX中提供一个在每个TouchGFX嘀嗒时间调用一次的函数,在该函数中处理需要的通信。以下是将信息输入至UI的软件组件代码:
001 void Model::tick()
002 {
003 static uint8_t tickCount = 0; //减少数据上传的次数,优化界面刷新
004 tickCount++;
005
006 #if defined LINK_HARDWARE
007 //
008 if(gSwitchSpace != 0) gSwitchSpace--;
009 /*************************硬件页面切换************************/
010 //表盘页面
011 if(gTaskEnMark.UPDATE_DIAL_EN && (gTaskStateBit.TouchPress == 0)
012 && (!gSwitchSpace))
013 {
014 modelListener->DialPageChange(gFiveKeyFunc);
015 gSwitchSpace = 0x0F; //使能切换时间计数
016 }
017 //应用页面
018 if(gTaskEnMark.UPDATE_APPPAGE && (gTaskStateBit.TouchPress == 0)
019 && (!gSwitchSpace))
020 {
021 modelListener->AppPageChange(gFiveKeyFunc);
022 gSwitchSpace = 0x0F; //使能切换时间计数
023 }
024 //六轴页面
025 if(gTaskEnMark.UPDATE_SIX_AXIS_EN && (gTaskStateBit.TouchPress == 0)
026 && (!gSwitchSpace))
027 {
028 modelListener->SixAxisPageChange(gFiveKeyFunc);
029 gSwitchSpace = 0x0F; //使能切换时间计数
030 }
031 //无线页面
032 if(gTaskEnMark.UPDATE_WIFIPAGE && (gTaskStateBit.TouchPress == 0)
033 && (!gSwitchSpace))
034 {
035 modelListener->WiFiPageChange(gFiveKeyFunc);
036 gSwitchSpace = 0x0F; //使能切换时间计数
037 }
038 //设置页面
039 if(gTaskEnMark.UPDATE_SETTINGPAGE && (gTaskStateBit.TouchPress == 0)
040 && (!gSwitchSpace))
041 {
042 modelListener->SettingPageChange(gFiveKeyFunc);
043 gSwitchSpace = 0x0F; //使能切换时间计数
044 }
045 //控制页面
046 if(gTaskEnMark.UPDATE_MOTORPAGE && (gTaskStateBit.TouchPress == 0)
047 && (!gSwitchSpace))
048 {
049 modelListener->MotorPageChange(gFiveKeyFunc);
050 gSwitchSpace = 0x0F; //使能切换时间计数
051 }
052 //信息页面,温度、湿度与光强度
053 if(gTaskEnMark.UPDATE_INFOPAGE && (gTaskStateBit.TouchPress == 0)
054 && (!gSwitchSpace))
055 {
056 modelListener->InfoPageChange(gFiveKeyFunc);
057 gSwitchSpace = 0x0F; //使能切换时间计数
058 }
059 //健康页面
060 if(gTaskEnMark.UPDATE_HEALTHPAGE && (gTaskStateBit.TouchPress == 0)
061 && (!gSwitchSpace))
062 {
063 modelListener->HealthPageChange(gFiveKeyFunc);
064 gSwitchSpace = 0x0F; //使能切换时间计数
065 }
066 //Chip页面
067 if(gTaskEnMark.UPDATE_CHIPPAGE && (gTaskStateBit.TouchPress == 0)
068 && (!gSwitchSpace))
069 {
070 modelListener->ChipPageChange(gFiveKeyFunc);
071 gSwitchSpace = 0x0F; //使能切换时间计数
072 }
073 //Battery页面
074 if(gTaskEnMark.UPDATE_BATTERYPAGE && (gTaskStateBit.TouchPress == 0)
075 && (!gSwitchSpace))
076 {
077 modelListener->BatteryPageChange(gFiveKeyFunc);
078 gSwitchSpace = 0x0F; //使能切换时间计数
079 }
080 //Sport页面退出
081 if(gTaskEnMark.UPDATE_SPORTPAGE && (gTaskStateBit.TouchPress == 0)
082 && (!gSwitchSpace))
083 {
084 modelListener->SportPageExit(gFiveKeyFunc);
085 gSwitchSpace = 0x0F; //使能切换时间计数
086 }
087 //Alipay页面退出
088 if(gTaskEnMark.UPDATE_ALIPAYPAGE && (gTaskStateBit.TouchPress == 0)
089 && (!gSwitchSpace))
090 {
091 modelListener->AlipayPageChange(gFiveKeyFunc);
092 gSwitchSpace = 0x0F; //使能切换时间计数
093 }
094 //Sport页面进入
095 if(!HAL_GPIO_ReadPin(USER_KEY_GPIO_Port,USER_KEY_Pin))
096 {
097 modelListener->SportPageEnter(3);
098 }
099 /***********************更新各类信息***********************/
100 //更新时间信息,为使表盘页面滑动操作正常,在屏幕被点按时不更新数据
101 if(gTaskEnMark.UPDATE_DIAL_EN && (gSystemTime.Seconds != gLastTimeSeconds)
102 &&(gTaskStateBit.TouchPress == 0))
103 {
104 modelListener->updateDate(gSystemDate.Year,gSystemDate.Month,
104 gSystemDate.Date,gSystemDate.WeekDay);
105 modelListener->updateTime(gSystemTime.Hours, gSystemTime.Minutes,
105 gSystemTime.Seconds);
106 //更新新值
107 gLastTimeSeconds = gSystemTime.Seconds;
108 //更新温度/步数/心率
109 modelListener->updateTempStepHeart(gTemRH_Val.Tem,gSportStep,gHeartRate);
110 }
111 //健康监测信息上传
112 if(gTaskEnMark.UPDATE_HEALTHPAGE && (gTaskStateBit.TouchPress == 0))
113 {
114 //send samples and calculation result to terminal
115 if(ch_hr_valid || ch_spo2_valid)
116 {
117 modelListener->updateHeartRateInfo(n_heart_rate/4, n_sp02);
118 gHeartRate = n_heart_rate/4; //保存心率数据值表盘页面
119 }
120 //
121 if(gTaskStateBit.Max30102) //单次测量完成,清除标志
122 {
123 ch_hr_valid =0;
124 ch_spo2_valid=0;
125 gTaskStateBit.Max30102 = 0;
126 }
127 }
128 //更新欧拉角
129 if(gTaskEnMark.UPDATE_SIX_AXIS_EN) //六轴界面活动时上传
130 {
131 modelListener->updateSixAxis(pitch, roll, yaw);
132 }
133 //只有在系统主页时,才进行WiFi的RSSI数据读取
134 if((gTaskEnMark.UPDATE_WIFI_RSSI_EN))
135 {
136 modelListener->updateWiFiRSSI(gWiFiInfo, ao_wifiRSSI.gRSSI);
137 }
138 //更新温湿度信息
139 if(gTaskEnMark.UPDATE_INFOPAGE) //INFO面活动时上传
140 {
141 modelListener->updateInfo(gTemRH_Val.Hum, gTemRH_Val.Tem, gAP3216C_Val.ALS);
142 }
143 //更新芯片温度、参考电压、Vbat
144 if(gTaskEnMark.UPDATE_CHIPPAGE && (!(tickCount % 5)))
145 {
146 modelListener->updateChipInfor(gChipTempVal, gVrefVal, gVbatVal);
147 }
148 //更新电压与电流
149 if(gTaskEnMark.UPDATE_BATTERYPAGE)//更新电压与电流
150 {
151 modelListener->updateBatteryPageInfo(gCurrentVal, gVoltageVal);
152 }
153 #else //Designer仿真
154 timeval timenow;
155 gettimeofday(&timenow, NULL);
156 //仿真更新时间
157 modelListener->updateTime((timenow.tv_sec / 60 / 60) % 24,
158 (timenow.tv_sec / 60) % 60,
159 timenow.tv_sec % 60);
160 #endif
161 }
以下是用于活动屏幕的产生的任务标志组的设置:
001 //风扇操作
002 void Model::turnFanStatus(bool enable)
003 {
004 #if defined LINK_HARDWARE
005 if(enable == true)//风扇状态的设置
006 HAL_GPIO_WritePin(EXT_FAN_GPIO_Port,EXT_FAN_Pin,GPIO_PIN_SET);
007 else
008 HAL_GPIO_WritePin(EXT_FAN_GPIO_Port,EXT_FAN_Pin,GPIO_PIN_RESET);
009 #endif
010 }
011 //振动电机操作
012 void Model::setMotorStatus(bool enable)
013 {
014 #if defined LINK_HARDWARE
015 if(enable == true)//振动电机状态的设置
016 HAL_GPIO_WritePin(EXT_MOTOR_GPIO_Port,EXT_MOTOR_Pin,GPIO_PIN_SET);
017 else
018 HAL_GPIO_WritePin(EXT_MOTOR_GPIO_Port,EXT_MOTOR_Pin,GPIO_PIN_RESET);
019 #endif
020 }
021 //排水操作
022 void Model::drainWaterStatus(bool enable)
023 {
024 #if defined LINK_HARDWARE
025 if(enable == true)//振动电机设置
026 HAL_GPIO_WritePin(EXT_MOTOR_GPIO_Port,EXT_MOTOR_Pin,GPIO_PIN_SET);
027 else
028 HAL_GPIO_WritePin(EXT_MOTOR_GPIO_Port,EXT_MOTOR_Pin,GPIO_PIN_RESET);
029 #endif
030 }
031 //蜂鸣器操作
032 void Model::setBuzzerStatus(bool enable)
033 {
034 #if defined LINK_HARDWARE
035 if(enable == true)//蜂鸣器状态的设置
036 HAL_GPIO_WritePin(RUN_BEEP_GPIO_Port,RUN_BEEP_Pin,GPIO_PIN_SET);
037 else
038 HAL_GPIO_WritePin(RUN_BEEP_GPIO_Port,RUN_BEEP_Pin,GPIO_PIN_RESET);
039 #endif
040 }
041 /*********************gTaskEnMark赋值*************************/
042 //DialView的任务的状态
043 void Model::DialPageViewTask(bool enable)
044 {
045 #if defined LINK_HARDWARE
046 if(enable == true)
047 gTaskEnMark.UPDATE_DIAL_EN = 1; //任务使能
048 else
049 gTaskEnMark.UPDATE_DIAL_EN = 0; //任务清除
050 #endif
051 }
052 //ApplicationPageView的任务的状态
053 void Model::ApplicationPageViewTask(bool enable)
054 {
055 #if defined LINK_HARDWARE
056 if(enable == true)
057 gTaskEnMark.UPDATE_APPPAGE = 1; //任务使能
058 else
059 gTaskEnMark.UPDATE_APPPAGE = 0; //任务清除
060 #endif
061 }
062 //SixAxisPageView的任务的状态
063 void Model::SixAxisPageViewTask(bool enable)
064 {
065 #if defined LINK_HARDWARE
066 if(enable == true)
067 gTaskEnMark.UPDATE_SIX_AXIS_EN = 1; //任务使能
068 else
069 gTaskEnMark.UPDATE_SIX_AXIS_EN = 0; //任务清除
070 #endif
071 }
072 //InfoPageView任务使能
073 void Model::InfoPageViewTask(bool newStatus)
074 {
075 #if defined LINK_HARDWARE
076 if(newStatus == true)
077 gTaskEnMark.UPDATE_INFOPAGE = 1; //任务使能
078 else
079 gTaskEnMark.UPDATE_INFOPAGE = 0; //任务清除
080 #endif
081 }
082 //ChipPageViewTask的任务的状态
083 void Model::ChipPageViewTask(bool enable)
084 {
085 #if defined LINK_HARDWARE
086 if(enable == true)
087 gTaskEnMark.UPDATE_CHIPPAGE = 1; //任务使能
088 else
089 gTaskEnMark.UPDATE_CHIPPAGE = 0; //任务清除
090 #endif
091 }
092 //设置健康监测任务
093 void Model::HealthPageViewTask(bool newStatus)
094 {
095 #if defined LINK_HARDWARE
096 if(newStatus == true)
097 gTaskEnMark.UPDATE_HEALTHPAGE = 1; //任务使能
098 else
099 gTaskEnMark.UPDATE_HEALTHPAGE = 0; //任务清除
100 #endif
101 }
102 //WiFi连接的任务状态
103 void Model::WiFiLinkTask(bool enable)
104 {
105 #if defined LINK_HARDWARE
106 if(enable == true)
107 gTaskEnMark.UPDATE_WIFI_RSSI_EN = 1; //任务使能
108 else
109 gTaskEnMark.UPDATE_WIFI_RSSI_EN = 0; //任务清除
110 #endif
111 }
112 //WiFi界面的任务状态
113 void Model::WiFiPageViewTask(bool enable)
114 {
115 #if defined LINK_HARDWARE
116 if(enable == true)
117 gTaskEnMark.UPDATE_WIFIPAGE = 1; //任务使能
118 else
119 gTaskEnMark.UPDATE_WIFIPAGE = 0; //任务清除
120 #endif
121 }
122 //Setting界面的任务状态
123 void Model::SettingPageViewTask(bool enable)
124 {
125 #if defined LINK_HARDWARE
126 if(enable == true)
127 gTaskEnMark.UPDATE_SETTINGPAGE = 1; //任务使能
128 else
129 gTaskEnMark.UPDATE_SETTINGPAGE = 0; //任务清除
130 #endif
131 }
132 //Motor界面的任务状态
133 void Model::MotorPageViewTask(bool enable)
134 {
135 #if defined LINK_HARDWARE
136 if(enable == true)
137 gTaskEnMark.UPDATE_MOTORPAGE = 1; //任务使能
138 else
139 gTaskEnMark.UPDATE_MOTORPAGE = 0; //任务清除
140 #endif
141 }
142 //Battery界面的任务状态
143 void Model::BatteryPageViewTask(bool enable)
144 {
145 #if defined LINK_HARDWARE
146 if(enable == true)
147 gTaskEnMark.UPDATE_BATTERYPAGE = 1; //任务使能
148 else
149 gTaskEnMark.UPDATE_BATTERYPAGE = 0; //任务清除
150 #endif
151 }
152 //运动界面的任务状态
153 void Model::SportPageViewTask(bool enable)
154 {
155 #if defined LINK_HARDWARE
156 if(enable == true)
157 gTaskEnMark.UPDATE_SPORTPAGE = 1; //任务使能
158 else
159 gTaskEnMark.UPDATE_SPORTPAGE = 0; //任务清除
160 #endif
161 }
162 //Aliplay界面任务状态
163 void Model::AlipayPageViewTask(bool enable)
164 {
165 #if defined LINK_HARDWARE
166 if(enable == true)
167 gTaskEnMark.UPDATE_ALIPAYPAGE = 1; //任务使能
168 else
169 gTaskEnMark.UPDATE_ALIPAYPAGE = 0; //任务清除
170 #endif
171 }