☰
GBK点阵字模:嵌入式中文显示与打印的索引算法及驱动适配
2026/10/18 16:38:30 网站建设 项目流程

简介:这是一份面向打印机固件开发、LCD汉字显示及嵌入式字库应用场景的GBK点阵字模资源,收录GBK1.0规范内22046个汉字,不含用户自定义区,可解决中文显示与打印中字模缺失、编码转换繁琐的问题。包内共21个文件,约3.41MB,以bin字库、txt编码表、exe工具、bpl与ptl组件、c与h源码等为主:GBK16.BIN与GBK24.BIN分别提供16×16和24×24点阵数据,两份GBK编码表按分区与编码顺序排列,另附字模提取、牧码字模、PCtoLCD2002等工具,以及GBK转BIG5、GBK转Unicode的函数与字体索引算法源码。资源已有1670人学习下载,适合需要快速集成字库、验证取模效果或研究编码转换的开发者参考,能直接用于显示驱动与打印模块的字模调用。

1. 从一次打印乱码说起:这套 GBK 点阵字模到底解决了什么问题

前阵子帮一个做嵌入式票据打印的朋友看问题:设备屏幕能正常显示中文,一走到打印环节,出来的全是方块或者错位笔画。排查了半天,根因不在驱动,而在字库——他用的那套点阵数据是按区位码排的,而打印模块按 GBK 编码去查表,索引对不上,自然满纸乱码。这类问题在打印机、POS 机、工控屏、电子价签上特别常见,屏幕显示和打印走的是两套字模数据源,一旦编码口径不统一就翻车。

这份资源给的就是一套完整的 GBK 汉字点阵字模,覆盖 16×16 和 24×24 两种规格,同时附带索引算法和配套工具。它面向的是需要在嵌入式设备上做中文显示与打印的开发者——尤其是那些不想依赖完整字库芯片、希望把字模数据直接烧进 Flash 或者按需裁剪的场景。16×16 适合小尺寸屏幕和低分辨率打印头,24×24 笔画更饱满,适合票据、标签这类对可读性要求高的输出。下面我按「这套字模怎么组织、怎么查、怎么跑、怎么改」的顺序拆一遍。

2. 字模数据组织与 GBK 索引算法:从编码到点阵的映射链路

2.1 点阵字模的存储格式

16×16 的汉字,每个字占 16 行、每行 16 个像素,按位存储就是每行 2 字节,一个字 32 字节。24×24 则是每行 3 字节,一个字 72 字节。这是最朴素的「横向取模、高位在前」格式,也是绝大多数打印机和 LCD 驱动直接能吃的格式。

但实际拿到的字模文件不一定都是这个排列。常见的有横向取模和纵向取模两种,还有高位在前和低位在前的区别。如果取模方向和驱动预期不一致,显示出来就是笔画被拉长或者错位。我一般会先拿一个「一」字或者「十」字做验证——这种笔画简单的字最容易看出取模方向对不对。

/* 16x16 汉字点阵的单字结构,横向取模,高位在前 */ typedef struct { unsigned char data[32]; /* 16 行 × 每行 2 字节 */ } Font16x16; /* 24x24 汉字点阵的单字结构 */ typedef struct { unsigned char data[72]; /* 24 行 × 每行 3 字节 */ } Font24x24; /* 取第 row 行第 col 列的像素值(0 或 1) */ unsigned char get_pixel_16x16(const Font16x16 *font, int row, int col) { /* 每行 2 字节,col/8 定位到字节,col%8 定位到 bit */ unsigned char byte = font->data[row * 2 + col / 8]; return (byte >> (7 - (col % 8))) & 0x01; }

这段代码定义了两种规格的字模结构,并给出了像素级读取的方法。row * 2 + col / 8是横向取模的地址计算方式,7 - (col % 8)表示高位在前。如果你的字模文件是纵向取模,这个公式要改成按列累加,读取逻辑完全不同。参数上,row范围 0~15,col范围 0~15,越界会读到相邻字的数据,调试时要注意边界。

2.2 GBK 编码到字模索引的换算

GBK 是双字节编码,第一个字节范围 0x81~0xFE,第二个字节范围 0x40~0xFE(剔除 0x7F)。要把一个 GBK 码位映射到字模数组的下标,核心是算「这个码位在 GBK 字符集里排第几」。

/* 计算 GBK 码位在字模数组中的索引,返回 -1 表示非法码位 */ int gbk_to_font_index(unsigned char high, unsigned char low) { int index; /* 合法性校验:GBK 高位 0x81~0xFE,低位 0x40~0xFE 且不等于 0x7F */ if (high < 0x81 || high > 0xFE) return -1; if (low < 0x40 || low > 0xFE || low == 0x7F) return -1; /* 计算该码位之前有多少个合法 GBK 字符 */ index = (high - 0x81) * 190; /* 每个高位字节对应 190 个低位 */ if (low >= 0x7F) { index += (low - 0x40 - 1); /* 跳过 0x7F 这个空洞 */ } else { index += (low - 0x40); } return index; }

