加入星計劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • 1 硬件介紹
    • 2 開發(fā)環(huán)境搭建
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請入駐 產(chǎn)業(yè)圖譜

Arduino應(yīng)用開發(fā)——LCD顯示GIF動圖

10/04 08:55
1552
閱讀需 28 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

前面我已經(jīng)介紹過了如何在Arduino環(huán)境下用LCD顯示文本、圖案和圖片,這一講主要介紹一下GIF動圖的顯示。

1 硬件介紹

1.1 硬件配置

本文的硬件配置如下:

模塊 型號 說明
LCD ST7789 LCD常用的驅(qū)動IC有很多,如:ILI9341、ST7735等,不同的驅(qū)動IC,驅(qū)動代碼也是有區(qū)別的
注:驅(qū)動IC型號和屏幕型號是不同的,不管屏幕的廠家是哪個,屏幕尺寸是多大,只有驅(qū)動IC型號一樣,驅(qū)動代碼都是一樣的
ESP8266 ESP-12F 這是安信可的一款模組,內(nèi)部主要是用樂鑫的ESP8266EX再加上一個片外FLASH組成
ESP32 ESP-WROOM-32 MCU是樂鑫的一款芯片,開發(fā)板型號ESP32 DEVKITV1

注:我這里以ESP8266和ESP32為例講解,實(shí)際上根據(jù)自己的MCU選擇一種即可,方法和原理都是一樣的。

1.2 硬件連接

ESP8266接線如下:

esp8266 lcd 說明
VCC VCC 電源
GND GND 電源負(fù)
GPIO14HSPI_CLK CLK SPI時鐘線
GPIO13HSPI_MOSI SDI SPI數(shù)據(jù)線,esp8266輸出,lcd輸入
GPIO12HSPI_MISO SDO SPI數(shù)據(jù)線,esp8266輸入,lcd輸出,注:該引腳一般可以不用,除非你要讀取LCD的信息
GPIO4 CS SPI片選
GPIO5 WR(D/C) 并口時作為寫入信號/SPI時作為命令或參數(shù)的選擇
RST RST LCD復(fù)位引腳,可以用普通IO控制,引腳不足的情況下也可以和esp8266的復(fù)位引腳接到一起

特別說明:不同廠家做的LCD對這幾個引腳的命名可能會有差異,但意思是一樣的。

ESP32接線如下:

esp32 lcd 說明
VCC VCC 電源正
GND GND 電源負(fù)
GPIO18SPI_CLK CLK SPI時鐘線
GPIO23SPI_MOSI SDI SPI數(shù)據(jù)線,esp32輸出,lcd輸入
GPIO19SPI_MISO SDO SPI數(shù)據(jù)線,esp32輸入,lcd輸出,注:該引腳一般可以不用,除非你要讀取LCD的信息
GPIO15 CS SPI片選
GPIO5 WR(D/C) 并口時作為寫入信號/SPI時作為命令或參數(shù)的選擇
GPIO4 RST LCD復(fù)位引腳,可以用普通IO控制,引腳不足的情況下也可以和esp32的復(fù)位引腳接到一起

特別說明:不同廠家做的LCD對這幾個引腳的命名可能會有差異,但意思是一樣的。

2 開發(fā)環(huán)境搭建

2.1 安裝開發(fā)板

關(guān)于ESP8266和ESP32的Arduino環(huán)境搭建我之前出過教程了,這里就不多說了,不懂的同學(xué)可以先看下我之前的博客。

esp8266開發(fā)入門教程(基于Arduino)——環(huán)境安裝

ESP32-S2 Arduino開發(fā)環(huán)境搭建

2.2 安裝庫

打開Arduino IDE,依次打開 工具 -> 管理庫…

在搜索框輸入需要安裝的庫名稱,找到對應(yīng)的庫,點(diǎn)擊安裝即可。

本文需要使用的Arduino庫如下:

Arduino庫 版本 說明
TFT_eSPI 2.4.2 該庫通過SPI方式驅(qū)動LCD,支持多種LCD常用驅(qū)動IC
AnimatedGIF 1.4.6 GIF的解碼庫,用來顯示GIF動圖

3 LCD驅(qū)動的使用和測試

關(guān)于TFT_eSPI 庫的使用我前面已經(jīng)介紹過幾次了,這里就不再講解了,不懂的同學(xué)可以先看下我之前的博客。

esp8266應(yīng)用教程——TFT LCD

