AT89C51温度检测报警系统设计:DS18B20时序与数码管扫描实践

发布时间:2026/9/12 16:31:33
AT89C51温度检测报警系统设计:DS18B20时序与数码管扫描实践 简介基于AT89C51与DS18B20的温度检测报警系统设计是一份经过指导教师审核的课程设计项目非常适合计算机、电子信息工程等专业在校学生作为期末大作业或毕业设计参考同时也可供嵌入式爱好者自学使用。项目通过AT89C51单片机的单总线接口与DS18B20数字温度传感器通信实时采集环境温度并与设定阈值进行比较一旦超出范围便驱动蜂鸣器和LED指示灯发出报警同时利用LCD1602液晶屏显示当前温度值。压缩包一共36个文件整体大小约97KB包含完整的C语言源文件.c、头文件.h、Keil工程文件.uvproj、原理图.dsn、烧录文件.hex以及调试生成的obj/lst等辅助文件文件类型齐全目录结构清晰便于按模块学习。已有36人浏览学习。通过该项目读者可以加深对单片机初始化、DS18B20驱动时序、数据解析、报警逻辑以及软硬件联调的理解也能学会基本的电子电路连接与电源管理是一份能够覆盖项目设计到测试全流程的实用参考资料。1. 从 AT89C51 温度检测报警系统看毕设课设里最值钱的三个环节做基于 AT89C51 的温度检测报警系统硬件上不过是传感器、单片机、显示、报警四个模块代码核心逻辑也就两百行左右。但每年毕设答辩这个题目仍然能刷掉一批人原因不在电路复杂而在于三个环节总被低估DS18B20 的单总线时序、数码管动态扫描的刷新节奏、报警阈值的无级可调设计。第一个环节决定你能不能读到温度第二个决定读到的温度能不能稳定显示出来第三个决定老师问“阈值怎么改”时你是有方案还是只能回去改代码重新烧录。AT89C51 是 12MHz 主频下典型单周期指令约 1µs 的老器件没有 I²C 硬件外设、没有内部 ADC、没有中断优先级配置的复杂寄存器它逼着你把时序用延时函数一个周期一个周期去抠这正是课设题目选它的意义。如果你手头的芯片是 STC89C52指令兼容代码可以直接搬但要注意 STC 的烧录电压和 AT89C51 的 12V 高压编程不同别把芯片烧了。本文按 Proteus 仿真 实物验证两条线走从传感器时序到报警阈值设计最后落到精度补偿和答辩常问的坑。2. DS18B20 温度采集单总线时序的参数取值与容错2.1 为什么选 DS18B20 而不是热敏电阻或热电偶温度检测报警系统的传感器选型直接决定后级电路复杂度。热敏电阻需要电桥 运放 ADC0809 才能把电阻变化变成数字量而且非线性校准曲线要自己建表热电偶需要冷端补偿芯片对焊接和 PCB 布局要求高。DS18B20 的优势是单总线协议一根 I/O 线既传时钟又传数据测温范围 -55°C ~ 125°C12 位分辨率下精度 ±0.5°C而且内置 64 位 ROM 序列号以后扩成多点多机采集有天然扩展性。供电方式上常见的是寄生供电只有两根线DQ 和 GND和外加 VDD 供电三根线。毕设板子建议直接用三根线接法省去寄生供电时需要 DQ 线在转换期间被强拉高提供充电电流的麻烦电路稳定性和程序调试难度都低一个档次。// 单总线初始化返回 1 表示存在设备 bit ds18b20_reset(void) { bit presence; DQ 1; // 释放总线 delay_us(2); // 给总线一个恢复时间 DQ 0; // 主机拉低复位脉冲 delay_us(500); // 复位脉冲保持 480-960us DQ 1; // 释放总线等待从设备应答 delay_us(60); // 等待 60-240us看 DS18B20 是否拉低 presence DQ; // 读取存在脉冲0 表示设备响应 delay_us(420); // 整个复位时序收尾 return ~presence; // 返回 1 表示检测到设备 }2.2 读写时序位操作的延时边界DS18B20 的读时序和写时序都是主机在拉低总线后开始区别在于拉低后的延时长度和采样点位置。写“1”是拉低后立即释放写“0”是保持拉低整个时隙60µs~120µs读时序则是主机拉低 1µs 后释放然后在 15µs 内采样 DQ 电平。用 12MHz 晶振的 AT89C51一个 NOP 大约是 1µs延时函数最稳妥的方式是用两层嵌套循环避免用 Keil 优化等级去猜 NOP 数量。void ds18b20_write_bit(bit b) { DQ 0; // 拉低总线启动写时隙 if (b) { DQ 1; // 写 1早期释放 delay_us(60); } else { delay_us(60); // 写 0全程保持低电平 DQ 1; } delay_us(2); // 两个位时隙之间的恢复时间 }这里有个新手必踩的坑Keil C51 的_nop_()只是内嵌一条 NOP 指令但开启Optimization Level 9后编译器可能把你自己写的空白循环优化掉。所以延时函数必须用volatile修饰循环变量或者直接关闭该函数的优化#pragma OPTIMIZE(0)。我一般会把延时函数单独放一个delay.c文件里并把相关变量全部声明为volatile unsigned char这样不管优化等级怎么调时序都不会被编译器自作主张改掉。2.3 温度读取流程ROM 跳过与转换等待单总线协议规定主机要先发复位脉冲再发 ROM 命令0xCC 跳过 ROM因为系统里只有一颗传感器再发功能命令0x44 启动温度转换0xBE 读暂存器。转换时间是重点12 位分辨率下最大 750ms这期间 DQ 线处于忙状态如果你在启动转换后立刻去读读到的是上一次的转换结果造成显示跳跃。常见做法是启动转换后把 750ms 拆成零碎时间片去调度比如每 200ms 查询一次忙标志而不是干等。但毕设项目里为了代码直观通常直接用delay_ms(750)阻塞等待。建议在答辩展示时说明阻塞等待的弊端并现场改成非阻塞查询版本这是一个明显的加分点。unsigned int ds18b20_read_temperature(void) { unsigned char low, high; unsigned int temp; 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); // 读暂存器共 9 个字节 low ds18b20_read_byte(); // 第 0 字节温度低字节 high ds18b20_read_byte(); // 第 1 字节温度高字节 temp (high 8) | low; // 16 位温度原始值 return temp; }读回来的 16 位原始值低 4 位是小数部分高 5 位是符号位。正温度直接右移 4 位得到整数部分负温度需要取反加一再右移。大部分毕设只做 0°C~99°C 量程可以忽略负温度处理但程序里保留符号位判断分支会让代码完整性更好。小数部分转 BCD 码时常用做法是把低 4 位乘以 0.0625得到 0.0~0.9 的小数值再拼到整数后面显示一位小数。3. 最小系统电路与显示报警模块的驱动选型3.1 晶振、复位、电源三处不起眼却致命的细节AT89C51 的最小系统由晶振电路、复位电路、电源电路三部分构成。晶振用 12MHz 配两个 33pF 负载电容复位电路用 10µF 电解电容加 10kΩ 电阻组成上电复位。这三处是 Proteus 里仿真永远报不出错、但实物必炸的地方。晶振旁边两个负载电容不是随便接的取值和晶振的负载电容参数有关常见 12MHz 晶振配 30pF 左右。电容太大起振慢太小频率偏。复位电路的关键是 RST 引脚需要保持至少 2 个机器周期的高电平10µF 电容充电到 2V 阈值的时间足够满足。实物板子如果出现“上电后程序不跑按复位键才正常”八成是复位电容容值偏小或焊盘虚焊。电源部分很多课设板子直接拿 USB 转 TTL 模块的 5V 输出供电电流够用但纹波大DS18B20 的数据线上容易叠加噪声导致读到的温度跳动。学生板子可以接受但如果是自己要长期用的系统建议加 100µF 电解电容 104 陶瓷电容去耦且放在 DS18B20 供电引脚附近而不是电源入口。3.2 LED 数码管动态扫描 vs LCD1602 显示哪种更适合报警场景显示模块两种选择LED 数码管动态扫描或 LCD1602 液晶。温度报警系统里数码管的优势是亮度高、远距离可读适合报警时快速扫一眼温度数值LCD1602 的优势是能显示“TEMP: 25.5C ALARM”这类带单位带状态的完整文案。从答辩效果看LCD1602 更容易做出“像产品”的界面但动态扫描数码管更能体现你对单片机定时器的理解。void display_scan(void) { static unsigned char pos 0; unsigned char code seg_code[] {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90}; unsigned char dig_code[] {0xFE,0xFD,0xFB,0xF7}; // 位选低电平选中 P0 0xFF; // 消隐防止上一位残影 P2 dig_code[pos]; // 位选切换 switch(pos) { case 0: P0 seg_code[temp_display / 1000 % 10]; break; case 1: P0 seg_code[temp_display / 100 % 10]; break; case 2: P0 seg_code[temp_display / 10 % 10] 0x7F; // 带上小数点 break; case 3: P0 seg_code[temp_display % 10]; break; } pos (pos 1) 0x03; // 循环切换 }动态扫描的核心参数有两个扫描周期和消隐处理。4 位数码管每位点亮时间取 2ms~4ms一轮扫描 8ms~16ms刷新率约 60Hz~125Hz肉眼看不到闪烁。低于 50Hz 会明显闪烁高于 200Hz 也没必要因为会压缩每位的导通时间导致亮度下降。消隐处理指的是切换位选之前先把段码全部置 1P0 0xFF否则上一位的段码会在位选切换瞬间残留在下一位上产生拖影。这个细节在 Proteus 里几乎看不出问题但实物板上一眼就能看到“数字后面拖着影子”。用定时器中断做扫描而不是主循环里delay扫描可以避免温度转换的 750ms 阻塞期间数码管直接熄灭。温度转换期间主程序在delay_ms(750)如果你把扫描放主循环显示器就黑屏 0.75 秒答辩时非常难看。报警电路常用蜂鸣器加三极管驱动。AT89C51 的 I/O 口高电平驱动能力很弱拉电流几十 µA必须用 PNP 三极管做开关I/O 口低电平导通。蜂鸣器分有源和无源两种有源的内部带振荡电路给电就响接一个 I/O 口高低电平控制即可无源的必须给 2kHz~4kHz 的方波才能响需要用定时器翻转 I/O 口。报警系统建议用有源蜂鸣器程序里只要BEEP 0就响、BEEP 1就停不会占用定时器资源。4. 报警阈值设计与按键输入从固定值到可在线调节4.1 阈值比较的两种策略滞后比较与死区控制报警系统的核心逻辑是温度超过阈值就触发报警但直接做单点比较会遇到一个问题温度在阈值附近波动时蜂鸣器会反复响停表现为急促的“嘀嘀嘀嘀”声非常干扰。工程上解决这个问题有两种思路。第一种是滞后比较也叫施密特触发思想设定上限 30°C 触发热报警但解除报警的条件不是降到 30°C 以下而是降到 28°C 以下。这样 2°C 的滞回区间避免噪声造成反复触发。第二种是死区控制一般用在制冷制热双控场景比如室温高于 30°C 启动风扇低于 25°C 停止中间 5°C 是死区。报警系统用滞后比较就够了代码实现也不复杂。#define ALARM_HIGH 30 // 上限报警温度 #define ALARM_RELEASE 28 // 解除报警温度低于上限 2 度形成滞回 void alarm_task(void) { static bit alarm_active 0; if (current_temp ALARM_HIGH) { alarm_active 1; // 超上限无条件进入报警态 } if (current_temp ALARM_RELEASE) { alarm_active 0; // 回落到释放值退出报警态 } BEEP alarm_active ? 0 : 1; // 低电平驱动有源蜂鸣器 LED_ALARM alarm_active ? 0 : 1; }这段逻辑里alarm_active是静态变量记住当前报警状态。注意和的边界等于 30°C 时进入报警等于 28°C 时退出中间 29°C 保持之前的状态。这个细节如果写反了比如退出条件写成 ALARM_RELEASE会出现 28°C 时退出、降到 27.9°C 又没退出、回到 28.1°C 再次触发的临界抖动。答辩时老师最爱问“你报警为什么不会反复触发”就是看你有没有这个滞回区间。4.2 用矩阵键盘或独立按键在线修改阈值键位不超过 5 个的项目不值得用矩阵键盘独立按键直接一根 I/O 一个键消抖逻辑简单程序可读性高。报警系统只需要三个键设置键进入设置模式并切换位置、加键、减键。阈值修改的基本流程是按设置键进入阈值设置模式数码管显示上限温度且某位闪烁表示当前位置加/减键调整对应位再按设置键切换到下一位或保存退出。闪烁效果用定时器中断里一个计数器实现比如每 300ms 翻转一次某位的段码实现“这位在呼吸”的效果。// 独立按键扫描带 10ms 消抖 unsigned char key_scan(void) { static unsigned char key_state 0; unsigned char key_val 0; if (KEY_SET 0 || KEY_UP 0 || KEY_DOWN 0) { delay_ms(10); // 10ms 消抖过滤按键按下时的机械抖动 if (KEY_SET 0) key_val KEY_SET_PRESSED; else if (KEY_UP 0) key_val KEY_UP_PRESSED; else if (KEY_DOWN 0) key_val KEY_DOWN_PRESSED; } return key_val; }消抖时间取 10ms 是行业默认值机械按键的抖动时间一般在 5ms~10ms 之间小于 10ms 的毛刺会被过滤大于 10ms 的真实按压不会被误判。这里没有做按键释放检测所以按住加速效果做不了但课设够用。如果想做“长按连续加减”需要在key_state里记录按住持续时长超过 500ms 后每 100ms 自动加一次——这属于体验优化不是必要项但答辩时可以提一句证明你考虑过交互细节。4.3 阈值存储掉电不丢失用 EEPROM课设用常量就行是否把阈值存到 EEPROM比如 AT24C02取决于项目要求。AT89C51 内部没有 EEPROM掉电后程序重新跑阈值回到代码里的默认值 30°C。如果做课设演示每次上电重新设置阈值也能接受如果做完整产品需要外加 AT24C02 把阈值存起来上电时先读 EEPROM 再写显示。AT24C02 是 I²C 接口AT89C51 没有硬件 I²C需要软件模拟时钟线 SCL 和数据线 SDA。这部分代码量不大但通信时序起始条件、停止条件、应答信号和 DS18B20 的单总线完全是两套逻辑容易搞混。我的建议是除非题目明确要求掉电存储否则阈值用#define常量就够把精力花在报警联动和精度处理上。5. 系统联调的 4 个高频故障时序、上拉、抖动、显示5.1 现象一温度永远是 85°C这是 DS18B20 故障里最经典的。通电后数码管显示 85.0°C怎么等都不变。85°C 这个数值是所有 DS18B20 默认的上电复位值存储在上电暂存器的第 6、7 字节里。读到这个值意味着复位时序没有成功让 DS18B20 进入命令接收状态或者总线上根本没检测到设备。排查顺序先用万用表量 DQ 引脚在不操作时的电平。DS18B20 的单总线是漏极开路结构必须在 DQ 线上接一个 4.7kΩ 上拉电阻到 VCC没有这个电阻总线空闲时处于不确定状态复位时序必然失败。很多 Proteus 仿真例程里画了上拉电阻学生抄电路时漏了实物通电就白屏或 85°C找半天找不出问题。第二个可能复位脉冲的延时不够。AT89C51 是 12MHz如果用delay(500)这种没校准的延时函数实际延时只有一两百微秒达不到 480µs 的最小要求。5.2 现象二温度数值跳动每次刷新差 0.5°C 以上先排除电源纹波。DS18B20 对供电比较敏感如果和继电器、蜂鸣器共用电源蜂鸣器响的瞬间温度值会跳一下。解决方法是报警动作放在温度转换完成之后执行或者在蜂鸣器驱动引脚加一个小电容滤波。排除电源后检查程序里是否有干扰总线的操作。单总线对时序要求虽然没严格到 µs 级——官方时序范围写得很宽——但如果你在温度读取过程中开了中断而中断服务函数里有较长的delay或做了数码管扫描就会打断位时序的精确延时。比如写一个位需要拉低 60µs中间插入一个 100µs 的中断处理时序直接错乱。温度转换期间开中断没问题读温度字节期间必须关中断或至少保证中断处理时间极短。EA 0; // 读温度字节期间关闭总中断 temp_low ds18b20_read_byte(); temp_high ds18b20_read_byte(); EA 1; // 读完恢复中断5.3 现象三数码管亮度不均匀有的位亮有的位暗动态扫描的每位导通时间由主循环或定时器的调度决定。常见问题是温度转换的delay_ms(750)阻塞期间显示函数根本没被调用扫描频率从正常变成 0自然全部熄灭转换完成后重新扫描亮度恢复。反映到视觉上就是每 1~2 秒闪一次。解决方法是把显示扫描搬进定时器中断并且保证定时器中断不被长时间关断。温度读取过程的EA 0只在读字节的几毫秒内生效完整读完两个字节大约 2ms~3ms如果定时器中断周期是 2ms可能丢一次扫描但人眼感知不到单次丢失。把EA 0的部分尽量缩短只包住真正读位的循环不要在前后追加不必要的延时。void timer0_isr(void) interrupt 1 { TH0 0xFC; // 12MHz 时钟下 1ms 重装值 TL0 0x18; display_scan(); // 中断里做动态扫描主循环不被阻塞影响 key_scan(); // 按键扫描顺便放到中断里省去主循环轮询 }5.4 现象四按下按键会导致蜂鸣器误响按键和蜂鸣器分别接了不同的 I/O 口但共用一个 5V 电源。按下按键瞬间按键引脚的电平变化如果直接通过电源耦合到蜂鸣器驱动端可能触发一次误动作。更常见的是程序逻辑问题按键扫描里没有做“按键释放后才响应下一次”一个按键按下后由于延时消抖delay_ms(10)主循环里正好执行到BEEP alarm_active ? 0 : 1把按键造成的电平抖动当成温度超限。解决办法是按键逻辑和报警逻辑分离按键处理只修改阈值变量不直接操作蜂鸣器蜂鸣器状态只在报警任务里更新。这样即使按键消抖期间程序跑到报警任务读到的阈值和温度都是稳定的旧值不会因为抖动产生误报。6. 精度校准与演示技巧把课设做成答辩范本温度精确度是这个系统容易被追问的点。DS18B20 标称 ±0.5°C但实际受环境温度影响会偏差更大尤其是测量对象的物理接触方式和传感器封装。嵌入到被测介质里测液体温度偏差小——因为热传导充分悬空测空气温度传感器自身发热会影响读数。做实物时用一个技巧校验把 DS18B20 和精密温度计或水银温度计放在同一个水杯里读取两者差值在程序里做软件补偿。#define TEMP_OFFSET 0 // 实测校准值实测比水银温度计高 0.3 度就填 -0.3 unsigned int display_temp; // 用于显示的整数温度值含小数点后一位 void temp_calc(unsigned int raw) { int integer_part; int decimal_part; int cali_temp_x10; // 校准后温度单位0.1 度 integer_part (int)(raw 4); // 高字节右移 4 位取整数 decimal_part (int)(raw 0x0F) * 625; // 小数部分 * 0.0625放大 10000 倍 cali_temp_x10 integer_part * 10 decimal_part / 1000; cali_temp_x10 (int)(TEMP_OFFSET * 10); // 叠加校准偏移量 if (cali_temp_x10 0) cali_temp_x10 0; // 限幅到 0 度以下不允许显示 if (cali_temp_x10 990) cali_temp_x10 990; // 限幅到 99.0 度 display_temp cali_temp_x10; }校准补偿的思路是把原始温度放大 10 倍保留一位小数偏移量用TEMP_OFFSET宏定义实测偏低了就填正数偏高了填负数。校准后display_temp第 3 位是十位、第 2 位是个位、第 1 位带小数点是十分位、第 0 位是小数后第二位。显示时小数点和整数部分用同一个seg_code[]取码小数点通过“与 0x7F”把 A 段点亮来实现。演示技巧上建议准备三样东西在答辩现场一杯热水、一杯冰水混合物、一个装半杯水的烧杯和加热台。先用冰水验证 0°C 附近显示是否正常——这时候能看到负温度处理逻辑是否生效再用热水缓慢升温观察温度上升是不是阶梯式刷新而不是跳变到最高值最后用手指捏住 DS18B20 金属外壳看 10 秒内温度是否稳定上升这能证明传感器热响应正常。每次测温前擦干传感器表面防止残留水分导致读数异常。如果你的系统有报警输出继电器控制风扇或加热丝答辩时做一个联动演示阈值设定到当前室温以下风扇自动启动同时用逻辑分析仪抓一下 I/O 口电平变化截图放进 PPT 里直接证明硬件逻辑真实性——这比口头叙述“程序里写了”有说服力得多。最后提醒一句单片机上电瞬间 DS18B20 要等几十毫秒才稳定别在上电立即读温度加一个上电延时再进入主循环。本文还有配套的精品资源点击获取