尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

12864液晶屏汉字显示全攻略:ST7920驱动与C51代码实践

12864液晶屏汉字显示全攻略:ST7920驱动与C51代码实践 简介一份面向单片机与嵌入式初学者的液晶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无几元低12864ST7920是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是什么DDRAMDisplay 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电源地GND2VDD电源正5V或3.3V按模块3V0对比度调节10K电位器中点4RSCS并口指令/数据选择MCU IO5RWSID并口读/写选择MCU IO6ESCLK并口使能MCU IO7-14DB0-DB78位数据总线MCU P0/P2等15PSB并口/串口选择并口接高电平17RST复位可悬空或接IO19BLA背光正串限流电阻接电源20BLK背光负GND一个非常容易忽略的点V0对比度脚如果直接接地屏幕可能黑成一块或者白成一块拧电位器也救不回来。正确做法是V0接10K电位器的中间抽头两端分别接VDD和GND这样点亮后可以慢慢调出清晰的字。3.2 初始化时序为什么是这三遍0x30ST7920上电后内部状态机需要时间稳定。数据手册给出的初始化套路是先连发三次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到3void LCD_SetPos(uchar x, uchar y) { uchar code row[] {0x80, 0x90, 0x88, 0x98}; LCD_WriteCmd(row[y] x); }这里有个细节如果y是2第三行行首是0x88x加0到7刚好落在0x88到0x8F之间。如果x按半角算一行最多加到15也就是0x88150x97这已经超出第三行范围了。所以调用时要自己做约束纯汉字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里直接写温度没问题是因为编辑器保存成了GB2312ANSI。如果你把源文件保存成了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个半角宽从x0开始如果写成“传感器状态:正常”“传感器状态:”是7个汉字加一个冒号共15个半角宽“正常”两个汉字需要4个半角宽加起来19已经超出一行必须分行显示。5. 进阶玩法图形区、自定义字符与实用界面技巧5.1 ST7920的GDRAM绘图能力12864叫12864是因为点阵是128列乘64行。ST7920内部除了DDRAM还有一块图形显示RAMGDRAM可以把整屏当作点阵来操作。进入图形模式的方法很简单写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。最后再分享一个小技巧新屏到手先写个满屏测试程序把四行地址全跑一遍记下自己模块的真实映射这个习惯能帮你省下后面无数个排错小时。本文还有配套的精品资源点击获取
返回列表