☰
W25Q128存储GB2312汉字点阵字库与LCD显示方案
2026/10/26 9:04:29 网站建设 项目流程

简介:这份工程资料面向嵌入式与STM32学习者,以及需要外扩汉字字库的开发者,围绕W25Q128串行闪存存储字库、LCD读取并显示汉字这一典型场景展开,重点解决字库数据在外部Flash中的存放与屏幕解析显示问题。包内共192个文件,约4.6MB,以C/H源码、Keil工程文件、hex/axf/map编译输出及o/d/crf中间文件为主,另含字库起始地址说明和keilkill.bat清理脚本,能够呈现SPI通信、字库定位与LCD驱动之间的源码组织关系。已有1971人学习下载,读者可据此对照理解W25Q128从0x1000地址存放字库、程序读取字模并驱动LCD显示汉字的完整工程结构,也可参考其编译产物与批处理脚本进行环境清理和工程复用。

1. 16MB SPI Flash 里的字库:用 W25Q128 存汉字点阵,给 LCD 腾出 MCU 空间

某显示终端项目的 MCU 内部 Flash 只剩几十 KB,界面却要显示菜单、告警和参数名,第一反应是把汉字点阵塞进内部 Flash,结果链接脚本直接爆掉。W25Q128 存储字库加 LCD 显示汉字这套做法,就是把字库从 MCU 搬到 16MB SPI NOR Flash,按汉字编码算出偏移,读回 32 字节点阵,再逐点写到 LCD。它解决的是内部 Flash 紧张、字库要多字号、后期还要换字库的嵌入式显示场景,适合已经能驱动 SPI Flash 和 LCD、但不想为字库换更大 MCU 的开发者。下面按容量规划、字库生成、偏移计算、烧录、显示和排查的顺序拆开讲。

2. 地址规划与容量账本:16×16 到 32×32 字库该占哪一段

2.1 先算一笔容量账:GB2312 与 GBK 在 16/24/32 点阵下的字节数

W25Q128 的容量是 128Mbit,也就是 16MB,地址线 24 位,可寻址 0x000000 到 0xFFFFFF。页大小 256 字节,扇区 4KB,块 64KB。字库放进去之前先算清楚占多少,不然做到一半发现 32×32 字库把预留区顶穿,回头改地址表比重新烧录还麻烦。

一个汉字点阵占多少字节,算法很直接:宽×高÷8。16×16 是 256 像素,32 字节;24×24 是 576 像素,72 字节;32×32 是 1024 像素,128 字节。GB2312 一级和二级汉字合计 6763 个,但实际字库文件常按 94×94 区位表留空位,16×16 会到 216576 字节左右,比纯汉字数多出几个空位。做地址规划时按“区位全表”算更稳,显示偏移和生成脚本用同一套公式,不会因为空位对不上。

字库类型单字字节6763 汉字纯点阵按 72×94 区位留空说明
GB2312 16×1632216416 B ≈ 211 KB216576 B ≈ 211.5 KB最常用,菜单够用
GB2312 24×2472486936 B ≈ 475 KB487296 B ≈ 476 KB标题和提示更清楚
GB2312 32×32128865664 B ≈ 845 KB866304 B ≈ 846 KB大字号,占空间但仍远小于 16MB
GBK 16×163221003×32≈672 KB视排列方式生僻字多,偏移公式要换
GBK 24×247221003×72≈1.51 MB视排列方式需要更细的分区
GBK 32×3212821003×128≈2.69 MB视排列方式16MB 仍放得下,但别和别的资源混放

这张表里“按 72×94 区位留空”对应的是从 16 区到 87 区、每区 94 个位全部保留的排列。它的好处是偏移公式简单:区号减 16、位号减 1,再乘单字字节。缺点是空位也占空间,但 16MB 的 W25Q128 完全承受得住。真正要小心的是 GBK:GBK 的低字节范围包含 0x40 到 0xFE,不能直接套 GB2312 的“低字节减 0xA0”,否则遇到扩展区就是乱码。项目里如果只显示常用汉字,先用 GB2312 把链路跑通;确实要生僻字,再换 GBK 并重做偏移表。

