如果还想了解一下衡山派的lvgl上位机软件 AiBuilder 可以去查看一下官方的手册

可以主要看一下第五章和第六章

然后我让codex依据之前的关于lvgl的东西给我出了个题目
题目
练习题:LVGL 串口灯控状态面板
做一个小项目:屏幕上有一个 LED 控制面板,既能通过 触摸屏按钮 控制板载 LED,也能通过 UART3 命令 控制 LED,并且两边状态要同步显示。
基础要求
- LVGL 页面上显示:
- 一个标题:LED Control Panel
- 一个状态文本:LED: ON / LED: OFF
- 一个开关或按钮:点击后切换 LED 状态
- 一个日志区域:显示最近收到的串口命令
- 触摸屏控制:
- 点击 LVGL 按钮,调用 user_led_on() / user_led_off()
- 屏幕状态文本同步变化
- 串口回发:LED ON by UI\r\n 或 LED OFF by UI\r\n
- 串口控制:
- 收到 Led_Board=1,LED 点亮,屏幕显示 LED: ON
- 收到 Led_Board=0,LED 熄灭,屏幕显示 LED: OFF
- 收到未知命令,日志区域显示 Unknown command
- 模块化要求:
- user_led.c/.h 继续只负责 LED
- user_uart3.c/.h 负责串口收发
- 新增一个 user_app_ctrl.c/.h,负责"业务状态"
- LVGL 页面不要直接解析串口命令,串口也不要直接乱改 UI
重点挑战
最关键的一点:不要在 UART 接收线程里直接操作 LVGL 控件。
更推荐的结构是:
c
UART线程收到命令
-> 解析成事件 LED_ON / LED_OFF
-> 通知 app_ctrl
-> app_ctrl 改 LED 状态
-> LVGL 页面定时刷新状态,或通过消息队列更新 UI
你可以先用简单版:定义一个全局 LED 状态变量,然后 LVGL 用 lv_timer 每 200ms 刷新一次状态。后面再升级成 RT-Thread 消息队列。
建议接口设计
你可以练习封装这些函数:
c
/* user_app_ctrl.h */
void app_led_set(int on);
void app_led_toggle(void);
int app_led_get(void);
void app_log_set(const char *text);
const char *app_log_get(void);
然后:
c
/* UART收到 Led_Board=1 */
app_led_set(1);
app_log_set("UART: Led_Board=1");
/* LVGL按钮点击 */
app_led_toggle();
app_log_set("UI: toggle LED");
加分项
如果你想更像真实项目,可以继续加:
- 一个亮度滑条:LVGL slider 控制屏幕背光 PWM
- 一个串口命令:Backlight=50
- 一个运行时间显示:Uptime: 123s
- 一个连接状态点:最近 5 秒收到串口命令显示绿色,否则灰色
验收标准
你做完以后应该能演示这 4 个场景:
- 点屏幕按钮,LED 状态变化。
- 串口发 Led_Board=1,LED 点亮,屏幕文字变成 LED: ON。
- 串口发 Led_Board=0,LED 熄灭,屏幕文字变成 LED: OFF。
- 串口发错误命令,LED 不乱变,屏幕日志显示错误命令。
这个题目刚好接在你博客里的 UART + LED 之后,再往前推一步就是"嵌入式 UI 控制硬件"。参考你昨天的文章:https://blog.csdn.net/The_superstar6/article/details/160742670
蜜汁提示词
现在我们怎么实现呢?
----哈哈哈ai时代当然是用ai了,我们之前不是讲过万能提示词吗--我们浅浅根据我们的需求修改一下然后发给ai即可
请你先检查一下我当前的衡山派 D133EBS 工程结构,重点看 luban-lite 里的 AiUIBuilder 生成代码、ui_objects.h、screen.c、
custom.c、ui_init.c、当前板级 pinmux.c。
我已经在 AiUIBuilder 里做好界面并移植到 SDK 了。现在我想实现这个功能:
下面是要求:【
基础要求
LVGL 页面上显示:
一个标题:LED Control Panel
一个状态文本:LED: ON / LED: OFF
一个开关或按钮:点击后切换 LED 状态
一个日志区域:显示最近收到的串口命令
触摸屏控制:
点击 LVGL 按钮,调用 user_led_on() / user_led_off()
屏幕状态文本同步变化
串口回发:LED ON by UI\r\n 或 LED OFF by UI\r\n
串口控制:
收到 Led_Board=1,LED 点亮,屏幕显示 LED: ON
收到 Led_Board=0,LED 熄灭,屏幕显示 LED: OFF
收到未知命令,日志区域显示 Unknown command
模块化要求:
user_led.c/.h 继续只负责 LED
user_uart3.c/.h 负责串口收发
新增一个 user_app_ctrl.c/.h,负责"业务状态"
LVGL 页面不要直接解析串口命令,串口也不要直接乱改 UI
重点挑战
最关键的一点:不要在 UART 接收线程里直接操作 LVGL 控件。
更推荐的结构是:
UART线程收到命令
-> 解析成事件 LED_ON / LED_OFF
-> 通知 app_ctrl
-> app_ctrl 改 LED 状态
-> LVGL 页面定时刷新状态,或通过消息队列更新 UI
你可以先用简单版:定义一个全局 LED 状态变量,然后 LVGL 用 lv_timer 每 200ms 刷新一次状态。后面再升级成 RT-Thread 消息队 列。
】
我用我的语言描述一下你看看【点击 button_1后让板载LED--PE17电平翻转,或者我用通过电脑的串口助手给单片机的串口3发数据来控制led的亮灭,lvgl中有一个控件用来显示当前的led的状态是ON还是OFF,如果led亮,则控件label_5显示LED:ON,反之则显示LED:OFF,然后还有一个控件label_4用来显示最近串口3接收到的数据,只会有三种形式Led_Board=1、Led_Board=0或者收到未知命令,日志区域显示 Unknown command
然后代码的编写要符合模块化,不要在 UART 接收线程里直接操作 LVGL 控件--那是定义全局变量来弄还是说利用操作系统的信息、队列这些东西来实现】
请你不要直接改代码,先告诉我:
1. 相关控件在代码里叫什么;
2. 应该改哪些文件;
3. 每个文件具体加什么代码;
4. 如果涉及 GPIO,需要在 pinmux.c 里怎么配置;
5. 编译后如果失败,我应该重点看哪些错误。

