建立点阵字符 API 实现单片机显示任意字符

一、前言

本次使用了 ESP32-S3-N16R8 和一块 3.2 寸 SPI 电容触摸屏,屏幕驱动使用 micropython-ili9341

这块儿 MCU 外置了 8M 的 PSRAM,我们可以轻松使用大尺寸的帧缓冲(Frame Buffer)。

二、字符显示

先来看看通常是如何显示字符的:

使用上一篇文章中的“F”举例:

我们之前使用了一个列表来存储它:

1
2
3
4
5
6
7
8
9
# 定义一个列表用来存储“F”的信息
F = [0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, ]

在实际中,更常用的做法是以一个字节(8位)为单位存储,那么得到一个 16 进制数的列表:

1
F = [0x00, 0x3C, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00]

进一步的,我会把相同字体相同大小的字符都存到一个字典:

1
2
3
4
5
6
7
# 宋体 32px*32px
SimSun32x32 = {
'天': [0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x08, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFF, 0x03, 0x02, 0x06, 0x06, 0x06, 0x0C, 0x0C, 0x18, 0x30, 0x30, 0x60, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x40, 0x40, 0x40, 0x20, 0x20, 0x10, 0x18, 0x08, 0x0C, 0x06, 0x03, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x70, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xF0, 0x7E, 0x30, 0x00, 0x00],
'气': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x03, 0x06, 0x04, 0x0C, 0x18, 0x17, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xE0, 0xC0, 0xC0, 0x80, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x02, 0x02, 0x03, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xF0, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x88, 0xC8, 0xF8, 0x78, 0x1C, 0x00],
'温': [0x00, 0x00, 0x00, 0x0C, 0x06, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x30, 0x18, 0x0C, 0x08, 0x01, 0x01, 0x01, 0x03, 0x02, 0x02, 0x06, 0x3E, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0F, 0x0C, 0x0C, 0x2C, 0x4C, 0x4F, 0x4C, 0x4C, 0x8C, 0x8C, 0x8F, 0x0C, 0x08, 0x00, 0x20, 0x3F, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x8C, 0x8C, 0x8C, 0x8C, 0x8C, 0x8C, 0x8C, 0x8C, 0x8C, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC0, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x20, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x6C, 0xFC, 0x00, 0x00],
'度': [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x07, 0x06, 0x06, 0x06, 0x06, 0x07, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x04, 0x04, 0x0C, 0x0C, 0x0C, 0x08, 0x08, 0x18, 0x10, 0x10, 0x20, 0x20, 0x47, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x04, 0x07, 0x06, 0x06, 0xFF, 0x06, 0x06, 0x06, 0x06, 0x07, 0x06, 0x00, 0x3F, 0x04, 0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x03, 0x0E, 0x70, 0x80, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xE0, 0x40, 0xFF, 0x04, 0x07, 0x06, 0x06, 0xFF, 0x06, 0x06, 0x06, 0x06, 0xFE, 0x06, 0x01, 0xFF, 0x03, 0x07, 0x06, 0x0C, 0xD8, 0x70, 0xF8, 0x9E, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0xFC, 0x00, 0x00, 0x00, 0x30, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xF8, 0x10, 0x00],
}

