
简介本资源是面向STM32初学者与嵌入式开发者的DS18B20单总线温度采集实战工程基于STM32F103系列芯片实现高精度温度读取与串口输出功能解决单总线协议驱动、时序控制及多器件寻址等典型难点。压缩包共67个文件涵盖9个C源文件含ds18b20.c、main.c、usart.c等核心模块、9个头文件h、10个编译目标文件o及调试相关文件axf、hex、map、uvproj等完整呈现Keil MDK工程结构便于理解启动流程、外设配置与单总线底层时序实现。已有1558人学习下载资源附带清晰的模块划分SYSTEM、DS18B20、LED、KEY、USART等并包含J-Link调试日志与构建日志有助于排查烧录失败、通信超时等常见问题适合动手实践单总线传感器集成与STM32基础外设协同开发。 不少人第一次碰单总线协议就是从DS18B20开始的。这个项目标题“STM32F103读取DS18B20温度数据”看起来简单但里面藏着不少值得抠的细节GPIO模拟时序、微秒级延时、复位脉冲和存在脉冲的配合、温度寄存器解析还有各种电气上的坑。这篇文章我就按我实际做项目时的思路把这个经典组合从零到一完整拆开讲适合刚入手STM32的读者也适合那些想彻底搞懂单总线协议、不想只抄代码了事的开发者。DS18B20这颗传感器在工业、家电、环境监测里到处都能看到测温范围-55℃到125℃12位分辨率下精度能做到±0.5℃而且单总线通信只需要一根数据线加上它的64位ROM序列号一条总线上能挂几十个传感器成本又低。配合STM32F103这颗Cortex-M3内核的老将无论是做恒温控制器、冷链记录仪还是简单的环境监测站这套方案都很实用。这篇文章我会把硬件接线、协议时序、驱动代码、踩坑经验全部写清楚你照着抄一遍基本就能在自己的板子上跑起来。1. 先把这个项目拆开看硬件选型和系统框架1.1 为什么是STM32F103这颗芯片STM32F103可以说是国内工程师接触ARM Cortex-M3最熟悉的一颗芯片主频最高72MHzFlash从16KB到512KB不等RAM最大64KB外设资源非常丰富价格也便宜。用它来做DS18B20的读取其实是“大材小用”但这恰恰是它的优势你不需要纠结资源不够剩下的资源还能去做PWM输出、CAN通讯、SPI驱动屏幕这些活儿。网上搜“stm32f103最小系统”搜出来的基本都是那几个经典组成3.3V电源、8MHz晶振、复位电路、BOOT0下拉、SWD下载接口。这套东西我建议你直接用现成的开发板或者自己画一版没必要自己搭洞洞板因为DS18B20的时序对电源稳定性有一定要求如果供电纹波太大温度数据会跳动得很厉害。1.2 DS18B20的数据手册关键信息DS18B20有一个64位ROM出厂时激光写入包含8位家族码0x28、48位唯一序列号、8位CRC校验码。我们在总线上操作它先要做初始化复位然后发ROM命令最后发功能命令。单传感器场景下最常用的ROM命令是0xCCSkip ROM意思是“不用管我是谁总线上所有传感器都听着”。它的暂存器Scratchpad有9个字节具体布局如下表所示其中前两个字节就是温度值第9字节是CRC校验。字节序号内容说明0温度LSB温度寄存器低字节1温度MSB温度寄存器高字节2用户寄存器1高温报警阈值默认值由E2PROM加载3用户寄存器2低温报警阈值4配置寄存器控制分辨率可配置9~12位5保留固定为0xFF6保留固定为0xFF7保留固定为0x108CRC前8个字节的CRC-8校验值温度寄存器的格式是16位有符号数LSB和MSB组合起来最高位是符号位S。12位分辨率下每一位代表0.0625℃直接把16位有符号数乘以0.0625就能得到实际温度。负温度时符号位为1这时候数据是补码形式用int16_t类型直接做乘法就能正确转换不需要手动取反加一很多教程在这块绕了弯路。DS18B20的供电范围是3.0V到5.5V接STM32F103的3.3V完全没问题。它还有一个寄生供电模式就是只接两根线DQ和GND数据线同时当电源用。这个模式在低功耗场景下很有用但实际工程里我建议还是老老实实接三根线省心很多。1.3 最小系统与接线方案项目接线图我非常推荐用这样的方案DS18B20的VCC接3.3VGND接GNDDQ接STM32F103的任意一个GPIO我这里用PB1作为示例。DQ到3.3V之间必须接一颗4.7kΩ上拉电阻这个是重点中的重点后面会详细解释。选引脚的时候有一点要注意DS18B20的DQ是开漏输出或者推挽输出都能工作但引脚上的外部上拉电阻必须保留。如果你用的是开漏模式配上4.7k上拉就最接近官方推荐的电气结构如果你用的是推挽模式上拉电阻也建议保留防止总线悬浮状态。STM32F103的GPIO翻转速度很快配合单总线的协议完全跑得动不存在性能瓶颈。我习惯把DQ引脚定义为宏方便以后换引脚#define DS18B20_DQ_PORT GPIOB #define DS18B20_DQ_PIN GPIO_PIN_1 #define DS18B20_DQ_LOW() HAL_GPIO_WritePin(DS18B20_DQ_PORT, DS18B20_DQ_PIN, GPIO_PIN_RESET) #define DS18B20_DQ_HIGH() HAL_GPIO_WritePin(DS18B20_DQ_PORT, DS18B20_DQ_PIN, GPIO_PIN_SET) #define DS18B20_DQ_READ() HAL_GPIO_ReadPin(DS18B20_DQ_PORT, DS18B20_DQ_PIN)GPIO的初始化用HAL库配置成推挽输出还是开漏输出都行但我在代码里一般配置成开漏输出因为开漏模式下写1就是高阻态相当于释放总线非常符合单总线的逻辑。不过要注意开漏模式必须要外部上拉电阻才能输出高电平没有上拉电阻总线就会一直拉低读回来的永远都是0。2. DS18B20的核心难点单总线时序2.1 单总线协议到底是个什么逻辑单总线1-Wire协议是Dallas公司搞的一套半双工通信协议。所谓半双工就是同一时刻只能有一方在发数据要么主机发要么从机发不能同时进行。它的特点是用一根线来传数据、传命令、传电源寄生供电模式所以时序要求非常严格。你可以把单总线理解成一种“用时间长短来表达0和1”的通信方式。主机往总线上发信号不是靠电平高低来区分0和1而是靠“拉低总线的时间长短”来区分。这个思想一开始可能不太习惯但理解了之后DS18B20的时序就变得非常简单。需要注意单总线的空闲状态是高电平。因为DS18B20的DQ引脚是漏极开路结构它只能主动拉低不能主动拉高必须依靠外部上拉电阻把总线恢复成高电平。所以上拉电阻的阻值直接影响总线电平的上升时间。阻值太大上升沿太慢时序就容易被破坏阻值太小电流过大传感器可能工作不稳定。4.7kΩ是一个比较通用的值如果总线很长比如超过1米可以适当减小到2.2kΩ。2.2 初始化时序复位脉冲和存在脉冲每次和DS18B20通信之前主机必须发一个复位脉冲来“唤醒”总线上所有的传感器。这个步骤很重要相当于打招呼。复位脉冲的过程是这样的主机先把总线拉低持续480μs到960μs通常取500μs然后释放总线。这时上拉电阻会把总线拉回高电平。DS18B20检测到这个下降沿后会等待15μs到60μs然后主动把总线拉低60μs到240μs这个由传感器拉低的信号就是“存在脉冲”用来告诉主机“我在线”。主机在释放总线后需要在15μs到60μs的时间窗口内读取总线电平。如果读到低电平说明总线上有DS18B20响应如果读到高电平说明没有传感器或者连接出问题了。我写驱动的时候习惯在复位函数里加一个小的超时判断防止程序卡死在等待上uint8_t ds18b20_reset(void) { uint8_t presence 1; DS18B20_DQ_LOW(); // 主机拉低总线 delay_us(500); // 保持低电平500us DS18B20_DQ_HIGH(); // 释放总线 delay_us(70); // 等待DS18B20响应15~60us内采样 presence DS18B20_DQ_READ(); // 读到0表示存在读到1表示无设备 delay_us(200); // 等待存在脉冲结束补完整复位时隙 return presence; }这段代码里返回值presence如果是0说明传感器在线如果是1说明离线。很多初学者会在这一步卡住因为无论如何都返回1排查到最后发现是上拉电阻没焊或者延时时间不对。2.3 写时隙与读时隙初始化完成之后主机就可以发命令了。单总线的每一位传输叫做一个“时隙”Time Slot每一个时隙至少需要60μs两个时隙之间至少要有1μs的恢复时间。写时隙分为写0和写1两种情况。写0时隙主机拉低总线持续60μs到120μs然后释放。也就是说整个时隙几乎都被拉低这样DS18B20就能识别出这是一个0。写1时隙主机拉低总线但只持续1μs到15μs普通做法是6μs到10μs然后立即释放让上拉电阻把总线拉回高电平持续到60μs以上。这里容易踩坑的地方是很多人的延时函数是毫秒级的或者用HAL_Delay这种被SysTick中断影响的延时写出来的时隙宽度就不准了。后面我会专门讲微秒级延时的实现这一步是整个项目的地基不能马虎。读时隙的过程稍微复杂一点。主机拉低总线1μs到15μs然后释放总线。如果DS18B20要发送0它会在主机释放总线后立刻把总线拉低并保持到整个时隙结束如果DS18B20要发送1它不会拉低总线总线就一直保持高电平。主机必须在释放总线后的15μs内采样总线电平因为DS18B20保持低电平的时间是有限的。我贴一段标准读时隙代码uint8_t ds18b20_read_bit(void) { uint8_t bit 0; DS18B20_DQ_LOW(); // 主机拉低总线 delay_us(6); // 保持6us DS18B20_DQ_HIGH(); // 释放总线 delay_us(8); // 等待DS18B20输出数据 bit DS18B20_DQ_READ(); // 采样总线电平 delay_us(50); // 补齐时隙时间 return bit; }读时隙采样的时间点非常关键。如果采样太早DS18B20可能还没来得及把数据放到总线上采样太晚总线可能已经被上拉电阻拉高了导致把0误读成1。经过多次实测6μs拉低、8μs等待、14μs左右开始采样这个组合在72MHz主频下很稳定。2.4 时序参数速查表为了方便查我把DS18B20的关键时序参数整理成了表格。这些都是我在实际调试中验证过的直接照用就行时序动作参数推荐值数据手册范围复位脉冲主机拉低时间500μs480~960μs存在脉冲等待释放后等待采样点70μs15~60μs采样窗口存在脉冲DS18B20拉低时间约200μs60~240μs写0时隙主机拉低时间70μs60~120μs写1时隙主机拉低时间10μs1~15μs读时隙主机拉低时间6μs1~15μs读时隙采样点释放后采样时间14μs左右释放后15μs内时隙总长单个位传输时间70~80μs60~120μs这份表格写出来基本上就是照着抄都能跑的程度。但真正到了实际项目里你还要考虑编译器优化等级、GPIO翻转速度、总线寄生电容这些因素所以最稳妥的做法是拿示波器去看DQ引脚的波形确保每个时隙的长度都在数据手册允许的范围内。3. STM32F103驱动代码实现3.1 工程框架与引脚规划我用的是STM32CubeMX生成工程选STM32F103C8T6这颗芯片时钟配置成最高72MHz。GPIO部分只配置PB1一个引脚为输出模式如果你用开漏输出注意PB1的初始化代码要设置成GPIO_MODE_OUTPUT_OD速度设成GPIO_SPEED_FREQ_HIGH。CubeMX的图形化配置对新手特别友好RCC选Crystal/Ceramic ResonatorSYS选Serial Wire这样SWD下载不会被占掉。时钟树里把HCLK拉到72MHz其他外设先不初始化。生成代码之后在main.c里加上DS18B20的驱动文件。有时你会看到有人把DQ引脚配置成推挽输出GPIO_MODE_OUTPUT_PP也能工作但需要额外处理“释放总线”这个动作因为推挽模式下写1就是强制输出高电平和开漏模式下“靠上拉电阻恢复高电平”在电气特性上是有一点差异的。单总线协议只规定了逻辑电平推挽输出在短距离、单设备场景下能跑但开漏输出更符合规范也更稳。3.2 先解决微秒级延时这是整个项目里最关键的底层函数。DS18B20的时序大量用到微秒级延时而STM32的HAL库只提供了毫秒级HAL_Delay没有精确的微秒延时。如果你用for循环空转来延时不同优化等级下时间会完全不一样代码换个编译选项可能就废了。我的做法是使用DWTData Watchpoint and Trace模块。Cortex-M3内核里有一个32位的CYCCNT寄存器它每个内核时钟周期加1非常适合做精确延时。只要系统主频确定用CYCCNT差值的个数除以主频就能得到精确时间。初始化代码很简单#include core_cm3.h static void delay_us_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 startTick DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - startTick) ticks); }注意SystemCoreClock在72MHz主频下是72000000SystemCoreClock/1000000等于72所以延时1μs就是等72个时钟周期。这个延时函数在中断里也能用非常方便。为什么不建议用SysTick做微秒延时因为SysTick往往已经被HAL库的HAL_InitTick占用用来给HAL_Delay做时基你再动SysTick会让HAL_Delay失效。DWT是独立于SysTick的互不影响。3.3 单总线底层函数编写有了微秒延时之后底层函数就水到渠成了。完整的底层代码包括写一个位、读一个位、写一个字节、读一个字节。写字节的时候注意LSB先行DS18B20的命令和数据都是低位在前。这个细节特别容易错写反了命令识别不了传感器完全没反应。void ds18b20_write_byte(uint8_t data) { for (uint8_t i 0; i 8; i) { ds18b20_write_bit(data 0x01); data 1; } } uint8_t ds18b20_read_byte(void) { uint8_t data 0; for (uint8_t i 0; i 8; i) { data 1; if (ds18b20_read_bit()) { data | 0x80; } } return data; }这段代码里的逻辑是每读到一个位先把已有的数据右移一位然后把新读到的位放到最高位。8次循环之后最先读到的LSB就移动到了最低位符合DS18B20的字节序要求。3.4 温度读取流程与数据解析DS18B20的完整读取流程分三步复位、启动温度转换、读取暂存器。启动转换之后要等待转换完成12位分辨率下转换时间最长为750ms所以我直接延时750ms。如果想加快读取速度可以发0x44之后轮询读取总线电平总线变高说明转换完成这种方式可以把12位转换等待时间缩短到约400ms左右具体要看传感器。读温度的完整函数float ds18b20_read_temperature(void) { uint8_t data[9] {0}; int16_t raw_temp 0; float temp 0.0f; if (ds18b20_reset()) { return -999.0f; // 传感器不存在 } ds18b20_write_byte(0xCC); // 跳过ROM匹配 ds18b20_write_byte(0x44); // 启动温度转换 delay_ms(750); // 等待12位转换完成 if (ds18b20_reset()) { return -999.0f; } ds18b20_write_byte(0xCC); // 跳过ROM匹配 ds18b20_write_byte(0xBE); // 读取暂存器 for (uint8_t i 0; i 9; i) { data[i] ds18b20_read_byte(); } raw_temp (data[1] 8) | data[0]; temp raw_temp * 0.0625f; return temp; }之前提到过raw_temp声明成int16_t直接用补码表示负温度乘以0.0625就能得到正确的负值。比如-10℃时温度寄存器的值是0xFF60也就是-160乘以0.0625就是-10.0℃。如果你还想做CRC校验可以用Dallas的CRC-8算法对data[0]到data[7]进行计算然后和data[8]比较。CRC多项式是0x8C如果不想深入了解直接把这个函数放进工程里就行。不过对于一般场景跳过去也不是不行因为单总线短距离通信出错的概率很低主要还是电气连接故障。4. 实操中的典型问题与排查思路4.1 常见异常现象速查表实际调板子的时候读不到温度或者温度不对的情况非常常见。我整理了一个速查表方便你对症下药现象可能原因排查思路温度固定返回85℃传感器上电默认值就是85℃说明初始化或命令没发对检查复位函数返回值确认是否有存在脉冲用逻辑分析仪看时序温度恒为0℃DQ引脚读数一直是0可能是上拉电阻没焊或开漏没有上拉测量DQ引脚空闲电压应该是3.3V左右温度跳动快接线松动、供电纹波大、采样周期太短检查连接端子加强电源滤波数据做滑动平均返回值永远是-999初始化失败没有检测到传感器检查引脚宏定义、GPIO时钟是否开启、上拉电阻阻值写入命令后无响应字节序写反LSB和MSB搞混确认write_byte是低位先发用示波器对比命令波形换了一块板子就不行引脚没改对应的宏定义检查GPIO时钟使能和引脚号是否匹配4.2 我踩过的几个坑第一个大坑就是HAL_Delay的精度问题。很多新手直接拿HAL_Delay(10)去当成微秒延时用结果完全不行。HAL_Delay的实现依赖SysTick中断最低精度就是1ms而且会被其他中断打断。DS18B20的时序是微秒级的用HAL_Delay写出来的代码能初始化成功都是运气后面大概率会稳定复现各种诡异问题。所以DWT微秒延时这一关必须过。第二个大坑是忘记使能GPIO时钟。CubeMX生成的代码一般不会漏但如果你手动写寄存器或者改了引脚很容易漏掉__HAL_RCC_GPIOB_CLK_ENABLE()这句。PB1引脚完全没有输出能力DS18B20自然没反应。排查这类问题最快的方法是看DQ引脚电压如果空闲状态不是高电平说明GPIO就没配置对。第三个大坑是采样窗口太靠后。读时隙要求主机在释放总线后15μs内采样如果太晚总线已经被上拉电阻拉高了结果就是所有数据位都读到1。这种情况最神奇的表现在于读出来的温度总是0xFFFFFFFF或者固定某个值因为读到的每一位都是1。第四个大坑是中断优先级干扰。如果系统里开了定时器中断、串口中断而且中断处理时间较长那么DS18B20读写某个位的过程中被打断整个时序就错乱了。解决办法是在读写时隙期间临时关中断或者用DMA传输配合定时器比较精确地控制时序。对于普通项目临时关中断最简单可靠void ds18b20_write_bit(uint8_t bit) { __disable_irq(); // 时序操作 __enable_irq(); }关中断的时间很短每个时隙最多100μs对系统实时性影响很小。但要注意如果你用了FreeRTOS这类操作系统这种方式要更谨慎不能关太久。4.3 提升稳定性的几个手段在实际项目中我一般会把DS18B20的读取放慢一点比如每隔1秒读一次。温度是慢变量没必要每秒读几十次。每次读取之间加上足够的间隔也能减少DS18B20和主控之间的时序压力。第二是软件滤波。单次读取的温度偶尔会有毛刺我通常连续读3次取中间值或者平均值。这不仅对DS18B20有用对任何模拟量采集都适用。你可以在驱动外面包一层滤波函数不要改底层驱动。第三是PCB布线。如果DS18B20离主板比较远尽量用双绞线或者屏蔽线DQ线不要和电源线、电机驱动线走在一起。上拉电阻可以放在靠近主控的地方然后在DS18B20那边并一个小电容100nF做电源去耦这样能明显减少噪声干扰。5. 项目扩展思路5.1 一条总线上挂多个DS18B20这个标题只说了单个传感器读取但DS18B20真正的价值在于多点测温。每条单总线最多可以挂几十个DS18B20每个传感器都有唯一的64位ROM序列号可以通过ROM命令来区分。挂多个传感器时流程要先做搜索ROM0xF0命令把总线上所有传感器的序列号枚举出来然后对每一个传感器使用匹配ROM0x55命令带上它的序列号单独启动转换和读取温度。搜索ROM的算法比较复杂网上的现成代码很多但如果你只是固定挂几个传感器完全可以在出厂前用读ROM0x33命令把序列号读出来直接写到程序里这样比每次枚举简单很多也更稳定。需要注意一条总线上挂多个DS18B20时上拉电阻要适当减小或者用2.2kΩ。因为多个传感器的漏极输出并联在一起总线的寄生电容会变大上拉电阻太大时上升沿太慢时序容易出错。5.2 与PWM/CAN等其他外设联动STM32F103读取温度只是开始拿到温度值之后往往要控制风扇转速或者报警。这时候就涉及到热词里说的“stm32f103的pwm输出配置”和“stm32f103 can通讯例程”。PWM控制是最常见的应用。温度高了风扇转速加快温度低了风扇降速。STM32F103的定时器输出PWM非常方便用CubeMX把TIM2的通道1配成PWM输出频率设成25kHz左右避免电机噪音然后根据温度值调整占空比就行。CAN通讯则适合多设备组网的场景比如工业现场有多个温度采集节点每个节点用STM32F103读取DS18B20再通过CAN总线把温度数据发送到主控。CAN的实时性和抗干扰能力比串口强很多尤其是信号线比较长、环境有电磁干扰的情况下。5.3 工业场景下的设计考虑如果这个项目要放在工业环境里有几个点需要额外注意。第一是电源隔离DS18B20和主控之间加隔离器件防止现场干扰通过地线窜进单片机。第二是防雷和浪涌保护如果传感器放在室外建议在DQ线上加TVS管。第三是量程校准DS18B20在出厂时有精度差异对温度精度要求高的场景需要通过报警寄存器做两点校准。还有一个容易被忽略的点DS18B20在3.3V供电和5V供电下测量精度略有差异。如果系统里已经有5V电源给DS18B20供5V会更接近数据手册标称的精度值。但注意DQ引脚的电平匹配如果DS18B20工作在5V而STM32F103的GPIO是5V容忍的绝大多数F103的GPIO都标注FT也就是5V容忍开漏输出配合上拉时可以直接连接推挽输出就要加电平转换或者串联电阻限流。我自己做项目时最终选用的方案是3.3V供电开漏输出4.7k上拉DWT延时每500ms采样一次并做三取中值滤波。这个方案在-20℃到60℃的环境温度下测了整整一周数据稳定没有出现一次初始化失败。温度显示稳定到小数点后一位完全满足大多数场景的需求。最后分享一个实用小技巧在调试DS18B20时序的时候不要把延时函数写在被编译器优化掉的代码里。我遇到过好几次同一个工程在-O0优化下正常改成-O2之后传感器就完全没响应了。后来发现是某个朋友写了个空的for循环延时被编译器直接优化掉了。用DWT延时就不会有这种问题因为它读取的CYCCNT是硬件寄存器编译器无法优化掉。这个项目虽然小而简单但把单总线协议、精确延时、GPIO操作、数据解析这几个嵌入式基本功都串起来了。做会了它你再去碰I2C、SPI这类总线协议会发现很多思想都是相通的。如果你正在为温度采集发愁希望这篇文章能帮你少走几步弯路直接跑通第一版程序。本文还有配套的精品资源点击获取