这里的关键数字是 190:GBK 每个高位字节下,低位从 0x40 到 0xFE 共 191 个值,去掉 0x7F 剩 190 个。(high - 0x81) * 190算出前面所有高位块的字符总数,再加上当前块内的偏移。low >= 0x7F时要减 1,因为 0x7F 不合法,不占索引位。

注意:这个公式假设字模文件是按 GBK 码位顺序连续排列的。如果你的字模文件只包含常用字(比如只收了 GB2312 的 6763 个汉字),索引算法要换成按区位码或者查表法,不能直接用这个公式。

2.3 索引表与二分查找的取舍

如果字模文件不是全量 GBK 顺序排列,而是只挑了部分汉字,那就需要一张索引表。索引表通常存「GBK 码位 → 字模偏移」的映射,查的时候先算码位,再在索引表里找偏移。

/* 索引表条目 */ typedef struct { unsigned short gbk_code; /* GBK 双字节合并成一个 short */ unsigned int offset; /* 字模数据在文件中的字节偏移 */ } FontIndexEntry; /* 二分查找,索引表按 gbk_code 升序排列 */ int find_font_offset(const FontIndexEntry *table, int count, unsigned short gbk_code, unsigned int *out_offset) { int lo = 0, hi = count - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (table[mid].gbk_code == gbk_code) { *out_offset = table[mid].offset; return 0; /* 找到 */ } else if (table[mid].gbk_code < gbk_code) { lo = mid + 1; } else { hi = mid - 1; } } return -1; /* 未找到 */ }

二分查找的前提是索引表按gbk_code升序排列。如果表是无序的,要么先排序,要么改用哈希表。在嵌入式环境里,索引表通常放在 Flash 里,二分查找的log2(N)次访问对 Flash 读取来说完全可以接受。参数count是索引表条目数,out_offset是输出参数,返回 0 表示成功。

3. 显示与打印的驱动适配:从字模数据到实际输出

3.1 屏幕显示的字模渲染

屏幕显示和打印对字模的使用方式有本质区别。屏幕是逐像素刷到显存,打印是逐行送到打印头。先看屏幕这边。

/* 将 16x16 字模渲染到 LCD 显存,x/y 为左上角坐标 */ void draw_font_16x16(unsigned char *lcd_buffer, int lcd_width, int x, int y, const Font16x16 *font, unsigned short color) { for (int row = 0; row < 16; row++) { for (int col = 0; col < 16; col++) { if (get_pixel_16x16(font, row, col)) { /* 计算显存中的像素位置,假设 RGB565 每像素 2 字节 */ int px = x + col; int py = y + row; int offset = (py * lcd_width + px) * 2; lcd_buffer[offset] = color >> 8; lcd_buffer[offset + 1] = color & 0xFF; } } } }

这段代码把字模的每个像素点映射到 LCD 显存的对应位置。lcd_width是屏幕宽度(像素),color是 RGB565 格式的颜色值。注意这里只写了前景色,背景色需要提前填充或者用透明叠加的方式处理。如果屏幕支持硬件加速或者有专门的字符叠加层,可以直接把字模数据送到硬件层,省掉逐像素循环。

3.2 打印机点阵数据的组装

打印机这边更关注「一行一行地送」。热敏打印机通常按行接收点阵数据,每行对应打印头的一个加热周期。汉字打印需要把字模的每一行提取出来,拼成打印机能识别的位流。

/* 将一行汉字字模转换为打印机点阵行数据 */ /* font_data: 连续排列的多个 16x16 字模,char_count 为字数 */ void build_print_line(const Font16x16 *font_data, int char_count, int row, unsigned char *out_line) { int out_byte = 0; for (int c = 0; c < char_count; c++) { /* 每个字取第 row 行的 2 个字节 */ out_line[out_byte++] = font_data[c].data[row * 2]; out_line[out_byte++] = font_data[c].data[row * 2 + 1]; } }

这个函数把一行里所有汉字的第row行数据拼成一个连续的字节流,直接送给打印头。char_count是一行打印多少个汉字,out_line的长度是char_count * 2。24×24 的字模同理,只是每行 3 字节,输出长度是char_count * 3。

提示:打印头的点阵宽度通常是固定的(比如 384 点),一行能打的汉字数 = 打印头宽度 / 字宽。16×16 字模一行能打 24 个字,24×24 字模一行能打 16 个字。超出部分要换行处理。

3.3 混合排版:汉字与 ASCII 的宽度对齐

