news 2026/11/2 9:17:08

基于AT89C52与DS18B20的数字温度计设计与报警实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于AT89C52与DS18B20的数字温度计设计与报警实现

简介:面向电子信息、自动化及单片机课程设计人群的毕业论文文档,围绕基于AT89C52单片机与DS18B20智能温度传感器的数字温度计展开,可用于课程设计、毕业设计选题参考与测温报警方案借鉴。压缩包共1个doc文件,约301KB,正文以论文结构组织,涵盖课题背景、Proteus与Keil开发工具简介、系统总体方案、DS18B20测温原理、AT89C52功能应用,以及LED显示与温度上下限报警的软硬件电路设计。文中给出0℃~+100℃测量范围、三线制连接、减少外部硬件等实现思路,并讨论系统调试与应用前景,便于读者梳理数字温度计的设计框架、模块划分和论文撰写要点。目前已有157人学习,适合需要低成本测温方案与完整论文参考的初学者和毕业设计学生。

1. 从指针温度计到可报警的数字表:这套设计要解决的问题

拿到「毕业论文基于单片机的数字温度计」这个题目,多数人的第一反应是热电偶配运放再接 ADC,模拟链路一长,误差来源就说不清楚了,答辩时被追问一句「零点漂移你怎么补偿」很容易卡住。这套设计反着走:用 DS18B20 单总线数字温度传感器把温度直接变成 12 位数字量送进 AT89C52,A/D 环节整条砍掉,硬件只剩传感器、单片机、显示、报警四块。测温范围 0~100℃,误差控制在 1℃ 以内,四位数码管显示当前温度,上下限报警值可以用按键改,超限蜂鸣器响。它面向的是要在两三周内交出原理图、程序和仿真结果的毕设与课设读者,也适合需要给小型设备补一个低成本测温环节的从业者。

2. 总体方案与技术选型:为什么把模拟链路整条砍掉

2.1 需求拆解:五项指标怎么变成可测的验收条件

题目里给的指标看着简单,真正落到答辩和验收环节,每一条都得能拿出证据。我习惯在动手画原理图之前先做一张「指标—验收条件」对照表,把模糊的描述翻译成能当场演示的动作,后面所有硬件选型和代码结构都围绕这张表走。

序号原始要求落到的验收条件
1测温范围 0~100℃用冰水混合物、室温、温水、沸水几个参照点覆盖 0℃、25℃、50℃、80℃、100℃
2精度误差小于 1℃每个测点与标准温度计读数差值绝对值 ≤1℃
3LED 显示温度四位数码管稳定显示 xx.x 格式,负温度能显示负号
4上下限可设置按键能把上限在 20~90℃、下限在 0~70℃ 内步进修改并保存到 RAM
5超限报警温度越过设定限值后 1 秒内蜂鸣器起振,回落到限值内自动停止

这张表的意义在于,它把「功能强大」这种形容词换成了五条可以逐条打勾的动作。后面如果时间不够,砍功能就砍表上没有的,表上有的一个都不能省。很多同学栽在答辩现场,不是因为做不出来,而是因为说不出自己的系统到底满足了哪几条要求,被老师一句「你这个精度怎么保证的」问穿。

从这张表还能反推出一个硬约束:误差要压到 1℃ 以内,而 DS18B20 在 0~100℃ 区间的最大线性偏差小于 1℃,本身已经贴着指标线,那就意味着显示电路、换算过程和传感器摆放都不允许再引入额外误差。这条约束直接决定了后面不能随便用浮点数做中间运算,也不能把传感器贴着稳压芯片放。

2.2 三条测温路线的取舍:精度、成本与调试成本

测温方案常见的就那么几条路,选哪条不取决于谁「高级」,取决于谁的错误来源最少、谁的调试成本最低。下面这张对比表是我做方案对比时常用的格式,摆出来之后选择基本就没有悬念了。

路线典型器件组合硬件复杂度主要误差来源适配场景
热电偶热电偶 + 冷端补偿 + 运放 + ADC高冷端补偿偏差、运放失调、ADC 量化高温、宽量程工业场合
热电阻铂电阻 + 恒流源 + 仪表放大 + ADC高引线电阻、激励电流精度、放大器漂移中低温高精度计量
热敏电阻NTC + 分压电阻 + ADC中阻值离散性大、曲线非线性低成本家电测温
单总线数字传感器DS18B20 + 一只上拉电阻低自热、单总线时序执行不准0~100℃ 常规测温

