本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32 18B20驱动程序是基于STM32C8T6微控制器与DS18B20数字温度传感器的嵌入式系统核心组件,通过模拟单线通信协议实现高精度温度采集。该驱动支持GPIO时序控制、温度转换命令发送、数据读取及CRC校验,并集成TFT显示功能,兼容串口输出以提升硬件适配性。模块化设计便于在物联网、智能家居和工业控制等项目中快速复用,显著提高开发效率。

DS18B20与STM32的深度交互:从单线协议到温度显示全流程实战 🌡️

你有没有遇到过这样的场景?在工业温控箱里,一堆传感器密密麻麻地排着队,而你的MCU引脚却少得可怜……这时候, DS18B20 就像一位“独行侠”闪亮登场了!👏 它只需要一根数据线就能搞定通信和供电(寄生模式下),还能挂多个设备不打架——简直是嵌入式工程师的救星!

但别高兴得太早。这颗芯片用的是 1-Wire 单总线协议 ,时序要求严苛到微秒级,稍有偏差就会“失联”。更头疼的是,咱们常用的 STM32F103C8T6 并没有原生支持这个协议的硬件模块,只能靠软件模拟 GPIO 来实现。

那问题来了:
“我能不能只靠代码让 STM32 精确控制每一个高低电平?”
“如何确保在噪声环境中也能稳定读取温度?”
“多设备怎么寻址?CRC 校验怎么写才高效?”

别急!今天我们就来一场硬核实战之旅,手把手带你打通 DS18B20 + STM32 软件模拟通信 → 数据解析 → 显示输出 的全链路闭环。准备好了吗?Let’s go!🚀


🔧 DS18B20 是什么?为什么它这么特别?

先来认识一下这位主角—— DS18B20 ,来自 Maxim Integrated(原 Dallas Semiconductor)的一款数字温度传感器。

它的几个关键特性,决定了它为何能在众多传感器中脱颖而出:

特性 数值/说明
测温范围 -55°C ~ +125°C
精度 ±0.5°C(-10°C ~ +85°C)
输出类型 数字信号(无需外部 ADC)
通信接口 1-Wire 单总线协议
地址唯一性 每个芯片内置 64 位 ROM 序列号
分辨率可调 9~12 bit,对应 0.5°C ~ 0.0625°C
工作模式 寄生电源或外部供电

💡 重点来了 :它使用的是 1-Wire 协议 ,也就是传说中的“单线通信”。这意味着:

📌 只需一根数据线 + GND,就可以完成所有通信任务!

而且, 多个 DS18B20 可以并联在同一根线上 ,通过各自的 64 位 ID 实现寻址,完全不需要额外的地址线或 I²C 上拉电阻那么复杂。

听起来是不是很香?但它也有代价—— 对主控的时序精度要求极高 。整个通信过程依赖于精确的微秒级延时,比如复位脉冲要维持至少 480μs,存在脉冲要在 15~60μs 内检测……

这对 STM32 这类 MCU 来说,意味着我们必须深入底层,直接操控 GPIO 和系统时钟,才能保证不出错。


💻 STM32F103C8T6:小身材大能量

我们选用的主控是 STM32F103C8T6 ,俗称“蓝 pill”开发板的核心芯片。虽然它不是高端型号,但足够胜任本次任务:

  • ✅ ARM Cortex-M3 内核
  • ✅ 主频最高 72MHz
  • ✅ 64KB Flash,20KB SRAM
  • ✅ 支持多种外设(USART、SPI、I²C)
  • ✅ 成本低、生态成熟

最关键的是:它的 GPIO 切换速度够快,配合精准延时,完全可以胜任 1-Wire 协议的软件模拟。

不过要注意一点: STM32 没有专用的 1-Wire 外设模块 ,所以我们必须采用“ 软件模拟 + 寄存器直写 ”的方式,手动控制每一位的发送与接收。

这就引出了一个核心挑战:

