Stm32F103 + NuttX(五):点亮 LCD(FSMC 实现 8080 并口+LVGL)
发布于 2026-01-06
|
更新于
2026-01-20
|
字数:9515
硬件型号:正点原子 2.8 寸 TFTLCD 电阻触摸屏(ATK-MD0280)
LCD 控制芯片:ILI9341/ST7789
本文仅涉及屏幕显示内容,不涉及触摸。
本章代码:https://github.com/jklincn/nuttxspace/tree/series/ch05
相关知识学习
这里还是挺多东西要自学或参考的,列一个目录
需要参考的手册
- (重要)精英V2 硬件参考手册_V1.0
- ATK-MD0280模块使用说明_V1.4
- ATK-MD0280模块用户手册_V1.2
以及参考的代码示例
- nuttx/boards/arm/stm32/stm3210e-eval/src/stm32_lcd.c
- nuttx/boards/arm/stm32/stm3210e-eval/src/stm32_selectlcd.c
正点原子资料可以在以下链接免费获得(百度网盘):
概念了解
8080 并行接口
这个 LCD 屏幕是自带控制器芯片与显存的,其发光原理就是通过芯片发送指令完成,这里的格式是 RGB565,这应该由芯片自己完成,我们可以不用管这部分内容。
然后开发板和 LCD 控制器芯片的通信协议使用的是 Intel 8080-16 位并口,这部分介绍可以看【液晶屏小知识:8080接口详述,其他常用接口简介】。
8080 除了数据总线外,还有
- CS:片选信号(使能这个设备)
- WR:写信号
- RD:读信号
- RS(也叫 D/C):数据 Data / 命令 Command 选择信号
8080 16 位接口用 D0~D15 传数据,用 WR/RD 控制时序,用 RS 区分命令和数据。
8080 接口可以用 GPIO 模拟(CPU 占用高,慢),也可以用 FSMC 硬件直接实现(快)。
FSMC
FSMC(Flexible Static Memory Controller,灵活的静态存储控制器)是 STM32 系列微控制器中用于管理外部静态存储器的一种核心高速通信外设。它允许芯片直接访问外部 SRAM、NOR Flash、NAND Flash 设备,提供高速的地址/数据映射及可配置的访问时序,通过将外部设备映射到内部内存地址,极大地扩展了存储容量并简化了软件驱动开发。
关于 FSMC 可以看看【STM32-FSMC学习(1)】,再权威细节的可以看【STM32F1 RM0008 参考手册】。
FSMC 控制 LCD 的核心思想是借用 SRAM 的访问模型:
- 由于 LCD 的 8080 并口时序与异步 SRAM 接口在电气和时序上高度一致,我们可以将 LCD 当作一个特殊的 SRAM 外设进行访问。
- 因此,只需将 FSMC 的相关引脚与 LCD 控制芯片相连,FSMC 即可在硬件层面自动输出符合 8080 标准的并口通信时序,而无需软件模拟。
根据 STM32F1 的参考手册,应该选择 NOR Flash/PSRAM controller,
以下是一个接口信号的举例
但 LCD 的 8080 接口没有那么复杂,不需要这么多的信号线,精英开发板接的线如下图所示:
其中 LCD 模块的 34 个引脚的详细描述如下表所示:
可以看到物理连接对应关系:
- CS -> FSMC_NE4(表示 FSMC bank1 NOR/PSRAM 4)
- RS -> FSMC_A10
- WR -> FSMC_NWE
- RD -> FSMC_NOE
- D0~D15 -> FSMC_D0~D15
图形调用栈
主要是梳理一下从用户应用程序到最终硬件显示的全过程。
NuttX 中大概有 3 种做法:
-
纯硬件 Framebuffer(CONFIG_VIDEO_FB)
- 硬件基础:MCU 有专门的 LCD 控制器(如 STM32 的 LTDC)和足够的 RAM,
- 工作方式:CPU 往 RAM 里写数据,LCD 控制器自动把 RAM 里的数据搬运到屏幕上,CPU 不需要管刷新,写完 RAM 屏幕就变了。
- 优势:速度最快,CPU 负担最小。劣势:需要高级的硬件支持(LTDC + 大 RAM)
-
LCD 字符设备 (CONFIG_LCD_DEV)
- 硬件基础:普通的 MCU(如 F103) + 带显存的屏幕(如 ILI9341)。
- 工作方式:没有统一的显存,CPU 想画一个点,必须发指令给屏幕,CPU 必须主动刷新。
- 优势:省 RAM。劣势:刷新速度受限于接口速度(SPI/FSMC),CPU 负担较重。
-
LCD Framebuffer 适配层 (CONFIG_LCD_FRAMEBUFFER)
- 硬件基础:普通的 MCU + 较大的 RAM。
- 工作方式:软件模拟(NuttX 在 RAM 里保留一块区域当作是显存),应用程序使用 Framebuffer 往 RAM 里写数据,需要同步(当应用程序写完后必须告诉系统写入结束),适配层收到通知后,把这块 RAM 里的数据,通过慢速接口(SPI/FSMC)一点点搬运到屏幕上。
- 优势:兼容性好(所有基于 Framebuffer 的应用都能跑)。劣势:需要大 RAM,对于 240*320 的屏幕需要 240*320*16bit/2 = 150 KB,而我的 MCU 只有 64 KB 的 SRAM。
如果使用 LCD 字符设备,再搭配 LVGL(一个嵌入式开源图形库),从层级来说应该是:
- 用户应用层:创建按钮、标签、列表等对象,然后更新界面状态,驱动 LVGL 跑起来。
- 图形库层:LVGL 会管理对象树、样式、布局等等。当需要刷新时,LVGL 会计算刷新区域,把这些像素渲染到 draw buffer (与 FrameBuffer 相比这是较小的缓冲)。然后调用
flush_cb 进行刷新,这是 LVGL 往系统驱动层交付像素的唯一出口。
- 系统适配层:把 LVGL 的
flush_cb() 变成对 NuttX 显示设备的操作,由于采用 LCD 后端,因此这里对接的设备节点是 /dev/lcd0。
- NuttX 图形设备抽象层:即 LCD Character Driver,这层是 NuttX 提供的“统一 LCD 设备接口”,把“屏”包装成一个字符设备,即
/dev/lcd0。
- NuttX 屏幕驱动层:比如 ILI9341 驱动,这一层是了解 LCD 控制器协议的驱动,比如设置显示方向、设置写窗口、写 GRAM等,属于发送命令。
- 板级接口层:这一层负责把 NuttX 传来的命令真正变成硬件动作。通过 FSMC 实现 8080 接口,真正与 LCD 控制芯片进行通信,进行连续写数据寄存器。
- LCD 控制芯片:将最终像素是写进 LCD 内部显存进行显示。
还是比较复杂的,需要自己实现的应该是用户应用层(写个简单 Demo)、系统适配层、板级接口层这三个层级。
现有板级代码参考
首先参考一下现有板级代码,这里比较复杂,分两个配置来说。
stm3210e-eval:nx
NX 是 NuttX 系统原生的轻量级图形系统,它会启动一个 NX Server 负责管理硬件和窗口裁剪,应用程序是 NX Client,通过消息队列与 Server 通信。更详细的介绍:NX Graphics Subsystem
这块板子是使用 FSMC 做初始化的,可以从 stm32_selectlcd.c 中的 stm32_selectlcd 看到,然后该函数被 stm32_lcd.c 的 board_lcd_initialize 使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
void stm32_selectlcd(void)
{
/* Configure new GPIO state */
stm32_extmemgpios(g_commonconfig, NCOMMON_CONFIG);
stm32_extmemgpios(g_lcdconfig, NLCD_CONFIG);
/* Enable AHB clocking to the FSMC */
stm32_fsmc_enable();
/* Bank4 NOR/SRAM control register configuration */
putreg32(FSMC_BCR_SRAM | FSMC_BCR_MWID16 |
FSMC_BCR_WREN, STM32_FSMC_BCR4);
/* Bank4 NOR/SRAM timing register configuration */
putreg32(FSMC_BTR_ADDSET(1) | FSMC_BTR_ADDHLD(1) |
FSMC_BTR_DATAST(2) | FSMC_BTR_BUSTURN(1) |
FSMC_BTR_CLKDIV(1) | FSMC_BTR_DATLAT(2) |
FSMC_BTR_ACCMODA, STM32_FSMC_BTR4);
putreg32(0xffffffff, STM32_FSMC_BWTR4);
/* Enable the bank by setting the MBKEN bit */
putreg32(FSMC_BCR_MBKEN | FSMC_BCR_SRAM |
FSMC_BCR_MWID16 | FSMC_BCR_WREN, STM32_FSMC_BCR4);
}
|
但继续看下来,stm3210e-eval 并没有启用通用的 LCD 接口,可以从 .config 看出。
1
2
3
4
5
6
7
8
9
10
|
#
# LCD Driver Support
#
CONFIG_LCD=y
#
# Common Graphic LCD Settings
#
# CONFIG_LCD_DEV is not set
# CONFIG_LCD_FRAMEBUFFER is not set
|
它是在板级中定义了属于自己的 LCD 设备 stm3210e_dev_s,并直接与 NX 系统对接(NX 可以直接使用 lcd_dev_s 接口)。这与我们的想法就不太一样,因为我的 LCD 芯片是 ILI9341/ST7789,在 NuttX 中是有驱动支持的,位于 nuttx/drivers/lcd/ili9341.c。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct stm3210e_dev_s
{
/* Publicly visible device structure */
struct lcd_dev_s dev;
#if defined(CONFIG_STM3210E_LCD_BACKLIGHT) && defined(CONFIG_STM3210E_LCD_PWM)
uint32_t reload;
#endif
/* Private LCD-specific information follows */
uint8_t type; /* LCD type. See enum lcd_type_e */
uint8_t power; /* Current power setting */
};
|
因此这个板子只能参考一下 FSMC 的初始化与用法。
stm32f429i-disco:lvgl
这个配置是使用了 ILI9341 驱动,并且用的是 LVGL 而不是 NX。但它有 LTDC (LCD-TFT Display Controller) 硬件控制器,并且默认配置是 Framebuffer 接口,因此也只能参考一部分。(虽然可以通过改配置的方式切换到 LCD 接口,但因为代码中没有创建 /dev/lcd0 设备,因此走的是 LCD Framebuffer 适配层)
我们直接查看在 CONFIG_STM32F429I_DISCO_ILI9341_LCDIFACE 下的 board_lcd_initialize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
int board_lcd_initialize(void)
{
/* check if always initialized */
if (!g_lcd)
{
/* Initialize the sub driver structure */
struct ili9341_lcd_s *dev = stm32_ili93414ws_initialize();
/* Initialize public lcd driver structure */
if (dev)
{
/* Get a reference to valid lcd driver structure to avoid repeated
* initialization of the LCD Device. Also enables uninitializing of
* the LCD Device.
*/
g_lcd = ili9341_initialize(dev, ILI9341_LCD_DEVICE);
if (g_lcd)
{
return OK;
}
}
return -ENODEV;
}
return OK;
}
|
这里有一个 stm32_ili93414ws_initialize 函数,这里的 4ws 代表 4-Wire Serial (SPI),因此它是走 SPI 来和 ILI9341 通信的,具体驱动位于 stm32_ili93414ws.c 文件中。
因此这个板子只能参考一下 NuttX 提供的 ILI9341 驱动的用法,即
- 板级提供一个下半部驱动,返回内核定义的
ili9341_lcd_s 对象
- 使用内核定义的
ili9341_initialize 做初始化,返回内核定义的抽象层次更高的 lcd_dev_s 对象。
官方案例参考
原文链接:TTGO T-Display ESP32 board,用的配置是 ttgo_t_display_esp32:lvgl_lcd。
在 esp32_bringup 中注册驱动
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#ifdef CONFIG_LCD_DEV
ret = board_lcd_initialize();
if (ret < 0)
{
syslog(LOG_ERR, "ERROR: board_lcd_initialize() failed: %d\n", ret);
}
ret = lcddev_register(0);
if (ret < 0)
{
syslog(LOG_ERR, "ERROR: lcddev_register() failed: %d\n", ret);
}
#endif
|
-
board_lcd_initialize 和 board_lcd_getdev 定义在 boards/xtensa/esp32/common/src/esp32_st7789.c 中
board_lcd_initialize 通过配置与显示控制器相连的 SPI 接口,完成开发板上 LCD 硬件的初始化。
-
随后,lcddev_register 会调用 board_lcd_getdev。
board_lcd_getdev 内部会调用 st7789_lcdinitialize,并返回指定 LCD 对应的 LCD 对象引用。
st7789_lcdinitialize 是 LCD 屏幕驱动的一部分,位于 drivers/lcd/st7789.c 中。
-
LVGL 示例应用(lvgldemo)通过使用 ioctl 系统调用,向高层设备驱动发起 LCDDEVIO_PUTAREA 请求,以使用指定的数据刷新 LCD 屏幕:
1
|
ioctl(state.fd, LCDDEVIO_PUTAREA, (unsigned long)((uintptr_t)&lcd_area));;
|
这里的描述其实有一个坑,或者说是和上面的区别:
-
stm32f429i-disco:lvgl 中的 board_lcd_getdev 是直接返回全局 lcd_dev_s 对象:
1
2
3
4
5
6
7
8
9
|
struct lcd_dev_s *board_lcd_getdev(int lcddev)
{
if (lcddev == ILI9341_LCD_DEVICE)
{
return g_lcd;
}
return NULL;
}
|
其初始化函数写到了 board_lcd_initialize 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int board_lcd_initialize(void)
{
if (!g_lcd)
{
struct ili9341_lcd_s *dev = stm32_ili93414ws_initialize();
if (dev)
{
g_lcd = ili9341_initialize(dev, ILI9341_LCD_DEVICE);
if (g_lcd)
{
return OK;
}
}
return -ENODEV;
}
return OK;
}
|
-
而 esp32 中的描述是 st7789_lcdinitialize 由 board_lcd_getdev 调用,这里其实是实现了一个“懒加载”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct lcd_dev_s *board_lcd_getdev(int devno)
{
g_lcd = st7789_lcdinitialize(g_spidev);
if (g_lcd == NULL)
{
lcderr("ERROR: Failed to bind SPI port %d to LCD %d\n", DISPLAY_SPI,
devno);
return NULL;
}
lcdinfo("SPI port %d bound to LCD %d\n", DISPLAY_SPI, devno);
return g_lcd;
}
|
其 board_lcd_initialize 函数仅做了 SPI、GPIO 的初始化工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
int board_lcd_initialize(void)
{
g_spidev = esp32_spibus_initialize(DISPLAY_SPI);
if (!g_spidev)
{
lcderr("ERROR: Failed to initialize SPI port %d\n", DISPLAY_SPI);
return -ENODEV;
}
/* SPI RX is not used. Same pin is used as LCD Data/Command control */
esp32_configgpio(DISPLAY_DC, OUTPUT);
esp32_gpiowrite(DISPLAY_DC, true);
/* Pull LCD_RESET high */
esp32_configgpio(DISPLAY_RST, OUTPUT);
esp32_gpiowrite(DISPLAY_RST, false);
up_mdelay(1);
esp32_gpiowrite(DISPLAY_RST, true);
up_mdelay(10);
/* Set full brightness */
esp32_configgpio(DISPLAY_BCKL, OUTPUT);
esp32_gpiowrite(DISPLAY_BCKL, true);
lcdinfo("LCD successfully initialized");
return OK;
}
|
梳理思路
看了三个例子,大概也有数了,直接思路是这两个文件:
- 在
stm32_lcd.c 中实现 board_lcd_initialize 和 board_lcd_getdev,其中初始化包括:
- 使用
stm32_ili93414_fsmc_initialize(暂时取的名字) 进行 FSMC 配置,实例化 ili9341_lcd_s 对象。
- 使用
ili9341_initialize 传入 ili9341_lcd_s 从而实例化 lcd_dev_s 对象。
- 在
stm32_bringup.c 中先调用 board_lcd_initialize 进行板级初始化再调用 lcddev_register 进行 LCD 设备注册。这样后续 LVGL 就可以通过 /dev/lcd0 接口来绘制图形了。
开始实现
打开板级配置
还是先沿用 nsh 的配置,创建配置后进行修改:
-
System Type -> STM32 Peripheral Support -> FSMC(选中)
-
Device Drivers -> LCD Driver Support -> Graphic LCD Driver Support(选中)
-
Device Drivers -> LCD Driver Support -> Graphic LCD Driver Support -> LCD character device(选中)
-
Device Drivers -> LCD Driver Support -> Graphic LCD Driver Support -> LCD driver selection -> ILI9341 LCD Single Chip Driver(选中)
-
Device Drivers -> LCD Driver Support -> Graphic LCD Driver Support -> LCD driver selection -> (1) LCD Display(选中)
可以看到默认分辨率就是 240*320,因此不用再修改了,LCD Orientation 是选择屏幕方向(竖屏/横屏)
新配置保存为 myboard/atk-dnf103-v2/configs/lcd/defconfig。
FSMC 初始化
这里用了 Nuttx 自带的 STM32F103Z 的引脚定义,但可能出于稳妥的原因,旧版 stm32f103z_pinmap_legacy.h 中定义的速率都是 50MHz,而新版的都是 2MHz,对于 FSMC 这种外设来说太慢了,因此手动做了调整。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// nuttx/arch/arm/src/stm32/hardware/stm32f103z_pinmap.h
/* FSMC: NOR/PSRAM/SRAM (NPS) */
#define GPIO_NPS_A0_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTF|GPIO_PIN0)
...
#define GPIO_NPS_A25_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTG|GPIO_PIN14)
#define GPIO_NPS_D0_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTD|GPIO_PIN14)
...
#define GPIO_NPS_D15_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTD|GPIO_PIN10)
#define GPIO_NPS_CLK_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTD|GPIO_PIN3)
#define GPIO_NPS_NOE_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTD|GPIO_PIN4)
#define GPIO_NPS_NWE_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTD|GPIO_PIN5)
#define GPIO_NPS_NWAIT_0 (GPIO_INPUT|GPIO_CNF_INFLOAT|GPIO_MODE_INPUT|GPIO_PORTD|GPIO_PIN6)
#define GPIO_NPS_NE1_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTD|GPIO_PIN7)
#define GPIO_NPS_NE2_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTG|GPIO_PIN9)
#define GPIO_NPS_NE3_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTG|GPIO_PIN10)
#define GPIO_NPS_NE4_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTG|GPIO_PIN12)
#define GPIO_NPS_NBL0_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTE|GPIO_PIN0)
#define GPIO_NPS_NBL1_0 (GPIO_ALT|GPIO_CNF_AFPP|GPIO_MODE_2MHz|GPIO_PORTE|GPIO_PIN1)
|
因此有 GPIO 配置代码:
1
2
3
4
5
6
7
8
9
10
|
static const uint32_t g_lcd_pins[] = {
GPIO_NPS_NE4_0, GPIO_NPS_A10_0, GPIO_NPS_NOE_0, GPIO_NPS_NWE_0,
GPIO_NPS_D0_0, GPIO_NPS_D1_0, GPIO_NPS_D2_0, GPIO_NPS_D3_0,
GPIO_NPS_D4_0, GPIO_NPS_D5_0, GPIO_NPS_D6_0, GPIO_NPS_D7_0,
GPIO_NPS_D8_0, GPIO_NPS_D9_0, GPIO_NPS_D10_0, GPIO_NPS_D11_0,
GPIO_NPS_D12_0, GPIO_NPS_D13_0, GPIO_NPS_D14_0, GPIO_NPS_D15_0};
for (int i = 0; i < sizeof(g_lcd_pins) / sizeof(uint32_t); i++) {
stm32_configgpio(GPIO_ADJUST_MODE(g_lcd_pins[i], GPIO_MODE_50MHz));
}
|
后续小更新:关于新版旧版引脚定义区别,可以参考 STM32_USE_LEGACY_PINMAP 设置,官方的想法是引脚速度取决于 PCB 布线,应由用户在 board.h 中指定。这里更好的写法应该是在 board.h 中添加
1
|
#define GPIO_NPS_A0 (GPIO_ADJUST_MODE(GPIO_NPS_A0_0, GPIO_MODE_50MHz))
|
然后正常进行配置即可。本章中先不进行更改,后续章节会将 STM32_USE_LEGACY_PINMAP 设为 n。
注释原文:
config STM32_USE_LEGACY_PINMAP
bool "Use the legacy pinmap with GPIO_SPEED_xxx included."
default y
---help---
In the past, pinmap files included GPIO_SPEED_xxxMhz. These speed
settings should have come from the board.h as it describes the wiring
of the SoC to the board. The speed is really slew rate control and
therefore is related to the layout and can only be properly set
in board.h.
STM32_USE_LEGACY_PINMAP is provided, to allow lazy migration to
using pinmaps without speeds. The work required to do this can be aided
by running tools/stm32_pinmap_tool.py. The tools will take a board.h
file and a legacy pinmap and output the required changes that one needs
to make to a board.h file.
Eventually, STM32_USE_LEGACY_PINMAP will be deprecated and the legacy
pinmaps removed from NuttX. Any new boards added should set
STM32_USE_LEGACY_PINMAP=n and fully define the pins in board.h
关于 FSMC 寄存器的信息可以参考手册 19.5.6 NOR/PSRAM controller registers 这一节,也可以看《STM32F103 精英开发指南》(25.1.4 FSMC 关联寄存器简介) ,有三类寄存器,x 代表哪一个 Bank:
FSMC_BCRx:控制寄存器
FSMC_BTRx:读时序控制寄存器
FSMC_BWTRx:写时序控制寄存器(当 BCRx.EXTMOD = 1 时才生效)
配置 Bank4 控制寄存器:
-
SRAM 类型设备
-
数据总线宽度为 16 位
-
允许写操作
-
开启扩展模式 (EXTMOD)
FSMC 支持独立的读写时序控制(通过 EXTMOD 配合 BTR/BWTR)。这一点对驱动 TFT LCD 非常有用,因为 TFT LCD 在读操作时通常较慢,而写操作可以比较快。如果读写共用同一套时序参数,就只能以读操作为基准,导致写速度降低;或者在每次读操作前后动态修改 FSMC 时序参数,这样虽然能保证写速度,但实现复杂且不优雅。通过在初始化阶段开启扩展模式配置独立的读写时序,可以在保证读可靠性的同时,充分发挥写性能,并避免运行过程中频繁修改 FSMC 配置。
1
2
|
putreg32(FSMC_BCR_SRAM | FSMC_BCR_MWID16 | FSMC_BCR_WREN | FSMC_BCR_EXTMOD,
STM32_FSMC_BCR4);
|
配置读时序寄存器:设慢一点,保证读取稳定
ADDSET:地址建立周期,可以理解为 RD/WR 的高电平时间。
DATAST:数据建立周期,可以理解为 RD/WR 的低电平时间。
ACCMODA:选择异步访问模式 A
1
2
|
putreg32(FSMC_BTR_ADDSET(1) | FSMC_BTR_DATAST(16) | FSMC_BTR_ACCMODA,
STM32_FSMC_BTR4);
|
配置写时序控制寄存器:设快一点,保证刷屏速度
1
2
|
putreg32(FSMC_BTR_ADDSET(1) | FSMC_BTR_DATAST(2) | FSMC_BTR_ACCMODA,
STM32_FSMC_BWTR4);
|
时序控制这些参数需要根据 LCD 控制器的数据手册(比如 ILI9341_DS)来调,否则容易出现花屏、读写错误、死机等问题(时序这部分也可以看看 《STM32F103 精英开发指南》(25.1.3 FSMC 简介)
加入 FSMC_BCR_MBKEN,使能 Bank4:
putreg32(FSMC_BCR_MBKEN | FSMC_BCR_SRAM | FSMC_BCR_MWID16 | FSMC_BCR_WREN,
STM32_FSMC_BCR4);
ILI9341 板级接口
由于我们采用的是 FSMC ,它会自动控制 WR/RD/CS 等这些信号,并且做了内存映射的,只需要往固定的内存地址写数据就行了。
根据 ST 官方 Datasheet 中对内存布局的描述,这里的基地址是 0x6C000000。
这里涉及到 STM32 FSMC 在 16位数据宽度下的地址映射机制。
STM32 的 FSMC 控制器在驱动 16 位宽度的存储器时,为了保证地址对齐,内部的地址线(HADDR)和外部引脚(FSMC_A)之间存在错位对应关系。
在 16 位模式下,CPU 每次读写都是 2 个字节(16位)。
- CPU 地址
0x00 -> 对应第 0 个 16位数据
- CPU 地址
0x02 -> 对应第 1 个 16位数据
- CPU 地址
0x04 -> 对应第 2 个 16位数据
为了让外部存储器也能正确索引到第 0、1、2 个数据,STM32 硬件自动做了如下映射:
- 内部地址 HADDR[0] 不接 FSMC 引脚
- 内部地址 HADDR[1] —> 输出到外部引脚 FSMC_A0
- 内部地址 HADDR[2] —> 输出到外部引脚 FSMC_A1
- …
- 内部地址 HADDR[n+1] —> 输出到外部引脚 FSMC_An
结论就是:内部地址位 = 外部引脚号 + 1
我们用于 LCD 命令、数据控制信号的 RS 是接在了 FSMC_A10 上面,因此内部的地址应该是 HADDR[11] 为 1,因此这里的偏移量是:2^11 = 2048 = 0x800。
因此有
1
2
|
#define LCD_CMD (*(volatile uint16_t *)(LCD_BASE))
#define LCD_DATA (*(volatile uint16_t *)(LCD_BASE + 0x800))
|
- 写命令时,访问基地址是 0x6C000000(…0000 0000 0000),HADDR[11] = 0,映射结果 FSMC_A10 = 0,即 RS = 0
- 写数据时,访问基地址是 0x6C000800(…1000 0000 0000),HADDR[11] = 1,映射结果 FSMC_A10 = 1,即 RS = 1
然后看一下我们需要补充哪些接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// nuttx/include/nuttx/lcd/ili9341.h
struct ili9341_lcd_s
{
void (*select)(FAR struct ili9341_lcd_s *lcd);
void (*deselect)(FAR struct ili9341_lcd_s *lcd);
int (*sendcmd)(FAR struct ili9341_lcd_s *lcd, const uint8_t cmd);
int (*sendparam)(FAR struct ili9341_lcd_s *lcd, const uint8_t param);
int (*recvparam)(FAR struct ili9341_lcd_s *lcd, uint8_t *param);
int (*recvgram)(FAR struct ili9341_lcd_s *lcd,
uint16_t *wd, uint32_t nwords);
int (*sendgram)(FAR struct ili9341_lcd_s *lcd,
const uint16_t *wd, uint32_t nwords);
int (*backlight)(FAR struct ili9341_lcd_s *lcd, int level);
};
|
select/deselect:选中/取消选中 LCD,在 SPI 下是拉低/拉高 CS,由于 FSMC 是自动控制 NE,因此这两个函数空实现就可以了。
sendcmd:发送 ILI9341 命令
sendparam:发送命令参数(数据)
recvparam:从 LCD 读回 1 个参数
recvgram:从 LCD GRAM 读像素
sendgram:往 LCD 的 GRAM 里写像素数据(刷屏函数)
backlight:调背光亮度。ILI9341 本身不控制背光,这是平台相关的,从精英硬件参考手册可以看到连接在 PB0 上
根据定义与 FSMC 的内存映射机制,我们就可以写出 ILI9341 的板级接口代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
static void stm32_ili9341_select(FAR struct ili9341_lcd_s *lcd) {
/* FSMC is memory mapped, no chip select handling needed here */
}
static void stm32_ili9341_deselect(FAR struct ili9341_lcd_s *lcd) {
/* FSMC is memory mapped, no chip select handling needed here */
}
static int stm32_ili9341_sendcmd(FAR struct ili9341_lcd_s *lcd,
const uint8_t cmd) {
LCD_CMD = cmd;
return OK;
}
static int stm32_ili9341_sendparam(FAR struct ili9341_lcd_s *lcd,
const uint8_t param) {
LCD_DATA = param;
return OK;
}
static int stm32_ili9341_recvparam(FAR struct ili9341_lcd_s *lcd,
uint8_t *param) {
*param = LCD_DATA;
return OK;
}
static int stm32_ili9341_sendgram(FAR struct ili9341_lcd_s *lcd,
const uint16_t *wd, uint32_t nwords) {
while (nwords--) {
LCD_DATA = *wd++;
}
return OK;
}
static int stm32_ili9341_recvgram(FAR struct ili9341_lcd_s *lcd, uint16_t *wd,
uint32_t nwords) {
while (nwords--) {
*wd++ = LCD_DATA;
}
return OK;
}
static int stm32_ili9341_backlight(FAR struct ili9341_lcd_s *lcd, int level) {
stm32_gpiowrite(GPIO_LCD_BACKLIGHT, level > 0);
return OK;
}
|
具体的 FSMC 驱动 LCD 刷屏流程如下:
- 通过 0x2A 和 0x2B 命令告诉 LCD 芯片像素刷新区域
- 通过 0x2C 命令告诉 LCD 芯片要开始传输像素数据(往 GRAM 中写数据)
- CPU 往 LCD_DATA 地址(0x6C000000)持续写入数据,FSMC 控制器检测到有写入时会自动产生时序:拉低片选(NE4)、拉低写信号(NWE)、拉高数据/命令选择线(A10,表示这是数据),并把 16 位颜色数据放到数据总线(D0-D15)上。
- LCD 芯片检测到写信号,接收这个 16 位颜色值,把它写入到对应的显存位置,然后计数器自动递增,写入偏移量自动指向到下一个像素位置。
修改 stm32_bringup
LCD 的初始化函数准备完成后,就可以加入到 stm32_bringup 中,并调用 lcddev_register 来进行设备注册。
1
2
3
4
5
6
7
8
9
10
11
12
|
#ifdef CONFIG_LCD
/* Initialize the LCD and get the LCD device instance */
ret = board_lcd_initialize();
if (ret < 0) {
syslog(LOG_ERR, "ERROR: board_lcd_initialize() failed: %d\n", ret);
}
ret = lcddev_register(0);
if (ret < 0) {
syslog(LOG_ERR, "ERROR: lcddev_register() failed: %d\n", ret);
}
#endif
|
正点原子的坑:混用 LCD 芯片型号
以上内容都是基于 ILI9341 芯片来写的,但实际上正点原子这块屏幕芯片写的是:ILI9341/ST7789,应该是混用的,因此需要先确定一下到底是什么芯片。
配置一下,把系统日志输出到控制台:
- Device Drivers -> System Logging -> Log to /dev/console(选中)
加入 stm32_read_id 代码,并把它加在 stm32_ili9341_fsmc_initialize 函数中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
static void stm32_read_id(void) {
uint16_t id;
uint16_t param[4];
/* Try ILI9341 ID (0xD3) */
LCD_CMD = 0xD3;
param[0] = LCD_DATA; /* Dummy */
param[1] = LCD_DATA; /* 0x00 */
param[2] = LCD_DATA; /* 0x93 */
param[3] = LCD_DATA; /* 0x41 */
id = ((param[2] << 8) | param[3]);
syslog(LOG_INFO, "LCD: Read ID (0xD3): %04x %04x %04x %04x -> ID: %04x\n",
param[0], param[1], param[2], param[3], id);
if (id == 0x9341) {
syslog(LOG_INFO, "LCD: Found ILI9341\n");
return;
}
/* Try ST7789 ID (0x04) */
LCD_CMD = 0x04;
param[0] = LCD_DATA; /* Dummy */
param[1] = LCD_DATA; /* ID1 */
param[2] = LCD_DATA; /* ID2 */
param[3] = LCD_DATA; /* ID3 */
syslog(LOG_INFO, "LCD: Read ID (0x04): %04x %04x %04x %04x\n",
param[0], param[1], param[2], param[3]);
if (param[2] == 0x85 && param[3] == 0x52) {
syslog(LOG_INFO, "LCD: Found ST7789\n");
}
}
struct ili9341_lcd_s *stm32_ili9341_fsmc_initialize(void) {
struct ili9341_lcd_s *priv = &g_lcddev;
stm32_fsmc_init();
stm32_read_id();
/* Initialize structure */
priv->select = stm32_ili9341_select;
priv->deselect = stm32_ili9341_deselect;
priv->sendcmd = stm32_ili9341_sendcmd;
priv->sendparam = stm32_ili9341_sendparam;
priv->recvparam = stm32_ili9341_recvparam;
priv->sendgram = stm32_ili9341_sendgram;
priv->recvgram = stm32_ili9341_recvgram;
priv->backlight = stm32_ili9341_backlight;
return priv;
}
|
结果输出一看,人麻了
1
2
3
|
LCD: Read ID (0xD3): 00d3 0000 0000 0000 -> ID: 0000
LCD: Read ID (0x04): 0004 0085 0085 0052
LCD: Found ST7789
|
本来还想着我只要再适配一个类似 st7789_lcd_s 的板级接口,再用 st7899_initialize 来初始化一下即可完成迁移 ,没想到一看代码发现人家的接口是 spi_dev_s,st7789_dev_s 和 SPI 绑死了。然而正点的触摸屏模块通信接口也写死了是 8080-16 位并口,因此这里的代码没法复用。(并且 FSMC 也比 SPI 要快得多)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// nuttx/include/nuttx/lcd/st7789.h
FAR struct lcd_dev_s *st7789_lcdinitialize(FAR struct spi_dev_s *spi);
// nuttx/drivers/lcd/st7789.c
struct st7789_dev_s
{
/* Publicly visible device structure */
struct lcd_dev_s dev;
/* Private LCD-specific information follows */
FAR struct spi_dev_s *spi; /* SPI device */
uint8_t bpp; /* Selected color depth */
uint8_t power; /* Current power setting */
#ifdef CONFIG_LCD_DYN_ORIENTATION
uint16_t xoff;
uint16_t yoff;
#endif
/* This is working memory allocated by the LCD driver for each LCD device
* and for each color plane. This memory will hold one raster line of data.
* The size of the allocated run buffer must therefore be at least
* (bpp * xres / 8). Actual alignment of the buffer must conform to the
* bitwidth of the underlying pixel type.
*
* If there are multiple planes, they may share the same working buffer
* because different planes will not be operate on concurrently. However,
* if there are multiple LCD devices, they must each have unique run
* buffers.
*/
uint16_t runbuffer[ST7789_LUT_SIZE];
};
|
感觉天塌了,无奈咨询了一下 AI,好在两个芯片在绘图指令上是兼容的,都遵循 MIPI DCS 标准,区别仅在于初始化序列(Gamma、电源、反色等)。那么就保留 ILI9341 的接口(因为在 ST7789 上也是可以用的),然后只做一个 ST7789 的特殊初始化工作即可(这里看正点原子的教程上是说不同的初始化代码都由 LCD 厂家提供,所以他们适配了很多芯片型号)。
修改 board_lcd_initialize,如果芯片型号是 ST7789,那就再应用一次初始化。虽然 ili9341_initialize 会执行 ili9341_hwinitialize 进行 ILI9341 芯片的硬件初始化,但一般对 ST7789 也不会产生负面影响,我们后续马上使用 stm32_st7789_init 来重新设置所有参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
int board_lcd_initialize(void) {
if (!g_lcd) {
struct ili9341_lcd_s *dev = stm32_ili9341_fsmc_initialize();
if (dev) {
g_lcd = ili9341_initialize(dev, ILI9341_LCD_DEVICE);
if (g_lcd) {
if (stm32_get_lcd_id() == 0x8552) {
syslog(LOG_INFO, "LCD: Applying ST7789 initialization\n");
stm32_st7789_init(dev);
}
return OK;
}
}
return -ENODEV;
}
return OK;
}
|
在 stm32_ili9341_fsmc.c 中加入 stm32_st7789_init,这里参考了 RT-Thread 的初始化命令序列(修改了屏幕方向、关闭了颜色反转)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
void stm32_st7789_init(struct ili9341_lcd_s *lcd) {
lcd->select(lcd);
/* Memory Data Access Control */
lcd->sendcmd(lcd, 0x36);
lcd->sendparam(lcd, 0xA0); // Landscape (MV=1, MY=1, MX=0), RGB
/* RGB 5-6-5-bit */
lcd->sendcmd(lcd, 0x3A);
lcd->sendparam(lcd, 0x55);
/* Porch Setting */
lcd->sendcmd(lcd, 0xB2);
lcd->sendparam(lcd, 0x0C);
lcd->sendparam(lcd, 0x0C);
lcd->sendparam(lcd, 0x00);
lcd->sendparam(lcd, 0x33);
lcd->sendparam(lcd, 0x33);
/* Gate Control */
lcd->sendcmd(lcd, 0xB7);
lcd->sendparam(lcd, 0x35);
/* VCOM Setting */
lcd->sendcmd(lcd, 0xBB);
lcd->sendparam(lcd, 0x19);
/* LCM Control */
lcd->sendcmd(lcd, 0xC0);
lcd->sendparam(lcd, 0x2C);
/* VDV and VRH Command Enable */
lcd->sendcmd(lcd, 0xC2);
lcd->sendparam(lcd, 0x01);
/* VRH Set */
lcd->sendcmd(lcd, 0xC3);
lcd->sendparam(lcd, 0x12);
/* VDV Set */
lcd->sendcmd(lcd, 0xC4);
lcd->sendparam(lcd, 0x20);
/* Frame Rate Control */
lcd->sendcmd(lcd, 0xC6);
lcd->sendparam(lcd, 0x0F);
/* Power Control 1 */
lcd->sendcmd(lcd, 0xD0);
lcd->sendparam(lcd, 0xA4);
lcd->sendparam(lcd, 0xA1);
/* Positive Voltage Gamma Control */
lcd->sendcmd(lcd, 0xE0);
lcd->sendparam(lcd, 0xD0);
lcd->sendparam(lcd, 0x04);
lcd->sendparam(lcd, 0x0D);
lcd->sendparam(lcd, 0x11);
lcd->sendparam(lcd, 0x13);
lcd->sendparam(lcd, 0x2B);
lcd->sendparam(lcd, 0x3F);
lcd->sendparam(lcd, 0x54);
lcd->sendparam(lcd, 0x4C);
lcd->sendparam(lcd, 0x18);
lcd->sendparam(lcd, 0x0D);
lcd->sendparam(lcd, 0x0B);
lcd->sendparam(lcd, 0x1F);
lcd->sendparam(lcd, 0x23);
/* Negative Voltage Gamma Control */
lcd->sendcmd(lcd, 0xE1);
lcd->sendparam(lcd, 0xD0);
lcd->sendparam(lcd, 0x04);
lcd->sendparam(lcd, 0x0C);
lcd->sendparam(lcd, 0x11);
lcd->sendparam(lcd, 0x13);
lcd->sendparam(lcd, 0x2C);
lcd->sendparam(lcd, 0x3F);
lcd->sendparam(lcd, 0x44);
lcd->sendparam(lcd, 0x51);
lcd->sendparam(lcd, 0x2F);
lcd->sendparam(lcd, 0x1F);
lcd->sendparam(lcd, 0x1F);
lcd->sendparam(lcd, 0x20);
lcd->sendparam(lcd, 0x23);
/* Display Inversion Off */
lcd->sendcmd(lcd, 0x20);
/* Sleep Out */
lcd->sendcmd(lcd, 0x11);
up_mdelay(100);
/* Display On */
lcd->sendcmd(lcd, 0x29);
/* Turn on backlight */
lcd->backlight(lcd, 1);
lcd->deselect(lcd);
}
|
继续加入一个型号判定,这样在 FSMC 初始化之后就先判定当前芯片型号,并赋给全局变量 g_lcd_id。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
uint16_t g_lcd_id = 0;
uint16_t stm32_get_lcd_id(void) { return g_lcd_id; }
static void stm32_read_id(void) {
uint16_t id;
uint16_t param[4];
/* Try ILI9341 ID (0xD3) */
LCD_CMD = 0xD3;
param[0] = LCD_DATA; /* Dummy */
param[1] = LCD_DATA; /* 0x00 */
param[2] = LCD_DATA; /* 0x93 */
param[3] = LCD_DATA; /* 0x41 */
id = ((param[2] << 8) | param[3]);
if (id == 0x9341) {
g_lcd_id = 0x9341;
syslog(LOG_INFO, "LCD: Found ILI9341\n");
return;
}
/* Try ST7789 ID (0x04) */
LCD_CMD = 0x04;
param[0] = LCD_DATA; /* Dummy */
param[1] = LCD_DATA; /* ID1 */
param[2] = LCD_DATA; /* ID2 */
param[3] = LCD_DATA; /* ID3 */
if (param[2] == 0x85 && param[3] == 0x52) {
g_lcd_id = 0x8552;
syslog(LOG_INFO, "LCD: Found ST7789\n");
}
}
|
这样重新配置上板后应该就正常了:
LCD: Found ST7789
NuttShell (NSH) NuttX-12.12.0
nsh> ls /dev
/dev:
console
lcd0
ttyS0
nsh>
小测试
在 myapps 下创建了一个 lcdtest,这里只放源码,其余的 Kconfig 和 CMakeLists.txt 可以参考以往文章或本章源码仓库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
#include <fcntl.h>
#include <nuttx/config.h>
#include <nuttx/lcd/lcd_dev.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int fd;
struct fb_videoinfo_s vinfo;
struct lcddev_area_s area;
uint16_t *buffer;
int ret;
int i;
printf("Opening /dev/lcd0...\n");
fd = open("/dev/lcd0", O_RDWR);
if (fd < 0) {
perror("Error opening /dev/lcd0");
return 1;
}
printf("Getting video info...\n");
ret = ioctl(fd, LCDDEVIO_GETVIDEOINFO, (unsigned long)((uintptr_t)&vinfo));
if (ret < 0) {
perror("Error getting video info");
close(fd);
return 1;
}
printf("Resolution: %d x %d, Format: %d\n", vinfo.xres, vinfo.yres,
vinfo.fmt);
/* Allocate buffer for a chunk of lines (e.g., 10 lines) to save SRAM */
int chunk_lines = 10;
size_t bufsize = vinfo.xres * chunk_lines * 2;
buffer = (uint16_t *)malloc(bufsize);
if (!buffer) {
perror("Error allocating buffer");
close(fd);
return 1;
}
uint16_t colors[] = {0xF800, 0x07E0, 0x001F};
const char *color_names[] = {"Red", "Green", "Blue"};
for (int c = 0; c < 3; c++) {
printf("Filling %s...\n", color_names[c]);
/* Fill buffer with current color */
for (i = 0; i < vinfo.xres * chunk_lines; i++)
buffer[i] = colors[c];
area.col_start = 0;
area.col_end = vinfo.xres - 1;
area.stride = vinfo.xres * 2;
area.data = (uint8_t *)buffer;
for (int y = 0; y < vinfo.yres; y += chunk_lines) {
int height = chunk_lines;
if (y + height > vinfo.yres)
height = vinfo.yres - y;
area.row_start = y;
area.row_end = y + height - 1;
ioctl(fd, LCDDEVIO_PUTAREA, (unsigned long)((uintptr_t)&area));
}
sleep(1);
}
free(buffer);
close(fd);
return 0;
}
|
运行后应该可以成功看到屏幕开始变色,顺序为红、绿、蓝。
LVGL Demo
集成基本步骤
参考文档:Learn the Basics - LVGL 9.5 documentation
集成 LVGL 的步骤:
- 驱动初始化:这是用户的职责,需要配置好时钟、定时器、外设等。
- 调用
lv_init():初始化 LVGL 本身。
- 创建显示和输入设备:创建显示设备(
lv_display_t)和输入设备(lv_indev_t),并设置它们的回调函数。
- 创建用户界面:调用 LVGL 的 API 创建屏幕、控件、样式、动画、事件等。
- 在循环中调用
lv_timer_handler():这个函数负责处理所有 LVGL 相关的任务,包括:
- 刷新显示
- 读取输入设备
- 根据用户输入(和其他条件)触发事件
- 运行动画
- 运行用户创建的定时器
我们前面已经完成了第一步驱动初始化,后续的流程都在 LVGL Demo 中添加即可。
开启相关配置
启用 LVGL
- Application Configuration -> Graphics Support -> Light and Versatile Graphic Library (LVGL)(选中)
以下起始路径为 Light and Versatile Graphic Library (LVGL) -> LVGL configuration,主要是为了优化内存管理
- Memory Setting -> Malloc functions source -> Standard C functions malloc/realloc/free(选中)
- Memory Setting -> String functions source -> Standard C functions memcpy/memset/strlen/strcpy(选中)
- Memory Setting -> Sprintf functions source -> Standard C functions vsnprintf(选中)
- Devices -> Use Nuttx to open window and handle touchscreen(选中)
- Devices -> Use NuttX LCD device(选中)
- Devices -> NuttX LCD buffer size -> Custom-sized buffer(选中)
- Devices -> Custom partial buffer size -> 设置为 40
注意这里在 Devices 中选中了 Nuttx 相关支持(即使用 apps/graphics/lvgl/lvgl/src/drivers/nuttx 下的代码),这样的好处是在 NuttX 中可以简化我们的 App 代码,缺点是失去了部分可移植性。
最后启用 Demo
- Application Configuration -> My Apps -> LVGL Demo(选中)
这个配置保存为 myboard/atk-dnf103-v2/configs/lvgl_demo/defconfig
代码
这部分可以参考 apps/examples/lvgldemo/lvgldemo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
#include <nuttx/config.h>
#include <stdio.h>
#include <unistd.h>
#include "lvgl/src/drivers/nuttx/lv_nuttx_entry.h"
#include <lvgl/lvgl.h>
/****************************************************************************
* Private Functions
****************************************************************************/
static void create_ui(void) {
lv_obj_t *label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "Hello LVGL!");
lv_obj_center(label);
}
/****************************************************************************
* Public Functions
****************************************************************************/
int main(int argc, char *argv[]) {
lv_nuttx_dsc_t info;
lv_nuttx_result_t result;
/* 1. Initialize LVGL */
if (lv_is_initialized()) {
printf("LVGL already initialized! aborting.\n");
return -1;
}
lv_init();
/* 2. Initialize NuttX driver descriptor */
lv_nuttx_dsc_init(&info);
/* Use hardware LCD driver at /dev/lcd0 */
info.fb_path = "/dev/lcd0";
/* 3. Initialize NuttX backend (creates display, input, etc.) */
lv_nuttx_init(&info, &result);
if (result.disp == NULL) {
printf("NuttX LVGL driver initialization failure!\n");
return 1;
}
/* 4. Create UI */
create_ui();
/* 5. Enter main loop */
/* The built-in runner handles timer handler and sleeping */
/* lv_nuttx_run will loop forever unless using libuv with specific exit cond
*/
printf("Starting LVGL loop...\n");
lv_nuttx_run(&result);
return 0;
}
|
最终结果
由于加入了 LVGL,编译和烧录的速度都会有所降低,Flash 资源占用达到了 44.9%。
这里贴一个实际运行的图