12864液晶屏汉字显示全攻略:ST7920驱动与C51代码实践
2026/9/8 14:10:05 网站建设 项目流程

简介:一份面向单片机与嵌入式初学者的液晶12864显示完整工程资源,聚焦在12864点阵屏上同时显示汉字、字母和数字,适合智能家居、工业控制及教学实验等场景。资源共17个文件,压缩包约60KB,包含C源程序(.c)、Keil工程(.uv2)、Proteus仿真电路(.dsn)、启动文件(.a51)以及可直接烧录的hex固件,从源码到仿真再到烧录文件一应俱全,尤其适合课程设计与自学练手。已有2903人学习,说明该主题实用且具参考价值。通过这套资料,读者既能学习用C语言控制液晶屏的方法,也能熟悉Keil与Proteus的联合调试流程,理解汉字点阵取模、字符显示和底层寄存器操作等关键环节;借助仿真文件还可以在无硬件条件下先行验证效果,降低入门门槛,是动手实践嵌入式显示技术的实用素材。 12864液晶屏大概是我玩单片机以来经手最多的一块屏。从大学焊第一块学习板开始,到后来做温湿度计、电压电流表、小控制面板,前后过手了不下二十块不同的12864模块。要说显示汉字,它确实担得起“绝对经典”四个字——不用取模、不用外挂字库芯片、不需要高深驱动,往控制器里写两个字节就能出一个汉字。这个标题勾起不少老开发者的回忆,但对刚入门的朋友来说,12864仍然是学习汉字显示成本最低、资料最全的一块屏。

这篇文章我把这几年的使用经验整理一遍,从选型、接线、底层原理到可以直接抄的C51代码全部讲清楚,顺便把我踩过的坑也交代一下。内容适合刚点亮1602、正打算让屏幕显示中文的初学者,也适合要在新项目里快速接入12864的工程师。

1. 经典与选型:为什么新手的第一块大屏还是它

1.1 一块自带中文字库的屏,省掉多少事

市面上绝大多数的12864模块,核心控制器是ST7920。这块芯片厉害的地方在于内部直接固化了8192个常用汉字和128个字符的点阵,单片机只要按GB2312编码把汉字的机内码送进显示RAM,控制器自己就会去字库里把点阵取出来,映射到屏幕上。

这意味着什么?对比一下就明白了。用1602显示英文字符没问题,但想显示汉字就得自己做字模、写CGRAM;用OLED显示汉字,要么用取模软件生成点阵数组,要么移植字库文件,光预处理就够折腾一阵。而ST7920方案的12864,一个LCD_WriteData("温")就完了,剩下的事交给屏本身。

1.2 跟其它常见屏放一起看

显示器件是否自带中文字库显示字符量图形能力参考价格驱动难度
1602字符屏2行16列ASCII几元
12864(ST7920)4行8个汉字或16个ASCII有GDRAM可绘图十几到二十几元
0.96寸OLED汉字需取模十元左右
3.5寸TFT一般需字库方案很强几十到上百元

从成本、信息量、驱动难度三个维度看,12864在“汉字显示”这个场景里几乎是绕不开的中间选项。屏幕小、价格低、一行能放下8个汉字,对设备状态、菜单信息这类内容,4行完全够用。

1.3 它到底适合做什么

我的实际经验是:所有以中文菜单、状态提示、参数设置为主界面的低成本设备,都很适合用12864。比如温湿度记录仪、电池容量测试仪、小型变频器操作面板、课设里的智能家居终端。相比OLED,它的显示区域大得多,考虑到息屏效果和可视角度,在室内固定设备上反而更实用。缺点是ST7920的刷新速度偏慢,不适合跑动画或者高频刷新数据曲线,这一点后面讲图形模式时再细说。

2. ST7920显示汉字的两大底层机制:DDRAM地址映射与GB2312编码

2.1 先搞懂DDRAM是什么

DDRAM(Display Data RAM)是ST7920里的显示数据RAM。文本模式下,往DDRAM写入的是字符编码,不是点阵。控制器每隔一段时间扫描DDRAM,把里面的编码翻译成CGROM字库里的点阵,再显示到屏上。所以我们在代码里看到的“写数据”,本质上是在填一张二维编码表。

