基于51单片机与Proteus仿真的NTC温度计设计实现

发布时间:2026/10/29 7:59:44
基于51单片机与Proteus仿真的NTC温度计设计实现

简介:一份基于51单片机与Proteus仿真的热敏电阻温度计设计系统,面向电子爱好者、嵌入式初学者以及需要完成课程设计或毕业设计的在校学生。系统以51单片机为核心,配合PT100热敏电阻、信号转换采集电路和ADC芯片,完成温度模拟量的采集、转换,并通过数码管实时显示温度值,能够直观演示从模拟信号到数字处理再到显示的完整链路。压缩包共24个文件,包含Proteus仿真工程(DSN)、Keil源程序(多份C/A51源文件及HEX烧录文件)、仿真截图、启动文件与文本说明等,整体仅372KB,便于快速下载、解压和本地复现。目前已有161人学习浏览。资料内带有可直接运行的仿真图与可编译的源代码,对照工程文件可以快速掌握51单片机外设连接、ADC采集编程、Proteus与Keil联合调试的方法,适合作为温度测量类项目的参考模板。

1. 从“做不出实物”到“用 Proteus 仿真先跑通”:这个温度计项目到底在解决什么

课程设计里最磨人的不是写代码,而是器件还没到、板子还没焊、示波器抢不到。这个标题给的方案,其实就是把“基于51单片机+热敏电阻温度计”的完整链路先搬进 Proteus 仿真:51 单片机做控制,NTC 热敏电阻感知温度,ADC0804 把模拟电压变成数字量,1602 液晶显示当前温度,整套系统还附带仿真图和源代码。适合没焊过板子的学生,也适合想快速验证测温逻辑、又不想先买一堆元件踩坑的嵌入式入门者。先用仿真把电路和程序一起跑通,再去焊实物,能帮你提前挡掉绝大多数翻车点。

2. 测温方案怎么选:NTC、B 值方程与 ADC 类型的取舍

2.1 NTC 热敏电阻的原理:先弄懂阻值变化,再谈温度换算

NTC 是负温度系数热敏电阻,温度升高、阻值下降,而且是明显的指数关系。工程上描述 NTC 阻值和温度的关系,常见做法是用 B 值方程:

R = R25 × exp(B × (1/T − 1/T25))

其中 R25 是 25℃ 时的标称阻值,B 是材料常数,T 和 T25 是开尔文温度。常用参数是 R25=10kΩ、B=3950。把这个式子反过来,就能由当前阻值算出温度。但 51 单片机没有硬件浮点单元,现场算 exp 会很吃力,所以实际项目里我很少在单片机里直接跑公式,而是提前算好一张表,运行时查表。

举个例子:R25=10kΩ、B=3950 的 NTC,25℃ 时阻值 10kΩ,50℃ 时大概降到 3.6kΩ,0℃ 时大约 33kΩ。这种跨度意味着你绝对不能把电位器的“旋转百分比”当成“温度百分比”,必须做阻值到温度的映射。仿真时最方便的做法是用一个可调电阻模拟 NTC 的阻值变化,代码层面看的还是分压点电压,而不是电阻本身。

查表法也是江科大 51 单片机学习路线里常见的一招:用脚本把 0~100℃ 对应的 ADC 读数预先算好,存到 code 区,单片机只做一次查表。这样程序简单、响应快,也避开了浮点库带来的代码体积膨胀。

2.2 ADC 怎么选:ADC0804、TLC549、PCF8591 还是内置 ADC

标题里的“51单片机”在 Proteus 里默认最常用的是 AT89C51/AT89C52,这类经典 51 芯片内部不带 ADC,所以温度计必须外挂模数转换芯片。常见的几种方案对比如下。

ADC 型号位数接口Proteus 仿真兼容性适合场景
ADC08048 位并行库模型完善,直接接 P1 口课程设计最稳的选择
TLC5498 位串行时序稍复杂,容易踩坑想省 IO 的场景
PCF85918 位IIC需要模拟 IIC 时序需要多路模拟输入的场景
STC12/15 内置 ADC10 位内置外设Proteus 默认无对应模型实物做小型化时使用

