☰
STM32与DS18B20单总线温度报警设计全解析
2026/10/20 11:53:39 网站建设 项目流程

简介:针对嵌入式课程设计常见选题,资源提供一套完整的基于STM32F401与DS18B20的温度报警系统方案,适合自动化、电子、计算机等专业学生参考或直接复现。设计涵盖DS18B20温度采集、液晶显示、手动预警参数设置、LED报警指示以及姓名学号等信息屏显,并支持-100~100℃宽范围预警配置,功能覆盖课程设计常见要求。资源包共190个文件,约9.27MB,主要包含Keil5工程源码(C文件、头文件、编译中间文件)、Proteus仿真图(pdsprj/pdsbak)、设计报告(doc/vsdx)以及Hex烧写文件等,从程序到硬件仿真再到文档一应俱全。目录结构清晰,便于按模块查阅代码与电路。该资源已有12670人学习下载,认可度较高,读者可从中获得可直接运行的工程模板、仿真验证思路及报告撰写参考,有助于快速完成课程设计并理解STM32 HAL库开发流程。

1. 嵌入式课程设计里的常客:STM32 温度报警设计

STM32 温度报警设计可能是嵌入式课程设计里被拿来复现次数最多的选题之一,原因是它的复杂度刚好卡在“能讲清楚”和“有技术含量”之间。整个链路拆开看只有三件事:用 DS18B20 读温度,把温度值换算成实际度数,再跟设定阈值比较后驱动蜂鸣器或继电器。但很多人在这个级别的设计上翻车,比如读出来永远是 85℃,或者温度偶尔跳到离谱的值,排查时又不知道问题到底出在硬件还是软件。这篇笔记把我自己完成的一版温度报警设计完整拆开,从选型理由、电路搭法、单总线时序到报警回差和排错路径都覆盖,适合正在做课程设计,或想快速跑通温度采集链路的开发者。对照自己的板子就能复用,也能读懂每一步为什么要这么设。

2. 硬件选型与电路搭建:为什么用 DS18B20,上拉电阻取 4.7kΩ

2.1 三类测温方案对比

温度报警的第一步是选传感器。常见三类方案:

  • NTC 热敏电阻加 ADC:材料便宜,但每个电阻的 B 值有离散性,实际要做标定;还需要 STM32 的 ADC 通道、参考电压和分压电阻一起参与,误差来源多。
  • 模拟电压输出型温度芯片:输出信号随温度近似线性变化,但同样占 ADC,而且容易受电源噪声干扰,采样时还要做软件滤波。
  • 数字单总线传感器:DS18B20 是数字输出,直接给 16 位温度寄存器,不需要额外 ADC 翻译;9~12 位分辨率可配,温度测量范围 -55℃ 到 +125℃,在 25℃ 附近按 12 位分辨率读出的步进是 0.0625℃。

这里选 DS18B20 还有一个课程设计层面的原因:它的单总线协议考察点非常集中,既能体现时序控制能力,又不会像 USB 或以太网那样需要协议栈,适合在有限的项目周期里把底层逻辑吃透。

方案输出类型需要的片上资源精度调试难度
NTC + ADC模拟电压ADC、参考电压需标定低
模拟输出芯片模拟电压ADC、运放或分压尚可低
DS18B20数字单总线任意 GPIO典型 ±0.5℃中

从课程设计的评分点来看,DS18B20 方案能聊的东西也更多:单总线时序、暂存器解析、多传感器寻址、报警回差,每个点都能展开成答辩问答。如果换成 NTC,你很难讲出控制时序层面的复杂逻辑。

2.2 供电方式与上拉电阻取值

DS18B20 的供电有两种方式。外部供电是把 VDD 接到电源上,最稳定,推荐优先用这种方式。另一种是寄生供电,VDD 和 GND 接一起,只靠 DQ 线上的上拉给传感器充电,这种方式省一根线,但在温度转换期间电流较大,如果上拉能力不足会导致转换结果异常。做课程设计不建议用寄生供电,少省那一根线,多省一晚上的调试时间。

