☰
带字库12864 ST7920自定义字符显示:CGRAM写入不显示的排查与实现
2026/10/14 18:56:44 网站建设 项目流程

简介:这份资源面向嵌入式开发初学者与单片机爱好者,围绕ST7920控制器驱动12864 LCD显示屏展开,重点解决自定义字符、反白显示、字体显示、指定位置显示字符串以及菜单图片绘制等实际问题。压缩包共14个文件,约43KB,以Keil工程文件为主,包含uvproj、uvopt等工程配置,c源码、hex烧录文件、lst与m51编译列表、obj目标文件及若干bak备份,构成一套可直接编译下载的完整示例工程。目前已有1121人学习下载,说明其在12864显示开发中具有一定参考价值。读者可借助源码理解ST7920的SPI通信流程、字符发生器RAM写入方式、位图数据查找表构建以及反白命令切换逻辑,并参考工程结构快速搭建自己的显示程序,适合作为课程设计、毕业设计或嵌入式界面开发的入门范例。

1. 带字库 12864 ST7920 显示自定义字符:为什么你的 CGRAM 写进去却不显示

很多人第一次用带字库 12864(ST7920 控制器)时都会遇到一个反直觉的现象:明明按手册往 CGRAM 里写了自定义字符的点阵数据,屏幕上也留出了位置,可那个位置要么是空白,要么显示的是字库里某个不相干的汉字。更让人抓狂的是,同样的代码换一块屏就好了,或者重启一次又变了。问题通常不在“写没写进去”,而在“写进去之后有没有切回正确的寻址模式、有没有把 DDRAM 地址算对、有没有在扩展指令集和基本指令集之间来回切干净”。带字库 12864 ST7920 显示自定义字符这件事,本质上是三件事的叠加:CGRAM 只有 8 个自定义位、每个字符 16×16 点阵占 32 字节、以及 ST7920 的指令集切换有严格的时序要求。这篇笔记面向正在用 12864 做界面、想塞图标或特殊符号的嵌入式开发者,从指令集切换讲到点阵取模,再到实际能跑通的写入流程和排错方法,尽量把每一步的参数和边界说清楚。

2. ST7920 的两种指令集与 CGRAM 的真实容量

2.1 基本指令集和扩展指令集到底差在哪

ST7920 把指令分成了两套,靠0x30和0x34两个命令切换。基本指令集负责显示开关、光标、清屏、DDRAM 读写;扩展指令集负责图形显示、CGRAM 操作、反白、待机。很多人翻车就翻在这里:写 CGRAM 必须在扩展指令集下进行,但写完如果不切回基本指令集,后续用0x80+addr定位光标就会失效,因为扩展指令集下0x80的含义变成了“设置 GDRAM 垂直地址”,不是 DDRAM 地址。

我一般会在初始化阶段就把两套指令集的切换封装成两个函数,避免在业务代码里到处写裸命令。下面这段是常见的初始化骨架,基于 8 位并口、ST7920 兼容控制器,串口模式只需要把写字节改成移位时序,命令值本身不变。

// ST7920 初始化骨架,8 位并口模式 // RS=0 写指令,RS=1 写数据;这里用宏区分 #define LCD_CMD(cmd) do { RS=0; RW=0; DATA=cmd; E=1; E=0; delay_us(50); } while(0) #define LCD_DAT(dat) do { RS=1; RW=0; DATA=dat; E=1; E=0; delay_us(50); } while(0) void st7920_init(void) { delay_ms(50); // 上电等待,ST7920 对复位时间敏感 LCD_CMD(0x30); // 基本指令集 delay_ms(5); LCD_CMD(0x30); // 再发一次,确保进入 8 位模式 delay_ms(1); LCD_CMD(0x0C); // 显示开,光标关,不反白 LCD_CMD(0x01); // 清屏 delay_ms(10); // 清屏指令需要较长时间 LCD_CMD(0x06); // 光标右移,画面不移动 }