c
screen_get(&ui_manager)->button_1 // 你的 LVGL 控制按钮
screen_get(&ui_manager)->label_4 // 最近串口命令日志
screen_get(&ui_manager)->label_5 // LED: ON / LED: OFF 状态
screen_get(&ui_manager)->label_1 // 标题 LED Control Panel

c
button_1 点击
-> custom.c 调 user_app_ctrl_toggle_led_by_ui()
-> app_ctrl 调 user_led_on/off()
-> custom.c 通过 user_uart3_send_string() 回发 "LED ON by UI\r\n"
-> lv_timer 每 200ms 刷新 label_5
UART3 收到 Led_Board=1/0
-> user_uart3.c 调 user_app_ctrl_handle_uart_command()
-> app_ctrl 改 LED 状态和最近日志
-> lv_timer 每 200ms 刷新 label_5 和 label_4


c
#ifndef __USER_APP_CTRL_H__
#define __USER_APP_CTRL_H__
#include <rtthread.h>
typedef enum {
USER_APP_CMD_LED_ON,
USER_APP_CMD_LED_OFF,
USER_APP_CMD_UNKNOWN,
} user_app_cmd_result_t;
int user_app_ctrl_init(void);
int user_app_ctrl_toggle_led_by_ui(void);
user_app_cmd_result_t user_app_ctrl_handle_uart_command(const char *cmd);
void user_app_ctrl_get_snapshot(int *led_on, char *last_log, rt_size_t log_size);
#endif
user_app_ctrl.c 负责 LED 状态和日志,不操作 LVGL:
c
#include <string.h>
#include <rtthread.h>
#include "../user_led/user_led.h"
#include "user_app_ctrl.h"
static struct rt_mutex app_lock;
static rt_bool_t app_inited = RT_FALSE;
static int led_on = 0;
static char last_uart_log[64] = "No UART command";
int user_app_ctrl_init(void)
{
if (app_inited) return RT_EOK;
rt_mutex_init(&app_lock, "appctrl", RT_IPC_FLAG_PRIO);
user_led_init();
user_led_off();
app_inited = RT_TRUE;
return RT_EOK;
}
static void set_led_locked(int on)
{
led_on = on ? 1 : 0;
if (led_on) user_led_on();
else user_led_off();
}
int user_app_ctrl_toggle_led_by_ui(void)
{
user_app_ctrl_init();
rt_mutex_take(&app_lock, RT_WAITING_FOREVER);
set_led_locked(!led_on);
int now_on = led_on;
rt_mutex_release(&app_lock);
return now_on;
}
user_app_cmd_result_t user_app_ctrl_handle_uart_command(const char *cmd)
{
user_app_ctrl_init();
rt_mutex_take(&app_lock, RT_WAITING_FOREVER);
if (strcmp(cmd, "Led_Board=1") == 0) {
set_led_locked(1);
rt_strncpy(last_uart_log, "Led_Board=1", sizeof(last_uart_log) - 1);
rt_mutex_release(&app_lock);
return USER_APP_CMD_LED_ON;
}
if (strcmp(cmd, "Led_Board=0") == 0) {
set_led_locked(0);
rt_strncpy(last_uart_log, "Led_Board=0", sizeof(last_uart_log) - 1);
rt_mutex_release(&app_lock);
return USER_APP_CMD_LED_OFF;
}
rt_strncpy(last_uart_log, "Unknown command", sizeof(last_uart_log) - 1);
rt_mutex_release(&app_lock);
return USER_APP_CMD_UNKNOWN;
}
void user_app_ctrl_get_snapshot(int *out_led_on, char *out_log, rt_size_t log_size)
{
user_app_ctrl_init();
rt_mutex_take(&app_lock, RT_WAITING_FOREVER);
if (out_led_on) *out_led_on = led_on;
if (out_log && log_size > 0) {
rt_strncpy(out_log, last_uart_log, log_size - 1);
out_log[log_size - 1] = '\0';
}
rt_mutex_release(&app_lock);
}

