衡山派lvgl小练

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

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

然后我让codex依据之前的关于lvgl的东西给我出了个题目

题目

练习题:LVGL 串口灯控状态面板

做一个小项目:屏幕上有一个 LED 控制面板,既能通过 触摸屏按钮 控制板载 LED,也能通过 UART3 命令 控制 LED,并且两边状态要同步显示。

基础要求

  1. LVGL 页面上显示:
    • 一个标题:LED Control Panel
    • 一个状态文本:LED: ON / LED: OFF
    • 一个开关或按钮:点击后切换 LED 状态
    • 一个日志区域:显示最近收到的串口命令
  2. 触摸屏控制:
    • 点击 LVGL 按钮,调用 user_led_on() / user_led_off()
    • 屏幕状态文本同步变化
    • 串口回发:LED ON by UI\r\n 或 LED OFF by UI\r\n
  3. 串口控制:
    • 收到 Led_Board=1,LED 点亮,屏幕显示 LED: ON
    • 收到 Led_Board=0,LED 熄灭,屏幕显示 LED: OFF
    • 收到未知命令,日志区域显示 Unknown command
  4. 模块化要求:
    • 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 个场景:

  1. 点屏幕按钮,LED 状态变化。
  2. 串口发 Led_Board=1,LED 点亮,屏幕文字变成 LED: ON。
  3. 串口发 Led_Board=0,LED 熄灭,屏幕文字变成 LED: OFF。
  4. 串口发错误命令,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 消息队列即可。

笔记整理

  1. 当前工程使用 AiUIBuilder 生成 LVGL 页面,控件对象在 ui_objects.h。
  2. screen->button_1 是 LED 控制按钮。
  3. screen->label_5 显示 LED: ON / LED: OFF。
  4. screen->label_4 显示最近串口命令或 Unknown command。
  5. user_led.c 只负责 PE17 LED,不掺杂 UI 和串口逻辑。
  6. user_uart3.c 负责 UART3 收发,收到命令后通知业务层。
  7. 新增 user_app_ctrl.c/.h 作为业务状态中心。
  8. UI 点击和串口命令都统一走 user_app_ctrl。
  9. LVGL 控件只能在 LVGL 相关上下文里更新,不能在 UART 接收线程里直接更新。
  10. 简单版用全局状态变量 + mutex + lv_timer 定时刷新 UI。
  11. 页面切换后,旧 screen 的控件可能被删除,timer 访问旧控件会卡死。
  12. 解决办法:刷新 UI 前判断当前页面和控件有效性。
  13. pinmux.c 中 PE17 已配置为 GPIO,PA6/PA7 已配置为 UART3。
  14. 编译时重点看头文件路径、SConscript 是否把新增模块纳入编译、函数是否有声明和定义。

这套结构你已经跑通了,挺关键的一步。后面你再做传感器、继电器、PWM、ADC 显示,本质上都可以沿用这个分层。

相关推荐
山木嵌入式2 天前
STM32 UART串口通信协议与3种底层驱动实现(寄存器/标准库/HAL库)
stm32·单片机·串口·uart
The_superstar65 天前
衡山派LVGL注意点
单片机·lvgl·衡山派·俊杰
xzl048 天前
LVGL显示移植:lv_port_disp.c 详情介绍
rt-thread·lvgl·ra6m3-hmi-board
The_superstar610 天前
衡山派结合串口与LED
串口通讯·衡山派·ds133ebs
xzl0411 天前
LVGL输入移植:lv_port_indev.c 详情介绍
lvgl·ra6m3-hmi-board
10Eugene11 天前
LVGL V9增加实体按键 驱动层和应用层的处理
c·lvgl
星光202511 天前
APM学习(9):串口管理
串口·ardupilot
The_superstar611 天前
衡山派学习之串口
单片机·嵌入式硬件·串口·衡山派
tzy23313 天前
电力通信规约的鼻祖:IEC101和串口通信
串口·iec104·iec101·rs-485·rs-232·8n1