拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MicroPython点阵字库:从字模生成到渲染接口的完整方案

简介一份专为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, encodingutf-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, fontfont, fill1) # 逐行扫描每行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, size16): 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, spacing1): 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, spacing1): 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, sclPin(9), sdaPin(8), freq400000) 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_char32x32的大字库也能直接用同一个接口渲染。我后来做的一款仪表盘界面就是同时挂了16x16的正文字库和32x32的数字大字库两个字体实例互不干扰上层排版逻辑完全不用改。字库的存储位置也可以继续往产品化方向扩展把字库文件放到外部SPI Flash后只要把get_matrix内部的读取逻辑改成chip.read(addr, length)上层调用代码一行都不用动。这就是接口拆分层级带来的好处初期多花一点时间把结构理清楚后面扩展起来是真的舒服。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门