Arduino應(yīng)用開發(fā)——LCD顯示圖片

4 將GIF動圖轉(zhuǎn)成數(shù)據(jù)

我這里用的轉(zhuǎn)換工具是在GitHub上面找到的一個代碼,你們想要的話可以在下面的鏈接下載。這個工具使用起來稍微有點(diǎn)麻煩,如果你有更好的工具也可以推薦給博主。

工具鏈接:https://pan.baidu.com/s/1f2rgD1a9PY-_hboPdbfaow
提取碼:4h6h

運(yùn)行方法如下:

提示:下面演示的示例是我之前用來轉(zhuǎn)換圖片數(shù)據(jù)的,轉(zhuǎn)換gif也是一樣的。

第一步:把要轉(zhuǎn)換的圖片放到這個工具的目錄下。

在這里插入圖片描述

 

第二步:打開電腦的運(yùn)行窗口(win10可以使用win+R快捷鍵),輸入“cmd”打開命令窗口。

在這里插入圖片描述

第三步:在命令窗口中輸入跳轉(zhuǎn)命令,跳轉(zhuǎn)到轉(zhuǎn)換工具所在的目錄下。

例如:

cd C:UserszDesktop圖片處理工具image_to_cdistWindows

在這里插入圖片描述

第四步:運(yùn)行應(yīng)用程序。

格式:應(yīng)用程序名+空格+圖片名+空格+>+空格+轉(zhuǎn)換后的文件名。
例如:

image_to_c64.exe demo-image1.jpg > demo-image1.h

在這里插入圖片描述

運(yùn)行成功的話就會生成相應(yīng)的頭文件。

在這里插入圖片描述

5 編寫應(yīng)用程序

示例代碼如下:

提示:示例里面同時使用了5個GIF demo,內(nèi)存占用了2兆多,這些數(shù)據(jù)都是直接存到代碼里面的,因此在下載程序時要注意FLASH的配置,保證APP分區(qū)的內(nèi)存足夠,否則會編譯出錯。也可以注釋掉一部分素材,單獨(dú)播放某個GIF,這樣內(nèi)存占用較少。

程序源碼和素材可以在文章底部下載。

#include <SPI.h>
#include <TFT_eSPI.h>
#include <AnimatedGIF.h>
// #include "Arduino.h"
// #include <esp_heap_caps.h>

#include "gif_demo1.h"
#include "gif_demo2.h"
#include "gif_demo3.h"
#include "gif_demo4.h"
#include "gif_demo5.h"

#define GIF_DEMO1 gif_demo1
#define GIF_DEMO2 gif_demo2
#define GIF_DEMO3 gif_demo3
#define GIF_DEMO4 gif_demo4
#define GIF_DEMO5 gif_demo5

#ifdef ESP8266
#include <avr/pgmspace.h>
#else
#include <pgmspace.h>
#endif

AnimatedGIF gif;
TFT_eSPI tft = TFT_eSPI();

#define GIF_ENABLE
#define NORMAL_SPEED    // Comment out for rame rate for render speed test

#ifdef GIF_ENABLE
// GIFDraw is called by AnimatedGIF library frame to screen
#define DISPLAY_WIDTH  tft.width()
#define DISPLAY_HEIGHT tft.height()
#define BUFFER_SIZE 256            // Optimum is >= GIF width or integral division of width

#ifdef USE_DMA
  uint16_t usTemp[2][BUFFER_SIZE]; // Global to support DMA use
#else
  uint16_t usTemp[1][BUFFER_SIZE];    // Global to support DMA use
#endif
bool     dmaBuf = 0;

