STM32温度报警器实战:从DS18B20驱动到滞回控制
2026/9/10 2:11:57 网站建设 项目流程

简介:基于STM32F401与DS18B20的温度报警设计资源,是一套面向嵌入式课程设计的完整配套方案,适合电子、电气及计算机类专业学生和相关开发者参考。资源包共190个文件,以.c/.h源文件为主,涵盖Keil5完整工程、Proteus仿真图、hex程序文件及详细设计报告,整体约9.27MB,目录结构清晰,便于查阅与二次开发。这套资源完整实现了DS18B20温度实时采集、液晶屏信息显示、预警参数手动设置、LED指示灯报警,以及-100至100℃范围内的上下限预警功能;同时支持在LCD上显示设计者姓名、学号和题目名称,可作为课程设计或综合实训的直接参考。其中还包含采用HAL库编写的定时器、RCC、UART、Flash等基础外设驱动代码,能帮助读者理解STM32外设初始化流程与底层配置思路。目前已有12656人学习,项目完成度高,从仿真到实物移植均有助益,非常适用于对嵌入式传感器应用与报警系统设计感兴趣的入门与进阶人员。 做毕业设计或者课程设计,很多人第一反应就是选一个“看起来不难、实际上能凑一块”的题目,就像这个“基于STM32温度报警设计”。说实话,这个题目在嵌入式领域确实算得上经典中的经典,原因很简单:它把温度传感器、数据处理、显示交互和报警输出全串起来了,硬件电路不复杂,代码逻辑也直白,非常适合拿来入门或者应付答辩。但我这篇文章不想只讲“怎么抄一份代码交差”,而是把设计背后每个关键选择的为什么讲清楚,比如为什么用DS18B20而不是NTC热敏电阻,为什么报警判断要带回差,为什么按键处理必须消抖,以及你在调不通板子时最可能踩到的那几个坑。

1. 项目需求与整体思路拆解

在动手写代码之前,先搞清楚这个系统到底要干什么。所谓的“温度报警设计”,拆开来看其实就三件事:测温度、显示温度、超限报警。看着简单,但我在帮学生和同事 review 这种项目时,发现大部分人栽在细节上——比如说阈值怎么设定?报警之后怎么撤销?温度在临界点附近来回跳的时候该怎么办?这些不做设计就直接写代码,后面调试起来会特别难受。

1.1 核心功能拆解

把需求拆成功能点,大概有这么几个模块:

  • 温度采集:实时读取环境温度,精度至少到小数点后一位,刷新频率不能太慢,否则报警滞后严重。
  • 温度显示:当前温度值直观显示在屏幕上,同时还要显示上限阈值和下限阈值,方便用户对比。
  • 阈值设定:用户可以通过按键修改报警上限和下限,修改之后要能保存,断电重启后不丢失。
  • 报警输出:当温度超过上限或低于下限时,触发声光报警,蜂鸣器响、LED灯闪烁。
  • 报警恢复:温度回到正常范围后,报警自动撤销,这里就牵扯到回差控制,下文详细说。

1.2 方案选型:主控和传感器的搭配

主控方面,最稳妥的选择是 STM32F103C8T6。这颗芯片江湖人称“小蓝板”或者“STM32最小系统板”,价格便宜、资料多、内核是 Cortex-M3,主频 72MHz,Flash 64KB、RAM 20KB,干这种温度报警的活儿绰绰有余。你如果手里只有 STM32F407 或者 G031 之类的板子,也没问题,逻辑是通用的,只是引脚映射和时钟配置略有区别。

重点说说传感器选型,这一步决定了你后面写驱动代码的难易程度。

方案优点缺点适用场景
DS18B20 数字温度传感器单总线协议,一根线传数据,精度±0.5℃,无需校准,直接输出数字量时序要求严格,驱动代码稍复杂,需仔细处理时序绝大多数课程设计和毕设首选
NTC 热敏电阻 + ADC成本极低,响应快需要查表或公式拟合,电路有分压电阻匹配问题,精度受电源波动影响对成本敏感、对精度要求不高的场景
LM75 / TMP75 I2C 传感器走I2C总线,驱动简单温度范围相对窄,需要I2C地址配置芯片内部温度监测或小型设备
SHT30 温湿度传感器精度高,能同时测湿度价格稍贵,I2C时序如果模拟的话需要注意初始化需要湿度信息的项目