提示:字库文件不是越大越好。16×16 已经能覆盖大多数菜单,24×24 用在标题,32×32 只给少量固定大字。把三个字号都烧进去之前,先确认 LCD 的显存和刷新方式撑得住。

2.2 地址分区怎么切:4KB 扇区对齐与索引表预留

地址分区要按扇区对齐,因为 W25Q128 擦除的最小单位是 4KB 扇区。如果字库起始地址落在 0x000800 这种非 4KB 边界,后面做整段更新时会把相邻数据一起擦掉。下面这套分区是按 GB2312 三字号加 ASCII 做的,每个区之间留了余量,末尾还给索引表和参数区留了位置。

/* W25Q128 字库分区,单位字节,所有起始地址按 4KB 对齐 */ #define FLASH_ASCII_8X16_ADDR 0x000000UL /* ASCII 8x16,预留 4KB */ #define FLASH_GB2312_16X16_ADDR 0x001000UL /* 16x16 汉字,约 211.5KB */ #define FLASH_GB2312_24X24_ADDR 0x040000UL /* 24x24 汉字,约 476KB */ #define FLASH_GB2312_32X32_ADDR 0x0C0000UL /* 32x32 汉字,约 846KB */ #define FLASH_FONT_INDEX_ADDR 0x1A0000UL /* 可选索引表/校验表 */ #define FLASH_FONT_PARAM_ADDR 0x1B0000UL /* 版本、CRC、字号表 */ #define FLASH_USER_RESERVED_ADDR 0x1C0000UL /* 预留给图片/配置 */ #define FONT_16X16_BYTES 32U #define FONT_24X24_BYTES 72U #define FONT_32X32_BYTES 128U #define GB2312_QU_START 16U #define GB2312_QU_END 87U #define GB2312_WEI_COUNT 94U

这段宏定义的关键不是名字,而是三个约束。第一,每个区的起始地址都是 0x1000 的整数倍,扇区擦除不会误伤。第二,16×16 区从 0x001000 到 0x040000 之间有约 0x3F000 字节,放 216576 字节足够。第三,24×24 区从 0x040000 开始,按 487296 字节算结束在 0x0B6E00 附近,0x0C0000 给 32×32 留出了安全边界。32×32 区从 0x0C0000 到 0x1A0000 有 0xE0000 字节,866304 字节约 0xD3800,也在范围内。

实际工程里还要注意一件事:如果字库文件是按“纯 6763 汉字压缩”生成的,偏移公式就不能用“区减 16、位减 1”直接乘,因为空位被删掉了。常见做法是生成时保留 94×94 区位空位,脚本和显示函数共用同一个宏;或者生成时建立索引表,把每个汉字的编码映射到 4 字节地址。前者实现简单,后者省空间。新手先用前者,熟手再根据容量做索引。

2.3 SPI 读时序与 24 位地址:0x03、0x0B 和片选拉低的顺序

W25Q128 的读数据命令常用 0x03 和 0x0B。0x03 是普通读,发完 24 位地址后直接出数据;0x0B 是快速读,地址后要加一个 dummy 字节,适合较高时钟。无论用哪个,片选 CS 必须全程拉低,地址按高字节、中字节、低字节顺序发,发完才能读 MISO。很多“读回全是 0xFF”的现场问题,最后查出来是 CS 提前拉高或者 SPI 模式配错。

/* 适配层:spi_cs_low/spi_cs_high、spi_xfer 由具体 MCU 的 SPI 驱动实现 */ static void w25q_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; spi_cs_low(); spi_xfer(0x03); /* 普通读命令 */ spi_xfer((uint8_t)(addr >> 16)); /* 24 位地址,高字节先发 */ spi_xfer((uint8_t)(addr >> 8)); spi_xfer((uint8_t)(addr)); for (i = 0; i < len; i++) { buf[i] = spi_xfer(0x00); /* 发哑字节,读回数据 */ } spi_cs_high(); } static uint32_t w25q_read_id(void) { uint32_t id = 0; spi_cs_low(); spi_xfer(0x9F); /* JEDEC ID 命令 */ id |= (uint32_t)spi_xfer(0x00) << 16; id |= (uint32_t)spi_xfer(0x00) << 8; id |= (uint32_t)spi_xfer(0x00); spi_cs_high(); return id; }