我的建议是直接选 ADC0804。原因有三:并口读取逻辑简单,代码一眼能看懂;Proteus 里有现成模型,不需要额外折腾;网上绝大多数的 51 单片机温度计源码都是围绕它写的,出了问题也好查。TLC549 虽然省 IO,但串行时序在仿真里经常遇到读不到数据的状况,新手容易在示波器上调一晚上。

注意一点:如果你手头的单片机不是 AT89C51,而是带内置 ADC 的 STC15 系列,那实物可以不外挂芯片。但 Proteus 默认元件库里没有完整的 STC15 模型,所以仿真时仍然会采用“经典 51 + 外部 ADC”的结构。这正好也验证了核心逻辑,等到实物阶段再替换成内置 ADC,代码只需要改读取函数那一层。

2.3 分压电阻怎么定:从公式到 Proteus 里的校准

NTC 不能直接进 ADC,它需要和一个固定电阻串联分压,把阻值变化变成电压变化。分压公式是:

Vout = VCC × R_fix / (R_ntc + R_fix)

选取 R_fix 的基本原则是让它在测温量程中段接近 NTC 阻值,这样分压灵敏度最高。对于 R25=10kΩ、量程 0~100℃ 的 NTC,R_fix 选 10kΩ 是比较均衡的折中:0℃ 时 R_ntc≈33kΩ,输出约 1.16V;100℃ 时 R_ntc很小,输出接近 4.68V。ADC0804 的输入范围是 0~5V,整个量程都被利用上了,不至于读数挤在一小段里。

仿真时我用一个 10kΩ 电位器来模拟 NTC:电位器两端接分压结构,一端和滑动端短路后作为“热敏电阻”,旋转电位器就相当于改变温度。刚开始做的时候会觉得这一步有点玄学,因为电位器明明是个线性的可调电阻,怎么模拟指数变化的 NTC?答案是不要在电位器上找温度,而是让程序按“ADC 读数 → 查阻值 → 查温度表”的路径计算,电位器只需要提供一个可变的阻值即可。

如果还想让仿真更贴近实物,可以在 NTC 支路串联一个几百欧的小电阻模拟导线电阻和接触电阻,然后在程序里做软件校准。这个细节实物上特别有用:焊完板子发现 0℃ 档偏了 1.5℃,大多数情况下不是代码问题,而是分压电阻精度和参考电压偏差,软件校准比重新换电阻快得多。

3. 在 Proteus 里把温度计仿真跑通:原理图、Keil 源码与烧录配置

3.1 原理图要摆哪些元件:一份可直接复制的清单

打开 Proteus 后,先新建原理图,然后从左侧元件库搜索表中元件。注意 Proteus 里 1602 液晶通常用 LM016L 模型,直接搜 1602 可能找不到。

元件Proteus 搜索关键词数量作用
单片机AT89C511主控
模数转换ADC08041读取模拟电压
1602 液晶LM016L1温度显示
固定电阻 10kΩRES2分压与复位
电位器 10kΩPOT-HG1模拟 NTC 变化
晶振 11.0592MHzCRYSTAL1系统时钟
瓷片电容 33pFCAP2晶振负载
电解电容 10μFCAP-ELEC1上电复位延时
时钟源CLOCK1ADC0804 外部时钟
排阻 10kΩRESPACK-81P0 口上拉

连线时按这样的引脚规划走:P0 接 LCD 数据口 D0~D7,P2.0 接 LCD 的 RS,P2.1 接 RW,P2.2 接 E;ADC0804 的 DB0~DB7 接 P1 口,WR 接 P2.3,RD 接 P2.4,INTR 接 P2.5;NTC 分压点接 ADC0804 的 VIN+,VIN- 直接接地。

很多 51 单片机新手容易忽略的是 P0 口上拉。P0 内部是开漏结构,直接接 LCD 会驱动不起来,必须接一排 10kΩ 上拉电阻到 VCC。如果你见过 51 单片机核心板原理图,会发现 P0 口几乎都带一排排阻,就是这个原因。

3.2 核心代码:ADC0804 读取、查表换算与温度查找

先写 ADC0804 的读取函数。它的转换时序是:WR 拉低再拉高,产生一个上升沿触发转换;转换完成后 INTR 引脚拉低;然后 RD 拉低,数据出现在 DB0~DB7 上。代码里的注释直接对应数据手册时序,建议照抄理解,不要凭感觉改顺序。