这段代码里delay_us(50)不是随便写的。ST7920 的使能脉冲宽度最小 450ns,但很多模块的排线较长、电平转换芯片有延迟,实际留 50us 更稳。0x30发两次是经典做法,第一次让控制器从可能的串口模式或未知状态回到并口 8 位,第二次才真正锁定。清屏后的delay_ms(10)也不能省,清屏是内部全屏写操作,期间不接受新指令。

2.2 CGRAM 只有 8 个字符,每个 16×16 占 32 字节

带字库 12864 的 CGRAM 容量是 64 字节,按 16×16 点阵算,正好 2 字节一行、16 行、共 32 字节一个字符,所以只能放 2 个自定义字符?不对,这里有个容易混淆的点。ST7920 的 CGRAM 在扩展指令集下,自定义字符的地址范围是0x40到0x7F,共 64 字节。但它的字符显示是 16×16 点阵,每个字符需要 32 字节,所以严格来说只能存 2 个完整的 16×16 自定义字符。那为什么很多人说“8 个自定义字符”?因为 ST7920 同时支持 8×16 半宽字符模式,这种模式下每个字符 16 字节,64 字节正好 4 个;而有些资料把 8×8 点阵的兼容模式也算进去,才出现 8 个的说法。

实际用带字库 12864 做界面时,我建议按 16×16 来规划,只做 2 个自定义字符,或者用 8×16 做 4 个窄图标。下面这张表是我整理的模式对照,选型时先看你要几个图标、每个图标多宽。

点阵模式单字符字节数CGRAM 可存数量适用场景
16×16 全宽32 字节2 个大图标、特殊符号
8×16 半宽16 字节4 个窄图标、单位符号
8×8 兼容8 字节8 个极小标记,显示效果一般

选 16×16 还是 8×16,不只看数量,还要看 DDRAM 的地址步进。16×16 模式下,一个字符占两个 DDRAM 地址(上半字和下半字各一个),写显示内容时地址要跳着写;8×16 只占一个地址,定位简单。新手如果只是想加一个温度符号或电池图标,16×16 做 1 到 2 个就够了,别贪多。

2.3 自定义字符的 CGRAM 地址怎么算

CGRAM 里每个 16×16 字符占 32 字节,地址从0x40开始。第一个自定义字符的起始地址是0x40,第二个是0x60。写点阵数据时,先发0x40设置 CGRAM 地址,然后连续写 32 个字节的数据,每写一个字节地址自动加一。注意这里写的是“数据”,RS 要置 1,不是写指令。

显示的时候,DDRAM 里填的字符码不是0x40,而是0x00和0x02这样的编号。ST7920 规定 CGRAM 自定义字符在 DDRAM 中的编码是:第一个字符用0x00,第二个用0x02,第三个用0x04,依次类推。为什么跳着来?因为 16×16 字符占两个 DDRAM 位置,编码0x00和0x01都指向同一个自定义字符的上半和下半,实际使用时只写0x00就会自动带出下半部分。这个映射关系如果搞错,就会出现“写进去了但显示的是别的字”的现象。

3. 从取模到写入:一个自定义字符的完整落地流程

3.1 用取模软件生成 16×16 点阵的注意事项

取模软件很多,设置上只要盯住四个参数:点阵大小选 16×16、取模方式选“纵向取模、字节倒序”或“纵向取模、高位在前”,具体看你的写入函数怎么解析。ST7920 的 CGRAM 数据是按行写入的,每行 16 个像素分两个字节,第一个字节是上半行,第二个字节是下半行。所以取模输出应该是每行两个字节、共 16 行、32 字节。

我见过最常见的翻车是取模软件选了“横向取模”,结果写进去的图标变成一堆横条。另一个坑是“字节倒序”没勾对,显示出来左右镜像。下面是一个手动构造的 16×16 点阵示例,画的是一个简单的向上箭头,用来验证流程是否跑通。

// 16x16 向上箭头,纵向取模,高位在前 // 每行两个字节:左半行、右半行 const unsigned char arrow_up[32] = { 0x01, 0x00, // 第 1 行 0x03, 0x80, 0x07, 0xC0, 0x0F, 0xE0, 0x1F, 0xF0, 0x3F, 0xF8, 0x7F, 0xFC, 0xFF, 0xFE, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xC0, 0x07, 0xC0, 0x00, 0x00 };