我的建议是首选 DS18B20。理由很现实:第一,它的单总线协议虽然写起来“有个性”,但从学习角度来看收益很高,面试或者答辩时你能把时序图说清楚,那就是加分项。第二,它不需要 ADC 外设参与,IO 口随便挑一个,引脚分配灵活。第三,测量范围 -55℃ 到 +125℃,日常环境温度绰绰有余。

1.3 为什么报警判断必须带“回差控制”

很多人写报警逻辑就三行代码:

if (temp > threshold_high) buzzer_on();

这样做有个很实际的问题:如果温度刚好卡在阈值线附近,比如上限设的 30.0℃,温度在 29.9℃ 和 30.1℃ 之间来回抖动,蜂鸣器就会“滴——停——滴——停”反复触发,半夜能把人整崩溃。解决这个问题的专业术语叫滞回控制,我给这个项目设定的规则是:超上限报警后,必须回落到 (上限 - 回差值) 以下才解除报警;同理,低于下限报警后,必须回升到 (下限 + 回差值) 以上才解除报警。实际测试下来,回差值取 1.0℃ 效果就挺舒服。这个思路就是经典温控器的工作方式,家里空调不也是这个逻辑嘛。

2. 核心硬件电路与驱动程序实现

硬件电路看上去简单,但每一根线都不能想当然。我画过几版这种小系统的原理图,后来发现真正影响稳定性的细节往往藏在大家容易忽略的地方。

2.1 硬件电路架构

系统整体的硬件拓扑非常清晰,包含四类外设连接:

  • 主控最小系统:STM32F103 的 boot0 拉低从 Flash 启动,8MHz 晶振配两个 20pF 负载电容,复位电路配 10kΩ 上拉电阻和 100nF 电容。
  • DS18B20 电路:数据引脚接 PA0,信号线上必须接一个4.7kΩ 上拉电阻到 3.3V。这是很多人第一次画板子容易忘记的。没有这个上拉,单总线通信直接罢工,现象就是读出来永远是 85℃ 或者 0℃。
  • 显示模块:LCD1602 带 I2C 转接板,SDA 接 PB7、SCL 接 PB6,通过 PCF8574 芯片扩展,只需要两根线解决显示,省 IO。如果你用的还是 16 引脚裸屏,那数据线 D0-D7 共 11 个引脚全接到主控上,也不是不行,但会占用大量 GPIO,扩展性差。
  • 按键:三个独立按键接 PA1、PA2、PA3,对地接 10kΩ 下拉电阻(或内部上拉+对地按键,取决于你的寄存器配置)。
  • 报警电路:一个无源蜂鸣器接 PA4,通过三极管 S8050 驱动;一只红色 LED 接 PA5,串 330Ω 限流电阻。

2.2 DS18B20 驱动代码的时序要点

DS18B20 是单总线器件,意味着数据发送和接收都走同一根线,而且协议上有严格时序要求。一个完整的通信过程分三步:初始化 → ROM 命令 → 功能命令。

我在这里直接给出用标准库实现的最简驱动逻辑,用的是 GPIO 模拟时序,因为这样才能彻底搞懂协议本身。如果你用的是 HAL 库,也可以基于同样流程调整。

初始化时序:

uint8_t DS18B20_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_0); // 拉高 Delay_us(10); GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 主机拉低 480us 以上,产生复位脉冲 Delay_us(500); GPIO_SetBits(GPIOA, GPIO_Pin_0); // 释放总线,等待从机应答 Delay_us(70); // 此时读引脚电平,低电平表示器件在线 return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0; }

