DS18B20单总线数字温度计:AT89C52时序与显示设计

发布时间:2026/11/2 9:17:54
DS18B20单总线数字温度计:AT89C52时序与显示设计

简介:围绕单片机数字温度计展开的毕业论文文档,面向电子、自动化、嵌入式相关专业学生及需要完成课程设计或毕业设计的人群,重点解决温度检测、显示与超限报警的系统方案梳理问题。压缩包仅含1个doc文件,大小约301KB,正文以论文形式组织,涵盖课题背景、Proteus与Keil开发工具、DS18B20智能温度传感器、AT89C52单片机、LED显示及温度报警上下限设置等软硬件内容,并附系统概述与各部分电路说明。已有157人学习下载,可作为低成本数字温度计设计的参考样本。读者可据此了解从传感器选型、单片机接口到测量报警电路的完整实现思路,也能借鉴论文目录结构、关键词与中英文摘要写法,用于整理开题、方案论证和答辩材料。

1. 数字温度计:为什么把 ADC 拿掉,读数反而更准

一个单片机数字温度计最容易让人翻车的地方,恰恰是很多人以为最没技术含量的部分:读温度。传统测温思路是「传感器 → 放大 → 滤波 → ADC → 单片机」,每多一级电路就多一个失调源、一个温漂源、一个参考电压源,最后全叠进读数里。而这种基于 DS18B20 与 AT89C52 的设计反其道而行——把 A/D 转换直接塞进传感器内部,单片机和传感器之间只留一根数据线,模拟链路上的抖动和漂移在物理上就不存在了。

它解决的是一个很具体的问题:怎么用最低的硬件代价,拿到一个不依赖外部基准源、不需要标定、上电就能用的数字温度值。DS18B20 把感温元件、A/D、数字逻辑、报警阈值存储全部集成在一颗三引脚封装里,AT89C52 只负责在时序上「敲」出正确的电平组合把数字读回来。想搞懂这类设计,需要把单总线时序、温度寄存器补码、单片机机器周期、串行数码管驱动这几件事拆开看,它们是这套系统真正的骨架。

2. DS18B20 单总线:一根线怎么同时完成供电、复位和读写

2.1 单总线拓扑:为什么一根线能挂多个器件

常规的 I²C 需要 SDA/SCL 两根线,SPI 需要 CS/SCK/MOSI/MISO 四根线,DS18B20 只用一根 DQ 线就完成了供电、复位、寻址、读写全部动作。原理在于「线缆电平 + 时间窗口」两件事同时编码:器件拉低总线表示逻辑 0,释放后由上拉电阻把线拉回高电平表示逻辑 1,每一位的持续时间定义了这一位是读还是写、是 1 还是 0。

一根线能挂多个器件靠的是每个 DS18B20 内部有一个 64 位激光 ROM 序列号,其中低 8 位是单总线家族码(DS18B20 是 0x28),中间 48 位是唯一序列号,最高 8 位是 CRC。主机先发0x33(Read ROM,总线上只挂一个器件时用)读出序列号,或者用0x55(Match ROM)后跟 64 位序列号精确点名某一个器件,再用0xCC(Skip ROM)广播给所有器件。所以一根线上挂多个温度探头的本质,是「一主多从的分时广播」,代价是主机必须严格区分「广播命令」和「点名命令」。

常见的实现思路是:总线上只有一个探头时,初始化后直接发0xCC跳过寻址,省掉读 ROM 那一步;多个探头时先用0x33扫出所有序列号存起来,再逐个0x55点名。

2.2 复位脉冲与存在脉冲:通信开始前的握手

单总线没有独立的时钟线,主机和从机之间靠一个固定的握手动作确认对方在线:

  1. 主机把 DQ 拉低至少 480µs;
  2. 主机释放总线,上拉电阻把线拉回高电平;
  3. DS18B20 等待 15~60µs 后,主动拉低总线 60~240µs 作为「我在这」的回应;
  4. 主机在这段时间内采样 DQ,读到低电平说明至少有一个从机在位。

这段握手的核心是从机用自己的拉低动作告诉主机它已经上电且准备好。主机如果没检测到这个存在脉冲,后续所有读写都是对着空气发命令,读回来的数据要么是全 1,要么是残留在寄存器里的旧值。

