STM32F103驱动DS18B20:单总线时序解析与代码实现
发布时间:2026/9/2 17:37:04 作者:尧图编辑部 阅读量:1,286

简介面向STM32F103的DS18B20温度传感器读取工程包完整实现了1-Wire单总线协议在STM32上的移植与串口回显。资源为Keil工程共187个文件包含标准外设库的c/h源码、编译生成的hex/axf、工程配置uvprojx/uvoptx以及map/lst等调试辅助文件压缩包约4.66MB。代码按main、DS18B20_Reset、WriteBit/ReadBit、ReadTemperature、USART_Transmit等模块划分清晰展示复位脉冲、读写时序、温度解析、异常处理等完整流程。同时附有硬件连接说明针对GPIO选择、4.7kΩ上拉电阻、寄生供电与独立供电方案、多传感器64位序列号识别等常见问题给出解释便于开发者在实际项目中应用与排查。已有8271人学习下载适合学习嵌入式温度采集、单总线通信及串口调试的初学者或进阶开发者。1. 项目概述与选型思路1.1 为什么选DS18B20而不是热敏电阻最近在做一个小型温控项目需要一个能直接喂给单片机的温度传感器。市面上常见方案无非就是热敏电阻、NTC配合ADC采样或者直接用DS18B20这类数字传感器。热敏电阻的方案其实也不难一个分压电路加上ADC读取代码量也不大但有个麻烦事热敏电阻本身的B值离散性比较大换一个批次就要重新标定而且整个温度区间内阻值变化是非线性的想要做得准还得查表或者拟合曲线工程上非常烦。DS18B20就不一样了。它直接把温度转换成数字量内部有12位ADC分辨率能做到0.0625摄氏度出厂前已经校准过不需要用户做任何标定工作。数据通过单总线协议传输一根信号线就能搞定通信节省IO资源。而且它支持寄生供电模式两根线地线和数据线就能工作这在一些结构紧凑的板子上非常友好。我这次选用的主控是STM32F103这个片子可以说是国内玩嵌入式几乎绕不开的一颗芯片Cortex-M3内核72MHz主频外设资源丰富价格也便宜。用它来读DS18B20完全是大材小用但也正因为这样整个工程的资源占用非常低后续想往上加协议栈、显示、存储什么的空间都很充裕。1.2 硬件接线与最小系统搭建DS18B20的封装常见有TO-92像三极管那种三个引脚、SO-8、还有防水探头形式的。不管是哪种本质上就三个引脚VDD、DQ、GND。这里要给新手提个醒TO-92封装正面对着你引脚朝下从左到右分别是GND、DQ、VDD千万别把顺序搞反了我见过好几个朋友第一次焊就把电源和地接反直接把传感器烧了。接线方式比较简单核心就一条DQ引脚必须接一个上拉电阻到VDD。DS18B20的单总线协议是开漏/开集电极输出也就是说它本身只能把总线拉低不能主动拉高高电平全靠外部上拉电阻提供。这个上拉电阻的阻值一般取4.7kΩ走线短的话10kΩ也能用。如果省掉这个电阻你会发现初始化老是失败温度读出来要么全是85要么是-127这个问题后面我会专门讲。从能量角度来说DS18B20在工作时需要的电流不大正常供电模式下一根I/O口线通过上拉电阻供电就够了。但如果你是接了一大串传感器或者用了很长的线超过1米建议老老实实给VDD引脚接3.3V或5V电源别用寄生供电防止电压跌落导致读数异常。STM32F103这边也简单我用的是最常见的“最小系统板”STM32F103C8T6外接8MHz晶振BOOT0接地从Flash启动3.3V供电PA1作为DS18B20的DQ引脚。如果你用的是其它的引脚代码里记得同步修改这个后面会说到。2. DS18B20通信协议与时序解析2.1 单总线时序的三个关键阶段DS18B20之所以让很多人头疼不是因为功能复杂而是它对时序的要求比较苛刻。单总线协议的一切操作都建立在对总线上高低电平持续时间的精确控制上。好在STM32F103主频够高用GPIO模拟时序完全可控只要把延时函数写准剩下的就是按部就班。整个通信过程分为三个阶段初始化复位、ROM操作、功能操作。初始化是所有通信的前置条件。主机把总线拉低至少480微秒然后释放DS18B20检测到这个下降沿后会等待15到60微秒再拉低总线60到240微秒作为应答脉冲。主机在读到时序里会先释放总线然后在释放后60到240微秒的窗口内读取总线状态读到低电平就说明传感器在线。这里有个容易忽略的细节主机释放总线后要经过15微秒左右才能开始采样太快了读到的可能是自己的释放电平而不是传感器的应答。ROM操作和功能操作都是由一个个位时隙组成的。写时序中主机拉低总线如果在拉低后15微秒内释放总线表示写1如果持续拉低至少60微秒再释放表示写0。读时序中主机拉低总线1到15微秒后释放然后在15微秒内采样总线电平读到高就是1低就是0。整个过程中位与位之间需要至少1微秒的恢复时间。2.2 ROM命令与功能命令的完整流程DS18B20内部有一个64位的ROM编码出厂时烧录相当于每个传感器的身份证。通信流程分为两步第一步发送ROM命令指定要对哪个传感器操作第二步发送功能命令告诉传感器要干什么。常用的ROM命令有这么几个ROM命令命令字功能搜索ROM0xF0搜索总线上的所有设备获取ROM编码读ROM0x33直接读取唯一设备的ROM编码总线上只有一个设备时用匹配ROM0x55根据64位ROM编码选择指定设备跳过ROM0xCC跳过ROM匹配直接操作所有设备总线上只有一个时最常用功能命令中和温度读取相关的核心是0x44启动温度转换和0xBE读取暂存器。0x44命令发出后传感器开始内部的A/D转换转换时间取决于分辨率设置12位分辨率下最长需要750毫秒。转换完成后温度数据会以16位二进制补码的形式存在暂存器的第0和第1字节里。0xBE命令就是为了把这9个字节的暂存器数据全部读出来我们只需要前两个字节。温度数据和实际温度值的换算很简单把两个字节拼成一个16位数低字节是低8位高字节是高8位如果是负数最高位为1先取反加一得到绝对值再乘以0.0625就是实际温度如果是正数直接乘以0.0625即可。3. 基于STM32F103的代码实现3.1 延时函数的精度处理DS18B20时序里最短的时间单位是微秒级所以延时函数的精度是整个项目成败的关键。我见过很多人直接用空循环加延时如果编译器优化等级改一下或者换一颗主频不同的芯片时序就全乱了。最稳妥的方案是用定时器或者SysTick来做微秒延时。SysTick是Cortex-M3内核自带的24位倒数计数器用它做延时不占用额外的定时器外设而且时间基准来自内核时钟只要时钟配置正确精度就有保障。我习惯先写一个基于SysTick的延时函数很多工程里直接用非常稳定。static uint8_t fac_us 0; void Delay_Init(void) { // 假设系统时钟为72MHzSysTick时钟源选择HCLK/8 9MHz SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); fac_us SystemCoreClock / 8000000; // 得到1微秒需要的计数次数 } void delay_us(uint32_t nus) { uint32_t temp; SysTick-LOAD nus * fac_us - 1; // 重装载值 SysTick-VAL 0; // 清空当前值 SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; // 使能SysTick do { temp SysTick-CTRL; } while((temp SysTick_CTRL_COUNTFLAG_Msk) 0); // 等待计数到0 SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; // 关闭SysTick }这里有个小技巧读取CTRL寄存器的COUNTFLAG标志来判断计数是否完成比读取VAL寄存器判断更可靠。COUNTFLAG在计数器从1计数到0时会被硬件置1读取后自动清零用这个作为结束条件不会漏判。大多数例程都是这么做的实测下来非常稳。如果你不想用SysTick也可以把TIM2、TIM3配置成微秒级定时器原理类似只是占用了一个外设。SysTick是内核资源用它做延时不会占用外设对于后续还要用定时器做PWM、输入捕获的场合更友好。3.2 完整代码与逐行说明硬件和延时都准备好了接下来就是核心的驱动代码。我用的标准外设库StdPeriph很多老工程师还是比较习惯这种风格HAL库的项目代码逻辑也差不多。这里的关键点在于GPIO模式的切换输出时配置为推挽输出输入时配置为上拉输入。DS18B20是开漏协议主机在输出低电平时直接拉低总线输出高电平时应该释放总线。最优雅的硬件接法是GPIO配置为开漏输出这样天然支持双向通信。但在标准外设库下很多朋友对开漏的双向特性不够熟悉所以我这里用更直观的方式读数据前把引脚切换成输入模式写完数据再切回输出模式。#define DS18B20_GPIO_PORT GPIOA #define DS18B20_GPIO_PIN GPIO_Pin_1 #define DS18B20_RCC RCC_APB2Periph_GPIOA static void DS18B20_Pin_Mode_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DS18B20_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DS18B20_GPIO_PORT, GPIO_InitStructure); } static void DS18B20_Pin_Mode_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DS18B20_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(DS18B20_GPIO_PORT, GPIO_InitStructure); } static void DS18B20_DQ_High(void) { DS18B20_Pin_Mode_Output(); GPIO_SetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); } static void DS18B20_DQ_Low(void) { DS18B20_Pin_Mode_Output(); GPIO_ResetBits(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN); } static uint8_t DS18B20_Read_Bit(void) { uint8_t dat; DS18B20_Pin_Mode_Output(); DS18B20_DQ_Low(); delay_us(2); DS18B20_DQ_High(); DS18B20_Pin_Mode_Input(); delay_us(5); dat GPIO_ReadInputDataBit(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN) ? 1 : 0; delay_us(50); return dat; } static void DS18B20_Write_Byte(uint8_t dat) { uint8_t i; for(i 0; i 8; i) { DS18B20_Pin_Mode_Output(); DS18B20_DQ_Low(); delay_us(2); if(dat 0x01) { DS18B20_DQ_High(); } delay_us(60); DS18B20_DQ_High(); dat 1; } } static uint8_t DS18B20_Read_Byte(void) { uint8_t i, dat 0; for(i 0; i 8; i) { dat 1; if(DS18B20_Read_Bit()) { dat | 0x80; } } return dat; } static uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_Pin_Mode_Output(); DS18B20_DQ_Low(); delay_us(500); DS18B20_DQ_High(); DS18B20_Pin_Mode_Input(); delay_us(60); presence GPIO_ReadInputDataBit(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN) ? 0 : 1; delay_us(400); return presence; } uint8_t DS18B20_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(DS18B20_RCC, ENABLE); GPIO_InitStructure.GPIO_Pin DS18B20_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DS18B20_GPIO_PORT, GPIO_InitStructure); return DS18B20_Reset(); } float DS18B20_Get_Temperature(void) { uint8_t temp_l, temp_h; int16_t temp_raw; float temperature; DS18B20_Reset(); DS18B20_Write_Byte(0xCC); // 跳过ROM DS18B20_Write_Byte(0x44); // 启动温度转换 delay_ms(750); // 等待转换完成12位分辨率 DS18B20_Reset(); DS18B20_Write_Byte(0xCC); // 跳过ROM DS18B20_Write_Byte(0xBE); // 读取暂存器 temp_l DS18B20_Read_Byte(); temp_h DS18B20_Read_Byte(); temp_raw (int16_t)((temp_h 8) | temp_l); temperature (float)temp_raw * 0.0625f; return temperature; }delay_ms函数我就不贴了基于delay_us循环调用即可。整个代码的流程非常清晰初始化时发送复位脉冲检测传感器是否在线读取温度时分两步操作先发转换命令并等待转换完成再发读暂存器命令读取两个字节的温度数据。3.3 内部晶振模式下的时序隐患很多人为了省一个晶振直接让STM32F103跑内部RC振荡器HSI8MHz然后PLL倍频到64MHz。这么做不是不行但要注意HSI的精度只有±1%左右温漂也大对串口通信可能影响不大但对DS18B20这种微秒级时序的外设来说误差会被放大。我测试过内部RC振荡器下如果延时函数还按72MHz计算实际延时会偏短导致初始化时复位脉冲不足480微秒就可能出现偶发性的初始化失败。解决办法有两个一是重新校准delay_us的fac_us参数比如64MHz主频下fac_us 8二是在正式产品中尽量用外部晶振或者开启HSI校准功能。3.4 停机模式与掉电保存的扩展思考DS18B20这种低功耗传感器经常用在电池供电的场景里。STM32F103支持停机模式Stop Mode进入停机模式后内核时钟停止但SRAM和寄存器内容保留。如果你在停机前把DS18B20的温度数据存在全局变量里唤醒后可以直接使用不需要重新读取。不过要注意一个坑停机模式唤醒后GPIO的外设时钟重新使能但GPIO的模式配置可能会被复位。所以唤醒后如果继续操作DS18B20最好把DS18B20_Init重新调用一遍重新初始化GPIO和复位传感器否则读出来的数据可能不对。如果是掉电保存温度数据STM32F103 C8T6有内置Flash可以自己写一个简单的Flash读写驱动把一个页擦除后写入温度值下次上电时读取。这个扩展做起来也不难核心就是FLASH_Unlock、FLASH_ErasePage、FLASH_ProgramHalfWord这几个函数。4. 常见问题与排查技巧实录4.1 温度一直显示85或-127这是DS18B20调试中最经典的问题。85度是DS18B20上电复位后暂存器里的默认值如果你读到85说明通信链路是通的但温度转换没有成功完成或者你在转换还没结束时就去读了暂存器。另外就是上电后第一次读取传感器尚未完成转换直接发出读命令也会读到85。-127就比较直白了通常是通信彻底失败。总线没有上拉电阻或者上拉电阻虚焊、引脚接错、传感器损坏都可能导致这个问题。排查思路是先量电压传感器VDD和GND之间是否有3.3V或5VDQ引脚在空闲时是否被上拉到高电平。然后把示波器探头夹在DQ上看主机发送复位脉冲时有没有应答脉冲回来。没有示波器的话用万用表量DQ静态电压也行理论上应该接近VDD。4.2 读数抖动、偶发错误怎么破如果温度读出来大体正确但偶尔跳变几度或者隔几次就出现一次错误值大概率是时序的边缘状态不够好。这时候从几个方向优化第一检查延时函数是否准确。用示波器实测GPIO翻转的周期如果延时50微秒实际变成了40微秒就要调整fac_us。第二检查上拉电阻和线路。我之前用过10k上拉加上20厘米的杜邦线在干扰较强的环境里就容易出错换成4.7k后问题消失。第三不要频繁连续读取。DS18B20每次转换至少需要750毫秒你如果每100毫秒就读一次读到的一定是上一次转换的旧数据这不算错误但会造成数据跳动。建议读取间隔设为1秒以上并在两次读取之间让GPIO休息不要持续拉低总线。还有一个很多人忽略的点读位时序的采样时刻在15微秒内。如果延时函数偏长采样点落在传感器输出窗口之后读到的全变成1温度自然就不对。读位时我的延时顺序是拉低2微秒、拉高、切输入、延时5微秒、采样这个采样点位于释放后的5微秒左右处于安全窗口内。4.3 实测中的避坑建议汇总结合我自己踩过坑和一些学员反馈整理几个高频问题现象可能原因解决办法初始化失败返回0上拉电阻缺失或太大接4.7kΩ上拉到VDD温度始终85℃转换未完成就读取等待750ms以上再读暂存器温度恒定-127℃VDD/GND接反或传感器损坏检查引脚定义更换传感器读数偶尔跳变延时不准/线太长校准延时缩短走线或用屏蔽线换GPIO后不工作只改了IO口没改RCC时钟GPIO和RCC要一起改如果用的是STM32F103的PA引脚还需要注意一个问题PA口在芯片上电复位后的默认状态是浮空输入如果DS18B20在单片机启动之前就已经上电总线状态可能会有一小段不稳定的时间。稳妥的做法是在DS18B20_Init函数里先把引脚配置为推挽输出并拉高再执行复位时序。最后再分享一个我的个人习惯在调试阶段我会在DS18B20_Get_Temperature函数里留一个调试串口输出把初始化返回值、两个温度字节原始值、最终转换结果都打印出来。这样即使温度数据不对也能快速定位是通信问题还是换算问题省去反复猜的时间。等一切正常了再把调试输出关掉或者改成条件编译。这个项目做到后面你会发现DS18B20的难度不在于代码量而在于耐心和细节。时序这东西只要有一两个微秒的偏差表现出来就是各种莫名其妙的症状。把延时时序吃透把硬件接法搞规范一次成功的概率会大很多。本文还有配套的精品资源点击获取