读取一个 bit 的秘诀是什么?关键在于时序窗口。主机先拉低总线约 1-2us,然后释放,在接下来的 15us 内读取数据线电平。逻辑 1 和逻辑 0 的区别就是释放后电平拉高的速度不同。所以读取代码里那些 Delay 参数尽量不要乱改,一旦偏了,读出来的数据就是乱码。

uint8_t DS18B20_ReadByte(void) { uint8_t data = 0; for (uint8_t i = 0; i < 8; i++) { data >>= 1; GPIO_ResetBits(GPIOA, GPIO_Pin_0); Delay_us(2); GPIO_SetBits(GPIOA, GPIO_Pin_0); Delay_us(8); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)) data |= 0x80; Delay_us(50); } return data; }

有个很有意思的坑:当你把 GPIO 配置为推挽输出后,直接调用 GPIO_ReadInputDataBit 是读不到数据线的电平的。正确做法是先把引脚模式切换成浮空输入或上拉输入,读完一个 bit 再切回推挽输出。这就是为什么我在读时序前有一行GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;重新配置模式。很多新手卡在这,代码看起来没问题,但读回来全是 0xFF。

2.3 LCD1602 的 I2C 驱动

I2C 驱动的 LCD1602 相比并口屏,焊线少了一大半。它的核心是 PCF8574 芯片,把 I2C 数据转换成并口信号。所以驱动逻辑分两层:

  • 底层:模拟 I2C 起始、停止、字节收发。这个用 GPIO 模拟很简单,SDA 和 SCL 两根线按时序拉高拉低就能跑。
  • 上层:向 LCD 发送命令或数据,把 8 bit 拆成高 4 位和低 4 位两次发送,中间穿插使能脉冲 EN 的上升沿锁存。

写驱动的时候注意,PCF8574 的默认 I2C 地址是 0x27 或 0x3F,根据板卡的 A0/A1/A2 地址引脚接法决定。如果你发现屏幕没反应,不要怀疑代码,先用 I2C 扫描程序打印出实际地址,再替换宏定义。这块我在调试中踩过不止一次。

void LCD_WriteByte(uint8_t data, uint8_t mode) { // mode = 0 表示命令,mode = 1 表示数据 uint8_t high = data & 0xF0; uint8_t low = (data << 4) & 0xF0; I2C_Start(); I2C_SendByte(PCF8574_ADDR | 0); // 写模式 I2C_SendByte(high | (mode << 4) | 0x08); // 使能拉高 I2C_Start(); // 重启总线,模拟EN下降沿锁存 // 实际项目中更多使用使能脚拉低来产生下降沿 // ... 省略细节 I2C_Stop(); }

如果你只想快速点亮屏幕,可以直接用现成的 i2c-lcd1602 库,网上不少。但我自己的习惯是,哪怕用库也要知道它发出去的数据格式到底是什么,否则出了问题只能干瞪眼。

2.4 按键扫描与阈值设置逻辑

三个按键我在电路设计时定义为:设置键 S1、加键 S2、减键 S3。按下设置键,进入阈值修改模式,此时 LCD 上对应的数值会闪烁(通过定时器中断刷新光标和显示实现);再按一次设置键,在上限和下限之间切换;长按设置键或者第三次按下,退出设置模式,保存数据到 Flash。

按键处理最容易犯的错就是没有消抖。机械按键按下时会产生 5-10ms 的抖动,GPIO 电平在这一段时间内会快速跳变,导致一次按下被识别成多次。消抖思路有两种:

  1. 硬件消抖:并联 100nF 电容,做低通滤波。
  2. 软件消抖:检测到电平变化后,延迟 10ms 再检测一次,确认电平稳定才认为按键有效。

更优雅的做法是用定时器做 10ms 周期扫描,每次扫描时记录按键状态,然后和上次状态做边沿检测。这样可以保证一个按下事件只触发一次逻辑,同时还能顺便实现长按识别——比如检测到按键持续低电平超过 3 秒,就判定为“长按保存”。

void KEY_Scan(void) { static uint8_t key_last = 0; uint8_t key_now = 0; key_now = (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == 0) ? 1 : 0 | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) == 0) ? 2 : 0 | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) == 0) ? 4 : 0; // 边缘检测:上次为0,本次为1才是有效按下 if (key_now && (key_last == 0)) { switch (key_now) { case 1: SET_Key_Handle(); break; case 2: ADD_Key_Handle(); break; case 4: SUB_Key_Handle(); break; } } key_last = key_now; }