/* DQ 接 P3.7,上拉电阻 4.7kΩ */ sbit DQ = P3^7; /* 返回 1 表示检测到至少一个从机在线 */ bit ow_reset(void) { bit presence; DQ = 0; /* 主机拉低总线 */ delay_us(480); /* 至少 480us,短了从机会漏检 */ DQ = 1; /* 释放,上拉电阻回高 */ delay_us(70); /* 跨过 15~60us 的从机等待窗 */ presence = ~DQ; /* 读到低电平 = 从机拉低回应 */ delay_us(410); /* 凑满 480us,等从机释放总线 */ return presence; }

这段代码里最容易出错的是delay_us(480)这个数。如果晶振换成 11.0592MHz 而延时函数没跟着调,实际拉低时间可能只有 440µs 或者 520µs,前者会让部分批次的 DS18B20 漏检,后者不会出错但会拖慢整个循环。delay_us(70)这个数也不是随便取的——必须落在从机从「释放总线」到「拉低回应」的 15~60µs 窗口之外,但又得在从机拉低结束(最长 240µs)之前采样。

2.3 写时隙与读时隙:15 微秒窗口里的 0 和 1

复位之后,每一位数据传输都是一个独立的时隙,主机和从机轮流控制总线:

操作主机动作从机动作主机采样点
写 1拉低 1~15µs 后释放不动作无需采样
写 0拉低 60µs 以上再释放不动作无需采样
读位拉低 ≥1µs 后释放把数据位放到总线上释放后 15µs 内
时隙间隔释放 ≥1µs准备下一位——

写时隙的关键在「拉低多长时间」:拉低短于 15µs 就释放,从机把它解读为写 1;拉低超过 60µs 再释放,从机解读为写 0。读时隙反过来——主机先拉低至少 1µs 制造时隙起点,然后释放,从机必须在 15µs 内把数据位推上总线,主机在 15µs 采样窗口内读 DQ 的电平。

/* 写一位,b=1 走短拉低分支,b=0 走长拉低分支 */ void ow_write_bit(bit b) { DQ = 0; if (b) { delay_us(2); /* 1us 以上即可,留 2us 余量 */ DQ = 1; /* 15us 窗口内释放 = 写 1 */ delay_us(60); /* 补完整时隙 */ } else { delay_us(60); /* 拉低保持 60us 以上 = 写 0 */ DQ = 1; delay_us(2); } } /* 读一位:先制造时隙,再在 15us 窗口内采样 */ bit ow_read_bit(void) { bit b; DQ = 0; delay_us(2); DQ = 1; /* 释放,让从机把数据推上来 */ delay_us(10); /* 落在 15us 采样窗口内 */ b = DQ; delay_us(50); /* 补完整读时隙 */ return b; }

这两个函数的时序全靠delay_us撑着,所以整个单总线通信的成败,九成取决于延时函数的精度。仿真软件里总线是理想导线、上升沿是瞬间的,所以 Proteus 上跑得通不代表实物上跑得通;实物总线上有几十 pF 的分布电容,上拉电阻 4.7kΩ 与这个电容构成 RC 回路,长线时上升沿可能拖到 5~10µs,读时隙采样点如果卡在 15µs 边缘就会读到 0。常见的工程做法是把采样点从 10µs 往后挪到 12µs,给上升沿留点余量,同时把探头引线控制在 1 米以内。

2.4 寄生供电:上拉电阻不是可有可无的

DS18B20 有两种供电模式:外部供电接三根线(VDD、GND、DQ),寄生供电只接两根线(GND、DQ),芯片从 DQ 线上的高电平「偷电」存进内部电容。寄生供电模式下,VDD 必须接地,且总线上需要加一个 MOSFET 做「强上拉」——因为在温度转换和写 E²PROM 期间,芯片耗电会明显上升,一个 4.7kΩ 上拉电阻提供的电流不够。

强上拉的做法是:正常通信时用 4.7kΩ 弱上拉;进入 Convert T 或 Copy Scratchpad 之前,用一个 PNP 或 PMOS 把 DQ 直接短接到 VDD,等操作完成再断开。寄生供电省一根线,代价是主机要多控制一路强上拉,而且多探头同时转换时总线负载会变重。大多数温度计 demo 用外部三线供电就够了,因为真正省下来的那根线在布线上感知不强,反而增加了强上拉控制逻辑。

