51单片机电子时钟设计:12864+DS1302+DS18B20驱动与Proteus仿真 简介面向51单片机初学者的DS1302时钟与18B20温度计综合实训包采用C语言编写配套Proteus仿真电路可实现实时时钟显示与环境温度测量适合课程设计、毕业设计或自学练手是一份可直接运行的完整工程。压缩包共23个文件整体仅267KB包含Keil工程C源码与头文件、编译生成的hex烧录文件、Proteus仿真文件、实验指导文档以及工程备份和中间文件其中.c/.h文件便于阅读修改hex文件可直接烧录验证结构紧凑索引方便。目前已有383人学习下载适合初学者参考借鉴。通过该实例可完整掌握DS1302的读写时序与18B20的单总线通信流程结合Proteus仿真实时观察时钟计数和温度变化实验指导文档对开发步骤、电路连接和调试要点做了说明便于在此基础上扩展按键调时、闹钟提示、温度阈值报警等实用功能。1. 12864DS130218B2051单片机课程设计里最常被问到的一套组合很多人做电子时钟课设一上来就搜“12864DS130218B20”是因为这个标题几乎把51单片机外设课设的考点全占了12864是并口液晶DS1302是串行实时时钟DS18B20是单总线数字温度传感器三种协议完全不同。真正卡人的不是C语言本身而是时序——DS1302的上升沿锁存、DS18B20的微秒级时隙、12864初始化要等指令执行任何一个延时估算错了结果不是乱码就是时间不走。这篇文章以Proteus仿真加C语言代码为主线从引脚分配讲到三块外设的驱动写法最后收在仿真和排错的具体做法上。适合课程设计入门也适合多年后回来捡起8051时序的工程师直接抄驱动框架。2. 硬件连接与引脚分配DS1302、18B20和12864的接线方案2.1 器件选型与Proteus元件库里的关键字常见做法是CPU选AT89C51或STC89C52两者指令完全兼容Proteus里默认就是AT89C51。时钟芯片用DS1302温度传感器用DS18B20液晶选ST7920控制器的12864。Proteus里搜“DS1302”“DS18B20”“LGM12641BS1L”就能放进去LGM12641BS1L就是常见的ST7920 12864模型控制脚和并口结构跟实物基本一致。晶振和复位电路要画上Proteus即使不画晶振也能按芯片属性里的频率跑但原理图保留12MHz晶振和两个30pF负载电容能和后面代码里的延时估算对齐。P0口外挂10k上拉排阻是12864能够正常工作的前提这一点在Proteus仿真里最容易忽略因为液晶偶尔也能显示但数据线浮空时显示内容会出现随机乱码。2.2 引脚分配为什么不让三个外设挤在P0口P0口是开漏结构作为输出必须接上拉电阻作为输入时读不到确定的高电平。12864数据线是双向的读写都要经过P0所以在P0上接一路10k排阻Proteus里搜RESPACK-8是最低要求。DS1302和DS18B20的访问速度不高接在准双向口的P2上更省事不用额外上拉。我一般把12864的8条数据线放P0控制线RS、RW、E放P2.5到P2.7DS1302和DS18B20放P2.0到P2.3P1完全留出来调试时可以直接接LED看数据变化。外设信号51单片机引脚说明12864DB0~DB7P0.0~P0.7接10k上拉排阻12864RSP2.5高电平数据低指令12864RWP2.6低电平写12864EP2.7使能信号下降沿锁存12864PSBVCC固定为并行模式DS1302CEP2.0片选读写期间保持高DS1302SCLKP2.1串行时钟DS1302I/OP2.2双向数据DS18B20DQP2.3经4.7k电阻上拉到VCC#include reg51.h sbit DS1302_CE P2^0; sbit DS1302_SCK P2^1; sbit DS1302_IO P2^2; sbit DQ P2^3; sbit LCD_RS P2^5; sbit LCD_RW P2^6; sbit LCD_E P2^7;这段代码把表里的连接变成可编译的引脚定义后面驱动文件里不再重复定义。reg51.h是Keil自带的寄存器头文件P0到P3的定义都在里面如果用了STC头文件引脚宏也保持一致。注意这里没有定义P0因为12864数据线直接写P0寄存器即可。2.3 特殊接法DS1302的双电源和DS18B20的上拉DS1302有两个电源脚VCC1接备用电池VCC2接主VCC。仿真里直接把3.3V DC Power接到VCC1VCC2接5V。系统运行时优先使用VCC2主电源断掉后自动切到VCC1实现“掉电走时”。这个切换在Proteus里可以用一个开关模拟验证DS1302是否真的在走。DS18B20的数据线DQ要求空闲为高所以要加4.7k上拉电阻。Proteus里不放这个电阻大多数情况下复位函数也能读到存在脉冲但温度转换后容易读回85℃或者干脆返回0这是单总线电平悬浮造成的。12864的VLCD脚通常接一个10k电位器中间抽头接到VEE调对比度。Proteus模型不接电位器也能显示但对比度过高容易看到双影把电位器调到中间位置即可。PSB脚直接接VCC让液晶固定工作在并行模式串行模式虽然只占用3根口线但初始化时序和读写函数完全不同课程设计默认并口即可。3. DS1302时钟驱动先懂三条线再写读写函数3.1 DS1302寄存器地址与BCD码换算DS1302内部有7个时间寄存器和1个写保护寄存器每个寄存器写和读分别用两个命令字。命令字最低位为1是读为0是写。很多初学者直接把地址当命令字用结果初始化成功但读回乱码。规律是命令字 0x80 | (寄存器地址 1) 读写位比如秒寄存器地址0x00写命令0x80读命令0x81。寄存器寄存器地址写命令读命令数据范围秒0x000x800x810x00~0x59bit7CH分0x010x820x830x00~0x59时0x020x840x850x00~0x2324小时制日0x030x860x870x01~0x31月0x040x880x890x01~0x12星期0x050x8A0x8B0x01~0x07年0x060x8C0x8D0x00~0x99写保护0x070x8E0x8Fbit71禁止写BCD码就是每位数字用4位二进制表示比如秒45写成0x45而不是十进制45的0x2D。51单片机显示时间时要分别把BCD的高4位和低4位拆出来加0x30转成ASCII字符。DS1302内部自动维护BCD值不涉及进制转换这是它比DS12C887之类芯片更容易上手的原因。3.2 读写一个字节的位操作与命令传送DS1302三条线里CE是高有效读写期间必须一直保持高电平SCLK提供串行时钟I/O在写时由单片机驱动读时切换到输入。写字节在SCLK上升沿锁存低位先发读字节在SCLK高电平期间采样I/O然后下降沿让DS1302移出下一位。具体代码如下void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS1302_SCK 0; DS1302_IO dat 0x01; // 低位先出 DS1302_SCK 1; // 上升沿DS1302采样 dat 1; } } unsigned char DS1302_ReadByte(void) { unsigned char i, dat 0; DS1302_IO 1; // 释放I/O准备读取 for (i 0; i 8; i) { dat 1; if (DS1302_IO) dat | 0x80; DS1302_SCK 1; // 高电平期间读入数据 _nop_(); _nop_(); DS1302_SCK 0; _nop_(); _nop_(); } return dat; }写字节里DS1302_IO dat 0x01之后立刻拉高SCLKDS1302在上升沿采集每次操作完把SCLK拉低保证下一次起始状态是低。读字节里先采样再翻转时钟顺序不能反过来因为DS1302是在SCLK下降沿之后才改变下一数据的。_nop_()来自intrins.h在12MHz、12T模式下约1us作用是让IO电平稳定后再操作时钟。注意读字节的移位方式第一次读到的最低位最后会出现在dat的最高位相当于把读入的8个bit按原顺序排列回来。3.3 初始化与读取时间写保护必须最后打开初始化分三步关闭写保护、写入初始时间、重新打开写保护。如果时间不走多半是秒寄存器的CH位没置0。CH是秒寄存器bit7为1时振荡器停止必须写0让DS1302开始走。初始化代码void DS1302_WriteCmd(unsigned char cmd, unsigned char dat) { DS1302_CE 0; DS1302_SCK 0; DS1302_CE 1; DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE 0; } unsigned char DS1302_ReadCmd(unsigned char cmd) { unsigned char dat; DS1302_CE 0; DS1302_SCK 0; DS1302_CE 1; DS1302_WriteByte(cmd | 0x01); // 最低位置1表示读 dat DS1302_ReadByte(); DS1302_CE 0; return dat; } void DS1302_Init(void) { DS1302_WriteCmd(0x8E, 0x00); // 关闭写保护 DS1302_WriteCmd(0x80, 0x45); // 秒45CH0 DS1302_WriteCmd(0x82, 0x30); // 分30 DS1302_WriteCmd(0x84, 0x12); // 时1224小时制 DS1302_WriteCmd(0x8E, 0x80); // 打开写保护 } void DS1302_GetTime(unsigned char *h, unsigned char *m, unsigned char *s) { *s DS1302_ReadCmd(0x81) 0x7F; // 去掉CH位 *m DS1302_ReadCmd(0x83); *h DS1302_ReadCmd(0x85); }写命令时CE在传输结束后拉低下一次传输重新拉高这个状态切换可以防止DS1302把残留数据当成新命令。读秒后0x7F是把CH位去掉否则秒值多出0x80。小时寄存器0x84写0x12时bit7为0表示24小时制bit4和bit3分别代表十位的1和个位的2。在Proteus里验证是否在工作可以直接在Keil全速运行后watchs变量如果秒值稳定递增驱动就没问题。4. DS18B20温度采集单总线协议的四个时隙4.1 单总线复位脉冲与存在检测DS18B20只有一根数据线所有命令和数据都走DQ。单总线空闲时靠上拉电阻保持高电平主机通过拉低总线不同的时间形成不同时隙。任何通信前都要先复位主机把DQ拉低480~960us然后释放如果总线上存在DS18B20它会在60~240us内把总线拉低产生存在脉冲。主机检测到这个低电平才能继续发命令。高速时序依赖精确延时先用一组软件延时把时序跑通等整个系统正常后再换定时器精确定时。以下延时在12MHz晶振、Keil默认优化级别下能满足Proteus和实物DS18B20的时序要求。void DelayUs(unsigned int n) { while (n--) { _nop_(); } } void DelayMs(unsigned int n) { unsigned int i; while (n--) { for (i 0; i 120; i); } } unsigned char DS18B20_Reset(void) { unsigned char presence; DQ 0; DelayUs(500); // 拉低约500us DQ 1; DelayUs(80); // 等待存在脉冲 presence DQ; // 读到0有设备 DelayUs(400); // 补齐整个复位周期 return presence; }拉低500us在规定的480~960us范围内释放后等80us读DQ。如果读到低电平说明存在。这里DelayUs用_nop_实现在12MHz下每个_nop_约1us循环判断也有开销所以实际延时比n略大但对单总线这种宽范围时序没有影响。DelayMs里的空循环在Keil默认优化下大约是每轮1ms如果开了高优化建议给循环变量加volatile否则编译器可能把空循环优化掉。4.2 写时隙与读时隙微秒级电平时序写时隙最小为60us写0时主机需要在整个时隙保持低电平写1时主机必须在开始后15us内释放总线让上拉电阻把总线拉高。读时隙主机也要先把总线拉低至少1us释放后15us内采样。这个15us是51单片机最容易翻车的地方如果读到的一定是1说明采样点太晚DS18B20的短暂下拉已经结束了。void DS18B20_WriteBit(unsigned char bit) { DQ 0; if (bit) { _nop_(); _nop_(); // 约2us DQ 1; // 写1时隙提前释放 DelayUs(65); } else { DelayUs(65); // 写0时隙持续拉低 DQ 1; _nop_(); } } unsigned char DS18B20_ReadBit(void) { unsigned char bit; DQ 0; _nop_(); _nop_(); // 拉低后保持2us DQ 1; _nop_(); _nop_(); bit DQ; // 15us内采样 DelayUs(55); return bit; } void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS18B20_WriteBit(dat 0x01); dat 1; } } unsigned char DS18B20_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; if (DS18B20_ReadBit()) dat | 0x80; } return dat; }写0时延用DelayUs(65)写1时也要延时65us保证时隙总长度在60~120us。读位释放总线后要立刻采样中间最多加两个_nop_读完之后再用DelayUs(55)把时隙补满。读字节里每次dat 1后当前读到的位会被放进最高位经过8次循环最低位刚好回到最低位得到的数值就是DS18B20返回的原始字节顺序。4.3 温度转换、读暂存器与负温度DS18B20单器件使用时命令流程是复位、发0xCC跳过ROM匹配、发0x44启动温度转换转换完成后再次复位、发0xCC、发0xBE读暂存器。暂存器前两个字节是温度低字节和高字节12位分辨率下低字节的高4位是小数部分高字节的低3位是整数部分高字节的高5位是符号扩展。指令功能说明0xCC跳过ROM单器件环境直接跳过地址匹配0x44启动温度转换12位转换最多750ms0xBE读暂存器从字节0开始回读9个字节int DS18B20_GetTempX10(void) { unsigned char high 0, low 0, sign 0; int raw, val10; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); DelayMs(750); // 等待转换完成 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); low DS18B20_ReadByte(); high DS18B20_ReadByte(); raw (high 8) | low; if (raw 0xF800) { // 高字节高5位全为1说明是负温度 raw -raw; // 补码转原码 sign 1; } val10 (raw * 5) / 8; // 实际温度*10避免浮点 if (sign) return -val10; return val10; }raw * 5 / 8来自12位原始值乘0.0625再乘10也就是乘0.625即5/8。比如raw192表示12℃返回192*5/8120显示时拆成t/10和t%10就是12.0。负温度判断用raw 0xF800而不是raw 0x8000因为负数的高字节高5位全部是1。读暂存器只读前两个字节下一次DS18B20_Reset会中断它继续输出实物上通常没问题如果要更严谨可以把剩余7个字节也读完。DS18B20转换时间太长如果每秒调用一次DS18B20_GetTempX10主循环会被卡住750ms时钟显示就会跳秒。常见做法是每5秒刷新一次温度其他时间只刷DS1302。5. 12864显示驱动与界面布局时间和温度怎么同时上屏5.1 12864并行模式初始化序列12864用的是ST7920控制器内部带字符表和字形产生器既可以显示ASCII字符也可以显示中文。Proteus中LGM12641BS1L模型默认工作在并行模式PSB接VCC用8条数据线直接写。ST7920初始化比较讲究上电后要等至少50ms然后连续发三次0x30让控制器稳定进入基本指令集接着设置显示开关、清屏、地址自动加一。void LCD_WriteCmd(unsigned char cmd) { P0 cmd; LCD_RS 0; // 命令模式 LCD_RW 0; // 写 LCD_E 1; DelayUs(2); LCD_E 0; // 下降沿锁存 DelayUs(50); // 等待指令执行 } void LCD_Init(void) { DelayMs(50); // 上电等待 LCD_WriteCmd(0x30); // 基本指令集 DelayMs(5); LCD_WriteCmd(0x30); DelayMs(5); LCD_WriteCmd(0x30); LCD_WriteCmd(0x0C); // 显示开光标关 LCD_WriteCmd(0x01); // 清屏 DelayMs(2); LCD_WriteCmd(0x06); // 写入后地址自动加1 }写指令时序要点RS和RW必须提前稳定然后把命令放到P0E拉高再拉低数据在E的下降沿被锁存。后面的DelayUs(50)是让ST7920内部执行指令不是给IO稳定用的。0x01清屏需要更长时间所以单独延时2ms。Proteus模型比较宽容但实物上如果省掉这几次延时首次设置会偶发失效。5.2 写数据、定位与字符串显示写数据函数和写指令几乎一样只是RS置1。显示位置通过写DDRAM地址实现ST7920在8×4行模式下的地址排列是第一行0x80~0x87第二行0x90~0x97第三行0x88~0x8F第四行0x98~0x9F。这个地址表很多人记反结果第二行内容显示到第三行。void LCD_WriteData(unsigned char dat) { P0 dat; LCD_RS 1; LCD_RW 0; LCD_E 1; DelayUs(2); LCD_E 0; DelayUs(50); } void LCD_ShowString(unsigned char row, unsigned char col, unsigned char *str) { unsigned char addr[] {0x80, 0x90, 0x88, 0x98}; LCD_WriteCmd(addr[row] col); while (*str) { LCD_WriteData(*str); str; } }行号DDRAM起始地址列地址范围第1行0x800x80~0x87第2行0x900x90~0x97第3行0x880x88~0x8F第4行0x980x98~0x9Faddr[row] col就是显示地址比如第一行第2列是0x82。ST7920的一行可以显示8个中文字符或16个ASCII字符col按字地址计显示“TIME:12:30:45”这种长字符串时实际ASCII字符会被压缩存储不需要手动跨行。课程设计里通常直接用英文标签和数字因为Proteus的12864模型对GB2312中文支持不一定完整实物上“时间”“温度”这些中文可以直接写在字符串里但在Proteus里可能显示成空白或乱码。要显示中文一般用取模软件生成16×16点阵数组再走图形模式这个放在课设扩展里做。5.3 主循环秒级刷新与温度更新策略把DS1302读出的BCD时分秒转成ASCII显示高4位加0x30变成字符低4位同理。温度用DS18B20_GetTempX10返回的整数拆成整数部分和小数部分。显示函数如下void ShowClock(unsigned char h, unsigned char m, unsigned char s) { unsigned char buf[16]; buf[0] 0 (h 4); buf[1] 0 (h 0x0f); buf[2] :; buf[3] 0 (m 4); buf[4] 0 (m 0x0f); buf[5] :; buf[6] 0 (s 4); buf[7] 0 (s 0x0f); buf[8] \0; LCD_ShowString(1, 0, buf); } void ShowTemp(int t10) { unsigned char buf[16], flag 0; if (t10 0) { flag 1; t10 -t10; } buf[0] T; buf[1] :; if (flag) buf[2] -; else buf[2] ; buf[3] 0 (t10 / 10) / 10; buf[4] 0 (t10 / 10) % 10; buf[5] .; buf[6] 0 (t10 % 10); buf[7] 0xDF; // 度符号 buf[8] C; buf[9] \0; LCD_ShowString(2, 0, buf); }ShowTemp里t10/10是整数部分t10%10是小数部分。0xDF在ST7920字符表里对应“°”如果Proteus模型显示不正常换成空格也能凑合看。主函数的循环用计数器隔开温度采样void main(void) { unsigned char h, m, s; unsigned char cnt 0; DS1302_Init(); LCD_Init(); LCD_ShowString(0, 0, DS1302 18B20); while (1) { DS1302_GetTime(h, m, s); ShowClock(h, m, s); if (cnt 5) { // 约0.5秒后触发一次温度刷新 ShowTemp(DS18B20_GetTempX10()); cnt 0; } DelayMs(100); } }这个循环每100ms刷新一次DS1302秒级变化不会跳。调用温度函数时DS18B20转换的750ms会让时间显示短暂停顿因为GetTempX10内部有DelayMs(750)。课程设计演示时每几秒停一下可以接受要做到完全无感需要把“启动转换”和“读结果”拆开在等待期间继续显示时钟这是进阶优化方向。如果项目里还有定时器中断建议在调用DS18B20_GetTempX10时临时EA 0避免中断把微秒级时序打断。6. Proteus仿真搭建、联调与最容易翻车的三个点6.1 Proteus元件摆放与仿真设置在Proteus 8 Professional里新建工程后用Component Mode搜索并放置AT89C51、DS1302、DS18B20、LGM12641BS1L、RESPACK-8、POT-HG、BUTTON和DC Power。晶振用12MHz复位电路用10uF电解电容加1k电阻加按钮。DS1302的VCC1接3.3VVCC2接5V。双击AT89C51在Program File里加载Keil生成的HEX文件Crystal Frequency填12MHz。DS18B20的温度调整方式是运行时点击元件会出现一个温度滑杆拖动就能看到12864上的温度数值变化。6.2 三个高频问题和检查顺序第一个问题是12864黑屏或乱码。先看P0上拉排阻是否接对RESPACK-8的公共端要接到VCC再看初始化延时50ms不能省最后确认PSB被拉高。如果显示很淡把POT-HG中间抽头接到VLCD调节对比度。第二个问题是DS1302时间不走。检查顺序写保护有没有先关闭秒寄存器的CH有没有清零CE有没有在传输过程中被意外拉低。Proteus里用一个开关控制VCC1通断重新上电后如果时间没有保留说明备用电池没生效。第三个问题是DS18B20老读回85℃。这是“没启动转换就读暂存器”的典型现象说明上一条转换命令没被正确执行。先查DQ上拉再查读时序采样点最后确认每次读温度前都做了完整复位。定位技巧在DS18B20_GetTempX10里加两个全局变量raw_low、raw_high函数返回前把low和high存进去然后在Keil里实时看这两个值。低字节0xFF、高字节0xFF说明总线没通低字节0x50、高字节0x05说明读到了DS18B20上电复位后的默认温度85℃这时先确认是否执行过0x44转换命令而不是急着改时序。本文还有配套的精品资源点击获取