w25q_read里没有页边界限制,因为读操作可以连续跨页、跨扇区,只要地址不越界。真正有限制的是写:页编程一次最多 256 字节,而且不能跨页。读字库时一次读 32 字节、72 字节或 128 字节,都远小于一页,所以读函数可以放心连续读。w25q_read_id用来做上电自检,常见 W25Q128 的 JEDEC ID 是 0xEF4018;读不到就不要再往下显示,先查供电、CS、SPI 模式和 WP/HOLD 引脚。

注意:SPI 模式要按从设备要求设置,常见是模式 0 或模式 3。模式错时 ID 可能偶尔读对,但连续读字库会错位,表现就是“前几个字正常,后面花屏”。不要用“能读 ID 就算通”当结论。

3. 从 GB2312 编码到点阵偏移:生成脚本、取模方向和画点函数

3.1 GB2312 区位码到偏移:区码、位码和 94×94 表

GB2312 汉字在字符串里占两个字节,高字节和低字节都大于 0x80。区码等于高字节减 0xA0,位码等于低字节减 0xA0。比如“中”的 GB2312 编码是 0xD6D0,高字节 0xD6 减 0xA0 得 54 区,低字节 0xD0 减 0xA0 得 48 位。偏移按“区减 16、位减 1”算:(54-16)×94×32 + (48-1)×32,得到 115808,加上 16×16 字库基址 0x001000,实际读地址是 0x1D460。

/* 返回 0 表示不是 GB2312 汉字,1 表示成功 */ static int gb2312_to_offset(uint8_t hi, uint8_t lo, uint32_t *offset, uint32_t bytes_per_char) { uint8_t qu, wei; if (hi < 0xA1 || hi > 0xF7 || lo < 0xA1 || lo > 0xFE) { return 0; } qu = hi - 0xA0; wei = lo - 0xA0; if (qu < GB2312_QU_START || qu > GB2312_QU_END) { return 0; } *offset = ((uint32_t)(qu - GB2312_QU_START) * GB2312_WEI_COUNT + (uint32_t)(wei - 1)) * bytes_per_char; return 1; }

这个函数只处理 GB2312 的汉字区,16 区到 87 区。1 到 9 区是符号,10 到 15 区为空,88 到 94 区也不在常用汉字范围。如果项目要显示全角标点,需要单独把符号区也生成进去,并且把偏移公式的起始区改成 1 区。不要一边用 16 区起始生成,一边用 1 区起始显示,否则所有汉字整体错位,表现是“显示的是别的字”。

参数bytes_per_char由字号决定:16×16 传 32,24×24 传 72,32×32 传 128。这个参数不要写死在函数里,显示函数按字号调用,后面扩字号时不用改偏移逻辑。hi和lo直接来自字符串的两个字节,但前提是源文件编码是 GB2312。如果编译器把中文字符串按 UTF-8 存,一个汉字三个字节,这里按两个字节解析必然乱码;这个问题放在避坑章节细说。

3.2 字库生成脚本:HZK16 源文件、输出 bin 和校验文件

字库源文件常见是 HZK16、HZK24 这类按区位排列的点阵文件。生成脚本要做的事很朴素:按 16 区到 87 区、每区 94 位,从源文件读 32 字节,写到输出 bin;源文件不够长就补 0xFF 或 0x00,保证输出大小固定。输出文件可以直接烧到 W25Q128 的 0x001000,也可以打包成整片镜像。下面脚本以 16×16 为例,24×24 和 32×32 只改单字字节数。

