毕业的事情忙完了开始搭建自己的信息收集渠道,然后电脑桌边上需要一个常量显示即时信息,但是又不需要亮着的玩意儿,那么就只能是墨水屏了。

这篇博文选用的是微雪的 7.5inch HD e-Paper V2 + ESP32 带 WIFI 的开发板来实现信息的渲染,微雪官方提供了对应的 Arduino 开发库和示例程序。

最开始在某宝看到这个套装还以为 ESP32 提供的 ESP32 WIFI 渲染程序是有一个接口的,可以在装载启动后直接提供一个内网的服务用来发送图片上去渲染。也就是官网的 WIFI 例程:https://www.waveshare.net/wiki/E-Paper_ESP32_Driver_Board。

在拿到板子后刷进去才发现,这个例程确实提供了一个网页用来上传图片并且进行采样生成 LCD 图片,但是它没有一个具体的接口,而且一张图片上传渲染完成之后就会关闭服务,所以需要达到自己想要的效果也就只能自己写程序了。示例程序可以用来测试一下板子正不正常,第四步里一种尺寸的板子有不同的型号,也需要正确选择才能正常渲染。

img

渲染接口

ESP32 的示例程序里有一个名为 esp32-waveshare-epd 的文件夹,在按照微雪文档里的指示放到 Arduino 文件夹里就可以,这里也没有太多好写的,配置过程不管是官网还是网上也有不少。这个文件夹里包含了一系列的示例程序和开发所需要用到的库(这里好评,不用自己去慢慢学更底层的代码)。

需要明确的一点是这款墨水屏里长的那一侧为 x 轴,另外一侧为 y 轴,在摆正后以左上角为原点,y 轴向下。摆正的方向的话 7.5 inch 这块板子是排线向下为正,其他的就需要自己测试了。下面的函数调用需要引入 EPD.h 和 DEV_Config.h 文件。

示例程序里基本上以调用 DEV_Module_Init 开始,这个函数是用来初始化引脚的,只管启动就调

根据屏幕使用的不同,需要调用屏幕的初始化和清屏函数,通常是下面这样:

1
2
EPD_7IN5_V2_Init();
EPD_7IN5_V2_Clear();

这两个函数根据使用屏幕的型号不同也存在差异,在 sp32-waveshare-epd\src\utility 中可以根据型号找到对应的 .h 文件查看有哪些函数

Init 函数在 7.5 inch 这款屏中有四种,对应了后续不同的渲染过程

1
2
3
4
UBYTE EPD_7IN5_V2_Init(void);					// 完全渲染,在后续渲染屏幕的时候整个屏幕会闪烁刷新
UBYTE EPD_7IN5_V2_Init_Fast(void); // 快速渲染,虽然不会闪烁,但是会出现花屏的情况
UBYTE EPD_7IN5_V2_Init_Part(void); // 部分渲染,针对屏幕中的部分区域进行渲染,也不会闪烁,但是时间长了也会花屏
UBYTE EPD_7IN5_V2_Init_4Gray(void); // 没用过,应该是根据灰度显示不同区域的?

清屏也存在两种,分别对应了将整个屏幕刷黑和刷白

1
2
void EPD_7IN5_V2_Clear(void);
void EPD_7IN5_V2_ClearBlack(void);

然后是渲染函数,传入图像数组进行渲染

1
2
3
4
void EPD_7IN5_V2_Display(UBYTE *blackimage);
void EPD_7IN5_V2_Display_Part(UBYTE *blackimage,UDOUBLE x_start, UDOUBLE y_start, UDOUBLE x_end, UDOUBLE y_end);
void EPD_7IN5_V2_Display_4Gray(const UBYTE *Image);
void EPD_7IN5_V2_WritePicture_4Gray(const UBYTE *Image);

这几个函数都是传入 byte 类型的图像进行渲染,对图像的绘制由另外一个库来实现。EPD_7IN5_V2_Display_Part函数用于进行部分渲染,部分渲染之前需要调用 EPD_7IN5_V2_Init_Part 函数,否则部分渲染的部分还是会进行闪烁。

图像绘制

上面的各种 Display 函数需要传入一个 Image 数组进行渲染,微雪所提供的库里也包含了绘制图像的方法,需要引入 GUI_Paint.h 文件

在绘制图像之前需要定义这个数组并初始化,通常直接全局声明避免爆栈,也可以参考一下示例程序,这里主要对不同的函数调用说明。这样的话主文件的头部如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UBYTE *BlackImage;
UWORD Imagesize = ((EPD_7IN5B_V2_WIDTH % 8 == 0) ? (EPD_7IN5B_V2_WIDTH / 8 ) : (EPD_7IN5B_V2_WIDTH / 8 + 1)) * EPD_7IN5B_V2_HEIGHT;

void setup() {
DEV_Module_Init();

if ((BlackImage = (UBYTE *)malloc(Imagesize)) == NULL) {
printf("Failed to apply for black memory...\r\n");
while (1);
}

Paint_NewImage(BlackImage, EPD_7IN5_V2_WIDTH, EPD_7IN5_V2_HEIGHT, 0, WHITE);
EPD_7IN5_V2_Init();
EPD_7IN5_V2_Clear();

Paint_Clear(WHITE);
}

Paint_NewImage 传入 BlackImage 用于设定图像,其实可以直接把它理解为一个画板了,后面的所有绘制操作本质上都是在这个画板上实现

Paint_Clear 将画板刷为对应的颜色,还有另外一个 Paint_ClearWindows 在局部刷新的时候用于刷新单独的一个长方形区域