热电偶和热电阻的方案并不是不好,而是它们的误差来源分散在模拟链路的每一级,任何一级出问题都要拿示波器一层层往前推,对一个两三周周期的课设来说,调试成本明显偏高。NTC 便宜,但每只阻值曲线都不一样,想做到 1℃ 以内得逐个标定,批量做不了。

DS18B20 把敏感元件、A/D 转换、数字接口全部集成在一颗三脚封装里,单片机拿到的已经是校准过的数字量,误差的主要来源只剩下时序执行和传感器自热两项,都是可以在代码里控制的。代价是它把复杂度从硬件挪到了软件时序上——这也正是后面第 4 章要重点处理的部分。选它,本质上是拿一段必须写对的微秒级时序,换掉一整条需要反复调零的模拟链路,对这类项目来说是划算的。

2.3 系统框图与模块职责划分

整个系统按「采集—处理—呈现—干预」四段划分,主控是 AT89C52,四个模块围绕它展开。框图用文字描述是这样一条链:DS18B20 挂在 P1.0 上做单总线通信,AT89C52 通过 P3.0 和 P3.1 两根线把段码串行移进 74LS164,由它并行驱动四位数码管的段选,位选走 P2.0~P2.3;三个按键接在 P1.2~P1.4 上负责设置上下限,蜂鸣器由 P1.5 经三极管驱动。

模块关键器件占用 IO职责
主控AT89C52—产生单总线时序、做数据换算、跑状态机
温度采集DS18B20P1.0输出 12 位温度值,分辨率 0.0625℃
显示驱动74LS164 + 四位共阳数码管P3.0、P3.1、P2.0~P2.3串行移位出段码,位选动态扫描
人机输入3 只轻触按键P1.2~P1.4进入设置、上限/下限加、减
报警输出有源蜂鸣器 + 三极管P1.5超限提示
时基与复位12MHz 晶振、RC 复位网络XTAL1、XTAL2、RST提供机器周期与上电复位

这里有个值得说明的取舍:为什么显示用 74LS164 串行驱动而不是直接用 P0 口并行输出段码。P0 口并行输出需要 8 根线做段选、4 根线做位选,一共 12 根;串行方案只用两根 IO 就能把段码送出去,省下来的口线可以留给后面的扩展(比如接时钟芯片或者串口上传)。原文提到这套系统可以用两节电池供电、做成便携式,口线省下来对体积和功耗都有好处。代价是每次刷新都要移位 8 次,占用一点 CPU 时间,但在 12MHz 主频下完全感觉不到。

2.4 工具链:Keil C51 编译与 Proteus 仿真联调

软件这边用的是 Keil C51,选它的理由很实际:51 内核的寄存器、位寻址、中断向量在这套工具里都有现成的头文件支持,sbit定义引脚、interrupt声明中断函数这些写法能直接把硬件结构映射到代码上,比汇编好维护得多。编译产物是一个 HEX 文件,这个文件同时喂给两个地方——烧录器和仿真软件。

Proteus 承担的是硬件验证的角色,把原理图在软件里搭出来,把 HEX 加载到虚拟单片机里跑,能在没有实物的情况下看到数码管亮、蜂鸣器响。这套组合最大的价值是把「硬件接错」和「程序写错」两类问题分离开了:先在仿真里确认逻辑对,再上实物确认电气对,出问题时排查范围直接砍一半。

但仿真有一个必须提前知道的特点:Proteus 里的 DS18B20 模型对时序的容忍度比真片子宽松得多。仿真跑通不代表实物能跑,这个坑在第 5 章会单独展开。这里先记住一条操作纪律:每次改完代码,HEX 文件要重新加载进仿真模型,Proteus 不会自动感知文件更新,很多人改了半天代码发现现象没变,其实是加载的还是旧文件。

3. 硬件设计:最小系统、单总线接口与显示报警电路

3.1 AT89C52 最小系统:晶振、复位与 EA 三个不能省的点

AT89C52 是 40 引脚、4 个 8 位并行口、片内 4KB Flash、128B RAM 的经典 51 内核器件,跑这套测温程序绰绰有余——全部代码编下来大概 3~4KB,片内空间刚好够,不需要外扩程序存储器,这也顺带省掉了一整块地址锁存和外部 ROM 电路。

最小系统有三处必须接对,接错任何一处,现象都是「程序不跑」,而且看不出任何提示。

第一处是时钟。XTAL1 和 XTAL2 之间接 12MHz 石英晶体,两端各接一只 22pF 瓷片电容到地。12MHz 的选择不是随意的,它让一个机器周期正好是 1 微秒,后面单总线时序的延时计算全部基于这个前提。换成 11.0592MHz 虽然串口波特率更准,但机器周期变成 1.085 微秒,所有微秒级延时都得重算,对初次做这套设计的人来说没必要增加这个变量。

第二处是复位。RST 引脚接一个 10μF 电解电容到 VCC、一个 10kΩ 电阻到地,构成上电复位。上电瞬间电容两端电压不能突变,RST 被拉高,随着电容充电电流减小,RST 电压回落,只要高电平持续时间超过两个机器周期就完成复位。按键复位可以在电容两端并一只按钮,但课设阶段上电复位够用。

第三处是 EA 引脚。EA 接高电平,CPU 从片内 Flash 取指令;接低电平则强制从外部存储器取指令。用片内程序的人如果忘了把 EA 拉高,程序烧进去了也一行都不执行,而且用万用表量所有引脚电压都正常,这是典型的「黑匣子」型故障。

提示:画完原理图先检查这三处再通电,能省掉大量无谓的排查时间。晶振不起振时用示波器看 XTAL2 有没有波形,复位是否正常看 RST 上电后是否从高电平回落到低电平。

3.2 DS18B20 接口电路:上拉电阻与两种供电方式的选择

DS18B20 采用三脚封装,1 脚接地,2 脚是数据线 DQ,3 脚接电源。它和单片机之间只需要一根数据线,再加一只上拉电阻就能通信,这就是所谓「一线总线」的含义。数据线接 P1.0,4.7kΩ 上拉电阻接到 VCC,这个电阻的作用是在总线空闲时把电平拉高,同时给器件的开漏输出提供上拉通路。

上拉电阻不能省,也不能随便取值。4.7kΩ 是常规选择,如果传感器引线较长(超过 1 米),总线分布电容变大,上升沿变缓,可以把阻值降到 2.2kΩ 加快充电;反过来如果现场干扰小、想降低静态功耗,可以适当加大到 10kΩ。这条经验是实际调试时摸出来的,不是查手册抄的。

供电方式有两种。第一种是外部电源供电,VDD 接 5V,DQ 接单片机,这种方式总线驱动能力强,转换期间不需要额外操作,是首选。第二种是寄生电源供电,VDD 直接接地,器件从数据线的高电平上「偷电」,靠内部电容储能维持工作。寄生供电省一根线,但在温度转换期间总线必须提供足够的电流,通常要用一个 MOSFET 在转换窗口内把总线强上拉到 VCC,上拉窗口最长 10 微秒。对这套设计来说,外部供电的接线成本几乎为零,没有理由为了省一根线去引入寄生供电那一堆时序约束。

还有一个容易被忽略的细节:DS18B20 在温度转换时会自热,器件自身温升会让读数偏高零点几度。0~100℃ 的测量范围里,这零点几度在 1℃ 的误差预算中占得不小。所以传感器不要贴着稳压芯片、单片机或者其他发热元件放,也不要在密封壳里紧贴金属壁,留出空气流通的余地。

3.3 显示电路:74LS164 串行驱动四位数码管的接线与段码

显示部分是这套设计里唯一需要仔细处理「时序」之外的电气细节的地方。方案用一片 74LS164 八位串入并出移位寄存器做段选驱动,串行数据从 P3.0 进,时钟从 P3.1 进,74LS164 的八路并行输出经过 330Ω 限流电阻接到数码管的 a~dp 八个段引脚。四位数码管的公共端(共阳接法时为阳极)分别由 P2.0~P2.3 经 PNP 三极管驱动,做位选。

之所以用 P3.0 和 P3.1,是因为这两根线本来就是串口的 RXD 和 TXD,用软件模拟串行移位的方式驱动 74LS164,等于用两根口线干了八根口线的活。移位过程是:把段码字节按从高位到低位的顺序逐位送到数据线,每送一位就产生一个时钟上升沿,八次之后段码就并行出现在 74LS164 的输出端。

段码表按共阳数码管整理,段位从低位到高位依次为 a、b、c、d、e、f、g、dp,低电平点亮:

显示字符段码(十六进制)显示字符段码(十六进制)
00xC050x92
10xF960x82
20xA470xF8
30xB080x80
40x9990x90
负号0xBF小数点该位段码 & 0x7F

小数点的处理方式是把对应位的段码和 0x7F 按位与,因为 dp 位于最高位,清零即可点亮。这个做法比另建一张带小数点的段码表省事,改显示格式的时候不用动表。

动态扫描的关键是消隐顺序:切换位选之前必须先把当前位关掉,再送新段码,最后打开下一位。如果顺序反了,上一位的段码会短暂串到下一位上,看起来就是数码管有重影、边缘发虚。这个现象在仿真里不明显,在实物上非常明显,属于典型的上实物才暴露的问题。

3.4 按键与报警电路:三只按键加一路蜂鸣器输出

按键部分没什么技术含量,但有细节。三只轻触按键一端接地,另一端分别接 P1.2、P1.3、P1.4。P1 口内部有上拉电阻,按键未按下时读到的就是高电平,按下读到低电平,不需要外接上拉。如果想让硬件层面就有一点抗抖动能力,可以在按键两端并一只 100nF 电容,但软件消抖更彻底,硬件电容只是辅助。

三个键的职责划分要提前定清楚,不然后面写状态机会乱:第一个键做「设置」,每按一次在「正常显示 → 设上限 → 设下限 → 回到正常」之间循环;第二个键做「加」,在设置状态下把当前项加 0.1℃ 或 1℃;第三个键做「减」,逻辑对称。正常显示状态下加减键不响应,避免误触改掉限值。

报警输出用一个 PNP 或 NPN 三极管驱动有源蜂鸣器,基极串 1kΩ 电阻接 P1.5。有源蜂鸣器内部自带振荡电路,给电就响,控制简单;无源蜂鸣器需要单片机输出方波,会额外占一个定时器,没必要。这里有一个必须加的元件:如果蜂鸣器是电磁式而不是压电式,线圈属于感性负载,三极管集电极到电源之间要反向并一只二极管做续流,否则关断瞬间的反电动势可能击穿三极管。这个小细节在仿真里永远不会暴露,实物上却能让你的板子在连续报警几分钟后突然不响了。

4. 软件设计:单总线时序、数据换算与报警状态机

4.1 主循环与定时中断的分工

程序结构上采用「前后台」模式,定时器中断做时基,主循环做业务。T0 工作在方式 1(16 位定时),12MHz 下机器周期 1 微秒,要定时 10 毫秒需要计数 10000 次,初值就是 65536 - 10000 = 55536,写成十六进制 0xD8F0,所以 TH0 装 0xD8、TL0 装 0xF0。

中断里干三件事:给显示扫描计数、给按键扫描计数、给秒计时。显示每位点亮 1 毫秒,四位轮一遍 4 毫秒,刷新率 250Hz,人眼完全看不出闪烁。按键每 10 毫秒采样一次,用于消抖。主循环负责温度转换、数据换算和报警判定,温度每 1 秒更新一次。

/* T0 中断服务:10ms 时基,兼顾显示扫描与按键采样 */ void timer0_isr(void) interrupt 1 { TH0 = 0xD8; /* 重装初值,12MHz 下 10ms */ TL0 = 0xF0; disp_tick++; /* 显示扫描计数,每 1ms 推进一位 */ if (disp_tick >= 4) disp_tick = 0; key_tick++; /* 按键采样计数 */ if (key_tick >= 1) { /* 10ms 采一次 */ key_tick = 0; key_flag = 1; /* 置标志,具体处理放主循环 */ } if (++sec_cnt >= 100) { /* 100 × 10ms = 1s */ sec_cnt = 0; sample_flag = 1; /* 通知主循环做一次温度采集 */ } }

这段代码的设计意图是「中断只打标记、不做重活」。中断里调用单总线读写函数是常见的翻车点,因为单总线时序里有毫秒级的等待(温度转换要 750 毫秒),放在中断里会把整个系统的时基拖垮,显示立刻卡住不动。把标记置起来交给主循环处理,中断的执行时间控制在几十微秒,系统的响应性才有保证。

参数上要说明的是sec_cnt >= 100这个阈值:它决定了温度刷新周期。改成 50 就是 0.5 秒刷新一次,改 200 就是 2 秒。刷新太快没必要,DS18B20 一次 12 位转换本身就要 750 毫秒,刷得比转换快只会读到上一次的旧值;刷得太慢又会让报警响应变迟钝。1 秒是个折中。

4.2 DS18B20 的复位、写字节、读字节时序

单总线协议的全部复杂性都集中在这一节。整个通信建立在四种时隙上:复位时隙、写 1 时隙、写 0 时隙、读时隙。所有时隙的长度都以微秒计,靠软件延时实现,所以延时函数的准确性直接决定通信成败。

#include <reg52.h> #include <intrins.h> sbit DQ = P1^0; /* DS18B20 数据线 */ /* 微秒级延时:12MHz 下约 t 微秒,需按编译器实际优化情况校准 */ void delay_us(unsigned char t) { while (t--) { _nop_(); _nop_(); } } /* 毫秒级延时,用于等待温度转换完成 */ void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 120; j++); /* 12MHz 下约 1ms,需实测校准 */ } /* 复位:拉低约 500us 后释放,读存在脉冲。返回 0 表示器件在线 */ bit ds18b20_reset(void) { bit presence; DQ = 0; delay_us(240); delay_us(240); /* 拉低约 480~500us */ DQ = 1; /* 释放总线,等待器件应答 */ delay_us(30); /* 15~60us 内器件会拉低总线 */ presence = DQ; /* 读到 0 说明存在脉冲到来 */ delay_us(200); delay_us(200); /* 等复位时隙走完 */ DQ = 1; return presence; /* 返回 1 表示无应答,器件未接好 */ } /* 写一个字节,低位先出 */ void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; _nop_(); _nop_(); /* 拉低约 2us 作为写时隙起始 */ DQ = dat & 0x01; /* 写 1 立即释放,写 0 继续保持低 */ delay_us(30); /* 整个时隙保持 60us 以上 */ 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_(); _nop_(); /* 拉低 1~2us 启动读时隙 */ DQ = 1; /* 释放总线,由器件驱动 */ _nop_(); _nop_(); /* 释放后 15us 内必须完成采样 */ if (DQ) dat |= 0x80; delay_us(30); /* 时隙总长保持 60us 以上 */ } return dat; } /* 启动转换并读回 16 位原始值,失败返回 0xFFFF */ unsigned int ds18b20_read_raw(void) { unsigned char tl, th; if (ds18b20_reset()) return 0xFFFF; /* 器件无应答 */ ds18b20_write_byte(0xCC); /* 跳过 ROM,单器件场合 */ ds18b20_write_byte(0x44); /* 启动一次温度转换 */ delay_ms(800); /* 12 位分辨率需 750ms,留余量 */ if (ds18b20_reset()) return 0xFFFF; ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); /* 读暂存器命令 */ tl = ds18b20_read_byte(); /* 低 8 位 */ th = ds18b20_read_byte(); /* 高 8 位 */ return ((unsigned int)th << 8) | tl; }

这段代码里有几个参数值得逐个说明。复位时拉低 480~500 微秒,这是协议规定的下限(480 微秒)往上留一点余量;释放后等 30 微秒再采样,因为器件的存在脉冲是在 15~60 微秒窗口内出现的,采早了器件还没拉低,采晚了脉冲已经结束。写时隙里「写 1 立刻释放、写 0 继续拉低」是单总线区分 0 和 1 的唯一方式,写 1 拉低了 60 微秒就会被器件理解成写 0。

读时隙最容易写错:拉低 2 微秒后必须马上释放,然后尽快采样,因为器件只在释放后的 15 微秒内把数据摆到总线上,超过这个窗口数据就消失了。代码里用两个_nop_()代替精确延时,是因为采样点越早越安全。

delay_us和delay_ms里的循环次数不能照抄,必须按自己编译器的优化等级实测校准。校准方法很简单:在函数前后翻转一个 IO,用示波器或者逻辑分析仪量脉宽,对不上就调循环次数。这一步不做,整套时序都是在猜,通信时通时不通的「玄学」现象就是这么来的。

4.3 原始值换算:0.0625℃ 分辨率与负温度处理

DS18B20 的 12 位模式下,最低位代表 0.0625℃。读回来的 16 位数据低 12 位有效,高 4 位是符号扩展。换算成摄氏度就是原始值乘以 0.0625,但直接上浮点运算会在 51 上引入几 KB 的库和上百微秒的计算时间,没必要。

工程上的常规做法是把结果转成「0.1℃ 为单位的整数」来做显示和比较,全程整数运算。乘以 0.0625 再乘以 10,等于乘以 0.625,也就是乘以 625 再除以 1000。

/* 原始 16 位数据转成 0.1℃ 为单位的带符号整数 */ int raw_to_tenth(unsigned int raw) { int t; if (raw & 0x8000) { /* 最高位为 1 表示负温度 */ raw = (unsigned int)(~raw) + 1; /* 取补码得到绝对值 */ t = -(int)((unsigned long)raw * 625UL / 1000UL); } else { t = (int)((unsigned long)raw * 625UL / 1000UL); } return t; }

换算的验证方法是手算几个点对一下:原始值 0x0191(401)对应 401 × 0.0625 = 25.0625℃,函数返回 250,即 25.0℃;原始值 0xFF5E 是 -10.125℃ 的补码,取反加一得 162,162 × 625 / 1000 = 101,返回 -101,即 -10.1℃。

这里用unsigned long做中间量是必须的。401 乘以 625 等于 250625,已经超出 16 位无符号整数的上限 65535,用unsigned int会溢出成错误结果。这个溢出在 25℃ 附近刚好开始出现,也就是常温下就开始错,非常隐蔽——温度低的时候显示正常,一到 25℃ 以上数字就乱跳,很多人第一反应是传感器坏了,其实是数据类型选窄了。

4.4 数码管动态扫描与显示格式

四位数码管的显示格式要按量程设计。0~100℃ 意味着整数部分最多三位,加上一位小数就是四位有效字符,但如果再加负号就放不下了。这套设计里我采用的规则是:温度在 -9.9~99.9℃ 区间时显示「符号位 + 十位 + 个位 + 小数位」共四位,小数点点亮在个位后面;温度达到 100℃ 时切换到整数显示「100」,小数点熄灭。正常测温都在 0~100℃ 内,只有调试时才会出现负值,这个规则够用。

/* 把 0.1℃ 单位的温度值拆成四位显示缓冲 */ void temp_to_display(int tenth) { unsigned char neg = 0; unsigned int v; if (tenth < 0) { neg = 1; v = (unsigned int)(-tenth); } else { v = (unsigned int)tenth; } if (v >= 1000) { /* 100.0℃ 及以上,显示三位整数 */ buf[0] = seg_code[v / 100 % 10]; buf[1] = seg_code[v / 10 % 10]; buf[2] = seg_code[v % 10]; buf[3] = 0xFF; /* 末位熄灭 */ } else { if (neg) buf[0] = 0xBF; /* 负号 */ else buf[0] = seg_code[v / 100 % 10]; /* 十位,为 0 时可熄灭 */ buf[1] = seg_code[v / 10 % 10]; /* 个位 */ buf[2] = seg_code[v % 10] & 0x7F; /* 小数位,点亮小数点 */ buf[3] = 0xFF; } }

显示缓冲算好之后,扫描函数每次只做一件事:关掉所有位选、把当前位的段码移进 74LS164、打开对应位选。段码移位的顺序必须和硬件接线一致,74LS164 是高位先出,如果代码里写成低位先出,显示出来的数字会完全对不上号。

/* 通过 P3.0(数据)/P3.1(时钟) 把一字节段码移入 74LS164 */ void seg_send(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SDA = (dat & 0x80) ? 1 : 0; /* 高位先出,与硬件接线对应 */ dat <<= 1; SCLK = 0; _nop_(); SCLK = 1; /* 上升沿把数据移入寄存器 */ } } /* 在 T0 中断里每 1ms 调用一次,扫描一位 */ void display_scan(void) { P2 = 0xFF; /* 先关掉全部位选,消隐防重影 */ seg_send(buf[disp_tick]); /* 送当前位段码 */ P2 = ~(1 << disp_tick); /* 打开当前位选,共阳低电平有效 */ }

把消隐放在送段码之前,是这段代码的核心。顺序反过来,切换位选的那一瞬间上一位的段码还留在 74LS164 的输出上,会短暂点亮下一位,视觉上就是所有数字都带着一层虚影。

4.5 按键状态机与报警判定

按键处理用「连续采样计数」的方式消抖:每 10 毫秒读一次键值,连续 5 次读到同一个键(也就是持续 50 毫秒)才认定有效,中间有一次不同就清零重来。这个方法比简单的延时消抖更可靠,因为它在整个按下过程中都在判断,不会因为抖动恰好落在延时窗口外而误判。

/* 按键扫描,返回键值:0 无键,1 设置,2 加,3 减 */ unsigned char key_scan(void) { static unsigned char last = 0, cnt = 0; unsigned char cur = 0; if (!KEY_SET) cur = 1; else if (!KEY_UP) cur = 2; else if (!KEY_DN) cur = 3; if (cur == last) { if (cnt < 5) cnt++; } else { cnt = 0; last = cur; } if (cnt == 5) { cnt = 4; return cur; } /* 保持 4,避免重复触发 */ return 0; }

这里cnt = 4而不是清零,是为了让「按住不放」只触发一次,防止长按连续加数。如果需要长按连加,就要额外计时,按 200 毫秒的间隔重复返回键值。

报警判定是整套代码里最容易被忽略但有讲究的部分:直接拿当前温度和限值比较,会在临界点附近产生抖动——温度在 35.0℃ 上下波动 0.1℃,蜂鸣器就会「嘀嘀嘀」地反复启停,听起来像坏了。常规做法是加回差,超上限后要降到上限以下 0.3℃ 才解除,低于下限后要升到下限以上 0.3℃ 才解除。

