2026/8/31 23:24:07

STM32F407驱动DS18B20:单总线时序与完整实现指南

STM32F407驱动DS18B20:单总线时序与完整实现指南 简介本资源是一份面向嵌入式初学者与STM32进阶开发者的DS18B20单总线温度采集实战代码包聚焦STM32F407微控制器与DS18B20数字温度传感器的底层驱动实现解决单线协议时序控制、温度读取与数据解析等典型难点。压缩包为RAR格式共含2个核心文件1个C源文件实现One-Wire底层时序、初始化、ROM搜索、温度转换启动与12位数据读取和1个H头文件封装函数接口、宏定义及寄存器配置总计仅4KB轻量精炼便于集成与学习。已有624人下载学习适用于课程设计、毕设温控模块开发或HAL库外的寄存器级编程训练。读者可直接复用该驱动框架快速构建串口打印、LCD显示或数据上传等上层应用代码注释清晰关键延时点明确标注附带典型错误处理逻辑有助于深入理解单总线通信的电平跳变检测、时间窗口约束及多器件地址管理机制。 接手这个项目的时候我手头正好有一批STM32F407开发板和几颗DS18B20温度传感器。DS18B20这颗芯片在嵌入式圈子里几乎是入门必玩的外设单总线协议、免外围器件、直接输出数字温度用起来确实方便。但把它接到F407上跑起来尤其是把时序调稳、数据读准还是有不少门道。这篇博客就把我这次完整的实现过程、时序分析和踩过的坑都整理出来给正准备在F407上驱动DS18B20的朋友做个参考。1. 选型分析与硬件准备1.1 为什么在F407上驱动DS18B20STM32F407这颗芯片主频168MHz带FPU一般大家拿它跑摄像头、跑网络、跑音频处理很少专门为了一颗温度传感器去用这么高规格的MCU。但实际项目中F407往往已经作为主控在跑业务逻辑顺带挂一路DS18B20采集环境温度这个场景非常常见。比如我这次是在一个温控采集节点上做改造主控是F407原本用NTC模拟量采集精度和一致性都一般换成DS18B20之后直接省掉了ADC校准和线性化处理。另外还有一点DS18B20的时序对延时精度比较敏感F407主频高跑GPIO翻转和空循环延时的分辨率更高比低主频的8位机更容易把微秒级时序做准。当然反过来也有坑主频高了之后循环延时函数的修正和编译优化问题会更突出这个后面详细说。1.2 硬件连接与上拉电阻的讲究DS18B20就三根脚VCC、GND、DQ。如果采用外部供电方式VCC接3.3V或5V都可以DQ数据线接MCU的一个GPIO同时必须接一个4.7kΩ的上拉电阻到VCC。为什么必须要上拉电阻因为DS18B20的数据脚是开漏输出结构也就是说它只能主动拉低电平不能主动输出高电平。高电平必须靠外部上拉电阻来实现。这个和I2C协议类似都是开漏加外部上拉的通信方式。如果不加上拉电阻读到的数据永远是0初始化也会一直失败。上拉电阻的取值也有讲究。4.7kΩ是最常用的值但实际使用中1kΩ到10kΩ都能工作。阻值越小总线上升沿越陡抗干扰能力越强但功耗也越大阻值越大上升沿越缓长线传输时波形容易失真。我实测下来3.3V供电加4.7kΩ上拉线长不超过30cm的情况下波形非常干净。如果线材较长或者现场干扰大可以换1kΩ试试。还有一个容易被忽略的点DS18B20的VCC和GND之间最好加一个0.1μF的退耦电容靠近传感器放置。虽然DS18B20功耗很低但在温度转换的瞬间电流会有一个小的跳变尤其是寄生供电模式下这个电容能起到稳定供电的作用减少转换结果跳变的问题。1.3 供电方式选择DS18B20支持两种供电方式外部供电和寄生供电。外部供电最简单VCC接电源DQ只负责数据通信。这种模式稳定可靠也是我最推荐的方式。寄生供电则是VCC和GND都接地DQ线在特定时序窗口内给芯片供电。寄生供电的好处是只需要两根线但问题也不少温度转换期间特别是12位分辨率下转换时间最长750ms需要总线强上拉供电否则转换结果会出错。这个强上拉在MCU直接驱动的情况下实现起来比较麻烦搞不好就烧IO口或者拉不上去。所以除非是那种线缆资源非常紧张的场景否则我建议直接用外部供电省心不是一点点。F407的IO口是3.3V电平DS18B20在2.5V到5.5V都能工作所以3.3V供电完全没问题。2. DS18B20单总线时序深度拆解2.1 单总线协议的基本逻辑DS18B20使用的是单总线1-Wire协议这个协议的特点就是所有通信都在一根数据线上完成包括供电寄生模式下、时钟同步和数据传输。因为只有一根线所以通信双方必须有严格的时序约定谁在什么时间段拉低总线、什么时候释放总线、什么时候采样数据都有明确的时间窗口。单总线协议里的基本操作单元就三个复位脉冲、写时隙、读时隙。所有复杂的命令和数据传输都是由这三个基本操作组合而成的。理解了这个基础后面写代码就是按时间参数去拼装这些基本操作。单总线通信的发起方永远是主机这里是STM32F407DS18B20作为从机只能被动响应。主机通过拉低总线的长短来区分不同类型的操作复位脉冲是拉低480μs以上写时隙是拉低1~15μs表示写0或写1读时隙是主机拉低1μs后释放总线然后在15μs内采样电平。2.2 初始化时序复位与存在脉冲每次和DS18B20通信之前主机都必须先发起一个复位脉冲让总线上的所有从设备复位并准备接收命令。复位时序分两个阶段第一阶段主机把总线拉低至少480μs然后释放总线。这个480μs的必要性在于DS18B20内部的上电复位逻辑需要足够长的低电平时间来识别这是一个复位信号而不是一个数据时隙。官方数据手册给的范围是480μs到960μs之间我一般取550μs左右留一些余量。第二阶段主机释放总线后总线在上拉电阻作用下恢复高电平。DS18B20检测到总线释放会等待15~60μs然后拉低总线60~240μs这就是存在脉冲。主机在这个窗口内读取总线电平读到低电平说明总线上有DS18B20应答了如果一直是高电平说明设备不存在或者接线有问题。这里有个重要的时间窗口必须注意主机释放总线后15μs之内不能采样因为DS18B20还在等待和准备阶段总线状态尚不稳定。等15~60μs期间采样才是有效的存在脉冲窗口。代码里我习惯在释放总线后延时20μs再读总线实测这个时间点很稳。2.3 写时序与读时序的细节写时序是主机向DS18B20发送一个bit的操作。每个写时隙最短60μs最长120μs时隙之间需要至少1μs的恢复时间。关键是低电平持续时间的长短来区分写0还是写1。写0时隙主机拉低总线保持60μs然后释放。整个时隙至少60μs保持低电平的时间就是整个时隙。写1时隙主机拉低总线但只保持1~15μs然后释放总线让上拉电阻把总线拉高保持到整个时隙结束。听起来简单但实际写代码的时候有个细微之处主机拉低的时间要在1~15μs之间太短了DS18B20采样不到起始信号太长了就会被误判为写0。F407主频168MHz一个空循环的延时很好控制但我建议写时序的时候用__NOP()或者DWT精确延时不要单纯依赖编译器的空循环优化因为不同优化级别下空循环的执行周期可能会变。读时序和写时序类似也是主机发起一个至少60μs的时隙。区别在于主机先拉低总线1~15μs一般取2~3μs然后释放总线之后DS18B20会控制总线如果它要发送0就会继续拉低总线如果要发送1就释放总线让上拉电阻拉高。主机需要在释放总线后15μs之内完成采样超过这个时间窗口就可能读到无效电平。采样时机我实测下来释放总线后10~12μs读取最稳。太早读DS18B20可能还没完全控制总线电平还在跳变太晚读如果DS18B20发的是1上拉电阻已经把总线拉高了也区分不出来。2.4 完整的操作流程ROM命令与功能命令DS18B20的命令分两层ROM命令和功能命令。每次操作都必须先发ROM命令再发功能命令。ROM命令用来寻址总线上特定的DS18B20。总线上只有一个设备时直接发0xCC跳过ROM即可不需要读取64位序列号。总线上有多个DS18B20时就得先执行0x33读ROM或者0xF0搜索ROM来获取每个设备的序列号然后用0x55匹配ROM来选中特定设备。温度采集的完整流程是复位发送0xCC跳过ROM发送0x44启动温度转换等待转换完成12位分辨率下最长750ms也可以读取总线电平判断转换是否完成转换期间总线被拉低复位发送0xCC跳过ROM发送0xBE读取暂存器连续读取9个字节温度低字节、温度高字节、报警阈值、配置寄存器、CRC读到的温度数据是16位有符号数高5位是符号扩展位。正温度时高5位全为0负温度时高5位全为1。有效数据是低11位12位分辨率下每1个LSB代表0.0625°C。实际使用中直接取低11位乘以0.0625就是实际温度值。3. STM32F407驱动代码实现3.1 GPIO初始化与延时函数F407驱动DS18B20的核心就是一个GPIO在两个模式之间切换输出模式主机拉低总线或发送数据和输入模式读取总线电平。我用的是PG9引脚来做演示实际项目中可以根据板子布局换任意GPIO。GPIO初始化比普通的推挽输出多了一步我们需要把引脚配置为开漏输出模式。开漏输出模式下寄存器写0时引脚拉低写1时引脚为高阻态正好配合外部上拉电阻实现总线的高电平。这样还有个好处——GPIO模式切换的时候不需要担心瞬间推挽冲突。void DS18B20_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOG_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_9; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull GPIO_NOPULL; // 外部已有上拉电阻 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOG, GPIO_InitStruct); }延时函数这里要特别注意。F407跑168MHz如果直接用HAL_Delay最小单位是1ms完全无法满足微秒级时序需求。我推荐用DWTData Watchpoint and Trace模块做微秒延时精度高且不受编译器优化影响。void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; DWT-CYCCNT 0; } void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }DWT延时的原理是利用CPU周期计数器计数168MHz下1μs就是168个周期。因为是32位计数器最大计数约4.29×10^9在168MHz下约25.6秒溢出一次做微秒级延时完全不用担心溢出问题。3.2 时序操作函数实现有了精确延时函数接下来就是实现三个核心时序函数复位、写一个bit、读一个bit。// 复位并检测存在脉冲返回1表示检测到DS18B20 uint8_t DS18B20_Reset(void) { uint8_t presence 0; DS18B20_DQ_LOW(); // 拉低总线 DWT_Delay_us(550); // 拉低550us DS18B20_DQ_HIGH(); // 释放总线 DWT_Delay_us(20); // 等20us避开无效窗口 presence DS18B20_DQ_READ(); // 读存在脉冲 DWT_Delay_us(400); // 等待时隙结束 return presence 0 ? 1 : 0; }这里要特别注意GPIO在输出模式下写1时因为是开漏模式引脚实际上处于高阻态总线被外部上拉拉高。读取存在脉冲前需要把GPIO切换为输入模式否则读到的永远是输出数据寄存器的值。void DS18B20_WriteBit(uint8_t bit) { if (bit) { DS18B20_DQ_LOW(); DWT_Delay_us(5); // 写1时隙拉低5us DS18B20_DQ_HIGH(); // 释放总线 DWT_Delay_us(60); // 确保时隙总长度至少60us } else { DS18B20_DQ_LOW(); DWT_Delay_us(60); // 写0时隙整个时隙保持低电平 DS18B20_DQ_HIGH(); DWT_Delay_us(5); // 恢复时间 } }读时序稍微复杂一点需要切换GPIO模式uint8_t DS18B20_ReadBit(void) { uint8_t bit 0; DS18B20_DQ_LOW(); DWT_Delay_us(2); // 拉低2us DS18B20_DQ_HIGH(); // 释放总线 DWT_Delay_us(10); // 等10us后采样 // 切换为输入模式读取 DS18B20_GPIO_INPUT_MODE(); bit DS18B20_DQ_READ(); DS18B20_GPIO_OUTPUT_MODE(); DWT_Delay_us(50); // 等待时隙结束 return bit; }GPIO模式切换可以用一个宏来实现实际上就是修改GPIO的MODER寄存器#define DS18B20_GPIO_INPUT_MODE() do { \ GPIOG-MODER ~(3UL (9 * 2)); \ GPIOG-MODER | (0UL (9 * 2)); \ } while(0) #define DS18B20_GPIO_OUTPUT_MODE() do { \ GPIOG-MODER ~(3UL (9 * 2)); \ GPIOG-MODER | (1UL (9 * 2)); \ } while(0)3.3 温度采集与数据校验有了bit级别的操作字节级别的读写就是循环8次注意先发最低位void DS18B20_WriteByte(uint8_t data) { for (int i 0; i 8; i) { DS18B20_WriteBit(data 0x01); data 1; } } uint8_t DS18B20_ReadByte(void) { uint8_t data 0; for (int i 0; i 8; i) { data 1; if (DS18B20_ReadBit()) { data | 0x80; } } return data; }接下来是完整的温度采集函数。这里有个容易踩的坑启动温度转换后等待时间一定要给够。12位分辨率下官方标称最大转换时间750ms但我实测有些批次的芯片需要800ms左右才稳定。如果读出来温度跳变尤其是数值在85°C和真实温度之间跳动大概率就是转换没完成就读取了。float DS18B20_ReadTemperature(void) { uint8_t tempL 0, tempH 0; int16_t rawTemp 0; float temperature 0.0f; if (!DS18B20_Reset()) { return -999.0f; // 设备不存在返回错误值 } DS18B20_WriteByte(0xCC); // 跳过ROM DS18B20_WriteByte(0x44); // 启动温度转换 // 等待转换完成可以轮询总线电平 // 转换期间DS18B20会拉低总线转换完成后释放 while (DS18B20_ReadBit() 0); if (!DS18B20_Reset()) { return -999.0f; } DS18B20_WriteByte(0xCC); // 跳过ROM DS18B20_WriteByte(0xBE); // 读取暂存器 tempL DS18B20_ReadByte(); tempH DS18B20_ReadByte(); // 跳过剩余7个字节报警阈值、配置、CRC for (int i 0; i 7; i) { DS18B20_ReadByte(); } rawTemp (int16_t)((tempH 8) | tempL); // 判断正负温度 if (rawTemp 0x8000) { rawTemp ~rawTemp 1; // 取补码 temperature -(float)rawTemp * 0.0625f; } else { temperature (float)rawTemp * 0.0625f; } return temperature; }这里用轮询DS18B20总线电平来替代固定延时等待效率更高。原理是DS18B20启动温度转换后会把总线拉低直到转换完成才释放。主机读到高电平就说明转换结束了。这个方法可以节省几百毫秒的等待时间对于需要频繁采样的场景很有用。3.4 主程序组织与多设备扩展主程序里调用就很简单了串口打印出来方便观察int main(void) { HAL_Init(); SystemClock_Config(); // 配置168MHz主频 UART_Init(); DWT_Delay_Init(); DS18B20_GPIO_Init(); float temperature 0.0f; while (1) { temperature DS18B20_ReadTemperature(); if (temperature -100.0f) { printf(Temperature: %.2f C\r\n, temperature); } else { printf(DS18B20 not found!\r\n); } HAL_Delay(1000); } }如果总线上要挂多个DS18B20就不能再用跳过ROM命令了得先把每个设备的64位序列号读出来存储然后通过匹配ROM命令按地址读取。多设备共用一个GPIO的好处是节省IO资源但代价是代码复杂度上了一个台阶涉及ROM搜索算法。F407的RAM和Flash资源完全够用不用担心空间问题。4. 实测效果与常见问题排查4.1 典型故障现象与解决方案速查表我在这次调试过程中以及给朋友远程排查问题时遇到了一些典型的故障现象整理成表格供大家快速定位故障现象可能原因解决办法初始化永远失败读不到存在脉冲上拉电阻没接或阻值过大检查DQ引脚是否接4.7kΩ上拉到VCC初始化永远失败读不到存在脉冲GPIO配置成了推挽输出改为开漏输出模式初始化永远失败读不到存在脉冲DQ引脚接错接到ADC引脚等核对原理图确认GPIO编号温度读出来都是85°C上电时总线状态不稳定每次读取前先复位复位后延时5ms再操作温度读出来都是85°C供电不足或接触不良检查VCC电压加0.1μF退耦电容温度值跳变不稳定转换未完成就读取用轮询总线电平代替固定延时确保转换完成温度值跳变不稳定延时函数不准用DWT延时替代空循环延时关闭编译器优化温度始终为0°C或-0.0625°C读时序采样点太早释放总线后延时10~12μs再采样温度始终为0°C或-0.0625°C数据线太长波形畸变缩短线长或减小上拉电阻到2.2kΩ负温度读不出来符号位处理错误检查16位补码转换逻辑4.2 用示波器看时序的排查经验调试DS18B20这种单总线协议最好的工具就是示波器。没有示波器的话逻辑分析仪也可以采样率至少要2MHz以上否则测不准微秒级的时序。我这次排查一个奇怪的问题时用示波器立了大功。现象是程序跑起来偶尔能读到正确温度但大部分时间是85°C。我第一反应是延时不准但DWT延时已经够精确了。最后用示波器看复位时序的波形发现复位低电平时间只有400多μs比设定的550μs少了不少。原因是我在拉低总线之前调用了几个函数函数的调用开销和GPIO寄存器写操作本身需要时间导致实际低电平时间比理论值短。这个问题反映了一个常见误区用DWT延时函数延时550μs但在调用DWT_Delay_us前后还有其他代码在执行这些代码的执行时间会侵占延时时间。解决办法是延时参数稍微加大一些比如设置600μs确保实际低电平时间在480μs以上。用示波器看时序还有一个好处就是能直观地看到写时序和读时序的波形是否符合手册要求。比如写1时隙拉低时间应该在1~15μs之间如果拉低时间过长从波形上看低电平部分会很明显地宽于写0时隙的1/4左右这时就能快速判断出时序参数有问题。4.3 工程化改造建议驱动调通只是第一步真正用到项目里还需要做几个改造。第一个是中断保护。DS18B20的时序对中断非常敏感如果在写时序或读时序中间来了一个中断中断服务函数里的代码执行时间可能会导致时序超时。比如串口中断或者定时器中断如果有数据要处理可能耽搁几十微秒这足以让DS18B20的通信失败。解决方案是在操作时序之前关闭可屏蔽中断操作完成后恢复。uint32_t primask __get_PRIMASK(); __disable_irq(); // 执行DS18B20时序操作 temperature DS18B20_ReadTemperature(); __set_PRIMASK(primask);但要注意关闭中断的时间不能太长否则会影响其他实时任务。DS18B20读取整个温度数据的时间大约在5ms左右如果系统里有严格的实时要求这个时间可能有点长。可以只在发送命令和读取暂存器时关中断等待转换完成的750ms不关中断这样中断关闭时间能缩短到1ms以内。第二个是数据滤波。DS18B20虽然数字输出但在工业现场环境中长线传输可能会引入偶发的通信错误导致读回来的温度值跳变。我一般会在软件层加一个简单的滑动平均滤波取最近5次采样的平均值能有效抑制偶发跳变。第三个是数据上传。F407的资源丰富串口、USB、以太网都经常用到。我这次就是把采集到的温度通过串口打印然后接了一个USB虚拟串口直接把温度数据上传到上位机。如果项目里已经接了LAN8720也可以把温度数据打包成UDP报文发送到服务器实现远程温度监控。DS18B20采集温度只是整个系统的一个小环节但它能融入F407的生态通过USB、以太网等方式把数据传出去这个扩展空间很大。5. 操作踩坑记录与心得总结5.1 编译器优化导致的时序漂移我在调试过程中踩过最大的一个坑是编译器优化导致的时序漂移。第一次用空循环做延时函数时我写了个简单的for(uint16_t i 0; i n; i);来延时调试模式下一切正常温度采集稳定。但把优化级别从-O0改成-O2之后温度采集立刻开始间歇性失败。这是因为-O2优化会识别出空循环是一个无操作循环直接把它优化掉了导致延时时间几乎为零。解决办法就是用DWT硬件延时因为DWT周期计数的读取是有效操作编译器无法优化掉。如果你的项目不想引入DWT也可以用volatile修饰循环变量但效果不如DWT稳定。5.2 总线上挂多个设备的地址管理如果项目里需要在一条总线上挂多个DS18B20建议在系统初始化时就把所有设备的序列号扫描出来存储在一个数组中然后在运行过程中按索引读取。不要每次采集温度时都重新搜索ROM因为搜索算法涉及二叉树遍历代码复杂度高且耗时较长。多设备场景下还有个常见问题某个设备偶尔丢失响应。排查方法是先用示波器看总线上是否存在毛刺或者波形畸变另外可以在每个DS18B20的VCC和GND之间都加一个退耦电容越靠近传感器越好。如果多个DS18B20的温度读数都一样且等于最后一个设备的读数多半是匹配ROM命令没有正确执行读到的永远是同一个设备。检查一下命令发送的顺序确保匹配ROM0x55之后正确发送了64位序列号先发最低字节。5.3 长时间运行稳定性验证最后补充一个长期运行的经验。DS18B20的时序通信偶尔有一两次失败是正常的关键是软件要有容错机制。我一般会在主循环里做错误计数连续3次读取失败才判定设备故障否则忽略单次错误继续重试。这样既保证了系统的稳定性又不会被偶发错误打扰。项目测试阶段我让这套系统连续运行了72小时每秒钟采集一次温度总共25万多次采样出现通信错误不到10次温度数据没有明显跳变。这说明在F407上驱动DS18B20只要时序参数调整好、中断保护做到位稳定性是完全有保障的。最后再分享一个小技巧DS18B20的配置寄存器可以设置转换分辨率默认是12位750ms转换时间如果对实时性要求高可以改成9位93.75ms转换时间温度精度从0.0625°C变成0.5°C。在大多数环境监测场景下0.5°C的精度完全够用但采集频率可以从1Hz提升到10Hz。这个权衡取舍看具体项目需求来定。本文还有配套的精品资源点击获取