把 handle_uart_led_command() 改成:
c
static void handle_uart_led_command(uint8_t *recv_buff)
{
user_app_cmd_result_t result;
trim_command_tail(recv_buff);
result = user_app_ctrl_handle_uart_command((char *)recv_buff);
if (result == USER_APP_CMD_LED_ON) {
user_uart3_send_string("LED ON\r\n");
} else if (result == USER_APP_CMD_LED_OFF) {
user_uart3_send_string("LED OFF\r\n");
} else {
user_uart3_send_string("Unknown command\r\n");
}
}
custom.c 里绑定按钮和定时刷新:
c
#include "lvgl.h"
#include "custom.h"
#include "ui_objects.h"
#include "user_app_ctrl.h"
#include "user_uart3.h"
static void led_ui_refresh(void)
{
int on = 0;
char log[64];
user_app_ctrl_get_snapshot(&on, log, sizeof(log));
lv_label_set_text(screen_get(&ui_manager)->label_5,
on ? "LED: ON" : "LED: OFF");
lv_label_set_text(screen_get(&ui_manager)->label_4, log);
}
static void led_button_clicked_cb(lv_event_t *e)
{
int on = user_app_ctrl_toggle_led_by_ui();
user_uart3_send_string(on ? "LED ON by UI\r\n" : "LED OFF by UI\r\n");
led_ui_refresh();
}
static void led_ui_timer_cb(lv_timer_t *timer)
{
led_ui_refresh();
}
void custom_init()
{
user_app_ctrl_init();
lv_obj_add_event_cb(screen_get(&ui_manager)->button_1,
led_button_clicked_cb,
LV_EVENT_CLICKED,
NULL);
lv_timer_create(led_ui_timer_cb, 200, NULL);
led_ui_refresh();
}
custom/SConscript 要把 app 头文件路径加进去,否则 user_app_ctrl.h 找不到。建议加:
py
Import('AIC_ROOT')
CPPPATH = [
cwd,
os.path.join(cwd, '..'),
os.path.join(AIC_ROOT, 'application/rt-thread/helloworld/user_app_ctrl'),
os.path.join(AIC_ROOT, 'application/rt-thread/helloworld/user_uart3'),
]
user_app_ctrl/SConscript 可以按现有风格写:
py
Import('AIC_ROOT')
Import('rtconfig')
from building import *
cwd = GetCurrentDir()
CPPPATH = [cwd]
src = []
if GetDepend('USER_LED_ON'):
src = Glob(os.path.join(cwd, '*.c'))
group = DefineGroup('lckfb-user-app-ctrl', src, depend = [''], CPPPATH = CPPPATH)
Return('group')

小Bug
我已经成功调试完成了,但是我发现了一个bug,就是我的lvgl有两个screen,然后当在screen下 按下控件button_1就跳转到screen_1,然后在screen_1下按下button_1返回到screen,前面实现没问题,但是我发现好像一进到screen_1就卡死了,请你帮我分析一下是什么问题

当然。你这次需求的核心其实不是"点灯",而是把 UI、串口、LED 硬件三件事拆清楚。
实现思路
我拿到需求后,第一步会先拆角色:
user_led.c/.h 只管 PE17 这个 LED,提供 user_led_on()、user_led_off()、user_led_set()。
user_uart3.c/.h 只管 UART3 收发,收到字符串后只做命令识别,不直接操作 LVGL。
user_app_ctrl.c/.h 是中间的业务控制层,负责"当前 LED 是 ON 还是 OFF"、"最近一条串口命令是什么"。
custom.c 是 LVGL 页面和业务层之间的桥,按钮点击调用业务层,页面显示也从业务层读取状态。
为什么要这样拆?因为 LVGL 不是随便哪个线程都能操作的。UART 接收线程里如果直接 lv_label_set_text(),很容易卡死、闪退、内存异常。所以我们让 UART 线程只更新业务状态,LVGL 页面自己用 lv_timer 每 200ms 去刷新显示。
两条执行路线
UI 控制路线:
点击 screen 的 button_1
-> custom.c 的按钮回调触发
-> user_app_ctrl_toggle_led_by_ui()
-> app_ctrl 切换 LED 状态
-> user_led_on/off 控制 PE17
-> UART3 回发 LED ON by UI / LED OFF by UI
-> label_5 显示 LED: ON / LED: OFF
串口控制路线:
电脑串口助手发送 Led_Board=1 或 Led_Board=0
-> user_uart3.c 收到字符串
-> handle_uart_led_command()
-> user_app_ctrl_handle_uart_command()
-> app_ctrl 修改 LED 状态和最近日志
-> user_led_on/off 控制 PE17
-> custom.c 的 lv_timer 定时刷新 label_4 / label_5
各文件代码含义
user_led.c:硬件层。它通过:
#define USER_LED_PIN_NAME "PE.17"
拿到 PE17,然后用 rt_pin_write() 输出高低电平。这个文件不应该知道 UART,也不应该知道 LVGL。
user_app_ctrl.c:业务层。里面保存两个关键状态:
static int led_on; static char last_uart_log[64];
led_on 表示当前 LED 状态;last_uart_log 表示页面 label_4 要显示的最近命令。因为 UART 线程和 LVGL timer 都可能访问这些变量,所以用了 rt_mutex 保护,避免同时读写出问题。
user_app_ctrl_handle_uart_command():负责解析:
Led_Board=1 -> 开灯
Led_Board=0 -> 关灯
其他 -> Unknown command
它只更新业务状态和 LED,不碰 LVGL 控件。
user_uart3.c:串口层。原来这里直接调用 user_led_on() / user_led_off(),后来改成调用:
user_app_ctrl_handle_uart_command()
这样 UART 不再直接处理 UI,也不直接承担业务状态管理。user_uart3_send_string() 是把原来的静态发送函数包装出来,让 custom.c 也能回发串口消息。
custom.c:UI 绑定层。custom_init() 在 ui_init.c 创建完 screen 后执行,它做三件事:
初始化 app_ctrl
给 button_1 绑定点击事件
创建 lv_timer 定时刷新 label_4 / label_5
led_ui_refresh() 的作用是从 user_app_ctrl 读取状态,然后更新:
label_5 -> LED: ON / LED: OFF
label_4 -> Led_Board=1 / Led_Board=0 / Unknown command
后来 screen 切换卡死的问题,也是这里引出的:timer 一直跑,但 screen 被切换后 label_4/label_5 已经被删除,所以必须判断当前页面是不是 screen,控件是否有效,再刷新。
pinmux.c:板级复用配置。当前工程是 target/d13x/JLC/pinmux.c,里面已经有:
{1, PIN_PULL_DIS, 3, "PE.17"},
所以 PE17 已经配置成 GPIO,不用额外改。UART3 的 PA6/PA7 也已经配置好了。
这次最重要的经验
这次不是简单"按钮点灯",而是一个小型嵌入式 GUI 架构:
硬件层 user_led 串口层 user_uart3 业务层 user_app_ctrl 界面层 custom.c / LVGL
UART 线程不要直接操作 LVGL;LVGL 页面通过 timer 主动读取业务状态。这是你现在这个阶段最稳、最容易调通的方案。以后如果状态越来越多,再把"全局状态 + timer"升级成 RT-Thread 消息队列即可。
笔记整理
- 当前工程使用 AiUIBuilder 生成 LVGL 页面,控件对象在 ui_objects.h。
- screen->button_1 是 LED 控制按钮。
- screen->label_5 显示 LED: ON / LED: OFF。
- screen->label_4 显示最近串口命令或 Unknown command。
- user_led.c 只负责 PE17 LED,不掺杂 UI 和串口逻辑。
- user_uart3.c 负责 UART3 收发,收到命令后通知业务层。
- 新增 user_app_ctrl.c/.h 作为业务状态中心。
- UI 点击和串口命令都统一走 user_app_ctrl。
- LVGL 控件只能在 LVGL 相关上下文里更新,不能在 UART 接收线程里直接更新。
- 简单版用全局状态变量 + mutex + lv_timer 定时刷新 UI。
- 页面切换后,旧 screen 的控件可能被删除,timer 访问旧控件会卡死。
- 解决办法:刷新 UI 前判断当前页面和控件有效性。
- pinmux.c 中 PE17 已配置为 GPIO,PA6/PA7 已配置为 UART3。
- 编译时重点看头文件路径、SConscript 是否把新增模块纳入编译、函数是否有声明和定义。
这套结构你已经跑通了,挺关键的一步。后面你再做传感器、继电器、PWM、ADC 显示,本质上都可以沿用这个分层。