// Draw a line of image directly on the LCD
void GIFDraw(GIFDRAW *pDraw)
{
  uint8_t *s;
  uint16_t *d, *usPalette;
  int x, y, iWidth, iCount;

  // pDraw->iX = 50;
  // pDraw->iY = 50;

  // Displ;ay bounds chech and cropping
  iWidth = pDraw->iWidth;
  if (iWidth + pDraw->iX > DISPLAY_WIDTH)
    iWidth = DISPLAY_WIDTH - pDraw->iX;
  usPalette = pDraw->pPalette;
  y = pDraw->iY + pDraw->y; // current line
  if (y >= DISPLAY_HEIGHT || pDraw->iX >= DISPLAY_WIDTH || iWidth < 1)
    return;

  // Old image disposal
  s = pDraw->pPixels;
  if (pDraw->ucDisposalMethod == 2) // restore to background color
  {
    for (x = 0; x < iWidth; x++)
    {
      if (s[x] == pDraw->ucTransparent)
        s[x] = pDraw->ucBackground;
    }
    pDraw->ucHasTransparency = 0;
  }

  // Apply the new pixels to the main image
  if (pDraw->ucHasTransparency) // if transparency used
  {
    uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent;
    pEnd = s + iWidth;
    x = 0;
    iCount = 0; // count non-transparent pixels
    while (x < iWidth)
    {
      c = ucTransparent - 1;
      d = &usTemp[0][0];
      while (c != ucTransparent && s < pEnd && iCount < BUFFER_SIZE )
      {
        c = *s++;
        if (c == ucTransparent) // done, stop
        {
          s--; // back up to treat it like transparent
        }
        else // opaque
        {
          *d++ = usPalette[c];
          iCount++;
        }
      } // while looking for opaque pixels
      if (iCount) // any opaque pixels?
      {
        // DMA would degrtade performance here due to short line segments
        tft.setAddrWindow(pDraw->iX + x, y, iCount, 1);
        tft.pushPixels(usTemp, iCount);
        x += iCount;
        iCount = 0;
      }
      // no, look for a run of transparent pixels
      c = ucTransparent;
      while (c == ucTransparent && s < pEnd)
      {
        c = *s++;
        if (c == ucTransparent)
          x++;
        else
          s--;
      }
    }
  }
  else
  {
    s = pDraw->pPixels;

    // Unroll the first pass to boost DMA performance
    // Translate the 8-bit pixels through the RGB565 palette (already byte reversed)
    if (iWidth <= BUFFER_SIZE)
      for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];
    else
      for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];

#ifdef USE_DMA // 71.6 fps (ST7796 84.5 fps)
    tft.dmaWait();
    tft.setAddrWindow(pDraw->iX, y, iWidth, 1);
    tft.pushPixelsDMA(&usTemp[dmaBuf][0], iCount);
    dmaBuf = !dmaBuf;
#else // 57.0 fps
    tft.setAddrWindow(pDraw->iX, y, iWidth, 1);
    tft.pushPixels(&usTemp[0][0], iCount);
#endif

    iWidth -= iCount;
    // Loop if pixel buffer smaller than width
    while (iWidth > 0)
    {
      // Translate the 8-bit pixels through the RGB565 palette (already byte reversed)
      if (iWidth <= BUFFER_SIZE)
        for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];
      else
        for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];

#ifdef USE_DMA
      tft.dmaWait();
      tft.pushPixelsDMA(&usTemp[dmaBuf][0], iCount);
      dmaBuf = !dmaBuf;
#else
      tft.pushPixels(&usTemp[0][0], iCount);
#endif
      iWidth -= iCount;
    }
  }
} /* GIFDraw() */
#endif

void setup() 
{
    Serial.begin(115200);
    tft.begin();
    tft.setRotation(2);
    tft.fillScreen(TFT_BLACK);

    gif.begin(BIG_ENDIAN_PIXELS);
}

