简介:围绕W25Q128串行闪存存储字库并在LCD上显示汉字的嵌入式实践工程,面向已具备C语言与STM32基础、希望打通SPI通信、字库管理与LCD驱动链路的学习者。工程以Keil项目组织,完整呈现从W25Q128读取字库数据、解析汉字点阵并送至LCD显示的软硬件思路,对字库烧录起始地址0x1000(十进制4096)也有对应说明。压缩包共192个文件、约4.6MB,以36个h头文件、33个c源文件为核心,配合32个o、32个d、31个crf等编译中间文件,以及uvprojx/uvoptx工程配置、map/hex/axf产物、bat清理脚本和txt地址记录,便于直接打开工程对照结构与编译结果。已有1971人学习,适合用作课程设计、毕业设计或嵌入式显示模块的参考底稿,也可据此梳理SPI Flash读写、字库寻址与LCD汉字显示的实现要点。
1. 一块 16MB 的 SPI Flash 装下整套字库:这项目解决什么问题
调一块 240×320 的小屏时踩过一次很典型的坑:MCU 内部 Flash 只剩三十几 KB,把 16×16 点阵的汉字库硬编成 const 数组塞进去,链接器当场报 region overflow。W25Q128 这类 16MB 的 SPI NOR 就是为这种场面准备的——字库整块挪到外部存储,MCU 只留一份读点阵的驱动,汉字照样上屏。这套方案适合做小屏 HMI、仪器面板、老设备改造的人:会配 SPI、能点亮 LCD、看得懂指针,基本就能跑通。真正麻烦的不是接线,而是地址怎么切、字库怎么烧、点阵怎么摆这三件事,下面一条条拆。
2. 硬件分工与地址规划:MCU 管逻辑、Flash 管字库、LCD 管像素
2.1 三个器件各干什么,以及为什么这么选
这套系统的分工非常清楚:MCU 负责取内码、算偏移、发 SPI 命令;W25Q128 负责把点阵字节原样吐出来;LCD 控制器负责把 RGB565 塞进显存。三者之间只有两条数据通路——MCU 到 Flash 的 SPI,MCU 到 LCD 的并口或 SPI。
选型上,MCU 一般用 Cortex-M3/M4 内核的片子,主频 72MHz 起步,SPI 挂在 APB2 上。原因很实际:一屏 240×320 全刷是 76800 个像素点,如果主频太低、又没有 DMA 或并口,刷屏时间会肉眼可见。
字库为什么放外部 Flash 而不是 MCU 内部?内部 Flash 通常 64KB~512KB,一套 HZK16 完整字库(87 区 × 94 位 × 32 字节)就有 261,696 字节,再叠一套 24×24 就彻底放不下了。更要命的是改字库就得重烧整个固件,产品出厂后想补几个生僻字基本没戏。
为什么不用 SD 卡?SD 卡要挂文件系统,上电枚举动辄几百毫秒,工业现场插拔还容易接触不良。SPI NOR 不需要文件系统,给地址就出数据,掉电保存,随机读延迟在微秒级,这才是字库该待的地方。
LCD 这边,如果 MCU 带 FSMC/FMC,优先用 8080 并口 16 位模式,一个像素一次写,刷一屏几十毫秒;引脚紧张就用 SPI 屏,代价是刷全屏要几百毫秒,做文字界面够用,做动画就翻车。
提示:W25Q128 单独占一路 SPI,别和 LCD 共用。共用总线时每次刷屏都要频繁切片选,Flash 的读时序会被挤乱,偶发读到错字节,排查起来非常玄学。
2.2 接线与 SPI 模式确认
W25Q128 是 8 脚封装,真正要接的只有 6 根:
| Flash 引脚 | 接到 MCU | 说明 |
|---|---|---|
| CS | 任意 GPIO(或硬件 NSS) | 低有效,一次命令拉低一次 |
| CLK | SPI SCK | 主机输出 |
| DI(IO0) | SPI MOSI | 主机输出 |
| DO(IO1) | SPI MISO | 从机输出 |
| WP(IO2) | 10kΩ 上拉到 3.3V | 拉低会进写保护 |
| HOLD(IO3) | 10kΩ 上拉到 3.3V | 拉低会进半双工暂停 |
WP 和 HOLD 这两根最容易被忽略。有人图省事直接悬空,结果就是"能读 ID、写不进去",或者读出来数据隔几个字节错一次。我一般直接焊两个 10k 电阻到 3.3V,省心。
SPI 模式选 Mode 0(CPOL=0,CPHA=0):空闲时时钟为低,数据在上升沿采样,MSB 先行。W25Q128 同时支持 Mode 0 和 Mode 3,但 Mode 0 是绝大多数例程的默认值,混用会读到全 0xFF。
时钟速度要克制。F103 的 SPI1 挂 APB2 72MHz,4 分频是 18MHz。芯片手册标称快速读能到 104MHz,但那是 PCB 走线阻抗匹配良好的前提。面包板加杜邦线,线长超过 10cm,18MHz 就开始出玄学问题了,建议直接降到 9MHz 甚至 4.5MHz 调通再往上加。
2.3 16MB 地址空间怎么切
W25Q128 的容量是 128Mbit = 16MB,地址 24 位,范围 0x000000~0xFFFFFF,用标准三字节地址命令就够,不需要开 4 字节地址模式。空间很富裕,但一定要提前规划,否则后面加字号时到处挪数据,改一处崩三处。
我一般是这么切的:
| 起始地址 | 长度 | 内容 |
|---|---|---|
| 0x000000 | 4KB | 参数区:magic、版本号、字号表、校验和 |
| 0x001000 | 4KB | ASCII 8×16 半角字库(95 字符 × 16 字节) |
| 0x010000 | 256KB | HZK16(16×16,32 字节/字) |
| 0x050000 | 576KB | HZK24(24×24,72 字节/字) |
| 0x0E0000 | 1MB | HZK32(32×32,128 字节/字) |
| 0x1F0000 | 剩余 | 图标、图片、日志等资源 |
参数区那 4KB 不是浪费。里面存一个字号表,记录每种字号的基地址、单字字节数、取模方向(逐行/逐列)、高低位顺序。以后换字库只要改参数区,显示函数一行不用动。这算是给未来的自己留的一颗后悔药。
按 4KB 扇区对齐也是刻意的:NOR Flash 擦除的最小单位就是扇区,地址不对齐会导致擦一个字号把隔壁字号的开头一起擦掉。
3. W25Q128 驱动层:读 ID、读一页、擦一个扇区
3.1 先读 JEDEC ID,确认芯片活着
任何 SPI Flash 调试的第一步都是读 ID。这条命令只读不写,不依赖擦除状态,是判断"硬件通没通"最干净的探针。
/* 读 JEDEC ID:0x9F + 3 字节返回,W25Q128 正常应为 EF 40 18 */ uint32_t w25q_read_id(void) { uint8_t cmd = 0x9F; uint8_t id[3] = {0}; W25Q_CS_LOW(); spi_transfer(cmd); /* 只发不收,返回值丢弃 */ id[0] = spi_transfer(0xFF); /* 厂商 ID,W25Q128 是 0xEF */ id[1] = spi_transfer(0xFF); /* 存储类型,0x40 */ id[2] = spi_transfer(0xFF); /* 容量码,0x18 表示 16MB */ W25Q_CS_HIGH(); return ((uint32_t)id[0] << 16) | ((uint32_t)id[1] << 8) | id[2]; }三个返回字节各有含义:最高字节是厂商码,中间是存储类型,最低字节是容量码,0x18 就是 2 的 24 次方,正好 16MB。返回值拼成 32 位只是为了打印方便,实际判断可以只比最低字节。
拿到 0xFFFFFF 说明 MISO 一直是被拉高的,可能是片选没生效、MISO 接错、或者从机根本没供电。拿到 0x000000 则是 MISO 被拉低,通常是短路或者引脚复用没配对。这两个结果指向的问题完全不同,先分清能省很多时间。
3.2 读数据与页编程:256 字节是不能越的硬边界
读命令 0x03 最简单,发完命令和 24 位地址之后,时钟继续打,数据就一个个往外吐,地址自动递增,可以一直读到片尾。
/* 从任意地址读任意长度,内部自动递增,不跨页限制 */ void w25q_read(uint32_t addr, uint8_t *buf, uint32_t len) { W25Q_CS_LOW(); spi_transfer(0x03); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); while (len--) { *buf++ = spi_transfer(0xFF); /* 发哑字节换数据 */ } W25Q_CS_HIGH(); }读没有边界问题。写就不一样了:页编程命令 0x02 一次最多写 256 字节,而且必须落在同一个 256 字节页内。如果从地址 0x0000F0 开始写 32 字节,写到第 16 个字节时地址会回卷到本页开头 0x000000,把前面的数据覆盖掉。这是 NOR Flash 的硬件行为,不是 bug。
/* 写使能:任何写/擦操作之前都必须发一次,发完自动清零 */ static void w25q_write_enable(void) { W25Q_CS_LOW(); spi_transfer(0x06); W25Q_CS_HIGH(); } /* 轮询状态寄存器 SR1 的 bit0(WIP),为 1 表示还在忙 */ static void w25q_wait_busy(void) { uint8_t sr; do { W25Q_CS_LOW(); spi_transfer(0x05); sr = spi_transfer(0xFF); W25Q_CS_HIGH(); } while (sr & 0x01); } /* 写一页,长度被强制截断到本页剩余空间 */ void w25q_page_program(uint32_t addr, const uint8_t *buf, uint16_t len) { uint16_t page_left = 256 - (addr & 0xFF); /* 本页还剩多少字节 */ if (len > page_left) len = page_left; /* 绝不跨页 */ w25q_write_enable(); W25Q_CS_LOW(); spi_transfer(0x02); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); while (len--) spi_transfer(*buf++); W25Q_CS_HIGH(); w25q_wait_busy(); }三个参数要盯住:addr是 24 位绝对地址;buf是待写数据;len会被page_left截断。真要用的时候再包一层循环,让它看起来能写任意长度:
/* 任意长度写:内部循环调 page_program,跨页自动切 */ void w25q_write(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len) { uint16_t n = 256 - (addr & 0xFF); if (n > len) n = len; w25q_page_program(addr, buf, n); addr += n; buf += n; len -= n; } }擦除的最小单位是 4KB 扇区,命令 0x20,同样要先写使能、后等忙。擦完之后整个扇区全是 0xFF。NOR 的特性是只能把 1 写成 0,想把 0 变回 1 必须擦除,所以"覆盖写"在 NOR 上是不存在的,必须先擦后写。
3.3 字库偏移:内码到 32 字节的算术
字库文件本身是一维的字节流,要定位某个汉字,全靠一段算术。GB2312 用区位码编码:高字节减 0xA0 得区码(1~94),低字节减 0xA0 得位码(1~94),汉字从第 16 区开始。
/* 16x16 汉字:内码 -> 字库内字节偏移,返回负数表示不是 GB2312 汉字 */ int32_t hzk16_offset(const uint8_t *gb) { uint8_t qh = gb[0], wh = gb[1]; /* 区码有效范围 0xB0~0xF7,位码 0xA1~0xFE */ if (qh < 0xB0 || qh > 0xF7) return -1; if (wh < 0xA1 || wh > 0xFE) return -1; uint16_t qu = qh - 0xA0; /* 区码,汉字从 16 开始 */ uint16_t wei = wh - 0xA0; /* 位码,1~94 */ /* 完整 HZK16 文件从第 1 区开始,含符号区,故用 qu-1 */ return ((int32_t)(qu - 1) * 94 + (wei - 1)) * 32; }32这个数字来自 16×16 点阵:16 行,每行 16 个像素,每个像素 1 bit,一行 2 字节,共 32 字节。换成 24×24 就是 72 字节(每行 3 字节 × 24 行),32×32 是 128 字节。
那个qu - 1是关键。市面上流通的 HZK16 文件有两种:完整版从第 1 区开始(含 1~9 区的标点符号),共 87 × 94 × 32 = 261,696 字节;精简版从第 16 区开始(只有汉字),共 72 × 94 × 32 = 216,576 字节。用精简版却照着完整版公式算,每个字都会偏移 15 个区,屏幕上显示的全是隔壁的字。血泪经验是:拿文件大小除以 32,得到的就是字符总数,再反推从第几区开始。
另外,qh > 0xF7那个判断不能省。GBK 扩展区的内码高字节可以到 0xFE,如果串口传上来的是 GBK 编码的生僻字,直接算下去会读到字库之外,屏幕上就是一坨乱码。返回 -1 让它显示空格或者方框,反而更好排查。
3.4 烧录:串口分包写入加整片校验
字库文件几百 KB,不可能手敲进 Flash,得有烧录通道。常见做法有三种:一是用烧录器直接对 SPI Flash 编程,速度快但要把芯片吹下来;二是走调试器的手动烧写插件,依赖特定工具链;三是在固件里留一个串口烧录模式,上位机分包发、MCU 收一包擦一扇区写一扇区。第三种最通用,现场升级也靠它。
上位机这边用 Python 写几十行就够:
# 把 HZK16.bin 按 4KB 扇区切块,通过串口发给 MCU, # MCU 端落到 W25Q128 的 0x00010000 起始处 import serial, struct SECTOR = 4096 BASE = 0x00010000 data = open("HZK16.bin", "rb").read() print("字库大小 %d 字节,共 %d 个字符" % (len(data), len(data) // 32)) ser = serial.Serial("COM3", 115200, timeout=3) for i in range(0, len(data), SECTOR): chunk = data[i:i + SECTOR].ljust(SECTOR, b"\xFF") # 不足一扇区补 0xFF addr = BASE + i # 帧头 0x5A + 4 字节地址 + 4 字节长度 + 数据 + 帧尾 0xA5 frame = b"\x5A" + struct.pack("<II", addr, len(chunk)) + chunk + b"\xA5" ser.write(frame) if ser.read(1) != b"\x06": # 等 MCU 回 ACK raise SystemExit("第 %d 扇区写入失败" % (i // SECTOR)) print("烧录完成")ljust那一步是必须的:最后一个扇区往往不满 4KB,补齐 0xFF 之后再擦写,才能保证扇区里没有残留的旧数据。
MCU 端收到一帧后要做四件事:擦除该地址所在扇区(0x20)、写使能、分包页编程、回 ACK。烧完之后一定要读回来校验,最省事的办法是读整个字库的前 32 字节和最后 32 字节,跟本地文件比对,再全片算一个累加和。校验不过就整片重烧,别指望它"下次就好了"。
4. 汉字上屏:从 32 字节点阵到 LCD 像素
4.1 取点阵:一次读 32 字节,别一个 bit 读一次
显示一个汉字的第一步,是把它那 32 个字节从 Flash 里取出来。这里最常见的性能翻车点,是有人在双层循环里对每个像素都调一次w25q_read,一个汉字 256 个像素就发 256 次 SPI 事务,一屏 20 个字就是 5000 多次片选翻转,刷屏慢到肉眼可见。
正确做法是先整块读到 RAM 缓冲区,再在缓冲里遍历:
#define HZK16_BASE 0x00010000UL int lcd_load_hz16(const uint8_t *gb, uint8_t *dot) { int32_t off = hzk16_offset(gb); if (off < 0) return -1; /* 非汉字,交给 ASCII 分支 */ w25q_read(HZK16_BASE + off, dot, 32); /* 一次 32 字节,一次片选 */ return 0; }HZK16_BASE就是第 2 章地址表里定的基址,改字号只改这个宏。dot是调用者传进来的 32 字节缓冲,别在函数里用 static 局部数组——中断里也可能要刷字,共用一块缓冲会互相覆盖。
4.2 逐位展开与窗口优化
点阵到手之后就是位操作。约定是"逐行式、高位在前、1 表示亮点",第 row 行的两个字节,高字节管左边 8 列,低字节管右边 8 列。
/* 在 (x,y) 处画一个 16x16 汉字 */ void lcd_show_hz16(uint16_t x, uint16_t y, const uint8_t *gb, uint16_t fc, uint16_t bc) { uint8_t dot[32]; if (lcd_load_hz16(gb, dot) != 0) return; /* 开一个 16x16 的显示窗口,之后连续写显存即可,地址自动递增 */ lcd_set_window(x, y, x + 15, y + 15); for (int row = 0; row < 16; row++) { uint8_t hi = dot[row * 2]; uint8_t lo = dot[row * 2 + 1]; for (int col = 0; col < 16; col++) { uint8_t bit; if (col < 8) bit = (hi >> (7 - col)) & 0x01; else bit = (lo >> (15 - col)) & 0x01; lcd_write_color(bit ? fc : bc); /* RGB565,前景/背景 */ } } }fc和bc都是 RGB565 的 16 位色值,比如白色 0xFFFF、黑色 0x0000、蓝色 0x001F。lcd_set_window对应 ILI9341 的 0x2A(列地址)和 0x2B(行地址)两条命令,设完之后连续发 0x2C 就能一个像素一个像素往里灌,不用每次重设坐标。
窗口这条优化很值钱。不开窗口的话,每个像素都要发一次设坐标命令,256 个像素就是 512 次命令开销;开了窗口,256 次纯数据写,速度快三倍以上。
4.3 字符串循环与中英混排
单个汉字能画了,接下去要把一串内码跑完。判断依据很简单:GB2312/GBK 的内码高字节第 7 位一定是 1,ASCII 一定是 0。
/* 在 (x,y) 起画一串以 '\0' 结尾的 GB2312/GBK 字符串 */ void lcd_draw_text(uint16_t x, uint16_t y, const char *s, uint16_t fc, uint16_t bc) { while (*s) { if ((uint8_t)*s & 0x80) { /* 高字节 bit7=1,双字节 */ lcd_show_hz16(x, y, (const uint8_t *)s, fc, bc); x += 16; s += 2; /* 汉字占两个字节 */ } else { /* 半角 ASCII */ lcd_show_ascii(x, y, *s, fc, bc); x += 8; /* 8x16 字模占 8 像素宽 */ s += 1; } if (x + 16 > LCD_W) { /* 到行尾就换行 */ x = 0; y += 16; } } }x += 16和x += 8这两个步进值,分别对应 16×16 汉字和 8×16 半角字符的宽度。中英混排时按各自的宽度推进,字距才对得齐。如果嫌挨得太紧,把步进各加 1~2 像素做字间距。
换行判断用的是x + 16,按最宽字符算,避免最后一个汉字被切掉半个。y += 16是行高,如果字号是 24 或 32,这里要跟着改成对应的字高。
真正做产品时,这一层通常还会再包一层:先用lcd_set_window把整行区域开出来,再把这一行所有字的点阵先在 RAM 里拼好,最后一次性 DMA 灌进显存。文字滚动、菜单高亮这类需求,全靠这层缓冲撑着。
5. 避坑与排查:读不到 ID、字是乱码、屏上花屏的现场记录
5.1 通信层:ID 读不到、写进去读出来全是 0
现象一:w25q_read_id()返回 0xFFFFFF,串口打印出来三个 FF。
原因是 MISO 线上一直没有从机响应,被上拉电阻拉住了。我遇到过三种具体情况:片选脚配成了推挽输出但初值写成低,芯片一直处于被选中状态,第一条命令就错位;SPI 的 GPIO 复用模式没配成复用推挽,SCK 根本没波形;还有一种最隐蔽的,WP 和 HOLD 悬空,芯片进了保护态,命令能收但数据线不驱动。
解决办法是按顺序查:先用示波器或逻辑分析仪看 SCK 上有没有 8 个时钟,有波形说明主机侧没问题;再量片选在发命令期间是不是低电平;最后确认 WP(IO2) 和 HOLD(IO3) 有没有上拉到 3.3V。这三步做完,九成的 ID 问题就定位了。
现象二:ID 读得到,写进去之后读回来全是 0x00。
NOR Flash 只能 1 写 0,不擦除直接写,写进去的位只会越来越少,最后变成全 0。另一种情况是写使能没发——每次页编程和扇区擦除之前都必须单独发一次 0x06,而且它只对紧随其后的那一条命令生效,做完自动清零,不能"发一次管一片"。
解决办法是在w25q_page_program里强制先擦后写:算出地址所在扇区基址(addr & 0xFFFFF000),调一次 0x20,等忙,再写。同时打印状态寄存器 SR1,bit1(WEL)为 0 说明写使能没生效,bit0(WIP)一直为 1 说明芯片卡在忙状态。
5.2 数据层:字是乱码、整体偏一个字、写一半丢一半
现象三:屏幕上每个字都能显示出点阵,但没有一个是对的,全是乱码方块。
八成是偏移公式和字库文件对不上。常见错法有三种:把内码直接当区码用(少减了 0xA0);用了精简版字库但公式按完整版算(差 15 个区);取模软件设的是"逐列式"或"低位在前",而显示代码按"逐行式、高位在前"解。
解决办法是拿一个已知字做定点验证:取"啊"字,它的 GB2312 内码是 B0A1,区码 16、位码 1,完整字库里的偏移应该是(16-1)*94 + 0 = 1410,乘 32 得 45120。用串口打印出这个位置的 32 个字节,和取模软件里"啊"字的预览对比,一致就说明算对了,不一致就从公式和取模设置两头查。
现象四:字符串前半段正常,后半段突然变成一片空白或者乱码。
这是跨页写的典型症状。字库烧录时如果没做页边界处理,一次写超过 256 字节,地址会回卷覆盖本页开头,被覆盖的那部分读出来就是错的。表现很有规律:每隔 256 字节出现一段坏数据,对应到屏幕上大概是每 8 个汉字坏一次。
解决办法是烧录端强制按 256 字节对齐切包,或者直接用w25q_write那层封装让它自动切。烧完之后一定要读回校验,最省事的是全片算累加和,和本地文件的累加和比一次,比逐字节比对快得多。
5.3 显示层:花屏、错位、坐标越界
现象五:字能显示,但位置对不上,或者右边被切掉一截、下面多出一条横线。
三个原因按概率排:一是 LCD 窗口设置和取模方向不一致,屏幕是横屏模式(MADCTL 里 MX/MY 位改了),代码却按竖屏坐标算,结果行列互换;二是坐标越界,x + 15超过了屏幕宽度,控制器自动回卷到下一行,看起来就是"字跑到别的地方去了";三是显存里残留了上一帧的数据,新窗口没覆盖到的地方还是旧的。
解决办法是在lcd_show_hz16开头加一句越界检查,if (x + 16 > LCD_W || y + 16 > LCD_H) return;,宁可画不出来也别画错位置。刷屏前用背景色填一遍窗口区域,别指望上一次的内容自动清掉。横竖屏切换时,把所有坐标宏集中在一个头文件里改,别散落在各处。
还有一个不那么容易复现的:上电第一次显示正常,运行几分钟后偶发花屏。这种多半是 SPI 时钟太快加上杜邦线太长,信号完整性不行。先把时钟从 18MHz 降到 9MHz 观察,如果问题消失,就说明是时序裕量不够,硬件该改板改板,软件该降频降频。
6. 二次开发:多字号、编码转换与字库在线升级
这套东西跑通之后,真正花时间的都在"加需求"上。我把几个改造方向按投入产出比排一下。
| 改造项 | 难度 | 改动位置 | 收益 |
|---|---|---|---|
| 增加 24×24 字号 | 低 | 地址表 + 偏移公式的字节数 | 标题和正文区分层次 |
| UTF-8 输入转 GBK | 中 | 显示函数入口 | 兼容网络下发和 JSON 配置 |
| 点阵 RAM 缓存 | 中 | lcd_load_hz16前加一层 | 常用字重复读,刷屏快 2~3 倍 |
| 字库在线升级 | 高 | 参数区 + 烧录协议 | 出厂后补字、换字体不用返修 |
多字号是最容易上手的。hzk16_offset里那个32抽成变量,参数区里存一张表记录每种字号的基址和单字字节数,显示函数多传一个字号参数就行。注意 24×24 是每行 3 字节,逐位展开的循环要改成三层,别直接拿 16 的代码复制粘贴。
编码转换这块值得单独说。现在上位机、云端、配置文件基本都是 UTF-8,而字库是 GB2312 索引,中间必须转一道。我一般的做法是在显示函数入口加一个判断,检测到 UTF-8 多字节序列就查表转成 GB2312 双字节,再进原来的流程。转换表只做常用 3500 字,能压到 20KB 左右,放内部 Flash 完全没问题。生僻字转不出来就显示方框,别硬算偏移,否则 0xF7 以上那一片会读到字库外面去。
字库在线升级要谨慎。我的做法是把字库分成两个区,A 区在用、B 区写入,写完校验通过再改参数区里的有效区标志,最后擦掉旧区。中途掉电最多是升级失败,不影响当前正在用的那一份。校验别用简单累加和,加一个 CRC32 更稳。
验证方法我固定成三步:先写一个自检函数,读参数区 magic、读 JEDEC ID、抽查字库首尾各 32 字节并和本地文件比对;再跑一遍全字符遍历,把 6763 个汉字依次显示并检查偏移是否连续;最后做一次断电重启循环,至少 20 次,确认每次上电都能正常出字。从那以后我每次改字库地址或者换字号,都强制走一遍这三步,不再靠"看起来没问题"糊过去。
希望帮到你。
本文还有配套的精品资源,点击获取