STC89C52万年历项目详解:原理图、BOM、源码与Proteus仿真
2026/9/16 21:48:45 网站建设 项目流程

简介:这套基于STC89C52单片机的万年历设计资料包,完整覆盖硬件与软件两大主线,适用于电子竞赛备赛、单片机课程设计及毕业设计阶段的独立实践。资源整合了AD原理图、BOM物料清单、Keil工程源码与Proteus仿真工程,可直接对照学习时序逻辑、LCD显示、按键调时与闹钟报警等功能实现。包内共31个文件,以C源码、头文件、原理图/PCB文档、仿真工程及HEX烧录文件为主,压缩包仅863KB,层次分明、便于快速定位硬件设计和程序模块。资料中包含DS1302时钟、DHT11温湿度、LCD1602显示、EEPROM掉电存储等典型外设驱动,主程序框架清晰,适合结合仿真图逐段理解和二次修改。目前已有1507人下载学习,对想快速搭建设计框架并深入理解51单片机综合应用的读者有较强参考价值。

1. 不是电子表:STC89C52万年历的资源包结构

拿到这个压缩包的人,大概率会先去找“万年历.hex”准备烧录,但真正值得先打开的是万年历.SchDoc和BOM.xls。整个项目用STC89C52做主控,DS1302提供RTC时间基准,DHT11感知温湿度,LCD1602显示年月日时分秒和温湿度,蜂鸣器负责闹钟告警。Proteus仿真文件把硬件和源码串在一起,能在不打板的情况下把大部分逻辑先跑通。这个组合对正在做课设的在校生、想从仿真过渡到实物的入门工程师、以及需要一套可复现的51外设例程的人都很合适。下文按“原理图→BOM→源码→仿真”的顺序,把每个文件里值得细看的位置拆开。

2. AD原理图怎么挂外设:STC89C52的引脚分配与DS1302/DHT11硬件接线

2.1 打开万年历.SchDoc先看最小系统

这套设计里的“AD”指Altium Designer,工程名是PCB_Project.PrjPcb,原理图是单页结构。单页原理图相比层次设计更容易读,代价是电源、地、网络标号混在一起,看错一个上拉就可能导致LCD1602不出字。我拿到这类51工程,会先找三处:复位电路、晶振电路、P0口上拉。STC89C52的RST脚接10uF电解电容到VCC,并联10k电阻到GND,保证上电可靠复位;12MHz晶振两端各一只22pF负载电容到地;P0口内部是开漏结构,必须接10k排阻RP1上拉到VCC,否则LCD数据线电平不完整。

外设信号STC89C52端口说明
DS1302SCLK / IO / RST(CE)P1.0 / P1.1 / P1.2三线串行,LSB在前
DHT11DATAP3.5单总线,需4.7k上拉
LCD1602RS / RW / ENP2.0 / P2.1 / P2.2控制信号
LCD1602DB0~DB7P0口并行数据总线
独立按键模式/加/减/闹钟P1.3 / P1.4 / P1.5 / P1.6低电平有效
蜂鸣器BEEPP3.6NPN三极管驱动

上表是按资料里常见的端口规划整理的,如果你手里的原理图网络标号有差异,改驱动文件头部的sbit定义即可,不用改逻辑。建议在AD里执行“Project → Compile PCB Project”编译一次,Messages窗口会直接列出悬空网络和重复标号;这一步比盯着PCB看半天下划线有效率得多。注意这里不要和模数转换中的ADC混淆,AD在这个工程里就是Altium Designer。

2.2 DS1302时钟电路:32.768kHz晶振和备用电池

DS1302的时间基准来自32.768kHz晶振,经过内部32768分频得到1Hz秒脉冲。32.768kHz是2的15次方,分频链恰好每秒翻转一次,所以选型不能换成12MHz。原理图里晶振两端各接一只6~12.5pF对地电容,这个值直接影响走时精度,DS1302手册推荐值为6pF。电源部分更关键:VCC2接5V主电源,VCC1通过CR2032电池座接3V备用电池,主电断电后芯片自动切到VCC1,时间不丢。画AD时电池负极直接进GND,正极到VCC1之间不要串电阻,因为DS1302的数据保持电流是微安级,串几欧姆电阻都会引入额外压降。实物打样时晶振紧贴IC引脚放,敷铜地不要铺到晶振正下方,否则寄生电容会拖慢起振。