DDRAM地址决定了这个字符落在屏幕的哪个位置。ST7920的地址空间并不是按第一行0x80、第二行0x90、第三行0xA0、第四行0xB0这样线性排下去的,很多初次接触的人在这里栽过跟头。

2.2 行首地址要按实际模块验证

我手上这批ST7920模块,经过满屏测试后确认的行首地址是:

#define LCD_LINE1 0x80 #define LCD_LINE2 0x90 #define LCD_LINE3 0x88 #define LCD_LINE4 0x98

也就是说第三行的地址不是0xA0,而是0x88。这个现象我第一次遇到时也很奇怪,后来查资料才明白,ST7920内部DDRAM的行扫描映射不是“从上到下按地址连续排列”,而是存在硬件上的折行关系。网上还有一种说法用0xA0、0xB0做三四行地址,在某些模块上也确实能用。我的建议是拿到屏先写一个满屏测试程序,把四排地址全打一遍,记住自己手上模块的真实映射,以后就不会纠结了。

行内地址怎么算?如果是纯ASCII显示,一行16个半角位置,地址从行首开始加0到15;如果是纯汉字显示,一行8个全角位置,地址加0到7。后面写坐标换算函数时,就是把行首地址和列偏移相加。

2.3 全角、半角与地址递增

ST7920的字符宽度分两种:半角字符(ASCII、数字、字母)占8个像素宽,写入一个字节后,内部地址计数器AC会自动加1;全角字符(汉字)占16个像素宽,写入两个字节后,AC自动加2。

所以在安排显示布局时要记住:一行16个半角位置,最多显示8个汉字。如果混排,比如“温度25.6C”,在坐标上“温度”占4个半角位置,“25.6C”占5个,一共9个,这样的排列是合法且可控的。

2.4 汉字编码与ST7920的字库机制

ST7920内嵌的GB2312字库覆盖了常用简体汉字。汉字机内码是两字节,高字节范围0xB0到0xF7,低字节范围0xA1到0xFE,两字节都大于0x80。控制器收到一个大于等于0x80的字节时,就知道接下来要组合一个全角汉字,会等到第二个字节后一起译码。

这也解释了为什么在Keil C51里,直接写LCD_ShowStr(0, 0, "你好12864")就能正常显示:Keil的字符串常量默认按本地ANSI编码保存,而ANSI在简体中文系统下就是GB2312,正好和ST7920对上。但到了STM32、GCC这类环境下,源文件默认编码是UTF-8,直接发中文字符串就会花屏,这个问题我放在第四章的翻车点里细讲。

3. 驱动代码实操:从GPIO连接到初始化时序,直接抄的C51写法

3.1 引脚定义与接线

以最常见的20Pin 12864模块为例,关键引脚如下:

引脚名称说明接法
1VSS电源地GND
2VDD电源正5V或3.3V按模块
3V0对比度调节10K电位器中点
4RS(CS)并口指令/数据选择MCU IO
5RW(SID)并口读/写选择MCU IO
6E(SCLK)并口使能MCU IO
7-14DB0-DB78位数据总线MCU P0/P2等
15PSB并口/串口选择并口接高电平
17RST复位可悬空或接IO
19BLA背光正串限流电阻接电源
20BLK背光负GND

一个非常容易忽略的点:V0对比度脚如果直接接地,屏幕可能黑成一块或者白成一块,拧电位器也救不回来。正确做法是V0接10K电位器的中间抽头,两端分别接VDD和GND,这样点亮后可以慢慢调出清晰的字。

3.2 初始化时序:为什么是这三遍0x30

ST7920上电后,内部状态机需要时间稳定。数据手册给出的初始化套路是先连发三次0x30,然后依次关显示、清屏、设输入模式、开显示。第一次做时我也纳闷,0x30发一次不就行了吗?后来看手册才明白,上电瞬间电源还没稳定,控制器可能还在内部复位中,前两条命令很可能被吞掉。重复发送是保证至少有一条命令被正确接收。

