一、单总线协议概要

单总线(1-Wire)由 Dallas 提出,数据线既做供电又做通信。STM32 上通常用开漏输出 + 上拉电阻模拟。DS18B20 与 DHT11 虽然都叫单总线,但时序完全不能互通,必须各自独立实现时序,更不能同时挂在同一根总线上。

两者的差异点:

  • DS18B20:严格的 60~240μs 时隙,需要 ROM 命令和暂存器操作。
  • DHT11:40bit 数据一次性输出,无设备寻址,起始时序要求更宽松。

由于标准库和 HAL 库在微秒延时上存在差异,本文基于 HAL 库 + 微秒级延时函数(如 DWT->CYCCNT)编写示例。

二、DS18B20 读取原理与实现

1. 初始化时序

主机拉低总线 480~960μs,然后释放。DS18B20 在 15~60μs 后拉低总线 60~240μs,表示存在脉冲。

// 复位并检测存在,返回1表示存在
uint8_t DS18B20_Reset(void) {
    uint8_t presence = 0;
    DS18B20_MODE_OUT();             // GPIO 配置为开漏输出
    DS18B20_WRITE_LOW();
    delay_us(600);                  // 拉低 600μs
    DS18B20_WRITE_HIGH();
    delay_us(30);
    DS18B20_MODE_IN();              // 切换输入
    presence = DS18B20_READ();      // 读取存在脉冲
    delay_us(500);
    return (presence == 0) ? 1 : 0;
}

2. 读写时隙

写 1/0:主机拉低总线,写 0 保持 60~120μs;写 1 等待 15μs 后释放。读时隙:主机拉低 1~15μs 后释放,并在 15μs 内采样。

void DS18B20_WriteBit(uint8_t bit) {
    DS18B20_MODE_OUT();
    DS18B20_WRITE_LOW();
    if (bit) {
        delay_us(5);
        DS18B20_WRITE_HIGH();
        delay_us(60);
    } else {
        delay_us(60);
        DS18B20_WRITE_HIGH();
        delay_us(5);
    }
}

uint8_t DS18B20_ReadBit(void) {
    uint8_t bit = 0;
    DS18B20_MODE_OUT();
    DS18B20_WRITE_LOW();
    delay_us(3);
    DS18B20_WRITE_HIGH();
    DS18B20_MODE_IN();
    delay_us(7);
    bit = DS18B20_READ();
    delay_us(50);
    return bit;
}

3. 温度读取完整流程

typedef struct {
    int8_t temp_int;
    uint16_t temp_frac; // 精确到0.0625℃
} DS18B20_Temp;

DS18B20_Temp DS18B20_GetTemp(void) {
    uint8_t data[2];
    if (!DS18B20_Reset()) return (DS18B20_Temp){0, 0};
    DS18B20_WriteByte(0xCC);  // 跳过 ROM
    DS18B20_WriteByte(0x44);  // 启动转换
    while (DS18B20_ReadBit() == 0); // 等待转换完成
    DS18B20_Reset();
    DS18B20_WriteByte(0xCC);
    DS18B20_WriteByte(0xBE);  // 读暂存器
    data[0] = DS18B20_ReadByte();
    data[1] = DS18B20_ReadByte();
    DS18B20_Reset();          // 释放总线

    int16_t raw = (data[1] << 8) | data[0];
    return (DS18B20_Temp){ raw >> 4, (raw & 0x0F) * 625 };
}

三、DHT11 读取原理与实现

1. 起始信号

主机拉低总线 ≥18ms,然后释放并延迟 20~40μs。DHT11 随后拉低 80μs,再拉高 80μs,表示准备发送数据。

uint8_t DHT11_Start(void) {
    DHT11_MODE_OUT();
    DHT11_WRITE_LOW();
    delay_ms(20);               // 拉低 20ms
    DHT11_WRITE_HIGH();
    delay_us(30);
    DHT11_MODE_IN();

    // 等待 DHT11 响应
    if (DHT11_READ()) return 0; // 总线应被拉低
    while (!DHT11_READ());      // 等待拉高
    while (DHT11_READ());       // 等待数据开始
    return 1;
}

2. 数据位解码

每 bit 开始于 50μs 低电平,随后高电平长短区分 0/1。通常高电平 26~28μs 为 0,70μs 左右为 1。采样点放在高电平中间偏后(约 40μs)。

uint8_t DHT11_ReadBit(void) {
    while (!DHT11_READ());          // 等待低电平结束
    delay_us(40);                   // 跳过前 40μs
    uint8_t bit = DHT11_READ();     // 读取电平状态
    while (DHT11_READ());           // 等待高电平结束,准备下一位
    return bit ? 1 : 0;
}

3. 完整读取函数

typedef struct {
    uint8_t humidity_int;
    uint8_t humidity_dec;
    uint8_t temp_int;
    uint8_t temp_dec;
} DHT11_Data;

uint8_t DHT11_ReadData(DHT11_Data *data) {
    if (!DHT11_Start()) return 0;

    uint8_t buf[5] = {0};
    for (int i = 0; i < 5; i++)
        for (int j = 7; j >= 0; j--)
            buf[i] |= DHT11_ReadBit() << j;

    // 校验:前 4 字节和等于第 5 字节
    if (buf[4] != (buf[0] + buf[1] + buf[2] + buf[3])) return 0;

    data->humidity_int = buf[0];
    data->humidity_dec = buf[1];
    data->temp_int = buf[2];
    data->temp_dec = buf[3];
    return 1;
}

四、STM32 工程要点

1. GPIO 配置

建议使用开漏输出,并外接 4.7kΩ 上拉电阻。若用内部上拉,阻值偏大,不适合长线,可能导致时序不稳。

void DS18B20_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    __HAL_RCC_GPIOB_CLK_ENABLE();
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;  // 开漏
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

2. 微秒延时实现

HAL 的 HAL_Delay 只能精确到毫秒。可基于 DWT 实现微秒延时:

void DWT_Delay_Init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

void delay_us(uint32_t us) {
    uint32_t start = DWT->CYCCNT;
    uint32_t ticks = us * (HAL_RCC_GetHCLKFreq() / 1000000);
    while ((DWT->CYCCNT - start) < ticks);
}

五、注意事项

  • 中断抢占:读取过程中若发生中断,会导致时序超时,建议在读取函数入口关闭中断,读取结束后再开启。
  • 再次读取间隔:DS18B20 转换时间最少 100ms,DHT11 读取间隔建议 ≥2s,否则易读失败。
  • 总线争夺:两传感器协议不兼容,严禁将两者直接并联。若需共用引脚,必须用跳线或模拟开关分时切换,并重新初始化时序。
  • 上拉电阻:开漏模式必须有上拉电阻,否则总线无法拉高。
  • 信号完整性:连接线尽量短(<50cm),若使用延长线,改用屏蔽双绞线并降低通信频率(拉长时间参数)。
  • 电源去耦:在传感器 VCC 与 GND 之间加 100nF 电容,避免供电波动影响时序。

总结

DS18B20 与 DHT11 的单总线读取核心在于精确控制时长。DS18B20 需要严格的时隙和 ROM 命令;DHT11 则更简单,但依赖于对高电平时长的判断。通过 GPIO 模拟时序,STM32 能轻松驱动这两类传感器。掌握上述原理后,你可以灵活应用于温湿度监控、环境采集等项目。