2.3 DHT11单总线:上拉电阻与供电去耦

DHT11使用单总线协议,只有一根DATA线同时承担双向传输。空闲时总线被上拉电阻保持高电平,主机拉低起始信号后释放,从机再回拉应答。原理图上DATA与VCC之间必须接4.7k上拉,如果省掉这根电阻,读到的温湿度会随机变成0xFF或0x00。电源引脚还要并一只100nF陶瓷电容做去耦,放在靠近传感器插座的位置。数字电路里100nF电容的谐振频率覆盖几十MHz突发噪声,而DHT11的时序最长只有几十微秒,正好需要这种高频旁路。仿真图里不容易体现这种区别,但做PCB时这是常见翻车点,看原理图时顺手确认一下就行。

2.4 LCD1602并行总线:P0上拉与对比度电位器

LCD1602的16个引脚里,最容易出问题的不是DB0~DB7,而是VL对比度脚。VL悬空时屏幕要么全亮要么全空,很多人误以为是程序初始化没写好。正确做法是在VL与GND之间接一只10k电位器,中心抽头接VL,调整到VDD与VSS之间大约1V分压。数据线接P0口,配合2.1节说的10k排阻上拉;RS、RW、EN接P2口低三位。背光A串一只220Ω电阻到VCC,K直接接GND,背光电流大约20mA,在STC89C52的供电能力范围内。软件里LCD_Init会做延时和功能设置,但硬件链路没走通,延时再长也救不回来。原理图理清楚后,再往下核对BOM清单。

3. BOM清单怎么核:AD导出BOM与元器件选型检查

3.1 从AD导出BOM.xls看什么

压缩包里的BOM.xls是Altium Designer导出的物料清单。在AD中打开PCB_Project.PrjPcb,执行Reports→Bill of Materials,左侧勾选Comment、Value、Designator、Footprint、Quantity、Description,右侧导出为Excel格式。导出后不要只看数量,而是把Designator这一列拆开数一数,确认每个独立元件都有位号。下面这个表是常见导出结果的手工简化版,列名可能因AD版本不同略有差异:

CommentValueQuantityFootprintDesignator
RES10k6AXIAL-0.4R1 R2 R3 R4 R5 R6
CAP22pF2RAD-0.2C1 C2
CAP10uF1RB.1/.2C3
CAP100nF3RAD-0.2C4 C5 C6
BATTERYCR20321BAT-2BT1
HDRLCD16021HDR1X16J1

对应到原理图,10k电阻会用在复位、上拉、按键限流几处;22pF只给12MHz晶振用;100nF给DHT11和两个IC电源去耦。每次从AD导BOM都要重新核对Footprint列,同一个封装名在两家PCB库里引脚定义可能不一致,例如AXIAL-0.4这种直插电阻封装,不同厂商叫法可能不同。把BOM的列名和位号规则统一成自己的模板,后面打样会省很多沟通成本。

3.2 晶振负载电容和电阻精度

晶振负载电容的匹配公式是CL = (C1 × C2) / (C1 + C2) + Cstray,Cstray为PCB分布电容,一般取2~5pF。12MHz晶振配两只22pF,并联后约11pF,加分布电容接近典型负载12pF;DS1302的32.768kHz常用6pF或12.5pF。电容容差建议5%以内,陶瓷电容X7R比Y5V稳定。电阻方面,泄放、分压、上拉用5%精度就够,但DHT11数据线上的4.7k上拉电阻最好选1%精度,阻值偏差太大会改变边沿时间,在长线读取时引起错误。LCD对比度电位器选3296W或3362P小体积可调电阻,注意它的封装和固定电阻不一样,BOM里这两项不能混用。