/* 带回差的报警判定,temp 与上下限单位均为 0.1℃ */ void alarm_check(int temp) { if (!alarm_on) { if (temp > upper || temp < lower) alarm_on = 1; /* 越限,启动报警 */ } else { if (temp <= upper - 3 && temp >= lower + 3) /* 回差 0.3℃ */ alarm_on = 0; /* 回到区间内才解除 */ } BUZZ = alarm_on ? 1 : 0; /* 三极管驱动,高电平导通 */ }

回差取 0.3℃ 是一个经验值,太小起不到作用,太大又会让报警响应显得迟钝。这个参数在验收时要专门演示:把温度缓慢推过上限,再拉回来,看蜂鸣器是不是干净地响一次、停一次。

5. 避坑与常见问题排查:五个最容易翻车的点

5.1 读回来的温度永远是 85℃

现象:程序烧进去,数码管稳定显示 85.0,不管传感器放在冰水里还是热水里都不变。

原因:85℃ 是 DS18B20 上电复位后的默认值,出现这个数只有两种可能——一是温度转换根本没执行成功,读到的还是上电默认值;二是复位时隙没有收到存在脉冲,后续命令全部石沉大海,但代码没有对返回值做判断,照样往下读,读回来的是总线上浮空的高电平。

解决:分两步查。先把ds18b20_reset()的返回值打印出来(或者用一个 LED 指示),确认器件有没有应答。没有应答就查硬件:上拉电阻有没有装、DQ 线是不是接错引脚、VDD 有没有供电。有应答但还是 85℃,就查转换命令之后有没有等够 750 毫秒,delay_ms的实际延时可能因为编译器优化只有标称值的一半,用示波器量一下翻转 IO 的脉宽,把循环次数改对。

5.2 数码管显示乱码、缺段或者有重影

现象:数字能显示但笔画对不上,或者每个数字边缘都有一层淡淡的重复笔画。

原因:笔画对不上通常是段码位序和硬件接线不一致——代码按 a 段在最低位写的段码表,实际接线段码是高位数在前,或者 74LS164 的数据位和数码管的段引脚接反了顺序。重影则是扫描顺序问题,切换位选时没有先消隐。

解决:先单独测一段。写一个只点亮 a 段的测试程序,把 0xFE 移进 74LS164,看亮的是不是 a 段,不是就调整段码表里各位的顺序,一直到八个段全部对上为止。这个笨办法比对着电路图猜快得多。重影就改扫描顺序,把「关位选」提到「送段码」之前,代码结构已经在 4.4 节给出来了。

注意:仿真里 74LS164 的模型对时序非常宽容,位选切换和段码移位几乎同时发生也不会显示重影,所以这个 bug 往往要到实物上才暴露。仿真阶段就要按正确顺序写,别指望它替你发现。

5.3 按键按一下跳好几个数

现象:设置上限时按一次「加」,数值从 30 直接跳到 33 甚至更多。

原因:机械按键在按下和松开的瞬间会产生 5~20 毫秒的抖动,单片机在主循环里读键的速度远快于抖动周期,一次按下被识别成了好几次。另一种可能是按键扫描放在主循环里,而主循环里有一段 800 毫秒的温度转换等待,导致采样间隔不均匀。

解决:按键扫描必须挂在 10 毫秒的定时中断标记上,保证采样间隔恒定,再配合连续 5 次一致的判定。不要在主循环里用delay_ms(10)做消抖,主循环的时间基准会被温度转换拖乱。如果已经用了状态机还是跳数,检查是不是cnt达到阈值后没有锁住,导致按住不放时持续触发。

5.4 仿真跑得好好的,实物一上电就不工作

现象:Proteus 里显示正常、报警正常,焊好的板子通电后数码管全灭或者全亮,蜂鸣器一直响。

原因:仿真模型不检查电气特性。常见的实物翻车点有三个:一是 EA 引脚忘了接高,程序压根没跑;二是 74LS164 的输出电流不够驱动数码管,仿真里模型是理想的,实物上亮度极暗或者完全不亮;三是蜂鸣器作为感性负载没有加续流二极管,工作一段时间后驱动三极管被反电动势击穿。

解决:上电前按清单逐项核对——EA 是否接高、晶振是否起振(示波器看 XTAL2)、复位脚上电波形是否正常、数码管限流电阻是否装了、蜂鸣器两端有没有反向二极管。这套核对流程走一遍只要五分钟,比事后拿万用表一根根量线快得多。限流电阻的取值也要核算:5V 电源、每段压降约 2V、目标电流 5~8 毫安,电阻取 330Ω 到 470Ω 之间。

5.5 报警限值越设越乱,或者断电后设置丢失

现象:设置时一路按加,上限能加到 120℃,加着加着上限比下限还小,蜂鸣器一直响;或者断电重启后限值回到默认值,之前设的全没了。

原因:按键处理里没做边界钳位,也没做上下限的交叉校验。AT89C52 片内没有 EEPROM,限值只存在 RAM 里,掉电自然丢失——这一点很多人做完毕设才意识到,然后被老师问「断电后设置还在吗」问住。

解决:在加减键处理里加硬边界,上限限制在 20~90℃、下限限制在 0~70℃,并且每次修改后校验「下限 + 1℃ ≤ 上限」,不满足就拒绝这次修改或者自动调整另一个值。掉电保存有两个选择:一是在原理图上加一片 I2C 接口的 EEPROM 存限值,二是接受这个限制,在报告里明确写「限值保存在 RAM,掉电恢复默认值」,别在答辩时被问穿。前者多一片芯片和一段读写代码,后者省事但要在论文里诚实交代。

6. 验收与答辩:指标怎么测出来,问题怎么答回去

验收这件事,我一般建议按一张测试记录表来走,测完当场填,别等最后回忆。表格按第 2 章那张指标表展开,每个测点记录三样东西:标准温度计读数、本机显示值、差值。

测点参照物标准读数本机显示差值判定
0℃冰水混合物0.2℃0.3℃0.1℃合格
25℃恒温室内静置24.8℃25.1℃0.3℃合格
50℃恒温水浴50.1℃50.4℃0.3℃合格
80℃恒温水浴80.0℃80.6℃0.6℃合格
100℃沸水

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 9:16:07

Zipkin分布式追踪系统:原理、实践与性能优化

1. Zipkin的核心价值与应用场景分布式系统已经成为现代互联网架构的标配&#xff0c;但随之而来的复杂性也带来了新的挑战。当用户请求在数十个微服务间流转时&#xff0c;如何快速定位性能瓶颈&#xff1f;如何理解服务间的依赖关系&#xff1f;这正是Zipkin这类分布式追踪系统…

作者头像 李华
网站建设 2026/11/2 9:15:02

Datawhale开源社区选拔机制与人才培养解析

1. 项目背景与意义Datawhale作为国内知名的开源学习组织&#xff0c;其成员选拔机制一直备受关注。第十四期录取名单的公布&#xff0c;不仅是对入选者能力的认可&#xff0c;也反映了当前技术社区的人才培养趋势。这份名单背后&#xff0c;是一个由志愿者运营、完全开源的学习…

作者头像 李华
网站建设 2026/11/2 9:14:24

Vitis HLS 2022.2安装配置、综合仿真与IP导出避坑

简介&#xff1a;Xilinx Vitis 高层次综合用户指南&#xff08;UG1399 v2022.2&#xff09;中文版面向用 HLS 做 FPGA 硬件加速的工程师与相关专业高年级学生&#xff0c;对应 2022 年 12 月 7 日发布的工具版本&#xff0c;帮助读者从 C/C 算法描述过渡到可综合的硬件 IP。全文…

作者头像 李华
网站建设 2026/11/2 9:13:34

戴森球计划蓝图库FactoryBluePrints:从导入到改造的工程实践指南

工厂类游戏玩到中后期&#xff0c;最磨人的从来不是资源够不够&#xff0c;而是“我明明知道该造什么&#xff0c;却懒得再摆一遍”。戴森球计划这款游戏把产线规模推到了星球级别&#xff0c;一条完整的高阶产物链路动辄几百台建筑、上千个传送带节点&#xff0c;纯手工从零搭…

作者头像 李华
网站建设 2026/11/2 9:12:30

直饮机租赁市场分析及品牌评测指南

1. 直饮机租赁市场现状与需求分析近年来&#xff0c;随着健康饮水意识的提升和办公场景的升级&#xff0c;直饮机租赁服务已成为企业行政采购和家庭用户的重要选择。根据第三方调研数据显示&#xff0c;2023年国内商用直饮机租赁市场规模已突破50亿元&#xff0c;年复合增长率保…

作者头像 李华
网站建设 2026/11/2 9:10:53

RCNN、DETR、YOLO三大目标检测算法选型与实战避坑指南

目标检测这块&#xff0c;RCNN、DETR、YOLO 是绕不开的三座大山。我最早接触的是 YOLO&#xff0c;那会儿还在用 v3 跑一些简单的车辆检测&#xff0c;后来因为项目需要做高精度的小目标识别&#xff0c;被迫回头啃了 Faster R-CNN 的两阶段逻辑&#xff0c;再后来 DETR 出来&a…

作者头像 李华