⚠️ 如何在没有硬件辅助的情况下,用软件精确还原 1-Wire 的复杂时序?

答案就是: 掌握三个关键技术点
1. GPIO 配置为开漏输出 + 外部上拉
2. 高精度微秒级延时函数
3. 严格的时序状态机管理

下面我们逐个击破!


🛠️ GPIO 模拟 1-Wire 总线:电气特性与硬件设计

开漏输出 vs 推挽输出?为什么一定要选开漏?

DS18B20 使用的是 开漏(Open-Drain)结构 ,也就是说:

🚫 它只能把总线拉低(接地),不能主动驱动高电平!

因此,总线空闲时必须由外部电路将电压拉高。通常做法是在 VDD 和数据线之间接一个 4.7kΩ 上拉电阻

如果我们用 STM32 的 推挽输出 ,当主机输出高电平时会强行驱动高电平;但如果此时从机也在拉低,就会造成短路风险 ❌。

所以正确姿势是:

✅ 将 STM32 的数据引脚配置为 开漏输出(Open-Drain Output)

这样,无论是主机还是从机,都只能“拉低”或“释放”,不会出现驱动冲突,符合 1-Wire 的“线与”逻辑。

// 配置 PA0 为开漏输出模式
#define GPIOA_CRL (*(volatile uint32_t*)(0x40010800 + 0x00))

GPIOA_CRL &= ~(0xF << 0);           // 清除 PA0 原有配置
GPIOA_CRL |=  (0x4 << 0);           // MODE=01 (10MHz), CNF=10 (开漏输出)

📌 CNF[1:0] = 10 表示通用开漏输出模式
📌 MODE[1:0] = 01 表示最大输出速度为 10MHz(足够用了)

同时,别忘了使能 GPIOA 的时钟:

#define RCC_APB2ENR (*(volatile uint32_t*)(0x40021000 + 0x18))
RCC_APB2ENR |= (1 << 2);  // 开启 GPIOA 时钟

❗ 如果没开时钟,后续任何寄存器操作都是徒劳——硬件根本没通电!

硬件连接图一览 🎯

circuitDiagram
    title 1-Wire 总线典型连接方式
    VCC o--/\/\/--o BUS (4.7kΩ 上拉电阻)
               |
              === DS18B20
               |
             MCU_PA0
  • VCC:3.3V 或 5V 电源
  • BUS:单根数据线
  • DS18B20:DQ 引脚接 BUS,GND 接地,VDD 可悬空(寄生供电)或接 VCC
  • MCU_PA0:STM32 的数据引脚,配置为开漏输出

⚠️ 注意事项:
- 上拉电阻建议 4.7kΩ,太小功耗大,太大上升沿变缓
- 若使用寄生供电,需确保主机能在转换期间提供强上拉(短时间内拉高电平)
- 长距离传输(>5m)建议加屏蔽线或降低速率


⏱️ 微秒级延时:决定成败的关键环节

1-Wire 协议中最敏感的部分就是 时间控制 。例如:

操作 最小时间 典型时间 最大时间
复位脉冲(主机) 480μs —— 960μs
存在脉冲(从机) 60μs —— 240μs
写 0 时间槽 60μs —— ——
写 1 时间槽 1–15μs 写低,其余释放 —— ——
读位采样窗口 15μs 后采样 —— ——

这些时间精度都在 微秒级别 ,而 STM32 在 72MHz 主频下每条指令仅约 13.89ns,理论上完全有能力实现。

但问题是: 编译器优化会让空循环失效!

来看一段“看似正确”的延时代码:

void delay_us(uint32_t us) {
    for(volatile int i = 0; i < us * 72; i++) {
        __asm__ volatile ("nop");
    }
}

你以为跑了 72 个周期就是 1μs?错!如果开了 -O2 编译优化,整个循环可能被直接删掉!😱

解决方案一:禁用编译器优化(临时可用)

最简单的办法是在项目设置中关闭优化( -O0 ),但这会影响整体性能,不适合发布版本。

解决方案二:使用 SysTick 定时器(推荐!)

Cortex-M3 内建了一个叫 SysTick 的 24 位倒计数定时器,非常适合做高精度延时。

初始化如下:

void systick_init(void) {
    SysTick->LOAD = 72 - 1;          // 每 1μs 中断一次(72MHz / 72 = 1MHz)
    SysTick->VAL  = 0;               // 清零当前值
    SysTick->CTRL = 7;               // 使能定时器、中断、选择主频
}

然后实现阻塞式延时:

void delay_us(uint32_t us) {
    for(uint32_t i = 0; i < us; i++) {
        while((SysTick->CTRL & (1<<16)) == 0); // 等待 COUNTFLAG 置位
    }
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 可选:关闭以省电
}

✅ 优点:
- 不受编译优化影响
- 精度高且稳定
- 可移植性强

🛠️ 提示:也可以封装成接口层,根据不同主频自动计算 LOAD 值:

void delay_us_safe(uint32_t us) {
    uint32_t ticks = (SystemCoreClock / 1000000) * us;
    SysTick->LOAD = ticks - 1;
    SysTick->VAL = 0;
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}

这样一来,即使换到 8MHz 内部 RC 振荡器,也能自适应调整。


🔄 单线通信三大步:复位 → 应答 → 数据交互

现在进入真正的协议层操作。DS18B20 的每一次通信都必须从 复位+应答 开始。

第一步:主机发复位脉冲(Reset Pulse)

主机主动拉低总线至少 480μs ,通知所有从机准备接收命令。

void ds18b20_reset_start(void) {
    GPIOA_ODR &= ~(1 << 0);     // PA0 输出低电平
    delay_us(480);              // 维持 480μs
    GPIOA_ODR |=  (1 << 0);     // 释放总线(上拉变高)
    delay_us(70);               // 等待从机响应
}

📌 注意:这里是先清零再置位,利用 ODR 寄存器直接控制输出电平。

第二步:从机回应存在脉冲(Presence Pulse)

DS18B20 会在 15~60μs 内 自动拉低总线,持续 60~240μs ,表示“我在!”

我们需要在这段时间内检测是否为低电平:

uint8_t ds18b20_check_presence(void) {
    delay_us(15);  // 等待最小响应时间
    if ((GPIOA_IDR & (1 << 0)) == 0) {
        delay_us(240);  // 等待脉冲结束
        return 1;       // 设备存在
    }
    return 0;           // 无响应
}

📌 IDR 是输入数据寄存器,用来读取引脚实际电平

可以合并成一个函数:

uint8_t ds18b20_reset(void) {
    GPIOA_CRL &= ~(0xF << 0);
    GPIOA_CRL |=  (0x3 << 0);   // 切换为推挽输出以便强力拉低(可选)

    GPIOA_ODR &= ~(1 << 0);
    delay_us(480);

    GPIOA_CRL &= ~(0xF << 0);
    GPIOA_CRL |=  (0x8 << 0);   // 切回输入模式(浮空或上拉),等待响应
    delay_us(70);

    uint8_t presence = (GPIOA_IDR & (1 << 0)) ? 0 : 1;
    delay_us(240);  // 完全恢复
    return presence;
}

📌 这里有个技巧:先用推挽强制拉低,再切回输入模式等待响应,避免驱动能力不足。

sequenceDiagram
    participant MCU as STM32 (主机)
    participant SENSOR as DS18B20 (从机)
    MCU->>SENSOR: 拉低总线 ≥480μs
    Note right of MCU: 发送复位脉冲
    MCU->>SENSOR: 释放总线
    SENSOR-->>MCU: 拉低总线 60–240μs
    Note left of SENSOR: 返回存在脉冲
    MCU->>SENSOR: 15–60μs 内检测低电平
    Note right of MCU: 判断设备是否存在

只要有一个设备响应,总线就是低电平(线与逻辑),所以该机制可用于快速判断是否有设备在线。


📦 ROM 命令与多设备寻址:你是谁?我要找你!

DS18B20 支持多设备共用一条总线,靠的就是 64 位唯一 ROM 地址

常用的 ROM 命令有五种:

命令 HEX 功能
READ ROM 0x33 读取设备 ID(仅单设备可用)
MATCH ROM 0x55 匹配特定 ID 的设备
SKIP ROM 0xCC 跳过匹配,广播命令给所有设备
SEARCH ROM 0xF0 扫描总线上所有设备
ALARM SEARCH 0xEC 查找报警设备

单设备场景:直接读 ID

如果你确定总线上只有一个 DS18B20,可以用 READ ROM 快速获取其 ID:

void read_rom_single(uint8_t *rom) {
    ds18b20_reset();
    write_byte(0x33);  // READ ROM
    for(int i = 0; i < 8; i++) {
        rom[i] = read_byte();
    }
}

多设备场景:必须用 SEARCH ROM!

⚠️ 注意:如果有多个设备, 绝对不能用 READ ROM ,否则它们会同时响应,导致数据冲突!

正确的做法是使用 二叉树搜索算法(Search ROM Algorithm) ,逐位探测每一位是 0、1 还是冲突。

简化版流程如下:

typedef struct {
    uint8_t rom[8];
    uint8_t last_discrepancy;  // 记录上次分支位置
} search_state_t;

int ds18b20_search_next(search_state_t *state);

每次调用返回一个新设备的 ROM,直到遍历完所有设备。

🧠 这个算法比较复杂,涉及位级探测和路径回溯,建议封装成独立模块使用。

一旦获得了所有设备的 ID,就可以通过 MATCH ROM 精准操作某一个:

void select_device(uint8_t *target_rom) {
    ds18b20_reset();
    write_byte(0x55);  // MATCH ROM
    for(int i = 0; i < 8; i++) {
        write_byte(target_rom[i]);
    }
}

之后发送的功能命令就只会作用于这个设备。


🌡️ 温度读取与解析:从原始数据到真实摄氏度

Scratchpad 结构一览

DS18B20 把温度和其他参数存在一个叫 Scratchpad 的 9 字节内存中:

地址 名称 描述
0 TEMP_LSB 温度低8位
1 TEMP_MSB 温度高8位
2 TH_REGISTER 高温报警阈值
3 TL_REGISTER 低温报警阈值
4 CONFIG 分辨率设置(9~12bit)
5~6 Reserved 保留
7 COUNT_REMAIN 斜率计数器剩余
8 COUNT_PER 每度计数值
9 CRC CRC-8 校验

我们主要关心前两个字节。

启动一次温度转换

void ds18b20_start_conversion(void) {
    ds18b20_reset();
    write_byte(0x44);  // CONVERT T
}

注意:如果是寄生供电模式,主机需要在此期间保持 DQ 高电平(强上拉)至少 750ms(12bit 分辨率)。

可以用另一个 GPIO 控制 MOSFET 提供强上拉,或者干脆外接 VDD。

读取温度值

float read_temperature(void) {
    uint8_t scratch[9];

    ds18b20_reset();
    write_byte(0xBE);  // READ SCRATCHPAD
    for(int i = 0; i < 9; i++) {
        scratch[i] = read_byte();
    }

    // CRC 校验
    if (crc8_calculate(scratch, 8) != scratch[9]) {
        return NAN;  // 校验失败
    }

    int16_t raw = (scratch[1] << 8) | scratch[0];

    // 补码处理
    if (raw & 0x8000) {
        raw -= 65536;  // 负数转换
    }

    // 分辨率判断(CONFIG 寄存器)
    uint8_t res = (scratch[4] >> 5) & 0x03;  // bit7:5
    float factor = 0.0625f;
    if (res == 0) factor = 0.5f;
    else if (res == 1) factor = 0.25f;
    else if (res == 2) factor = 0.125f;

    return raw * factor;
}

📌 示例:
- 0x0190 → 400 × 0.0625 = +25.0°C
- 0xFF60 → (-160) × 0.0625 = -10.0°C


🔐 CRC-8 校验:守护数据完整性的最后一道防线

DS18B20 使用 CRC-8 多项式:$ x^8 + x^5 + x^4 + 1 $,即 0x31

我们可以预先生成一张查表:

const uint8_t crc_table[256] = {
    0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83,
    0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41,
    /* ...中间省略... */
    0x21, 0x7F, 0x9D, 0xC3, 0x40, 0x1E, 0xFC, 0xA2
};

uint8_t crc8_calculate(const uint8_t *data, uint8_t len) {
    uint8_t crc = 0;
    while (len--) {
        crc ^= *data++;
        crc = crc_table[crc];
    }
    return crc;
}

校验失败怎么办?启动重试机制!

graph TD
    A[开始读取Scratchpad] --> B{CRC校验成功?}
    B -- 是 --> C[解析温度值]
    B -- 否 --> D[标记通信错误]
    D --> E[启动重采样机制]
    E --> F{重试次数<3?}
    F -- 是 --> A
    F -- 否 --> G[上报故障至主机或UI]

🖥️ 显示集成:把温度秀出来!

最后一步,把读到的温度显示在 OLED 或串口上。

假设使用 SSD1306 OLED 屏:

char buf[32];
sprintf(buf, "Temp: %.2f C", temp);
ssd1306_draw_string(0, 0, buf, &Font_11x18, White);
ssd1306_update_screen();

效果类似这样:

-------------------
| Temp: 23.75°C   |
|                 |
| Device: 0x28... |
-------------------

还可以加入刷新动画、单位切换、历史曲线等高级功能。


🧩 总结与工程建议

经过这一整套流程,你应该已经掌握了:

✅ 如何用 STM32 软件模拟 1-Wire 协议
✅ 如何实现高精度微秒延时
✅ 如何处理复位、应答、读写时序
✅ 如何进行 ROM 寻址与多设备管理
✅ 如何解析温度并做 CRC 校验
✅ 如何集成到显示系统中

🎯 工程实践建议:

  1. 优先使用寄存器直写 ,减少 HAL 库带来的不确定性延迟
  2. 封装成模块化驱动 ,提供 ds18b20_init() read_temp() 等简洁 API
  3. 加入超时与重试机制 ,提升鲁棒性
  4. 使用状态机管理采集流程 ,避免阻塞主循环
  5. 长距离布线时增加终端匹配电阻 (120Ω)减少反射干扰

🚀 展望:不止于 DS18B20

这套方法论不仅适用于 DS18B20,还可以扩展到其他基于 1-Wire 的器件:

  • 💧 DS2438:电量测量芯片
  • 🔐 DS2433:EEPROM 存储
  • 🧊 DS1822:低成本替代品
  • 📡 MAX31850:带冷端补偿的热电偶前端

甚至你可以构建一个 分布式温度监控网络 ,几十个节点共用一根线,通过 ID 区分位置,再通过 LoRa/WiFi 回传数据。

这才是嵌入式系统的魅力所在: 用最少的资源,解决最复杂的现实问题 。💪


🎯 一句话总结
DS18B20 虽小,乾坤极大。只要掌握好 GPIO 控制、时序精度与协议逻辑,哪怕是最基础的 STM32,也能玩转“一根线控天下”的黑科技!

现在,轮到你动手试试了——准备好你的蓝 pill 和 DS18B20,点亮第一行温度数据吧!🔥

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32 18B20驱动程序是基于STM32C8T6微控制器与DS18B20数字温度传感器的嵌入式系统核心组件,通过模拟单线通信协议实现高精度温度采集。该驱动支持GPIO时序控制、温度转换命令发送、数据读取及CRC校验,并集成TFT显示功能,兼容串口输出以提升硬件适配性。模块化设计便于在物联网、智能家居和工业控制等项目中快速复用,显著提高开发效率。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