拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F407驱动DS18B20:单总线时序、DWT延时与故障排查

简介面向嵌入式初学者的STM32F407与DS18B20温度监测工程包集成TFT屏幕图形化显示完整演示了单总线协议读写、GPIO开漏配置、HAL库驱动及GUI应用等核心环节。压缩包共248个文件总大小约10.96MB主要包含C源文件、H头文件、库文件及编译生成的o、d、crf等中间文件另有Keil工程文件uvprojx可直接用MDK打开编译目录按Library、Project、User等模块划分便于对照学习。已有1005人学习下载适合希望从零掌握DS18B20时序驱动并完成可视化界面设计的嵌入式开发者。通过这套资源读者可以获得一个可直接运行的工程模板参考其代码结构快速搭建自己的温度采集系统同时通过阅读驱动实现与GUI绘制流程理解单总线通信的数据交互机制以及STM32F407外设的初始化逻辑为后续IoT或智能家居项目落地提供实用范本。 大概是两个月前我在整理旧硬盘时翻出了一个刚开始学STM32时写的DS18B20驱动工程。那会儿为了让stm32f407和DS18B20正常通信我整整折腾了好几天温度读出来不是85°C就是干脆卡死在while循环里一度怀疑是芯片坏了。后来静下心把单总线时序彻底捋了一遍才意识到问题几乎都出在STM32这侧的GPIO切换和延时实现上而不是传感器本身。这篇文章就围绕stm32f407 DS18B20这个组合把单总线的时序原理、驱动代码、实测故障排查和几个进阶玩法一次讲透给正准备入坑或者已经被DS18B20折磨过的朋友做个参考。我默认你手里有一块F407核心板、一个DS18B20模块或者裸芯片加一个4.7kΩ电阻以及一个能跑代码的调试环境。接下来要讲的东西标准库和HAL库通用核心是时序逻辑框架搭对了移植到别的单片机也只是改个GPIO操作的事。1. DS18B20难调的本质无时钟线的单总线时序1.1 单总线最容易被忽略的约束条件DS18B20用Dallas的1-Wire协议通信物理上只有一根数据线时钟信息全靠主机也就是STM32控制电平跳变的时间窗口。这和SPI、I2C最大的区别在于其他总线有专门的时钟线从机照着时钟边沿采样就行单总线没有时钟线从机完全是靠检测“拉低多长时间”“释放后多长时间采样”来判断主机在发什么。这意味着什么意味着你的代码里每个延时都是通信协议的一部分。拉低2μs和拉低15μs在DS18B20看来可能就是一个“写1”和一个“写0”的区别。时序窗口很短尤其读时隙里主机释放总线后必须在15μs内完成采样手一抖或者代码里多夹了一条无关指令读回来的就是乱码。DS18B20的官方时序参数里几个核心时间窗口如下复位脉冲主机拉低480μs以上然后释放。释放后等待15~60μs如果DS18B20在线它会主动拉低总线60~240μs作为存在脉冲。写时隙主机拉低总线。如果是写0需要继续保持拉低60~120μs如果是写1则在拉低后15μs内释放总线。读时隙主机拉低总线至少1μs然后释放。释放后约15μs内如果DS18B20要返回0它会继续拉低总线如果返回1它就不动作。主机必须在这段时间里采样电平。位与位之间至少要有1μs的恢复时间整个时隙的总时间在60~120μs之间。这里有个很多人容易栽跟头的地方写1时“15μs内释放总线”不是“立刻释放”而是主机必须先拉低至少1μs再释放。网上有些代码写的是“拉低后马上释放”虽然大概率也能工作但在高温、长线、噪声大的场景下就很容易出读写错误。我后来做的驱动全部严格按“拉低1~2μs再释放”来写稳定性明显提升。1.2 STM32F407上GPIO模拟通信的资源准备STM32F407是Cortex-M4内核主频最高168MHz跑DS18B20这种μs级时序完全不是问题。但正因为主频高反而带来了一个新问题一条指令的执行时间只有几ns你随手写个空循环延时不同编译器优化等级下实际延时可能差好几倍。我第一次调DS18B20的时候用的是一段网上抄的for(i0;i100;i);延时在Keil默认优化等级下能跑通后来把优化等级调成-O3温度直接读不出来了。排查了半天才发现编译器把那个空循环整个优化没了。所以资源准备的第一件事不是连GPIO而是先准备一个可靠的微秒级延时函数。关于这个函数怎么实现我在下一章详细展开这里先提醒一句不要依赖编译器推荐的while(i--);计数延时除非你确认过反汇编代码。GPIO本身按常规配置就行推荐用开漏输出模式。单总线是线与逻辑开漏输出配合外部上拉电阻天然支持多设备挂载同一个总线引脚。如果用的是模块通常模块上已经带了上拉电阻如果是裸芯片需要自己在DQ引脚和VDD或3.3V之间接一个4.7kΩ电阻。2. 硬件连接与延时基础火花的起点2.1 上拉电阻、供电方式与GPIO模式的搭配DS18B20支持两种供电方式外部供电和寄生供电。寄生供电下传感器会从数据线上偷电对时序要求更苛刻STM32F407这侧的驱动能力也需要额外考虑。我的建议是除非你的应用场景真的没有多余电源线否则一律用外部供电。VDD接3.3VGND共地DQ接一个GPIO引脚比如我用的是PH7。GPIO模式的选择直接决定时序稳定性。开漏输出模式下MCU只能主动拉低或者释放总线释放后电平由外部上拉电阻决定。这种模式下写0就是GPIO_ResetBits写1就是GPIO_SetBits释放总线逻辑非常干净。推挽输出模式也能用但需要注意推挽输出在输出高电平时是主动驱动到3.3V如果总线上有其他设备在拉低会形成短暂直通轻则信号畸变重则损坏IO。除非你很确定总线上只有你这一个从机且没有其他主机否则不推荐推挽。上拉电阻的取值也讲究。4.7kΩ是数据手册推荐的典型值适合大多数场景。如果DQ线很长超过20cm寄生电容会明显增大此时可以减小到2.2kΩ甚至1kΩ增加驱动能力。反过来如果线很短且总线只有一个设备10kΩ也能工作但余量很小。我实测下来F407开发板和传感器之间用10cm杜邦线连接时4.7kΩ表现最稳定换成50cm双绞线之后1kΩ明显比4.7kΩ可靠。2.2 用DWT实现稳定的微秒级延时前面提到过循环计数延时在编译器优化面前不堪一击而SysTick延时标准库里的delay_us或者HAL库的HAL_Delay也不适合直接用HAL_Delay是毫秒级的而且在中断服务函数里调用会死锁。最优雅的方案是利用Cortex-M4内核自带的DWTData Watchpoint and Trace单元中的CYCCNT计数器。DWT-CYCCNT是一个32位的自由运行计数器每个内核时钟周期加1。F407主频168MHz时它每秒计数1.68亿次分辨率大概5.95ns远远满足DS18B20的μs级时序需求。最关键的是它不依赖任何中断也不会被编译器优化掉。初始化代码很简单static void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; }延时函数利用无符号减法自动处理溢出不需要关闭中断static void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }为什么说这个方案特别适合DS18B20因为读时隙里有个15μs采样窗口用DWT延时的话把释放总线和读引脚之间精确控制在5μs左右就能稳定采到数据不用担心中断嵌套或者编译器优化把时序搞乱。我在博客评论区看到不少人问“用HAL_Delay不行吗”在这里统一回答不行至少不能用它做微秒级操作。HAL_Delay基于SysTick中断调用一次最少也要几十微秒多个几次时序窗口就被吃光了。3. 完整驱动实现从复位到温度换算的代码拆解3.1 复位与存在脉冲的判定标准DS18B20每一次通信的前奏都是一次复位操作。主机拉低总线480μs以上释放后等待15~60μs然后读取总线电平。如果DS18B20在线它会主动拉低总线60~240μs。很多人写复位函数时只做了“拉低-释放”两步没有严格检查存在脉冲的时序位置导致连不上传感器时根本不知道问题出在哪。我的驱动里会加一个超时判断和一个电平检查uint8_t DS18B20_Reset(void) { uint8_t presence 0; DQ_HIGH(); // 先释放总线 DWT_Delay_us(10); // 让总线稳定 DQ_LOW(); // 拉低发起复位 DWT_Delay_us(500); // 低电平持续500μs满足480μs要求 DQ_HIGH(); // 释放总线 DWT_Delay_us(30); // 等待15-60μs窗口这里取30μs presence DQ_READ(); // 读取存在脉冲低电平代表设备在线 DWT_Delay_us(450); // 等待整个复位周期完成 return presence; }这里有个细节presence读到0说明设备在线读到1说明设备不在。所以判断时应写if (DS18B20_Reset() 0)。我做主函数测试时第一次就搞反了以为传感器坏了其实是逻辑写反了。3.2 写时隙、读时隙的位级操作复位之后是ROM命令和功能命令这些命令的传输都封装在写字节和读字节函数里。DS18B20是低位先传写一个字节本质就是循环8次写时隙。写时隙的代码void DS18B20_WriteBit(uint8_t bit) { if (bit) { DQ_LOW(); DWT_Delay_us(2); // 拉低2μs然后释放 DQ_HIGH(); DWT_Delay_us(60); // 保持高电平到整个时隙结束 } else { DQ_LOW(); DWT_Delay_us(60); // 持续拉低60μs DQ_HIGH(); DWT_Delay_us(2); // 时隙末尾恢复时间 } } void DS18B20_WriteByte(uint8_t byte) { for (uint8_t i 0; i 8; i) { DS18B20_WriteBit(byte 0x01); byte 1; } }读时隙的代码uint8_t DS18B20_ReadBit(void) { uint8_t bit; DQ_LOW(); DWT_Delay_us(1); // 拉低1μs DQ_HIGH(); // 释放总线 DWT_Delay_us(5); // 延时5μs后采样远小于15μs上限 bit DQ_READ(); DWT_Delay_us(50); // 等待时隙结束 return bit; } uint8_t DS18B20_ReadByte(void) { uint8_t byte 0; for (uint8_t i 0; i 8; i) { byte 1; if (DS18B20_ReadBit()) byte | 0x80; } return byte; }注意读字节里的移位顺序因为是低位先收所以先右移再按位或到最高位循环8次后正好还原出一个完整字节。这也是网上代码最容易写错的地方之一。3.3 读取温度的完整流程与CRC校验DS18B20单次温度读取流程是复位发送0xCC跳过ROM单设备场景发送0x44启动温度转换等待转换完成12位分辨率需要750ms复位发送0xCC发送0xBE读暂存器连续读9个字节用第9个字节CRC校验前8个字节完整代码float DS18B20_ReadTemperature(void) { uint8_t data[9]; int16_t raw; float temp; if (DS18B20_Reset() 0) return -100.0f; // 设备没在线 DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); DWT_Delay_us(750000); // 等待转换完成 if (DS18B20_Reset() 0) return -100.0f; DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); for (uint8_t i 0; i 9; i) { data[i] DS18B20_ReadByte(); } if (DS18B20_CRC8(data, 8) ! data[8]) { return -101.0f; // CRC校验失败返回错误码 } raw (data[1] 8) | data[0]; temp raw * 0.0625f; // 12位分辨率每个LSB对应0.0625°C return temp; }CRC校验函数按数据手册给出的多项式X^8 X^5 X^4 1实现uint8_t DS18B20_CRC8(uint8_t *data, uint8_t len) { uint8_t crc 0; for (uint8_t i 0; i len; i) { uint8_t byte data[i]; for (uint8_t bit 0; bit 8; bit) { uint8_t mix (crc ^ byte) 0x01; crc 1; if (mix) crc ^ 0x8C; byte 1; } } return crc; }如果不大了解CRC可以把它理解成一个算数校验把前8个字节经过特定运算得到第9个字节如果传输过程有任何位翻转算出来的校验值就和收到的第9字节对不上。这样能自动发现时序毛刺造成的偶发读错而不是拿到一个看起来正常实则错误的数据。还有一个很多人不知道的点DS18B20的温度寄存器里负数是以补码形式存储的。raw * 0.0625f这个公式在C语言里对负数同样成立因为补码转成int16_t后就是真实幅值。我在网上见过有人专门写负数判断分支其实没必要直接用int16_t类型接住然后乘0.0625即可。4. 实测中的典型故障排查链路4.1 温度永远85°C先从流程找问题如果读出来的温度恒为85.0°C恭喜你传感器其实是好的。85°C是DS18B20上电后温度寄存器的默认值0x0550对应85.0°C。这意味着你已经成功执行了复位、跳过ROM、读暂存器这一串命令但没有正确执行温度转换命令0x44或者0x44发出后转换还没完成你就去读了。我的第一次调试就是这个问题。代码里把0x44和0xBE的顺序写反了发完0xBE直接读读到的自然是上一次启动瞬间的默认值。排查方法很简单在示波器上看DQ引脚的波形复位脉冲之后是否有一长段完整的高速脉冲串写字节然后有一段至少几百毫秒的低活跃期转换期间传感器不响应再出现一段同样的脉冲串。如果0x44之后没有任何低活跃期说明命令根本没发出去或者被时序错误吞掉了。还有一种情况复位后发送0xCCDS18B20确实收到了但0x44因为时序偏差没收到传感器就一直停在那里你读的时候拿到的是上电默认值。这就要检查写时序的波形确认每个时隙的低电平持续时间在合理范围。4.2 温度跳变与持续偏高硬件与采样策略温度跳变或者读数忽高忽低没有规律通常是读时隙采样点太靠后导致的。DS18B20返回0时它会从主机释放总线后的约15μs开始拉低总线拉低持续约45μs。如果你的采样点正好落在15~45μs窗口的临界处会因为线缆电容导致电平翻转不够陡峭偶尔采到错误电平。解决办法是把采样点前移。我上面的代码里释放总线后只等5μs就采样这个位置远离临界区即使信号波形被电容钝化也能采到稳定电平。要是条件允许用示波器看一下波形采样点应该选在数据电平稳定区域的中段而不是边缘。温度持续偏高1~2°C就要联想到自热效应。DS18B20转换时功耗约1mA如果代码里循环不停地读温度几乎每秒钟都在转换芯片自身发热积累起来读数就会系统性偏高。我测试过连续高频读取和每2秒读一次相比前者的读数会高出1.3°C左右。建议实际项目里温度转换间隔至少设1秒以上不需要那么高频率的场景可以更慢。偏高还有一种可能是ADC参考误差但DS18B20是数字输出不存在这个环节所以一旦发现读数偏高先怀疑自热再怀疑供电电压。如果把供电从3.3V降到2.8V转换精度确实会受影响但绝大多数F407开发板上的3.3V是稳定的基本可以排除。4.3 delay卡死中断优先级与延时实现的博弈“程序跑到延时函数里就卡死”是搜索引擎里关联度极高的一个问题我自己也遇到过。用HAL库时如果在某个中断回调里调用HAL_Delay而该中断优先级比SysTick低或相同系统会直接死锁。SysTick中断优先级默认为最低用户中断一旦占用了SysTick的触发时机HAL_Delay内部的等待标志永远等不到置位。换成DWT延时后这个问题彻底消失因为DWT不依赖任何中断。但需要注意DWT延时受中断影响依然存在波动如果一个高优先级中断在延时过程中插进来执行了几十μs那么延时的实际时长会被拉长。对于DS18B20这种对时序上限也有要求的协议长个几十μs通常问题不大因为时隙本身有120μs的余量。但如果你在一个很紧的时隙里调了中断还是可能翻车。稳妥做法是在读写时隙的关键路径上暂时屏蔽中断。Cortex-M4内核自带__disable_irq()和__enable_irq()但直接全局开关中断可能影响系统的实时性。更精细的方式是用__set_PRIMASK(1)/__set_PRIMASK(0)搭配保存状态或者只调高SysTick优先级保证留给DS18B20的时序窗口十拿九稳。我自己最终的做法是整个DS18B20驱动内部用临界区宏保护读一个完整字节期间禁止中断其余时间中断自由。8个读时隙加起来也就几百微秒对实时系统的影响完全可以接受。5. 进阶玩法多点采集、低功耗与可视化监控5.1 多点总线上的ROM匹配策略一条DQ线上可以并联多个DS18B20因为它们出厂时烧录了唯一64位ROM序列号。多点采集时不能再用0xCC跳过ROM必须先知道每个设备的ROM然后用0x55匹配ROM命令指定操作对象。获取所有从设备ROM的办法有两个单点总线法先把所有传感器逐个单独挂到总线上用0x33读ROM命令读出序列号记录到数组里然后再统一挂到总线上。搜索ROM算法总线上直接执行0xF0搜索命令利用每个位时隙的两次读时隙判断逻辑0、逻辑1和冲突位递归枚举出所有ROM。搜索ROM算法逻辑比较绕大概思路是DS18B20在搜索过程中会先把本位的值写到总线上然后取反再写一次。主机据此判断总线上所有设备在该位上是都为0、都为1还是既有0又有1。如果冲突就通过写一个比特来选择走哪条分支逐位走完64位后得到一条完整ROM路径。第一次实测时写了一百多行递归代码调试了两天才跑通确实费劲但搞清楚一次之后后续设备扩展就很省事了。如果懒得写搜索算法工程上还有个取巧做法用0x33读每个传感器的ROM并存储到Flash里做成“设备注册表”。之后每次启动就按注册表里的ROM序列依次用0x55匹配以数组方式遍历读取每个传感器的温度。5.2 低功耗场景下的唤醒采集设计如果项目做的是电池供电的IoT温度节点DS18B20和STM32F407就得考虑低功耗配合。F407的STOP模式待机电流能做到微安级DS18B20在待机状态下典型电流小于1μA两者组合非常适合低功耗采集场景。关键是时序上的配合。F407进入STOP模式前把DQ引脚配置成模拟输入或高阻态避免引脚漏电。唤醒后先等一小段时间让电源轨稳定再重新初始化DWT延时和DS18B20时序。因为DS18B20在掉电/深度待机后需要一定唤醒时间建议复位前先让总线空闲几百微秒。另一个细节是从唤醒到读出第一次有效温度至少要经过一次完整的转换周期12位分辨率下是750ms。如果系统要求“唤醒后10秒内上报数据”这个时间预算是够的但如果要求毫秒级上报就必须在睡前的最后一次数据里带上时间戳或者降低分辨率到9位转换时间只需93.75ms。实测9位分辨率为0.5°C对大多数室内环境监控足够了性价比很高。5.3 温度数据上云与可视化温度读出来只是第一步怎么用才是“碰出火花”的关键。最简单的方案是串口直传F407通过USART把温度字符串发给PCPC端用Python的pyserial库读取再用matplotlib实时画曲线。这个方案半小时就能搭好适合验证传感器稳定性和标定精度。更实用的方案是挂一个ESP8266模块把温度通过MQTT协议上报到公共或本地MQTT服务器然后在Node-RED或Grafana里做仪表盘。我在一个宿舍环境监控项目里就是这么干的F407每5秒读一次温湿度通过UART把JSON包发给ESP8266ESP8266再走WiFi发到EMQX broker。整套链路稳定性很好连续运行一个月没掉过线。要注意的是DS18B20上电后的第一次读取建议在代码里主动丢弃因为默认值和当前真实温度是两回事。我习惯在初始化后先读一次但不用结果之后才开始正式采集相当于给传感器一个“热身”周期。另外每个DS18B20的出厂校准略有差异如果追求精度可以把传感器放在冰水混合物0°C和沸水100°C注意海拔修正里做两点校准在软件里加一个线性修正系数。这个操作我用下来能把误差从±0.5°C压到±0.1°C左右。最后再分享一个我自己的习惯所有DS18B20的驱动代码我都会在文件头留下测试日期、示波器抓到的时序参数和对应编译器优化等级。上次调完隔了半年再回去改功能全靠这个注释帮我快速恢复现场。时序型外设就是这样——代码跑通了是一回事能稳定复现、敢交付给别人又是另一回事。把这个细节养成习惯你在F407上写任何单总线协议驱动都会少走很多弯路。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门