此时我们要用到哪个字符的数据直接查字典就行,下面是一个使用了帧缓存的文本显示函数:

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
# 显示字符串
def display_str(display, text, font, x, y, color=65535, background=0):
# 获得字体中字符大小
char_width, char_height = get_char_size(font)
# 获得字符数目
char_num = len(text)
# 获得所有字符的数据
char_data_list = [font[char] for char in text]
# 最终的数据
final_bytes = bytearray()
# 得到每行的比特数据
for i in range(char_height):
row_data = []
for char_data in char_data_list:
for k in range(char_width // 8):
item = '{:08b}'.format(char_data[i + k * char_height])
row_data.append(item)

# 将所有行数据拼接成一个字符串
row_data_str = ''.join(row_data)

# 将本行的数据转换为字节
for char in row_data_str:
pixel_color = color if char == '1' else background
# 将单个像素点的颜色转换为两个字节(RGB565)
final_bytes.extend(pixel_color.to_bytes(2, 'big'))

# 显示
display.block(x, y, x + char_width*char_num-1, y + char_height-1, final_bytes)

简单的调用:

1
2
3
4
5
# 以 (10, 10) 点为文本左上角顶点坐标
# 字体为宋体,大小为 32px*32px
# 字体颜色为黑色,背景颜色为红色
# 显示“天气”:
display_str(display, "天气", "SimSun32x32", 10, 10, color565(0, 0, 0), color565(255, 0, 0))

三、点阵 API

一般情况下我们将所有要用到的字符都取模存到单片机里,就可以满足使用需求了。但如果单片机需要显示消息,那么字库就必须比较全面。

以常用的 3000 个中文汉字为例,每个字符均为 32px*32px ,那么点阵数据大小为:

1
32*32*3000/8 = 375 KiB

解析这么大的字典对单片机的性能要求很大,存储大量字符在本地处理是不可行的。

ESP32-S3 是可以联网的,因此可以使用一个曲线救国的路子:

建立一个点阵字库 API —— 单片机向服务端发起请求(携带文本、字体、颜色等信息)—— 服务端解析得到字节列表返回响应 —— 单片机渲染字节列表

(一)点阵 API

当然是使用 FastAPI 来实现,下面是一个简单的实现:

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
from fastapi import FastAPI, Query
import uvicorn


# SimSun32x32 是一个存储了超过 7000 个字符的点阵数据的字典
SimSun32x32 = {
'一': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
'丨': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
'丿': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x03, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x10, 0x20, 0x40, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
...,
'@': [0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x06, 0x0E, 0x0C, 0x18, 0x18, 0x18, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x18, 0x18, 0x18, 0x1C, 0x0C, 0x06, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x3E, 0xF0, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x01, 0x0F, 0x18, 0x30, 0x30, 0x70, 0x60, 0x60, 0x60, 0x60, 0x60, 0x31, 0x1E, 0x00, 0x00, 0x00, 0x80, 0xE0, 0x7C, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3E, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8, 0x70, 0x30, 0x30, 0x70, 0x60, 0x60, 0x60, 0xE0, 0xC0, 0xC0, 0x63, 0x7E, 0x00, 0x00, 0x00, 0x01, 0x1E, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x40, 0x20, 0x30, 0x10, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x10, 0x10, 0x30, 0x20, 0x40, 0xC0, 0x80, 0x00, 0x00, 0x00, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00],
'#': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x10, 0x10, 0x10, 0x10, 0x30, 0x30, 0x30, 0xFF, 0x20, 0x20, 0x20, 0x20, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x0C, 0xFF, 0xFF, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0xFF, 0x18, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
'$': [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x07, 0x19, 0x31, 0x61, 0x41, 0xC1, 0xC1, 0x61, 0x71, 0x39, 0x1F, 0x07, 0x01, 0x01, 0x01, 0x01, 0x01, 0x81, 0x81, 0x81, 0xC1, 0xA1, 0x9F, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0xF1, 0x8D, 0x87, 0x83, 0x81, 0x81, 0x81, 0x80, 0x80, 0x80, 0x80, 0xF0, 0xF8, 0x9E, 0x8E, 0x87, 0x83, 0x83, 0x87, 0x86, 0x86, 0x9C, 0xF0, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
}


app = FastAPI()


# get_char_size(font) 可以获取该字体的长宽信息
def get_char_size(font):
...
return char_width, char_height


@app.get("/bytes")
def get_bytes(font: str = Query, text: str = Query(...), color: int = Query(65535), background: int = Query(0)):
font = fonts[font]
# 获得字体中字符大小
char_width, char_height = get_char_size(font)
# 获得字符数目
char_num = len(text)
# 获得所有字符的数据
char_data_list = [font[char] if char in font else font['?'] for char in text]
# 最终的数据
bytes_list = []
# 得到每行的比特数据
for i in range(char_height):
row_data = []
for char_data in char_data_list:
for k in range(char_width // 8):
item = bin(char_data[i + k * char_height])[2:].zfill(8)
row_data.append(item)

# 将所有行数据拼接成一个字符串
row_data_str = ''.join(row_data)

# 将本行的数据转换为字节
for char in row_data_str:
pixel_color = color if char == '1' else background
# 将单个像素点的颜色(RGB565)转换为两个数字(256进制)存入列表
# 将高字节添加到列表中
bytes_list.append((pixel_color >> 8) & 0xFF)
# 将低字节添加到列表中
bytes_list.append(pixel_color & 0xFF)

return {
"char_width": char_width,
"char_height": char_height,
"char_num": char_num,
"byte_list": bytes_list
}


# 启动服务
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)

这个的完整程序及部分我已经整理的字体可以在 DotMatrixAPI 查看。

(二)使用 API 显示任意文本

直接贴代码:

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
import urequests, json, time
import config


# 编码 URL 参数
def urlencode(params):
encoded_params = []
for key, value in params.items():
if isinstance(value, str):
value = value.encode('utf-8').hex()
value = ''.join(['%' + value[i:i+2] for i in range(0, len(value), 2)])
encoded_params.append(f'{key}={value}')
return '&'.join(encoded_params)


# 通过 API 显示字符串
def display_str_by_api(display, text, font, x, y, color=65535, background=0):
raw_data = None # 初始化 raw_data 变量
API_ENDPOINT = config.DotMatrixAPI # API 地址
# 调用 API 获取字符串
for i in range(5):
try:
print("第", i+1, "次尝试")
params = {
'font': font,
'text': text,
'color': str(color),
'background': str(background)
}
url = f'{API_ENDPOINT}/bytes?{urlencode(params)}'
print("请求 URL:", url)
response = urequests.get(url)
raw_data = response.content
response.close()
break
except Exception as e:
print("请求错误:", e)
time.sleep(1)
else:
print("API 请求失败")

# 解析数据
try:
data = json.loads(raw_data)
char_width = data['char_width']
char_height = data['char_height']
char_num = data['char_num']
byte_list = data['byte_list']
except ValueError as e:
print("JSON 解析错误:", e)

# 将数据转换为字节
final_bytes = bytes(byte_list)

# 显示
display.block(x, y, x + char_width * char_num - 1, y + char_height - 1, final_bytes)

(三)API 测试

使用 Reqable 测试API:

dark
sans