# gen_gb2312_font.py # 源文件按 94x94 区位排列,每个汉字点阵连续存放 SRC = "HZK16" OUT = "font_gb2312_16x16.bin" BYTES_PER_CHAR = 32 QU_START, QU_END, WEI_COUNT = 16, 87, 94 with open(SRC, "rb") as f: src = f.read() out = bytearray() missing = 0 for qu in range(QU_START, QU_END + 1): for wei in range(1, WEI_COUNT + 1): src_off = ((qu - 1) * WEI_COUNT + (wei - 1)) * BYTES_PER_CHAR chunk = src[src_off:src_off + BYTES_PER_CHAR] if len(chunk) < BYTES_PER_CHAR: chunk = chunk + b"\x00" * (BYTES_PER_CHAR - len(chunk)) missing += 1 out.extend(chunk) with open(OUT, "wb") as f: f.write(out) print("output bytes:", len(out)) print("missing chars:", missing)

脚本输出大小是72×94×32 = 216576字节,和 2.2 节的分区一致。src_off是按源文件的 94×94 区位表算的,不是按输出文件算的;如果手里的源文件是“只存 6763 个汉字”的压缩格式,这个偏移会取到错误位置,必须先确认源文件大小和排列。常见的判断方法是看文件大小能否被 32 整除,以及前几个字节是否是“啊”字的点阵。missing不为 0 说明源文件覆盖不全,后面显示到对应区就会缺字或黑块。

24×24 和 32×32 的脚本只需把BYTES_PER_CHAR改成 72 或 128,输出文件名改成对应地址的分区文件。烧录时 16×16 写 0x001000,24×24 写 0x040000,32×32 写 0x0C0000,不要写错。脚本末尾建议再生成一个 CRC32 文本,烧录后回读比对;这一点放在第 6 章展开。

3.3 LCD 画汉字:逐行取模、位序和背景色处理

显示函数最容易翻车的地方不是 Flash 读,而是取模方向和 LCD 画点方向对不上。下面这段按“横向取模、逐行、高位在前”写:16×16 每行 2 字节,第一字节管左 8 像素,第二字节管右 8 像素,字节内 bit7 在左、bit0 在右。如果你的取模软件选的是“纵向取模”或“逆向”,这段代码画出来就是旋转或镜像的,需要改位序或换取模模板。

/* 画一个 16x16 汉字,font 为从 W25Q128 读出的 32 字节 */ static void lcd_draw_chinese_16(uint16_t x, uint16_t y, const uint8_t *font, uint16_t color, uint16_t bg) { uint8_t row, col, b1, b2; for (row = 0; row < 16; row++) { b1 = font[row * 2]; /* 左 8 像素 */ b2 = font[row * 2 + 1]; /* 右 8 像素 */ for (col = 0; col < 8; col++) { lcd_draw_point(x + col, y + row, (b1 & (0x80 >> col)) ? color : bg); } for (col = 0; col < 8; col++) { lcd_draw_point(x + 8 + col, y + row, (b2 & (0x80 >> col)) ? color : bg); } } } /* 显示 GB2312 字符串,逐字读 Flash 后画到 LCD */ static void lcd_show_gb2312(uint16_t x, uint16_t y, const char *str, uint16_t color, uint16_t bg) { uint8_t buf[32]; uint32_t off; while (*str) { if ((uint8_t)*str < 0x80) { /* ASCII 走 8x16 字库,这里省略具体绘制 */ lcd_show_ascii_8x16(x, y, (uint8_t)*str, color, bg); x += 8; str++; } else { uint8_t hi = (uint8_t)str[0]; uint8_t lo = (uint8_t)str[1]; if (gb2312_to_offset(hi, lo, &off, FONT_16X16_BYTES)) { w25q_read(FLASH_GB2312_16X16_ADDR + off, buf, sizeof(buf)); lcd_draw_chinese_16(x, y, buf, color, bg); } x += 16; str += 2; } if (x > 220) { x = 0; y += 16; } /* 简单换行,按屏宽调整 */ } }

lcd_draw_chinese_16里把bg也画上去,适合不透明背景;如果 LCD 支持透明色或需要叠加在图片上,可以把bg分支去掉,只画前景点,但背景要先由调用方填好。b1 & (0x80 >> col)是高位在前的判断,如果取模软件选“低位在前”,要改成0x01 << col。x + 8 + col对应右半字节,别写成x + col + 1,否则左右两半会挤在一起。

