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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/982504.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DeepSeek未来发展趋势:开创智能时代的新风口

DeepSeek未来发展趋势&#xff1a;开创智能时代的新风口 随着人工智能&#xff08;AI&#xff09;、深度学习&#xff08;DL&#xff09;和大数据的飞速发展&#xff0c;众多创新型技术已经逐渐走向成熟&#xff0c;而DeepSeek作为这一领域的新兴力量&#xff0c;正逐步吸引越…

Go红队开发—编解码工具

文章目录 开启一个项目编解码工具开发Dongle包Base64编解码摩斯密码URL加解密AES加解密 MD5碰撞工具开发 开启一个项目 这作为补充内容&#xff0c;可忽略直接看下面的编解码&#xff1a; 一开始用就按照下面的步骤即可 1.创建一个文件夹&#xff0c;你自己定义名字(建议只用…

算法·搜索

搜索问题 搜索问题本质也是暴力枚举&#xff0c;一般想到暴力也要想到利用回溯枚举。 排序和组合问题 回溯法 去重问题&#xff1a;定义全局变量visited还是局部变量visited实现去重&#xff1f; 回溯问题 图论中的搜索问题 与一般的搜索问题一致&#xff0c;只不过要多…

常见的限流算法有哪些?

好的&#xff0c;关于这个问题&#xff0c;我会从几个方面来回答。 首先&#xff0c;限流算法是一种系统保护策略&#xff0c;主要是避免在流量高峰导致系统被压垮&#xff0c;造成系统不可用的问题。 常见的限流算法有 5 种。 1. &#xff08;如图&#xff09;计数器限流&a…

BetaFlight源码解读01

1.打开main.c init();run(); void systemInit(void) {int ret;clock_gettime(CLOCK_MONOTONIC, &start_time);printf("[system]Init...\n");SystemCoreClock 500 * 1e6; // virtual 500MHzif (pthread_mutex_init(&updateLock, NULL) ! 0) {printf("Cr…

FPGA-按键消抖

按键消抖 1. 原理 2. 关键程序实现 always (posedge clk or negedge rst) beginif(!rst)begincnt_wait < 26d0;end else if(flag_nege || flag_pose)begincnt_wait < 26d1;end else if(cnt_wait MAX_CNT) begincnt_wait < 26d0;end else if(cnt_wait > 26d0 &am…

Qt 进度条与多线程应用、基于 Qt 的文件复制工具开发

练习1&#xff1a;Qt 进度条与多线程应用 题目描述 开发一个基于 Qt 的应用程序&#xff0c;该应用程序包含一个水平进度条&#xff08;QSlider&#xff09;&#xff0c;并且需要通过多线程来更新进度条的值。请根据以下要求完成代码&#xff1a; 界面设计&#xff1a; 使用 QS…

C语言100天练习题【记录本】

C语言经典100题&#xff08;手把手 编程&#xff09; 可以在哔哩哔哩找到 已解决的天数&#xff1a;一&#xff0c;二&#xff0c;五&#xff0c;六 下面的都是模模糊糊的 可以学学这些算法&#xff0c;我是算法白痴&#xff0c;但是我不是白痴&#xff0c;可以学&#xff…

L1G5000XTuner 微调个人小助手认知

环境配置与数据准备 本节中&#xff0c;我们将演示如何安装 XTuner。 推荐使用 Python-3.10 的 conda 虚拟环境安装 XTuner。 步骤 0. 使用 conda 先构建一个 Python-3.10 的虚拟环境 cd ~ #git clone 本repo git clone https://github.com/InternLM/Tutorial.git -b camp4 mk…

【JavaEE】阻塞队列

【JavaEE】阻塞队列 一、什么是阻塞队列&#xff1f;二、阻塞队列的特点三、阻塞队列的常用方法3.1 抛出异常3.2 有返回结果&#xff0c;不会抛出异常3.3 阻塞 四、常见的阻塞队列4.1 ArrayBlockingQueue4.2 LinkedBlockingQueue4.3 SynchronousQueue4.4 PriorityBlockingQueue…

【Python项目】基于深度学习的车辆特征分析系统

【Python项目】基于深度学习的车辆特征分析系统 技术简介&#xff1a;采用Python技术、MySQL数据库、卷积神经网络&#xff08;CNN&#xff09;等实现。 系统简介&#xff1a;该系统基于深度学习技术&#xff0c;特别是卷积神经网络&#xff08;CNN&#xff09;&#xff0c;用…

蓝桥杯备赛:每日一题

只学习不思考不记笔记假把式 这道题目的难度很难&#xff0c;当然主要的原因在于模型的转化&#xff0c;刚看的这道题也是一脸懵&#xff0c;但是转换成覆盖模型后就好了很多&#xff0c;归跟接地就是每块区域的中取最大的最少的牛覆盖天数&#xff0c;然后根据这个天数求每一块…

7. 机器人记录数据集(具身智能机器人套件)

1. 树莓派启动机器人 conda activate lerobotpython lerobot/scripts/control_robot.py \--robot.typelekiwi \--control.typeremote_robot2. huggingface平台配置 huggingface官网 注册登录申请token&#xff08;要有写权限&#xff09;安装客户端 # 安装 pip install -U …

突破极限:高性能ROCK 220A-M 工业级无人机电调深度测评 —— 无人机动力系统的核心守护者

引言 在无人机技术高速发展的今天&#xff0c;动力系统的稳定性与效率成为决定任务成败的关键。作为工业级电调领域的标杆产品&#xff0c;ROCK 220A-M 凭借其卓越的性能与多重安全设计&#xff0c;为专业级无人机应用提供了可靠的动力解决方案。本文将从技术架构、防护…

OceanBase-obcp-v3考试资料梳理

集群架构 基本概念 集群: 集群由一个或多个Region组成,Region 由一个或多个Zone组成,Zone由一个或多个OBServer组成,每个OBServer里有若干个partition的Replica。 Region: 对应物理上的一个城市或地域,当OB集群由多个Region组成时, 数据库的数据和服务能力就具备地域…

深度学习PyTorch之13种模型精度评估公式及调用方法

深度学习pytorch之22种损失函数数学公式和代码定义 深度学习pytorch之19种优化算法&#xff08;optimizer&#xff09;解析 深度学习pytorch之4种归一化方法&#xff08;Normalization&#xff09;原理公式解析和参数使用 深度学习pytorch之简单方法自定义9类卷积即插即用 实时…

Gartner发布2025年网络安全六大预测

文章目录 前言趋势1&#xff1a;生成式AI推动数据安全计划趋势2&#xff1a;管理机器身份趋势3&#xff1a;战术型AI趋势4&#xff1a;优化网络安全技术趋势5&#xff1a;扩大安全行为与文化计划的价值趋势6&#xff1a;应对网络安全倦怠 前言 Gartner发布2025年网络安全六大预…

WPS Word中英文混杂空格和行间距不一致调整方案

文章目录 问题1&#xff1a;在两端对齐的情况下&#xff0c;如何删除参考文献&#xff08;英文&#xff09;的空格问题2&#xff1a;中英文混杂行间距不一致问题问题3&#xff1a;设置中文为固定字体&#xff0c;设置西文为固定字体参考 问题1&#xff1a;在两端对齐的情况下&a…

代码随想录算法训练营第22天 | 组合 组合总和 电话号码的字母组合

77. 组合 77. 组合 - 力扣&#xff08;LeetCode&#xff09; class Solution {List<Integer> path new ArrayList<>();List<List<Integer>> result new ArrayList<>();public void backTracking(int n,int k,int startIndex){if(path.size() …

Hadoop、Hive、Spark的关系

Part1&#xff1a;Hadoop、Hive、Spark关系概览 1、MapReduce on Hadoop 和spark都是数据计算框架&#xff0c;一般认为spark的速度比MR快2-3倍。 2、mapreduce是数据计算的过程&#xff0c;map将一个任务分成多个小任务&#xff0c;reduce的部分将结果汇总之后返回。 3、HIv…