void LCD_Init(void) { LCD_DelayUs(50000); // 上电等待50ms LCD_WriteCmd(0x30); // 8位数据,基本指令集 LCD_DelayUs(5000); LCD_WriteCmd(0x30); LCD_DelayUs(5000); LCD_WriteCmd(0x30); LCD_WriteCmd(0x08); // 显示关闭 LCD_WriteCmd(0x01); // 清屏 LCD_WriteCmd(0x06); // 写入后地址自动加1,画面不移动 LCD_WriteCmd(0x0C); // 显示开、光标关、闪烁关 }

这里的0x30不只是“8位接口模式”,它同时把控制器切到基本指令集。后面想进图形模式的话,用的是扩展指令集0x36,两者不能混。

3.3 写命令和写数据的底层逻辑

ST7920并口写的时序很简单:RS决定写入的是指令还是数据,RW拉低表示写,E脚产生一个下降沿锁存数据。顺序是先把数据放到总线上,再拉高E,延时一下,再拉低E。E下降沿才是关键动作。

void LCD_WriteCmd(uchar cmd) { LCD_RS = 0; // 指令模式 LCD_RW = 0; // 写 LCD_DATA = cmd; LCD_E = 1; LCD_DelayUs(10); LCD_E = 0; // 下降沿锁存 LCD_DelayUs(50); // 等待命令执行 } void LCD_WriteData(uchar dat) { LCD_RS = 1; // 数据模式 LCD_RW = 0; LCD_DATA = dat; LCD_E = 1; LCD_DelayUs(10); LCD_E = 0; LCD_DelayUs(50); }

关于延时,这里直接用固定延时的原因很简单:对初学者来说最省事,不会出现忙标志读取时序不对导致死等的问题。ST7920执行指令需要时间,清屏指令0x01耗时最长,实测约1.6ms,其它指令几十微秒就能完成。所以写命令后至少给50us,清屏后给2ms,实际项目都这么做。

3.4 串行模式与并行模式怎么选

ST7920支持8位并口、4位并口和串行SPI三种模式。8位并口要占11个IO口,很多小封装单片机根本挤不出来。串行模式只占3个IO口,分别是CS(对应RS脚)、SID(对应RW脚)、SCLK(对应E脚),把PSB拉低即可。

串行模式下写指令和数据有点不一样,不是直接往总线丢,而是先发一个5字节起始串0xF8或0xFA,然后把数据拆成高低两个4位,分别用5字节格式发出去。代码长一些,但逻辑不复杂。我在小资源项目里常用串行模式,后面第四章示例代码用并口写,方便新手理解原理,需要串口的可以直接照这个思路改。

4. 汉字、字母数字混合显示:坐标换算、完整示例与三种常见翻车点

4.1 坐标换算:一个函数搞定

有了行首地址,坐标定位函数就很简单。x表示列,单位是半角位置;y表示行号,范围0到3:

void LCD_SetPos(uchar x, uchar y) { uchar code row[] = {0x80, 0x90, 0x88, 0x98}; LCD_WriteCmd(row[y] + x); }

这里有个细节:如果y是2(第三行),行首是0x88,x加0到7刚好落在0x88到0x8F之间。如果x按半角算,一行最多加到15,也就是0x88+15=0x97,这已经超出第三行范围了。所以调用时要自己做约束:纯汉字x取0到7,纯ASCII x取0到15。

4.2 完整示例:中文标题加字母数字加变量

下面是一段完整的C51示例,实现在第一行显示中文标题,第二行显示“温度”和实时数字:

#include <reg52.h> #define uchar unsigned char #define uint unsigned int sbit LCD_RS = P2^6; sbit LCD_RW = P2^5; sbit LCD_E = P2^7; #define LCD_DATA P0 void LCD_DelayUs(uint us) { while (us--) { _nop_(); } } void LCD_WriteCmd(uchar cmd) { LCD_RS = 0; LCD_RW = 0; LCD_DATA = cmd; LCD_E = 1; LCD_DelayUs(10); LCD_E = 0; LCD_DelayUs(50); } void LCD_WriteData(uchar dat) { LCD_RS = 1; LCD_RW = 0; LCD_DATA = dat; LCD_E = 1; LCD_DelayUs(10); LCD_E = 0; LCD_DelayUs(50); } void LCD_Init(void) { LCD_DelayUs(50000); LCD_WriteCmd(0x30); LCD_DelayUs(5000); LCD_WriteCmd(0x30); LCD_DelayUs(5000); LCD_WriteCmd(0x30); LCD_WriteCmd(0x08); LCD_WriteCmd(0x01); LCD_DelayUs(2000); LCD_WriteCmd(0x06); LCD_WriteCmd(0x0C); } void LCD_SetPos(uchar x, uchar y) { uchar code row[] = {0x80, 0x90, 0x88, 0x98}; LCD_WriteCmd(row[y] + x); } void LCD_ShowStr(uchar x, uchar y, uchar code *str) { LCD_SetPos(x, y); while (*str) { LCD_WriteData(*str++); } } void LCD_ShowUint(uchar x, uchar y, uint num, uchar len) { uchar buf[6]; uchar i; for (i = 0; i < len; i++) { buf[len - 1 - i] = num % 10 + '0'; num /= 10; } buf[len] = '\0'; LCD_ShowStr(x, y, buf); } void main(void) { uint temp = 256; // 实际是25.6℃的整数部分显示 LCD_Init(); LCD_ShowStr(0, 0, "绝对经典12864"); LCD_ShowStr(0, 1, "温度:"); LCD_ShowUint(8, 1, temp / 10, 2); LCD_ShowStr(10, 1, "."); LCD_ShowUint(11, 1, temp % 10, 1); LCD_ShowStr(12, 1, "C"); while (1); }

这里LCD_ShowStr的形参用了uchar code *,是因为Keil C51里字符串常量默认存储在CODE区。如果写成uchar *str而实参传字符串常量,编译时会有指针空间类型不匹配的问题,显示的字符串也可能乱掉。这个细节很多教程没提,卡住过不少人。

数字显示函数的核心思路是取余、取商、加'0'转ASCII。注意sprintf在C51里很占资源,我一般自己拼,省ROM也更快。

4.3 三种常见翻车点

第一个坑是源文件编码。Keil C51里直接写"温度"没问题,是因为编辑器保存成了GB2312(ANSI)。如果你把源文件保存成了UTF-8,哪怕代码一个字不改,显示也是花的。解决办法是编辑器里把文件编码改成ANSI再重新输入中文。

第二个坑是STM32/GCC环境。源文件默认UTF-8,一个汉字在UTF-8里是3个字节,而且每个字节都可能大于0x80。ST7920不知情,会把3个字节拆来拆去,最后全是乱码。解决方案有两种:一是用"\xC4\xE3\xBA\xC3"这种转义序列写死GB2312内码;二是把要显示的文本集中放到一个转码脚本里,编译前统一处理。

第三个坑是中文和ASCII混排时的坐标估算。如果一行要显示8个汉字加几个字母,我习惯先把所有字符按半角宽度数一遍,算好总数不超过16,再决定起始x。比如“温度:25.6C”共9个半角宽,从x=0开始;如果写成“传感器状态:正常”,“传感器状态:”是7个汉字加一个冒号共15个半角宽,“正常”两个汉字需要4个半角宽,加起来19,已经超出一行,必须分行显示。

5. 进阶玩法:图形区、自定义字符与实用界面技巧

5.1 ST7920的GDRAM绘图能力

12864叫12864,是因为点阵是128列乘64行。ST7920内部除了DDRAM,还有一块图形显示RAM(GDRAM),可以把整屏当作点阵来操作。进入图形模式的方法很简单:写0x36进入扩展指令集并开启图形显示,之后设置GDRAM地址,写入16位数据。

LCD_WriteCmd(0x36); // 开启绘图 LCD_WriteCmd(0x80 | y); // 设置垂直地址 0~31 LCD_WriteCmd(0x80 | x); // 设置水平地址 0~63 LCD_WriteData(dat >> 8); // 高8位 LCD_WriteData(dat & 0xFF); // 低8位

注意GDRAM一个地址对应16列点,所以水平地址范围是0到63,而不是128。要画一个像素点,得先算出它落在哪一组16列,再把对应的16位数据读回来修改某一位,最后写回。这种“读-改-写”在ST7920上比较麻烦,因为并口模式下读GDRAM要先假读一次。所以我的建议是:简单图形(进度条、分隔线、简单曲线)可以在GDRAM里直接填固定数据;复杂图形不如换OLED或TFT。ST7920的图形模式更多是应急用的。

5.2 CGRAM自定义字符

基本指令集下写0x40 | 地址可以进入CGRAM,把自定义点阵写到字符生成RAM里。ST7920的CGRAM空间能放4个16乘16的全角字符或者8个16乘8的半角字符,适合做箭头、电池图标、单位符号这类特殊图形。

实际项目中,我常常用自定义字符拼进度条。先定义几个不同填充程度的半角字符,显示时按百分比选择字符,效果比逐点画图简单稳定,也不影响旁边文字显示。

5.3 实用界面布局技巧

把菜单文字集中到表驱动里管理,是后期维护最省心的方法。把所有要显示的内容按行号、列号、字符串组织成一个结构体数组:

struct MenuItem { uchar x; uchar y; uchar code *text; }; void LCD_ShowMenu(struct MenuItem *menu, uchar num) { uchar i; for (i = 0; i < num; i++) { LCD_ShowStr(menu[i].x, menu[i].y, menu[i].text); } }

改界面文案时只需要改数据表,不用动显示逻辑。这种做法在做多级菜单、多语言切换时非常实用,也是我用12864多年后最想推荐给新手的一个习惯。

6. 实测踩坑:从花屏到地址错乱,我排过的四个真问题

6.1 上电不显示或满屏黑块

这个问题的排查顺序很固定:先看V0对比度电位器是否调到合适位置,再看背光是否点亮,然后用万用表量VDD与VSS之间有没有正常电压。如果都正常,检查RST引脚,有的模块RST悬空时内部上电复位不可靠,需要用IO口给一个低脉冲复位。再不行就增大初始化前面的延时,把上电等待从50ms改成200ms。我遇到过一块屏就是上电太快导致内部复位没完成,前几次初始化全失败。

6.2 清屏指令后内容莫名其妙消失

0x01清屏指令需要的时间比普通指令长很多。刚开始写驱动时,我清屏后马上显示字符串,结果经常只有后半段能显示出来。后来查手册,发现0x01的执行时间在1.6ms左右,而普通指令只要几十微秒。我的代码里在清屏后加了一个LCD_DelayUs(2000),问题就没了。这个坑很隐蔽,因为大部分例子都只写LCD_WriteCmd(0x01),没提后面要延时,很多新手会直接复制。

6.3 第三行地址总是跳错

我第一次用ST7920做四行菜单时,第三行怎么都显示不对,内容跑到第一行后半截。折腾了半天,最后才意识到ST7920第三行地址是0x88,不是0xA0。这个事给我最大的教训是:不要凭直觉猜地址,拿到屏先写一个循环,把0x80到0xBF全部打一遍字符,肉眼看到底哪些地址对应屏幕哪些位置,然后再写正式代码。一次实测胜读十遍手册。

6.4 串行模式下数据发太快导致丢字

用串行模式驱动12864时,CS、SID、SCLK三个引脚是模拟SPI,每个字节要拆成高低4位发两遍。一开始我图快,严格按照芯片手册的最小时间写,结果偶尔出现某一行少字或者乱码。后来在每发送一个字节后加了几十微秒延时,问题消失。串行模式的本质是低速通信,ST7920内部处理速度有限,宁可多等一点,也不要为了“看起来高效”牺牲稳定性。

一点个人体会:ST7920这块控制器确实老了,速度慢、绘图弱、背光也远不如LED均匀,但它有一个后来屏很难替代的优点——资料多、稳定、调试成本低。凡是中文菜单为主的低成本设备,我的第一选择仍然是12864。最后再分享一个小技巧:新屏到手,先写个满屏测试程序把四行地址全跑一遍,记下自己模块的真实映射,这个习惯能帮你省下后面无数个排错小时。

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

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

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

立即咨询