MCU:STM32F407VET6
GUI Guider:1.8.1 CLion:2024.2.2
MinGW:11.4 arm-gnu-eabi-:13.3rel
C:11 C++:17
目录
一、简介
以前版本我记得是可以在GUI Guider里面的代码编辑器了敲代码后是可以编译运行。现在使用了1.8.0和1.8.1,发现编写代码保存后,再点击生成代码编译运行,结果编写的代码都消失了。如果只是在其中一个文件中敲完代码,切换到另一个文件再切回来时,那么敲的代码也会消失不见。查了许多资料也没有找到答案或者类似的问题。
后来想到,可能是自带的代码编辑器没法保存代码,于是使用记事本打开工程的源码,进行了修改并保存。此番操作后发现可以正常编译出我修改后的代码,也不知是我电脑的原因,还是说这是某种跨越两个版本的bug?
基于上面问题有了本篇,本篇主要内容:
- 第二章:在CLion里编写代码,代替GUI Guider的编辑器,同时可以在CLion里运行模拟器
- 第三章:裁剪模板工程,渐渐自定义工程。评价:内容冗余
- 第四章:把模拟器工程由Makefile构建转为CMake,速度提升数倍
- 第五章:依托GUI Guider生成的工程,放飞自我转为C++工程。评价:内容跳跃,可跳过不看
- 第六章:借助GUI Guider生成自定义字库和图片C库,并尽量减小ROM占用
本篇也可简单认为:依托GUI Guider和CLion快速搭建自己的LVGL模拟器
二、缺失的代码编辑器
为了解决这个缺失写功能的代码编辑器,可以使用CLion、VScode搭建个visual Studio、MinGW(GUI Guider用的是这个)或者别的什么工具链,亦或者使用CodeBlock等IDE。
这里有两种办法,一种是CLion在此只起到了静态检查、代码补全、编译检查等代码编辑器的功能。另一种就是把它当做模拟器。后者这个虽然编译很慢,但是配置起来非常简单。
Makefile
前者不用多说,与使用VS Code操作相同。接下来介绍后者,首先需要找到你创建的GUI Guider工程目录
然后右键lv-simulator使用CLion打开
打开一般会自动弹出Readme文档,你可以先浏览一遍看看大体内容,它在里面也介绍了如何配置模拟器。此时注意到右上角会自动加载一个Makefile目标,后面会介绍Makefile工程转为CMakeLists。
运行all目标时,上来就是大惊喜,虽然可以直接无缝加载Makefile目标,但是不能正常使用。这是因为CLion默认使用的终端是Powershell,执行不了cp、rm等命令,这时候就需要进行一些“检测”了
首先,我们在Makefile里偷偷加点料,总而言之就是随便在一个目标下添加一些显示语句。注意,前面不是空格而是制表符“Tab”!!
echo $(SHELL) echo $(PATH)
然后我们回到GUI Guider里面,按住“Ctrl+Q”执行编译运行,就可以在日志上看到打印的信息了(去掉“@”,可以显示出该条命令)
这说明两点,一是终端为sh.exe,而是找到了关键路径。按照一般理性,这些工具应该放在GUI Guider的安装目录下,那么我们到安装目录下去搜索sh.exe,结果没找到,这可不妙
那么直接搜索*.exe,我们可以轻松地看到黑色的图标,这不是cp、echo命令嘛,那我们找到随便一个黑色程序所在的目录
然后就可以看到这些东西了,原来Makefile里面执行的不是命令,而是程序!难怪我尝试where命令时也会报错
现在还记得前面用“echo $(PATH)”打印的信息吗
没错,就是这个路径保存有cp等工具的位置,这也就是我们前面直接编译时会出现错误的原因。现在有两种办法,一种是在make命令中加上PATH环境变量,另一种是直接在Makefile最上面加上这个变量。这个路径需要自己到刚才GUI Guider的日志中复制一下
PATH = E:\Tools\Develop\Embedded\NXP\GUI-Guider-1.8.1-GA\environment\mingw\bin\;E:\Tools\Develop\Embedded\NXP\GUI-Guider-1.8.1-GA\environment\cmake\bin\;E:\Tools\Develop\Embedded\NXP\GUI-Guider-1.8.1-GA\environment\tools\unixTools
此时,大功告成!(运行目标时,点击图标就行,不是运行)
【all】
【run】
如果你编译失败并且提示缺少gui_guider.h等文件,这是因为在GUI Guider里没有点击生成代码
为了减少操作,让我们只需按一下运行就行了。我们需要编辑这个配置
然后选择可执行文件,点击这个框最右边的三个点一样的按钮
此时弹出来的目录可能不是工程路径,我们点击一下箭头指向的按钮就进入了工程目录
然后选择build/bin/simulator.exe,之后一直点击确定就行了
这个时候,就可以按一下运行键就能构建、编译、执行程序了
总结:
先在随便一个Makefile目标下加上echo $(PATH),然后在GUI Guider使用“Ctrl+Q”编译运行(需要先生成代码),得到PATH的信息后,复制下来
之后,在Makefile的开头加上PATH = 【你复制的路径们】
最后就可以在CLion上构建了
三、从模板到本地工程
直接使用模板生成的工程不能随便进行增删,不然会编译不通过,这里面的底层逻辑是“模板工程里定义了一些事件函数在event_init.c/h,并且在custom.c/h搞了一些接口变量什么的”。这样的话,如果你直接删除控件,或者改变控件的名字。
现在假如我要做个语音存储与回放的项目,那么就可以这个模板上的基础上创建工程。此时GUI Guider用于设计界面、添加事件、规划内存,而CLion则用于编写、修改、裁剪
依照“语音存储与回放“”这个项目,我们假如使用的是片上的2MB Flash作为音频存储介质,那么这个项目就不需要做太大,只需一个播放界面就行了,不需要音频列表等
1,清事件
创建工程后,依据前面所言,我们可以去除一些不要的功能,比如歌曲清单
我们观察左侧组件,可以发现,有两个容器。上面这个是播放界面,下面这个是歌单界
面,我们需要把下面这个歌单给去除掉
如果我们直接去除这个容器组件,那么毫无疑问会报错,此时我们需要进行一些处理。不过在处理之前我们先开个设置
进入系统设置后,把这俩都勾选上,然后再【生成代码】->【运行部署C】
此时我们可以观察到所用的内存为24KB左右,帧率为33
接下来我们就要正式清理一些组件了,先选择不要的那个容器下有什么组件,然后点击下方的【事件添加】。
此时我们可以看到这个按钮下有不少事件,我们把这些事件全部清空
接着把播放容器内的这个按钮的事件也清空了,因为它可以引出第二个界面。
接着,依次类推,把这个容器下包含的所有组件的事件全部清空,然后点击生成代码并编译运行。这个时候,与播放界面无关的事件就清空了
2,清组件
接下来,我们先直接把事件清空后的这个组件给删除了,然后“Ctrl+G”“Ctrl+Q”一套连招。显然会报错。
接着我们找到这个工程的目录,右上那个红方框里是这个工程的根目录,下面这个红方框是我们的目标
右键文件夹,点击更多,然后使用CLion打开。初进入,点击确定
打开Makefile
随便找到构建目标,然后在下面加入命令。注意左边不是空格,而是Tab(制表符)
echo $(PATH)
回到GUI Guider,来一套快捷键小连招,然后在日志找定位我们的echo以及后面的信息。把这个路径给复制下来
再回到CLion,把添加的echo语句给删除,然后在Makefile的最上方添加上PATH = xxx,xxx就是你刚才复制的信息
PATH = E:\Tools\Develop\Embedded\NXP\GUI-Guider-1.8.1-GA\environment\mingw\bin;E:\Tools\Develop\Embedded\NXP\GUI-Guider-1.8.1-GA\environment\cmake\bin;E:\Tools\Develop\Embedded\NXP\GUI-Guider-1.8.1-GA\environment\tools\unixTools
开始构建目标all
然后就得到了与GUI Guider相同的报错提示
3,清残留
接下来就是我们把残留的代码给清理了。为何是残留呢?因为custom.c/h是提供接口供event_init.c/h使用的,由于我们把事件清理完了,那么event_init.c/h里面没有残留的代码。但custom.c/h并不受影响,里面会有以前的接口调用我们已经删除过的组件,这样就会产生冲突。
点击报错提示,我们进入报错的地方,这些正是已经消失的组件。把这些包含组件的函数理智地全部删除,你就会得到一份健康的代码
此时我们点击构建,默默等上片刻。然后喜提一份报错,看来是刚才删除得不够理智
根据提示,随便在一个地方把这个函数敷衍地定义一下然后再构建,这些成功了
回到GUI Guider,继续一套快捷键小连招。Look!你看到了什么?成功构建好了程序并且内存直接降了一半
不过旁边的百分比在单片机里就有些极端了,我们得让它合理起来。
如果调得太低比如12KB,那么程序就会白屏崩溃,很符合常理
调内存之后再重新编译部署,这样的占比就很科学
4,精简主体
接下来把目标投向主体部分——播放界面, 清除掉我们所不需要的图片组件,记得观察它身上有没有事件。
经过一顿咵咵乱减,变成了如今的模样
经过一套连招下来,变成了这样
你已经是一个成熟的工程师了,可以在CLion自行解决问题了。我们回到GUI Guider,可以看到一切简单了不少。
只不过有一点点瑕疵,那就是我明明已经把另外两个图片专辑组件给删除了为什么还能够切换专辑呢?这到底是为什么呢?
5,走近逻辑
欢迎来到走近逻辑阶段,我们进入右边这个“按钮”的事件里。然后点击Edit code,就可以进入代码编辑区(因为有bug,改了也没有)
我们在CLion中找到这个函数,右击这个函数,点击查找用法
可以看到四种不同的用法
这里我们不讲lv_demo_music_album_next的四种写法,先看一下函数定义。
点击左边【函数】下的函数,右边会出现它的定义
我们可以稍稍看出,它通过形参next来左右id,这个id显然指的是专辑图片。
至于下方,为何需要playing,先搁置一下
我们继续看函数的下一个用法,可以看出我们的函数被album_gesture_event_cb这个函数所调用,从函数名的后缀event_cb可以知道它是事件回调(event callback),且与album有关。
如果我们删除它,那么显然点击左右“按钮”后,专辑图片就不会切换了
我们继续往下看,这真是一个奇怪又冷酷的函数,仅仅是瞥了lv_demo_music_album_next一眼就走了。
呐,动态用法就是声明
接下来我们得好好深究一下那个冷酷的函数了,把它的名称复制下来
然后连续按两下“左Shift”,把名称粘贴上去。不愧是高冷的函数,只有四种“写法”,其中一种还是python
我们点击图中的最后一个,可以看见的是,原本高冷的函数竟被一群花枝招展的动画anim围住,真是太无,无可厚非了
如果你安装了通义灵码插件,那么可以让它解释一下代码
好了,这就是查找函数用法的一点小技巧,我们需要看点更宏观的东西
6,漫游处理
我们前往events_init.c/h(看来前面拼写少了一个s),这里是无法触及的领域,因为它在generated目录下,会在【生成代码】时重新被覆盖。
下面events_init_screen函数里就是我们的所有组件的事件了,一共三个播放、前进一曲、退后一曲。
我们跟进播放事件所定义的处理函数,意为:当按钮状态处于Released时,如果按钮被选中,那么就播放,否则停止。人话:点击按钮后就播放,再点击一次就停止
从这个角度上我们看看播放函数是如何做到,播放会动画一直动,进度条一直走。
此时,注意到里面有个定时器,那么这个定时器何时来的呢?我们查找它的用法
初见时,它定下了千毫一见的承诺
可很快它又不得不陷漫长的深梦中
直到一声呼唤将其从沉梦中唤醒
一声的呼唤是一生的陪伴,直到生命走到了最后的时刻
窥见它的一生后,我们回到与它初见的那一刻。当时它委托给了timer_cb一件任务,细数流沙,但显然这不足以切换歌曲
回到初始化函数里,我们可以看到两个自定义函数
至此,拼上了最后两块拼图,切换专辑、频谱动画
7,定事件
想要定义事件,那么在此之前先设计我们的UI,下面是一个简陋的设想。我希望实现录音、放音、快进、慢放、显示频谱的功能,同时左上角显示CPU温度。在相关驱动、模块都配置好的情况下,我们可以正式工作了
【快进】【慢放】
首先我们先把这个图片替换成图片按钮(删掉重新添加一个)
添加之前,记得把位置、大小信息复制一下
为了让我们的快进有所反馈,我们在按钮按下时随便添加一个图片,然后一套快捷键小连招
接下来我们要做的就是添加事件,一快进按钮为例,我希望按下它时,它能加速播放,并且在左上角显示“快进x1.75”
我们可以先右键组件,点入事件添加,也可以在下方中找到它
我们希望是点击的时候触发
触发后,可以执行我们相应的逻辑,所以需要执行我们编写的代码
由于事件里默认包含custom头文件,所以无需添加,此时我们假装已经实现了lv_playspeed_acc();
但这样的逻辑显然不够,我们再添加点逻辑:当处于播放状态时,添加倍速才有效。此时需要一个变量存储播放状态,原模板给的是playing,我们就先用它好了。
可如果我们只是在外面套一个逻辑处理的话,那看着就很不好,我们希望调用一个简洁、功能完善的接口,意味着我们可以把这个逻辑判断封装进lv_playspeed_acc();函数里
所以,我们先不在这里加上逻辑处理,然后点击右上角的保存。然后我们一套连招,正常情况下会出现下面函数未定义的报错,我们先不管
回顾前面的设计,我们还需要按下快进/慢放按钮时屏幕右上角会显示出相应提示,此时拉出一个文本框,字体我一般选用最后一种,因为这个字体库里有汉字和各种奇怪的符号
接下来思考我们的处理逻辑,我们按一下按钮,调用事件处理:显示提示信息,开启快进。即我们需要两个接口,前者可起名为lv_playspeed_print().
此时你会想到,显示只有一个函数,而快进播放是两个相似的功能。那么我们就可以把设置播放速度这个概念提取出来,通过传参来实现不同的信息。我们需要一个枚举体PlaySpeed来表示我们的播放状态,后续需要什么添加什么。保存后,进行一套连招(为了生成代码)
回到CLion,我们可以看到events_init.c里有我们定义的函数,上方也有我们定义的枚举体
不过像变量声明定义声明的啦全部放在custom文件夹里更好,events_init.c/h只管调用接口就行了,所以我们把刚才的枚举体搬到custom.h里,并且在GUI Guider里把那个枚举体声明删掉并重新生成代码(这个不演示了)
准备实现这两个接口
在实现过程中,根据具体情况又更改了一些,比如舍弃了判断播放状态的逻辑
但是这样就会又一个问题,那就是设置播放速度的地方用注释代替很容易就忘。为此,我们可以使用一条预编译命令“#warning”,编译时就会出现下面警告,但不影响你的程序,相当于某种意义上的“日志”
但这样还不行,我们还需要进行调试,那么可以使用printf功能。但防止我们的printf泛滥,可以进行封装,用宏来控制
/********宏定义********/ #define DEBUG_PRINTF // 使用Debug打印 #ifdef DEBUG_PRINTF #define print(x, ...) printf(x, ##__VA_ARGS__) #else #define print(x, ...) do { } while(0) #endif
接下来开始调试代码,但需要先在CLion里设置好执行文件,并在对应的地方使用调试打印的函数。然后就可以在CLion里运行,因为在GUI Guider里你看不到终端
此时如果你运行程序,发现终端打印的码是乱码,那是因为终端(Powershell)默认使用的是GBK 18030(除非你设置计算机的全局语言),所以我们需要把文件编码换成GB18030。后面开发如果遇到编码问题,只要把编码格式转换一下就行了,一般就UTF-8、GB18030、GBK这些。
But!!现在还是不要用中文的好,因为如果你转换为GBK18030,GUI Guider给你生成代码时又搞成了UTF-8,最后文件的编码就紊乱了。所以尽量不用中文,不调编码类型
调试了才发现,事件里忘记判断按钮的状态了
if(lv_obj_has_state(guider_ui.screen_imgbtn_acc, LV_STATE_CHECKED)) { lv_playspeed_print(LV_PLAYSPEED_ACC);// 显示播放速度信息 lv_playspeed_set(LV_PLAYSPEED_ACC);// 快进 } else { lv_playspeed_print(LV_PLAYSPEED_NORMAL); lv_playspeed_set(LV_PLAYSPEED_NORMAL); }
可以在GUI Guider里把显示速度的那个文本框隐藏起来,然后生成代码。
接下来开始在CLion中测试
至此,漫长的【快进】事件终于结束了,【慢放】则同理,不过现在先不必做
8,制动画
播放时,我们注意到会有一个“频谱”的动画,但是做这个项目时,我们想要在专辑的地方显示真正的频谱。那么我们先看看这个动画的实现流程
先看第一个动画执行回调,有些平平无奇,只是在触发重绘后spectrum变来变去
我们再看结束回调,更加平凡,只是在动画结束后切换下一个专辑图片。我们也因此得知专辑切换并不是由定时器引起的,而仅仅是动画完成后执行的,显然这是为了做demo给客户看。
我们需要把它停掉
难道真相就这么浅层吗?包不会的,记得前面我们提到过,这里有两个隐藏时间,其中一个就是我们所需的,真正绘制谱线的操作。
同时要注意其上方有个spectrum_area对象,这是自定义的一个组件,而不是GUI Guider生成的。
找到它后,我们就可以看到一个百来行的代码,相当复杂。但我们只需要知道它是怎么绘制谱线就行了(其实可以直接上网查怎么绘制)。
lv_draw_polygon绘制的,通义如是说
让通义随便生成一份频谱代码,效果是这样的。它使用的是lv_draw_line函数,用于绘制简单谱线,而刚才找到的lv_draw_polygon则用于绘制更复杂的谱线。
至此,我们知道可以怎么生成谱线了,那么就不必要专辑图片了
相信你已经是一名成熟的UI设计师了,可以自行删去不必要的组件、函数(要善用【查找用法】)。现在我们也可以把Player这个容器删除,把里面的组件全部归为screen管
9,掌全域
在前面主体UI布置好的情况下,下面我们可以完全在CLion中设计我们的UI了,可以不尽情放飞自我了。
此时只剩下频谱制作了,在与AI的斗智斗勇中,我们可以得到这样的频谱
下面是用于测试的代码,有些乱糟糟的,后面我们会逐步完善它
custom.h
/* * Copyright 2023 NXP * NXP Proprietary. This software is owned or controlled by NXP and may only be used strictly in * accordance with the applicable license terms. By expressly accepting such terms or by downloading, installing, * activating and/or otherwise using the software, you are agreeing that you have read, and that you agree to * comply with and are bound by, such license terms. If you do not agree to be bound by the applicable license * terms, then you may not retain, install, activate or otherwise use the software. */ #ifndef __CUSTOM_H_ #define __CUSTOM_H_ #ifdef __cplusplus extern "C" { #endif #include "gui_guider.h" /********宏定义********/ #define DEBUG_PRINTF // 使用Debug打印 #ifdef DEBUG_PRINTF #define print(x, ...) printf(x, ##__VA_ARGS__) #else #define print(x, ...) do { } while(0) #endif /********变量声明********/ enum PlaySpeed { LV_PLAYSPEED_NORMAL,// 正常播放 LV_PLAYSPEED_ACC,// 快进1.75 LV_PLAYSPEED_SLOW,//慢放0.25 }; /*********接口********/ void custom_init(lv_ui *ui); void lv_demo_music_resume(void); void lv_demo_music_pause(void); void lv_playspeed_print(enum PlaySpeed speed);// 播放速度信息打印 void lv_playspeed_set(enum PlaySpeed speed);// 播放速度设置 #ifdef __cplusplus } #endif #endif /* EVENT_CB_H_ */
custom.c
/* * Copyright 2023 NXP * NXP Proprietary. This software is owned or controlled by NXP and may only be used strictly in * accordance with the applicable license terms. By expressly accepting such terms or by downloading, installing, * activating and/or otherwise using the software, you are agreeing that you have read, and that you agree to * comply with and are bound by, such license terms. If you do not agree to be bound by the applicable license * terms, then you may not retain, install, activate or otherwise use the software. */ /********************* * INCLUDES *********************/ #include <stdio.h> #include "lvgl.h" #include "custom.h" #include "spectrum_1.h" /********************* * DEFINES *********************/ /********************** * TYPEDEFS **********************/ /********************** * STATIC PROTOTYPES **********************/ /********************** * STATIC VARIABLES **********************/ /** * Create a demo application */ #define ACTIVE_TRACK_CNT 3 #define BAR_CNT 20 #define BAND_CNT 4 #define BAR_PER_BAND_CNT (BAR_CNT / BAND_CNT) #define DEG_STEP (180/BAR_CNT) #define BAR_COLOR1_STOP 80 #define BAR_COLOR2_STOP 100 #define BAR_COLOR3_STOP (2 * LV_HOR_RES / 3) #define BAR_COLOR1 lv_color_hex(0xe9dbfc) #define BAR_COLOR2 lv_color_hex(0x6f8af6) #define BAR_COLOR3 lv_color_hex(0xffffff) static uint32_t time; static uint32_t track_id; static lv_timer_t *sec_counter_timer; static bool playing; static uint32_t spectrum_i = 0; static uint32_t spectrum_len; static lv_obj_t *spectrum_area; static uint32_t spectrum_i_pause = 0; static const uint16_t rnd_array[30] = {994, 285, 553, 11, 792, 707, 966, 641, 852, 827, 44, 352, 146, 581, 490, 80, 729, 58, 695, 940, 724, 561, 124, 653, 27, 292, 557, 506, 382, 199}; static void album_gesture_event_cb(lv_event_t *e); static void spectrum_draw_event_cb(lv_event_t *e); static void spectrum_update_timer_cb(lv_timer_t *timer); static void update_spectrum(); static const char *title_list[] = { "Waiting for true love", "Need a Better Future", "Vibrations", }; static const char *artist_list[] = { "The John Smith Band", "My True Name", "Robotics", }; static const uint32_t time_list[] = { 1 * 60 + 14, 2 * 60 + 26, 1 * 60 + 54, }; static void timer_cb(lv_timer_t *t) { time++; lv_label_set_text_fmt(guider_ui.screen_label_slider_time, "%d:%02d", time / 60, time % 60); lv_slider_set_value(guider_ui.screen_slider_1, time, LV_ANIM_ON); } #define FFT_NUM 256 #define SPECTRUM_START_X 50 #define SPECTRUM_START_Y 50 #define SPECTRUM_WIDTH 380 #define SPECTRUM_HEIGHT 170 #define SPECTRUM_NUM 64 #define BAR_WIDTH 2 // 每个频谱条的宽度 static float bar_spacing = (float)((SPECTRUM_WIDTH - SPECTRUM_NUM * BAR_WIDTH) / (SPECTRUM_NUM - 1.0)); // 频谱条之间的间距 lv_timer_t *spectrum_update_timer = NULL; void custom_init(lv_ui *ui) { LV_IMG_DECLARE(_icn_slider_alpha_37x37); sec_counter_timer = lv_timer_create(timer_cb, 1000, NULL); lv_timer_pause(sec_counter_timer); lv_obj_set_style_bg_img_src(guider_ui.screen_slider_1, &_icn_slider_alpha_37x37, LV_PART_KNOB); spectrum_update_timer = lv_timer_create(spectrum_update_timer_cb, 100, ui); lv_timer_pause(spectrum_update_timer); spectrum_area = lv_obj_create(guider_ui.screen); lv_obj_remove_style_all(spectrum_area); lv_obj_set_pos(spectrum_area, SPECTRUM_START_X, SPECTRUM_START_Y); lv_obj_set_size(spectrum_area, SPECTRUM_WIDTH, SPECTRUM_HEIGHT); lv_obj_move_background(spectrum_area); lv_obj_add_event_cb(guider_ui.screen, spectrum_draw_event_cb, LV_EVENT_ALL, NULL); } static void spectrum_draw_event_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_DRAW_POST) { lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e); // 定义渐变色 lv_grad_dsc_t grad; memset(&grad, 0, sizeof(lv_grad_dsc_t)); // 清零结构体 grad.stops_count = 2; // 设置渐变色的数量 grad.stops[0].color = lv_color_make(239, 221, 121); // 结束颜色(流萤_金色) grad.stops[0].frac = 0; // 起始位置 grad.stops[1].color = lv_color_make(133, 238, 223); // 起始颜色(流萤_浅绿色) grad.stops[1].frac = 255; // 结束位置 grad.dir = LV_GRAD_DIR_VER; // 垂直渐变 const uint32_t start_x = SPECTRUM_START_X; const uint32_t start_y = SPECTRUM_START_Y + SPECTRUM_HEIGHT; const uint32_t bar_width_plus_spacing = BAR_WIDTH + bar_spacing; lv_draw_line_dsc_t line_dsc; lv_draw_line_dsc_init(&line_dsc); line_dsc.width = BAR_WIDTH; line_dsc.opa = LV_OPA_COVER; for (uint32_t i = 0; i < SPECTRUM_NUM; i++) { // 计算当前频谱条的位置 uint32_t x1 = start_x + i * bar_width_plus_spacing; uint32_t y1 = start_y - spectrum[(i << 2)] * SPECTRUM_HEIGHT / 255; // 从底部开始,但考虑最大高度 // 计算当前频谱条的高度比例,用于渐变色 uint8_t height_ratio = (start_y - y1) * 255 / SPECTRUM_HEIGHT; // 根据高度比例调整渐变色 lv_color_t color = lv_color_mix(grad.stops[0].color, grad.stops[1].color, height_ratio); line_dsc.color = color; // 定义两个点 lv_point_t point1 = {x1, y1}; lv_point_t point2 = {x1, start_y}; // 绘制竖直线 lv_draw_line(draw_ctx, &line_dsc, &point1, &point2); } } } void start_spectrum_update() { lv_timer_resume(spectrum_update_timer); } void stop_spectrum_update() { lv_timer_pause(spectrum_update_timer); } static void spectrum_update_timer_cb(lv_timer_t *timer) { update_spectrum(); // 更新FFT频谱数据 lv_obj_invalidate(spectrum_area); // 使频谱区域无效,触发重绘 } void lv_demo_music_pause() { stop_spectrum_update(); lv_timer_pause(sec_counter_timer); } void lv_demo_music_resume() { start_spectrum_update(); lv_timer_resume(sec_counter_timer); lv_slider_set_range(guider_ui.screen_slider_1, 0, time_list[track_id]); } /*******更新FFT*******/ static void update_spectrum() { uint8_t temp = spectrum[0]; spectrum[FFT_NUM - 1] = temp; for (uint32_t i = 0; i < FFT_NUM - 1; ++i) { temp = spectrum[i + 1]; spectrum[i] = temp; } } /** * @brief 播放速度显示 * @param speed 播放速度 */ void lv_playspeed_print(enum PlaySpeed speed) { switch (speed) { case LV_PLAYSPEED_ACC: lv_obj_clear_flag(guider_ui.screen_label_speed, LV_OBJ_FLAG_HIDDEN); lv_label_set_text(guider_ui.screen_label_speed, "快进×1.75"); break; case LV_PLAYSPEED_SLOW: lv_obj_clear_flag(guider_ui.screen_label_speed, LV_OBJ_FLAG_HIDDEN); lv_label_set_text(guider_ui.screen_label_speed, "慢放0.25"); break; case LV_PLAYSPEED_NORMAL: default: lv_obj_add_flag(guider_ui.screen_label_speed, LV_OBJ_FLAG_HIDDEN); break; } } /** * @brief 设置播放速度 * @param speed 播放速度 */ void lv_playspeed_set(enum PlaySpeed speed) { switch (speed) { case LV_PLAYSPEED_ACC: #warning "设置速度" print("acc_1.75\r\n"); break; case LV_PLAYSPEED_SLOW: print("slow_0.25\r\n"); break; case LV_PLAYSPEED_NORMAL: print("speed_normal\r\n"); default: break; } }
四、CMakeLists再尝试
本来想手动转CMakeLists,但实在太麻烦。后来想到了bear这个工具,于是把工程复制了一份到Ubuntu,想通过bear把Makefile转为CMakeLists,然后再移到本机上。没想到在Ubuntu里使用make竟然会那么快,但是!链接库时出现了各种问题,有一种不兼容的美
搞了两天,钻了不少牛角尖,后来把里面的每个Makefile都看了一遍,测试各种变量,终于试出来了。首先是CMakeLists得要重写,前面测试中遇到的那个CMakeLists并不是用于模拟器的,这个倒显而易见。
前面Makefile之所以配置得如此之快,那是因为GUI Guider就是用它来编译模拟器程序的,但它并没有提供一个完整的CMakeLists(工程目录的那个不算),这个是需要自己手写的。
cmake_minimum_required(VERSION 3.10) project(Simulator) # 设置编译器 set(CMAKE_C_COMPILER gcc) # 设置编译选项以生成 32 位代码,但没有用 #set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32") #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32") #set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -m32") # 定义路径 set(LIBFILE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../lib/native) set(LVGL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) set(SIMULATOR_DIR ${CMAKE_CURRENT_SOURCE_DIR}) set(PROJECT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) # 生成的目标文件目录 set(BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR}/build) set(OBJ_DIR ${BUILD_DIR}/object) set(GEN_OBJ_DIR ${OBJ_DIR}/generated) set(BIN_DIR ${BUILD_DIR}/bin) # 编译选项 add_definitions(-O2 -g0 -DLV_CONF_INCLUDE_SIMPLE=1) # 链接选项 set(LIBRARIES decoder openh264 rlottie pthread stdc++ jansson curl m SDL2) link_directories(${LIBFILE_PATH} ${SIMULATOR_DIR}/SDL2/i686-w64-mingw32/lib ${GEN_OBJ_DIR}) # 添加包含路径 include_directories( ${LVGL_DIR} ${SIMULATOR_DIR} # ${SIMULATOR_DIR}/rlottie ${LVGL_DIR}/lvgl # 根据generated.mk ../generated ../generated/guider_fonts ../generated/guider_customer_fonts ../generated/images # 根据lvgl.mk ../lvgl/src/core ../lvgl/src/draw ../lvgl/src/extra ../lvgl/src/font ../lvgl/src/hal ../lvgl/src/misc ../lvgl/src/widgets # 根据lv_drivers.mk ../lvgl-simulator/lv_drivers ../lvgl-simulator/lv_drivers/display ../lvgl-simulator/lv_drivers/indev # 根据custom.mk ../custom # 补充 SDL2/i686-w64-mingw32/include ) # 源文件 file(GLOB_RECURSE SRCS "${LVGL_DIR}/lvgl/*.c" "${SIMULATOR_DIR}/lv_drivers/*.c" "${SIMULATOR_DIR}/lv_drivers/*.c" "${PROJECT_SOURCE_DIR}/custom/*.c" "${PROJECT_SOURCE_DIR}/generated/*.c" ) list(APPEND SRCS ${SIMULATOR_DIR}/main.c) # 创建可执行文件 add_executable(simulator ${SRCS}) target_link_libraries(simulator ${LIBRARIES}) set_target_properties(simulator PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_DIR}) # 定义静态库 add_library(libgenerated STATIC ${SRCS}) target_link_libraries(simulator libgenerated) # 设置静态库的输出目录 set_target_properties(libgenerated PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${GEN_OBJ_DIR}) # 如果是Windows,创建DLL add_library(simulator_dll SHARED ${SRCS}) target_link_libraries(simulator_dll ${LIBRARIES}) set_target_properties(simulator_dll PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_DIR} LINK_FLAGS "--entry=_DllMainCRTStartup@12") # 复制所需的库文件 add_custom_target(copy_libs COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/SDL2/lib/SDL2.dll ${BIN_DIR}/SDL2.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/libgcc_s_dw2-1.dll ${BIN_DIR}/libgcc_s_dw2-1.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/pthreadGC-3.dll ${BIN_DIR}/pthreadGC-3.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/libjansson-4.dll ${BIN_DIR}/libjansson-4.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/libcurl.dll ${BIN_DIR}/libcurl.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/SDL2/lib/libopenh264.dll ${BIN_DIR}/libopenh264.dll ) add_dependencies(simulator copy_libs) # 运行模拟器 add_custom_target(run COMMAND ${CMAKE_COMMAND} -E echo "Running simulator..." COMMAND ${BIN_DIR}/simulator )
前期在于找资源文件,不能多找也不能少找,得通过看Makefile以及各目录下的mk文件(其实就是子Makefile)
中期难点在于找库,这个库很容易就搞错了,也是各种报错
后期最让我想不到的就是编译工具链,中期之所以反复报错,是因为编译工具链没有选用对,下意识忽略了MinGW的版本。前面提到过,我用的是CLion自带的MinGW,但这个是64位架构的,我们需要的是32位。
埋头搞了许久后才猛然回想起Makefile前面加了一个环境变量
看到这终于回想起GUI Guider编译报错时会有mingw32-make什么的提示信息,那么显然是编译工具链配错了,那么接下来只要配置一个MinGW就行了。
此时我们有两种配置方法,一种是完全使用GUI Guider的编译工具链,另一种只要把MinGW工具集换了就行。拿我这个项目简单实测了一下(都是-O2级优化、14核并行编译),完全使用GUI Guider的编译工具链,重头编译一次大概花了14秒。而仅仅更换MinGW工具集则用时10s。解决依赖关系的话,ninja和ming32-make几乎不耗费时间(毕竟都是CMake构建系统)
作为对比,使用make构建系统的情况下,仅仅是解决依赖关系就用了14秒,不过后面编译速度挺快的,共用时33秒。不过让我疑惑的是使用GUI Guider解决依赖关系的速度与CLion的cmake差不多(按理说使用的也是Makefile,可能是我缓存忘清了?),但编译速度却很慢,共用时36秒。话说这个速度好像比我以前用时要快,难道是“相对论效应”?
此时说一下结论:
- 使用ninja构建工具更快
- CLion自带的64位编译器很快,但会因为架构而链接失败
- 不要想着-m32了,还是乖乖换工具集
【只替换工具集】工具集那一栏填的是mingw而不是mingw/bin
【替换工具集和cmake】 CMake那一栏添的是cmake.exe【指定编译器】或者你也可以在CMakeLists里面直接指定编译器的路径(可能还要指定链接器的位置)
set(CMAKE_C_COMPILER "E:/Tools/Develop/Embedded/NXP/GUI-Guider-1.8.1-GA/environment/mingw/bingcc.exe") set(CMAKE_CXX_COMPILER "E:/Tools/Develop/Embedded/NXP/GUI-Guider-1.8.1-GA/environment/mingw/bin/g++.exe")
【工具集】可自行到MinGW-W64官网下载i686什么的工具集
此时你应该已经注意到了,现在可以说它是一个lvlg模拟器,但也可以说它是个Windows程序。这有什么区别呢?前者表明它只是一个测试工具,后者则表明你也可以利用lvgl开发一个32位的Windows桌面程序
顺便给工具链换个浅显的名称
使用通义对CMakeLists进行了优化
cmake_minimum_required(VERSION 3.10) project(Simulator) # 设置编译器 set(CMAKE_C_COMPILER gcc) # 定义路径 set(LIBFILE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../lib/native) set(LVGL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) set(SIMULATOR_DIR ${CMAKE_CURRENT_SOURCE_DIR}) set(PROJECT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) # 生成的目标文件目录 set(BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR}/build) set(OBJ_DIR ${BUILD_DIR}/object) set(GEN_OBJ_DIR ${OBJ_DIR}/generated) set(BIN_DIR ${BUILD_DIR}/bin) # 编译选项 add_compile_options(-O2 -g0 -DLV_CONF_INCLUDE_SIMPLE=1) # 链接选项 set(LIBRARIES decoder openh264 rlottie pthread stdc++ jansson curl m SDL2) link_directories(${LIBFILE_PATH} ${SIMULATOR_DIR}/SDL2/i686-w64-mingw32/lib ${GEN_OBJ_DIR}) # 源文件 file(GLOB_RECURSE LVGL_SRCS "${LVGL_DIR}/lvgl/*.c") file(GLOB_RECURSE DRIVER_SRCS "${SIMULATOR_DIR}/lv_drivers/*.c") file(GLOB_RECURSE CUSTOM_SRCS "${PROJECT_SOURCE_DIR}/custom/*.c") file(GLOB_RECURSE GENERATED_SRCS "${PROJECT_SOURCE_DIR}/generated/*.c") # 定义静态库 add_library(libgenerated STATIC ${LVGL_SRCS} ${DRIVER_SRCS} ${CUSTOM_SRCS} ${GENERATED_SRCS} ) target_include_directories(libgenerated PUBLIC ${LVGL_DIR} ${SIMULATOR_DIR} ${LVGL_DIR}/lvgl ${SIMULATOR_DIR}/lv_drivers ${PROJECT_SOURCE_DIR}/custom ${PROJECT_SOURCE_DIR}/generated ${SIMULATOR_DIR}/SDL2/i686-w64-mingw32/include ) set_target_properties(libgenerated PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${GEN_OBJ_DIR}) # 创建可执行文件 add_executable(simulator ${SIMULATOR_DIR}/main.c) target_link_libraries(simulator PRIVATE libgenerated ${LIBRARIES}) target_include_directories(simulator PRIVATE ${SIMULATOR_DIR} ) set_target_properties(simulator PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_DIR}) # 创建DLL add_library(simulator_dll SHARED ${SIMULATOR_DIR}/main.c) target_link_libraries(simulator_dll PRIVATE libgenerated ${LIBRARIES}) target_include_directories(simulator_dll PRIVATE ${SIMULATOR_DIR} ) set_target_properties(simulator_dll PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_DIR} LINK_FLAGS "--entry=_DllMainCRTStartup@12" ) # 复制所需的库文件 add_custom_target(copy_libs COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/SDL2/lib/SDL2.dll ${BIN_DIR}/SDL2.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/libgcc_s_dw2-1.dll ${BIN_DIR}/libgcc_s_dw2-1.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/pthreadGC-3.dll ${BIN_DIR}/pthreadGC-3.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/libjansson-4.dll ${BIN_DIR}/libjansson-4.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/multi_thread/libcurl.dll ${BIN_DIR}/libcurl.dll COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SIMULATOR_DIR}/SDL2/lib/libopenh264.dll ${BIN_DIR}/libopenh264.dll ) add_dependencies(simulator copy_libs) # 运行模拟器 add_custom_target(run COMMAND ${CMAKE_COMMAND} -E echo "Running simulator..." COMMAND ${BIN_DIR}/simulator )
|
|——>示例工程
|
ichliebedich-DaCapo/STM32F407VET6: stm32f407vet6 (github.com)
五、UI的自我救赎
这一章主要内容是“放飞自我”,会省去许多操作,重点描述的是流程。对于初学者来说极易出错,建议使用版本控制(VCS),比如git来备份自己的工程。这一章内容跳跃性比较大,可以先不看,不影响使用。跳转
前面罗里吧嗦半天其实就是为现在做准备的,经过前面一系列折腾,现在我们可以把这整个模拟器搬到自己的项目了。这样可以一边开发各种驱动,也可以上模拟器设计UI界面。
现在我们在主工程项目(单片机项目)里创建一个文件夹GUI,然后把下面模拟器里面的项目移动过去。之后要把lv-simiulator目录下的
也许可以用ExternalProject模块来控制两个工程,可自行摸索,这里还是使用两个工程。下面这幅图是主项目,可以看到我们的模拟器项目已经被引入了
接下来,我们找到项目路径,然后把其下的GUI/lvgl-simulator用CLion打开,这样就会有两个窗口
打开后是这样的界面,我们选择好工具链,构建选项可以输入-jN,N就是你的最大核数
接下来我们以这个窗口为准,开始UI界面的设计,注意到Base目录了吗?这个是用于提供GUI接口供主项目使用,说人话就是模拟器与主项目都通过调用这个文件里的函数来实现UI,这样便于主项目与模拟器的同步
在CMakeLists里面要添加目录和相应文件,根据具体情况自行添加
1,项目同步
进入到main.c里面,我们可以看到这样的调用,我们要做的就是在GUI.cpp里面整一些接口,供模拟器的main.c和主项目的main.c调用
在这里,界面开发用C++是为了方便开发。那么由于main.c要调用GUI.cpp的接口,只能main.c -> main.cpp。改了之后记得把CMakeLists中的也改掉,然后cmake一下
接下来简简单单创建一个静态类,把setup_ui()和custom_init()先封装进去,这里使用的是C++17返回值类型后置的特性
回到main中,我们把原先的函数替换掉
回到我们主项目的main.cpp中,把这个初始化函数也添加进去
至此,两个工程就同步起来了。需要说明的是,模拟器工程使用的是裸机单线程,主工程可裸机单线程也可多线程(FreeRTOS)。也就是说即便主工程使用多线程,GUI也只是作为其中的单线程,这样既方便两个工程同步开发,也方便多线程下GUI的调试处理
2,屏幕精简
我们回到屏幕初始化函数,我们会发现这里面有许多冗余的初始化步骤
比如这里,基本上不写就默认为0,所以这里相当于重复初始化了。之所以说“基本上”,那自然是有特例,初次删除的时候没经验容易误删,建议删完后运行一下。
在删除过程中,你会发现有许多代码经常会重复,那么就有了C++的用武之地了,我们把这个文件改为cpp,同时在CMakeLists里面把"xxx/*.c"改为"xxx/*.*"
下面我们把set_scr_screen先重构一下名称,然后为它创建一个头文件。
【图片组件】
这里的图片组件的初始化代码有些冗余了,我们先尝试提炼一个类,需要什么函数我们就加上什么函数。这里使用到了一个成员变量,不过只有一个32位局部变量,对于单片机来说完全能承受得住(51不好说)
不过细细想来,我好想不需要图片,于是又把图片删除了
【图钮组件 】
如法炮制,写一个图片按钮类,使用时还是挺轻松的,因为基本上按一下换行,通义灵码就知道写什么代码了,然后按Tab
效果不变,但代码却精简了很多
【基本组件】
通过前面两个类,你会发现它们都会有设置位置大小、创建组件等函数,我们完全可以创建一个基类 。后面组件以此类推
3,事件绑定
接下来我们就来处理事件,不过在此之前我们先把所有文件变为C++类型,同时把头文件里的extern"C"去除。改为C++后,会有类型错误,这是因为C++对类型检查很严,使用static_cast<>强制转换即可
有些地方我们需要用到C++17的特性,所以需要把C++标准设为17
set(CMAKE_CXX_STANDARD 17)
为了减少参数传递还有乱七八糟的ui和guider_ui全局变量什么的麻烦,下面先创建了一个GUI_Base类
class GUI_Base { protected: constexpr static inline lv_ui * gui=&guider_ui;// 为了使用内联变量,需要C++17 };
然后让事件类也继承它,这样之后就清爽了许多,代价是一只4字节大小的静态指针
但这样之后,事件处理还是有些冗余了,我们使用一个通用函数和lambda表达式来优化它
就这样,我们先不做一些很过分的优化
4,重整框架
接下来我们需要考虑一下整个架构的设计,直接套用原先的框架会很乱。那么我们可以在草稿上从顶层开始设计
【顶层】
既然我们把工程集成了一个LVGL模拟器,那也意味着可以在这个工程里开发多个GUI。事实上,我发的示例工程就是在一个工程里做多个简单的小项目,如语音存储与回放、双音频信号发生器等,是通过App_Conf.h里添加宏来控制项目的启闭的。
这里也是如此,设想很简单:
- App_Conf里的使用宏来控制Application和UI里的项目文件。前者实现的是功能逻辑,比如各种驱动模块、事件的初始化与实现逻辑;后者实现的是界面设计,与之前的setup_screen()一致。
- GUI目录里(后面更名为Base),有GUI.hpp/cpp,里面有GUI_Base和GUI两个类,前者目前只有一个功能封装lv_ui_t*gui用于代替guider_ui。后者目前只有一个接口init供主项目和模拟器调用。而这个init接口里实际上是对屏幕初始化(ui_init)与事件初始化(events_init)进行了一次封装
- Component里是之前我们对lvgl的一部分组件初始化的封装,提供接口供ui_init()调用(这个ui_init其实就是前面的screen_init)
- Events里就是的前面事件绑定,不过此处不再在里面实现事件逻辑了。因为我们前面提到,这个模拟器里会放许多项目的GUI,那么就需要将事件初始化与实现隔离开。
【目录】
既然理顺了我们要做的事,接下来我们就理理目录
- Base里放到文件只有GUI.hpp/cpp(图中没改过来),供主项目和模拟器调用接口
- Component里是各种常见组件的封装实现
- Events提供了事件绑定和通用事件处理的接口
- UI存放了各种项目的界面设计
- lv_simulator里又创建了一个resource目录,用于把前面分散在GUI目录的文件收录起来,其余不变(为了以后方便修改)
【实现】
依据前面,我们可以设计出这样的一个大概实现流程。各项目下的ui.cpp负责实现ui_init和events_init函数。这里要注意的是,此处的events_init是不包含任何驱动模块接口的,方便模拟器调试。events_init会主动设计一些接口,用于测试,这些接口会采用stm32里的__weak定义的方式等(gcc扩展)。
在ui.hpp里你还可以看到这里定义了许多结构体,这其实是为了封装结构体,原本在lv_ui_t一个结构体里把所有屏幕、组件都堆叠在一起就很杂乱。所以下面会封装屏幕结构体,屏幕结构体里是各种组件(当然也可以继续封装),这样调用起来顺序就是
gui->screen->compent,从gui到屏幕,再到组件。
分类更加清晰,也不会带来格外的开销
5,开始整理
移动文件、文件夹比较简单,直接拖过去,会让你重构。但在这里并不好,因为它会直接把其他文件包含的头文件直接改了,这很不好
我们可以先直接移动lib其他无用的删除,custom和generated可以直接重构命名为UI和Component(原谅我把它拼错拼成了GUI)
重构完成之后记得把CMakeLists也改一下
至于lvgl直接拖到resource,然后取消勾选“搜索引用”后重构(相当于直接移动),然后改一下包含路径(前面的图resource又拼错了(x_x))
接下来,准备我们第一个项目UI目录(此处为语音存储与回放,可自拟),在里面创建ui.hpp/cpp。然后各种整理,这一步非常麻烦,但却是必须要学会的技能
可能是没有添加ui头文件就把它链接成静态库,导致CLion无法把UI目录识别为工程目录,不过问题不大
此外还需要把event_init()和ui_init()实现了,即scr_screen、events等分离出来
6,重构终宴
【设计重构】
回到gui_guider.hpp中,我们把这个lv_ui划分一下,可以根据自行喜好决定是否继续划分。
(示范用的,不保真)
按照前面的文件组织,我们可以得到这样一张草图,箭头指向的方向,表示被其包含头文件,虚线表示间接实现(草图仅供参考)
现在,我们重新理一下依赖关系:
- GUI.hpp/cpp供模拟器供主项目和模拟器调用接口初始化界面和事件,所以GUI.hpp不能被其他目录包含。基于此,之前的GUI_Base类不能放在GUI.hpp/cpp里,我们需要为它找个头文件,用于被其他文件包含
- component.hpp/cpp与events.hpp/cpp只提供接口,不直接参与编写界面和事件
- ui.cpp实现component.hpp/cpp与events.hpp/cpp声明的初始化函数,供GUI.hpp/cpp调用。同时可能依赖其他自定义文件(相当于以前的custom.h/c)
- 由于界面初始化需要的组件只在ui.cpp里初始化和调用,而ui.cpp里不需要在ui.hpp声明任何接口。所以lv_ui结构体可以直接在ui.hpp里声明,供其他文件调用这个变量gui(主要是主项目触发事件时需要组件),这是此刻它唯一的作用。其实ui.hpp完全可以换个名字避免歧义
(草图仅供参考)
思路理清,不过此时有个小问题,前面我们提到过,有个工程里是有多个项目的,UI界面分处GUI/UI下的子目录,子目录中都有ui.hpp/cpp。这意味着编译时我们需要把路径手动指定,使得CMake能找寻到正确路径
【ui重构】
现在我们开始操作,现在ui.hpp里定义我们的声明结构体
然后在GUI_Base.hpp里实现我们的静态类,此时它只有一个作用,那就是把ui封装起来,避免全局变量到处乱跑。下面那个gui指针可加可不加,看个人使用习惯,如果你喜欢"->"胜于".",那么可以加上
然后让GUI、Screen和Events类继承GUI_Base
取消ui_init和events_init的设定,这是因为已经把ui结构体封装起来了,普通函数访问不到。那么接下来把Screen::init()和Events::init的实现从Component和Events里转移到ui.cpp
此时尝试把所有guider_ui命名的组件替换,记得保存进度,替换过程中会有很多错误,因为custom.hpp/cpp和guider.hpp/cpp我们并没有改动
如下,只要能显示界面即可
从guider.hpp里把代码转移到ui.hpp中,并删除不要的一些资源
接下来我们观察到custom.hpp里只是添加了自定义初始化界面和事件,我们可以把custom.cpp里的内容全部移动到ui.cpp
然后再慢慢整理 ,把它拆解为界面和事件
如今摆在我们面前的难题就一种,之前实现事件逻辑时遗留下来的guider_ui的使用
【组件重构】
那么接下来我们开始分析这个问题,由于gui被封装了,如此一来我们想用gui就必须创建一个类来继承GUI_Base。虽然看起来麻烦,但实际上要求我们实现事件逻辑时要把它抽象出来,利于后续代码组织。
下面这是个简单的示例
接下来按照功能把事件处理整理为:
【定时器】
从下面两个类的定义中可以看出它们有很多相同点,后续完全可自行把它们抽象出一个定时器组件类放在Component.hpp/cpp里。至于前面的界面初始化过程,其实还可以再简化一些或者也能提抽象出一些类
【事件】
事件我们可以把它们拆解为事件回调(Event类)和若干个事件实现类,可根据功能模块将其划分为频谱事件类和播放事件类。总之,怎么清晰怎么来
如果实现的事件逻辑比较复杂,可以为它们创建几个文件,这样更清晰。由于我们使用的是静态类,提高代码复用和可读性、利于维护方便改需求的同时,开销方面并没有增加多少,甚至还会减少
# 自定义命令,构建后执行 ## 获取当前日期和时间 string(TIMESTAMP CURRENT_DATE_TIME "%Y-%m-%d %H:%M:%S") set(SIZE_OUTPUT_FILE "size_history.txt") # 显示exe文件大小 add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND echo. COMMAND echo ${CURRENT_DATE_TIME} >> ${SIZE_OUTPUT_FILE} COMMAND ${CMAKE_SIZE_UTIL} ${BIN_DIR}/${PROJECT_NAME}.exe COMMAND ${CMAKE_SIZE_UTIL} ${BIN_DIR}/${PROJECT_NAME}.exe >> ${SIZE_OUTPUT_FILE} COMMAND echo "--------------------------------------------------------------------------------------------" >> ${SIZE_OUTPUT_FILE} COMMENT "Displaying size of the executable" )
鉴于我这个工程暂时用不到复杂的动画、交互等功能,就删除一些文件
【注意事项】
- 后续可以自行根据自己的需求设计框架,使用C++的同时最好不要用虚继承、模板等泛型编程,标准库什么的一些费内存的如容器,能不用就不用,否则会极大增加二进制文件体积,甚至会增加运行开销。
- 善用C++的一些特性,当然特性不能泛滥,需要什么用什么,C++17还是有不少语法糖的。不熟悉就用浏览器或AI去搜
- 创建类时,建议创建静态类,一些简单的函数能内联就内联。如果是普通类,那么动态分配能不用就不用
- 注意代码隔离,接口是接口,私有成员变量是为了不像全局变量那样四处暴露。驱动编写尽量都用C,方便与别人互相移植代码,实际上也很难有用到C++的地方
- 使用重构重命名时,容易把其他文件“误构”了,为解决这一点,可以把一些不需要改动的文件如lvgl库等设置为只读模式
- 回到主项目时,一定不要包含标准库了,不然ROM会直接增加近三百KB
7,事件巡回
前面提到事件是在ui.cpp里开发完成的,如果要与驱动模块进行通信,那么就要利用到lvgl的一些通信机制,比如lv_event_get_user_data(e);。如果是要触发事件,可以使用lv_event_send(my_button, LV_EVENT_CLICKED,nullptr);来触发自定义事件
如果觉得麻烦,也可以在模拟器项目中编写测试接口,当测试完毕后讲整个事件的初始化,配置迁移到主项目了。这并非难事,因为前面代码隔离得比较好,编写事件初始化时,只用一些头文件,并不需要改动其他文件。迁移过程中,可以考虑预编译宏的一些机制【没有录入lvgl输入设备】
如果你使用的输入设备没有添加进LVGL里,比如没有触摸屏只有外置矩形键盘等情况。我们可以试想一下这种情景,事件的触发只能通过唯一的按键响应,而按键响应在应用级文件,那么可以把事件的初始化和实现全部迁移至应用层文件。
【向LVGL添加了输入设备】
可以在ui.cpp里完成事件的初始化,调用各驱动模块接口。
总之,选择适合自己的方式
六、资源制作
把LVGL模拟器嵌入到工程里之后,实际上用到GUI Guider就没有以前那么多了,这一点有点像STM32CubeMX,初期依赖,后期成为纯粹的工具。
此时如果要设计界面,可以先用GUI Guider的组件直接堆叠出一个草图,把位置、大小的信息记录下来即可,实际初始化可由CLion里完成,为便于开发,也可模仿声明式UI的创建
1,资源优化
此刻GUI Guider还能发挥出不少作用,比如字体、图片等转换,所谓“资源优化”其实就是减小一些二进制文件的大小,毕竟开发的是单片机项目,资源紧缺
【字体】
我们可以注意到现在工程还有不少字体文件,但voiceStorageAndPlay完全用不到这些
它们是通过这些宏来加载的,其实就是extern
我们能用到的就这三种,其中有两个字体大小相同,那么我们可以把他它们换成同一种字体类型,把arial换成Source类型。为何?因为后者能显示的前者显示不了,比如说中文,可以说后者兼容前者,
把字体改成相同的就可以节省掉一小部分ROM了
(时间不太准,因为使用的是CMakeLists自带的时间,需要手动刷新CMake才更新)
但只有这样还远远不够,因为做单片机UI时,字体一般不需要那么多,那么可以“定制字体”
我们可以先创建一个font.txt文件用于记录我们需要哪些字体,方便一些更改。12号字体里面有个空格(不是中文的冒号:)
打开GUI Guider,注意不要在我们前面辛苦整理的工程里打开,否则生成代码时会把你的文件删干净。这里随便创建一个工程或者利用以前的旧工程就可以了
接下来我们带着这些字体到GUI里生成字库文件 ,先生成12号,字体类型选择最后一个Source什么的
生成成功以后,文件会保存在这个工程目录下的,我们直接把这个文件复制到我们的工程目录里
回到CLion刷新一下CMake,把文件名称复制下来,并替换掉不用的字体,再到ui.cpp界面初始化里改掉
不影响使用的同时还可以节省不少内存
另一个字体同理,两顿操作下来之后,还是减少了不少空间。改了字库以后,记得与界面初始化使用的字符串相一致,不然会显示一堆“口”
【图片】
图片的优化有限,但我们还是可以操作操作的。如果你的背景颜色与需要的图片背景颜色一致,那么可以使用jpg文件,如果你的图片的背景是透明的话,那么可以使用png文件。
一般来说使用的是png文件,因为好抠图,且透明的地方是真的透明(jpg没有透明)
图片资源我们可以自己上网搜集,可以用模板工程里提供的图片资源。当你创建一个模板工程时,GUI Guider会自动把相关资源文件放到import目录下
我们可以挑选我们需要用到的,然后放到我们工程或者别的什么地方(目录一定不要中文)。此时你会发现这些图片的文件还不小,这说明我们可以使用做一点点小操作
可以使用WPS(需要VIP)或者别的一些工具来对图片进行压缩,应该也有一些在线压缩的网站。
其中我们能经常用到的操作有修改尺寸、压缩、抠图、消除、去水印等 。虽然下面这张图的大小本来就有限,但我们还是能挤一挤的,把旁边多余的空白裁剪掉(这次先放过它)
这里我们使用压缩体积,一般来说默认即可,如果有需求可以进一步降低品质或期望输出大小
也可以批量压缩
然后我们导出图片的C库,为了方便,我们可以把方才的图片文件复制到GUI Guider工程里的import/image目录下(之前的文件全删了)
generated下的images里,把C库文件删了
在GUI Guider里,我们把图片全选,然后点击转换图片,输出格式选择C,色彩默认是16位,即ARGB8565,由于我们使用了png,里面有透明度(Alpha),那么选择真色彩Alpha
生成后会自动弹出文件夹
只不过这种批量生产的方法,往往质量不佳,我们最好手动单独生成。
在如下界面,把对应组件上选择对应图片即可,但要确保组件的大小与你需要的一致。为了清晰度,一般最好让组件大小和图片大小一致或成比例。成比例时需要注意一下,假设你的图片是200*200,即便你的组件只有20*20,但生成的C库文件依然是按照图片标准生成的
大小相同的普通塞进一个组件即可,然后点击生成代码
我们回到工程目录,这些带有尺寸后缀名的就是刚才这种方式生成的。如你所见,这些C库的后缀并非真是图片的尺寸,而是刚才组件的尺寸,只不过C库大小嘛就相反喽
实验效果如下,有那么一点点糊,看来是刚才品质选得有些低了,应该选最清晰的
不过大小是降了一些
这便是最后一幕了
在主工程里,经过了我们上面的优化存储由原先的92%降到现在42%,当然,之所以降得这么厉害,有很大一部分原因是把顶部和底部的图片组件删了至此,我们告一段落