lcd_show_gb2312每次显示一个汉字读 32 字节 Flash,一屏 20 个汉字是 640 字节,SPI 时钟 10MHz 时数据时间很短;真正慢的是lcd_draw_point逐点画。后面第 5 章会讲缓存和批量写,先把单字显示跑通,再优化刷新。换行阈值 220 只是示意,实际按 LCD 宽度减 16 计算,坐标不要越界。

4. 烧录与显示主流程:把字库写进 W25Q128,再从 LCD 读出来

4.1 烧录链路选型:离线编程器、MCU 转发和文件系统

字库怎么进 W25Q128,常见三条路。第一条是离线编程器,把生成好的 bin 直接烧到 Flash 芯片,适合量产前打样,缺点是每次改字库都要拆焊或夹夹具。第二条是 MCU 转发,MCU 通过串口、USB 或调试接口收文件,再按扇区擦除、页编程写入 W25Q128,适合现场升级和小批量。第三条是文件系统,把字库当文件存进 SPI Flash 文件系统,灵活但多一层索引和磨损管理,显示时还要读文件。做 LCD 汉字显示,字库是只读资源,我更倾向第二条:生产时用编程器烧整片镜像,现场用 MCU 转发只更新字库区。

方式适合阶段优点注意点
离线编程器打样、量产速度快,整片镜像一次写好夹具接触要可靠,注意 3.3V
MCU 转发小批量、现场升级不用拆芯片,可带 CRC 校验写前擦除,跨页拆分,忙等待
文件系统多资源、频繁换字库按文件管理,扩展方便增加索引开销,读文件比直接读地址慢

如果选 MCU 转发,写入函数必须遵守 W25Q128 的擦写规则:扇区擦除最小 4KB,页编程一次最多 256 字节且不能跨页,写之前要发写使能 0x06,写之后轮询状态寄存器 BUSY 位。下面的写函数按“先擦除覆盖到的扇区,再按页边界拆分”实现,适合整段字库更新;如果只改一个字,不要直接调它,否则会把同一个 4KB 扇区里的其他字一起擦掉。

static void w25q_wait_busy(void) { uint8_t st; do { spi_cs_low(); spi_xfer(0x05); /* 读状态寄存器 */ st = spi_xfer(0x00); spi_cs_high(); } while (st & 0x01); /* bit0 为 BUSY */ } static void w25q_write_enable(void) { spi_cs_low(); spi_xfer(0x06); /* 写使能 */ spi_cs_high(); } static void w25q_sector_erase_4k(uint32_t addr) { w25q_write_enable(); spi_cs_low(); spi_xfer(0x20); /* 4KB 扇区擦除 */ spi_xfer((uint8_t)(addr >> 16)); spi_xfer((uint8_t)(addr >> 8)); spi_xfer((uint8_t)(addr)); spi_cs_high(); w25q_wait_busy(); } static void w25q_page_program(uint32_t addr, const uint8_t *data, uint16_t len) { uint16_t i; w25q_write_enable(); spi_cs_low(); spi_xfer(0x02); /* 页编程 */ spi_xfer((uint8_t)(addr >> 16)); spi_xfer((uint8_t)(addr >> 8)); spi_xfer((uint8_t)(addr)); for (i = 0; i < len; i++) { spi_xfer(data[i]); } spi_cs_high(); w25q_wait_busy(); } int w25q_write_font(uint32_t addr, const uint8_t *data, uint32_t len) { uint32_t sec, sec_end, page_left, n; sec = addr & ~0xFFFUL; sec_end = (addr + len - 1) & ~0xFFFUL; for (; sec <= sec_end; sec += 0x1000UL) { w25q_sector_erase_4k(sec); } while (len > 0) { page_left = 256U - (addr & 0xFFU); n = (len < page_left) ? len : page_left; w25q_page_program(addr, data, (uint16_t)n); addr += n; data += n; len -= n; } return 0; }

