51单片机驱动DS12C887实时时钟与LCD1602显示的设计与仿真
2026/9/12 10:13:37 网站建设 项目流程

简介:基于51单片机和DS12C887设计的LCD1602实时时钟完整工程包,面向单片机初学者、课程设计及嵌入式硬件开发者。系统支持按键切换日期、星期、时分秒修改,闹钟到达时蜂鸣器鸣叫,DS12C887掉电后时间不丢失,并预留三节5号电池供电接口,采用AT89S52配合ISP下载电路,方便反复烧录调试。工程包共24个文件,约468KB,主要涵盖Keil3的C源码与工程文件、Proteus仿真DSN、AD原理图SCHDOC与PCB图PCBDOC,以及Gerber制板文件和Hex烧录文件,可直接用于实物验证与PCB打样。已有634人学习下载。对于需要完成单片机实时钟设计或学习DS12C887、LCD1602、Proteus仿真、AD绘板的读者,这套方案提供了从程序逻辑、仿真调试到硬件布局的完整参考,能快速上手并二次开发。

1. 51 单片机和 DS12C887、LCD1602 合体前,先想清楚定时从哪来

用一片 DS12C887 做实时时钟,最容易被低估的不是芯片本身,而是它和 51 单片机的总线握手。这颗芯片内置晶振和电池,插上电就开始走时,但要让 51 正确读写它的时间寄存器和状态寄存器,需要把 CE、DS、R/W、A0~A2、D0~D7 按外部 RAM 的方式挂到总线上,而不是像 DS1302 那样用 GPIO 模拟时序。这个项目把 51 单片机、DS12C887、LCD1602 串成一条完整链路,交付的是 C 程序、Proteus 仿真文件、AD 原理图和 PCB 文件,覆盖从代码到打样的全过程。适合做课程设计、桌面时钟,以及想在真实硬件上验证外部总线时序的工程师。先看懂 DS12C887 的接口,再谈 LCD1602 显示和 PCB 布局。

2. DS12C887 的 Intel 总线接法与 C 读写驱动

2.1 为什么是 DS12C887,而不是 DS1302 或纯软件计时

DS12C887 本质上是带 RAM 的实时时钟,内部集成 32.768kHz 晶振、锂电池和振荡电路,外部不需要再接晶振或备用电池。它最大的特点是接口被设计成可以直接挂到类似 51 的外部数据总线上,CPU 用 MOVX 指令就能读写。相比之下 DS1302 走的是串行三线协议,需要软件模拟时序,虽然省引脚,但读取速度慢,而且 DS1302 通常需要外接晶振和备用电池,抗干扰和保持时间都要自己做设计。

软件计时方案(定时器中断累加)也不是不能用,但掉电清零、误差累积、主程序跑飞后时钟跟着停这三个问题很难绕开。DS12C887 把时间基准从 CPU 里剥离出去,51 单片机哪怕复位、下载程序、跑飞,时间都照走不误。这就是这个项目选它的核心原因。而且它的数据总线是 8 位并行,读一次时间只需要几条 MOVX 指令,给 LCD1602 刷新留出了充足余量。

2.2 MOT 引脚接地,CE/DS/R/W 和 51 的对应关系

DS12C887 的 MOT 引脚决定总线模式,接地后进入 Intel 总线模式,匹配 51 单片机的时序。在 Intel 模式下,DS 引脚承担读选通,R/W 引脚承担写选通,CE 是低有效片选。标准的接法是把 DS 接到 51 的 RD(P3.7),R/W 接到 51 的 WR(P3.6),CE 接到某个地址译码位,比如 P2.7。地址线 A0~A2 决定访问哪一个内部寄存器,需要接到 51 的高位地址总线或者直接接 P2.0~P2.2。

我一般把 DS12C887 的数据线 D0~D7 接到 51 的 P0 口,因为 MOVX 访问外部数据存储器时 P0 分时输出低 8 位地址和数据,DS12C887 没有地址锁存功能,所以低 8 位地址不能直接接 P0。正确做法是把 A0~A2 接到 P2.0~P2.2,利用 P2 口输出高 8 位地址时同时驱动这三个地址位,省掉锁存器。CE 接 P2.7 后,寄存器地址被映射到 0x7F00 到 0x7F07 这段区域。

