3. 字库文件解析:HZK16与ASC16结构
字库文件解析,说白了就是搞清楚「点阵数据怎么存、怎么取」。我刚开始做嵌入式GUI时,以为字库就是个二进制文件,直接读出来就能用。结果第一次显示汉字,屏幕上全是乱码——嗯,从那以后我再也不敢跳过字库结构分析了。
这一章,我们就把HZK16和ASC16这两个最常用的字库文件,彻底拆开来看。
3.1 HZK16字库结构
HZK16是16×16点阵的汉字字库。每个汉字用16行×16列的点阵表示,每行16个点正好占2个字节。所以一个汉字占32个字节。
我个人习惯把字库想象成一个「二维表格」:行号对应区码,列号对应位码。GB2312标准把汉字分成94个区,每区94个位。你想想看,94×94=8836,正好覆盖了常用的6763个汉字和符号。
核心公式:
汉字在HZK16中的偏移地址 = ((区码 - 0xA1) × 94 + (位码 - 0xA1)) × 32
其中32是每个汉字的字节数,94是每区的汉字数。
我在项目中遇到过一个问题:有些字库文件开头有文件头,有些没有。HZK16是纯数据文件,没有文件头,直接从第一个汉字开始存。这一点很容易被忽略。
3.2 ASC16字库结构
ASC16是16×8点阵的ASCII字库。每个字符用16行×8列的点阵表示,每行8个点占1个字节。所以一个ASCII字符占16个字节。
为什么ASCII字库是8列?因为英文字母不需要那么宽。16列的话,字母之间间距太大,显示效果反而不好看。
经验之谈:
ASC16字库通常包含128个ASCII字符(0x00-0x7F),但实际显示时,0x00-0x1F是控制字符,没有图形。我一般从0x20(空格)开始取。
ASCII字符的定位更简单:
偏移地址 = 字符ASCII码 × 16
16是每个ASCII字符的字节数。
3.3 两种字库的对比
| 对比项 | HZK16 | ASC16 |
|---|---|---|
| 点阵大小 | 16×16 | 16×8 |
| 每字符字节数 | 32 | 16 |
| 编码方式 | GB2312(区码+位码) | ASCII码 |
| 字符总数 | 8836(含符号) | 128 |
| 定位公式 | ((区-0xA1)×94 + (位-0xA1))×32 | ASCII码 × 16 |
| 文件头 | 无 | 无 |
3.4 字库文件读取与定位
读取字库文件,本质上就是「打开文件 → 计算偏移 → 读取固定字节数」。我建议用标准C的fopen/fread/fclose,或者用POSIX的open/read/close。在嵌入式环境下,如果字库放在SPI Flash里,那就用Flash驱动接口。
下面是一个完整的读取示例:
// 读取HZK16中的汉字点阵数据
int get_hzk16_bitmap(unsigned char q, unsigned char w, unsigned char *buffer) {
// q: 区码, w: 位码, buffer: 输出缓冲区(至少32字节)
FILE *fp;
long offset;
int ret;
fp = fopen("HZK16", "rb");
if (fp == NULL) {
return -1; // 文件打开失败
}
// 计算偏移:((区码-0xA1)*94 + (位码-0xA1)) * 32
offset = ((q - 0xA1) * 94 + (w - 0xA1)) * 32;
ret = fseek(fp, offset, SEEK_SET);
if (ret != 0) {
fclose(fp);
return -2; // 定位失败
}
ret = fread(buffer, 1, 32, fp);
if (ret != 32) {
fclose(fp);
return -3; // 读取失败
}
fclose(fp);
return 0; // 成功
}
// 读取ASC16中的ASCII字符点阵数据
int get_asc16_bitmap(unsigned char ch, unsigned char *buffer) {
// ch: ASCII字符, buffer: 输出缓冲区(至少16字节)
FILE *fp;
long offset;
int ret;
fp = fopen("ASC16", "rb");
if (fp == NULL) {
return -1;
}
// 计算偏移:ASCII码 * 16
offset = ch * 16;
ret = fseek(fp, offset, SEEK_SET);
if (ret != 0) {
fclose(fp);
return -2;
}
ret = fread(buffer, 1, 16, fp);
if (ret != 16) {
fclose(fp);
return -3;
}
fclose(fp);
return 0;
}
注意:
我曾经在STM32上踩过一个坑:SPI Flash的读取速度很慢,每次显示字符都去读Flash,导致屏幕刷新卡顿。后来我加了一层缓存,把常用字符的点阵数据预读到RAM里,问题就解决了。
3.5 字库定位流程图
下面这张图展示了从字符编码到点阵数据的完整流程:
3.6 避坑指南
我总结几个实际开发中容易踩的坑:
- 区码位码范围:GB2312的区码范围是0xA1-0xFE,位码也是0xA1-0xFE。如果遇到0xA0以下的编码,那可能是扩展字库或者编码错误。
- 文件大小校验:HZK16文件大小应该是8836×32=282752字节。如果文件大小不对,说明字库文件可能损坏或者版本不匹配。
- 多线程安全:如果多个任务同时读取字库,记得加锁。我遇到过两个线程同时fseek,结果定位错乱,显示出来的字全是乱的。
- 内存对齐:有些MCU要求读取缓冲区4字节对齐。32字节的缓冲区一般没问题,但如果你只读16字节,注意检查对齐。
调试小技巧:
如果你不确定字库文件是否正确,可以写一个简单的测试程序:读取「啊」字(区码0xB0,位码0xA1)的点阵数据,然后逐字节打印出来。正常的「啊」字点阵数据不会是全0或全1,而且前几个字节应该有明显的图案特征。