w25q_write_font里的sec_end用(addr + len - 1) & ~0xFFF算最后一个扇区,避免少擦一个扇区导致后半段写不进。page_left保证每次页编程不跨 256 字节边界,n取剩余长度和页内剩余空间的较小值。w25q_wait_busy每次写完都要等,不能靠延时“大概好了”,页编程和扇区擦除的时间随芯片状态变化,延时不够就是随机失败。

注意:擦除后的 Flash 是 0xFF,不是 0x00。字库点阵里 0xFF 表示全亮还是全不亮,取决于取模的阴阳码。如果显示出来是反白,先查取模设置,不要先怀疑 Flash 写坏。

4.2 上电初始化与显示主流程:读 ID、取字模、画到屏幕

上电流程不要一上来就显示,先做三件事:初始化 SPI 和 LCD,读 W25Q128 的 JEDEC ID,读字库版本或 CRC 参数。ID 不对或者参数区是 0xFF,说明 Flash 没焊好、没供电或者字库没烧,继续显示只会得到黑屏或花屏。下面的主流程把读 ID、显示测试字和简单自检串起来,实际工程里可以把font_param换成自己的版本结构体。

typedef struct { uint32_t magic; /* 例如 0x464F4E54 */ uint16_t ver; uint16_t crc16; uint32_t font16_len; uint32_t font24_len; uint32_t font32_len; } font_param_t; int main(void) { uint32_t id; font_param_t param; board_spi_init(); lcd_init(); lcd_clear(0xFFFF); id = w25q_read_id(); if (id != 0xEF4018) { lcd_show_ascii_8x16(0, 0, 'E', 0xF800, 0xFFFF); while (1) { } } w25q_read(FLASH_FONT_PARAM_ADDR, (uint8_t *)&param, sizeof(param)); if (param.magic != 0x464F4E54) { /* 参数区无效,提示先烧字库 */ lcd_show_gb2312(0, 16, "字库未烧录", 0xF800, 0xFFFF); while (1) { } } lcd_show_gb2312(0, 32, "汉字显示测试", 0x0000, 0xFFFF); lcd_show_gb2312(0, 56, "W25Q128字库", 0x001F, 0xFFFF); while (1) { } }

param.magic是一个约定值,烧字库时一并写入参数区;没有它就只能靠读点阵判断,效率低。id != 0xEF4018的判断按常见 W25Q128 写,如果你手里的批次 ID 不同,先读出来确认再改,不要盲改。lcd_show_gb2312里的字符串必须是 GB2312 编码,如果源文件是 UTF-8,这里会乱码,解决办法在第 5 章。

显示顺序也有讲究:先清屏,再画背景,再画字。如果 LCD 初始化后显存是随机值,直接画字会出现“字周围有雪花”。清屏一次能排除大部分背景干扰。测试字建议用“中”“国”“A”三个,分别覆盖汉字、常用字和 ASCII,能同时验证 GB2312 偏移、字库读取和 ASCII 分支。

4.3 读缓存与局部刷新:别让每个像素都去读 Flash

字库在外部 Flash,但显示瓶颈通常不在 Flash,而在 LCD 画点。一个 16×16 汉字 256 个像素,每个像素调一次lcd_draw_point,一屏 20 个字就是 5120 次函数调用,如果 LCD 是并口或 SPI 屏,刷新会明显卡。优化的第一步不是换 QSPI,而是把字模先读到 RAM,再按行批量写 LCD。很多 LCD 驱动支持“设置窗口 + 连续写像素”,一次写一行 16 个像素,比逐个画点快得多。

/* 简易字模缓存:最近 16 个 16x16 字模,每个 32 字节 */ typedef struct { uint16_t code; /* GB2312 双字节拼成的 16 位编码 */ uint8_t used; uint8_t data[32]; } font_cache_t; static font_cache_t cache[16]; static const uint8_t *font16_get_cached(uint16_t code) { uint8_t i, victim = 0; uint32_t off; for (i = 0; i < 16; i++) { if (cache[i].used && cache[i].code == code) { return cache[i].data; } if (!cache[i].used) { victim = i; break; } } if (!gb2312_to_offset((uint8_t)(code >> 8), (uint8_t)code, &off, 32)) { return 0; } w25q_read(FLASH_GB2312_16X16_ADDR + off, cache[victim].data, 32); cache[victim].code = code; cache[victim].used = 1; return cache[victim].data; }