DS12C887 信号51 单片机引脚说明
MOTGND接地进入 Intel 总线模式
CEP2.7低有效,配合地址判断选通
DSP3.7 / RD读选通,低电平有效
R/WP3.6 / WR写选通,低电平有效
A0P2.0寄存器地址最低位
A1P2.1寄存器地址位
A2P2.2寄存器地址最高位
D0~D7P0.0~P0.78 位数据总线,注意 P0 需要上拉

P0 口是开漏结构,一定要加 8 个 10kΩ 上拉电阻,否则高电平上不去,DS12C887 读回的数据全是 0xFF,LCD1602 也会跟着乱码。很多 Proteus 仿真跑不通,都是缺了这一排上拉电阻。

2.3 用 Keil C51 的 xdata 指针直接访问 DS12C887

映射地址定义和驱动函数可以这样写:

#include <reg52.h> #define RTC_BASE 0x7F00 #define RTC_REG(reg) (*(unsigned char volatile xdata *)(RTC_BASE + (reg))) #define RTC_REG_A 0x0A #define RTC_REG_B 0x0B #define RTC_REG_C 0x0C unsigned char rtc_read(unsigned char reg) { return RTC_REG(reg); } void rtc_write(unsigned char reg, unsigned char val) { RTC_REG(reg) = val; }

RTC_REG 这个宏把寄存器编号加到 0x7F00 基地址上,生成一个指向 xdata 空间的指针,访问它等价于执行 MOVX 指令。CE 接 P2.7、A0~A2 接 P2.0~P2.2 后,写寄存器 0x00 就是写地址 0x7F00,写寄存器 0x0B 就是写 0x7F0B。这里有个容易忽略的点:0x7F0B 的地址里,A0~A2 对应低三位是 3,所以访问的是寄存器 3,片选位是 0,地址成立。

2.4 初始化时间寄存器,注意 SET 位不能漏

DS12C887 上电后不一定在走时,要先设置寄存器 A 和 B。寄存器 A 的 0x20 用于启动振荡器并关闭方波输出,寄存器 B 的 0x80 表示 SET 位置 1,此时时间更新周期停止,可以安全写入时间。写完时间后必须清 SET 位,否则时钟一直停在初值。

#define BIN2BCD(n) (((n) / 10) << 4 | ((n) % 10)) void rtc_init_time(unsigned char hour, unsigned char minute, unsigned char second) { rtc_write(RTC_REG_A, 0x20); /* 启动振荡器,关闭SQW */ rtc_write(RTC_REG_B, 0x80); /* SET=1,BCD模式,24小时制 */ rtc_write(0x00, BIN2BCD(second)); /* 秒 */ rtc_write(0x02, BIN2BCD(minute)); /* 分 */ rtc_write(0x04, BIN2BCD(hour)); /* 时 */ rtc_write(0x06, 1); /* 星期:1表示周日 */ rtc_write(0x07, BIN2BCD(5)); /* 日 */ rtc_write(0x08, BIN2BCD(1)); /* 月 */ rtc_write(0x09, BIN2BCD(25)); /* 年,2025年写25 */ rtc_write(RTC_REG_B, 0x00); /* SET=0,开始走时 */ }

写入的值必须是 BCD 码,不能直接把十进制数写进去。BIN2BCD 宏把十位左移四位再和个位取或,正好适配 DS12C887 内部的 BCD 存储格式。时间寄存器地址不是连续的,寄存器 1 被闹钟秒占用,日常读写时间只要关注 0、2、4、6、7、8、9 这几个地址。

读时间时最怕遇到更新进位的瞬间,秒和分钟刚好处于跳变状态。DS12C887 的寄存器 A bit7 是 UIP 标志,内部更新周期约为 244 微秒,轮询 UIP 比较干净利落,但更简单的做法是连读两次同一寄存器,直到两次结果一致。我在项目里通常用后者,代码短,效果也可靠:

#define BCD2DEC(b) (((b) >> 4) * 10 + ((b) & 0x0F)) unsigned char rtc_read_second(void) { unsigned char t1, t2; do { t1 = rtc_read(0x00); t2 = rtc_read(0x00); } while (t1 != t2); return BCD2DEC(t2); }

读分钟和小时同样处理。要注意连续读同一个寄存器两次本身耗时极短,击穿更新窗口的概率很低,两次不一致就重读,不会出现死循环。

3. LCD1602 的 8 线驱动与时间刷新逻辑

3.1 引脚分配避开 DS12C887 的占用

