行业资讯
📅 2026/9/7 7:01:00
嵌入式GBK点阵字模实战:16x16与24x24显示打印全攻略
简介面向打印机驱动、LCD汉字显示等嵌入式显示与打印场景这套经个人整理的专业GBK汉字点阵字库覆盖GBK1.0标准中22046个汉字不含用户自定义区并提供按分区与按编码顺序排列的两份GBK编码表以及高效的汉字内码索引算法开发者可据此快速取得任意汉字的点阵偏移。压缩包共21个文件总体积仅3.41MB核心包含16×16与24×24两套字库bin文件同时附带txt编码表、xls码表、c/h格式的GBK转BIG5与GBK转Unicode源码以及exe格式的PCtoLCD2002、牧码字模、字模提取等取模工具便于按需生成字模数据。资源已有1661人学习适合打印机、嵌入式LCD、手持终端等汉字显示开发场景也适合初学者研究点阵字库的编码与索引原理。整体内容紧凑从字库、编码表到索引函数和转换工具形成完整链路可直接嵌入项目使用能有效节省汉字点阵数据整理与调试时间。 搞嵌入式显示、小票打印、工控人机界面的朋友对“中文字模”这三个字应该不陌生。工作里最朴素的需求就是想在12864屏幕上显示几个汉字或者让热敏打印机打出一张带中文的小票可MCU里没有操作系统字体可用。怎么办只能把每个汉字提前变成点阵数据存进字库运行时按编码把对应点阵捞出来再扔给屏幕或打印头。这就是一套典型的GBK编码16x16和24x24点阵字模应用从显示到打印从索引算法到配套工具看着简单真做起来坑不少。这篇文章把我自己做过一次这类项目的完整思路、算法、工具和踩坑记录整理出来给正准备整中文显示、打印固件或者被中文乱码折磨到怀疑人生的同学直接抄作业。1. 先搞明白这个项目到底要解决什么问题1.1 一个需求拆出两个应用场景同一个项目里显示和打印两个模块对字模的需求其实不一样。屏幕显示的场景比如12864这类128x64分辨率的LCD一屏16x16点阵汉字正好是4行8列共32个汉字取模数据量小、刷新快非常适合做菜单和实时状态展示。打印的场景就不同了热敏打印机一个点行要输出一长串数据字符太小容易糊尤其在快递面单、小票这种不干胶热敏纸上24x24点阵打印出来明显更清晰客户拿在手里也好看。所以需求就变成了同一套系统里既要能调16x16的字模给屏幕也要能调24x24的字模给打印模块。两个尺寸的点阵数据不能混用但索引逻辑和工具链完全可以是同一套。1.2 为什么选GBK而不是GB2312最早我也想过直接用GB2312字库毕竟HZK16这类文件网上遍地都是拿到就能用。但实际一跑就发现问题客户点名要在打印内容里带上“喆”“淼”这类生僻字GB2312字库里根本没有。而且很多日文假名、繁体字、特殊符号也经常出现在物料名称里。GB2312只覆盖6763个汉字日常写文章够用做真实业务远远不够。换成GBK之后汉字覆盖量到了20902个人名地名生僻字基本都能覆盖。更重要的是GBK向下兼容GB2312原来GB2312能显示的字符在GBK里编码不变迁移成本很低。至于UTF-8编码是变长的每个汉字占3字节存文本没问题但做嵌入式字库寻址非常难受因为每个字符的字模索引计算要对齐字节数GBK这种双字节等宽结构天然适合做点阵字库。1.3 整体方案字库文件加索引算法加提取工具我的技术路线拆成三块第一是准备字库文件按GBK编码顺序排列的16x16和24x24二进制点阵文件第二是写索引算法输入一个汉字的GBK编码输出它在这个二进制文件里的偏移量第三是写提取工具把字库里的点阵数据转换成单片机需要的C语言数组或者直接打印出可视化点阵方便校对。这三块里最容易被忽视的是第二块。很多人拿到的字库文件是完整版可以直接套公式算偏移但也有人用的字库是被裁剪过的只保留了一两千个常用字这时候线性公式就完全失效了必须有另一套索引方案。我把两种方案都在下边讲清楚。2. GBK编码和点阵字模的底层知识2.1 GBK的双字节结构区码、位码和边界GBK编码每个汉字占2字节。第一字节叫区码范围是0x81到0xFE第二字节叫位码范围是0x40到0xFE但中间跳过了0x7F这个值。为什么跳过0x7F因为0x7F在ASCII里是DEL删除字符很多老协议栈会把它当成控制字符处理所以GBK规定区位码的第二字节不能等于0x7F防止在通信时出幺蛾子。这个边界问题直接影响索引算法。如果像GB2312那样位码从0xA1开始连续分布公式非常好写。但GBK的位码被切成两段0x40到0x7E是一段共63个位置0x80到0xFE是另一段共127个位置。两段加起来正好每区190个位置。这个190就是GBK索引公式里最核心的系数。另外GBK虽然覆盖了2万多个汉字但整个编码空间里并不是所有码位都有对应字符很多位置是空的。如果是完整线性字库这些空位也要占空间如果是自己裁剪的字库就必须处理空位。2.2 16x16和24x24字模在文件里长什么样16x16点阵就是把一个汉字画在一个16行乘16列的格子里笔画的格子填1空白填0。每一行有16个点正好2字节16行总共32字节。24x24点阵则是每行24个点3字节一行24行总共72字节。实际存储顺序通常是行优先。以16x16为例第0到1字节是第一行的左半部分和右半部分第2到3字节是第二行以此类推。字节内部采用高位在前也就是说一个字节的bit7对应这一行最左边那个点。这个“高位在前”的约定和SPI屏、并口屏的扫描方向是匹配的但如果用习惯了LSB first的协议就会看到字模完全左右镜像。为了直观理解可以看一眼“中”字16x16字模的开头几字节大概是0x00 0x00这样的空白行然后中间某个字节出现0x80或者0x08表示竖笔画的起点。真正的点阵看起来就是一组十六进制数组但当你把这些数据按位展开之后能明显看出汉字轮廓。2.3 取模方向被忽略但最容易翻车的细节取模方向是指一个汉字点阵按什么顺序扫描。最常见的两种横向取模和纵向取模。横向取模是一行一行从左往右扫适合绝大多数LCD屏幕和热敏打印机纵向取模是一列一列从上往下扫常见于某些LED点阵屏因为它们的硬件扫描机制就是逐列刷新的。我在项目里吃过一次亏同一个字库文件在12864上显示完全正常换到另一家厂商的点阵屏上全乱。排查了半天发现那家屏的控制器要求纵向取模而我手里的字库是横向的。解决办法不是重新生成所有字模而是写一个转换函数把横向点阵重排成纵向点阵在显存写入前做一次位转置。这个操作在STM32上大概每字模多花几十微秒完全能接受。3. 索引算法从GBK编码到文件偏移3.1 线性公式索引适合完整字库如果手头是一份完整的GBK线性字库文件所有码位按顺序排列那么求偏移量可以直接用公式。核心思路是先算这个字符在GBK编码空间里的序号再用序号乘上单个字模的字节数。我用C语言写过一个完整的索引函数直接贴在工程里用/** * 计算GBK字符在完整线性字库中的字节偏移 * param gbk_code GBK编码例如0xC4E3表示你 * param bytes_per_char 单个字模字节数16x16传3224x24传72 * return 在字库文件中的偏移量非法编码返回0xFFFFFFFF */ uint32_t gbk_font_offset(uint16_t gbk_code, uint32_t bytes_per_char) { uint8_t high (gbk_code 8) 0xFF; uint8_t low gbk_code 0xFF; if (high 0x81 || high 0xFE) return 0xFFFFFFFF; if (low 0x40 || low 0xFE || low 0x7F) return 0xFFFFFFFF; uint32_t zone_index; uint32_t pos_index; zone_index (uint32_t)(high - 0x81); if (low 0x7F) { pos_index (uint32_t)(low - 0x40); } else { pos_index (uint32_t)(low - 0x80) 63; } return (zone_index * 190 pos_index) * bytes_per_char; }注意中间的pos_index计算。当low小于0x7F时用low减0x40得到0到62当low大于0x7F时不能用low减0x40因为这样0x80会跳到64中间63就空了。正确做法是用low减0x80再加63这样0x80映射到630xFE映射到189两段正好连续。这个公式的前提是字库文件必须完整。如果是全部GBK编码都占位16x16字库文件大小应该是23940乘32等于766080字节24x24是23940乘72等于1723680字节。拿到文件先看一眼大小基本就能判断是不是这种线性格式。3.2 映射表加二分查找适合裁剪后的字库实际项目里经常有人把字库裁剪了只留业务需要的几百个汉字生成一个小字库文件。这时候线性公式失效因为编码号段不连续。比如编码0xC4E3后面紧挨着的不是0xC4E4而可能是0xD6D0因为中间的字没被收录。这种情况下最稳妥的方案是生成字库时同时生成一张索引表。索引表是升序排列的GBK码数组每个码对应一个文件偏移。运行时先用二分查找找到这个码在数组中的位置再取出对应的偏移。查找一个汉字的时间复杂度是O(log n)几百个字规模下查找一次不超过十次比较比线性公式慢一点点但完全不影响体验。索引表可以设计成紧凑结构每条记录8字节2字节GBK码、4字节偏移、2字节保留。整个表直接放在字库文件头部或者单独烧写成一个bin文件程序启动时加载进内存运行时就查这张表。3.3 两种方式怎么选一张对比表说清楚对比项线性公式索引映射表加二分查找字库要求完整线性排列任意顺序可裁剪索引速度最快几次运算较慢约log n次比较内存占用无额外表每条记录8字节文件大小固定偏大可按需裁剪更小适用场景全量GBK字库资源充足嵌入式flash紧张只用部分汉字我个人的习惯是如果MCU的flash不低于1MB直接用完整字库加线性公式省心如果flash只有512KB甚至更小那就裁剪字库把索引表一起烧进去。这里特别提醒一个坑GBK和GB2312的索引公式不能混用。网上很多教程给的公式是GB2312的形如offset((high-0xB0)*94(low-0xA1))*32。如果你拿这个公式去索引GBK字库凡是落在GB2312汉字区之外的字符取出来的数据全是错位的显示出来就是乱码。判断方法是看字库文件大小GB2312的16x16字库HZK16只有267KB而GBK线性16x16字库是766KB差了3倍。4. 配套工具设计与实现4.1 取模工具选型现成的还是自己写字模提取工具市面上不少老牌的PCtoLCD2002、字模3在线取模工具也很多。它们的基本用法都是输入汉字、选字体、字号设置取模方向然后生成一个数组复制到代码里。但这类工具在小批量取模时很好用一旦遇到批量需求就会崩溃。比如要提取500个常用汉字手工配置不现实。我自己写了一组Python脚本直接把整个字库文件按需解析生成C语言头文件。这才是“工具”的完整形态输入是一个文本文件里面是需要的汉字列表输出是一个C数组每项都有注释标注原始编码方便以后排错。4.2 自研Python小工具解析字库并输出C数组下面这个脚本是从GBK完整字库文件中提取指定汉字字模的核心逻辑我把它精简过保留了最骨干的功能import struct def gbk_index(code, bytes_per_char): high (code 8) 0xFF low code 0xFF if high 0x81 or high 0xFE or low 0x40 or low 0x7F: return None zone high - 0x81 pos (low - 0x40) if low 0x7F else (low - 0x80 63) return (zone * 190 pos) * bytes_per_char def extract_fonts(font_path, char_list, width, height, out_path): bytes_per_char (width // 8) * height with open(font_path, rb) as f: font_data f.read() lines [] lines.append(fconst unsigned char font_data[] {{) for ch in char_list: try: code ord(ch.encode(gbk)) except UnicodeEncodeError: code ord(?.encode(gbk)) offset gbk_index(code, bytes_per_char) if offset is None or offset bytes_per_char len(font_data): continue chunk font_data[offset:offset bytes_per_char] hex_str , .join(f0x{b:02X} for b in chunk) lines.append(f // {ch} (GBK: 0x{code:04X})) lines.append(f {hex_str},) lines.append(};) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines))要特别注意encoding的处理。在Windows下源文件本身的编码可能是UTF-8也可能是GBK直接用open默认编码读文本可能撞上UnicodeDecodeError。脚本里读字库文件一律用rb二进制模式字符列表文件单独声明编码。字符列表里有个字符编码成GBK失败时我会用一个问号顶替同时打印告警绝不让整个流程崩溃。关于输出C数组有两点经验一是每行加注释标出这个数组对应的是哪个字、原始GBK编码是多少八百个汉字排布的时候找问题能省大量时间二是数组元素用大写的0xXX格式有些MCU编译器对hex常量的格式很敏感统一大写避免风格混乱。4.3 反向可视化工具把点阵打印出来看对不对有段时间我调试一个显示乱码的问题吃了一整天亏。后来我写了个小函数把汉字点阵在终端里用“#”和空格打印出来配合SSD1306屏幕对照看问题立刻就清晰了。def dump_font(chunk, width, height): bytes_per_row width // 8 for row in range(height): line base row * bytes_per_row for col in range(width): byte_idx base col // 8 bit_idx 7 - (col % 8) line # if (chunk[byte_idx] bit_idx) 1 else print(line)这个工具的输入是一段字模数据输出是一个16行或者24行的字符串图案。用肉眼看一眼就能判断点是横着的还是竖着的是反的还是有锯齿。建议所有做字模工具链的朋友都加上这个功能比盯着十六进制数组猜靠谱得多。5. 实际项目中的坑与排查记录5.1 编码转换相关的经典报错编码问题是最常见的绊脚石。不少人在Windows下用Python处理文本文件遇到UnicodeDecodeError: gbk codec cant decode byte 0x94 in position 2952这类报错第一反应是调编码但这往往是治标不治本。0x94这个字节在GBK里可能是某个合法字符的一部分也可能根本不在GBK范围内因为文件实际可能是GB18030或者UTF-8编码。处理二进制字库和文本列表时一定要清楚每个文件的真实编码并且统一用二进制模式或者显式指定编码去读写。另外做编码转换时不能毫无防备。比如一个字符串里混了全角空格、特殊符号、生僻字在UTF-8转GBK时可能直接抛异常即使不抛异常也可能转成问号。这会导致打印内容和原文字对不上。我的做法是在工具脚本里加一个兜底函数转换失败时输出日志确定是哪些字符引起的再决定是补字库还是换一个等价字符。5.2 取模方向与硬件显示不匹配这个坑我前面提过这里把一个实际现象写清楚如果屏幕显示的字是上下颠倒的那是因为数据是从最后一行开始存的如果是左右镜像的大概率是字节内位序反了如果整行乱成一团那多半是纵向取模的字库用到横向扫描的屏上。排查时先用一个自己代码里的简单图案比如画一条横线一条竖线分别确认X方向和Y方向的扫描顺序再判断字模问题。这个步骤不要跳过它能帮你把“显示问题”和“取模问题”划清边界。5.3 文件偏移和缓冲区边界还有一个隐蔽的坑藏在文件末尾。GBK编码空间里不是每个码位都有字有的码位对应的是空位在线性字库里这部分是空白数据取出来显示会是一片空白这倒还好。但如果是裁剪过的字库索引表里指向的偏移可能已经超出文件大小fseek不报错fread却读不满。我在初始化显示时遇到过花屏后来才发现是偏移越界读到文件末尾后面的随机内存。解决方案很简单每次读取前判断offset加bytes_per_char是否超过文件长度超了就返回一个全0的空白字模。还有一次遇到的是取模工具生成的数据被编译器优化掉了。原因是我把字模数组定义成局部变量而不是全局的const数组编译器觉得这组数据没被实际使用直接给优化没了。解决方法是加volatile或者直接定义成全局const。5.4 常见问题速查表现象原因解决办法屏幕显示乱码拿UTF-8字节当GBK用或索引公式用错字符集先encode成GBK再取字模核对字库大小和索引公式字模左右镜像字节内位序搞反确认是否高位在前反向映射位序字模上下颠倒点阵按从下到上存放转换时倒序遍历行生僻字显示成问号GB2312字库不含该字或GBK转码失败换GBK字库或补字库并重建索引表打印的汉字模糊点阵密度不足或打印头换行步进不对显示场景换24x24或调整打印行间距字库文件读取越界裁剪字库偏移越界读取前做边界检查越界返回空白字模UnicodeDecodeError文件不是GBK编码用错编码打开二进制模式读文件文本文件显式指定编码写在最后分享一个个人经验字模这事儿不算高深但特别磨人。我的体会是尽量把索引和字库文件的关系理顺之后再做界面。先花十分钟写个脚本把整个GBK编码范围内已知有定义的字模数据抽样打印成点阵图案确认你手里的字库是横向还是纵向、高位在前还是低位在前、是不是完整的线性排列。这一步做扎实了后面所有显示和打印工作都顺了。别一上来就堆功能最终你会发现所有乱码和花屏的根因往往就藏在这些最底层、最容易被忽略的参数里。本文还有配套的精品资源点击获取