阈值保存我推荐存到 STM32 内部 Flash 的最后一页(例如 F103C8 的 0x0800FC00),利用 HAL 库的 HAL_FLASH_Program 函数写入。注意 Flash 写入前要先擦除整个扇区,而且 Flash 擦写是有寿命限制的(大约一万次),所以不能在每次按键变化时都写 Flash,应该是退出设置模式时才保存一次。这个细节虽然简单,但在答辩时可以主动说出来,很加分。

3. 系统主流程与状态机设计

写完底层驱动,剩下的就是把这些模块串起来。这个过程就是典型的嵌入式主逻辑设计,我见过太多人把所有代码塞进一个大 while 循环里,结果每个模块互相干扰。

3.1 主循环框架

我选择的架构是:主循环 + 定时器中断标志位

主循环大致结构:

  • 系统上电初始化:时钟、GPIO、Delay、LCD、DS18B20、按键。
  • 进入主循环后,周期性任务靠标志位触发:
    • 温度刷新标志位:每 500ms 读一次 DS18B20,刷新 LCD 显示。
    • 按键扫描标志位:每 10ms 扫描一次按键,并处理短按/长按逻辑。
    • 报警判断标志位:每 100ms 检查一次当前温度和阈值,更新报警输出。

这样设计的好处是:温度读取和按键检测互不阻塞,LCD 显示刷新不会因为 I2C 通信而卡住主循环。定时器用 TIM2 产生 1ms 的中断基准,在中断里累加计数,设置各个任务的执行标志位。

void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); tick_ms++; if (tick_ms % 10 == 0) key_scan_flag = 1; if (tick_ms % 100 == 0) alarm_check_flag = 1; if (tick_ms % 500 == 0) temp_refresh_flag = 1; } }

3.2 温度刷新周期设置

DS18B20 是 12 位分辨率时,单次温度转换时间约 750ms。连续读取的话,有一个很容易犯的错误:发出“跳过 ROM 并启动温度转换”的命令之后,立刻就去读温度寄存器,但此时转换还没完成,读出来的还是上一次的数据,或者直接是 0xFFFF。正确做法是:发出转换命令后,等待至少 750ms,再发出“读暂存器”命令。我的实际做法是设两个时间点:发完转换命令后立刻返回,在主循环里等温度刷新标志位置位后再读。这样系统在等待的过程中还能响应按键,不会卡死。

如果你觉得 750ms 太慢,可以把 DS18B20 的分辨率配置成 9 位,转换时间只有约 93.75ms,但精度只有 0.5℃。对温度报警来说,这个响应速度完全够用,不过我还是建议保持默认的 12 位,因为报警系统在乎的是稳定不是极限速度。

3.3 报警状态机:从判断到输出

报警输出这块我特意用状态机来管理,而不是简单的 if-else,因为报警这件事有“触发、保持、释放”三个阶段,状态多了之后 if-else 会越套越乱。

状态定义:

状态含义触发条件跳转目标
ALARM_STATE_NORMAL正常状态温度在上下限之间超限则跳转 ALARM_STATE_OVERHEAT 或 ALARM_STATE_LOWTEMP
ALARM_STATE_OVERHEAT超上限报警温度 > 上限温度回到 (上限 - 回差) 以下时,跳回 NORMAL
ALARM_STATE_LOWTEMP低于下限报警温度 < 下限温度回到 (下限 + 回差) 以上时,跳回 NORMAL

每个状态里做的事情不一样:

  • NORMAL:蜂鸣器关,LED 灭。
  • OVERHEAT:蜂鸣器以 2Hz 频率鸣叫,LED 快闪(500ms 翻转一次)。
  • LOWTEMP:蜂鸣器以 1Hz 频率鸣叫,LED 慢闪(1s 翻转一次)。

