STM32F407驱动DS18B20温度传感器:单总线协议时序与HAL库实战解析
2026/9/7 9:53:55 网站建设 项目流程

简介:面向STM32开发者的F407与DS18B20温度采集显示工程包,完整实现单总线通信、温度读取及TFT屏幕GUI显示,适合嵌入式初学者及物联网温度监测场景。包内共248个文件,约10.96MB,涵盖55个H头文件、44个C源文件,以及HAL库、DS18B20驱动、TFT图形库,另有工程配置、可执行文件、map映射文件等编译产物,目录按Library、Project、User分类,便于定位。已有1005人学习下载。通过阅读和编译代码,可深入理解DS18B20单总线时序、STM32F407 GPIO开漏配置,以及如何在TFT上绘制温度仪表;从库到源码的组织方式,也为完整嵌入式项目开发提供实践模板。 很多玩STM32的朋友,手里大概率都有一块F407的板子。这芯片性能没得说,168MHz主频,带FPU,各种外设齐全,跑个电机控制、界面交互、音频处理都绰绰有余。但要说拿它去读一个DS18B20温度传感器,很多人第一反应是“杀鸡用牛刀”。可恰恰是这种组合,在实际项目里出现频率极高,尤其是做温控系统、环境监测、设备自检的时候,F407强大的处理能力配合DS18B20极简的硬件接口,往往能省掉一整套复杂电路。

这个项目看着简单,其实里面藏着不少门道。DS18B20用的是单总线协议,时序要求微妙,而F407主频高、GPIO翻转速度快,稍不注意时序就容易“跑飞”。我见过太多人直接把网上51单片机的代码搬过来,结果在F407上死活读不到温度,最后卡在排查时序问题上。这篇文章我就把这套组合从硬件连接到底层时序再到代码实现全部拆开揉碎,讲讲那些文档里不会明说、但实际调试中一定会碰到的坑。

1. 整体设计与方案选型思路

1.1 为什么用F407驱动DS18B20不算浪费

这里得先说说F407和DS18B20各自的定位。DS18B20是个数字温度传感器,测温范围-55℃到+125℃,精度正负0.5℃,分辨率可配置,最高12位。它最讨喜的地方是只需要一根数据线就能通信,还能多点组网挂在同一条总线上。而STM32F407ZGT6是意法半导体的高性能MCU,内置1MB Flash、192KB RAM,各种通信接口齐全,主频最高能到168MHz。

把它俩放一起,最实在的理由有三个。第一,F407的GPIO翻转速度极快,达到几十MHz级别,这意味着在软件模拟单总线时序时,延时的精度反而好控制,只要代码优化得当,时序稳定性远超低速单片机。第二,F407的5V容忍引脚可以直接接DS18B20的数据线,不需要电平转换芯片,硬件设计极其简单。第三,实际产品里F407通常还要干很多别的事,比如驱动LCD屏幕、处理按键、跑Modbus协议栈,顺便挂个DS18B20采集温度,这属于“顺手牵羊”的性价比选择。

1.2 单总线协议核心机制

DS18B20用的单总线协议,说到底是根半双工的双向数据线,一个主机可以挂载多个从机。这套协议有几个关键机制必须理解到位。

数据线默认状态是高电平,所有通信都由主机发起。主机通过拉低总线来产生不同的时隙信号,从机通过拉低总线来回复“存在脉冲”或者输出数据位。单总线上没有时钟线,所以每个比特的宽度和采样点就是通信的生命线。

DS18B20内部有一个64位激光ROM编码,前8位是家族码(0x28),中间48位是唯一序列号,最后8位是CRC校验。这套编码的意义在于,多点组网时主机可以通过ROM命令精确寻址每一个从机。单点使用时,我们可以直接发跳过ROM命令,不用关心序列号。

通信流程通常分四步:初始化(复位脉冲和存在脉冲)、ROM命令(跳过或匹配地址)、功能命令(启动温度转换或读暂存器)、数据传输。温度转换命令发出后,12位分辨率下最长需要750ms转换时间,这个在写代码时必须有心理预期,不能发了转换命令立刻去读数据,读回来大概率是上次转换的旧值。