这段点阵里,前 8 行逐渐变宽形成箭头头部,后 8 行是等宽的箭杆。每一行的两个字节分别控制左半 8 列和右半 8 列。写的时候按数组顺序从0x40开始连续写 32 次,不要中间插入其他指令。

3.2 写 CGRAM 的完整命令序列

写一个自定义字符到 CGRAM,必须严格按下面的顺序来。少一步或者顺序错了,数据就丢了。

// 将 arrow_up 写入第一个自定义字符位置(CGRAM 0x40) void lcd_write_cgram(const unsigned char *dots) { unsigned char i; LCD_CMD(0x34); // 切到扩展指令集 delay_us(100); LCD_CMD(0x40); // 设置 CGRAM 地址为 0x40 delay_us(100); for (i = 0; i < 32; i++) { LCD_DAT(dots[i]); // 连续写 32 字节点阵数据 delay_us(50); } LCD_CMD(0x36); // 扩展指令集下打开图形显示(可选) delay_us(100); LCD_CMD(0x30); // 切回基本指令集 delay_us(100); }

逻辑说明:0x34进入扩展指令集,这是写 CGRAM 的前提。0x40设置 CGRAM 起始地址,之后每写一个数据字节地址自动加一,所以不需要手动改地址。32 字节写完后,0x30切回基本指令集,否则后面用0x80+addr定位 DDRAM 会失效。中间的delay_us是给控制器内部操作留时间,尤其是0x34和0x30切换,ST7920 在这两个命令后需要约 72us 的等待,实际留 100us 更保险。

参数说明:dots指针指向 32 字节的点阵数组,顺序必须是先左半行后右半行、从上到下。如果你的取模软件输出的是先右后左,显示会左右分家,需要手动交换每两个字节的顺序。

3.3 在 DDRAM 里定位并显示自定义字符

写进 CGRAM 只是第一步,要让它出现在屏幕上,还得在 DDRAM 对应位置写入字符码0x00。带字库 12864 的 DDRAM 地址和普通 12864 类似:第一行0x80到0x87,第二行0x90到0x97,第三行0x88到0x8F,第四行0x98到0x9F。注意第三行地址不是接着第二行,而是插在0x88,这是 ST7920 的常见坑。

// 在第一行第 3 个位置显示自定义字符(16x16 占两个地址) void lcd_show_custom_at_line1_col3(void) { LCD_CMD(0x30); // 确保基本指令集 delay_us(100); LCD_CMD(0x80 + 2); // 第一行第 3 个位置(从 0 开始计数) delay_us(100); LCD_DAT(0x00); // 写入自定义字符编号 0x00 delay_us(100); // 16x16 字符会自动占用下一个 DDRAM 地址,不需要再写 }

这里0x80 + 2定位到第一行第三个位置,写入0x00后,控制器会自动把下一个地址也当作该字符的下半部分处理。如果你写的是0x02,那就是第二个自定义字符。注意不要在写完0x00后再往下一个地址写东西,否则会把下半部分覆盖掉。

4. 避坑:CGRAM 写入后不显示的 5 个排查方向

4.1 现象:屏幕对应位置空白,但其他字库汉字正常

原因通常是 CGRAM 数据没写进去,或者写进去后又被清屏指令冲掉了。ST7920 的清屏0x01只清 DDRAM,不清 CGRAM,但有些模块在上电初始化时会内部复位 CGRAM。解决方法是把写 CGRAM 的操作放在初始化之后、清屏之后,并且不要在写 CGRAM 后再发0x01。另外检查0x34和0x40之间是否有足够延时,太快会导致地址没锁存。

4.2 现象:显示出来是乱码或某个固定汉字

这多半是 DDRAM 里写的字符码不对。自定义字符的编码是0x00、0x02、0x04,不是0x40。如果你写了0x40,控制器会去字库里找对应的汉字,显示出来当然不对。另一个可能是 CGRAM 地址设成了0x00而不是0x40,数据写到了 DDRAM 区域,把显示内容搞乱了。