这样设计的好处是报警行为没有歧义,看灯的状态就能分辨是高温还是低温,用户不用盯着屏幕上的数字也能感知异常。

3.4 Flash 掉电保存阈值

阈值保存的代码,如果要用标准库,大概是这样的逻辑:

uint32_t th_high = 30; // 例如 30.0 度,乘10存储,避免浮点 uint32_t th_low = 20; // 读取保存值 void TH_Load(void) { th_high = *(volatile uint32_t*)0x0800FC00; th_low = *(volatile uint32_t*)0x0800FC04; if (th_high == 0xFFFFFFFF) // 首次上电Flash为空 { th_high = 30; th_low = 20; } }

把温度阈值用整数乘以 10 的方式保存,能完美避开浮点数比较的精度问题,同时 Flash 存储不需要处理浮点格式。这是我在项目里坚持的一个小习惯:** 嵌入式里能不用浮点就不用浮点**,能省就省,不仅仅是省空间,调试时打印整数总比打印浮点方便得多。

4. 常见问题与排查技巧实录

我把这个项目从零到出成品的过程复现了三遍,每遍都能踩到不同的坑。这些坑你在做的时候大概率也会碰到,整理一下高发的几个问题,每一类我都附上了现场排查手段。

4.1 报错连不上芯片

报错信息里最经典的就是 ST-LINK 或者 J-LINK 连接不上目标板,类似error: no stm32 target found!,或者Can not connect to target!。这个问题 80% 不是芯片坏了,而是以下原因:

  • 接线问题:SWDIO、SWCLK、GND 三根线必须连准,SWDIO 对应 PA13,SWCLK 对应 PA14。
  • 供电问题:目标板没有独立供电,调试器那点电流带不动整块板子,特别当你接了蜂鸣器和 LCD 之后。
  • Boot 引脚配置问题:如果 BOOT0 被拉高,芯片上电后进入 ISP 模式而非用户 Flash 模式,调试器同样连不上,解决办法是 BOOT0 接地。
  • 内核跑飞导致 SWD 引脚被复用:之前代码里如果初始化过 SWD 引脚为普通 GPIO,又没在初始化代码里加“调试端口释放”的操作,第二次烧录就接不上了。解决办法是按住复位键,在点击 Download 的瞬间松开复位,抢在代码跑飞之前把新程序烧进去。

4.2 DS18B20 读到 85℃ 或 -55℃

这两个值其实不是“读错了”,而是 DS18B20 的上电默认值。上电后温度寄存器的默认值是 0x0550,正好对应 85℃。如果你在新程序烧进去之后立刻读温度而没有等到第一次转换完成,读出来的就是这个默认值。解决办法很简单:上电后延时 1 秒以上,或者执行一次完整的“启动转换 → 等待 750ms → 读取”流程。

另外,如果你读到的温度一直稳定在 -55℃ 或者变化幅度特别大,大概率是初始化时序里的 480us 复位脉冲没拉够时间,或者上拉电阻没接。这两类问题的排查方法是用示波器看数据线上的波形,如果是一片平直线完全无变化,先查上拉电阻。

4.3 LCD1602 显示乱码或花屏

LCD 显示乱码九成是初始化时序和时序等待时间不够。LCD1602 上电后需要一个较长的启动时间(大约 40ms),如果你在初始化命令之前就立刻发命令,屏幕根本来不及响应,会出现花屏或者显示两行方块。解决方案是在 LCD_Init() 开始前加至少 50ms 的延时。

I2C 转接板还有一个问题:PCF8574 输出高电平的能力非常弱(典型的拉电流只有 1mA 左右),如果你选的转接板没有额外加 P-MOS 上拉管,背光灯一亮,整个屏的对比度就变得很奇怪。解决办法是给背光电路供电,或者调整 I2C 数据线速率到 50kHz 左右再试。

4.4 蜂鸣器误报警