2. 硬件电路设计与连接要点

2.1 引脚选择与上拉电阻计算

DS18B20的硬件连接可以用“极简”来形容,VCC接电源(3.3V到5V都行),GND接地,DQ数据线接一个GPIO,然后在DQ和VCC之间接一个上拉电阻。这个上拉电阻非常关键,因为单总线协议要求空闲时为高电平,而DS18B20的输出结构是开漏的,只能拉低,不能主动拉高。

上拉电阻的阻值选取有讲究。常见取值是4.7kΩ,但我实际测试下来,在3.3V供电且线长较短(小于50cm)的情况下,4.7kΩ完全没问题。如果线比较长或者环境干扰大,可以换成2.2kΩ甚至1kΩ,拉电流能力更强,抗干扰更好。但也要注意,阻值太小会增大静态功耗,对低功耗设计不友好。

电阻的计算逻辑其实很简单。DS18B20在3.3V下输出低电平的最大压降是0.4V,灌电流能力IOL最大4mA,所以R = (VCC - VOL) / IOL = (3.3 - 0.4) / 0.004 ≈ 725Ω,这是理论最小值。实际留足余量,取4.7kΩ远小于这个约束,拉高能力绰绰有余。

2.2 硬件连接中的几个细节

GPIO要选带FT(5V容忍)标识的引脚。F407很多引脚都支持5V容忍,数据手册的引脚定义表里带FT标记的都可以直接用。我习惯用PA0、PB12这种随便一个普通IO,没必要非要去挑特殊功能引脚。

还有个很多人踩过的坑:DS18B20的GND一定不能省,而且如果走线长,VCC和GND最好用双绞线或者靠近布线,否则容易引入共模干扰,导致温度读数跳变。

如果你用的是寄生供电方式(只接两根线,数据线兼做电源),那数据线上的上拉电阻必须很小,还要在温度转换期间强制拉高总线给传感器充电。这种玩法省一根线,但时序要求更苛刻,新手不建议折腾,老老实实三根线接法最稳。

3. DS18B20时序解析与F407适配要点

3.1 初始化时序:复位与存在脉冲

单总线通信的第一步永远是复位。主机将总线拉低,保持480到960微秒,然后释放总线。释放后,上拉电阻会把总线拉回高电平。DS18B20检测到总线上升沿后,等待15到60微秒,主动将总线拉低60到240微秒,这就是存在脉冲。

在F407上写这段代码,最关键的是延时精度。HAL库的HAL_Delay只能做到毫秒级,480微秒用HAL_Delay完全做不到。必须用微秒级延时函数,这个在后面代码分析部分专门讲。

代码层面,复位函数首先要确认总线被拉低足够久,释放后能读到低电平(从机应答),如果读不到低电平,说明总线上没设备,或者接线有问题。注意,有些从机上电慢,第一次复位会失败,最好重试几次。

3.2 写时隙的时序细节

写时序是单总线里最容易出错的环节。一个写时隙最少60微秒,最长120微秒,两个写时隙之间至少要有1微秒的恢复时间。

写0时隙:主机拉低总线,保持60到120微秒,然后释放。 写1时隙:主机拉低总线,但只需保持1到15微秒,然后释放,让上拉电阻把总线拉高,保持40微秒以上。

这里的关键在于,DS18B20在每个写时隙开始后的15到60微秒窗口内采样总线状态。也就是说,写0要保持低电平足够久(超过采样窗口),写1要尽快释放(在采样窗口到来之前拉高)。

在168MHz主频下,几条指令的耗时都是纳秒级,所以时隙内的延时操作要精确。很多人写1时觉得1到15微秒很宽裕,随手delay一下,结果整个时隙超长,或者释放太早,通信不稳定。实操上,我一般把写0的拉低时间控制在60微秒整,写1的拉低时间控制在2到5微秒,释放后补充到整个时隙至少70微秒。

3.3 读时隙与采样时机

