简介:一份基于STC89C52单片机的数字温度计完整设计PDF,面向单片机入门者与电子课程设计学生,系统讲解从器件选型到软硬件实现的全部环节。文档对比了STC89C52与ATmega8主控方案,选用1602LCD与DS18B20温度传感器,详细给出了51单片机最小系统、电源供电、LCD显示与温度检测电路的设计要点,并配合温度数据读取流程图、Keil编程思路和Proteus仿真界面,帮助读者快速建立单片机测温系统的整体认知。内容涵盖绪论、方案选择、最小系统设计、电源与显示电路、温度检测电路、软件流程图、Keil与Proteus仿真调试等章节,结构完整,便于按需查阅。压缩包内仅包含一个PDF格式的文档文件,整体大小为473KB,轻量便于随时查阅。目前已有98人学习浏览。附录部分还提供完整原理图与程序清单,可作为课程设计报告撰写和实物调试的参考模板,适合需要完成类似课题的初学者参考借鉴。
1. 基于 STC89C52 的数字温度计:一份能直接复现的工程参考
把「基于 STC89C52 的数字温度计」做成能跑起来的东西,卡住大多数人的往往不是 C 语言,而是 DS18B20 的单总线时序和 1602 液晶的接口对接。这套资料同时给了方案论证、原理图、完整源码和 Proteus 仿真,从 Keil 编译到 hex 烧录是闭环的,适合课程设计、毕业设计,也适合想快速搭建温度采集原型的工程师参考。实际复现时要注意三处陷阱:复位电路文字与常规接法矛盾、DS18B20 延时依赖 Keil 优化等级、P0 口驱动 1602 必须加上拉电阻。下面按选型、电路、时序、显示、仿真验证的顺序,把每一处的判断依据和改动说明白。
2. 主控与传感器的选型逻辑和最小系统设计
2.1 为什么最终选了 STC89C52 而不是 ATmega8
资料里把 STC89C52RC 和 ATmega8 放在一起比较,结论是 STC 系列更适合这次设计。STC89C52RC 是 8051 内核,片内 8KB Flash,支持 ISP 在线编程,指令系统兼容 MCS-51,零售价通常在 5 元以内;ATmega8 的存储容量和硬件接口更强,属于 AVR 高端系列的低引脚版本,但对一个只做温度采集和字符显示的项目来说,性能优势发挥不出来,反而要面对一套不同的寄存器风格和工具链。选型的原则实际是「够用 + 熟悉」,而不是「参数最强」。
| 对比项 | STC89C52RC | ATmega8 |
|---|---|---|
| 内核/指令 | 8051,兼容 MCS-51 | AVR RISC |
| 程序存储 | 8KB Flash,ISP 在线编程 | 8KB Flash,ISP 在线编程 |
| 最高工作频率 | 资料标称最高 80MHz,常用 12M/11.0592M | 16MHz |
| 上手成本 | 低,教材和例程多 | 中,寄存器风格差异大 |
| 本次结论 | 采用 | 不采用 |
对温度计这个场景,12MHz 主频足够,I/O 只用了 P0 数据口、P2.0/P2.1 控制线、P3.7 单总线,8KB Flash 放 1602 驱动、DS18B20 时序和中断刷新还有富余。后续做复杂项目时可以再评估 AVR 或 STM32,但作为教学和原型验证,STC89C52 的生态优势非常明显。
2.2 显示与测温模块的取舍
显示部分在数码管和 1602 液晶之间选。数码管亮度高、成本低,但多位显示需要动态扫描,扫描频率不合适会明显闪烁,而且每个数码管要占用段选和位选 I/O,整机功耗也偏大。1602 是字符型液晶,两行 16 字符,接口简单,内部自带显示控制器,写命令和数据就能显示,功耗比数码管低,还能输出字母和提示语。资料中选 1602,价格约 10 元内,带背光,5V 驱动,正好和 STC89C52 共电源。
测温部分比较了热敏电阻加 ADC 和 DS18B20 两种方案。热敏电阻方案需要额外的 A/D 转换电路,感温器件本身存在非线性,软件上还要做查表或拟合,校准工作量大。DS18B20 是 DALLAS 公司的单总线数字温度传感器,测温范围 -55~+125°C,不需要外部器件,数据直接以 16 位数字量读出,省掉整条模拟链路。最终组合确定为 STC89C52 + DS18B20 + 1602,三样东西加在一起成本很低,但覆盖了嵌入式设计最典型的几个环节:最小系统、外设驱动、时序协议、中断刷新。
2.3 最小系统要过的三道关
最小系统以 STC89C52 为核心,12MHz 晶振配两只 30pF 起振电容。晶振和电容的位置要尽量靠近单片机 XTAL1/XTAL2 引脚,引线过长可能导致无法起振。复位部分,资料文字写的是「按键低电平复位」,后面又写「按下时 RET 端为高电平」,这两句自相矛盾。实际常见做法是按键高电平复位:10uF 电解电容并 10k 电阻到地,按键并联在电容两端,按下按键 RST 被拉高,高电平持续约 10ms,远大于 4us 的复位要求。EA 引脚必须接 VCC,否则单片机从外部程序存储器取指,内部 Flash 不生效,这个引脚漏接是最容易被忽略的问题之一。
| 环节 | 器件/参数 | 常见问题 |
|---|---|---|
| 时钟 | 12MHz 晶振 + 30pF 电容 ×2 | 引线过长不起振 |
| 复位 | 10uF 电容 + 10k 电阻 + 按键 | RST 高电平时间不足 |
| 使能 | EA 接 VCC | 无法执行内部程序 |
| 供电 | 5V,100uF + 0.1uF 滤波 | 纹波大导致复位异常 |
端口规划在这套源码里是这样定义的:
#include <reg52.h> // 8051 系列寄存器定义 #define uchar unsigned char #define uint unsigned int sbit rs = P2^0; // 1602 寄存器选择:0=命令,1=数据 sbit lcden = P2^1; // 1602 使能信号,高脉冲有效 sbit DATA = P3^7; // DS18B20 单总线数据线sbit是 Keil C51 的位寻址语法,把某个引脚映射为位变量。P2.0/P2.1 作为控制线、P3.7 作为单总线数据线,都在可位寻址区域内,后续代码直接对rs、lcden、DATA赋值即可。注意这里和资料正文「LCD 的 RS 接 P1.0、E 接 P1.1」的描述不一致,附录代码以 P2.0/P2.1 为准,实际接线要看编译进 hex 的这份代码,而不是看前面的文字描述。
3. 供电电路、1602 接口与单总线测温电路的实物要点
3.1 电源链路:LM2940 与滤波电容
资料用 4 节 1.5V 电池串联得到 6V,经 LM2940 稳压到 5V 给系统供电。选 LM2940 而不是 7805,关键在压差和静态电流。7805 典型压差约 2V,6V 输入时输出可能只有 4V 左右,接近 51 单片机的复位门限,系统会不稳定;LM2940 是低压差稳压器,压差约 0.5V,6V 输入能稳出 5V,静态电流也比 7805 小,电池供电场景下更合适。稳压器输入输出两端分别并 100uF 和 0.1uF 电容,100uF 吸收低频纹波,0.1uF 滤高频噪声,两只电容尽量贴近稳压器引脚。D1 是电源指示灯,串联限流电阻按 1k 左右取值,避免 LED 过流。
电池电压降到约 5.5V 以下时,LM2940 输出开始跌落,DS18B20 的转换结果会出现跳变。实作时先用万用表确认电源与地之间不短路,再插芯片,避免烧片。这一步是资料总结里特别强调的,也是很多实物失败的直接原因。
3.2 1602 接线与时序:文字描述和代码以哪个为准
1602 是 5V 驱动、两行 16 字符的字符型液晶。1/2 脚接电源,3 脚 V0 通过一只 10k 电位器调对比度,4 脚 RS、5 脚 RW、6 脚 E,15/16 脚是背光,背光串一只 1k 电阻限流。资料正文说 RS 接 P1.0、E 接 P1.1,但附录程序里sbit rs=P2^0; sbit lcden=P2^1;,两者冲突。以附录代码为准,因为它是真正编译进 hex 的版本;如果按正文的 P1.0/P1.1 接线,显示不会有任何反应。RW 端在这套设计里始终是写状态,直接接地。
另一个容易踩的坑是 P0 口。代码用 P0 作为 1602 的 8 位数据总线,但 STC89C52 的 P0 是开漏结构,内部没有上拉,输出高电平时要靠外部上拉电阻拉到 5V。常见做法是在 P0 口接一只 10k 排阻到 VCC。Proteus 仿真往往不加上拉也能通过,实物则会随机乱码,这是「仿真能跑、实物必挂」的典型原因。
/* 液晶写命令:com 为命令字 */ void write_lcd_com(uchar com) { rs = 0; // 命令模式 lcden = 0; // E 置低,准备写入 P0 = com; // 命令字送到数据总线 delay_ms(1); // 等待总线稳定 lcden = 1; // E 拉高,完成锁存 delay_ms(1); // 保持高电平时间 lcden = 0; // E 拉低,结束写周期 } /* 液晶写数据:date 为要显示的字符编码 */ void write_lcd_date(uchar date) { rs = 1; // 数据模式 lcden = 0; P0 = date; delay_ms(1); lcden = 1; delay_ms(1); lcden = 0; }命令模式时rs=0,数据模式时rs=1;E 引脚产生一个高脉冲,1602 在 E 下降沿把 P0 上的内容锁存进去。两次delay_ms(1)一是保证数据建立时间,二是给出命令执行时间,1602 的写命令周期在微秒级,1ms 已经足够宽。初始化时依次发送 0x38(8 位数据、两行、5×7 点阵)、0x0C(开显示、关光标)、0x06(写数据后地址指针自动加 1)、0x01(清屏),lcd_init()按这个顺序调用即可。
3.3 温度检测电路:上拉、供电与走线
DS18B20 有三个引脚:GND、DQ、VDD。DQ 接 P3.7,单总线是开漏结构,必须在 DQ 与 VCC 之间接 4.7k~10k 上拉电阻,总线空闲时保持高电平。DS18B20 支持寄生供电,即只接 DQ 和 GND,由总线高电平给内部电容充电,但这种模式下上拉电阻不能太大,否则转换期间电压跌落会读回 85°C 或随机值。实作建议直接把 VDD 接 5V,省去寄生供电的麻烦。
| 功能模块 | 单片机引脚 | 说明 |
|---|---|---|
| 1602 RS | P2.0 | 0=命令,1=数据 |
| 1602 E | P2.1 | 写使能高脉冲 |
| 1602 D0~D7 | P0.0~P0.7 | 8 位数据总线,需外部上拉 |
| DS18B20 DQ | P3.7 | 单总线数据,需外部上拉 |
| 复位按键 | RST | 高电平复位 |
| 晶振 | XTAL1/XTAL2 | 12MHz + 30pF 电容 ×2 |
注意:这里的
delay()是软件循环延时,与 Keil 优化等级强相关。调试时优先用逻辑分析仪看 DQ 波形,而不是凭延时函数推算。
DQ 走线尽量短,避免与 1602 背光电源线长距离平行。背光电流变化会在 DQ 上耦合出毛刺,严重时直接让时序错乱。第 4 章的时序分析和这里说的物理层问题会互相印证。
4. DS18B20 单总线时序协议与驱动实现
4.1 先理解时隙,再写驱动
DS18B20 只有一根数据线,所有操作都靠拉低和释放的时间长度来表达。协议分三层:初始化(复位脉冲加存在脉冲)、ROM 命令、功能命令。初始化时主机把 DQ 拉低至少 480us 后释放,DS18B20 检测到上升沿后等待 15~60us,再把总线拉低 60~240us 作为存在脉冲。写 0 时隙是主机拉低总线并保持 60~120us;写 1 时隙是主机拉低 1~15us 后立即释放,由外部上拉把总线拉高;相邻两个时隙之间至少间隔 60us。读时隙由主机发起,拉低 1us 后释放,并在 15us 内采样电平:传感器输出 0 时会把总线继续拉低,输出 1 时总线保持高。
| 操作 | 主机动作 | 关键时间参数 |
|---|---|---|
| 复位 | 拉低后释放 | 低电平 ≥480us,释放后等 15~60us |
| 写 0 | 拉低保持 | 60us~120us |
| 写 1 | 拉低后释放 | 低电平 1~15us,时隙总长 ≥60us |
| 读 | 拉低后释放并采样 | 低电平 ≥1us,15us 内采样 |
这里最常见的问题不是理解协议,而是延时不准。delay(uint num){ while(num--); }是纯软件循环,延时长度由 Keil 优化等级和晶振频率共同决定,同一个函数在不同工程里时间可以差一倍。资料里delay(80)注释是「大于 480us」,但在 12MHz、12T 模式下经过编译器优化后实际可能只有 200~400us,不满足复位要求。Proteus 仿真对时序有一定宽容度,过了仿真不代表实物一定过,建议把参数改到delay(200)或更长,实测时用示波器确认 DQ 低电平时间确实超过 480us。
4.2 初始化、读一字节、写一字节的源码拆解
核心驱动三件套如下:
/* DS18B20 初始化:拉低总线产生复位脉冲,等待存在脉冲 */ void Init_DS18B20(void) { uchar x = 0; DATA = 1; // 先拉高总线 delay(10); // 空闲一段时间 DATA = 0; // 主机拉低,开始复位 delay(200); // 保持低电平,确保超过 480us DATA = 1; // 释放总线,等待传感器响应 delay(20); // 等待存在脉冲出现 x = DATA; // 采样:传感器会拉低总线表示响应 delay(30); // 等完存在脉冲剩余时间 }复位后的低电平时间由delay(200)承担,这段延时建议实测确认。采样x=DATA读到 0 说明 DS18B20 已经拉低总线,初始化成功;读到 1 则检查接线、上拉电阻或延时长度。
/* 读一个字节:低位在前,高位在后 */ uchar ReadOneChar(void) { uchar i = 0; uchar dat = 0; for (i = 8; i > 0; i--) { DATA = 0; // 启动读时隙 dat >>= 1; // 先把旧位右移 DATA = 1; // 释放总线 if (DATA) // 15us 内采样 dat |= 0x80; delay(8); // 等待时隙结束 } return dat; } /* 写一个字节:每次写一个位,先写低位 */ void WriteOneChar(uchar dat) { uchar i = 0; for (i = 8; i > 0; i--) { DATA = 0; // 启动写时隙 if (dat & 0x01) DATA = 1; // 写 1:短暂拉低后释放 else DATA = 0; // 写 0:保持低电平 delay(10); // 保持时隙长度 DATA = 1; // 结束当前位 dat >>= 1; } delay(8); }读字节时每一位先dat>>=1再采样,DQ 为高就把最高位置 1,8 位结束后低位在前、高位在后的顺序正好对应 DS18B20 温度寄存器的布局。写字节时先判断当前位:写 1 时总线只被短暂拉低然后靠上拉电阻拉高,写 0 时总线保持低电平 10 个延时单位,时隙宽度足够。这里的delay(10)在 12MHz 下约为几微秒到十几微秒,处于写 0 时隙和写 1 时隙的重叠范围内,实测兼容性较好。
4.3 温度读取的完整命令序列与返回值处理
正常测温流程中,两次初始化之间插入命令:先发 0xCC 跳过 ROM(总线上只有一个传感器时),再发 0x44 启动温度转换;12 位分辨率下转换时间最长约 750ms,之后重新初始化,发 0xCC 和 0xBE 读暂存器,连续读两个字节得到温度低字节和高字节。
| 命令字 | 命令名 | 功能 |
|---|---|---|
| 0xCC | Skip ROM | 跳过 64 位序列号匹配,适合单点测温 |
| 0x44 | Convert T | 启动一次温度转换 |
| 0xBE | Read Scratchpad | 从暂存器 0 地址开始读 9 个字节 |
/* 读取温度,返回值放大 10 倍,例如 256 表示 25.6℃ */ int ReadTemperature(void) { uchar a = 0, b = 0; int t = 0; float tt = 0; Init_DS18B20(); // 复位 WriteOneChar(0xCC); // 跳过 ROM 匹配 WriteOneChar(0x44); // 启动温度转换 Init_DS18B20(); // 第二次复位 WriteOneChar(0xCC); WriteOneChar(0xBE); // 读取暂存器 a = ReadOneChar(); // 温度低字节 b = ReadOneChar(); // 温度高字节 t = b; // 高字节先装入 t <<= 8; t |= a; // 拼成 16 位有符号数 if (b & 0x80) // 符号位为 1,说明是负温度 { t = ~t + 1; // 取补码的绝对值 flag_Negative_number = 1; } else { flag_Negative_number = 0; } tt = t * 0.0625; // 12 位分辨率,最低位代表 1/16 ℃ t = tt * 10 + 0.5; // 放大 10 倍并四舍五入到 0.1℃ return t; }ReadTemperature 内部做了两次初始化,第一次启动转换,第二次读取结果,中间必须留出转换时间。主程序每 3 秒调用一次读函数,天然满足 750ms 的转换等待。返回值的处理分两步:先乘 0.0625 得到实际温度,再乘 10 并加 0.5 取整,得到保留一位小数的整数结果,方便后面逐位拆分显示。b&0x80判断的是高字节最高位,即温度寄存器补码的符号位;负温度在寄存器里以补码存放,必须先做~t+1还原绝对值。
5. 温度换算、1602 显示与 3 秒刷新机制
环境上,Keil C51 新建工程时器件选 AT89C52 即可;Proteus 元件库里通常没有 STC89C52,用 AT89C52 替代仿真,引脚和指令完全兼容。编译时在 Options for Target 的 Output 页面勾选 Create HEX File,生成的 hex 文件加载到 Proteus 原理图的单片机芯片里,就能看到 1602 第一行显示 tempreture:,第二行每 3 秒刷新一次温度。下面拆解这段流程里温度数据是怎么变成屏幕字符的。
5.1 十六位温度数据的补码解析
DS18B20 温度寄存器是 16 位补码,低 4 位是小数位,高 5 位是符号扩展位。12 位分辨率下每个最低位代表 0.0625°C,所以代码里tt=t*0.0625。几个典型值:
| 寄存器原始值 | 十进制 | 实际温度 |
|---|---|---|
| 0x0191 | 401 | 25.0625℃ |
| 0x00D2 | 210 | 13.125℃ |
| 0xFC90 | -880 | -55.0℃ |
| 0x07D0 | 2000 | 125.0℃ |
0xFC90 按无符号读是 64656,但在 C51 的 16 位 int 里是 -880,b&0x80判断到符号位后取补,得到 880,再乘 0.0625 就是 55.0。如果不做符号处理直接按无符号数乘 0.0625,会得到一个四千多度的离谱值。负号由flag_Negative_number记录,显示时在最高位补一个 ASCII 负号 0x2D。
5.2 显示拆分和 1602 的字符映射
返回值temp是放大 10 倍的温度,比如 25.6°C 对应 256。拆分时用bai_18b20=temp%1000/100、shi_18b20=temp%100/10、ge_18b20=temp%10。变量名叫 bai、shi、ge,实际对应的是整数十位、整数个位、小数十分位,名字和含义对不上是原资料的遗留问题,但不影响逻辑。显示位置选在第二行开头,地址为 0x80+0x40,之后连续写符号或数字、小数点、十分位、度符号、字母 C。
| 情况 | 显示内容 |
|---|---|
| 正数且十位为 0 | 5.6°C |
| 正数且十位非 0 | 25.6°C |
| 负数且十位为 0 | -5.6°C |
| 负数且整数部分三位 | -125.0°C |
字符编码方面,0x30+数字是把数字转成 ASCII 码,'0' 是 0x30,加 n 就是字符 n;0x2D 是负号 -,0x2E 是小数点,0x43 是字母 C。0xDF 在 1602 的常见字库中对应度符号 °,很多 DS18B20 例程都这么写。不同批次 1602 的字符映射可能略有差异,如果仿真或实物上 0xDF 显示的不是 °,查一下屏的数据手册字模表,或者改用自定义字符实现。
5.3 主循环与定时器中断的刷新节奏
整套程序不是靠死等循环刷新温度,而是用定时器 0 产生 50ms 时基,计数 60 次后置一个标志位,主循环看到标志位才去读温度并更新显示。这样每 3 秒刷新一次,避免连续读 DS18B20 长时间占用 CPU,也让 1602 显示稳定不抖动。为什么不把读取直接放进中断?因为单总线协议里的 480us 复位、750ms 转换等待都是相对漫长的过程,在中断里做会拖垮其他任务。
void main() { lcd_init(); // 1602 初始化 TMOD = 0x01; // 定时器 0,方式 1,16 位定时 TH0 = (65536 - 50000) / 256; // 50ms 定时初值高字节 TL0 = (65536 - 50000) % 256; // 低字节 TR0 = 1; // 启动定时器 ET0 = 1; // 开定时器 0 中断 EA = 1; // 开总中断 write_lcd_com(0x80); // 第一行起始地址 for (num = 0; num < 11; num++) write_lcd_date(table[num]); // 显示提示语 tempreture: while (1) { if (FLAG_DIS == 1) // 3 秒时间到 { FLAG_DIS = 0; dis_D18B20(); // 读取温度并刷新第二行 } } } void timer0() interrupt 1 { uchar count; TH0 = (65536 - 50000) / 256; // 重装初值 TL0 = (65536 - 50000) % 256; count++; if (count == 60) // 60 次 50ms,合计 3s { FLAG_DIS = 1; count = 0; } }TMOD=0x01 把定时器 0 配成 16 位定时模式,初值取 65536-50000,在 12MHz、12T 模式下机器周期为 1us,50000 个机器周期正好 50ms。中断函数里每次进入 count 加 1,到 60 时置位标志并清零。资料原样代码里 count 声明成局部变量,C51 编译器通常会把它放到可覆盖段,单个中断场景问题不大;如果再开第二个中断且同样声明局部变量,就有覆盖风险,常见做法是直接提成全局变量,行为最稳定。dis_D18B20()内部调用 ReadTemperature,里面有浮点运算,放在主循环里不会阻塞中断,因为定时器中断优先级高于主循环普通代码。
6. 让这套数字温度计更实用:定点换算与抗干扰调整
6.1 用整数运算替代浮点换算
ReadTemperature 里tt=t*0.0625和t=tt*10+0.5引入了浮点运算,Keil C51 的浮点库会额外占用几百字节 ROM,每次转换还要走浮点指令。常见做法是全程整数:低 4 位是 1/16 度,放大 10 倍显示一位小数时,直接对原始值做(raw * 10 + 8) >> 4,加 8 是为了四舍五入。
/* raw 为拼好的 16 位原始值,返回放大 10 倍的温度 */ int raw_to_temp10(int raw) { int t = raw; if (raw & 0x8000) // 负数取补 { t = ~raw + 1; flag_Negative_number = 1; } else { flag_Negative_number = 0; } return (t * 10 + 8) >> 4; // 等效 t*10/16 四舍五入 }(t*10+8)>>4等价于t*10/16后再四舍五入,没有浮点类型,代码量和执行时间都明显下降。负数部分沿用补码还原逻辑,显示分支完全不用改。如果你的 51 工程对 ROM 容量敏感,这个替换可以直接用。
6.2 分辨率、上拉和两个实物调试技巧
DS18B20 配置寄存器的低两位决定分辨率:9 位对应 0x1F,10 位对应 0x3F,11 位对应 0x5F,12 位对应 0x7F。长线干扰导致偶尔读到 85°C 时,可以把分辨率降到 11 位,步进 0.25°C,转换时间从 750ms 降到 375ms,时序容限更好。写入配置寄存器的序列是:复位 → 0xCC → 0x4E(写暂存器)→ 三个配置字节(TH、TL、配置值)。注意重新上电后配置会恢复默认 12 位,需要随启动代码重新写入。
| 分辨率 | 步进 | 配置值 | 典型转换时间 |
|---|---|---|---|
| 9 位 | 0.5℃ | 0x1F | 93.75ms |
| 10 位 | 0.25℃ | 0x3F | 187.5ms |
| 11 位 | 0.125℃ | 0x5F | 375ms |
| 12 位 | 0.0625℃ | 0x7F | 750ms |
实测调试时,先用读出的原始值对照冰水(约 0°C)和沸水(约 100°C)做两点验证,确认符号扩展和放大倍数没有写错;再检查 DQ 上拉电阻,尽量把 4.7k 电阻放在靠近传感器引脚的位置,而不是单片机一侧。如果还有偶发跳变,可以在 DQ 对地并联一只 100nF 电容滤高频噪声,容量不能太大,否则 1us 级别的读时隙上升沿变缓,采样点偏移反而更容易出错。把上拉电阻缩短到传感器一侧,这类长线场景下的温度跳动通常立刻就能改善。
本文还有配套的精品资源,点击获取