diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f0818a..f5c2a51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ target_sources(app PRIVATE src/modules/ble_battery_module.c src/modules/ble_bond_module.c src/modules/ble_slot_ctrl_module.c + src/modules/display_module.c src/modules/hid_tx_manager_module.c src/modules/keyboard_module.c src/modules/led_state_module.c diff --git a/app.overlay b/app.overlay index d4f648b..496849d 100644 --- a/app.overlay +++ b/app.overlay @@ -1,6 +1,14 @@ #include / { + chosen { + zephyr,display = &st7789v3; + }; + + aliases { + backlight = &backlight; + }; + zephyr,user { vbat-en-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>; io-channels = <&adc 5>, <&adc 7>; @@ -78,3 +86,23 @@ qdec: &qdec { status = "okay"; }; + +&spi3 { + status = "okay"; +}; + +&mipi_dbi { + status = "okay"; +}; + +&st7789v3 { + status = "okay"; +}; + +&pwm_leds { + status = "okay"; +}; + +&pwm0 { + status = "okay"; +}; diff --git a/docs/lvgl_zephyr_porting_notes.md b/docs/lvgl_zephyr_porting_notes.md new file mode 100644 index 0000000..ff159f9 --- /dev/null +++ b/docs/lvgl_zephyr_porting_notes.md @@ -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` diff --git a/prj.conf b/prj.conf index 08c00a7..ca2263d 100644 --- a/prj.conf +++ b/prj.conf @@ -5,15 +5,16 @@ CONFIG_ASSERT=y CONFIG_ASSERT_VERBOSE=y CONFIG_RESET_ON_FATAL_ERROR=n CONFIG_FAULT_DUMP=2 +CONFIG_SIZE_OPTIMIZATIONS=y -CONFIG_ZCBOR=y -CONFIG_BOOTLOADER_MCUBOOT=y -CONFIG_MCUMGR=y -CONFIG_MCUMGR_TRANSPORT_BT=y -CONFIG_MCUMGR_GRP_IMG=y -CONFIG_MCUMGR_GRP_OS=y -CONFIG_IMG_MANAGER=y -CONFIG_STREAM_FLASH=y +CONFIG_ZCBOR=n +CONFIG_BOOTLOADER_MCUBOOT=n +CONFIG_MCUMGR=n +CONFIG_MCUMGR_TRANSPORT_BT=n +CONFIG_MCUMGR_GRP_IMG=n +CONFIG_MCUMGR_GRP_OS=n +CONFIG_IMG_MANAGER=n +CONFIG_STREAM_FLASH=n CONFIG_BT=y CONFIG_BT_PERIPHERAL=y @@ -79,5 +80,22 @@ CONFIG_ADC=y CONFIG_I2C=y CONFIG_IP5305=y CONFIG_SENSOR=y +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_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 diff --git a/src/modules/display_module.c b/src/modules/display_module.c new file mode 100644 index 0000000..72c6ee5 --- /dev/null +++ b/src/modules/display_module.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#define MODULE display +#include + +#include +LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); + +#define DISPLAY_UPDATE_PERIOD_MS 1000 +#define DISPLAY_BACKLIGHT_BRIGHTNESS 100 + +struct display_ctx { + const struct device *dev; + 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; +}; + +static struct display_ctx disp = { + .dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)), +}; + +static const struct led_dt_spec display_backlight = + LED_DT_SPEC_GET(DT_NODELABEL(backlight)); + +static void display_schedule_update(k_timeout_t delay) +{ +#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); + + if (!disp.initialized) { + return; + } + + lvgl_lock(); + + if (!disp.ui_ready) { + display_create_ui_locked(); + } + + 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(); + + display_schedule_update(K_MSEC(DISPLAY_UPDATE_PERIOD_MS)); +} + +static int display_demo_init(void) +{ + int err; + + if (!device_is_ready(disp.dev)) { + LOG_ERR("Display device not ready"); + 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; + disp.ui_ready = false; + + err = display_blanking_off(disp.dev); + if (err) { + LOG_ERR("Display blanking off failed: %d", err); + return err; + } + + 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; +} + +static bool app_event_handler(const struct app_event_header *aeh) +{ + if (is_module_state_event(aeh)) { + const struct module_state_event *event = cast_module_state_event(aeh); + + if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) { + int err = display_demo_init(); + + if (err) { + module_set_state(MODULE_STATE_ERROR); + } else { + module_set_state(MODULE_STATE_READY); + } + } + + return false; + } + + __ASSERT_NO_MSG(false); + return false; +} + +APP_EVENT_LISTENER(MODULE, app_event_handler); +APP_EVENT_SUBSCRIBE(MODULE, module_state_event); diff --git a/sysbuild.conf b/sysbuild.conf index 721a76f..47f00ff 100644 --- a/sysbuild.conf +++ b/sysbuild.conf @@ -1 +1 @@ -SB_CONFIG_BOOTLOADER_MCUBOOT=y \ No newline at end of file +SB_CONFIG_BOOTLOADER_MCUBOOT=y