Linux平台下STM32移植RTThread+LVGL
写在前面,由于Leo酱是先写的Markdown在复制过来的,格式可能有问题,请担待,那个闪烁的确实闪烁了,PDF我会发到QQ群。
开发平台与工具链
VSCode
GCC 交叉编译工具链 + Make +OpenOCD
STM32CubeMX
所使用的库和第三方组件
STM32 HAL
RT-Thread Nano
LVGL
1. 准备工作
1.1 准备第三方源代码
准备RTThread源码与LVGL源码
官方下载地址:
RT-Thread Nano
LVGL Git仓库
下载后进行解压,Leo酱的目录组织方法是在工作区目录(家目录下新建的Workspace目录,用于存放当前各种工作区)下新建一个ThirdParty目录,用以存放各类第三方组件,然后将RT-Thread和LVGL源码解压到或克隆到该目录下。
由此,便准备好了第三方源码。
1.2 生成STM32项目结构
到此,我们开始对于项目的创建,需要注意的是,无论使用那种集成开发环境或者交叉工具链,一个项目最核心的永远是编译相关的文件,其他的对于项目组织的方式由着具体开发环境而异,因此,在本教程中,Leo酱会注意强调这点,无论是使用HAL库还是标准库,用STM32CubeMX配置项目初始源码还是手动添加相关的硬件库甚至使用寄存器开发都无所谓。核心在于思路。因此这一步,工作的实质是生成一个能够使用的支撑STM32基本运行的源码集合,并且用一种方式去组织这些文件。
首先打开,STM32CubeMX,新建一个项目,选择对应型号MCU,进入到项目配置中。
Leo酱使用的是STM32F103RET6。
首先我们配置好调试为串口调试(因为使用的是SWD),并且配置HAL库的时钟源为其他的定时器,至于哪一个定时器无所谓,看你工程中留着哪一个。反正别用系统滴答定时器(SysTick),因为RT-Thread需要使用它来提供系统时基。

其他的方式就是配置屏幕相关GPIO,由于Leo酱使用的板子和LCD显示屏决定需要使用PB4-PB9(推挽输出)作为LCD的硬件接口。另外,为了演示RT-Thread,我们使用一个GPIO来进行LED的闪烁。在此,Leo酱配置了PA8作为LED的驱动端口(推挽输出)。
最后使能外部高速时钟HSE输入,设置HCLK主频为72MHz(这一步看你自己)
最后,配置项目信息,首先需要在Code Generator选项卡中配置对于每一个外设单独生成.c和.h文件。然后配置项目生成信息如下图。

需要注意的点就是,把堆栈的大小设置的大一些,因为我们需要配置LVGL使用c标准库进行动态内存管理。另外,工具链配置为Makefile,设置好项目名就直接点击生成项目吧。
在生成项目之后,我们得到了一个目录,其中的目录结构如下

其中Core目录是项目程序的核心逻辑业务实现代码所在的目录,Drivers目录是驱动所在目录,HAL库和CMSIS支持被放在这里。如果您不满这种目录组织方式,那就自己改就好了,毕竟怎么组织是人定的。Leo酱觉得这样组织其实也不错,就直接用了。在Core目录中存放自己的业务逻辑,把自己写的板级支持包(BSP)也正好可以放在Drivers目录中。
至于剩下的文件,.ioc为拓展名的文件还有一个.mxproject文件是CubeMX工程文件,如果你以后不打算再用CubeMX 进行其他的配置,删了也无妨,Leo酱有精神洁癖并且也确实不打算后期继续使用CubeMX进行配置(毕竟只是一个教学用的工程),所以就删了。
Makefile是make使用的自动编译脚本,我们直接用。
startup_stm32f103xe.s是汇编启动文件,是项目源码。
最后的.ld文件是链接脚本,如果要配置堆栈大小可以从这里配置。其他的可能在汇编启动文件中配置,但是使用Makefile进行编译就是在这里配置。工程编译要用。
1.3 编写VSCode配置文件
用VSCode打开该目录。
按下Ctrl+Shift+P,选择打开工作区设置

