简介:这是一套基于51单片机的温度测量与超限报警课设方案,采用汇编语言驱动DS18B20采集温度,通过1602液晶实时显示,并利用定时器配合实现阈值检测与报警,适合单片机初学者、课程设计或毕业设计参考。压缩包共885个文件,约28.97MB,主要包含hex烧录文件、asm汇编源码、C语言源文件、工程配置以及jpg/png原理图和txt说明文档,便于对照源码、编译烧录与查阅设计思路。项目代码完整且经过老师验收,实际可运行,读者可直接移植或在此框架上改进阈值设置、显示界面与报警逻辑。资源已有2022人学习下载,无论用于快速完成课设演示,还是深入理解51单片机中断、定时器与单总线通信,都具备不错的参考价值。
1. 用51单片机做温度显示与报警,先想清楚这套系统的边界
温度显示并报警系统是51单片机课程设计里出镜率最高的题目之一,但真正做完的人都知道,难点不在“读温度”本身,而在于把DS18B20的单总线时序、LCD1602的显示刷新、按键设定阈值和蜂鸣器报警这几块拼成一个长时间不崩的整体。我见过不少方案温度能显示、报警也写了,可一进设置界面就回不来,或者温度在阈值附近时蜂鸣器哒哒乱响。这些坑基本都集中在工程细节上,和51单片机本身关系不大。
这套系统适合两类人:一类是做51单片机课程设计的学生,需要一份从原理图、引脚表到Keil源码都能直接落地、Proteus能跑仿真的完整方案;另一类是玩过数码管和流水灯、第一次想接触单总线协议和按键状态机的爱好者。用STC89C52这类51内核芯片跑这个任务,性能是富余的,12MHz晶振下DS18B20要求的微秒级时序完全卡得住,不需要上实时操作系统,一个裸机主循环加几个状态判断就够。
选型结论先放在前面:测温用DS18B20,显示用LCD1602,报警用有源蜂鸣器,阈值用三个独立按键设定,整套系统在Proteus里可以完整仿真。下面按硬件接线、单总线时序、报警逻辑、仿真验证四步展开,每步都会给出对应的引脚表、参数和可编译的代码片段,最后再讲Proteus里怎么验证波形和排除故障。
2. 温度报警系统的 51 单片机硬件设计选型与接线
2.1 测温器件选型:DS18B20 为什么比热敏电阻更省事
常见做法是拿热敏电阻加ADC芯片来做测温,NTC热敏电阻本身是非线性器件,想要在全温度范围得到0.5℃以内的读数,要么做分段查表标定,要么在电路上加运放做调理,工程量和调试时间都会明显拉长。做单点和短距离的测温显示,DS18B20是更稳的选择:它把测温、AD转换、单总线通信全部集成在一个三脚封装里,出厂前校准完毕,51单片机只需要一根IO口线就能读到数字结果。
DS18B20的工作范围是-55℃到+125℃,12位分辨率模式下最小量化单位0.0625℃,典型精度±0.5℃,这个量级对温度告警场景绰绰有余。它的局限也要说清楚:一次12位转换最长需要750ms,连续快速采样不可取;单总线一根线串多个器件时还要处理ROM编号,课程设计里绝大多数情况是单点测温,直接跳过ROM寻址即可。相比之下DHT11虽然便宜,但温度精度只有±2℃,用在报警系统里阈值设得再准,判决依据本身就不够可靠。
所以在51单片机温度显示并报警这个题目里,DS18B20替代热敏电阻是这类项目最常见的升级路线,代码量和硬件复杂度都更低。新手容易误以为“数字传感器一定比模拟方案复杂”,实际正好相反,需要写代码的只有时序驱动那一层。
2.2 DS18B20、LCD1602、蜂鸣器的引脚分配与最小系统接线
以STC89C52为例,温度报警系统的引脚分配可以按下表规划。DS18B20放在P1.0,LCD1602数据线占P0整口,三个独立按键用P1.1到P1.3,蜂鸣器驱动脚选P2.0,剩余IO口留给扩展功能。
| 模块 | 信号 | 51单片机引脚 | 连接说明 |
|---|---|---|---|
| DS18B20 | DQ | P1.0 | 单总线数据线,接4.7k上拉电阻到VCC |
| DS18B20 | VDD / GND | +5V / GND | 外部供电方式,VDD经0.1uF电容就近去耦 |
| LCD1602 | RS / RW / EN | P3.5 / P3.6 / P3.4 | RW可固定接地,只写不读 |
| LCD1602 | D0~D7 | P0.0~P0.7 | P0口开漏,必须外接10k上拉排阻 |
| 蜂鸣器 | 正极 | +5V | 选用5V有源蜂鸣器,低电平驱动发声 |
| 蜂鸣器 | 负极 | S8050三极管C极 | B极串1k电阻接P2.0,E极接地 |
| 按键 | SET / UP / DOWN | P1.1 / P1.2 / P1.3 | 另一端接GND,IO口内部或外部上拉 |
单片机最小系统还包括11.0592MHz晶振加两个30pF负载电容、10uF电解电容和10k电阻组成的上电复位电路。晶振频率选11.0592MHz是为了将来接串口时波特率好算,温度报警系统本身对晶振精度没有苛刻要求,12MHz也一样跑。
Proteus仿真时注意两个元件名称:LCD1602在元件库里搜LM016L,它是同型号的Proteus模型;P0口的上拉用元件RESPACK-8排阻,一端统一接VCC。DS18B20模型默认显示25℃,仿真运行时可以手动改它的温度值来模拟升温和降温。
2.3 LCD1602 初始化代码与三条容易踩的接线细节
LCD1602是典型的HD44780兼容控制器,上电后先要等它完成内部自检,再按固定顺序发送功能设置指令。下面的初始化代码在Keil C51下可直接编译,用到了两个基础函数lcd_write_cmd和lcd_write_data,区别只在RS电平:命令模式RS=0,数据模式RS=1。
void lcd_write_cmd(unsigned char cmd) { RS = 0; // 命令模式 RW = 0; // 写模式,RW接地时可省略此句 P0 = cmd; EN = 1; _nop_(); _nop_(); EN = 0; // EN高脉冲下降沿锁存数据 delay_us(40); // 大部分指令执行时间都在37us以内 } void lcd_write_data(unsigned char dat) { RS = 1; // 数据模式 RW = 0; P0 = dat; EN = 1; _nop_(); _nop_(); EN = 0; delay_us(40); } void lcd_init(void) { delay_ms(15); // 等待LCD1602上电内部复位 lcd_write_cmd(0x38); // 8位数据线、2行显示、5x7点阵 lcd_write_cmd(0x0C); // 开显示、不显示光标 lcd_write_cmd(0x06); // 写入后地址自动加1,画面不滚动 lcd_write_cmd(0x01); // 清屏,清屏指令需要1.64ms delay_ms(2); }接线层面最容易翻车的三处:第一,P0口内部是开漏结构,不加上拉电阻LCD数据线上全是低电平,显示必然异常;第二,有源蜂鸣器工作电流超出单片机IO口驱动能力,必须用三极管放大,直接接引脚声音小且可能烧口线;第三,DS18B20的DQ上拉电阻不可省略,线长超过半米时把4.7k换成2.2k,否则长线寄生电容会让时序边沿变缓,出现间歇性读不到器件的情况。
另外,Proteus里的BUZZER元件有正负极性标注,仿真时蜂鸣器不响先查三极管和极性,不要只盯着代码看。按键引脚在无外部上拉时,建议在初始化里把P1口相关位写成1,利用内部上拉,实物和仿真行为才一致。
3. 51单片机读取 DS18B20 温度:单总线时序与源码实现
3.1 单总线握手:复位脉冲、存在脉冲和时隙宽度
DS18B20用的是单总线协议,一根DQ线既要传数据又要传时序参考,所以所有操作都由主机发起,器件被动响应。每次通信以复位开始:主机把DQ拉低至少480us再释放,DS18B20检测到这个下降沿后,会在60到240us窗口内把一个低电平放在总线上,这个低电平叫存在脉冲。主机在释放总线后约70us时读一次电平,读到0说明器件在线,读到1说明总线上没有DS18B20或接线有误。
数据读写用的是时隙机制,每位占60到120us。写1时隙是拉低后马上释放让总线回高,写0时隙是拉低后保持满60us再释放;读时隙由主机拉低至少1us后释放,然后在第15us附近采样电平,DS18B20在这个时刻把当前位的电平放到总线上。时序参数汇总如下:
| 时序段 | 最短时间 | 最长时间 | 关键点 |
|---|---|---|---|
| 复位低电平 | 480us | 不限 | 太短则器件不识别 |
| 存在脉冲 | 60us | 240us | 主机在释放后约70us采样 |
| 写0时隙 | 60us | 120us | 全程保持低电平 |
| 写1时隙 | 1us | 15us | 拉低后尽快释放 |
| 读时隙采样点 | 约15us | 15us前后 | 边沿后第15us读数最稳 |
这些时间要求只有下限是刚性的,上限相对宽松,所以51单片机用普通延时函数就能满足,不需要定时器介入。要注意的是12MHz晶振加12T内核(STC89C52默认)时,一条_nop_()约等于1us,延时函数多跑几条指令关系不大;如果换成STC15系列等1T内核,同样代码时序会整体加快,延时函数需要按比例放大,否则会出现复位时序不足。
3.2 复位、读写字节的最小实现
以下代码是DS18B20驱动的三个基础函数,写字节和读字节都按“低位在前”的顺序逐位处理,这也是所有单总线器件共通的约定。
sbit DQ = P1^0; // 单总线数据线 bit ds18b20_reset(void) { bit presence; DQ = 0; delay_us(500); // 主机拉低至少480us发起复位 DQ = 1; // 释放总线 delay_us(70); // 等待60~240us内的存在脉冲 presence = DQ; // 采样:0=器件应答,1=无器件 delay_us(400); // 让整个复位时隙完整结束 return presence; } void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; // 每个时隙都从拉低开始 _nop_(); DQ = dat & 0x01; // 低位移到总线 delay_us(60); // 保持时隙宽度 DQ = 1; // 释放,准备下一位 dat >>= 1; } } unsigned char ds18b20_read_byte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; DQ = 0; // 主机发起读时隙 _nop_(); DQ = 1; // 释放,等待器件控制总线 _nop_(); _nop_(); // 走到第15us采样点附近 if (DQ) dat |= 0x80; // 读到高电平则该位置1 delay_us(45); // 等时隙结束再进入下一位 } return dat; }写字节时把数据位放到总线上的时机很关键:必须是拉低后的1到15us内完成,因为DS18B20以这个下降沿开始计时,15us之后总线电平才被器件采样。用dat & 0x01判断当前位再移位,整个函数的执行路径固定,避免用if语句产生不同分支、带来不确定的时序偏移。
读字节函数里两个_nop_()的作用是凑出15us采样点,12MHz 12T内核下延时约2us,加上DQ=0到DQ=1的操作耗时,读数时刻落在器件要求的窗口内。如果换成1T内核,这两个_nop_()需要加量,否则采样过早,高位数据容易读成低电平。
3.3 完整测温流程:启动转换与读取暂存器
一次完整的测温要发送两条命令:0x44启动温度转换,0xBE读暂存器。单点应用可以用0xCC跳过ROM寻址,省去读取64位序列号的过程。读取时暂存器的第0、1字节分别是温度值的低字节和高字节,后7个字节是报警阈值、配置字和CRC,做显示报警只用前两个字节就够。
int ds18b20_get_temp(void) // 返回温度值,单位0.1℃ { unsigned char tl, th; int raw; ds18b20_reset(); // 复位并检查器件在线 ds18b20_write_byte(0xCC); // 跳过ROM:单点采集可省去寻址 ds18b20_write_byte(0x44); // 启动温度转换 delay_ms(750); // 12位分辨率最大转换时间750ms ds18b20_reset(); // 转换结束后重新复位 ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器,从字节0开始 tl = ds18b20_read_byte(); th = ds18b20_read_byte(); raw = (th << 8) | tl; // 拼出16位原始值 if (raw & 0x8000) { // bit15为1表示负温度 raw = ~raw + 1; // 取补码绝对值 return -(long)raw * 625 / 1000; // ×0.0625再×10,整数运算 } return (long)raw * 625 / 1000; }原始值换算成摄氏度的公式是数值乘以0.0625。为了在LCD上显示一位小数,我习惯把结果统一放大10倍存储,即乘以0.625。这里故意不用浮点数,写成(raw * 625 / 1000)的整数运算,是因为Keil C51里float运算会引入大段的浮点库代码,51单片机本身没有硬件浮点单元,一个乘除法就要吃掉几十个机器周期,还要多占几百字节的代码空间。课程设计跑得起来,但追求简洁和可读性时,整数方式更合适。
转换等待有两种写法:固定延时750ms最简单也最稳;另一种是转换命令发出后用while(!DQ)等待,因为DS18B20在转换期间会把DQ拉低,转换结束自动释放。后者的平均等待时间更短,但风险在于如果器件没接好或总线浮空,程序会死等在这个循环里出不来。我给课设方案的默认做法是固定延时,把等待时间做成宏,后续想优化再换。
负温度处理容易被忽略:低温时原始值的符号位是1,必须按补码取反加一得到幅度,再在前面补负号。DS18B20支持到-55℃,如果只做正温度范围的课程设计,这一段可以裁掉,但写完整能从源码层面杜绝暖冬测试正常、冬天一冻就显示异常的尴尬。
3.4 显示85.0和-0.1时先怀疑什么
DS18B20上电后暂存器里有一个默认值0x0550,换算过来正好是85.0℃。如果代码跳过了0x44转换命令直接读暂存器,或者0x44发出了但等待时间不够,读回来的就是85.0℃。这个现象是判断“转换链路没走通”的典型标志。看到85.0不要怀疑传感器坏了,先查函数调用顺序和延时。
还有一种常见现象是显示-0.1:总线空闲被上拉电阻拉高时,读字节读到的是全1,拼出来raw = 0xFFFF,按负温度处理换算正好是-0.0625,放大10倍后显示-0.1。看到这个值意味着复位或读时序有问题,器件根本没把数据放到总线上,优先检查DQ上拉电阻、接线和复位函数返回的存在脉冲。
4. 报警阈值设定、按键扫描与蜂鸣器联动的完整逻辑
4.1 三键设定阈值:按键功能分配与消抖实现
温度报警系统的交互部分由三个按键完成,SET进入设置模式和切换设置对象,UP和DOWN调整当前阈值。设置流程做成一个两层状态机:正常模式下阈值不显示调整入口,按下SET后先进入上限设置状态,LCD第二行显示当前上限值;再按SET切到下限设置状态;第三次按SET退出设置,回到正常显示。所有调整立即生效,主循环里的报警判断用的是两个全局变量。
| 按键 | 正常模式 | 上限/下限设置模式 |
|---|---|---|
| SET | 进入上限设置 | 按顺序切换:上限→下限→退出 |
| UP | 无操作 | 当前阈值加0.5℃ |
| DOWN | 无操作 | 当前阈值减0.5℃ |
按键处理最基础的坑是抖动和长按连跳。机械按键按下和松开时电平会在几毫秒内反复跳变,一次按压可能被识别成多次。硬件上可以加RC滤波,课程设计里更常见的做法是软件消抖:第一次读到按下后延时10ms再确认一次,两次都为低才认定为有效按键。
#define KEY_NONE 0 #define KEY_SET 1 #define KEY_UP 2 #define KEY_DOWN 3 unsigned char key_scan(void) { static unsigned char last_key = KEY_NONE; unsigned char cur = KEY_NONE; delay_ms(10); // 消抖延时,等待电平稳定 if (KEY_SET == 0) cur = KEY_SET; else if (KEY_UP == 0) cur = KEY_UP; else if (KEY_DOWN == 0) cur = KEY_DOWN; if (cur == last_key && cur != KEY_NONE) return KEY_NONE; // 按住未松手,不重复触发 last_key = cur; return cur; // 只有按下瞬间返回有效键值 }这个函数把一个常见误区规避掉了:如果不记录上次状态,每10ms扫一次就会在按键持续按住期间返回几十上百次有效值,设置温度时按一下UP要等它自己跳半天。把上一次扫描结果存成静态变量,只有“从无键到有键”的边沿才返回键值,就实现了按下一次只响一次的效果。消抖延时放在扫描函数开头,10ms对机械开关足够。
阈值范围也要做限幅,下限不让设到-55℃以下,上限不让超过125℃,并且强制上限至少高出下限1℃。这个约束在退出设置模式时统一检查,避免用户把上下限设反导致报警逻辑永远成立。
4.2 带上下限和回差的报警核心函数
报警逻辑不能写成裸的“超限就响、回界就停”,原因很实际:DS18B20在12位分辨率下的最小量化单位是0.0625℃,当温度恰好停在阈值附近时,读数会在上下两个量化值之间来回跳动,直接比较会产生蜂鸣器快速通断的“哒哒”声。解决方案是加回差,也叫迟滞比较,工程上沿用施密特触发器那一套思路。
#define HYST 10 // 回差1.0℃,以0.1℃为单位 bit buzzer_on = 0; // 当前报警状态 void alarm_update(int temp) // temp为当前温度,单位0.1℃ { if (!buzzer_on) { if (temp > high_limit || temp < low_limit) buzzer_on = 1; // 越限则触发 } else { if (temp <= high_limit - HYST && temp >= low_limit + HYST) buzzer_on = 0; // 回落到区间内且留出回差才解除 } BEEP = buzzer_on ? 0 : 1; // 有源蜂鸣器低电平驱动,0发声 }触发条件用的是严格大于和严格小于,解除条件则额外扣除1℃回差。以35℃上限为例,升到35.1℃报警,之后温度要跌回34.0℃以下才停止,中间这1℃的区间不会再次触发。下限方向同理。回差大小不是固定的,对温度控制这类慢变量,1℃已经是比较保守的设置,如果系统装在风机直吹的环境里还嫌抖,可以加到2℃。
有源蜂鸣器内部自带振荡源,只要给直流电平就发声,所以驱动函数里只需要置高低电平,不需要输出PWM方波。低电平触发的原因见第2章的三极管接法,发射极接地、集电极接蜂鸣器负极,基极高电平时三极管导通,蜂鸣器负极被拉到地形成回路。代码里出现BEEP = 0就是响,BEEP = 1是停,这个约定要跟硬件接法保持一致,反了就会变成温度正常时响、越限时安静。
回差机制还有一个附带好处:报警解除时的阈值和触发时不同,主循环里不会出现临界温度下状态机在两个分支间反复横跳,整个系统表现为一次越限对应一次明确的报警事件,而不是一串连续的鸣响。
4.3 主循环刷新节奏与定时器时基思路
主程序的做法是用一个while(1)死循环,顺序执行“读温度、刷显示、判报警、扫按键”四个步骤。DS18B20一次转换750ms,整个循环天然被卡在750ms的节奏上,人眼刷新LCD完全够用,按键扫描的响应也在可接受范围内。
void main(void) { int temp_x10; lcd_init(); lcd_write_string(0, 0, "TEMP: --.- C"); lcd_write_string(1, 0, "OK"); while (1) { temp_x10 = ds18b20_get_temp(); // 阻塞约750ms完成一次采集 lcd_show_temp(temp_x10); // 更新第一行温度显示 alarm_update(temp_x10); // 更新报警状态 if (key_scan() == KEY_SET) enter_set_mode(); // 进入阈值设置状态机 } }如果觉得750ms的按键响应太钝,可以把温度读取拆成两步:发完0x44转换命令后不等待,先回去扫描按键和刷新上次温度,借助一个标志位判断“转换完成没有”,完成后再发0xBE读取。这样循环周期降到几十毫秒,按键秒响应。课程设计里多数评委看到阻塞式写法都能接受,但能在答辩时说清楚阻塞和非阻塞的取舍,属于加分项。
更规范的做法是引入定时器做时基,这也是51单片机定时器在温度报警系统里的典型用法:定时器0配置成50ms中断一次,中断里只做按键消抖计时和设置界面闪烁计时,主循环专注于温度读取和显示。好处是主循环里的delay_ms全部消除,按键消抖从轮询变成了带时间基准的状态判断,代码扩展性更好,后续想加LED状态灯、串口打印都方便。
设置界面enter_set_mode内部同样用一个循环,循环里持续扫描按键并修改high_limit或low_limit,同时刷新LCD第二行。退出时做一次限幅检查,再回到主循环。注意设置模式下温度采集会暂停,这是有意的:让温度值停在设置前的时刻,避免设置途中显示频繁跳动干扰操作。退出后下一轮循环自动恢复新温度显示。
5. Proteus 仿真 51 单片机温度报警系统的验证与故障排查
5.1 Proteus 元件连接与仿真时的温度模拟操作
在Proteus ISIS里搭建这套系统时,单片机选AT89C51或AT89C52,LCD1602用LM016L模型,DS18B20在元件库搜索DS18B20直接放置。P0口务必放上RESPACK-8排阻,一端接VCC,另一端分别接到P0.0到P0.7,漏了这一步LCD会一直白屏。DS18B20的DQ同样要放一个上拉电阻,Proteus里省略上拉有时也能跑通,但实物必挂,仿真就按实物的标准接。
温度模拟是Proteus仿真51单片机温度报警系统的核心操作:双击DS18B20元件,把Temperature属性从默认的25改成其他值,点运行后主循环下一轮就能读到新温度。想演示报警过程,把温度改成45,蜂鸣器应当在一次转换周期内动作;再把值调回25,观察回差逻辑是否正确解除报警。用这种方法可以快速验证阈值边界,不用真的拿烙铁去烫传感器。
需要提醒的是,Proteus里的BUZZER是直流蜂鸣器模型,瞬间压降和实际器件有差异,仿真里能响不代表实物的驱动电路一定够劲,三极管选型还得按实物电流核算。STC单片机内部EEPROM做阈值断电保存在Proteus里仿真支持不完整,建议实物阶段再验证那一部分。
5.2 用虚拟示波器检查 DQ 波形,定位单总线时序错误
Proteus左侧工具栏的虚拟仪器里可以调出示波器,把通道A接到DS18B20的DQ引脚,地接GND,运行后能看到一段段的时序波形。先找复位段:一个约500us的低电平脉冲,紧跟一个短低脉冲,那就是存在脉冲。存在脉冲消失说明器件没有应答,优先查上拉电阻和连线,而不是查代码。
再放大波形看写时隙:每个时隙约60us,写0的时隙全程低电平,写1的时隙先低后高,高度清晰。如果看到时隙宽度严重超长或总线一直拉低不释放,多半是延时函数在主频配置上出了问题,检查Keil工程里的晶振频率设置和单片机型号是否一致。这段波形是判断单总线驱动是否正确的标准依据,比打印数值可靠得多。
5.3 温度报警系统常见故障对照表
把项目里反复出现的故障整理成对照表,遇到问题时按行排查效率最高:
| 故障现象 | 判断方向 | 优先排查项 |
|---|---|---|
| LCD有背光但无字符 | 初始化未完成或对比度失调 | 调对比度电位器,检查RS/EN接线 |
| 显示85.0℃ | 读到上电默认值 | 确认0x44转换命令已发出且等待750ms |
| 显示-0.1℃ | 总线浮空,读到全1 | 检查DQ上拉电阻和DS18B20供电 |
| 温度正常但乱报警 | 阈值临界抖动 | 检查报警函数是否带回差 |
| 按键一次触发多次 | 没有松手检测 | 用4.1节的边沿检测扫描函数 |
| 蜂鸣器不响 | 驱动电路或极性反 | 查三极管接法和BUZZER极性 |
| 仿真正常实物异常 | 晶振和上拉差异 | 核对12T/1T内核时序差异 |
这七类现象覆盖了从显示到报警的全部链路。做实物时还有一个顺手的技巧:把DS18B20的DQ单独引出一根测试线,示波器探头夹在上面,温度显示和报警都正常时记住这个波形,之后任何一次改版都先拿波形对照,比每次烧录后看现象猜原因快得多。
本文还有配套的精品资源,点击获取