feat(display): 添加LVGL显示支持和PWM背光控制

添加了完整的LVGL集成支持,包括:

- 在app.overlay中配置显示设备树,添加背光别名和SPI3总线支持
- 集成PWM背光控制,通过pwm-leds子系统管理背光亮度
- 配置LVGL自动初始化和工作队列运行模式
- 实现显示模块的工作队列更新机制,包含UI创建和定时刷新
- 添加详细的LVGL移植说明文档,涵盖设备树配置、调试步骤和常见问题
- 调整分区配置以适应LVGL固件大小需求
- 启用MCUBoot bootloader支持OTA功能

该变更使得系统能够在ST7789V显示屏上正常运行LVGL界面,并通过PWM控制背光。
This commit is contained in:
2026-03-23 09:16:34 +08:00
parent 6ca70d2580
commit d02e33d97b
6 changed files with 916 additions and 39 deletions

View File

@@ -5,6 +5,10 @@
zephyr,display = &st7789v3;
};
aliases {
backlight = &backlight;
};
zephyr,user {
vbat-en-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
io-channels = <&adc 5>, <&adc 7>;
@@ -83,7 +87,7 @@ qdec: &qdec {
status = "okay";
};
&spi2 {
&spi3 {
status = "okay";
};
@@ -94,3 +98,11 @@ qdec: &qdec {
&st7789v3 {
status = "okay";
};
&pwm_leds {
status = "okay";
};
&pwm0 {
status = "okay";
};

View File

@@ -0,0 +1,791 @@
# 基于 Zephyr 的 LVGL 移植说明
## 1. 目标
本文档记录 `new_kbd` 项目在 `atguigu_mini_keyboard/nrf52840` 板卡上调通 LCD 与 LVGL 的过程,重点覆盖以下内容:
- 板级设备树需要满足的显示与背光条件
- 如何先用最基础的 Zephyr `display` 子系统验证硬件链路
- 如何切回 Zephyr 自带的 LVGL 适配层
- 当前项目已经落地的关键配置
- 常见问题与排查顺序
本文档对应的工程位置:
- 应用目录: `E:\projects\new_kbd`
- 板卡目录: `E:\extra\boards\atguigu\atguigu_mini_keyboard`
---
## 2. 硬件链路确认
当前板卡上的显示相关资源如下:
- 面板驱动: `sitronix,st7789v`
- 总线: `mipi-dbi-spi`
- 显示节点: `st7789v3`
- 背光: `pwm_leds/backlight`
- 背光 PWM 输出: `pwm0`
板级 DTS 中的关键节点在:
- [atguigu_mini_keyboard.dts](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard.dts)
- [atguigu_mini_keyboard-pinctrl.dtsi](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard-pinctrl.dtsi)
当前已经确认:
- 屏幕本身可以通过 Zephyr `display_write()` 正常显示颜色切换画面
- 背光不是普通 GPIO 开关,而是 `pwm-leds` 设备
- `p0.13` 与当前 LCD 使能无关,已经从显示路径里移除
这意味着:
- SPI/MIPI DBI 链路是通的
- ST7789V 初始化参数至少在当前板上可用
- 背光控制链路是通的
- 后续如果 LVGL 出现黑屏,优先怀疑的是 LVGL 初始化、线程模型或对象刷新,而不是底层显示驱动
---
## 3. 设备树要求
应用侧 overlay 在:
- [app.overlay](E:\projects\new_kbd\app.overlay)
当前与显示相关的关键要求有三条:
### 3.1 选择显示设备
必须在 `chosen` 中指定:
```dts
/ {
chosen {
zephyr,display = &st7789v3;
};
};
```
否则 Zephyr 的 `display` 子系统和 LVGL 模块都找不到目标显示设备。
### 3.2 打开显示控制器和面板节点
应用 overlay 中需要显式把下面这些节点设为 `okay`:
```dts
&spi2 {
status = "okay";
};
&mipi_dbi {
status = "okay";
};
&st7789v3 {
status = "okay";
};
```
### 3.3 打开 PWM 背光
当前背光不是由显示驱动自动管理,而是走 LED/PWM 子系统,所以还需要:
```dts
/ {
aliases {
backlight = &backlight;
};
};
&pwm_leds {
status = "okay";
};
&pwm0 {
status = "okay";
};
```
这样应用代码里可以通过 `LED_DT_SPEC_GET(DT_NODELABEL(backlight))` 拿到背光设备,再调用 `led_set_brightness_dt()` 设亮度。
---
## 4. 先验证裸显示,再切回 LVGL
这次移植采用了两阶段策略:
### 4.1 第一阶段: 先验证 Zephyr `display` 子系统
在这个阶段,应用不启用 LVGL而是直接调用:
- `display_get_capabilities()`
- `display_blanking_off()`
- `display_write()`
通过 RGB565 彩条整屏写入,确认:
- 面板已经真正收到像素数据
- 刷新链路不是空转
- 背光与显示内容是解耦的
这个阶段已经验证成功,屏幕可以正常显示颜色切换画面。
### 4.2 第二阶段: 切回 Zephyr 自带 LVGL 适配层
在确认底层链路没问题后,再恢复 LVGL。这样如果再次黑屏就可以把排查范围收缩到:
- LVGL 是否真的初始化成功
- UI 是否在正确线程创建
- 对象是否被正确刷新
- `display_blanking_off()` 与背光使能时机是否正确
这个两阶段方法是这次调试中最关键的分界线。
---
## 5. 当前 LVGL 方案
当前项目使用的是 Zephyr 自带的 LVGL 移植,不再手写 `lv_timer_handler()` 主循环。
相关应用代码在:
- [display_module.c](E:\projects\new_kbd\src\modules\display_module.c)
当前方案要点如下:
### 5.1 使用 Zephyr 自带的 LVGL 自动初始化
依赖 `CONFIG_LV_Z_AUTO_INIT=y`,由 Zephyr 在应用启动前完成:
- `lv_init()`
- display 注册
- 渲染缓冲区创建
- LVGL 核心初始化
因此应用层不需要再手动调用 `lvgl_init()`
### 5.2 使用 Zephyr 的 LVGL workqueue
依赖 `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y`
这样 LVGL 核心 `lv_timer_handler()` 由 Zephyr 模块自己调度,应用层不再手动驱动 LVGL 时钟。
应用里如果还需要定期更新 UI当前做法是:
- 自己维护一个 `k_work_delayable`
- 通过 `lvgl_get_workqueue()` 把这个 work 投递到 LVGL 专用 workqueue
- 在 work 回调里 `lvgl_lock()` 后访问 LVGL API
这样做的原因是:
- 避免 UI 更新逻辑和 LVGL 核心不在同一执行上下文
- 避免对象创建、样式更新与内部刷新竞争
- 对当前板子来说,这是比“任意线程加锁直接操作 LVGL”更保守、更容易稳定的方案
### 5.3 先开背光,再创建 UI
当前时序是:
1. `display_blanking_off()`
2. `display_backlight_init()`
3. 将显示模块标记为 ready
4. 立即在 LVGL workqueue 上调度一次 UI 更新
背光初始化通过 `pwm-leds` 完成,不再依赖额外 GPIO。
### 5.4 当前 UI 验证策略
为了避免“画面有了但刚好看不见”的误判,当前 UI 使用了高对比度方案:
- 背景色在两种颜色之间切换
- 标题为 `Zephyr LVGL running`
- 计数文本为 `tick N`
如果这三项都能显示,基本可以判定 LVGL 渲染链路已经正常工作。
---
## 6. 当前关键配置
应用配置文件在:
- [prj.conf](E:\projects\new_kbd\prj.conf)
当前与 LVGL/显示相关的关键配置如下:
```conf
CONFIG_DISPLAY=y
CONFIG_MIPI_DBI=y
CONFIG_ST7789V=y
CONFIG_LVGL=y
CONFIG_LV_CONF_MINIMAL=y
CONFIG_LV_BUILD_EXAMPLES=n
CONFIG_LV_BUILD_DEMOS=n
CONFIG_LV_USE_LABEL=y
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_Z_AUTO_INIT=y
CONFIG_LV_Z_BITS_PER_PIXEL=16
CONFIG_LV_Z_LVGL_MUTEX=y
CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y
CONFIG_LV_Z_MEM_POOL_SIZE=16384
```
各项含义:
- `CONFIG_LV_Z_BITS_PER_PIXEL=16`
与 ST7789V 当前 `RGB565` 像素格式对应,避免渲染缓冲格式不匹配
- `CONFIG_LV_Z_LVGL_MUTEX=y`
提供 `lvgl_lock()` / `lvgl_unlock()`,确保应用层访问 LVGL API 时有统一互斥保护
- `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y`
让 Zephyr 自动维护 LVGL 核心执行循环,不需要应用手动跑 `lv_timer_handler()`
- `CONFIG_LV_Z_MEM_POOL_SIZE=16384`
作为当前最小可工作的内存池配置
---
## 7. 构建方式
当前构建已不再默认强制使用 `-Og`,而是遵从工程自身配置。
当前工程已经启用:
```conf
CONFIG_SIZE_OPTIMIZATIONS=y
```
因此默认会走 size optimization适合当前 OTA 分区大小约束。
### 7.1 当前构建 skill 的行为
`ncs-fresh-build` skill 已经调整为:
- 不再默认注入 `CONFIG_DEBUG_OPTIMIZATIONS=y`
- 默认构建目录为项目根下的 `build`
- 如果项目下已有 `build` 且带构建元数据,则执行 `resume build`
- 如果不存在,则在项目根下创建 `build`
也就是说,当前默认构建输出目录是:
- `E:\projects\new_kbd\build`
### 7.2 当前已验证的构建结果
在当前 LVGL 配置下,系统构建通过,成功产出:
- `merged.hex`
- `dfu_application.zip`
最近一次 LVGL 镜像的资源占用约为:
- FLASH: `437780 / 482816`
- RAM: `96052 / 256 KB`
说明在当前 OTA 分区布局下LVGL 版本仍然可以装下。
---
## 8. 当前显示模块代码结构
当前显示模块入口在:
- [display_module.c](E:\projects\new_kbd\src\modules\display_module.c)
整体逻辑如下:
1. 等待 `main` 模块进入 `MODULE_STATE_READY`
2. 检查显示设备是否 ready
3. 读取显示能力并打印日志
4. 调用 `display_blanking_off()`
5. 初始化背光 PWM 亮度
6. 将模块标记为 initialized
7. 把第一个 UI 更新 work 投递到 LVGL workqueue
8. 后续每秒更新一次背景与计数文本
这种结构的好处是:
- 显示模块与主应用启动顺序解耦
- LVGL 核心循环交给 Zephyr 模块维护
- 应用只负责自己的 UI 逻辑
- 以后如果要继续扩展页面,也可以沿用同样的工作队列模型
---
## 9. 常见问题与排查顺序
### 9.1 背光亮,但完全黑屏
先不要怀疑 LVGL先切回裸 `display_write()` 测试版本。
如果裸显示也黑屏,优先检查:
- `zephyr,display` 是否指向正确节点
- `spi2` / `mipi_dbi` / `st7789v3` 是否都为 `okay`
- ST7789V 初始化参数是否匹配当前屏
- 面板 offset / colmod / madctl 是否正确
### 9.2 裸 `display_write()` 正常,但 LVGL 黑屏
优先检查:
- `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE` 是否启用
- 应用是否仍在手动调用 `lv_timer_handler()`
- UI 更新是否发生在错误线程
- 是否遗漏 `lvgl_lock()`
- 文本和背景颜色是否刚好一致
### 9.3 背光不亮
优先检查:
- `backlight` alias 是否存在
- `&pwm_leds` 是否启用
- `&pwm0` 是否启用
- `pwm0_default` 的 pinctrl 是否正确
- 应用是否真的调用了 `led_set_brightness_dt()`
### 9.4 构建能过,但镜像塞不进 OTA 分区
优先检查:
- 是否误用了 `-Og`
- `CONFIG_SIZE_OPTIMIZATIONS` 是否生效
- 是否启用了不必要的 LVGL demos/examples
- 字体、部件和日志级别是否过大
---
## 10. 后续建议
如果后续继续完善显示功能,建议按下面顺序推进:
1. 先固定当前最小可工作 UI不要同时引入太多控件
2. 增加 RTT 日志,确认 LVGL 初始化和周期更新是否稳定
3. 再逐步接入输入设备或更复杂页面
4. 最后再考虑动画、图片资源和更大的字体
对当前项目来说,最重要的经验是:
- 先把底层 `display` 写屏跑通
- 再把问题收敛到 LVGL
- 最后再处理更高层的 UI 逻辑
这样可以显著降低显示移植的排查成本。
---
## 11. ST7789V 设备树参数详细说明
当前板上显示节点位于:
- [atguigu_mini_keyboard.dts](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard.dts)
典型配置如下:
```dts
st7789v3: st7789v@0 {
compatible = "sitronix,st7789v";
status = "disabled";
reg = <0>;
mipi-max-frequency = <32000000>;
width = <320>;
height = <172>;
x-offset = <0>;
y-offset = <34>;
vcom = <0x2b>;
gctrl = <0x35>;
vrhs = <0x11>;
vdvs = <0x20>;
mdac = <0x60>;
lcm = <0x2c>;
colmod = <0x55>;
gamma = <0x01>;
porch-param = [ 0c 0c 00 33 33 ];
cmd2en-param = [ 5a 69 02 01 ];
pwctrl1-param = [ a4 a1 ];
pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ];
nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ];
ram-param = [ 00 f0 ];
rgb-param = [ c0 02 14 ];
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
};
```
下面按类别说明这些参数的意义。
### 11.1 设备身份与总线绑定
#### `compatible = "sitronix,st7789v"`
作用:
- 告诉 Zephyr 使用 ST7789V 显示驱动
- 绑定文件来自 [sitronix,st7789v.yaml](C:\ncs\v3.2.3\zephyr\dts\bindings\display\sitronix,st7789v.yaml)
- 驱动实现位于 [display_st7789v.c](C:\ncs\v3.2.3\zephyr\drivers\display\display_st7789v.c)
如果这个 `compatible` 不对,后续所有参数即使写对也不会被正确解释。
#### `reg = <0>`
作用:
- 这是该显示器在父 `mipi_dbi` 总线上的片选号/地址索引
- 对 SPI 模式来说,它最终会映射到父 SPI 控制器 `cs-gpios` 数组中的第几个片选
当前值是 `0`,表示使用父 SPI 控制器的第 0 个 CS。
#### `mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE"`
作用:
- 指定 ST7789V 通过 MIPI DBI Type-C 的 4 线 SPI 模式工作
- 命令/数据区分不是靠 9-bit SPI而是靠单独的 `dc-gpios`
这对当前板子很重要,因为板上已经专门引出了 DC GPIO。
#### `mipi-max-frequency = <32000000>`
作用:
- 指定面板接口希望使用的最高 SPI 时钟
- 这个值会进入 `struct spi_config.frequency`
- 最终实际频率还会受底层 SPI 控制器实例上限限制
当前项目中:
- 面板这里写的是 `32 MHz`
- 底层已经把 LCD 切到 `spi3`
- `spi3` 是 nRF52840 上支持 `32 MHz` 的高速 SPIM 实例
因此这个值现在是有效的,不再像早期挂在 `spi2` 时那样被 SoC 侧 `8 MHz` 上限卡住。
---
### 11.2 分辨率与有效显示窗口
#### `width = <320>`
作用:
- Zephyr `display_get_capabilities()` 返回的水平分辨率
- LVGL 也会按这个宽度创建显示对象和渲染缓冲
在当前横屏模式下,宽度已经从竖屏时的 `172` 改为 `320`
#### `height = <172>`
作用:
- Zephyr `display_get_capabilities()` 返回的垂直分辨率
- 决定应用层看到的逻辑屏高
在当前横屏模式下,高度已经从竖屏时的 `320` 改为 `172`
#### `x-offset = <0>`
作用:
- 指定 LCD 实际可视窗口在 ST7789V GRAM 中的列偏移
- 驱动在写入时,会把应用层传入的 `x` 坐标再加上这个偏移
对应驱动代码:
- `st7789v_set_lcd_margins()`
- `st7789v_set_mem_area()`
#### `y-offset = <34>`
作用:
- 指定 LCD 实际可视窗口在 ST7789V GRAM 中的行偏移
- 驱动会把应用层传入的 `y` 坐标再加上这个偏移
为什么横屏时从 `x-offset=34` 变成 `y-offset=34`:
- 这块屏的物理可视区域不是完整的 240x320而是裁剪出来的 `172x320`
- 当通过 `mdac` 做 XY 轴交换后,原来沿 X 方向的裁剪,需要同步转移到 Y 方向
这是横屏改造里最容易遗漏的一点。如果只改 `width/height``mdac`,不改 offset画面通常会出现:
- 偏移错位
- 局部黑边
- 显示越界
---
### 11.3 方向控制
#### `mdac = <0x60>`
作用:
- 对应 ST7789V 的 `MADCTL` 寄存器,即 Memory Data Access Control
- 用于控制:
- X/Y 轴交换
- 左右镜像
- 上下镜像
- RGB/BGR 顺序
对应驱动里的寄存器命令:
- `ST7789V_CMD_MADCTL`
当前值 `0x60` 的核心意义是:
- 打开 `MV`,进行 XY 轴交换
- 再配合镜像位,把竖屏坐标系旋转为横屏
对当前项目来说,`mdac` 是“横竖屏切换的核心参数”。
如果上板后发现:
- 画面已经横过来,但方向反了
- 或左右/上下镜像不对
通常只需要继续调整 `mdac`,而不一定需要再动 `width/height`
---
### 11.4 面板电气与模拟参数
下面这几项大多属于“面板电气初始化参数”,通常来自原厂例程、模组 demo、已有验证过的初始化序列或者靠经验调通。
它们的共同特点是:
- 不建议随便改
- 改错后往往不是“完全不亮”,而是
- 偏色
- 闪烁
- 对比度差
- 稳定性差
- 局部异常
#### `vcom = <0x2b>`
作用:
- VCOM 电压相关设置
- 影响液晶驱动偏置和整体显示稳定性
常见现象:
- 值不合适时,可能出现闪烁、灰阶异常、残影或对比度不自然
#### `gctrl = <0x35>`
作用:
- Gate Control控制面板栅极驱动相关参数
它更接近面板扫描电路的底层设置,一般按已知可工作配置保留。
#### `vrhs = <0x11>`
作用:
- VRH setting电压参考相关参数之一
#### `vdvs = <0x20>`
作用:
- VDV setting和驱动电压微调相关
注意:
- 驱动里只有同时存在 `vrhs``vdvs` 时,才会开启对应的 `VDVVRHEN` 流程
#### `lcm = <0x2c>`
作用:
- LCM control面板控制相关寄存器值
一般与模组硬件特性绑定,不建议脱离原配置单独尝试。
#### `gamma = <0x01>`
作用:
- Gamma curve 选择
- 会影响亮度过渡、灰阶、色彩观感
---
### 11.5 像素格式与颜色相关
#### `colmod = <0x55>`
作用:
- 对应 `COLMOD`,即 Interface Pixel Format
- 决定总线传输的像素位宽
当前值 `0x55` 表示 `RGB565 / 16-bit` 模式,这与当前软件配置完全一致:
- `CONFIG_ST7789V_RGB565=y`
- `CONFIG_LV_Z_BITS_PER_PIXEL=16`
如果这里和软件配置不匹配,常见现象包括:
- 颜色错乱
- 红蓝交换
- 图像看起来像“马赛克”或数据错位
#### `ram-param = [ 00 f0 ]`
作用:
- 对应 `RAMCTRL`
- 控制显示 RAM 访问的一些底层行为
这类参数通常和像素格式、总线模式、厂商推荐初始化序列成组使用。
#### `rgb-param = [ c0 02 14 ]`
作用:
- 对应 `RGBCTRL`
- 设置 RGB 接口/时序相关参数
虽然当前走的是 SPI MIPI DBI不是传统 RGB 并口,但 ST7789V 仍要求这组寄存器初始化。
---
### 11.6 时序与前后肩参数
#### `porch-param = [ 0c 0c 00 33 33 ]`
作用:
- 对应 `PORCTRL`
- 设置 porch也就是显示时序中的前后肩、空白区参数
这些参数会影响:
- 帧时序稳定性
- 扫描边界
- 某些情况下的闪烁或边缘异常
这组值一般与具体模组匹配,建议保留已有验证值。
---
### 11.7 扩展命令与电源控制参数
#### `cmd2en-param = [ 5a 69 02 01 ]`
作用:
- 对应 `CMD2EN`
- 开启 ST7789V 扩展命令页
如果这一步配置错误,后面某些扩展寄存器写入可能根本不会生效。
#### `pwctrl1-param = [ a4 a1 ]`
作用:
- 对应 `PWCTRL1`
- 电源控制相关参数
这会影响面板驱动供电行为和显示稳定性。
---
### 11.8 Gamma 曲线表
#### `pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ]`
作用:
- 对应 `PVGAMCTRL`
- Positive Voltage Gamma Control 参数表
#### `nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ]`
作用:
- 对应 `NVGAMCTRL`
- Negative Voltage Gamma Control 参数表
这两组参数共同决定:
- 亮暗过渡
- 色调倾向
- 灰阶表现
调这些参数通常属于“画质微调”,不是基础 bring-up 阶段的首选手段。除非当前已经能稳定显示,只是观感明显不对,否则不建议优先改这里。
---
### 11.9 这些参数里最关键、最常改的是哪些
在实际移植中,最常需要改的是下面这几项:
- `width`
- `height`
- `x-offset`
- `y-offset`
- `mdac`
- `mipi-max-frequency`
- `colmod`
其中:
- `width/height/x-offset/y-offset/mdac`
主要决定方向、有效显示区域和坐标映射
- `mipi-max-frequency`
主要决定带宽上限
- `colmod`
主要决定像素格式是否与软件配置匹配
而像下面这些:
- `vcom`
- `gctrl`
- `vrhs`
- `vdvs`
- `lcm`
- `gamma`
- `porch-param`
- `cmd2en-param`
- `pwctrl1-param`
- `pvgam-param`
- `nvgam-param`
- `ram-param`
- `rgb-param`
更像是“面板模组初始化模板的一部分”,通常是在已有可工作基础上尽量保持不动。
---
### 11.10 当前项目的实用建议
对当前这块屏来说,后续如果还要继续调整显示方向或显示区域,建议按下面顺序操作:
1. 先改 `mdac`
2. 再配套检查 `width/height`
3. 最后再修 `x-offset/y-offset`
不要一上来同时改所有寄存器,否则很难判断到底是哪一项导致了:
- 图像颠倒
- 画面偏移
- 部分区域黑边
- 写入越界
对当前项目,真正应该高频修改的设备树参数,其实主要就是这 5 项:
- `width`
- `height`
- `x-offset`
- `y-offset`
- `mdac`

View File

@@ -12,20 +12,26 @@ mcuboot_pad:
app:
address: 0xc200
end_address: 0xf8000
end_address: 0x82000
region: flash_primary
size: 0xebe00
size: 0x75e00
mcuboot_primary:
address: 0xc000
end_address: 0xf8000
end_address: 0x82000
orig_span: &id001
- mcuboot_pad
- app
region: flash_primary
size: 0xec000
size: 0x76000
span: *id001
mcuboot_secondary:
address: 0x82000
end_address: 0xf8000
region: flash_primary
size: 0x76000
settings_storage:
address: 0xf8000
end_address: 0x100000

View File

@@ -88,6 +88,13 @@ CONFIG_LV_BUILD_EXAMPLES=n
CONFIG_LV_BUILD_DEMOS=n
CONFIG_LV_USE_LABEL=y
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_Z_AUTO_INIT=y
CONFIG_LV_Z_VDB_SIZE=25
CONFIG_LV_Z_BITS_PER_PIXEL=16
CONFIG_LV_Z_LVGL_MUTEX=y
CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y
CONFIG_LV_Z_FLUSH_THREAD=y
CONFIG_LV_Z_DOUBLE_VDB=y
CONFIG_LV_Z_MEM_POOL_SIZE=16384
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096

View File

@@ -1,12 +1,10 @@
#include <stdio.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/led.h>
#include <zephyr/kernel.h>
#include <app_event_manager.h>
#include <lvgl.h>
#include <lvgl_zephyr.h>
@@ -16,14 +14,17 @@
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define DISPLAY_UPDATE_PERIOD_MS 10
#define DISPLAY_UPDATE_PERIOD_MS 1000
#define DISPLAY_BACKLIGHT_BRIGHTNESS 100
struct display_ctx {
const struct device *dev;
struct k_work_delayable refresh_work;
lv_obj_t *hello_label;
struct display_capabilities caps;
struct k_work_delayable update_work;
lv_obj_t *title_label;
lv_obj_t *count_label;
uint32_t tick_count;
bool ui_ready;
bool initialized;
};
@@ -31,10 +32,70 @@ static struct display_ctx disp = {
.dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)),
};
static void display_refresh_work_fn(struct k_work *work)
static const struct led_dt_spec display_backlight =
LED_DT_SPEC_GET(DT_NODELABEL(backlight));
static void display_schedule_update(k_timeout_t delay)
{
char count_str[12];
uint32_t wait_ms;
#ifdef CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE
k_work_schedule_for_queue(lvgl_get_workqueue(), &disp.update_work, delay);
#else
k_work_reschedule(&disp.update_work, delay);
#endif
}
static int display_backlight_init(void)
{
int err;
if (!led_is_ready_dt(&display_backlight)) {
LOG_WRN("Display backlight device not ready");
return 0;
}
/*
* 背光亮度交给 pwm-leds 驱动管理,这样后面如果要做调光、呼吸灯或亮度档位,
* 都可以直接沿用 Zephyr 的 LED/PWM 接口,而不需要再单独碰 PWM 寄存器。
*/
err = led_set_brightness_dt(&display_backlight, DISPLAY_BACKLIGHT_BRIGHTNESS);
if (err) {
LOG_ERR("Failed to set backlight brightness: %d", err);
return err;
}
return 0;
}
static void display_create_ui_locked(void)
{
lv_obj_t *screen = lv_screen_active();
/*
* 先显式设置背景和文字颜色,避免把“有画面但颜色刚好看不见”误判为
* “LVGL 没有刷新”。这里使用高对比度配色,便于快速验证渲染链路。
*/
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_bg_color(screen, lv_color_hex(0x102A43), LV_PART_MAIN);
lv_obj_set_style_text_color(screen, lv_color_hex(0xF0F4F8), LV_PART_MAIN);
lv_obj_clean(screen);
disp.title_label = lv_label_create(screen);
lv_label_set_text(disp.title_label, "Zephyr LVGL running");
lv_obj_set_style_text_color(disp.title_label, lv_color_hex(0xF0F4F8), LV_PART_MAIN);
lv_obj_align(disp.title_label, LV_ALIGN_CENTER, 0, -16);
disp.count_label = lv_label_create(screen);
lv_label_set_text(disp.count_label, "tick 0");
lv_obj_set_style_text_color(disp.count_label, lv_color_hex(0xFFD166), LV_PART_MAIN);
lv_obj_align(disp.count_label, LV_ALIGN_CENTER, 0, 16);
disp.ui_ready = true;
}
static void display_update_work_fn(struct k_work *work)
{
char count_str[24];
lv_color_t bg_color;
ARG_UNUSED(work);
@@ -44,17 +105,21 @@ static void display_refresh_work_fn(struct k_work *work)
lvgl_lock();
if ((disp.tick_count % 100U) == 0U) {
snprintk(count_str, sizeof(count_str), "%u", disp.tick_count / 100U);
lv_label_set_text(disp.count_label, count_str);
if (!disp.ui_ready) {
display_create_ui_locked();
}
wait_ms = lv_timer_handler();
bg_color = ((disp.tick_count & 0x01u) == 0U) ? lv_color_hex(0x102A43) :
lv_color_hex(0x1F6F8B);
lv_obj_set_style_bg_color(lv_screen_active(), bg_color, LV_PART_MAIN);
snprintk(count_str, sizeof(count_str), "tick %u", disp.tick_count++);
lv_label_set_text(disp.count_label, count_str);
lv_obj_invalidate(lv_screen_active());
lvgl_unlock();
disp.tick_count++;
k_work_reschedule(&disp.refresh_work,
K_MSEC(MAX(wait_ms, DISPLAY_UPDATE_PERIOD_MS)));
display_schedule_update(K_MSEC(DISPLAY_UPDATE_PERIOD_MS));
}
static int display_demo_init(void)
@@ -66,22 +131,13 @@ static int display_demo_init(void)
return -ENODEV;
}
display_get_capabilities(disp.dev, &disp.caps);
LOG_INF("Display caps: %ux%u fmt=%d", disp.caps.x_resolution, disp.caps.y_resolution,
disp.caps.current_pixel_format);
k_work_init_delayable(&disp.update_work, display_update_work_fn);
disp.tick_count = 0U;
k_work_init_delayable(&disp.refresh_work, display_refresh_work_fn);
lvgl_lock();
lv_obj_clean(lv_screen_active());
disp.hello_label = lv_label_create(lv_screen_active());
lv_label_set_text(disp.hello_label, "LVGL demo running");
lv_obj_align(disp.hello_label, LV_ALIGN_CENTER, 0, -12);
disp.count_label = lv_label_create(lv_screen_active());
lv_label_set_text(disp.count_label, "0");
lv_obj_align(disp.count_label, LV_ALIGN_BOTTOM_MID, 0, -12);
lv_timer_handler();
lvgl_unlock();
disp.ui_ready = false;
err = display_blanking_off(disp.dev);
if (err) {
@@ -89,10 +145,15 @@ static int display_demo_init(void)
return err;
}
disp.initialized = true;
k_work_reschedule(&disp.refresh_work, K_MSEC(DISPLAY_UPDATE_PERIOD_MS));
err = display_backlight_init();
if (err) {
return err;
}
disp.initialized = true;
display_schedule_update(K_NO_WAIT);
LOG_INF("LVGL display demo initialized");
return 0;
}

View File

@@ -1 +1 @@
SB_CONFIG_BOOTLOADER_MCUBOOT=n
SB_CONFIG_BOOTLOADER_MCUBOOT=y