本教程展示如何使用 Espressif ESP32 WiFi/BLE SoC 为 LVGL 改造 Apple iPod Nano 6 的屏幕。
所有源代码可在本页末尾下载。
理解 MIPI DSI#
iPod Nano 6 的 LCD 使用 MIPI 显示串行接口(MIPI DSI),这是主机处理器与显示模块之间的高速串行接口。此类 LCD 在智能手机、平板电脑和智能手表中非常常见。参考规范可从 MIPI Alliance 页面获取。

在 Google 上搜索关键词 MIPI 会出现数百页的 PDF 文档。从像这样的规范中学习总是很有趣。它指出"MIPI DSI 规定了主机处理器与显示器之间的接口……",并附有如下图表:

如果 100 页的规范需要太多时间,这篇 EDN 文章可能就是您需要了解的关于 MIPI D'PHY RX 的全部内容。
MIPI 的传输速度非常高,从 1.0 Gbps/lane 到 4.5 Gbps/lane 不等,有 1-4 条数据通道加上 1 条时钟信号,全部使用差分总线。差分总线驱动的电压摆幅也与 RGB/MCU 类型的 LCD 不同。对于 MIPI DSI,有高速(HS)和低速(LS)模式,分别驱动 200mV 峰峰值和 1.2V,而 RGB/MCU 类型 LCD 的数据使用与 MCU 主机 VDDIO 匹配的单端信号传输。

通常,MIPI LCD 的接口需要的引脚数量和电压都远低于其 MCU/RGB 对应产品。

SSD2805 MIPI 桥接芯片#
问题是:当我们的 MCU(如 ESP32)没有 DSI 输出时,如何驱动 MIPI 显示屏,以及如何将其移植到 LVGL?这时就需要 MIPI 桥接 IC - SSD2805,它是一种接口芯片,可在 RGB/8080 视频信号和 MIPI 信号之间进行转换。

这是一个非常小的芯片,5×5mm,0.5mm 间距 BGA!

系统架构#

ESP32 使用 ESP-IDF(Espressif IoT 开发框架)编程。安装步骤在 Espressif 文档网站有详细描述。我的主机是 Windows 7 Pro SP1 64 位,Intel Core i5 处理器和 8GB 内存。我遵循 ESP-IDF 入门指南中描述的默认安装路径,该路径在 C:\msys32\ 下提供了 mingw32.exe 应用程序。

起初我对像 mingw32.exe 这样的命令行工具不太习惯。经过无数次 Google 搜索后,我尝试安装 Eclipse IDE。不幸的是,在 Eclipse 上花费的所有时间都白费了。最后我发现配置 Eclipse 的时间甚至比编程本身还多,所以我放弃了。不要误会 - Eclipse 并不差,我只是无法让它工作。
由于没有 ESP32 + SSD2805 + MIPI 显示屏组合的标准评估套件,我被迫使用跳线来连接所有东西,搞得一团糟,如下所示:
硬件设置#
使用的开发板包括:
- ESP32-Pico-Kit v4
- SSD2805 扩展板 Release 3
- 1.54 英寸 LH154Q01 MIPI 显示屏,PCB 上带有 CTP。SSD2541 CTP 驱动焊接在此板上。
- 加上大量跳线!
引脚图如下所示:

重要说明#
- 在 SSD2805 扩展板上,
VDDIO_CTRL引脚应拉低以导通VDDIO_CORE的 MOSFET。 EXT_5V应提供 5V 电源(USB 供电即可)。这是为背光控制器 IC 供电。- 背光控制器 IC 的 PWM 引脚应拉高(3.3V)以启用它。
- SSD2805 EVK 上的 DIP 开关设置从左到右为:
01000011(DIP 开关上 0=ON)。 - SSD2805 EVK 板上的
PCLK/RD#应拉高,而不是悬空。此引脚默认为下拉引脚。如果不需要读取操作,将其拉高以永久驱动RD#引脚为高电平。
软件开发#
要使用 LVGL,前提是在其外部有完全工作的 LCD 和触摸屏驱动。我从 5 个源文件的程序开始驱动 LCD:
1. i2s_8080_hello_world.c
2. SSD2805_8080_drv.c and .h
3. i2s_lcd.c and .h源文件 i2s_lcd.c 和 .h 从它们的 GitHub 源代码修改而来。
ESP32 使用 I2S 模块以 8080 8 位并行模式写入。DMA 用于排队命令和数据。
i2s_8080_lcd 项目的完整源代码可在本页末尾下载。
要编译此项目,将完整文件夹复制到方便的位置(我的情况是 D:\esp32\i2s_8080_lcd)。
从 C:/msys32 启动 mingw32.exe:

使用 cd D:/esp32/i2s_8080_lcd 切换到 Makefile 的根目录:

使用 make menuconfig 设置正确的串口:

浏览到 Serial flasher config 并将其设置为 COM2(我的情况):

多次点击 EXIT,最后点击 <Yes> 保存新配置。
最后一步是 make flash:

现在可以看到一个假的 Apple Watch!
下面的截图显示了 SSD2805 的所有公共函数。没有文本打印、图形绘制或帧缓冲操作。所有 GUI 相关功能都留给 LVGL,只有一个 API 函数 SSD2805_dispFlush(args),它的设计恰好符合要求 - 不多也不少。这也是需要刷新或更新屏幕时调用的唯一函数。

类似地,CTP 的驱动程序已开发并用一个基本程序进行了测试,该程序将手指坐标和压力打印到串口。SSD2541.h 的截图如下所示。API 函数 SSD2541_getPoint(args) 是 LVGL 唯一需要的接口。

在 mingw32 控制台中,输入 cd D:/esp32/SSD2541_drv_test,然后重复与 SSD2805 相同的步骤:make menuconfig,将 Serial flasher config 设置为 COM2(我的情况),保存更改,最后 make flash。这次我们需要一个终端程序,比如 Arduino 的串口监视器。下面的截图显示了串口监视器的数据流,手指从 (96,113) 释放,在 (88,117) 触摸并有不同的压力,然后再次释放。LVGL 要求触摸坐标应该是手指释放时的最后一个有效点。

移植到 LVGL#
一切似乎都准备好移植 LVGL 了。最终程序 littlevgl_port 演示了几个 LVGL 功能(不是全部),包括标签、按钮和图像显示。浏览到 components 文件夹,您会看到完全相同的 SSD2805 和 SSD2541 驱动。LVGL(版本 5.3 commit 17c19fc)直接从 GitHub 拉取。

在使用 LVGL 之前需要做几件事:
1. 修改 lv_conf.h#
从模板修改 lv_conf.h 以适配我们的屏幕分辨率。此头文件位于与 Makefile 相同的根目录,即项目目录。
/* Horizontal and vertical resolution of the library.*/
#define LV_HOR_RES (240)
#define LV_VER_RES (240)
#define LV_DPI 100c2. 定义显示刷新函数#
在主文件 littlevgl_example.c 中,定义一个本地函数来调用 SSD2805_dispFlush(args),然后用 lv_flush_ready() 通知 LVGL 屏幕刷新已准备就绪。
/**
* @brief API for LittlevGL with LV_VDB_SIZE!=0 in lv_conf.h
*/
static void ex_disp_flush(int32_t x1, int32_t y1, int32_t x2, int32_t y2, const lv_color_t * color_p)
{
SSD2805_dispFlush(x1, y1, x2, y2, (const uint16_t*)color_p);
lv_flush_ready();
}c3. 定义触摸读取函数#
在主文件 littlevgl_example.c 中,定义一个本地函数来调用 SSD2541_getPoint(args),将最后一个有效的手指位置存储到 data->point.x 和 data->point.y。LVGL 不需要压力值,因此传递 NULL。
/**
* @brief API for touch screen
*/
static bool ex_tp_read(lv_indev_data_t * data)
{
int16_t ctp_x, ctp_y;
bool sta = SSD2541_getPoint(&ctp_x, &ctp_y, NULL);
(sta==true)? (data->state = LV_INDEV_STATE_PR):(data->state = LV_INDEV_STATE_REL);
data->point.x = ctp_x;
data->point.y = ctp_y;
return false;
}c4. 定义时钟函数#
定义一个时钟函数作为 LVGL 的心跳,并为 ESP32 注册此函数。
/**
* @brief Heart beat for LittlevGL
*/
static void lv_tick_task(void)
{
lv_tick_inc(portTICK_RATE_MS);
}
//...
esp_register_freertos_tick_hook(lv_tick_task); //this is specific to ESP32c5. 初始化和注册#
最后一步是初始化 SSD2805、SSD2541、lv_init(),并注册 API 函数。
SSD2805_begin();
SSD2541_begin();
lv_init();
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.disp_flush = ex_disp_flush;
lv_disp_drv_register(&disp_drv);
lv_indev_drv_t indev_drv; /*Descriptor of an input device driver*/
lv_indev_drv_init(&indev_drv); /*Basic initialization*/
indev_drv.type = LV_INDEV_TYPE_POINTER; /*The touchpad is pointer type device*/
indev_drv.read = ex_tp_read; /*Library ready your touchpad via this function*/
lv_indev_drv_register(&indev_drv); /*Finally register the driver*/c结果#
结果是一个完全可操作的电容触摸面板,具有按钮、图像显示和文本打印功能!