读时序更微妙。主机拉低总线至少1微秒,然后释放总线,在释放后的15微秒内采样数据线电平。DS18B20看到主机拉低后,会立刻决定是否拉低总线来表示数据“0”,如果表示“1”则不做任何动作,让上拉电阻维持高电平。

F407因为主频高,在拉低总线、释放后,代码执行到读取引脚电平之间,可能只过了几百纳秒,这时候DS18B20还没来得及驱动总线。所以读时序里必须有一个延时,从释放总线开始计算,大约9到12微秒时读取电平最稳定。这个采样点太早会误读,太晚可能进入下一个时隙。

我测试过的经验值:释放后延时10微秒再读,实测成功率非常高。注意每次读时隙之间也要留足恢复时间,至少保证整个读时隙在60微秒以上。

3.4 温度数据格式与计算

DS18B20的测温结果存于暂存器的第0和第1字节(LSB和MSB)。12位分辨率下,温度寄存器的格式如下:bit11是符号位S,bit10到bit4是整数部分(2的幂次,从64℃到0.5℃),bit3到bit0是小数部分(0.0625℃的倍数)。

举个例子,读取到的值为0x0191,二进制是0000 0001 1001 0001。符号位为0,说明是正温度。去掉符号位,数值为401,乘以0.0625,得到25.0625℃。如果是负温度,比如读到0xFC90,先取反加一得到0x0370,也就是880,乘以0.0625得55℃,取负号就是-55℃。

实际写代码时,我的做法是判断原始值的bit11。如果为1,就用0xFFFF - rawValue + 1算出补码绝对值,再取负。如果为0,直接乘系数转换。这个逻辑虽然简单,但漏掉符号判断的错误在论坛上屡见不鲜,读出来的负温度变成一个大正数。

4. 基于HAL库的工程实现与代码分析

4.1 CubeMX基础配置

用STM32CubeMX初始化工程很快,GPIO配置也不需要特别复杂。把接DQ的引脚设为推挽输出模式,初始电平设高,速度设高(GPIO_SPEED_FREQ_HIGH),其他默认就行。不需要开上拉下拉,因为外部已经有了4.7kΩ上拉电阻,内部上拉反而会改变时序特性。

有一点要注意:在程序运行中,同一个引脚要来回切换输入和输出模式。这没法在CubeMX里一次性配好,必须在代码里动态切换。HAL库提供了HAL_GPIO_Init函数,但每次调用都重新配置寄存器开销太大。更高效的做法是直接操作寄存器,把引脚的方向寄存器位切换一下即可。

我这里分享一个常用的宏定义:

#define DS18B20_GPIO_PORT GPIOA #define DS18B20_GPIO_PIN GPIO_PIN_0 #define DS18B20_OUT_MODE() do{ GPIOA->MODER &= ~(3UL << (0 * 2)); GPIOA->MODER |= (1UL << (0 * 2)); }while(0) #define DS18B20_IN_MODE() do{ GPIOA->MODER &= ~(3UL << (0 * 2)); }while(0) #define DS18B20_HIGH() HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_SET) #define DS18B20_LOW() HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_RESET) #define DS18B20_READ() HAL_GPIO_ReadPin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)

这里MODER寄存器的最后两位对应PA0的模式:00是输入,01是输出,10是复用功能。一次清位再置位的写法,保证切模式时不干扰其他引脚。

4.2 微秒级延时的三种实现方案

前面反复强调微秒延时的重要性,这里给出三个方案。

方案一:使用DWT(Data Watchpoint and Trace)单元。Cortex-M4内核自带DWT,其中的CYCCNT计数器每个时钟周期加一,精度极高。使能方法很简单:

static void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); }

方案二:利用定时器做延时。比如用TIM2的16位向上计数实现1MHz时基,也能达到微秒级精度,但占用一个定时器资源。

方案三:空循环校准法。简单粗暴,编译后用示波器或逻辑分析仪慢慢调循环次数。不推荐,因为优化等级一变、主频一变,延时就全乱套了。

我做项目首选DWT方案,零资源占用,精度又高,而且不受中断影响(只要不关DWT时钟)。

4.3 完整读取流程示例

