DS18B20单总线温度传感器驱动与实战应用
1. DS18B20温度传感器基础入门
大家好,今天我们来聊聊DS18B20这个神奇的温度传感器。我在嵌入式开发领域摸爬滚打十多年,用过各种温度传感器,但DS18B20一直是我最喜欢的之一。它最大的特点就是只需要一根数据线就能工作,这在布线复杂、接口紧张的项目中简直是救星。
DS18B20是Dallas Semiconductor(现在被Maxim Integrated收购)推出的一款数字温度传感器。它的测量范围从-55°C到+125°C,精度可以达到±0.5°C。最让我惊喜的是它可编程的分辨率功能,可以从9位到12位自由设置,对应的温度精度分别是0.5°C、0.25°C、0.125°C和0.0625°C。
在实际项目中,我经常遇到需要多点测温的场景。DS18B20支持单总线上挂载多个设备,每个设备都有唯一的64位ROM地址,这样就能用一根线同时监测多个位置的温度。记得有一次做恒温箱项目,我在一条总线上挂了8个DS18B20,分别监测箱内不同区域的温度,效果非常不错。
硬件连接方面,DS18B20只有三个引脚:VDD(电源)、GND(地)和DQ(数据线)。连接时需要注意,DQ引脚需要接一个4.7kΩ的上拉电阻到电源,这样可以保证信号传输的稳定性。电源电压可以是3.3V或者5V,兼容性很好。
2. 单总线通信协议深度解析
单总线协议是DS18B20的核心技术,理解这个协议对正确驱动传感器至关重要。我在刚开始接触这个协议时也踩过不少坑,后来通过示波器一点点分析时序才彻底搞明白。
单总线通信分为三个主要步骤:初始化、ROM操作命令和功能命令。初始化过程是主机和DS18B20建立通信的第一步。主机会拉低数据线至少480μs,然后释放总线。如果DS18B20存在,它会在15-60μs内拉低数据线60-240μs作为响应。这个响应信号很关键,如果没有收到,说明传感器可能没接好或者损坏了。
ROM操作命令用于识别总线上的设备。常用的命令有:
- 搜索ROM(0xF0):识别总线上所有DS18B20的64位ROM代码
- 读取ROM(0x33):读取单个DS18B20的ROM代码
- 匹配ROM(0x55):指定与特定DS18B20通信
- 跳过ROM(0xCC):当总线上只有一个设备时,可以跳过ROM操作
功能命令用于控制传感器的工作模式:
- 启动温度转换(0x44):让传感器开始温度测量
- 读取暂存器(0xBE):读取温度数据和其他配置信息
在实际编程中,时序控制是最容易出问题的地方。我建议使用示波器来验证时序,特别是延时时间要精确。不同型号的微控制器执行指令的速度不同,需要根据实际情况调整延时函数。
3. STM32硬件连接与配置
现在我们来具体看看如何在STM32上连接和配置DS18B20。我通常使用STM32的HAL库来开发,这样代码可移植性更好。
首先看硬件连接。以STM32F103C8T6为例,我一般选择PA0引脚作为数据线。连接方式很简单:DS18B20的VDD接3.3V,GND接地,DQ引脚通过4.7kΩ电阻上拉到3.3V,同时连接到PA0引脚。
在代码配置方面,需要先初始化GPIO。由于单总线协议需要在输入和输出模式之间切换,我们需要动态配置GPIO的工作模式。这是我的典型配置代码:
#define DS18B20_PORT GPIOA
#define DS18B20_PIN GPIO_PIN_0
void DS18B20_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
// 初始化为推挽输出
GPIO_InitStruct.Pin = DS18B20_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
// 默认拉高总线
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
}
在实际项目中,我发现GPIO速度的设置很重要。如果设置得太高,可能会产生振铃现象影响通信稳定性。我一般选择低速或中速,这样虽然速度慢一些,但稳定性更好。
电源方面,DS18B20支持两种供电模式:外部供电和寄生供电。外部供电就是正常接VDD引脚,这种方式最稳定。寄生供电时VDD接地,传感器从数据线窃电,这种方式布线更简单,但在温度转换期间需要保持总线为高电平。
4. HAL库驱动实现详解
现在我们来深入讲解HAL库下的驱动实现。我把自己在实际项目中优化的代码分享给大家,这些代码经过多次验证,稳定性很好。
首先是初始化函数,这个函数负责检测传感器是否存在:
uint8_t DS18B20_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 配置为输出模式
GPIO_InitStruct.Pin = DS18B20_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
// 拉低总线480μs以上
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
delay_us(480);
// 释放总线
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
delay_us(60);
// 切换为输入模式检测响应
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
// 检测存在脉冲
if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN) == GPIO_PIN_RESET)
{
delay_us(240);
if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN) == GPIO_PIN_SET)
{
return 1; // 初始化成功
}
}
return 0; // 初始化失败
}
写字节函数需要严格按照时序操作:
void DS18B20_WriteByte(uint8_t data)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DS18B20_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
for (uint8_t i = 0; i < 8; i++)
{
// 拉低总线开始写时序
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
delay_us(2);
// 根据数据位设置电平
if (data & 0x01) {
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
}
delay_us(60);
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
data >>= 1;
delay_us(2);
}
}
读字节函数同样需要注意时序:
uint8_t DS18B20_ReadByte(void)
{
uint8_t data = 0;
GPIO_InitTypeDef GPIO_InitStruct = {0};
for (uint8_t i = 0; i < 8; i++)
{
GPIO_InitStruct.Pin = DS18B20_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
// 拉低总线开始读时序
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET);
delay_us(2);
// 释放总线
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
delay_us(10);
// 切换为输入模式读取数据
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
data >>= 1;
if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN) == GPIO_PIN_SET) {
data |= 0x80;
}
delay_us(50);
}
return data;
}
5. 温度读取与数据处理
温度数据的读取和处理是整个驱动中最关键的部分。DS18B20的温度数据以16位二进制补码格式存储,需要正确解析才能得到实际温度值。
启动温度转换的函数如下:
void DS18B20_StartConvert(void)
{
DS18B20_Init();
DS18B20_WriteByte(0xCC); // 跳过ROM
DS18B20_WriteByte(0x44); // 启动温度转换
}
读取温度值的函数:
float DS18B20_ReadTemperature(void)
{
uint8_t temp_l, temp_h;
int16_t temp;
float temperature;
DS18B20_Init();
DS18B20_WriteByte(0xCC); // 跳过ROM
DS18B20_WriteByte(0xBE); // 读取暂存器
temp_l = DS18B20_ReadByte(); // 读取低字节
temp_h = DS18B20_ReadByte(); // 读取高字节
// 组合成16位数据
temp = (temp_h << 8) | temp_l;
// 转换为实际温度值
temperature = temp / 16.0;
return temperature;
}
在实际应用中,温度数据的处理还需要考虑一些特殊情况。比如负温度的处理,DS18B20使用二进制补码表示负温度。当温度是负值时,高字节的bit7到bit3都是1,我们需要先取反再加1,然后转换为十进制数。
分辨率设置也很重要。DS18B20默认是12位分辨率,转换时间最长(最多750ms),但精度最高。如果需要更快的转换速度,可以设置为较低的分辨率:
void DS18B20_SetResolution(uint8_t resolution)
{
DS18B20_Init();
DS18B20_WriteByte(0xCC); // 跳过ROM
DS18B20_WriteByte(0x4E); // 写暂存器
// 写TH和TL报警值(通常不使用)
DS18B20_WriteByte(0x00);
DS18B20_WriteByte(0x00);
// 设置分辨率
DS18B20_WriteByte(0x1F | ((resolution - 9) << 5));
}
6. 多设备寻址与管理
当总线上有多个DS18B20时,我们需要使用ROM命令来区分和管理各个设备。这是我在实际项目中总结出来的一套多设备管理方法。
首先需要搜索总线上的所有设备:
uint8_t DS18B20_SearchRom(uint64_t *rom_list, uint8_t max_devices)
{
uint8_t devices_found = 0;
// 实现ROM搜索算法
// ...
return devices_found;
}
ROM搜索算法相对复杂,需要实现一个递归算法来遍历所有可能的ROM地址。基本思路是:
- 发送搜索ROM命令(0xF0)
- 读取所有位的位置冲突
- 选择一条路径继续搜索
- 重复直到找到所有设备
匹配特定设备进行读数:
void DS18B20_MatchRom(uint64_t rom_code)
{
DS18B20_Init();
DS18B20_WriteByte(0x55); // 匹配ROM命令
// 发送64位ROM代码
for (uint8_t i = 0; i < 8; i++) {
DS18B20_WriteByte((rom_code >> (i * 8)) & 0xFF);
}
}
在实际项目中,我建议先将所有设备的ROM地址搜索出来并保存,然后根据需要选择特定设备进行读数。这样可以避免每次读数都要搜索整个总线,提高效率。
温度报警功能也是DS18B20的一个实用特性。可以设置温度上下限,当温度超出范围时,设备会置位报警标志。使用报警搜索命令(0xEC)可以快速找到报警的设备,不需要读取每个设备的温度值。
7. 实战应用与优化技巧
在实际项目中,DS18B20的应用远不止简单的温度读取。我来分享一些实战经验和优化技巧。
首先是电源管理。如果使用寄生电源模式,在温度转换期间需要保持总线为高电平,否则转换会失败。我通常这样处理:
void DS18B20_StartConvert_Parasitic(void)
{
DS18B20_Init();
DS18B20_WriteByte(0xCC);
DS18B20_WriteByte(0x44);
// 在转换期间保持总线为高
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DS18B20_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(DS18B20_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET);
// 等待转换完成
delay_ms(750);
}
抗干扰处理也很重要。在工业环境中,单总线容易受到干扰。我通常采取以下措施:
- 使用屏蔽线缆
- 在数据线靠近MCU端加滤波电容
- 增加重试机制
- 使用CRC校验数据完整性
DS18B20提供CRC校验功能,可以验证数据传输的正确性:
uint8_t DS18B20_CheckCRC(uint8_t *data, uint8_t len)
{
uint8_t crc = 0;
for (uint8_t i = 0; i < len; i++) {
crc = _crc8(crc, data[i]);
}
return crc;
}
低功耗设计是很多嵌入式项目的需求。DS18B20在静止状态下耗电很低,但温度转换时电流较大。如果使用电池供电,需要合理设计采样频率,在不需要测量时让传感器进入低功耗状态。
我常用的采样策略是:
- 正常模式下每10秒采样一次
- 当温度变化较大时提高采样频率
- 当温度稳定时降低采样频率
8. 常见问题与解决方案
在使用DS18B20的过程中,大家可能会遇到各种问题。我整理了一些常见问题及解决方法,希望能帮到大家。
问题1:读取温度值总是85°C 这是最常见的问题。85°C是DS18B20的上电默认值,如果一直读到这个值,通常意味着温度转换没有正确执行。检查以下几点:
- 电源电压是否稳定
- 上拉电阻是否接好
- 时序控制是否准确
- 是否等待了足够的转换时间
问题2:通信不稳定,时好时坏 这通常是时序问题或者干扰导致的。建议:
- 用示波器检查时序波形
- 检查电源滤波
- 缩短总线长度
- 降低通信速度
问题3:多设备时无法正确识别 ROM搜索算法比较复杂,容易出错。确保:
- 搜索算法正确实现
- ROM地址存储正确
- 匹配ROM时发送完整的64位地址
问题4:负温度读数错误 负温度使用二进制补码表示,需要特殊处理:
float DS18B20_ConvertTemperature(int16_t raw)
{
if (raw & 0x8000) { // 负温度
raw = ~raw + 1;
return -((float)raw / 16.0);
} else {
return (float)raw / 16.0;
}
}
问题5:转换时间过长 DS18B20的转换时间取决于设置的分辨率:
- 9位分辨率:93.75ms
- 10位分辨率:187.5ms
- 11位分辨率:375ms
- 12位分辨率:750ms
如果觉得转换时间太长,可以降低分辨率。但要注意精度也会相应降低。
问题6:寄生电源模式工作不稳定 寄生电源模式需要特别注意:
- 温度转换期间保持总线为高
- 总线负载不能太重
- 电源电压要稳定
我一般建议使用外部电源供电,这样稳定性更好。如果必须使用寄生电源,要确保布线简短,并在数据线加滤波电容。
更多推荐


所有评论(0)