ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HZK/ASC点阵字库读取原理与嵌入式应用实践

HZK/ASC点阵字库读取原理与嵌入式应用实践 简介需要制作点阵文字的开发者常因字库分散、读取代码不全而耗费时间这份资源正好补齐这一缺口。它整合HZK12至HZK48系列汉字字库与ASC12至ASC48系列ASCII字库覆盖12x12到48x48等多种规格并配套Java与C语言读取代码可直接用于点阵提取、字符转换与矩阵生成。压缩包共55个文件核心为各类HZK/ASC字库文件同时包含txt说明文档、Java源码、class字节码、C源码、obj与exe辅助工具整体仅7.53MB结构清晰便于按需取用。目前已有1652人学习查看适合嵌入式开发者、单片机爱好者以及Java图形界面项目开发者快速接入。字库格式涵盖HZK16、HZK24、HZK48等常见变体配合字符转矩阵、数字转矩阵等示例程序可帮助理解汉字与ASCII字模的存储格式、地址映射原理并在工程中快速实现文字点阵化。 我在嵌入式显示这条路上第一次被HZK16折磨是在电子设计竞赛的12864屏幕上。厂商例程里全是英文菜单想改成中文却没人告诉你那些字是怎么画上去的。后来翻遍了网上的帖子才搞明白点阵汉字得从字库里按偏移地址一个个“抠”出来再填进显存。从那以后HZK12、HZK24、HZK32、HZK40、HZK48以及配套的ASC12到ASC48英文点阵字库我在项目里统统用了一遍。这篇就把这套“老古董”的读取原理、完整代码和踩坑记录一次性说透给正在做LCD、OLED、LED点阵屏的兄弟省点时间。1. HZK和ASC字库到底是什么怎么选1.1 名字里的数字就是点阵尺寸HZK和ASC的命名规则其实非常简单HZK取自“汉字库”三个字的拼音首字母后面的数字代表点阵的边长单位是像素。HZK16就是16×16像素的汉字点阵HZK24就是24×24HZK48就是48×48。ASC就是ASCIIASC16表示8×16像素的半角英文字符点阵ASC24是12×24ASC48是24×48。整套字库的核心逻辑就一句话每个字符按固定大小存成一段字节数据读取时找到这段数据的起始位置按行或按列取出来拼到屏幕上。之所以常见尺寸是12、16、24、32、40、48是因为这些尺寸在像素上很方便按字节对齐。16和32刚好是8的整数倍每一行像素正好对应整数个字节存起来干净利落。12、24、40、48虽然行宽不是8的整数倍但按字节向上取整后依然规整。实际项目里选哪个尺寸主要看屏幕分辨率和显示信息量。12864屏幕用HZK16刚合适一块能显示4行8列中文320×240的TFT屏用HZK24或HZK32更舒服做户外LED门头屏HZK48以上的大字才有冲击力。1.2 一个汉字字模占多少字节点阵字模的存储规则是每一行像素按每8个点一组打包成一个字节不足8个点的也占一个字节再把所有行的字节依次排列。所以每个字符的总字节数可以用一个简单公式算出来bytes_per_char ceil(width / 8) * height拿HZK16举例16×16点阵每行16个点分成2个字节共16行所以每个汉字正好32字节。HZK24则是每行3字节、24行共72字节。HZK48更夸张每行6字节、48行共288字节。这个字节数就是后面所有偏移计算的基础凡是字模显示错乱十有八九是这里乘错了。1.3 各尺寸字库体积对照一套标准GB2312点阵字库大约覆盖87个区每个区94个字符位总字符数在8000个左右。不同尺寸的文件体积差异非常大做存储规划前最好先看清楚。字库点阵规格每字符字节数单套GB2312体积(约)HZK1212×1224196KBHZK1616×1632262KBHZK2424×2472589KBHZK3232×321281.0MBHZK4040×402001.6MBHZK4848×482882.3MBASC126×12123KBASC168×16164KBASC2412×244812KBASC3216×326416KBASC4824×4814436KBASC字库通常只存256个字符甚至只存可见的ASCII字符体积很小。HZK系列到了40、48这个级别就已经是MB级文件了内部Flash不够用的场景就得想别的办法后面专门讲裁剪方案。2. 读取前必须搞懂的两个底层原理2.1 汉字的区位码与偏移量计算HZK系列字库内部是按GB2312区位码顺序排列的不是按Unicode码或拼音顺序。GB2312编码里一个汉字占两个字节都落在0xA1到0xFE之间。字库文件把汉字分成94个区、每区94个位。为了在文件里定位某个字先要把它的GB2312内码转换成区位码区码 qu 内码第一字节 - 0xA0 位码 wei 内码第二字节 - 0xA0注意这只是拿到了区位码文件里的实际偏移还需要按“从第1区第1位开始连续存储”的规则来算。第1区第1位的字模一定在文件开头所以某个汉字的字模起点是offset ((qu - 1) * 94 (wei - 1)) * bytes_per_char我拿“中”字算一遍给你看。它的GB2312内码是0xD6 0xD0代入公式区码0xD6-0xA054位码0xD0-0xA048偏移为((54-1)*94(48-1))*32算出来是160928字节。这个数字是相对HZK16文件开头的偏移量用fseek定位到这个位置再连续读32字节就是“中”的16×16点阵数据。2.2 ASCII字库为什么可以直接按字符编号索引ASC系列字库的结构比HZK简单得多因为ASCII字符本身就有一个统一的编码表而且ASC字库文件通常就按ASCII码从小到大排列。字符“A”的ASCII码是65那么它的字模起点就是offset ASCII码 * bytes_per_charASC16中每个字符16字节所以字母“A”的偏移就是65×161040。看代码时如果有人直接用字符本身当数组下标原因就在这里——ASC字库天然支持“O(1)直接索引”完全不需要查表。不过要注意ASC字库通常从0x20空格才开始有有效字模0x00到0x1F是控制字符区文件里可能存的是空数据或者设计者塞的图形符号。读取前最好判断一下字符是否在可见范围否则把控制字符塞进显示缓冲区会画出奇怪的东西。3. 字库读取代码实现与验证3.1 HZK16与ASC16的读取函数可直接运行先给你一套最经典、最简明的PC端读取实现。它在Windows/Linux上都能跑验证通过后可以原封不动地移植到单片机只需要把fopen/fread换成都支持的库函数版本。#include stdio.h #include stdlib.h #define HZK16_PATH HZK16 #define ASC16_PATH ASC16 // 读取16x16汉字字模code指向GB2312双字节内码 int read_hzk16(FILE *fp, unsigned char *buf, const unsigned char *code) { unsigned char qu code[0] - 0xA0; unsigned char wei code[1] - 0xA0; if (qu 1 || qu 94 || wei 1 || wei 94) { return -1; // 非法GB2312汉字 } long offset ((long)(qu - 1) * 94 (wei - 1)) * 32L; if (fseek(fp, offset, SEEK_SET) ! 0) { return -2; } return (fread(buf, 1, 32, fp) 32) ? 0 : -3; } // 读取8x16 ASCII字模 int read_asc16(FILE *fp, unsigned char *buf, unsigned char ch) { if (ch 0x20 || ch 0x7E) { return -1; } long offset (long)ch * 16L; if (fseek(fp, offset, SEEK_SET) ! 0) { return -2; } return (fread(buf, 1, 16, fp) 16) ? 0 : -3; }这段代码的核心就是把“公式”变成“指针”。fseek跳到理论偏移fread读出定长数据两步都不复杂但缺一不可。有些人只做了fread没做fseek结果读到的永远是文件开头那一段字自然全错。3.2 一套代码通吃HZK12到HZK48如果你在项目里要用到多个尺寸的字库别为每个尺寸写一套独立函数。把它们抽象成一个结构体加一个通用方法后面增加字号只需要填参数#include stdio.h #include string.h typedef struct { int width; // 点阵宽度像素 int height; // 点阵高度像素 int bytes_per_char; // 每字符字节数 int encoding; // 0GB2312区位码索引(汉字) // 1ASCII直接索引(英文字符) FILE *fp; // 字库文件句柄 } font_t; int font_open(font_t *f, const char *path, int width, int height, int encoding) { if (!f || !path) return -1; memset(f, 0, sizeof(*f)); f-fp fopen(path, rb); if (!f-fp) return -2; f-width width; f-height height; f-bytes_per_char ((width 7) / 8) * height; f-encoding encoding; return 0; } int font_get_glyph(font_t *f, unsigned char *buf, const unsigned char *code) { long offset; if (f-encoding 0) { unsigned char qu code[0] - 0xA0; unsigned char wei code[1] - 0xA0; if (qu 1 || qu 94 || wei 1 || wei 94) { return -1; } offset ((long)(qu - 1) * 94 (wei - 1)) * f-bytes_per_char; } else { offset (long)code[0] * f-bytes_per_char; } if (fseek(f-fp, offset, SEEK_SET) ! 0) { return -2; } return (fread(buf, 1, f-bytes_per_char, f-fp) (size_t)f-bytes_per_char) ? 0 : -3; }用的时候像这样初始化font_t hzk16, hzk48, asc16; font_open(hzk16, HZK16, 16, 16, 0); // 汉字库 font_open(hzk48, HZK48, 48, 48, 0); // 大字汉字库 font_open(asc16, ASC16, 8, 16, 1); // 英文字库这样设计的好处是换字号的时候界面代码不用动只需要换font_t里的参数和文件路径。后面我要在12864和320240两块屏之间切换就是改初始化参数的事省了一堆重复劳动。3.3 字模打印验证程序光读出来还不够得确认字模数据是对的。最简单的验证方法是在PC端把字模打印成可视化的“点阵图”。我习惯用一个16×16的打印函数把每个字节展开成16个点点亮的位置输出“#”空的位置输出空格。这样一眼就能看出字对不对、方向对不对。// 打印16x16字模方便验证 void print_16x16(const unsigned char *buf) { for (int row 0; row 16; row) { for (int col 0; col 16; col) { int byte_idx row * 2 col / 8; int bit 0x80 (col % 8); // 高位在前 putchar((buf[byte_idx] bit) ? # : ); } putchar(\n); } }把3.1的主函数跑起来打印“中”字应该能看到一个横竖交叉的“中”字形。如果打出来是一个左右镜像的“中”说明位序取反了如果字是躺着的说明行列顺序算反了。这个验证步骤虽然土但在写驱动之前提前暴露问题比焊好板子再debug省一百倍时间。4. 取模方向与屏幕适配别在显示阶段翻车4.1 横向取模和纵向取模的区别字库文件从不同渠道下载下来内部字节排列可能不同。最常见的两种姿势是横向取模和纵向取模。横向取模的意思是先从左上角往右取前8个点打包成一字节然后这行剩下的点继续打包一行完成后再换下一行。16×16横向取模的字节顺序是第0字节对应第0行左8点第1字节对应第0行右8点第2字节对应第1行左8点以此类推。纵向取模则是从上往下按列取先把第0列前8个点打包成第0字节再打包第1列列完成后再换下一组。两种取模在显示上都可以用但驱动代码必须匹配。拿横向取模的字模直接按照纵向方式送去显示就会出现整屏字符像二维码被打散了一样。判断字库到底是哪种取模最直接的方法是读一个已知汉字打印出来肉眼看。打印出来正常就是匹配不正常就调整取模方向的索引逻辑。4.2 适配常见LCD/OLED驱动常见屏幕驱动芯片的显存写入顺序也分横向和纵向两种。比如ST7735、SSD1306、ST7789这些驱动可以通过设置扫描方向改变显示坐标的增长方向。我在适配时优先保证字库本身的取模方式不变用驱动寄存器和绘制坐标来补偿方向差异。这里有一个通用绘制思路不管驱动芯片的扫描方向怎么变先把字模展开成一个width×height的位图数组再按屏幕坐标逐点写点。虽然逐点写比整块写稍慢但适配性极强后续换屏不用改字库代码。性能敏感时再优化成按驱动需要的格式打包批量写入。如果要逐点绘制像素点判断函数可以这样写// 判断横向取模字模中某行某列像素是否为1 int font_pixel_on(const unsigned char *buf, int width, int height, int row, int col) { if (row 0 || row height || col 0 || col width) { return 0; } int bytes_per_row (width 7) / 8; int byte_idx row * bytes_per_row col / 8; int bit_mask 0x80 (col % 8); // 高位在前 return (buf[byte_idx] bit_mask) ? 1 : 0; }只要把这个函数的返回值喂给屏幕的描点函数就能把一个字画出来。我在项目里的做法是把绘制循环抽成通用函数传入字模数据、字库点阵尺寸和目标坐标内部套用font_pixel_on逐点描画这样一套代码能适配所有尺寸。4.3 中英文混排的宽度处理屏幕上显示混合文本时最容易忽略的是中英文宽度关系。16×16汉字在宽度上正好是8×16英文字符的两倍24×24汉字是12×24英文的两倍32×32汉字是16×32英文的两倍。所以在混排时推进光标前进的单位宽度需要区分全角和半角读取一个字节如果大于等于0xA1说明是汉字内码的第一个字节需要再取一个字节组成完整GB2312编码去HZK字库取模显示宽度推进一个汉字宽度。如果字节小于0xA1按ASCII处理去ASC字库取模显示宽度推进半个汉字宽度即一个英文字符宽度。这个判断逻辑在文本解析阶段就要做不能让中间层的绘制函数去猜。我一开始偷懒把所有字符都按同一宽度处理结果中英文混排后错位得乱七八糟后来老实按全角半角区分才算解决。5. 嵌入式场景下的资源优化5.1 字库裁剪只留用得到的字HZK16本身就要262KB在单片机内部Flash里直接塞全套字库不是不行但会很心疼。实际产品界面上的汉字往往就几百个完全没必要存全部。裁剪的思路是把界面里可能出现的汉字收集起来按GB2312编码去重生成一个只包含这些字的迷你字库。裁剪工具可以用Python写本质就是遍历需要的字符串对每个汉字算出它在原字库里的偏移把32字节或其他尺寸对应的字节数拷贝出来再按同样的偏移规律重建一个新文件。需要注意的是裁剪后字库文件的排列顺序自己定义就行关键是把原字模数据按新索引规则存储同时保存一份字符码到新偏移的映射表。简单场景也能直接用哈希表或线性查表驱动代码稍作修改就能配合。5.2 从文件io换成Flash数组MCU上没有文件系统或者不想用SD卡时把字库压缩成C语言数组是标准做法。用工具把字节流转成十六进制数组写入一个.c文件编译时直接烧进Flash// 以HZK16为例裁剪后只保留需要的字 const unsigned char hzk16_mini[] { 0x00, 0x00, 0x7F, 0xFC, /* ... */ };如果字库比较大要用__attribute__((aligned))或分区表把它放到独立Flash分区避免和代码段挤在一起。读取时直接按memcpy从数组拷贝到显存缓冲区比fopen/fseek/fread快一个量级而且没有文件系统依赖。我做过一次实测在STM32F103上从SD卡文件读取字模单字耗时约1.2ms改成Flash数组后同样一个字只需要几十微秒性能提升很明显。5.3 显示性能优化思路如果刷新整屏文字觉得慢先别急着换主控有几个优化点可以查。第一减少重复计算区码位码和偏移量尽量在文本解析阶段算好并缓存。第二减少读字库的次数一屏文字先全部解析成字模数据放进缓冲区再一次批量刷新屏幕。第三能用块的不用点如果驱动支持按页或按列连续写显存把字模字节直接拼成驱动需要的格式避免逐点调用描点函数。这些优化做完同样主频下刷新速度能提升三到五倍。6. 常见问题与调试经验速查6.1 问题排查表现象可能原因解决办法显示全乱码或问号源字符串不是GB2312编码常见于UTF-8工程把源码文件或字符串转成GB2312或写一个UTF-8转GB2312函数字是左右镜像的取模位序反了高位低位搞反把bit_mask从0x80(col%8)改成0x01(col%8)字是上下颠倒的字模行序反了打印字模时把row从height-1往0遍历或调整copy顺序字偏斜/错位bytes_per_row计算错或宽度不是8的倍数时没向上取整统一用(width7)/8计算每行字节数读出来是空的文件路径不对fopen失败检查字库文件是否在运行目录下路径大小写中文最后一个字消失中英文混排时把汉字内码拆开当成两个ASCII中文按双字节处理不能直接逐字节画6.2 三个排查乱码的顺序遇到字库显示异常不要上来就改代码。我排查的顺序是固定的第一步验证编码先用printf把待显示字符串按十六进制打印出来确认真的是GB2312内码。很多“乱码”问题其实不是字库代码的锅而是工程文件默认用了UTF-8编码编译器把字符串转成了别的字节序列。第二步验证字模在PC端把同一个字模打印成点阵图看是否正常。这一步能直接区分“数据读错了”还是“画法不对”。PC端正常而单片机不正常问题多半在驱动适配PC端就不正常问题在字库文件本身或取模方向。第三步验证文件核对字库文件大小和头文件偏移。HZK16应该在261KB到267KB之间如果只有几十KB多半是下载到了残缺文件。ASC16则应该正好4KB左右大小不对就别往项目里放了。最后再说一句关于“字库”搜索的题外话网上搜索HZK、ASC点阵字库时结果里经常混进来一些“五笔字库”“输入法码表”甚至“密码字典”之类的文件那跟嵌入式点阵字库完全是两码事。下载文件前最好先看扩展名和体积HZK系列通常是.bin或.datASC系列也在几KB到几十KB之间认准这个范围基本不会错。我个人用这套点阵字库方案做过电子价签、LED胸牌、工业手持终端最深的体会是只要把“区位码计算”和“取模方向匹配”这两件事吃透HZK12到HZK48、ASC12到ASC48在任何屏幕上都是同一套逻辑。现在主流的GUI框架虽然都支持矢量字体但在小内存MCU和低分辨率黑白屏上点阵字库依然是不可替代的实用方案。后续如果时间充裕我还打算把HZK字库批量转成LVGL的字体格式让老字库在新框架里继续发光。本文还有配套的精品资源点击获取
返回列表