#include <REG51.H> sbit ADC_WR = P2^3; sbit ADC_RD = P2^4; sbit ADC_INTR = P2^5; unsigned char read_adc(void) { unsigned char val; ADC_WR = 0; // 产生 WR 上升沿 ADC_WR = 1; while (ADC_INTR); // 等待转换完成,INTR 被拉低 ADC_RD = 0; // RD 拉低,打开输出使能 val = P1; // 读 8 位转换结果 ADC_RD = 1; return val; }

这段代码里需要注意 while 等待循环。Proteus 仿真速度很快,INTR 低电平可能一闪而过,如果程序读不到,可以加一个超时保护,避免死循环。下面这个是加了计数上限的版本,实物上也能避免异常卡死。

unsigned char read_adc_safe(void) { unsigned int timeout = 60000; ADC_WR = 0; ADC_WR = 1; while (ADC_INTR) { if (--timeout == 0) break; // 防死等 } ADC_RD = 0; return P1; }

接下来是温度换算。NTC 是非线性器件,直接用电压算温度误差很大,所以先离线生成一张“温度 0~100℃ 对应的 ADC 读数表”,单片机运行时查表。生成表用下面的 Python 脚本,改一下 R25、B、R_fix 三个参数就能适配你自己的 NTC 型号。

import math R25 = 10000.0 # 25℃标称阻值 B = 3950.0 # NTC 的 B 值 R_fix = 10000.0 # 分压固定电阻 VCC = 5.0 ADC_MAX = 255.0 for t in range(0, 101): T = t + 273.15 T25 = 298.15 R_ntc = R25 * math.exp(B * (1.0 / T - 1.0 / T25)) vout = VCC * R_fix / (R_ntc + R_fix) adc = vout * ADC_MAX / VCC print(f"{t},{int(round(adc))}")

把这 101 个数粘进 C 代码的 code 数组,然后做查表。查表逻辑是从 0℃ 开始找第一个“ADC 表值大于当前读数”的温度,这个温度就是当前温度近似值。

code unsigned char temp_table[101] = { // 由 Python 脚本生成,0~100℃ 对应 ADC 读数 }; unsigned char adc_to_temp(unsigned char adc) { unsigned char t; for (t = 0; t < 101; t++) { if (adc <= temp_table[t]) break; } return t; }

这里说明一下,ADC 读数随着温度升高是增大的,因为 NTC 阻值下降、分压点电压升高。所以表里元素是递增的,查表用顺序扫描已经足够快;如果强迫症想优化,改成二分查找也行,但 101 个元素顺序扫一遍在 12MHz 主频下几乎不耗时。

3.3 显示模块:1602 的初始化与温度刷新

1602 的驱动是 51 单片机项目里的常客,这里贴出最小可用的一组函数:写命令、写数据、初始化、显示温度。RW 引脚在 Proteus 里直接接地,然后代码里每次操作只用控制 RS 和 E,省去读忙检测的逻辑。

sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_E = P2^2; #define LCD_DATA P0 void lcd_delay(void) { unsigned char i; for (i = 0; i < 200; i++); } void lcd_cmd(unsigned char cmd) { LCD_RS = 0; LCD_RW = 0; LCD_DATA = cmd; LCD_E = 1; // 高脉冲写入 LCD_E = 0; lcd_delay(); } void lcd_data(unsigned char dat) { LCD_RS = 1; LCD_RW = 0; LCD_DATA = dat; LCD_E = 1; LCD_E = 0; lcd_delay(); } void lcd_init(void) { lcd_cmd(0x38); // 8 位数据、两行显示、5x8 点阵 lcd_cmd(0x0c); // 显示开启、不显示光标 lcd_cmd(0x06); // 写入数据后地址自动加 1 lcd_cmd(0x01); // 清屏 } void show_temperature(unsigned char t) { unsigned char h = t / 10; unsigned char l = t % 10; lcd_cmd(0x80); // 设置到第一行第一列 lcd_data('0' + h); // 十位 lcd_data('0' + l); // 个位 lcd_data(0xdf); // 度符号 ° lcd_data('C'); }

注意 0xdf 是 1602 字符库里的“°”符号。如果 Proteus 的 LM016L 模型不显示这个符号,就改成 lcd_data('''); 或者直接不要度数符号,换成 lcd_data('C'); 也不影响功能。主循环里可以加一个 200ms 左右的延时再刷新一次,避免显示闪烁以及 ADC 读数跳变引起的视觉疲劳。

3.4 把 HEX 文件放进仿真芯片:Keil 与 Proteus 的联调配置

Keil 工程建好后,先确认编译目标选的是 AT89C51 或者兼容的 51 系列。在 Target Options 的 Output 页面里勾上 Create HEX File,编译一次,生成 hex 文件。

然后回到 Proteus,双击原理图上的 AT89C51 芯片,在 Program File 一栏选刚生成的 hex 文件。这里有一个很容易被忽略的参数:Crystal Frequency 要填 11.0592MHz,和 Keil 工程、串口波特率的计算保持一致。如果你后续要加串口输出,这个参数决定波特率是否准确。

Proteus 8 以上版本有个习惯:程序重新编译后,必须先停止当前仿真再运行,否则加载的还是旧 hex。如果点了运行发现单片机没反应,先按一下复位按键,至少能排除上电复位电路的问题。晶振频率不是越高越好,仿真模型在过高频率下会拖慢运行速度,11.0592MHz 是 51 项目最常见的选择,时钟周期计算和串口波特率都能整除。

4. Proteus 仿真 51 单片机的避坑清单:温度计项目 5 个高频故障

4.1 现象:ADC0804 的 CLK 端没有波形,读数一直为 0

运行仿真后,屏幕上无论怎么调电位器,温度都停在 0℃。示波器夹到 ADC0804 的 CLK 引脚上,看不到任何方波。

原因是 ADC0804 不是静态芯片,它内部没有振荡器,必须由外部提供时钟才能完成逐次逼近转换。Proteus 默认放置 ADC0804 时不会自动带时钟源,很多人忽略了这一点。

解决办法是在元件库放一个 CLOCK 元件,频率设 1MHz,输出接到 ADC0804 的 CLK IN 引脚上。接好后用示波器能看到 CLK 端有规则方波,读数就正常了。如果放好 CLOCK 后仍然没有波形,检查一下 CLOCK 的频率单位,Proteus 默认单位是 Hz,1MHz 要填成 1000000,不是 1。

4.2 现象:1602 液晶上电全是黑块,清屏指令没反应

1602 显示两行黑方块,看起来像是通电了,但不管怎么发指令都没内容。

典型原因是 VO 对比度引脚悬空。LM016L 模型的 VO 如果不接电位器,对比度电压可能落在显示异常区间,导致满屏方块。另一个可能原因是初始化时序里 E 脉冲宽度不够,Proteus 仿真速度太快,旧的延时函数还没有结束就执行了下一步。

解决办法是给 VO 接一个 10kΩ 电位器,滑动端调到输出电压大约 0.5~1V 的位置,黑块会立刻消失。同时在 lcd_cmd 和 lcd_data 里把延时函数调大,比如循环次数从 200 改成 2000,确保 1602 内部控制逻辑有时间完成操作。如果实物上也出现同样现象,优先检查 VO 电压,再检查 RW 是否接低电平。

4.3 现象:温度读数跳变,个位和十位来回闪

电位器完全不动,但显示温度在几个值之间跳,比如 25℃、26℃、27℃ 来回变。

原因是采样周期太短,ADC0804 转换结果本身有量化误差,同时 Proteus 仿真里电位器阻值微小扰动都会被 ADC 放大。加上主循环里如果每次读取后立刻刷新液晶,人眼就会看到跳变。

解决办法是连续采样 3 次取中间值,再做显示。取中间值比取平均值更能抗脉冲干扰,代码量也很小。显示刷新周期放到 200ms 以上,主循环里加一个延时,不要每毫秒都刷一次液晶。仿真里看不到真实温度波动,所以这个“平滑”逻辑是为了让显示稳定,也顺便为实物上的噪声预留处理空间。

4.4 现象:电位器阻值明明调到 10kΩ,显示温度却对不上 25℃

理论上 10kΩ 对应 NTC 在 25℃ 的标称阻值,但显示结果偏差很大。

原因有两层。第一,分压输出是 VCC × R_fix / (R_ntc + R_fix),阻值和电压不是线性关系;第二,NTC 阻值与温度是指数关系,你不能用“电位器旋到一半就是 50℃”这种方式去验证程序。很多第一次做的人把查表步骤省了,直接用 ADC 读数乘一个系数当成温度,这在窄量程内还能凑合,放到 0~100℃ 就会严重失真。

解决办法是严格走“ADC 读数 → 温度表”的映射。把 NTC 的分压输出换算成电压,再用 B 值公式离线计算温度表,最后在程序里查表。校准的时候,记住温度表是由 R25 和 B 两个参数决定的,如果你的热敏电阻实际 B 值是 3600,而程序里按 3950 算,中段测温偏差会达到好几度,这点实物上特别明显。

4.5 现象:仿真运行卡顿,旋转电位器后温度半天才更新

不是死机,而是拖动电位器滑块,画面反应迟钝,整机运行速度明显变慢。

原因是 Proteus 在交互式仿真时,对 ADC0804、电位器这类模拟模型要做逐时间步的数值迭代,整个模拟子网的每个节点电压都要重新算。仿真里挂了一堆探针和波形分析工具时更慢。

解决方法是先把无关的探针全部删掉,尤其是夹在模拟节点上的电压表、示波器探头。再把主循环里的 LCD 刷新频率降下来,200~300ms 刷一次已经足够。最后一个技巧是把 AT89C51 的时钟频率保持 11.0592MHz,不要为了“跑更快”改成 40MHz,仿真速度反而会因步长细化变得更慢。这些操作都不会影响逻辑正确性,但能显著改善联调体验。

5. 进阶:用虚拟示波器和串口终端验证温度计

5.1 用虚拟示波器盯住 ADC 时序

仿真跑通只是第一步,真正要验证的是 ADC0804 和单片机之间的握手时序。把 Proteus 虚拟示波器的两路探头分别接到 ADC_WR 和 ADC_INTR 引脚,运行后暂停仿真,观察波形:WR 变成高电平后,INTR 应当在一小段延时后被拉低;RD 拉低时,数据线上的数据应当处于有效状态。

我在实际调试中吃过亏:代码里把 RD 拉低和读 P1 写在同一行,仿真里碰巧能跑通,但实物上就是偶尔读错值。后来养成习惯,RD 拉低后加一个短暂延时再读数据,这样无论仿真还是实物都更稳。时序波形是最直观的验证手段,比反复看代码猜问题高效得多。

5.2 用串口把温度发出来

1602 显示只是温度计的一种输出方式。做实物时,你可能想把温度传到上位机或者用串口调试助手观察。在仿真里加一个 Virtual Terminal 就能先验证串口逻辑。

void uart_init(void) { SCON = 0x50; // 串口模式 1,允许接收 TMOD = 0x20; // 定时器 1 模式 2,8 位自动重装 TH1 = 0xFD; // 11.0592MHz 下对应 9600 波特率 TL1 = 0xFD; TR1 = 1; } void uart_send_char(unsigned char c) { SBUF = c; while (!TI); TI = 0; } void uart_send_temp(unsigned char t) { uart_send_char('0' + t / 10); uart_send_char('0' + t % 10); uart_send_char(0x0d); // 回车 uart_send_char(0x0a); // 换行 }

Proteus 里从右侧虚拟仪器菜单选 Virtual Terminal,把单片机的 TXD 接到终端 RX,设置波特率 9600,运行后就能看到温度数据流。这套串口逻辑在实物上同样可用,而且帮你提前确认了 TH1 初值是否和晶振匹配。如果波形不对,先查晶振频率和 TH1 的配对关系,而不是怀疑代码逻辑。

我最早做这个仿真时,ADC0804 没接时钟,硬生生调了半个晚上,最后在虚拟示波器上看到 CLK 引脚一条直线才发现问题。从那以后我学到一个习惯:无论仿真还是实物,先把每个芯片的必要时钟和使能引脚逐个核对一遍,再回头怀疑软件逻辑。希望帮到你。

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