简介基于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 的 RDP3.7R/W 接到 51 的 WRP3.6CE 接到某个地址译码位比如 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 读回的数据全是 0xFFLCD1602 也会跟着乱码。很多 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); /* SET1BCD模式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); /* SET0开始走时 */ }写入的值必须是 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 路接 P0CE 接线是否连到 P2.7不能悬空DS / R/WDS 接 RDR/W 接 WR接反则读写全部异常LCD 数据口D0~D7 是否按顺序接 P1LCD 对比度VO 脚是否接电位器接地会黑块电源单片机、DS12C887、LCD1602 是否都接 5VProteus 里 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 没接对导致读回 0xFFBCD 解码后出现 99程序里没做合法性判断。给时间寄存器读取加一个过滤BCD2DEC 后超过 59 就丢弃再读一次能有效避免仿真和真实硬件上的脏数据。5. AD 原理图和 PCB 文件的设计要点5.1 原理图分页与封装核对用 AD 画原理图时把这个系统拆成三页单片机最小系统、DS12C887 模块、LCD1602 接口。三页之间用电源符号和网络标号连接。这样后续改版时换单片机只动第一屏不用碰 LCD 部分的线。重点核对封装。DS12C887 是 DIP24引脚间距 2.54mm焊接直插。不要随手选成 DIP24要检查 AD 库里这个封装的第 1 脚位置和丝印方向。LCD1602 的封装通常是 16 脚单排插针间距 2.54mmProteus 里的引脚顺序和 AD 的封装必须一致否则打样回来插上去左思右想都不亮。元件清单可以参考这个最低配置元件参数封装单片机STC89C52RCDIP40 或 LQFP44DS12C887RTC 芯片DIP24LCD1602字符液晶单排 16Pin 插针排阻10kΩ × 8SIP-8晶振11.0592MHzHC-49S复位电容10uF电解电容去耦电容100nF0805电位器10kΩ3296W5.2 PCB 布局总线短、上拉靠引脚、地要连成片PCB 布局上第一优先级是 DS12C887 和 51 单片机之间的总线。D0~D7、A0~A2、DS、R/W、CE 这些线尽量短且等长避免出现信号反射。P0 上拉排阻要贴着单片机 P0 引脚放排阻一端进 P0另一端直接接 5V不要在中间再绕一圈。地线处理是整个板子的关键。晶振、RTC、单片机的地要就近打孔到地层如果不能做双层板铺地也要保证地线不形成大面积包围的小回路。5V 电源线进入板子后先过一个 10uF 电解电容再分到各个芯片每个芯片旁边放一个 100nF 去耦电容。布线对象线宽间距电源 5V30mil与信号线间距 10milGND 地直接铺铜热焊盘连接晶振走线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); /* DV010 启动振荡器RS1110 对应 1Hz */ rtc_write(RTC_REG_B, 0x08); /* SQWE1 */ }注意 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 交付前的验证清单出板之后花十分钟把这几项过一遍能避免大部分返工上电后 LCD1602 是否有对比度、是否有初始字符不亮先调电位器。校准过的时间是否能在断电后保持拔掉电源等半分钟再插回时间应该继续走而不是回到初值。用短路点强制复位单片机观察时钟是否受影响。连续跑 24 小时记录误差和 6.2 的计算值做对比。用示波器量 SQW 引脚确认有 1Hz 方波输出再决定是否启用中断刷新。DS12C887 的设计里最容易翻车的是把读时间的轮询和液晶刷新混在一起。把 RTC 当成一个独立的外设处理读寄存器和显示刷新解耦这个系统的代码后续要加按键调时、闹钟、温度显示都只是线性扩展不会牵连底层总线时序。本文还有配套的精品资源点击获取
