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地址。基本思路是:

  1. 发送搜索ROM命令(0xF0)
  2. 读取所有位的位置冲突
  3. 选择一条路径继续搜索
  4. 重复直到找到所有设备

匹配特定设备进行读数:

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);
}

抗干扰处理也很重要。在工业环境中,单总线容易受到干扰。我通常采取以下措施:

  1. 使用屏蔽线缆
  2. 在数据线靠近MCU端加滤波电容
  3. 增加重试机制
  4. 使用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:寄生电源模式工作不稳定 寄生电源模式需要特别注意:

  • 温度转换期间保持总线为高
  • 总线负载不能太重
  • 电源电压要稳定

我一般建议使用外部电源供电,这样稳定性更好。如果必须使用寄生电源,要确保布线简短,并在数据线加滤波电容。

更多推荐