先给结论:如果你的 Proteus 里 LCD12864B 一片黄、什么都不显示、还不报任何错,八成不是接线问题,是忙检测函数写成死循环了。
我在这个坑里待了整整两天。万用表查接线、对着例程一行行比对、换模型、换晶振、怀疑 DS1302 虚焊 ------ 全试了一遍。最后定位到 4 行代码上。
网上关于 12864 仿真不显示的回答,翻来覆去就那一句"加个上拉电阻"。加完还是黄。这篇把我那天实际走过的排查顺序完整写下来,按命中概率排好序,你对着往下查就行。
一、先分清你是哪一种"不显示"
不同现象对应的原因完全不一样,先对号入座,别一上来就改代码。
| 现象 | 大概率原因 | 直接跳到 | | --- | --- | --- | | 全屏黄,一个字没有 | 程序卡死在忙检测 / PSB 没拉高 | 第二节、第四节 | | 全屏白(背光亮,无字) | 对比度或初始化指令没收到 | 第五节 | | 有时亮有时不亮 | 上电延时不够 | 第五节 | | 亮了,字位置错开/串到下一行 | DDRAM 地址算错 | 第九节 | | 亮了,汉字乱码 | 用了有符号 char* 传汉字内码 | 下一篇 | | 元件根本放不下去 / 放了不显示 | 第三方模型没装 | 第七节 |
第一条占了我见过的绝大多数情况。 下面按这个顺序讲。
二、真凶:BusyCheck 死循环
先看你是不是这么写的 ------ 这也是几乎所有 51 例程里的标准写法:
cpp
```c
void BusyCheck()
{
P1 = 0xff;
RS = 0;
RW = 1;
do
{
E = 1;
temp = P1;
E = 0;
}while((temp & 0x80) != 0); // ← 死在这
}
```
逻辑本身没错:ST7920 的忙标志是数据总线的最高位 BF,拉高表示液晶还在处理上一条指令,等它变 0 再发下一条。
问题出在 Proteus 的模型上。 LCD12864B 是第三方仿真模型,在下面几种情况下,你从 P1 读回来的最高位恒为 1:
-
模型刚上电、内部复位还没完成的那几十毫秒;
-
总线处于高阻/悬空状态(比如 PSB 还没拉高、并口模式没建立);
-
模型本身对读忙时序的响应和真实芯片不完全一致。
最高位恒为 1,那个 do...while 就永远不会退出。整段程序静默卡死在这一行。
为什么这个坑特别难查
因为它没有任何报错,也没有任何现象上的线索:
-
Proteus 不报时序错误,仿真一直在"运行";
-
屏幕就是初始的黄底,看不出是硬件不动还是软件卡住;
-
你按暂停去看变量,只会看到 PC 停在一个谁也没注意的小函数里。
我当时查了两天的根本原因就在这:我一直在怀疑硬件,其实是软件卡住了。
改法:加超时退出
cpp
```c
void BusyCheck() // 2026-10-13 改:加超时,防止总线悬空时永久死循环(Proteus 仿真必需)
{
unsigned char temp;
unsigned int t = 0;
P1 = 0xff;
RS = 0;
RW = 1;
do
{
E = 1;
temp = P1;
E = 0;
}while(((temp & 0x80) != 0) && (++t < 1000));
}
```
只加了一个计数器 t。循环最多转 1000 次就强制退出,后面该发的指令照发。真忙的时候它还是等,模型抽风的时候它自己跳出来 ------ 两条路都堵上了。
三、别猜,直接验证是不是卡在这
改之前先确认,别盲目改。三个办法,从快到慢:
办法 1:把忙检测换成固定延时(30 秒出结果)
cpp
```c
void BusyCheck()
{
// 临时替换:不做忙检测,直接死等
unsigned int i;
for(i = 0; i < 200; i++);
}
```
如果换成这样屏幕立刻亮了,那就是忙检测的问题,不用再查别的。 这是最快的判定法,代价是指令间等待变长,正式代码里不推荐这么写。
办法 2:给超时分支挂个 LED
cpp
```c
void BusyCheck()
{
unsigned char temp;
unsigned int t = 0;
P1 = 0xff; RS = 0; RW = 1;
do { E = 1; temp = P1; E = 0; }while(((temp & 0x80) != 0) && (++t < 1000));
if(t >= 1000) P2^7 = ~P2^7; // 每次超时翻转一次,接个 LED 看它闪不闪
}
```
LED 疯狂闪 = 每次都在超时 = 确认无疑。
办法 3:Proteus 单步 / 暂停看 PC
暂停仿真,看程序计数器停在哪。如果停在 BusyCheck 里的 temp = P1; 这一行,直接实锤。
四、PSB 没拉高:屏一点反应都没有
ST7920 支持串行和并行两种模式,PSB = 1 才是 8 位并口 。PSB 接地或悬空,它就进串口模式,这时候你按并口时序往里写指令,屏幕完全不动 ------ 同样不报错、不提示。
cpp
```c
sbit PSB = P2^0;
void main()
{
PSB = 1; // 必须在任何 write_com 之前
// ...
}
```
两条实践建议:
-
硬件上把 PSB 直接接到 VCC 最省事,代码里那行可以删;
-
仿真图里 PSB 如果画成悬空,必须靠代码拉高,否则必踩。
我第一次翻车就是在这里。后来养成习惯:上电第一件事先把 PSB 拉高,再做别的。
五、上电延时:最烦人的"时好时坏"
液晶上电后内部复位需要时间,主控 12MHz 跑得比它快得多。不等它就发初始化指令,第一条直接丢,后面的跟着全乱。
cpp
```c
unsigned int dly;
PSB = 1;
for(dly = 0; dly < 30000; dly++); // 约 50ms @12MHz
write_com(0x0C);
write_com(0x30); // 8位并口,基本指令集
write_com(0x06);
```
典型表现:偶尔点得亮,偶尔不亮。 这种"时好时坏"是最难查的一类问题,因为它会让你误以为是接触不良、晶振不稳、模型抽风。
六、AT89C52 没有 IAP:抄来的 STC 代码会让仿真跑飞
这是从实物例程往仿真搬时最容易带进来的东西。STC 单片机有片内 EEPROM,掉电保存靠这几个寄存器
cpp
```c
sfr ISP_DATA = 0xe2;
sfr ISP_ADDRH = 0xe3;
sfr ISP_ADDRL = 0xe4;
sfr ISP_CMD = 0xe5;
sfr ISP_TRIG = 0xe6;
sfr ISP_CONTR = 0xe7;
```
AT89C52 压根没有这些寄存器。 往里写值,行为完全不可预测 ------ 轻则无效,重则程序跑飞、屏幕照样一片黄。
main() 里那两行必须注释掉:
cpp
```c
//cc(0x1000); // 2026-10-13 注释:STC 专属 IAP,AT89C52 仿真无效
//xcx(0x1002, DS1302_Time); // 2026-10-13 注释:同上
```
我的闹钟掉电保存最后改用了 DS1302 自带的 31 字节 NV RAM,仿真里能直接验证,比 IAP 这条路好用得多 ------ 具体写法见系列第 1 篇。
七、两个"不在代码里"的坑
排查代码之前,先确认这两件事,否则查到死也查不出来。
① LCD12864B 是第三方模型,Proteus 自带库里没有
自带库里那个"LM016L""AMPIRE128X64"都不是它。没有装模型的直接表现是:元件放不下去,或者放下去仿真时屏幕完全无响应。 模型文件我已经和使用说明一起打包进资源了。
② 换电脑打开工程要重新指定 hex 路径
Proteus 工程存的是原作者的绝对路径。你下载别人的工程打开,单片机属性里的 Program File 指向一个你电脑上不存在的路径 ------ 仿真能启动,屏幕一片黄。
对,一片黄,跟忙检测死循环的表现一模一样。所以这一条要放在最前面排除。
八、排查流程图
cpp
```
屏幕不亮 / 黄屏
│
├─ 0. hex 路径指向对了吗?模型装了吗? → 否 → 先装模型 / 重指 hex
├─ 1. BusyCheck 有超时退出吗? → 否 → 加 ++t<1000 ← 命中率最高
├─ 2. PSB 拉高了吗(硬件或代码)? → 否 → PSB = 1
├─ 3. 上电延时加了吗? → 否 → for(dly<30000)
├─ 4. 调用了 STC 专属 IAP 函数吗? → 是 → 注释掉
├─ 5. 亮了但字错位 / 串到别的行? → 是 → DDRAM 地址算错
└─ 6. 亮了但汉字乱码 / 半角字符错位? → 是 → 见系列下一篇
```
九、亮了,但字跑到别的行去了
如果屏幕已经点亮、字也能出来,就是位置不对 ------ 那恭喜,最难的部分过了。剩下的是 DDRAM 地址问题。
ST7920 的行首地址不是顺着排的:
| 行 | 起始地址 | 可写范围 | | --- | --- | --- | | 第一行 | 0x80 | 0x80--0x87 | | 第二行 | 0x90 | 0x90--0x97 | | 第三行 | 0x88 | 0x88--0x8F | | 第四行 | 0x98 | 0x98--0x9F |
看清楚:第三行(0x88)接在第一行后面,不是第二行后面。 多少人第一行第二行都对,写到第三行字直接窜到下半屏,就是死在这。
还有一个是连续半角字符必须偶数个 :ST7920 按 16 位宽处理显示数据,连续 ASCII 出现奇数个,后面全部错位一格 。比如 hzkdis("%RH") 会出问题,hzkdis("%RH ")(末尾补个空格)就正常。这个坑我单独写了一篇。
写在最后
回头看,这两天里真正卡住我的就一件事:我默认了"屏幕不亮 = 硬件问题"。
实际上 Proteus 里屏幕不亮,最常见的原因是程序在某个看不见的地方卡死了。硬件接线反而是最不容易出错的那一环。所以排查顺序应该是:
先确认程序在跑,再确认指令发出去了,最后才去动接线。
本文是「51 万年历」系列第 2 篇。 第 1 篇是这个项目的完整功能演示(农历、闹钟、掉电记忆全套跑通),源码和 Proteus 工程都在那一篇里,需要的可以去翻。
源码和 Proteus 工程我打包好了,需要的在评论区留言,我看到就发。
后面还有:ST7920 并口驱动、ASCII 偶数对齐、DS1302 时序与 BCD 码、DHT11 单总线、农历查表算法、整合篇。
有问题评论区问,我看到就回。如果你卡的地方这篇没写到,把现象描述清楚发出来,我帮你看看。