DS12C887 已经把 P0 和 P2.7、P2.6、P2.0~P2.2 占用了,LCD1602 再往这几个口上搭就会互相干扰。我倾向把 LCD1602 的数据线放在 P1 口,控制线用 P3 的高位空闲引脚,RS 接 P3.0、RW 接 P3.1、E 接 P3.2。这样 DS12C887 的总线访问和 LCD1602 的 GPIO 操作完全隔离,Proteus 仿真和实际 PCB 走线都好查。

LCD1602 引脚51 单片机引脚说明
D0~D7P1.0~P1.78 位数据线
RSP3.00 写指令,1 写数据
RWP3.10 写,1 读
EP3.2使能脉冲
VL10k 电位器中点调节对比度
VCC / GND5V / GND供电

LCD1602 的 RW 在只写模式下可以直接接地省一根线,但这个项目里我建议保留,因为调屏或排错时读忙标志是很有用的手段。初始化阶段用延时等时序,稳定运行后改成忙检查,这是两种都能跑的写法。

3.2 初始化命令和写字符函数

8 线模式下初始化命令是 0x38,表示 8 位数据总线、两行显示、5×7 点阵。接着 0x0C 关闭光标,0x06 设置地址自动加一,0x01 清屏。LCD1602 上电时序有要求,每步之间要有延时,5V 供电下上电等待 15ms 以上再发第一条命令。

sbit LCD_RS = P3^0; sbit LCD_RW = P3^1; sbit LCD_E = P3^2; #define LCD_DATA P1 void lcd_delay(unsigned int t) { unsigned int i; while (t--) { for (i = 0; i < 120; i++); } } void lcd_write_cmd(unsigned char cmd) { LCD_RS = 0; LCD_RW = 0; LCD_DATA = cmd; LCD_E = 1; lcd_delay(1); LCD_E = 0; lcd_delay(2); } void lcd_write_data(unsigned char dat) { LCD_RS = 1; LCD_RW = 0; LCD_DATA = dat; LCD_E = 1; lcd_delay(1); LCD_E = 0; lcd_delay(1); } void lcd_init(void) { lcd_delay(100); lcd_write_cmd(0x38); lcd_write_cmd(0x0C); lcd_write_cmd(0x06); lcd_write_cmd(0x01); }

lcd_write_cmd 和 lcd_write_data 的区别只有一个 RS 位,数据线 P1 都是先赋值再拉高 E,形成一个下降沿锁存。这里延时函数的长度不必精确到微秒,LCD1602 最慢的操是清屏,大概 1.6ms,普通的延时循环在 12MHz 晶振下足够覆盖。把延时参数放到表格里方便换晶振时调整:

操作最小等待时间延时参数参考
上电等待15mslcd_delay(120)
写指令40uslcd_delay(2)
写数据40uslcd_delay(1)
清屏1.6ms额外延时

3.3 时间刷新减少整屏闪烁,只改变化的位

主循环里每次读回时分秒后,如果直接清屏重写,LCD 会以肉眼可见的频率闪。正确写法是把秒、分、时分别缓存,跟上次值比较,哪一位变了就只更新那两位字符,没变就不写。这样即使刷新频率很高,屏幕也保持稳定。

void lcd_set_cursor(unsigned char row, unsigned char col) { lcd_write_cmd(row ? (0xC0 | col) : (0x80 | col)); } void display_time(void) { static unsigned char last_h, last_m, last_s; unsigned char h, m, s; h = rtc_read_time_reg(0x04); m = rtc_read_time_reg(0x02); s = rtc_read_second(); if (h != last_h) { lcd_set_cursor(0, 4); lcd_write_data('0' + h / 10); lcd_write_data('0' + h % 10); last_h = h; } if (m != last_m) { lcd_set_cursor(0, 7); lcd_write_data('0' + m / 10); lcd_write_data('0' + m % 10); last_m = m; } if (s != last_s) { lcd_set_cursor(0, 10); lcd_write_data('0' + s / 10); lcd_write_data('0' + s % 10); last_s = s; } }

字符位置里 hour 在 4、5 列,冒号在 6 列,minute 在 7、8 列,冒号 9 列,second 在 10、11 列。每次只有秒变化时只需要写两个字符,LCD 的数据总线负载大大降低。rtc_read_time_reg 是复用第 2 章的 rtc_read 加 BCD2DEC 转换,建议在读取层统一转成十进制,显示层只做除法和取余。

这套刷新逻辑放在 while(1) 主循环里跑,51 单片机一秒钟能跑几千遍,秒变化肯定能捕捉到。不要加 delay(1000),否则按键响应和后续扩展都会被拖死。