实际打印内容往往是汉字和数字、字母混排。ASCII 字符通常用 8×16 或者 12×24 的点阵,宽度和汉字不一样。如果直接拼接,会出现对不齐的情况。

/* 混合排版:汉字 16x16,ASCII 8x16,按行输出 */ void build_mixed_line(const Font16x16 *hanzi, int hanzi_count, const unsigned char *ascii, int ascii_count, int row, unsigned char *out_line) { int pos = 0; /* 先输出汉字部分 */ for (int i = 0; i < hanzi_count; i++) { out_line[pos++] = hanzi[i].data[row * 2]; out_line[pos++] = hanzi[i].data[row * 2 + 1]; } /* 再输出 ASCII 部分,每个 ASCII 占 1 字节宽 */ for (int i = 0; i < ascii_count; i++) { out_line[pos++] = ascii[i * 16 + row]; } }

这里假设 ASCII 字模是 8×16、纵向取模、每字 16 字节。ascii[i * 16 + row]取出第i个 ASCII 字符第row行的字节。混排的关键是宽度对齐——如果 ASCII 用 8 点宽,汉字 16 点宽,那一个汉字正好占两个 ASCII 的位置,排版时按这个比例算列数就不会错位。

4. 配套工具与完整运行链路:从字模文件到设备输出

4.1 字模提取工具的使用

配套工具通常负责从标准字库文件(比如 Windows 的 .ttf 或者点阵字库文件)里提取指定规格的字模,按 GBK 顺序导出成二进制文件。常见做法是提供一个命令行工具,指定字体、字号、输出格式。

# 从系统字体提取 16x16 点阵,输出 GBK 顺序的二进制字模 ./font_extract --font simsun.ttf --size 16 --encoding gbk \ --format bin --output font16.bin # 提取 24x24 点阵 ./font_extract --font simsun.ttf --size 24 --encoding gbk \ --format bin --output font24.bin

参数说明:--font指定源字体文件,--size指定点阵尺寸,--encoding指定编码方式,--format指定输出格式(bin 是纯二进制,c 是 C 数组)。输出文件的大小可以验证:全量 GBK 汉字约 2 万字,16×16 字模约 640KB,24×24 约 1.4MB。如果文件大小对不上,说明提取范围或者取模方式有问题。

4.2 字模数据的烧录与寻址

嵌入式设备上,字模数据通常放在外部 Flash 或者 SD 卡里。烧录方式取决于硬件,常见的是通过烧录器把 bin 文件写到 Flash 的固定偏移,或者通过文件系统挂载后按文件读取。

/* 从 Flash 读取指定 GBK 码位的字模数据 */ /* flash_base 是字模数据在 Flash 中的起始地址 */ int read_font_from_flash(unsigned int flash_base, unsigned char high, unsigned char low, unsigned char *out, int size) { int index = gbk_to_font_index(high, low); if (index < 0) return -1; unsigned int addr = flash_base + index * size; flash_read(addr, out, size); /* 底层 Flash 读取函数 */ return 0; }

flash_base是字模数据在 Flash 中的起始地址,size是单个字模的字节数(16×16 是 32,24×24 是 72)。flash_read是硬件相关的读取函数,不同平台实现不同。如果 Flash 读取速度慢,可以考虑加一层缓存,把常用字预加载到 RAM。

4.3 完整验证流程

拿到字模文件和工具后,我一般按这个顺序验证:

  1. 用工具提取几个已知汉字(比如「中」「文」「测」「试」),导出成 C 数组,肉眼检查点阵数据是否符合预期。
  2. 在 PC 上写一个小程序,把字模数据渲染成图片,确认字形正确、取模方向正确。
  3. 把字模数据烧到设备,先显示单个汉字,再显示一行汉字,最后测试打印输出。
  4. 测试边界情况:GBK 扩展区汉字、生僻字、ASCII 混排、超长文本换行。
/* PC 端验证:把字模渲染成 PGM 图片,方便肉眼检查 */ void dump_font_to_pgm(const Font16x16 *font, const char *filename) { FILE *fp = fopen(filename, "wb"); fprintf(fp, "P1\n16 16\n"); /* P1 是 ASCII 位图格式 */ for (int row = 0; row < 16; row++) { for (int col = 0; col < 16; col++) { fprintf(fp, "%d ", get_pixel_16x16(font, row, col)); } fprintf(fp, "\n"); } fclose(fp); }

这个函数把字模导出成 PGM 图片,用任何图片查看器都能打开。如果看到的字形是反的、旋转的或者笔画断裂,说明取模方向或者位序有问题,回到 2.1 节检查存储格式。

5. 避坑与常见问题排查:那些让我熬夜的点阵问题

5.1 显示正常但打印乱码

