一、基本情况
本文代码基于以下软件版本:
Arduino2.X
LVGL8.3.11
GXEPD1.6.2
epdpaint---微雪EPD驱动的一部分,你可以在微雪的官网下载到
硬件:
MCU:ESP32-S3-N16R8
屏幕:GDEY042Z98黑白红三色墨水屏,某宝上买的。卖家给的资料是大连佳显的资料,但好像又不是大连佳显的屏,后面会说。驱动芯片是SSD1683。
二、LVGL中加载屏幕驱动
为什么要用LVGL来驱动?因为LVGL构建UI比较灵活快速呗。
之前写过一些关于LVGL的文章,提到了LVGL使用TFT-eSPI库来驱动显示屏的方法。
但是这次我们在TFT-eSPI中并没有找到合适的驱动。因为TFT-eSPI库是LVGL推荐的屏幕驱动库,所以刚开始时我还是尝试用厂商给的驱动来改,加入到TFT-eSPI库里面去,后来发现实在是有点麻烦,放弃了。
所以这次采用的是GxEPD2库驱动,因为该库里有GDEY042Z98的驱动。但是我们要用LVGL,而LVGL驱动的屏幕通常是普通液晶屏,所以要做一些调整,主要是颜色处理上。
所以,我们实际要做的是,把GxEPD2驱动适配到LVGL上去。
1.首先研究下GxEPD2
GxEPD2是一个专门驱动墨水屏的驱动库,支持的屏幕很多。
GxEPD2.cpp是其核心;再上一层是GXEPD2_3C.h、GXEPD2_4C.h、GXEPD2_7C.h、GXEPD2_BW.h分别是3色、4色、7色和黑白屏幕的适配层;然后再上一层是各种型号屏幕对应的封装,都放在不同的文件夹里了。
要使用该库驱动我手里的这块屏幕,只需要定义一个全局ePaper对象即可。
GxEPD2_3C<GxEPD2_420c_GDEY042Z98, GxEPD2_420c_GDEY042Z98::HEIGHT> ePaper(GxEPD2_420c_GDEY042Z98(/*CS=5*/ 11, /*DC=*/ 12, /*RST=*/ 9, /*BUSY=*/ 14));
注意定义ePaper对象时,我们已经选好了屏幕型号,屏幕颜色类型,同时把ESP32与屏幕之间连接的引脚(不包括SPI引脚)也传入进去了。
此外我们还要确定ESP32与墨水屏之间的SPI接口连接所使用的是哪个SPI口,引脚号是多少。这里全局定义使用HSPI。
SPIClass hspi(HSPI);
引脚我们需要在Arduino的setup部分代码去启动spi时传入,并将SPI口绑定到ePaper上去。
hspi.begin(/*SCK_PIN*/10, /*MISO_PIN*/3, /*MOSI_PIN*/13, /*CS_PIN*/11); // remap hspi for EPD (swap pins)
ePaper.epd2.selectSPI(hspi, SPISettings(1000000, MSBFIRST, SPI_MODE0));
到此GxEPD2就可以把屏幕驱动起来了。加入初始化代码,屏幕就会闪烁起来。
ePaper.init();
ePaper.setRotation(1);
ePaper.clearScreen();
2.LVGL如何与GxEPD对接
(1)首先我们要搞清楚LVGL刷新屏幕的工作机制
LVGL驱动显示屏时,会定义一个屏幕刷新回调函数my_disp_flush()之类的。
这个函数的主要任务,就是把你定义的显示缓存,通过MCU与屏幕的连接端口,不停地往屏幕里面去写,从而实现屏幕显示的刷新。
LVGL会在你的程序在主循环中加入这种代码:
void loop() {
lv_timer_handler(); /* let the GUI do its work */
delay(5);
}
这个就是在主循环中往LVGL核心发信号,通知其不停刷新屏幕(当然还有些其他任务)。
(2)驱动墨水屏与普通液晶屏幕的区别
墨水屏是一种断电后显示不会消失的屏幕,但其刷新也比较麻烦,也很慢。
通常墨水屏的刷新分为:全刷、快刷、局刷,搞不懂的可以问一下AI。
我们要明确一点,墨水屏掉电后,其显示还保持,但是其显示缓存掉电后是会被清空的。
全刷和快刷我们要写所有缓存,局刷我们是写局部缓存。
(3)让LVGL刷新虚拟屏幕
读到这里,我想你应该想到了,LVGL的工作机制下并不适合墨水屏,普通液晶屏几十帧的FPS,墨水屏根本接收不了。
所以呢,我们得考虑一种间接的办法。这里我们考虑在MCU的内存中创建一块虚拟的屏幕,让LVGL去不停地刷新这块虚拟屏幕。只有在我们程序逻辑需要的时候,我们才调用墨水屏的刷新程序去刷新屏幕。
这个虚拟屏幕就是EPDPaint。它的源代码可以在微雪的EPD驱动程序里面找到。
结合代码来讲。下面代码我们定义了两个虚拟屏幕。
EXT_RAM_ATTR uint8_t img_buf_BW[MY_DISP_HOR_RES * MY_DISP_VER_RES / 8]; //整屏的显示图像缓存,每bit对应一个点
EXT_RAM_ATTR uint8_t img_buf_R[MY_DISP_HOR_RES * MY_DISP_VER_RES / 8]; //整屏的显示图像缓存,每bit对应一个点
Paint paint_BW(img_buf_BW, MY_DISP_HOR_RES, MY_DISP_VER_RES); //Paint对象,操作img_buf图像缓存
Paint paint_R(img_buf_R, MY_DISP_HOR_RES, MY_DISP_VER_RES); //Paint对象,操作img_buf图像缓存
为什么要定义两块呢?因为三色墨水屏里面有两部分显存。
第一部分是黑白的,其大小是屏幕WIDTH*HEIGHT/8;显存的1bit对应屏幕上一个像素点,bit=0显示黑,bit=1显示白。
第二部分是红色专用,其大小同样是WIDTH*HEIGHT/8;同样显存的1bit对应屏幕上一个像素点。bit=1是显示红色。注意如果bit=1了,那么黑白显存部分的bit是0还是1都不起作用了。
搞懂了这个逻辑,我们就可以在LVGL的显示刷新回调函数里,编写一个转换程序,报LVGL的显存转换到这个虚拟屏幕里来。
上显示刷新回调函数的代码。
static void my_disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
Serial.print("area x1 =");Serial.println(area->x1);
Serial.print("area x2 =");Serial.println(area->x2);
Serial.print("area y1 =");Serial.println(area->y1);
Serial.print("area y2 =");Serial.println(area->y2);
uint8_t r,g,b;
uint8_t gray;
for(int i = area->y1; i <= area->y2; i ++){
for(int j = area->x1; j <= area->x2; j ++){
// 提取 RGB 分量
r = (color_p[(j-area->x1) + (i-area->y1)*w].ch.red);// 5 位红色
g = (color_p[(j-area->x1) + (i-area->y1)*w].ch.green); // 6 位绿色
b = (color_p[(j-area->x1) + (i-area->y1)*w].ch.blue); // 5 位蓝色
// 扩展到8位
r = (r * 255) / 31;
g = (g * 255) / 63;
b = (b * 255) / 31;
// 计算灰度值
gray = (r * 77 + g * 150 + b * 29) >> 8;
//Serial.println(gray);
// 判断颜色
if (gray < 128)
{ // 灰度值较低,可能是黑色或红色
if (r > g && r > b && r > 32) { // 红色分量较强
paint_R.DrawPixel(j, i, 0);
paint_BW.DrawPixel(j, i, 0);
}
else
{
paint_R.DrawPixel(j, i, 1);
paint_BW.DrawPixel(j, i, 0);
}
}
else
{ // 灰度值较高,判定为白色
paint_R.DrawPixel(j, i, 1);
paint_BW.DrawPixel(j, i, 1);
}
}
}
lv_disp_flush_ready(disp_drv);
}
这个回调函数里,我们把LVGL的RGB565这种16色的颜色,转换到我们定义的虚拟屏幕上。使用的是虚拟屏幕Paint的DrawPixel方法绘制每个像素点。
(4)LVGL部分
到此,还没有讲LVGL的部分。其实LVGL部分的代码和普通的LVGL适配并没有太大的区别。
上LVGL部分代码。
//在全局定义
static lv_disp_draw_buf_t draw_buf;
static lv_color_t * buf;
static lv_disp_drv_t disp_drv;
//在setup部分初始化
lv_init();
// 初始化 LVGL 显示缓冲区
buf = (lv_color_t*)ps_malloc(EPD_WIDTH * EPD_HEIGHT );
lv_disp_draw_buf_init(&draw_buf, buf, NULL, EPD_WIDTH * EPD_HEIGHT);
// 注册显示驱动
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = EPD_WIDTH; // 设置水平分辨率
disp_drv.ver_res = EPD_HEIGHT; // 设置垂直分辨率
disp_drv.flush_cb = my_disp_flush; // 设置刷新回调函数
disp_drv.draw_buf = &draw_buf; // 设置显示缓冲区
lv_disp_drv_register(&disp_drv); // 注册显示驱动
注意这里我定义的LVGL显示缓存并不是推荐的屏幕的1/10,而是整屏,因为我的ESP32-S3有外挂PSRAM,内存够用,所以豪横,我就用ps_malloc把LVGL的显示缓存定义到了PSRAM里。
注意我前面定义Paint虚拟屏幕的代码时,也用EXT_RAM_ATTR关键字把其定义在了PSRAM里。
内存不够的情况下,可以把LVGL显存定义小些的。LVGL会自动根据你定义的缓存大小,在每次屏幕刷新回调函数只刷新一部分屏幕。比如定义成屏幕大小的1/10,那么每次只刷新屏幕的1/10,需要调用10次回调函数,整个屏幕才刷新完毕。这个你可以在回调函数里通过打印area参数指向的x1、x2、y1、y2变量观察到。
(5)虚拟屏幕到真实屏幕的刷新
这个调用一下刷新接口就可以了。上一个全刷的代码。在你的程序逻辑中需要刷新屏幕时,调用该函数就行了。但要注意屏幕刷新是很慢的,所以要注意程序逻辑超时的问题,可以考虑采用单独线程来刷新屏幕,避免主程序逻辑超时。
void refresh_ePaper()
{
//因为存在本地图像缓冲区img_buf_BW和img_buf_R,因此每次刷新都是整屏刷
//整屏刷也可以用局刷的方式,但使用一段时候后要执行一次全刷,提升显示效果
//!!!如果要显示红色的东西,那么就需要全刷,因为局刷不支持红色
ePaper.clearScreen();
ePaper.drawImage((const uint8_t *)(paint_BW.GetImage()),(const uint8_t *)(paint_R.GetImage()),0,0,400,300,false,false,false);
}
局刷的代码嘛,因为我手里这块屏的局刷有问题,使用大连佳显的原厂代码局刷也不行,所以我暂时还没有研究。待我花重金买原厂屏后再来测试。
三、补充的内容
1.关于颜色
LVGL绘制东西的时候,尽量把颜色都定义清楚,因为颜色有个映射转换过程,不想出现你意想不到的情况,就把颜色定义成黑白红三种纯色。包括字体、图形的边框等,否则可能出现颜色不够深,在转换程序里转换成了黑色或白色显示不出来的情况。
比如:
lv_obj_set_style_text_color(label_Notes, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT);//字体定义成黑色
lv_obj_set_style_text_color(label_Notes, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);//字体定义成白色
lv_obj_set_style_text_color(label_Notes, lv_color_hex(0xFF0000), LV_PART_MAIN | LV_STATE_DEFAULT);//字体定义成红色
//lv_color_hex(0xFF0000)三个字节分别对应RGB分量
2.我要做个什么出来
做这个东西的其他一些点,有时间我会写出来。
全文到这里也就结束了。本人业余选手纯为了玩,专业选手勿喷。