当前位置: 首页 > news >正文

使用LVGL驱动三色墨水屏,Arduino

一、基本情况

本文代码基于以下软件版本:

    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.我要做个什么出来

做这个东西的其他一些点,有时间我会写出来。

全文到这里也就结束了。本人业余选手纯为了玩,专业选手勿喷。

相关文章:

  • Android进程间通信方式之AIDL
  • Oracle SQL优化实战要点解析(11)
  • fetch为什么加了允许跨域请求mode: ‘no-cors‘,添加的多个header就丢失了?
  • Android Broadcast广播封装
  • 博客系统自动化测试_测试报告
  • OceanBase-obcp-v3考试资料梳理
  • LLMs之Agent:Manus(一款通用人工智能代理)的简介、安装和使用方法、案例应用之详细攻略
  • Redis—01—分布式系统
  • 使用 Docker 部署 RabbitMQ 并实现数据持久化
  • Kubernetes 的正式安装
  • 【数分】Numpy入门及进阶(四)
  • 数据集路径出错.yaml‘ images not found , missing path
  • 【Java线程基础操作详解】
  • SpringBoot3—场景整合:AOT
  • [LeetCode]day34 347.前k个高频元素
  • 使用开源OPUS-MT模型进行文本翻译(python)
  • android中activity1和activity2中接收定时消息
  • (C/S)架构、(B/S)架构
  • 粉尘环境下的智能生产革命 ——助力矿山行业实现高效自动化作业
  • 第九篇《行军篇》
  • 从《缶翁的世界》看吴昌硕等湖州籍书画家对海派的影响
  • 2024年全国博物馆接待观众14.9亿人次
  • 上海市第二十届青少年科技节启动:为期半年,推出百余项活动
  • 北邮今年本科招生将首次突破四千人,新增低空技术与工程专业
  • 淮安市车桥中学党总支书记王习元逝世,终年51岁
  • 雅典卫城上空现“巨鞋”形状无人机群,希腊下令彻查