4. Proteus 仿真文件搭建:从元件放置到秒跳变

4.1 元件清单和连线自检表

Proteus 仿真文件的搭建步骤不复杂,但连线错了直接看不到预期效果。新建工程后放置 STC89C52RC、DS12C887、LCD1602、RESPACK-8 排阻、两个 10k 电位器(一个给 LCD 对比度,一个留给后续调时),再加一个按钮复位电路。

检查项确认内容
MOT 引脚是否接地,悬空时总线模式不固定
P0 上拉RESPACK-8 一端 VCC,一端分 8 路接 P0
CE 接线是否连到 P2.7,不能悬空
DS / R/WDS 接 RD,R/W 接 WR,接反则读写全部异常
LCD 数据口D0~D7 是否按顺序接 P1
LCD 对比度VO 脚是否接电位器,接地会黑块
电源单片机、DS12C887、LCD1602 是否都接 5V

Proteus 里 STC89C52RC 和 AT89C51 的引脚完全兼容,选哪个都行。DS12C887 在元件库里直接搜 DS12C887,不需要额外添加晶振和电池,这正好对应真实芯片的内部结构。

4.2 模型的晶振频率与时间初始值

双击 DS12C887 打开属性,能看到 Crystal Frequency 项,默认 32.768kHz,保持默认即可。仿真运行后内部时钟按这个频率推进,如果你把仿真加速跑,虚拟时间也会成比例变快,这不影响功能验证,但会让你误以为走时不准。

模型自带的时间初值通常是固定的,不同的 Proteus 版本差异很大。我一般不在仿真里依赖默认时间,而是在开机时调用一次 rtc_init_time,主动写入当前虚拟时间。这样仿真每次从设定值开始跑,判断走时是否有效也更直观。

4.3 仿真不走或者显示乱码的常见原因

秒位不动先看寄存器 B 的 SET 位,初始化写时间后有没有把它清成 0x00。如果 SET 一直为 1,时间寄存器被冻结,指针变量被锁死,读到的永远是初值。

LCD1602 显示乱码时可以看它是不是一个长条方块排满两行,如果是,对比度电位器一般拧到了极限位置,把 VO 电压调到 0.5V 左右。显示内容不对但 LCD 有反应,说明 RS 和数据线接对了但时序不匹配,检查 E 脉冲宽度和初始化命令顺序。

读时间函数卡住的情况我也遇到过,通常是 DS12C887 的 CE 没接对导致读回 0xFF,BCD 解码后出现 99,程序里没做合法性判断。给时间寄存器读取加一个过滤:BCD2DEC 后超过 59 就丢弃,再读一次,能有效避免仿真和真实硬件上的脏数据。

5. AD 原理图和 PCB 文件的设计要点

5.1 原理图分页与封装核对

用 AD 画原理图时把这个系统拆成三页:单片机最小系统、DS12C887 模块、LCD1602 接口。三页之间用电源符号和网络标号连接。这样后续改版时,换单片机只动第一屏,不用碰 LCD 部分的线。

重点核对封装。DS12C887 是 DIP24,引脚间距 2.54mm,焊接直插。不要随手选成 DIP24,要检查 AD 库里这个封装的第 1 脚位置和丝印方向。LCD1602 的封装通常是 16 脚单排插针,间距 2.54mm,Proteus 里的引脚顺序和 AD 的封装必须一致,否则打样回来插上去左思右想都不亮。

元件清单可以参考这个最低配置:

元件参数封装
单片机STC89C52RCDIP40 或 LQFP44
DS12C887RTC 芯片DIP24
LCD1602字符液晶单排 16Pin 插针
排阻10kΩ × 8SIP-8
晶振11.0592MHzHC-49S
复位电容10uF电解电容
去耦电容100nF0805
电位器10kΩ3296W

5.2 PCB 布局:总线短、上拉靠引脚、地要连成片

PCB 布局上第一优先级是 DS12C887 和 51 单片机之间的总线。D0~D7、A0~A2、DS、R/W、CE 这些线尽量短且等长,避免出现信号反射。P0 上拉排阻要贴着单片机 P0 引脚放,排阻一端进 P0,另一端直接接 5V,不要在中间再绕一圈。

地线处理是整个板子的关键。晶振、RTC、单片机的地要就近打孔到地层,如果不能做双层板铺地,也要保证地线不形成大面积包围的小回路。5V 电源线进入板子后先过一个 10uF 电解电容再分到各个芯片,每个芯片旁边放一个 100nF 去耦电容。