4.3 现象:图标左右镜像或上下颠倒

取模软件的取模方向设错了。ST7920 要求纵向取模、高位在前,每行先左半字节后右半字节。如果取模选了横向,或者字节顺序反了,就会出现镜像。解决办法是在取模软件里把“取模方式”改成“纵向取模”,把“字节倒序”勾选状态切换一下试试。也可以写个小函数在运行时交换每两个字节,但最好从源头改。

4.4 现象:只有上半部分显示,下半部分是空白或杂点

16×16 字符占两个 DDRAM 地址,如果你在写入0x00后,紧接着又往下一个地址写了空格或其他字符,下半部分就被覆盖了。检查显示函数里有没有在自定义字符后面多写一个0x20。另外,CGRAM 只写了 16 字节而不是 32 字节,也会导致下半部分没数据。确认点阵数组长度是 32。

4.5 现象:换一块同型号屏就正常,原来那块死活不行

这种“玄学”问题通常是时序余量不够。不同批次的 ST7920 兼容芯片对使能脉冲宽度和指令间隔的容忍度不同。把delay_us(50)加到delay_us(100),把0x34和0x30后的延时加到 200us,往往能救回来。另外检查排线长度,超过 10cm 的并口排线容易导致数据错位,降低写入速度或加缓冲芯片。还有一点,有些模块的 PSB 引脚必须接地才能进并口模式,悬空会进串口模式,表现就是完全没反应。

5. 进阶:用 8×16 模式塞进 4 个图标,以及一个验证写入是否成功的小技巧

当你需要更多自定义符号时,16×16 只能放 2 个就捉襟见肘了。这时候可以切到 8×16 半宽模式,每个字符 16 字节,CGRAM 能放 4 个。8×16 的取模是每行 1 个字节、共 16 行,写入时 CGRAM 地址还是从0x40开始,但每个字符占 16 字节,第二个字符从0x50开始,第三个0x60,第四个0x70。显示时 DDRAM 编码对应0x00、0x01、0x02、0x03,不再跳 2。

下面是一个 8×16 电池图标的点阵示例和写入函数。

// 8x16 电池图标,纵向取模,高位在前 const unsigned char battery_icon[16] = { 0x3C, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x3C, 0x00 }; // 写入第 n 个 8x16 自定义字符,n=0..3 void lcd_write_cgram_8x16(unsigned char idx, const unsigned char *dots) { unsigned char i; unsigned char addr = 0x40 + idx * 0x10; // 每个字符 16 字节 LCD_CMD(0x34); delay_us(100); LCD_CMD(addr); delay_us(100); for (i = 0; i < 16; i++) { LCD_DAT(dots[i]); delay_us(50); } LCD_CMD(0x30); delay_us(100); }

参数说明:idx取 0 到 3,对应 CGRAM 里的四个位置。addr计算是0x40 + idx * 0x10,因为每个 8×16 字符占 16 字节。显示时用LCD_DAT(idx)即可,不需要跳地址。注意 8×16 模式下,DDRAM 里一个字符只占一个地址,所以定位更简单,但显示宽度只有 16×16 的一半,适合做小标记。

验证 CGRAM 是否写入成功,我常用的一个笨办法但很有效:写一个全0xFF的 16×16 点阵进去,显示出来应该是一个实心方块。如果显示的是空白或半截,说明写入流程有问题。然后再写一个全0x00的,应该什么都不显示。这两个极端测试能快速区分是数据问题还是定位问题。另外,在写 CGRAM 之前先读一下 CGRAM 地址处的数据(ST7920 支持 CGRAM 读操作),如果读回来和写进去的一致,说明时序没问题;如果不一致,就是延时不够或指令集没切对。

我自己在这块屏上栽过最久的一次,是忘了在写 CGRAM 后切回0x30,结果后面所有0x80+addr的定位全部失效,显示内容跑到 GDRAM 区域去了,查了一整天才发现是指令集没回来。从那以后,我养成了一个习惯:任何涉及0x34的函数,末尾必须跟一个0x30,哪怕后面马上又要切回去,也不省这一步。希望帮到你。

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

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

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

立即咨询