回到典型电路。DS18B20 是单总线设备,数据线 DQ 必须外接上拉电阻,典型值 4.7kΩ。这里用的是一块 3.3V 系统板,DS18B20 的 VDD 也接 3.3V,DQ 上拉到 3.3V,整个采集部分电气域统一,最省事。

接线如下:

DS18B20 引脚接到说明
1 GNDSTM32 GND共地
2 DQ任意 GPIO,本文用 PB0数据线,开漏输出
2 DQ 上拉4.7kΩ 到 3.3V单总线终结偏置
3 VDD3.3V电源,就近加 100nF 去耦

注意不要为了“兼容”把 DS18B20 的 VDD 接到 5V,而上拉又用 3.3V。STM32 的 GPIO 在开漏模式下虽然不强输出高电平,但输入引脚耐压有限,DS18B20 供电高于 IO 耐压时,总线高电平会超过 IO 口的容忍范围。长时间运行后 IO 口损坏的案例不算少见。我一般把温度采集相关的电路统一按 3.3V 设计。

如果 DS18B20 到 STM32 的连接线超过 20cm,建议把上拉电阻减小到 2.2kΩ 左右,因为线缆的分布电容会把上升沿拖慢,4.7kΩ 在长线下可能出现上升沿过缓的问题。反过来,如果总线上挂了 5 个以上的 DS18B20,也要适当降低上拉阻值。

2.3 上电后的硬件自检

接线完成之后先别急着写软件,花两分钟做硬件自检:

  1. 万用表量 DQ 对地电压,正常应该接近 3.3V。如果测到 0V,大概率是 DQ 和 GND 接反,或者上拉电阻没接对。
  2. 量 VDD 对地电压,应该稳定在 3.3V。如果供电电压偏低,检查电源模块输出能力。
  3. 用手摸一下 DS18B20 封装表面,温度应该接近室温,不会发烫。

提示:DS18B20 的引脚顺序是 GND、DQ、VDD,很多人第一次接线会记错。上电前先对着封装平面图核对一遍,能省掉最基础的一类问题。

3. 软件读取温度:单总线时序的完整落地流程

3.1 GPIO 开漏配置与复位脉冲

单总线通信要求数据线默认高电平,主机发送数据时通过拉低总线产生下降沿。STM32 的 GPIO 配成开漏输出模式最合适:输出 1 时引脚呈高阻态,由外部上拉把电平拉高;输出 0 时引脚强拉低。这样既满足单总线“线与”逻辑,也不会在主机释放总线时把电平钳在错误位置。

启动前先配置 GPIO:

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_NOPULL; // 不使能内部上下拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 保证波形沿速度 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); }

这段配置基于 HAL 库。开漏模式、不使能内部上拉,是为了避免内部上拉和外部 4.7kΩ 并联后改变时序窗口。GPIO 速度尽量设成高速,因为后面读时序的采样点是在微秒级完成的,沿太慢会导致窗口判断错误。

复位脉冲是单总线通信的第一步:

uint8_t ds18b20_reset(void) { uint8_t presence = 0; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 拉低总线 delay_us(500); // 复位脉冲,需 480~960μs HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 释放总线 delay_us(60); // 等待 DS18B20 把总线拉低 if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) { presence = 1; // 检测到存在脉冲 } delay_us(420); // 等待剩余复位时隙结束 return presence; }

复位脉冲合法的宽度是 480~960μs,拉低时间不足 480μs 时传感器可能不回应。释放总线后,DS18B20 会在 15~60μs 内把总线拉低 60~240μs 表示存在。这里延时 60μs 再去读取电平,是为了避开传感器刚从高阻态切换过来的模糊区间。返回 1 表示总线上有设备。

参数说明:delay_us 必须基于实际系统时钟实现。常见的错误是用 for 循环空转,不标定时钟就换板子,延时偏差会很大。如果条件允许,用 DWT 或 TIM 实现微秒延时,比空循环稳定得多。

3.2 写位、读位时序与字节级封装

DS18B20 的每一位传输都靠拉低总线启动,区别在于拉低后的保持时间。写 1 时,拉低 1~15μs 后释放,剩余时间总线保持高;写 0 时,拉低整个 60μs 写时隙。读取时隙则是主机拉低至少 1μs 后释放,DS18B20 在释放后的 15μs 内把总线驱动到对应电平。

void ds18b20_write_bit(uint8_t bit) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); delay_us(2); // 起始脉冲 if (bit) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); delay_us(58); // 写1:提前释放总线 } else { delay_us(58); // 写0:低电平保持满时隙 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); } delay_us(2); // 位间隔,约1μs以上 } uint8_t ds18b20_read_bit(void) { uint8_t bit = 0; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); delay_us(2); // 读取起始脉冲 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); delay_us(8); // 在 15μs 窗口内采样 bit = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); delay_us(50); // 等待读时隙结束 return bit; }