布线对象线宽间距
电源 5V30mil与信号线间距 10mil
GND 地直接铺铜热焊盘连接
晶振走线15mil 短走线周围包地
数据总线 D0~D712mil等长,尽量少过孔
控制信号10mil远离晶振

DS12C887 内部带了锂电池,PCB 上不需要额外画电池座。它同时说明了焊接注意事项:直插芯片用烙铁焊接时温度不要超过 300℃,每个引脚停留时间尽量控制在 2 秒内,过热会损伤内部电池和晶振。设计批量产时给这个位置留一个插座,换芯片和维护都方便。

5.3 打样前用 AD 的 DRC 跑一遍未布线检查

AD 里转出 PCB 后,先把板子外形画好,再 Board Shape 确定边界。摆放完元件后用 DRC 检查,重点看网络状态里的 Un-Routed 是否为 0,这是最容易被忽略的交付标准。原理图和 PCB 的封装不一致,DRC 不会报错,需要打开原理图交叉选择模式,逐个元件核对。

PCB 布完线后再补一步检查:DS12C887 的正反面有没有 5V 和 GND 大面积铜箔重叠,如果叠在一起,电源噪声会串进 RTC 的模拟部分。最后出 Gerber 前把丝印层文字整理到元件本体旁边,避免被焊盘压住。Gerber 文件生成后用一个免费的 Gerber 查看器加载一遍,确认每个层都有内容再发给板厂。

6. 校准、断电保持与 1Hz 中断的进阶玩法

6.1 用 SQW 引脚输出 1Hz 中断代替轮询

DS12C887 的寄存器 A 低四位 RS 可以配置方波输出频率,寄存器 B 的 SQWE 位使能 SQW 引脚。配置成 1Hz 输出后,SQW 接到 51 单片机的外部中断 INT0,每秒钟产生一次下降沿中断,主循环彻底解放。轮询方式下主循环要不停读秒寄存器,而中断方式只在该干活的时候触发一次,功耗和总线占用都更低。

void rtc_enable_sqw_1hz(void) { rtc_write(RTC_REG_A, 0x2E); /* DV=010 启动振荡器,RS=1110 对应 1Hz */ rtc_write(RTC_REG_B, 0x08); /* SQWE=1 */ }

注意 RS 位和频率的对应关系在 DS12C887 数据手册里有明确表格,0x2E 的 RS 是 1110,对应 1Hz。如果芯片的 SQW 输出在 Proteus 里没反应,先确认寄存器 A 写入时有没有动到 UIP 位的读操作,读 UIP 不会改变寄存器内容,直接写入即可。

6.2 走时误差的软件校准思路

DS12C887 的晶振误差大约在每个月 1 到 2 分钟,这个量级接受不了时可以用软件补偿。先用外部时间源做基准,记录它每小时快多少秒,比如实测每小时快 0.5 秒,那么每 2 小时需要把内部时间回拨 1 秒。

回拨动作在秒为 0 时执行,往秒寄存器写 59,分寄存器不动,内部会在下一秒继续进位。这种校准不需要停机,也不影响正在进行的 LCD 刷新。代码里放一个静态计数器,累计到设定周期就执行一次校准。

另一种做法是在整点时刻直接重写分钟,误差会更小,但一旦误差超过 30 秒,重写分钟会把秒位也带偏。推荐只动秒寄存器,校准粒度控制在一秒以内。

6.3 交付前的验证清单

出板之后花十分钟把这几项过一遍,能避免大部分返工:

  1. 上电后 LCD1602 是否有对比度、是否有初始字符,不亮先调电位器。
  2. 校准过的时间是否能在断电后保持,拔掉电源等半分钟再插回,时间应该继续走而不是回到初值。
  3. 用短路点强制复位单片机,观察时钟是否受影响。
  4. 连续跑 24 小时,记录误差,和 6.2 的计算值做对比。
  5. 用示波器量 SQW 引脚,确认有 1Hz 方波输出,再决定是否启用中断刷新。

DS12C887 的设计里,最容易翻车的是把读时间的轮询和液晶刷新混在一起。把 RTC 当成一个独立的外设处理,读寄存器和显示刷新解耦,这个系统的代码后续要加按键调时、闹钟、温度显示都只是线性扩展,不会牵连底层总线时序。

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

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

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

立即咨询