Proteus仿真LCD12864B不显示/黄屏?别急着改接线,真凶是BusyCheck死循环(51单片机实战排查)

先给结论:如果你的 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 单总线、农历查表算法、整合篇。

有问题评论区问,我看到就回。如果你卡的地方这篇没写到,把现象描述清楚发出来,我帮你看看。

相关推荐
Rain的Java大神之路2 小时前
Java 三层架构,90% 的人只答对一半:Controller 不是瘦,Service 不是透传,Dao 不是只写 SQL
java·后端·面试
小羊没烦恼!2 小时前
浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色
前端·数据库·后端·python·web安全·oracle
IT_陈寒3 小时前
一文搞懂Vite的核心概念
前端·人工智能·后端
一个简单的TTS开发者3 小时前
中文TTS工具有哪些-8款主流方案横向对比
人工智能·后端·python·indextts
高频因子挖掘机4 小时前
量化回测应该保存不复权数据吗?从数据口径到收益计算的工程实践
后端·github·api
高频因子挖掘机5 小时前
实时行情为什么不应该逐只股票请求?批量查询与标的池管理实践
后端·github·api
程序猿DD10 小时前
OctaFuse Gateway 2.15.0:MCP 聚合、向量 Embeddings 与官方目录同步
后端·agent
余槐i12 小时前
PostgreSQL 函数索引报错 must be immutable:3种绕法与代价
数据库·后端·sql·postgresql·性能优化
需要82613 小时前
Spring Bean 生命周期:从实例化到销毁的完整钩子
java·后端·spring