Paint 还有其他的函数,但是这次开发没有涉及到也就不说明,感兴趣可以探索一下(懒了

1
2
3
4
5
void Paint_SelectImage(UBYTE *image);
void Paint_SetRotate(UWORD Rotate);
void Paint_SetMirroring(UBYTE mirror);
void Paint_SetPixel(UWORD Xpoint, UWORD Ypoint, UWORD Color);
void Paint_SetScale(UBYTE scale);

点、线、面

绘制点、线、面有四个函数,分别绘制点、线、长方形和圆形:

1
2
3
4
void Paint_DrawPoint(UWORD Xpoint, UWORD Ypoint, UWORD Color, DOT_PIXEL Dot_Pixel, DOT_STYLE Dot_FillWay);
void Paint_DrawLine(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend, UWORD Color, DOT_PIXEL Line_width, LINE_STYLE Line_Style);
void Paint_DrawRectangle(UWORD Xstart, UWORD Ystart, UWORD Xend, UWORD Yend, UWORD Color, DOT_PIXEL Line_width, DRAW_FILL Draw_Fill);
void Paint_DrawCircle(UWORD X_Center, UWORD Y_Center, UWORD Radius, UWORD Color, DOT_PIXEL Line_width, DRAW_FILL Draw_Fill);

这些函数里需要传入的有:

坐标:XpointYpoint 或者 XstartYstart 这样的字段都是用于指示坐标,直接传入整数,注意不要超过绘制范围,也就是屏幕的像素长宽

颜色:在墨水屏里就只有 BLACK 和 WHITE 黑白两种选项,其他类型的屏幕里可能会有 RED 这样的红色选项

画笔粗细:也就是 DOT_PIXEL,可以直接使用定义好的 DOT_PIXEL_1X1DOT_PIXEL_8X8DOT_PIXEL_DFT 默认为 1x1

点、线、面里存在不同的是最后一个参数

  • 在点的绘制里,DOT_STYLE 可选为 DOT_FILL_AROUNDDOT_FILL_RIGHTUP,代表的是 1x1 或 2x2 的像素点
  • 在线的绘制里,LINE_STYLE 可选为 LINE_STYLE_SOLIDLINE_STYLE_DOTTED 代表实线和虚线
  • 在面的绘制里,DRAW_FILL 可选为 DRAW_FILL_EMPTYDRAW_FILL_FULL 代表空心或实心

文字

微雪的库里自带了一些英文或中文的文字库,如果需要引入其他字体就需要自行构建文字库进行传入绘制,绘制文字的函数有:

1
2
3
4
5
void Paint_DrawChar(UWORD Xstart, UWORD Ystart, const char Acsii_Char, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawString_EN(UWORD Xstart, UWORD Ystart, const char * pString, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawString_CN(UWORD Xstart, UWORD Ystart, const char * pString, cFONT* font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawNum(UWORD Xpoint, UWORD Ypoint, int32_t Nummber, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);
void Paint_DrawTime(UWORD Xstart, UWORD Ystart, PAINT_TIME *pTime, sFONT* Font, UWORD Color_Foreground, UWORD Color_Background);

Paint_DrawChar:绘制单个 ascii 字符

Paint_DrawString_EN:绘制英文字符串

Paint_DrawString_CN:绘制中文字符串,也可以混杂英文

Paint_DrawNum:绘制数字

Paint_DrawTime:绘制时间序列

(Xpoint,Ypoint)和 (Xstart, Ystart) 这样的字样也不用多说,代表坐标,随后的就是字符、字符串或者时间序列

cFONT 和 sFONT 则代表了不同的字体的定义,可以直接看它们的定义:

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
>//ASCII
>typedef struct _tFont
>{
const uint8_t *table;
uint16_t Width;
uint16_t Height;

>} sFONT;

>//GB2312
>typedef struct // 汉字字模数据结构
>{
unsigned char index[3]; // 汉字内码索引
const char matrix[MAX_HEIGHT_FONT*MAX_WIDTH_FONT/8]; // 点阵码数据
>}CH_CN;

>typedef struct
>{
const CH_CN *table;
uint16_t size;
uint16_t ASCII_Width;
uint16_t Width;
uint16_t Height;

>}cFONT;

在不需要使用其他字体的时候可以不用管,直接传入定义好的字体就可以,例如下面就是英文和中文里各种字号的字体

1
2
3
4
5
6
7
8
>extern sFONT Font24;
>extern sFONT Font20;
>extern sFONT Font16;
>extern sFONT Font12;
>extern sFONT Font8;

>extern cFONT Font12CN;
>extern cFONT Font24CN;

如果需要使用自定义字体,那么需要将它们导出,例如 github 上的 https://github.com/theHEXstyle/font2bytes 是一个将 ASCII 字符导出为字节码的程序,应该也有中文的相关开源库。在导出为字节码后放入到项目目录里引用就可以,例如我用了 Firacode 字体,那么就是先声明一个 external_fonts.h 文件:

1
2
3
4
5
6
7
8
>#ifndef __EXTERNAL_FONTS_H
>#define __EXTERNAL_FONTS_H

>#include <fonts.h> // 这个字体文件是微雪里的,便于调 sFONT 类型

>extern sFONT FiraBoldFont42;

>#endif

然后将转换好的字形文件导入,并把主要的字形类改为对应的名字,在 external_fonts.cpp 文件里:

1
2
3
4
5
>sFONT FiraBoldFont42 = {
FiraBoldFont42_Table,
26, /* Width */
42, /* Height */
>};

Color_ForegroundColor_Background 字段分别代表了文字的颜色和背景颜色

在所需要的绘制完成后,通过前面的各种 Display 函数就可以将图像显示在屏幕上