简介:本资源是一套面向单片机初学者与嵌入式开发者的LCD12864液晶(带字库)图文显示实验完整KEIL C51工程源码,适用于51系列单片机课程设计、实训项目及硬件驱动入门学习。资源包含36个文件,涵盖核心C源码(.c)、编译生成文件(.hex/.obj/.m51)、工程配置(.uv2/.opt/.lnp)、调试日志(.lst/.plg)及中文字符/图形显示测试专用文件(.12864),总大小仅59KB,轻量易导入,便于快速验证驱动逻辑与显示效果。已有910人下载学习,适合在Keil uVision环境下直接编译烧录,无需额外配置即可运行汉字显示、ASCII字符定位、自定义图片加载等典型功能。代码结构清晰,主循环中分阶段演示清屏、中英文混合显示、点阵图像刷新及延时控制等关键操作,配套的实验说明.txt进一步明确了接线方式与显示原理,是理解12864并行/串行驱动机制的实用参考范例。
1. 为什么用C51驱动LCD12864带字库屏,不是直接上STM32或GD32?
很多刚从ARM生态转回51单片机开发的工程师会下意识问:现在都用GD32F103跑EMWIN了,还折腾C51+LCD12864干啥?答案很实在——工业现场存量设备升级、超低功耗待机场景、成本敏感型仪表、以及教学验证中对时序控制的硬性要求。LCD12864(ST7920控制器)带GB2312字库芯片(如HD7279A或内置ROM),意味着它不依赖MCU渲染字体,仅需发送ASCII/GB2312编码即可显示中文,这对仅有4KB ROM、128B RAM的STC89C52RC这类经典C51芯片是不可替代的方案。KEIL C51 v9.56及以下版本仍是最稳定支持该类工程的IDE,尤其在处理__code段定位、_at_绝对地址定义、以及#pragma code段切换时,比KEIL5混合编译环境下手动配置C51+ARM双工具链更可靠。本例程不是“怀旧”,而是解决一个具体问题:在无外部Flash、无RTOS、无串口下载字库能力的裸机系统中,如何让128×64点阵屏稳定显示预置中文菜单与动态参数。
2. LCD12864带字库屏的硬件连接与ST7920控制器通信协议解析
2.1 硬件接口必须匹配ST7920的并行8位模式(非SPI)
LCD12864模块虽有SPI/I²C可选版本,但带字库型号(如JLX12864G-086)默认采用8080并行总线+ST7920控制器,其引脚定义与标准HD44780不兼容。关键信号如下:
| 引脚 | 名称 | 连接说明 | 注意事项 |
|---|---|---|---|
| D0–D7 | 数据总线 | 接C51 P0口(需外接10k上拉) | P0口无内部上拉,不加电阻会导致读状态失败 |
| RS | 寄存器选择 | 接P2.0 | 高电平写数据,低电平写指令 |
| RW | 读写选择 | 接P2.1 | 写操作时必须为低;读忙标志时RW=1 |
| E | 使能信号 | 接P2.2 | 下降沿锁存,脉宽≥450ns,建议用_nop_()延时 |
| PSB | 并/串选择 | 接VCC(高电平) | 必须拉高启用8位并行模式,否则无法访问字库 |
| RST | 复位 | 接VCC或独立按键 | 上电后需>10ms低电平复位 |
提示:若使用STC12C5A60S2等增强型51,P0口可设为准双向模式,但仍建议外接上拉——实测某批次模块在无上拉时,
Read Busy Flag返回值恒为0xFF,导致死循环等待。
2.2 ST7920指令集与字库寻址机制详解
ST7920内置GB2312字库(约8000汉字),但字库地址不等于GB2312区位码。其映射关系为:字库地址 = (区码 - 0x21) × 94 + (位码 - 0x21)
例如“中”字GB2312编码为0xD6D0→ 区码0xD6=214,位码0xD0=208→ 实际字库地址=(214-33)×94+(208-33)=181×94+175=17189(十进制)。
但ST7920提供更简捷方式:直接向DDRAM写入GB2312双字节,硬件自动查表。因此C51代码中无需计算地址,只需按顺序发送高位字节→低位字节。
ST7920关键指令(均通过RS=0, RW=0写入):
0x30:基本指令集(清屏、光标归位)0x36:扩展指令集(启用字库、设置RE=1)0x34:关闭字库(RE=0)0x36:开启字库(RE=1),此时0x80–0xFF范围可写入GB2312高位字节0x80 + 行偏移:设置DDRAM地址(0x80=第1行首,0x90=第2行首,0x88=第3行首,0x98=第4行首)
2.3 KEIL C51工程中关键配置项设置
在Project → Options for Target → Target页中:
- Crystal (MHz):填入实际晶振频率(如11.0592MHz),影响
_nop_()精度 - Code Rom Size:选
Large(因字库数据占ROM空间大) - Use Memory Layout from Target Dialog:勾选,确保
__code段正确映射
在Output页中:
- Create HEX File:必须勾选,烧录需.hex文件
- Browse Information:勾选,便于调试时查看变量地址
在C51页中(重点!):
- Pointer Type:
Generic(避免指针越界) - Code Optimization:Level 8(平衡速度与体积,Level 9可能破坏时序关键代码)
- Misc Controls:添加
-p8051(强制生成8051兼容代码)
注意:若使用STC官方头文件(如
stc12c5a60s2.h),需在Reg51.h前#define对应芯片型号,否则P0^0等位定义失效。
3. C51驱动LCD12864的核心函数实现与KEIL工程结构拆解
3.1 基础时序函数:LCD12864_WriteCmd()与LCD12864_WriteData()
#include <reg51.h> #include <intrins.h> sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_E = P2^2; // 严格按ST7920时序:E高电平期间数据有效,下降沿锁存 void LCD12864_WriteCmd(unsigned char cmd) { LCD_RS = 0; // 指令模式 LCD_RW = 0; // 写操作 P0 = cmd; // 数据送上P0口 _nop_(); _nop_(); _nop_(); // 延时确保建立时间 LCD_E = 1; // E上升沿采样 _nop_(); _nop_(); _nop_(); LCD_E = 0; // E下降沿锁存,完成写入 } void LCD12864_WriteData(unsigned char dat) { LCD_RS = 1; // 数据模式 LCD_RW = 0; P0 = dat; _nop_(); _nop_(); _nop_(); LCD_E = 1; _nop_(); _nop_(); _nop_(); LCD_E = 0; }逻辑说明:_nop_()生成1个机器周期(12T模式下≈1.08μs@11.0592MHz),三次调用确保满足ST7920要求的E脉宽≥450ns及建立/保持时间。若用1T模式单片机(如STC15),需改用_nop_()×6或for(i=0;i<2;i++);。
3.2 忙状态检测与初始化流程
bit LCD12864_BusyCheck(void) { bit busy; LCD_RS = 0; LCD_RW = 1; // 读操作 P0 = 0xFF; // P0设为输入 _nop_(); _nop_(); LCD_E = 1; _nop_(); _nop_(); busy = P0 & 0x80; // D7为忙标志 LCD_E = 0; return busy; } void LCD12864_Init(void) { unsigned char i; for(i=0; i<10; i++) _nop_(); // 上电延时>10ms LCD12864_WriteCmd(0x30); // 基本指令集 _nop_(); _nop_(); _nop_(); LCD12864_WriteCmd(0x30); _nop_(); _nop_(); _nop_(); LCD12864_WriteCmd(0x30); // 重复3次确保识别 LCD12864_WriteCmd(0x36); // 扩展指令集:开启字库 LCD12864_WriteCmd(0x0C); // 显示开,光标关,闪烁关 LCD12864_WriteCmd(0x01); // 清屏 for(i=0; i<100; i++) _nop_(); // 清屏延时 }参数说明:0x36是开启字库的关键指令,若遗漏则后续写入GB2312码无效;0x01清屏后需等待约1.6ms(手册规定),此处用空循环代替精确延时,适配不同晶振。
3.3 中文字符串显示函数与字库存储优化
// 字符串存储在CODE区,节省RAM code unsigned char menu_text[] = "温度:25.6℃\n湿度:65%\0"; // 将GB2312字符串逐字节写入DDRAM void LCD12864_ShowString(unsigned char x, unsigned char y, code unsigned char *str) { unsigned char addr; switch(y) { case 0: addr = 0x80 + x; break; // 第1行 case 1: addr = 0x90 + x; break; // 第2行 case 2: addr = 0x88 + x; break; // 第3行 case 3: addr = 0x98 + x; break; // 第4行 default: return; } LCD12864_WriteCmd(addr); while(*str != '\0') { if(*str == '\n') { // 换行处理 y++; if(y > 3) y = 0; addr = (y==0)?0x80:(y==1)?0x90:(y==2)?0x88:0x98; LCD12864_WriteCmd(addr); str++; } else if(*str >= 0x80 && *(str+1) >= 0x80) { // 判定GB2312双字节 LCD12864_WriteData(*str++); // 先送高位 LCD12864_WriteData(*str++); // 再送低位 } else { LCD12864_WriteData(*str++); // ASCII字符 } } } // 主函数调用示例 void main(void) { LCD12864_Init(); LCD12864_ShowString(0, 0, menu_text); while(1); }提示:
menu_text声明为code类型,编译后存入ROM而非RAM,避免占用本就紧张的128B内部RAM。KEIL链接器自动将code段分配至C:0x0000起始地址。
3.4 KEIL工程文件结构与关键源码组织
本例程ZIP解压后典型结构:
LCD12864_C51_KEIL/ ├── STARTUP.A51 // 启动代码,KEIL自带,勿修改 ├── LCD12864.C // 核心驱动函数(含上述所有函数) ├── MAIN.C // 主程序,含初始化与显示逻辑 ├── LCD12864.H // 函数声明、宏定义(如#define LCD_RS P2^0) ├── REG51.H // 标准51寄存器定义 └── LCD12864.Uv2 // KEIL uVision2工程文件(兼容v9.56)在LCD12864.H中必须定义:
#ifndef __LCD12864_H__ #define __LCD12864_H__ #include <reg51.h> #include <intrins.h> // 引脚重定义(适配不同PCB布局) sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_E = P2^2; // 函数声明 void LCD12864_Init(void); void LCD12864_WriteCmd(unsigned char cmd); void LCD12864_WriteData(unsigned char dat); bit LCD12864_BusyCheck(void); void LCD12864_ShowString(unsigned char x, unsigned char y, code unsigned char *str); #endif4. 常见编译错误与硬件调试技巧:从KEIL报错到屏幕亮起
4.1 KEIL C51典型编译错误及修复方案
| 错误代码 | 错误信息示例 | 根本原因 | 解决方法 |
|---|---|---|---|
| C249 | 'P0': redefinition; different storage class | 头文件重复包含reg51.h多次 | 在所有.C文件中只#include "LCD12864.H",其内部再#include <reg51.h>并用#ifndef防护 |
| C141 | 'LCD_RS': undefined identifier | 未在.C文件中声明sbit或头文件未包含 | 检查LCD12864.H是否被#include,且sbit定义在函数外全局作用域 |
| C250 | 'main': function already has a body | MAIN.C与另一文件(如STARTUP.A51)同时定义main | 删除STARTUP.A51中?C_STARTUP段的main引用,或确认KEIL未误将.A51加入编译 |
| L104 | unresolved external symbol _LCD12864_Init | 函数声明与定义名不一致(如声明LCD12864_Init,定义LCD12864_init) | 统一大小写,C51区分大小写 |
| L105 | segment 'CODE' overflow | 字库数据+代码超过ROM容量(如STC89C52RC仅8KB) | 将长字符串移至code段,或启用ROM(LARGE)模型,在Options → Target → Off-chip ROM中添加地址范围 |
注意:若使用STC新芯片(如IAP15W4K58S4),需在KEIL中
Project → Manage → Components, Environment, Books里安装对应.DFP包,并在Target页选择正确Device。
4.2 硬件级调试四步法:从黑屏到滚动菜单
当烧录HEX后屏幕无反应,按此顺序排查:
测电源与复位:用万用表量LCD模块VDD(5V)、VEE(负压,-8V~-10V,由模块内升压电路生成)、V0(对比度调节端,应为-1V~-2V)。若VEE=0V,检查模块背光LED是否短路导致升压失效。
抓E信号波形:用示波器探头接P2.2(LCD_E),运行
LCD12864_Init(),应看到周期约100μs的方波(对应_nop_()延时)。若无波形,检查KEIL是否生成了正确代码(反汇编窗口看LCALL指令是否跳转)。验证忙检测:注释掉
LCD12864_BusyCheck(),强制延时(如for(i=0;i<100;i++);),若此时能显示,则证明原忙检测电路接触不良(常见于P0口上拉电阻虚焊)。分段注入测试码:
- 先写
LCD12864_WriteCmd(0x01);清屏,观察是否全黑(成功则初始化OK) - 再写
LCD12864_WriteData('A');,看第一行首是否出"A"(验证数据通路) - 最后写
LCD12864_WriteData(0xB6); LCD12864_WriteData(0xC8);("中"字高位+低位),确认字库生效
- 先写
4.3 动态参数刷新与抗干扰设计
工业现场常需实时更新温度值(如25.6℃),直接ShowString会闪烁。优化方案:
// 仅刷新数字区域,保留固定字符 void LCD12864_UpdateTemp(float temp) { unsigned char buf[6]; unsigned char i; // 将浮点数转字符串,如25.6→"25.6" itoa((int)temp, buf, 10); // 整数部分 buf[2] = '.'; // 加小数点 buf[3] = (int)(temp*10)%10 + '0'; // 小数位 buf[4] = 0xE3; buf[5] = 0xA7; // "℃"字GB2312码(0xE3A7) // 定位到"温度:"后(第1行第4列) LCD12864_WriteCmd(0x84); // 0x80+4 for(i=0; i<6; i++) { LCD12864_WriteData(buf[i]); } }此方法避免整行重绘,减少闪烁;buf[4]和buf[5]直接写入GB2312码,省去查表开销。实测在11.0592MHz下,单次刷新耗时<1.2ms,满足100Hz刷新需求。
5. 字库扩展与多语言支持:从GB2312到自定义图标
5.1 添加自定义16×16图标到LCD12864
ST7920支持CGRAM(Custom Graphic RAM),可定义8个16×16点阵图标。以“WiFi信号强度”图标为例:
// 定义4级信号图标(每图标16字节,共4×16=64字节) code unsigned char wifi_icon[][16] = { {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 无信号 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} }; void LCD12864_LoadCGRAM(void) { unsigned char i, j; for(i=0; i<4; i++) { // 加载4个图标 LCD12864_WriteCmd(0x40 + i*16); // CGRAM地址:0x40~0x7F for(j=0; j<16; j++) { LCD12864_WriteData(wifi_icon[i][j]); } } }调用LCD12864_LoadCGRAM()后,在DDRAM中写入0x00即显示第一个图标。此技术常用于替代文字状态(如用图标代替“ON/OFF”),节省字库空间。
5.2 GBK字库兼容性处理(针对非标准模块)
部分廉价LCD12864模块使用UC1698控制器,其字库为GBK而非GB2312。此时0xD6D0("中")需转换为GBK码0xD6D0(巧合相同),但“镕”字GB2312=0xCEC4,GBK=0x81EC。解决方案:
// GB2312到GBK映射表(精简版,仅含常用字) code unsigned int gbk_map[][2] = { {0xCEC4, 0x81EC}, // "镕" → GBK {0xC4C2, 0x81ED}, // "铻" → GBK // ... 添加其他字 }; unsigned int GB2312_to_GBK(unsigned int gb2312) { unsigned char i; for(i=0; i<sizeof(gbk_map)/4; i++) { if(gbk_map[i][0] == gb2312) return gbk_map[i][1]; } return gb2312; // 未映射则直通 } // 显示时调用 LCD12864_WriteData((unsigned char)(GBK >> 8)); LCD12864_WriteData((unsigned char)GBK);该表占用ROM约200字节,适用于需少量GBK字的场景。若需全字库,应选用外挂字库芯片(如SP204),通过SPI加载。
5.3 KEIL工程中ROM空间不足的终极优化技巧
当L105错误持续出现,除前述方法外,还可:
- 启用代码压缩:在
Options → C51 → Misc Controls中添加-f(函数调用优化)和-o+(常量折叠) - 分离字库到外部ROM:用
XDATA指针访问外部62256芯片,需额外硬件支持 - 使用RLE压缩字库:对连续0/1区域编码,解压时用查表法,增加CPU负担但节省50% ROM
最实用的是条件编译剔除不用功能:
#define LCD_SHOW_MENU 1 #define LCD_SHOW_GRAPH 0 // 关闭图形显示,节省2KB #if LCD_SHOW_GRAPH #include "GRAPH.C" #endif编译时KEIL仅链接标记为1的模块,工程体积立减。
本文还有配套的精品资源,点击获取