#ifdef NORMAL_SPEED // Render at rate that is GIF controlled
void loop()
{
#ifdef GIF_DEMO1
    if (gif.open((uint8_t *)GIF_DEMO1, sizeof(GIF_DEMO1), GIFDraw))
    {
      Serial.printf("Successfully opened GIF; Canvas size = %d x %dn", gif.getCanvasWidth(), gif.getCanvasHeight());
      tft.startWrite(); // The TFT chip slect is locked low
      while (gif.playFrame(true, NULL))
      {
        yield();
      }
      gif.close();
      tft.endWrite(); // Release TFT chip select for other SPI devices
    }
#endif

#ifdef GIF_DEMO2
    if (gif.open((uint8_t *)GIF_DEMO2, sizeof(GIF_DEMO2), GIFDraw))
    {
      Serial.printf("Successfully opened GIF; Canvas size = %d x %dn", gif.getCanvasWidth(), gif.getCanvasHeight());
      tft.startWrite(); // The TFT chip slect is locked low
      while (gif.playFrame(true, NULL))
      {
        yield();
      }
      gif.close();
      tft.endWrite(); // Release TFT chip select for other SPI devices
    }
#endif

#ifdef GIF_DEMO3
    if (gif.open((uint8_t *)GIF_DEMO3, sizeof(GIF_DEMO3), GIFDraw))
    {
      Serial.printf("Successfully opened GIF; Canvas size = %d x %dn", gif.getCanvasWidth(), gif.getCanvasHeight());
      tft.startWrite(); // The TFT chip slect is locked low
      while (gif.playFrame(true, NULL))
      {
        yield();
      }
      gif.close();
      tft.endWrite(); // Release TFT chip select for other SPI devices
    }
#endif

#ifdef GIF_DEMO4
    if (gif.open((uint8_t *)GIF_DEMO4, sizeof(GIF_DEMO4), GIFDraw))
    {
      tft.fillScreen(TFT_BLACK);
      Serial.printf("Successfully opened GIF; Canvas size = %d x %dn", gif.getCanvasWidth(), gif.getCanvasHeight());
      tft.startWrite(); // The TFT chip slect is locked low
      while (gif.playFrame(true, NULL))
      {
        yield();
      }
      gif.close();
      tft.endWrite(); // Release TFT chip select for other SPI devices
    }
#endif

#ifdef GIF_DEMO5
    if (gif.open((uint8_t *)GIF_DEMO5, sizeof(GIF_DEMO5), GIFDraw))
    {
      Serial.printf("Successfully opened GIF; Canvas size = %d x %dn", gif.getCanvasWidth(), gif.getCanvasHeight());
      tft.startWrite(); // The TFT chip slect is locked low
      while (gif.playFrame(true, NULL))
      {
        yield();
      }
      gif.close();
      tft.endWrite(); // Release TFT chip select for other SPI devices
    }
#endif
}
#else // Test maximum rendering speed
void loop()
{
    long lTime = micros();
    int iFrames = 0;

    if (gif.open((uint8_t *)GIF_DEMO4, sizeof(GIF_DEMO4), GIFDraw))
    {
        tft.startWrite(); // For DMA the TFT chip slect is locked low
        while (gif.playFrame(false, NULL))
        {
            // Each loop renders one frame
            iFrames++;
            yield();
        }
        gif.close();
        tft.endWrite(); // Release TFT chip select for other SPI devices
        lTime = micros() - lTime;
        Serial.print(iFrames / (lTime / 1000000.0));
        Serial.println(" fps");
    }
}
#endif

6 測試驗證

前面代碼示例中前三個素材是冰墩墩,結(jié)果上傳之后提示我圖片違規(guī)。

就賊離譜,連帶著我后面拍的LCD實(shí)際運(yùn)行的視頻也上傳不了。試了幾次都不行,累了,毀滅吧。

想要看效果的同學(xué)可以在文章底部的的鏈接里面下載。

運(yùn)行結(jié)果:
上面也說了,原本想展示一下運(yùn)行效果的,結(jié)果冰墩墩的這幾個圖上傳之后老是說我違規(guī),算了,不想搞了,就這樣吧。

上面的示例源碼是按GIF本身正常的速度運(yùn)行的,其實(shí)也可以全速運(yùn)行。全速運(yùn)行的刷新率主要取決于GIF里面每張圖片的像素和顏色復(fù)雜度,我測試過幾個GIF,一些簡單的素材刷新率能達(dá)到60fps,一般普通240*240像素的GIF刷新率在20fps左右。

但是要注意的是我現(xiàn)在測試的結(jié)果是基于動圖數(shù)據(jù)存到spi flash的情況,CPU和flash的頻率都調(diào)到最大了,LCD這邊spi的通訊速率則是27MHz,如果數(shù)據(jù)存到RAM,SPI通訊速率再快一些,刷新率應(yīng)該還能再高一些。

手機(jī)拍攝LCD屏幕會有很明顯的色差和紋波,這個沒法消除,只有這個能發(fā)出來,將就著看吧。

結(jié)束語

這一講簡單介紹了在Arduino環(huán)境下使用LCD顯示GIF動圖,整個流程總的來說還是不難的,把驅(qū)動調(diào)好之后直接凋庫顯示就完了。如果還有什么問題,歡迎在評論區(qū)留言或者私信給我。

想要源代碼、素材或圖片處理工具的自行下載:
鏈接:https://pan.baidu.com/s/1Ptc2F9yYrjCQJkycG129wg
提取碼:4a1i

Arduino開發(fā)教程匯總:
https://blog.csdn.net/ShenZhen_zixian/article/details/121659482

相關(guān)推薦

電子產(chǎn)業(yè)圖譜