写时序的逻辑是:先拉低 2μs 产生起始信号,写 1 时提前释放,由外部上拉把总线拉高;写 0 时保持低电平直到时隙结束。两个时隙之间保留 2μs 的位间隔,保证传感器有足够的内部采样时间。

读时序的采样点是关键。主机拉低 2μs 后释放,DS18B20 会在释放后开始驱动总线。延时 8μs 后再采样,正好落在数据手册给出的 15μs 采样建议窗口内。过早采样会读到主机释放后的高电平,过晚则传感器可能已经释放总线,读到上拉后的高电平,两种情况都会把位读错。

封装成字节函数:

void ds18b20_write_byte(uint8_t data) { for (int i = 0; i < 8; i++) { ds18b20_write_bit((data >> i) & 0x01); // 低位在前 } } uint8_t ds18b20_read_byte(void) { uint8_t data = 0; for (int i = 0; i < 8; i++) { if (ds18b20_read_bit()) { data |= (0x01 << i); } } return data; }

字节传输顺序是低位在前,和大多数单总线设备一致。如果你发现读到的数据位序颠倒,检查这里的移位方向。

3.3 读取暂存器与温度计算

读取温度要按固定流程走:复位 → 跳过 ROM → 启动温度转换 → 等待转换完成 → 复位 → 跳过 ROM → 读暂存器 → 连续读两个字节。

float ds18b20_get_temp(void) { uint8_t temp_lsb = 0, temp_msb = 0; int16_t raw = 0; float temp = 0.0f; ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过 ROM,总线上只有一个 DS18B20 ds18b20_write_byte(0x44); // 启动温度转换 while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) { ; // 转换期间总线被拉低,结束后释放 } ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器 temp_lsb = ds18b20_read_byte(); temp_msb = ds18b20_read_byte(); raw = ((int16_t)temp_msb << 8) | temp_lsb; temp = (float)raw / 16.0f; return temp; }

0x44 启动转换后,DS18B20 在转换期间会把总线拉低,转换结束时释放。用这个特性做等待条件,不需要死等 750ms。12 位分辨率下转换时间典型值是 750ms,9 位模式下大约是 93.75ms,总线检测法对分辨率配置更兼容,不会白白浪费时间。

把两个字节拼成 int16_t 再除以 16,是因为温度寄存器低四位是小数部分,12 位模式下每个 LSB 对应 0.0625℃。这里必须用有符号数强转,否则负温度会显示成 655xx,具体原因放到避坑章里讲。

如果你需要设置分辨率,可以在初始化时写配置寄存器。流程是复位 → 跳过 ROM → 写暂存器 0x4E → 依次写入 TH、TL、配置值。配置寄存器低五位是分辨率控制,默认上电值是 12 位,大多数场景不用改。

3.4 报警逻辑与回差设计

温度读到之后,报警逻辑不是简单的“大于阈值就报警”。如果蜂鸣器或继电器直接在阈值上切换,温度在临界点附近抖动时,输出会反复动作,既影响继电器寿命也容易误报。正确的做法是加入回差,也就是滞回区间。

typedef struct { float high_threshold; // 上限报警阈值 float low_threshold; // 下限报警阈值 float hysteresis; // 回差 uint8_t alarm_status; // 当前报警状态 } alarm_config_t; void alarm_update(alarm_config_t *cfg, float current_temp) { if (cfg->alarm_status == 0) { // 当前未报警,温度越过上限才触发 if (current_temp >= cfg->high_threshold) { cfg->alarm_status = 1; } } else { // 当前已报警,温度回落到上限减回差才解除 if (current_temp <= (cfg->high_threshold - cfg->hysteresis)) { cfg->alarm_status = 0; } } }

上限报警的解除条件不是降到上限以下,而是降到“上限减回差”。比如上限 60℃,回差 2℃,那么 60℃ 时报警,要降到 58℃ 以下才解除。因为正常温度波动只有 0.3℃ 左右,加入回差后继电器不会来回吸合。下限报警的逻辑对称:温度低于下限触发,回升到“下限加回差”以上才解除。

DS18B20 内部也有 TH 和 TL 报警寄存器,可以直接让传感器输出报警标志。但我一般更推荐在软件里做判断,好处是调试方便,阈值随时能改,不用每次改配置寄存器都小心翼翼地走写时序。软件报警的实时性也足够,温度变化本来就是一个慢过程。

4. 避坑指南:温度报警设计里五个容易翻车的地方

4.1 初始化失败,总线上永远没有存在脉冲

现象:ds18b20_reset() 一直返回 0,传感器像没接到一样,任你怎么换引脚都没用。

原因:复位脉冲宽度不够。很多移植代码里的延时函数没校准,标称 500μs 实际只有两百微秒甚至更短,DS18B20 根本来不及识别复位。另一个隐蔽的原因是上拉电阻位置不对,面包板上 4.7kΩ 电阻插错孔,DQ 上拉实际上没生效。

解决:先把微秒延时函数用 DWT 定时器校准一遍,再用示波器或逻辑分析仪看复位段的实际低电平时间。没有示波器的话,可以临时加长延时到 700μs 试一下,但不建议把复位脉冲无限制拉长,太长的低电平时间同样会触发设备异常。如果确认延时没问题,再检查上拉电阻两端电压。

4.2 温度读出来永远是 85℃

现象:每次上电读到的温度都是 85.00 或 85.06,之后刷新多少次都不变。

原因:85℃ 是 DS18B20 上电后暂存器的默认值,不是真实温度。出现这个现象,通常是代码启动了转换后立刻读取,没有等待转换完成。也有一种情况是等待循环里总线电平判断条件写反,把转换完成的信号当成未完成信号,一直在循环里出不来。

解决:在 0x44 之后,用 3.3 节里的“总线被传感器拉低,转换结束自动释放”方式等待。也可以直接延时 750ms 再读,虽然慢一点但不会踩到默认值的坑。如果用了 HAL 库读取引脚,注意释放后要等总线电平稳定再采样,否则也可能提前读到低电平。

4.3 温度值偶尔跳变,特别是电机启动瞬间

现象:正常读数稳定在 25.4℃,但继电器一吸合或者旁边电机一转,温度突然跳到 30℃ 又跳回来,串口打印能看到明显的毛刺。

原因:电源被大负载瞬时拉低,DS18B20 供电不稳导致转换结果异常。另一个原因是总线靠近感性负载,电动机或继电器的反向电动势通过走线耦合到 DQ 线上,干扰脉冲被当成单总线时序。

解决:DS18B20 的电源脚就近加 100nF 陶瓷电容,数据线上可以加一个小阻值串联电阻吸收毛刺。继电器和蜂鸣器不能和 DS18B20 共用同一路供电,至少要分开走线。继电器线圈两端必须反并联续流二极管,这一步不做,继电器吸合的瞬间就够让温度读数跳一下。这个问题很多人会归因于传感器玄学,实际上把电源分开就解决了。

4.4 负温度显示成 655xx

现象:环境温度到 -5℃ 时,串口打印出来的是 65531 这样的数值,怎么算都不对。

原因:没有做符号扩展。temp_lsb 和 temp_msb 都是 uint8_t,如果先把两个字节拼成 uint16_t 再运算,负数的补码会被当成大正数。比如 -5℃ 对应的原始数据是 0xFFB0,当成无符号数就是 65456,除以 16 后自然变成 4091,和实际值差了十万八千里。

解决:拼接后的中间变量必须用 int16_t,并且先左移后取或。关键顺序是:

int16_t raw = (int16_t)(((int16_t)temp_msb << 8) | temp_lsb);

先让 temp_msb 以有符号数身份左移,再和低字节按位或,符号位才能正确扩展到高 16 位。如果直接用 uint16_t 中间变量,之后再转 int16_t 也可以,但不如一开始就用有符号类型直观。

4.5 报警输出抖动,继电器频繁吸合

现象:温度在阈值附近波动时,继电器像打点一样频繁开关,甚至能听到蜂鸣器断续响,整个板子看起来特别忙。

原因:比较逻辑只有单一阈值,没有回差。DS18B20 的分辨率是 0.0625℃,真实温度本身也有细微波动,单阈值比较必然导致临界点附近输出反复翻转。

解决:按 3.4 节把回差加进去。回差大小不能超过报警容差,一般取 1~3℃ 比较合适。如果系统对温度波动特别敏感,可以在进报警判断之前先做滑动平均,把短期噪声平滑掉。这三层顺序是:先滤波平滑,再回差判断,最后驱动输出,别把顺序搞反。

5. 进阶技巧:把阈值、回差和采样周期做成可配置参数

5.1 用结构体管理报警配置,而不是一把裸宏

新手习惯把阈值写成 #define HIGH_TEMP 60.0f,但项目要调参的时候就得改代码重新编译,不同场景想用不同阈值也得烧两遍固件。更好的做法是把报警参数集中放到一个配置结构体里,运行时可以改,也方便通过串口下发配置。

alarm_config_t alarm_cfg = { .high_threshold = 60.0f, .low_threshold = 5.0f, .hysteresis = 2.0f, .alarm_status = 0 }; void task_temperature_monitor(void) { float temp = ds18b20_get_temp(); alarm_update(&alarm_cfg, temp); HAL_GPIO_WritePin(ALARM_GPIO_Port, ALARM_Pin, alarm_cfg.alarm_status ? GPIO_PIN_RESET : GPIO_PIN_SET); }

主循环保持一个固定采样节奏,比如每 1 秒读一次温度,然后更新报警状态和输出引脚。采样周期不建议太短,DS18B20 在 12 位分辨率下转换一次就要 750ms,读得太快反而浪费 CPU。这里的回差参数就是后悔药,现场觉得报警太灵敏,串口发一条命令就能改,不用重新烧录。

5.2 串口打印与协议化参数调整

调试时串口是最直接的观察窗口。不建议直接 printf 一个 float,各种工具链对浮点 printf 支持不一致,在嵌入式环境里很容易踩到输出格式的坑。我一般先把温度值拆成整数和小数部分再发送:

char buf[16]; int int_part = (int)temp; int frac_part = (int)((temp - int_part) * 100); sprintf(buf, "%d.%02d\r\n", int_part, frac_part); HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 10);

串口助手看到的就是可读的 25.43 格式。负温度时先判断符号,单独在开头输出负号,避免 int 强转截断导致符号丢失。这个打印函数不用依赖平台库的 float 支持,稳定性和可移植性都好很多。

如果还想再进一步,可以加一组简单的串口指令。接收缓冲区收到格式化的命令后,解析出参数并更新 alarm_cfg 里的字段,报警阈值、回差都能实时修改,不用动代码。这种“参数可配置”的设计放到课程设计答辩里,比静态宏定义能多讲出两三分钟的实现思路。

从那以后我每次做温度采集相关的设计,都会强制先做一遍复位时序验证,确认延时函数真实准确再往下写。这个习惯花不了十分钟,但能过滤掉至少一半的单总线怪问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询