简介:一份专为MicroPython开发者准备的中文点阵字库集合,适用于TFT屏幕等需要显示汉字的嵌入式场景。资源包含12x12、16x16、24x24、32x32四套尺寸规格,每套均覆盖宋体、黑体、楷体、仿宋、隶书、幼圆、小标宋等七种字体,ASCII字符与汉字字符一并收录;取模方式为按行取值、高位在前,并配有Python调用接口,可直接用于字库读取与像素渲染。压缩包共24个文件,包括字库数据、调用脚本py文件和说明txt文件,整体大小5.74MB,目录按字体尺寸分门别类,便于按需选用。目前已有529人学习下载,适合正在做显示驱动的嵌入式开发者、DIY爱好者及MicroPython项目实践者,可免去自行取模的重复劳动,快速获得从12点阵到32点阵的完整中文字库支持。
1. 为什么MicroPython里显示中文,总要先解决字库问题
做MicroPython点阵字库这件事,说到底是给自己造轮子。网上能搜到的字库方案不少,但要么是Arduino风格的老代码,要么是取模软件生成一段静态数组然后硬编码进程序,真正愿意给MicroPython提供一套清爽的调用接口的,基本没有。
你要在LCD、OLED上显示一行中文,底层的本质就是"把每个字的点阵数据喂给屏幕驱动"。但MicroPython和嵌入式C还是有明显差别的:解释型语言跑得慢、内存小、文件系统也紧张,直接照搬C语言里的字库组织方式,往往跑起来又卡又费内存。这套东西我折腾了近一周,最后整理出一个带完整调用接口的原创方案,核心就三件事:一份按索引顺序排好的字模数据、一张可检索的字符索引表、以及一套不关心底层的渲染接口。
这个方案适合这几类人:用MicroPython驱动SSD1306这类OLED屏,想显示中文又不想每次手工粘贴取模数据的初学者;准备做菜单系统、温湿度仪表盘、小游戏界面,需要频繁渲染文字的嵌入式爱好者;以及恰好也在纠结"字模到底应该怎么存、怎么取、怎么画"的开发者。
做完你会发现,字库本身不难,难的是把存储、索引、渲染这三层拆干净,让上层调用者只传坐标和字符串就够了。
2. 点阵字库模型:一个字32字节是怎么算出来的
2.1 16x16点阵的存储本质
先说最基础的点阵模型。一个汉字,在16x16的点阵里就是16行、每行16个像素点。每个像素只有黑/白两种状态,用一个bit表示:1代表显示、0代表空白。这样每行16个像素正好是2个字节,整个字就是16行乘2字节,等于32字节。
16 像素 / 8 = 2 字节/行 2 字节/行 × 16 行 = 32 字节/字别看这32字节少,3755个一级汉字的字库,换算过来就是3755 × 32 ≈ 117KB。加上ASCII字符、符号之后,一套完整基础字库大概是120KB上下。这个体积放到ESP32这类4MB Flash的板子上完全没问题,但如果是ESP8266或者内存比较小的RP2040项目,就要考虑按需裁剪成"仅含几百个常用汉字"的精简字库。
这个"32字节一个字"的模型,是整个字库系统的最底层。字模数据本质上就是一个超长的bytes串,按字符索引顺序排列,每个字符占用固定长度的切片。后面所有设计都围绕这个固定长度展开。
2.2 取模方向必须跟显示驱动"同频"
这是我在一开始就踩进去的坑,也是最值得提前说清楚的。
同样一个字,取模软件可以按逐行取、逐列取、正向取、反向取,出来的32字节完全不一样。而屏幕驱动(尤其是MicroPython里最常见的SSD1306 + framebuf组合)对数据的排列方式是有要求的。
MicroPython的framebuf.MONO_VLSB格式,本质是横向取模、低位在左:每个字节的bit0对应最左边的列,8个bit对应横向连续的8个像素;一行一行的数据从上往下排列。所以,字模数据要按"逐行式"生成,也就是先第一行的两个字节,再第二行的两个字节,这样小字模的FrameBuffer才能直接blit到屏幕的FrameBuffer上,不用做任何数据重排。
如果你手头的取模软件默认输出的是"逐列式"格式——即先取第0列的上8点、下8点,再取第1列的上8点、下8点——那么直接往framebuf里放会得到一整片乱码。不同字体引擎、不同LCD控制器,对取模方向的要求都可能不一样。最好的做法是在PC端生成字模的时候,就按目标驱动的格式一次到位,而不是在MicroPython运行期去转换,节省的那点存储不值得付出速度代价。
| 取模方式 | 数据顺序 | 适配情况 | 能否直接blit |
|---|---|---|---|
| 逐行式,高位在左 | 行0字节0、字节1,行1字节0、字节1... | SSD1306/ST7735配合framebuf | 可以 |
| 逐列式,每列上下字节连续 | 列0上、列0下,列1上、列1下... | 某些TFT LCD控制器 | 不可以,需要转置 |
| 逐行式,低位在左 | 行0字节0、字节1...,但bit方向相反 | 某些国产屏驱动 | 会有镜像问题 |
所以在最开始的方案设计阶段,就要先确认你用的显示驱动和buffer格式,再决定取模方向。这个顺序一定不能反,否则后面全是在给前面的错误买单。
3. 字库生成:从TTF字体到MicroPython能加载的二进制
3.1 PC端生成脚本,一次搞定字模和索引表
手工在取模软件里一个个点字,工作量太恐怖。我的做法是写一个Python脚本,在PC端用Pillow读取TTF字体,把字符逐像素渲染成16x16点阵,按"逐行式、低位在左"的规则压缩成字节。
# PC端生成脚本,python3环境运行 # 依赖:pip install pillow from PIL import Image, ImageFont, ImageDraw SIZE = 16 FONT_PATH = 'wqy-microhei.ttc' # 换成你的中文字体 CHARSET = open('charset.txt', encoding='utf-8').read() font = ImageFont.truetype(FONT_PATH, SIZE) data = bytearray() for ch in CHARSET: img = Image.new('1', (SIZE, SIZE), 0) draw = ImageDraw.Draw(img) # 用getbbox校正字形偏移,否则部分汉字会偏上或偏下 bbox = font.getbbox(ch) draw.text((-bbox[0], -bbox[1]), ch, font=font, fill=1) # 逐行扫描,每行16像素 = 2字节,低位在左 for y in range(SIZE): for x in range(0, SIZE, 8): byte = 0 for bit in range(8): if x + bit < SIZE and img.getpixel((x + bit, y)): byte |= 1 << (7 - bit) data.append(byte) open('font16.bin', 'wb').write(bytes(data))这里有个细节不能漏:直接用draw.text((0,0), ch)画出来的字形,很多字会被裁掉顶部或者挪到底部,因为中文字体的行高和16x16点阵不是天然对齐的。用font.getbbox(ch)拿到字形的实际包围盒,再取负值作为绘制坐标,可以把字形"拎"到点阵左上角对齐。这个处理不做好,生成的字库会出现整体上移下移,画出来像没对齐一样。
3.2 索引表:让MicroPython不依赖GB2312编码也能查字
MicroPython标准库对字符编码的支持是精简过的,很多固件里根本没有encode('gb2312')这种方法,源码文件里的中文字符串又默认是Unicode编码。如果按照传统C语言方案用区码位码去计算字模地址,MicroPython环境里很难实现。
我的做法是同步生成一份"索引字符串":把字库里的汉字按Unicode码点排序后拼成一个字符串,与字模文件里的二进制顺序严格一致。比如charset.txt里有哪些字,脚本就按相同顺序生成INDEX_STR。
这样在MicroPython端,查字库地址就退化成一次字符串查找:
index = INDEX_STR.find(ch) if index < 0: index = INDEX_STR.find('?') # 缺字回退到全角问号 addr = index * 32str.find()在MicroPython里是有内置实现的,对3755字的索引串做一次线性查找,耗时在几十微秒级别,显示文字时完全感觉不到。这个方案虽然不如字典查找优雅,但胜在简单可靠,任何固件都能跑。
3.3 字库规模和存储策略怎么选
字库大文件有三种存放方式,各有取舍:
- 全部塞进源码:生成一个
font_data.py,里面放一个超长的bytes字面量。好处是加载简单,缺点是编译出来的固件体积大,并且MicroPython加载模块时会把整个bytes对象放进堆内存。 - 放文件系统:把
font16.bin放到LittleFS或SD卡,程序启动后用open()读入内存,或者保留文件句柄、按地址跳过读取。适合RAM紧张的板子,但读文件比读内存慢。 - 外部Flash字库:把字库烧到SPI Flash芯片,用
addr直接寻址读取,容量可以做到几十MB。这是产品级方案,个人项目里属于杀鸡用牛刀。
我推荐第一档:如果只做一级汉字字库,3755字带来的约118KB数据在ESP32-S3这类2MB PSRAM的板上毫无压力;如果用的是ESP8266,建议把字库裁剪到300个高频汉字,体积不到10KB,加载和渲染都快很多。
4. 调用接口设计:输入输出清爽,调用方不需要懂内部
4.1 接口设计的底层思路
这个项目叫"带调用接口的点阵字库",那么接口的清爽程度就是核心。我设计接口时参考了一个标准:上层调用者拿到这个类,不需要理解字模、地址、偏移、取模方向,只需知道"传进字符和坐标,就能把字画到屏幕上"。
就像HTTP接口有明确的入参和返回码一样,我的底层方法也遵循同样的原则:入参明确、返回明确、异常明确。渲染接口返回本次绘制文字的像素宽度,方便上层做居中、右对齐、自动换行这些排版操作,而不是让调用者自己去猜"我的字符串到底占了多宽"。
4.2 核心类实现
完整代码如下,这是整个字库模块的骨架:
import framebuf class DotMatrixFont: def __init__(self, font_data, index_str, size=16): self._data = font_data self._index = index_str self._size = size self._bytes_per_char = size * size // 8 self._fallback = '\uff1f' # 全角问号 def has_char(self, ch): return self._index.find(ch) >= 0 def bytes_per_char(self): return self._bytes_per_char def char_addr(self, ch): pos = self._index.find(ch) if pos < 0: pos = self._index.find(self._fallback) if pos < 0: raise ValueError('fallback char missing') return pos * self._bytes_per_char def get_matrix(self, ch): addr = self.char_addr(ch) # 注意:返回memoryview切片,避免大bytes切片复制 return memoryview(self._data)[addr:addr + self._bytes_per_char] def char_width(self, ch): # 半角字符按半个汉字宽处理 return self._size // 2 if ord(ch) < 256 else self._size def text_width(self, text, spacing=1): width = 0 for ch in text: width += self.char_width(ch) + spacing return max(0, width - spacing) def render(self, display_fb, x, y, text, spacing=1): cursor_x = x for ch in text: if ch == '\n': y += self._size cursor_x = x continue self._draw_char(display_fb, cursor_x, y, ch) cursor_x += self.char_width(ch) + spacing def _draw_char(self, display_fb, x, y, ch): data = bytes(self.get_matrix(ch)) char_fb = framebuf.FrameBuffer( bytearray(data), self._size, self._size, framebuf.MONO_VLSB ) display_fb.blit(char_fb, x, y)几个接口的设计点值得展开说说。
get_matrix返回memoryview而不是bytes切片,是为了避免每次取字模都产生一次内存复制。120KB的原始数据如果每显示一个字都要复制32字节,短期内看不出问题,但在大量渲染时会造成频繁的内存分配,MicroPython的GC一多,帧率就会肉眼可见地掉。memoryview切片是零拷贝的,只保留一个视图引用,速度差别很明显。
char_width方法解决了中英文混排的宽度问题。英文和数字按半个汉字的宽度处理,这样"温度:25C"这类字符串显示出来不会歪歪扭扭。所有排版接口都基于这个方法计算,后续如果想支持8x16的ASCII小字库,只需要重写char_width和_draw_char,上层完全无感。
render返回值的说明我在docstring里写得比较啰嗦,实际用起来就一句话:w = font.text_width('你好'),然后x = (128 - w) // 2就能居中。
4.3 和OLED驱动的实际衔接
以SSD1306为例,上层使用代码长这样:
from machine import Pin, I2C from ssd1306 import SSD1306_I2C import framebuf i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=400000) oled = SSD1306_I2C(128, 64, i2c) # 加载字库 with open('font16.bin', 'rb') as f: font_data = f.read() font = DotMatrixFont(font_data, INDEX_STR) # 创建屏幕framebuf screen = framebuf.FrameBuffer(oled.buffer, 128, 64, framebuf.MONO_VLSB) # 渲染一行文字并居中 text = '你好MicroPython' w = font.text_width(text) font.render(screen, (128 - w) // 2, 24, text) oled.show()注意这里有个小技巧:SSD1306_I2C内部已经包含一个framebuf.FrameBuffer,它和显示buffer共用一块内存。不要在它之外另建一个大buffer,而是直接对oled.buffer创建FrameBuffer引用,这样渲染完成后调用oled.show()就能把内容推送到屏幕,全程只占用一份显示RAM。
5. 实测效果与三个让人头大的坑
5.1 128x64 OLED上的实际表现
在ESP32-S3 + SSD1306 128x64的组合上实测:加载118KB字库到内存,启动耗时约200ms;渲染一行10个汉字(含索引查找、blit、屏幕刷新),单帧耗时大约50ms,主要瓶颈在I2C 400kHz传输整屏数据上,渲染本身的CPU时间很少。肉眼观察基本流畅,翻菜单、滚动文字都没问题。
如果是ESP8266或内存更小的板子,强烈建议只在字库里放常用字,程序启动时用open()按需读取文件,而不是一次性读入全部数据。实测下来,300字的精简字库整包也就9.6KB,加载时间几乎为零。
5.2 坑一:字模镜像,根因在取模方向
我第一次生成字库后,所有文字整体左右翻转,看起来像镜子里的字。排查后发现,问题出在生成脚本里每行字节的bit方向:我用byte |= 1 << bit得到的是"低位在右",而framebuf期望"高位在右、bit0对应最左像素"。修复方式就是把移位改成byte |= 1 << (7 - bit)。这个坑藏得很深,因为单个字符看起来只是一堆不太规则的竖线,不放大对比根本不知道是镜像还是乱码。
排查技巧:不要拿汉字调试,先用一个大号的数字"6"做测试。数字方向感强,一眼就能看出是镜像、旋转还是错位。
5.3 坑二:SOURCE文件编码不一致导致索引错乱
MicroPython源码文件如果用GBK编码保存,INDEX_STR里的中文字符串在编译时就会发生混乱,find查找结果部分正确部分错误。后来我把IDE的默认文件编码改成UTF-8,并且在源码第一行加了# -*- coding: utf-8 -*-,问题彻底解决。
如果你是把源码复制到MicroPython设备上运行,尤其注意传输工具是否做了编码转换。用rshell、ampy这类工具时,默认编码通常是UTF-8,但如果用了Windows记事本保存,就要格外小心。
5.4 坑三:大bytes对象在低内存设备上触发GC卡顿
把118KB字库一次性放进bytes对象后,在ESP8266上出现了一个现象:程序跑着跑着突然卡几百毫秒,然后恢复。用gc.mem_alloc()观察后发现,由于字库占了大量内存,MicroPython的垃圾回收频繁触发,而回收时扫描大对象的时间明显偏高。
解决方案有两个:一是换用bytearray并提前分配固定容量,利用memoryview访问;二是把字库改为分段读取,只用一个小窗口缓存当前显示区域的字模。对于个人项目,如果板子连128KB都紧张,直接走"精简字库"路线通常最省心。
最后再分享一个扩展思路
这套接口设计其实没有绑死在16x16点阵上。类初始化时有size参数,只要生成字模时对应修改bytes_per_char,32x32的大字库也能直接用同一个接口渲染。我后来做的一款仪表盘界面,就是同时挂了16x16的正文字库和32x32的数字大字库,两个字体实例互不干扰,上层排版逻辑完全不用改。
字库的存储位置也可以继续往产品化方向扩展:把字库文件放到外部SPI Flash后,只要把get_matrix内部的读取逻辑改成chip.read(addr, length),上层调用代码一行都不用动。这就是接口拆分层级带来的好处,初期多花一点时间把结构理清楚,后面扩展起来是真的舒服。
本文还有配套的精品资源,点击获取