下面给出一份我调试通过的完整读取流程,可以直接抄作业。这里用代码块展示,附详细注释。

uint8_t DS18B20_Reset(void) { uint8_t presence = 0; DS18B20_OUT_MODE(); DS18B20_LOW(); delay_us(600); // 拉低至少480us,600us留足余量 DS18B20_HIGH(); DS18B20_IN_MODE(); // 切换为输入,读取存在脉冲 delay_us(70); // 等待从机拉低总线,典型为15~60us后出现 presence = DS18B20_READ(); // 读到0表示存在 delay_us(410); // 剩余时隙填满,保证复位周期至少960us return presence; } void DS18B20_WriteBit(uint8_t bit) { DS18B20_OUT_MODE(); DS18B20_LOW(); delay_us(2); // 写时隙起始拉低,2us足够 if (bit) { DS18B20_HIGH(); // 写1:释放总线 delay_us(60); } else { delay_us(60); // 写0:保持拉低 DS18B20_HIGH(); } } uint8_t DS18B20_ReadBit(void) { uint8_t data = 0; DS18B20_OUT_MODE(); DS18B20_LOW(); delay_us(2); // 读时隙起始拉低 DS18B20_HIGH(); DS18B20_IN_MODE(); // 转输入 delay_us(10); // 等约10us,从机驱动总线稳定后采样 data = DS18B20_READ(); delay_us(50); // 填满整个读时隙 return data; } void DS18B20_WriteByte(uint8_t byte) { for (int i = 0; i < 8; i++) { DS18B20_WriteBit(byte & 0x01); byte >>= 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; } float DS18B20_GetTemperature(void) { uint8_t lsb = 0, msb = 0; int16_t raw = 0; float temperature = 0.0f; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM DS18B20_WriteByte(0x44); // 启动温度转换 HAL_Delay(750); // 12位分辨率转换时间最长750ms DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器,从第0字节开始 lsb = DS18B20_ReadByte(); msb = DS18B20_ReadByte(); raw = (msb << 8) | lsb; if (raw & 0x8000) { // 负温度:补码转原码,取绝对值后加负号 raw = ~raw + 1; temperature = raw * 0.0625f; temperature = -temperature; } else { temperature = raw * 0.0625f; } return temperature; }

这段代码的几个细节值得说明。HAL_Delay(750)在高优先级中断频繁触发时不够精确,750毫秒是最长转换时间,即便中断干扰了延时,通常也足够DS18B20完成转换。如果你非要极限提速,可以在等待期间先干别的活,不阻塞式等待。

读暂存器时,字节内数据是LSB在前,所以循环移位用右移,读到第0位先放到数据最高位。这个顺序弄反了,读出来的数据会完全对不上。

4.4 关于F407 GPIO速度与时序稳定性的优化

F407的GPIO翻转速度特别快,这在模拟单总线时既是优势也是隐患。如果代码里频繁切换输入输出模式后立刻读数据,GPIO端口可能还在“震荡”,读到的电平不稳定。我的经验是,在读取前加一个非常短的延时(上文代码里10us就是这么来的),让总线电平稳定后再采样。

另外一个优化点是关闭编译器优化或者设置优化等级为-O2后重新测试。不同优化等级下,DWT延时的计数循环可能被编译器调整,导致延时偏短。实测-O2和-O0之间,同样代码读出的温度值偶尔会偏差0.1℃左右。如果你发现不同工程配置下时序表现不一致,先去查优化等级。

5. 常见问题排查与工程化经验

5.1 典型故障一览

主流的单总线调试问题,我整理成了一张表,方便对照排查。

现象可能原因排查方法
复位检测不到存在脉冲接线错误、上拉电阻缺失、IO模式切换失败先量DQ引脚对地电压,空闲应为高;用万用表量DS18B20供电
温度读数恒定85℃转换命令没发出,读的是上电默认寄存器值检查是否执行了0x44命令,并等待足够时间
读出的温度跳变、偶尔出错时序采样点偏早或偏晚用逻辑分析仪抓取读时隙波形,调整采样延时
多设备组网时读数错乱ROM命令未正确使用、设备地址冲突先用0x33读ROM命令逐个识别设备,确认序列号唯一
数据线上电平只有0.8V左右上拉电阻太小导致灌电流过大换回4.7kΩ,检查DS18B20方向性(DQ、VCC接反也会这样)

5.2 逻辑分析仪是排查时序的利器

我调试这类传感器时序时,基本离不开逻辑分析仪。市面上几十块的24MHz采样率逻辑分析仪就够用,配合PulseView或Saleae Logic软件,把DQ引脚接上去采样,能直接看到每个时隙的实际波形。

通过波形可以直观判断三个关键点:复位时拉低时间是否在480-960us窗口内;写时序的释放点是否准确;读时序中从机输出数据的窗口是否被正确覆盖。我在实际项目中,曾经因为一个delay_us函数在-O2优化下执行时间缩短了近一半,导致写时序全部偏短,用逻辑分析仪一抓波形就水落石出,省去了大量猜测。

提示:调试单总线时序时千万别只靠调试器断点。单总线是硬实时协议,一旦进入断点,时钟照走,通信早就断了。要么用波形分析工具,要么在代码里加状态标志位,串口打印关键状态。

5.3 长时间稳定运行的设计考究

如果你的设备不是实验室跑着玩,而是要7x24小时在线监控温度,有几个设计层面的坑必须提前避开。

第一,DS18B20连续频率不宜过高。每次温度转换都要消耗几十毫安的浪涌电流(主要是内部电荷泵给寄生电容充电),如果每秒读一次,长期运行会加速传感器老化。一般场景建议1到5秒采一次样就够了,温控场景可能会用到100到200毫秒一次的频率,但要做好滤波和平滑。

第二,软件上要做异常值过滤。传感器偶尔会给出明显不合理的读数,比如-55℃以下或125℃以上。我的做法是保留上次有效值,连续3次读到越界值才更新当前温度,中间任一一次恢复有效就立刻更新。这个策略简单有效,能过滤掉大部分总线上的毛刺干扰。

第三,如果DS18B20和电机、继电器等感性负载共板,数据线要走远离大电流回路,最好穿过磁珠或者加一个小电容(比如0.1uF)到地。我遇到过继电器吸合瞬间温度读数跳1-2℃的情况,加了RC滤波后彻底解决。

6. 这套方案还能怎么扩展

读完上面的内容,你已经能驱动单个DS18B20了。如果想让这套东西发挥更大价值,有两条扩展路线值得琢磨。

一条是多点组网。单总线的多设备挂载能力是它的一大特色。只要把多个DS18B20并联在同一条数据线上,主机用0x55匹配ROM命令逐个读取即可。但要注意,挂载数量越多,总线电容越大,上拉电阻可能要相应调小(比如挂5个设备时用2.2kΩ),否则波形沿变缓导致通信不稳定。还有一点,每个DS18B20的序列号都要在出厂时读取并登记到程序里,否则程序没法寻址。

另一条是给设备加日志存储功能。既然已经用了F407,完全可以把温度数据周期性地写入F407内部Flash或者外接的SD卡、EEPROM。网上关于“基于stm32f407的日志存储记录方法”的热词,本质上就是这么个需求:定期采集温度,连同时间戳一起存储,掉电不丢失。用F407的RTC获取实时时间,再用Flash模拟EEPROM,或者用FATFS挂SD卡,都是成熟方案。

我个人的建议是,别急着把功能堆满。先把单总线时序这个最底层的东西吃透,把读写稳定性做到位,再往上扩展。底层不稳,上层功能再多也是空中楼阁。

我在实际做项目时,F407 + DS18B20这种组合已经用在了好几个产品上。最有意思的一个是某冷库的温度监控器,24小时不间断记录温度,挂了8个传感器分布在库房不同位置,上位机通过RS485轮询。从那以后我对单总线的抗干扰能力有了新的认识——只要硬件设计规范,时序代码写正确,单总线在工业现场也完全能干活。做技术就是这样,越是看起来简单的器件,越要敬畏它背后的协议细节。希望这篇东西能帮你少走点弯路。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询