配置Json文件如下:
用于启动代码提示和语法检查
选择运行菜单中的添加配置,添加启动配置(点击配置任务后随便选,反正得删了,我们只需要一个launch.json文件罢了),文件如下:
上述代码中,我们指定了一个Build任务用于调试前编译,我们需要写这个任务。
所以我们按Ctrl+Shift+P,选择配置任务。(点击配置任务后随便选,反正得删了,我们只需要一个tasks.json文件罢了)
任务代码如下:
至此,我们就配置完了VSCode的调试编译环境。我们完全可以按F5编译运行一下,会生成一个build目录,里面就是各种目标文件。
2.移植RT-Thread
2.1 源码添加与裁剪
首先我们得想一下,我们的RT-Thread源码怎么放。这你们自己决定,我只按照我的方式来讲。Leo酱的话,会在项目顶级目录新建一个目录叫做System顾名思义就是用来存放系统相关的文件,CubeMX生成第三方组件时会统一放到一个叫做Middlewares(中间件)的目录中,我的话,就直接放在系统中,毕竟硬件和业务之间也就是隔着一个系统层和驱动层咯。
所以新建System目录并将RT-Thread源码放到这里面,并改名为rt-thread(默认就是这个名,是就不用改了)。之后的目录结构如下

下一步就是裁剪,把没用的给删咯。首先rt-thread目录下不包括目录的所有文件都删咯。然后把docs目录也删咯。把bsp子目录下的所有目录删咯。把board.c 和 rtconfig.h留着。
libcpu目录下一个arm一个risc-v。这是针对架构的底层支持包。Leo酱使用的芯片架构是ARM-Cortex M3所以其他的全删咯。剩下的目录结构如下

那几个.S汇编文件是针对不同编译器的。我们用的是gcc所以把剩下俩也删了。然后把那个拓展名.S中的大写S改成小写s,因为Makefile文件去编译的时候只认小写的.s(这不是Make的锅,是CubeMX自动生成的Makefile就是这么写的,你也可以该Makefile,但是需要改的汇编文件就这一个,不如直接改它)。
改完就是这样。

2.2 修改Makefile添加RT-Thread文件
在移植RT-Thread之前,我们需要先添加RT-Thread项目文件。我们使用Makefile进行构建,所以我们需要修改Makefile文件。
在C_SOURCES中添加.c文件 (components我们不用所以就不用添加)
在ASM_SOURCES中添加汇编文件
System/rt-thread/libcpu/arm/cortex-m3/context_gcc.s
在C_INCLUDES中添加头文件包含路径
2.3 修改源码,进行移植
对于使用HAL库而言,一般的启动顺序是在汇编完成,C语言运行环境初始化后直接跳转到main函数进行执行,如下图

而在RT-Thread中main函数用以提供main线程的入口函数,并且按照原本的启动次序,并无法执行任何RT-Thread相关源码,谈何对于系统环境进行初始化呢?
RT-Thread提供了另外一个入口函数entry进行替代。我们可以来看一下这个函数。
在components.c中有对这个函数的定义。如下
可见,对于不同的编译器,RT-Thread提供了不同的入口函数,对于GNUC,我们只需要将入口函数替换或者添加到gcc参数中,一般替换吧,方便。
修改完启动入口,我们需要移植中断服务函数,系统需要使用中断服务函数进行系统调度等系统级过程。需要移植的中断处理函数有三个分别是
HardFault_Handler 硬中断服务函数,RT-Thread 接管了这个用以打印系统信息。
PendSV_Handler PendSV中断服务函数,RT-Thread接管用以产生上下文切换
SysTick_Handler 系统滴答定时器中断服务函数,RT-Thread用来产生系统时基
这三个中断处理函数在 Core/Src/stm32f1xx_it.c中有定义,需要注释掉或者在定义前添加__weak标识,删了也行,在此使用添加__weak标识,毕竟共存而不对抗,融合而不破坏是一种美学。至于前面entry替换main,因为main也会被用到,而且改回去其实也很方便。当然如果需要针对搭载系统和不搭载系统进行同步开发,建议添加编译器选项,这样可以在make时通过指定而方便切换。
中断处理结束后,其实系统就能够跑起来了,但是现在有一个问题就是,我们来看一下这段代码

这是main函数里对于硬件的初始化,其中涉及了HAL库的初始化,系统时钟的初始化,这放在系统环境建立后,属实不合适也不应该。这类初始化难道不应该在系统环境建立前期进行的么?所以我们应该对这部分代码进行搬移。我们回过头看一下entry函数,这是替代了原本main的入口,物归原主,我们理所当然应当这么去想。
在entry函数中调用了rtthread_startup(),rtthread_startup函数代码如下
我们可以看到,对于系统初始化的工作应当放在最前方,因为代码第一段禁用了中断,这是系统初始化中一个很重要的部分,如果我们在硬件初始化时启用了中断,那么这可能会是很BadBad的事情。所以,我们将原本初始化的那部分代码移动到这里或者我们在main.c中对其封装为Hardware_Init()方法。如下:

我们接下来就只需要在RT-Thread入口函数对应位置进行调用即可(沿着这个思路你甚至可以通过重写入口函数进行初始化)

最后,我们去瞅一眼rtconfig.h,对于其中的系统组件启用与否都通过宏在其中进行定义。
在检查过程中,Leo酱发现了一个问题,请看

系统没有启用内存堆,这对于动态生成线程来说根本就是涩涩监狱级别的禁止。取消注释,启用它!
至此,系统移植OK。
2.4 测试并准备移植LVGL
我们的测试通过创建两个线程进行,一个用于LED闪烁,一个用于LVGL的服务线程。
可以单独创建.c和.h文件用来初始化各个组件,我这个教学的项目也可以,但没必要。所以直接写到了main.c中,以函数命名进行区分。
代码如下:
测试结果

3. 移植LVGL
3.1 添加并裁剪LVGL文件
在System目录中新建一个目录名为lvgl,在lvgl目录中再新建一个lvgl目录和一个drivers目录,为了防止无意义的大量删除,我们这次只复制src目录到lvgl/lvgl中,将lvgl源码目录下examples/porting目录中的所有.c和.h文件复制到drivers目录中并且删除其文件名里的template(不删也行,看你自己)。将lvgl源码目录中的lv_conf_template.h和lvgl.h复制到新建的lvgl/lvgl目录中并删除template。完成后的目录结构如下

3.2 准备显示屏驱动
这一块自己写,写完以后自己放。我只以自己的作为演示。
在Drivers目录中新建BSP目录,并在BSP目录中新建st7735目录。将驱动文件st7735.c 和 st7735.c放在里面,并且添加源文件和头文件包含目录到Makefile
而对于驱动程序,应当提供如下接口(我直接给我的.h文件内容)
其中 LCD_Init用于液晶屏初始化,LCD_SetPixel用于像素点设置,LCD_FillRegion用于区域填充。
其中LCD_SetPixel和LCD_FillRegion二者可以只实现其一,但必须得有一个,LVGL需要调用它们进行输出,实现LCD_SetPixel比较灵活,而LCD_FillRegion的写入速度会快。
3.3 修改Makefile文件添加LVGL代码
先修改C_INCLUDES添加头文件包含路径,如下
然后我们添加所有的.c文件,由于里面的.c文件是真的多,所以Leo酱不得不写一个Python脚本用于检索目录获取所有的.c文件
Python脚本如下
该Python脚本将检索所有当前目录下的文件和子目录并且将.c文件以Makefile待添加的格式写入到csourcelist.txt中。
生成后的源文件列表如下
3.4 移植LVGL
移植操作需要做的仅仅是实现驱动,配置心跳并且配置LVGL。
3.4.1 配置lv_conf.h
打开lv_conf.h
将最上面的 #if 0改为#if 1以启用该文件
修改颜色深度
#define LV_COLOR_DEPTH 16
默认为16(RGB565格式),Leo酱用的不需要改。
找到这一行
#define LV_MEM_SIZE (48U * 1024U)
修改提供给LVGL进行内存分配的空间,Leo酱设置为
#define LV_MEM_SIZE (8U * 1024U)
3.4.2 实现驱动(仅实现显示驱动)
打开 drivers目录下的lv_port_disp.c和lv_port_disp.h,将最上面的 #if 0改为#if 1以启用该文件。
修改头文件包含,毕竟我们删了template,所以,#include 中也得删咯。另外,包含我们实现的LCD驱动头文件。并且修改lvgl.h路径,如下:
找到lv_port_disp_init函数,找到

这三个是三种缓冲区的实现模板,留一个就行。我留了第一个。并且把MY_DISP_HOR_RES 改为具体的硬件尺寸定义。
将驱动程序结构体的hor_res和ver_res也改为具体硬件尺寸定义。
在disp_init函数中调用LCD初始化程序
在disp_flush函数中调用绘制函数,在此使用LCD_SetPixel
3.4.3 配置心跳
lvgl需要给定一个心跳来控制显示效果的时间。所以我们通过一个定时器来进行时间控制。可以通过RT-Thread软件定时器,也可以绑定在其他的时基上,在这里我们直接使用HAL库的时基中断为LVGL提供心跳。
至此,移植完毕。
3.5 LVGL测试
打开那个LVGL初始化线程
然后在LVGL_Entry函数中执行初始化操作,代码如下:
编译运行。
运行结果:
