LCD是项目中比较常用的外设,基于Arduino开发有个好处就是它很多相关的库可用,这对于项目的开发或者前期的方案验证来说是非常方便的,缺点是灵活性较差。Arduino支持很多硬件,我们这一讲主要基于ESP8266和ESP32来讲解图片的显示。
1 硬件介绍
1.1 硬件配置
本文的硬件配置如下:
模块 | 型号 | 说明 |
---|---|---|
LCD | ST7789 | LCD常用的驱动IC有很多,如:ILI9341、ST7735等,不同的驱动IC,驱动代码也是有区别的 注:驱动IC型号和屏幕型号是不同的,不管屏幕的厂家是哪个,屏幕尺寸是多大,只有驱动IC型号一样,驱动代码都是一样的 |
ESP8266 | ESP-12F | 这是安信可的一款模组,内部主要是用乐鑫的ESP8266EX再加上一个片外FLASH组成 |
ESP32 | ESP-WROOM-32 | MCU是乐鑫的一款芯片,开发板型号ESP32 DEVKITV1 |
注:我这里以ESP8266和ESP32为例讲解,实际上根据自己的MCU选择一种即可,方法和原理都是一样的。
1.2 硬件连接
ESP8266接线如下:
esp8266 | lcd | 说明 |
---|---|---|
VCC | VCC | 电源正 |
GND | GND | 电源负 |
GPIO14HSPI_CLK | CLK | SPI时钟线 |
GPIO13HSPI_MOSI | SDI | SPI数据线,esp8266输出,lcd输入 |
GPIO12HSPI_MISO | SDO | SPI数据线,esp8266输入,lcd输出,注:该引脚一般可以不用,除非你要读取LCD的信息 |
GPIO4 | CS | SPI片选 |
GPIO5 | WR(D/C) | 并口时作为写入信号/SPI时作为命令或参数的选择 |
RST | RST | LCD复位引脚,可以用普通IO控制,引脚不足的情况下也可以和esp8266的复位引脚接到一起 |
特别说明:不同厂家做的LCD对这几个引脚的命名可能会有差异,但意思是一样的。 |
ESP32接线如下:
esp32 | lcd | 说明 |
---|---|---|
VCC | VCC | 电源正 |
GND | GND | 电源负 |
GPIO18SPI_CLK | CLK | SPI时钟线 |
GPIO23SPI_MOSI | SDI | SPI数据线,esp32输出,lcd输入 |
GPIO19SPI_MISO | SDO | SPI数据线,esp32输入,lcd输出,注:该引脚一般可以不用,除非你要读取LCD的信息 |
GPIO15 | CS | SPI片选 |
GPIO5 | WR(D/C) | 并口时作为写入信号/SPI时作为命令或参数的选择 |
GPIO4 | RST | LCD复位引脚,可以用普通IO控制,引脚不足的情况下也可以和esp32的复位引脚接到一起 |
特别说明:不同厂家做的LCD对这几个引脚的命名可能会有差异,但意思是一样的。 |
2 开发环境搭建
2.1 安装开发板
关于ESP8266和ESP32的Arduino环境搭建我之前出过教程了,这里就不多说了,不懂的同学可以先看下我之前的博客。
esp8266开发入门教程(基于Arduino)——环境安装
ESP32-S2 Arduino开发环境搭建
2.2 安装库
打开Arduino IDE,依次打开 工具 -> 管理库…
在搜索框输入需要安装的库名称,找到对应的库,点击安装即可。
本文需要使用的Arduino库如下:
Arduino库 | 版本 | 说明 |
---|---|---|
TFT_eSPI | 2.4.2 | 该库通过SPI方式驱动LCD,支持多种LCD常用驱动IC |
JPEGDecoder | 1.8.0 | JPEG格式解码库,用来显示JPEG图片 |
注:TFT_eSPI库本身就支持显示图片,但是只能是16进制的位图数据,而JPEGDecoder是可以显示JPEG格式图片,两种方式其实都是可以的。 |
3 LCD驱动的使用和测试
LCD驱动的方式一般是用SPI、并口或者IIC,我这里是以SPI为例。我之前也发布过一篇关于LCD驱动讲解的博客,有什么不懂的话也可以去看一下。
esp8266应用教程——TFT LCD
3.1 修改TFT_eSPI库基础配置
安装好TFT_eSPI库之后需要根据自己电路实际的情况配置底层接口。
Arduino安装的库一般在C盘文档目录下,如:C:UsersxxxDocumentsArduinolibraries (xxx是你电脑的用户名)
找到TFT_eSPI文件夹,打开User_Setup.h文件,修改以下几项参数。
1)驱动IC
根据自己使用的LCD驱动IC打开对应的宏。注意这些驱动只能选择一个打开,不用的要注释掉。
2)RGB数据格式
RGB格式指的是像素点颜色数据的排列方式,一般就BGR和RGB两种,区别其实就是数据高位在前还是低位在前,这个主要是用于图片显示,要用哪种格式主要是看你要显示的内容是怎么排的,不确定的话可以先不改,调试的时候如果颜色不对的话再换过来就好了。
3)像素
根据自己屏幕的像素修改,也可以先不改,直接在后面应用的时候再改。
4)GPIO
根据自己的电路设置引脚,除了几个必要的引脚,有些引脚可以不配置,如:RST可以通过硬件和MCU的RST接到一起,软件配置成-1即可。BL背光也可以硬件直接控制。
还有像ESP8266也可以不自定义SPI的几个引脚,它默认用的就是ESP8266硬件SPI的接口,你的接线保持一致即可。
注意:相同的GPIO定义只能打开一个,默认有些打开了的要注释掉。
特别说明:如果你用的是ESP32,TFT_eSPI建议使用2.4.0以上版本,因为之前的版本关于ESP32的引脚定义是分成HSPI和VSPI的,默认使用VSPI,如果要用HSPI要打开USE_HSPI_PORT定义,但是这套框架兼容性不强,不适用于ESP32-S2和ESP32-C2,而2.4.0以上版本取消了这个定义,直接都按引脚号来定义,这样一来就不用区分HSPI和VSPI了。
esp8266的引脚如下图所示,esp32的我就不贴出来了,都是差不多的。
5)字库
TFT_eSPI自带的这些字库你可以直接用,如果你有自己的字库不用这里的话也可以注释掉。flash空间足够的情况下,这点代码加不加都没关系。
6)SPI通讯速率
SPI通讯速率一般默认即可,默认的这个速率一般是足够了的,如果需要更快的话可以自己修改。
7)ESP32的特殊定义
TFT_eSPI旧版本关于ESP32的SPI接口是分为HSPI和VSPI两种的,默认使用VSPI,如果要用HSPI要打开USE_HSPI_PORT定义,如果你只是用ESP32,那这个框架是没什么问题的。
但是我之前因为项目需要从ESP32改用ESP32-S2,结果发现ESP32-S2就没有HSPI和VSPI,所有接口都是FSPI,于是我就要把底层很多东西都改掉才能正常使用。
不过现在TFT_eSPI库2.4.0以上版本就已经把这个问题改掉了,兼容了ESP32-S2和ESP-C3,取消了USE_HSPI_PORT这个定义,SPI接口都以GPIO引脚号来定义。
所以,我建议都用新版本的库吧,兼容性更好,也不用去考虑用HSPI还是VSPI。
3.2 LCD驱动测试
TFT_eSPI库配置好了之后可以先烧录一个简单的程序测试一下硬件和代码是否能正常运行。
PS:当然,到了后面把图片数据做好直接显示图片也是可以的。
#include <SPI.h>
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
void setup()
{
Serial.begin(115200);
tft.begin();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
}
void loop()
{
tft.fillScreen(TFT_GREEN);
delay(1000);
tft.fillScreen(TFT_BLUE);
delay(1000);
tft.fillScreen(TFT_RED);
delay(1000);
}
4 制作图片数据
图片数据可以按位图的形式保存和显示,也可以按jpg格式保存。区别在于位图的方式占用的内存都是固定的,只跟像素大小有关,而相同像素下jpg的内存一般要更小,具体占用的内存大小跟图片的色彩复杂度有关,但是jpg的缺点是需要解码,而位图数据则不需要解码。
两种方式都可以选,根据自己的需求来用就行了。位图的话直接用TFT_eSPI库就行了,而jpg格式还需要使用JPEGDecoder库解码。
4.1 制作图片素材
网上随便找一张图片,借助WIN10自带图片编辑器或者其他图片处理软件把图片处理一下,裁剪出自己想要显示的内容之后,再把分辨率调整成适合的大小,图片以jpg,bmp或者其他格式保存都是可以的。
4.2 生成图片数据
我们可以借助一些工具来实现图片到数据的转换。我们的数据要以C语言数组的形式存储,像素颜色数据以16进制的形式保存。
4.2.1 位图数据生成方法
位图数据的工具很多,比如:Img2Lcd,lcd-image-converter等。
工具可以在下面的链接下载,也可以在网上自己查找。
工具链接:https://pan.baidu.com/s/1f2rgD1a9PY-_hboPdbfaow
提取码:4h6h
1)Img2Lcd使用方法
打开一张图片。
设置配置如下:
输出数据类型:C语言数组
扫描方式:水平扫描
输出灰度:16位真彩色
最大宽度和高度:自定义
下面的几个选项只勾选“高位在前(MSB First)”即可。(说明一下:高位在前的意思是指数据以GBR方式导出,如果不勾选的话则是以RGB方式导出,用哪种其实取决于你后面显示图片的那个函数处理数据的方式,保持一致即可。)
配置好参数之后点击保存,以.h文件保存即可。保存好这个头文件,后面会用到。
保存的这个头文件数据格式如下:
这个数组的定义我们可以改一下,因为图片的数据一般比较多,全部用RAM来存的话内存可能不足,所以我们可以把这些数据存放到flash里面。
修改定义之前:
const unsigned char gImage_demo_image1[115200] = { /* 0X10,0X10,0X00,0XF0,0X00,0XF0,0X01,0X1B, */
修改定义之后:
#ifndef PROGMEM
#define PROGMEM
#endif
const uint8_t gImage_demo_image1[115200] PROGMEM = { /* 0X10,0X10,0X00,0XF0,0X00,0XF0,0X01,0X1B, */
PROGMEM的用法具体我就不说了,不懂的可以自己去查。
2)lcd-image-converter使用方法
打开一张图片,点击 Options -> Conversion…
参数配置好了之后点击"Show Preview"即可看到图片转换后的位图数据。
这个软件不能直接生成头文件,需要自己另外新建一个头文件,然后定义一个数组,再把这些数据拷贝进去。
4.2.2 JPEG格式图片数据生成方法
我这里用的转换工具是在GitHub上面找到的一个代码,你们想要的话可以在下面的链接下载。这个工具使用起来稍微有点麻烦,如果你有更好的工具也可以推荐给博主。
工具链接:https://pan.baidu.com/s/1f2rgD1a9PY-_hboPdbfaow
提取码:4h6h
运行方法如下:
第一步:把要转换的图片放到这个工具的目录下。
第二步:打开电脑的运行窗口(win10可以使用win+R快捷键),输入“cmd”打开命令窗口。
第三步:在命令窗口中输入跳转命令,跳转到转换工具所在的目录下。
例如:
cd C:UserszDesktop图片处理工具image_to_cdistWindows
第四步:运行应用程序。
格式:应用程序名+空格+图片名+空格+>+空格+转换后的文件名。
例如:
image_to_c64.exe demo-image1.jpg > demo-image1.h
运行成功的话就会生成相应的头文件。
5 编写应用程序
使用Arduino IDE新建并保存一个工程,把图片数据(.h文件)放到工程目录下。然后编写应用代码。
示例代码如下:
特别说明:该代码是用ESP32-S2进行测试的,ESP32和ESP8266我之前也调试过,不是下面的这个代码,不过写法基本都是一样的。主要是TFT_eSPI库的引脚改一下即可。
本文所用源码和图片素材都上传到云盘了,可以在文章底部链接下载。
#include <SPI.h>
#include <TFT_eSPI.h>
// #include "Arduino.h"
#ifdef ESP8266
#include <avr/pgmspace.h>
#else
#include <pgmspace.h>
#endif
// 图片位图数据,注意:下面这几个只是示例文件,而且都是240x240分辨率的图片,内存比较大,如果你用的FLASH内存不足的话编译会出错
#include "demo_image1.h"
#include "demo_image2.h"
#include "demo_image3.h"
#include "test_image.h"
// #define JPEG_ENABLE // 打开该宏使用JPEG解码
TFT_eSPI tft = TFT_eSPI();
// JPEG图片显示相关函数
#ifdef JPEG_ENABLE
// JPEG decoder library
#include <JPEGDecoder.h>
#include "demo_jpg1.h"
#include "demo_jpg2.h"
#include "demo_jpg3.h"
// Return the minimum of two values a and b
#define minimum(a,b) (((a) < (b)) ? (a) : (b))
//####################################################################################################
// Draw a JPEG on the TFT pulled from a program memory array
//####################################################################################################
void drawArrayJpeg(const uint8_t arrayname[], uint32_t array_size, int xpos, int ypos) {
int x = xpos;
int y = ypos;
JpegDec.decodeArray(arrayname, array_size);
jpegInfo(); // Print information from the JPEG file (could comment this line out)
renderJPEG(x, y);
Serial.println("#########################");
}
//####################################################################################################
// Draw a JPEG on the TFT, images will be cropped on the right/bottom sides if they do not fit
//####################################################################################################
// This function assumes xpos,ypos is a valid screen coordinate. For convenience images that do not
// fit totally on the screen are cropped to the nearest MCU size and may leave right/bottom borders.
void renderJPEG(int xpos, int ypos) {
// retrieve infomration about the image
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;
// Jpeg images are draw as a set of image block (tiles) called Minimum Coding Units (MCUs)
// Typically these MCUs are 16x16 pixel blocks
// Determine the width and height of the right and bottom edge image blocks
uint32_t min_w = minimum(mcu_w, max_x % mcu_w);
uint32_t min_h = minimum(mcu_h, max_y % mcu_h);
// save the current image block size
uint32_t win_w = mcu_w;
uint32_t win_h = mcu_h;
// record the current time so we can measure how long it takes to draw an image
uint32_t drawTime = millis();
// save the coordinate of the right and bottom edges to assist image cropping
// to the screen size
max_x += xpos;
max_y += ypos;
// read each MCU block until there are no more
while (JpegDec.readSwappedBytes()) {
// save a pointer to the image block
pImg = JpegDec.pImage ;
// calculate where the image block should be drawn on the screen
int mcu_x = JpegDec.MCUx * mcu_w + xpos; // Calculate coordinates of top left corner of current MCU
int mcu_y = JpegDec.MCUy * mcu_h + ypos;
// check if the image block size needs to be changed for the right edge
if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
else win_w = min_w;
// check if the image block size needs to be changed for the bottom edge
if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
else win_h = min_h;
// copy pixels into a contiguous block
if (win_w != mcu_w)
{
uint16_t *cImg;
int p = 0;
cImg = pImg + win_w;
for (int h = 1; h < win_h; h++)
{
p += mcu_w;
for (int w = 0; w < win_w; w++)
{
*cImg = *(pImg + w + p);
cImg++;
}
}
}
// draw image MCU block only if it will fit on the screen
if (( mcu_x + win_w ) <= tft.width() && ( mcu_y + win_h ) <= tft.height())
{
tft.pushRect(mcu_x, mcu_y, win_w, win_h, pImg);
}
else if ( (mcu_y + win_h) >= tft.height()) JpegDec.abort(); // Image has run off bottom of screen so abort decoding
}
// calculate how long it took to draw the image
drawTime = millis() - drawTime;
// print the results to the serial port
Serial.print(F( "Total render time was : ")); Serial.print(drawTime); Serial.println(F(" ms"));
Serial.println(F(""));
}
//####################################################################################################
// Print image information to the serial port (optional)
//####################################################################################################
void jpegInfo() {
Serial.println(F("==============="));
Serial.println(F("JPEG image info"));
Serial.println(F("==============="));
Serial.print(F( "Width :")); Serial.println(JpegDec.width);
Serial.print(F( "Height :")); Serial.println(JpegDec.height);
Serial.print(F( "Components :")); Serial.println(JpegDec.comps);
Serial.print(F( "MCU / row :")); Serial.println(JpegDec.MCUSPerRow);
Serial.print(F( "MCU / col :")); Serial.println(JpegDec.MCUSPerCol);
Serial.print(F( "Scan type :")); Serial.println(JpegDec.scanType);
Serial.print(F( "MCU width :")); Serial.println(JpegDec.MCUWidth);
Serial.print(F( "MCU height :")); Serial.println(JpegDec.MCUHeight);
Serial.println(F("==============="));
}
//####################################################################################################
// Show the execution time (optional)
//####################################################################################################
// WARNING: for UNO/AVR legacy reasons printing text to the screen with the Mega might not work for
// sketch sizes greater than ~70KBytes because 16 bit address pointers are used in some libraries.
// The Due will work fine with the HX8357_Due library.
void showTime(uint32_t msTime) {
//tft.setCursor(0, 0);
//tft.setTextFont(1);
//tft.setTextSize(2);
//tft.setTextColor(TFT_WHITE, TFT_BLACK);
//tft.print(F(" JPEG drawn in "));
//tft.print(msTime);
//tft.println(F(" ms "));
Serial.print(F(" JPEG drawn in "));
Serial.print(msTime);
Serial.println(F(" ms "));
}
#endif
void setup()
{
Serial.begin(115200);
tft.begin();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
}
void loop()
{
#if 1
tft.pushImage(0, 0, 240, 240, (uint16_t*)test_image);
delay(1000);
tft.pushImage(0, 0, 240, 240, (uint16_t*)gImage_demo_image1);
delay(1000);
tft.pushImage(0, 0, 240, 240, (uint16_t*)gImage_demo_image2);
delay(1000);
tft.pushImage(0, 0, 240, 240, (uint16_t*)gImage_demo_image3);
delay(1000);
#endif
#ifdef JPEG_ENABLE
drawArrayJpeg(demo_image1, sizeof(demo_image1), 0, 0);
delay(1000);
drawArrayJpeg(demo_image2, sizeof(demo_image2), 0, 0);
delay(1000);
drawArrayJpeg(demo_image3, sizeof(demo_image3), 0, 0);
delay(1000);
#endif
}
6 测试验证
4张图片间隔轮1秒流播放的效果如下,手机拍屏幕会有很大的色差,这个没办法,将就着看吧,反正上面例程效果大概就是这样的。不管是用位图数据显示还是jpg格式显示,最终的结果是一样的。
结束语
这一讲简单介绍了在Arduino环境下使用LCD显示图片,主要介绍了位图和JPEG格式的显示,其他格式比如PNG其实也是可以解码的,不过这里就不再讲解了,感兴趣的同学自己去找相应的库吧。整个流程总的来说还是不难的,把驱动调好之后直接凋库显示就完了。如果还有什么问题,欢迎在评论区留言或者私信给我。
想要源代码或者图片处理工具的自行下载:
链接:https://pan.baidu.com/s/1f2rgD1a9PY-_hboPdbfaow
提取码:4h6h
Arduino开发教程汇总:
https://blog.csdn.net/ShenZhen_zixian/article/details/121659482