STM32驱动DS18B20温度传感器完整模块化设计
简介: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 校验
✅ 如何集成到显示系统中
🎯 工程实践建议:
- 优先使用寄存器直写 ,减少 HAL 库带来的不确定性延迟
- 封装成模块化驱动 ,提供
ds18b20_init()、read_temp()等简洁 API - 加入超时与重试机制 ,提升鲁棒性
- 使用状态机管理采集流程 ,避免阻塞主循环
- 长距离布线时增加终端匹配电阻 (120Ω)减少反射干扰
🚀 展望:不止于 DS18B20
这套方法论不仅适用于 DS18B20,还可以扩展到其他基于 1-Wire 的器件:
- 💧 DS2438:电量测量芯片
- 🔐 DS2433:EEPROM 存储
- 🧊 DS1822:低成本替代品
- 📡 MAX31850:带冷端补偿的热电偶前端
甚至你可以构建一个 分布式温度监控网络 ,几十个节点共用一根线,通过 ID 区分位置,再通过 LoRa/WiFi 回传数据。
这才是嵌入式系统的魅力所在: 用最少的资源,解决最复杂的现实问题 。💪
🎯 一句话总结 :
DS18B20 虽小,乾坤极大。只要掌握好 GPIO 控制、时序精度与协议逻辑,哪怕是最基础的 STM32,也能玩转“一根线控天下”的黑科技!
现在,轮到你动手试试了——准备好你的蓝 pill 和 DS18B20,点亮第一行温度数据吧!🔥
简介:STM32 18B20驱动程序是基于STM32C8T6微控制器与DS18B20数字温度传感器的嵌入式系统核心组件,通过模拟单线通信协议实现高精度温度采集。该驱动支持GPIO时序控制、温度转换命令发送、数据读取及CRC校验,并集成TFT显示功能,兼容串口输出以提升硬件适配性。模块化设计便于在物联网、智能家居和工业控制等项目中快速复用,显著提高开发效率。
更多推荐
所有评论(0)