news 2026/10/15 1:18:51

DS18B20驱动避坑指南:单总线时序分析与稳定读取实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DS18B20驱动避坑指南:单总线时序分析与稳定读取实战

DS18B20是很多单片机学习者的第一个数字温度传感器,也是蓝桥杯单片机组试题里反复出现的外设。这篇笔记是系列里的第八篇,不打算再重复基础接线那一套,而是把驱动代码背后真正容易踩的坑全部过一遍。如果你已经点亮过数码管、能控制继电器,却写不出一个稳定的温度读取函数,这篇应该能帮你少走不少弯路。

我在带训练的时候经常遇到一种现象:程序明明照着网上的模板抄的,编译也没有任何报错,但温度读数要么固定不变,要么直接是85℃。这不是运气问题,而是对单总线时序的理解不够深入。DS18B20虽然只有一根数据线,可这一根线上的电平变化有着严格的时间窗口,差一点就白读。下面我按从原理到实战的顺序拆开讲。

1. 一根线怎么聊出温度:单总线协议与竞赛出题逻辑

1.1 单总线为什么让很多人头大

IIC有SCL时钟线,SPI有SCK时钟线,主机和从机至少有个时钟同步的基准。DS18B20不一样,它只有一根DQ数据线,既要传命令又要传数据,还要传时序同步信息。这就意味着,通信双方必须靠“拉低多久、释放多久、什么时候采样”这类的电平占空比来传递信号。

这个设计在竞赛里其实非常讨巧。命题人不需要你背复杂的协议栈,只需要你真正理解“微秒级的延时和采样窗口”就够了。说得难听一点,DS18B20这道题不是考你记不记得0x44是温度转换命令、0xBE是读暂存器命令,而是考你在写代码的时候,有没有意识到这条线当前是输出还是输入,有没有意识到延时函数在主频不同的芯片上会产生完全不同的实际长度。

蓝桥杯单片机组的板子常用STC15系列或者IAP15系列的单片机,这类芯片默认工作在1T模式,也就是一个机器周期等于一个时钟周期。有不少人之前学的是12T模式的STC89C52,把老代码直接搬过来,延时函数的时间全部缩到原来的十二分之一,DS18B20能正常工作才怪。这里涉及的具体换算我在后面章节会细说。

1.2 竞赛板上的引脚映射与电路细节

先说明一点:不同年份、不同版本的竞赛板,DS18B20的DQ引脚接法不完全一样,但常见做法是把它通过跳线帽接到P1口的某个引脚。所以在你开始写代码之前,第一件事是翻原理图,确认三件事:

  • DQ接到的是P1组的哪一个引脚
  • 板上是否已经集成4.7kΩ上拉电阻
  • 跳线帽是否已经正确短接

很多人程序没问题,结果温度读不出来,最后发现是跳线帽没插。这种问题在训练中出现得非常频繁,因为板子上跳线帽太多了,DS18B20座子附近的那个,和红外接收头附近那个,长得一模一样。

DQ引脚是开漏输出结构,正常工作时必须有一个上拉电阻把电平拉到高,DS18B20自身才能通过拉低总线来输出数据。竞赛板上一般都有这个电阻,不用你外接。但如果你是自己搭的最小系统板,一定记得在DQ和VCC之间接一个4.7kΩ电阻。没有这个电阻,通信大概率时好时坏,因为浮空输入的电平完全不可控。

2. 复位、写位、读位:DS18B20时序的三个核心段落

2.1 复位脉冲与存在脉冲的判定逻辑

单总线所有通信的第一步都是复位,相当于主机对从机说:我要开始跟你通信了,你准备好了吗。

主机的操作是:把DQ拉低,保持至少480微秒,然后释放总线,让上拉电阻把电平拉高。DS18B20检测到电平从低跳变到高之后,会等待一段时间,然后主动把总线拉低60到240微秒,这个动作叫存在脉冲。主机在释放总线后的60到240微秒窗口内采样DQ,如果读到低电平,说明从机在线;如果读到高电平,说明总线上没有设备,或者设备根本没起来。

常见实现代码可以这样写:

bit DS18B20_Reset(void) { bit presence; DQ = 0; // 主机拉低总线 delay_us(500); // 保持至少480us DQ = 1; // 释放总线 delay_us(60); // 等待从机响应 presence = DQ; // 采样存在脉冲 delay_us(240); // 等待整个复位周期结束 return presence; // 返回1表示无设备响应 }

需要注意一个细节:读DQ引脚之前,要确保引脚对应的锁存器已经写入了1。这是51单片机准双向IO口的特性。如果你前面执行过DQ = 0,此时锁存器是0,释放总线后即使外部上拉电阻已经拉高,读回来的电平也可能不对。上面的代码里用DQ = 1显式释放,这一步不仅仅是“把线拉高”,同时也是把锁存器设置为高,让后续读取动作有效。

2.2 写0和写1,本质是拉低时间不同

写时序是用来给DS18B20发送命令字节的,比如0xCC、0x44。每一位的传输都需要主机先拉低总线,然后再根据要写的数据决定何时释放。

写1的流程是:主机拉低总线,保持1到15微秒,然后释放,让总线在高电平状态下保持剩余的时间,总时长60微秒以上。

写0的流程是:主机拉低总线,保持足足60到120微秒,然后再释放。

也就是说,判断这一位是0还是1,不是看有没有拉低,而是看拉低了多久。时间短就是1,时间长就是0。

对应的代码可以这样实现:

void DS18B20_WriteBit(bit dat) { DQ = 0; _nop_(); if (dat) { DQ = 1; // 写1:短拉低后释放 delay_us(60); } else { delay_us(60); // 写0:持续拉低 DQ = 1; } }

写完第一位后,紧接着写第二位,每一位之间的时间间隔要控制好,不能太慢,也不能太快。整体上每位在60到120微秒之间都是可以接受的,DS18B20的容错窗口其实没有想象中那么小。

2.3 读时序,难点在“尽早采样”

读时序就更有意思了。主机要读取DS18B20输出的数据,但单总线上没有时钟线,主机必须自己产生一个读时隙,然后在这个时隙的特定窗口内采样。

主机先拉低总线,至少1微秒,然后释放。DS18B20如果想让主机读到0,它会在释放后继续把总线拉低;如果想让主机读到1,它就不做动作,让上拉电阻把电平拉高。主机必须在读时隙开始后的15微秒内完成采样,因为DS18B20输出数据的时间窗口很短。

bit DS18B20_ReadBit(void) { bit dat; DQ = 0; _nop_(); DQ = 1; delay_us(2); // 让总线稳定一下 dat = DQ; // 采样,必须在15us内 delay_us(60); // 等待整个读时隙结束 return dat; }

读数据字节和写数据字节的过程是相反的操作:从最低位开始,连续读8个位,拼成一个字节。读完每个位之后,建议留出足够的时间槽总长,保证每个读时隙的间隔稳定。

3. 从转换到读值:寄存器的拼装与温度的换算逻辑

3.1 一次完整的温度读取流程

先梳理一次温度读取的完整命令序列,一共两轮通信:

第一轮:复位 → 发0xCC跳过ROM匹配 → 发0x44启动温度转换。转换需要时间,12位精度下最大耗时750毫秒,之后温度数据会更新到暂存器的第0和第1字节。

第二轮:复位 → 发0xCC跳过ROM匹配 → 发0xBE读取暂存器。连续读9个字节,其中前两个字节就是温度值。

为什么要分成两轮?因为0x44命令之后,DS18B20要把模拟测温结果经过ADC转换成数字量,这个转换过程是异步的,转换期间芯片可能还在忙。正确做法是发完0x44后等待转换完成,然后再重新握手一次读取结果。

新手最常犯的错是把两轮通信合在一起,发完0x44马上就读暂存器,结果读回来的永远是上一次的旧数据。上电第一次读时,因为没有做过转换,读到的就是上电复位默认值0x0550,也就是85℃。

等待转换完成有两种常见方式:一种是硬延时750毫秒,简单粗暴但浪费时间;另一种是查询DQ引脚,当转换完成后,DS18B20会自动把总线拉低。第二种方式效率更高,代码也不复杂:

void DS18B20_StartConvert(void) { DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); // 等待转换完成,DQ被从机拉低表示转换结束 while (DQ); }

注意while(DQ)这个循环里,DQ是输入状态,循环退出条件是从机把总线拉低。因为之前已经释放总线并且锁存器是1,所以可以正常读取。如果总线一直为高,说明转换失败或者设备异常,程序会卡死在这里。实际产品代码要加超时处理,竞赛代码一般不管,但你要知道有这回事。

3.2 两个字节怎么拼接成温度值

读取暂存器之后,第0字节是温度低字节,第1字节是温度高字节。这两个字节拼成一个16位有符号数,规则并不是简单的左移8位或右移8位,而是固定的位分配。

12位精度下,温度寄存器的格式是:

  • bit15:符号位,1表示负温度
  • bit14到bit4:温度整数部分的补码
  • bit3到bit0:温度小数部分,每一位对应的权重依次是2^-1、2^-2、2^-3、2^-4,也就是0.5、0.25、0.125、0.0625

所以整个16位值乘以0.0625,就得到实际温度。比如0x0191是25.0625℃(0x0191 = 401,401 × 0.0625 = 25.0625)。

在51单片机上直接算浮点数当然可以,但浮点运算慢、占用资源多,竞赛中更常见的做法是直接用整数处理,比如把温度值扩大10倍保留一位小数。

unsigned int DS18B20_ReadTemp(void) { unsigned char low, high; unsigned int temp; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); temp = high; temp = (temp << 8) | low; return temp; }

拿到原始值之后,要做一次符号判断和正负处理。如果是负数,原始值是按补码存的,直接乘0.0625你会得到一大串无意义的数据。正确处理方式是判断最高位,如果是1,就先取反加1转成原码,再乘以0.0625,并且记录符号位是负。

signed int TempC10(unsigned int raw) { signed int result; if (raw & 0x8000) { raw = ~raw + 1; result = (signed int)(raw * 0.625); // 保留一位小数,0.0625*10=0.625 result = -result; } else { result = (signed int)(raw * 0.625); } return result; }

3.3 暂存器第9字节的CRC,要不要处理

暂存器一共9个字节,最后一个字节是前8个字节的CRC校验值。DS18B20内置了CRC发生器,主机如果连读9个字节,可以自行计算CRC并与最后一个字节比对,判断通信过程中有没有数据跳变。

竞赛场景下,绝大多数选手不会做CRC校验,因为多读一个字节不费事,但自己实现CRC8计算会增加代码量,而且竞赛的通信距离很短、环境干扰不大,数据出错的概率很低。不过有一个细节值得知道:如果你只读取了前两个字节就结束通信,DS18B20会把剩余字节继续输出,如果你想节省时间提前结束读取,可以直接把总线释放,但下一次通信前必须重新复位握手。

4. 实测中最容易翻车的三个场景

4.1 中断把微秒级时序打断,温度读着读着就飘了

DS18B20的位时序对时间窗口的要求虽然有一定容错,但不是无限容错。以读时序为例,主机必须在释放总线后的15微秒内采样,如果你的程序开了定时器中断,在拉低总线之后、采样之前突然跳进中断服务程序,哪怕只执行了几条指令,回来再采样时,DS18B20输出的数据窗口已经结束了,读到的高电平会使数据全部错位。

这个问题在实验室里很难复现,因为单纯读取温度时,中断频率不高,偶尔一次采样错误,显示屏上的温度变化不明显,甚至被当成正常波动忽略。但在竞赛中,如果主循环里有按键扫描、数码管动态刷新、串口输出和温度读取并存,中断密度会大幅度上升,温度读数就会出现偶尔跳变或者固定错误值的现象。

我推荐的方案是给读时序加临界区保护。最简单粗暴的做法是读温度前关闭总中断EA,读完整个温度值后再打开。但这样有一个新问题:如果等待转换完成的750毫秒也把中断关了,数码管亮度会明显抖动,按键扫描也会卡顿,这是不可接受的。

正确做法是分工段关闭:

EA = 0; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); EA = 1; while (DQ); // 等待转换,不关中断 EA = 0; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); EA = 1;

第一次关中断的时间很短,主要覆盖命令发送;中间等待转换的750毫秒完全放开中断,让数码管照常刷新;第二次关中断覆盖整个读字节过程,读取本身耗时大概几百微秒到1毫秒,这么短的时间对数码管刷新影响很小,肉眼基本看不出闪烁。

4.2 温度恒定85℃:不是传感器坏了,是通信一直没建立

85℃在调试中出现的频率高得离谱。它的来源不是真实温度,而是DS18B20上电复位后温度寄存器的默认值:0x0550,十六进制换算过来正好是85℃。也就是说,如果你读取到的原始值始终是0x0550,大概率不是环境温度真的到了85度,而是你的主机和从机之间的通信链路根本没有建立成功。

我在训练营里帮人查过很多次这个问题,排查链路基本是固定的:

第一步,确认供电。DS18B20的VCC和GND是否接对,注意有些模块是GND、DQ、VCC三根线的顺序,接反了芯片会发热但不一定烧,表现就是没有任何响应。

第二步,确认上拉。把DQ引脚用万用表量一下,正常空闲状态应该是高电平,如果量出来是0V或者悬空,就是上拉电阻缺失或跳线帽没接。

第三步,确认引脚。代码里的sbit声明和实际板子原理图是否一致。

第四步,确认复位时序。把DS18B20_Reset函数的返回值通过串口打印出来,如果返回1,说明根本没读到存在脉冲,问题出在初始化时序或硬件连接上。

要注意的是,每次通信都是以复位开始的,复位成功不代表后面读写一定成功,但复位失败一定什么都读不到。

4.3 同一套代码两块板子表现不同,先别急着怀疑芯片

有一次训练,同一个程序在一块板子上温度读取一切正常,换到另一块板子就读出85℃,两块板子型号相同。排查了十分钟,发现第二块板子的DS18B20接口旁边插着一个多余的杜邦线,那个杜邦线的另一端碰巧接触到了另一个IO口,导致DQ电平被干扰。拔掉杜邦线之后一切正常。

这种问题在实验室环境下特别容易发生,因为大家习惯用杜邦线临时搭电路,线束又乱又长。DS18B20对外接导线的长度比较敏感,导线一长,分布电容变大,电平跳变的边沿会变缓,加上上拉电阻不匹配,时序就容易跑偏。解决办法是把连接线尽量缩短,或者把上拉电阻从4.7kΩ换成2.2kΩ,给总线更强的上拉能力。竞赛板自带的走线长度固定,一般不会出这种问题,但自己搭板子的人要注意。

5. 一个可以直接照抄的驱动骨架与提速思路

5.1 基础读写函数的组织方式

驱动层建议按照“底层位操作 → 字节操作 → 功能命令”三个层次组织。这样代码结构清爽,出问题时也好定位。

// 底层位操作 void DS18B20_DelayUs(unsigned int t) { while (t--) { _nop_(); } } bit DS18B20_ReadBit(void) { bit dat; DQ = 0; _nop_(); DQ = 1; DS18B20_DelayUs(2); dat = DQ; DS18B20_DelayUs(60); return dat; } void DS18B20_WriteBit(bit dat) { DQ = 0; _nop_(); if (dat) { DQ = 1; DS18B20_DelayUs(60); } else { DS18B20_DelayUs(60); DQ = 1; } } // 字节操作 void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS18B20_WriteBit((bit)(dat & 0x01)); dat >>= 1; } } unsigned char DS18B20_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; if (DS18B20_ReadBit()) { dat |= 0x80; } } return dat; }

需要提醒一点,我这里的DS18B20_DelayUs是用NOP循环做的近似延时,它不精确,但在绝大多数情况下够用。如果你做产品或者做高可靠性设计,建议用定时器或者STC15系列硬件自带的延时工具来校准。所谓够用,是指DS18B20的时间窗口本身有裕量,复位时480微秒多几十微秒没问题,写周期60到120微秒也在容忍范围内。

5.2 不要在while(1)里反复读温度

主循环的写法直接决定整体不掉帧。如果每隔几毫秒就调一次完整的温度读取,每一轮都包含至少750毫秒的转换等待,整个系统的实时性就会被拖垮。正确做法是定时启动转换,隔一段时间后再读取结果,其余时间让单片机处理显示和按键。

最简单的状态机写法大概是这样:

unsigned int g_temp_raw = 0; unsigned char g_temp_ready = 0; void PollDS18B20(void) { static unsigned char step = 0; static unsigned int waitCnt = 0; switch (step) { case 0: EA = 0; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); EA = 1; waitCnt = 0; step = 1; break; case 1: waitCnt++; if (waitCnt > 5000) { // 让时间流过 step = 2; } break; case 2: EA = 0; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); g_temp_raw = DS18B20_ReadByte(); g_temp_raw |= (unsigned int)DS18B20_ReadByte() << 8; EA = 1; g_temp_ready = 1; step = 0; break; } }

这个函数放在主循环里调用,每次调用只执行一个步骤,不会阻塞其他任务。等待转换完成的那段时间完全可以用来看按键、刷数码管。竞赛程序能稳定跑到最后的,基本都是这种结构。

5.3 验证温度驱动是否正常的小技巧

程序写完先别急着接其他外设,单独做一个只显示温度的实验。用手捏住传感器,观察温度是否平稳上升,用嘴哈一口气,温度应该快速上升几度,过一会儿再降回来。如果温度变化毫无规律,跳来跳去,优先怀疑时序被中断干扰;如果温度纹丝不动,优先怀疑通信没建立。

还有一个细节,读取原始值0x0550也就是85℃,和真实环境温度差距很大,这种明显的异常千万不要先去怀疑传感器坏了。先查通信链路,再查时序,最后才考虑换芯片,顺序反了会浪费很多时间。

最后分享一点个人体会。DS18B20驱动在蓝桥杯里属于性价比很高的外设:代码量不大,原理不复杂,但能把复位时序、位时序、字节拼接、临界区保护、状态机这些基本功都串起来。认真把它调通一次,后面再做其他单总线器件,比如EEPROM或者各类单总线传感器,你会发现思路完全是通用的。一个驱动写得好不好,不是看它能读回温度,而是看它在按键、数码管、中断同时存在的情况下还能不能稳定读回温度,这才是竞赛真正要考的东西。

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

Python+ImageJ图像分析自动化:从环境配置到批量处理的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:18:24

UART接收模块设计:抗亚稳态、高鲁棒性Verilog实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:18:04

MIPS逆向工程实战:从固件分析到ROP利用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:17:53

OCR与STR本质区别:文档识别vs场景文本理解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:16:28

Java实现BD-09、GCJ-02与WGS84坐标系精准互转

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:15:55

如何构建第一时间发现线上故障的监控告警体系

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华