ESP-32 播放GIF动图

让你的ESP32显示一张动图吧

最近在填TinyMonitor(暂定)的坑,突然想增加更加高级的动态效果,而我实在是想不出什么高级的动画效果了,于是决定借助一些GIF来实现一些有趣的效果。

然后就发现了这个AnimatedGIF,使用起来也灰常简单。

animation-preview

因为我TinyMonitor(暂定)驱动用的是TFT_eSPI
(至于如何驱动屏幕,可以参考之前的文章ESP32驱动ST7789液晶屏)

稍作修改后的代码

代码基于AnimatedGIF自带的example–TFT_eSPI_memory.ino稍作修改.

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// TFT_eSPI_memory
//
// Example sketch which shows how to display an
// animated GIF image stored in FLASH memory
//
// written by Larry Bank
// bitbank@pobox.com
//
// Adapted by Bodmer for the TFT_eSPI Arduino library:
// https://github.com/Bodmer/TFT_eSPI
//
// To display a GIF from memory, a single callback function
// must be provided - GIFDRAW
// This function is called after each scan line is decoded
// and is passed the 8-bit pixels, RGB565 palette and info
// about how and where to display the line. The palette entries
// can be in little-endian or big-endian order; this is specified
// in the begin() method.
//
// The AnimatedGIF class doesn't allocate or free any memory, but the
// instance data occupies about 22.5K of RAM.

//#define USE_DMA // ESP32 ~1.25x single frame rendering performance boost
// for badgers.h
// Note: Do not use SPI DMA if reading GIF images from SPI SD card on same bus
// as TFT
#define NORMAL_SPEED // Comment out for rame rate for render speed test

// Load GIF library
#include <AnimatedGIF.h>
AnimatedGIF gif;

// Example AnimatedGIF library images
#include "loading.h"
// ESP32 40MHz SPI single frame rendering performance
// Note: no DMA performance gain on smaller images or transparent pixel GIFs
#define GIF_IMAGE angry_80px_gif // No DMA 63 fps, DMA: 71fps

#include <SPI.h>
#include <TFT_eSPI.h>
#define DISPLAY_WIDTH tft.width()
#define DISPLAY_HEIGHT tft.height()
#define BUFFER_SIZE \
60 // 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;
TFT_eSPI tft = TFT_eSPI();

// 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;

// Display 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;
uint8_t *pEnd, c, ucTransparent = TFT_BLACK;
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() */

void setup() {
Serial.begin(115200);

tft.begin();
#ifdef USE_DMA
tft.initDMA();
#endif
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);

gif.begin(BIG_ENDIAN_PIXELS);

}

#ifdef NORMAL_SPEED // Render at rate that is GIF controlled
void loop() {
// Put your main code here, to run repeatedly:
if (gif.open((uint8_t *)GIF_IMAGE, sizeof(GIF_IMAGE), GIFDraw)) {
Serial.printf("Successfully opened GIF; Canvas size = %d x %d\n",
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
}
}
#else // Test maximum rendering speed
void loop() {
long lTime = micros();
int iFrames = 0;

if (gif.open((uint8_t *)GIF_IMAGE, sizeof(GIF_IMAGE), 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

下面为loading.h的内容位于Gist,因为内容太长影响排版,就只贴一个连接了。

如何对GIF文件转换成可用的格式

loading.h的制作方法如下.

1
xxd -i angry_80px.gif >> loading.h
名称 描述
xxd 以2进制或者16进制显示文件内容
angry_80px.gif 输入文件
loading.h 输出文件

XXD是一个Linux命令,windows下默认没有这个命令

参考资料