3. 温度寄存器:0.0625℃ 的分辨率是怎么拼出来的

3.1 16 位温度寄存器与符号扩展

DS18B20 内部有一个 9 字节的 Scratchpad,其中第 0 字节是温度低 8 位,第 1 字节是温度高 8 位,两者拼成一个 16 位有符号整数。这个整数的权重是按二进制小数位定义的:

位1514131211109876543210
权重SSSSS2⁶2⁵2⁴2³2²2¹2⁰2⁻¹2⁻²2⁻³2⁻⁴

低 4 位是小数部分,权重分别是 0.5、0.25、0.125、0.0625℃,所以 12 位分辨率下最小刻度的确就是 0.0625℃。高 5 位是符号扩展位——正温度时全 0,负温度时全 1,这是二进制补码的标准做法。比如 +25℃ 对应原始值 400(0x0190),×0.0625 = 25.0;−10.125℃ 对应原始值 −162(0xFF5E),×0.0625 = −10.125。

这里有个很隐蔽的坑:51 单片机的int是 16 位,对 0xFF5E 这样的值做算术右移会自动做符号扩展,但如果先把低字节和高字节各自存到unsigned char再拼,符号位就丢了。

/* sp[0] 是温度低字节,sp[1] 是高字节,按小端顺序 */ unsigned char sp[9]; /* 从 Read Scratchpad 读回的 9 字节 */ /* 错误写法:sp[1] 是 unsigned char,左移后变成正整数 65280+ */ int raw_bad = (sp[1] << 8) | sp[0]; /* 正确写法:先拼成 16 位,再强制转换让编译器按有符号处理 */ int raw = (int)(((unsigned int)sp[1] << 8) | sp[0]);

raw_bad在读到 −10.125℃ 时会变成 65374,除以 16 得到 4085.875,显示出来就是 4085℃ 这种荒谬的数。这种 bug 在实验室常温下测不出来,只有冬天或者冷库里才会翻车。

3.2 整数换算避免浮点

51 没有硬件浮点单元,用float做一次乘除会链接进几百字节的浮点库,在 4KB 程序空间里是很奢侈的。温度值完全可以用整数表示:因为raw的单位本来就是 1/16℃,直接把它保留为「十六分之一度」这个定点数,显示的时候再做整数除法和取余。

/* raw 单位是 1/16℃,不转换成浮点,先取整数部分再取小数部分 */ int temp_int = raw / 16; /* 整数部分,C 语言整除向零取整 */ int temp_frac = (raw % 16) * 625; /* 小数部分,单位 1e-4 ℃ */ /* 显示时:temp_int 送整数位,temp_frac / 100 送小数点后两位 */

raw % 16在正温度时得到 0~15,乘以 625 得到 0~9375,再除以 100 就是小数点后两位的数值。负温度时raw / 16向零取整会丢精度,比如 −162 / 16 = −10,余数 −2,temp_frac变成 −1250,需要单独处理符号。常见的做法是统一转成「0.01℃ 单位的整数」再比较和显示,这样报警阈值、当前温度、显示缓冲三处的单位一致,避免量纲错位。

3.3 分辨率配置:9 位到 12 位的取舍

DS18B20 的分辨率不是固定的,由 Scratchpad 第 4 字节(配置寄存器)的 bit6、bit5 决定:

R1R0分辨率LSB(℃)最大转换时间
009 位0.593.75 ms
0110 位0.25187.5 ms
1011 位0.125375 ms
1112 位0.0625750 ms

这是一个精度与刷新率之间的直接取舍。12 位分辨率能显示到 0.0625℃,但每次转换要 750ms,意味着温度刷新率只有 1Hz 多一点;9 位分辨率只要 93.75ms,刷新率能到 10Hz,但最小刻度变成 0.5℃,室温下显示会「跳」,人眼看起来不连续。数字温度计 demo 通常默认 12 位,因为显示一位小数时 0.5℃ 的跳变太明显;但如果同时要做温度闭环控制,750ms 的转换延迟就会成为控制周期的瓶颈,这时候更常见的做法是降到 10 位或 11 位,用 0.25℃ 或 0.125℃ 的精度换 2~4 倍的采样率。

配置寄存器的写入需要通过0x4E(Write Scratchpad)命令写第 2、3、4 字节,再发0x48(Copy Scratchpad)把配置固化到 E²PROM,否则掉电后回到默认的 12 位。

4. 从时序到显示:AT89C52 怎么把 1-Wire 数据变成数码管上的数字

4.1 机器周期与软件延时:12MHz 和 11.0592MHz 差在哪

AT89C52 是标准 12 分频 8051 内核,机器周期 = 12 / 晶振频率。12MHz 晶振下一个机器周期正好 1µs,这是最直观的选择——延时函数里一个空循环大约就是几个微秒,算起来方便。11.0592MHz 的机器周期是 12 / 11.0592MHz ≈ 1.085µs,每个机器周期多出 8.5%,看起来不整齐,但这个频率的存在意义是能精确分频出标准串口波特率:9600bps 在 11.0592MHz 下可以由定时器 1 的 8 位自动重载模式精确产生,而 12MHz 下会产生约 8.5% 的波特率误差,长时间串口通信会累积到丢字节。

所以晶振选择本质上是「单总线时序精度」和「串口波特率精度」之间的取舍。如果系统不需要串口通信,12MHz 更省心;如果需要和上位机通信,11.0592MHz 更稳妥,但所有delay_us里的计数值都要按 1.085 倍重新算,否则单总线读写就会踩坑。这也是为什么同一份代码换一块开发板就翻车的最常见原因:晶振换了,没人去改延时常量。

4.2 读写字节的循环骨架

单总线一次读/写一个字节,都是低位在前、循环 8 次、每次调一位的读写函数:

/* 向 DS18B20 写一个字节,低位先发 */ void ow_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { ow_write_bit(dat & 0x01); /* 取最低位 */ dat >>= 1; /* 右移,下一位变成最低位 */ } } /* 从 DS18B20 读一个字节,低位先收 */ unsigned char ow_read_byte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; /* 先右移腾出最高位 */ if (ow_read_bit()) dat |= 0x80; /* 读到 1 就置最高位 */ } return dat; }

注意读字节的顺序:先右移再或上 0x80,这样第一次读到的位最终落在 bit0,第八次读到的位落在 bit7,符合「低位先收」的约定。如果反过来先或再移,整个字节的位序会颠倒,读出来的温度值完全错乱,但数值范围看起来还在合理区间——这是排查时最迷惑人的一类 bug。

一次完整的温度读取流程是:ow_reset()→ow_write_byte(0xCC)跳过 ROM →ow_write_byte(0x44)启动转换 → 延时 750ms(12 位分辨率)→ow_reset()→ow_write_byte(0xCC)→ow_write_byte(0xBE)读 Scratchpad → 连续读 9 个字节 → 取前两个字节拼温度。

4.3 串口模式 0 + 74LS164:一对引脚驱动四位数码管

四位数码管如果直接用并行 IO 驱动,段码 8 根 + 位选 4 根要 12 根 IO,对 AT89C52 来说太奢侈。这套设计用的是串口模式 0 + 74LS164 移位寄存器:把串口配置成模式 0(同步移位寄存器模式),TXD(P3.1)输出同步时钟,RXD(P3.0)输出串行数据,每发一个字节,74LS164 就把 8 位数据从串行转成并行输出,直接驱动一位数码管的 8 个段。

74LS164 的 Q7 引脚还可以级联到下一片 74LS164 的串行输入,所以两片 164 级联就能驱动两位,四片级联驱动四位——只用 TXD、RXD 两根线就搞定了四位数码管,代价是每次刷新要串行送出 4 个字节,刷新周期比并行驱动长。串口模式 0 的波特率固定为 fosc/12,也就是每个时钟周期移一位,四位数码管全刷新一次大约需要 32 个机器周期,对 1ms 级别的扫描周期来说完全够用。

/* 串口模式 0 发送一个字节,硬件自动移位输出到 74LS164 */ void led_shift_out(unsigned char dat) { SBUF = dat; /* 写入 SBUF 启动发送 */ while (!TI); /* 等发送完成 */ TI = 0; /* 手动清标志,模式 0 不会自动清 */ }

段码表的共阴/共阳要与数码管硬件匹配:共阴数码管的段码是「1 亮 0 灭」,共阳反过来。段码表搞反的典型现象是上电后四位数码管全显示 8 或者全灭,很多人第一反应是接线问题,实际是段码表的方向错了。

4.4 报警阈值比较:主循环还是中断

温度报警的逻辑很朴素:把当前温度和用户设定的上限 TH、下限 TL 比较,超出范围就驱动蜂鸣器。TH 和 TL 存在两个全局int变量里,通过按键修改,掉电不保存——这是教学 demo 的标准做法,量产产品会再加一颗 24C02 之类的 E²PROM 存阈值。

比较放在哪里很关键:放在主循环里,意味着报警响应延迟受主循环周期影响,如果主循环里有个 750ms 的转换等待,报警最多会晚 750ms 触发;放在定时器中断里,则每 1ms 比较一次,响应及时但要注意中断里不要做耗时操作,蜂鸣器驱动最好用「置标志位 + 主循环执行」的方式,而不是在中断里直接拉 IO。

注意:温度比较一定要用有符号整数。如果把温度存在unsigned char里,−5℃ 会被存成 251,和上限 30 比较直接触发报警,冬天室外测试时就会看到「越冷越报警」的诡异现象。

5. 常见问题排查:85℃、乱码、报警不响

5.1 读数恒为 85℃:上电默认值在作怪

现象:数码管或者串口打印出来的温度永远停在 85.0,不管用手捂还是放冰袋都不变。

原因:DS18B20 的温度寄存器上电默认值就是 0x0550,也就是 +85℃。出现这个读数意味着主机从来没成功读到过真实转换结果,常见分支有三个:复位脉冲后没检测到存在脉冲(从机根本没应答);Convert T 命令没发出去或者被总线冲突吞掉;转换后等待时间不够,读回的还是上一次的旧值。

解决:先单独打印ow_reset()的返回值,确认存在脉冲能检测到;再确认0x44命令之后确实等了 750ms(12 位分辨率);最后用示波器看 DQ 线上有没有出现从机拉低的 60~240µs 响应脉冲。如果存在脉冲一直是 0,检查上拉电阻是否焊了、DQ 是否接错引脚、探头是否供电。

5.2 数码管乱码或全 8:段码表和级联顺序

现象:四位数码管每一位都显示 8,或者显示的数字顺序整体反了(比如 25.6 显示成 6.52)。

原因:全 8 通常是段码表方向与数码管共阴/共阳不匹配,所有段都被点亮;顺序反了则是 74LS164 级联时第一片的 Q7 接到了第二片的 Q0 之类,移位方向搞反。

解决:先只点一位数码管,确认段码表能正确显示 0~9,再级联第二片;级联时牢记「前一级的 Q7 接后一级的串行输入」,第一片收到的字节最终会移到最远端,所以送数据的顺序要和物理位置对应。共阴/共阳的判别方法是看数码管公共端接 VCC 还是 GND,接 VCC 是共阳,接 GND 是共阴。

5.3 报警该响不响:单位不一致和有符号数

现象:上限设成 30℃,实际温度已经 35℃,蜂鸣器一动不动;或者下限设成 10℃,20℃ 时就报警。

原因:最常见的错误是当前温度用 0.0625℃ 单位的原始值(比如 35℃ 存成 560),阈值却用摄氏度整数(30),560 > 30 永远成立或永远不成立,取决于比较方向。另一种错误是把温度存unsigned char,负温度变成 200 多,和阈值一比较就误触发。

解决:把当前温度、上限、下限统一转成同一个单位(推荐 0.01℃ 整数),比较之前先做单位换算;温度变量一律用int或long,不用unsigned char。阈值范围要覆盖 −55~125℃,不要因为「应用场景是室温」就把下限设成 0,否则冬天室外测试会看到莫名其妙的报警。

5.4 仿真通过、实物不稳:理想导线与现实分布电容

现象:Proteus 里跑得好好的,焊到板子上读温度时好时坏,有时候读回 0,有时候读回 85,重启又正常。

原因:仿真软件里总线是理想导线、没有电容,实物上 1-Wire 总线的上升沿由 4.7kΩ 上拉和线缆分布电容共同决定。引线超过 1 米、或者探头附近有强干扰源时,上升时间可能超过 10µs,读时隙的 15µs 采样点就变得很危险。这类问题就是单总线的「玄学」部分——同样的代码,换根短线就好了,换根长线又坏了。

解决:把探头引线控制在 1 米以内并远离电机、继电器;上拉电阻从 4.7kΩ 降到 2.2kΩ(代价是功耗略增,但上升沿更陡);读时隙采样点从 10µs 挪到 12µs 但不超 15µs;每次读温度前都完整走一遍复位和存在脉冲检测,读到 85 时重试一次。

5.5 显示闪烁:750ms 阻塞延时吃掉了刷新率

现象:数码管显示的温度值明显一跳一跳,目测刷新率不到 2Hz。

原因:如果显示扫描和温度转换都放在主循环里,而 Convert T 之后用delay_ms(750)硬等,主循环周期就被拉到 750ms 以上,数码管扫描频率掉到 1.3Hz 左右,人眼能明显看到闪烁。

解决:把数码管扫描放进定时器中断,每 1~2ms 刷一位,主循环只负责温度读取和报警比较;或者把 750ms 等待拆成 75 次 10ms 延时,每次延时后调用一次显示刷新函数。后一种做法不需要改中断结构,是教学 demo 里成本最低的改法。

6. 设计取舍:为什么这套组合在教学和量产中都活得很好

6.1 传感器选型:数字输出替掉的是整条模拟链路

选 DS18B20 而不是 PT100、热电偶,本质上是用「一颗集成芯片」替掉「传感器 + 运放 + 基准源 + ADC + 标定流程」这整条链路。模拟链路的每一级都有失调和温漂,出厂前需要标定,使用中还会随温度和时间漂移;DS18B20 的数字输出在芯片内部已经完成了转换,外部只需要处理数字时序,不需要任何标定。代价是单总线时序对晶振和延时精度敏感,长线多探头时布线要求比 I²C、SPI 更苛刻。

这个取舍在什么场景下划算?温度范围在 −55~125℃、精度要求在 ±0.5℃ 以内、探头数量少、布线距离短的场合,DS18B20 几乎是无脑选择;超出这个范围(高温、高精度、强干扰、长距离)就应该换 PT100 + 外部 ADC 的方案。

6.2 单片机选型:够用就好的工程理性

AT89C52 是 8 位 8051 内核,4KB Flash、128B RAM,跑这套温度计程序绰绰有余。选它的理由不是性能,而是工具链成熟、调试器便宜、引脚定义在教科书里出现过无数次。相比 STM32 之类的 32 位平台,51 的开发门槛低一个数量级,适合教学和快速原型;相比更便宜的台系 8 位 MCU,51 的资料和例程又丰富得多。

这套组合的真正价值在于:它把一个完整的嵌入式系统拆成了「传感器时序 + 数据处理 + 人机界面」三个可独立验证的模块,任何一个模块出问题都能单独隔离排查。对于要理解底层机制的人来说,这种「每个环节都能用示波器和串口看到」的透明度,比性能本身重要得多。

6.3 局限与延伸

这套设计的局限也很清楚:单总线对时序敏感,12 位分辨率下 750ms 转换时间限制了控制周期,4KB Flash 跑不下复杂的滤波算法,没有硬件 I²C/SPI 意味着扩展其他传感器要靠软件模拟。常见的延伸方向是加 DS1302 做时间戳、加 24C02 存历史温度、加 MAX232 做 RS232 上传,把「本地温度计」变成「带记录功能的数据采集节点」。

我自己的血泪经验是:每次换晶振、换编译器优化等级、换探头批次,都要重新跑一遍 85℃ 检测和负温度测试。延时函数的实际耗时会被编译器优化改变,探头批次的时序容差也会有差异,这两件事叠加起来就是「昨天还好好的今天就坏了」的经典场景。从那以后我每次改完延时参数,都强制在 −10℃ 和 +85℃ 两个极端点各测一次,再上常温验证。希望帮到你。

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