现象:屏幕上汉字显示完全正常,一打印就变成方块或者笔画错位。

原因:屏幕和打印用了两套不同的字模数据,或者打印模块的取模方向与字模文件不一致。屏幕驱动可能做了自动旋转或者位序转换,打印模块没有。

解决:确认打印模块用的字模文件和屏幕是同一套,检查取模方向参数。用「一」字打印一行,看输出的是横线还是竖线,就能判断方向对不对。

5.2 部分汉字显示为空白

现象:大部分汉字正常,少数汉字显示为空白或者乱码。

原因:字模文件不完整,缺少 GBK 扩展区或者某些生僻字。或者索引算法在计算扩展区码位时越界了。

解决:用工具检查字模文件的总字数,和 GBK 全量字符数对比。如果只收了 GB2312 的 6763 个汉字,那 GBK 扩展区的字自然找不到。索引算法要加边界判断,找不到时返回默认字模或者空白。

5.3 24×24 字模打印出来笔画粘连

现象:16×16 字模打印正常,换成 24×24 后笔画糊在一起。

原因:打印头的点间距和字模尺寸不匹配,或者打印浓度设置过高导致墨迹扩散。也可能是 24×24 字模的取模方式与 16×16 不同。

解决:先确认 24×24 字模的取模方式和 16×16 一致。如果一致,调整打印浓度和走纸速度。热敏打印对浓度很敏感,浓度高了小字容易糊。

5.4 索引计算速度慢导致刷新卡顿

现象:屏幕刷新时汉字显示有明显延迟,尤其是整屏刷新时。

原因:每次显示都重新计算索引并读取 Flash,没有缓存。

解决:加一层 RAM 缓存,把当前屏幕用到的字模预加载到内存。或者用索引表 + 二分查找替代实时计算,减少 Flash 随机读取次数。

5.5 字模文件大小对不上

现象:提取出来的字模文件比预期小很多或者大很多。

原因:提取工具的字符范围设置不对,或者取模方式导致每个字的字节数不同。

解决:先算预期大小:全量 GBK 约 2 万字,16×16 每字 32 字节,总共约 640KB。如果文件只有几十 KB,说明只提取了部分字符。如果文件特别大,检查是不是把 ASCII 或者其他编码的字符也提进来了。

6. 二次开发与裁剪:按需定制字模数据的几个实用技巧

拿到这套字模和工具后,最常见的二次开发需求是「裁剪」——设备 Flash 空间有限,不可能把全量 GBK 字模都烧进去。我一般会按项目实际用到的字符集来裁剪,比如只保留 GB2312 的 6763 个常用汉字,加上数字、字母和常用标点,文件能缩小到原来的三分之一左右。

裁剪的关键是维护一张「需要的 GBK 码位列表」,然后用工具按列表提取。如果工具不支持按列表提取,可以先用全量字模生成,再写脚本按索引表筛选。筛选时注意保持索引表的顺序,否则二分查找会失效。

# 按码位列表裁剪字模文件 def trim_font(full_font_path, code_list, char_size, output_path): with open(full_font_path, 'rb') as f: full_data = f.read() with open(output_path, 'wb') as out: for high, low in code_list: index = (high - 0x81) * 190 if low >= 0x7F: index += (low - 0x40 - 1) else: index += (low - 0x40) offset = index * char_size out.write(full_data[offset:offset + char_size])

这段 Python 脚本按给定的 GBK 码位列表从全量字模里抽取对应的字模数据,重新拼成一个裁剪后的文件。char_size是单个字模的字节数,16×16 是 32,24×24 是 72。裁剪后的文件需要配套一张新的索引表,否则原来的索引算法算出来的下标会对不上。

另一个常见需求是「换字体」。不同字体的点阵字形差异很大,尤其是 16×16 这种小尺寸,有些字体在小字号下笔画会糊。我一般会准备两套字模:一套宋体用于正文,一套黑体用于标题。换字体时只需要重新跑一遍提取工具,索引算法和驱动代码都不用动。

还有一个技巧是「动态加载」。如果设备有 SD 卡或者外部 Flash,可以把字模文件放在文件系统里,按需读取。这样既不占内部 Flash,又能支持全量字符。代价是读取速度慢一些,需要加缓存。我一般会在 RAM 里开一个 4KB 左右的缓存区,用 LRU 策略管理,命中率能到 90% 以上。

最后说一个我踩过的坑:裁剪字模时一定要把 ASCII 字符也包含进去。很多项目只裁剪了汉字,结果打印数字和字母时找不到字模,输出空白。ASCII 字符虽然简单,但少了它整个排版就废了。从那以后我每次裁剪字模都强制走一遍「汉字 + ASCII + 常用标点」的检查清单,确认三类字符都齐了才烧录。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询