简介:这套资源围绕 W25Q128 SPI 闪存与 LCD 汉字显示,是一份面向掌握 STM32 基础、希望扩展外部存储与字库应用的嵌入式学习者的完整工程实例。项目基于 Keil 开发环境,通过 SPI 接口读取 W25Q128 中从 0x1000 地址开始存放的 GBK/UTF-8 汉字字库,再经驱动解码后在 LCD 上点阵显示,涉及芯片页写入、扇区擦除、字库地址规划与液晶驱动等多个关键环节,适合课程设计、毕业设计或实战练习。压缩包共 192 个文件,约 4.6MB,以 C/H 源码、Keil 配置工程、编译生成文件及中间列表为主,同时含字库起始地址说明、清除编译中间文件的批处理脚本和开发文档,目录结构包含 Libraries、User、Output、Doc 等分类,便于按模块比对与复现。目前已有 1971 人学习下载,资源完整呈现 SPI 读写、字库解析、LCD 驱动、烧录地址规划等流程,读者可对照工程源码理解 W25Q128 的存储结构,并直接修改字库地址或显示函数用于自身项目,是学习串行 Flash 外扩字库显示的实用参考。
1. 做屏显先撞上的容量墙:W25Q128 为什么是字库方案的常见解法
做带汉字显示的 LCD 产品,最先撞到的是字库放哪。常见 ARM Cortex-M 系列单片机的内部 Flash 就几百 KB,一个 24x24 中文字库接近 500 KB,直接爆掉。W25Q128 是一颗 128 Mbit 的 SPI NOR Flash,换算下来 16 MB,存好几套不同字号的中文字库都绰绰有余,而且读取时序简单、坏块管理压力小、扩容只要换芯片型号不用改软件。这篇笔记把我从选型、取模、烧录、驱动到排错的完整过程记录下来,适合正在做 LCD 菜单、HMI 界面、工控屏,或者想绕开内置 Flash 容量瓶颈的嵌入式开发者。核心结论是:字库方案能不能落地,不取决于 Flash 容量,而在于你把地址算对了没有。
2. W25Q128 与字库格式:容量估算、编码偏移和读取命令
2.1 汉字点阵的本质:一个字到底占多少字节
汉字在屏幕上显示,本质是把汉字图形变成 0/1 点阵。16x16 点阵就是把一个汉字画在 16 乘 16 的格子上,每个点用 1 bit 表示,所以一个字占 16 乘 16 除以 8,等于 32 字节。24x24 点阵是 72 字节,32x32 点阵是 128 字节。这个换算关系是整个字库容量计算的基础,很多人后面地址算错,就是因为在这里把“bit”和“byte”混了。
点阵的二进制排列方式不统一。最常见的是横向取模:第一行左 8 个点放第一个字节,第一行右 8 个点放第二个字节,第二行放第三、四字节,依此类推。也有纵向取模的,先列出第 1 列上 8 点,再第 1 列下 8 点。横向和纵向取模的数据排列完全不一样,如果你用的 LCD 驱动 IC 扫描方向是横向,而字库 bin 却是纵向取模,那显示出来的汉字就是整体旋转或者镜像的。我后面把这个问题单独列成一条避坑记录,因为它是字库方案里出现频率最高的故障。
字库 bin 文件就是一个汉字的点阵数据,按编码顺序连续存放。GB2312 编码下,一个汉字占两个字节,高位和低位都在 0xA1 到 0xFE 之间。要从字库文件里取出某个汉字的点阵,关键是把它的编码换算成文件偏移。下面用一个 Python 示例演示标准转换方法:
def get_gb2312_offset(high: int, low: int, bytes_per_char: int) -> int: """ 计算 GB2312 汉字在字库 bin 中的偏移。 high/low 是编码的两个字节,例如 '中' 的 GB2312 是 0xD6D0。 bytes_per_char 为每字字节数,16x16 是 32。 """ if 0xA1 <= high <= 0xA9: # 符号区 0xA1A1 ~ 0xA9FE zone = high - 0xA1 pos = low - 0xA1 return (zone * 94 + pos) * bytes_per_char elif 0xB0 <= high <= 0xF7: # 汉字区 0xB0A1 ~ 0xF7FE,一级汉字从 0xB0 开始 zone = high - 0xB0 pos = low - 0xA1 return (zone * 94 + pos) * bytes_per_char else: raise ValueError("unsupported GB2312 code")逻辑说明:GB2312 编码的每个区固定有 94 个字符,符号区从第 1 区到第 9 区,汉字区从第 16 区开始。代码里把高位字节减去区起点,再乘 94,加上位差值,得到的是这个字符在整张字库表里的序号,最后乘每字字节数,得到 bin 文件偏移。注意这里用的是编码字节,不是区位码。如果手里拿到的是区位码,就不要减 0xA0 之外的东西。
参数说明:high 和 low 必须取值在 GB2312 范围内。实际工程中,你拿到的字符串可能是 UTF-8 编码,尤其是从上位机或文件系统读出来的,需要先转成 GB2312 再算偏移,否则 high 落在 0xE0 到 0xEF 之间,直接走进异常分支。我在某个项目里就在这上面跳过坑:串口发送的是 UTF-8 的“显”,MCU 里直接拿字节算偏移,结果显示成乱七八糟的方块。
2.2 容量测算和地址规划:先算清楚再动手
GB2312 的汉字区一共 72 个区,每区 94 个字符,总共 6763 个汉字。实际上常用的一级汉字只有 3755 个,二级汉字 3008 个。很多网上流传的“GB2312 字库”只收录了一级,文件大小只有全量的约 55%。如果你要显示人名、古诗、生僻字,就必须确认字库文件是完整的。
下面这张表是我常用字号和空间消耗的测算结果:
| 字号 | 每字字节数 | 6763 汉字占用 | 预留符号区 | 合计 |
|---|---|---|---|---|
| 16x16 | 32 | 216416 字节 | 约 8192 字节 | 约 224 KB |
| 24x24 | 72 | 486936 字节 | 约 18432 字节 | 约 505 KB |
| 32x32 | 128 | 865664 字节 | 约 32768 字节 | 约 896 KB |
W25Q128 的 16 MB 空间放上面三套字库毫无压力。但要注意还有 4 KB 扇区对齐问题:Flash 写入前必须先擦除,擦除最小单位是 4 KB,所以字库的起始地址最好按 4 KB 整数倍规划。我习惯的地址分配是这样:
- 0x000000:16x16 汉字库
- 0x040000:16x16 ASCII 符号库
- 0x080000:24x24 汉字库
- 0x100000:32x32 汉字库
- 0xFE0000:参数区,存放索引表和版本号
这个分配把 16x16 放在最前面,因为菜单滚屏时最常用,读地址低还能省去部分偏移计算。参数区放在最高地址,避免字库扩容时撞到参数区。如果你还要放图片或其他资源,建议把这些固定地址段写成一个头文件,全工程只改这一处。
生成字库 bin 后,我用一个 Python 小脚本检查文件大小,防止取模软件阴我一手:
import os for name in ["font16.bin", "font24.bin", "font32.bin"]: size = os.path.getsize(name) print(f"{name}: {size} bytes, expect 16x16:216416, 24x24:486936, 32x32:865664")逻辑说明:脚本只是把文件大小打印出来,和上面表格里的期望值对比。如果大小不对,先不要烧写,回头检查取模软件的字符集范围。曾经有同事拿到的 16x16 字库是 120160 字节,少了差不多一半,就是缺二级汉字。
参数说明:这里 216416 字节对应完整的 6763 个汉字,加符号区是另外的。如果你下载的字库 bin 本身就带符号区,那文件大小会比表格大,地址偏移也要按“从符号区开始”计算。
2.3 读取命令和时序:0x03 慢读就够用
W25Q128 的读取命令里,最基础的是 0x03 Read Data,命令后跟 24 位地址,然后芯片从该地址连续输出数据。还有 0x0B Fast Read,命令后跟 24 位地址,再跟一个 dummy byte,然后输出数据。快速读支持更高时钟频率,但多了 8 个 dummy 周期,对字库这种小批量随机读场景并不划算。字库点阵一次读取只有几十到一百多字节,我一般直接用 0x03,代码更简单,时序余量也更大。
下面是一个标准 SPI 读函数:
void w25q_read(uint32_t addr, uint8_t *buf, uint32_t len) { cs_low(); // 片选拉低 spi_write_byte(0x03); // READ 命令 spi_write_byte((addr >> 16) & 0xFF); // 地址高 8 位 spi_write_byte((addr >> 8) & 0xFF); // 地址中 8 位 spi_write_byte(addr & 0xFF); // 地址低 8 位 for (uint32_t i = 0; i < len; i++) { buf[i] = spi_read_byte(); // 每读一个字节,主机也要发时钟 } cs_high(); // 读完拉高片选 }逻辑说明:地址是 24 位,先发最高字节。读数据的时钟由主机产生,spi_read_byte 通常是 SPI 外设的交换函数,主机发送一个 0xFF 垃圾字节,同时从 MISO 上采样到从机输出的数据。读取期间片选必须一直保持低电平,读完再拉高,否则芯片会中止本次读取。
参数说明:W25Q128 默认支持 SPI 模式 0 和模式 3,常用的是模式 0,即 CPOL=0、CPHA=0。如果你的 SPI 外设在初始化时配置成模式 1 或模式 2,读回来的数据会偶尔错位。这个故障很难查,因为大部分字节是对的,只有某个 bit 错。另外,0x03 命令虽然规格书标称最高 50 MHz,实际板子上走线长了或电平不匹配,我一般降到 10 MHz 到 20 MHz 之间,换来的是稳定。读操作不需要发 0x06 写使能,也不需要查 BUSY,因为读命令不会改变 Flash 内部状态。
3. 把字库写进 W25Q128:离线烧录、在线写入和索引表
3.1 离线烧录:生成 bin 文件再交给编程器
最省事的量产方式是 PC 端生成字库 bin,再用通用 SPI Flash 编程器烧录。这个方法的好处是 MCU 不需要参与烧写逻辑,缺点是换字库必须拆芯片,适合产品定型后批量复制。
生成 bin 文件时,最重要的设置是编码格式和取模方向。取模软件通常支持“逐行显示模式”和“逐列显示模式”,我选横向逐行。编码顺序要选 GB2312,不要选 Unicode 或 UTF-8,否则地址偏移公式全都要改。生成后建议写个小脚本把 bin 文件按 32 字节一个记录拆分,抽查几个已知汉字的点阵数据,确认取模方向正确。
# 示例:检查 font16.bin 前 32 字节的十六进制输出 xxd -l 32 font16.bin逻辑说明:前 32 字节就是 GB2312 编码顺序里第一个汉字的点阵,通常是“啊”字。如果你知道这个字的字形结构,可以人工核对第一行点阵是否对应。这一步看起来原始,但能把取模方向搞错的资源挡在生产之前。
参数说明:xxd 是 Linux 下的十六进制工具,Windows 下可以用 Hex Fiend 或 Notepad++ 的 HEX 插件。重点看第一行的 16 个 bit 是不是符合“啊”字的横笔位置。
3.2 在线写入:MCU 自己把字库写进 Flash
开发调试阶段不可能每次都拆芯片。我习惯给板子留一个在线烧写接口,MCU 通过串口接收 bin 数据后再写入 SPI Flash。这需要实现三个基础函数:写使能、页编程、等待 BUSY。
W25Q128 的页大小是 256 字节,页编程命令 0x02 一次最多写 256 字节,而且不能跨页。下面是一个完整的页编程实现:
void w25q_program_page(uint32_t addr, uint8_t *data, uint16_t len) { if (len == 0 || len > 256) { return; // 一次页编程最多 256 字节 } w25q_write_enable(); // 0x06 写使能,必须在写之前发 cs_low(); spi_write_byte(0x02); // Page Program spi_write_byte((addr >> 16) & 0xFF); spi_write_byte((addr >> 8) & 0xFF); spi_write_byte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { spi_write_byte(data[i]); } cs_high(); w25q_wait_busy(); // 等待内部擦写忙信号结束 }逻辑说明:0x02 命令发送完地址后,主机把待写数据连续送出。芯片内部会把数据缓存到页缓冲区,然后进入编程状态。BUSY 位在状态寄存器 bit 0 里,写操作期间必须等待它变为 0,才能进行下一次操作。如果跳过 wait_busy,下一次写使能或读操作可能撞上内部编程中,导致数据丢失。
参数说明:len 不能超过 256。关键约束是地址不能跨页,比如地址 0x001F00,页起始是 0x001F00,页结束是 0x001FFF,如果从页内偏移 0x1F0 开始写 0x20 字节,正好跨到下一页,Flash 不会崩,但写进去的数据会卷回页起始地址,把前面的内容覆盖掉。很多人第一次写自定义字库就碰到这个问题,读回来头尾错位。解决办法是写数据前先计算剩余页空间,把数据拆成不超过页边界的一段段。
完整的写入函数需要封装扇区擦除和跨页拆分:
void w25q_write(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t remain = len; uint32_t offset = 0; uint32_t current_addr = addr; while (remain > 0) { if (w25q_is_sector_dirty(current_addr, data + offset, remain)) { w25q_erase_sector(current_addr & 0xFFFFF000); // 擦除所在 4KB 扇区 } uint16_t page_remained = 256 - (current_addr & 0xFF); // 本页剩余空间 uint16_t chunk = (remain > page_remained) ? page_remained : remain; w25q_program_page(current_addr, data + offset, chunk); current_addr += chunk; offset += chunk; remain -= chunk; } }逻辑说明:这个函数先在目标扇区擦除,然后按页边界切块写入。is_sector_dirty 比对要写的数据和 Flash 里已有数据是否相同,只有不同才擦除,否则直接跳过,能明显缩短更新时间。注意擦除是整个扇区,擦完必须等 BUSY 再继续写。
参数说明:addr 最好按 4 KB 对齐,这样一次擦除刚好覆盖要写的区域。current_addr & 0xFFFFF000 是让地址对齐到扇区起始,如果字库文件跨多个扇区,就会循环擦除多次。
3.3 索引表:让程序自动定位字库位置
固定地址方案够用,但维护不方便。我习惯在 Flash 里放一份索引表,让启动代码动态读取字库参数。这样后续更新字库版本、增加字号,都不需要改编译代码里的硬编码地址。
typedef struct { uint32_t magic; // 固定为 0xA5A5A5A5,用于校验 uint32_t version; // 字库版本号 uint32_t start_addr[4]; // 每种字号的起始地址 uint32_t char_count[4]; // 每种字号的字符数 uint8_t width[4]; // 每种字号的宽度 uint8_t height[4]; // 每种字号的高度 } font_index_t;逻辑说明:我把 font_index_t 放在参数区 0xFE0000 处,开机后用 w25q_read 读出来,校验 magic。如果 magic 不对,说明参数区为空或字库没烧录,程序进入“低内存模式”,只显示 ASCII。如果 magic 对,就把结构体参数填入一个全局变量,后面所有显示调用都走这个指针。
参数说明:这里的“4”是我给自己预留的最大字号套数,当前工程只需要 3 套。结构体里存起始地址和尺寸,显示函数可以根据需要选择字号。更新字库时,程序通过串口收到“切换到更新模式”指令,先擦除参数区,再写新的索引,最后写字库数据,写完后进行一次 CRC 校验,失败就自动回滚到旧索引。
4. 从 Flash 到 LCD 显示:内码换算、取模转换和刷屏优化
4.1 内码到 Flash 地址:不能只算一级汉字
拿到一个汉字内码后,第一步是判断它属于符号区还是汉字区,然后计算它对应字库 bin 里的偏移。我在第 2 章已经给出基本公式,这里补一个完整的 C 函数:
uint32_t get_font_addr(uint16_t gb_code, const font_info_t *info) { uint8_t high = (gb_code >> 8) & 0xFF; uint8_t low = gb_code & 0xFF; uint32_t index; uint32_t font_start = info->start_addr; if (high >= 0xB0 && high <= 0xF7) { // 汉字区:从 0xB0A1 开始,共 72 区,每区 94 字 index = (uint32_t)(high - 0xB0) * 94 + (low - 0xA1); if (index >= 6763) { return 0; // 超过 GB2312 汉字范围,返回无效地址 } return font_start + index * info->bytes_per_char; } else if (high >= 0xA1 && high <= 0xA9) { // 符号区:如果字库 bin 包含符号区 index = (uint32_t)(high - 0xA1) * 94 + (low - 0xA1); return font_start + index * info->bytes_per_char; } return 0; // 非法编码 }逻辑说明:汉字区 high 从 0xB0 到 0xF7,对应区号 16 到 87。如果是符号区,high 从 0xA1 到 0xA9。这里的 font_start 是索引表里记录的字库起始地址,不是 Flash 的 0 地址。这样设计的好处是,即使我把字库 bin 烧到了偏移位置,只要索引表里的 start_addr 正确,代码不用改。
参数说明:info->bytes_per_char 在每次初始化索引表时已经算好,16x16 是 32,24x24 是 72。如果你的字库存的是 GBK 而非 GB2312,那 high 可以达到 0xFE,上面的范围判断要改。GBK 和 GB2312 的兼容性很迷,我建议直接锁死 GB2312,避免溢出。
4.2 点阵到 LCD 像素:字节位序和旋转问题
拿到 32 字节的 16x16 点阵后,直接把它当作像素数组丢给 LCD 不一定能出正确字形。原因有两个:字节内 bit 顺序可能相反,以及字库取模方向和 LCD 逐行扫描方向不一致。
字节内 bit 顺序:横向取模时,一行的 16 个点用两个字节表示。如果第一个字节的最高位代表最左点,那就是“高位在前”;如果第一个字节的最低位代表最左点,那就是“低位在前”。LCD 驱动 IC 的移位寄存器往往是从低位开始接收,所以需要把字节内 bit 反转。反转用查表最快,或者用下面这种位运算:
uint8_t reverse_byte(uint8_t b) { b = (b & 0x55) << 1 | (b & 0xAA) >> 1; b = (b & 0x33) << 2 | (b & 0xCC) >> 2; b = (b & 0x0F) << 4 | (b & 0xF0) >> 4; return b; }逻辑说明:这是标准的分治位反转。第一行交换相邻 bit,第二行交换相邻双 bit,第三行交换半字节。查表法更快,但 256 字节的 const 表也占用 Flash,我平衡后还是用位运算。
参数说明:如果你的 LCD 驱动 IC 的 MADCTL 参数里已经打开了 Mirror X 或 BGR 翻转,那字节内位序可能不需要反转。正确做法是写一个测试函数,显示“显”字,左边和右边各打印一条竖线的图案,快速判断是横向问题还是纵向问题。
4.3 刷屏性能优化:先读到 RAM 再刷 LCD
最慢的写法是每读一个点阵字节,就向 LCD 控制器写一个包含命令、地址、数据的 3 字节序列。SPI Flash 读和 LCD 写共用同一条 SPI 总线时,还要反复切换片选,效率很低。我一般先把整个汉字的点阵读到一个局部缓存里,再一次性通过 LCD 的“窗口写”命令连续发送数据。
下面是一个 16x16 字符显示函数的骨架:
void lcd_show_char_16x16(uint16_t x, uint16_t y, uint16_t code) { uint8_t font_buf[32]; uint8_t lcd_buf[32]; uint32_t addr = get_font_addr(code, &g_font[0]); w25q_read(addr, font_buf, 32); for (int i = 0; i < 32; i++) { lcd_buf[i] = reverse_byte(font_buf[i]); } lcd_set_window(x, y, x + 15, y + 15); lcd_write_data(lcd_buf, 32); }逻辑说明:lcd_set_window 告诉 LCD 接下来要写入的区域,然后 lcd_write_data 连续发送 32 字节,内部会自动按窗口区域逐行排列。这样就避免每个像素或每个字节都去执行命令和地址,刷屏速度能提升好几倍。
参数说明:这个函数的 32 字节是 16x16 点阵的数据长度,如果换成 24x24,就是 72 字节。lcd_buf 和 font_buf 都要根据最大字号预留,我的工程里把它们定义成 128 字节,兼容 32x32。
5. 避坑/常见问题/排查:字库方案背后的五个典型翻车现场
5.1 汉字左右镜像或者上下颠倒
现象:显示“福”时,笔画变成镜像;显示“正”时,看起来像“丁”反过来。数字和英文字母因为左右对称性好,大部分还能辨认,所以这个故障经常只在中文显示时才被发现。
原因:字库取模方向和 LCD 驱动 IC 的扫描方向不一致。典型组合是:字库用了纵向取模,而 LCD 被设置为横向逐行扫描,于是点阵数据被错位解读,产生镜像或旋转。
解决:先确认 LCD 驱动 IC 的 MADCTL 参数,找到 X Mirror、Y Mirror、Row/Col Exchange 这三个开关。另一种处理方式是软件反转字节内 bit 序或者在点阵读取后做矩阵转置。我倾向于把底层显示函数做成两点:一个是“标准转换”函数,一个是“镜像修正”函数,通过一个编译宏选择,避免在显示逻辑里到处打补丁。
5.2 生僻字显示空白或乱码
现象:显示“喆”“鎏”“龘”这类字,屏幕上是空白,或者显示成另一个完全不相关的字。但常用汉字如“你”“我”“好”都正常。
原因:字库 bin 不是完整的 GB2312 全字符集,只包含一级汉字的 3755 个字。二级汉字区在 GB2312 里高字节范围从 0xD8 到 0xF7,如果你的偏移公式只处理到 0xD7,那么高字节在 0xD8 以上的字会落到空地址或者错位地址。
解决:第一步,用文件大小判断字库是否完整。16x16 全量 6763 字应该是 216416 字节,减去符号区可能更小。如果只有 120160 字节,那就是缺二级汉字。第二步,把偏移计算改成完整的 GB2312 公式,不要自己写简化版。第三步,如果产品确实需要生僻字,可能得考虑 GBK 字库或者 Unicode 映射表,但那个对 Flash 和 RAM 占用更高。
5.3 写入后读回来前面对后面错
现象:在线写入 16x16 字库到某个地址,写完立即读回校验,发现同一页数据的前几十字节正确,后几十字节缺失,读回来全是 0xFF。
原因:页编程跨页。比如地址 0x001F00 处页起始是 0x001F00,页长度为 256 字节,从偏移 0x1F0 开始写 0x30 字节,正好跨到下一个页,Flash 内部会把超出的部分回卷到当前页起始地址,把前面的数据覆盖掉。所以读回来不是“后几十字节正确”,而是正确数据和覆盖数据混在一起,看起来像错位。
解决:写数据前计算当前地址的页偏移,如果剩余长度超过页剩余空间,就拆成多段。我封装 w25q_write 时把这一判断放在循环里,强制 chunk 不超过 256 - (addr & 0xFF)。从那以后,我再也没有出现过页编程跨页问题。
5.4 SPI 读取偶发错位:偶尔某个字节的某一位不对
现象:整屏显示时,大部分汉字正常,偶尔某个汉字出现一两个缺笔画的点。刷新多次,错误点位置固定不变,像是数据本身坏了,但读回来做 CRC 又完全正确。
原因:SPI 时钟配置在较高频率,板子上 MISO 走线过长或杜邦线接触不良,导致采样点落在边沿附近。另一种可能是 SPI 外设配置成了模式 1,而 W25Q128 在模式 0 下采样,两者对时钟边沿的相位理解不一致。
解决:先把 SPI 时钟降到 10 MHz 以下测试,如果错误消失,就确认是信号完整性问题。再把 SPI 模式改成 CPOL 等于 0、CPHA 等于 0。如果还不能根治,给 w25q_read 的读取循环加一个简单的软件 CRC,把读出数据再算一遍和已知校验值比对,但这种办法只能检测不能恢复。我在量产板上最终用硬件 SPI 加短走线解决,杜邦线调式时才会用软件 CRC 兜底。
5.5 误发全片擦除命令,参数区一起归零
现象:程序里写错了一个命令字节,把 0x20 扇区擦除写成了 0xC7 整片擦除。上电后设备找不到字库索引,连校准参数也没了,整块板子变砖。
原因:SPI Flash 的命令很多都是单字节,错一个值就是灾难。尤其是 0xC7 Chip Erase,不需要额外地址,全片擦除只要 0xC7 和前面的写使能。我在调试过程中有过一次手滑,把宏定义里的数字填错,导致上电自动更新字库时把整个 Flash 擦了一遍。
解决:给危险命令加一层编译期保护,比如#define ALLOW_CHIP_ERASE 0,只有在需要整片清空时才改为 1。另一个习惯是把参数区放在最高地址,更新字库时只擦除字库占用的扇区,不要碰参数区。文件覆盖更新前,先把旧参数区读出来备份到 RAM,擦除后写回,等于加了四道保险。
6. 进阶:多字号索引表和 DMA 流水线
当一个界面同时要显示 16x16 的菜单标题、24x24 的数据数值、32x32 的警告图标时,索引表的价值会真正体现。上电后把 font_index_t 全部读进一个全局数组,显示函数根据当前字体类型直接查表拿起始地址,不需要在代码里保留 if 分支判断“第几套字库”。我一般把字号做成一个枚举,显示函数入口先统一查表,再决定点阵缓冲区和字节数。
另一个能明显提升刷新率的手段是 DMA 双缓冲。Flash 读取和 LCD 写屏共用 SPI 时,普通的阻塞式读取会让 LCD 等待。常见做法是:先把一行中的多个字符点阵一次性读进一个缓冲区,再用 DMA 连续写到 LCD。DMA 传输期间,CPU 可以计算下一批字符的地址,两个缓冲区交替使用,等效把 SPI 利用率拉满。具体实现里,我一般把 Flash 读和 LCD 写拆成两个独立线程:一个线程负责从 W25Q128 读点阵,另一个线程负责把点阵搬进 LCD 显示 RAM,中间用环形队列串起来。刷新速度从每秒 15 帧提升到 45 帧以上,视觉上完全是两个产品。
验证这套方案是否可靠,我有一套固定动作:先连续显示全量汉字库,肉眼扫一遍有没有缺字和错位;再对字库区做一次全读 CRC 校验;最后在高温老化条件下跑 8 小时,看有没有偶发错位。从那以后,我每次做字库方案都先画一张地址映射表,把字号、起始地址、字节数、版本号都列清楚,再开始写代码。希望这篇笔记能帮你走完从“字库放哪”到“汉字显示正常”的全流程,省掉那些我已经替你踩过的坑。
本文还有配套的精品资源,点击获取