蜂鸣器误报警除了回差没做好之外,还有一个很容易被忽略的因素:STM32 引脚默认状态不确定。上电瞬间,GPIO 处于浮空输入状态,如果外部电路刚好有一点耦合噪声,PA4 的电平可能瞬间拉低,蜂鸣器“滴”一声。解决方法是主函数一进来立刻先把所有报警相关 GPIO 初始化为推挽输出并拉高(如果蜂鸣器是低电平驱动)或拉低,然后在阈值加载完成之前关闭报警中断。

还有,无源蜂鸣器是需要脉冲驱动的,不能简单给个高电平就完事,正确做法是用定时器产生 2kHz 的方波输出,驱动蜂鸣器发声。我在调试时用有源蜂鸣器代替了无源蜂鸣器,方便排查逻辑问题,但最终的成品更好用无源的,声音更响亮、可控性更强。

4.5 编译或下载过程中的环境问题

不少朋友在用 Keil5 编译工程时容易遇到stm32 virtual com port驱动异常,下载器能识别但虚拟串口挂感叹号,或者提示No ST-LINK detected。这种情况建议去官网更新 ST-LINK 固件和驱动,不要用盗版或者精简版装环境。还有一个常见问题是,从网上下的工程模板一打开就报 500 多个错误,多半是头文件路径没配好,用 Keil 的魔术棒工具把 CMSIS、标准库 Include 目录手动添加一遍,问题就消失了大半。如果之前装了 C51 版 Keil,再装 ARM 版时经常默认路径混用,导致编译时找不到设备,建议安装时分开目录,不要覆盖安装。

4.6 现场排查的“三板斧”

我把它总结为调电路和调试代码时最常用的三个排查手段:

  1. 先量电源,再量信号。拿到一个“不工作”的板子,先确认 3.3V 和 GND 之间电压正常,再用万用表查蜂鸣器和传感器供电。
  2. 用串口打印代替屏幕显示,LCD 挂了的时候,串口往电脑打印当前状态和数据,最快定位问题。
  3. 最小化测试。如果整体代码跑不通,先屏蔽报警和 LCD 部分,只留一个 LED 闪灯的程序,确认主控是否正常跑起来,再一个一个挂外围设备,逐个击破。

5. 一点个人经验和扩展建议

按我的看法,温度报警设计这个题目,能做的深度完全可以自由伸缩。如果只想毕业答辩拿个分,做到目前的程度已经绰绰有余了。但如果想趁这个机会多学一点东西,下面几个方向性价比很高:

  • 把显示升级成 OLED 屏(I2C 接口的 SSD1306),显示信息量更大,可以同时画温度曲线,而且 OLED 的驱动库非常成熟,移植难度极低。
  • 把阈值设置升级成闭环控制,比如加入继电器驱动,温度低于下限时自动打开加热装置,高于上限时自动打开风扇。这就是一个简单的温控箱雏形,很多做孵化器、干燥箱的成品都是这样做的。
  • 加一个 ESP8266 模块,把温度数据通过 Wi-Fi 上传到手机,实现远程监控报警。这个方向往物联网靠,在就业市场上更好聊。

有一点我必须提醒一下:网上现成的代码很多,照抄一份跑通没问题,但我强烈建议你自己动手把 DS18B20 时序和按键扫描这两块逻辑写一遍。原因很简单,答辩时老师最喜欢问的一个问题是“如果把这个温度传感器换成 I2C 接口的你怎么办”,你答不上来,分数就很难看。真正理解了底层协议,换一个传感器无非就是换一层驱动而已。

我最初做这个项目时也犯过不少低级错误,比如忘了接上拉电阻导致读出来全是 85℃,再比如按键没有消抖导致按一次设定跑飞好几度。但正因为踩过这些坑,后来调其他项目的 I2C 外设、传感器驱动时,一看波形就能判断问题在协议层还是电路层。做嵌入式就是这样,真正的经验全都来自把这些小问题一个个消灭的过程。希望这篇东西能帮你少走一点弯路。

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

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

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

立即咨询