SquareLine代码调用
216.3.15 SquareLine代码调用
/* main.c:接入 SquareLine 导出的 UI。 */
#include "ui.h" // 引入 SquareLine 生成的 UI 声明,使 main.c 能调用 ui_init。
lv_init(); // 初始化 LVGL 核心;必须先于显示、输入设备和 UI 的初始化。
lv_port_disp_init(); // 注册并初始化前面课程已经移植好的显示接口。
lv_port_indev_init(); // 注册并初始化前面课程已经移植好的触摸输入接口。
//lv_demo_widgets(); // 停用 LVGL 官方控件演示,避免它与本课的 SquareLine 界面同时运行。
ui_init(); // 创建 SquareLine 设计的控件、注册事件并装载 Screen1。
//lv_demo_stress(); // 压力测试演示同样保持停用,本课只运行自定义 UI。
/* ui.c:SquareLine 生成的界面初始化函数。 */
void ui_init(void) // 定义整个导出界面的初始化入口。
{ // ui_init 函数体开始。
lv_disp_t * dispp = lv_disp_get_default(); // 取得当前默认显示设备,供主题初始化使用。
lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT); // 用蓝、红主色及默认字体创建 LVGL 默认主题。
lv_disp_set_theme(dispp, theme); // 把刚创建的主题应用到默认显示设备。
ui_Screen1_screen_init(); // 创建 Screen1 及其中的按钮、圆弧、滑块等控件。
ui____initial_actions0 = lv_obj_create(NULL); // 创建 SquareLine 用于保存初始动作的内部对象。
lv_disp_load_scr(ui_Screen1); // 把 Screen1 设为当前显示的界面。
} // ui_init 函数体结束。
/* screens/ui_Screen1.c:课堂核对过的 Button111 尺寸、位置和事件注册。 */
ui_Button111 = lv_btn_create(ui_Screen1); // 在 Screen1 上创建名为 Button111 的按钮对象。
lv_obj_set_width(ui_Button111, 200); // 将按钮宽度设为 200 像素,对应设计器的 Width。
lv_obj_set_height(ui_Button111, 80); // 将按钮高度设为 80 像素,对应设计器的 Height。
lv_obj_set_x(ui_Button111, 1); // 将按钮相对对齐点的 X 偏移设为 1 像素。
lv_obj_set_y(ui_Button111, -39); // 将按钮相对对齐点的 Y 偏移设为 -39 像素。
lv_obj_set_align(ui_Button111, LV_ALIGN_CENTER); // 以父对象中心作为按钮位置的对齐基准。
lv_obj_add_event_cb(ui_Button111, ui_event_Button111, LV_EVENT_ALL, NULL); // 给按钮注册事件入口;先接收全部事件,再由入口函数筛选点击事件。
/* ui.c:SquareLine 生成的按钮事件分发函数。 */
void ui_event_Button111(lv_event_t * e) // 定义 Button111 的统一事件入口,e 指向本次 LVGL 事件。
{ // 按钮事件入口函数体开始。
lv_event_code_t event_code = lv_event_get_code(e); // 从事件对象中读取本次事件类型。
if(event_code == LV_EVENT_CLICKED) { // 只有完整点击事件才执行课程配置的 Call function 动作。
led1contrl(e); // 把事件对象传给 SquareLine 生成的业务回调函数。
} // 点击事件判断结束。
} // 按钮事件入口函数体结束。
/* ui_events.c:课堂实际补写的 LED 控制业务代码。 */
#include "ui.h" // 引入 UI 对象、事件函数及 led1contrl 的声明。
#include "main.h" // 引入 HAL GPIO 接口以及 CubeMX 生成的 LED 引脚宏。
void led1contrl(lv_event_t * e) // 定义 SquareLine 的 Call function 回调;函数名按课程源码原样保留。
{ // LED 控制回调函数体开始。
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 翻转 PE5 的输出电平,使低电平有效的板载 LED 在亮、灭之间切换。
} // LED 控制回调函数体结束;本例不需要读取参数 e。
/* main.h:CubeMX 根据 PE5 的 LED 标签生成的两个宏。 */
#define LED_Pin GPIO_PIN_5 // 把 LED_Pin 映射为端口的第 5 号引脚。
#define LED_GPIO_Port GPIOE // 把 LED_GPIO_Port 映射为 GPIOE,因此完整引脚是 PE5。
说明:配套源码中的函数名确实是
led1contrl,其中contrl少了字母o。为保持课堂源码原样,本笔记不擅自改成led1control。导出文件头记录的环境为 SquareLine Studio 1.5.0、LVGL 8.3.11。
一、软件自动生成界面
SquareLine Studio 的职责是把可视化设计转换成基于 LVGL API 的 C 文件;它不会替代 LCD 驱动、触摸驱动,也不会自动完成 Keil 工程配置。本课的完整关系是:
SquareLine 设计界面 → 导出 ui 文件夹 → 加入 Keil 编译 → main.c 调用 ui_init() → LVGL 创建界面 → 触摸事件调用业务回调 → HAL 控制 PE5 LED
1.1 界面生成示例
课堂继续使用上一课创建的 19sql_test SquareLine 工程。界面中已经放置了 Screen1、按钮、圆弧、滑块、图片、标签、键盘等控件。执行导出后,设计器属性会转换为 LVGL 调用,例如:
Screen1对应ui_Screen1,其创建过程位于screens/ui_Screen1.c的ui_Screen1_screen_init()中;- 设计器里的
Button111对应代码变量ui_Button111; - 按钮的宽 200、高 80、X 坐标 1、Y 坐标 -39,会转换成
lv_obj_set_width()、lv_obj_set_height()、lv_obj_set_x()和lv_obj_set_y(); ui.c中的ui_init()负责设置主题、调用屏幕初始化函数并装载ui_Screen1。
这里生成的是“界面应用代码”,而不是 LVGL 库源码。ui_helpers.c 等文件会调用 LVGL 已有接口,LVGL 再通过前面课程移植好的显示与触摸端口连接真实硬件。
1.2 界面导出
课程所用版本的操作为:
- 在 SquareLine Studio 顶部选择
Export → Export UI Files; - 进入
uint3code/19sql_test,新建并选择ui文件夹; - 确认导出,软件把界面代码写入该目录;
- 可用 VS Code 打开整个
ui文件夹,查看生成结果。
本课实际涉及的导出结构如下:
| 路径 | 作用 |
|---|---|
ui.c、ui.h | UI 总入口、全局对象声明和事件分发 |
screens/ui_Screen1.c | 创建 Screen1 及本屏幕的各个控件 |
ui_events.c、ui_events.h | Call function 动作对应的业务回调骨架与声明 |
ui_helpers.c、ui_helpers.h | SquareLine 生成代码内部使用的辅助函数 |
components/ui_comp_hook.c | 组件创建完成后的扩展钩子 |
fonts/、images/ | 使用自定义字体或图片时存放相应资源代码 |
导出成功只表示文件已经生成,不表示 STM32 工程已经能编译;仍需把 .c 文件加入构建、添加头文件搜索路径并调用初始化入口。
1.3 软件使用流程
本课可将接入过程归纳为六步:复制 UI 文件夹、新建 Keil 代码组、添加 .c 文件、添加头文件路径、在 main.c 包含 ui.h、在 LVGL 和端口初始化之后调用 ui_init()。
1.3.1 文件导出路径选择
首次执行导出时可以直接选择路径;若以后需要修改,则进入:
File → Project Settings → UI Files Export Path → Apply Changes
UI Files Export Path决定后续Export UI Files写到哪里;- 修改完必须执行
Apply Changes,否则新路径可能未生效; - 路径应指向 UI 输出目录,不要误选 Keil 工程根目录,以免大量生成文件混入其他源码;
- 不同 SquareLine 版本的菜单文字可能略有变化,课程版本以视频中的
Export UI Files为准。
1.3.2 代码调用
接入的关键不是“复制成功”,而是同时满足以下三类关系:
- 文件关系:所有需要编译的生成
.c文件都已加入工程; - 声明关系:编译器能通过包含路径找到
ui.h; - 时序关系:先初始化 LVGL、显示和输入端口,再执行
ui_init(),之后继续周期调用lv_timer_handler()。
1.3.2.1 复制到工程文件夹
课堂先复制第 18 个 STM32 LVGL 工程,建立第 20 个工程 20STM32_Lvgl_sql_test,再把第 19 个 SquareLine 工程导出的整个 ui 文件夹复制到:
20STM32_Lvgl_sql_test/midware/ui
第 19 个工程负责保存和导出界面,第 20 个工程负责在 STM32 上编译、烧录和运行。不要把“19”和“20”理解成同一个工程的两个目录。
以后重新导出 UI 时,课程采用“用最新导出内容替换第 20 个工程中的 midware/ui”的方法。替换前应确认自写业务代码是否已保存;尤其不要把长期维护的逻辑直接写进 ui.c 或 screens/ui_Screen1.c,因为再次导出会重建这些文件。
1.3.2.2 打开Q工程并操作
原笔记中的“Q 工程”是语音识别错误,实际是 Keil µVision 工程。操作如下:
- 打开第 20 个工程的
.uvprojx; - 在工程树中新建代码组
midware\ui; - 首次接入时加入
ui.c、ui_helpers.c、components/ui_comp_hook.c、screens/ui_Screen1.c; - 给按钮添加
Call function事件并重新导出后,再加入新出现的ui_events.c; - Keil 提示外部文件发生变化时选择重新加载,然后检查工程树中的路径是否仍指向第 20 个工程。
只复制文件而不把 .c 文件加入 Keil 代码组,会导致这些文件根本没有参加编译,最终出现未定义符号或功能缺失。
1.3.2.3 添加头文件路径
在 Keil 的 Options for Target → C/C++ → Include Paths 中加入:
../midware/ui
本课导出内容的主要头文件位于 ui 根目录,components、fonts、images、screens 当时没有需要单独搜索的头文件,因此课堂只添加这一条路径。不能机械照搬:以后若导出结构或版本发生变化,应按实际 #include 所在位置补充路径。
接着在 main.c 的用户头文件区域加入 #include "ui.h"。头文件只提供声明;真正的函数实现仍来自已加入工程并参加编译的各个 .c 文件。
1.3.2.4 调用ui_init函数
正确的初始化次序是:
lv_init()初始化 LVGL;lv_port_disp_init()注册显示设备;lv_port_indev_init()注册触摸输入设备;- 停用原来的
lv_demo_widgets(); - 调用
ui_init()创建并装载 SquareLine 界面; - 在主循环中继续运行原有的
lv_timer_handler()和短延时。
ui_init() 不能放在 lv_init() 前面,因为生成代码会立即创建 LVGL 对象并访问默认显示设备。它也不能替代主循环中的 lv_timer_handler();后者负责刷新、输入处理、动画和事件时序。
课堂编译、烧录后,LCD 正确显示设计界面,按钮有按压反馈,圆弧与滑块也能通过触摸改变,说明 UI 文件、LVGL 显示端口和输入端口已经连通。
1.4 修改并编译
1.4.1 修改并编译示例
课堂在 VS Code 中直接修改生成文件里的标签文本,把 LVGL 改成 LVGL233,随后重新编译、烧录并观察 LCD。第一次显示没有变化的原因不是 LVGL 失效,而是修改后没有保存文件;保存并重新构建后,新文本才出现在屏幕上。
这说明生成代码本质上仍是普通 C 代码,可以手工修改。但每次验证都要保证完成“保存源文件 → 编译 → 烧录 → 观察硬件”这条完整链路,不能只改编辑器里的缓冲内容。
1.4.2 通过软件改代码
手工改生成代码适合临时排查和理解属性映射;需要长期保留的界面变化,更适合回到 SquareLine 修改后重新导出:
- 布局、尺寸、颜色和文本:优先在 SquareLine 修改,使设计文件与导出代码保持一致;
- 由事件触发的业务逻辑:写在
ui_events.c的回调骨架中,或由该回调调用独立业务模块; - 底层显示、触摸和 GPIO 驱动:保留在 STM32 工程原有模块中,不要搬进生成的屏幕文件;
- 再次导出前:确认哪些文件会被生成器重建,并备份尚未迁移的手工修改。
1.4.3 应用案例
本课把触摸界面与真实硬件连接起来:单击屏幕上的 Button111,让 STM32 翻转 PE5 电平,从而改变板载红色 LED 的亮灭状态。
1.4.3.1 例题: 按键控制灯开关
在 SquareLine 中选中 Button111,切换到 EVENTS 面板并配置:
| 项目 | 课堂设置 | 含义 |
|---|---|---|
| Trigger | CLICKED | 一次完整点击产生后触发,不是刚按下就立即触发 |
| Action | CALL FUNCTION | 让生成的事件分发代码调用用户回调 |
| Function | led1contrl | 生成在 ui_events.c/.h 中的回调名称 |
设置完成后再次执行 Export UI Files。SquareLine 会生成 led1contrl(lv_event_t * e) 的函数骨架,并在 ui.c、ui_Screen1.c 中补齐事件注册与分发关系。函数名必须是合法的 C 标识符,且要与导出代码完全一致。
1.4.3.2 例题: 灯开关控制示例
这次点击的调用链不是从按钮直接跳到 HAL,而是经过三层:
触摸驱动 → LVGL 识别点击 → ui_event_Button111(e) → led1contrl(e) → HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5)
关键点如下:
lv_obj_add_event_cb(..., LV_EVENT_ALL, ...)让生成的入口函数接收该对象的所有事件;lv_event_get_code(e)取出当前事件码;if (event_code == LV_EVENT_CLICKED)再筛选出设计器配置的点击动作;led1contrl(e)是用户业务回调,课程只在这里加入一行 GPIO 翻转代码;e虽然在本例业务函数中未被使用,但 SquareLine 的回调签名仍会传入完整事件对象。
最终工程的链接映射文件也能验证这条关系:ui_Screen1_screen_init 引用 ui_event_Button111,后者引用 led1contrl,而 led1contrl 再引用 HAL_GPIO_TogglePin。
二、复制代码至新工程
添加按钮事件并重新导出后,要把第 19 个工程的最新 ui 内容同步到第 20 个工程的 midware/ui。Keil 可能要求 Reload;同步完成后还要把此前没有加入工程的 ui_events.c 加入 midware\ui 代码组。
这里不需要把 main.h 复制到 UI 文件夹。正确做法是在 ui_events.c 中包含工程原有的 main.h,再通过已配置的 Core/Inc 包含路径取得 LED_Pin、LED_GPIO_Port 和 HAL 声明。
2.1 电路图查找灯所在的引脚
开发板原理图“自定义 LED 指示灯”区域给出了三个灯:
| MCU 端或节点 | 网络名 | 限流电阻 | LED 器件位号 | 作用 |
|---|---|---|---|---|
| PB5 | LED0 | R2,1 kΩ | LED1,0603 红灯 | 可由 PB5 控制 |
| PE5 | LED1 | R4,1 kΩ | LED2,0603 红灯 | 本课选用,可由 PE5 控制 |
| GND | — | R6,1 kΩ | LED3,0603 蓝灯 | 电源指示灯,不能由 GPIO 控制 |
因此,本课选择的是 PE5。视频转写后半段偶尔出现“PB5”,但同一段先明确说“用 PE5”,最终 CubeMX 配置、main.h 宏和配套工程也都指向 GPIOE + GPIO_PIN_5,所以 PB5 是语音识别误差。
PE5 这一支路的电流路径可写成:
3.3 V → LED2 → R4(1 kΩ)→ PE5
- 当 PE5 输出低电平时,3.3 V 与 PE5 之间形成压差,电流流过 LED2 和 R4,LED 点亮;
- 当 PE5 输出高电平、接近 3.3 V 时,支路两端几乎没有足够压差,LED 熄灭;
- R4 是限流电阻,用来限制流过 LED 和 MCU 引脚的电流,不能省略;
- 这种接法需要 MCU 引脚在点亮时“灌电流”,所以称为低电平有效或低电平点亮。
网络名 LED1 与器件位号 LED2 容易混淆:前者是导线标签,后者是原理图中具体 LED 元件的编号;它们不是同一个命名体系。
2.2 灯的初始化
在 STM32CubeMX 中把 PE5 设置为 GPIO_Output,并采用课程最终工程的配置:
| 配置项 | 设置 | 原因 |
|---|---|---|
| GPIO output level | High | LED 低电平点亮,初始化为高电平可使上电后先熄灭 |
| GPIO mode | Output Push Pull | 主动输出高、低两种电平,适合直接控制该 LED 支路 |
| GPIO Pull-up/Pull-down | Pull-up | 按课堂界面原样选择;但 STM32F1 HAL 在推挽输出分支不会使用 Pull 字段,真正决定输出的是 ODR/BSRR 与输出模式 |
| Maximum output speed | Low | LED 开关不需要高速边沿,低速已经足够 |
| User Label | LED | 让 CubeMX 生成 LED_Pin 和 LED_GPIO_Port 宏 |
生成代码后,gpio.c 会先用 HAL_GPIO_WritePin(..., GPIO_PIN_SET) 把输出数据置高,再把 PE5 配置成推挽输出。这种先预置电平、后切换输出模式的次序有助于避免初始化瞬间出现不必要的低电平闪亮。
要区分两个概念:
- 初始输出为 High:决定 GPIO 进入输出状态后 LED 默认熄灭;
GPIO_InitStruct.Pull = GPIO_PULLUP:虽然 CubeMX 把它写进了结构体,但 STM32F1 的HAL_GPIO_Init()只在输入、中断或事件模式分支读取Pull;配置为GPIO_MODE_OUTPUT_PP时,该字段不会额外产生一个内部上拉效果。因此不能把它当成 LED 熄灭的原因,也不能把它当成限流电阻。
三、连接硬件并配置代码
完成 UI 更新与 PE5 配置后,按以下顺序整合并验证:
- 在 Keil 的
midware\ui代码组中加入ui_events.c; - 在
ui_events.c中加入#include "main.h",使 LED 宏和 HAL GPIO 接口可见; - 在
led1contrl()中加入课程原代码HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);; - 编译工程,确认没有头文件缺失、未定义符号或重复定义;
- 连接并烧录开发板,确认界面仍能正常显示和触摸;
- 单击屏幕中的
Button111:PE5 从高变低,红色 LED 点亮;再次点击时从低变高,LED 熄灭。
HAL_GPIO_TogglePin() 的作用是根据 GPIO 当前输出状态反转指定引脚。由于本电路低电平有效,逻辑电平与肉眼看到的灯状态相反:
| PE5 输出 | LED2 状态 | 下一次 Toggle 后 |
|---|---|---|
| High | 灭 | 变为 Low,灯亮 |
| Low | 亮 | 变为 High,灯灭 |
若触摸按钮有动画但 LED 不变,可按调用链逐层检查:
ui_events.c是否已经加入 Keil 工程,而不仅仅存在于磁盘;Button111的事件是否为CLICKED + CALL FUNCTION + led1contrl;- 最新导出的
ui_Screen1.c是否注册了ui_event_Button111; ui.c是否在LV_EVENT_CLICKED分支调用led1contrl(e);ui_events.c是否包含main.h,宏是否最终展开为GPIOE和GPIO_PIN_5;- CubeMX 是否把 PE5 配置为输出且已重新生成代码;
- 观察的是否是 PE5 对应的红色 LED2,而不是 PB5 的 LED1 或常亮的蓝色电源灯。
四、内容结束
本节完成了从可视化设计到真实外设控制的闭环:导出 SquareLine UI、接入 Keil、调用 ui_init()、添加点击事件、配置 PE5,并在回调中通过 HAL 翻转板载 LED。
课程录制时提到软件部分功能收费、免费功能已够课堂使用。这是当时版本与许可状态的描述,许可方案可能随版本变化;实际使用时应以当前 SquareLine 官方说明为准。
本节最容易出现的错误如下:
| 错误 | 结果或修正方法 |
|---|---|
把 Export 识别成“x pot” | 正确菜单是 Export → Export UI Files |
把 Screen 识别成“scoring/sql” | 正确函数是 ui_Screen1_screen_init() |
| 把 Keil 识别成“Q/Kill” | 实际操作对象是 Keil µVision 工程 |
只复制 UI 文件、不加入 .c 文件 | 文件不参加编译,出现链接错误或功能缺失 |
在 lv_init() 之前调用 ui_init() | LVGL 尚未准备好,初始化顺序错误 |
忘记停用 lv_demo_widgets() | 官方 demo 与自定义界面可能相互覆盖或额外占用资源 |
| 手改生成文件后忘记保存 | 编译器仍使用旧内容,屏幕看不到变化 |
再次导出后漏加 ui_events.c | 事件回调没有参加编译 |
| 把 PE5 误写为 PB5 | 控制到另一只灯或观察不到预期结果 |
| 把高电平有效套用到本板 | 本板所选 LED 是低电平点亮,逻辑会相反 |
官方资料核对: