尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32F103读取DS18B20温度传感器:从时序原理到完整驱动实现

STM32F103读取DS18B20温度传感器:从时序原理到完整驱动实现 简介本资源是基于STM32F103系列MCU实现DS18B20单总线数字温度传感器数据读取的完整Keil工程源码面向嵌入式初学者与STM32开发实践者解决单总线协议驱动、温度数据解析及硬件时序精准控制等典型难点问题。压缩包共67个文件涵盖9个核心C源文件如main.c、ds18b20.c、delay.c、9个头文件h、10个编译中间文件o、10个依赖描述d及工程配置文件uvproj/uvopt/sct/hex/axf等全面支撑编译、调试与烧录全流程包体大小为1.1MB。已有1558人学习下载体现了其在STM32基础外设实战中的高参考价值。读者可直接导入Keil MDK运行获得包含初始化、ROM搜索、温度转换、12位数据读取与串口打印的完整功能链目录结构清晰划分为SYSTEM、DS18B20、LED、KEY、USART等模块便于理解分层设计逻辑并为扩展多点测温或低功耗应用提供可靠基础。 做单片机开发的人迟早会碰到DS18B20这个温度传感器。STM32F103读取DS18B20可以说是一套非常经典的入门组合。无论是课程设计、毕业设计还是实际项目里的温度采集模块这对组合出现的频率高得惊人。我手头这个工程包就是一套完整的“STM32F103读取DS18B20温度数据”代码包含初始化、时序控制、温度转换、数据读取和串口打印的完整流程。这篇文章就把这套方案从头到尾讲透适合刚接触STM32的初学者也适合那些想快速把DS18B20驱动移植到自己项目里的开发者。这个项目解决的核心问题很简单用STM32F103的普通GPIO引脚按照DS18B20规定的单总线时序来读写数据最终获取并解析出当前环境温度。硬件上只需要一个DS18B20、一个4.7kΩ上拉电阻和几根杜邦线软件上核心就是一个GPIO模拟时序的驱动文件。整套方案下来成本极低而且代码可以直接抄到自己的工程里跑通。1. 方案设计与硬件基础1.1 为什么选择DS18B20而不是其他温度传感器做温度采集市面上的方案其实很多。热敏电阻NTC最便宜但需要自己搭桥电路、做ADC采样、查表校准温度标定非常麻烦精度还受电源波动影响。I2C接口的数字传感器比如LM75、TMP102虽然通信简单但都需要额外的I2C外设或者模拟I2C的实现多一根时钟线就意味着多一个GPIO占用。DHT11虽然也便宜但湿度温度一体精度只有±2℃而且采样周期至少1秒很多工业场景根本不够用。DS18B20能在这么多方案里胜出核心优势有三点第一单总线通信只占一个IO口。在STM32F103这种引脚资源其实不算丰富的芯片上省引脚就是省事。第二数字信号直接输出不需要ADC采样。这意味着你拿到手的就是一个可以直接换算的温度值温度数据分辨率为9到12位可调12位分辨率模式下精度达到0.0625℃这个精度在绝大多数民用和半工业场景里完全够用。第三支持多点组网。每个DS18B20出厂时都有一个64位唯一序列号理论上可以在同一条总线上挂载多个设备通过ROM指令来寻址实现多路温度采集。所以选择DS18B20本质上是用“模拟复杂时序”来换取“硬件简洁”和“成本低廉”。这种取舍在嵌入式开发里非常常见——芯片贵一点没关系但引脚少、布线省、量产后成本低才是项目里真正要考虑的。1.2 硬件连接与最小系统注意事项先讲硬件连接。DS18B20有三个引脚但实际上很多人并不知道它有三种供电方式。第一种是外部供电就是VDD接3.3V或者5VGND接地DQ数据线接STM32的一个GPIO再在DQ和VDD之间接一个4.7kΩ上拉电阻。第二种是寄生供电VDD和GND都接地只靠DQ线在特定时序窗口内给传感器内部的电容充电来维持工作。第三种是强上拉寄生供电适合多设备组网场景。我强烈建议初学者直接采用第一种外部供电方式。寄生供电虽然能省一根线但STM32F103的GPIO驱动能力在高温转换期间可能撑不住DS18B20的功耗需求导致转换结果异常。实测中寄生供电在12位分辨率、750ms转换期间经常出现读回85℃这种典型错误值。外部供电则完全不用操心功耗问题。STM32F103这边的硬件连接我通常选择PB1或者PA0这种普通GPIO作为数据引脚不占用I2C和SPI资源。上拉电阻选4.7kΩ这个值不是随便定的。DS18B20的数据线是开漏结构需要外部上拉到高电平才能实现线与逻辑。上拉电阻太小低电平灌入电流大会增加功耗上拉电阻太大总线电容充放电时间变长影响时序。4.7kΩ是官方数据手册推荐值在3.3V供电下实测也最稳。这里必须提醒一个很多人踩过的坑STM32F103内部虽然有上拉电阻但内部上拉的阻值约在30kΩ到50kΩ之间远高于DS18B20推荐的4.7kΩ上拉电阻在很多情况下会导致时序边沿变缓从而读出的温度数据异常。所以外部必须外接一个4.7kΩ电阻到3.3V别指望内部上拉就能跑起来。完整的接线表如下DS18B20引脚连接目标说明VDD红色3.3V电源外部供电模式GND黑色GND共地DQ黄色STM32F103的PB1数据线需外接4.7kΩ上拉电阻到3.3VSTM32F103最小系统板上通常已经集成了8MHz晶振、复位电路、电源稳压芯片这些都不需要你额外操心。你只需要确保最小系统板能正常供电SWD下载口能连上调试器即可。1.3 GPIO模式选择的几个关键细节很多初学者在这里会犯一个错误就是机械地把GPIO配置成推挽输出。实际上DS18B20的单总线协议要求数据线支持双向传输而推挽输出模式在读取数据时无法释放总线控制权。正确的做法是使用开漏输出模式GPIO_Mode_Out_OD这也是我代码里的标准配置。开漏模式下如果输出寄存器写0引脚被拉低如果输出寄存器写1引脚处于高阻态靠外部4.7kΩ上拉电阻把电平拉高。这样就能实现“写1时释放总线、读数据时引脚作为输入”的效果。直接读取时可以把GPIO配置成浮空输入模式GPIO_Mode_IN_FLOATING但更高效的办法是保持开漏输出模式把输出寄存器写1再通过读IDR寄存器来获取引脚电平。这个技巧可以避免频繁切换GPIO模式减少模式切换导致的时序误差。我在实际工程里把GPIO配置和引脚宏定义写成了一个独立的头文件方便不同板子之间切换引脚。如果你用的是标准外设库而不是HAL库配置代码大概是这个样子的GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_1);注意GPIO速度要配置成50MHz因为单总线时序中的部分微秒级延时需要在高速指令下才更准确。速度配置过低可能会导致电平翻转时间变长进而影响时序裕量。2. DS18B20通信协议与时序解析2.1 单总线协议的核心机制DS18B20采用的是1-Wire单总线协议所有数据通信都在一根数据线上完成。这种协议的时序特点非常鲜明主机通过控制每个时隙内拉低总线的时间长短来表达逻辑0和逻辑1从机通过拉低总线的时间长短来回应。单总线的通信过程可以拆解为三个基本环节初始化时序、ROM命令时序、功能命令时序。每次温度采集都需要先执行初始化复位脉冲存在脉冲然后发送ROM命令如跳过ROM的0xCC或匹配ROM的0x55接着发送功能命令如启动温度转换的0x44或读取暂存器的0xBE。整个流程顺序不能乱一旦跳步或者时序不对传感器就不会响应。为什么这套机制能工作关键在于DS18B20内部有一个状态机它对输入的电平跳变进行计时并根据计时结果判断当前接收到的位是0还是1。所以外部MCU要做的本质上就是精确地控制每一个电平跳变的时间点。这就是为什么DS18B20的驱动代码对延时函数的精度要求这么高。初学阶段建议用逻辑分析仪抓取时序波形亲眼对比数据手册时序图比单纯看代码理解得快得多。我在调这个驱动的过程中逻辑分析仪帮我节省了至少半天在盲调上面的时间。2.2 初始化时序复位脉冲与存在脉冲初始化是所有单总线通信的起点。主机先将总线拉低至少480μs然后释放总线这就构成了一个复位脉冲。释放总线后由于上拉电阻的作用总线回到高电平。如果总线上有DS18B20它会在60μs到240μs之后主动拉低总线产生一个60μs到240μs的低电平脉冲这个就是存在脉冲。初始化时序的代码实现如下uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_DQ_LOW(); // 拉低总线 delay_us(480); // 复位脉冲至少480us DS18B20_DQ_HIGH(); // 释放总线 delay_us(60); // 等待DS18B20响应窗口 presence DS18B20_DQ_READ(); // 读取存在脉冲 delay_us(420); // 等待结束完成整个复位时序 return presence; // 若为0表示设备存在 }判断设备是否存在的方法是读取总线电平是否被拉低。如果读到0说明传感器正常响应如果仍然为1说明总线上没有设备或者接线错误或者上拉电阻没接。这个返回值可以用来做设备检测很多代码里会忽略这个返回值但工程上我建议必须处理否则后续读取的全是无效数据。2.3 读写时序写0、写1、读时隙时序通信的基础先讲写时隙。每个写时隙持续60μs到120μs主机通过控制总线拉低的时间来确定写的是0还是1。写0时主机拉低总线并保持60μs写1时主机拉低总线至少1μs然后在15μs内释放总线靠上拉电阻把电平拉高。写时隙的实现代码如下void DS18B20_WriteBit(uint8_t bit) { if (bit) { DS18B20_DQ_LOW(); delay_us(2); DS18B20_DQ_HIGH(); delay_us(60); } else { DS18B20_DQ_LOW(); delay_us(60); DS18B20_DQ_HIGH(); delay_us(2); } }这里有个细节写1和写0之后一定要给一小段时间让总线恢复然后再进入下一个时隙。别看2μs的差异总线上所有设备的时序判定都建立在这些微小的时间差上。如果时序太紧比如1μs的拉低时间实际只有不到1μsDS18B20可能无法识别这是读时隙的起始信号。读时隙比写时隙更讲究。主机先拉低总线至少1μs然后释放总线在释放总线后的15μs内DS18B20会把当前位的数据输出到总线上。如果数据位是1总线保持高电平如果是0DS18B20内部会继续拉低总线。读取数据的关键在于采样的时间点——必须在释放总线后15μs内完成采样最好是在10μs左右采样太早可能读到起始脉冲的残留低电平太晚可能读到下一个时隙的起始信号。读时隙的实现代码如下uint8_t DS18B20_ReadBit(void) { uint8_t data; DS18B20_DQ_LOW(); delay_us(2); // 至少1us DS18B20_DQ_HIGH(); // 释放总线 delay_us(5); // 等待数据稳定约5us data DS18B20_DQ_READ(); delay_us(50); // 完成整个读时隙 return data; }一次读一个位显然不够高效实际应用中是按字节读取。每个字节从最低位开始读写字节也是从最低位开始写。这个字节序是DS18B20协议规定的很多初学者在这里把位序搞反导致读出来的温度数据完全不对。3. 程序实现与关键代码3.1 延时函数的精确实现DS18B20的时序是以微秒为单位的所以延时函数的精确性直接决定了通信是否稳定。这里先说一个很多新手都会犯的错误直接用for循环空转实现微秒延时不做任何校准结果在不同编译优化等级下延时完全不同。我在STM32F103上推荐使用基于SysTick的微秒延时。SysTick是ARM Cortex-M3内核自带的定时器24位递减计数器用它的时钟频率来计算延时精度高且不受编译器优化影响。标准库的Delay函数虽然简单但使用的是系统时钟循环我建议改造一下。最简单的实现方式是利用SysTick-LOAD加载计数值然后等待SysTick-CTRL的COUNTFLAG置位void delay_us(uint32_t us) { uint32_t temp; SysTick-LOAD us * 72; // 72MHz主频1us计数72次 SysTick-VAL 0; SysTick-CTRL 0x01; // 使能SysTick使用HCLK do { temp SysTick-CTRL; } while ((temp 0x01) !(temp (1 16))); // 等待COUNTFLAG SysTick-CTRL 0; // 关闭SysTick }这个实现里把SysTick配置为72MHz时钟源也就是1μs计数72次。对于96μs这种非整数值直接用乘除法可能会有细微误差但DS18B20的时序窗口都在几十微秒级别这种误差完全可以接受。实测下来这个延时误差在1μs以内能稳定驱动DS18B20。3.2 初始化与ROM操作跳过ROM与匹配ROM在单总线通信中主机每次和DS18B20通信前都要先发送ROM命令。ROM命令的作用是让总线上的某个或某几个DS18B20响应后续的功能命令。如果总线上只有一个DS18B20可以直接发送0xCC跳过ROM寻址这样代码最简单。如果总线上挂了多个DS18B20就必须使用0x55匹配ROM命令并在后面发送对应设备的64位序列号。这个序列号需要在初始化时通过0x33读ROM命令获取。单设备场景下的初始化代码void DS18B20_Init(void) { DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM DS18B20_WriteByte(0x44); // 启动温度转换 }要注意的是启动温度转换之后12位分辨率下需要750ms才能完成转换。如果转换未完成就发送读暂存器命令读到的数据可能是上一次的转换结果或者是未定义的值。这个等待时间在工程中不能省略除非你用到了DS18B20的“忙状态标志位”即在转换过程中读总线总线会被拉低表示忙。3.3 温度读取与数据转换当温度转换完成后再次复位并跳过ROM然后发送0xBE读暂存器命令。DS18B20将依次输出9个字节的暂存器数据温度低字节、温度高字节、高低报警触发值、配置寄存器、保留位、CRC校验位。温度数据只在前两个字节组合方式如下uint8_t temp_lsb, temp_msb; temp_lsb DS18B20_ReadByte(); temp_msb DS18B20_ReadByte(); int16_t temp_raw (temp_msb 8) | temp_lsb;注意这里必须使用有符号的16位整数来存储组合结果因为温度值可能是负的。DS18B20的温度数据使用补码表示最高位为符号位。正温度时高字节的高位为0负温度时高字节的高位为1。例如读到的原始数据是0x0550即二进制0000 0101 0101 0000对应的十进制是1360。12位分辨率下每一位代表0.0625℃所以实际温度就是1360 × 0.0625 85℃。这个85℃在DS18B20里是一个标志性数值当读到这个温度时几乎可以断定是初始化时序没通过或转换还没完成就读取数据。负温度的处理就麻烦一些。如果temp_raw的最高位是1说明是负温度需要先取反加1得到绝对值再乘以0.0625最后加上负号。具体代码如下float DS18B20_GetTemperature(void) { uint8_t temp_lsb, temp_msb; int16_t temp_raw; float temperature; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); temp_lsb DS18B20_ReadByte(); temp_msb DS18B20_ReadByte(); DS18B20_Reset(); temp_raw (temp_msb 8) | temp_lsb; if (temp_raw 0x8000) { temp_raw ~temp_raw 1; temperature temp_raw * 0.0625f; temperature -temperature; } else { temperature temp_raw * 0.0625f; } return temperature; }如果你只需要整数部分可以把结果强制转换为uint8_t或int8_t但注意12位分辨率下小数部分是有实际意义的直接截断会损失精度。如果项目只需要0.1℃精度也可以把0.0625扩展一下用temp_raw * 625 / 10000的方式计算避免浮点数运算带来的性能开销。4. 常见问题与排查技巧实录4.1 读到的温度恒为85℃或-127℃这是一个出现频率极高的问题。85℃这个数值的来源我在前面已经解释过——未完成的转换或初始化失败时暂存器中的初始值会保留。DS18B20上电复位后暂存器中的温度寄存器默认值为0x0550也就是85℃。如果程序复位了传感器但没等转换完成就直接读数据读回的基本都是这个值。而-127℃这个值通常出现在读时序不稳定的时候。0xFF是总线的空闲电平如果DS18B20的读时隙没能把数据成功输出或者读时序的采样点不对读回来的字节就可能全是1也就是0xFF对应温度是-127℃。另一种常见情况是数据线没接上拉电阻读回来的永远是高电平。排查思路按照以下顺序来确认上拉电阻是否接好阻值是否为4.7kΩ。确认GPIO配置为开漏输出而不是推挽输出。用示波器或逻辑分析仪观察复位时序确认存在脉冲是否存在。确认每次读温度前都执行了完整的“复位→跳过ROM→启动转换→等待750ms→复位→跳过ROM→读暂存器”流程。我在实际调试中发现至少一半的“读不到温度”问题不是代码问题而是硬件问题。用万用表量一下DQ引脚的电压正常待机时应该是3.3V如果接近0V大概率是引脚短路了。4.2 时序漂移与延时可靠性问题时序漂移通常在温度变化比较大的环境下出现或者换了主频不同芯片后出现。我最初用8MHz内部时钟跑的时候延时是准的后来切换到72MHz外部晶振忘了改延时参数导致整个通信彻底罢工。这个问题本质上是延时函数的时钟源依赖性问题。只要代码里使用了基于系统主频的延时换主频就必须重新计算。建议把延时函数做成独立的模块并且在初始化时做一次自校准或者直接使用SysTick定时器让延时不依赖CPU主频。另外编译器的优化选项也会影响延时函数的可靠性。开了-O2优化后普通的for循环空转延时会被编译器部分优化掉导致延时变短。我自己遇到过开优化后从正常变为时序错乱的情况排查了很久才发现是优化导致延时函数失效。解决方法是把延时函数中用到的循环变量声明为volatile或者干脆用SysTick。4.3 多设备挂载与搜索ROM策略单总线上挂多个DS18B20最麻烦的就是ROM搜索。每个DS18B20都有唯一的64位ROM码需要在初始化时逐个搜索并记录下来。搜索过程需要逐位比较而且必须按照单总线协议的搜索算法来写代码量不小。但如果你确认总线上的设备数量不多可以用一个更简单的技巧先对所有设备执行跳过ROM并启动温度转换等转换完成后再逐个读。这个技巧在多设备系统中能减少搜索操作的复杂度。不过这个方法有个前提所有设备的转换命令需要同时触发所以初始化时必须发0xCC跳过ROM然后0x44启动转换。转换结束后如果要从总线上读取某个特定设备的温度就必须发送匹配ROM命令并附上该设备的ROM码。我把多设备场景的推荐流程整理如下步骤命令说明1复位检测总线上所有设备20xCC0x44跳过ROM启动所有设备同时转换3等待750ms12位分辨率转换时间4复位重新初始化总线50x55发送某设备ROM码0xBE匹配特定设备并读取其温度6重复步骤4-5读取下一个设备在实际多设备项目里我建议每读取完一个设备就做一次完整复位避免总线电平残留影响下一轮通信。单总线的时序容错性并不高复用同一根数据线时安全第一。4.4 一个实际排查案例读回数据偶尔跳变最后分享一个我实际遇到的问题。我的系统里DS18B20和电机驱动共用一个电源轨电机启动时读到的温度数据偶尔会跳变一个大值然后又恢复正常。刚开始以为是代码bug查了半天时序和CRC发现都没问题。后来用示波器观察发现电机启动瞬间3.3V电源轨上出现了约1V的跌落而上拉电阻正好接在这个不稳定的3.3V上。解决方案很简单给DS18B20的电源脚加一个10μF电解电容和100nF陶瓷电容去耦同时把上拉电阻改接到独立的3.3V模拟电源轨上。从那以后温度数据再也没有出现过随机跳变。这个案例想说明的是DS18B20虽然是数字传感器但它对电源质量并没有那么多强的免疫力。STM32F103的ADC和数字电路可以容忍一定的电源纹波但DS18B20的单总线通信对电平阈值非常敏感一旦电源波动导致电平判定反转整个时序就乱了。所以如果你的系统里还有其他大功率负载一定要做好电源隔离和去耦。4.5 工程文件组织与移植建议最后说一下工程文件组织。我习惯把DS18B20驱动封装成三个文件ds18b20.h、ds18b20.c、delay.c。ds18b20.h里定义引脚和宏ds18b20.c里实现时序和功能函数delay.c提供微秒延时。这样做的目的是方便移植——换一块板子时只需要改头文件里的引脚宏定义即可其他代码不用动。引脚宏定义建议写成这样#define DS18B20_GPIO_PORT GPIOB #define DS18B20_GPIO_PIN GPIO_Pin_1 #define DS18B20_RCC RCC_APB2Periph_GPIOB #define DS18B20_DQ_LOW() GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN) #define DS18B20_DQ_HIGH() GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN) #define DS18B20_DQ_READ() GPIO_ReadInputDataBit(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)这样换引脚时只改宏定义整个驱动函数体和调用代码完全不用动五分钟就能完成移植。这个习惯在我好几个项目里帮了大忙尤其是画了PCB之后发现引脚冲突需要换引脚的时候只改一个头文件就能解决。我在实际使用这条驱动时还有一个体会DS18B20的驱动看似简单但它是对时序敏感度要求很高的外设之一。读懂它之后再去驱动DHT11、DHT22甚至WS2812这类时序敏感型设备都会有茅塞顿开的感觉。所以说这个组合虽然经典但学习价值是真的高。如果你打算在自己的项目里用这个模块我的建议是先把时序图画在纸上再对着代码一步一步走一遍流程直到能完整说出每个延时的作用这时候无论再遇到什么问题你都有能力自己排查了。本文还有配套的精品资源点击获取
返回列表