STC89C52实战:DS1302实时时钟与18B20温度显示系统详解 📅 发布时间:2026/9/16 4:30:45 👁 浏览次数: 简介面向51单片机学习者和电子设计爱好者压缩包整合了DS1302实时时钟、18B20数字温度传感及LCD12864液晶显示的完整C语言工程既适合课堂实训也可作为课程设计或电子钟、温度监测站等小项目的参考原型。包内共3个文件C源程序提供底层驱动与主逻辑HEX文件可直接烧录验证JPG安装图辅助硬件接线整体仅62KB轻量易用。已有266人学习下载。程序中完整展示了DS1302的寄存器配置与读写时序、18B20的1-Wire通信流程以及LCD12864的汉字和字符显示方法代码结构清晰便于修改参数或移植到其他51系列项目中。对于想要综合运用定时、采集、显示三大模块的开发者来说是一份直接可用且易于二次开发的入门范例。1. 这块 12864 屏上为什么时间总比手机慢DS1302 配置没到位拿到这套 DS1302 时钟 18B20 温度 LCD12864 显示的程序先看一个很多人都会遇到的怪现象程序烧进去时间能走但走一周能慢好几分钟或者温度显示正常时间却停在开机那一刻。这通常不是单片机坏了而是 DS1302 的写保护位没有正确操作、晶振负载电容没配或者对 18B20 的时序要求理解不到位。这套程序的本质是让 STC89C52 这种 51 单片机同时协调三路外设DS1302 通过三线串口提供秒级时间基准18B20 用一线总线回传温度数字量LCD12864 把汉字和数字拼接到屏幕上。适合做课程设计、桌面电子钟或温控仪表的前端显示也适合想把多外设分时驱动这个套路吃透的开发者。2. 三路外设的接口选型与上拉电阻设计2.1 DS1302 三线接口的引脚规划DS1302 不是 I2C 也不是 SPI它是加强版的串行实时时钟用 CE、SCLK、I/O 三根线完成通信。数据在 SCLK 上升沿写入芯片、下降沿输出一次传输至少一个命令字节加若干数据字节。与标准 SPI 最大的区别是它没有片选字地址的概念所有操作都以命令字节开头最高位必须是逻辑 1。在本项目中DS1302 建议接到 P2.0、P2.1、P2.2分别对应 CE、SCLK、I/O。这种接法的好处是 P0 口整组留给 LCD12864 的数据总线P2 的高位引脚还能兼顾其他按键输入不浪费资源。需要特别注意的是DS1302 的 I/O 引脚是双向的读操作时单片机要把它切换为输入模式这在标准 51 中要对 P2.2 写 1准双向口否则读回来的数据永远是低电平。实际布线时还要注意一点DS1302 的 VCC1 接 3V 纽扣电池可选VCC2 接系统 5V。程序初始化时必须先判断是主电源供电还是备份电池供电否则会出现拔掉 USB 线时间就清零的假故障。// DS1302 引脚定义 sbit DS1302_CE P2^0; sbit DS1302_SCLK P2^1; sbit DS1302_IO P2^2;2.2 18B20 上拉电阻为什么必须是 4.7k18B20 是一线总线器件总线空闲时靠单片机内部上拉保持高电平但 STC89C52 内部上拉太弱只有 20k 到 50k 的等效阻抗拉不动长导线或接多个从机时的总线电容波形上升沿会变得很缓。DS18B20 的数据手册明确要求外部上拉 4.7k短线实验可以凑合用杜邦线接到 30cm 以上时没有上拉电阻几乎必挂。还有个常用技巧可以在 18B20 的 VDD 和 GND 之间并联一个 0.1uF 电容。这不是必需的但能显著减少温度读数抖动。很多人碰到温度值每隔几秒跳 0.5℃ 就怀疑传感器坏了实际上就是电源纹波叠加到数据线上导致的采样错误。本项目 18B20 接 P2.3DQ 线上拉 4.7k 到 VCC属于外部供电模式不是寄生供电模式。因为 STC89C52 的 I/O 驱动能力有限寄生供电在温度转换期间需要强上拉处理不好会读到 85℃ 的固化复位值。2.3 LCD12864 串并行模式选择与接线LCD12864 屏内部是 ST7920 控制器支持 8 位并口和串口两种模式。本项目源码默认走 8 位并口P0 口接 DB0 到 DB7RS、RW、E 分别接 P2.4、P2.5、P2.6。PSB 引脚必须拉高接 VCC才能启用并口模式很多人打好板子屏幕不亮一查是 PSB 悬空或者接地了。并口的读写时序要注意 Tsu 和 Th 参数RS、RW 要提前于 E 的上升沿至少 140ns 稳定E 的高电平脉宽最小 450ns。12MHz 晶振下一条普通指令周期才 1us所以驱动里写几个 NOP 就能满足 ST7920 的要求。// LCD12864 写命令函数8位并口模式 void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; // 命令模式 LCD_RW 0; // 写操作 LCD_DataPort cmd; // 送命令字 LCD_E 1; _nop_(); // 高电平保持时间约1us LCD_E 0; LCD_CheckBusy(); // 检查忙标志避免丢指令 }这个函数的关键在于先状态后数据RS 置 0 表示后面总线上是命令而不是显示数据RW 置 0 表示写入。_nop_()提供最短 1 个机器周期的保持时间对 12MHz 晶振就是 1us满足 ST7920 的 E 高电平宽度要求。最后的忙检查不能省——ST7920 执行清屏指令需要约 1.6ms期间不接受新指令而 51 主频下如果不做忙标志检测连续写两条指令会直接丢第二条。3. DS1302 读写时序与 18B20 温度转换的实现细节3.1 DS1302 的写保护与突发模式DS1302 的命令字节格式是最高位固定为 1第 6 位是 RAM/CK 选择时钟寄存器为 0第 5 到第 1 位是寄存器地址最低位是读写方向1 为读。所以读秒寄存器是 0x81写秒寄存器是 0x80。要修改时间先要往写保护寄存器 0x8E 写 0x00 解锁写完时间后再写 0x80 重新上锁。源码中用突发模式burst mode一次读取全部时间数据命令字是 0xBF。这种做法的好处是一条命令搞定 7 个寄存器避免多次拉高 CE 造成的时序缝隙。但要注意突发的读取顺序固定是秒、分、时、日、月、星期、年不能跳项而且读回来的 BCD 码不能直接当十进制用。// 读取DS1302全部时间突发模式 void DS1302_ReadTime(void) { unsigned char i; DS1302_CE 1; // 使能通信 DS1302_WriteByte(0xBF); // 读突发模式命令 for (i 0; i 7; i) { time_buf[i] DS1302_ReadByte(); } DS1302_CE 0; // 禁止通信 // BCD码转十进制高4位×10 低4位 for (i 0; i 7; i) { time_buf[i] (time_buf[i] 4) * 10 (time_buf[i] 0x0F); } }这段代码有个隐患如果读取过程中 DS1302 正在进行秒进位59 秒变 00 秒分数据可能还没来得及同步读到的时间会跳变 1 分钟。常见做法是读两遍取一致结果或者先读一次丢弃、再读一次作为有效值。这个项目里直接单次读取在课程设计演示场景下问题不大但如果做计时精度要求高的产品这里必须加防抖。3.2 18B20 初始化时序的时延分配18B20 总线操作的核心是复位脉冲、存在脉冲、写时隙、读时隙四种波形。初始化时主机把总线拉低 480us 以上再释放传感器检测到下降沿后等 15 到 60us拉低总线 60 到 240us 作为应答。STM32 或 Arduino 上可以直接用微秒延时但 51 单片机跑 12MHz 晶振时一条空语句_nop_()只有 1usC 语言函数调用和循环本身要占用几十个指令周期所以延时子程序必须反复实测。// 18B20复位函数返回1表示存在0表示未检测到 bit DS18B20_Reset(void) { bit presence; DQ 1; _nop_(); DQ 0; Delay480us(); // 拉低480us以上 DQ 1; // 释放总线 Delay60us(); // 等待传感器应答窗口 presence DQ; // 传感器拉低则presence0 Delay240us(); // 完成整个时隙 return ~presence; // 取反返回1表示检测到 }这个函数的时序锚点有三个拉低时间480us、等待应答的起始点释放总线后 15us 开始采样都有效、采样点位置60us 处。如果返回一直为 0先拿示波器看复位时有没有出现传感器拉出的窄低电平脉冲没有就查上拉电阻和接线有但读温度失败则大概率是后续读时隙的采样点偏晚了。3.3 温度读取的 CRC 校验与 12 位分辨率配置18B20 出厂默认 12 位分辨率转换时间最长 750ms。读取温度的标准流程是发跳过 ROM 命令 0xCC发启动转换命令 0x44等待转换完成后发 0xCC 和 0xBE 读暂存器。程序里通常把转换和读取分两次调用间隔至少 750ms否则读出来的是上一次的旧数据。// 读取温度返回整数部分单位℃ int DS18B20_GetTemperature(void) { unsigned char tempL, tempH; int temp; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM匹配 DS18B20_WriteByte(0x44); // 启动温度转换 Delay750ms(); // 等待12位转换完成 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 tempL DS18B20_ReadByte(); tempH DS18B20_ReadByte(); temp (tempH 8) | tempL; return (temp 4) 0x07FF; // 取高11位去掉符号位 }代码里的(temp 4) 0x07FF是 12 位有符号扩展的简易取整。如果要做小数部分保留低 4 位再乘 0.0625 即可但要处理好负温度的二补数问题。18B20 在 -55℃ 下的输出是 0xFF92直接右移得到负值需要先判断符号位再取绝对值否则 -0.5℃ 会被算成 255.5℃。CRC 校验不是必须的但这个项目里如果温度偶发跳变可以把暂存器的第 9 字节读出来用查表法校验确认是不是总线干扰。4. 主程序架构与 LCD12864 显示刷新的协作机制4.1 定时器 0 产生 1ms 基准时基51 单片机无法像 PC 那样跑多线程三路外设的协同时序全靠定时器中断和主循环轮询配合。本程序用定时器 0 工作在方式 116 位定时产生 1ms 中断软件计数器累加到 1000 就是 1 秒。这个 1 秒信号有两层作用触发 DS1302 时间读取、触发温度转换的开始信号。void Timer0_Init(void) { TMOD 0xF0; // 定时器0模式清零 TMOD | 0x01; // 方式116位定时器 TH0 0xFC; // 12MHz晶振定时1ms初值 TL0 0x66; ET0 1; // 开启定时器0中断 EA 1; // 开启总中断 TR0 1; // 启动定时器0 } void Timer0_ISR(void) interrupt 1 { TH0 0xFC; // 重装初值 TL0 0x66; ms_count; if (ms_count 1000) { ms_count 0; second_flag 1; // 置位秒标志主循环处理 } }这里的初值是 0xFC66对应 12MHz 晶振下定时 1ms。但要注意STC89C52 有 12T 和 6T 两种模式出厂默认 12T也就是机器周期为晶振周期的 12 倍12MHz 下机器周期是 1us1ms 需要 1000 个计数。如果芯片被配置成 6T 模式机器周期减半初值就要改成 0xFF38否则时间会快一倍。这种做法很常见但要用逻辑分析仪实测一下秒脉冲间隔再决定是否微调初值。4.2 主循环的分时调度主循环不阻塞、不等待每次循环只做三件事有秒标志就刷时间温度转换完成就读温度有新数据就更新 LCD。这种架构的好处是任何外设卡住都不会拖死整个系统DS1302 读失败只影响时间显示不影响温度刷新。void main(void) { LCD_Init(); DS1302_Init(); Timer0_Init(); while (1) { if (second_flag) { // 每秒刷新一次时间 second_flag 0; DS1302_ReadTime(); LCD_ShowTime(); } if (temp_flag) { // 温度转换完成标志 temp_flag 0; temp DS18B20_GetTemperature(); LCD_ShowTemp(temp); } } }时间每秒刷新一次温度转换 750ms 一次二者在不同时刻发生主循环空闲率很高。这种标志位模式比在中断里直接读 DS1302 要稳得多——DS1302 的时序要求 CE 和 SCLK 严格配合如果在中断服务函数里执行长时序操作正好撞上 LCD 操作的 E 脉冲就会互相干扰。4.3 LCD12864 显示区规划与汉字取模12864 屏幕在 ST7920 模式下支持 4 行 × 8 个汉字或者 4 行 × 16 个 ASCII 字符。显示温度和时间时可以用汉字时分秒℃配合数字变量。ST7920 内置了 8192 个中文字型但℃这个符号不在字库内需要自行取模或者用英文字符拼接显示。源码的显示策略是把时间放在上半屏温度放在下方固定行用lcd_pos设置 DDRAM 地址。ST7920 的 DDRAM 地址不是连续递增的第一行从 0x80 到 0x87第二行从 0x90 到 0x97第三行从 0x88 到 0x8F第四行从 0x98 到 0x9F。写错地址是屏幕显示错位的最常见原因。// 显示时间到第1行格式 HH:MM:SS void LCD_ShowTime(void) { LCD_WriteCmd(0x80); // 第一行首地址 LCD_WriteData(0 time_buf[2] / 10); // 时十位 LCD_WriteData(0 time_buf[2] % 10); // 时个位 LCD_WriteData(:); LCD_WriteData(0 time_buf[1] / 10); // 分十位 LCD_WriteData(0 time_buf[1] % 10); LCD_WriteData(:); LCD_WriteData(0 time_buf[0] / 10); // 秒十位 LCD_WriteData(0 time_buf[0] % 10); }逐位拆分显示的好处是避免使用sprintf这类重量级库函数printf 系函数在 51 上会占用大量代码空间还会拖慢显示刷新。如果要在第二行显示温度可以用同样的方式把整数、小数点、小数位分别拆分输出。5. 秒慢故障定位与 Keil 仿真中的时序陷阱5.1 DS1302 走时偏慢的排查路径DS1302 走时偏慢主要有三个诱因晶振频率偏、读操作覆盖了秒进位、写操作频率太高把时钟寄存器改乱了。第二步可以用示波器量 DS1302 的 X1 引脚波形正常频率是 32768Hz任何偏差都直接换算成日误差。但要注意探头的 10pF 电容会改变晶振负载测量值只能参考不能作为校准依据。DS1302 数据手册推荐的晶振负载电容是 6pF。很多开发板用的晶振标称负载就是 6pF但 PCB 走线寄生电容已经有 2 到 3pF再加焊盘电容就会偏大导致走慢。常见做法是减小外接电容或者干脆不焊匹配电容用微调电容5 到 20pF 可调并联在晶振两端调到一天误差小于 1 秒为止。故障现象可能原因验证手段时间走慢32768Hz 晶振频率偏差示波器测量 X1 引脚波形周期时间停止WP 写保护未解锁时写寄存器单步调试观察写命令返回值断电时间丢失备用电池未接或电压低于 2V万用表测 VCC1 电压读取时间跳变秒进位瞬间读操作连续读取两次比对结果5.2 Proteus 仿真与真实硬件的差异用 Proteus 仿真这套程序时DS1302 的仿真模型不会真实走时而是在每次仿真开始时从主机时间加载初始值。18B20 模型同样不会真实测量温度而是返回用户在元件属性里设定的数值。这是仿真环境与真实硬件最核心的区别仿真通过只能证明程序的逻辑分支正确时序是否符合芯片要求必须上板验证。Proteus 仿真中常见的一个坑是延时精度失真仿真器按指令周期模拟 CP 时间12MHz 下延时函数的实际耗时和真实芯片基本一致但 Proteus 的 CPU 占用过高时会导致仿真速度变慢看起来像延时变长。另一个坑是 Proteus 的 DS1302 模型对 CE 时序不敏感在真实芯片上 CE 必须先拉高再发数据仿真中即使顺序颠倒也能读出数据这会让初学者产生错误认知。5.3 Keil 中调试中断冲突的方法如果 LCD 花屏、时间跳动优先怀疑中断和主循环共用资源。Keil 里可以用断点法在DS1302_ReadTime()入口设断点单步执行看时间寄存器值是否正确然后在LCD_ShowTime()入口再设断点检查传给显示函数的数据有没有被中断修改。另一个排查手段是暂时关闭中断EA 0运行主循环如果这样运行一切正常说明问题出在中断抢占如果仍然花屏说明是 LCD 时序本身不对和中断无关。要注意的是在关闭总中断调试时定时器中断不再触发主循环里的second_flag永远不会置位可以临时改成用按键触发读取或者直接在主循环里无条件调用读取函数。6. 进阶DS1302 涓流充电配置与 18B20 多路扩展DS1302 还有一个容易被忽略的功能涓流充电。当系统掉电后VCC1 上的备用电池如果没有涓流充电电路镍氢电池或超级电容会逐渐亏电。配置寄存器是 0x90写 0xA6 就开启 2kΩ 电阻和标准二极管路径的涓流充电。这个参数不能乱写0xAD 用的是 8kΩ 电阻加二极管0xA5 是 1kΩ 无二极管具体选哪档要看电池内阻。多路 18B20 挂同一条总线时不能用跳过 ROM 命令 0xCC必须先用搜索 ROM 命令 0xF0 找出总线上所有传感器的 64 位序列号再用匹配 ROM 命令 0x55 逐个指定读取。这需要把源码的单点读取改成搜索-匹配-读取三段式还要考虑 1-wire 总线上多个传感器的时序冲突。LCD12864 的温度曲线绘制是另一个值得扩展的方向。ST7920 支持绘图模式写 0x34 开启扩展指令集再写 0x36 开启绘图显示之后用连续写数据的方式设置 16×16 像素区域。每隔一分钟把温度值换算成纵坐标画一个点就能在屏幕上看到 24 小时温度变化曲线。要注意 ST7920 的绘图模式和文本模式不能同时启用切换时必须重新设置地址。把 DS1302 的年份寄存器读出来就能做日期戳画曲线时横坐标按小时取模刚好一屏画完 24 小时。整个界面刷新策略建议用标志位控制整分钟刷新一次曲线每秒钟只刷新数字区域避免绘图模式切换时闪屏。本文还有配套的精品资源点击获取