这个缓存只做最近使用,没有做 LRU 淘汰,victim取第一个空位或 0 号槽,适合菜单这种重复字多的场景。code用高字节左移 8 加低字节拼成 16 位,比较时不会把“中”和别的字混淆。缓存命中后不再读 Flash,画字仍然逐点,但 Flash 读次数降下来了。进一步优化是把lcd_draw_chinese_16改成按行写 LCD 的窗口函数,一次写 16 位,速度会再上一个台阶。

局部刷新适合参数界面:只重画变化的那一行,不要整屏清。整屏清一次要写整个显存,SPI 屏上很贵。做法是记录上次显示的内容,比较后只刷新差异区域;如果嫌麻烦,至少把“清屏 + 重画”改成“填背景矩形 + 重画该行”。这些优化不改变字库地址和偏移,先把显示跑通再加。

5. 避坑与排查:花屏、缺字、读回 0xFF 和刷新卡顿的现场记录

5.1 显示类踩坑:花屏、缺字和编码不匹配

现象:汉字显示成乱码方块,笔画左右错位或上下颠倒。原因通常是取模方向和画点代码不一致。取模软件里选了“纵向取模”,代码却按横向逐行解析;或者选了“低位在前”,代码用0x80 >> col。解决方法是固定一套取模模板:16×16、横向取模、逐行、高位在前、阴码(1 表示亮)。先用一个 0x01 和 0x80 的测试图案确认位序,再看汉字。不要一边改代码一边换取模设置,否则变量太多,排不出来。

现象:前几个字正常,后面缺字、错字或显示成别的汉字。原因多半是偏移公式和生成文件的排列不一致。比如生成脚本按 16 区起始、94 位全表输出,显示函数却用 6763 压缩偏移;或者 24×24 字库烧到了 16×16 的基址。解决方法是打印第一个出错汉字的编码、区码、位码、计算偏移和实际读地址,再用编程器或 MCU 回读该地址的 32 字节,和 PC 上的字库文件比对。抽样“中”“国”“啊”三个字,覆盖不同区,能快速定位是整体错位还是局部缺失。

现象:汉字显示成三个字节的乱码,或者只显示半个字。原因是字符串编码不是 GB2312。很多编辑器默认 UTF-8,C 文件里写“汉字显示测试”,编译后每个汉字是 3 个字节,显示函数按 2 个字节解析,必然错位。解决办法有两个:源文件保存为 GB2312/GBK 编码,或者字库改用 Unicode 并让显示函数按 UTF-8 解码。不要在同一份代码里混用两种编码。检查方法很简单,把字符串按字节打印出来,“中”的 GB2312 是 D6 D0,UTF-8 是 E4 B8 AD,一看便知。

现象:字库明明有,某个生僻字显示成空白。原因是 GB2312 只覆盖 6763 个汉字,生僻字在 GBK 扩展区。GBK 的低字节范围包含 0x40 到 0xFE,不能套 GB2312 的lo - 0xA0。解决方法是换 GBK 字库和 GBK 偏移表,或者给生僻字做小索引表。不要用 GB2312 的公式硬算 GBK 编码,算出来的偏移会落到别的字上。

5.2 存储类踩坑:0xFF、写入失败和刷新卡顿

现象:读字库全是 0xFF,读 ID 也时好时坏。原因可能是 CS 没拉低、SPI 模式错、WP/HOLD 引脚没上拉、供电不足,或者地址超出 24 位范围。解决步骤是先用w25q_read_id读 JEDEC ID,读不到就查硬件:3.3V 供电、地、CS、CLK、MOSI、MISO、WP 和 HOLD。ID 能读但字库读 0xFF,查地址是否越界、字库是否真的烧过、扇区是否被误擦。逻辑分析仪看 CS 和 CLK 最直接,能看出命令字节和地址字节

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

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

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

立即咨询