简介:面向嵌入式、单片机及点阵显示项目开发者,这套字库合集覆盖 HZK12、HZK14、HZK16、HZK24、HZK32、HZK40、HZK48 等汉字点阵字库,以及 ASC12/16/24/32/48 等 ASCII 字模,并且为每种字体都配好了读取代码,解决了制作点阵文字时字库分散、取模方式不统一的问题,省去四处寻找字库的麻烦。压缩包共 55 个文件,总大小约 7.53MB,按 txt、java、c、class、obj、exe 及各类 hzk、asc 字库文件分门别类,既可直接查阅也可用于二次开发。目前已有 1652 人学习下载,适合单片机屏幕显示、LED 点阵屏、字符取模与字符渲染等项目的开发人员。示例代码提供 Java 与 C 两种实现,可快速理解汉字和 ASCII 字库的存储偏移、字模排列与读取流程,再配合工具和说明文件,能在实际项目中直接生成所需字模数据,有效减少底层摸索时间,提升开发效率。 做点阵屏最崩溃的时刻,不是代码调不出来,而是中文字库读出来的数据全是乱的。我头一回做16×16 LED屏的时候,显示“欢迎光临”四个字,屏幕上出来四团完全看不出形状的噪声点,排查到半夜才意识到,问题根本不在驱动电路,而在字库文件本身——HZK16到底按什么顺序存字模,代码按什么偏移去取,这两个一错,后面全白搭。
这话题看着冷,但只要你碰LCD、OLED、LED点阵屏,迟早要撞上。这里说的HZK系列、ASC系列,就是GB2312编码下最常用的点阵汉字库和ASCII字库文件。本文把这批字库的结构、偏移规则、读取代码和实际排坑经验一次讲清楚,适合刚接触单片机显示、正准备做汉字屏的开发者。
1. 从HZK16开始认识这批点阵字库
1.1 HZK命名与“一个字占多少字节”
HZK是汉字库的拼音缩写,后面的数字代表点阵规格。HZK16就是16×16点阵的汉字库,即每个汉字用16行、每行16个像素点来表示,一个像素点用1位数据,0不亮、1点亮。16×16总共256位,除以8就是32字节,所以HZK16里每个汉字固定占32字节。
这个“每字32字节”是所有后续计算的地基。显示一个汉字时,只要从字库文件里正确读出这32字节,再按点阵位置逐位点亮,字形就出来了。
HZK系列里的HZK24、HZK32、HZK40、HZK48也是同一个逻辑,点阵越大字形越精细,占的字节也越多:
- HZK24:24×24点阵,每字 24×24÷8 = 72字节
- HZK32:32×32点阵,每字 32×32÷8 = 128字节
- HZK40:40×40点阵,每字 40×40÷8 = 200字节
- HZK48:48×48点阵,每字 48×48÷8 = 288字节
唯独HZK12要小心。12×12算下来只有18字节,但12不是8的整数倍,很多字库文件实际按字节对齐存储,一行存2字节、共12行,于是每字变成24字节;也有的按18字节存。所以拿到HZK12文件时,第一件事就是确认每字到底占多少字节,不能想当然。我见过不止一个项目因为默认按24字节读、实际文件却是18字节,结果汉字全部错位。
1.2 GB2312区位码与偏移计算公式
HZK字库文件内部是按GB2312编码顺序排列字模的。GB2312用两个字节表示一个汉字,两个字节减去0xA0后,得到区号和位号。区号范围01~94,位号范围01~94,汉字就按“区”和“位”排成一张二维表。
读取任意汉字字模的偏移公式是:
偏移 = ((区号 - 1) * 94 + (位号 - 1)) * 每字字节数但在实际代码里,一般直接拿编码字节计算,区号就是高字节 - 0xA1,位号就是低字节 - 0xA1,写成:
offset = ((uint32_t)(q - 0xA1) * 94 + (uint32_t)(w - 0xA1)) * bytes_per_char;这个公式适用于HZK12、HZK16、HZK24、HZK32、HZK40、HZK48全套,区别只在最后乘的每字字节数不同。GB2312的94区×94位理论上能排8836个字模,但符号区、空白区实际未必都有内容,不同来源的字库文件大小会有差异,偏移公式仍按区位顺序走。
1.3 先算一笔存储账再挑字库
做项目选字库尺寸,第一个制约因素是存储空间。按94×94全表估算,16点阵全字库约276KB,24点阵约622KB,32点阵约1.08MB,40点阵约1.69MB,48点阵约2.43MB。很多单片机的内部Flash连1MB都没有,把32点阵以上整库塞进去基本不现实。
所以实际项目里通常这样配:小尺寸LCD、OLED屏用HZK16或HZK12;中等尺寸LED点阵屏用HZK24、HZK32;户外大屏、高密度显示屏才考虑HZK40、HZK48,而且一般配外部Flash或SD卡。选择顺序应该是:先看存储预算,再看显示效果,而不是越大越好。
2. 字模的字节排列:横向、纵向和那点“先左后右”的讲究
2.1 HZK16标准文件里的横向排列
HZK16文件内部存放的是横向取模数据,排列方式很规整:第0~1字节是第一行的左右两半,第2~3字节是第二行的左右两半,依此类推,共16行、32字节。
具体到一个字节内部,高位对应左边的像素。也就是说,某一行左边8个点对应字节的bit7到bit0,bit7是这一行最左边的点。如果你读出来第一行的第一个字节是0x80,那左上角那个点必然点亮。
这个排列方式对应的是逐行扫描逻辑,很方便在LCD上按行刷数据。但市面上很多取模软件默认生成的是纵向取模,即先取每一列的上下像素,再取下一列。这两种数据互不兼容,直接套用必然乱码。
2.2 纵向取模:LED屏为什么总让你“转个方向”
很多LED点阵屏驱动采用逐列扫描,一列一列地送数据,这种情况下纵向取模更方便:第0~1字节是第一列的上下两半,第2~3字节是第二列的上下两半,共16列、32字节。
HZK16文件不是这个格式,所以需要做一次转换。从横向转纵向的关键代码长这样:
void hzk_h2v(const uint8_t *hbuf, uint8_t *vbuf, uint8_t w, uint8_t h) { // hbuf为横向取模数据,vbuf输出纵向取模数据 uint8_t bytes_per_row = (w + 7) / 8; // 每行字节数 memset(vbuf, 0, w * ((h + 7) / 8)); for (uint8_t row = 0; row < h; row++) { for (uint8_t col = 0; col < w; col++) { if (hbuf[row * bytes_per_row + col / 8] & (0x80 >> (col % 8))) { vbuf[col * ((h + 7) / 8) + row / 8] |= (0x80 >> (row % 8)); } } } }先想清楚这两件事再写转换:你的屏幕是逐行扫描还是逐列扫描,你的字库源文件是横向还是纵向。确定了,转换就是一重简单的坐标映射;不确定,就是靠猜,猜错就等着花屏。
2.3 ASC系列字库:ASCII字符的简单偏移规则
ASC系列是配套的ASCII半角字库,和HZK配合使用,用来显示英文字母、数字和符号。这类字库的偏移规则比汉字简单得多,通常直接用字符码作为索引。
以最常见的ASC16为例,每个字符是8×16点阵,每行1字节、共16行,所以每字符占16字节。读取第ch个字符的偏移就是:
offset = (uint32_t)ch * 16;字符‘A’的ASCII码是0x41,偏移就是0x41×16=1040,读16字节就是这个字母的字模。
ASC12、ASC24、ASC32、ASC48的区别只在点阵尺寸和每字符字节数:ASC12常见为6×12或12×12,每字符12字节左右;ASC24常见宽度12或16像素、高度24像素,每字符48字节上下;ASC32常见16×32,每字符64字节;ASC48常见24×48,每字符144字节。这组数不是绝对的,很多取模工具生成的具体规格都不太一样,拿到文件后先确认“每字符字节数”和“是否保留0x20之前的控制字符位置”,确认后再写代码。
3. C语言读取代码:直接可用的那几段
3.1 文件版:fopen加fseek读取HZK16与ASC16
最直接的用法是把字库文件放在SD卡或文件系统里,用标准C库读取。下面这套函数足够覆盖大多数场景。
#include <stdio.h> #include <stdint.h> #include <string.h> #define HZK16_BYTES 32 #define ASC16_BYTES 16 static FILE *hzk_fp = NULL; static FILE *asc_fp = NULL; int font_init(const char *hzk_path, const char *asc_path) { if (hzk_path) { hzk_fp = fopen(hzk_path, "rb"); if (!hzk_fp) return -1; } if (asc_path) { asc_fp = fopen(asc_path, "rb"); if (!asc_fp) return -1; } return 0; } int get_hzk16(uint8_t q, uint8_t w, uint8_t *out) { uint32_t offset = ((uint32_t)(q - 0xA1) * 94 + (uint32_t)(w - 0xA1)) * HZK16_BYTES; fseek(hzk_fp, offset, SEEK_SET); return fread(out, 1, HZK16_BYTES, hzk_fp) == HZK16_BYTES ? 0 : -1; } int get_asc16(uint8_t ch, uint8_t *out) { fseek(asc_fp, (uint32_t)ch * ASC16_BYTES, SEEK_SET); return fread(out, 1, ASC16_BYTES, asc_fp) == ASC16_BYTES ? 0 : -1; }注意fopen的模式必须用"rb",不是"r"。在Windows上,文本模式下遇到0x1A这类特殊字节会被当成文件结束符处理,字模数据是二进制数据,用文本模式读取大概率会出现字模不完整的问题。
3.2 内存数组版:把字库烧进Flash的用法
如果你的平台片内Flash够大,或者字库已经裁剪成数组文件,可以直接把整个字库定义成const数组,读取时用memcpy,效率比fseek高很多,还没有文件系统依赖。
// 假设已有外部生成的字符数组,内容就是整个HZK16字库 extern const uint8_t hzk16_font[]; void get_hzk16_mem(uint8_t q, uint8_t w, uint8_t *out) { uint32_t offset = ((uint32_t)(q - 0xA1) * 94 + (uint32_t)(w - 0xA1)) * HZK16_BYTES; memcpy(out, hzk16_font + offset, HZK16_BYTES); }这个版本最适合STM32这类直接外挂SPI Flash、或者把字模通过工具链转成C数组的项目。数组方式读取没有文件系统开销,在循环刷新大量汉字时优势非常明显。
3.3 字符串遍历:混合中英文时的编码判断
实际显示时,字符串里往往中英文混排。遍历时每遇到一个字节,要先判断它是不是ASCII字符,再决定按1字节还是2字节处理。
void draw_string(const char *str) { const uint8_t *p = (const uint8_t *)str; while (*p) { if (*p < 0x80) { // ASCII字符,读ASC字库 uint8_t asc_buf[ASC16_BYTES]; get_asc16(*p, asc_buf); draw_asc16(asc_buf); p++; } else { // 汉字,双字节,读HZK字库 uint8_t hz_buf[HZK16_BYTES]; uint8_t q = p[0]; uint8_t w = p[1]; get_hzk16(q, w, hz_buf); draw_hzk16(hz_buf); p += 2; } } }这里有个极其容易踩的C语言坑:char类型在不加说明时默认是有符号的。如果直接用char指针判断*p < 0x80,高字节像0xC4会被当成负数,判断结果仍然正确,但如果你把字节直接参与区位计算,负数的符号扩展会让偏移变成天文数字。一定要用uint8_t指针,或者每次强转uint8_t,再参与计算。
3.4 验证字模:先在PC上把点阵打印出来
字模读出来对不对,不要直接上屏,先在PC上打印出来看。写一个极简的打印函数,传到PC串口,或者本地跑一下,能直观看到字形:
void show_hzk16(const uint8_t *buf) { for (int row = 0; row < 16; row++) { for (int col = 0; col < 16; col++) { // 横向取模:第row行第col列 if (buf[row * 2 + col / 8] & (0x80 >> (col % 8))) { putchar('#'); } else { putchar('.'); } } putchar('\n'); } }只要这个打印能还原出完整的字形,说明偏移和取模方向都没问题,接下来上屏就算扫向不匹配,也是肉眼可查的镜像或颠倒,而不是无从下手的乱码。我调字库时每次必先做这一步,能省掉一半调试时间。
4. 我踩过的坑:乱码、错位和找不到字
4.1 源码编码不对:UTF-8取出来的“汉字”是三个字节
这是新手问得最多的问题之一。单片机工程在Keil、IAR里写"你好",编译器默认按GB2312/GBK存储,字符串里就是两个字节“C4 E3”、“BA C3”,上面代码直接可用。但从上位机、手机App、网络接口收到的中文字符串,绝大多数是UTF-8编码,一个小汉字在UTF-8里占3个字节,比如“你”是“E4 BD A0”。直接把这三个字节当成GB2312区位码去读字库,会得到完全不相关的字模。
解决办法:在PC端发送前用工具转好,或在下位机做一次UTF-8到GB2312的转换。调试阶段最省事的办法就是串口助手里把发送编码改成GBK,别用UTF-8。如果一定要支持UTF-8输入,需要引入转换表或者特定的转换代码,那是另一个话题,这里不展开。
4.2 偏移公式踩雷:多减0xA1、文件被截断、符号区没算
偏移公式看起来简单,实战里至少有三类错法。
一是把区号位号直接套公式,忘了减0xA1。有人说区号16区开始减0xA1,高位读出来0xB0就减0xB0,这取决于字库文件是否含符号区。最通用、最不容易错的就是按GB2312标准区位顺序,高字节、低字节统一减0xA1再乘以94加位号。
二是文件被截断。网上能找到的HZK16来源五花八门,有的文件缺了一部分,有些汉字区偏移越过了文件末尾。fread返回的字节数小于预期时,说明这个字库文件不完整,别继续往下调了,先换一个可靠的字库源。
三是符号区没算。全字库里1~9区是标点符号和数字,10~15区是空区,16区开始才是汉字。如果你只需要中文,按16区起算的简化公式可能没问题;但如果要显示标点、全角符号,偏移就不能按汉字区单独算,必须走标准的GB2312全区公式。
4.3 扫描方向不匹配:字会镜像、上下颠倒、花掉
即便字模读对了,屏幕上依然可能出现三种典型症状:字左右镜像、上下颠倒、整体撕裂成斜的。这些通常不是字库问题,而是数据排列方式和扫描方向不匹配。
排查顺序建议是这样:先看打印出来的ASCII图形是否正常,正常就说明字库读取没问题;再上屏,如果镜像,把每行字节的位序反过来试试;如果上下颠倒,把字模按行逆序送;如果一列一列乱,大概率是横向数据按纵向屏刷了,用前面给的转换函数转成你的屏幕对应的取模方式。
这步排查完全靠经验和肉眼对比,所以项目里最好写一个专用的字模调试页,能随时切换横向、纵向、反向几组参数,屏上直接对比效果。这个调试页初期看起来多余,实际能帮你把“莫名其妙的乱码”快速定位成“方向不对”,强烈建议保留。
5. 工程落地建议:选型、裁剪与存储
5.1 不同尺寸字库适合什么样应用
选字库尺寸,除了看字号,还得看像素密度和观看距离。
HZK12、HZK16适合小尺寸LCD、OLED屏,比如0.96寸OLED、12864这类,字号小、存储压力小,字库还能塞进片内Flash。HZK24、HZK32适合LED点阵屏、TFT大屏,近距离看字形边缘比较平滑。HZK40、HZK48一般用在户外LED显示屏、灯板这类远距离观看的大字场景,再配合高密度灯珠才有效果。
一句话:字库尺寸不是越大越好,要让点阵尺寸和实际物理显示尺寸匹配,否则要么字太小看不清,要么资源白白浪费。
5.2 字库裁剪:从276KB到几十KB的常用做法
如果只是显示固定菜单、固定提示语,完全没必要带整库。我做过一个项目只显示30多个指定汉字,用全量HZK16白白占掉276KB,后来把需要显示的汉字去重后,算下来只有不到3KB字模。
做法很简单:把项目中所有可能出现的中文文本收集到一个文本文件里,去重后按GB2312编码排序,在PC上用脚本逐个读取字模,输出成一个紧凑的二进制文件,同时生成一个索引表。运行时用索引差值或二分查找定位字模。这样字库占用能缩减到原来的十分之一甚至更少,代价只是代码里多几十行索引逻辑。
如果要支持用户输入任意汉字,那就别裁剪了,老老实实按场景选整库尺寸。
5.3 性能优化:别让每个字都去fseek
用文件系统读字库,每读一个字都做一次fseek加fread,在SD卡上频繁寻道是很慢的,尤其在整屏刷几十个汉字时,帧率会被拉得很难看。
工程上常用两种优化思路:一是按行缓存,先把一屏所有字符的偏移算出来,一次性读一批字模到RAM,再统一上屏;二是把字库从SD卡拷贝到外部SPI Flash或内部Flash,用内存映射或直接从Flash地址读取。后者的随机读取性能通常远高于文件系统,而且不依赖FATFS这类文件系统组件,系统更稳定。
另外,如果你用文件系统且字库文件较大,打开文件后建议保持句柄不关,避免反复open/close带来的开销。很多文件系统的open操作本身就很费时。
最后再分享一个我个人的习惯:不管字库文件是网上下的还是工具生成的,拿到手先写几分钟的校验代码,读取几个已知字符的ASCII打印图确认格式,再往项目里集成。这一步看起来多花时间,实际上每次都能提前暴露一批编码、偏移、取模方向的问题。字库这东西坑不深,但错了就是整屏乱闪,提前校验永远比上了屏再排查划算。
本文还有配套的精品资源,点击获取