3.3 用Python对BOM做标准化审查

BOM.xls是Excel 97-2003格式,可以用pandas加xlrd做快速体检,重点看位号重复和关键列空缺。下面这段脚本直接保存成check_bom.py就能跑:

import pandas as pd bom = pd.read_excel('BOM.xls', header=0, engine='xlrd') required = ['Comment', 'Designator', 'Footprint', 'Quantity'] for col in required: miss = bom[col].isna().sum() print(f'{col}: {miss} 行空缺') designators = bom['Designator'].str.split(', ').explode() dup = designators[designators.duplicated()] if len(dup): print('存在重复位号:', dup.unique()) else: print('位号唯一,可进入PCB布局')

engine='xlrd'只处理.xls老格式,这个包里的BOM.xls正好是这种格式;如果用AD导出的是.xlsx,直接去掉engine参数,或改成engine='openpyxl'。脚本把Designator按逗号加空格拆分后展开,把“R1 R2 R3”这种合并格拆到每行一个位号,再检查重复。位号重复在AD原理图编译阶段通常会被拦住,但手工改过原理图后重新导出的BOM里仍可能出现,自动化检查比肉眼快得多,也适合批量审查多个工程的物料清单。

3.4 在AD内做标准化审查而不是只靠Excel

Excel只能验证数据完整性,封装和库的对应关系还要回到AD里校验。打开原理图,执行Tools→Footprint Manager,逐项核对每个元件的PCB封装是否在已安装的库中;再执行Tools→Design Rule Check,把Un-connected Pin和Duplicate Net Names勾上。很多人做设计分享时也会做一遍这种标准化审查,目的就是让其他人打开工程时,不会因为封装缺失或位号错乱而全部标红。审查结束后,再进到源码部分。

4. 软件源码拆解:main.c里的BCD时间、DHT11单总线与闹钟比较

4.1 main()的变量初始化与LCD模板

源码目录下有main.c、ds1302.h、dht11.h、LCD1602.h、nongli.h和eepom52.h。main.c开头的变量初值是理解整个程序的一把钥匙。整理后的主体如下:

void main(void) { uchar seconds = 0x55, points = 0x12, when = 0x12; uchar years = 0x14, month = 0x12, day = 0x11, week = 0x04; uchar T_data = 0, RH_data = 0; uchar nao_when = 0x12, nao_points = 0x13; uchar shezhi_flag = 1, Mode_flag = 1; uchar bueezr_flag = 0, count = 0, buzzer_time = 9; uint Mode_count = 0; uchar disp1[16] = {"20 - - Week "}; uchar disp[16] = {" : : C %"}; LCD_Init(); DS1302_Init(); DHT11_Init(); while (1) { DS1302_ReadTime(); DHT11_ReadData(&RH_data, &T_data); LCD_Display(); Buzzer_Check(); } }

seconds、points、when都是BCD码,0x55代表55秒,0x12代表12分,0x14作为年份低字节即2014年,month和day分别初始化为12月11日,week=0x04表示周四。闹钟初值nao_when=0x12、nao_points=0x13,即12:13,当前时间12:12:55,所以上电后很快会触发一次闹钟。LCD模板disp1里“20”是年份前两位,后面三个空格分别放年、月、日;“Week ”后面放星期数字。uchar和uint是51工程里常见的类型简写,一般通过typedef定义成unsigned char和unsigned int。显示字符串固定16字节,正好对应LCD1602一行的宽度。

4.2 DS1302的BCD读写与写保护

DS1302是SPI-like的三线接口,但协议细节和标准SPI不同,地址字节和数据字节都是低位先发,命令字bit0为读/写标志。常用寄存器:秒0x80、分0x82、时0x84、日0x86、月0x88、星期0x8A、年0x8C,写保护0x8E。下面给出单字节写和读的骨架:

sbit DS1302_SCLK = P1^0; sbit DS1302_IO = P1^1; sbit DS1302_RST = P1^2; void DS1302_WriteByte(uchar addr, uchar dat) { uchar i; DS1302_RST = 1; // CE拉高,启动通信 for (i = 0; i < 8; i++) { DS1302_SCLK = 0; DS1302_IO = addr & 0x01; // 地址位LSB在前 addr >>= 1; DS1302_SCLK = 1; } for (i = 0; i < 8; i++) { DS1302_SCLK = 0; DS1302_IO = dat & 0x01; // 数据位LSB在前 dat >>= 1; DS1302_SCLK = 1; } DS1302_RST = 0; // 通信结束 } uchar DS1302_ReadByte(uchar addr) { uchar i, dat = 0; DS1302_RST = 1; for (i = 0; i < 8; i++) { DS1302_SCLK = 0; DS1302_IO = addr & 0x01; // 发送命令字或地址 addr >>= 1; DS1302_SCLK = 1; } for (i = 0; i < 8; i++) { DS1302_SCLK = 0; // 时钟下降沿后IO输出数据 dat >>= 1; if (DS1302_IO) dat |= 0x80; // 先读最高位,再右移 DS1302_SCLK = 1; } DS1302_RST = 0; return dat; } #define BCD2DEC(x) (((x) >> 4) * 10 + ((x) & 0x0F))

写函数里DS1302_IO在地址字节和数据阶段都保持输出,不需要切换方向;读函数在时钟下降沿之后采样IO引脚。注意读函数是先dat >>= 1,再根据IO置dat |= 0x80,这样第一个读到的bit落到最高位,之后逐次移到低位,最终组合出完整字节。BCD转十进制的宏等价于高位乘10加低位。DS1302_Init里会先往0x8E写0x00关写保护,再写初始时间和允许振荡;秒寄存器0x80的最高位CH必须写0,否则芯片内部振荡器停振,时间不走。如果只读时间不设置,也要保证上电初始化时把写保护关掉,不然后续写闹钟寄存器会被拒绝。

4.3 DHT11单总线时序:读取温湿度到T_data和RH_data

DHT11数据帧是40位:湿度整数8位、湿度小数8位、温度整数8位、温度小数8位、校验和8位。这个工程只取湿度和温度的整数部分,所以T_data和RH_data都是uchar。典型读取函数如下:

sbit DHT11_DQ = P3^5; bit DHT11_ReadData(uchar *humi, uchar *temp) { uchar i, cnt; uchar buf[5] = {0}; DHT11_DQ = 1; DHT11_DQ = 0; // 起始信号:拉低 delay_ms(20); // 至少持续18ms DHT11_DQ = 1; // 释放总线 delay_us(30); // 等待20~40us if (!DHT11_DQ) // 从机应答,低80us { while (!DHT11_DQ); // 等应答结束 while (DHT11_DQ); // 跳过80us高电平 for (i = 0; i < 5; i++) for (cnt = 0; cnt < 8; cnt++) { while (!DHT11_DQ); // 每个bit先低50us delay_us(28); // 延时后采样,区分0/1 buf[i] <<= 1; if (DHT11_DQ) buf[i] |= 0x01; while (DHT11_DQ); // 等该bit高电平结束 } if ((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) { *humi = buf[0]; // 湿度整数部分 *temp = buf[2]; // 温度整数部分 return 1; // 校验通过 } } return 0; // 无应答或校验失败 }

判断0和1的思路是:数据位“0”的高电平约26~28us,“1”约70us。主机在检测到低电平结束后延时28us采样,此时“0”的电平已经变低,而“1”仍保持高,所以一次采样就能区分。延时精度依赖delay_us的实现,12MHz晶振下51的机器周期是1us,用几个空转的for循环即可,但在Proteus仿真模型里指令周期可能不准,真正调试时最好用逻辑分析仪看波形。校验公式把前四字节相加,结果低8位必须等于第5字节,否则温湿度数据丢弃,这是单总线协议里最常见的错误过滤手段。main()里如果DHT11_ReadData返回0,可以保留上一次的T_data和RH_data,避免界面出现闪动。

4.4 蜂鸣器闹钟:BCD比较与buzzer_time递减

源码里蜂鸣器变量bueezr_flag、count、buzzer_time配合动作。假设when和points是DS1302读时间后的全局BCD变量,BEEP用sbit定义在P3^6:

sbit BEEP = P3^6; void Buzzer_Check(void) { if (when == nao_when && points == nao_points && seconds < 10) bueezr_flag = 1; if (bueezr_flag) { count++; if (count >= 100) { count = 0; BEEP = ~BEEP; // 翻转蜂鸣器电平 if (buzzer_time > 0) buzzer_time--; if (buzzer_time == 0) { bueezr_flag = 0; // 响铃结束 BEEP = 0; buzzer_time = 9; // 恢复初值 } } } }

关键点在于whenpoints直接沿用DS1302读出的BCD值,闹钟变量也以BCD存储,两者可以直接用等于比较,不用先转成十进制。seconds < 10限制的是触发窗口,防止在某一次读秒恰好落在目标分且秒数变化时重复触发或漏触发。蜂鸣器不是用定时器驱动,而是在主循环里每100次翻转一次,buzzer_time初值9表示翻转约9次后停止,实际响铃时长与主循环周期有关,调用频率变了可以调整count阈值。按键设置时shezhi_flag和Mode_flag控制光标位置,这部分的代码在main.c后半段,逻辑是按键消抖后置位,再对照模式切换。

5. Proteus仿真:万年历.pdsprj加载与DS1302/LCD/DHT11的调试细节

5.1 加载Hex并设置双晶振频率

打开“万年历.pdsprj”后,双击STC89C52元件,在Program File里选择output目录下的万年历.hex,同时把Clock Frequency改成12MHz,与原理图晶振一致。别忘了DS1302旁边那颗32.768kHz晶振也要在属性里把频率设为32.768kHz,两颗晶振频率相差近366倍,设错任何一个,显示的时间都会错得离谱。仿真工程默认可能没有指向hex文件,这是拿到手后第一步要做的,而不是直接Run。

5.2 DS1302不走时或秒位乱跳

仿真里最常见的现象是时间停在初始值。先检查秒寄存器0x80的CH位是否被写成1,CH=1会让芯片停止振荡,DS1302_Init里应该对秒寄存器写0x00或其他最高位为0的BCD值。再检查RST(CE)是否在通信结束后拉低,通信过程中CE必须保持高,Proteus模型对时序比真实芯片更挑剔,地址毛刺会导致寄存器错位。把数字示波器挂在P1.0和P1.1上,可以看到SCLK下降沿与IO数据的对应关系。

提示:如果仿真和实物的时间都卡在00:00:00不动,先查秒寄存器0x80的CH位,再查CE脚是否在通信结束后拉低。

5.3 LCD1602显示乱码或全黑

出现16个小方块,基本是VL对比度设置不对,把10k电位器的中心电压调到1V左右,显示内容就会出来。如果内容错位,优先检查P0口是否接上拉排阻,Proteus的P0口默认也是开漏,仿真图中不加上拉网络,LCD的读时序就会错。另外仿真模型对RW接地时序不敏感,但实物中RW全程置低仍要满足写操作的建立时间,不能因为仿真能跑就跳过这一步。

5.4 DHT11在Proteus里的响应滞后

Proteus的DHT11模型比真实传感器迟钝,连续快速读取可能一直返回0。把读取间隔改为1秒以上,用软件计数器做门控再调用读取函数,能明显提高响应率。仿真速度偏低时delay_ms会偏慢,可以临时把起始信号下拉时间从20ms加大到25ms;但如果实物也照这个值跑,温湿度刷新频率会变低。另一个值得做的小实验是临时改大DS1302的32.768kHz晶振容差,观察LCD秒显示的实际跳变速度,这样能直接判断你的延时循环在仿真和真机之间差多少,再决定要不要调整delay函数。

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

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

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

立即咨询